Terminal spreadsheet editor for Vim users. Modal editing (Normal/Insert/Visual/Command), 50+ live formulas, multi-sheet workbooks, CSV · XLSX · ODS support, full undo stack, system clipboard, named ranges, filter/freeze panes, cell notes, live conditional formatting, auto-fill series, macros, marks,
.repeat, Python plugins, searchable help screen, and circular reference detection. Written in Rust with ratatui.
Website: https://okt4v.github.io/ASAT/
- Modal editing — Normal, Insert, Visual (char/line/block), Command, Search, and Macro Recording modes, exactly like Vim
- Live formula engine — 50+ built-in functions across math, text, logic, lookup, statistical, finance, and date. Includes volatile functions (
NOW,TODAY,RAND,RANDBETWEEN) that recalculate every frame.=NOW()auto-displays asYYYY-MM-DD HH:MM; use:fmt datetimefor other date-serial cells. Formulas re-evaluate after every edit (lazy dirty-cell tracking) - Circular reference detection — cells that form dependency cycles display
#CIRC!instead of crashing or hanging - Named ranges —
:name SALES A1:C10defines a named range usable in formulas as=SUM(SALES) - Multi-sheet workbooks — tab bar,
:tabnew,:tabclose,gt/gTto switch sheets - File format support — read and write CSV, TSV, XLSX, and ODS (OpenDocument Spreadsheet); native
.asatformat with bincode + zstd compression; ODS formula round-trip with cached computed values - System clipboard — yank (
yy,yc, visualy) copies values and styles as TSV;Ctrl+Vin Insert mode pastes from system clipboard; delete (dd,x, visuald) also yanks to register + clipboard before clearing - Formula-aware paste — pasting a formula adjusts relative cell references (
A1becomesB2when pasted one row down and one column right); absolute references ($A$1) stay fixed; mixed references ($A1,A$1) lock only the$-prefixed axis - Full undo stack — 1000-deep undo/redo covering cell edits, row/column operations, pastes, and style changes; undo/redo repositions the cursor to the affected cell
- Sort & find/replace —
:sort [COL] [!]sorts rows by any column (e.g.:sort B!= column B descending);:s/pat/repl/gdoes regex find & replace across cells; both undoable - Filter rows —
:filter <col> <op> <val>hides non-matching rows (supports=,!=,>,<,>=,<=,~);:filter offrestores all rows - Freeze panes — frozen rows and columns render as sticky headers with a visual separator; set via
:freeze rows N/:freeze cols N - Fill down / right —
Ctrl+D/Ctrl+Rin Visual mode copies anchor cell across selection; also:filldown/:fillrightex-commands - Auto-fill series —
Ctrl+F/Ctrl+Ein Visual mode detects and extends arithmetic sequences, weekday names, and month names across the selection - Live conditional formatting —
:cf <range> <cond> <val> bg=#hexapplies colour rules that re-evaluate every frame; supports>,<,>=,<=,=,!=,contains,blank,error;:cf clearremoves all rules - Formula color distinction — formula cells render in muted blue to distinguish them from literal data at a glance
- Live formula preview — while typing a formula, the current evaluated result appears in the formula bar as
→ value - Cell reference highlighting — when the cursor rests on a formula cell, all referenced cells are highlighted in the grid
- Go-to definition —
gdin Normal mode jumps to the first cell referenced in the current cell's formula - Visual mode ex-commands — press
:from Visual/V-Row/V-Col mode to enter a command that applies to the entire selection (e.g.:bold,:fg #ff0000,:sort) - Repeat last change —
.in Normal mode replays the last insert or delete operation (like Vim) - Goto cell —
g<letter>jumps to a column;:goto B15jumps to any cell address - Transpose —
:transposeswaps rows and columns in the visual selection - Remove duplicates —
:dedupremoves duplicate rows by the current cursor column - Cell notes —
:note <text>attaches a comment to the current cell; cells with notes show a▸corner marker;:notewith no argument shows the current note;:note!clears it - Thousands separator —
:fmt thousandsformats numbers with comma separators (#,##0);:fmt t2adds two decimal places - Formula tab-completion — press
Tabwhile typing a formula (=SU…) to cycle through matching function names - Time-based autosave — configurable autosave interval in seconds (edit
autosave_intervalinconfig.toml; 0 = disabled) - Macros — record key sequences to named registers (
qa…qz), replay with@a, chain with{N}@a - Marks — set named positions (
ma), jump back ('a), and swap with'' - Cell merging — merge a visual selection into one spanning cell with
Min Visual mode or:merge; unmerge withUor:unmerge; line wrap flows text into covered rows below the anchor - Line wrap — toggle per-cell line wrap with
gw(Normal mode) or:wrap; text reflows into merged rows below for vertical merges, and row height auto-expands for single cells - Cell styling — bold, italic, underline (text-only by default;
:underline fullextends the underline across the full cell width), strikethrough, foreground/background colour, alignment, and number formats - Themes — built-in theme picker (
:theme) with multiple colour presets, saved to config - Formula reference picker — press
Ctrl+Rinside a formula to navigate the grid and insert cell/range references interactively - Searchable help screen —
:help/:hopens a full-screen overlay with Keybindings and Formulas tabs; type to filter,Tabto switch tabs,j/kto scroll,qto close - Plugin system — extend ASAT with Python via PyO3 (enabled by default); hook into cell changes, mode transitions, and file events; register custom formula functions from
~/.config/asat/init.py; manage plugins with:plugins
Download a binary for your platform from the v0.1.28 release:
| Platform | Link |
|---|---|
| Linux x86_64 (glibc) | asat-x86_64-unknown-linux-gnu.tar.gz |
| Linux x86_64 (musl) | asat-x86_64-unknown-linux-musl.tar.gz |
| Linux aarch64 | asat-aarch64-unknown-linux-gnu.tar.gz |
| macOS arm64 | asat-aarch64-apple-darwin.tar.gz |
| macOS x86_64 | asat-x86_64-apple-darwin.tar.gz |
| Windows x86_64 | asat-x86_64-pc-windows-msvc.zip |
Extract the archive and place the asat binary somewhere on your $PATH (e.g. ~/.local/bin/).
yay -S asat-bin
# or
paru -S asat-binbrew tap okt4v/tap
brew install asatcurl -LO https://github.com/okt4v/ASAT/releases/download/v0.1.28/asat_0.1.28-1_amd64.deb
sudo apt install ./asat_0.1.28-1_amd64.debapt install ./file.deb resolves dependencies automatically and registers the package so apt remove asat works as expected.
git clone https://github.com/okt4v/ASAT.git
cd ASAT
bash install.shThe install script builds in release mode and copies the binary to ~/.local/bin/asat. It will warn you if that directory is not on your $PATH.
To build manually instead:
cargo build --release
cp target/release/asat ~/.local/bin/Note: The Python plugin engine is enabled by default. To build without it (smaller binary, no Python dependency):
cargo build --release --no-default-features
asat # open welcome screen
asat file.csv # open a CSV file
asat budget.xlsx # open an Excel workbook
asat report.ods # open an ODS file
asat new_file.csv # create a new file at this path| Key | Action |
|---|---|
h / ← |
Move left |
j / ↓ |
Move down |
k / ↑ |
Move up |
l / → |
Move right |
w / b |
Jump to next / previous non-empty cell (horizontal) |
W / B |
Jump to next / previous non-empty cell (vertical) |
} / { |
Jump to next / previous paragraph boundary (empty row) |
0 / Home |
Jump to first column |
$ / End |
Jump to last column with data |
gg |
Jump to first row |
G |
Jump to last row with data |
H / M / L |
Jump to top / middle / bottom of visible area |
f{A-Z} |
Jump to column by letter (fC → column C) |
Ctrl+d / Ctrl+f / PgDn |
Page down |
Ctrl+u / Ctrl+b / PgUp |
Page up |
zz / zt / zb |
Scroll cursor to centre / top / bottom of screen |
{N} + motion |
Repeat motion N times (e.g. 5j moves down 5 rows) |
| Key | Action |
|---|---|
i / Enter / F2 |
Edit current cell |
a |
Edit current cell (append) |
s / cc |
Clear cell and enter insert mode |
ci" / ci( / ci[ / ci{ |
Change inner text object — clear content inside delimiters and edit |
. |
Repeat last change (last insert, delete, or destructive key) |
r |
Enter replace mode |
x / Del / D |
Delete cell content (supports count, e.g. 3x) |
~ |
Toggle case of text cell (supports count) |
J |
Join cell below into current cell (space-separated), clear below |
gd |
Jump to the first cell referenced in the current formula |
Ctrl+a |
Increment number / cycle date forward (day → month → weekday) |
Ctrl+x |
Decrement number / cycle date backward |
gw |
Toggle line wrap on current cell |
U |
Unmerge cell under cursor |
o / O |
Insert row below / above and enter insert mode |
u |
Undo |
Ctrl+r |
Redo |
| Key | Action |
|---|---|
yy / yr |
Yank current row → register + system clipboard |
yc |
Yank current cell → register + system clipboard |
yC |
Yank entire column → register + system clipboard |
yj / yk |
Yank row below / above cursor |
yS |
Copy current cell's style to style clipboard |
p / P |
Paste after / before cursor (supports count) |
pS |
Paste style clipboard onto current cell |
| Key | Action |
|---|---|
dd |
Delete current row |
dc |
Clear cell content (alias for x) |
dC |
Delete current column |
dj / dk |
Delete row below / above cursor |
>> / << |
Increase / decrease current column width |
= |
Auto-fit current column to widest content |
+ / - |
Increase / decrease current row height |
_ |
Reset row height to auto |
| Key | Action |
|---|---|
Esc |
Confirm edit, return to Normal |
Enter |
Confirm edit, move down |
Tab |
Confirm edit, move right |
Ctrl+a |
Move edit cursor to start |
Ctrl+e |
Move edit cursor to end |
Ctrl+w |
Delete word backward |
Ctrl+u |
Delete to start of buffer |
Ctrl+k |
Delete to end of buffer |
Ctrl+r |
Enter F-REF mode to pick a cell/range reference (formulas only) |
Ctrl+v |
Paste from system clipboard into edit buffer |
Tab / Shift+Tab |
Cycle through formula function name completions (when buffer starts with =) |
| Key | Action |
|---|---|
v |
Character/cell visual mode |
V |
Line (full-row) visual mode |
Ctrl+v |
Column block visual mode |
M |
Merge selection into one spanning cell |
d / x / Del |
Delete selection |
c / s |
Clear selection and enter insert mode |
y |
Yank selection → register + system clipboard (TSV) |
S |
Insert =SUM(range) below the selection |
> / < |
Widen / narrow all columns in selection |
Ctrl+d |
Fill down — copy anchor row to all selected rows |
Ctrl+r |
Fill right — copy anchor column to all selected columns |
Ctrl+f |
Auto-fill series down — extends arithmetic, weekday, or month patterns |
Ctrl+e |
Auto-fill series right |
: |
Enter Command mode with the selection range pre-loaded (e.g. :bold, :fg #hex) |
| Key | Action |
|---|---|
m{a-z} |
Set named mark |
'{a-z} |
Jump to named mark |
'' |
Jump back to position before last jump |
q{a-z} |
Start recording macro to register |
q |
Stop recording |
@{a-z} |
Play macro from register |
@@ |
Replay last macro |
{N}@{reg} |
Play macro N times |
| Key | Action |
|---|---|
/ |
Forward search (regex supported) |
? |
Backward search |
n / N |
Next / previous match |
* |
Search for content of current cell |
| Key | Action |
|---|---|
Tab / gt / Ctrl+t |
Next sheet |
Shift+Tab / gT / Ctrl+T |
Previous sheet |
Enter command mode with :. From Visual mode, : pre-loads the current selection range so style and formatting commands apply to all selected cells.
Tab-completion works in command mode — press Tab to cycle through matching commands.
| Command | Action |
|---|---|
:w [file] |
Save (optionally to a new path) |
:q / :q! |
Quit / force quit |
:wq / :x |
Save and quit |
:e <file> |
Open file |
:tabnew [name] |
New sheet |
:tabclose |
Close current sheet |
:ic / :icr |
Insert column left / right |
:dc |
Delete current column |
:ir [N] |
Insert row at cursor or line N |
:dr [N] |
Delete row at cursor or line N |
:cw <N> |
Set column width to N characters |
:rh <N> |
Set row height to N lines |
:bold / :italic / :underline / :underline full / :strike |
Toggle text style; :underline underlines text characters only; :underline full extends the underline across the full cell width |
:fg <color> / :bg <color> |
Set foreground / background colour (hex or named) |
:hl <color> / :hl |
Highlight cell / clear highlight |
:align <l/c/r> |
Set alignment: left, center, or right |
:fmt <spec> |
Number format: %, $, 0.00, int, date, none |
:cs |
Clear all styles |
:theme [name] |
Open theme picker or apply a theme by name |
:sort [COL] [!] |
Sort by column letter (e.g. :sort A, :sort B! = descending); defaults to cursor column (undoable) |
:s/pat/repl/[g][i] |
Find & replace in text cells — g = all occurrences, i = case-insensitive (undoable) |
:goto <addr> / :go <addr> |
Jump to cell address (e.g. :goto B15) |
:name <NAME> <range> |
Define a named range (e.g. :name SALES A1:C10) |
:filter <col> <op> <val> |
Hide rows where column does not match (ops: = != > < >= <= ~) |
:filter off |
Unhide all filtered rows |
:transpose / :tp |
Transpose the visual selection (swap rows and columns) |
:dedup |
Remove duplicate rows by the current cursor column |
:note [text] |
Set a note on current cell; :note shows it; :note! clears it |
:cf <range> <cond> <val> bg=#hex [fg=#hex] |
Add live conditional format rule (e.g. :cf A1:C10 > 100 bg=#ff0000); conditions: > < >= <= = != contains blank error |
:cf clear |
Remove all conditional format rules from the active sheet |
:cf list |
Show number of active conditional format rules |
:filldown / :fd |
Fill the cursor cell value down to the selection end |
:fillright / :fr |
Fill the cursor cell value right to the selection end |
:fmt thousands / :fmt t2 |
Thousands-separator number format (#,##0 or #,##0.00) |
:freeze rows <N> |
Freeze top N rows as sticky header |
:freeze cols <N> |
Freeze left N columns as sticky header |
:freeze off |
Clear all frozen panes |
:merge |
Merge visual selection (or current cell) into one spanning cell |
:unmerge |
Unmerge the merged region under the cursor |
:wrap / :ww |
Toggle line wrap on current cell or selection |
:help / :h |
Open full-screen searchable help (Keybindings + Formulas tabs) |
:home |
Return to the welcome / home screen |
:plugins |
Open plugin manager TUI |
Start any cell with = to write a formula. Formulas re-evaluate automatically after every edit.
=SUM(A1:A10)
=IF(B2>100, "Over budget", "OK")
=AVERAGE(C1:C20) * 1.1
=CONCATENATE(A1, " ", B1)
=NOW() → current date and time (recalculates every frame)
=RAND() → random float between 0 and 1
Supported functions:
| Category | Functions |
|---|---|
| Math | SUM, AVERAGE, MIN, MAX, ABS, ROUND, ROUNDUP, ROUNDDOWN, FLOOR, CEILING, MOD, POWER, SQRT, LN, LOG, LOG10, EXP, INT, TRUNC, SIGN |
| Text | LEN, LEFT, RIGHT, MID, TRIM, UPPER, LOWER, PROPER, CONCATENATE, TEXT, VALUE, FIND, SEARCH, SUBSTITUTE, REPLACE, REPT |
| Logic | IF, AND, OR, NOT, ISNUMBER, ISTEXT, ISBLANK, ISERROR, IFERROR, ISLOGICAL |
| Lookup | VLOOKUP, HLOOKUP, XLOOKUP, INDEX, MATCH, OFFSET, CHOOSE |
| Date | NOW, TODAY, DATE, YEAR, MONTH, DAY |
| Random | RAND, RANDBETWEEN |
| Statistical | COUNT, COUNTA, SUMIF, COUNTIF, AVERAGEIF, MAXIFS, MINIFS, SUMPRODUCT, MEDIAN, STDEV, VAR, LARGE, SMALL, RANK, PERCENTILE, QUARTILE |
| Finance | PV, FV, PMT, NPER, RATE, NPV, IRR, MIRR, IPMT, PPMT, SLN, DDB, EFFECT, NOMINAL, CUMIPMT, CUMPRINC |
| Constants | TRUE, FALSE, PI() |
Volatile functions (NOW, TODAY, RAND, RANDBETWEEN) recalculate on every frame, not just on cell edits.
Circular references: If a formula creates a dependency cycle (e.g. A1=B1+1 and B1=A1+1), all cells in the cycle display #CIRC!. Break the cycle by editing one of the cells.
Reference syntax:
| Syntax | Meaning |
|---|---|
A1 |
Relative cell reference (adjusts on paste) |
$A$1 |
Absolute cell reference (fixed on paste) |
$A1 / A$1 |
Mixed reference (lock column or row only) |
A1:B10 |
Range |
Sheet2!C4 |
Cross-sheet reference |
Interactive reference picking: While editing a formula, press Ctrl+R to enter F-REF mode. Navigate with hjkl/arrows. Press v (or :) to anchor a range start, then move to the end cell and press Enter to insert the range reference (e.g. A1:C5). Press Enter without anchoring to insert a single cell reference. Press Esc to cancel. The formula bar continues to show your in-progress formula throughout.
| Function | Signature | Description |
|---|---|---|
PV |
PV(rate, nper, pmt, [fv], [type]) |
Present value of an investment or annuity |
FV |
FV(rate, nper, pmt, [pv], [type]) |
Future value of an investment or annuity |
PMT |
PMT(rate, nper, pv, [fv], [type]) |
Periodic payment for a loan or annuity |
NPER |
NPER(rate, pmt, pv, [fv], [type]) |
Number of periods to pay off a loan |
RATE |
RATE(nper, pmt, pv, [fv], [type], [guess]) |
Interest rate per period (iterative) |
NPV |
NPV(rate, cf1, cf2, …) |
Net present value of a series of cashflows |
IRR |
IRR(values, [guess]) |
Internal rate of return (iterative) |
MIRR |
MIRR(values, finance_rate, reinvest_rate) |
Modified internal rate of return |
IPMT |
IPMT(rate, per, nper, pv, [fv], [type]) |
Interest portion of a payment |
PPMT |
PPMT(rate, per, nper, pv, [fv], [type]) |
Principal portion of a payment |
CUMIPMT |
CUMIPMT(rate, nper, pv, start, end, type) |
Cumulative interest paid over a range of periods |
CUMPRINC |
CUMPRINC(rate, nper, pv, start, end, type) |
Cumulative principal paid over a range of periods |
SLN |
SLN(cost, salvage, life) |
Straight-line depreciation per period |
DDB |
DDB(cost, salvage, life, period, [factor]) |
Double-declining balance depreciation |
EFFECT |
EFFECT(nominal_rate, npery) |
Effective annual interest rate |
NOMINAL |
NOMINAL(effect_rate, npery) |
Nominal annual interest rate |
Convention: pv/pmt follow the Excel cash-flow sign convention (money paid out is negative). type = 0 means end-of-period payments (default), 1 means beginning-of-period.
Examples:
=PMT(5%/12, 360, 200000) → monthly payment on a 30yr £200k mortgage at 5%
=PV(8%/12, 60, -500) → present value of 60 monthly payments of £500
=FV(6%/12, 120, -200, -5000) → future value after 10yrs of £200/mo + £5k lump sum
=NPV(10%, -10000, 3000, 4000, 5000, 6000) → NPV of a project at 10% discount rate
=IRR({-10000, 3000, 4000, 5000}) → IRR of a cashflow series
=IPMT(5%/12, 1, 360, 200000) → interest paid in month 1 of the mortgage above
=SLN(50000, 5000, 5) → £9000/yr straight-line depreciation
=EFFECT(5%, 12) → effective annual rate for 5% nominal compounded monthly
Press :help (or :h) to open a full-screen searchable help overlay.
- Keybindings tab — all Normal, Insert, and Visual mode keybinds organized by category
- Formulas tab — all 50+ built-in functions with one-line descriptions
- Search — type any text to filter entries across both tabs instantly
- Navigation —
j/kto scroll,Tabto switch tabs,qorEscto close
ASAT reads a config file on startup at ~/.config/asat/config.toml (respects $XDG_CONFIG_HOME).
Press c on the welcome screen to open it in your $EDITOR. The file is created automatically with all options commented on first open.
# Select a built-in theme by name — no hex values needed
theme_name = "nord"
# Display
default_col_width = 10
min_col_width = 3
max_col_width = 60
scroll_padding = 3
show_line_numbers = false
relative_line_numbers = false
highlight_cursor_row = false
highlight_cursor_col = false
show_formula_bar = true
show_tab_bar = true
show_status_bar = true
status_timeout = 3 # seconds before status message fades (0 = never)
# Editing
undo_limit = 1000
autosave_interval = 0 # seconds between auto-saves (0 = disabled)
backup_on_save = false # write a .bak before overwriting
confirm_delete = false # ask before dd / :dr / :dc
wrap_navigation = false # wrap cursor at sheet edges
# Number formatting
number_precision = 6 # max decimal places for unformatted numbers
date_format = "YYYY-MM-DD"
# Files
default_format = "csv" # fallback format when saving without extension
csv_delimiter = ","
remember_recent = 20 # files shown on welcome screen
# Custom colors — only used when theme_name = "custom" or ""
# [theme]
# cursor_bg = "#268BD2"
# cell_bg = "#002B36"
# ...Built-in themes: solarized-dark, solarized-light, nord, dracula, gruvbox-dark, gruvbox-light, tokyo-night, catppuccin-mocha, catppuccin-latte, one-dark, monokai, rose-pine, everforest-dark, kanagawa, cyberpunk, amber-terminal, ice, github-dark
Browse and apply themes interactively with :theme.
ASAT includes a Python plugin engine via PyO3, enabled by default. Place your script at ~/.config/asat/init.py — it is loaded on startup and can be hot-reloaded with :plugin reload.
import asat
# React to events
@asat.on("cell_change")
def on_change(sheet, row, col, old, new):
if isinstance(new, (int, float)) and new > 1_000_000:
asat.notify("Very large value!")
@asat.on("mode_change")
def on_mode(mode):
if mode == "INSERT":
asat.notify("Editing...")
# Register custom formula functions (callable as =DOUBLE(A1))
@asat.function("DOUBLE")
def double(x):
return x * 2
# Read/write cells by address
@asat.on("open")
def on_open(path):
asat.write("A1", "Loaded: " + str(path))
val = asat.read("B2")Available events: open, pre_save, post_save, cell_change, mode_change, sheet_change
Plugin API: asat.notify(msg), asat.command(cmd), asat.read("A1"), asat.write("A1", val), asat.get_cell(row, col), asat.set_cell(row, col, val)
Commands: :plugins — open the plugin manager TUI (shows engine status and registered custom functions); :plugin reload — hot-reload init.py; :plugin list — list loaded handlers
To build without Python (smaller binary, no Python dependency):
cargo build --release --no-default-features| Format | Read | Write | Notes |
|---|---|---|---|
.csv / .tsv |
✅ | ✅ | Active sheet only |
.xlsx |
✅ | ✅ | Multi-sheet, via calamine + rust_xlsxwriter |
.xls / .xlsm |
✅ | — | Read via calamine |
.ods |
✅ | ✅ | Multi-sheet OpenDocument Spreadsheet |
.asat |
✅ | ✅ | Native format — bincode + zstd, multi-sheet |
ASAT is structured as a Cargo workspace with focused, single-responsibility crates:
crates/
asat-core/ — Workbook, Sheet, Cell, CellValue, CellStyle (no internal deps)
asat-formula/ — Lexer, parser (AST), evaluator, 50+ built-in functions
asat-io/ — CSV, XLSX, ODS, .asat drivers (calamine read / rust_xlsxwriter write)
asat-tui/ — ratatui widgets: grid, formula bar, status bar, tab bar, command line
asat-input/ — Modal state machine, InputState, AppAction enum
asat-commands/ — Command trait, UndoStack, SetCell, InsertRow/Col, DeleteRow/Col
asat-plugins/ — Plugin manager: PyO3 Python backend (enabled by default)
asat-config/ — Config struct, config.toml parsing, ThemeConfig
asat/ — Binary: main loop, AppAction dispatch, ex-command handler
GNU General Public License v3.0 — see LICENSE for details.
Any fork or modified version distributed publicly must remain open source, credit the original authors, and use the same license.