# 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. - Menus may be split into sections: an item whose text starts with `\x01` (`TUI_MARK_HEADING`) is a non-selectable header, drawn dim and without a number. Numbering, `1`–`9` and `g` count only the selectable items and continue across sections; arrows/PgUp/PgDn/Home/End skip the headers. A cursor that points at a header snaps to the next selectable item. ## 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. Backspace/Del/`^U` still delete normally instead of replacing. - A prompt edits a scratch copy: only `Enter` commits it to the field; `Esc` leaves the field exactly as it was. - 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 shows one derived line (balance, attachment list) on its own row above the hints (`LINES-2`), never over a table row; `\n` in the text becomes a space. - 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: one `tui_rt_run` with `tui_rt_set_fields` draws the fields above the table on the same screen. Focus starts on the first field; `Tab`/`Shift-Tab` walk fields, then the cells row by row, and wrap back to the first field. `Up`/`Down` move between fields (in the table they change row). `Enter` opens the field's editor (text/date/amount/ choice) and advances to the next field; `Esc` cancels the editor without changing anything. Typing a printable character hands the focus to the first table cell and inserts it there, so rows can be entered without touching the header. `F5`/`Ctrl+Enter`/`Esc` work the same from both. - 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. Drawing uses the semantic styles below, never raw `A_BOLD`/`A_DIM`. ## Theme and styles `clients/tui.c` owns the theme. The widget layer (and only the widget layer) calls `tui_style(enum tui_style)` to set a semantic style and `tui_style_reset()` to return to the terminal default: | Style | Meaning | |---|---| | `TUI_TITLE` | screen title in the frame | | `TUI_HEADING` | headings, column headers, form labels | | `TUI_SUBHEADING` | secondary headings | | `TUI_DIM` | derived/secondary data, section headers | | `TUI_ACCENT` | interactive prompts (goto) | | `TUI_POS` / `TUI_NEG` | positive/negative amounts | | `TUI_RULE` | full-width rules | | `TUI_STATUS` | status line, hints, footers | Colours are initialised in `tui_keys_setup()`: a small set of pairs (cyan headings, yellow accent, green/red amounts, blue rules) on the default background. If the terminal has no colour support or `NO_COLOR` is set in the environment, every style falls back to attributes (bold/dim) via the pure `tui_style_attrs(style, colours_ok, no_color)`; reverse video for the selection is kept in both modes. ## Pager markup Pager text may carry a one-byte leading marker per line (set by the report formatters, interpreted by `tui_markup`): | Marker | Effect | |---|---| | `\x01` | line is a heading (`TUI_HEADING`) | | `\x02` | line is dimmed (`TUI_DIM`) | | `\x03` | full-width `ACS_HLINE` rule (`TUI_RULE`); the rest of the line is ignored | Report formatters mark headings, sums/derived lines and rules before the text reaches `tui_pager`; the markers never change column widths. Anything that saves pager text to a file strips the markers first (`strip_markup()` in `bokftui.c`). ## 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. Pager lines go through `tui_markup` (see below). - Styles come from `tui_style`/`tui_style_attrs`; report markup and menu sections from `tui_markup`/`TUI_MARK_HEADING`. Their fallbacks and the navigation over section headers are unit-tested in `tests/test_tui.c`. - `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.