diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-19 23:05:44 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-19 23:05:44 +0200 |
| commit | 8da6a7e849513e1cb63410d95b121df729260d0c (patch) | |
| tree | 77de7f5b3952d153e072cda8cc7c4f4bcd5f0c87 /clients/tui.c | |
| parent | 730d7159e3b9b3adfba2c3779893fdb4e107647a (diff) | |
| download | bokf-0.1.40.tar.gz bokf-0.1.40.zip | |
tui: themes, pager markup, menu sections; merge Bokslut and year infov0.1.40
Diffstat (limited to 'clients/tui.c')
| -rw-r--r-- | clients/tui.c | 339 |
1 files changed, 280 insertions, 59 deletions
diff --git a/clients/tui.c b/clients/tui.c index de9b9f9..473cc0e 100644 --- a/clients/tui.c +++ b/clients/tui.c @@ -39,6 +39,108 @@ static int in_key(void) return g_in(); } +/* ------------------------------------------------------------------ */ +/* theme: semantic styles, colour pairs and attribute fallback */ +/* ------------------------------------------------------------------ */ + +enum { + TUI_CP_HEAD = 1, + TUI_CP_ACCENT, + TUI_CP_POS, + TUI_CP_NEG, + TUI_CP_RULE, + TUI_CP_STATUS, +}; + +static int g_colours; + +int tui_style_attrs(enum tui_style s, int colours_ok, int no_color) +{ + int cp = 0; + if (colours_ok && !no_color) { + switch (s) { + case TUI_TITLE: + case TUI_HEADING: + case TUI_SUBHEADING: + cp = COLOR_PAIR(TUI_CP_HEAD); + break; + case TUI_ACCENT: + cp = COLOR_PAIR(TUI_CP_ACCENT); + break; + case TUI_POS: + cp = COLOR_PAIR(TUI_CP_POS); + break; + case TUI_NEG: + cp = COLOR_PAIR(TUI_CP_NEG); + break; + case TUI_RULE: + cp = COLOR_PAIR(TUI_CP_RULE); + break; + case TUI_STATUS: + cp = COLOR_PAIR(TUI_CP_STATUS); + break; + case TUI_DIM: + default: + break; + } + } + int a; + switch (s) { + case TUI_TITLE: + case TUI_HEADING: + a = A_BOLD; + break; + case TUI_SUBHEADING: + a = A_BOLD; + break; + case TUI_DIM: + case TUI_STATUS: + a = A_DIM; + break; + case TUI_ACCENT: + a = A_BOLD; + break; + case TUI_RULE: + a = colours_ok && !no_color ? 0 : A_DIM; + break; + case TUI_POS: + case TUI_NEG: + default: + a = 0; + break; + } + return a | (int)cp; +} + +void tui_style(enum tui_style s) +{ + attrset(tui_style_attrs(s, g_colours, 0)); +} + +void tui_style_reset(void) +{ + attrset(A_NORMAL); +} + +const char *tui_markup(const char *line, int *style) +{ + const char *p = line ? line : ""; + int st = -1; + if (*p == TUI_MARK_HEADING) { + st = TUI_HEADING; + p++; + } else if (*p == TUI_MARK_DIM) { + st = TUI_DIM; + p++; + } else if (*p == TUI_MARK_RULE) { + st = TUI_RULE; + p++; + } + if (style) + *style = st; + return p; +} + void tui_keys_setup(void) { /* ^Enter has no control code; enable xterm modifyOtherKeys and bind the @@ -48,9 +150,24 @@ void tui_keys_setup(void) define_key("\033[1~", KEY_HOME); define_key("\033OH", KEY_HOME); define_key("\033[H", KEY_HOME); + define_key("\033[7~", KEY_HOME); define_key("\033[4~", KEY_END); define_key("\033OF", KEY_END); define_key("\033[F", KEY_END); + define_key("\033[8~", KEY_END); + /* Colour only when the terminal has it and NO_COLOR is not set; without + it every style falls back to attributes. */ + g_colours = has_colors() && !getenv("NO_COLOR"); + if (g_colours) { + start_color(); + use_default_colors(); + init_pair(TUI_CP_HEAD, COLOR_CYAN, -1); + init_pair(TUI_CP_ACCENT, COLOR_YELLOW, -1); + init_pair(TUI_CP_POS, COLOR_GREEN, -1); + init_pair(TUI_CP_NEG, COLOR_RED, -1); + init_pair(TUI_CP_RULE, COLOR_BLUE, -1); + init_pair(TUI_CP_STATUS, COLOR_CYAN, -1); + } putp("\033[>4;1m"); fflush(stdout); } @@ -456,9 +573,9 @@ static WINDOW *dlg_open(const char *title, int h, int w) h = LINES - 2; WINDOW *win = newwin(h, w, (LINES - h) / 2, (COLS - w) / 2); box(win, 0, 0); - wattron(win, A_BOLD); + wattrset(win, tui_style_attrs(TUI_TITLE, g_colours, 0)); mvwaddnstr(win, 0, 2, title, w - 4); - wattroff(win, A_BOLD); + wattrset(win, A_NORMAL); wrefresh(win); nodelay(stdscr, FALSE); timeout(-1); @@ -505,7 +622,9 @@ void tui_message(const char *title, const char *fmt, ...) p = nl ? nl + 1 : NULL; } free(copy); + wattrset(win, tui_style_attrs(TUI_DIM, g_colours, 0)); mvwaddstr(win, h - 1, w - 13, " tryck Enter"); + wattrset(win, A_NORMAL); wrefresh(win); for (;;) { int ch = wgetch(win); @@ -555,7 +674,9 @@ int tui_confirm(const char *title, const char *fmt, ...) p = nl ? nl + 1 : NULL; } free(copy); + wattrset(win, tui_style_attrs(TUI_DIM, g_colours, 0)); mvwaddstr(win, h - 1, w - 25, " j = ja n = nej"); + wattrset(win, A_NORMAL); wrefresh(win); int yes = 0; for (;;) { @@ -590,9 +711,9 @@ int tui_prompt_into(char *buf, size_t cap, const char *label, int y = LINES - 3; move(y, 2); clrtoeol(); - attron(A_BOLD); + tui_style(TUI_HEADING); mvaddstr(y, 2, label); - attroff(A_BOLD); + tui_style_reset(); refresh(); int prev = curs_set(1); int rc = tui_edit_field(y, (int)strlen(label) + 3, buf, cap, mask); @@ -609,9 +730,9 @@ int tui_date_prompt_into(char *buf, size_t cap, const char *label, int y = LINES - 3; move(y, 2); clrtoeol(); - attron(A_BOLD); + tui_style(TUI_HEADING); mvaddstr(y, 2, label); - attroff(A_BOLD); + tui_style_reset(); size_t pos = (size_t)-1; int fresh = 1; int prev = curs_set(1); @@ -664,9 +785,9 @@ int tui_choice_prompt(const char *label, const char *const *opts, int n, clrtoeol(); char line[512]; snprintf(line, sizeof line, "%s< %s >", label, opts[cur]); - attron(A_BOLD); + tui_style(TUI_HEADING); mvaddnstr(y, 2, line, COLS - 4); - attroff(A_BOLD); + tui_style_reset(); char hint[256]; snprintf(hint, sizeof hint, "vänster/höger = välj Enter = OK " "Esc = avbryt"); @@ -691,11 +812,53 @@ int tui_choice_prompt(const char *label, const char *const *opts, int n, /* list navigation core (pure, unit-tested) */ /* ------------------------------------------------------------------ */ +static int nav_selectable(const struct tui_list_nav *v, int i) +{ + return i >= 0 && i < v->n && (!v->selectable || v->selectable[i]); +} + +/* First selectable row at or after `from` moving in `dir`. */ +static int nav_snap(const struct tui_list_nav *v, int from, int dir) +{ + for (int i = from; i >= 0 && i < v->n; i += dir) + if (nav_selectable(v, i)) + return i; + for (int i = from; i >= 0 && i < v->n; i -= dir) + if (nav_selectable(v, i)) + return i; + return from; +} + +/* Row index of the ord-th (0-based) selectable row, or -1. */ +static int nav_row_of(const struct tui_list_nav *v, int ord) +{ + int c = 0; + for (int i = 0; i < v->n; i++) { + if (!nav_selectable(v, i)) + continue; + if (c == ord) + return i; + c++; + } + return -1; +} + +static int nav_count(const struct tui_list_nav *v) +{ + int c = 0; + for (int i = 0; i < v->n; i++) + if (nav_selectable(v, i)) + c++; + return c; +} + /* Keys return the same codes the screens have always used: the selected index, or -1 back, -2 refresh, -4 new, -6 remove, -7 toggle. */ int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new, int allow_remove, int allow_toggle, int menu_mode) { + if (v->n > 0 && !nav_selectable(v, v->sel)) + v->sel = nav_snap(v, v->sel, 1); if (v->n <= 0) { /* an empty list is never a dead end: refresh, add and back work */ if (ch == KEY_F(5)) @@ -711,17 +874,17 @@ int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new, size_t gl = strlen(v->gotobuf); v->gotobuf[gl] = (char)ch; v->gotobuf[gl + 1] = '\0'; - int x = atoi(v->gotobuf); - if (x >= 1 && x <= v->n) - v->sel = x - 1; + int r = nav_row_of(v, atoi(v->gotobuf) - 1); + if (r >= 0) + v->sel = r; } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { size_t gl = strlen(v->gotobuf); if (gl) v->gotobuf[gl - 1] = '\0'; if (v->gotobuf[0]) { - int x = atoi(v->gotobuf); - if (x >= 1 && x <= v->n) - v->sel = x - 1; + int r = nav_row_of(v, atoi(v->gotobuf) - 1); + if (r >= 0) + v->sel = r; } } else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER || ch == 27 || ch == 'g' || ch == 'G') { @@ -730,22 +893,33 @@ int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new, } return -3; } - if (ch == KEY_UP && v->sel > 0) - v->sel--; - else if (ch == KEY_DOWN && v->sel < v->n - 1) - v->sel++; - else if (ch == KEY_NPAGE) - v->sel = v->sel + v->view < v->n ? v->sel + v->view : v->n - 1; - else if (ch == KEY_PPAGE) - v->sel = v->sel - v->view > 0 ? v->sel - v->view : 0; - else if (ch == KEY_HOME) - v->sel = 0; - else if (ch == KEY_END) - v->sel = v->n - 1; - else if (ch >= '1' && ch <= '9' && ch - '1' < v->n) { - v->sel = ch - '1'; - if (menu_mode) - return v->sel; /* menus activate directly */ + if (ch == KEY_UP) { + if (v->sel > 0) + v->sel = nav_snap(v, v->sel - 1, -1); + } else if (ch == KEY_DOWN) { + if (v->sel < v->n - 1) + v->sel = nav_snap(v, v->sel + 1, 1); + } else if (ch == KEY_NPAGE) { + int x = v->sel + v->view; + v->sel = nav_snap(v, x < v->n ? x : v->n - 1, 1); + } else if (ch == KEY_PPAGE) { + int x = v->sel - v->view; + v->sel = nav_snap(v, x > 0 ? x : 0, -1); + } else if (ch == KEY_HOME) { + int r = nav_row_of(v, 0); + if (r >= 0) + v->sel = r; + } else if (ch == KEY_END) { + int r = nav_row_of(v, nav_count(v) - 1); + if (r >= 0) + v->sel = r; + } else if (ch >= '1' && ch <= '9') { + int r = nav_row_of(v, ch - '1'); + if (r >= 0) { + v->sel = r; + if (menu_mode) + return v->sel; /* menus activate directly */ + } } else if (ch == 'g' || ch == 'G') { v->goto_active = 1; v->gotobuf[0] = '\0'; @@ -795,9 +969,9 @@ static void list_draw(const char *title, char **items, int n, } if (v->goto_active) { move(LINES - 2, 2); - attron(A_BOLD); + tui_style(TUI_ACCENT); printw("%s%s", v->gotolabel, v->gotobuf); - attroff(A_BOLD); + tui_style_reset(); clrtoeol(); } else { move(LINES - 2, 2); @@ -883,7 +1057,27 @@ int tui_menu(const char *title, const char *const *items, int n, struct tui_list_nav v; memset(&v, 0, sizeof v); v.n = n; + unsigned char *flags = xcalloc(n > 0 ? (size_t)n : 1, 1); + int nsel = 0; + for (int i = 0; i < n; i++) { + if (items[i][0] == TUI_MARK_HEADING) + continue; + flags[i] = 1; + nsel++; + } + v.selectable = flags; + if (nsel == 0) { + free(flags); + return -1; + } v.sel = (cursor && *cursor >= 0 && *cursor < n) ? *cursor : 0; + if (!flags[v.sel]) + v.sel = nav_snap(&v, v.sel, 1); + v.numw = 1; + for (int tmp = nsel; tmp >= 10; tmp /= 10) + v.numw++; + if (v.numw < 2) + v.numw = 2; v.view = (LINES - 4) / 2; snprintf(v.gotolabel, sizeof v.gotolabel, "Gå till nummer: "); for (;;) { @@ -892,21 +1086,33 @@ int tui_menu(const char *title, const char *const *items, int n, list_view_sync(&v); if (g_frame) g_frame(title); - for (int i = 0; i < v.view && v.top + i < n; i++) { - int idx = v.top + i; - char line[512]; - snprintf(line, sizeof line, "%2d. %s", idx + 1, items[idx]); + int num = 0; + for (int i = 0; i < v.top && i < n; i++) + if (v.selectable[i]) + num++; + for (int idx = v.top; idx < n && idx < v.top + v.view; idx++) { + char line[1024]; + if (!v.selectable[idx]) { + snprintf(line, sizeof line, "%*s %s", v.numw, "", + items[idx] + 1); + tui_style(TUI_DIM); + mvaddnstr(3 + (idx - v.top) * 2, 4, line, COLS - 6); + tui_style_reset(); + continue; + } + num++; + snprintf(line, sizeof line, "%*d. %s", v.numw, num, items[idx]); if (idx == v.sel) attron(A_REVERSE); - mvaddnstr(3 + i * 2, 4, line, COLS - 6); + mvaddnstr(3 + (idx - v.top) * 2, 4, line, COLS - 6); if (idx == v.sel) attroff(A_REVERSE); } if (v.goto_active) { move(LINES - 2, 2); - attron(A_BOLD); + tui_style(TUI_ACCENT); printw("%s%s", v.gotolabel, v.gotobuf); - attroff(A_BOLD); + tui_style_reset(); clrtoeol(); } else { move(LINES - 2, 2); @@ -924,6 +1130,7 @@ int tui_menu(const char *title, const char *const *items, int n, if (r != -3) { if (cursor) *cursor = v.sel; + free(flags); return r; } } @@ -945,20 +1152,20 @@ void tui_set_status(const char *status, const char *right) void tui_frame(const char *title) { erase(); - attron(A_BOLD); + tui_style(TUI_TITLE); box(stdscr, 0, 0); mvaddstr(0, 2, title); - attroff(A_BOLD); + tui_style_reset(); if (!g_status[0]) return; - attron(A_DIM); + tui_style(TUI_STATUS); mvaddnstr(1, 2, g_status, COLS - 4); if (g_right[0]) { int x = COLS - 3 - tui_disp_width(g_right); if (x > tui_disp_width(g_status) + 6) mvaddstr(1, x, g_right); } - attroff(A_DIM); + tui_style_reset(); } /* Flush a frame drawn by the application outside a widget loop. */ @@ -971,9 +1178,9 @@ void tui_hints(const char *s) { char line[512]; snprintf(line, sizeof line, "%s ^R = ladda om", s); - attron(A_DIM); + tui_style(TUI_STATUS); mvaddstr(LINES - 1, 2, line); - attroff(A_DIM); + tui_style_reset(); clrtoeol(); } @@ -1016,8 +1223,22 @@ int tui_pager_hook(const char *title, const char *text, for (int i = 0; i < view; i++) { move(2 + i, 2); clrtoeol(); - if (top + i < n) - mvaddnstr(2 + i, 2, lines[top + i], COLS - 4); + if (top + i >= n) + continue; + int st = -1; + const char *s = tui_markup(lines[top + i], &st); + if (st == TUI_RULE) { + attrset(tui_style_attrs(TUI_RULE, g_colours, 0)); + for (int x = 2; x < COLS - 1; x++) + mvaddch(2 + i, x, ACS_HLINE); + attrset(A_NORMAL); + } else if (st >= 0) { + attrset(tui_style_attrs((enum tui_style)st, g_colours, 0)); + mvaddnstr(2 + i, 2, s, COLS - 4); + attrset(A_NORMAL); + } else { + mvaddnstr(2 + i, 2, s, COLS - 4); + } } char hint[512]; if (extra_hint && *extra_hint) @@ -1125,10 +1346,10 @@ int tui_form_run_hook(const char *title, struct tui_form_field *f, int n, if (sel < 0) sel = 0; tui_frame(title); - attron(A_BOLD); + tui_style(TUI_HEADING); mvaddstr(3, 2, "Uppgift"); mvaddstr(3, 34, "Värde"); - attroff(A_BOLD); + tui_style_reset(); for (int i = 0; i < n; i++) { char lbl[160], val[640], line[832]; snprintf(lbl, sizeof lbl, "%s", f[i].label); @@ -1157,12 +1378,12 @@ int tui_form_run_hook(const char *title, struct tui_form_field *f, int n, if (i == sel) attron(A_REVERSE); else if (!can_edit) - attron(A_DIM); + attron(tui_style_attrs(TUI_DIM, g_colours, 0)); mvaddnstr(5 + i, 2, line, COLS - 4); if (i == sel) attroff(A_REVERSE); else if (!can_edit) - attroff(A_DIM); + attroff(tui_style_attrs(TUI_DIM, g_colours, 0)); } tui_hints(can_edit ? "upp/ned Enter = ändra F5 = uppdatera" @@ -1454,10 +1675,10 @@ static void rt_draw_fields(const struct tui_rt *t) { if (t->nfields <= 0) return; - attron(A_BOLD); + tui_style(TUI_HEADING); mvaddstr(3, 2, "Uppgift"); mvaddstr(3, 34, "Värde"); - attroff(A_BOLD); + tui_style_reset(); for (int i = 0; i < t->nfields; i++) { char lbl[160], val[640], line[832]; snprintf(lbl, sizeof lbl, "%s", t->fields[i].label); @@ -1532,14 +1753,14 @@ int tui_rt_run(const char *title, struct tui_rt *t, const char *hint) rt_draw_fields(t); - attron(A_BOLD); + tui_style(TUI_HEADING); for (int i = 0; i < t->ncols; i++) { char hdr[64]; snprintf(hdr, sizeof hdr, "%s", t->cols[i].header); tui_pad_field(hdr, sizeof hdr, t->cols[i].width); mvaddstr(y - 1, t->cols[i].x, hdr); } - attroff(A_BOLD); + tui_style_reset(); int caret_y = -1, caret_x = -1; if (t->focus >= 0 && t->focus < t->nfields) { char val[640]; @@ -1592,9 +1813,9 @@ int tui_rt_run(const char *title, struct tui_rt *t, const char *hint) continue; t->info(t->ud, top + v, ibuf, sizeof ibuf); tui_pad_field(ibuf, sizeof ibuf, t->cols[i].width); - attron(A_DIM); + attron(tui_style_attrs(TUI_DIM, g_colours, 0)); mvaddstr(y + v, t->cols[i].x, ibuf); - attroff(A_DIM); + attroff(tui_style_attrs(TUI_DIM, g_colours, 0)); } } } @@ -1607,9 +1828,9 @@ int tui_rt_run(const char *title, struct tui_rt *t, const char *hint) move(fy, 2); clrtoeol(); if (foot[0]) { - attron(A_DIM); + tui_style(TUI_STATUS); addnstr(foot, COLS - 4); - attroff(A_DIM); + tui_style_reset(); } if (g_hints) g_hints(hint); |
