aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/DECISIONS.md19
-rw-r--r--docs/STATE.md12
-rw-r--r--docs/TUI-GUIDELINES.md125
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.