Compare commits

...

3 Commits

Author SHA1 Message Date
Julien Calixte
767742ba12 docs(v0.6): mark boot-read + catalog done, finalise the catalog list
Record the firmware boot-read (serde_json builds for xtensa) and the
just-init catalog as delivered, and replace the proposed French catalog
with the finalised 17-snippet English set (three groups). Note the
→/≠ base-font caveat and the no-clobber seeding guard.
2026-07-12 10:36:58 +02:00
Julien Calixte
85cbeceea9 feat(provisioning): seed a snippet catalog + starter prefs on just init
Add a curated snippet catalog (firmware/snippets-catalog/, three opt-in
groups: Symbols, Structure, Prose — 17 snippets, English prefixes,
translated from the source Zed set) and a _seed-configs step in `just
init` that jq-merges the chosen groups into repo/.typoena.snippets.json
and writes a starter repo/.typoena.toml. Seeds only files that are
absent, so re-running init never clobbers a synced library.
2026-07-12 10:36:46 +02:00
Julien Calixte
8527f75bf8 feat(firmware): read .typoena.snippets.json at boot into set_snippets
Mirror the prefs boot-read: load the git-tracked snippet library from the
SD repo, parse it with Snippets::parse, and hand it to the editor before
the first render. Missing/unreadable/malformed is non-fatal (no snippets,
editor runs). Verified serde_json builds for xtensa (cargo check, 0.6.0).
2026-07-12 10:30:02 +02:00
8 changed files with 241 additions and 29 deletions

View File

