formulaa 0.1.0

WYSIWYG TUI math editor rendering Unicode/ASCII-art formulas
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
# Architecture decision records

The running log of design decisions, condensed one entry per decision.
Numbering is stable — code and older notes cite entries as `§N`.
Entries marked *superseded* are kept because the reasoning still
explains the shape of the code. The roadmap lives at the end.

## Vision

Make math a first-class citizen of plain text. A human-readable 2D
picture (Unicode-first ASCII art) and a math AST convert
deterministically in both directions, so you can think in AA, convert
mechanically to LaTeX, and re-edit any picture structurally. The
internal representation is the AST (`src/ast.rs`); rendering, LaTeX
output and the structure editor all derive from it. `render` and
`parse` are the two sides of one spec ([aa-spec.md](aa-spec.md)) and
must change together; correctness is the roundtrip contract, enforced
by a corpus plus randomized property tests.

## Decisions

1. **AST as the internal representation** — never TeX strings.
   Structural editing needs the tree anyway, and AA⇄AST bijection is
   the end goal.

2. **Aggressive Unicode** — inline scripts (``), math italics (`𝑥`),
   distinct code points remove ambiguity at the root; parsing gets
   easier than pure-ASCII art, not harder.

3. **The band `┈┈∑┈┈` for limits** — unmarked above/below limits are
   provably ambiguous (`∑_{n=1}^{∫}` vs `∫_{∑_{n=1}}` draw the same
   picture; `parse.rs`'s `ambiguity_counterexample_is_now_distinguishable`
   is that pair). The band marks the range and carries
   the baseline, like a fraction bar.

4. **Compact (script-style) typesetting — removed** (2026-07).
   Automatic operator spacing and two-tier layout were dropped once
   formatting spacers (§21) covered manual spacing; LaTeX does the
   typesetting anyway.

5. **Explicit `` base** — a leading script renders as `⬚ˣ`, keeping
   it distinct from an empty row; empty required slots are `` too.

6. **Accents are 2D marks, not combining characters** — combining
   characters break the one-cell grid. The cells straight above/below a
   base are otherwise unused, so a column walk reads stacks; over- and
   under-mark glyph sets are disjoint.

7. **Matrices are bracket pairs + blank-band separators***superseded
   by §21–23 (lattice markers)*. Round-paren matrices were rejected
   (`(x)` would collide with plain parens).

8. **Upright = function, italic = variable** — render-time
   italicization makes upright ASCII runs self-identifying; canonical
   form is always italic.

9. **No unconditional margins** — a full-height blank column may only
   mean "sibling separator"; property tests showed padding inside
   script arguments breaks the outer chunking. Spacers are inserted
   only where bars would fuse.

10. **normalize** — adjacent same-kind scripts merge (`Sup(a);Sup(b)`    `Sup(a++b)`, same picture); merging can create new adjacency, so
    normalize is re-applied to a fixed point (idempotence bug found by
    property test).

11. **Symbol coverage** — 4000+ entries auto-extracted from
    [ho-oto/mathematical-symbols]https://github.com/ho-oto/mathematical-symbols;
    later curated down (§55, §60).

12. **Compatibility display mode — removed** (2026-07). A display-only
    ASCII fallback double-managed the canonical form; font problems are
    better solved by the merged font (§13) or an external filter.

13. **Merged-font generator** (`tools/merge_math_font.py`) — copies
    missing math glyphs from JuliaMono into any monospace font with
    advances forced to the base cell width. Verified 2026-08 against
    the charset formulAA can actually emit (1122 code points): a stock
    JetBrains Mono misses 756 of them, Cascadia Code 927, and the
    merge closes the gap with the columns still aligned. Two fixes came
    out of it — the curated ranges had missed the subscript letters
    (`ᵢ ᵣ ᵤ ᵥ` live in Phonetic Extensions, `` in Latin Extended-C),
    and donors are now repeatable (at the time JuliaMono had no glyph
    for the drawn vec accent — §86 moved that mark to one it has).

14. **Marker-atom display decorations** — jump labels and block
    highlights insert private-use `Sym`s into a display-only clone of
    the AST, decoded after rendering; the layout engine needs no
    coordinate tracking. (14b: the Ctrl+O structure view built on this
    was removed once ^B block-select covered it.)

