Apps

Sheet

# Sheet — reactive deft spreadsheet A spreadsheet where every cell is a deft expression. Cells are evaluated in a calculation sandbox inside the app's own runtime (one namespace per workbook); each cell's value is bound as a live variable, so other cells reference it directly. The UI is a hand-drawn Lotus 1-2-3-style panel: a four-row control panel on top (cell address + contents, the cursor cell's metadata line — `every 5s · watch … · pub · num=%.2f · align=center`, with `(row)`/`(col)` tags when formatting is inherited from an overlay —, the command/menu line, a description line), the cell grid with column letters and a row-number gutter, and a status line at the bottom. The mode indicator sits in the top-right corner. ## Cells - `=` prefix → expression. The rest of the cell is a deft expression: `=[abs $a1]`, `=($a1 + 8)`, `=[somefunc $b5]`. Infix arithmetic must be parenthesized (deft rule) — also inside call arguments: `=[triple ($b3 * 2)]`, not `=[triple $b3*2]`. Multi-line expressions are fine. - No `=` → literal. Numeric-looking text becomes a number (typing `5` gives `5`, so `=($b1 * 2)` works); other text stays a string, and numeric-looking literals right-align (numbers that don't fit show `>>>>>>>`). Literal text spills into empty right-neighbour cells, Lotus 1-2-3 style. `==` forces the string form. - `~` prefix → symbolic expression. The rest of the cell is read by a symbolic mini-language: infix arithmetic over the exact golden field Q(√5) with `phi`, `sqrt5`, `√`, `^` and `$refs` — `~(phi * phi + 1)`, `~√5 * $a1`, `~(1 / $a1)`, `~(x + 1) * (x - 1)`. Arithmetic stays exact: φ² renders `(3 + √5)/2`, φ+1/φ renders `√5`, and `phi^n` is Fibonacci-exact (to φ⁷⁸, the f64 coefficient horizon). `$refs` create normal dependencies; empty cells read as 0. Unknown names (`x`) stay symbolic. A `:num` printf format applies to the numeric projection (`φ` with `%.4f` shows `1.6180`); without one the cell shows the exact form. Requires `source "deft/sheet/sheet-sym"` in the workbook prog (the template includes it for new workbooks). - Cell values are deft data: numbers, strings, booleans, maps, vectors. - References: `$a1`, `$b5`, … resolve to those cells' latest values (referencing an empty cell yields nil — guard with `[or $a1 0]`). Bare Excel-style refs work too: `=A1*2`. - Feeds: `[sub "btc/price"]` reads the latest value published on that pub/sub channel. When a message lands, the cell and everything downstream recompute and re-publish. - Live cells: a `# every N` suffix re-evaluates the cell every N seconds (sync deft only — `/proc`, sqlite, math): `=[gc/stats] # every 5`. The marker is INPUT syntax only — it is stripped into the cell's `every` metadata (shown on the meta line), never stored in or displayed as part of the source. Content edits keep the interval; manage it with `/Cell Every…` (0 clears). - Shared defs: `<name>.dfs/prog.dft` (next to the workbook's snapshot DB) loads into the calculation sandbox at open (`/File Program` opens it in the editor; saving reloads it live). Its source is snapshotted along with the workbook on every save, and a timeline restore writes the matching program back. - Function library (`deft/sheet/sheet-fns`): the prog template starts with `source "deft/sheet/sheet-fns"` — deft's textual-include statement — which loads the bundled library of spreadsheet functions (sum, avg, count, rounding, text/date helpers, …) into the same namespace as your defs. Defs written below the `source` line override library functions. Existing workbooks: add the line at the top of the prog file. - Ranges in formulas expand to their live values: a single cell → scalar, one row or one column → a flat vector (`=[sum A1:A10]` passes one list), a 2D block → a vector of rows. Library aggregates flatten any of these shapes via `sheet/values`, so `=[sum A1:A10]` and `=[sum A1:C3]` both just work; empty cells are nil (aggregates skip). `:` is the only range separator (`..` is left free for deft's spread syntax). - Errors show as `ERR …` in the cell; cycle cells are marked without evaluating. - Formatting — see Formatting: number formats (printf specs via deft's `format`) and left/center/right alignment, per cell, per range, or as whole-column/whole-row overlays. - Watches (`/Cell Watch…`): an expression that runs whenever the cell's live value changes — user edits, upstream recomputes, feed ticks, `# every` ticks. It runs in the calculation sandbox (so prog-file defs like `rds-set` are in scope) with `$new` and `$old` bound: `[rds-set "Ha" $new]` pushes every change to Redis. Empty submit clears the watch. Watch errors surface on the status line, never in the cell. Scrubbing the timeline never fires; restores/opens fire only when a recompute genuinely changes a value. ## Programmatic control (prog.dft) `prog.dft` defs — and cell formulas — can manipulate the workbook itself through the `[sheets/…]` user API, referred into the calculation sandbox at open (the same namespace as the sheet-fns library — no `source` line needed). `[sheet/…]` remains as the legacy spelling of the same functions. Every call is one undo step.
def seed {||
    [sheets/set-cells %{ "A1" "Item" "B1" "Cost" }]
    [sheets/fill "A2" @{ @{"rent" 1200} @{"food" 400} }]
    [sheets/set-cell "B4" "=[sum B2:B3]"]
    [sheets/set-format "B2:B4" %{ :num "%.2f" :align :right }]
    [sheets/append-row @{"total" [sheets/cell-value "B4"]}]   # log rows
    [sheets/set-column-width "B" 12]
}
- Writes — `sheets/set-cell` (typed-input source), `sheets/set-cells` (bulk map), `sheets/fill` (a value block from an anchor), `sheets/append-row` (one row below a column's last used cell), `sheets/clear-cell`, `sheets/clear-range`. - Reads — `sheets/cell-value`, `sheets/cell-meta` (source, kind, value, error, effective format, every/watch/pub, table info, refs, names), `sheets/cells` (sorted addresses, optional range filter), `sheets/get-range`, `sheets/named-ranges`. - Formats — `sheets/set-format` (cells in a range), `sheets/set-column-format` / `sheets/set-row-format` (overlays that cover empty cells), `sheets/set-column-width`. Patch maps take `%{:num "%.2f" :align :left|:center|:right}`; `:default` clears. - Cell settings — `sheets/set-every` (`# every N` seconds), `sheets/set-watch` (`$new`/`$old` bound), `sheets/set-publish` (the `sheet/<wb>/cells` feed; an optional trailing name overrides the feed key so chart series read `rss` instead of `H14`). - Named ranges — `sheets/define-range`, `sheets/remove-range`, `sheets/get-range`. - Tables — `sheets/table`, `sheets/table-rows`, `sheets/table-cols`, the view verbs (`sheets/table-transpose` / `-hide-cols` / `-filter` / `-view`), `sheets/table-spill`, `sheets/table-refresh(-all)` and `sheets/table-owner` (see "Tables" above). - Workbook — `sheets/save` (a snapshot, like Ctrl+S), `sheets/workbook-name`, `sheets/prog-file` / `prog-source` / `write-prog` / `reload-prog`. Writes from a formula or watch run after the current recalculation pass (the status says `(queued)`), so they can't corrupt an in-flight recalc. Bad addresses, format patches and intervals return `%Err` instead of raising. ## Selecting ranges - shift+arrows (or click-drag) select a range from the cursor; ctrl+shift+arrows extend it to the edge of the data block (the anchor stays put; repeat to extend across discontiguous blocks). Plain arrows drop the selection and move, and ctrl+arrows drop it and jump to the edge of the data block (ADD mode keeps the selection). - ctrl+click / ctrl+drag add a non-contiguous region (Excel-style); ctrl+click inside a selected region removes that region. shift+click extends the active region to the clicked cell. - Shift+F8 toggles ADD mode (the mode indicator shows `ADD`): shift+arrows start a new region and plain arrows move the cursor without dropping the selection, so several regions can be built entirely from the keyboard. Esc clears the selection and leaves ADD mode. - The control panel shows the selection as an address list (`B2:D4,F6`). Range commands take the whole selection: `/Range Erase` and `/Range Format` apply to every region, `/Copy` and `/Move` use the selection's bounding box with only the selected cells travelling (holes stay empty), `/Worksheet` Insert/Delete merge the selected rows/columns into spans, and `/Range Names Name…` names the active region (a name is a single rect). `Delete` clears every selected region. ## Tables (spilled results) A cell whose expression produces `%Table{:name "NAME" :value rows}` spills the result over the cells below/right of it as virtual members (no per-cell storage — the data lives in the anchor cell's result and its source, e.g. a database). The name is obligatory and is auto-registered as a named range over the table's region on every evaluation, so `$NAME` always tracks the dataset's size — charts/subscribers get change events when it grows or shrinks. - `=[table "USERS" @{ @{1 "alice"} @{2 "bob"} }]` — a flat vector spills across one row, nested rows spill as a block; ragged rows pad with nil; `nil` gives an empty (degenerate) table whose name still resolves. - The anchor cell's value is its top-left element; every member reads as an ordinary cell: `=[$b2]`, `=[myfn $b2 $c2]`, `=[sum A1:C20]` all work. The whole dataset rides the named range: `=[sum $USERS]`. - Members can't be edited (`cell is part of the table at A1`); erasing a table's anchor removes the table; a second table (or a manual named range) claiming a taken name errors `#NAME!`; a table whose region collides with real cells or an earlier table errors `#TABLE!`. - Spilled members count as filled cells for navigation: `Ctrl`+arrows and `End`+arrow jumps stop at the spill's edges instead of skipping over the table. - Copying a section of a spill copies computed values only: members paste as literals (`Ctrl+C` on a member copies its value; with a selection active, `Ctrl+C` copies the WHOLE selection as a block and `Ctrl+V` pastes its shape at the cursor, re-selecting what it pasted), and when the selection includes spill members the anchor copies as a value too, so the pasted block never re-spills. A selection of just the anchor still copies its formula, so `/Copy` can duplicate a table (rename the copy's table name). - Snapshot/scrub, `get-range` RPC, and charts read spilled members transparently. - Kernel caveat (open-repo): deft-ify'd expansions and sandbox compiles top out well below the 450-cell formula cap (~a few dozen to a hundred cells in one expression) — keep formula ranges over tables modest, or read whole datasets through the named-range RPC path. ### Query tables (async) `sheet/dbquery` (from the sheet-fns library) is an `@async` function: it returns a Future that resolves to the query's matrix — await it where you call it:
=[table "USERS" [await [sheet/dbquery "data/app.db" "select id, name from users order by id"]]]   # async
Cells whose expression calls `sheet/dbquery`, carries an `[await …]`, or a `# async` marker evaluate asynchronously: the sync recalc skips them, they run as a coroutine on the app runtime's event loop (the query itself rides the sqlite executor threads — the UI never waits on the database), and when the result lands the value spills and dependents recompute. Combined with `# every N` a table re-queries on a cadence:
=[table "PRICES" [await [sheet/dbquery ".deft/btc.db" "select ts, price from ticks order by ts desc limit 500"]]] # async # every 30
The optional third argument is a bind-params vector or `%{ :headers true :params @{} }` (a header row of column names prepended). ### Table views (pivot-style derives) `%Table` is a real `deftype` — view verbs derive NEW tables (the snapshot is never mutated) and record what was applied as recipe metadata (`:hidden-cols`/`:filter`/`:transposed`). Because a named range reads as a plain row matrix inside a formula, the verbs work both as methods on a table object and as plain functions over matrices:
=[table "EU" $USERS(hide-columns @{"region"})]            # postfix on the range matrix
=[table "TT" [transpose $USERS]]                          # bracket form, same fns
=[table "FF" $USERS(filter-rows "\$age > 30")]            # infix expression over $columns
=[table "CH" [table "W" rows %{ :headers true }](hide-columns @{0} |> transpose)]  # chain
- `transpose` — flip axes (ragged rows nil-pad; headers do not survive). - `hide-columns cols` — drop columns by header-row name or 0-based index, scalar or vector. Unknown columns error the cell. - `filter-rows spec` — a predicate fn (a `%{col val}` row map when the table has `:headers true`, else the row vector), ANDed `%{ :col :op :value }` triples (`= != < <= > >=`), or an infix expression string over `\$`-prefixed column names (needs a header row). Name-based specs keep the header row; a row whose predicate errors is dropped. - `col-names` — the header row as strings. - `table "NAME" value %{ :headers true :source … }` — the wrapper takes opts: `:headers` marks row 0 as column names, `:source` attaches provenance metadata. Wrapping a derived view re-names it for spilling. The `sheets/` API drives the same transforms by NAME (move-proof — like `sheets/table-owner`) from prog code, cells, RPC and agents: `sheets/table` (the live Table object), `sheets/table-rows`, `sheets/table-cols`, `sheets/table-transpose`, `sheets/table-hide-cols`, `sheets/table-filter`, `sheets/table-view` (spec map `%{ :filter :hide :transpose }`, fixed pipeline filter → hide → transpose). `sheets/table-spill "NAME" "CELL" src` spills a derived table in one call as a LIVE expression (re-derives when the source refreshes — the spilled cell is registered as a reader of the source name and a dependent of its anchor). src forms: a spec map `%{ :table "SRC" :hide … }`, a call-expression string, or a literal rows matrix (a frozen copy — no refresh propagation):
[sheets/table-spill "EU" "D2" %{ :table "USERS" :hide @{"region"} }]
[sheets/table-refresh "USERS"]        # EU re-derives via the dep graph
Refresh: `/Data Refresh` (or `Ctrl+R`) re-runs the datasource behind the table under the cursor — query tables re-query, derived views follow; `/Data Refresh All` re-runs every table anchor. Programmatically: `sheets/table-refresh "NAME"` (also accepts an anchor cell id), `sheets/table-refresh-all`. ## Formatting - Number formats are printf-style specs evaluated with deft's [`format`](https://deft/help/format) at draw time — stored values stay raw (recalc, `get-range` RPC and publishing all see the unformatted number): `%.2f` → `3.14`, `%05d` → `00042`, `%+.1f` → `+3.1`, and literal text around the verb works (`$%.2f`). Formats apply to numeric values only; strings pass through untouched. - Alignment: left / center / right overrides the type default (numbers right, text left). Centered and right-aligned text never spills into neighbouring cells. - Precedence, per property: cell format → row overlay → column overlay → default. A cell that sets only alignment still inherits the column's number format. - `/Range Format` — `Number…` (prompts for the spec; empty submit clears), `Left`, `Center`, `Right`, `Clear`. Works selection-first or POINT mode like the other range commands, and applies to the existing cells in the range — empty cells carry no metadata, so format those regions with the column/row overlays below. - `/Worksheet Column Format…` and `/Worksheet Row Format…` — number format for a whole column/row; the `Align` submenus set alignment. These overlays cover empty and future cells too. - `/Copy` and `/Move` carry cell formats with the cells; editing a cell's contents keeps its format; erasing a cell drops it. Inserting or deleting columns/rows shifts the overlays and widths with the data. - Column formats, row formats and column widths persist in every snapshot; older snapshots (saved before the feature) simply load with none. ## Undo and redo `Ctrl+Z` / `Ctrl+Y` (or `/Worksheet Undo` / `Redo`) step back and forward through your commands — one undo step per command: cell edits and clears, paste, `/Copy`, `/Move`, `/Range Erase`, `/Worksheet Insert` / `Delete`, range/column/row formatting, column widths, named ranges, and cell settings (watch, every, publish). - The document state (cells, named ranges, format overlays, column widths) is snapshotted before each command and rebuilt on undo, so formulas recompute — undoing an edit to `A1` also reverts everything that reads `$a1`. The cursor returns to where the change was made. - Not covered: cursor movement, scrolling, the snapshot timeline (scrub/restore are the history for that), prog-file edits, and `/Worksheet Erase` (which clears the history along with everything else). - The history also clears on workbook open and timeline restore, and is capped by the `sheet/undo-max` config key (default 200; 0 disables undo). ## The / menu `/` opens the classic command menu on the control panel's second row — move with the arrow keys, jump with an item's first letter, Enter selects, Esc backs out one level. The third row describes the highlighted item. - `/Worksheet` — Undo/Redo (Ctrl+Z/Ctrl+Y — one step per command, see Undo and redo), Column (Set-Width live-adjusts the current column with the arrows, Reset restores defaults, Format… sets a printf number format for the whole column, Align sets whole-column alignment, Insert inserts blank columns at the cursor — point or type the span; cells shift right/down, refs follow, ranges spanning the insert expand, cells pushed off the grid are dropped — and Delete removes columns — cells shift left/up, refs into the deleted region become `#REF!` errors, ranges shrink), Row (Format… / Align — same rules for rows, Insert / Delete), Erase (whole workbook), GoTo - `/Range` — Erase (POINT a range), Format (Number… / Left / Center / Right / Clear for the range's existing cells), GoTo, Names (manage named ranges: `Name…` POINTs/types a range then prompts for the name, `Delete…` picks a name to remove, `GoTo` jumps the cursor to a named range's top-left — F3 in READY picks a name to jump to, F5 accepts names alongside cell addresses, and F3 while pointing inserts a name into the formula/range) - `/Copy` — point a source range, then a destination: a single cell copies once, a destination the source's size copies once, and an exact multiple of the source tiles to fill it (non-multiples error). Relative refs shift per copy; `$`-anchored refs stay put. Spilled table members copy as their computed values (see Tables) - `/Move` — point a source range, then a destination: the block lands with its top-left at the destination (point or type a single cell); relative refs inside shift, external refs into the block follow - `/Cell` — Edit, Clear, Delete (Left / Up: remove the selected cells and pull the cells to the right / below back into the gap; refs into the removed band become `#REF!`), Publish toggle, Every…, Watch… (fires an expression with `$new`/`$old` when the value changes) - `/File` — Save, Save As… (save a copy under a new name and switch to it in the same panel — same name grammar as New…; the copy starts from the current state with a fresh timeline and the original keeps its own; submitting the open sheet's own name is a plain save), Open… (pick a sheet directory and open it in its own panel — an already-open sheet is focused instead of duplicated), New… (prompt for a name and open it in a new panel), Close (save this sheet and start a new empty one in the same panel — the app stays open), Program (edit in the editor), Reload (force a prog-file reload now — editor saves reload automatically, Reload is the deterministic fallback) - `/Snapshot` — Take, Restore…, Scrub, Live - `/Data` — Recalc - `/Quit` POINT mode: while a command waits for a range, arrows extend the highlight from the anchor (ctrl+arrows jump the corner to the block edge), typing enters a range directly (`A1:B10` or a defined name), Enter accepts, Esc cancels. ## Keys | Key | Action | |-----|--------| | `/` | open the command menu | | typing | edit the cursor cell (VALUE mode) | | arrows / PgUp / PgDn / Home / End | move the grid cursor | | `Ctrl`+arrows | jump to the edge of a data block: from a filled cell to the block's last cell, from a blank cell to the next filled cell (or the grid edge) | | `Ctrl`+`Shift`+arrows | extend the selection to the edge of a data block (anchor stays put; repeat to keep extending) | | arrows (while entering) | point at a cell when an operand is expected (see below), else commit the entry and move | | `Enter` · `Tab` | edit the cursor cell · commit + move right | | `F2` | edit the existing text; F2 again forces POINT mode (arrows point anywhere in the formula) | | `F3` | NAME: pick a defined range name — insert it into the formula/range being pointed, fill the GOTO / Range-Name prompt, or jump to it in READY | | `Escape` | cancel the pointed ref (pointer returns to the formula cell) / cancel edit / menu / point, drop the selection and ADD mode, or return to live while scrubbing | | `Delete` | clear the selection (every region) or the cursor cell | | `shift`+arrows / shift+click | select a range (extends the active region) | | `ctrl`+click / ctrl+drag | add a non-contiguous region (ctrl+click a selected region removes it) | | `Shift+F8` | toggle ADD mode for keyboard multi-selection | | `F4` | cycle the ref's anchors — `B2` → `$B$2` → `B$2` → `$B2` → `B2` (the pointed ref, the ref at the edit cursor, or the typed ref in the GOTO prompt) | | `F5` | go to a cell (`A1`, `B2:D4`) or a defined range name — works while editing/pointing too | | `F9` | recalculate every cell | | `[` / `]` | scrub the snapshot timeline (graphs follow) | | `Ctrl+Z` / `Ctrl+Y` | undo / redo the last command | | `Ctrl+S` | save the workbook | | click / drag / wheel | move cursor, extend point ranges, scroll | ## Point editing (Lotus 1-2-3 semantics) While entering a formula, arrows pick cells whenever the text before the cursor expects an operand: after an operator (`= + - * / ( [ , :`), at a call head (`=[sum` — the first argument) or at an argument boundary inside an unclosed call (`=[sum 5 ` — a trailing space). The pointed ref splices into the formula and keeps updating as you move; typing an operator re-arms pointing from the pointer's cell, so `+` → `→` → `+` → `→` builds `+B1+C1`. - `.` anchors a range (`B2:D4` grows with the arrows); pressing `.` again cycles the free corner clockwise — arrows then reshape the range from the new corner. - `F3` NAME shows the defined names: picking one splices `$NAME` into the formula (replacing an active pointed ref) and ends pointing. - `F4` cycles the pointed ref's anchors: `B2` → `$B$2` → `B$2` → `$B2` → `B2`; with no active point it cycles the ref at the edit cursor (a range cycles both endpoints together). - `Esc` cancels the pointed spec: the ref is removed and the pointer returns to the formula cell. - `Enter`/`Tab` commit; the pointer moves down/right from the formula cell, not the last pointed cell. - While pointing: `Home` jumps to A1, `End` + arrow (or `Ctrl`+arrow) jumps to the last cell of a data block (or the next block / grid edge), `End Home` jumps to the bottom-right of the used range, `F5` (goto) moves the pointer — by cell address or defined name (`F3` picks the name, `F4` cycles a typed ref's anchors) — and the ref follows. - A complete operand followed by an arrow (e.g. `=[sum b1` with no trailing space) commits the entry, Lotus-style; press `F2` to force pointing instead. ## Storage A workbook is a `<name>.dfs/` directory you place anywhere in the project — there is no storage root (`.deft/` holds metadata only). A reference is either a bare name, resolved in the CWD (`main` → `./main.dfs`), or a path (`sheets/main` → `./sheets/main.dfs`). - `<name>.db` — SQLite WAL snapshot store, the single source of truth. Every save (Ctrl+S) appends a snapshot row of the full state (cells + named ranges + format overlays + column widths + program source); the latest row is the current save. Manual snapshots share the same timeline (labelled by time, saves by `save HH:MM:SS`); `[`/`]` scrub across all of them. - `prog.dft` — the live-editable program file (watched, reloaded on save). Each sheet opens in its own panel (multiple sheets side by side): `/File` Open…` recursively lists the `.dfs` directories found under the project` directory (CWD) — nested sheets show as `sheets/main` — and launches a fresh sheet panel for the chosen one. `/File New…` takes a name (a typed path places it anywhere; a bare name lands next to the current sheet), `/File Save As…` saves the current sheet under a new name in the same panel (a fresh snapshot store; the original keeps its history), and `/File Close` saves the sheet and leaves an empty new sheet (`untitled`, `untitled-2`, …) in the SAME panel — the app stays open. The panel is tagged `sheet:<dir>` so opening an already-open sheet focuses it instead of duplicating it, and the open workbook's path is recorded in config `sheet/last-workbook` (`.deft/config.dft`) so the next launch auto-opens it. Legacy layouts migrate on first open: a workbook directory left in the old `.deft/sheets/` root moves to the resolved location, a bare `.deft/sheets/<name>/` directory is renamed to `<name>.dfs/`, flat files (`<name>.db`, `<name>.prog.dft` — next to the workbook or in the old root) move into the directory, and the old `<name>.dft` — which always held the latest save — is imported once as the newest snapshot (label `migrated`) and then left in place as a backup, no longer written. ## Debugging The app starts a debug repl on 127.0.0.1:9312 at launch — attach a shell (or the editor) with *Session → Attach Runtime → "Connect to port…"*. The shell's roster-driven attach (broadcast) also finds the panel without the port. Override or disable via the config key `sheet/debug-port` (0 disables). Handy from the attached repl: `[keys _cells]` (every cell id), `dbg-cells` (same), `_wb-name`, `[get-cell :a1 "value"]`, `[reload-prog]`. ## Publishing After each recalc the workbook publishes `sheet/<name>/cells` → `%{ :ts :seq :cells %{ a1 42 … } }` — but only cells that opted in via `/Cell Publish` (publishing is off by default; toggling re-sends the full snapshot, so a cell turned off simply drops out of the next one). A published cell may carry a publish name (`sheet/set-publish "H14" true "rss"`), which becomes the feed key — chart legends show the metric, not the address. Scrubbing replays snapshots through the same channel, so a subscribing chart app follows the timeline automatically. ## Named ranges A named range binds a name to an `A1:B10`-style rect (`/Range Names…`). Names are 1-40 chars of letters/digits/`_`/`-` and must not look like a cell ref. They persist with the workbook and are the chart-facing data path: - They follow their cells: `/Move` carries a name whose rect is entirely inside the moved selection (single cells included) and its formula readers recompile against the new location; `/Worksheet` Insert`/`Delete` and `/Cell Delete Left|Up` shift names in the affected` band (a name fully swallowed by a delete is removed). A name only partially covered by a move is left untouched. - RPC (any app can call them; the runtime name is the panel's app name, e.g. `"sheet"`). The full workbook surface is exposed — cells, formatting, cell settings, named ranges, tables, prog file, save. RPC args travel as source text (strings, numbers, booleans, nil, keywords), so collection-shaped methods take flat/CSV forms over RPC and rich forms in-process: - `[await [rpc "sheet" "get-range" "sales"]]` — rows of values (list of lists, nil for empty cells; accepts a name or `A1:B10`). - `[await [rpc "sheet" "named-ranges"]]` — sorted `%{:name :range :topic}` maps. - `[await [rpc "sheet" "workbook-name"]]` — the workbook this runtime has open (`"main"`, …); lets a consumer (charts) target one of several sheet panels. - `[await [rpc "sheet" "define-range" "sales" "A1:B10"]]` / `[await [rpc "sheet" "remove-range" "sales"]]`. - `[await [rpc "sheet" "set-cells" "A1" "Item" "B1" "Cost"]]` — flat address/source pairs (the map form works in-process). - `[await [rpc "sheet" "set-format" "B2:B4" "%.2f" :right]]` — num spec + align keyword, either optional. - `[await [rpc "sheet" "cell-meta" "B4"]]` / `"cells"` / `"fill"` / `"append-row"` / `"clear-range"` / `"set-column-width"` / `"set-every"` / `"set-watch"` / `"set-publish"` / `"save"` — the same verbs as the `[sheets/…]` API. - `[await [rpc "sheet" "table-rows" "USERS"]]` and the other `table-*` verbs — `table-filter` takes an infix expression string (`"$age > 30"`), `table-hide-cols` a `"a,b"` string, `table-view` flat `filter hide transpose` args. - Change events: `sheet/<workbook>/range/<name>` receives `%{:ts :seq :range "A1:B10"}` whenever a recalc, scrub, restore or workbook open touches cells inside the range (a `:removed true` event fires when the name is deleted). Events are signals only — refetch the data via `get-range`. Named ranges ignore the per-cell `:pub` toggle; naming a range is the authorization. ## Example: sysmon live model `examples/sysmon/prog.dft` turns a workbook into a live system/memory monitor: one `# every N` cell samples process RSS/VmData, CPU, GC heap, per-app heap (`tui/app-threads`) and the workbook's own stored-cell count, checks per-metric boundary cells (min / max / max Δ per tick), and — instead of accumulating a growing history in the grid — records a workbook snapshot every `snapshot every (ticks)` ticks (`B3`, 0 disables). The snapshot timeline is the time-series store: scrub it with `[`/`]` (or reopen the workbook later) and the recorded samples replay. Breaches record to `SYSMON_ALERTS` — a single latest-alert row (per-app heap/CPU alerts carry the app name; process metrics log `sys`) whose history lives in the snapshot timeline — and publish on `sysmon/alert`; the worst status cell is watched and re-broadcasts transitions. The blocks are movable: cross-block refs are fully anchored (`$A$23` — `$a23` pins only the column, and a vertical /Move would rewrite the row and stall the table), the bounds block is read through the `SYSMON_BOUNDS` named range (names follow /Move; inline range literals in formulas do not), and `sheet/table-owner` resolves a table's live anchor wherever it lands. ```sh mkdir -p sheets/sysmon.dfs cp <packages>/sheet/examples/sysmon/prog.dft sheets/sysmon.dfs/prog.dft ```` Open the `sysmon` sheet (`/File Open…` → `sheets/sysmon`) or run it headless (`deft packages/sheet/src/run.dft sheets/sysmon`) — the prog auto-seeds an empty workbook. One region per concern: the tick anchor IS the `SYSMON_NOW` table (metric | value | min | max | Δ | trend | status), `SYSMON_APPS` (per-app heap/peak/cpu plus the gc/stats columns gc | freed | next-gc) and `SYSMON_ALERTS` (time | app | metric | kind | value | bound — latest alert, single row; app is `sys` for process metrics). Each table starts with a header row inside the range, is titled by the label cell one row above its anchor, and charts labels bar/pie series by column (`rss·value`, `heap·peak`, …) and skips the header when drawing lines. The published `I17:I23` strip (labelled at `I16`) mirrors the latest values as real published cells — spilled table members can't carry publish flags — and gives charts' cells mode its own live rolling window. Replacing the prog in an existing workbook? Run `[sysmon-reseed]` once (debug repl) to clear the old layout and re-seed. See `workspace-sysmon.dft` for a sheet+charts workspace and the manual page `help/manual/07-TUI/08-sheet-sysmon.md`. ## Headless daemon (model only) The calculation model lives in `model.dft` (cells, dependency graph, recalc, tables, async cells, watches, intervals, named ranges, snapshots, undo, RPC, tickers); the visual panel in `sheet.dft` sources it. The model runs with no panel at all: deft packages/sheet/src/run.dft [workbook] [--backend NAME] [--dsn DSN] - Boots the workbook — a reference (bare name in the CWD or a path) or the config `sheet/last-workbook` default, else `main` — marks the runtime live so async cells (`# async`, `sheet/dbquery`, `[await …]`) evaluate, and starts the tickers: `# every N` intervals and the auto-snapshot cron. - `sheet/auto-snapshot-s` (config; default 0 = off) snapshots the live workbook every N seconds, so the timeline accrues autonomously. - The debug repl listens on `127.0.0.1:9312` (`sheet/debug-port`); a shell/editor can attach via Session → Attach Runtime, or any process can drive the `@rpc` surface. - Stop with Ctrl+C; the cron snapshots are the durable record. Open the same workbook later in retro/retra and scrub the timeline to browse what accrued. ### Snapshot backends Snapshots are recorded data (cell sources plus the values they held at save time); the timeline replays them frozen, with no re-evaluation. The store is pluggable via the `SnapshotStore` contract (`snapshot-store.dft`) with three implementations: | Backend | Store | Notes | |---------|-------|-------| | `sqlite` (default) | `<wb>.dfs/<wb>.db` (workbook dir anywhere) | the panel's local store | | `postgres` | shared `sheet_snapshots` table, `(workbook, ts)` key | for headless daemons | | `memory` | process memory | tests / ephemeral runs | Select with `--backend` (daemon) or config: - `sheet/snapshot-backend` — `"sqlite"` (default), `"postgres"`, `"memory"`. - `sheet/snapshot-dsn` — Postgres DSN (e.g. `postgres://user:pass@host:5432/db?sslmode=require`); also read from `SHEET_SNAPSHOT_DSN`, and `pg/connect` honors the `PG*` env vars. - `sheet/snapshot-table` — table name (default `sheet_snapshots`, sanitized to `[A-Za-z0-9_]+`). One table holds many workbooks, keyed by a `workbook` column. deft packages/sheet/src/run.dft prices \ --backend postgres --dsn 'postgres://user:pass@host:5432/deft' The Postgres backend awaits the native pg client, so it is intended for the daemon; panels stay on sqlite (a panel configured for Postgres would block its thread on every snapshot). The VM shutdown save is skipped for non-sqlite stores — the cron snapshots are the durable path. Cells that pull from databases/HTTP are just cells — e.g. =[table "PRICES" [await [sheet/dbquery "prices.db" "select * from t"]]] # every 60 # async re-derives the table every minute while the daemon runs.