summaryrefslogtreecommitdiff
path: root/docs/TUI-GUIDELINES.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/TUI-GUIDELINES.md')
-rw-r--r--docs/TUI-GUIDELINES.md164
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.