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 /docs/TUI-GUIDELINES.md | |
| parent | 0f286b6eda8405f14f18543f6b56bf0bd29c4fc9 (diff) | |
| download | bokf-597811e780e423120bdbd61a17d78fb7c5733415.tar.gz bokf-597811e780e423120bdbd61a17d78fb7c5733415.zip | |
docs: TUI interaction model (draft lifecycle, explicit Spara, F2 actions)v0.1.64
Diffstat (limited to 'docs/TUI-GUIDELINES.md')
| -rw-r--r-- | docs/TUI-GUIDELINES.md | 125 |
1 files changed, 123 insertions, 2 deletions
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. |
