diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-19 21:58:58 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-19 21:58:58 +0200 |
| commit | 4329ffb98d7eeffe2b3874d2b31cb0857d33d944 (patch) | |
| tree | 16e777dc90fa334ce3f436a3b5d3a7a9efdbd8b9 /clients/tui.c | |
| parent | 34fc2b1dce788fbb093e70c2124630167e7980ef (diff) | |
| download | bokf-4329ffb98d7eeffe2b3874d2b31cb0857d33d944.tar.gz bokf-4329ffb98d7eeffe2b3874d2b31cb0857d33d944.zip | |
tui: shared widget layer (fields, prompts, lists, forms, pager) + testsv0.1.36
Diffstat (limited to 'clients/tui.c')
| -rw-r--r-- | clients/tui.c | 1128 |
1 files changed, 1128 insertions, 0 deletions
diff --git a/clients/tui.c b/clients/tui.c new file mode 100644 index 0000000..e8508db --- /dev/null +++ b/clients/tui.c @@ -0,0 +1,1128 @@ +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <ncursesw/ncurses.h> + +#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(); +} + +void tui_keys_setup(void) +{ + /* ^Enter has no control code; enable xterm modifyOtherKeys and bind the + sequence it produces. Terminals without it ignore the request. */ + define_key("\033[27;5;13~", TUI_KEY_CTRL_ENTER); + putp("\033[>4;1m"); + fflush(stdout); +} + + +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; +} + +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 == KEY_BACKSPACE || ch == 127 || ch == 8 || + ch == KEY_DC || (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 == KEY_BACKSPACE || ch == 127 || ch == 8 || + ch == KEY_DC || (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); + wattron(win, A_BOLD); + mvwaddnstr(win, 0, 2, title, w - 4); + wattroff(win, A_BOLD); + 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); + mvwaddstr(win, h - 1, w - 13, " tryck Enter"); + 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); + mvwaddstr(win, h - 1, w - 25, " j = ja n = nej"); + 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) +{ + if (def && def != buf) + snprintf(buf, cap, "%s", def); + + int y = LINES - 3; + move(y, 2); + clrtoeol(); + attron(A_BOLD); + mvaddstr(y, 2, label); + attroff(A_BOLD); + refresh(); + int prev = curs_set(1); + int rc = tui_edit_field(y, (int)strlen(label) + 3, buf, cap, mask); + curs_set(prev == ERR ? 0 : prev); + return rc != 0; +} + +int tui_date_prompt_into(char *buf, size_t cap, const char *label, + const char *def) +{ + if (def && def != buf) + snprintf(buf, cap, "%s", def); + + int y = LINES - 3; + move(y, 2); + clrtoeol(); + attron(A_BOLD); + mvaddstr(y, 2, label); + attroff(A_BOLD); + size_t pos = (size_t)-1; + int fresh = 1; + int prev = curs_set(1); + for (;;) { + move(y, (int)strlen(label) + 3); + clrtoeol(); + addnstr(buf, (int)cap); + clrtoeol(); + size_t slen = strlen(buf); + size_t cp = pos == (size_t)-1 || pos > slen ? slen : pos; + move(y, (int)strlen(label) + 3 + (int)tui_disp_width_n(buf, cp)); + refresh(); + int ch = in_key(); + if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) { + curs_set(prev == ERR ? 0 : prev); + return 1; + } + if (ch == 27) { + curs_set(prev == ERR ? 0 : prev); + return 0; + } + tui_date_field_edit(buf, cap, &pos, ch, &fresh); + } +} + +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]); + attron(A_BOLD); + mvaddnstr(y, 2, line, COLS - 4); + attroff(A_BOLD); + 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) */ +/* ------------------------------------------------------------------ */ + +/* 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) + return -3; + 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 x = atoi(v->gotobuf); + if (x >= 1 && x <= v->n) + v->sel = x - 1; + } 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; + } + } 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 && 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 */ + } 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); + attron(A_BOLD); + printw("%s%s", v->gotolabel, v->gotobuf); + attroff(A_BOLD); + 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) +{ + 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; + for (int tmp = n; tmp >= 10; tmp /= 10) + v.numw++; + 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 r = tui_nav_key(&v, in_key(), allow_new, + archive_action != NULL, allow_toggle, 0); + if (r == -2 && !allow_refresh) + continue; + if (r != -3) + 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; + v.sel = (cursor && *cursor >= 0 && *cursor < n) ? *cursor : 0; + 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); + 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]); + if (idx == v.sel) + attron(A_REVERSE); + mvaddnstr(3 + i * 2, 4, line, COLS - 6); + if (idx == v.sel) + attroff(A_REVERSE); + } + if (v.goto_active) { + move(LINES - 2, 2); + attron(A_BOLD); + printw("%s%s", v.gotolabel, v.gotobuf); + attroff(A_BOLD); + 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; + return r; + } + } +} + +/* ------------------------------------------------------------------ */ +/* frame, status and hints */ +/* ------------------------------------------------------------------ */ + +static char g_status[512]; +static char g_right[160]; + +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(); + attron(A_BOLD); + box(stdscr, 0, 0); + mvaddstr(0, 2, title); + attroff(A_BOLD); + if (!g_status[0]) + return; + attron(A_DIM); + 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); +} + +void tui_hints(const char *s) +{ + char line[512]; + snprintf(line, sizeof line, "%s ^R = ladda om", s); + attron(A_DIM); + mvaddstr(LINES - 1, 2, line); + attroff(A_DIM); + clrtoeol(); +} + +/* ------------------------------------------------------------------ */ +/* pager (text_view) */ +/* ------------------------------------------------------------------ */ + +int tui_pager(const char *title, const char *text, const char *action_hint) +{ + 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; + } + int top = 0; + int view = LINES - 4; + for (;;) { + tui_frame(title); + 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); + } + char hint[256]; + if (action_hint) + snprintf(hint, sizeof hint, + "piltangenter/PgUp/PgDn rullar F5 = uppdatera %s " + "q = tillbaka", + action_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 (ch == 's' && action_hint) { + ret = 2; + break; + } + if (ch == KEY_DOWN && top + view < n) + top++; + else if (ch == KEY_UP && top > 0) + top--; + else if (ch == KEY_NPAGE) + top += view; + else if (ch == KEY_PPAGE) + top -= view; + else if (ch == KEY_HOME) + top = 0; + else if (ch == KEY_END) + top = n > view ? n - view : 0; + if (top + view > n) + top = n > view ? n - view : 0; + if (top < 0) + top = 0; + } + 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_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; +} + +int tui_form_run(const char *title, struct tui_form_field *f, int n, + int can_edit) +{ + int sel = 0; + for (;;) { + if (sel >= n) + sel = n - 1; + if (sel < 0) + sel = 0; + tui_frame(title); + attron(A_BOLD); + mvaddstr(3, 2, "Uppgift"); + mvaddstr(3, 34, "Värde"); + attroff(A_BOLD); + for (int i = 0; i < n; 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, "(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 { + 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(A_DIM); + mvaddnstr(5 + i, 2, line, COLS - 4); + if (i == sel) + attroff(A_REVERSE); + else if (!can_edit) + attroff(A_DIM); + } + tui_hints(can_edit + ? "upp/ned Enter = ändra F5 = uppdatera" + " ^Enter = spara Esc/q = tillbaka ^C = avsluta" + : "upp/ned F5 = uppdatera Esc/q = tillbaka" + " ^C = avsluta (endast behöriga kan ändra)"); + refresh(); + int ch = in_key(); + int nxt = tui_form_next(sel, n, ch); + if (nxt >= 0) { + sel = nxt; + continue; + } + if (nxt == TUI_FORM_REFRESH || nxt == TUI_FORM_SUBMIT || + nxt == TUI_FORM_BACK) + return nxt; + if (ch != '\n' && ch != '\r' && ch != KEY_ENTER) + continue; + 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 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, 0)) + return sel; + } else if (fl->kind == TUI_F_DATE) { + if (fl->value && tui_date_prompt_into(fl->value, fl->cap, label, + fl->value)) + return sel; + } else if (fl->kind == TUI_F_AMOUNT) { + if (fl->ore && tui_amount_prompt_into(fl->ore, label, *fl->ore)) + return 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 sel; + } + } + } +} |
