diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-23 10:58:17 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-23 10:58:17 +0200 |
| commit | 71a702f375750829c634b552217c9925d549828b (patch) | |
| tree | 2b963da1d0110ed43920722ba4193ee9c79f2521 /docs/TUI-GUIDELINES.md | |
| parent | 8ccb6d58d8e2f1c65dfd7ab5f0eb7d93f1f19def (diff) | |
| download | bokf-71a702f375750829c634b552217c9925d549828b.tar.gz bokf-71a702f375750829c634b552217c9925d549828b.zip | |
tui: context menu on → / ^O; no F-keys, ^N or ^Enter
→ (where no caret uses it) and ^O (everywhere, also in table cells)
open "Åtgärder": a box at the right edge with every action of the view
and its key. Screens declare their actions with tui_set_actions();
choosing one feeds its key to the widget, so the menu and accelerators
share one path. Function keys, Ctrl+N (now n), Ctrl+Enter and Ctrl+F are
gone and no extended keyboard protocol is enabled, so the TUI works in a
browser terminal; save/post/validate/preview/template/attach are menu
actions. make check rejects F-keys, ^N and ^Enter in clients/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs/TUI-GUIDELINES.md')
| -rw-r--r-- | docs/TUI-GUIDELINES.md | 164 |
1 files changed, 106 insertions, 58 deletions
diff --git a/docs/TUI-GUIDELINES.md b/docs/TUI-GUIDELINES.md index 28b7f89..650f026 100644 --- a/docs/TUI-GUIDELINES.md +++ b/docs/TUI-GUIDELINES.md @@ -7,7 +7,9 @@ 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 +row and draft deletion work there. The context menu (`→`/`^O`, below) and +the web-safe keys are implemented in every screen (2026-09-23, +`DECISIONS.md` #29). Every other screen still follows the sections below; the interaction model wins where they conflict as each screen moves over. @@ -38,16 +40,17 @@ saves a whole form, never deletes and never posts. | 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 | +| **Register** | in memory + local draft file | explicit `Spara` (the save action row or the context menu) after validation | back; the draft stays | customers, employees, templates, momsregler, org data | +| **Document** | the form is the draft | explicit `Posta` (the save action row or the context menu); 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. + `Posta` for documents), or — in a row table, where rows follow — offers it + first in the context menu (`^O`, `Enter`). `Enter` on the focused row + commits (it is an item with an action). +- There is no commit hotkey: `Ctrl+Enter` cannot be sent by gnome-terminal/ + VTE and `F9` is dropped with the other function keys (see "Keys and the + web"). The action row and the menu work in every terminal and browser. - 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. @@ -64,8 +67,8 @@ saves a whole form, never deletes and never posts. 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 +- **Delete draft** is an action both on the list row (`d`, also in the + context menu) and inside the editor (context menu), 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. @@ -76,7 +79,7 @@ saves a whole form, never deletes and never posts. Bitwarden session file; decide whether such fields are excluded from drafts. -### Actions and the `F2` menu +### Actions and the context menu Screens declare actions, never keys: @@ -84,41 +87,79 @@ Screens declare actions, never keys: struct tui_action { const char *id; /* stable, e.g. "customer.archive" */ const char *label; /* Swedish UI text */ - int key; /* accelerator; 0 = menu only */ + int key; /* accelerator, or a virtual key (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). +- **`→` opens the context menu** ("Åtgärder") wherever no caret uses the + arrow: lists, menus, pagers (report/detail views), forms and a row + table's header fields. **`^O` opens it everywhere**, also while typing in + a table cell, where `→` moves the caret. The menu is a box at the right + edge next to the selected row; each line shows the label and, right- + aligned, the key that does the same thing — the menu doubles as the + hotkey list. Up/down/Home/End move, `Enter` (or the action's own key) + runs, `Esc`/`←`/`q`/`^O` close. A disabled action is dim and `Enter` + shows its reason; a separator line divides the groups. +- The screen registers its actions with `tui_set_actions(acts, n)` right + before the widget call; the widget takes them at entry, so they never leak + into a widget opened from a hook. The menu lists the screen's actions + first, then the widget's own (Öppna, Ny, Gå till/sök, Uppdatera, + Tillbaka; in forms Ändra/Utför; in row tables Ändra fält/Rensa raden, + Validera, Spara, Avbryt), skipping own actions whose key the screen took + (`tui_ctx_merge`). +- **Choosing an action is the same as pressing its key**: the widget gets + the key as if it was typed, so the accelerator and the menu share one + code path in the screen's key hook. An action without a physical key uses + a virtual key: `TUI_KEY_SUBMIT` (spara/bokför/utfärda — was F9), + `TUI_KEY_REFRESH` (uppdatera/validera/förhandsvisa — was F5) or + `TUI_KEY_ACTION(n)` for the screen's own menu-only actions (mall, bifoga + in the voucher form, nästa fakturanummer). +- Right before the menu opens the key hook gets `TUI_KEY_MENU`, so a screen + can set `enabled`/`label`/`reason` for the current row in its own array + (the Kunder list shows "Radera utkast" for a draft row and "Arkivera" or + "Återaktivera" otherwise, all on `d`). +- Footers show `→ = åtgärder` (`^O` in row tables) and at most the few + letter accelerators that matter on the screen. Universal navigation keys + are not repeated there. + +### Keys and the web + +The TUI is meant to run in a browser terminal too (xterm.js/ttyd). A +browser keeps some keys for itself, so the TUI only uses keys every +terminal and browser delivers (checked by `make check`): + +- **No function keys.** F1/F3/F5/F6/F7/F10/F11/F12 are browser keys and F2, + F4, F9 need `Fn` on many laptops. Their old jobs are context-menu actions + (F2 → `→`, F5 → "Uppdatera"/"Validera"/"Förhandsvisa" with `r` in lists + and pagers, F9 → "Spara"/"Bokför"/"Utfärda", F4 → "Hämta mall…"). +- **No `Ctrl+N`, `Ctrl+T`, `Ctrl+W`, `Ctrl+Tab`**: Chrome never lets a page + have them. "Ny" is `n` in lists, menus and the voucher detail. +- **No `Ctrl+Enter`** and no extended keyboard protocol: the TUI no longer + enables xterm `modifyOtherKeys` or the Kitty protocol, so every key comes + in its legacy encoding. +- **Allowed**: arrows, `Enter`, `Esc`, `Tab`/`Shift-Tab`, PgUp/PgDn, + Home/End, letters and digits, and the control keys a page can take + (`^O` menu, `^X` clear row, `^A`/`^E`/`^U` in fields, `^C` quit, `^R` + reload — `^R` is a developer convenience and never the only way). +- Every action is reachable with arrows + `Enter` through the menu; a + letter accelerator is a shortcut, never the only path. ### 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. +1. Done: `struct tui_action`, the context menu (`tui_action_menu_at`, + `tui_set_actions`, `tui_ctx_merge`, `tui_key_name`) and `→`/`^O` in all + widgets, unit-tested in `tests/test_tui.c`; pty scenario `context-menu`. +2. Done: every screen declares its actions; no F-keys, `^N` or `^Enter` + remain (`scripts/check-consistency.sh` fails on `KEY_F(`, `^N` and + `CTRL_ENTER` in `clients/`). 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. +4. Done for Kunder: draft/`Spara` model with the `Spara` action row 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. @@ -143,8 +184,10 @@ there. | Key | Meaning | |---|---| -| `Ctrl+N` | Add: new verifikat (dashboard, voucher list/detail), new mall (Mallar), new fiscal year (year picker), open editor (IB), upload file (Underlag) | -| `F5` | Refresh the view | +| `→` | Open the context menu ("Åtgärder") for the selected row/view: every action with its key. Not in a table cell, where `→` moves the caret | +| `^O` | Open the context menu anywhere, also while typing in a table cell | +| `n` | Ny: new verifikat (dashboard, voucher list/detail), new mall, kund, faktura, anställd, lönekörning, regel, ledamot, new voucher from a bank transaction, upload (Underlag) | +| `r` | Uppdatera: reload the list or view | | `Esc` / `q` | Back one level. At the dashboard it does nothing — Esc never exits the app. In a row table (`tui_rt_run`) and while a field is being edited `q` is text; only `Esc` goes back there | | `Ctrl+C` | Quit the application (closes the session). The only key that exits | | `Ctrl+R` | Reload the client in place: re-execs the installed binary and restores session, org, fiscal year and the current view (for after a rebuild) | @@ -153,21 +196,22 @@ there. | `g` | Live goto or search in every list and menu. The first key decides: a digit makes it "Gå till rad/nummer:" (the selection follows the number as you type, backspace steps back, `g` closes); any other printable key makes it "Sök:" — the selection jumps to the first row (from the top) whose text contains the typed text, case-insensitive also for åäö; up/down step to the next/previous match and wrap; "(ingen träff)" when nothing matches. Digits, `g`, `q` and screen keys (`s` = sortera, `d`…) are text while searching. Backspace to empty returns to the undecided prompt. Enter/Esc close the prompt without opening anything; the selection stays | | arrows, PgUp/PgDn, Home/End | Move/scroll; selection always stays visible. Pages stop at the first/last row, they never wrap | | `e` | Edit the shown object (IB, where applicable) | -| `a` | Add/upload (Underlag) | +| `a` | Add/upload: upload (Underlag), import a bank file (Bankavstämning), attach a file (voucher detail) | | `c` | Correct (voucher detail) | | `d` | Delete/arkivera the selected row (only where the action exists; asks for confirmation) | | `s` | Verifikat list: cycle the sort order — nummer stigande/fallande, datum stigande/fallande (ties by series and number). The title shows the current order; it is saved as `voucher_sort` in `tui.conf` | -| up/down (voucher detail) | Previous/next voucher in the list's current order; the title shows `(n av N)`. The detail scrolls with PgUp/PgDn/Home/End (`TUI_PAGER_NO_ARROWS`). A voucher posted from the detail (`^N`) is shown without stepping until the list is reopened | +| up/down (voucher detail) | Previous/next voucher in the list's current order; the title shows `(n av N)`. The detail scrolls with PgUp/PgDn/Home/End (`TUI_PAGER_NO_ARROWS`). A voucher posted from the detail (`n`) is shown without stepping until the list is reopened | | `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) | +| `p` / `s` / `u` / `b` | Faktura detail: `p` shows the PDF, `s` sends it, `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 | +| `u` | Bankavstämning: remove the highlighted transaction's match | | `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. 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 -written compactly as `^N`, `^A`, `^C`, `^R` to save width. +Saving, posting, validating, previewing, templates and attaching in forms +have no hotkey: they are context-menu actions (see "Keys and the web"). +Every key a screen handles is listed in its context menu; the footer shows +`→ = åtgärder` and the most important letters. Control keys are written +compactly as `^O`, `^X`, `^C`, `^R` to save width. ## Lists (`select_list`, `menu`) @@ -175,7 +219,7 @@ written compactly as `^N`, `^A`, `^C`, `^R` to save width. - Verifikation ids are shown concatenated as `series+number` (`V-8`, `A8`), 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 +- The last row may be an action (e.g. `+ Nytt verifikat (n)`); selecting it runs the action instead of opening a detail view. - Selection memory: lists remember the selected row by identity (voucher id), not index, across detail round-trips, refreshes and screen re-entry. @@ -200,7 +244,7 @@ written compactly as `^N`, `^A`, `^C`, `^R` to save width. address as the last argument to `tui_form_run`, `tui_form_run_hook` and `tui_form_run_actions` (like a list cursor). On entry `*focus` is clamped to a selectable row — out of range snaps to the nearest end, a heading row - snaps forward and wraps — and on every return (`Esc`, `F5`, `^Enter`, a + snaps forward and wraps — and on every return (`Esc`, Uppdatera, Spara, a chosen action, an edited field) the row is written back, so a re-run after a save or a round-trip through another screen lands on the same row. `NULL` starts at the first focusable row and stores nothing. @@ -234,7 +278,7 @@ written compactly as `^N`, `^A`, `^C`, `^R` to save width. the dashes themselves: type `20260315` and the field shows `2026-03-15`. Backspace deletes a digit (with its separator), ←/→/Home/End move by digit, and the caret renders like in any other field. Validation (`util_parse_iso_date`) - happens on F5/`^Enter`. + happens on Validera/Spara. - Derived values (account names, balances) are dim and non-editable. - Row tables: always exactly one empty trailing row; entering data appends a new empty row; an empty row followed by another empty row collapses. @@ -259,7 +303,7 @@ written compactly as `^N`, `^A`, `^C`, `^R` to save width. does nothing. The hardware cursor only shows in a table cell. `q` is text here — only `Esc` leaves the table. Moving to another cell (arrows, `Enter`, `Tab`) resets the caret to the end of the new cell. A focused - cell scrolls horizontally like a form field. `F5`/`Ctrl+Enter`/`Esc` + cell scrolls horizontally like a form field. `^O`/`Esc` work the same from both. - A form may carry an action list under its fields (`tui_form_run_actions`): fields first, then action rows, one focus ring (`Tab`/`Shift-Tab`/ @@ -276,17 +320,20 @@ written compactly as `^N`, `^A`, `^C`, `^R` to save width. resultat` voucher that `bokslut.post` creates; a closed year counts as posted) and the actions Årsredovisning (K2), Inkomstdeklaration (INK2/SRU), Bokslutsplan (torrkörning) and Bokför bokslut — the last - dimmed with a reason when the year is closed or already posted. `F5` - shows the plan, `Ctrl+Enter` posts it. -- Validation: `F5` validates without writing and reports exactly what is wrong + dimmed with a reason when the year is closed or already posted. The + context menu's "Visa bokslutsplan" shows the plan, "Bokför planen…" posts + it. +- Validation: the "Validera" action (`TUI_KEY_REFRESH`) validates without + writing and reports exactly what is wrong (field, row number). Server `dry_run` is used where available. -- Saving: `Ctrl+Enter` writes, shows a confirmation message, and returns to +- Saving: the "Spara"/"Bokför" action (`TUI_KEY_SUBMIT`) writes, shows a + confirmation message, and returns to the previous view. `Esc` cancels without saving. - Screen-specific keys (template picker, attach, link, download) are handled in a key hook, never with a local loop. The hook runs for keys the widget itself does not handle and returns `TUI_HOOK_*`: `STAY` (consumed, redraw), - `BACK`, `REFRESH` or `SUBMIT` (the widget returns as on `Esc`/`F5`/ - `Ctrl+Enter`). Hooks exist on `tui_form_run_hook`, `tui_rt_set_key`, + `BACK`, `REFRESH` or `SUBMIT` (the widget returns as on `Esc` or the + Uppdatera/Spara actions). Hooks exist on `tui_form_run_hook`, `tui_rt_set_key`, `tui_pager_hook` and `tui_select_list_hook`. - File browser (`file_browser`): starts in the directory where the last attachment was picked (remembered in `tui.conf`), falling back to `$HOME` @@ -404,8 +451,9 @@ only place that touches ncurses. Rules: one-off questions use the prompts (`tui_prompt_into`, `tui_date_prompt_into`, `tui_amount_prompt_into`, `tui_choice_prompt`). All of them share one field editor. -5. Support `F5` if the data can change elsewhere. +5. Support refresh (`r` / "Uppdatera") 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 + interaction model is implemented); dispatch, the context menu and the + footer hint all read that list, so a key cannot exist without a visible action. |