15. **Cancel via combining overlays***superseded by §67*.

16. **`` baseline marker — removed** (2026-07). Lenient parsing made
    it unnecessary; ambiguous input errors instead.

17. **Space is an explicit atom ``; the Space key is input; Tab
    exits** — real spaces in canonical form would collide with the
    structural meaning of blank columns. Visible spacing is the atom
    `` (a LaTeX control space); automatic readability spacers were removed.

18. **`~` as a lenient band — removed** (§25).

19. **Paren/Matrix unified into Delim/Array**    `Delim{left,right,mids,segs}` is the only bracket node; a matrix is
    a Delim around a bare-grid Array. Lessons: grid baselines need a
    "is the interior a grid" branch; mismatched pairs require resolving
    shared glyphs by column walk; glyph sides are fixed (`\left)` is
    not expressible — a variable side would make `[a]b]` ambiguous).

20. **Requirements reframed: AA is source code** — the invariants are
    (a) parse is deterministic over everything it accepts, and (b)
    `parse(render(normalize(x))) == normalize(x)`. Canonical form is a
    formatter's output, not the only accepted spelling; input
    whitespace is free.

21. **Lattice markers and formatting spacers** — bare arrays
    self-delimit with junction glyphs (`┌┬┐ ├┼┤ └┴┘`), erasing all
    meaning from whitespace *width*; the Space key became
    `Node::Spacer` (renders one column), relaxing the contract to
    `parse∘render == strip_spacers∘normalize` — until §85 made the
    parser read those columns back.

22. **Grids are lattices everywhere; whitespace-count rules abolished**
    — blank-separated grids removed; `[ ]` lost every special case.
    Arrows use the same maximal-munch principle (`───>` is an arrow,
    `─── >` a fraction and an atom).

23. **Fused grids use minimal markers** — a delimiter whose sole
    segment is an Array absorbs the lattice edge; only `` separator
    rows (or `┬┴` / `├┤` for single row/column) remain. 1×1 fusions
    normalize away; angles and braces don't fuse.

24. **The band generalizes to any material** (`┈lim┈`, `┈argmax┈`) —
    one consistent rule replaced per-operator special cases; empty-limit
    bands normalize to bare material, so the promotion rule for bare
    `` disappeared.

25. **Lenient `~` band removed; "never drop silently"** — the lenient
    form was one misread away from data loss. Principle: accept more
    only where the reading is unique; anything overlapping a baseline
    token errors instead of vanishing.

26. **One keymap** — host-independent `Key`/`Effect` and
    `Editor::input` (`src/input.rs`); TUI and wasm only translate.
    Random key-sequence property tests (with a per-keystroke roundtrip
    check) found real bugs on day one.

27. **Editing UX round 1**`[` became an ordinary auto-pair; `//`
    makes fractions; internal clipboard (^C/^X/^V); Shift+↑ selects the
    parent; delimiter specs unified as Typst-style `\lr` in visual
    order; the help line is context-dependent.

28. **Block select (^B) and `\op`/`\op*`** — ^B selects structural
    ancestors; `\op` is `\operatorname` (an upright run), `\op*` a
    Text-based band.

29. **Overlay labels, not inserted ones** — display markers are
    stripped from layout and overdrawn, so decorations never shift the
    picture.

30. **Mouse cursor placement** — click maps to the nearest edit
    position via probe rendering.

