summaryrefslogtreecommitdiff
path: root/docs/TUI-GUIDELINES.md
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-22 12:26:48 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-22 12:26:48 +0200
commit597811e780e423120bdbd61a17d78fb7c5733415 (patch)
treed98f1a8ad27daf223622523afe028cbf2cb3a6f8 /docs/TUI-GUIDELINES.md
parent0f286b6eda8405f14f18543f6b56bf0bd29c4fc9 (diff)
downloadbokf-235e9110b6d4ac137b727328b9dbc1ee6e93049e.tar.gz
bokf-235e9110b6d4ac137b727328b9dbc1ee6e93049e.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.md125
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.