aboutsummaryrefslogtreecommitdiff
path: root/docs/TUI-GUIDELINES.md
blob: decca09d36d32cbba33db996421b0fab6c26e847 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# 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.