diff options
Diffstat (limited to 'docs/TUI-GUIDELINES.md')
| -rw-r--r-- | docs/TUI-GUIDELINES.md | 131 |
1 files changed, 127 insertions, 4 deletions
diff --git a/docs/TUI-GUIDELINES.md b/docs/TUI-GUIDELINES.md index 190a5c5..1c201aa 100644 --- a/docs/TUI-GUIDELINES.md +++ b/docs/TUI-GUIDELINES.md @@ -4,6 +4,124 @@ 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). The **Kunder** pilot is implemented: drafts in +`$XDG_CACHE_HOME/bokf/drafts.json`, `<UTKAST>` marking, the explicit `Spara` +row, the `F2` action menu and draft deletion work there. Every other screen +still follows the sections below; 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). + +### Implementation status + +1. Done: `struct tui_action`, `tui_action_menu()` and `tui_action_hint()` in + `clients/tui.[ch]`, unit-tested in `tests/test_tui.c`. +2. Partly done: the Kunder list builds its `F2` actions in a key hook and + appends `F2 = åtgärder` to the footer via `tui_list_hint_extra()`; other + lists have no actions yet, and `tui_rt` still uses its own key branches. +3. Done for Kunder: `clients/drafts.[ch]` (JSON store, atomic 0600 write, + temporary ids, `<UTKAST>` marking, delete action), unit-tested. +4. Done for Kunder: draft/`Spara` model with the `Spara` action row, + `F2` menu and draft deletion from both the list and the editor. The + other register screens and the settings forms (explicit `Spara`, no + per-field autosave) are next. +5. Done for Kunder: pty scenarios `customer-draft` and + `customer-draft-save` cover create, edit, `<UTKAST>`, delete and save. + ## Session start After login the org picker ("Välj organisation att representera") is always @@ -29,11 +147,12 @@ there. | `a` | Add/upload (Underlag) | | `c` | Correct (voucher detail) | | `d` | Delete/arkivera the selected row (only where the action exists; asks for confirmation) | -| `f` | Voucher detail: list the voucher's underlag — Enter fetches, `d` removes the link (asks first). Underlag: Enter fetches | +| `f` | Voucher detail: list the voucher's underlag — Enter opens Granska (text in a pager, PDFs/images in the desktop viewer) or Ladda ned…, `d` removes the link (asks first). Underlag: Enter does the same | +| `u` / `b` | Faktura detail: `u` duplicates the invoice into a new draft (same rows, dates reset to today), `b` (unpaid invoices) prefills and posts the payment voucher, then marks the invoice paid | | `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. Enabled via xterm `modifyOtherKeys` level 2 or the Kitty keyboard protocol; terminals that send neither keep `F9` working, and the hints show `^Enter/F9` | +| `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 @@ -43,7 +162,7 @@ written compactly as `^N`, `^A`, `^C`, `^R` to save width. - Rows are numbered `NN. text`, right-aligned so 2- and 3-digit numbers line up. - Verifikation ids are shown concatenated as `series+number` (`V-8`, `A8`), - using the org's `default_series` (Bolaget → Fakturauppgifter) for new + using the org's `series_voucher` (Bolaget → Verifikationsserier) for new vouchers. - The last row may be an action (e.g. `+ Nytt verifikat (Ctrl+N)`); selecting it runs the action instead of opening a detail view. @@ -235,10 +354,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. |
