Compare commits

...

4 Commits

Author SHA1 Message Date
Julien Calixte
22aeb449e3 docs(qfd): align with v0.1 refresh, ADR-010, and CONTEXT.md
- Re-anchor W1..W4 source links to the renumbered v0.1 user stories
  (story 1 "First run" was deleted in the v0.1 refresh).
- W2 now points at ADR-010 and CONTEXT.md as well as the product spec.
- W4 reframed from "first-run setup" to "one-shot provisioning, never
  repeated mid-session" so it covers both the v0.1 build-time path and
  the v0.9 on-device path.
- ADR-005 tradeoff row rewritten: "captive-portal form" was wrong after
  the v0.1 refresh; replaced with the binary-as-secret-store framing.
- New ADR-010 row in the tradeoffs table.
- Read-across notes flag that C12 owns ADR-010's implementation but not
  its UX contract, and that C11 LittleFS is unused in v0.1.
- Section 8 picks up three new cross-doc inconsistencies that were
  spotted and fixed during the refresh (commit-message format mismatch,
  captive-portal vs. solo target user, vocabulary leak resolved by
  CONTEXT.md).
2026-05-14 20:40:30 +02:00
Julien Calixte
7055d01e9d docs: refresh v0.1 spec for gct publish flow and dev-only build-time config
Two coupled changes that emerged from one /deep-design session, both touching
the same v0.1 paragraphs:

(1) Align Ctrl-G with the user's existing gct shell alias: git add . ->
    short-circuit if nothing staged -> commit with an ISO-8601 timestamp (no
    wip prefix) -> push -> on push failure, git pull --no-edit then retry.
    Atomic from the user's view. Recorded as ADR-010. The previously-planned
    v0.7 commit-message-prompt item is removed; it contradicts the
    gct/timestamp model.

(2) Replace the v0.1 captive-portal first-run with build-time env-var
    config: build.rs reads TW_* env vars and embeds them as constants. No
    NVS read, no LittleFS mount, no AP mode, no HTTP server. The v0.1
    target user is the dev themselves; the first release usable by non-dev
    users is v0.9, and the v0.9 settings entry is reframed accordingly.
    ADR-005 updated to describe the build-time path and the v0.9 migration.

The two changes share files because the v0.1 spec is one interlocked
document; splitting further would require line-level surgery without
improving auditability.
2026-05-14 20:40:09 +02:00
Julien Calixte
668478aa40 docs: add CONTEXT.md glossary and cross-link from README + ADR log
CONTEXT.md fixes the project's user-facing vocabulary (Tracked, Local,
Save, Publish) and the principles that fall out of those definitions
(the device is a writing tool, not a sync engine; it does no git work
the user did not explicitly request). README's file-scopes section and
repo-layout listing point at the new glossary so a first-time reader
lands on canonical definitions, and the ADR log carries an inbound link
so future ADRs can rely on the same terms.
2026-05-14 20:37:43 +02:00
Julien Calixte
ae555e40a4 docs(adr): split C / C++ option into two ADR-001 rows
The original row collapsed C and C++ together, but they trade differently
on the ESP-IDF target (RAII, generics, binary size, exception story). Split
them so the language-and-runtime decision actually reflects what was
considered.
2026-05-14 20:36:59 +02:00
7 changed files with 315 additions and 102 deletions

102
CONTEXT.md Normal file
View File

