# bokftui guidelines 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. ## Session start After login the org picker ("Välj organisation att representera") is always shown; the selected org is fixed for the session (`--org ID` bypasses it for scripts). The fiscal year is chosen from the dashboard and is changeable during the session; the `Räkenskapsår` screen also closes and reopens years there. ## Universal keys | 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 | | `Esc` / `q` | Back one level. At the dashboard it does nothing — Esc never exits the app | | `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) | | `Ctrl+A` | Close or reopen the highlighted räkenskapsår (Räkenskapsår screen; asks for confirmation) | | `1`–`9` | In lists: jump to that row. In menus: activate that item | | `g` | Live goto: "Gå till rad/nummer:" updates the selection as you type digits (backspace steps back); Enter closes the prompt without opening anything | | arrows, PgUp/PgDn, Home/End | Move/scroll; selection always stays visible | | `e` | Edit the shown object (IB, where applicable) | | `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 | | `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`; terminals that cannot send it keep `F9` working | 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. ## Lists (`select_list`, `menu`) - 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` (Inställningar) 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. - Selection memory: lists remember the selected row by identity (voucher id), not index, across detail round-trips, refreshes and screen re-entry. - Digits move the highlight; only Enter activates. Menus are the exception: digits activate directly (they are shortcuts). - Empty lists are never a dead end: show a message or keep the add-row. ## Forms - Layout: bold header fields at the top, then a bold column header, then rows; footer hints carry the keys. - Field navigation: `Tab` / `Shift-Tab` forward/back, arrows Up/Down between rows. `Enter` advances in simple forms; it never saves unless the screen is a one-line prompt. - The active field is drawn reverse-video and holds the hardware cursor at the caret. The caret is a real position: ←/→/Home/End/Del work inside the field, `Ctrl+U` clears it (see `field_edit`). - First keystroke in a freshly focused field replaces its content (`field_fresh`), so prefilled values like dates can be typed over. - Date fields (`date_field_edit`, `date_prompt`) accept digits only and insert 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`. - 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. `Ctrl+X` clears the selected row. `Tab` walks the editable cells and wraps to the next row's first cell (`Shift-Tab` back). A dim footer callback may show derived lines (balance, attachment list) directly under the table. - Fields with `mask` set are shown as `*` (`tui_form_field.mask`); used for passwords. ACTION fields show their `value` when set, else `(lista)`. - Header fields plus rows: the header is a `tui_form_run`, the rows a `tui_rt_run`. Editing a header field hands over to the row table; `Esc` in the table goes back to the header. `F5`/`Ctrl+Enter` work in both stages. - Validation: `F5` 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 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`, `tui_pager_hook` and `tui_select_list_hook`. - File browser (`file_browser`): starts in the org's `attachment_dir` (or `$HOME`; a leading `~` is expanded to `$HOME`), `.. (uppåt)` is the first row, directories sort first with a trailing `/`, hidden files are skipped. Enter enters a directory or picks a file; `Esc` cancels. Selected files are uploaded immediately as unlinked underlag and linked when the voucher is posted. ## Messages - Info/confirmation: `message(title, ...)` box, dismissed with Enter. - Quitting: only `Ctrl+C` (from anywhere) or "Logga ut / avsluta" ends the app; `Esc`/`q` only navigate. After `Ctrl+C` every screen unwinds, the session is closed and the terminal restored. - Errors: `show_error(title, resp)` prints `CODE: message` from the server error object; if the response is missing/empty it says "Inget svar från servern (kör daemonen?)". Never render an empty error. - Irreversible actions (close year, lock, archive) ask first. ## Layout and text - Screen frame: title top-left, dim status line under it (`org | start - end | role | user`), hints in the last line. - Columns are padded with `pad_field()` (UTF-8 display width) and truncated to their column; amounts right-aligned via `kr_format()`; never pad with `%s` widths directly on user text. - Selection = reverse video, headers = bold, derived data = dim. - All user-visible TUI text is Swedish; server messages are English. ## Widget layer (`clients/tui.[ch]`) Every element a screen needs is a widget here, and the widget layer is the only place that touches ncurses. Rules: - Screens never call ncurses directly (`mvadd*`, `attron`, `getch`…) and never keep a local one-off input or drawing loop. - If no widget fits, **extend the widget layer first** (spec in this file + unit tests in `tests/test_tui.c`) and then use it. There is no "do it locally once" option. - Pure logic (line editing, date input, list navigation, prompts parsing) is separated from drawing so it can be tested without a terminal. - Widgets return key codes (`-1` back, `-2` refresh, `-4` new, `-6` remove, `-7` toggle) and write text into the caller's buffer; nothing returns static storage. `TUI_NAV_NONE`, `TUI_FORM_BACK/REFRESH/SUBMIT` and the `TUI_HOOK_*` codes are all distinct. - `tui_pager_hook` takes `TUI_PAGER_SAVE` to enable the `s` save action; the `extra_hint` is shown in the footer. Plain `tui_pager` keeps `s` on when a save hint is passed. - `tui_redraw` flushes a `tui_frame`/`tui_set_status` pair drawn by the application outside a widget loop (startup/reconnect messages). - The application supplies input, frame/hints and quit through `tui_set_input`/`tui_set_screen`/`tui_set_quit`. ## 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. 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.