31. **↑/↓ promote bare big operators** back into bands (empty-limit
    bands don't exist in canonical form, so this is the only way in).

32. **Zero-width caret** — the cursor is `Block.caret` metadata; the
    displayed geometry always equals the cursor-free render. TUI shows
    reverse video.

33. **All display decoration is zero-width** — markers propagate as
    `Block.marks` through every composition; mode displays share the
    edit view's geometry exactly. Invisible slots materialize as ghost
    `` only while needed.

34. **Jump v2***superseded by §68 (jump removed)*.

35. **^F free cursor mode** — free 2D movement with a live snap target;
    auto-expands unexpanded scripts near the cursor with hysteresis and
    re-anchoring.

36. **Multi-line formulas**`Node::Break`; canonical AA stacks lines
    with a lone-`` separator row; segments parse as independent
    formulas. No align.

37. **Null delimiter glyphs `┆ ┊`** — dashed "no wall here" glyphs in
    the same ghost family as ``.

38. **`\op` name box** — an in-place input box; words become dictionary
    functions or upright runs; `\op*` makes each word a band piece.
    Free-form band-base editing rejected: delete and retype.

39. **Grid edit mode** — a key layer for matrix surgery; input dispatch
    became a chain of mode layers, one method each.

40. **Undo/redo** — snapshot `(root, path, col)` before every
    tree-changing key; cursor restores too; redo clears on new edits.

41. **Context-sensitive quoting for 1-letter romans** (`d𝑦` bare, `'d'`
    isolated); session files spell spacers `` — later removed with
    sessions (§52).

42. **Tall angle brackets are diagonal arms only** — mixing `` with
    `╱╲` kinks; even height, fold = vertical pair in one column, upper
    row is the baseline.

43. **In-place minibuffer** — the typed `\command` overlays the cursor
    position with zero layout shift; the dedicated status line is gone.

44. **^F/marker integration***superseded by §68*; lessons about
    preview-not-move selection sweeps are recorded in the git history.

45. **Dotted roman runs** (`i.i.d.`), `\rm`/`\text` boxes, `\rcases`.

46. **`\text` real spaces; KaTeX function dictionary; ceil/floor/norm**
    — ceil/floor reuse bracket pieces with one corner dropped; family
    resolution by "which corners does the column run contain"; the norm
    `` resolves side by parity and cannot nest directly inside itself.

47. **Wide accents** — mark-side band rows hugging a bare base
    (`WideAccent{overs, unders, base}`); the base may be any block.
    Four fuzz findings folded into the scan rules.

48. **Mark fill glyphs** — every accent draws in a base-hugging form
    (`_` bar, `˰` hat, `˷`/`˜` tilde pair, `` dot, `` vec (§86), `․․` ddot
    with a one-column overhang); fill glyphs are side-exclusive so
    baseline recovery knows which way to dive. Breve deleted.

49. **Glyph consolidation** — band is `` everywhere; null delimiters
    `┆ ┊`; norm stacks the same ``; fused-grid junctions are light
    `├ ┤`; braces don't fuse and have minimum height 3; legacy forms
    dropped from the parser; `FUNCS` became a single `FuncSpec` table;
    `symbols/` and `output/` directories.

50. **`┈→┈` vec (now `┈⇾┈`, §86), `┌─` radical overline, `\^z` script commands.**

51. **^B ancestor chain; `\abs` = `⎢⎥`; word operators; CLI cleanups.**

52. **Prime `` vs quotes; text mode `"`; phf tables**`'` is always
    a quote delimiter; upright runs are `\operatorname` (2+ letters) or
    `\mathrm` (1); alphabet-family spellings collapsed into rule +
    exception tables; all lookup tables moved to phf (duplicate keys
    become build errors). Typst-era `also` spellings unified as
    aliases.

53. **Typst output removed** — double maintenance for little value;
    the AST stays target-neutral, so it can return as a module if
    needed.

54. **AST reshape** — LaTeX always uses `\operatorname` (so the
    dictionary only decides limits and lexing); `Text{t, math}` split
    into `Func`/`Roman`/`Text`; bands hold one piece (`┈argmax┈`),
    making band⇄bare a 1:1 unconditional rule; `BigOpSym`/`BigOp`
    split.

55. **Atom allow-list; total LaTeX spellings**`is_atom` derives from
    the tables; every accepted atom has a LaTeX spelling (gap=0 is a
    test); `^ ~ \` and backtick excluded from atoms; `# $ % &`
    escaped on output.

56. **WideAccent stacks**`overs`/`unders` vectors, same as compact
    accents; one picture, one AST.

57. **Table roles: pinning, not duplication** — the curated symbol
    table pins meanings against regeneration of the extracted one;
    intentional overrides are enumerated and tested. Aliases live in
    one table.

58. **Table consolidation and file splits** — delimiters became one
    `DELIMS` row per family (was 8 scattered sites); sup/sub bijection
    is one paired-string table; `editor.rs` and `main.rs` split by
    role.

59. **`symbols/` one-concern-per-file** — atoms/funcs/accents/delims/
    arrows/scripts/alphabets/ext; `mod.rs` only wires and re-exports.

60. **Atoms keyed by char** — the character itself is the command;
    input spellings are aliases; `latex` is the single output spelling.

61. **The `Edit` enum**`resolve(cmd) -> Option<Edit>` (pure) +
    `apply(Edit)` replaced the monolithic `execute`; most commands
    collapse into `Insert{node, wrap}` with one selection rule. Mode
    switches stay out of `Edit` (the boundary: does it mutate the
    undo-tracked tree?).

62. **Arrow/Accent enums** — mark characters became enum variants with
    `drawn()`; four parallel lookup functions disappeared.

63. **render/parse node-level unification — rejected** — render is
    compositional per node but parse's essence is the *recognition*
    problem before any node is known; acceptance is wider than the
    canonical form, so recognizers aren't derivable from the renderer.
    Share the glyph vocabulary (tables), not the control flow; the
    roundtrip property tests are the real "one rule per node" enforcer.

64. **`symbols`/`glyphs` split; `Delim = Col(ColDelim) | Angle`**    spelling tables and structural glyph constants are different kinds
    of thing; angles differ from column delimiters in type, not in a
    `None` field. Angle stays inside the pair node (bra-kets mix
    `⟨x│`).

65. **Cancel restricted to atom forms***superseded by §67*.

66. **Cancel restricted to `Sym`***superseded by §67*.

67. **Cancel removed entirely** — combining-overlay strikes are
    font-unstable (double-width cells, ratatui ghosting). Negation uses
    precomposed slashed atoms via one `negated` table (`\!=`, `\in!`    resolve through it); U+0338 input is an explicit parse error.

68. **Jump labels removed** — ^G and label-key selection deleted
    everywhere; ^B stays as an arrows-only mode; `\!` became the
    negation toggle command.

69. **Italic display toggle removed; grid edit moved to ^T.**

70. **^B paints only the current step** — with label jumps gone, the
    arrows walk one step at a time, so only the highlighted ancestor
    and its two neighbors are shown; the purple palette shrank to two
    shades.

71. **Review-round lessons** (2026-08) — two multi-agent review rounds
    over the completion work; recurring root causes: canvas vs screen
    coordinates confused in three separate places (fix: placement
    centralized in `Viewport`/`place_below`), and one-directional
    tables (spellings `resolve` accepted but completion didn't know —
    fix: independent-list tests in both directions).

72. **Unwrap generalized; deletes announce** (2026-08) — the staged
    unwrap (arm → lift contents → delete) extends to `\sqrt` (from its
    root's side) and `\norm`; Shift-selecting *just* a bracket arms the
    pair from either side, and an armed delete unwraps without
    selecting (the gesture already said what it wanted). Deleting into
    a non-empty structure from outside selects it whole first; entering
    is the arrows' job. Containers (`\norm`, `Edit::Delim` commands)
    wrap the selection like `(` does.

73. **Mode commands** (2026-08) — minibuffer spellings for the ctrl
    chords (`\free`, `\blockselect`, `\gridedit`, `\clipboard`,
    `\quit`), dispatched in the input layer (they move modes or the
    app, so they are not `Edit`s and `resolve` never sees them). Shown
    apart: purple minibuffer, bold `[^F]` chord markers in the
    completion; Enter-only commit (never Tab); no one-letter `\q`/`\c`.
    The minibuffer color is three-valued: green = runs, purple = mode,
    red = not yet anything.

74. **Roundtrip failures refuse the edit** (2026-08) — the guard undoes
    an edit whose picture stops parsing (the state before it is the
    last one that survives its own file format). `--debug` kept the
    broken state and dumped a report instead; both are gone (§82).

75. **Docs restructured in English** (2026-08) — README from the
    English draft; `aa-spec.md` rewritten as a self-contained spec for
    third-party parser authors (absorbing `parse-model.md`);
    `design.md` renamed to this ADR log; `keys.md` slimmed and split
    from `commands.md`; `editors.md` folded into the README and the
    roadmap.

76. **Renamed: mascii → formulAA** (2026-08) — "mascii" claimed ASCII
    while the format is Unicode-first, and read ambiguously. The new
    name keeps the AA (ASCII-art) identity where it is true — the
    *picture* — and reads as the word it contains (crate and binary:
    `formulaa`). Debug artifacts moved to `formulaa_debug/`, property
    test env vars to `FORMULAA_*`. The GitHub repositories and the
    editor extensions followed suit (formulaa, formulaa-vscode,
    formulaa-obsidian — old URLs redirect, and the legacy ```mascii
    fence stays accepted in the extensions).

77. **Command vocabulary cleanup; grid edit moves to ^G** (2026-08) —
    after examining what "canonical" actually touches (only the
    completion's commit string and a few UI messages — spellings never
    reach the format), the cleanup kept the changes with substance:
    `\tex` dropped (one letter from `\text`, a different feature);
    `\Vert` dropped (read as `\vert`'s sibling while doing something
    unrelated); standalone `\langle` dropped for consistency with the
    other `\lr`-only side names (a lone angle pair is `\lr<>`), with
    `\bra`/`\ket` added; `\smallmatrix` dropped as a command (its AA
    equals `\matrix`, so the smallness was silently lost — it still
    reads from LaTeX); `\negate` added as `\!`'s word form. Mode
    spellings: grid edit is `\g` `\G` `\grid` on **^G** (freeing ^T;
    `\t` was one letter from τ's `\ta`), block select gains `\block`
    and loses `\bs`. The `\lr`/`\delim` usage message now answers in
    whichever spelling was typed. The `\op*` box commits on Space (the
    band name is one piece — a typed space used to vanish silently).
    Help lines: the base line gains a context prefix (`\mid` in a
    pair, `^G` in a grid) instead of being replaced wholesale.

78. **Message and help overhaul; ^B ends on the whole formula**
    (2026-08) — one term per concept: "grid" everywhere (no more
    matrix/array vs grid drift), "clipboard" only for the system
    clipboard (^Y), the internal ^C/^X/^V store is "the buffer". The
    `\lr` message dropped its `usage:` prefix ("\lr takes a spec …"),
    and errors answer in the alias actually typed (`\negate`,
    `\limits` — an `executing`/`op_cmd` spelling is threaded through
    `execute` and the open box). The name-box help line is per-kind
    (the shared line claimed Space commits inside `\text`, where Space
    is content). ^B gained a final whole-formula target, so it works
    at the top level and the "no enclosing block" message is gone, as
    is the mode-entry info that duplicated the help line; obvious
    chords (⇧ selection) left the base help line. Second pass, on the
    "an experienced user shouldn't be lectured" principle: the startup
    greeting, the no-completion notice, and every ^C/^X/^V/^Z/^R
    message (success and whiff alike) are gone — the chords act or
    don't; the `\lr` message is one line with no examples (the
    completion is the manual); negate whiffs read like accent whiffs
    ("negation needs a symbol before the cursor"); errors are
    subject-first ("\X is not a command"); ^Y failure says "could not
    reach the system clipboard"; the box help lines only *name* the
    open box instead of teaching its keys.

79. **Accent deletion joins the grammar** (2026-08) — Backspace behind
    an accented atom peels the outermost mark first (the inverse of
    typing); the bare atom deletes last. The wide accent's base became
    a real cursor field (`Field::WideBase`), so the cursor walks in
    and edits it, the inner edge arms and unwraps through the staged
    bracket flow (`unwrap_contents` gained a WideAccent arm), and from
    outside it deletes like any structure — select whole, then remove.

80. **Copy blips, mids die by pointing, accents light up** (2026-08) —
    ^C acknowledges itself by inverting the selection for ~120ms (the
    only animation; the main loop polls just until the blip ends). A
    `` middle can be removed directly: Shift toward it from a
    segment's edge arms that one column (`Mark::MidArm` walks right to
    the │ and lights its full run), and the next delete merges the two
    segments — Backspace inside the segment keeps its old meaning. An
    armed wide accent now lights its `` bands (it has no delimiter
    columns, so the armed tint used to show nothing). Roadmap pruned:
    MathML, AsciiMath, paste work, `\roman`, ≥10 grid sizes are out;
    `\divides` is in (the ∣ atom by name, everywhere — `\mid` keeps
    its contextual double life). Help lines separate their entries
    with broken bars (¦ — a plain pipe collided with the c/| key),
    spell chords with the control glyph (⌃F, freeing the literal `^`
    to mean only the superscript key), and bold their key tokens; the
    ^F and ^B lines shrank to what isn't self-evident. The
    whole-formula ^B target's marks now insert right-to-left per row —
    same-row targets used to displace each other, cutting the ring
    short of the trailing atoms.

81. **The legacy angle form dropped** (2026-08) — a tall angle is the
    `╱╲` fold and nothing else: the baseline scan no longer takes a ``
    vertex sitting between the arms, the form the renderer stopped
    writing in §49. Nothing emits that picture, so the branch only
    bought reading files older than the format; the spec's baseline
    recovery (§4) names brace vertices only. The prose calls the editor
    WYSIWYG rather than LyX-style — the model is the common one, not
    that program's.

82. **`^O` replaces the CLI flags; the debug reports are gone**
    (2026-08) — `--print` and `--debug` are both removed, so `formulaa`
    takes a subcommand or nothing. `^O` (`\stdout`) writes the
    canonical AA to stdout and quits, the pipe-side twin of `^Y`:
    whether the formula should go to stdout is known when you are
    done, not before the first keystroke. The debug mode of §74 is
    withdrawn along with the report files — writing to the user's
    working directory uninvited is not the program's business, and a
    refusal that names its failure kind, next to the picture on
    screen, is what a bug report needed from it anyway.

83. **The editor reads and writes files** (2026-08) — `^O` printing the
    AA to stdout was a workaround for an editor that could not open
    anything: it started empty every time, and what it drew left only
    through the clipboard. Now `formulaa formula.aa` opens the file (a
    name that does not exist yet is simply where the first save goes)
    and `formulaa -` reads the formula from stdin. A file that does not
    parse is fatal — stderr says where, and the editor does not stand
    in for a document it cannot read back. `^O` saves, `^W` saves and
    quits, and `^Q`/Esc ask before dropping unsaved work; without a
    name the save asks for one. Both questions are answered on the
    status line: the editor holds them (`Editor::ask`) so the key
    meanings stay in `input.rs`, and the host only reads the answer off
    the `Effect`. Printing to stdout is gone with its reason, and the
    interface draws on stdout like every other full-screen editor.
    Reading the document from stdin and taking the keyboard from the
    terminal is the ordinary shape (`vim -`, `nano -`); here it needs
    crossterm's `use-dev-tty`, because its default reader takes keys
    from stdin and will not start once stdin is a spent pipe.

84. **The subcommands become flags** (2026-08) — the positional
    argument is the file to edit now, so `fmt` and friends would read
    as file names. They are `--format`, `--aa2latex` and `--latex2aa`
    (with `--aa2tex` / `--tex2aa` as aliases; `latex` is the spelling
    the rest of the vocabulary uses, §60). Parsing them by hand next to
    `--`, `-` and the positional was no longer worth it, so **clap**
    joins the `tui` feature — the library and its wasm build stay free
    of it.

85. **The picture keeps its spacing** (2026-08) — blank columns between
    siblings used to be separators and nothing else: the parser made no
    `Spacer`, so the editor could write a space it could not read back,
    and opening a hand-spaced file and saving it tightened the formula.
    Now every blank column between siblings comes back as a `Spacer`,
    bar the ones a picture cannot show: where the reading separates the
    two anyway, a lone blank *is* that separator (`render::absorb_row`,
    the same fuse predicate the renderer uses, asked of the row in
    context — a row-initial script's `` base and `Roman` glue change
    the answer). The contract tightens from
    `parse∘render == strip_spacers∘normalize` to
    `parse∘render == absorb_spacers∘normalize` (§21 relaxed it; this
    takes most of it back), and the renderer no longer adds its own
    blank beside a spacer — the spacer already is one. Fallout worth
    naming: the LaTeX serializer braces a band before a following
    script, and that scan had to learn to look *through* spacers, which
    write nothing in LaTeX (`\operatorname*{f}_{x}^{y}` would otherwise
    read back as the band's own upper limit).

86. **The vec accent is ``, not ``** (2026-08) — the drawn mark for
    `\vec` was U+FFEB, the halfwidth arrow: chosen because no one would
    ever want it as an atom, but only three of eight coding fonts here
    draw it and JuliaMono is not among them, so not even the merged
    font (§13) could show a vector. U+21FE RIGHTWARDS OPEN-HEADED ARROW
    fixes that — JuliaMono has it, so one donor is enough again — and
    it keeps the original virtue: no LaTeX spelling claims it, where
    `` (`\rightharpoonup`) and `` (`\dashrightarrow`) are both worth
    leaving free to become atoms.

87. **`^B` walks slots as well as nodes** (2026-08) — the ring used to
    step only through ancestor *nodes*, so a numerator holding more
    than one node could not be selected at all: from inside `π_θ(y|x)`
    the first stop was the whole fraction, and copying just the
    numerator meant a linear `Shift+←` sweep. The ring now alternates —
    the slot the cursor stands in taken whole, then the structure
    owning it, outward — which is also what the top level already did
    (the root row is the outermost slot, and it was there for exactly
    this reason). Two guards keep it honest: an empty slot is no
    target, and a slot holding a single node is the same box as that
    node seen from inside it, so consecutive duplicates collapse.

## Test strategy

- `tests/roundtrip.rs`: a corpus of real formulas (Cardano,
  Cauchy–Schwarz, Vandermonde, Gaussian integral, Schrödinger, Bayes,
  rotation matrices, continued fractions, nested limits …) plus
  randomized ASTs (2000 per run, scalable via `FORMULAA_PROP_N`).
- `tests/ui.rs`: key-script DSL plus random key sequences with a
  per-keystroke roundtrip check (`FORMULAA_UI_PROP_N`).
- Counterexample hunting: render two candidate ASTs and compare
  pictures (`ambiguity_counterexample_is_now_distinguishable`).
- After fixing a bug, revert the fix once to confirm the new test
  fails (several vacuous tests were caught this way).

## Roadmap

### Short term

- [x] **ANSI-16 palette audit** — portability: keep the theme within
  the standard + bright ANSI colors. Current state: named ANSI colors
  only (green = OK, maroon = errors, purple = selection, white =
  selection-secondary with forced black text, grey = popup ground);
  the caret is blinking reverse video; the ^F free cursor and ^B's
  provisional selection stay selection-purple and blink (the linear
  Shift selection blinks too); the secondary marks — ^B's
  one-step-outward ring and the ^F snap preview — are reverse video
  with no color of their own (the snap preview blinks); ^B shows only
  the selection and its outward step (the
  inner step is where you just came from); glyphs on any themed ground
  take a fixed foreground for light-terminal safety. Verified
  hands-on, 2026-08.

- (`\roman` and ≥10 grid sizes: rejected; MathML / AsciiMath /
  paste-behavior work: dropped from the map. `\divides` was adopted —
  the ∣ atom by name, everywhere.)

### Editor integrations (own repositories)

[formulaa-vscode](https://github.com/ho-oto/formulaa-vscode) and
[formulaa-obsidian](https://github.com/ho-oto/formulaa-obsidian) each carry
their own wasm crate and depend on this repo by git; bindings are
duplicated deliberately (repo independence over sharing). Both are
prototypes (code-reviewed, not yet field-tested). Zed has no extension
UI API yet — use CLI tasks (`formulaa --aa2latex` / `--format` over
`$ZED_SELECTED_TEXT`) or the TUI in its terminal. Staging toward the
inline ideal:

1. Now: fenced ```math blocks edited in a panel/modal (implemented).
2. Obsidian: replace the fenced block in Live Preview with the editor
   via a CodeMirror 6 `ReplaceDecoration` widget (technically the
   shortest path).
3. VS Code: no inline-webview API — consider a keystroke-applied
   "virtual structure edit" mode or a Notebook/Custom Editor route.
4. Zed: wait for the extension UI API, then mirror the VS Code shape.

### Mid term

- [ ] Multi-line big-operator glyphs (⎲⎳ / ⌠⌡) as an option.
- [ ] East Asian Width handling (currently every glyph is width 1).

### Long term

- [ ] Equation numbers.
- [ ] Templates (theorem environments), search and replace.
- [ ] crates.io release.