#ifndef BOKF_TUI_H #define BOKF_TUI_H #include #include #include /* Shared TUI widgets: the only place that touches ncurses (screens compose these; see docs/TUI-GUIDELINES.md). Everything with logic of its own is unit-tested in tests/test_tui.c, which links only the pure parts. */ /* Universal keys that have no control code or need a stable name. */ #define TUI_KEY_CTRL_N 0x0e #define TUI_KEY_CTRL_X 0x18 #define TUI_KEY_CTRL_ENTER (KEY_MAX + 1) /* Semantic styles: the widget layer maps them to colour pairs when the terminal supports colour and NO_COLOR is unset, otherwise to plain attributes. "Ordinary" drawing uses the terminal default. */ enum tui_style { TUI_TITLE = 0, TUI_HEADING, TUI_SUBHEADING, TUI_DIM, TUI_ACCENT, TUI_POS, TUI_NEG, TUI_RULE, TUI_STATUS, }; /* Attribute bits for a style; pure so the colour/no-colour fallback is unit-tested. colours_ok is the terminal capability, no_color the env. */ int tui_style_attrs(enum tui_style s, int colours_ok, int no_color); void tui_style(enum tui_style s); void tui_style_reset(void); /* Pager line markup. A leading marker selects a style; \x03 is a rule and is drawn full width as ACS_HLINE. tui_markup returns the text after the marker and sets *style to the matching style, or -1 for plain text. */ #define TUI_MARK_HEADING 0x01 #define TUI_MARK_DIM 0x02 #define TUI_MARK_RULE 0x03 const char *tui_markup(const char *line, int *style); /* Key hooks: widgets call the screen's hook for keys they do not handle. */ #define TUI_HOOK_NONE 0 /* not handled: process the key normally */ #define TUI_HOOK_STAY 1 /* handled: redraw and keep the widget up */ #define TUI_HOOK_BACK 2 /* handled: return "back" */ #define TUI_HOOK_REFRESH 3 /* handled: return "refresh" */ #define TUI_HOOK_SUBMIT 4 /* handled: return "submit" (forms only) */ /* The application supplies input, frame/hints drawing and a quit hook so this module stays free of app state (and testable). */ void tui_set_input(int (*fn)(void)); void tui_set_screen(void (*frame)(const char *), void (*hints)(const char *)); void tui_set_quit(void (*fn)(void)); void tui_keys_setup(void); /* --- text measure and formatting --- */ int tui_disp_width(const char *s); int tui_disp_width_n(const char *s, size_t n); int tui_num_width(int n); void tui_pad_field(char *buf, size_t cap, int width); void tui_pad_label(char *buf, size_t n, const char *text, int width); void tui_pad_hdr(char *buf, size_t n, int width, const char *text); void tui_amt_col(char *buf, size_t n, int width, int64_t ore); void tui_kr_format(int64_t ore, char *buf, size_t n); void tui_kr0_format(int64_t ore, char *buf, size_t n); int tui_parse_kr(const char *s, int64_t *out); /* --- line editor (pure) --- */ struct tui_ledit { char *buf; size_t cap; size_t len; size_t pos; /* caret in bytes */ }; void tui_le_init(struct tui_ledit *e, char *buf, size_t cap); void tui_le_clear(struct tui_ledit *e); int tui_le_key(struct tui_ledit *e, int ch); /* Applies one key to a form field. *pos is the caret ((size_t)-1 = end); *fresh replaces the content on the first editing key. Returns whether the key was consumed. */ int tui_field_edit(char *buf, size_t cap, size_t *pos, int ch, int *fresh); /* Date field: digits only, dashes inserted ("20260215" -> "2026-02-15"), caret tracked in digit space. */ int tui_date_field_edit(char *buf, size_t cap, size_t *pos, int ch, int *fresh); /* Line editor at (y,x) with the label already drawn. Returns 1 on Enter, 2 on Tab, 3 on Shift-Tab, 0 on Esc. */ int tui_edit_field(int y, int x, char *buf, size_t cap, int mask); /* --- modal dialogs --- */ void tui_message(const char *title, const char *fmt, ...) __attribute__((format(printf, 2, 3))); int tui_confirm(const char *title, const char *fmt, ...) __attribute__((format(printf, 2, 3))); /* --- prompts: write into the caller's buffer, return 0 on Esc --- */ int tui_prompt_into(char *buf, size_t cap, const char *label, const char *def, int mask); int tui_date_prompt_into(char *buf, size_t cap, const char *label, const char *def); int tui_amount_prompt_into(int64_t *out, const char *label, int64_t def_ore); int tui_choice_prompt(const char *label, const char *const *opts, int n, int cur); /* --- lists --- */ #define TUI_NAV_NONE (-3) struct tui_list_nav { int n, sel, top, view, numw; char gotobuf[12]; char gotolabel[32]; int goto_active; /* Optional row flags: 0 marks a non-selectable row (a menu section header). Navigation, numbering and goto skip them. */ const unsigned char *selectable; }; /* Returns the selected index, or -1 back, -2 refresh, -4 new, -6 remove, -7 toggle, TUI_NAV_NONE when nothing happened. */ int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new, int allow_remove, int allow_toggle, int menu_mode); int tui_select_list(const char *title, char **items, int n, int start, int allow_refresh, int *cursor, int allow_new, const char *archive_action, int allow_toggle); int tui_select_list_hook(const char *title, char **items, int n, int start, int allow_refresh, int *cursor, int allow_new, const char *archive_action, int allow_toggle, int (*key)(void *ud, int ch), void *ud); /* Menu items whose text starts with TUI_MARK_HEADING are non-selectable section headers: drawn dim, not numbered and never highlighted. Numbering and 1-9/g continue over the selectable items only. */ int tui_menu(const char *title, const char *const *items, int n, int allow_new, int *cursor); /* --- frame, status, hints, pager --- */ void tui_set_status(const char *status, const char *right); void tui_frame(const char *title); void tui_hints(const char *s); void tui_redraw(void); #define TUI_PAGER_SAVE 0x1 /* 's' returns 2 (the save action) */ int tui_pager(const char *title, const char *text, const char *action_hint); int tui_pager_hook(const char *title, const char *text, const char *extra_hint, unsigned flags, int (*key)(void *ud, int ch), void *ud); /* --- forms --- */ enum { TUI_F_TEXT = 0, TUI_F_DATE, TUI_F_AMOUNT, TUI_F_CHOICE, TUI_F_ACTION, }; #define TUI_FORM_BACK (-1) #define TUI_FORM_PAGE 10 #define TUI_FORM_REFRESH (-2) #define TUI_FORM_SUBMIT (-8) struct tui_form_field { const char *label; char *value; /* text storage for TEXT/DATE (also CHOICE labels/ACTION) */ size_t cap; int kind; int mask; /* TEXT: '*' instead of the content */ int64_t *ore; /* TUI_F_AMOUNT */ const char *const *choices; /* TUI_F_CHOICE */ int nchoices; int *choice; /* TUI_F_CHOICE: current index */ }; int tui_form_next(int sel, int n, int ch); int tui_form_run(const char *title, struct tui_form_field *f, int n, int can_edit, int *focus); void tui_form_hint(const char *hint); /* overrides the footer for the next run */ int tui_form_run_hook(const char *title, struct tui_form_field *f, int n, int can_edit, int (*key)(void *ud, int ch), void *ud, int *focus); /* --- action list in a form (tui_form_run_actions) --- */ /* enabled: 1 = selectable and runs; 0 = dimmed but selectable (Enter shows disabled_reason in a message); -1 = non-selectable heading/status row, drawn dim like a menu section and skipped by navigation. */ struct tui_form_action { const char *label; int enabled; const char *disabled_reason; }; #define TUI_FORM_ACTION 1000 /* action i is returned as TUI_FORM_ACTION + i */ /* Focus memory: the screen owns an int and passes its address to tui_form_run*; on entry *focus is clamped to a selectable row — out of range snaps to the nearest end and a heading row snaps forward, wrapping — and on every return the row the form stopped on is written back. Passing NULL starts at the first focusable row and stores nothing. */ int tui_form_focus_clamp(int focus, int nf, const struct tui_form_action *acts, int na); int tui_form_focus_store(int *focus, int sel, int ret); /* Focus ring over fields (0..nf-1) and actions (nf..nf+na-1); heading rows are skipped and the ring wraps. dir +1 = Tab/Down, -1 = Shift-Tab/Up. */ int tui_form_act_step(int focus, int nf, const struct tui_form_action *acts, int na, int dir); /* Same ring for one key, plus Home/End/PgUp/PgDn and the form return keys (F5/^Enter/F9/Esc/q). Returns the new focus, a TUI_FORM_* code or TUI_NAV_NONE. */ int tui_form_act_key(int sel, int nf, const struct tui_form_action *acts, int na, int ch); /* Index of the first focusable row (field 0 when fields exist, else the first selectable action), or -1. */ int tui_form_act_first(int nf, const struct tui_form_action *acts, int na); /* "label", or "label (reason)" for a disabled selectable action. */ void tui_form_act_label(const struct tui_form_action *a, char *buf, size_t n); /* Form with fields and an action list below them (see TUI-GUIDELINES.md). Returns a field index after an edit, TUI_FORM_ACTION + i for a chosen action, or TUI_FORM_BACK/REFRESH/SUBMIT. `hint` overrides the default footer; tui_form_hint() still takes precedence. */ int tui_form_run_actions(const char *title, struct tui_form_field *f, int nf, int can_edit, const struct tui_form_action *acts, int na, const char *hint, int *focus); /* --- row table --- */ enum { TUI_RT_EDIT = 0, TUI_RT_INFO, TUI_RT_DATE }; struct tui_rt_col { const char *header; int x; int width; int kind; }; struct tui_rt { int nrows, maxrows, ncols, neditable; const struct tui_rt_col *cols; int y; int row, col; int fresh; size_t pos; void (*cell)(void *, int, int, char **, size_t *); void (*info)(void *, int, char *, size_t); void *ud; int (*key)(void *, int); /* Writes the dim status line shown under the table ('\n' -> ' '). */ void (*footer)(void *, char *, size_t); /* Optional header fields drawn above the table; focus starts there. */ struct tui_form_field *fields; int nfields; int focus; /* header field index, or -1 for the table */ }; void tui_rt_init(struct tui_rt *t, int nrows, int maxrows, int ncols, const struct tui_rt_col *cols, int y, void (*cell)(void *, int, int, char **, size_t *), void (*info)(void *, int, char *, size_t), void *ud); void tui_rt_set_key(struct tui_rt *t, int (*key)(void *ud, int ch)); void tui_rt_set_footer(struct tui_rt *t, void (*footer)(void *ud, char *buf, size_t n)); void tui_rt_set_fields(struct tui_rt *t, struct tui_form_field *fields, int n); int tui_rt_blank_row(struct tui_rt *t, int row); int tui_rt_normalize(struct tui_rt *t); int tui_rt_move(struct tui_rt *t, int dr, int dc); int tui_rt_tab(struct tui_rt *t, int dir); int tui_rt_clear(struct tui_rt *t); int tui_rt_col_of(const struct tui_rt *t, int ecol); int tui_rt_run(const char *title, struct tui_rt *t, const char *hint); /* Focus order for a row table with header fields: fields first, then the editable cells row by row, wrapping back to the first field. focus -1 is the table, 0..nf-1 the fields; dir 1 = Tab, -1 = Shift-Tab. When the new focus is in the table, updates the row and col pointers. */ int tui_rt_focus_step(int focus, int nf, int *row, int *col, int nrows, int neditable, int dir); /* The footer line (above the hints) and its flattening. */ int tui_rt_footer_row(int lines); void tui_rt_footer_flatten(char *s); #endif