#include #include #include #include #include #include "tui.h" #include "util.h" /* ------------------------------------------------------------------ */ /* hooks: the application supplies input, frame/hints and quit */ /* ------------------------------------------------------------------ */ static int (*g_in)(void) = getch; static void (*g_frame)(const char *title); static void (*g_hints)(const char *hints); static void (*g_quit)(void); void tui_set_input(int (*fn)(void)) { g_in = fn ? fn : getch; } void tui_set_screen(void (*frame)(const char *), void (*hints)(const char *)) { g_frame = frame; g_hints = hints; } void tui_set_quit(void (*fn)(void)) { g_quit = fn; } 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_STICKY) p++; 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; } int tui_sticky_split(char **lines, int n, char **head, int *nhead) { int h = 0, b = 0; for (int i = 0; i < n; i++) { if (lines[i][0] == TUI_MARK_STICKY) head[h++] = lines[i]; else lines[b++] = lines[i]; } if (nhead) *nhead = h; return b; } void tui_keys_setup(void) { /* ^Enter has no control code; enable xterm modifyOtherKeys level 2 (level 1 leaves control+Enter alone) and the Kitty keyboard protocol, then bind the sequences they produce. Terminals with neither keep F9 working. */ putp("\033[>4;2m"); putp("\033[>1u"); fflush(stdout); define_key("\033[27;5;13~", TUI_KEY_CTRL_ENTER); define_key("\033[13;5u", TUI_KEY_CTRL_ENTER); define_key("\033[27;2;9~", KEY_BTAB); define_key("\033[9;2u", KEY_BTAB); define_key("\033[27u", 27); define_key("\033[27;1u", 27); define_key("\033[27;5;97~", 1); define_key("\033[97;5u", 1); define_key("\033[27;5;99~", 3); define_key("\033[99;5u", 3); define_key("\033[27;5;101~", 5); define_key("\033[101;5u", 5); define_key("\033[27;5;102~", 6); define_key("\033[102;5u", 6); define_key("\033[27;5;110~", 14); define_key("\033[110;5u", 14); define_key("\033[27;5;114~", 18); define_key("\033[114;5u", 18); define_key("\033[27;5;117~", 21); define_key("\033[117;5u", 21); define_key("\033[27;5;120~", 24); define_key("\033[120;5u", 24); /* Home/End arrive as CSI or SS3 depending on the terminal */ 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); } } int tui_disp_width(const char *s) { int w = 0; for (const unsigned char *p = (const unsigned char *)s; *p; p++) if ((*p & 0xC0) != 0x80) w++; return w; } int tui_disp_width_n(const char *s, size_t n) { int w = 0; for (size_t i = 0; i < n && s[i]; i++) if (((unsigned char)s[i] & 0xC0) != 0x80) w++; return w; } int tui_num_width(int n) { int w = 1; for (int tmp = n; tmp >= 10; tmp /= 10) w++; return w; } void tui_le_init(struct tui_ledit *e, char *buf, size_t cap) { e->buf = buf; e->cap = cap; e->len = cap ? strnlen(buf, cap - 1) : 0; if (cap) buf[e->len] = '\0'; e->pos = e->len; } void tui_le_clear(struct tui_ledit *e) { e->len = 0; e->pos = 0; e->buf[0] = '\0'; } static void le_insert(struct tui_ledit *e, unsigned char ch) { if (e->len + 1 >= e->cap) return; memmove(e->buf + e->pos + 1, e->buf + e->pos, e->len - e->pos + 1); e->buf[e->pos] = (char)ch; e->pos++; e->len++; } static void le_backspace(struct tui_ledit *e) { if (e->pos == 0) return; size_t start = e->pos - 1; /* delete the whole UTF-8 code point */ while (start > 0 && ((unsigned char)e->buf[start] & 0xC0) == 0x80) start--; memmove(e->buf + start, e->buf + e->pos, e->len - e->pos + 1); e->len -= e->pos - start; e->pos = start; } static void le_delete(struct tui_ledit *e) { if (e->pos >= e->len) return; size_t end = e->pos + 1; /* delete the whole UTF-8 code point */ while (end < e->len && ((unsigned char)e->buf[end] & 0xC0) == 0x80) end++; memmove(e->buf + e->pos, e->buf + end, e->len - end + 1); e->len -= end - e->pos; } /* Consumes the key when it is an editing key; returns 1 then. */ int tui_le_key(struct tui_ledit *e, int ch) { if (ch == KEY_LEFT) { if (e->pos > 0) { e->pos--; while (e->pos > 0 && ((unsigned char)e->buf[e->pos] & 0xC0) == 0x80) e->pos--; } return 1; } if (ch == KEY_RIGHT) { if (e->pos < e->len) { e->pos++; while (e->pos < e->len && ((unsigned char)e->buf[e->pos] & 0xC0) == 0x80) e->pos++; } return 1; } if (ch == KEY_HOME || ch == 1) { /* Ctrl+A */ e->pos = 0; return 1; } if (ch == KEY_END || ch == 5) { /* Ctrl+E */ e->pos = e->len; return 1; } if (ch == KEY_DC) { le_delete(e); return 1; } if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { le_backspace(e); return 1; } if (ch == 21) { /* ^U */ tui_le_clear(e); return 1; } if (ch >= 32 && ch < 256) { le_insert(e, (unsigned char)ch); return 1; } return 0; } int tui_date_field_edit(char *buf, size_t cap, size_t *pos, int ch, int *fresh) { char d[9]; size_t n = 0; for (const char *q = buf; *q && n < 8; q++) if (*q >= '0' && *q <= '9') d[n++] = *q; size_t ci = n; /* caret in digit space; byte pos -> digit index */ if (*pos != (size_t)-1) { size_t seen = 0; for (size_t i = 0; i < *pos && buf[i]; i++) if (buf[i] >= '0' && buf[i] <= '9') seen++; ci = seen; } if (*fresh) { int editing = ch >= '0' && ch <= '9'; int moving = ch == KEY_LEFT || ch == KEY_RIGHT || ch == KEY_HOME || ch == KEY_END || ch == 1 || ch == 5; if (editing || moving) { if (editing) { n = 0; ci = 0; } *fresh = 0; /* movement keeps the date and moves the caret */ } } if (ch >= '0' && ch <= '9') { if (ci > n) ci = n; if (n < 8) { for (size_t i = n; i > ci; i--) d[i] = d[i - 1]; d[ci] = (char)ch; n++; } else { /* full field: overwrite the digit at the caret */ if (ci > 7) ci = 7; d[ci] = (char)ch; } ci++; } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { if (ci > 0) { for (size_t i = ci - 1; i + 1 < n; i++) d[i] = d[i + 1]; n--; ci--; } } else if (ch == KEY_DC) { if (ci < n) { for (size_t i = ci; i + 1 < n; i++) d[i] = d[i + 1]; n--; } } else if (ch == KEY_LEFT) { if (ci > 0) ci--; } else if (ch == KEY_RIGHT) { if (ci < n) ci++; } else if (ch == KEY_HOME || ch == 1) { ci = 0; } else if (ch == KEY_END || ch == 5) { ci = n; } else if (ch == 21) { n = 0; ci = 0; *fresh = 0; } else { return 0; /* not a date editing key */ } /* rebuild "YYYY-MM-DD" (partial while typing) */ char out[16]; size_t o = 0; for (size_t i = 0; i < n; i++) { if (i == 4 || i == 6) out[o++] = '-'; out[o++] = d[i]; } out[o] = '\0'; snprintf(buf, cap, "%s", out); size_t byte = ci; if (n > 4 && ci >= 4) byte++; if (n > 6 && ci >= 6) byte++; if (byte > strlen(buf)) byte = strlen(buf); *pos = byte; return 1; } int tui_field_edit(char *buf, size_t cap, size_t *pos, int ch, int *fresh) { struct tui_ledit e; tui_le_init(&e, buf, cap); if (*pos != (size_t)-1 && *pos <= e.len) e.pos = *pos; if (*fresh) { int editing = ch >= 32 && ch < 256; int moving = ch == KEY_LEFT || ch == KEY_RIGHT || ch == KEY_HOME || ch == KEY_END || ch == 1 || ch == 5; if (editing || moving) { if (editing) tui_le_clear(&e); *fresh = 0; /* movement keeps the content and moves the caret */ } } int handled = tui_le_key(&e, ch); *pos = e.pos; return handled; } int tui_edit_field(int y, int x, char *buf, size_t cap, int mask) { size_t pos = (size_t)-1; int fresh = 1; for (;;) { move(y, x); if (mask) { size_t len = strlen(buf); for (size_t i = 0; i < len; i++) addch('*'); } else { addnstr(buf, (int)cap); } clrtoeol(); size_t len = strlen(buf); size_t cp = pos == (size_t)-1 || pos > len ? len : pos; move(y, x + (mask ? (int)cp : tui_disp_width_n(buf, cp))); refresh(); int ch = in_key(); if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) return 1; if (ch == 27) return 0; if (ch == '\t') return 2; if (ch == KEY_BTAB) return 3; tui_field_edit(buf, cap, &pos, ch, &fresh); } } int tui_parse_kr(const char *s, int64_t *out) { if (!s || !*s) { *out = 0; return 0; } int64_t whole = 0, frac = 0; int fd = 0, seen_dot = 0; for (const char *p = s; *p; p++) { if (*p == ' ' || *p == '\t') continue; if (*p == '.' || *p == ',') { if (seen_dot) return -1; seen_dot = 1; continue; } if (*p < '0' || *p > '9') return -1; if (!seen_dot) { whole = whole * 10 + (*p - '0'); } else { if (fd >= 2) return -1; frac = frac * 10 + (*p - '0'); fd++; } } if (fd == 1) frac *= 10; *out = whole * 100 + frac; return 0; } void tui_kr_format(int64_t ore, char *buf, size_t n) { long long v = (long long)ore; int neg = v < 0; if (neg) v = -v; char digits[32]; snprintf(digits, sizeof digits, "%lld", v / 100); char out[28]; size_t o = 0; size_t len = strlen(digits); for (size_t i = 0; i < len && o + 2 < sizeof out; i++) { if (i > 0 && (len - i) % 3 == 0) out[o++] = ' '; out[o++] = digits[i]; } out[o] = '\0'; snprintf(buf, n, "%s%s,%02lld", neg ? "-" : "", out, v % 100); } void tui_kr0_format(int64_t ore, char *buf, size_t n) { long long v = (long long)(ore >= 0 ? (ore + 50) / 100 : (ore - 50) / 100); int neg = v < 0; if (neg) v = -v; char digits[32], out[40]; snprintf(digits, sizeof digits, "%lld", v); size_t o = 0, len = strlen(digits); for (size_t i = 0; i < len && o + 2 < sizeof out; i++) { if (i > 0 && (len - i) % 3 == 0) out[o++] = ' '; out[o++] = digits[i]; } out[o] = '\0'; snprintf(buf, n, "%s%s", neg ? "-" : "", out); } void tui_pad_field(char *buf, size_t cap, int width) { size_t slen = strlen(buf); int w = 0; size_t i = 0; while (i < slen && buf[i]) { unsigned char c = (unsigned char)buf[i]; size_t len = 1; if ((c & 0xE0) == 0xC0) len = 2; else if ((c & 0xF0) == 0xE0) len = 3; else if ((c & 0xF8) == 0xF0) len = 4; if (i + len > slen || w + 1 > width) break; w++; i += len; } buf[i] = '\0'; while (w < width && strlen(buf) + 1 < cap) { strcat(buf, " "); w++; } } void tui_pad_hdr(char *buf, size_t n, int width, const char *text) { int w = tui_disp_width(text); int pad = width > w ? width - w : 0; snprintf(buf, n, "%*s%s", pad, "", text); } void tui_pad_label(char *buf, size_t n, const char *text, int width) { snprintf(buf, n, "%s", text); tui_pad_field(buf, n, width); } void tui_amt_col(char *buf, size_t n, int width, int64_t ore) { char a[48]; tui_kr_format(ore, a, sizeof a); snprintf(buf, n, "%*s", width, a); } /* ------------------------------------------------------------------ */ /* modal dialogs */ /* ------------------------------------------------------------------ */ /* Center a box for a fixed number of lines and draw the frame. */ static WINDOW *dlg_open(const char *title, int h, int w) { if (w > COLS - 4) w = COLS - 4; if (w < 30) w = 30; if (h > LINES - 2) h = LINES - 2; WINDOW *win = newwin(h, w, (LINES - h) / 2, (COLS - w) / 2); box(win, 0, 0); wattrset(win, tui_style_attrs(TUI_TITLE, g_colours, 0)); mvwaddnstr(win, 0, 2, title, w - 4); wattrset(win, A_NORMAL); wrefresh(win); nodelay(stdscr, FALSE); timeout(-1); return win; } static void dlg_close(WINDOW *win) { delwin(win); touchwin(stdscr); refresh(); } void tui_message(const char *title, const char *fmt, ...) { char text[2048]; va_list ap; va_start(ap, fmt); vsnprintf(text, sizeof text, fmt, ap); va_end(ap); int lines = 1; size_t maxw = 0, cur = 0; for (const char *p = text; *p; p++) { if (*p == '\n') { lines++; if (cur > maxw) maxw = cur; cur = 0; } else { cur++; } } if (cur > maxw) maxw = cur; int h = lines + 4, w = (int)maxw + 4; WINDOW *win = dlg_open(title, h, w); int row = 2; char *copy = xstrdup(text); for (char *p = copy; p && *p; row++) { char *nl = strchr(p, '\n'); if (nl) *nl = '\0'; mvwaddnstr(win, row, 2, p, w - 4); 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); if (ch == 3) { if (g_quit) g_quit(); break; } if (ch == '\n' || ch == '\r' || ch == ' ' || ch == 27 || ch == KEY_ENTER) break; } dlg_close(win); } /* Yes/no dialog: j/Enter = yes, n/Esc = no. */ int tui_confirm(const char *title, const char *fmt, ...) { char text[2048]; va_list ap; va_start(ap, fmt); vsnprintf(text, sizeof text, fmt, ap); va_end(ap); int lines = 1; size_t maxw = 0, cur = 0; for (const char *p = text; *p; p++) { if (*p == '\n') { lines++; if (cur > maxw) maxw = cur; cur = 0; } else { cur++; } } if (cur > maxw) maxw = cur; int h = lines + 4, w = (int)maxw + 4; WINDOW *win = dlg_open(title, h, w); int row = 2; char *copy = xstrdup(text); for (char *p = copy; p && *p; row++) { char *nl = strchr(p, '\n'); if (nl) *nl = '\0'; mvwaddnstr(win, row, 2, p, w - 4); 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 (;;) { int ch = wgetch(win); if (ch == 3) { if (g_quit) g_quit(); break; } if (ch == 'j' || ch == 'J' || ch == '\n' || ch == '\r' || ch == KEY_ENTER) { yes = 1; break; } if (ch == 'n' || ch == 'N' || ch == 27) break; } dlg_close(win); return yes; } /* ------------------------------------------------------------------ */ /* prompts (write into the caller's buffer, 0 on Esc) */ /* ------------------------------------------------------------------ */ int tui_prompt_into(char *buf, size_t cap, const char *label, const char *def, int mask) { char scratch[512]; size_t scap = cap < sizeof scratch ? cap : sizeof scratch; snprintf(scratch, scap, "%s", def ? def : buf); int y = LINES - 3; move(y, 2); clrtoeol(); tui_style(TUI_HEADING); mvaddstr(y, 2, label); tui_style_reset(); refresh(); int prev = curs_set(1); int rc = tui_edit_field(y, (int)strlen(label) + 3, scratch, scap, mask); curs_set(prev == ERR ? 0 : prev); if (rc != 0) snprintf(buf, cap, "%s", scratch); return rc != 0; } int tui_date_prompt_into(char *buf, size_t cap, const char *label, const char *def) { char scratch[16]; size_t scap = cap < sizeof scratch ? cap : sizeof scratch; snprintf(scratch, scap, "%s", def ? def : buf); int y = LINES - 3; move(y, 2); clrtoeol(); tui_style(TUI_HEADING); mvaddstr(y, 2, label); tui_style_reset(); size_t pos = (size_t)-1; int fresh = 1; int prev = curs_set(1); int rc = 0; for (;;) { move(y, (int)strlen(label) + 3); clrtoeol(); addnstr(scratch, (int)scap); clrtoeol(); size_t slen = strlen(scratch); size_t cp = pos == (size_t)-1 || pos > slen ? slen : pos; move(y, (int)strlen(label) + 3 + (int)tui_disp_width_n(scratch, cp)); refresh(); int ch = in_key(); if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) { rc = 1; break; } if (ch == 27) break; tui_date_field_edit(scratch, scap, &pos, ch, &fresh); } curs_set(prev == ERR ? 0 : prev); if (rc) snprintf(buf, cap, "%s", scratch); return rc; } int tui_amount_prompt_into(int64_t *out, const char *label, int64_t def_ore) { char def[48], buf[48]; tui_kr_format(def_ore, def, sizeof def); if (!tui_prompt_into(buf, sizeof buf, label, def, 0)) return 0; if (tui_parse_kr(buf, out) != 0) { tui_message("Ogiltigt belopp", "%s", buf); return 0; } return 1; } int tui_choice_prompt(const char *label, const char *const *opts, int n, int cur) { if (n <= 0) return -1; if (cur < 0 || cur >= n) cur = 0; int y = LINES - 3; int prev = curs_set(0); for (;;) { move(y, 2); clrtoeol(); char line[512]; snprintf(line, sizeof line, "%s< %s >", label, opts[cur]); tui_style(TUI_HEADING); mvaddnstr(y, 2, line, COLS - 4); tui_style_reset(); char hint[256]; snprintf(hint, sizeof hint, "vänster/höger = välj Enter = OK " "Esc = avbryt"); g_hints ? g_hints(hint) : (void)0; refresh(); int ch = in_key(); if (ch == KEY_LEFT) cur = cur > 0 ? cur - 1 : n - 1; else if (ch == KEY_RIGHT) cur = cur + 1 < n ? cur + 1 : 0; else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) { curs_set(prev == ERR ? 0 : prev); return cur; } else if (ch == 27) { curs_set(prev == ERR ? 0 : prev); return -1; } } } /* ------------------------------------------------------------------ */ /* 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)) return -2; if (ch == TUI_KEY_CTRL_N && allow_new) return -4; if (ch == 27 || ch == 'q') return -1; return TUI_NAV_NONE; } if (v->goto_active) { if (ch >= '0' && ch <= '9' && strlen(v->gotobuf) < 9) { size_t gl = strlen(v->gotobuf); v->gotobuf[gl] = (char)ch; v->gotobuf[gl + 1] = '\0'; 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 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') { v->goto_active = 0; v->gotobuf[0] = '\0'; } return -3; } 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'; } else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) return v->sel; else if (ch == KEY_F(5)) return -2; else if (ch == TUI_KEY_CTRL_N && allow_new) return -4; else if ((ch == 'd' || ch == 'D') && allow_remove) return -6; else if (ch == 1 && allow_toggle) /* ^A */ return -7; else if (ch == 27 || ch == 'q') return -1; if (v->sel < v->top) v->top = v->sel; if (v->sel >= v->top + v->view) v->top = v->sel - v->view + 1; if (v->top < 0) v->top = 0; return -3; } /* ------------------------------------------------------------------ */ /* list and menu rendering */ /* ------------------------------------------------------------------ */ static void list_draw(const char *title, char **items, int n, const struct tui_list_nav *v, const char *hint) { 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[1024]; if (v->numw) snprintf(line, sizeof line, "%*d. %s", v->numw, idx + 1, items[idx]); else snprintf(line, sizeof line, "%2d. %s", idx + 1, items[idx]); if (idx == v->sel) attron(A_REVERSE); mvaddnstr(2 + i, 2, line, COLS - 4); if (idx == v->sel) attroff(A_REVERSE); } if (v->goto_active) { move(LINES - 2, 2); tui_style(TUI_ACCENT); printw("%s%s", v->gotolabel, v->gotobuf); tui_style_reset(); clrtoeol(); } else { move(LINES - 2, 2); clrtoeol(); } if (g_hints) g_hints(hint); refresh(); } static void list_view_sync(struct tui_list_nav *v) { if (v->view < 1) v->view = 1; if (v->sel < v->top) v->top = v->sel; if (v->sel >= v->top + v->view) v->top = v->sel - v->view + 1; } 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) { return tui_select_list_hook(title, items, n, start, allow_refresh, cursor, allow_new, archive_action, allow_toggle, NULL, NULL); } 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) { struct tui_list_nav v; memset(&v, 0, sizeof v); v.n = n; v.sel = start; if (v.sel < 0) v.sel = 0; if (n > 0 && v.sel >= n) v.sel = n - 1; v.view = LINES - 4; v.numw = tui_num_width(n); snprintf(v.gotolabel, sizeof v.gotolabel, "Gå till rad: "); for (;;) { if (cursor) *cursor = v.sel; list_view_sync(&v); char hint[320], dbuf[64] = ""; if (archive_action) snprintf(dbuf, sizeof dbuf, " d = %s", archive_action); snprintf(hint, sizeof hint, "upp/ned, 1-9 = hoppa, g = gå till, PgUp/PgDn, Home/End," " Enter%s%s%s%s Esc/q = tillbaka ^C = avsluta", allow_refresh ? " F5 = uppdatera" : "", allow_new ? " ^N = ny" : "", dbuf, allow_toggle ? " ^A = öppna/stäng" : ""); list_draw(title, items, n, &v, hint); int ch = in_key(); int r = tui_nav_key(&v, ch, allow_new, archive_action != NULL, allow_toggle, 0); if (r == TUI_NAV_NONE && key) { int h = key(ud, ch); if (h == TUI_HOOK_BACK) return -1; if (h == TUI_HOOK_REFRESH) return -2; if (h != TUI_HOOK_NONE) continue; } if (r == -2 && !allow_refresh) continue; if (r != TUI_NAV_NONE) return r; } } int tui_menu(const char *title, const char *const *items, int n, int allow_new, int *cursor) { 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 = tui_num_width(nsel); if (v.numw < 2) v.numw = 2; v.view = (LINES - 4) / 2; snprintf(v.gotolabel, sizeof v.gotolabel, "Gå till nummer: "); for (;;) { if (cursor) *cursor = v.sel; list_view_sync(&v); if (g_frame) g_frame(title); 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 + (idx - v.top) * 2, 4, line, COLS - 6); if (idx == v.sel) attroff(A_REVERSE); } if (v.goto_active) { move(LINES - 2, 2); tui_style(TUI_ACCENT); printw("%s%s", v.gotolabel, v.gotobuf); tui_style_reset(); clrtoeol(); } else { move(LINES - 2, 2); clrtoeol(); } if (g_hints) g_hints(allow_new ? "upp/ned, 1-9 = snabbval, g = gå till, PgUp/PgDn," " Home/End, Enter ^N = ny Esc/q = tillbaka" " ^C = avsluta" : "upp/ned, 1-9 = snabbval, g = gå till, PgUp/PgDn," " Home/End, Enter Esc/q = tillbaka ^C = avsluta"); refresh(); int r = tui_nav_key(&v, in_key(), allow_new, 0, 0, 1); if (r != -3) { if (cursor) *cursor = v.sel; free(flags); return r; } } } /* ------------------------------------------------------------------ */ /* frame, status and hints */ /* ------------------------------------------------------------------ */ static const char *g_form_hint; static char g_status[512]; static char g_right[160]; void tui_form_hint(const char *hint) { g_form_hint = hint; } void tui_set_status(const char *status, const char *right) { snprintf(g_status, sizeof g_status, "%s", status ? status : ""); snprintf(g_right, sizeof g_right, "%s", right ? right : ""); } void tui_frame(const char *title) { erase(); tui_style(TUI_TITLE); box(stdscr, 0, 0); mvaddstr(0, 2, title); tui_style_reset(); if (!g_status[0]) return; 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); } tui_style_reset(); } /* Flush a frame drawn by the application outside a widget loop. */ void tui_redraw(void) { refresh(); } void tui_hints(const char *s) { char line[512]; snprintf(line, sizeof line, "%s ^R = ladda om", s); tui_style(TUI_STATUS); mvaddstr(LINES - 1, 2, line); tui_style_reset(); clrtoeol(); } /* ------------------------------------------------------------------ */ /* pager (text_view) */ /* ------------------------------------------------------------------ */ int tui_pager(const char *title, const char *text, const char *action_hint) { return tui_pager_hook(title, text, action_hint, action_hint ? TUI_PAGER_SAVE : 0, NULL, NULL); } static void pager_line(int row, const char *line) { move(row, 2); clrtoeol(); if (!line) return; int st = -1; const char *s = tui_markup(line, &st); if (st == TUI_RULE) { attrset(tui_style_attrs(TUI_RULE, g_colours, 0)); for (int x = 2; x < COLS - 1; x++) mvaddch(row, x, ACS_HLINE); attrset(A_NORMAL); } else if (st >= 0) { attrset(tui_style_attrs((enum tui_style)st, g_colours, 0)); mvaddnstr(row, 2, s, COLS - 4); attrset(A_NORMAL); } else { mvaddnstr(row, 2, s, COLS - 4); } } int tui_pager_hook(const char *title, const char *text, const char *extra_hint, unsigned flags, int (*key)(void *ud, int ch), void *ud) { int ret = 0; int nlines = 0; for (const char *p = text; *p; p++) if (*p == '\n') nlines++; nlines += 1; char **lines = xcalloc(nlines ? nlines : 1, sizeof(char *)); int n = 0; char *copy = xstrdup(text); for (char *p = copy;;) { char *nl = strchr(p, '\n'); if (nl) *nl = '\0'; lines[n++] = p; if (!nl) break; p = nl + 1; } char **head = xcalloc(nlines ? nlines : 1, sizeof(char *)); int nhead = 0; int nb = tui_sticky_split(lines, n, head, &nhead); int top = 0; int view = LINES - 4; int vbody = view - nhead; if (vbody < 1) vbody = 1; for (;;) { tui_frame(title); int row = 0; for (int i = 0; i < nhead && row < view; i++, row++) pager_line(2 + row, head[i]); for (int i = 0; i < vbody && row < view; i++, row++) { if (top + i < nb) pager_line(2 + row, lines[top + i]); else pager_line(2 + row, NULL); } char hint[512]; if (extra_hint && *extra_hint) snprintf(hint, sizeof hint, "piltangenter/PgUp/PgDn rullar F5 = uppdatera %s " "q = tillbaka", extra_hint); else snprintf(hint, sizeof hint, "piltangenter/PgUp/PgDn rullar F5 = uppdatera " "q = tillbaka"); tui_hints(hint); refresh(); int ch = in_key(); if (ch == 'q' || ch == 27) break; if (ch == KEY_F(5)) { ret = 1; break; } if ((flags & TUI_PAGER_SAVE) && ch == 's') { ret = 2; break; } if (key) { int h = key(ud, ch); if (h == TUI_HOOK_BACK) break; if (h == TUI_HOOK_REFRESH) { ret = 1; break; } if (h == TUI_HOOK_STAY) continue; } if (ch == KEY_DOWN && top + vbody < nb) top++; else if (ch == KEY_UP && top > 0) top--; else if (ch == KEY_NPAGE) top += vbody; else if (ch == KEY_PPAGE) top -= vbody; else if (ch == KEY_HOME) top = 0; else if (ch == KEY_END) top = nb > vbody ? nb - vbody : 0; if (top + vbody > nb) top = nb > vbody ? nb - vbody : 0; if (top < 0) top = 0; } free(head); free(copy); free(lines); return ret; } /* ------------------------------------------------------------------ */ /* forms */ /* ------------------------------------------------------------------ */ /* Pure navigation for tui_form_run; returns the new selection or one of TUI_FORM_REFRESH/SUBMIT/BACK, TUI_NAV_NONE when the key changes nothing. */ int tui_form_next(int sel, int n, int ch) { if (ch == KEY_UP) return sel > 0 ? sel - 1 : TUI_NAV_NONE; if (ch == KEY_DOWN) return sel < n - 1 ? sel + 1 : TUI_NAV_NONE; if (ch == KEY_HOME) return 0; if (ch == KEY_END) return n - 1; if (ch == KEY_NPAGE) { int p = sel + TUI_FORM_PAGE; return p > n - 1 ? n - 1 : p; } if (ch == KEY_PPAGE) { int p = sel - TUI_FORM_PAGE; return p < 0 ? 0 : p; } if (ch == KEY_F(5)) return TUI_FORM_REFRESH; if (ch == TUI_KEY_CTRL_ENTER || ch == KEY_F(9)) return TUI_FORM_SUBMIT; if (ch == 27 || ch == 'q') return TUI_FORM_BACK; return TUI_NAV_NONE; } static int act_selectable(const struct tui_form_action *acts, int i, int na) { return acts && i >= 0 && i < na && acts[i].enabled != -1; } int tui_form_focus_clamp(int focus, int nf, const struct tui_form_action *acts, int na) { if (nf < 0) nf = 0; if (na < 0) na = 0; int n = nf + na; if (n <= 0) return -1; int pos = focus; if (pos < 0) pos = 0; if (pos >= n) pos = n - 1; for (int i = 0; i < n; i++) { if (pos < nf || act_selectable(acts, pos - nf, na)) return pos; pos = (pos + 1) % n; } return -1; } int tui_form_focus_store(int *focus, int sel, int ret) { if (focus) *focus = sel; return ret; } int tui_form_act_step(int focus, int nf, const struct tui_form_action *acts, int na, int dir) { if (nf < 0) nf = 0; if (na < 0) na = 0; int n = nf + na; if (n <= 0) return -1; int pos = focus >= 0 && focus < n ? focus : 0; for (int i = 0; i < n; i++) { pos = (pos + (dir >= 0 ? 1 : -1) + n) % n; if (pos < nf || act_selectable(acts, pos - nf, na)) return pos; } return focus; } int tui_form_act_first(int nf, const struct tui_form_action *acts, int na) { if (nf > 0) return 0; for (int i = 0; i < na; i++) if (act_selectable(acts, i, na)) return i; return -1; } int tui_form_act_page(int focus, int nf, const struct tui_form_action *acts, int na, int dir) { if (nf < 0) nf = 0; if (na < 0) na = 0; int n = nf + na; if (n <= 0) return -1; int pos = focus >= 0 && focus < n ? focus : 0; for (int i = 0; i < TUI_FORM_PAGE; i++) { int next = -1; for (int p = pos + (dir >= 0 ? 1 : -1); p >= 0 && p < n; p += (dir >= 0 ? 1 : -1)) { if (p < nf || act_selectable(acts, p - nf, na)) { next = p; break; } } if (next < 0) break; pos = next; } return pos; } int tui_form_act_key(int sel, int nf, const struct tui_form_action *acts, int na, int ch) { int n = (nf > 0 ? nf : 0) + (na > 0 ? na : 0); if (ch == KEY_UP || ch == KEY_DOWN || ch == '\t' || ch == KEY_BTAB) { int dir = ch == KEY_UP || ch == KEY_BTAB ? -1 : 1; return tui_form_act_step(sel, nf, acts, na, dir); } if (ch == KEY_HOME) return tui_form_act_first(nf, acts, na); if (ch == KEY_END) { for (int i = n - 1; i >= 0; i--) { if (i < nf || act_selectable(acts, i - nf, na)) return i; } return tui_form_act_first(nf, acts, na); } if (ch == KEY_NPAGE || ch == KEY_PPAGE) { int dir = ch == KEY_NPAGE ? 1 : -1; return tui_form_act_page(sel, nf, acts, na, dir); } return tui_form_next(sel, nf, ch); } void tui_form_act_label(const struct tui_form_action *a, char *buf, size_t n) { const char *label = a && a->label ? a->label : ""; if (a && a->enabled == 0) { const char *reason = a->disabled_reason && *a->disabled_reason ? a->disabled_reason : "otillgänglig"; snprintf(buf, n, "%s (%s)", label, reason); } else { snprintf(buf, n, "%s", label); } } /* Action rows sit under the fields, one blank line below them. */ static void form_draw_actions(const struct tui_form_action *acts, int na, int nf, int sel) { int y = 5 + nf + 1; for (int i = 0; i < na; i++) { if (y + i >= LINES - 2) break; char line[832]; tui_form_act_label(&acts[i], line, sizeof line); int dim = acts[i].enabled != 1; if (dim) attron(tui_style_attrs(TUI_DIM, g_colours, 0)); if (sel == nf + i) attron(A_REVERSE); mvaddnstr(y + i, 2, line, COLS - 4); if (sel == nf + i) attroff(A_REVERSE); if (dim) attroff(tui_style_attrs(TUI_DIM, g_colours, 0)); } } static int form_run(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 (*key)(void *ud, int ch), void *ud, int *focus) { int sel = tui_form_focus_clamp(focus ? *focus : -1, nf, acts, na); for (;;) { int ntot = nf + (na > 0 ? na : 0); if (ntot <= 0) return tui_form_focus_store(focus, sel, TUI_FORM_BACK); if (sel < 0) sel = tui_form_act_first(nf, acts, na); if (sel >= ntot) sel = ntot - 1; tui_frame(title); tui_style(TUI_HEADING); mvaddstr(3, 2, "Uppgift"); mvaddstr(3, 34, "Värde"); tui_style_reset(); for (int i = 0; i < nf; i++) { char lbl[160], val[640], line[832]; snprintf(lbl, sizeof lbl, "%s", f[i].label); tui_pad_field(lbl, sizeof lbl, 31); if (f[i].kind == TUI_F_ACTION) { snprintf(val, sizeof val, "%s", f[i].value ? f[i].value : "(lista)"); } else if (f[i].kind == TUI_F_AMOUNT && f[i].ore) { tui_kr_format(*f[i].ore, val, sizeof val); } else if (f[i].kind == TUI_F_CHOICE && f[i].choice && f[i].choices) { int c = *f[i].choice; snprintf(val, sizeof val, "%s", c >= 0 && c < f[i].nchoices ? f[i].choices[c] : ""); } else if (f[i].mask && f[i].kind == TUI_F_TEXT) { size_t vl = f[i].value ? strlen(f[i].value) : 0; if (vl >= sizeof val) vl = sizeof val - 1; memset(val, '*', vl); val[vl] = '\0'; } else { snprintf(val, sizeof val, "%s", f[i].value ? f[i].value : ""); } snprintf(line, sizeof line, "%s %s", lbl, val); if (i == sel) attron(A_REVERSE); else if (!can_edit) 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(tui_style_attrs(TUI_DIM, g_colours, 0)); } if (na > 0) form_draw_actions(acts, na, nf, sel); const char *h = g_form_hint ? g_form_hint : hint; if (!h) h = na > 0 ? (can_edit ? "upp/ned/Tab = flytta Enter = ändra/utför" " F5 = uppdatera F9 = spara" " Esc/q = tillbaka ^C = avsluta" : "upp/ned/Tab = flytta Enter = utför" " F5 = uppdatera Esc/q = tillbaka" " ^C = avsluta" " (endast behöriga kan ändra)") : (can_edit ? "upp/ned/Home/End Enter = ändra" " F5 = uppdatera F9 = spara" " Esc/q = tillbaka ^C = avsluta" : "upp/ned/Home/End F5 = uppdatera" " Esc/q = tillbaka ^C = avsluta" " (endast behöriga kan ändra)"); tui_hints(h); g_form_hint = NULL; refresh(); int ch = in_key(); int nxt = na > 0 ? tui_form_act_key(sel, nf, acts, na, ch) : tui_form_next(sel, nf, ch); if (nxt >= 0) { sel = nxt; continue; } if (nxt == TUI_FORM_REFRESH || nxt == TUI_FORM_SUBMIT || nxt == TUI_FORM_BACK) return tui_form_focus_store(focus, sel, nxt); if (key) { int hk = key(ud, ch); if (hk == TUI_HOOK_BACK) return tui_form_focus_store(focus, sel, TUI_FORM_BACK); if (hk == TUI_HOOK_REFRESH) return tui_form_focus_store(focus, sel, TUI_FORM_REFRESH); if (hk == TUI_HOOK_SUBMIT) return tui_form_focus_store(focus, sel, TUI_FORM_SUBMIT); if (hk != TUI_HOOK_NONE) continue; } if (ch != '\n' && ch != '\r' && ch != KEY_ENTER) continue; if (sel < 0) continue; /* no focusable row: only the return keys work */ if (sel >= nf) { const struct tui_form_action *act = &acts[sel - nf]; if (act->enabled == 0) { tui_message(title, "%s", act->disabled_reason && *act->disabled_reason ? act->disabled_reason : "Otillgänglig."); continue; } if (act->enabled == -1) continue; return tui_form_focus_store(focus, sel, TUI_FORM_ACTION + (sel - nf)); } if (!can_edit) { tui_message(title, "Endast behöriga kan ändra."); continue; } struct tui_form_field *fl = &f[sel]; if (fl->kind == TUI_F_ACTION) return tui_form_focus_store(focus, sel, sel); char label[192]; snprintf(label, sizeof label, "%s: ", fl->label); if (fl->kind == TUI_F_TEXT) { if (fl->value && tui_prompt_into(fl->value, fl->cap, label, fl->value, fl->mask)) return tui_form_focus_store(focus, sel, sel); } else if (fl->kind == TUI_F_DATE) { if (fl->value && tui_date_prompt_into(fl->value, fl->cap, label, fl->value)) return tui_form_focus_store(focus, sel, sel); } else if (fl->kind == TUI_F_AMOUNT) { if (fl->ore && tui_amount_prompt_into(fl->ore, label, *fl->ore)) return tui_form_focus_store(focus, sel, sel); } else if (fl->kind == TUI_F_CHOICE && fl->choice) { int c = tui_choice_prompt(label, fl->choices, fl->nchoices, *fl->choice); if (c >= 0) { *fl->choice = c; return tui_form_focus_store(focus, sel, sel); } } } } int tui_form_run(const char *title, struct tui_form_field *f, int n, int can_edit, int *focus) { return form_run(title, f, n, can_edit, NULL, 0, NULL, NULL, NULL, focus); } 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) { return form_run(title, f, n, can_edit, NULL, 0, NULL, key, ud, focus); } 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) { return form_run(title, f, nf, can_edit, acts, na, hint, NULL, NULL, focus); } /* ------------------------------------------------------------------ */ /* row table (dynamic multi-column editor with a trailing blank row) */ /* ------------------------------------------------------------------ */ 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) { memset(t, 0, sizeof *t); t->nrows = nrows > 0 ? nrows : 1; t->maxrows = maxrows; t->ncols = ncols; /* only the editable columns take part in Tab/field navigation */ for (int i = 0; i < ncols; i++) if (cols[i].kind != TUI_RT_INFO) t->neditable++; t->cols = cols; t->y = y; t->cell = cell; t->info = info; t->ud = ud; t->fresh = 1; t->pos = (size_t)-1; t->focus = -1; } void tui_rt_set_key(struct tui_rt *t, int (*key)(void *ud, int ch)) { t->key = key; } void tui_rt_set_footer(struct tui_rt *t, void (*footer)(void *ud, char *buf, size_t n)) { t->footer = footer; } void tui_rt_set_fields(struct tui_rt *t, struct tui_form_field *fields, int n) { t->fields = fields; t->nfields = n > 0 ? n : 0; t->focus = t->nfields > 0 ? 0 : -1; } int tui_rt_focus_step(int focus, int nf, int *row, int *col, int nrows, int neditable, int dir) { int ncells = nrows > 0 && neditable > 0 ? nrows * neditable : 0; int n = nf + ncells; if (n <= 0) { if (row) *row = 0; if (col) *col = 0; return -1; } int pos; if (focus >= 0 && focus < nf) { pos = focus; } else { int r = row ? *row : 0; int c = col ? *col : 0; if (r < 0) r = 0; if (r >= nrows) r = nrows - 1; if (c < 0) c = 0; if (c >= neditable) c = neditable - 1; pos = nf + r * neditable + c; } if (pos < 0 || pos >= n) pos = 0; pos = (pos + (dir >= 0 ? 1 : -1) + n) % n; if (pos < nf) return pos; if (ncells > 0) { int ci = pos - nf; if (row) *row = ci / neditable; if (col) *col = ci % neditable; return -1; } return 0; } int tui_rt_footer_row(int lines) { int y = lines - 2; return y < 2 ? 2 : y; } void tui_rt_footer_flatten(char *s) { for (; s && *s; s++) if (*s == '\n' || *s == '\r') *s = ' '; } /* Actual column index of the ecol-th editable column. Cell callbacks always receive the actual column index, also when INFO columns are interleaved. */ int tui_rt_col_of(const struct tui_rt *t, int ecol) { int ei = 0; for (int i = 0; i < t->ncols; i++) { if (t->cols[i].kind == TUI_RT_INFO) continue; if (ei == ecol) return i; ei++; } return 0; } static void rt_get(struct tui_rt *t, int row, int col, char **buf, size_t *cap) { t->cell(t->ud, row, col, buf, cap); } int tui_rt_blank_row(struct tui_rt *t, int row) { for (int i = 0; i < t->ncols; i++) { if (t->cols[i].kind == TUI_RT_INFO) continue; char *buf = NULL; size_t cap = 0; rt_get(t, row, i, &buf, &cap); if (buf && buf[0]) return 0; } return 1; } int tui_rt_normalize(struct tui_rt *t) { if (t->nrows < 1) t->nrows = 1; while (t->nrows > 1 && tui_rt_blank_row(t, t->nrows - 1) && tui_rt_blank_row(t, t->nrows - 2)) t->nrows--; if (!tui_rt_blank_row(t, t->nrows - 1) && t->nrows < t->maxrows) t->nrows++; if (t->row >= t->nrows) t->row = t->nrows - 1; if (t->col >= t->neditable) t->col = t->neditable - 1; if (t->col < 0) t->col = 0; return t->nrows; } int tui_rt_move(struct tui_rt *t, int dr, int dc) { int moved = 0; if (dr < 0 && t->row > 0) { t->row--; moved = 1; } else if (dr > 0 && t->row + 1 < t->nrows) { t->row++; moved = 1; } if (dc && t->neditable > 0) { t->col = (t->col + dc + t->neditable) % t->neditable; moved = 1; } t->fresh = 1; return moved; } /* Tab/Shift-Tab: next/previous editable cell, wrapping to the next row. */ int tui_rt_tab(struct tui_rt *t, int dir) { if (t->neditable <= 0) return 0; int moved = tui_rt_move(t, 0, dir); if (dir > 0 && t->col == 0) moved |= tui_rt_move(t, 1, 0); else if (dir < 0 && t->col == t->neditable - 1) moved |= tui_rt_move(t, -1, 0); return moved; } int tui_rt_clear(struct tui_rt *t) { if (t->row < 0 || t->row >= t->nrows) return 0; for (int i = 0; i < t->ncols; i++) { if (t->cols[i].kind == TUI_RT_INFO) continue; char *buf = NULL; size_t cap = 0; rt_get(t, t->row, i, &buf, &cap); if (buf && cap) buf[0] = '\0'; } tui_rt_normalize(t); t->fresh = 1; t->pos = (size_t)-1; return 1; } static void rt_field_value(const struct tui_form_field *f, char *val, size_t n) { if (f->kind == TUI_F_ACTION) { snprintf(val, n, "%s", f->value ? f->value : "(lista)"); } else if (f->kind == TUI_F_AMOUNT && f->ore) { tui_kr_format(*f->ore, val, n); } else if (f->kind == TUI_F_CHOICE && f->choice && f->choices) { int c = *f->choice; snprintf(val, n, "%s", c >= 0 && c < f->nchoices ? f->choices[c] : ""); } else if (f->mask && f->kind == TUI_F_TEXT) { size_t vl = f->value ? strlen(f->value) : 0; if (vl >= n) vl = n - 1; memset(val, '*', vl); val[vl] = '\0'; } else { snprintf(val, n, "%s", f->value ? f->value : ""); } } static void rt_draw_fields(const struct tui_rt *t) { if (t->nfields <= 0) return; tui_style(TUI_HEADING); mvaddstr(3, 2, "Uppgift"); mvaddstr(3, 34, "Värde"); 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); tui_pad_field(lbl, sizeof lbl, 31); rt_field_value(&t->fields[i], val, sizeof val); snprintf(line, sizeof line, "%s %s", lbl, val); if (i == t->focus) attron(A_REVERSE); mvaddnstr(5 + i, 2, line, COLS - 4); if (i == t->focus) attroff(A_REVERSE); } } /* Opens the editor for header field i; on success focus advances to the next field (or the table). Esc cancels and keeps the focus. */ static void rt_edit_field(struct tui_rt *t, int i) { if (i < 0 || i >= t->nfields) return; struct tui_form_field *fl = &t->fields[i]; char label[192]; snprintf(label, sizeof label, "%s: ", fl->label); int ok = 0; if (fl->kind == TUI_F_TEXT) { if (fl->value && tui_prompt_into(fl->value, fl->cap, label, fl->value, fl->mask)) ok = 1; } else if (fl->kind == TUI_F_DATE) { if (fl->value && tui_date_prompt_into(fl->value, fl->cap, label, fl->value)) ok = 1; } else if (fl->kind == TUI_F_AMOUNT) { if (fl->ore && tui_amount_prompt_into(fl->ore, label, *fl->ore)) ok = 1; } else if (fl->kind == TUI_F_CHOICE && fl->choice) { int c = tui_choice_prompt(label, fl->choices, fl->nchoices, *fl->choice); if (c >= 0) { *fl->choice = c; ok = 1; } } if (!ok) return; tui_rt_normalize(t); t->focus = tui_rt_focus_step(i, t->nfields, &t->row, &t->col, t->nrows, t->neditable, 1); } /* Returns -1 back, -2 refresh, -3 submit; otherwise an edited key was consumed. */ int tui_rt_run(const char *title, struct tui_rt *t, const char *hint) { int top = 0; int prev_cursor = curs_set(1); for (;;) { if (g_frame) g_frame(title); int y = t->y; if (t->nfields > 0 && y < t->nfields + 6) y = t->nfields + 6; /* fields at 5.., column header, then rows */ int view = LINES - 2 - y; if (view < 1) view = 1; if (t->row < top) top = t->row; if (t->row >= top + view) top = t->row - view + 1; if (top < 0) top = 0; rt_draw_fields(t); 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); } tui_style_reset(); int caret_y = -1, caret_x = -1; if (t->focus >= 0 && t->focus < t->nfields) { char val[640]; rt_field_value(&t->fields[t->focus], val, sizeof val); caret_y = 5 + t->focus; caret_x = 34 + (int)tui_disp_width(val); if (caret_x > COLS - 1) caret_x = COLS - 1; } int ei = 0; for (int i = 0; i < t->ncols; i++) { int focused_col = (t->focus < 0 && t->cols[i].kind != TUI_RT_INFO && ei == t->col); if (t->cols[i].kind != TUI_RT_INFO) ei++; if (t->cols[i].kind == TUI_RT_INFO) { continue; } for (int v = 0; v < view && top + v < t->nrows; v++) { int row = top + v; char *buf = NULL; size_t cap = 0; rt_get(t, row, i, &buf, &cap); char padded[512]; snprintf(padded, sizeof padded, "%s", buf ? buf : ""); tui_pad_field(padded, sizeof padded, t->cols[i].width); int vy = y + v; if (focused_col && row == t->row) { attron(A_REVERSE); caret_y = vy; size_t slen = strlen(buf ? buf : ""); size_t cp = (t->fresh || t->pos == (size_t)-1 || t->pos > slen) ? slen : t->pos; int cw = tui_disp_width_n(buf ? buf : "", cp); caret_x = t->cols[i].x + (cw > t->cols[i].width ? t->cols[i].width : cw); } mvaddstr(vy, t->cols[i].x, padded); if (focused_col && row == t->row) attroff(A_REVERSE); } } if (t->info) { for (int v = 0; v < view && top + v < t->nrows; v++) { char ibuf[512]; for (int i = 0; i < t->ncols; i++) { if (t->cols[i].kind != TUI_RT_INFO) continue; t->info(t->ud, top + v, ibuf, sizeof ibuf); tui_pad_field(ibuf, sizeof ibuf, t->cols[i].width); attron(tui_style_attrs(TUI_DIM, g_colours, 0)); mvaddstr(y + v, t->cols[i].x, ibuf); attroff(tui_style_attrs(TUI_DIM, g_colours, 0)); } } } char foot[2048]; foot[0] = '\0'; if (t->footer) t->footer(t->ud, foot, sizeof foot); tui_rt_footer_flatten(foot); int fy = tui_rt_footer_row(LINES); move(fy, 2); clrtoeol(); if (foot[0]) { tui_style(TUI_STATUS); addnstr(foot, COLS - 4); tui_style_reset(); } if (g_hints) g_hints(hint); if (caret_y >= 0) move(caret_y, caret_x); refresh(); int ch = in_key(); if (ch == 27 || ch == 'q') { curs_set(prev_cursor == ERR ? 0 : prev_cursor); return -1; } if (ch == KEY_F(5)) { curs_set(prev_cursor == ERR ? 0 : prev_cursor); return -2; } if (ch == TUI_KEY_CTRL_ENTER || ch == KEY_F(9)) { curs_set(prev_cursor == ERR ? 0 : prev_cursor); return -3; } if (t->key) { int h = t->key(t->ud, ch); if (h == TUI_HOOK_BACK) { curs_set(prev_cursor == ERR ? 0 : prev_cursor); return -1; } if (h == TUI_HOOK_REFRESH) { curs_set(prev_cursor == ERR ? 0 : prev_cursor); return -2; } if (h == TUI_HOOK_SUBMIT) { curs_set(prev_cursor == ERR ? 0 : prev_cursor); return -3; } if (h == TUI_HOOK_STAY) continue; } if (ch == '\t' || ch == KEY_BTAB) { tui_rt_normalize(t); t->focus = tui_rt_focus_step(t->focus, t->nfields, &t->row, &t->col, t->nrows, t->neditable, ch == '\t' ? 1 : -1); t->fresh = 1; t->pos = (size_t)-1; continue; } if (ch == KEY_UP || ch == KEY_DOWN) { if (t->focus >= 0) { if (ch == KEY_UP && t->focus > 0) t->focus--; else if (ch == KEY_DOWN && t->focus + 1 < t->nfields) t->focus++; } else { tui_rt_move(t, ch == KEY_UP ? -1 : 1, 0); } continue; } if (t->focus >= 0) { if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) { rt_edit_field(t, t->focus); continue; } if (ch < 32 || ch >= 256) continue; /* typing is meant for the rows: hand the focus to the table */ t->focus = -1; t->col = 0; t->fresh = 1; t->pos = (size_t)-1; } if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) { tui_rt_move(t, 1, 0); continue; } if (ch == TUI_KEY_CTRL_X) { tui_rt_clear(t); continue; } if (t->row < 0) t->row = 0; if (t->row >= t->nrows) t->row = t->nrows - 1; if (t->col < 0) t->col = 0; if (t->col >= t->neditable) t->col = t->neditable - 1; int ci = tui_rt_col_of(t, t->col); char *buf = NULL; size_t cap = 0; rt_get(t, t->row, ci, &buf, &cap); if (!buf) continue; if (t->cols[ci].kind == TUI_RT_DATE) tui_date_field_edit(buf, cap, &t->pos, ch, &t->fresh); else tui_field_edit(buf, cap, &t->pos, ch, &t->fresh); tui_rt_normalize(t); } }