@@ -127,9 +127,11 @@ rather than refusing to start over a stray comma.
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
seeds this file from a curated catalog (`firmware/snippets-catalog/`) — you pick
which snippet groups you want and it `jq`-merges the selected subset into
`repo/.typoena.snippets.json` (committed on the device's first `:sync`). It
writes **only if the file is absent**, so re-running `init` on a card whose
clone already carries your library never overwrites it. 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

View File

@@ -53,10 +53,12 @@ behaviour.
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
- [x] **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.
host-testable `editor` crate via `serde_json` — the one new dependency,
confirmed to build for xtensa (`cargo check`, firmware 0.6.0). On-device
smoke-test still pending.
## The palette, generalised (`Cmd-P` · `>` · `$`) — done in core
@@ -81,25 +83,30 @@ them: `Go to file · > settings · $ snippets`.
Labels avoid `…`/`↹` — the palette/panel fonts are ISO-8859-15, which has neither
glyph — so `new file...` uses ASCII dots and the pause hint leads with `»`.
## First-time setup — snippet catalog (`just init`)
## First-time setup — snippet catalog (`just init`) — done
[`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]`,
`<v-clicks>`, 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):**
[`just init`](../firmware/README.md#provisioning-an-sd-card) seeds the two
git-tracked config files into `repo/` (so they commit + sync on the device's
first `:sync`), **writing only a file that is absent** so a re-`init` never
clobbers a synced library: a starter [`.typoena.toml`](typoena-toml.md) (the five
keys at their defaults) and a [`.typoena.snippets.json`](typoena-snippets.md)
assembled from a **curated catalog** (`firmware/snippets-catalog/`, `jq`-merged).
The catalog is grouped and opt-in — you pick the groups (each `[Y/n]`, all-yes on
a non-interactive run) rather than getting one writer's whole personal set. Not
every Zed snippet is worth proposing: the Slidev/blog-pipeline ones (`@[youtube]`,
`<v-clicks>`, frontmatter, mermaid) and hyper-specific personal ones are left out,
since a distraction-free prose appliance can't render them and you'd never miss
them. Prefixes are **English** (except `edanso`, the mnemonic for `œ`), bodies
translated from the source Zed snippets. **Three groups, 17 snippets:**
- [ ] **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`.
- [x] **Symbols** (inline, the keyboard can't type them): `arrow``→`,
`neq``≠`, `times``×`, `middot``·`, `deg``°`, `euro``€`, `edanso``œ`
(dead keys in v0.2.5 cover accents, *not* these). **Caveat:** `→` (U+2192)
and `≠` (U+2260) are outside ISO-8859-15, so they store correctly and sync
but render as a missing-glyph box on the device panel until the font is
extended; the other five are Latin-9 and draw fine. Kept deliberately.
- [x] **Structure**: `todo``- [ ] `, `link``[$1]($2)$0`, `img``![$1]($2)$0`,
`table` (2-col), `code` (fenced block). `img`/`code` are net-new.
- [x] **Prose / PKM templates** (`${n:label}` stripped to `$n`): `booknotes`,
`reference` (the English reference block — `refangl` folded in), `bias`,
`capture`, `standard`. (`5w1h` dropped.)

View File

@@ -1,6 +1,6 @@
[package]
name = "firmware"
version = "0.5.0"
version = "0.6.0"
authors = ["Julien Calixte <juliencalixte@gmail.com>"]
edition = "2024"
resolver = "2"

View File

@@ -161,13 +161,15 @@ ports:
# The `_`-prefixed recipes are shared internals (hidden from `just --list`).
sd_repo_dir := "repo"
# Full prep of a fresh card: copy the notes repo + write config, then eject.
# Run this once per card. <repo-src> is a clone made on this computer.
# Full prep of a fresh card: copy the notes repo, seed the git-tracked config
# files (first-time only), write the runtime config, then eject. Run this once
# per card. <repo-src> is a clone made on this computer.
init repo_src sd_volume="":
#!/usr/bin/env bash
set -euo pipefail
vol="$(just _card "{{sd_volume}}" | tail -n1)"
just _load-repo "{{repo_src}}" "$vol"
just _seed-configs "$vol"
just _write-conf "$vol" "{{repo_src}}"
just _eject "$vol"
@@ -280,6 +282,67 @@ _load-repo repo_src vol:
# -P for progress on the ~700 MB copy. Scoped to repo/, never the card root.
rsync -rtP --delete --modify-window=1 --exclude-from="$ignore_list" "$src/" "$dest/"
# First-time setup: seed the two git-tracked config files into the card's repo/
# so they commit + sync on the device's first `:sync`. Writes ONLY a file that is
# absent — a card whose clone already carries `.typoena.toml` /
# `.typoena.snippets.json` keeps its own, so a re-`init` never clobbers a synced
# library. The starter `.typoena.toml` mirrors the editor defaults; the snippet
# library is assembled from the curated catalog (snippets-catalog/, opt-in per
# group) with `jq`. Non-interactive runs take every default (all groups).
_seed-configs vol:
#!/usr/bin/env bash
set -euo pipefail
repo="{{vol}}/{{sd_repo_dir}}"
catalog="{{justfile_directory()}}/snippets-catalog"
interactive=1; [ -t 0 ] || interactive=0
# ask_yn "label" → 0 (yes) / 1 (no); default yes, non-interactive → yes.
ask_yn() {
local ans
if [ "$interactive" = 0 ]; then return 0; fi
read -r -p " $1 [Y/n]: " ans || ans=""
[[ "${ans:-Y}" =~ ^[Yy] ]]
}
# ── starter prefs (.typoena.toml) ────────────────────────────────────────
prefs="$repo/.typoena.toml"
if [ -e "$prefs" ]; then
echo "keeping existing .typoena.toml"
elif ask_yn "seed a starter .typoena.toml (editor defaults)?"; then
# printf per line (matching _write-conf), so no heredoc indentation trap.
{
printf '# Typoena editor preferences — hand-editable, git-tracked.\n'
printf '# Edit here, or change live from the Cmd-P palette (type `>`).\n'
printf 'save_on_idle = true\n'
printf 'format_on_save = true\n'
printf 'line_numbers = true\n'
printf 'theme = "light"\n'
printf 'auto_sync = "10m"\n'
} > "$prefs"
echo "wrote .typoena.toml"
fi
# ── snippet library (.typoena.snippets.json) from the catalog ────────────
snips="$repo/.typoena.snippets.json"
if [ -e "$snips" ]; then
echo "keeping existing .typoena.snippets.json"
elif ! command -v jq >/dev/null 2>&1; then
echo "warning: jq not found — skipping the snippet catalog (install jq, or" >&2
echo " hand-write $snips later)" >&2
else
echo "snippet catalog — choose groups to include:" >&2
picked=()
ask_yn "Symbols (→ ≠ × · ° € œ)" && picked+=("$catalog/symbols.json")
ask_yn "Structure (todo, link, img, table, code)" && picked+=("$catalog/structure.json")
ask_yn "Prose (booknotes, reference, bias, capture, standard)" && picked+=("$catalog/prose.json")
if [ "${#picked[@]}" -gt 0 ]; then
jq -s 'add' "${picked[@]}" > "$snips"
echo "wrote .typoena.snippets.json ($(jq 'length' "$snips") snippets, ${#picked[@]} group(s))"
else
echo "no groups chosen — skipping .typoena.snippets.json"
fi
fi
# Resolve + write <vol>/typoena.conf (Wi-Fi + PAT + git identity). Each value
# runs a ladder: firmware/.env (loaded via `set dotenv-load`) → derived from
# tools already on the machine (git config, gh, the active Wi-Fi network + its

View File

@@ -0,0 +1,58 @@
{
"Book notes": {
"prefix": "booknotes",
"body": ["# $1", "", "## $2 - $3", "", "## What the book is about", ""],
"description": "Reading notes (title / author - year)"
},
"Reference block": {
"prefix": "reference",
"body": ["___", "", "## References", ""],
"description": "Reference block"
},
"Cognitive bias": {
"prefix": "bias",
"body": [
"# $1",
"",
"## Misconception",
"",
"$3",
"",
"## The reality",
"",
"$4",
"",
"___",
"",
"## References",
"",
"- You are not so smart - p. $2",
""
],
"description": "Cognitive-bias note"
},
"Capture": {
"prefix": "capture",
"body": ["# $1", "", "## Captured inspiration", "", "$2", ""],
"description": "Inspiration capture"
},
"Standard": {
"prefix": "standard",
"body": [
"# $1",
"",
"## Key points",
"",
"## Mistakes to avoid",
"",
"___",
"",
"## Examples",
"",
"___",
"",
"## References"
],
"description": "Standard / playbook"
}
}

View File

@@ -0,0 +1,27 @@
{
"Todo item": {
"prefix": "todo",
"body": "- [ ] ",
"description": "Task-list item"
},
"Link": {
"prefix": "link",
"body": "[$1]($2)$0",
"description": "Inline link"
},
"Image": {
"prefix": "img",
"body": "![$1]($2)$0",
"description": "Image"
},
"Table": {
"prefix": "table",
"body": ["| $1 | $2 |", "|-|-|", "| $3 | $4 |"],
"description": "Two-column table"
},
"Code block": {
"prefix": "code",
"body": ["```$1", "$2", "```"],
"description": "Fenced code block"
}
}

View File

@@ -0,0 +1,37 @@
{
"Arrow": {
"prefix": "arrow",
"body": "→",
"description": "Rightwards arrow (→)"
},
"Not equal": {
"prefix": "neq",
"body": "≠",
"description": "Not-equal sign (≠)"
},
"Times": {
"prefix": "times",
"body": "×",
"description": "Multiplication sign (×)"
},
"Middle dot": {
"prefix": "middot",
"body": "·",
"description": "Middle dot (·), inclusive writing"
},
"Degree": {
"prefix": "deg",
"body": "°",
"description": "Degree sign (°)"
},
"Euro": {
"prefix": "euro",
"body": "€",
"description": "Euro sign (€)"
},
"OE ligature": {
"prefix": "edanso",
"body": "œ",
"description": "Ligature œ (e dans l'o)"
}
}

View File

@@ -10,7 +10,10 @@ use esp_idf_svc::hal::spi::{Dma, SpiBusDriver, SpiDriver};
use esp_idf_svc::hal::units::FromValueType;
use display::Frame;
use editor::{Editor, Effect, Mode, Prefs, Scope, CH, LOCAL_DIR, PREFS_PATH, REPO_DIR};
use editor::{
Editor, Effect, Mode, Prefs, Scope, Snippets, CH, LOCAL_DIR, PREFS_PATH, REPO_DIR,
SNIPPETS_PATH,
};
use firmware::epd::{self, Epd};
use firmware::persistence::{Storage, NOTES};
@@ -130,6 +133,21 @@ fn main() -> anyhow::Result<()> {
};
log::info!("prefs: {prefs:?}");
ed.set_prefs(prefs);
// Snippet library (.typoena.snippets.json, git-tracked). Parsed with
// serde_json in the editor crate; a missing / unreadable / malformed file is
// non-fatal — the editor simply has no snippets and runs unchanged.
let snippets = match storage.load_path(SNIPPETS_PATH) {
Ok(src) => match Snippets::parse(&src) {
Ok(s) => s,
Err(e) => {
log::warn!("snippets parse FAILED ({e}); none loaded");
Snippets::default()
}
},
Err(_) => Snippets::default(),
};
log::info!("snippets: {} loaded", snippets.0.len());
ed.set_snippets(snippets);
let mut updates: u32 = 0;
let mut cursor_shown = true; // the initial render includes the caret
let mut last_activity = Instant::now();