@@ -0,0 +1,102 @@
# Typewriter
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.
**Related docs:**
[`README.md`](README.md) — project overview, hardware, macro roadmap.
[`docs/adr.md`](docs/adr.md) — load-bearing decisions; **ADR-010** is the
formal record of the **Publish** UX defined below.
[`docs/qfd.md`](docs/qfd.md) — requirements ↔ functions ↔ components, ranked
by user-facing weight. References the terms in this file as canonical.
[`docs/v0.1-mvp-product.md`](docs/v0.1-mvp-product.md) — the v0.1 product
surface, expressed in this vocabulary.
[`docs/v0.1-mvp-technical.md`](docs/v0.1-mvp-technical.md) — how v0.1 is
built.
[`docs/roadmap.md`](docs/roadmap.md) — per-version scope, where new terms
(e.g. multi-file **Buffer** concepts at v0.5) will enter this glossary.
## Language
### File scopes
**Tracked**:
A file that lives in the device's git working copy and can be published to the
remote. Lives under `/sd/repo/`.
_Avoid_: synced, public, remote, committable.
**Local**:
A file that exists only on the device and can never be published. A
permanently-private scope, not a draft staging area — files are born Local and
stay Local for their lifetime. Lives under `/sd/local/`.
_Avoid_: draft, private, untracked, scratch (these all imply impermanence or
promotability, which is not the model).
### User-facing actions
**Save**:
The act of durably writing the current buffer to the SD card. Triggered by
`Ctrl-S`. Applies to both **Tracked** and **Local** files.
_Avoid_: write, flush, persist (use them only in implementation talk).
**Publish**:
The atomic act of pushing the current state of the entire **Tracked** working
copy to the git remote. Workspace-scoped, not buffer-scoped: a **Publish**
ships every dirty **Tracked** file on the device, not just the one the user is
viewing. Triggered by `Ctrl-G`. Internally: stage all → commit with a
timestamp message → push → on push failure, pull (merge, no-edit) → push
again. Unavailable in **Local**.
_Avoid_: push, commit, sync, upload, git-push (these leak transport details
into user-facing language).
> **Commit** is deliberately *not* a user-facing term. The device authors all
> commit messages itself (ISO-8601 timestamp); the user never sees a commit
> prompt. A **Publish** is the only user-observable unit of "shipping work";
> internal commits are an implementation detail of that.
## Relationships
- A **File** belongs to exactly one scope (**Tracked** or **Local**), fixed at
creation. There is no operation that moves a file between scopes.
- **Save** applies to any **File**; **Publish** applies only to **Tracked**.
- A single **Publish** is atomic from the user's view: a Wi-Fi failure or
remote divergence surfaces as a single retry-able outcome, not as a multi-
step progression the user has to reason about.
## Example dialogue
> **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."
> **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
> built-in operation."
> **Dev:** "And if the remote has changed since I last pulled — does
> **Publish** fail?"
> **Domain expert:** "It pulls (merge, no edit) and pushes again. From the
> user's view it's one action with one outcome — success or retry."
## Principles
- **The device is a writing tool, not a sync engine.** Every git operation is
the direct, in-session consequence of a `Ctrl-G` the user pressed. The
device does not auto-publish, auto-pull, retry-on-boot, or otherwise
reconcile remote state in the background. If a previous **Publish** ended
mid-flight and left a local commit unpushed, the next user-initiated
**Publish** picks it up; until then, the device is silent about it.
- **No state the user didn't ask for.** No banners about pending work, no
prompts about divergence, no "did you mean to publish" warnings. The status
line reflects the *current* action's outcome, nothing else.
## Flagged ambiguities
- "Local" was initially ambiguous between (a) a draft pen that gets promoted,
(b) a permanently-private scope, (c) a second git repo, (d) `.gitignore`'d
files inside the working copy. Resolved: (b). Each **File**'s scope is fixed
at creation; there is no promotion operation.
- "Commit" was used loosely across early docs as if it were a user-facing
action. Resolved: it is not. The user has **Save** and **Publish**. Commits
are an internal unit inside **Publish**, never authored or named by the
user.

View File

