diff --git a/docs/typoena-snippets.md b/docs/typoena-snippets.md new file mode 100644 index 0000000..8ebe6c0 --- /dev/null +++ b/docs/typoena-snippets.md @@ -0,0 +1,144 @@ +# `.typoena.snippets.json` — snippet library + +> The git-tracked file that holds your trigger-driven text expansions for +> Markdown authoring. Hand-editable (and Zed-compatible, so you can paste your +> existing snippets straight in), synced across devices like your notes. Landed +> in **v0.6** (see [`macroplan.md`](macroplan.md)). The editing surfaces — inline +> Tab-expansion and the `$` palette — are specified in +> [`v0.6-markdown.md`](v0.6-markdown.md). +> +> **Three files, three concerns, don't confuse them.** `.typoena.snippets.json` +> is *content* (your templates). [`.typoena.toml`](typoena-toml.md) is *behaviour* +> (auto-save, gutter). `/sd/typoena.conf` is *secrets* (Wi-Fi, PAT), gitignored +> and never committed. The first two live in the repo and sync; the third is +> per-device. + +## Location + +``` +/sd/repo/.typoena.snippets.json +``` + +It sits in the Tracked repo beside [`.typoena.toml`](typoena-toml.md), so it is +**committed and pushed** like any note and **syncs to every device** that clones +the repo. Your snippet library follows you. It is read **once at boot**; a +**missing, empty, or malformed file is fine** — you simply have no snippets, and +the editor runs unchanged. + +## Format + +Deliberately **Zed's snippet JSON shape**, so the contents of a Zed +`snippets/markdown.json` paste in unmodified: + +```json +{ + "Markdown link": { + "prefix": "link", + "body": "[$1]($2)$0", + "description": "Inline link" + }, + "Book notes": { + "prefix": "fiche", + "body": ["# $1", "", "## $2 — $3", "", "## What the book is about", ""], + "description": "Fiche de lecture" + } +} +``` + +- The top-level key is the **display name** (what the `$` palette shows). +- `prefix` — the word that triggers inline Tab-expansion. +- `body` — a **string**, or an **array of lines** joined with `\n` (Zed's form; + it sidesteps embedded-newline escaping and reads cleanly for multi-line + templates). +- `description` — optional but recommended: the `$` palette fuzzy-matches it and + shows it, so it is how you find a snippet you don't remember the prefix for. + +### Tab stops + +A body is literal text plus numbered stops: + +- `$1 … $n` — empty stops the caret visits in order. +- `$0` — the final resting place (defaults to the end of the insertion if absent). +- `${n:label}` — **accepted, but the label is stripped** to a bare `$n`. The + editor has no selection/overtype model, so a label would just be text to + delete; on a device with no completion popup it could never be shown as a + prompt anyway. The **headings and structure carry the template** — the labels + were only hints. This is what lets a Zed file with `${1:Titre}` load as-is. +- **No dynamic or computed values** (no `date`, no `clipboard`). There is no RTC + — the wall clock is valid only after Wi-Fi + SNTP, so a `date` snippet would + stamp 1970 on a cold boot. A stop is empty or it is literal; nothing else. + +## The two surfaces + +Every snippet works both ways — there is **no hidden two-tier rule** where some +are "inline only" and some are "palette only". Inline Tab is the fast path you +reach for once a prefix is in muscle memory; the `$` palette is discovery. + +### Inline Tab-expansion (Insert mode) + +Type a prefix, press **Tab**. If the word immediately before the caret matches a +snippet prefix, it expands; otherwise Tab inserts spaces as it does today. (Tab +arrives as an ordinary character, so this is a check inside the Insert-mode +handler, alongside the existing list-continuation transform.) + +On a **typing pause** — the same throttle as the word-count / cursor refresh, so +never a per-keystroke e-ink flash — if the word before the caret is a prefix, the +right side panel shows a quiet hint (`↹ fiche de lecture`). The panel is ~17 +columns, so the hint is the **snippet name / first line**, not the whole body; +the full preview is what the `$` palette is for. + +### `$` palette (browse + insert) + +Open the palette (`Cmd-P`) and type **`$`** — the same sigil mechanism as `>` for +commands. The query after the `$` fuzzy-matches name, prefix, and description; +`Ctrl-N`/`Ctrl-P` move the selection; **Enter inserts the body at the caret** and +starts the tab-stop session (dropping you into Insert at `$1`). The empty-palette +placeholder legends the sigils: `Go to file · > settings · $ snippets`. + +## The tab-stop session + +Identical whether the snippet was expanded inline or inserted from the palette: + +- After insertion the caret lands on **`$1`** (or the end, if the body has no + stops), in **Insert** mode. +- **Tab advances** to the next stop, **forward only** (no Shift-Tab). The last + Tab lands on `$0` / the end and ends the session. +- Pending stop offsets sit **after the caret** and shift with the edits you make + at each stop, so typing at `$1` keeps `$2 … $n` correctly placed. +- The session **auto-aborts** on Esc, a mode change, or a motion that leaves the + stop range — after which the buffer is just text and Tab inserts spaces again. + +## Parsing + +The parse lives in the host-testable `editor` crate (`Snippets::parse`), using +`serde_json` — JSON string escapes (`\n`, `\"`, `\uXXXX`) are a foot-gun to +hand-roll, and `serde_json` is battle-tested; the editor crate is `std`, so it +compiles for xtensa via esp-idf. This is the **one new dependency** the feature +adds. The firmware reads the file at boot and hands the parsed list to +`Editor::set_snippets`, mirroring how `.typoena.toml` is read and applied via +`set_prefs`. A parse error is **non-fatal**: log it and boot with no snippets, +rather than refusing to start over a stray comma. + +## Editing it + +- **On your computer (the normal path).** It's plain JSON in your notes repo — + edit it in your real editor, copy entries over from Zed, commit, and it reaches + the device on the next clone/sync. This is deliberately where the heavy editing + happens; the appliance is for writing, not for maintaining a JSON library. +- **First-time setup.** [`just init`](../firmware/README.md#provisioning-an-sd-card) + seeds this file from a curated catalog — you pick which snippet groups you + want and it writes the selected subset into `repo/.typoena.snippets.json` + (committed on the device's first `:sync`). See + [`v0.6-markdown.md`](v0.6-markdown.md) for the catalog. +- **On-device hand-edit — deferred.** The palette hides dotfiles, and `:e` was + dropped in v0.6, so there is no in-editor path to this file yet. When one is + wanted it returns as a discoverable `> edit snippets` command that opens the + file directly, rather than resurrecting a general `:e`. + +## See also + +- [`v0.6-markdown.md`](v0.6-markdown.md) — the editing surfaces, the `$`/`>` + palette model, and the setup-recipe snippet catalog. +- [`typoena-toml.md`](typoena-toml.md) — the sibling prefs file this is kept + separate from, and the `>` command palette snippets share the surface with. +- [`macroplan.md`](macroplan.md) — v0.6 scope. diff --git a/docs/v0.6-markdown.md b/docs/v0.6-markdown.md index 3427034..62c18ef 100644 --- a/docs/v0.6-markdown.md +++ b/docs/v0.6-markdown.md @@ -3,36 +3,93 @@ > Part of the [Typoena macro plan](macroplan.md). Requirements and targets: > [qfd.md](qfd.md). Load-bearing decisions: [adr.md](adr.md). -**Status:** render affordances done early; the snippet -engine remains (snippets are net-new scope, added 2026-07-08). +**Status:** render affordances done early; the snippet engine is the remaining +work (snippets are net-new scope, added 2026-07-08; reshaped 2026-07-12 from a +hard-coded table into a git-synced, Zed-compatible library with a `$` palette +launcher — see [`typoena-snippets.md`](typoena-snippets.md) for the file format). - [x] Heading lines bolded in render (faux-bold double-strike) - [x] List continuation on Enter inside `- ` / `1. ` (with empty-item exit) - [x] Soft-wrap at word boundaries -- [ ] **Snippets** — trigger-driven text expansion for Markdown authoring - (Zed-inspired, but no completion popup: e-ink's ~630 ms refresh rules out - a live filtering menu, and it fights the distraction-free premise). Shape, - mirroring the existing `list_marker` insert-transform: - - [ ] Tab in Insert mode triggers expansion: if the word immediately before - the caret matches a snippet prefix, expand it; otherwise insert spaces - as today (`expand_snippet(word) -> Option<(body, stops)>`, alongside - `list_marker`). - - [ ] A snippet body is literal text plus numbered empty tab stops `$1 … $n` - and a final `$0`. There is no placeholder text (`${1:label}`) — the - editor has no selection/overtype model, so a placeholder would just be - text to delete. There are no dynamic or computed values either (e.g. no - `date` — there's no RTC; the wall clock is valid only after Wi-Fi+SNTP, - so it'd stamp 1970 on a cold boot). - - [ ] After expansion the caret lands on `$1`; Tab advances to the next stop, - forward only (no Shift-Tab). Stored stop offsets shift with edits at the - caret (all pending stops are always after it). The session auto-aborts - on Esc, a mode change, or a motion that leaves the stops. - - [ ] On a typing pause (same throttle as the insert cursor / word-count - refresh — the panel never repaints per keystroke), if the word before - the caret is a snippet prefix, the side panel shows the hint (the target - expansion). Quiet while typing; the hint appears on pause. - - [ ] The snippet table is hard-coded in the binary to start; a git-syncable - file on SD (`/sd/repo/.snippets`) is a later option, deferred while SD - is still blocked. - - [ ] Starter set: link `[$1]($2)$0`, image `![$1]($2)$0`, fenced code block, - etc. +## Snippets + +Trigger-driven text expansion for Markdown authoring (Zed-inspired, but **no +completion popup**: e-ink's ~630 ms refresh rules out a live filtering menu, and +it fights the distraction-free premise). The library is a git-synced, +Zed-compatible JSON file — full file-format reference in +[`typoena-snippets.md`](typoena-snippets.md). This section is the *editor* +behaviour. + +- [x] **The tab-stop engine** — the shared core both surfaces drive. A body is + literal text plus numbered stops `$1 … $n` and a final `$0`; on insertion + the caret lands on `$1` (or the end), in Insert. Tab advances to the next + stop, **forward only** (no Shift-Tab); pending stops sit after the caret + and shift with edits there. The session auto-aborts on Esc, a mode change, + or a motion that leaves the stops. `${n:label}` parses to a bare `$n` (the + label is stripped — no selection model to fill); no dynamic values (no RTC, + so no `date`). +- [x] **Inline Tab-expansion (Insert mode).** If the word immediately before the + caret matches a snippet prefix, Tab expands it and starts the tab-stop + session; otherwise Tab inserts spaces as today. A check in the Insert + handler alongside the existing `list_marker` transform + (`expand_snippet(word) -> Option`). Tab already arrives as + `Key::Char('\t')`, so no new key event. +- [ ] **Hint-on-pause.** On the typing pause (same throttle as the word-count / + cursor refresh — never a per-keystroke repaint), if the word before the + caret is a prefix, the right side panel shows a quiet hint. The panel is + ~17 cols, so the hint is the snippet **name / first line**, not the whole + body — the full preview is the `$` palette's job. +- [x] **`$` palette (browse + insert).** `Cmd-P` then `$` switches the palette to + the snippet list (the same sigil mechanism as `>`). Fuzzy-matches name / + prefix / description; Enter inserts the body at the caret and starts the + tab-stop session. Lists **all** snippets — the fuzzy filter handles clutter, + so there's no hidden "inline-only vs palette-only" split. Rows read + `Name [prefix]`, so browsing also teaches the inline trigger. +- [ ] **Boot wiring.** The host reads `/sd/repo/.typoena.snippets.json` at boot + and calls `Editor::set_snippets` (mirroring `set_prefs`); a missing or + malformed file is non-fatal (no snippets, editor runs). Parse lives in the + host-testable `editor` crate via `serde_json` — the one new dependency. + +## The palette, generalised (`Cmd-P` · `>` · `$`) + +v0.5 shipped `Cmd-P` = files and `>` = a five-entry settings list (`save_on_idle`, +`format_on_save`, `line_numbers` toggles + `theme`/`auto_sync` rotations). v0.6 +makes the sigils a clean split by verb, and the empty-palette placeholder legends: +`Go to file · > settings · $ snippets`. + +- **bare `Cmd-P`** → *navigate*: go to file (unchanged). +- **`>`** → *act on the editor* — the command palette. The pref toggles are just + its stateful entries, not a special section. Dispatch differs per entry: a + **toggle** flips and the list **stays open** (as today); a **one-shot** (e.g. + `format`, `publish`) runs and **closes**; a **parameterised** command morphs + the palette into a second **input step** (select `New file` → the box becomes a + filename prompt → Enter creates it, scope read from a `repo/`/`local/` prefix as + `:enew` does today). This retires `:e` (bare `Cmd-P` covers file-opening; + dotfiles get a dedicated `> edit …` command if/when wanted). +- **`$`** → *insert content* — the snippet launcher above. + +`format` / `publish` as `>` entries are a small opportunistic add (they prove `>` +is a real action registry, not a settings box); the headline is snippets. + +## First-time setup — snippet catalog (`just init`) + +[`just init`](../firmware/README.md#provisioning-an-sd-card) gains a step that +seeds the two git-tracked config files into `repo/` (so they commit + sync on the +device's first `:sync`): a starter [`.typoena.toml`](typoena-toml.md) (the four +keys at their defaults, or confirmed at a prompt) and a +[`.typoena.snippets.json`](typoena-snippets.md) chosen from a **curated catalog** +checked into the repo. The catalog is grouped and opt-in — you pick the groups +you want rather than getting all of one writer's personal templates. Not every +Zed snippet is worth proposing: the Slidev/blog-pipeline ones (`@[youtube]`, +``, frontmatter, mermaid) and render-dependent or hyper-specific ones +are left out of the menu, since a distraction-free prose appliance can't render +them and you'd never miss them. **Proposed groups (pending sign-off):** + + - [ ] **Symbols** (inline, keyboard can't type them): `fleche`→`→`, + `different`→`≠`, `fois`→`×`, `median`→`·`, `degre`→`°`, `euro`→`€`, + `edanso`→`œ` (dead keys in v0.2.5 cover accents, *not* these). + - [ ] **Structure**: `todo`→`- [ ] `, `link`→`[$1]($2)$0`, `img`→`![$1]($2)$0` + (net-new — obvious for Markdown), `table`, `code` (fenced block). + - [ ] **Prose / PKM templates** (`${n:label}` stripped to `$n`): `fiche` + (book notes), `reference`/`refangl` (reference block), `biais`, + `capture`, `standard`, `5w1h`.