aboutsummaryrefslogtreecommitdiff
path: root/clients/tui.c
diff options
context:
space:
mode:
Diffstat (limited to 'clients/tui.c')
-rw-r--r--clients/tui.c1128
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;
+ }
+ }
+ }
+}