@@ -15,14 +15,17 @@ edits Markdown files, and (optionally) pushes them to a git remote (GitHub
first) over Wi-Fi. No browser, no notifications, no apps. Open lid → write →
push (or don't) → close lid.
Two file scopes coexist on the SD card:
Two file scopes coexist on the SD card — formal definitions in
[`CONTEXT.md`](CONTEXT.md):
- **Tracked** — lives in the git working copy, gets committed and pushed.
- **Local** — never leaves the device. Drafts, journal entries, scratch, things
that aren't ready or aren't anyone else's business.
- **Tracked** — lives in the git working copy, gets **Published** when the
user presses `Ctrl-G`.
- **Local** — never leaves the device. Permanently-private: journal entries,
scratch, things that aren't anyone else's business. There is no "promote
to Tracked" gesture — scope is fixed at file creation.
Same editor, same keymap; the difference is just whether `Ctrl-G` (commit &
push) is offered.
Same editor, same keymap; the difference is just whether `Ctrl-G` (publish to
the remote) is offered.
---
@@ -143,7 +146,7 @@ gantt
| [v0.4](docs/roadmap.md#v04--visual-mode--ex-commands--) | Visual + ex | `v V`, `:w :q :e`, status 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`, commit msg. |
| [v0.7](docs/roadmap.md#v07--search--better-git--) | Search + git | `/`, `:Gpull`, `:Gbranch`. |
| [v0.8](docs/roadmap.md#v08--power-battery--sleep--) | Power | 18650 + sleep + lid switch. |
| [v0.9](docs/roadmap.md#v09--robustness--) | Robustness | Crash-safe writes, reconnect, settings. |
| [v1.0](docs/roadmap.md#v10--polish--) | Polish | Boot ≤ 3 s, fonts, enclosure, guide. |
@@ -162,10 +165,12 @@ gantt
git/ gitoxide wrapper, auth
usb/ TinyUSB host glue
fs/ SD + NVS
build.rs
build.rs reads TW_* env vars (Wi-Fi, PAT, author) — v0.1 config path
sdkconfig.defaults
/hardware BOM, schematic, enclosure (later)
/docs ADRs, power measurements
/docs ADRs, QFD, roadmap, per-version product + technical specs
CONTEXT.md project glossary — Tracked / Local / Save / Publish, and the
principles that fall out of them
package.json pnpm + oxfmt — formatting toolchain for docs/JSON
(companions: pnpm-lock.yaml, .oxfmtrc.json, .node-version)
```

View File

@@ -9,6 +9,9 @@ Format inspired by Michael Nygard's ADR template, kept short on purpose.
**Related docs:**
[`../README.md`](../README.md) — project overview, hardware table, macro plan.
[`../CONTEXT.md`](../CONTEXT.md) — project glossary: **Tracked**, **Local**,
**Save**, **Publish**, plus the principles ("writing tool, not sync engine")
that constrain ADR-010 specifically.
[`roadmap.md`](roadmap.md) — per-version scope (v0.1 → v1.x).
[`v0.1-mvp-product.md`](v0.1-mvp-product.md) — what the v0.1 device must do.
[`v0.1-mvp-technical.md`](v0.1-mvp-technical.md) — how v0.1 is built.
@@ -33,7 +36,8 @@ up across nine downstream releases.
| Option | Pros | Cons |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| **C / C++ on ESP-IDF** | Reference platform, every peripheral has a driver, fastest path to first pixel. | Refactoring at scale is painful; memory safety is on you. |
| **C on ESP-IDF (no Arduino)** | Reference platform on the bare native SDK; every peripheral has a driver; smallest binary of the C-family options; no C++ runtime / exceptions / RTTI to reason about. | All memory safety on you; no RAII for resource cleanup; no generics so widget / state code gets repetitive; refactoring at scale is painful. |
| **C++ on ESP-IDF** | Same peripheral coverage as C; RAII, templates, and `std::` containers ease widget / state code; mature in the ESP-IDF examples. | Exception / RTTI story on embedded is messy; ABI / linker surprises; memory safety still on you; binary larger than plain C. |
| **Rust on `esp-idf-rs` (std)** | First-class Espressif-sponsored Rust support; `std` gives heap / threads / VFS / mbedtls; can use the broader Rust ecosystem (`gitoxide`, `ropey`, `embedded-graphics`). | Larger binary than `no_std`; longer build times; some `unsafe` at FFI seams. |
| **Rust on `esp-hal` (no_std)** | Smallest binary, most "pure" embedded experience. | No `std` = no off-the-shelf git, no easy TLS, would re-implement a lot of plumbing. |
| **Gleam + Shore on AtomVM** | Beautiful language, the user's stated preference. | BEAM on ESP32 is memory-hungry; no bindings for USB host, e-ink, SD, TLS, git in that ecosystem. Two research projects stacked. |
@@ -207,14 +211,17 @@ on-device, and reasonably secure for a personal appliance.
### Decision
**HTTPS + PAT.** Stored in internal LittleFS, encrypted with a key derived
from the chip's eFuse so a stolen SD card alone is not enough. Captive
portal accepts the PAT during first-run setup.
**HTTPS + PAT.** In v0.1 the PAT (and all other config) is compiled into the
firmware binary via build-time env vars — the dev's-only-user model makes the
binary-as-secret-store acceptable. From v0.9 onward, the PAT moves to
encrypted LittleFS with a key derived from the chip's eFuse, so a stolen SD
card alone is not enough.
### Consequences
- The user must generate a PAT with `repo` scope. Documented in
[v0.1 product → first-run flow](v0.1-mvp-product.md#first-run-provisioning-flow).
- The user (= dev, in v0.1) must generate a PAT with `repo` scope and supply
it as a build-time env var. Provisioning is build-time only — see
[v0.1 product → provisioning](v0.1-mvp-product.md#provisioning-build-time-dev-only).
- PAT is never logged. Validated in code review.
- Rotation in v0.1 = wipe NVS and re-run setup. Proper rotation UI is v0.9
— see [roadmap → v0.9](roadmap.md#v09--robustness--).
@@ -367,6 +374,54 @@ if TinyUSB host turns out unstable
---
## ADR-010: Publish UX — atomic `Ctrl-G`, auto-timestamp commit message, no user prompt
**Status:** Accepted — 2026-05-14
**Scope:** Whole project, all releases.
### Context
The device needs an action that ships writing to the git remote. Most
git-using tools expose `commit` and `push` as distinct user gestures, often
with a commit-message prompt. The device's actual user (= the author of this
firmware) already uses the [`gct` shell alias](../CONTEXT.md#user-facing-actions)
for their own writing: `git add . && git commit -m "<timestamp>" && git push`,
with a `git pull --no-edit` fallback when the push fails non-fast-forward.
`gct` is the established workflow; the typewriter mirrors it.
### Options considered
| Option | Pros | Cons |
| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Three separate gestures** (save / commit / push) | Maximally git-native; user has fine control. | Three keys to remember, three failure modes to surface, three concepts in the user's head. Wrong shape for an appliance whose job is to remove ceremony. |
| **One gesture, prompt for message** (`Ctrl-G` → modal asking for message → commit → push) | Conventional "publish" pattern; each commit is named. | A modal prompt on e-ink is hostile (latency, full refresh); the user's actual workflow (`gct`) explicitly avoids authoring messages; messages would be noise (`"updated notes"` × 1000). |
| **One gesture, auto-timestamp message** (`Ctrl-G` mirrors `gct`) | Matches the user's real workflow; one key, one outcome; no prompts, no modes, no decisions in the writing path. | Commit history is timestamp-noise (useless for code archaeology); a future reader will wonder where the commit messages went; locks in a UX assumption that's hard to undo without breaking muscle memory. |
### Decision
**One gesture, auto-timestamp message, atomic from the user's view.** `Ctrl-G`
runs the full `gct` sequence (stage all → short-circuit if nothing staged →
commit with ISO-8601 timestamp → push → on push failure, `pull --no-edit` then
retry). Failure surfaces as a single retry-able outcome in the status line.
### Consequences
- The user's vocabulary collapses to **Save** and **Publish**;
[`CONTEXT.md`](../CONTEXT.md#user-facing-actions) pins this — *commit* is
not a user-facing term.
- Commit history is a stream of timestamps. The device is a writing tool, not
a code repository — the history is here for recoverability, not narrative.
- The pull-merge-retry path means the device may author merge commits on the
user's behalf, with git's default merge message. Acceptable: the user
doesn't read commit history from the device anyway.
- The previously-planned "commit message prompt" item in v0.7 has been
removed from the roadmap.
- Reversing this later (introducing message prompts) would change the
semantics of `Ctrl-G` and break the user's muscle memory. Hard-to-reverse
by design.
---
## How to add a new ADR
1. Append a new `## ADR-NNN: <title>` section to this file.

View File

@@ -9,7 +9,9 @@ Scope: v0.1 MVP — see
[`v0.1-mvp-product.md`](v0.1-mvp-product.md) for user-facing scope and
[`v0.1-mvp-technical.md`](v0.1-mvp-technical.md) for implementation —
with the v0.2v1.0 trajectory ([README](../README.md),
[roadmap](roadmap.md)) in mind so we don't paint into a corner.
[roadmap](roadmap.md)) in mind so we don't paint into a corner. Terminology
(e.g. **Tracked**, **Local**, **Save**, **Publish**) follows the project
glossary at [`../CONTEXT.md`](../CONTEXT.md).
Format inspired by the classic House of Quality, kept compact. Strength
weights: **9** strong, **3** medium, **1** weak, blank none.
@@ -23,10 +25,10 @@ What a user (= me) values about the device, with importance weights on a
| ID | Requirement | Weight | Source |
| --- | ------------------------------------------------------- | :----: | ------------------------------------------------------------------------------------------------------------------ |
| W1 | Sub-second visible response to typing | 10 | [product → story 2](v0.1-mvp-product.md#user-stories), [README → UX](../README.md#ux-boundaries-set-by-the-medium) |
| W2 | `Ctrl-G` reliably lands a commit on GitHub | 9 | [product → story 4](v0.1-mvp-product.md#user-stories) |
| W3 | Pulling power never corrupts the file | 10 | [product → story 5](v0.1-mvp-product.md#user-stories), [acceptance](v0.1-mvp-product.md#acceptance-criteria) |
| W4 | One-shot first-run setup, never repeated | 7 | [product → story 1](v0.1-mvp-product.md#user-stories) |
| W1 | Sub-second visible response to typing | 10 | [product → Write](v0.1-mvp-product.md#user-stories), [README → UX](../README.md#ux-boundaries-set-by-the-medium) |
| W2 | `Ctrl-G` reliably **Publishes** to the remote | 9 | [product → Publish](v0.1-mvp-product.md#user-stories), [ADR-010](adr.md#adr-010-publish-ux--atomic-ctrl-g-auto-timestamp-commit-message-no-user-prompt), [CONTEXT → Publish](../CONTEXT.md#user-facing-actions) |
| W3 | Pulling power never corrupts the file | 10 | [product → Recover](v0.1-mvp-product.md#user-stories), [acceptance](v0.1-mvp-product.md#acceptance-criteria) |
| W4 | One-shot provisioning, never repeated mid-session | 7 | [product → Provisioning](v0.1-mvp-product.md#provisioning-build-time-dev-only), [roadmap → v0.9](roadmap.md#v09--robustness--) |
| W5 | Quick boot to a writing cursor | 6 | [product → acceptance](v0.1-mvp-product.md#acceptance-criteria) (≤ 5 s) |
| W6 | Long sessions without crash / lag / drift | 9 | [product → acceptance](v0.1-mvp-product.md#acceptance-criteria) (1 h soak) |
| W7 | Distraction-free, single-purpose surface | 8 | [README → vision](../README.md#vision) |
@@ -218,6 +220,15 @@ Function-to-component matrix (9 strong / 3 medium / 1 weak):
touch it. That's why ADR-004 includes a kill-switch (fall back to
`libgit2-sys` if spike 7 fails). It's also why H9 sits in the top three
priorities — `gitoxide`'s memory profile is the unknown.
[ADR-010](adr.md#adr-010-publish-ux--atomic-ctrl-g-auto-timestamp-commit-message-no-user-prompt)
pins the *shape* of the publish sequence (the `gct` flow); C12 is just the
library that implements it. Changing ADR-010 doesn't change C12's column,
but changing C12 (the kill-switch) does not change ADR-010's user
contract.
- **C11** (LittleFS) is unused in v0.1 — config is build-time. Its non-zero
cells in the matrix describe the v0.9+ shape per
[ADR-007](adr.md#adr-007-storage-split--fat-on-sd-for-working-copy-littlefs-on-flash-for-config),
not v0.1 reality.
- **C2** (std runtime) sits underneath almost everything, but it's the
_enabler_ (H4 boot, H10 binary, H12 Wi-Fi) rather than the bottleneck.
Reversing ADR-001 would force re-deciding ADR-004, ADR-005, ADR-006,
@@ -258,11 +269,12 @@ Plain-language summary of what we accepted in exchange for what.
| Custom widget layer over Ratatui | Dirty-rects aligned to e-ink regions; 200 KB binary back | 500 LoC we own and maintain | ADR-002 |
| 5.79" strip panel over 7.5" page or 10.3" reader | 27 KB framebuffer, fast partial refresh, "Freewrite" UX | Only ~11 visible lines | ADR-003 |
| `gitoxide` over `libgit2-sys` | Pure Rust, modular, no FFI cross-compile pain | Smart-HTTP path is newer; PSRAM profile unproven (spike 7) | ADR-004 |
| HTTPS + PAT over OAuth device-flow or SSH | First-run UX fits in a captive-portal form | Long-lived secret on device; manual rotation in v0.1 | ADR-005 |
| HTTPS + PAT over OAuth device-flow or SSH | Simplest auth that `gitoxide` smart-HTTP already supports | Long-lived secret on device; in v0.1 the PAT is compiled into the binary (dev-only target user makes this acceptable); v0.9 moves it to encrypted NVS | ADR-005 |
| `std::thread` over `embassy` or `tokio` | Boring, debuggable, real stack traces; no exec to tune | ~76 KB total stack across 5 tasks | ADR-006 |
| FAT-on-SD + LittleFS-on-flash split | Desktop can read SD; config survives SD reformat | Two filesystems to manage; FAT's power-loss weakness mitigated by atomic-rename | ADR-007 |
| Wall power for v0.1, battery deferred | Measure real draw before sizing the cell | Tethered MVP; not the final aesthetic | ADR-008 |
| USB host (TinyUSB) over BLE-HID | No radio contention with Wi-Fi during push; keyboard powered from the device | One more USB connector on enclosure | ADR-009 |
| Atomic `Ctrl-G` + auto-timestamp commit message | One key, one outcome; matches the user's existing `gct` workflow; no modal prompt to slow H1 latency | Commit history is timestamp noise; the device may author merge commits the user never sees; reversal would break muscle memory | ADR-010 |
### Conflicts left explicitly _unresolved_ by v0.1
@@ -290,12 +302,31 @@ These are the live tensions we are watching, not deciding harder:
cross-reference the tech doc. The 76 KB figure still fits comfortably
in the ESP32-S3's 512 KB internal SRAM, so no design change — just
documentation accuracy.
- **Commit-message format triple-mismatch.** README said `git commit -m
"wip"`, the v0.1 product doc said `"wip <timestamp>"`, and the user's
actual shell alias (`gct` / `git-commit-timestamp`) uses a pure ISO-8601
timestamp with no `wip` prefix. Resolved by aligning all docs on `gct`
and recording the decision as
[ADR-010](adr.md#adr-010-publish-ux--atomic-ctrl-g-auto-timestamp-commit-message-no-user-prompt).
Pulled the v0.7 roadmap item "Commit message prompt instead of hard-coded
`wip`" — it's now contradicted by ADR-010 and removed.
- **First-run flow vs. target user.** The v0.1 product doc described a
captive-portal first-run, but the same doc names the v0.1 target user as
the dev themselves ("Me. Solo."). Provisioning a solo-dev device through
a captive portal is ceremony without a user. Resolved by switching v0.1
to build-time env-var config (no NVS, no LittleFS, no AP mode); on-device
provisioning is the v0.9 release that introduces non-dev users. Touches
ADR-005, ADR-007, the v0.1 product + technical docs, and the v0.9
roadmap entry.
- **Vocabulary leak.** Earlier docs used "commit" and "push" as if they
were distinct user actions; the gct/ADR-010 model collapses them into a
single user-facing **Publish**. Resolved by introducing
[`CONTEXT.md`](../CONTEXT.md) as the canonical glossary; user-facing text
now uses **Save** and **Publish** only.
No other cross-doc contradictions found between `adr.md`, README, and the
v0.1 product/technical docs at the time of writing. 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 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.
---

View File

@@ -17,9 +17,11 @@ The minimum thing that justifies the hardware existing. Full design:
- [ ] Insert-only editing (no modes yet), backspace, enter, arrow keys
- [ ] Line wrap, no line numbers yet
- [ ] Save on `Ctrl-S` → SD
- [ ] Wi-Fi credentials via captive portal on first boot, stored in NVS
- [ ] `Ctrl-G` runs: `git add notes.md && git commit -m "wip" && git push` to a
pre-configured remote, using a PAT entered during setup
- [ ] Wi-Fi credentials + remote URL + PAT + author baked into the binary at
build time via env vars (no NVS, no on-device provisioning UI in v0.1)
- [ ] `Ctrl-G` runs: `git add .` → commit with an ISO-8601 timestamp message →
`git push`; on push failure, `git pull --no-edit` then retry the push
(no-op short-circuit when nothing is staged). PAT from first-run setup.
- [ ] Partial refresh on edits; full refresh on save
Out of scope: Vim, palette, multiple files, branches, conflict handling.
@@ -52,6 +54,9 @@ 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
more than one dirty Tracked file (e.g. `"publishing 3 files: abc1234"`),
so workspace-scoped behaviour stays visible to the user
## v0.6 — Markdown affordances — [ ]
@@ -65,7 +70,6 @@ Out of scope: Vim, palette, multiple files, branches, conflict handling.
- [ ] `/` forward search, `n N`
- [ ] `:Gpull` (fetch + fast-forward only; refuse on conflict and surface it)
- [ ] `:Gbranch` to switch branches; refuse with dirty tree
- [ ] Commit message prompt instead of hard-coded `"wip"`
## v0.8 — Power: battery + sleep — [ ]
@@ -81,7 +85,9 @@ Out of scope: Vim, palette, multiple files, branches, conflict handling.
- [ ] Recover from interrupted push (re-attempt on next save)
- [ ] SD card removal / reinsert handling
- [ ] Wi-Fi reconnect with backoff
- [ ] Settings screen: SSID, PAT rotation, default remote, commit author
- [ ] On-device provisioning + settings screen: SSID, PAT rotation, default
remote, commit author (replaces the v0.1 dev-only NVS-flashing path —
first release usable by someone who is not the firmware author)
## v1.0 — Polish — [ ]

View File

@@ -13,7 +13,7 @@
## One-line summary
Boot the device, type into one file, hit `Ctrl-S` to save, hit `Ctrl-G` to
push it to GitHub. That's the entire product surface.
publish it to GitHub. That's the entire product surface.
## Target user
@@ -22,62 +22,49 @@ the loop closes end to end.
## User stories
1. **First run.** I plug in the device for the first time. It walks me through
Wi-Fi setup, asks for a GitHub Personal Access Token and a repository URL,
and stores them. I never want to do this step twice.
2. **Write.** I open the lid (= plug in power). Within a few seconds the
1. **Write.** I open the lid (= plug in power). Within a few seconds the
screen shows my `notes.md` file. I start typing. What I type appears on
screen with sub-second latency.
3. **Save.** I hit `Ctrl-S`. The file on SD is updated. A short status flash
2. **Save.** I hit `Ctrl-S`. The file on SD is updated. A short status flash
confirms it.
4. **Push.** I hit `Ctrl-G`. The device commits and pushes to GitHub. The
status line shows progress; if it succeeds, the commit hash is briefly
shown. If it fails, I see why.
3. **Publish.** I hit `Ctrl-G`. The device ships my latest changes to GitHub
atomically — if the remote has moved on, it merges in (no-edit) and pushes
again, so I only see one outcome. If it succeeds, a short hash is briefly
shown. If it fails, I see why and I can retry.
5. **Recover.** Power gets pulled mid-edit. I plug it back in. My file is at
4. **Recover.** Power gets pulled mid-edit. I plug it back in. My file is at
the last saved state — I lose unsaved changes, but the file isn't
corrupted.
That's it. No other stories ship in v0.1.
## First-run provisioning flow
## Provisioning (build-time, dev-only)
```
[POWER ON, no config in NVS]
Splash: "typewriter v0.1 — first-run setup"
Wi-Fi: device starts AP "typewriter-setup"
Screen shows: SSID, password, http://192.168.4.1
User connects from phone/laptop, enters:
• home Wi-Fi SSID + password
• git remote URL (HTTPS)
• GitHub username
• GitHub PAT (with `repo` scope)
• commit author name + email
Device validates: connects to Wi-Fi, clones the repo into /sd/repo
Screen: "ready. opening notes.md"
[STEADY STATE]
```
v0.1 has **no on-device provisioning** — no captive portal, no in-editor
wizard, no NVS config file, no LittleFS config. The device is the dev's own
machine (see [Target user](#target-user)); config is baked into the firmware
at build time:
If the clone fails (bad PAT, no `notes.md` in the repo, etc.), the screen
shows the error and offers retry. The repo is expected to already exist on
GitHub and contain (or be willing to contain) `notes.md` — v0.1 does not
create remote repos. Auth model rationale:
[ADR-005](adr.md#adr-005-auth--https--github-personal-access-token).
- Wi-Fi SSID + password, git remote URL, GitHub username, PAT, and commit
author name + email are read by `build.rs` from environment variables (or
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/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/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.
On-device provisioning UI (Wi-Fi captive portal, PAT rotation, settings
screen) lands in v0.9 when the first non-dev user becomes a concern; at that
point the constants migrate to NVS-on-LittleFS per
[ADR-007](adr.md#adr-007-storage-split--fat-on-sd-for-working-copy-littlefs-on-flash-for-config).
Auth model rationale: [ADR-005](adr.md#adr-005-auth--https--github-personal-access-token).
## Steady-state flow
@@ -96,10 +83,12 @@ create remote repos. Auth model rationale:
├── Ctrl-S → write file, full refresh, status: "saved 14:02"
└── Ctrl-G → save if dirty, then:
git add notes.md
git commit -m "wip <timestamp>"
git add .
(if nothing staged: status "nothing to publish", stop)
git commit -m "<ISO-8601 timestamp>"
git push
status: "pushed abc1234" or "push failed: <reason>"
└─ on failure: git pull --no-edit, retry push
status: "published abc1234" or "publish failed: <reason>"
```
## Screen layout
@@ -150,9 +139,10 @@ don't fight it.
| Failure | What the user sees |
| ----------------------------------------------------- | ------------------------------------------------------------------------ |
| No SD card on boot | "no SD card — insert one and reboot" |
| SD card mounted but `/sd/repo` missing | drop back into first-run setup |
| 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) | 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` push rejected (auth, non-fast-forward, etc.) | status shows reason; commit stays local; user can retry |
| `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 |
@@ -169,11 +159,13 @@ Explicitly **not** in this release, to keep it shippable:
- Branches. Always on whatever branch was cloned (default).
- `git pull`. Manual recovery if the remote diverges.
- Conflict handling.
- Commit message editing — always `"wip <timestamp>"`.
- Commit message editing — always an ISO-8601 timestamp, no user prompt ever
(see [`gct`](../CONTEXT.md#user-facing-actions)).
- Battery. Wall power only.
- Sleep / lid-close behaviour.
- Font selection.
- Settings UI after first-run (re-flash or wipe NVS to reconfigure).
- On-device provisioning UI (captive portal, wizard, settings screen). v0.1 is
provisioned out-of-band from the dev's laptop; users are a v0.9 concern.
- Markdown rendering (headings, bold, etc.) — plain text rendering of MD.
- Search.
@@ -184,14 +176,14 @@ to an engineering function with a measured target in
[qfd.md §6](qfd.md#6-critical-performance-budget); that's the
place to check before declaring an item done.
- [ ] First-run setup completes in under 5 minutes including typing in a PAT.
- [ ] After a cold boot with valid config, cursor is ready in ≤ 5 s.
- [ ] After a cold boot with valid pre-flashed config, cursor is ready in ≤ 5 s.
- [ ] Typing a 1000-word paragraph never drops a keystroke and never lags
more than 300 ms behind the keyboard.
- [ ] `Ctrl-S` durably writes the file (verified by power-cycling immediately
after the status confirms save — file is intact).
- [ ] `Ctrl-G` produces a commit visible on github.com within 30 s on a
typical home Wi-Fi.
typical home Wi-Fi (including the pull-then-retry path when the remote
has moved on since the last publish).
- [ ] Pulling power during typing never corrupts the file; the previous saved
state is recoverable.
- [ ] One hour of continuous typing without crash, freeze, or memory

View File

@@ -45,7 +45,7 @@ blocked by I/O.
| Task | Core | Stack | Responsibility |
| ------------- | ---- | ----- | -------------------------------------------------------- |
| `usb_task` | 0 | 8 KB | TinyUSB host loop, decode HID reports, post `KeyEvent`s |
| `wifi_task` | 0 | 8 KB | Provisioning AP or station mode; expose status |
| `wifi_task` | 0 | 8 KB | Station mode only (no provisioning AP); expose status |
| `ui_task` | 1 | 16 KB | Consume `KeyEvent`s, mutate editor state, enqueue render |
| `render_task` | 1 | 12 KB | Drain render queue, do partial/full refresh on EPD |
| `git_task` | 1 | 32 KB | Triggered by `Ctrl-G`; runs gitoxide commit + push |
@@ -60,11 +60,10 @@ read snapshot (render diff).
```
1. ROM → bootloader → app_main
2. Init PSRAM allocator, set as default for large alloc
3. Mount LittleFS on internal flash → read /nvs/config.toml
├─ no config → enter PROVISIONING mode
└─ config OK → continue
4. Mount FAT on SD → verify /sd/repo exists
├─ missing → enter PROVISIONING mode (clone)
3. Config is compiled into the binary (`build.rs` reads env vars) — no
filesystem read needed for it. LittleFS is unused in v0.1.
4. Mount FAT on SD → verify /sd/repo and /sd/repo/notes.md exist
├─ missing → fatal: "missing /sd/repo — re-mount SD and reboot"
└─ present → continue
5. Init SPI bus (shared: EPD + SD on different CS)
6. Init EPD, full refresh: splash + boot log
@@ -125,7 +124,7 @@ stack decision (e.g. fall back to libgit2; switch to a separate SD SPI bus).
| `←` `→` `↑` `↓` | move cursor (visual lines for ↑↓) |
| `Home` `End` | line start / end |
| `Ctrl-S` | save |
| `Ctrl-G` | save (if dirty) + commit + push |
| `Ctrl-G` | publish (save → stage → commit push, with pull-and-retry on remote divergence) |
Anything else is ignored in v0.1.
@@ -178,21 +177,44 @@ PSRAM heap during push is a top-3 watched metric — see
- `gix` with the smart-HTTP transport backed by `esp-idf` mbedtls (via a
custom transport impl, or `gix-transport` with `reqwest`+`rustls-mbedtls`
if that path is cleaner — decided in spike 7).
- Operations needed in v0.1:
- Operations needed in v0.1 (the [`gct` shell function](../CONTEXT.md#user-facing-actions) is the reference):
- `gix::open` the existing working copy at `/sd/repo`
- stage `notes.md` (`gix::index` add)
- commit with author from config, message `"wip <ISO-8601 timestamp>"`
- stage everything under the working copy (`git add .` equivalent)
- short-circuit return if nothing is staged — status: "nothing to publish"
- commit with author from config, message `"<ISO-8601 timestamp>"` (no `wip`
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.
- The PAT is loaded into the Authorization header per request; never logged.
- Push errors propagate as a string back to the status line.
- The whole sequence is atomic from the user's view — see
[`CONTEXT.md` → Publish](../CONTEXT.md#user-facing-actions).
### `provisioning`first-run wizard
### Provisioning — build-time only (no module on device)
- Triggered when `config.toml` is absent OR `/sd/repo` is absent.
- Captive portal posts a JSON blob; device validates by:
1. Connecting to the supplied Wi-Fi credentials.
2. Cloning the supplied repo URL into `/sd/repo` using the supplied PAT.
- Only on both successes does it persist config and reboot into steady state.
v0.1 has **no provisioning module, no NVS config, no LittleFS mount**. All
config (Wi-Fi creds, remote URL, GitHub user, PAT, commit author) is supplied
as environment variables at build time:
```sh
export TW_WIFI_SSID=... TW_WIFI_PASS=...
export TW_REMOTE_URL=... TW_GH_USER=... TW_PAT=...
export TW_AUTHOR_NAME=... TW_AUTHOR_EMAIL=...
cargo espflash --release
```
`build.rs` reads these (or fails the build) and emits constants the runtime
references directly. The `.env` file used to source these is gitignored.
The git working copy is set up out-of-band: the dev clones the remote onto
the mounted SD card from their laptop. There is no "first clone on device"
in v0.1.
Net savings vs. an on-device wizard: ~300500 LoC of firmware (HTTP server,
captive AP, form parsing, validation state machine, NVS read/write, TOML
parser). On-device provisioning + NVS-backed config land in v0.9 when
non-dev users enter the picture.
## Memory plan