aboutsummaryrefslogtreecommitdiff
path: root/clients/tui.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-19 23:05:44 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-19 23:05:44 +0200
commit8da6a7e849513e1cb63410d95b121df729260d0c (patch)
tree77de7f5b3952d153e072cda8cc7c4f4bcd5f0c87 /clients/tui.c
parent730d7159e3b9b3adfba2c3779893fdb4e107647a (diff)
downloadbokf-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.c339
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);