Compare commits
4 Commits
c12539d933
...
b5274b6e68
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5274b6e68 | ||
|
|
ac56bc4aec | ||
|
|
a848c05afa | ||
|
|
d3fd6f6ed1 |
29
CONTEXT.md
29
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).
|
||||
|
||||
@@ -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`. |
|
||||
@@ -172,7 +172,8 @@ gantt
|
||||
build.rs reads TW_* env vars (Wi-Fi, PAT, author) — v0.1 config path
|
||||
sdkconfig.defaults
|
||||
/hardware BOM, schematic, enclosure (later)
|
||||
/docs ADRs, QFD, roadmap, per-version product + technical specs
|
||||
/docs ADRs, QFD, roadmap, per-version product + technical specs,
|
||||
rendering/UX spike log (spikes.md)
|
||||
CONTEXT.md project glossary — Tracked / Local / Save / Publish, and the
|
||||
principles that fall out of them
|
||||
GLOSSARY.md methodology glossary — the WHAT / Function / Characteristic /
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
10
docs/qfd.md
10
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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 <path>`
|
||||
- [ ] Status line shows file path, dirty flag, mode
|
||||
|
||||
## v0.5 — File palette + multi-file — [ ]
|
||||
|
||||
@@ -55,8 +54,11 @@ Out of scope: Vim, palette, multiple files, branches, conflict handling.
|
||||
- [ ] Open, switch, close buffers (keep ≤ 3 in memory)
|
||||
- [ ] `:e` and palette share the same recent-files list
|
||||
- [ ] `:enew` creates a new file — prompts for scope (tracked vs local)
|
||||
- [ ] Delete a file — removes it from the SD card; for a Tracked file the
|
||||
removal reaches the next `Ctrl-G` Publish's staged set (`git rm` / `add -A`
|
||||
semantics, not plain `git add .`); a Local file is just unlinked
|
||||
- [ ] `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 +80,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 — [ ]
|
||||
|
||||
|
||||
140
docs/spikes.md
Normal file
140
docs/spikes.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Rendering & UX spikes
|
||||
|
||||
> Scope: bench experiments that retire **display/UX** risk, run after the v0.1
|
||||
> hardware bring-up spikes (1–7) in
|
||||
> [`v0.1-mvp-technical.md`](v0.1-mvp-technical.md#hardware-bring-up-order).
|
||||
> Unlike 1–7, these are **not** a v0.1 integration gate, and each feeds a
|
||||
> different release (noted per spike).
|
||||
>
|
||||
> They ride on the `epd::Frame` `DrawTarget` and the Spike 5 partial-refresh
|
||||
> path. Project overview: [`../README.md`](../README.md). Vocabulary:
|
||||
> [`../CONTEXT.md`](../CONTEXT.md). Release sequence: [`roadmap.md`](roadmap.md).
|
||||
|
||||
These prove display/UX risks, not stack risks, so they sit outside the 1–7
|
||||
"prove before integration" gate. **Run Spike 8 first:** it partitions the panel
|
||||
into the writing column and the side panel that the later spikes (scrollbar,
|
||||
line-number gutter, status readouts) draw within and around. Each feeds a
|
||||
different release, but the bench experiment is worth running now to retire the
|
||||
risk early.
|
||||
|
||||
| Spike | Feeds |
|
||||
| ----------------------------------------- | ---------------------------------------------------- |
|
||||
| 8 — Layout: side panel + writing column | v0.1 |
|
||||
| 9 — Boot splash bitmap | v0.1 |
|
||||
| 10 — Dark / light theme | v1.0 |
|
||||
| 11 — Transient panel (help / config) | v0.4 `:` · v0.5 palette · v0.9 settings (mechanism) |
|
||||
| 12 — Scroll position indicator | reading / `View`-mode UX |
|
||||
| 13 — Line-number gutter | v0.2 |
|
||||
| 14 — Multi-file navigation | v0.5 |
|
||||
|
||||
8. **Spike 8 — Layout: side panel + writing column.** Split the 792×272 landscape
|
||||
into a full-height **writing column** (~60 cols, left) and an always-visible
|
||||
**side panel** (~150 px / ~20 cols, right), per the
|
||||
[Screen layout](v0.1-mvp-product.md#screen-layout). Today the editor fills the
|
||||
full 79-col width with text and a cramped 12 px bottom status
|
||||
([`firmware/src/editor.rs`](../firmware/src/editor.rs), `COLS`/`ROWS` +
|
||||
`draw_status`); this spike narrows the text to a legible measure and moves all
|
||||
metadata into the panel, retiring the header/status bands entirely. Two driver
|
||||
facts shape it. First, narrowing does *not* speed up typing: `update_part`
|
||||
already drives both controllers full width, windowed only in Y, because "the
|
||||
waveform time dominates, not the data clock-out"
|
||||
([`firmware/src/epd.rs`](../firmware/src/epd.rs)) — the win is line length and
|
||||
persistent info, not latency. Second, since every partial refresh spans the full
|
||||
width, a keystroke's windowed-Y band repaints the panel's pixels on that row
|
||||
*for free* (redrawn identically), but a panel field that changes on a *different*
|
||||
row than the cursor costs a **second** windowed-Y band — burning the "20 partials
|
||||
→ forced full refresh" ghosting counter twice as fast (render module). That sorts
|
||||
what the panel may hold: static (filename, dirty), event-driven (mode, Wi-Fi,
|
||||
keyboard-disconnect, publish state), and throttled (clock, word count, session)
|
||||
fields only — nothing that repaints per keystroke, so no live cursor *column*.
|
||||
The panel sits entirely in the master half (right of the `x = 396` seam), so its
|
||||
glyphs never split the seam; the writing column still straddles it, as today.
|
||||
Prove: render column + panel; type and confirm the windowed-Y refresh stays a
|
||||
single band (panel redraws in place, no second region); then update a throttled
|
||||
field (word count on a typing pause) and a discrete one (mode flip) and confirm
|
||||
each is one isolated band that forces no extra full refresh. Decides the
|
||||
column/panel split in pixels, the panel's field layout and update cadence, and
|
||||
whether the ghosting counter needs per-region accounting. (Feeds v0.1's screen
|
||||
layout — the writing column + side panel the product doc now draws.)
|
||||
|
||||
9. **Spike 9 — Boot splash bitmap.** Embed the Typoena logo as a 1-bit asset and
|
||||
blit it, with the build tag, at boot before the editor opens. Spike 2 already
|
||||
proved vector + font rendering, so the only new risk is the image-asset
|
||||
pipeline (embedding + blit) and that a large near-full-frame image takes a
|
||||
clean full refresh. Mostly a feature — kept as a spike only to prove the
|
||||
asset path. (Feeds v0.1's "e-ink shows Typoena splash + boot log".)
|
||||
|
||||
10. **Spike 10 — Dark / light theme.** Invert the 1-bit framebuffer (white on
|
||||
black) and refresh. The invert is trivial (XOR at blit); the real unknowns
|
||||
are (a) ghosting and refresh time on a predominantly-black panel — full-black
|
||||
waveforms stress the panel differently — and (b) whether partial-refresh
|
||||
ghosting accumulates faster in dark mode, forcing a more frequent full
|
||||
refresh. Prove: legible inverted text, acceptable ghosting, and
|
||||
partial-refresh latency unchanged from Spike 5. (Feeds v1.0's light/dark
|
||||
theme.)
|
||||
|
||||
11. **Spike 11 — Transient panel (help / config).** Prove a modal full-screen
|
||||
view that swaps in over the editor and restores the buffer exactly on exit —
|
||||
the shared primitive behind a help screen, the v0.9 settings screen, the v0.5
|
||||
file palette (`Ctrl-P`), and the v0.4 `:` command line. The risk is
|
||||
e-ink-specific: entering/leaving with partial refresh leaves ghosting, and
|
||||
the editor viewport underneath must return artifact-free. Prove with a static
|
||||
help screen (trivial content): a full refresh on enter and on exit restores
|
||||
the prior text exactly; measure in/out latency. Decides overlay-box vs.
|
||||
full-screen swap — recommend the swap, since a dimmed partial overlay is both
|
||||
slow and ghost-prone at ~630 ms. Panel *content* (which config keys, help
|
||||
text) is feature work deferred to the owning release; this spike proves only
|
||||
the mechanism.
|
||||
|
||||
12. **Spike 12 — Scroll position indicator.** The buffer already scrolls (`View`
|
||||
mode `j`/`k`/`space`); this spike proves how to *show* position without
|
||||
wrecking latency. Measure two affordances on the bench: (a) a right-edge
|
||||
scrollbar repainted each scroll step, and (b) a compact side-panel readout
|
||||
(`34/128` or `27%`). Risk: the scrollbar is a tall pixel column repainted on
|
||||
every scroll — measure its partial-refresh cost against the small
|
||||
side-panel field, on the windowed-Y path, during held-key scroll. Prove:
|
||||
pick the affordance whose refresh cost is acceptable and confirm it forces no
|
||||
extra full refreshes. (Feeds the reading/`View`-mode UX; the a-vs-b choice is
|
||||
a design decision this spike hands data to.)
|
||||
|
||||
13. **Spike 13 — Line-number gutter.** Draw a fixed-width digit column left of
|
||||
the text area. The v0.2 spec is the hard case: **relative** numbers in
|
||||
Normal mode (current line as its absolute number), **absolute** in Insert.
|
||||
The naive part — reserving columns and drawing digits — is trivial; the
|
||||
e-ink risk is churn. Relative numbering renumbers the *entire visible gutter
|
||||
on every `j`/`k`*, so a single cursor move becomes a partial refresh of a
|
||||
tall digit column, straight into the Spike 5 windowed-Y path and the "20
|
||||
partials → forced full refresh" ghosting counter
|
||||
([`firmware/src/epd.rs`](../firmware/src/epd.rs), render module). Absolute
|
||||
numbering is cheap by comparison — only the rows below an inserted/deleted
|
||||
line renumber. Two more interactions: the gutter steals horizontal columns
|
||||
from the render-time soft-wrap, and wrapped continuation rows have no number
|
||||
(blank vs. tilde — a layout decision). Prove: measure gutter partial-refresh
|
||||
cost for (a) a held-`j` scan that renumbers the whole relative gutter vs.
|
||||
(b) a single-line absolute edit, and confirm neither blows the ghosting
|
||||
budget or forces extra full refreshes. Decides whether relative numbering is
|
||||
viable on this panel or must be gated (absolute-only, or a batched/coalesced
|
||||
gutter repaint). (Feeds v0.2's line-number gutter; genuine new e-ink risk.)
|
||||
|
||||
14. **Spike 14 — Multi-file navigation (open / switch / new / delete).** The
|
||||
panel *mechanism* is already Spike 11 (which names this v0.5 file palette),
|
||||
and filename entry reuses the v0.4 `:` command line — so this spike proves
|
||||
neither, and depends on both. What is genuinely new is **persistence and
|
||||
buffer lifecycle**, not rendering: enumerate a FAT directory across *both*
|
||||
`/sd/repo` and `/sd/local`, keep ≤ 3 ropes resident, and on switch swap the
|
||||
active rope + cursor + soft-wrap after an atomic save-of-current
|
||||
(persistence module). Two sharp edges. **New file** (`:enew`) must prompt
|
||||
for scope — tracked (`/sd/repo`, pushed) vs. local (`/sd/local`, `Ctrl-G`
|
||||
disabled). **Delete** extends v0.5 scope (the roadmap lists `:enew` but not
|
||||
delete): removing the currently-open buffer must close it and fall back to
|
||||
another resident buffer or an empty one, and — the load-bearing check — the
|
||||
FAT unlink must reach the next Publish's *staged* set, which the git
|
||||
module's `git add .`-equivalent staging may not catch for removals (needs
|
||||
`git rm` / `add -A` semantics —
|
||||
[`git` module](v0.1-mvp-technical.md#git--commit--push)). Prove:
|
||||
open/switch/new/delete across both roots with a correct ≤ 3-buffer
|
||||
lifecycle, a delete that propagates into the following commit, and an
|
||||
artifact-free full-refresh swap on every buffer change (leaning on Spike
|
||||
11). Latency is not the risk here; buffer-state and git-staging correctness
|
||||
are. (Feeds v0.5 multi-file, delete included — now authorized in
|
||||
[roadmap](roadmap.md#v05--file-palette--multi-file--) v0.5.)
|
||||
@@ -52,11 +52,11 @@ at build time:
|
||||
a `.env` file gitignored locally) and embedded as `const &'static str`s in
|
||||
the binary.
|
||||
- The dev clones the git working copy onto the SD card from their laptop
|
||||
(`git clone` directly onto the mounted SD), so `/sd/local/notes.md` is
|
||||
(`git clone` directly onto the mounted SD), so `/sd/repo/notes.md` is
|
||||
present at first boot.
|
||||
- Re-configuring = re-build + re-flash. For a single-dev MVP, that's fine.
|
||||
|
||||
If the firmware boots and finds no `/sd/local` or no `/sd/local/notes.md`, it
|
||||
If the firmware boots and finds no `/sd/repo` or no `/sd/repo/notes.md`, it
|
||||
shows a one-line error on the splash and stops. No recovery flow ships in
|
||||
v0.1 — the dev re-mounts the SD and reboots.
|
||||
|
||||
@@ -75,7 +75,7 @@ Auth model rationale: [ADR-005](adr.md#adr-005-auth--https--github-personal-acce
|
||||
Boot (~3 s): Wi-Fi stays off (brought up on demand by `Ctrl-G`)
|
||||
│
|
||||
▼
|
||||
Open /sd/local/notes.md, place cursor at end of file
|
||||
Open /sd/repo/notes.md, place cursor at end of file
|
||||
│
|
||||
▼
|
||||
[EDITING] ← keypresses, partial refresh per edit
|
||||
@@ -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: <reason>`, 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: <reason>`, 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
|
||||
|
||||
|
||||
@@ -103,6 +103,15 @@ spike 4 is the gate for
|
||||
Only after spike 7 do we start integration. Any spike that fails forces a
|
||||
stack decision (e.g. fall back to libgit2; switch to a separate SD SPI bus).
|
||||
|
||||
### Rendering spikes (post-bring-up)
|
||||
|
||||
The display/UX spikes (8–14) — panel layout, boot splash, theme, transient
|
||||
panel, scroll indicator, line-number gutter, multi-file navigation — are **not**
|
||||
part of the v0.1 integration gate and span several releases, so they live in
|
||||
their own log: [`spikes.md`](spikes.md). Spikes **8 (layout)** and **9 (splash)**
|
||||
feed v0.1; the rest feed later releases. Run Spike 8 first — it partitions the
|
||||
panel into the writing column and side panel the others draw within.
|
||||
|
||||
## Module breakdown
|
||||
|
||||
### `editor` — text buffer + cursor
|
||||
@@ -135,9 +144,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 +176,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 +213,8 @@ PSRAM heap during push is a top-3 watched metric — see
|
||||
prefix; the timestamp _is_ the message)
|
||||
- push HEAD to `origin/<current branch>`
|
||||
- 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).
|
||||
|
||||
Reference in New Issue
Block a user