cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

cat/module-path
012 / docs / contributing / architecture.md
architecture.md 63.8K rendered
1---2title: "Architecture"3sidebar_position: 24---56# Architecture78012 is a single pure-Go binary (`CGO_ENABLED=0`) built on Bubble Tea v2 and9Lip Gloss v2.1011```12cmd/012 entry point: flags and config, 012 config, 012 serve, JEV setup, opening or importing a file, 012 - and --pipe on the terminal, the commands without the screen13internal/config the config file and the registry of options14internal/sheet the engine: cells, recalculation, undo, files, names, pivots15internal/functions the function library: the FuncDef table, evaluation, decimal arithmetic, JEV questions16internal/value cell values, number formats, typed-entry parsing, the clock17internal/formula the formula language: references, lexer, parser, printer, rewriting18internal/numfmt number formats, rounding, General, date serials19internal/locale the locales: separators, date order, currency, formula separators20internal/fileio import and export: CSV, TSV, XLSX, SQLite, Parquet, Lotus .wk1, JSON, NUON; tables on streams21internal/nuon nushell's object notation: typed values, tables read a row at a time, written back22internal/live the sources of linked regions: files followed as they grow or are rewritten23internal/paged linked sources read in place: the host answering formulas and pivots by streaming them, the pages a tab shows24internal/headless workbook files without the screen: references, get, set, recalc, notebooks and JEV when asked, atomic saves25internal/diff workbooks compared and merged cell by cell, from their files' fields26internal/chart chart layout, text rendering and kitty image encoding27internal/jev the API key's resolution, answer cache and TypeSafe client28internal/keyring the OS credential store the API key lives in29internal/macro macros: the Starlark scripting API, recorded actions as scripts, step limits30internal/notebook a notebook's document: code and note cells, names, what each reads, run orders, stale outputs, save caps31internal/nushell a notebook cell's run: nu as a process, tables in as NUON files, NUON back32 module 012.nu, the nushell module with sheet, embedded for 012 nu --module and --install-module33internal/telemetry opt-in JSON log and OTLP export of spans, events and frame stats34internal/serve the SSH server: auth, host key, a Model per session (charm.land/wish/v2)35internal/room shared workbooks: a room per file, a seat per participant, taking turns on the one workbook36internal/confine resolving typed file names, confined to a directory when served37internal/stress synthetic worst-case sheets for the -tags stress benchmarks38internal/ui the Bubble Tea model: modes, menus, overlays, rendering39 theme style roles, color schemes and the widgets drawn with them (frames, key chips)40 rowtext laying out a row of cell text across the columns on screen41 formula reading the formula being typed (F4, the word and call at the caret)42 overlay the contract of what takes over input: Overlay, boxes, mouse events, lists43 lineedit the one-line editor every text field shares, and the multi-line one of notebook cells44 picker the searchable list behind the palette and every picker45 cmdline the : command line and its completions46 nbview a notebook tab as Jupyter's: its toolbar, cells, outputs, command and edit modes, mouse, the code editor and its language providers47 srcview a linked source's tab: its rows a page at a time, the row numbers and scrollbar over every row48 findbar find and replace, a bar on the context line49 evalview Data > Evaluate formula: a formula stepped through part by part50 themepicker File > Settings > Theme, previewing as it moves51 rules the conditional formatting and data validation panel52 sortbar the sort bar: the columns and order to sort a range by53 filterpick a filter column's values and condition54 choicebar small questions on the context line answered with a key55 shortcuts the keyboard shortcuts view56 suggest formula suggestions and function signatures while typing57 tabstrip the sheet tabs' layout and where each sheet was left58 transfer imports running in the background and their progress59e2e/ end-to-end tests through libghostty (separate module, cgo)60oracle/ differential tests against excelize's calculation (separate module)61```6263## The engine6465`internal/sheet` knows nothing about terminals. It builds on the function66library, which knows nothing about sheets or storage, and on packages67that know nothing about cells. Dependencies point one way, down the68graph of imports below the engine; `internal/locale`, at the bottom,69holds the table of locales and depends on nothing:7071```mermaid72flowchart TD73 subgraph callers [the engine's callers]74 direction LR75 ui[internal/ui] ~~~ fileio[internal/fileio] ~~~ live[internal/live] ~~~ nushell[internal/nushell]76 chart[internal/chart] ~~~ jev[internal/jev] ~~~ stress[internal/stress] ~~~ oracle[oracle]77 headless[internal/headless] ~~~ diff[internal/diff] ~~~ room[internal/room] ~~~ paged[internal/paged]78 end79 callers --> sheet[internal/sheet]80 sheet --> functions[internal/functions]81 sheet --> formula[internal/formula]82 sheet --> value[internal/value]83 sheet --> numfmt[internal/numfmt]84 sheet --> locale[internal/locale]85 functions --> formula86 functions --> value87 functions --> numfmt88 value --> numfmt89 value --> locale90 formula --> locale91 numfmt --> locale92```9394- `internal/locale` is the conventions of each locale: decimal and95 thousands separators, date order and patterns, currency symbol and96 place, and the formula separators that follow from the decimal one.97 Cells, formulas and files keep en-US's form whatever the locale; the98 packages above translate at the edges, where entries are typed and99 shown: `value.Canonicalize` and `Localize` for typed values,100 `formula.Delocalize` and `Localize` for formulas (one byte for one,101 so positions carry over), `numfmt.FormatIn` and `Format.CodeIn` for102 display, and `sheet.CanonicalEntry` and `LocalEntry` for whole103 entries. The workbook's locale is a file setting beside decimal104 arithmetic ([Locale](../sheets/locale.md)).105106- `internal/formula` is the language: `Addr` and `Rect`, A1 references107 with their `$` markers, sheet names in references and their quoting,108 the lexer and Pratt parser, the syntax tree, the printer and the109 reference rewriting for copies, moves and inserted or deleted rows and110 columns. The parser learns which functions exist through a small111 `Func` interface that the library's `FuncDef` implements.112- `internal/value` is what a cell computes to and how it is shown:113 `Value` with its kinds and error codes, the coercions and ordering114 every function shares (`ToNum`, `Compare`), `Format` and its kinds, and115 typed-entry recognition (`ParseValue`, which turns "$1,200" or "9/26"116 into a number and a format), with the clock it reads (`value.Now`,117 which TODAY and NOW read too; tests replace it).118- `internal/functions` is the function library; see119 [Functions](#functions) below.120- `internal/numfmt` renders numbers: number format patterns (as in TEXT121 and custom formats), Sheets' General form, rounding on 15 significant122 digits, and the calendar of serial day numbers.123124`sheet` re-exports the types and helpers its callers need (`sheet.Addr`,125`sheet.Rect`, `sheet.Parse`, `sheet.Value`, `sheet.Format`,126`sheet.ParseValue`, `sheet.FuncDef`, `sheet.Funcs`, the remote types,127`sheet.FormatPattern`, the sheet-name helpers) as aliases and thin128wrappers, so the UI, importers and charts see one engine package.129130A `Workbook` holds ordered `Sheet`s, the named ranges, the undo history,131the arithmetic setting, the JEV source and recalculation, as a Sheets132spreadsheet does. Each sheet keeps its cells, sparse, by address, with133its column widths, frozen panes, filter and charts; each cell keeps what134was typed, its parsed expression, its computed value, and its format and135style.136137- **Cells.** Each sheet keeps its cells in a `cellStore` (`store.go`):138 get, set, delete, count, iterate all or a range, and narrower reads139 (a value, presence, formatting, the formula cells). Nothing else140 touches the representation beneath. Occupancy indexes (`occupancy.go`,141 bitmaps of 1024 rows per column) record which cells are stored and142 which have contents, so a range of the million-row grid is read at the143 cost of what it holds; the stored index's blocks hold the cells144 themselves, a 16-byte slot each in row order (`slot.go`). A plain cell145 (a number, boolean or text as typed, with a format and style) lives in146 its slot, with its text in a table of strings kept once each, its147 formatting in a table of looks, and its input only when that isn't148 the value's own text ("1.50" is kept as 1.5 printed with two149 decimals). What pivots and spills write is a slot too, marked as150 derived, holding the value and, in its look, a spill's inferred151 format; its entry is the value's text. Formulas and notes are whole152 `Cell`s in a side table, which recalculation updates in place. `get`153 hands out a plain cell as a `Cell` made for the caller, a copy whose154 changes reach nothing; `set` is the one way to change a cell. What155 each costs is in [Bounds of support](limits.md#sheet-size). Formats of156 whole columns and rows, and of the whole sheet, live on the lines157 (`lines.go`); a cell falls back on them.158 Copy, paste and move carry the formatting cells show (`clipfmt.go`):159 whole lines as line formats, blocks as the cells' own. A line's format160 changing recalculates the formulas reading any cell of it, found through161 the dependency indexes (`linereaders.go`), so they infer their format162 from blank cells too.163- **Layout.** A cell's style holds how its text wraps and the lines on164 its edges (`borders.go`), so both fall back on lines and travel with165 copies. Rows' heights set by hand are kept per sheet as column widths166 are, and merged ranges in the view state, replaced whole, so undo, the167 file and inserted lines follow them (`rowlayout.go`, `merge.go`, which168 finds a cell's merge through an interval index). Cells whose own style169 wraps or draws borders are indexed by row, so the grid measures the170 rows it shows at their cost, and a sheet with none of these (not171 `Shaped`) is drawn a line per row without asking.172- **Parsing.** `internal/formula`'s hand-written Pratt parser turns173 formulas into an AST, keeping absolute markers so references can be174 rewritten when cells move. Its printer turns ASTs back into text in175 Sheets' spelling. Decimal arithmetic marks the operators it computes176 with node types of the function library's own, so trees stay as177 parsed.178- **Recalculation.** Formulas record the cells and ranges they read on179 their own sheet; references that name a sheet (`Sheet2!A1`) are kept by180 name and resolved when evaluated, so renaming rewrites them and a deleted181 sheet's references wait, as `#REF!`, for a sheet of that name. Changing a182 cell marks it and everything that transitively depends on it, on any183 sheet, then evaluates the marked cells lazily in dependency order.184 Before evaluating, cycles among the marked formulas are found from the185 references as written, IF branches not taken and range reads cut short186 included, with a segment tree per column standing in for ranges and187 Tarjan's algorithm (`cyclefind.go`), so a cycle's cells are `#REF!`188 whatever order they're computed in; a cell reached again while it is189 being evaluated is part of a cycle too. Volatile190 functions (TODAY, RAND, the JEV functions) are recomputed on every191 recalculation (`recalc.go`). Formulas read other cells through each192 sheet's `reader`, the engine's side of the function library's `Book`193 (below): one cell, the cells a range holds in chunks, a range's bounds,194 and the running aggregates SUM-like functions share within a195 recalculation (`rangememo.go`). The formulas whose ranges contain a196 changed cell are found through interval trees per column197 (`rangeindex.go`).198- **Functions.** One table (`FuncDef`, in `internal/functions`) holds199 every function's name, signature, description and arity; it drives200 parsing, evaluation, autocomplete, in-app help and201 [Functions](../reference/functions.md). See [Functions](#functions).202- **Undo.** Every mutation goes through a small set of paths that snapshot203 the cells, widths, names, charts, view state and sheet list they change,204 on any sheet, so a step can be reversed exactly; multi-cell operations205 are one step. A step keeps each sheet's before-images as an image206 (`historyimage.go`) in the store's own form: plain cells as slots in207 columns of blocks, their text and formatting in the image's tables,208 and whole `Cell`s only for rich ones, so clearing a whole sheet holds209 about what the sheet does. A deleted or replaced sheet keeps its210 cells, so undo brings it back. The history keeps at most 100 steps and211 about 256 MB of before-images, counted as they're recorded, dropping212 the oldest steps first; a change whose step would pass 1 GB213 (`UndoCost`, `MaxStepBytes`) asks first, and runs `WithoutUndo` if told214 to go on.215- **Pivot tables.** A sheet may hold a pivot (`pivot.go`): a definition216 naming its source by sheet name, as formulas do, and a region of217 derived cells from A1 that the engine owns. When a recalculation marks218 a cell of a pivot's source range, or its definition changes, the pivot219 is recomputed after the formulas (`pivotcalc.go` groups the source220 rows into a tree of accumulators, `pivotlayout.go` lays out Sheets'221 table) and only the result cells that changed are rewritten, outside222 the undo history, then what reads them recalculates, pivots of pivots223 included (bounded, as a loop can't be defined). Undo restores the224 source and the definition, and the results follow. Derived cells are225 constants to formulas, values to copies and exports, refused by `Set`,226 and never saved: the file keeps the definition (`pivotfile.go`). A227 frequency table is a pivot with a preset definition.228- **Spills.** A formula computing an array (its anchor) shows the first229 value and spills the rest into the cells to its right and below230 (`spill.go`): spilled cells are derived cells as a pivot's results231 are, flagged `spilled`. Each evaluation pass notes the arrays anchors232 computed; after it, each is written (only the cells that changed, and233 nothing when the array and its cells are as they were), outside the234 undo history, then what reads the changed cells is recalculated, which235 may spill again (at most 64 passes). A sheet indexes its anchors by236 the cells they cover or would (an interval tree, as range users are),237 so a cell placed in a spill or in the way of a blocked one has its238 anchor recalculate, and a spilled cell placed (by undo, a move or a239 format) keeps only its formatting and note. A blocked anchor shows240 `#REF!` with the reason. `Set` refuses spilled cells; files keep only241 the anchor.242- **Regions.** A region (`region.go`) is a block of cells whose values243 come from outside the engine, of two kinds: a linked file244 (`linked.go`), a file the UI follows, and a notebook cell's output sent245 to a sheet (`Region.Output`), each its table at its anchor. Both are246 one list per sheet, the sheet's `regionState`, replaced whole on every247 change so undo steps keep the definitions; what isn't undone (the248 cells written, how the rows arrive) is kept beside it (`regionMeta`).249 Their cells have one write path, `writeTable` (`regionwrite.go`): a250 header and rows, written as spilled cells are, derived and outside the251 undo history, only those that differ, with the region's cells it no252 longer needs cleared; a table that would overwrite other contents253 isn't shown, its first cell saying where, and clearing that cell shows254 it; regions in each other's way settle as arrays do, the first anchor255 winning (`regionclash.go`), so the order rows arrive in doesn't256 matter. Rows arrive only as live operations, the change stream below, and257 are never part of the undo state, since undo can't bring back what a258 file held or a run printed: a region undo brings back or moves is259 emptied and marked stale, and the UI sends its rows again, the file260 read again or the cell's output as it is. Placing a cell in a region261 (a format, undo) keeps the region's value. Formulas name a region's262 table as `nu.name`, resolved when evaluated as named ranges are. The263 file keeps the definitions (`regionfile.go`), and converts the command264 regions of earlier files into notebook cells (`notebookfile.go`).265- **Linked sources.** A [linked source](../files/sources.md) is a tab266 whose one region is `Paged` (`source.go`): a Parquet file or a SQLite267 table named as a table is (`sales[amount]`, `nu.sales`), holding no268 cells, whose table may run past the grid's last row. The engine never269 reads it. What its host found on opening it (the columns, their270 formats, the rows) is kept outside the undo history271 (`SetSourceShape`), and everything that reads it asks the workbook's272 `SourceHost` (`sourceask.go`): a function given one of its ranges is273 asked whole as a `functions.StreamCall`, a cell or a range read some274 other way as its values, and a pivot table over it as its groups275 (`pivotdata.go`, where a pivot reads a sheet's cells or a source's276 rows streaming past through one interface). An answer not known yet277 is `Pending`, the formula or pivot waiting for it, and278 `RecalcSourced` recomputes what waited once it's known;279 `SourceChanged` recomputes what read a source whose file changed.280- **Notebooks.** A notebook tab is a sheet whose `regionState` holds281 cells (`notebook.go`), so adding, editing, moving and deleting cells282 are undo steps as any change is, and the tab is named, moved, hidden283 and deleted as a sheet is. The document itself, cells, names, what a284 cell reads, run orders and stale outputs, is `internal/notebook`,285 which knows nothing of sheets. Outputs aren't undone: the workbook286 keeps each cell's last `notebook.Output` by the cell's ID, its NUON as287 nu printed it, and the file keeps them up to the caps288 (`notebookfile.go`).289- **Rules.** A sheet's conditional formats and data validation290 (`rules.go`, `condfmt.go`, `validation.go`) are lists of rules on291 ranges, replaced whole on every change so undo steps keep them as they292 were; they follow inserted and deleted lines, and their formulas follow293 renamed sheets, as formulas do. How they draw a cell (`Look`, in294 `looks.go`) is worked out when the screen asks for it and kept until295 the next recalculation, which counts itself in `Workbook.gen`: a color296 scale reads its range's numbers once per recalculation, a custom297 formula is evaluated for the cells drawn, with its relative references298 moved as a copy's would be. `CheckEntry` tells the UI whether a cell299 takes an entry, evaluating a formula in place without storing it. The300 file keeps each rule as one line of JSON (`rulefile.go`).301- **JEV.** The engine never touches the network. JEV functions describe a302 question and ask the `Book` for the answer, which the engine looks up303 in the workbook's `RemoteSource`, set with `SetRemote`; `internal/jev`304 answers from a cache and queues new questions, and the UI sends them as305 background commands.306307### The change stream308309Rows reach a region as `sheet.LiveOp`s, each applied at once by310`Workbook.ApplyLive` through the regions' one write path, `writeTable`:311what follows a linked file sends them, the UI sends a notebook cell's312output to the region it was sent to whenever the cell runs, and a cell313run as a stream sends each batch of rows its pipeline prints.314315```mermaid316flowchart TD317 file[a linked file] -->|polled| source[live.Source]318 stream[a cell run as a stream: nu printing a value a line] -->|live.Stream| source319 source -->|live.Update| op[sheet.LiveOp]320 cell[a notebook cell's run] -->|its output, fileio.NUONRows| op321 op --> apply[Workbook.ApplyLive]322 apply --> write[writeTable: the cells that differ, outside the undo history]323 write --> recalc[recalculation: what reads them, pivots and spills included]324 recalc --> frame[the next frame draws the cells on screen]325```326327| Field | Holds |328|---|---|329| `Region` | the region, by name, as the file keeps it |330| `At` | when the source changed |331| `Reset` | the rows replace every row under the header; otherwise they follow the last |332| `Header` | the first row, when it is new or changed |333| `Rows` | data rows: values with the formats they arrived in (`LiveCell`) |334| `Err`, `Note` | why the source can't be read, what it left out |335336An op is applied outside the undo history, as a spill is written. A337window drops the oldest rows (read back from the region's cells and338written again one row up); inside an open step (a macro run) what reads339the cells recalculates when the step ends. An op carries whole340values and names nothing but its region, so applying the same ops in the341same order to the same workbook makes the same cells, whoever applies342it. `OnLive` times each op for telemetry.343344### Questions to linked sources345346A formula reading a linked source asks, and the answer comes back in347the background, as a JEV question's does:348349```mermaid350sequenceDiagram351 participant E as the engine (a recalculation)352 participant H as paged.Host353 participant J as a job, on a goroutine354 participant F as fileio.Source355 E->>H: SUM(sales[amount]) as a StreamCall356 H-->>E: not known: queued, the cell Loading…357 H->>J: the call, and the source as it was opened358 J->>F: the column, streamed in batches359 J->>J: functions.EvalStream over a Book that reads the source360 J-->>H: the answer, kept until the file changes361 H->>E: RecalcSourced: the cells that waited362```363364The job runs the function library's own function (`EvalStream`) over365a `Book` that reads the source (`paged/book.go`): an aggregate folds366the column as it streams, a lookup walks it, and the functions over367aligned ranges read them a window of rows at a time when the `Reader`368says it is streaming (`functions/streamed.go`), so every function369means what it means on a sheet. A function that holds what it reads is370given a source's range only up to `max-cells` cells. Pivot tables371gather their groups the same way (`sheet.GatherPivot`), and the engine372lays them out when the answer arrives.373374### Functions375376`internal/functions` holds the function library: the `FuncDef` table377(one file per category: `everyday.go`, `math.go`, `stats.go`,378`logic.go`, `text.go`, `regex.go`, `lookup.go`, `dynamic.go`,379`lambda.go`, `date.go`, `finance.go`, `link.go`, `jev.go`, which380[Functions](../reference/functions.md) is grouped by), the evaluation of formulas381(`eval.go`: operators and calls; arrays in `array.go`, functions mapped382over them in `lift.go`, the names LET and LAMBDA bind in `scope.go`), the helpers383functions share (arguments and blocks of cells in `args.go`, criteria384over aligned ranges in `masked.go`, searched lines in `seq.go`), format385inference for Automatic cells (`format.go`), the decimal twins and386`Decimalize` (`decimal.go`) and the questions JEV functions ask387(`remote.go`, `remotekey.go`). It imports `formula`, `value` and `numfmt`,388never the engine.389390A formula reads cells through a `Reader` over a `Book`, the interface391the engine implements once per sheet (`reader` in `recalc.go`):392393| `Book` method | What it is |394|---|---|395| `Cell(sheet, a)` | one cell's value, evaluated first if it's dirty |396| `Scan(sheet, r, from, addrs, vals)` | the next chunk of the cells a range holds, row by row: their addresses and, when asked, their values |397| `Bounds(sheet, r)` | the smallest range holding a range's cells, for the data area of blocks and lookups |398| `RangeAgg(sheet, r)` | the running aggregate SUM-like functions share within a recalculation |399| `Fold(sheet, r, agg)` | a range added to a SUM-like function's aggregate, as the engine reads it |400| `Ask(call)` | a JEV question's answer, `Pending` or `ErrNoRemote` |401| `Paged(sheet)` | whether a sheet is a linked source's tab, whose ranges a function is given whole |402| `Stream(call)` | the answer to a call over linked sources (`StreamCall`), `Pending` while it's worked out |403404Sheets are named as references write them, so resolving names, missing405sheets (`#REF!`), cycles and the depth limit stay the engine's. The406engine finds functions through the table (`LookupFunc`, which the parser407uses) and evaluates a cell's formula with `functions.EvalCell`, giving the408cell's address, so a range used where one value is wanted reads the cell409in the formula's row or column (implicit intersection; the `Reader`410keeps the address, and the one before it while formulas nest), and411getting back the array it computed, if any, to spill.412413Arrays (`functions.Array`: rows and columns of values, of which only the414top-left block holding data is stored, the rest one fill value, so a415whole column read as an array costs what it holds) travel through the416evaluator as a `Value` of kind `value.Array` whose number indexes the417`Reader`'s arena, so a cell's `Value` stays as small as it is; the arena418is emptied when the outermost formula is done. Where an array goes419depends on what asked: an operator works value by value, a function420that takes ranges reads it whole (`arrayArg`, `matrixArg`, `each`), and421anywhere else one value is wanted it reads as its first. In an array422context (ARRAYFORMULA, or an argument taking ranges) a range reads whole423and a function of one value is called once with its arguments standing424in (`liftArg`): those it reads as one value are the ones mapped over, so425VLOOKUP maps over its keys and not its table without a list of which426argument is which. Each `FuncDef` says whether it takes one value, may427pass an array through (IF, IFERROR, INDEX) or takes arrays itself428(FILTER, LET). Ranges read whole are shared by the formulas of a429recalculation (`Reader.Forget` empties them as one starts and ends), as430running aggregates are.431432The boundary costs no allocation on the hot paths, which an interface433usually would: a callback passed through an interface escapes to the434heap, so a `Book` that took one would allocate for every range a435formula reads. Every `Book` method takes and returns plain values436instead:437438- SUM-like functions hand the engine their aggregate (`Agg`, a value) and439 it adds a range's cells as it reads them (`Fold`), or answers from the440 running aggregates (`RangeAgg`), calling `Agg.Add` directly.441- Other range walks (`Reader.cells`: AND, MEDIAN, TEXTJOIN, JEV lists)442 ask `Scan` for a chunk of cells into buffers the `Reader` keeps (they443 start at 8 cells and double up to 1024 as a range proves long; nested444 reads each have their own) and walk it with the function's callback,445 which stays on the stack. A chunk evaluates no cell past the first446 error, where every walk stops, so no cell is evaluated that reading447 one at a time wouldn't have.448- Lookups and criteria walk the cells' positions only (`Reader.walk`, a449 cursor, and `stored`), in chunks that start small at every search as a450 search stops at its match, and read the value of each cell they reach451 (`Cell`).452453The engine fills a chunk from the occupancy index without a call per cell454(`colFill`, `rangeFill`), and keeps each sheet's reader between455recalculations, so the buffers are made once. See456[Bounds of support](limits.md#the-function-library) for its costs.457458What stays in the engine is what needs cells or the workbook: the459running aggregates' storage and extension (`rangememo.go`), the links a460HYPERLINK cell opens (`link.go`), the questions a cell's JEV formulas ask461(`RemoteCalls`), explaining errors (`explain.go`), tracing precedents462and dependents through the dependency indexes (`trace.go`,463`tracedeps.go`), stepping through a formula (`steps.go`, where the464function library records each part's value where it stands,465`functions.EvalParts`) and the decimal setting (`SetDecimal`, which466marks formulas with `functions.Decimalize`).467468### Where the engine can grow469470The package boundaries leave the bounds in [Bounds of support](limits.md) room to471move without touching callers:472473- **Storage.** `cellStore`'s methods are the whole contract, so what is474 kept whole or in a slot can change (spilled and pivot cells in slots)475 without touching callers.476- **Range reads.** Shared range results, prefix sums for running totals477 and column-block scans go behind the engine's `Scan` and `RangeAgg`,478 the places functions read ranges.479- **Files.** The `.012` reader and writer stream (`fileread.go`,480 `filescan.go`) and meet the store only through its methods, so another481 encoding would sit beside them; [Bounds of support](limits.md#the-012-file)482 says why there is one.483- **Depth limits.** `internal/formula`'s parser caps nesting at484 `formula.MaxDepth` (1024 levels), so a pathological formula fails to485 parse before anything evaluates it; evaluation puts off cells past486 65,536 levels of a chain rather than recursing further487 (`evaluate.go`).488- **Functions.** A new function is an entry in `internal/functions`489 that reads cells through its `Reader`; a new way of reading them (a490 column block, a cached result) is a `Book` method that takes and491 returns values, so the boundary stays free of allocations.492493## The UI494495`internal/ui` is a Bubble Tea program. Inside the grid it follows Google496Sheets; around it, the control panel keeps a 1-2-3 look. See497[UX and visual bar](ux.md) for the rules every change follows.498499**Commands.** Every action is a registered command with a title and500description (`commands.go`), reached from key bindings, the menu bar,501context menus, the command palette, the help overlay and mouse gestures502that stand for one (double-clicking a tab renames it through503`sheet.rename`), so they can't disagree. `runCommand` is the one place a504command runs: telemetry times it there, and the macro recorder listens505there (see [Macros](#macros)). A command that writes cells declares the506range it writes (`edits`), and `runCommand` refuses it over a pivot507table's results, so a new editing command is guarded by saying what it508edits.509510A keystroke's way from the terminal to the screen, as a command or as511an entry typed into a cell:512513```mermaid514flowchart TD515 key[a key] --> update[Model.Update]516 update --> mode{the mode}517 mode -->|READY| keymap[the keymap, Sheets or vim]518 mode -->|MENU| overlay[the open overlay]519 mode -->|ENTER, EDIT, POINT| entry[the cell entry]520 keymap -->|a command id| run[runCommand: its telemetry span, the pivot guard, the macro recorder]521 overlay -->|a command id| run522 run --> cmd[the command's run]523 entry -->|Enter, Tab: validation, the recorder| commit[the entry stored]524 cmd --> step[an undo step: Sheet.Batch, Set]525 commit --> step526 step --> recalc[the step ends: recalculate what it changed, push it on the undo stack]527 recalc --> view[View: the control panel, the visible rows, the overlays on top]528```529530**Model and components.** `Model` (`model.go`) is the root: it holds the531file, the mode and the note on the context line, owns one component for532each thing that takes input or draws part of the screen, routes each533message to the component it's for, and composes the screen from what they534draw. The components:535536| Component | Type | Owns |537|---|---|---|538| grid (embedded) | `grid` | the sheet shown, active cell, scroll, window size, selection; mapping rows and columns to the screen (rows as bands of lines, `bands.go`), frozen panes, moving and selecting (`grid.go`, `panes.go`, `selection.go`) |539| edit line | `lineedit.Line` | the one-line editor shared by cell entries, prompts and search fields (package `lineedit`) |540| cell entry | `entry`, `suggest.List` | typing into a cell, pointing at references, other sheets while pointing (`entry.go`, `assistsheets.go`); formula suggestions and signatures (package `suggest`, with `assist.go`) |541| prompt | `prompt` | a question on the context line, typed or pointed at (`prompt.go`) |542| overlays | `overlay.Overlay` | whatever has taken over input: menus (`menuoverlay.go`), the palette and pickers (package `picker`, with `palette.go`, `names.go`), the command line (package `cmdline`), the theme picker (package `themepicker`), the find bar (package `findbar`), the filter picker (package `filterpick`, with `filter.go`), the sort bar (package `sortbar`, with `sort.go`), choice bars (package `choicebar`, with `dialog.go`), the chart editor and selection (`charteditor.go`, `chartsel.go`), the pivot editor (`pivoteditor.go`, `pivotactions.go`), the rules panel (package `rules`, with `rules.go`), the shortcuts (package `shortcuts`, with `help.go`), Evaluate formula (package `evalview`, with `evaluate.go`), the agents' suggestions (package `review`, with `agents.go`) |543| sheet tabs | `tabstrip.Strip` | where each sheet was left, the tab strip's scroll and layout (package `tabstrip`); what clicks on it do (`tabstrip.go`) |544| mouse | `mouseState` | drags, hover, double clicks, the fill handle (`mouse.go`, `fill.go`) |545| import | `transfer.Transfer`, `pipeState` | the import in progress, its progress display and cancelling (package `transfer`); choosing and placing imports (`transfer.go`, `importplace.go`); standard input read as a sheet, and what a pipeline gets on quitting (`pipe.go`) |546| linked files | `followState` | a source (`live.Source`) for each linked region, polled one poll at a time on a command every `live.Interval` and reconciled with the workbook after every update, so undo, opening a file and unlinking need nothing of their own; trust in files outside a workbook's folder (`follow.go`); Data > Linked file and Import's Follow the file (`linked.go`) |547| linked sources | `sourceState` | the workbook's `paged.Host` (a room's in `012 serve`, run by whoever keeps the room) and its jobs, run a few at a time on commands, their answers applied as they come; each file looked at every `live.Interval` (`sources.go`); the tab shown, package `srcview`, its pages read as it scrolls, and the commands that act on a source (`srcscreen.go`, `srccmds.go`) |548| notebooks | `nbview.View`, `nbState` | a view of each notebook tab, its selection, mode, scroll and editor (package `nbview`); the tab on the screen (`nbscreen.go`); the notebook's commands and keys (`notebook.go`); cells running one at a time in the background, their queue, stale outputs, trust (`nbrun.go`); what a run reads (`nbjob.go`); outputs sent to sheets (`nbsend.go`) |549| macros | `recorder`, `macroState` | a recording in progress (`macrorec.go`); a macro running, trust in the file's macros (`macrorun.go`); what scripts act on (`macrohost.go`, `macrohostnav.go`); Data > Macros and the manager (`macro.go`, `macromanage.go`) |550| others | `clipboard`, `trace`, `traceView`, `chartState`, `jevRunner`, `terminal`, `session` | what Ctrl+V pastes, a trace being stepped through and tracing that stays on (`traceview.go`), chart commands' target, JEV questions in flight, what the terminal supports and the chart images sent to it, what outlasts the file open (the `:` history, whether keys can be held: `keyboard.go`) |551552Overlays implement `overlay.Overlay` (package `overlay`): an indicator for553the mode, `Key` and `Mouse` handlers, a `Layout` of boxes to draw, and554the status line while open; bars on the context line add `ContextLine`555(`overlay.Liner`), and those with a text field `Cursor` and `Changed`556(`overlay.Text`). The model routes to them without handing itself over:557each overlay is built with the host it acts on and keeps it. A new558overlay is a new type; `Model` needs no new fields, only a way to open it559(usually a command).560561**Hosts.** A component names what it needs of the model in a small562interface, its host, and is tested against a fake of it. Components in563packages of their own declare an exported `Host`, which the model564implements through `host` (`hosts.go`), an adapter that keeps those565methods off `ui.Model`'s exported API:566567| Package | Host | Methods |568|---|---|---|569| `picker` | `picker.Host` | theme, size, the edit line, close, record the answer to a command's question (5) |570| `choicebar` | `choicebar.Host` | theme, close, record the key chosen (3); the choices are closures over the model |571| `shortcuts` | `shortcuts.Host` | theme, size, close, the rows, built from the key bindings and the registry (4) |572| `evalview` | `evalview.Host` | theme, size, close, the locale (4); the formula's steps are the engine's `sheet.Steps` |573| `sortbar` | `sortbar.Host` | theme, size, the sheet, close, sort (recorded as the bar's command) (5) |574| `filterpick` | `filterpick.Host` | theme, size, the edit line, close, the locale (5); what applying and cancelling do are callbacks, as the sheet's filter and a pivot's differ |575| `cmdline` | `cmdline.Host` | theme, size, the edit line, close, the commands to complete, run a line, fail, the session's history (8) |576| `nbview` | `nbview.Host` | theme, locale, the cells, a cell's output, how its run stands, run a command, keep a cell's new source (7); the language's highlighter, completer and checker are `nbview.Providers`, small interfaces of their own |577| `srcview` | `srcview.Host` | theme, locale, the source's columns, how many rows the tab shows, a row, ask for rows (6); the rows come from `paged.Pages`, read a page at a time |578| `suggest` | `suggest.Host` | theme, size, the edit line, whether an entry is being typed, the entry's sheet, the formula as parsed, where the formula bar's text starts (7) |579| `review` | `review.Host` | theme, size, close, the agents' suggestions waiting, accept, reject, show a cell (7) |580| `themepicker` | `themepicker.Host` | a picker's host, and the current theme, the themes directory, preview, keep, whether keys can be held (10) |581| `rules` | `rules.Host` | theme, size, the edit line, close, the sheet and selection, save a conditional format or a validation rule, follow a rule removed or moved (each recorded as the commands that do it), the terminal's palette colors (10) |582| `findbar` | `findbar.Host` | theme, size, the edit line, the workbook, the trace, the sheet and cell shown, show a cell, note, run a replacement (recorded as Find and replace), leave keeping the search, pass a click to the grid (11) |583| `tabstrip`, `transfer` | none | they're handed a view or messages and draw what they're given |584585Components that stay in package `ui` declare an unexported host the model586implements itself:587588| Host | For | Methods | Why it stays |589|---|---|---|---|590| `namesHost` | the named ranges picker | 6 | a few keys over a picker; what they do is the model's name prompts and selection, which open the picker again |591| `promptHost` | prompts | 8 | a prompt is one of the model's modes, not an overlay: ranges are pointed at with the grid's movement and drawn by the grid |592| `menuHost` | menus | 9 | their items are the command registry and the menu bar's definitions |593| `macrosHost` | the macro manager | 9 | a few keys over a picker; what they do is the macro machinery |594| `pivotHost` | the pivot editor and its field picker | 16 | it opens the model's pickers, filter values and range prompt and comes back from them |595| `chartHost` | the chart editor and a selected chart | 25 | besides the charts, the grid's geometry, the pointer shape, the chart menu, holding Space to zoom, and prompts, and passing keys and clicks on to the grid |596597The cell entry has no host: typing into a cell is the model's ENTER,598EDIT and POINT modes, whose state is the model's edit line, active cell599and pointer, and committing goes through validation, the undo step and600the recorder, so an interface for it would be the model's own methods601over again. Its suggestions, which only read the entry, are package602`suggest`. Commands still run in one place: a host's way to run one603(`runFromOverlay`, `cmdline.Host.Run`, `chartHost.runCommand`) goes604through `runCommand`, so macros record them and pivots guard them.605606**Drawing.** `View` (`panel.go`) stacks the control panel, the header,607the grid rows and the status line, then composites the floating layers608with Lip Gloss: charts over the grid, then the open overlay's boxes or the609formula suggestions, so the grid never shifts under them. Only the visible610cells are rendered; `rowtext` lays out each line of a row's text in one611pass over the cells that can reach the screen. A row is a band of lines612(`bands.go`): a rule line when a border lies along its top, then as many613lines as its height or the text it wraps takes (`rowtext.Shapes`, kept614until the sheet changes); navigation, the mouse and charts map rows to615lines through the bands. Borders, merged cells and the rule lines are616drawn over the laid-out text (`gridlines.go`), with what a frame has617drawn in each role kept for the rest of it. Styles come from `theme`, a small set618of roles with dark and light variants on the terminal's 16 ANSI colors,619so the user's palette applies; its widgets (framed boxes, key chips, key620hints) are what every overlay is drawn with.621622**Packages.** `theme`, `rowtext`, `formula`, `overlay` and `lineedit`623depend on nothing in `ui`, so they can be tested and measured alone. The624components in `picker`, `cmdline`, `nbview`, `srcview`, `themepicker`, `findbar`, `rules`,625`sortbar`, `filterpick`, `choicebar`, `shortcuts`, `evalview`, `suggest`, `review`, `tabstrip`626and `transfer` build on them and reach the model only through their627hosts, with unit tests of their own against fake hosts. A component moves out of package628`ui` when its host stays small (about ten methods or fewer); one that629needs more keeps a narrow interface inside `ui` instead, since moving it630would mean exporting half of the model. New leaf packages are split off631the same way when a part needs only the sheet or the theme.632633## Files634635`internal/fileio` moves data between sheets and other formats. Each format636is one row of the table in `formats.go` (its name, extensions, the words637the UI shows for it, whether it holds several sheets or tables, and its638importer and exporter) plus a file of its own; `Import`, `Export`,639recognizing a file by its extension, the import picker and the Download640menu all come from the table. A file takes one of these ways into a641sheet, imported or followed:642643```mermaid644flowchart TD645 file[a file, or standard input] --> row[its format's row in formats.go]646 row --> importer[the format's importer, streaming]647 importer --> builder[the builder]648 builder -->|the engine's public API| wb[a new workbook]649 followed[a followed file] --> livefile[live.File, watching os.Stat]650 livefile -->|grown: the bytes it gained| tail[Tail: the importers' readers over a pipe]651 livefile -->|rewritten, once it holds still| importer652 tail -->|typed rows| op[sheet.LiveOp, the change stream]653 wb -->|a followed file's table, as rows| op654```655656The shared `builder` keeps text as text, falls back to a formula's657cached value when it can't be translated, and counts what didn't fit.658Importers stream: CSV and TSV are read record by record, NUON and JSON659a row at a time (`nuon.Reader`, which yields rows from a pipe as they660arrive), Parquet a batch of rows at a time, SQLite a row at a time, and661Parquet files and SQLite tables stop at the sheet's last row, taking the662number of rows left out from the file.663664A `Tail` reads CSV, TSV, JSON and NUON as they arrive in pieces, for a665file followed as it grows: the importers' readers run on a goroutine of666its own over a pipe that waits for the next piece, so a record cut off667at the end of one waits for the rest, and rows come out typed as an668import types them (`tail.go`). A `live.Source` is anything that yields a669table's rows over time: a followed file (`live.File`), or a notebook670cell run as a stream (`live.Stream`), whose pipeline nu runs printing671each value as NUON on a line of its own (`nushell.Stream`), read by the672same `Tail` as it arrives.673674A `Source` (`source.go`) is a Parquet file or a SQLite table or query675read in place, for [linked sources](../files/sources.md): it streams676the columns asked for (Parquet reads only their column chunks, from the677row group holding the first row), fetches rows by number, and orders678them as a `SourceView`, pushed down to SQL for SQLite (the rowids in679order in a work database of its own, the source opened read-only) and680streamed for Parquet (a filter's matches found in one pass, a sort681merged from sorted runs in temporary files, either kept as a file of682row numbers a page reads). Values come out typed as an import types683them (`sourcecell.go`), and every method is safe from several684goroutines at once.685686XLSX is read by 012's own SpreadsheetML reader on `archive/zip` and687`encoding/xml`: the workbook, shared strings (kept end to end in one688buffer) and styles first, then each worksheet a token at a time, a row689at a time, expanding shared formulas only for the cells kept. The file690is untrusted, so the reader caps what it can be made to do691(`xlsxLimits`: uncompressed bytes per part and in all, compression692ratio, zip entries, XML nesting and token size, shared strings, styles,693sheets, and the text shared formulas expand to) and refuses unsafe part694names and references past Excel's edges; `FuzzReadXLSX` and695`FuzzReadXLSXParts` fuzz it. XLSX is written with the same packages, a696worksheet part streamed per sheet.697698Exporters write a `Snapshot` taken on the UI goroutine, in the699background, through one atomic write-then-rename; CSV and TSV are700written a row at a time. One package suits formats that share this much701(the builder, number formats, serial dates and Excel formula702translation); a format that grew its own dependencies would move to a703subpackage behind the same table row.704705`internal/headless` is the engine without the screen, for `012 get`,706`set`, `recalc` and `export` ([Scripts](../files/scripts.md)): it707resolves references, writes values through the same snapshots and708encoders as downloads, types entries through `Sheet.Set` in one709`Batch`, and saves with a write-then-rename. It sends the outputs a710file kept to their sheets as the screen does on opening, runs notebook711cells through `internal/nushell` and asks `internal/jev` only when its712caller says so; `cmd/012` decides, by the flags and the macro-origin713trust.714715`internal/diff` reads a `.012` file as its JSON fields: the workbook's,716and each sheet's with its cells by address. Comparing and merging go717field by field, so a field added to the format is compared and merged718without teaching them about it, and the engine is used only to compute719values (`Compare`) and to check and write the merged workbook720(`Merge`), which it reads back as 012 would open it.721722## Macros723724Macros are Starlark scripts (`go.starlark.net`, pure Go) kept in the725workbook (`sheet.Macro`, saved in the file). `internal/macro` knows726neither the engine nor the terminal: it defines the functions scripts call727over a `Host` interface, turns a recording (a list of `Action`s, the728command log) into a script, and runs scripts with a step limit, a cancel729switch and errors placed at their line and column. Scripts have no730`load`, and Starlark itself has no file, network, clock or random access,731so the Host is the only way out.732733The UI implements the Host on the model (`scriptHost`), through the same734paths keys take: entries go through `Sheet.Set`, the selection through735the grid, commands through `runCommand`, with a question answered as if736typed. A run's script executes on its own goroutine, and each call it737makes comes back to the UI goroutine as a message (`macroCallMsg`), is738run on the model, and releases the script; the UI keeps taking calls739within one update for a few milliseconds, so a 1000-action replay costs740about 2 ms, and gives the screen back when the script pauses, so Esc can741stop a runaway. A script whose program is gone (it quit, or its SSH742session dropped) stops after waiting 30 s for a call to be taken. The whole run is one undo step opened with743`Workbook.Begin`.744745Recording (`macrorec.go`) listens where actions happen: `runCommand` for746commands (with the answer to a prompt or choice bar), the entry commit,747pastes, the fill handle, column borders and tab drags. A dialog records748itself when it acts (`recordDialog`), as its command answered with the749choices made, a JSON object the recording writes as a dict; the command's750`answer` hook makes the same choices and applies them when a script runs751it with that answer. Charts moved or resized record as Edit chart with752the fields changed (`chartmacro.go`), and the rules panel as the commands753that change rules from a line of the file (`rulemacro.go`). The selection is754recorded lazily, as absolute `select()` or relative `move()`/`extend()`,755just before something acts on it. Files record the computer their macros756were made or trusted on (an id from `cmd/012`); a macro from elsewhere757asks once before it runs. Only the local app lets scripts be edited in758the user's editor (`AllowEditor`), since that starts a program.759760## Notebooks761762`internal/nushell` runs a code cell: a `Job` holds the pipeline and the763NUON of what it reads, and the script `nu -c` runs binds each from a764file named by an environment variable765(`let files = (open --raw $env.NU012_TABLE_0 | from nuon)`) and ends in766`to nuon`, so no data is ever spliced into the pipeline; a range of a767sheet (`$sheet.A1:C9`) is renamed to a variable of its own first768(`notebook.Bind`). What nu prints is kept as it is, NUON, the cell's769output: a later cell reads it byte for byte as `$name`, `nbview` parses770it once to draw it, and `fileio.NUONRows` reads it as a region's rows to771send to a sheet:772773```mermaid774sequenceDiagram775 participant UI as internal/ui776 participant R as nushell.Runner777 participant Nu as nu -c778 UI->>R: a Job: the pipeline, the NUON of what it reads779 R->>R: each to a file, named in NU012_TABLE_0, 1, ...780 R->>Nu: the script781 Nu->>Nu: let files = (open --raw $env.NU012_TABLE_0 | from nuon)782 Nu-->>R: the pipeline's value, to nuon783 R-->>UI: the NUON, up to 512 MB784 UI->>UI: the cell's output, not an undo step, sent on to its region785```786787`Runner` is the seam: tests hand the UI a fake, the e2e788screens a stand-in `nu` script. The UI runs one cell at a time as a789`tea.Cmd` with a context Stop cancels and a timeout; the queue a run790makes comes from `notebook.Order` (each cell after those it reads) and791`notebook.Inputs` (what it reads that hasn't run), and a reactive792notebook adds `notebook.Dependents`. An output records the source it793ran and the `Seq` of each output it read, so `notebook.Stale` tells794which outputs may be out of date without running anything. Trust is795the macros': a file's cells run only once they were made or trusted on796this computer, or the user agrees.797798`nbview` draws each cell as a block of lines, working out only how many799lines each takes and drawing those on screen, so an expanded output of800ten thousand rows costs the rows showing; a table's column widths are801fitted once, when its output is parsed. The code editor802(`lineedit.Area`) wraps at the cell's width, before pipes where it can;803highlighting, completion and diagnostics come from `nbview.Providers`,804asked in the background once typing pauses, with a tokenizer and the805notebook's names as the built-in answers.806807## Charts808809`internal/chart` draws a chart in two layers that share one layout: text810for any terminal (block elements for bars, braille for lines, half blocks811for pies, eighths filled column by column for areas) and, on terminals812with kitty or sixel graphics, an image of the plot area with the axes and813legend still terminal text. A kitty image is placed by placeholder814characters, which the renderer treats as text; a sixel image815(`chart.Sixel`) is pixels Bubble Tea's renderer doesn't know of, so816`internal/ui/sixel.go` keeps blank cells for it, draws it once the frame817has settled, and clears the screen when it moves. Each chart type is a layout in the `types` table,818returning a plan that draws it both ways, and the series its legend819lists; `Draw` places the legend and gives the layout the room left. The820type constants and their names, and the options of each chart821(`sheet.ChartOptions`: stacking, trend lines, the value axis, gridlines,822the legend), belong to `internal/sheet`, since charts are saved with823sheets. Stacked bars are piles of spans, and a cell where two meet draws824the lower as eighths over the upper as background. Work is bounded by the825chart's size, not its data: only the categories that fit are drawn, areas826are sampled once per column (per pixel column in images), pie slices are827found by binary search, and a pie image supersamples only pixels on a828slice edge or the rim.829830## Serving over SSH831832`internal/serve` wraps `charm.land/ssh` (wish's server) with only a833session channel, public-key auth against authorized_keys, and a shell834request or, with a PTY, a one-word exec request allowed. The exec835request is never run: its word is a file name. A session from login to836its end:837838```mermaid839sequenceDiagram840 participant C as ssh client841 participant S as internal/serve842 participant M as the session's ui.Model843 C->>S: public key844 S->>S: authorize: authorized_keys read again845 C->>S: session channel, pty-req846 C->>S: shell, or exec with one word847 S->>S: allowSession, fileArg: the word resolved through confine.Root848 S->>S: a slot under --max-sessions849 S->>M: ui.New, Model.Serve (client's environment, confine.Root), OpenOnStart850 S->>M: its own tea.Program over the session851 loop until it quits, the client goes, it idles or the server stops852 C->>M: keys, window size changes853 M-->>C: frames854 end855 S->>M: Unsaved, Recover (idle or stopping)856 S-->>C: why it ended, where unsaved work was kept857```858859Each session runs its `tea.Program` over wish's emulated PTY; the860client's environment is what the model detects the terminal from, and861its `confine.Root` resolves every file name. Everything a model knows862lives in the model, so sessions share nothing but read-only tables (the863command and function registries), the process-wide telemetry, with JEV864on the HTTP client (each gets its own `jev.Cache`, whose queue belongs865to that session's program), and the workbooks of the rooms they're in866(below). See [Serving over SSH](../terminal/ssh.md).867868### Shared workbooks869870Sessions opening the same file (its path on disk is the key) or the871same named room share one `*sheet.Workbook` (`internal/room`). There is872nothing to merge: the server holds the workbook, and participants take873turns on it. A `room.Registry` holds the rooms; `Join` seats a874participant, opening the room on the file's workbook when nobody has875it open, and the last `Leave` closes it. A seat's `Do` is a turn: the876room's lock held, the workbook's author set to the seat, so every step877made is attributed; when the turn ends, the room notes the operations878it made and tells the others. A served session runs its model under879`ui.Shared`, which makes every `Init`, `Update` and `View` a turn,880publishes where the user is (`Seat.Move`), and between turns joins and881leaves rooms, never holding two locks. An operation's way from one882session's key to another's screen:883884```mermaid885sequenceDiagram886 participant A as session A (ui.Shared)887 participant R as the room (room.Seat.Do)888 participant W as the workbook889 participant B as session B890 A->>R: a key: Update, a turn891 R->>W: SetAuthor(A's seat)892 A->>W: Sheet.Set, Batch: a step, recalculated893 W->>W: the step pushed: author, operation number (sheet.Op)894 R->>R: turn ends: the new operations marked (who, which cells)895 R-->>B: Participant.Notify896 B->>R: roomMsg: Update, a turn897 B->>R: the marks since it last looked, the others' presence898 B->>B: View in its turn: the change, A's pointer in A's color899```900901Undo in a shared history takes back the author's own latest step,902from under others' later steps when none of them changed what it903changed, and refuses otherwise, naming whose step is in the way904(`sheet/authors.go`, where the rule is written down); cancelling a905dialog undoes the user's own steps since it opened (`Checkpoint`,906`UndoTo`). `TestRandomSharedEdits` holds it to undoing, in any order907it allows, back to the start. A step left open across turns (a macro908run, `Workbook.Begin`) holds the others' input until it ends, and one909left open by a session that crashed is closed (`EndStep`).910911What the room runs is the room's: linked files are followed by the912seat there longest (`Seat.Keeper`), notebook cells' runs and streams913(`nbRuns`) are a value the room keeps (`Seat.Value`), and what their914commands report goes to whoever keeps the room then (`Seat.Post`,915`Take`), so a run outlasts the session that started it. The room's916last save (`Seat.SetSaved`) is what every session counts unsaved917changes from, and the last session out keeps them for recovery.918919A participant is anything with a name that can be told the room920changed; everything else it does, it does through its seat:921922| `room.Participant` | What it is |923|---|---|924| `Name()` | who they are to the others: the SSH user, or the agent's MCP host |925| `Notify()` | the room changed; called with the room locked, so it only schedules a turn (a session sends its program a `roomMsg`) |926| `Agent()`, optional (`room.Agent`) | the participant is one of the agents: marked as one, never the keeper, and the room closes when its last person leaves |927928| `room.Seat` | What it does |929|---|---|930| `Do(fn)` | a turn: `fn` on the workbook, the lock held, steps in the seat's name |931| `Move`, `Peers`, `Marks`, `Last` | where this participant is; the others and where they are; who changed which cells since an operation |932| `Writing`, `HandTo`, `Writer` | the one-writer mode's role |933| `Saved`, `SetSaved`, `Value`, `Keeper`, `Post`, `Take` | what the room shares: its file's saved state, values such as the notebook runs, and messages for whoever keeps it |934| `Leave` | the seat given up; reports whether the room closed |935936**Unsaved work.** `Server.Shutdown` closes a channel every session937watches; a session quitting on it, or on its idle timeout, asks its938model `Unsaved` and `Recover`, which writes the workbook to939`.012-recovery/` (0700, files 0600, three kept per name) and returns940the name to tell the client. The directory is hidden, so `confine`941refuses it to names typed in a session; only `ui/recovery.go` reads and942writes it, and a model opening a file (or starting on a new sheet)943offers the newest recovery file kept for that name.944945**File names.** The UI keeps names as typed and turns them into paths946only to read or write, through the model's `confine.Root`: the zero947root (the local app) uses them as they are, a served session's root948keeps them inside its directory. Every open, save, import, download and949file listing goes through `Model.path` or the root.950951### Live mode952953Agents take part in rooms as participants ([Live mode](../agents/live.md)),954through `internal/cowork`. A local session invited to (`012 --listen`,955File > Invite an agent) takes its workbook into a room of its own as it956is (`enterLocal`: its steps adopted as its person's, `Workbook.Adopt`),957runs under `ui.Shared` as served sessions do, waits for its room's958changes itself (`waitRoom`), and listens on a Unix socket959(`cowork.Listen`); `012 serve` listens beside its rooms. `012 mcp960--attach` is a pipe between the host's standard input and output and961that socket, after a line each way naming the agent and the workbook.962In the session, each connection is a seat (`cowork.Agent`, which963implements `room.Agent`) and an `mcp.Server` whose `Options.Live` is964that agent: `internal/mcp`'s tools stand on a `Backend` of `View`,965`Scratch` and `Change`, and the agent's are its seat's turns, `Scratch`966a copy taken in one, and changes go through `mcp.Live.Make`:967968```mermaid969flowchart TD970 tool[a write tool] --> make[Agent.Make, a turn]971 make --> propose[sheet.Propose: the change on a copy; its cells and what else it does]972 propose --> scope{Scope.Check}973 scope -->|outside| refuse[refused, with the reason]974 scope -->|direct edits| apply[Proposal.Apply: a step in the agent's name]975 scope -->|otherwise| board[a Suggestion on the room's Board; Seat.Touch]976 board --> screen[the person's frame: cells marked, the review panel]977 screen -->|accept| apply978 screen -->|reject| told[marked rejected]979 apply --> notify[the agent's Notify: the suggestions resource updated]980 told --> notify981```982983The board (`cowork.Board`, a room value) holds the suggestions, the984scope, direct edits, the grants and the agent's questions, so it's read985and changed in turns like the workbook. A proposal of cells alone is986applied as the cells it made on the copy, cell by cell if the person987picks; one that does more is applied whole by running the change again988(`sheet/proposal.go`). Questions and grants wait on the board until the989person asked is free (`agentask.go`); a notebook run the agent is990allowed is posted to the room's keeper, as other runs are.991992## Configuration and secrets993994`internal/config` reads one Ghostty-style file (`key = value`, `#`995comments, `config-file` includes) from `$XDG_CONFIG_HOME/012` or the OS996config directory; the same directory holds `themes/` and 012 serve's host997key. Every option is one entry in `config.Options`: name, type, default,998environment variables, flag, whether Reload config applies it, and a999description. Parsing and validation, the flags `cmd/012` accepts, `0121000config`'s listing and default file, and [Configuration](../reference/config.md)'s1001reference (checked by a test) all come from that table. Values are layered defaults < file < environment < flags, each1002remembering its source; problems are warnings, never fatal. The UI gets a1003`ui.Settings` (the config, a reload function, the credential store, a JEV1004connector) through `Model.Configure`, so tests hand it a temporary config1005and a store in memory.10061007The JEV API key is never in the config. `jev.ResolveKey` takes it from1008`TYPESAFE_API_KEY`, then the credential store, then1009`jev-api-key-command` (split into words and run without a shell, with a1010timeout, only when a sheet first asks JEV something). The base URL comes1011from the config or environment only and must be https unless it's1012loopback, so a file next to a sheet can't redirect the key. Settings >1013JEV API key checks a new key with `jev.Check`: one fixed, trivial1014question, its error worded for the context line and never holding the1015key.10161017`internal/keyring` is the credential store, chosen for the least1018dependency weight that is still correct on each OS, with `CGO_ENABLED=0`:10191020| OS | Store | How |1021|---|---|---|1022| macOS | Keychain | `/usr/bin/security`. Writing uses `security -i`, which reads its command from stdin, with the key hex-encoded (`-X`), so it's never in any process's arguments. |1023| Linux, BSD | Secret Service (GNOME Keyring, KWallet, KeePassXC) | libsecret's `secret-tool`, which reads the secret to store from stdin. |1024| Windows | Credential Manager | `CredReadW`, `CredWriteW`, `CredDeleteW` from advapi32 through `golang.org/x/sys/windows`, already in the module graph. |10251026The alternatives were a keyring module (zalando/go-keyring, which does1027the same on macOS but brings godbus for Linux, or 99designs/keyring,1028which brings several backends and needs cgo for the macOS Keychain) or1029cgo bindings to Security.framework and libsecret, which would end the1030pure-Go build. Two1031small command-line programs and one system DLL cover the three OSes with1032no new module. The stores share a `Store` interface; tests use a1033recording runner or `keyring.Memory`, and the e2e binary is built with1034`-tags fakekeyring`, whose store is a file under `XDG_CONFIG_HOME`, so no1035test reaches a real keychain.10361037## Themes10381039`theme.Theme` is a set of style roles, each defined on the 16 ANSI colors1040and the default background and foreground (`theme.New`). The terminal1041theme sends them as ANSI numbers, so the terminal's palette applies. A1042color scheme (`theme.Palette`: VHS's embedded catalog, or a Ghostty,1043kitty or VHS JSON file in `themes/`) is drawn by `theme.FromPalette`,1044which maps each ANSI color to the scheme's, adds full-width bands for the1045bars and a screen background, and corrects contrast (see1046[Themes](../terminal/themes.md)). Bands are applied by `theme.Fill`, one pass over1047a rendered line's escape sequences that sets the band's colors at the1048start and after each reset and pads to the width; `View` fills the bar1049lines before overlays are composited and the whole screen after, so bars1050reach the edge under menus too. Charts take their colors from the scheme1051when there is one, and from the terminal's palette otherwise.10521053## Telemetry10541055`internal/telemetry` is off unless asked for. When on, spans, events and1056per-second frame summaries go to a JSON log, an OTLP/HTTP collector, or1057both (see [Observability](observability.md)). The OTLP exporter uses1058only the standard library: bounded queues per signal, drained by one1059goroutine in batches; when a queue is full new items are dropped and1060counted, so the UI never waits on the network. Operation durations are1061histograms keyed by span name, which are fixed in the code. Spans nest1062through a `telemetry.Trace` per owner (each Model, each import) and1063explicit `Parent` handles across goroutines; a workbook holds its1064owner's trace, opaque to the engine, so recalculations nest in the1065command that caused them.10661067## Testing10681069See [Testing](testing.md).