diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-22 12:26:48 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-22 12:26:48 +0200 |
| commit | 597811e780e423120bdbd61a17d78fb7c5733415 (patch) | |
| tree | d98f1a8ad27daf223622523afe028cbf2cb3a6f8 | |
| parent | 0f286b6eda8405f14f18543f6b56bf0bd29c4fc9 (diff) | |
| download | bokf-235e9110b6d4ac137b727328b9dbc1ee6e93049e.tar.gz bokf-235e9110b6d4ac137b727328b9dbc1ee6e93049e.zip | |
docs: TUI interaction model (draft lifecycle, explicit Spara, F2 actions)v0.1.64
| -rw-r--r-- | docs/DECISIONS.md | 19 | ||||
| -rw-r--r-- | docs/STATE.md | 12 | ||||
| -rw-r--r-- | docs/TUI-GUIDELINES.md | 125 |
3 files changed, 154 insertions, 2 deletions
diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 6b3467b..1522842 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -238,6 +238,25 @@ kept verbatim from the STATE.md they were pruned from (2026-09-21). lönebesked prints the employer name in its header too, so the `MAKANDRA AB` outlines are unused at runtime (kept in the generated header). Images remain out of scope. +28. **TUI interaction model (2026-09-22, design)**: settled in the UX + session; the spec is `TUI-GUIDELINES.md` "Interaction model" and is not + implemented yet. Two focus modes: the navigation keys (`Tab`, arrows, + `Home`/`End`, `PgUp`/`PgDn`) only move focus/selection/scroll and never + mutate data, while an active field keeps caret semantics. `Enter` + activates the focused item only when it owns an action (menu item, + action row, opening list row); a plain report has none, so `Enter` does + nothing there — it never saves a whole form, deletes or posts. Entities + have one of three lifecycles: **register** (explicit `Spara` after + validation; drafts live in memory and in + `$XDG_CACHE_HOME/bokf/drafts.json`, marked `<UTKAST>`, deletable from + the list and from the editor), **document** (the form is the draft, + explicit post, immutable) and **settings** (explicit `Spara`; the + per-field autosave is dropped). Every savable form ends with a `Spara` + (or `Posta`) action row; commit is blocked while invalid and a server + error keeps the draft. Screens declare actions in one ordered + `struct tui_action` list that drives the `F2` menu, the accelerator keys + and the footer hints; `F2` only, no `§`; `Ctrl+Enter` is dropped as a + commit key (`F9` and the save action row remain). Pilots on Kunder. ## Completed work formerly listed under "Pending decisions" diff --git a/docs/STATE.md b/docs/STATE.md index cbfd8dd..8bcf83e 100644 --- a/docs/STATE.md +++ b/docs/STATE.md @@ -53,6 +53,13 @@ unit tests and the docs consistency check. employer name there too (it previously used the Comfortaa wordmark). The invoice's `FAKTURA` outline is unchanged; the `MAKANDRA AB` outlines in `src/wordmark.h` are now unused at runtime. +- **TUI interaction model (2026-09-22, design, not implemented)**: the UX + session settled two focus modes, three entity lifecycles, explicit + `Spara` in every savable form and `<UTKAST>` drafts persisted to + `$XDG_CACHE_HOME/bokf/drafts.json` (deletable from both the list and the + editor), plus one `tui_action` registry per screen behind `F2` (no `§`). + Spec in `TUI-GUIDELINES.md` "Interaction model"; decisions in + `DECISIONS.md` #28. Rollout is backlog item 17. - **Mail configuration**: Makandra AB (org 2) has **no** `smtp_*` settings in bokf, so `invoice.send` there is `SMTP_NOT_CONFIGURED` (fine if invoices are sent elsewhere — set them up when wanted). Mock AB (org 1) @@ -121,6 +128,11 @@ None open. Completed items that used to be listed here are archived in 16. Test fixtures (`t_fresh_org()`) + one test file per domain (`tests/core_<domain>.c`) so `--only` stops cascading; pilot with one domain. +17. Interaction-model rollout (spec: `TUI-GUIDELINES.md` "Interaction + model", decisions #28): `struct tui_action` + `F2` menu in the widget + layer, `clients/drafts.[ch]`, the Kunder pilot, then the other + registers; settings get `Spara`; pty scenarios for draft + create/save/delete/reload. Original entries for the struck items are in `docs/DECISIONS.md`. diff --git a/docs/TUI-GUIDELINES.md b/docs/TUI-GUIDELINES.md index c81b3a7..fbd3b75 100644 --- a/docs/TUI-GUIDELINES.md +++ b/docs/TUI-GUIDELINES.md @@ -4,6 +4,123 @@ Rules for the ncurses client so every view behaves the same. When in doubt, copy the behaviour of the voucher list / voucher form; they are the reference implementations. Inspired by Midnight Commander, htop, mutt and calcurse. +The **Interaction model** section is the agreed target (settled 2026-09-22, +`DECISIONS.md` #28) and has not been implemented yet. The sections after it +describe today's widget behaviour and stay authoritative until the widget +layer and the screens are migrated; the interaction model wins where they +conflict as each screen moves over. + +## Interaction model (target, 2026-09-22) + +Every entity has **one lifecycle**, every form **one commit gesture**, and +every action **one declaration**. Nothing is written to the backend +implicitly. + +### Two focus modes + +The cursor is always in one of two modes: + +- **Navigation** — `Tab`/`Shift-Tab`, arrows, `Home`/`End`, `PgUp`/`PgDn` + only move focus, selection or the viewport. They never mutate data, never + save and never run an action. +- **Editing** — the focused field is reverse video with a caret. Inside a + field, `←`/`→`/`Home`/`End` move the caret, `Backspace`/`Del`/`Ctrl+U` + edit the text and `Up`/`Down` leave the field. `Enter` commits the field + and advances; `Esc` restores it (the scratch-copy semantics stay). + +`Enter` activates the focused item **when that item owns an action** (a menu +item, an action row, a list row that opens a detail). A report, a pager or a +blank area has no such item, so `Enter` does nothing there. `Enter` never +saves a whole form, never deletes and never posts. + +### Entity lifecycle + +| Class | Draft | Commit event | `Esc`/`q` | Examples | +|---|---|---|---|---| +| **Register** | in memory + local draft file | explicit `Spara` (the save action row, `F9`) after validation | back; the draft stays | customers, employees, templates, momsregler, org data | +| **Document** | the form is the draft | explicit `Posta` (the save action row, `F9`); immutable once written | back; confirm only when dirty | verifikat, fakturor, löneruns, bokslut | +| **Settings** | an edit buffer until `Spara` | explicit `Spara` | back | faktura/SMTP/serie-inställningar | + +- Every savable form ends with a visible commit action row (`Spara`, or + `Posta` for documents); `Enter` on the focused row commits (it is an item + with an action), `F9` is the accelerator. +- `Ctrl+Enter` is **dropped** as a commit key: gnome-terminal/VTE cannot + send it. The save action row and `F9` are the only commit gestures, which + makes every terminal behave the same. +- Commit is blocked while the data is invalid: the first invalid field is + focused and its error shown. The draft is untouched. A server error at + commit keeps the draft as well; only a successful command removes it. +- `Ctrl+R` (reload) and `Ctrl+C` never flush drafts to the backend; the + client-side draft file makes them survive both, so no work is lost. + +### Drafts + +- Every non-committed edit is a **draft**, held in memory and mirrored to + `$XDG_CACHE_HOME/bokf/drafts.json` (mode 0600, atomic replace) on every + change. This is the "nothing is ever lost" guarantee — `Ctrl+R`, a crash + and `Ctrl+C` included. +- A draft is keyed by `(org, entity, id)`; a new entity gets a temporary id + and shows up in its list immediately. +- Drafts are marked `<UTKAST>` directly after the row number in lists + (`3. <UTKAST> Namn`) and in the editor's frame title (`Kund <UTKAST>`). +- **Delete draft** is an action both on the list row (via `F2`) and inside + the editor, with a confirmation. It removes the memory and file draft and + never touches the backend. A successful commit removes the draft too. +- Drafts are client-local and never synced; another client sees the last + committed value. +- When the backend entity is gone at commit time (`NOT_FOUND`), the editor + offers "spara som ny" or "radera utkast". +- Review point: a draft of an encrypted field (an employee's personnummer) + puts plaintext in the cache file. 0600 is the same protection as the + Bitwarden session file; decide whether such fields are excluded from + drafts. + +### Actions and the `F2` menu + +Screens declare actions, never keys: + +```c +struct tui_action { + const char *id; /* stable, e.g. "customer.archive" */ + const char *label; /* Swedish UI text */ + int key; /* accelerator; 0 = menu only */ + int enabled; /* 1 runnable, 0 dimmed with a reason, -1 heading */ + const char *reason; /* why a disabled action is dim */ +}; +``` + +- One ordered action list per context drives everything: `F2` opens the + `Åtgärder` menu, the same list dispatches the accelerator keys and builds + the footer hint. A key can no longer exist outside the registry. +- The menu is sectioned: **Aktuell rad** (item actions), **Skärmen** (save, + delete draft, attach, …), **Globalt** (`F5` uppdatera, `Ctrl+R` ladda om, + …). Destructive actions are last and still ask for confirmation. +- `Enter` in the menu runs the highlighted action; disabled actions are dim + with their reason (as in `tui_form_action` today); `Esc` closes. The + actions that complex forms hide behind hotkeys today live here unchanged. +- The footer shows at most the two or three most important contextual + actions plus `F2 = fler`. Universal navigation keys (`Tab`, arrows, + `PgUp`/`PgDn`, `Home`/`End`) are not repeated there. +- The session's key decisions: letter accelerators stay, `F9` is the only + commit key (`Ctrl+Enter` is dropped) and `F2` only — no `§` binding (it is + not reliably encodable across terminals). + +### Deltas to implement + +1. `struct tui_action` + `tui_action_menu()` in `clients/tui.[ch]` + (`tui_form_action` is generalized); the pure ordering/dimming/hint logic + is unit-tested in `tests/test_tui.c`. +2. Action lists on `tui_select_list` and `tui_rt`, replacing the per-screen + key branches; hints and dispatch read the same list. +3. `clients/drafts.[ch]`: JSON store, atomic write, dirty tracking, + temporary ids, `<UTKAST>` marking and the delete action. +4. Registers get the draft/`Spara` model first — pilot on **Kunder** — then + the other register screens; settings drop their per-field autosave and + get a `Spara` row. +5. pty scenarios: a new empty entity is a visible `<UTKAST>`; fill + `Spara` + commits and clears it; delete from the list and from the editor; a draft + survives `Ctrl+R`. + ## Session start After login the org picker ("Välj organisation att representera") is always @@ -34,7 +151,7 @@ there. | `Ctrl+F` | Attach a file via the file browser (voucher form and voucher detail) | | `k` | Underlag: link the highlighted attachment to a voucher picked from a list | | `Ctrl+X` | Clear the current row — only inside row editors (never "new") | -| `Ctrl+Enter` | Save/post the current form. Needs xterm `modifyOtherKeys` level 2 or the Kitty keyboard protocol (xterm, kitty, foot, WezTerm); gnome-terminal/VTE sends neither, so the hints advertise `F9`, which works everywhere | +| `Ctrl+Enter` | Save/post the current form. Needs xterm `modifyOtherKeys` level 2 or the Kitty keyboard protocol (xterm, kitty, foot, WezTerm); gnome-terminal/VTE sends neither, so the hints advertise `F9`, which works everywhere. The interaction model drops `Ctrl+Enter` entirely — don't add it to new views | Every screen prints its keys in the footer via `hints()`. If a key exists, the footer shows it; if the footer shows it, the key works. Control keys are @@ -236,10 +353,14 @@ only place that touches ncurses. Rules: ## Adding a view — checklist 1. Data comes from public protocol commands only. -2. Wrap the screen in `frame()`/`hints()`; return `Esc`/`q` to the parent. +2. Wrap the screen in `tui_frame()` and let the widgets carry the footer + hints; return `Esc`/`q` to the parent. 3. Use `tui_menu`/`tui_select_list` instead of writing a new loop; pass `allow_new`/`allow_refresh` so the universal keys apply. 4. Forms use the shared editor (`tui_edit_field`, `tui_prompt_into`, `tui_date_prompt_into`, `tui_amount_prompt_into`) and the row helpers. 5. Support `F5` if the data can change elsewhere. 6. Update `PROTOCOL.md` §8 and this file if you add a new key or interaction. +7. Declare the screen's actions in one `tui_action` list (once the + interaction model is implemented); dispatch, the `F2` menu and the footer + hint all read that list, so a key cannot exist without a visible action. |
