diff --git a/CONTEXT.md b/CONTEXT.md index 793a9ab..b5b35e7 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -2,7 +2,8 @@ A single-purpose writing appliance: e-ink + mechanical keyboard + ESP32-S3. The user opens the lid, writes Markdown, and (when they choose) publishes to a git -remote. This glossary fixes the language of that workflow. +remote. This glossary fixes the language of that workflow, and of the screen +the writer looks at while doing it. **Related docs:** [`README.md`](README.md) — project overview, hardware, macro roadmap. @@ -55,6 +56,28 @@ into user-facing language). > prompt. A **Publish** is the only user-observable unit of "shipping work"; > internal commits are an implementation detail of that. +### Screen regions + +**Writing column**: +The left region of the panel showing the text being edited — the _only_ region +that repaints per keystroke. ~60 columns, full panel height; straddles the +driver's `x = 396` seam invisibly. +_Avoid_: edit area, text area, main pane (superseded — they named the old +full-width text region before the side panel carved out its right edge). + +**Side panel**: +The right region (~150 px / ~20 cols, full height) holding all metadata: +filename + dirty dot, mode, word count, session, clock, Wi-Fi, +keyboard-disconnect flag, publish state. Sits entirely in the master half +(right of the `x = 396` seam). Every field is static, event-driven, or +throttled — never per-keystroke. +_Avoid_: header, status line, status bar (retired — the old top header band and +bottom status band are both collapsed into this one right-hand region); sidebar. +Do not write bare **panel**: it collides with the **transient panel** (the +modal full-screen help/config view that swaps in over the editor — a later +release, see [`docs/spikes.md`](docs/spikes.md) Spike 11). Always qualify: +_side panel_ vs _transient panel_. + ## Relationships - A **File** belongs to exactly one scope (**Tracked** or **Local**), fixed at @@ -68,7 +91,7 @@ into user-facing language). > **Dev:** "If I'm in a **Local** file and I press `Ctrl-G`, what happens?" > **Domain expert:** "Nothing — **Publish** is unavailable in **Local**. The -> status line says so. There is no path from **Local** to the remote." +> side panel says so. There is no path from **Local** to the remote." > **Dev:** "So if I want to publish something that started as a journal entry, > I have to copy-paste it into a **Tracked** file?" > **Domain expert:** "Yes, deliberately. Promotion is a manual gesture, not a @@ -94,7 +117,7 @@ into user-facing language). the remote was cloned on, and never switches. - **Durability before delivery.** A **Publish**'s user-meaningful moment is when the local commit lands (~0.2 s), not when the push completes - (~5–10 s). The status line surfaces the commit-landed state as soon as + (~5–10 s). The side panel surfaces the commit-landed state as soon as it exists; the remaining push time is the transport of an already-safe thing. Long-form rationale: [`docs/notes/ctrl-g-perceived-latency.md`](docs/notes/ctrl-g-perceived-latency.md). diff --git a/README.md b/README.md index 8b69e6f..5573fef 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ the remote) is offered. | Part | Choice | Why | | --------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | MCU | **ESP32-S3-N16R8** (16 MB flash, 8 MB octal PSRAM) | USB OTG host (for the keyboard), Wi-Fi, BLE, dual core @ 240 MHz, plenty of PSRAM for git pack data and screen buffer. Best-supported Rust target in the ESP family. | -| Display | **GDEY0579T93 + DESPI-c579 breakout** (5.79", 792×272, 1-bit) | Good Display panel matched with its own FPC breakout. Strip aspect (~2.9:1) — Freewrite-coded: ~12 lines of edit area, ~95 cols. Tiny framebuffer (~27 KB) leaves PSRAM headroom. The DESPI-c579 is a passive level-shifter / FPC-to-header board, not an active controller — driven over plain SPI like any other epd. | +| Display | **GDEY0579T93 + DESPI-c579 breakout** (5.79", 792×272, 1-bit) | Good Display panel matched with its own FPC breakout. Strip aspect (~2.9:1) — Freewrite-coded: ~13 lines, ~79 cols at the editor's 10px font. Tiny framebuffer (~27 KB) leaves PSRAM headroom. The DESPI-c579 is a passive level-shifter / FPC-to-header board, not an active controller — driven over plain SPI like any other epd. | | Keyboard | **Nuphy Air60/Halo65 wired USB-C** | ESP32-S3 acts as USB host via TinyUSB. BLE-HID is a fallback but contends with Wi-Fi for radio time during push. | | Storage | microSD over SPI | Holds both the git working copy (`/sd/repo/`) **and** the local-only scratch space (`/sd/local/`). Internal flash is for firmware + config only. | | Power | **USB-C wall power for MVP**, 18650 + IP5306 in Phase 3 | Measure power profile on real hardware before sizing the battery. E-ink + sleep should give multi-day battery life but battery introduces charging, safety, and BMS complexity we don't need on day one. | @@ -147,7 +147,7 @@ gantt | [v0.1](docs/roadmap.md#v01--mvp-it-writes-it-pushes--) | MVP | Boots, edits one file, `Ctrl-G` pushes. | | [v0.2](docs/roadmap.md#v02--vim-navigation--) | Vim nav | Normal/Insert, motions, line numbers. | | [v0.3](docs/roadmap.md#v03--vim-editing--) | Vim edit | `dd yy p`, undo/redo, counts. | -| [v0.4](docs/roadmap.md#v04--visual-mode--ex-commands--) | Visual + ex | `v V`, `:w :q :e`, status line. | +| [v0.4](docs/roadmap.md#v04--visual-mode--ex-commands--) | Visual + ex | `v V`, `:w :q :e` command line. | | [v0.5](docs/roadmap.md#v05--file-palette--multi-file--) | Files | `Ctrl-P` over `/repo` + `/local`, buffers. | | [v0.6](docs/roadmap.md#v06--markdown-affordances--) | Markdown | Headings, list continuation, soft-wrap. | | [v0.7](docs/roadmap.md#v07--search--better-git--) | Search + git | `/`, `:Gpull`, `:Gbranch`. | diff --git a/docs/adr.md b/docs/adr.md index 0c0c1e0..aab4a01 100644 --- a/docs/adr.md +++ b/docs/adr.md @@ -152,7 +152,7 @@ as the price of those properties. ### Consequences -- Visible edit area on this panel is ~11 lines. UI must embrace the +- Visible writing column on this panel is ~13 lines. UI must embrace the constraint — no multi-pane, no large headers. See [v0.1 product → screen layout](v0.1-mvp-product.md#screen-layout). - Framebuffer is ~27 KB; keeps PSRAM free for git pack data — a top-3 diff --git a/docs/notes/ctrl-g-perceived-latency.md b/docs/notes/ctrl-g-perceived-latency.md index 7f855b7..eca56be 100644 --- a/docs/notes/ctrl-g-perceived-latency.md +++ b/docs/notes/ctrl-g-perceived-latency.md @@ -19,6 +19,6 @@ The reframing question: **what is the user actually waiting for?** For `Ctrl-S`, For `Ctrl-G`, the equivalent moment isn't "push complete." It's "commit landed locally" — which happens at ~0.2 seconds, well before the push even starts. From that moment on, your work is preserved across power loss, SD removal, the apocalypse — everything except remote delivery. The remaining 5–10 seconds is _transport of an already-safe thing_. -Surface that moment in the status line at ~0.2 seconds (`✓ committed abc1234 · pushing…`) and the perceived latency of `Ctrl-G` collapses from 10 seconds to roughly 200 milliseconds. The gap with `Ctrl-S` disappears. +Surface that moment in the side panel at ~0.2 seconds (`✓ committed abc1234 · pushing…`) and the perceived latency of `Ctrl-G` collapses from 10 seconds to roughly 200 milliseconds. The gap with `Ctrl-S` disappears. **Durability before delivery.** The moment that matters to the user is the moment durability is achieved, not the moment delivery completes. Once you see that, the slow operations stop feeling slow. diff --git a/docs/qfd.md b/docs/qfd.md index 15b15aa..641c0a6 100644 --- a/docs/qfd.md +++ b/docs/qfd.md @@ -474,9 +474,13 @@ These are the live tensions we are watching, not deciding harder: transformation; H4's "to cursor" is implicit in Boot's definition. Matrix cell strengths held; no Σ recompute. -The minor variance between README's "~12 lines" and product/[ADR-003]'s -"~11 lines" of edit area is within rounding for a 14 px glyph in a 240 px -tall edit region and is not load-bearing. +The earlier variance between README's "~12 lines" and product/[ADR-003]'s +"~11 lines" of "edit area" is now superseded: the side-panel redesign removed +the top header and bottom status bars (metadata moved into the **side panel**), +so the **writing column** spans the full panel height — ~13 lines at the +editor's 20 px font (`FONT_10X20`, `editor.rs` `ROWS = HEIGHT / 20 = 13`). +README, the product/technical docs, and [ADR-003] are all updated to ~13 lines +(writing column). --- diff --git a/docs/roadmap.md b/docs/roadmap.md index f618fe8..7420acc 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -29,7 +29,7 @@ Out of scope: Vim, palette, multiple files, branches, conflict handling. ## v0.2 — Vim navigation — [ ] -- [ ] Mode state machine (Normal / Insert), mode indicator in status line +- [ ] Mode state machine (Normal / Insert), mode indicator in the side panel - [ ] Movement: `h j k l`, `w b e`, `0 $`, `gg G`, `Ctrl-d Ctrl-u` - [ ] `i a o O A` to enter Insert - [ ] `Esc` returns to Normal @@ -46,7 +46,6 @@ Out of scope: Vim, palette, multiple files, branches, conflict handling. - [ ] Visual char (`v`) and line (`V`) modes, `y d c` on selections - [ ] `:` command line: `:w :q :wq :e ` -- [ ] Status line shows file path, dirty flag, mode ## v0.5 — File palette + multi-file — [ ] @@ -56,7 +55,7 @@ Out of scope: Vim, palette, multiple files, branches, conflict handling. - [ ] `:e` and palette share the same recent-files list - [ ] `:enew` creates a new file — prompts for scope (tracked vs local) - [ ] `Ctrl-G` is disabled / hidden when the current buffer is local-scope -- [ ] Status line briefly shows file count on `Ctrl-G` when the publish bundles +- [ ] The side panel briefly shows file count on `Ctrl-G` when the publish bundles more than one dirty Tracked file (e.g. `"publishing 3 files: abc1234"`), so workspace-scoped behaviour stays visible to the user @@ -78,7 +77,7 @@ Out of scope: Vim, palette, multiple files, branches, conflict handling. - [ ] 18650 + IP5306 charge board, soft power switch - [ ] Light sleep on idle > 30 s (keyboard interrupt wakes) - [ ] Deep sleep on lid close (reed switch); restore cursor + buffer -- [ ] Battery indicator in status line +- [ ] Battery indicator in the side panel ## v0.9 — Robustness — [ ] diff --git a/docs/v0.1-mvp-product.md b/docs/v0.1-mvp-product.md index 99b6e81..af9bba4 100644 --- a/docs/v0.1-mvp-product.md +++ b/docs/v0.1-mvp-product.md @@ -99,49 +99,59 @@ strip aspect (~2.9:1) — biases the UX toward "current line and recent context," not a full page. Hardware rationale: [ADR-003](adr.md#adr-003-display-medium--e-ink-gdey0579t93-panel). ``` -┌─────────────────────────────────────────────────────────────────────────────────────┐ -│ notes.md ● 14:02:33 │ ← header -├─────────────────────────────────────────────────────────────────────────────────────┤ -│ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor │ -│ incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud │ -│ exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. │ -│ │ -│ Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu │ -│ fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in │ ← edit area -│ culpa qui officia deserunt mollit anim id est laborum. │ (~11 lines) -│ │ -│ The quick brown fox jumps over the lazy▎ │ (cursor block) -│ │ -│ │ -│ │ -├─────────────────────────────────────────────────────────────────────────────────────┤ -│ INSERT • unsaved • Wi-Fi — │ ← status -└─────────────────────────────────────────────────────────────────────────────────────┘ +┌───────────────────────────────────────────────────────┬─────────────────────┐ +│ Lorem ipsum dolor sit amet, consectetur adipiscing │ notes.md ● │ +│ elit. Sed do eiusmod tempor incididunt ut labore et │ INSERT │ +│ dolore magna aliqua. Ut enim ad minim veniam, quis │ │ +│ nostrud exercitation ullamco laboris nisi ut aliquip │ 1 240 words │ +│ ex ea commodo consequat. │ +318 this session │ +│ │ 12:07 elapsed │ +│ Duis aute irure dolor in reprehenderit in voluptate │ │ +│ velit esse cillum dolore eu fugiat nulla pariatur. │ │ +│ Excepteur sint occaecat cupidatat non proident. │ │ +│ │ 14:02 │ +│ The quick brown fox jumps over the lazy▎ │ Wi-Fi — │ +│ │ ✓ pushed abc1234 │ +└───────────────────────────────────────────────────────┴─────────────────────┘ + writing column (~60 cols, full height) side panel (~20 ch) ``` -- Header: filename, dirty indicator (●/○), wall clock if SNTP has set time. -- Status: mode (always `INSERT` in v0.1), save state, Wi-Fi state - (`—` = off, `✓` = connected, `✗` = failed; default is `—` because the - radio is on-demand). `Ctrl-S` briefly flashes "saved at HH:MM" and - returns. `Ctrl-G` transitions through `✓ committed abc1234 · pushing…` - at ~0.2 s — the commit-landed moment, when the user's work is durable - (see "Durability before delivery" in [`CONTEXT.md`](../CONTEXT.md)) — - and ends at `✓ pushed abc1234` (or `publish failed: `, with the - local commit preserved either way). -- Cursor: solid block at the insertion point. **No blink.** +- **Writing column** (left, ~60 cols, full panel height): only the text and a + solid-block cursor at the insertion point. **No blink.** The seam at `x = 396` + runs through it invisibly (the driver's split-and-mirror blit handles it). + This is the only region that repaints as you type. +- **Side panel** (right, ~150 px / ~20 cols, full height): all metadata lives + here, so the writing column keeps the full height. Top — **filename** + dirty + dot (●/○) and **mode** (always `INSERT` in v0.1). Middle — **word count** and + the **session** (words written + elapsed time), both refreshed on a short + typing pause, *not* per keystroke. Bottom — ambient state: **clock** (if SNTP + has set the time), **Wi-Fi** (`—` = off/on-demand, `✓` = connected, + `✗` = failed), a **keyboard-disconnect** flag (`⌨ ✗`, shown *only* while the + keyboard is dropped; blank when healthy — no permanent `⌨ ✓`, per + [`CONTEXT.md`](../CONTEXT.md) "No state the user didn't ask for"), and + **publish state**. `Ctrl-S` briefly flashes "saved HH:MM"; + `Ctrl-G` transitions through `✓ committed abc1234 · pushing…` at ~0.2 s — the + commit-landed moment, when the user's work is durable (see "Durability before + delivery" in [`CONTEXT.md`](../CONTEXT.md)) — and ends at `✓ pushed abc1234` + (or `publish failed: `, with the local commit preserved either way). +- **Deliberately not shown:** any cursor-position readout (*line* or *column*), + character count, or reading-time estimate — each repaints a panel row on cursor + movement or per keystroke, on a row apart from the cursor, doubling the ghosting + rate for little writerly value. v0.1 ships no positional readout at all. -Fonts: one mono font, embedded in firmware. Target sizing: a 14 px mono -glyph at roughly 8 px advance gives ~95 columns × ~11 visible lines of edit -area after header (16 px) and status (16 px) bars. Final size tuned during -spike 2 against real legibility on the panel. +Fonts: one mono font for the writing column, a smaller one for the panel, both +embedded in firmware. Sizing target: ~60 columns of writing (down from the +full-width ~79 — too wide for a comfortable line length) across the full ~13 +visible lines, now that metadata has moved off the top/bottom bars into the side +panel. Final size tuned during spike 2 against real legibility on the panel. **UX consequence of the strip aspect:** the cursor stays near the bottom of -the visible area during normal typing; the edit area scrolls one line at a +the visible area during normal typing; the writing column scrolls one line at a time. There is no "page" — just a continuously advancing strip. Embrace it, don't fight it. **What scroll looks like on e-ink:** every line-scroll is a partial refresh -of the full edit area — every line moves, there is no small dirty rect for +of the full writing column — every line moves, there is no small dirty rect for scroll — so each scroll costs ~200–300 ms. Per-keystroke partial refreshes accumulate ghosting; the render module clears it by upgrading every 20th refresh to a full refresh, deferred to the next idle pause ≥ 1 s so it @@ -156,15 +166,16 @@ a save. | Failure | What the user sees | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | No SD card on boot | "no SD card — insert one and reboot" | -| SD card mounted but `/sd/local` missing | "missing /sd/local — re-provision and reboot" (dev re-clones onto SD) | -| Wi-Fi credentials wrong (build-time env vars) | status shows `Wi-Fi ✗`; dev rebuilds with correct env vars and re-flashes | -| Wi-Fi won't connect | status shows `Wi-Fi ✗`; editing still works; `Ctrl-G` shows "no network" | -| `Ctrl-G` publish fails (auth, network, merge conflict) | status shows reason; local commit is preserved; user can retry — non-fast-forward auto-recovers via pull-then-push and is not surfaced as a failure unless the pull itself conflicts | -| File write fails | status flashes "save failed"; buffer stays dirty | -| Keyboard disconnects | header shows `⌨ ✗`; editing pauses; reconnects automatically | +| SD card mounted but `/sd/repo` missing | "missing /sd/repo — re-provision and reboot" (dev re-clones onto SD) | +| Wi-Fi credentials wrong (build-time env vars) | panel Wi-Fi field shows `✗`; dev rebuilds with correct env vars and re-flashes | +| Wi-Fi won't connect | panel Wi-Fi field shows `✗`; editing still works; `Ctrl-G`'s publish-state field shows "no network" | +| `Ctrl-G` publish fails (auth, network, merge conflict) | panel publish-state field shows the reason; local commit is preserved; user can retry — non-fast-forward auto-recovers via pull-then-push and is not surfaced as a failure unless the pull itself conflicts | +| File write fails | panel save field flashes "save failed"; buffer stays dirty | +| Keyboard disconnects | panel shows `⌨ ✗` (blank once reconnected); editing pauses; reconnects automatically | -No modal dialogs. Errors live in the status line. The editor is never blocked -from accepting keystrokes (except during the partial-refresh frame itself). +No modal dialogs. Errors live in the side panel's ambient-state fields. The +editor is never blocked from accepting keystrokes (except during the +partial-refresh frame itself). ## Out of scope for v0.1 diff --git a/docs/v0.1-mvp-technical.md b/docs/v0.1-mvp-technical.md index 74ce9b0..f61bb3f 100644 --- a/docs/v0.1-mvp-technical.md +++ b/docs/v0.1-mvp-technical.md @@ -135,9 +135,11 @@ Custom widget layer on `embedded-graphics` — Owns the top-ranked engineering functions (latency, refresh-region area) in [qfd.md §3](qfd.md#3-house-of-quality--whats--hows). -- A render request is a `RenderOp { Lines(range) | Status(text) | FullRefresh }`. +- A render request is a `RenderOp { Lines(range) | Panel(field, text) | FullRefresh }`. + The **side panel** is multi-field (Spike 8), so the old single `Status(text)` + op becomes a per-field write; the exact field enum is what Spike 8 decides. - The render thread maintains a shadow of the current screen contents per - region (header / edit area lines / status). + region (**writing-column** lines / **side-panel** fields). - For `Lines(range)`: clear the region's bounding box, draw glyphs, partial refresh that rect. - Glyph cache: rasterised mono font kept in PSRAM; one entry per (codepoint). @@ -165,7 +167,7 @@ Storage split rationale: [qfd.md H13](qfd.md#6-critical-performance-budget). - **Bring-up on `Ctrl-G`.** `wifi_task` transitions `Off → Associating → Connected`. Association timeout: 10 s. If association fails, the local - commit is preserved and the status line shows `⌁ ✗`; no background retry. + commit is preserved and the side panel's Wi-Fi field shows `✗`; no background retry. - **Grace window** of ~90 s after a Publish completes. A `Ctrl-G` inside the window reuses the connection (no second association cost). If no Publish fires before the window expires, the radio tears down. @@ -202,8 +204,8 @@ PSRAM heap during push is a top-3 watched metric — see prefix; the timestamp _is_ the message) - push HEAD to `origin/` - on push failure: `git pull --no-edit` (merge), then retry the push once. - Only surface failure to the status line if the pull conflicts or the - second push also fails. + Only surface failure to the side panel's publish-state field if the pull + conflicts or the second push also fails. - The PAT is loaded into the Authorization header per request; never logged. - The whole sequence is atomic from the user's view — see [`CONTEXT.md` → Publish](../CONTEXT.md#user-facing-actions).