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 | |
| parent | 34fc2b1dce788fbb093e70c2124630167e7980ef (diff) | |
| download | bokf-4329ffb98d7eeffe2b3874d2b31cb0857d33d944.tar.gz bokf-4329ffb98d7eeffe2b3874d2b31cb0857d33d944.zip | |
tui: shared widget layer (fields, prompts, lists, forms, pager) + testsv0.1.36
| -rw-r--r-- | AGENTS.md | 7 | ||||
| -rw-r--r-- | Makefile | 14 | ||||
| -rw-r--r-- | clients/bokftui.c | 1918 | ||||
| -rw-r--r-- | clients/tui.c | 1128 | ||||
| -rw-r--r-- | clients/tui.h | 131 | ||||
| -rw-r--r-- | docs/STATE.md | 6 | ||||
| -rw-r--r-- | docs/TUI-GUIDELINES.md | 23 | ||||
| -rw-r--r-- | tests/test_tui.c | 221 |
8 files changed, 1967 insertions, 1481 deletions
@@ -66,8 +66,11 @@ Read `docs/STATE.md` first for status, locked decisions, pending decisions and the prioritized backlog. Then read `docs/TUI-GUIDELINES.md` and follow it. Summary: one shared line editor, universal keys (Ctrl+N new, F5 refresh, Esc/q back, digits jump, `g` goto), forms behave the same everywhere, hints always visible, errors shown as -`CODE: message`. Prefer reusing the shared helpers (`select_list`, `menu`, -`field_edit`, `pad_field`, `message`, `show_error`) over new local loops. +`CODE: message`. Screens call only the widget layer (`clients/tui.[ch]`: +`tui_menu`, `tui_select_list`, `tui_prompt_into`, `tui_edit_field`, +`tui_pad_field`, `tui_message`, `show_error`) — never ncurses directly and +never a local one-off loop. If no widget fits, extend the widget layer +first (spec in `TUI-GUIDELINES.md` + tests in `tests/test_tui.c`). ## Verifying @@ -35,7 +35,8 @@ $(BUILD)/bokfctl: $(BUILD)/clients/bokfctl.o $(BUILD)/clients/client.o \ $(BUILD)/vendor/sha256.o $(CC) $(CFLAGS) -o $@ $^ -lm $(SSL_LIBS) -$(BUILD)/bokftui: $(BUILD)/clients/bokftui.o $(BUILD)/clients/client.o \ +$(BUILD)/bokftui: $(BUILD)/clients/bokftui.o $(BUILD)/clients/tui.o \ + $(BUILD)/clients/client.o \ $(BUILD)/src/util.o $(BUILD)/src/log.o $(BUILD)/src/formula.o \ $(BUILD)/vendor/yyjson.o $(BUILD)/vendor/sha256.o $(CC) $(CFLAGS) -o $@ $^ -lm -lncursesw $(SSL_LIBS) @@ -44,8 +45,14 @@ $(BUILD)/test_core: $(BUILD)/tests/test_core.o $(BUILD)/clients/client.o \ $(CORE_OBJ) $(VENDOR_OBJ) $(CC) $(CFLAGS) -o $@ $^ -lm $(SSL_LIBS) -test: $(BUILD)/test_core +$(BUILD)/test_tui: $(BUILD)/tests/test_tui.o $(BUILD)/clients/tui.o \ + $(BUILD)/src/util.o $(BUILD)/src/log.o $(BUILD)/vendor/yyjson.o \ + $(BUILD)/vendor/sha256.o + $(CC) $(CFLAGS) -o $@ $^ -lm -lncursesw + +test: $(BUILD)/test_core $(BUILD)/test_tui $(BUILD)/test_core + $(BUILD)/test_tui $(BUILD)/vendor/%.o: vendor/%.c @mkdir -p $(dir $@) @@ -65,7 +72,8 @@ $(BUILD)/tests/%.o: tests/%.c -include $(VENDOR_OBJ:.o=.d) $(CORE_OBJ:.o=.d) $(BUILD)/src/bokfd.d \ $(BUILD)/clients/bokfctl.d $(BUILD)/clients/bokftui.d \ - $(BUILD)/clients/client.d $(BUILD)/tests/test_core.d + $(BUILD)/clients/tui.d $(BUILD)/clients/client.d \ + $(BUILD)/tests/test_core.d $(BUILD)/tests/test_tui.d install: all install -d $(DESTDIR)/usr/local/bin $(DESTDIR)/usr/local/share/bokf diff --git a/clients/bokftui.c b/clients/bokftui.c index 9ee5859..0967f51 100644 --- a/clients/bokftui.c +++ b/clients/bokftui.c @@ -13,6 +13,7 @@ #include <unistd.h> #include "client.h" +#include "tui.h" #include "formula.h" #include "util.h" #include "version.h" @@ -46,6 +47,11 @@ static int ui_getch(void) return ch; } +static void request_quit(void) +{ + g_quit = 1; +} + struct app { struct client_conn conn; char socket[256]; @@ -139,10 +145,8 @@ static int jbool_val(const char *resp, const char *path, int def) /* ui primitives */ /* ------------------------------------------------------------------ */ -static char g_status[512]; static char g_server_version[32]; -static int disp_width(const char *s); static void tui_log(const char *fmt, ...) __attribute__((format(printf, 1, 2))); @@ -151,759 +155,12 @@ static void bokslut_screen(struct app *a); static void arsredovisning_screen(struct app *a); static char *rpc_dry(struct app *a, const char *cmd, const char *args); static int64_t vouchers_new(struct app *a); -static void acct_cache_free(void); -static void acct_cache_load(struct app *a); -static const char *acct_name(struct app *a, const char *number); -static int acct_exists(struct app *a, const char *number); - -/* ---- shared line editor used by every form and prompt ---- */ -struct ledit { - char *buf; - size_t cap; - size_t len; - size_t pos; /* caret position in bytes */ -}; - -static size_t disp_width_n(const char *s, size_t n) -{ - size_t w = 0; - for (size_t i = 0; i < n && s[i]; i++) - if (((unsigned char)s[i] & 0xC0) != 0x80) - w++; - return w; -} - -static void le_init(struct ledit *e, char *buf, size_t cap) -{ - e->buf = buf; - e->cap = cap; - e->len = strlen(buf); - e->pos = e->len; -} - -static void le_clear(struct ledit *e) -{ - e->len = 0; - e->pos = 0; - e->buf[0] = '\0'; -} - -static void le_insert(struct 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 ledit *e) -{ - if (e->pos == 0) - return; - memmove(e->buf + e->pos - 1, e->buf + e->pos, e->len - e->pos + 1); - e->pos--; - e->len--; -} - -static void le_delete(struct ledit *e) -{ - if (e->pos >= e->len) - return; - memmove(e->buf + e->pos, e->buf + e->pos + 1, e->len - e->pos); - e->len--; -} - -/* Consumes the key when it is an editing key; returns 1 then. */ -static int le_key(struct 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 */ - le_clear(e); - return 1; - } - if (ch >= 32 && ch < 256) { - le_insert(e, (unsigned char)ch); - return 1; - } - return 0; -} - -/* Date field: only digits are accepted, dashes are inserted automatically - ("20260215" -> "2026-02-15"). The caret is tracked in digit space and - reported back as a byte offset so forms render it like any field. */ -static int 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; -} - -/* Prompt for a date with the date editor. Returns a static buffer or NULL - on Esc. */ -static char *date_prompt(const char *label, const char *def) -{ - static char buf[16]; - snprintf(buf, sizeof buf, "%s", def ? 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)sizeof buf); - clrtoeol(); - size_t slen = strlen(buf); - size_t cp = pos == (size_t)-1 || pos > slen ? slen : pos; - move(y, (int)strlen(label) + 3 + (int)disp_width_n(buf, cp)); - refresh(); - int ch = ui_getch(); - if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) { - curs_set(prev == ERR ? 0 : prev); - return buf; - } - if (ch == 27) { - curs_set(prev == ERR ? 0 : prev); - return NULL; - } - date_field_edit(buf, sizeof buf, &pos, ch, &fresh); - } -} - -/* Applies a key to a form field. *pos is the caret, (size_t)-1 = at the - end. When *fresh is set, the first editing key replaces the content. */ -static int field_edit(char *buf, size_t cap, size_t *pos, int ch, int *fresh) -{ - struct ledit e; - 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) - le_clear(&e); - *fresh = 0; /* movement keeps the content and moves the caret */ - } - } - int handled = le_key(&e, ch); - *pos = e.pos; - return handled; -} - -static void frame(const char *title) -{ - erase(); - attron(A_BOLD); - box(stdscr, 0, 0); - mvaddstr(0, 2, title); - attroff(A_BOLD); - if (g_status[0]) { - attron(A_DIM); - mvaddnstr(1, 2, g_status, COLS - 4); - char ver[96]; - if (g_server_version[0]) - snprintf(ver, sizeof ver, "tui %s srv %s", BOKF_VERSION, - g_server_version); - else - snprintf(ver, sizeof ver, "tui %s", BOKF_VERSION); - int x = COLS - 3 - disp_width(ver); - if (x > disp_width(g_status) + 6) - mvaddstr(1, x, ver); - attroff(A_DIM); - } -} - -static void 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(); -} - -static void message(const char *title, const char *fmt, ...) - __attribute__((format(printf, 2, 3))); - -static void message(const char *title, const char *fmt, ...) -{ - char text[1024]; - va_list ap; - va_start(ap, fmt); - vsnprintf(text, sizeof text, fmt, ap); - va_end(ap); - int h = 5, w = (int)strlen(text) + 4; - if (w > COLS - 4) - w = COLS - 4; - if (w < 30) - w = 30; - 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); - mvwaddnstr(win, 2, 2, text, w - 4); - mvwaddstr(win, h - 1, w - 12, " tryck Enter"); - wrefresh(win); - nodelay(stdscr, FALSE); - timeout(-1); - for (;;) { - int ch = wgetch(win); - if (ch == 3) { - g_quit = 1; - break; - } - if (ch == '\n' || ch == '\r' || ch == ' ' || ch == 27 || ch == KEY_ENTER) - break; - } - delwin(win); - touchwin(stdscr); - refresh(); -} - -/* Line editor inside the current window at (y,x) with fixed label already - drawn. Returns 1 on Enter, 2 on Tab, 3 on Shift-Tab, 0 on Esc. */ -static int 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 : (int)disp_width_n(buf, cp))); - refresh(); - int ch = ui_getch(); - 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; - field_edit(buf, cap, &pos, ch, &fresh); - } -} - -static char *prompt(const char *label, const char *def, int mask) -{ - static char buf[512]; - snprintf(buf, sizeof buf, "%s", def ? 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 = edit_field(y, (int)strlen(label) + 3, buf, sizeof buf, mask); - curs_set(prev == ERR ? 0 : prev); - if (rc == 0) - return NULL; - return buf; -} - -static int text_view(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 (;;) { - 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"); - hints(hint); - refresh(); - int ch = ui_getch(); - 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; -} - -static int menu(const char *title, const char *const *items, int n, - int allow_new, int *cursor) -{ - int sel = (cursor && *cursor >= 0 && *cursor < n) ? *cursor : 0; - int top = 0; - char gotobuf[12] = ""; - int goto_active = 0; - for (;;) { - if (cursor) - *cursor = sel; - int view = (LINES - 4) / 2; - if (view < 1) - view = 1; - if (sel < top) - top = sel; - if (sel >= top + view) - top = sel - view + 1; - frame(title); - for (int i = 0; i < view && top + i < n; i++) { - int idx = top + i; - char line[512]; - snprintf(line, sizeof line, "%2d. %s", idx + 1, items[idx]); - if (idx == sel) - attron(A_REVERSE); - mvaddnstr(3 + i * 2, 4, line, COLS - 6); - if (idx == sel) - attroff(A_REVERSE); - } - if (goto_active) { - move(LINES - 2, 2); - attron(A_BOLD); - printw("Gå till nummer: %s", gotobuf); - attroff(A_BOLD); - clrtoeol(); - } else { - move(LINES - 2, 2); - clrtoeol(); - } - 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 ch = ui_getch(); - if (goto_active) { - if (ch >= '0' && ch <= '9' && strlen(gotobuf) < 9) { - size_t gl = strlen(gotobuf); - gotobuf[gl] = (char)ch; - gotobuf[gl + 1] = '\0'; - int v = atoi(gotobuf); - if (v >= 1 && v <= n) - sel = v - 1; - } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { - size_t gl = strlen(gotobuf); - if (gl) - gotobuf[gl - 1] = '\0'; - if (gotobuf[0]) { - int v = atoi(gotobuf); - if (v >= 1 && v <= n) - sel = v - 1; - } - } else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER || - ch == 27 || ch == 'g' || ch == 'G') { - goto_active = 0; - gotobuf[0] = '\0'; - } - continue; - } - if (ch == KEY_UP && sel > 0) - sel--; - else if (ch == KEY_DOWN && sel < n - 1) - sel++; - else if (ch == KEY_NPAGE) - sel = sel + view < n ? sel + view : n - 1; - else if (ch == KEY_PPAGE) - sel = sel - view > 0 ? sel - view : 0; - else if (ch == KEY_HOME) - sel = 0; - else if (ch == KEY_END) - sel = n - 1; - else if (ch >= '1' && ch <= '9' && ch - '1' < n) { - sel = ch - '1'; - if (cursor) - *cursor = sel; - return sel; - } - else if (ch == 'g' || ch == 'G') { - goto_active = 1; - gotobuf[0] = '\0'; - } else if (ch == KEY_CTRL_N && allow_new) - return -4; - else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) - return sel; - else if (ch == 27 || ch == 'q') - return g_quit ? -5 : -1; - } -} - -static int 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) -{ - int sel = start; - if (sel < 0) - sel = 0; - if (n > 0 && sel >= n) - sel = n - 1; - int top = 0; - int view = LINES - 4; - char gotobuf[12] = ""; - int goto_active = 0; - for (;;) { - if (cursor) - *cursor = sel; - frame(title); - if (sel < top) - top = sel; - if (sel >= top + view) - top = sel - view + 1; - int numw = 1; - for (int tmp = n; tmp >= 10; tmp /= 10) - numw++; - if (numw > 9) - numw = 9; - for (int i = 0; i < view && top + i < n; i++) { - char line[1024]; - snprintf(line, sizeof line, "%*d. %s", numw, top + i + 1, - items[top + i]); - if (top + i == sel) - attron(A_REVERSE); - mvaddnstr(2 + i, 2, line, COLS - 4); - if (top + i == sel) - attroff(A_REVERSE); - } - if (goto_active) { - move(LINES - 2, 2); - attron(A_BOLD); - printw("Gå till rad: %s", gotobuf); - attroff(A_BOLD); - clrtoeol(); - } else { - move(LINES - 2, 2); - clrtoeol(); - } - { - char hint[256], dbuf[48] = ""; - 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", - allow_refresh ? " F5 = uppdatera" : "", - allow_new ? " ^N = ny" : "", dbuf, - allow_toggle ? " ^A = öppna/stäng" : ""); - snprintf(hint + strlen(hint), sizeof hint - strlen(hint), - " ^C = avsluta"); - hints(hint); - } - refresh(); - int ch = ui_getch(); - if (goto_active) { - if (ch >= '0' && ch <= '9' && strlen(gotobuf) < 9) { - size_t gl = strlen(gotobuf); - gotobuf[gl] = (char)ch; - gotobuf[gl + 1] = '\0'; - int v = atoi(gotobuf); - if (v >= 1 && v <= n) - sel = v - 1; - } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { - size_t gl = strlen(gotobuf); - if (gl) - gotobuf[gl - 1] = '\0'; - if (gotobuf[0]) { - int v = atoi(gotobuf); - if (v >= 1 && v <= n) - sel = v - 1; - } - } else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER || - ch == 27 || ch == 'g' || ch == 'G') { - goto_active = 0; - gotobuf[0] = '\0'; - } - continue; - } - if (ch == KEY_UP && sel > 0) - sel--; - else if (ch == KEY_DOWN && sel < n - 1) - sel++; - else if (ch == KEY_NPAGE) - sel = sel + view < n ? sel + view : n - 1; - else if (ch == KEY_PPAGE) - sel = sel - view > 0 ? sel - view : 0; - else if (ch == KEY_HOME) - sel = 0; - else if (ch == KEY_END) - sel = n - 1; - else if (ch >= '1' && ch <= '9') { - int idx = ch - '1'; - if (idx < n) - sel = idx; /* goto row, Enter opens */ - } else if (ch == 'g' || ch == 'G') { - goto_active = 1; - gotobuf[0] = '\0'; - } else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) - return sel; - else if (allow_refresh && ch == KEY_F(5)) - return -2; - else if (allow_new && ch == KEY_CTRL_N) - return -4; - else if (archive_action && (ch == 'd' || ch == 'D') && n > 0) - return -6; - else if (allow_toggle && ch == 1 && n > 0) - return -7; - else if (ch == 27 || ch == 'q') - return g_quit ? -5 : -1; - } -} - -static int 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; -} - -static void 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); -} /* Whole kronor with space thousands, öre truncated like the blankett. */ static void kr_whole(int64_t ore, char *buf, size_t n) { char tmp[48]; - kr_format((ore / 100) * 100, tmp, sizeof tmp); + tui_kr_format((ore / 100) * 100, tmp, sizeof tmp); char *comma = strchr(tmp, ','); if (comma) *comma = '\0'; @@ -951,40 +208,7 @@ static void replace_x_into(const char *src, double x, char *dst, size_t cap) /* Display width in terminal columns, counting UTF-8 lead bytes. Good enough for Swedish text; combining marks are ignored. */ -static int 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; -} -static void 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++; - } -} /* ------------------------------------------------------------------ */ /* app helpers */ @@ -1075,8 +299,15 @@ static void app_refresh_context(struct app *a) static void update_status(struct app *a) { - snprintf(g_status, sizeof g_status, "%s | %s - %s | %s | %s", - a->org_name, a->fy_start, a->fy_end, a->role, a->username); + char st[512], right[160]; + snprintf(st, sizeof st, "%s | %s - %s | %s | %s", a->org_name, + a->fy_start, a->fy_end, a->role, a->username); + if (g_server_version[0]) + snprintf(right, sizeof right, "tui %s srv %s", BOKF_VERSION, + g_server_version); + else + snprintf(right, sizeof right, "tui %s", BOKF_VERSION); + tui_set_status(st, right); } @@ -1120,107 +351,63 @@ static int64_t fy_new_form(struct app *a) else snprintf(label, sizeof label, "%d/%d", sy, ey); - int field = 0; - int field_fresh = 1; - size_t field_pos = (size_t)-1; - curs_set(1); + struct tui_form_field ff[3]; for (;;) { - frame("Nytt räkenskapsår"); - int caret_y = -1, caret_x = -1; - attron(A_BOLD); - mvaddstr(4, 4, "Etikett"); - mvaddstr(6, 4, "Startdatum"); - mvaddstr(8, 4, "Slutdatum"); - attroff(A_BOLD); - { - const char *vals[3] = { label, start, end }; - int ys[3] = { 4, 6, 8 }; - for (int k = 0; k < 3; k++) { - char padded[64]; - snprintf(padded, sizeof padded, "%-24s", vals[k]); - if (field == k) { - attron(A_REVERSE); - caret_y = ys[k]; - size_t slen = strlen(vals[k]); - size_t cp = (field_fresh || field_pos == (size_t)-1 || - field_pos > slen) - ? slen - : field_pos; - caret_x = 16 + (int)disp_width_n(vals[k], cp); - } - mvaddstr(ys[k], 16, padded); - if (field == k) - attroff(A_REVERSE); - } - } - hints("Tab = byta fält F5 = validera ^Enter = skapa Esc = avbryt"); - if (caret_y >= 0) - move(caret_y, caret_x); - refresh(); - - int ch = ui_getch(); - if (ch == 27) { - curs_set(0); + memset(ff, 0, sizeof ff); + ff[0].label = "Etikett"; + ff[0].value = label; + ff[0].cap = sizeof label; + ff[0].kind = TUI_F_TEXT; + ff[1].label = "Startdatum"; + ff[1].value = start; + ff[1].cap = sizeof start; + ff[1].kind = TUI_F_DATE; + ff[2].label = "Slutdatum"; + ff[2].value = end; + ff[2].cap = sizeof end; + ff[2].kind = TUI_F_DATE; + int r = tui_form_run("Nytt räkenskapsår", ff, 3, 1); + if (r == TUI_FORM_BACK) return 0; - } - if (ch == '\t') { - field = (field + 1) % 3; - field_fresh = 1; + if (r != TUI_FORM_REFRESH && r != TUI_FORM_SUBMIT) + continue; /* a field was edited; keep the form up */ + if (!label[0]) { + tui_message("Räkenskapsår", "Etiketten får inte vara tom."); continue; } - if (ch == KEY_BTAB) { - field = (field + 2) % 3; - field_fresh = 1; + if (!util_parse_iso_date(start) || !util_parse_iso_date(end)) { + tui_message("Räkenskapsår", + "Start- och slutdatum måste vara ÅÅÅÅ-MM-DD."); continue; } - if (ch == KEY_F(5) || ch == KEY_CTRL_ENTER || ch == KEY_F(9)) { - if (!label[0]) { - message("Räkenskapsår", "Etiketten får inte vara tom."); - continue; - } - if (!util_parse_iso_date(start) || !util_parse_iso_date(end)) { - message("Räkenskapsår", - "Start- och slutdatum måste vara ÅÅÅÅ-MM-DD."); - continue; - } - if (strcmp(start, end) >= 0) { - message("Räkenskapsår", + if (strcmp(start, end) >= 0) { + tui_message("Räkenskapsår", "Startdatum måste ligga före slutdatum."); - continue; - } - if (ch == KEY_F(5)) { - message("Validering OK", "%s %s - %s", label, start, end); - continue; - } - yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); - yyjson_mut_val *o = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, o); - yyjson_mut_obj_add_strcpy(d, o, "label", label); - yyjson_mut_obj_add_strcpy(d, o, "start_date", start); - yyjson_mut_obj_add_strcpy(d, o, "end_date", end); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - resp = client_rpc(&a->conn, "fiscal_year.open", a->session, a->org, - args); - free(args); - if (resp && client_ok(resp)) { - int64_t id = jint_val(resp, "result.id", 0); - message("Räkenskapsår", "%s skapat.", label); - free(resp); - curs_set(0); - return id; - } - show_error("Kunde inte skapa räkenskapsåret", resp); - free(resp); continue; } - char *buf = field == 0 ? label : (field == 1 ? start : end); - size_t cap = field == 0 ? sizeof label - : (field == 1 ? sizeof start : sizeof end); - if (field == 0) - field_edit(buf, cap, &field_pos, ch, &field_fresh); - else - date_field_edit(buf, cap, &field_pos, ch, &field_fresh); + if (r == TUI_FORM_REFRESH) { + tui_message("Validering OK", "%s %s - %s", label, start, end); + continue; + } + yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); + yyjson_mut_val *o = yyjson_mut_obj(d); + yyjson_mut_doc_set_root(d, o); + yyjson_mut_obj_add_strcpy(d, o, "label", label); + yyjson_mut_obj_add_strcpy(d, o, "start_date", start); + yyjson_mut_obj_add_strcpy(d, o, "end_date", end); + char *args = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + char *oresp = client_rpc(&a->conn, "fiscal_year.open", a->session, + a->org, args); + free(args); + if (oresp && client_ok(oresp)) { + int64_t id = jint_val(oresp, "result.id", 0); + tui_message("Räkenskapsår", "%s skapat.", label); + free(oresp); + return id; + } + show_error("Kunde inte skapa räkenskapsåret", oresp); + free(oresp); } } @@ -1273,7 +460,7 @@ static void select_fiscal_year(struct app *a) closed[i] = strcmp(status ? status : "", "closed") == 0; char line[256], stbuf[16]; snprintf(stbuf, sizeof stbuf, "%s", closed[i] ? "stängd" : "öppen"); - pad_field(stbuf, sizeof stbuf, 6); + tui_pad_field(stbuf, sizeof stbuf, 6); snprintf(line, sizeof line, "%-25s %s %s", ranges[i], stbuf, labels[i]); lines[i] = xstrdup(line); @@ -1287,7 +474,7 @@ static void select_fiscal_year(struct app *a) int start = cursor >= 0 ? cursor : sel_default; if (start >= (int)n) start = (int)n - 1; - int sel = select_list("Räkenskapsår", lines, (int)n, start, 0, + int sel = tui_select_list("Räkenskapsår", lines, (int)n, start, 0, &cursor, 1, NULL, 1); for (size_t i = 0; i < n; i++) free(lines[i]); @@ -1309,7 +496,7 @@ static void select_fiscal_year(struct app *a) snprintf(label, sizeof label, "%s räkenskapsåret %s? (j/n): ", closed[cursor] ? "Återöppna" : "Avsluta", labels[cursor]); - char *ans = prompt(label, "n", 0); + char ansbuf[512]; char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, label, "n", 0) ? ansbuf : NULL; if (ans && (ans[0] == 'j' || ans[0] == 'J')) { char args[64]; snprintf(args, sizeof args, @@ -1321,7 +508,7 @@ static void select_fiscal_year(struct app *a) : "fiscal_year.close", a->session, a->org, args); if (r && client_ok(r)) - message("Räkenskapsår", "Räkenskapsåret %s är %s.", + tui_message("Räkenskapsår", "Räkenskapsåret %s är %s.", labels[cursor], closed[cursor] ? "återöppnat" : "avslutat"); else @@ -1362,11 +549,11 @@ static void show_error(const char *title, const char *resp) char *code = jstr_dup(resp, "error.code"); char *msg = jstr_dup(resp, "error.message"); if ((!code || !*code) && (!msg || !*msg)) { - message(title, "%s", resp && *resp + tui_message(title, "%s", resp && *resp ? resp : "Inget svar från servern (kör daemonen?)"); } else { - message(title, "%s: %s", code && *code ? code : "fel", + tui_message(title, "%s: %s", code && *code ? code : "fel", msg && *msg ? msg : "okänt fel"); } free(code); @@ -1442,7 +629,7 @@ static char *file_browser(struct app *a, const char *start_dir) for (;;) { DIR *d = opendir(dir); if (!d) { - message("Filväljare", "Kan inte öppna %s", dir); + tui_message("Filväljare", "Kan inte öppna %s", dir); return NULL; } struct fentry *ents = NULL; @@ -1481,7 +668,7 @@ static char *file_browser(struct app *a, const char *start_dir) } char title[1200]; snprintf(title, sizeof title, "Välj fil — %.200s", dir); - int sel = select_list(title, items, (int)n + 1, 0, 1, NULL, 0, NULL, 0); + int sel = tui_select_list(title, items, (int)n + 1, 0, 1, NULL, 0, NULL, 0); for (size_t i = 0; i <= n; i++) free(items[i]); free(items); @@ -1561,7 +748,7 @@ static void attachment_download(struct app *a, int64_t att_id) unsigned char *data = NULL; size_t n = 0; if (!b64 || util_b64_decode(b64, strlen(b64), &data, &n) != 0) { - message("Underlag", "Kunde inte avkoda innehållet."); + tui_message("Underlag", "Kunde inte avkoda innehållet."); goto done; } const char *home = getenv("HOME"); @@ -1571,7 +758,7 @@ static void attachment_download(struct app *a, int64_t att_id) if (stat(dl, &sb) != 0 || !S_ISDIR(sb.st_mode)) snprintf(dl, sizeof dl, "%s", home && *home ? home : "."); snprintf(def, sizeof def, "%s/%s", dl, fn && *fn ? fn : "underlag"); - char *path = prompt("Spara underlag: ", def, 0); + char pathbuf[512]; char *path = tui_prompt_into(pathbuf, sizeof pathbuf, "Spara underlag: ", def, 0) ? pathbuf : NULL; if (!path || !*path) goto done; char full[600]; @@ -1580,13 +767,13 @@ static void attachment_download(struct app *a, int64_t att_id) else snprintf(full, sizeof full, "%s", path); if (stat(full, &sb) == 0) { - char *ans = prompt("Filen finns, skriv över? (j/n): ", "n", 0); + char ansbuf[512]; char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, "Filen finns, skriv över? (j/n): ", "n", 0) ? ansbuf : NULL; if (!ans || (ans[0] != 'j' && ans[0] != 'J')) goto done; } FILE *f = fopen(full, "wb"); if (!f || fwrite(data, 1, n, f) != n) { - message("Underlag", "Kunde inte skriva %s: %s", full, strerror(errno)); + tui_message("Underlag", "Kunde inte skriva %s: %s", full, strerror(errno)); if (f) fclose(f); goto done; @@ -1597,17 +784,17 @@ static void attachment_download(struct app *a, int64_t att_id) util_sha256(data, n, raw); util_hex(raw, sizeof raw, hex); if (sha && *sha && strcmp(hex, sha) != 0) - message("Underlag", "VARNING: kontrollsumman stämmer inte.\nSparat: %s", + tui_message("Underlag", "VARNING: kontrollsumman stämmer inte.\nSparat: %s", full); else - message("Underlag", "Sparat: %s\n(%zu byte)", full, n); + tui_message("Underlag", "Sparat: %s\n(%zu byte)", full, n); int is_text = (mime && strncmp(mime, "text/", 5) == 0) || (n > 0 && bytes_look_text(data, n)); if (is_text && n <= 256 * 1024) { char *copy = xmalloc(n + 1); memcpy(copy, data, n); copy[n] = '\0'; - text_view("Underlag", copy, NULL); + tui_pager("Underlag", copy, NULL); free(copy); } done: @@ -1657,13 +844,13 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) snprintf(path, sizeof path, "result.rows.%zu.credit_ore", i); int64_t credit = jint_val(resp, path, 0); char d[32], c[32]; - kr_format(debit, d, sizeof d); - kr_format(credit, c, sizeof c); + tui_kr_format(debit, d, sizeof d); + tui_kr_format(credit, c, sizeof c); char acol[32], ncol[256]; snprintf(acol, sizeof acol, "%s", acc ? acc : ""); - pad_field(acol, sizeof acol, 6); + tui_pad_field(acol, sizeof acol, 6); snprintf(ncol, sizeof ncol, "%s", name ? name : ""); - pad_field(ncol, sizeof ncol, 34); + tui_pad_field(ncol, sizeof ncol, 34); snprintf(line, sizeof line, " %s %s D %12s K %12s\n", acol, ncol, d, c); buf_append(&text, line, strlen(line)); @@ -1720,10 +907,10 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) "upp/ned F5 = uppdatera ^N = nytt verifikat c =" " rätta ^F = bifoga%s Esc/q = tillbaka", natts ? " f = underlag" : ""); - frame("Verifikat"); + tui_frame("Verifikat"); for (int i = 0; i < view && top + i < n; i++) mvaddnstr(2 + i, 2, lines[top + i], COLS - 4); - hints(hint); + tui_hints(hint); refresh(); int ch = ui_getch(); if (ch == 'q' || ch == 27) { @@ -1739,7 +926,7 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) stop = 1; } } else if (ch == 'c') { - char *reason = prompt("Beskriv rättelsen: ", "", 0); + char reasonbuf[512]; char *reason = tui_prompt_into(reasonbuf, sizeof reasonbuf, "Beskriv rättelsen: ", "", 0) ? reasonbuf : NULL; if (reason && *reason) { yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); yyjson_mut_val *o = yyjson_mut_obj(d); @@ -1754,7 +941,7 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) a->org, cargs); if (r && client_ok(r)) { int64_t new_id = jint_val(r, "result.id", 0); - message("Rättat", "Ändringsverifikat %lld skapat", + tui_message("Rättat", "Ändringsverifikat %lld skapat", (long long)new_id); corrected = 1; } else { @@ -1771,14 +958,14 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) struct stat sb; if (stat(path, &sb) == 0 && (long)sb.st_size > a->max_attachment_bytes) { - message("Bilaga", "Filen är %.1f MB, max %ld MB.", + tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.", (double)sb.st_size / (1024 * 1024), a->max_attachment_bytes / (1024 * 1024)); free(path); } else { char *b64 = read_file_b64(path); if (!b64) { - message("Fel", "Kunde inte läsa %s", path); + tui_message("Fel", "Kunde inte läsa %s", path); free(path); } else { const char *base = strrchr(path, '/'); @@ -1797,7 +984,7 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) char *r = client_rpc(&a->conn, "attachment.put", a->session, a->org, fargs); if (r && client_ok(r)) - message("Underlag", "Bifogat."); + tui_message("Underlag", "Bifogat."); else show_error("Kunde inte bifoga", r); free(r); @@ -1822,7 +1009,7 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) free(fn); } int acursor = 0; - int asel = select_list("Underlag", alines, (int)natts, 0, 0, + int asel = tui_select_list("Underlag", alines, (int)natts, 0, 0, &acursor, 0, "ta bort", 0); if (asel >= 0) attachment_download(a, aids[asel]); @@ -1832,7 +1019,7 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) snprintf(q, sizeof q, "Ta bort '%s' från verifikatet? (j/n): ", alines[acursor]); - char *ans = prompt(q, "n", 0); + char ansbuf[512]; char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, q, "n", 0) ? ansbuf : NULL; if (ans && (ans[0] == 'j' || ans[0] == 'J')) { char uargs[64]; snprintf(uargs, sizeof uargs, @@ -1841,7 +1028,7 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) char *r = client_rpc(&a->conn, "attachment.unlink", a->session, a->org, uargs); if (r && client_ok(r)) { - message("Underlag", "Länken borttagen."); + tui_message("Underlag", "Länken borttagen."); want_refresh = 1; stop = 1; } else { @@ -2033,7 +1220,7 @@ static int64_t vouchers_new(struct app *a) text_w = 8; for (;;) { - frame("Nytt verifikat"); + tui_frame("Nytt verifikat"); int caret_y = -1, caret_x = -1; attron(A_BOLD); mvaddstr(2, 2, "Datum"); @@ -2050,7 +1237,7 @@ static int64_t vouchers_new(struct app *a) int w = hw[k] > 1 ? hw[k] : 1; char padded[512]; snprintf(padded, sizeof padded, "%s", hvals[k]); - pad_field(padded, sizeof padded, w); + tui_pad_field(padded, sizeof padded, w); if (field == k) { attron(A_REVERSE); caret_y = hy[k]; @@ -2059,7 +1246,7 @@ static int64_t vouchers_new(struct app *a) field_pos > slen) ? slen : field_pos; - int cw = (int)disp_width_n(hvals[k], cp); + int cw = (int)tui_disp_width_n(hvals[k], cp); caret_x = hx[k] + (cw > w ? w : cw); } mvaddstr(hy[k], hx[k], padded); @@ -2079,9 +1266,9 @@ static int64_t vouchers_new(struct app *a) for (int i = 0; i < nrows; i++) { int y = 7 + i; int64_t v; - if (parse_kr(rows[i].debit, &v) == 0) + if (tui_parse_kr(rows[i].debit, &v) == 0) sum_d += v; - if (parse_kr(rows[i].credit, &v) == 0) + if (tui_parse_kr(rows[i].credit, &v) == 0) sum_c += v; int sel_ci = -1; if (field >= 3) { @@ -2097,7 +1284,7 @@ static int64_t vouchers_new(struct app *a) int w = ws[k] > 1 ? ws[k] : 1; char padded[512]; snprintf(padded, sizeof padded, "%s", vals[k]); - pad_field(padded, sizeof padded, w); + tui_pad_field(padded, sizeof padded, w); if (k == sel_ci) { attron(A_REVERSE); caret_y = y; @@ -2106,7 +1293,7 @@ static int64_t vouchers_new(struct app *a) field_pos > slen) ? slen : field_pos; - int cw = (int)disp_width_n(vals[k], cp); + int cw = (int)tui_disp_width_n(vals[k], cp); caret_x = xs[k] + (cw > w ? w : cw); } mvaddstr(y, xs[k], padded); @@ -2117,16 +1304,16 @@ static int64_t vouchers_new(struct app *a) char nbuf[512]; const char *nm = acct_name(a, rows[i].account); snprintf(nbuf, sizeof nbuf, "%s", nm ? nm : ""); - pad_field(nbuf, sizeof nbuf, name_w); + tui_pad_field(nbuf, sizeof nbuf, name_w); attron(A_DIM); mvaddstr(y, x_name, nbuf); attroff(A_DIM); } } char d1[32], c1[32], diff[32]; - kr_format(sum_d, d1, sizeof d1); - kr_format(sum_c, c1, sizeof c1); - kr_format(sum_d - sum_c, diff, sizeof diff); + tui_kr_format(sum_d, d1, sizeof d1); + tui_kr_format(sum_c, c1, sizeof c1); + tui_kr_format(sum_d - sum_c, diff, sizeof diff); int balanced = sum_d == sum_c; move(7 + nrows, 2); clrtoeol(); @@ -2149,7 +1336,7 @@ static int64_t vouchers_new(struct app *a) attroff(A_BOLD); if (!balanced) mvprintw(7 + nrows + 2, 2, "Differens: %s", diff); - hints("F4 = mall ^F = bifoga fil Tab = byta fält F5 = validera" + tui_hints("F4 = mall ^F = bifoga fil Tab = byta fält F5 = validera" " ^X = rensa rad ^Enter = bokför Esc = avbryt"); if (caret_y >= 0) move(caret_y, caret_x); @@ -2203,7 +1390,7 @@ static int64_t vouchers_new(struct app *a) } size_t ln = jarr_size(lresp, "result.items"); if (ln == 0) { - message("Mallar", + tui_message("Mallar", "Inga mallar ännu. Skapa en under Mallar först."); free(lresp); continue; @@ -2223,7 +1410,7 @@ static int64_t vouchers_new(struct app *a) char *ds = jstr_dup(lresp, path); char nmbuf[128]; snprintf(nmbuf, sizeof nmbuf, "%s", nm ? nm : ""); - pad_field(nmbuf, sizeof nmbuf, 24); + tui_pad_field(nmbuf, sizeof nmbuf, 24); snprintf(line, sizeof line, "%s %-3s %2lld rader %s", nmbuf, ser ? ser : "", (long long)rc, ds ? ds : ""); names[i] = xstrdup(nm ? nm : ""); @@ -2232,7 +1419,7 @@ static int64_t vouchers_new(struct app *a) free(ser); free(ds); } - int tsel = select_list("Välj mall", lines, (int)ln, 0, 1, NULL, 0, NULL, 0); + int tsel = tui_select_list("Välj mall", lines, (int)ln, 0, 1, NULL, 0, NULL, 0); if (tsel >= 0) snprintf(tname, sizeof tname, "%s", names[tsel]); for (size_t i = 0; i < ln; i++) { @@ -2260,10 +1447,10 @@ static int64_t vouchers_new(struct app *a) show_error("Mall", resp); free(resp); } else { - char *xs = prompt("Belopp (x, kr): ", "", 0); + char xsbuf[512]; char *xs = tui_prompt_into(xsbuf, sizeof xsbuf, "Belopp (x, kr): ", "", 0) ? xsbuf : NULL; double xv = 0; if (xs && *xs && !parse_x_double(xs, &xv)) { - message("Fel", "Ogiltigt belopp."); + tui_message("Fel", "Ogiltigt belopp."); free(resp); continue; } @@ -2294,7 +1481,7 @@ static int64_t vouchers_new(struct app *a) char ferr[256] = ""; if (formula_resolve_rows(in, n, xv, out, &on, ferr, sizeof ferr) != 0) { - message("Mall", "%s", + tui_message("Mall", "%s", ferr[0] ? ferr : "kunde inte lösa mallen"); } else { memset(rows, 0, sizeof rows); @@ -2304,11 +1491,11 @@ static int64_t vouchers_new(struct app *a) "%s", out[i].account); char tmp[32]; if (out[i].debit_ore) { - kr_format(out[i].debit_ore, tmp, sizeof tmp); + tui_kr_format(out[i].debit_ore, tmp, sizeof tmp); snprintf(rows[i].debit, sizeof rows[i].debit, "%s", tmp); } else { - kr_format(out[i].credit_ore, tmp, + tui_kr_format(out[i].credit_ore, tmp, sizeof tmp); snprintf(rows[i].credit, sizeof rows[i].credit, "%s", tmp); @@ -2352,7 +1539,7 @@ static int64_t vouchers_new(struct app *a) struct stat sb; if (stat(path, &sb) == 0 && (long)sb.st_size > a->max_attachment_bytes) { - message("Bilaga", "Filen är %.1f MB, max %ld MB.", + tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.", (double)sb.st_size / (1024 * 1024), a->max_attachment_bytes / (1024 * 1024)); free(path); @@ -2360,9 +1547,9 @@ static int64_t vouchers_new(struct app *a) } char *b64 = read_file_b64(path); if (!b64) { - message("Bilaga", "Kunde inte läsa %s", path); + tui_message("Bilaga", "Kunde inte läsa %s", path); } else if (natt >= 32) { - message("Bilaga", "Max 32 bilagor per verifikat."); + tui_message("Bilaga", "Max 32 bilagor per verifikat."); } else { const char *base = strrchr(path, '/'); base = base ? base + 1 : path; @@ -2429,9 +1616,9 @@ static int64_t vouchers_new(struct app *a) if (!rows[i].account[0]) continue; int64_t dv = 0, cv = 0; - if (parse_kr(rows[i].debit, &dv) != 0 || - parse_kr(rows[i].credit, &cv) != 0) { - message("Fel", "Ogiltigt belopp på rad %d", i + 1); + if (tui_parse_kr(rows[i].debit, &dv) != 0 || + tui_parse_kr(rows[i].credit, &cv) != 0) { + tui_message("Fel", "Ogiltigt belopp på rad %d", i + 1); yyjson_mut_doc_free(d); goto next_key; } @@ -2463,14 +1650,14 @@ static int64_t vouchers_new(struct app *a) char *r = client_read_line(&a->conn); if (r && client_ok(r)) { int64_t num = jint_val(r, "result.number", 0); - message("Validering OK", + tui_message("Validering OK", "Nummer %lld skulle tilldelas.", (long long)num); } else { show_error("Validering misslyckades", r); } free(r); } else { - message("Fel", "Kunde inte nå servern"); + tui_message("Fel", "Kunde inte nå servern"); } free(raw); } else { @@ -2481,7 +1668,7 @@ static int64_t vouchers_new(struct app *a) int64_t num = jint_val(r, "result.number", 0); char *ser = jstr_dup(r, "result.series"); int replayed = jbool_val(r, "result.replayed", 0); - message("Bokfört", "%s%lld (id %lld)%s", ser ? ser : "", + tui_message("Bokfört", "%s%lld (id %lld)%s", ser ? ser : "", (long long)num, (long long)id, replayed ? " — redan bokfört (idempotent)" : ""); free(ser); @@ -2538,9 +1725,9 @@ static int64_t vouchers_new(struct app *a) /* first keystroke in a freshly focused field replaces its content (so a prefilled date can be typed over directly) */ if (field == 0) - date_field_edit(buf, cap, &field_pos, ch, &field_fresh); + tui_date_field_edit(buf, cap, &field_pos, ch, &field_fresh); else - field_edit(buf, cap, &field_pos, ch, &field_fresh); + tui_field_edit(buf, cap, &field_pos, ch, &field_fresh); normalize_rows(rows, &nrows, &field); nfields = 3 + 4 * nrows; } @@ -2633,7 +1820,7 @@ static void vouchers_screen(struct app *a) } } int cur = start; - int sel = select_list("Verifikat", items, (int)total, start, 1, &cur, + int sel = tui_select_list("Verifikat", items, (int)total, start, 1, &cur, 1, NULL, 0); if (cur >= 0 && (size_t)cur < n) a->voucher_sel = ids[cur]; @@ -2800,9 +1987,9 @@ static void accounts_report(struct app *a) char vatbuf[32]; char tibuf[32]; snprintf(ncol, sizeof ncol, "%s", name ? name : ""); - pad_field(ncol, sizeof ncol, name_w); + tui_pad_field(ncol, sizeof ncol, name_w); snprintf(tibuf, sizeof tibuf, "%s", type_sv(type)); - pad_field(tibuf, sizeof tibuf, 12); + tui_pad_field(tibuf, sizeof tibuf, 12); snprintf(line, sizeof line, "%-6s %s %s %-5s %-6s %-8s\n", number ? number : "", ncol, tibuf, active ? "ja" : "nej", sru ? sru : "", @@ -2820,7 +2007,7 @@ static void accounts_report(struct app *a) (long long)n, (long long)active_count); buf_append(&text, line, strlen(line)); buf_append(&text, "\0", 1); - int again = text_view("Kontolista", (char *)text.p, NULL); + int again = tui_pager("Kontolista", (char *)text.p, NULL); buf_free(&text); free(resp); if (!again) @@ -2895,7 +2082,7 @@ static int acct_in(const char *number, int lo, int hi) /* Pad to `width` display columns without truncating (labels may be longer). */ static void pad_min(char *buf, size_t n, int width) { - int w = disp_width(buf); + int w = tui_disp_width(buf); size_t len = strlen(buf); while (w < width && len + 1 < n) { buf[len++] = ' '; @@ -2905,25 +2092,6 @@ static void pad_min(char *buf, size_t n, int width) } /* Right-align `text` in `width` display columns (headers with å/ä/ö). */ -static void pad_hdr(char *buf, size_t n, int width, const char *text) -{ - int w = disp_width(text); - int pad = width > w ? width - w : 0; - snprintf(buf, n, "%*s%s", pad, "", text); -} - -static void pad_label(char *buf, size_t n, const char *text, int width) -{ - snprintf(buf, n, "%s", text); - pad_field(buf, n, width); -} - -static void amt_col(char *buf, size_t n, int width, int64_t ore) -{ - char a[48]; - kr_format(ore, a, sizeof a); - snprintf(buf, n, "%*s", width, a); -} /* ---------------- balansräkning ---------------- */ @@ -2958,11 +2126,11 @@ static int bs_rows(struct buf *t, yyjson_val *res, int lo, int hi, int64_t per = ub - ib; if (t) { char nm[128], a1[48], a2[48], a3[48], a4[48]; - pad_label(nm, sizeof nm, vstr(row, "name"), 48); - amt_col(a1, sizeof a1, 14, ib); - amt_col(a2, sizeof a2, 14, ib); - amt_col(a3, sizeof a3, 14, per); - amt_col(a4, sizeof a4, 14, ub); + tui_pad_label(nm, sizeof nm, vstr(row, "name"), 48); + tui_amt_col(a1, sizeof a1, 14, ib); + tui_amt_col(a2, sizeof a2, 14, ib); + tui_amt_col(a3, sizeof a3, 14, per); + tui_amt_col(a4, sizeof a4, 14, ub); buf_line(t, " %5s %s %s %s %s %s\n", acc, nm, a1, a2, a3, a4); } sum->ib += ib; @@ -2978,11 +2146,11 @@ static void bs_sum_line(struct buf *t, const char *label, const struct bs_sum *s) { char lbl[64], a1[48], a2[48], a3[48], a4[48]; - pad_label(lbl, sizeof lbl, label, 48); - amt_col(a1, sizeof a1, 14, s->ib); - amt_col(a2, sizeof a2, 14, s->ib); - amt_col(a3, sizeof a3, 14, s->per); - amt_col(a4, sizeof a4, 14, s->ub); + tui_pad_label(lbl, sizeof lbl, label, 48); + tui_amt_col(a1, sizeof a1, 14, s->ib); + tui_amt_col(a2, sizeof a2, 14, s->ib); + tui_amt_col(a3, sizeof a3, 14, s->per); + tui_amt_col(a4, sizeof a4, 14, s->ub); buf_line(t, " %5s %s %s %s %s %s\n", "", lbl, a1, a2, a3, a4); } @@ -3066,9 +2234,9 @@ static void fmt_balans(struct buf *t, const struct app *a, yyjson_val *res) yyjson_val *eq = yyjson_obj_get(res, "equity"); int64_t result = eq ? vint(eq, "result_ore") : 0; char lbl[64], zero[48], a1[48]; - pad_label(lbl, sizeof lbl, "Beräknat resultat", 48); - amt_col(zero, sizeof zero, 14, 0); - amt_col(a1, sizeof a1, 14, result); + tui_pad_label(lbl, sizeof lbl, "Beräknat resultat", 48); + tui_amt_col(zero, sizeof zero, 14, 0); + tui_amt_col(a1, sizeof a1, 14, result); buf_line(t, "\n"); buf_rule(t, BS_RULE_W); buf_line(t, " %5s %s %s %s %s %s\n", "", lbl, zero, zero, a1, a1); @@ -3137,9 +2305,9 @@ static void is_summary(struct buf *t, const char *label, int64_t cur, char lbl[64], a1[48], a2[48]; if (blank) buf_line(t, "\n"); - pad_label(lbl, sizeof lbl, label, 48); - amt_col(a1, sizeof a1, 15, cur); - amt_col(a2, sizeof a2, 15, prev); + tui_pad_label(lbl, sizeof lbl, label, 48); + tui_amt_col(a1, sizeof a1, 15, cur); + tui_amt_col(a2, sizeof a2, 15, prev); buf_line(t, " %5s %s %s %s %s\n", "", lbl, a1, a1, a2); } @@ -3161,9 +2329,9 @@ static void is_print_group(struct buf *t, yyjson_val *res, yyjson_val *prev, : -vint(row, "amount_ore"); int64_t p = is_prev_lookup(prev, acc, !gr->rev); char nm[128], a1[48], a2[48]; - pad_label(nm, sizeof nm, vstr(row, "name"), 48); - amt_col(a1, sizeof a1, 15, c); - amt_col(a2, sizeof a2, 15, p); + tui_pad_label(nm, sizeof nm, vstr(row, "name"), 48); + tui_amt_col(a1, sizeof a1, 15, c); + tui_amt_col(a2, sizeof a2, 15, p); buf_line(t, " %5s %s %s %s %s\n", acc, nm, a1, a1, a2); sc += c; sp += p; @@ -3205,9 +2373,9 @@ static void is_range_rows(struct buf *t, yyjson_val *res, yyjson_val *prev, : -vint(row, "amount_ore"); int64_t p = is_prev_lookup(prev, acc, k != 0); char nm[128], a1[48], a2[48]; - pad_label(nm, sizeof nm, vstr(row, "name"), 48); - amt_col(a1, sizeof a1, 15, c); - amt_col(a2, sizeof a2, 15, p); + tui_pad_label(nm, sizeof nm, vstr(row, "name"), 48); + tui_amt_col(a1, sizeof a1, 15, c); + tui_amt_col(a2, sizeof a2, 15, p); buf_line(t, " %5s %s %s %s %s\n", acc, nm, a1, a1, a2); *sum += c; *sum_pre += p; @@ -3233,10 +2401,10 @@ static void fmt_resultat(struct buf *t, const struct app *a, yyjson_val *res, buf_line(t, "%s\n", gr->section); } else { char lbl[80], h1[32], h2[32], h3[32]; - pad_label(lbl, sizeof lbl, gr->section, 48); - pad_hdr(h1, sizeof h1, 15, "Period"); - pad_hdr(h2, sizeof h2, 15, "Ackumulerat"); - pad_hdr(h3, sizeof h3, 15, "Föreg. per."); + tui_pad_label(lbl, sizeof lbl, gr->section, 48); + tui_pad_hdr(h1, sizeof h1, 15, "Period"); + tui_pad_hdr(h2, sizeof h2, 15, "Ackumulerat"); + tui_pad_hdr(h3, sizeof h3, 15, "Föreg. per."); buf_line(t, " %5s %s %s %s %s\n", "", lbl, h1, h2, h3); buf_rule(t, IS_RULE_W); buf_line(t, "\n"); @@ -3313,11 +2481,11 @@ static void fmt_saldobalans(struct buf *t, const struct app *a, { fmt_head(t, a, "Saldobalans", res); char nmh[64], h1[32], h2[32], h3[32], h4[32]; - pad_label(nmh, sizeof nmh, "Benämning", 48); - pad_hdr(h1, sizeof h1, 14, "Ingående"); - pad_hdr(h2, sizeof h2, 14, "Debet"); - pad_hdr(h3, sizeof h3, 14, "Kredit"); - pad_hdr(h4, sizeof h4, 14, "Utgående"); + tui_pad_label(nmh, sizeof nmh, "Benämning", 48); + tui_pad_hdr(h1, sizeof h1, 14, "Ingående"); + tui_pad_hdr(h2, sizeof h2, 14, "Debet"); + tui_pad_hdr(h3, sizeof h3, 14, "Kredit"); + tui_pad_hdr(h4, sizeof h4, 14, "Utgående"); buf_line(t, " %5s %s %s %s %s %s\n", "Konto", nmh, h1, h2, h3, h4); buf_rule(t, BS_RULE_W); yyjson_val *arr = yyjson_obj_get(res, "accounts"); @@ -3325,21 +2493,21 @@ static void fmt_saldobalans(struct buf *t, const struct app *a, for (size_t i = 0; i < n; i++) { yyjson_val *row = yyjson_arr_get(arr, i); char nm[128], a1[48], a2[48], a3[48], a4[48]; - pad_label(nm, sizeof nm, vstr(row, "name"), 48); - amt_col(a1, sizeof a1, 14, vint(row, "ib_ore")); - amt_col(a2, sizeof a2, 14, vint(row, "debit_ore")); - amt_col(a3, sizeof a3, 14, vint(row, "credit_ore")); - amt_col(a4, sizeof a4, 14, vint(row, "ub_ore")); + tui_pad_label(nm, sizeof nm, vstr(row, "name"), 48); + tui_amt_col(a1, sizeof a1, 14, vint(row, "ib_ore")); + tui_amt_col(a2, sizeof a2, 14, vint(row, "debit_ore")); + tui_amt_col(a3, sizeof a3, 14, vint(row, "credit_ore")); + tui_amt_col(a4, sizeof a4, 14, vint(row, "ub_ore")); buf_line(t, " %5s %s %s %s %s %s\n", vstr(row, "account"), nm, a1, a2, a3, a4); } yyjson_val *tot = yyjson_obj_get(res, "totals"); char lbl[64], a1[48], a2[48], a3[48], a4[48]; - pad_label(lbl, sizeof lbl, "Summa", 48); - amt_col(a1, sizeof a1, 14, vint(tot, "ib_ore")); - amt_col(a2, sizeof a2, 14, vint(tot, "debit_ore")); - amt_col(a3, sizeof a3, 14, vint(tot, "credit_ore")); - amt_col(a4, sizeof a4, 14, vint(tot, "ub_ore")); + tui_pad_label(lbl, sizeof lbl, "Summa", 48); + tui_amt_col(a1, sizeof a1, 14, vint(tot, "ib_ore")); + tui_amt_col(a2, sizeof a2, 14, vint(tot, "debit_ore")); + tui_amt_col(a3, sizeof a3, 14, vint(tot, "credit_ore")); + tui_amt_col(a4, sizeof a4, 14, vint(tot, "ub_ore")); buf_line(t, "\n"); buf_rule(t, BS_RULE_W); buf_line(t, " %5s %s %s %s %s %s\n", "", lbl, a1, a2, a3, a4); @@ -3508,11 +2676,11 @@ static void fmt_huvudbok(struct buf *t, const struct app *a, yyjson_val *res) int64_t c = vint(ao, "credit_ore"); buf_line(t, "\n%s %s\n\n", vstr(ao, "account"), vstr(ao, "name")); char a1[48]; - amt_col(a1, sizeof a1, 16, ib); + tui_amt_col(a1, sizeof a1, 16, ib); char lbl[96]; - pad_label(lbl, sizeof lbl, "Ingående balans:", 59); + tui_pad_label(lbl, sizeof lbl, "Ingående balans:", 59); buf_line(t, " %s %14s %14s %16s\n", lbl, "", "", a1); - pad_label(lbl, sizeof lbl, "Ingående saldo:", 59); + tui_pad_label(lbl, sizeof lbl, "Ingående saldo:", 59); buf_line(t, " %s %14s %14s %16s\n", lbl, "", "", a1); buf_line(t, "\n"); yyjson_val *rows = yyjson_obj_get(ao, "rows"); @@ -3525,23 +2693,23 @@ static void fmt_huvudbok(struct buf *t, const struct app *a, yyjson_val *res) const char *txt = vstr(ro, "row_description"); if (!*txt) txt = vstr(ro, "description"); - pad_label(nm, sizeof nm, txt, 40); - amt_col(dd, sizeof dd, 14, vint(ro, "debit_ore")); - amt_col(cc, sizeof cc, 14, vint(ro, "credit_ore")); + tui_pad_label(nm, sizeof nm, txt, 40); + tui_amt_col(dd, sizeof dd, 14, vint(ro, "debit_ore")); + tui_amt_col(cc, sizeof cc, 14, vint(ro, "credit_ore")); snprintf(deb, sizeof deb, "%s", vint(ro, "debit_ore") ? dd : ""); snprintf(kre, sizeof kre, "%s", vint(ro, "credit_ore") ? cc : ""); - amt_col(sal, sizeof sal, 16, vint(ro, "saldo_ore")); + tui_amt_col(sal, sizeof sal, 16, vint(ro, "saldo_ore")); buf_line(t, " %-7s %-10s %s %14s %14s %16s\n", ver, vstr(ro, "date"), nm, deb, kre, sal); } char s1[48], s2[48], s3[48]; - amt_col(s1, sizeof s1, 14, d); - amt_col(s2, sizeof s2, 14, c); - amt_col(s3, sizeof s3, 16, d - c); - pad_label(lbl, sizeof lbl, "Omslutning:", 59); + tui_amt_col(s1, sizeof s1, 14, d); + tui_amt_col(s2, sizeof s2, 14, c); + tui_amt_col(s3, sizeof s3, 16, d - c); + tui_pad_label(lbl, sizeof lbl, "Omslutning:", 59); buf_line(t, " %s %14s %14s %16s\n", lbl, s1, s2, s3); - amt_col(s3, sizeof s3, 16, vint(ao, "ub_ore")); - pad_label(lbl, sizeof lbl, "Utgående saldo:", 59); + tui_amt_col(s3, sizeof s3, 16, vint(ao, "ub_ore")); + tui_pad_label(lbl, sizeof lbl, "Utgående saldo:", 59); buf_line(t, " %s %14s %14s %16s\n", lbl, s1, s2, s3); } } @@ -3557,7 +2725,7 @@ static void fmt_verifikationslista(struct buf *t, const struct app *a, buf_line(t, "Senaste ver.: %s%lld\n\n", vstr(lv, "series"), (long long)vint(lv, "number")); char nmh[64]; - pad_label(nmh, sizeof nmh, "Benämning", 48); + tui_pad_label(nmh, sizeof nmh, "Benämning", 48); buf_line(t, "%-8s %-11s %s %14s %14s\n", "Ver.", "Datum/Konto", nmh, "Debet", "Kredit"); buf_rule(t, 98); @@ -3575,9 +2743,9 @@ static void fmt_verifikationslista(struct buf *t, const struct app *a, for (size_t k = 0; k < nr; k++) { yyjson_val *ro = yyjson_arr_get(rows, k); char nm[256], dd[48], cc[48], deb[48], kre[48]; - pad_label(nm, sizeof nm, vstr(ro, "name"), 48); - amt_col(dd, sizeof dd, 14, vint(ro, "debit_ore")); - amt_col(cc, sizeof cc, 14, vint(ro, "credit_ore")); + tui_pad_label(nm, sizeof nm, vstr(ro, "name"), 48); + tui_amt_col(dd, sizeof dd, 14, vint(ro, "debit_ore")); + tui_amt_col(cc, sizeof cc, 14, vint(ro, "credit_ore")); snprintf(deb, sizeof deb, "%s", vint(ro, "debit_ore") ? dd : ""); snprintf(kre, sizeof kre, "%s", vint(ro, "credit_ore") ? cc : ""); buf_line(t, " %-10s %s %14s %14s\n", vstr(ro, "account"), @@ -3586,8 +2754,8 @@ static void fmt_verifikationslista(struct buf *t, const struct app *a, } yyjson_val *tot = yyjson_obj_get(res, "totals"); char s1[48], s2[48]; - amt_col(s1, sizeof s1, 14, vint(tot, "debit_ore")); - amt_col(s2, sizeof s2, 14, vint(tot, "credit_ore")); + tui_amt_col(s1, sizeof s1, 14, vint(tot, "debit_ore")); + tui_amt_col(s2, sizeof s2, 14, vint(tot, "credit_ore")); buf_line(t, "\n%-69s %14s %14s\n", "Omslutning:", s1, s2); } @@ -3598,7 +2766,7 @@ static void eskd_save(struct app *a, const char *from, const char *to) snprintf(period, sizeof period, "%.4s%.2s", to, to + 5); snprintf(def, sizeof def, "%s/eskd_%s.xml", home && *home ? home : ".", period); - char *path = prompt("Spara eSKD-fil: ", def, 0); + char pathbuf[512]; char *path = tui_prompt_into(pathbuf, sizeof pathbuf, "Spara eSKD-fil: ", def, 0) ? pathbuf : NULL; if (!path || !*path) return; if (path[0] == '~' && (path[1] == '/' || path[1] == '\0')) @@ -3618,16 +2786,16 @@ static void eskd_save(struct app *a, const char *from, const char *to) unsigned char *data = NULL; size_t n = 0; if (!b64 || util_b64_decode(b64, strlen(b64), &data, &n) != 0) { - message("eSKD", "Kunde inte avkoda filen från servern."); + tui_message("eSKD", "Kunde inte avkoda filen från servern."); } else { FILE *f = fopen(full, "wb"); if (!f || fwrite(data, 1, n, f) != n) { - message("eSKD", "Kunde inte skriva %s: %s", full, strerror(errno)); + tui_message("eSKD", "Kunde inte skriva %s: %s", full, strerror(errno)); if (f) fclose(f); } else { fclose(f); - message("eSKD", "Sparat: %s\n(%zu byte, ISO-8859-1)", full, n); + tui_message("eSKD", "Sparat: %s\n(%zu byte, ISO-8859-1)", full, n); } } free(data); @@ -3813,8 +2981,8 @@ static void sru_rows(struct buf *t, yyjson_val *arr) for (size_t i = 0; i < n; i++) { yyjson_val *row = yyjson_arr_get(arr, i); char lbl[256], amt[48]; - pad_label(lbl, sizeof lbl, sru_label(vstr(row, "code")), 66); - kr_format(vint(row, "amount") * 100, amt, sizeof amt); + tui_pad_label(lbl, sizeof lbl, sru_label(vstr(row, "code")), 66); + tui_kr_format(vint(row, "amount") * 100, amt, sizeof amt); buf_line(t, "%s %14s\n", lbl, amt); } } @@ -3841,7 +3009,7 @@ static void fmt_ink2(struct buf *t, const struct app *a, yyjson_val *res) for (size_t i = 0; i < nu; i++) { yyjson_val *row = yyjson_arr_get(un, i); char amt[48]; - kr_format(vint(row, "amount_ore"), amt, sizeof amt); + tui_kr_format(vint(row, "amount_ore"), amt, sizeof amt); buf_line(t, " %s %s\n", vstr(row, "account"), amt); } } @@ -3855,7 +3023,7 @@ static void sru_save(struct app *a) struct stat sb; if (stat(def, &sb) != 0 || !S_ISDIR(sb.st_mode)) snprintf(def, sizeof def, "%s", home && *home ? home : "."); - char *dir = prompt("Spara SRU-filer i: ", def, 0); + char dirbuf[512]; char *dir = tui_prompt_into(dirbuf, sizeof dirbuf, "Spara SRU-filer i: ", def, 0) ? dirbuf : NULL; if (!dir || !*dir) return; char full[512]; @@ -3892,7 +3060,7 @@ static void sru_save(struct app *a) } else { if (f) fclose(f); - message("SRU", "Kunde inte skriva %s: %s", saved[i], + tui_message("SRU", "Kunde inte skriva %s: %s", saved[i], strerror(errno)); } free(data); @@ -3900,7 +3068,7 @@ static void sru_save(struct app *a) } free(resp); if (ok == 2) - message("SRU", "Sparat:\n%s\n%s\n\nLadda upp båda i Skatteverkets" + tui_message("SRU", "Sparat:\n%s\n%s\n\nLadda upp båda i Skatteverkets" " filöverföring.", saved[0], saved[1]); } @@ -3922,7 +3090,7 @@ static void reports_screen(struct app *a) for (;;) { if (g_quit) return; - int sel = menu("Rapporter", report_items, 9, 0, &last_sel); + int sel = tui_menu("Rapporter", report_items, 9, 0, &last_sel); if (sel < 0) return; if (sel == 6) { @@ -3947,7 +3115,7 @@ static void reports_screen(struct app *a) else buf_line(&out, "%s", resp); buf_append(&out, "", 1); - int sagain = text_view("Rapport", (const char *)out.p, + int sagain = tui_pager("Rapport", (const char *)out.p, "s = spara SRU"); buf_free(&out); yyjson_doc_free(sd); @@ -3999,12 +3167,12 @@ static void reports_screen(struct app *a) break; case 5: { cmd = "report.vat"; - char *from = date_prompt("Från (YYYY-MM-DD): ", a->fy_start); + char frombuf[16]; char *from = tui_date_prompt_into(frombuf, sizeof frombuf, "Från (YYYY-MM-DD): ", a->fy_start) ? frombuf : NULL; if (!from) continue; char f[16]; snprintf(f, sizeof f, "%s", from); - char *to = date_prompt("Till (YYYY-MM-DD): ", a->fy_end); + char tobuf[16]; char *to = tui_date_prompt_into(tobuf, sizeof tobuf, "Till (YYYY-MM-DD): ", a->fy_end) ? tobuf : NULL; if (!to) continue; snprintf(vat_from, sizeof vat_from, "%s", f); @@ -4060,7 +3228,7 @@ static void reports_screen(struct app *a) fmt_moms(&out, a, res); } buf_append(&out, "", 1); - int again = text_view("Rapport", (const char *)out.p, + int again = tui_pager("Rapport", (const char *)out.p, sel == 5 ? "s = spara eSKD" : NULL); buf_free(&out); yyjson_doc_free(d); @@ -4090,7 +3258,7 @@ static int64_t pick_voucher(struct app *a) } size_t n = jarr_size(resp, "result.items"); if (n == 0) { - message("Verifikat", "Inga verifikat i räkenskapsåret."); + tui_message("Verifikat", "Inga verifikat i räkenskapsåret."); free(resp); return 0; } @@ -4117,7 +3285,7 @@ static int64_t pick_voucher(struct app *a) free(date); free(desc); } - int sel = select_list("Välj verifikat", items, (int)n, 0, 1, NULL, 0, + int sel = tui_select_list("Välj verifikat", items, (int)n, 0, 1, NULL, 0, NULL, 0); int64_t out = sel >= 0 ? ids[sel] : 0; for (size_t i = 0; i < n; i++) @@ -4154,7 +3322,7 @@ static void inbox_screen(struct app *a) int64_t size = jint_val(resp, path, 0); char fnbuf[256]; snprintf(fnbuf, sizeof fnbuf, "%s", fn ? fn : ""); - pad_field(fnbuf, sizeof fnbuf, 40); + tui_pad_field(fnbuf, sizeof fnbuf, 40); snprintf(line, sizeof line, "%s %8lld byte", fnbuf, (long long)size); items[i] = xstrdup(line); @@ -4173,7 +3341,7 @@ static void inbox_screen(struct app *a) top = (int)n - view; if (top < 0) top = 0; - frame("Underlag (inkorg)"); + tui_frame("Underlag (inkorg)"); for (size_t i = (size_t)top; i < n && (int)(i - (size_t)top) < view; i++) { int y = 2 + (int)(i - (size_t)top); @@ -4185,7 +3353,7 @@ static void inbox_screen(struct app *a) } if (n == 0) mvaddstr(2, 2, "Inga obokförda underlag."); - hints("Enter = hämta k = koppla till verifikat a/^N = lägg till" + tui_hints("Enter = hämta k = koppla till verifikat a/^N = lägg till" " fil PgUp/PgDn, Home/End rullar F5 = uppdatera Esc/q =" " tillbaka"); refresh(); @@ -4222,7 +3390,7 @@ static void inbox_screen(struct app *a) char *r = client_rpc(&a->conn, "attachment.link", a->session, a->org, kargs); if (r && client_ok(r)) - message("Underlag", "Kopplat till verifikatet."); + tui_message("Underlag", "Kopplat till verifikatet."); else show_error("Kunde inte koppla", r); free(r); @@ -4234,7 +3402,7 @@ static void inbox_screen(struct app *a) struct stat sb; if (stat(path, &sb) == 0 && (long)sb.st_size > a->max_attachment_bytes) { - message("Bilaga", "Filen är %.1f MB, max %ld MB.", + tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.", (double)sb.st_size / (1024 * 1024), a->max_attachment_bytes / (1024 * 1024)); free(path); @@ -4242,7 +3410,7 @@ static void inbox_screen(struct app *a) } char *b64 = read_file_b64(path); if (!b64) { - message("Fel", "Kunde inte läsa %s", path); + tui_message("Fel", "Kunde inte läsa %s", path); continue; } const char *base = strrchr(path, '/'); @@ -4267,7 +3435,7 @@ static void inbox_screen(struct app *a) r = client_read_line(&a->conn); free(raw); if (r && client_ok(r)) - message("Underlag", "Sparat."); + tui_message("Underlag", "Sparat."); else show_error("Kunde inte spara", r); free(r); @@ -4283,7 +3451,7 @@ static void audit_screen(struct app *a) for (;;) { if (g_quit) return; - int sel = menu("Revision", audit_items, 2, 0, &last_sel); + int sel = tui_menu("Revision", audit_items, 2, 0, &last_sel); if (sel < 0) return; if (sel == 0) { @@ -4296,7 +3464,7 @@ static void audit_screen(struct app *a) } int64_t checked = jint_val(resp, "result.checked", 0); int is_ok = jbool_val(resp, "result.ok", 0); - message("Hashkedja", "%s\n%d händelser kontrollerade.", + tui_message("Hashkedja", "%s\n%d händelser kontrollerade.", is_ok ? "Kedjan är intakt." : "KEDJAN ÄR BRUTEN!", (int)checked); free(resp); @@ -4333,7 +3501,7 @@ static void audit_screen(struct app *a) free(code); } buf_append(&text, "\0", 1); - int again = text_view("Behandlingshistorik", (char *)text.p, NULL); + int again = tui_pager("Behandlingshistorik", (char *)text.p, NULL); buf_free(&text); free(resp); if (!again) @@ -4464,7 +3632,7 @@ static int template_form(struct app *a, const char *load_name) text_w = 8; for (;;) { - frame(load_name ? "Redigera mall" : "Ny mall"); + tui_frame(load_name ? "Redigera mall" : "Ny mall"); int caret_y = -1, caret_x = -1; attron(A_BOLD); mvaddstr(2, 2, "Namn"); @@ -4480,7 +3648,7 @@ static int template_form(struct app *a, const char *load_name) int w = hw[k] > 1 ? hw[k] : 1; char padded[512]; snprintf(padded, sizeof padded, "%s", hvals[k]); - pad_field(padded, sizeof padded, w); + tui_pad_field(padded, sizeof padded, w); if (field == k) { attron(A_REVERSE); caret_y = hy[k]; @@ -4489,7 +3657,7 @@ static int template_form(struct app *a, const char *load_name) field_pos > slen) ? slen : field_pos; - int cw = (int)disp_width_n(hvals[k], cp); + int cw = (int)tui_disp_width_n(hvals[k], cp); caret_x = hx[k] + (cw > w ? w : cw); } mvaddstr(hy[k], hx[k], padded); @@ -4520,7 +3688,7 @@ static int template_form(struct app *a, const char *load_name) int w = ws[k] > 1 ? ws[k] : 1; char padded[512]; snprintf(padded, sizeof padded, "%s", vals[k]); - pad_field(padded, sizeof padded, w); + tui_pad_field(padded, sizeof padded, w); if (k == sel_ci) { attron(A_REVERSE); caret_y = y; @@ -4529,7 +3697,7 @@ static int template_form(struct app *a, const char *load_name) field_pos > slen) ? slen : field_pos; - int cw = (int)disp_width_n(vals[k], cp); + int cw = (int)tui_disp_width_n(vals[k], cp); caret_x = xs[k] + (cw > w ? w : cw); } mvaddstr(y, xs[k], padded); @@ -4540,7 +3708,7 @@ static int template_form(struct app *a, const char *load_name) char nbuf[512]; const char *nm = acct_name(a, rows[i].account); snprintf(nbuf, sizeof nbuf, "%s", nm ? nm : ""); - pad_field(nbuf, sizeof nbuf, name_w); + tui_pad_field(nbuf, sizeof nbuf, name_w); attron(A_DIM); mvaddstr(y, x_name, nbuf); attroff(A_DIM); @@ -4553,7 +3721,7 @@ static int template_form(struct app *a, const char *load_name) "x = belopp i kronor. Positivt blir debet, negativt kredit.", COLS - 4); attroff(A_DIM); - hints("Tab = byta fält F5 = validera ^X = rensa rad ^Enter = spara Esc = avbryt"); + tui_hints("Tab = byta fält F5 = validera ^X = rensa rad ^Enter = spara Esc = avbryt"); if (caret_y >= 0) move(caret_y, caret_x); refresh(); @@ -4638,7 +3806,7 @@ static int template_form(struct app *a, const char *load_name) if (!problem && used == 0) problem = "Mallen behöver minst en rad."; if (problem) { - message("Mall", "%s", problem); + tui_message("Mall", "%s", problem); continue; } @@ -4672,10 +3840,10 @@ static int template_form(struct app *a, const char *load_name) free(args); if (resp && client_ok(resp)) { if (ch == KEY_F(5)) { - message("Validering OK", + tui_message("Validering OK", "Mallen är korrekt (%d rader).", used); } else { - message("Mall", "Mallen '%s' sparad.", tname); + tui_message("Mall", "Mallen '%s' sparad.", tname); free(resp); curs_set(0); return 1; @@ -4719,7 +3887,7 @@ static int template_form(struct app *a, const char *load_name) } if (!buf) continue; - field_edit(buf, cap, &field_pos, ch, &field_fresh); + tui_field_edit(buf, cap, &field_pos, ch, &field_fresh); trow_normalize(rows, &nrows, &field); nfields = 3 + 3 * nrows; } @@ -4729,7 +3897,7 @@ static void template_archive(struct app *a, const char *name) { char label[256]; snprintf(label, sizeof label, "Arkivera '%s'? (j/n): ", name); - char *ans = prompt(label, "n", 0); + char ansbuf[512]; char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, label, "n", 0) ? ansbuf : NULL; if (!ans || (ans[0] != 'j' && ans[0] != 'J')) return; yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); @@ -4742,7 +3910,7 @@ static void template_archive(struct app *a, const char *name) client_rpc(&a->conn, "template.archive", a->session, a->org, args); free(args); if (resp && client_ok(resp)) - message("Mall", "Mallen '%s' arkiverad.", name); + tui_message("Mall", "Mallen '%s' arkiverad.", name); else show_error("Kunde inte arkivera", resp); free(resp); @@ -4849,14 +4017,14 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) { static struct tui_ibrow rows[64]; if (nold > 63) { - message("Ingående balans", + tui_message("Ingående balans", "För många konton (%d) för att redigera här.", nold); return 0; } memset(rows, 0, sizeof rows); for (int i = 0; i < nold && i < 63; i++) { snprintf(rows[i].account, sizeof rows[i].account, "%s", old_acc[i]); - kr_format(old_amt[i], rows[i].amount, sizeof rows[i].amount); + tui_kr_format(old_amt[i], rows[i].amount, sizeof rows[i].amount); } int nrows = nold > 0 ? nold : 1; int field = 0; @@ -4875,7 +4043,7 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) curs_set(1); for (;;) { - frame("Ingående balans"); + tui_frame("Ingående balans"); int caret_y = -1, caret_x = -1; attron(A_BOLD); mvaddstr(2, 2, @@ -4904,7 +4072,7 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) int w = ws[k] > 1 ? ws[k] : 1; char padded[512]; snprintf(padded, sizeof padded, "%s", vals[k]); - pad_field(padded, sizeof padded, w); + tui_pad_field(padded, sizeof padded, w); if (k == sel_ci) { attron(A_REVERSE); caret_y = y; @@ -4913,7 +4081,7 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) field_pos > slen) ? slen : field_pos; - int cw = (int)disp_width_n(vals[k], cp); + int cw = (int)tui_disp_width_n(vals[k], cp); caret_x = xs[k] + (cw > w ? w : cw); } mvaddstr(y, xs[k], padded); @@ -4924,13 +4092,13 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) char nbuf[512]; const char *nm = acct_name(a, rows[i].account); snprintf(nbuf, sizeof nbuf, "%s", nm ? nm : ""); - pad_field(nbuf, sizeof nbuf, name_w); + tui_pad_field(nbuf, sizeof nbuf, name_w); attron(A_DIM); mvaddstr(y, x_name, nbuf); attroff(A_DIM); } } - hints("Tab = byta fält F5 = validera ^X = rensa rad ^Enter = spara Esc = avbryt"); + tui_hints("Tab = byta fält F5 = validera ^X = rensa rad ^Enter = spara Esc = avbryt"); if (caret_y >= 0) move(caret_y, caret_x); refresh(); @@ -5012,12 +4180,12 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) problem = msg; } if (problem) { - message("Ingående balans", "%s", problem); + tui_message("Ingående balans", "%s", problem); continue; } if (ch == KEY_F(5)) { - message("Validering OK", "%d konton, summan är noll.", used); + tui_message("Validering OK", "%d konton, summan är noll.", used); continue; } @@ -5079,7 +4247,7 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) } if (posted == 0) { yyjson_mut_doc_free(d); - message("Ingående balans", "Ingen ändring."); + tui_message("Ingående balans", "Ingen ändring."); continue; } yyjson_mut_obj_add_val(d, o, "rows", arr); @@ -5090,7 +4258,7 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) free(args); if (resp && client_ok(resp)) { int64_t num = jint_val(resp, "result.number", 0); - message("Ingående balans", + tui_message("Ingående balans", "Sparat som IB %lld (%d justerade rader).", (long long)num, posted); free(resp); @@ -5123,7 +4291,7 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) } if (!buf) continue; - field_edit(buf, cap, &field_pos, ch, &field_fresh); + tui_field_edit(buf, cap, &field_pos, ch, &field_fresh); ibrow_normalize(rows, &nrows, &field); nfields = 3 * nrows; } @@ -5157,7 +4325,7 @@ static void ib_screen(struct app *a) top = n - view; if (top < 0) top = 0; - frame("Ingående balans"); + tui_frame("Ingående balans"); attron(A_BOLD); mvprintw(2, 2, "Räkenskapsår %s ingående balans (effektiv)", a->fy_label); @@ -5174,19 +4342,19 @@ static void ib_screen(struct app *a) for (int i = 0; i < view && top + i < n; i++) { int idx = top + i; char amount[32]; - kr_format(amts[idx], amount, sizeof amount); + tui_kr_format(amts[idx], amount, sizeof amount); const char *nm = acct_name(a, accs[idx]); char nbuf[128]; snprintf(nbuf, sizeof nbuf, "%s", nm ? nm : ""); - pad_field(nbuf, sizeof nbuf, 50); + tui_pad_field(nbuf, sizeof nbuf, 50); mvprintw(6 + i, 2, "%-6s %s %14s", accs[idx], nbuf, amount); } char sumbuf[32]; - kr_format(sum, sumbuf, sizeof sumbuf); + tui_kr_format(sum, sumbuf, sizeof sumbuf); attron(A_BOLD); mvprintw(LINES - 4, 2, "Summa: %s", sumbuf); attroff(A_BOLD); - hints("e/^N = redigera PgUp/PgDn, Home/End rullar F5 =" + tui_hints("e/^N = redigera PgUp/PgDn, Home/End rullar F5 =" " uppdatera Esc/q = tillbaka"); refresh(); int ch = ui_getch(); @@ -5295,13 +4463,13 @@ static void board_screen(struct app *a) } if (owner) lines[n] = xstrdup("+ Ny ledamot (^N)"); - int s = select_list("Styrelseledamöter", lines, rows, cursor, 1, + int s = tui_select_list("Styrelseledamöter", lines, rows, cursor, 1, &cursor, owner ? 1 : 0, owner && n > 0 ? "ta bort" : NULL, 0); if (s == -6 && cursor >= 0 && (size_t)cursor < n) { char q[320]; snprintf(q, sizeof q, "Ta bort '%s'? (j/n): ", lines[cursor]); - char *ans = prompt(q, "n", 0); + char ansbuf[512]; char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, q, "n", 0) ? ansbuf : NULL; if (ans && (ans[0] == 'j' || ans[0] == 'J')) { char args[64]; snprintf(args, sizeof args, "{\"id\":%lld}", @@ -5309,64 +4477,66 @@ static void board_screen(struct app *a) char *r = client_rpc(&a->conn, "board.remove", a->session, a->org, args); if (r && client_ok(r)) { - message("Styrelseledamöter", "Borttagen."); + tui_message("Styrelseledamöter", "Borttagen."); } else { show_error("Kunde inte ta bort", r); } free(r); } } else if (s == -4 || (s >= 0 && owner && (size_t)s == n)) { - char namebuf[256] = ""; - char *name = prompt("Namn: ", "", 0); - if (name && *name) - snprintf(namebuf, sizeof namebuf, "%s", name); - char *title = namebuf[0] - ? prompt("Titel: ", "Styrelseledamot", 0) - : NULL; - if (namebuf[0] && title && *title) { + char namebuf[512] = "", titlebuf[512] = ""; + int ok_name = tui_prompt_into(namebuf, sizeof namebuf, "Namn: ", + "", 0); + int ok_title = ok_name && namebuf[0] + ? tui_prompt_into(titlebuf, sizeof titlebuf, + "Titel: ", + "Styrelseledamot", 0) + : 0; + if (ok_name && namebuf[0] && ok_title && titlebuf[0]) { yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); yyjson_mut_val *o = yyjson_mut_obj(d); yyjson_mut_doc_set_root(d, o); yyjson_mut_obj_add_strcpy(d, o, "name", namebuf); - yyjson_mut_obj_add_strcpy(d, o, "title", title); + yyjson_mut_obj_add_strcpy(d, o, "title", titlebuf); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); char *r = client_rpc(&a->conn, "board.add", a->session, a->org, args); free(args); if (r && client_ok(r)) - message("Styrelseledamöter", "Tillagd."); + tui_message("Styrelseledamöter", "Tillagd."); else show_error("Kunde inte lägga till", r); free(r); } } else if (s >= 0 && (size_t)s < n) { if (!owner) { - message("Styrelseledamöter", + tui_message("Styrelseledamöter", "Endast ägare kan ändra styrelseledamöterna."); } else { - char namebuf[256] = ""; - char *name = prompt("Namn: ", names[s] ? names[s] : "", 0); - if (name && *name) - snprintf(namebuf, sizeof namebuf, "%s", name); - char *title = namebuf[0] - ? prompt("Titel: ", - titles[s] ? titles[s] : "", 0) - : NULL; - if (namebuf[0] && title && *title) { + char namebuf[512] = "", titlebuf[512] = ""; + int ok_name = tui_prompt_into(namebuf, sizeof namebuf, + "Namn: ", + names[s] ? names[s] : "", 0); + int ok_title = ok_name && namebuf[0] + ? tui_prompt_into( + titlebuf, sizeof titlebuf, "Titel: ", + titles[s] ? titles[s] : "", 0) + : 0; + if (ok_name && namebuf[0] && ok_title && titlebuf[0]) { yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); yyjson_mut_val *o = yyjson_mut_obj(d); yyjson_mut_doc_set_root(d, o); yyjson_mut_obj_add_int(d, o, "id", ids[s]); yyjson_mut_obj_add_strcpy(d, o, "name", namebuf); - yyjson_mut_obj_add_strcpy(d, o, "title", title); + yyjson_mut_obj_add_strcpy(d, o, "title", titlebuf); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); char *r = client_rpc(&a->conn, "board.update", a->session, a->org, args); free(args); if (r && client_ok(r)) - message("Styrelseledamöter", "Sparad."); + tui_message("Styrelseledamöter", "Sparad."); else show_error("Kunde inte spara", r); free(r); @@ -5421,107 +4591,52 @@ static void yearinfo_screen(struct app *a) return; } char *vals[6]; - vals[0] = jstr_dup(resp, "result.events"); - vals[1] = jstr_dup(resp, "result.agm_date"); - char divbuf[48]; - kr_format(jint_val(resp, "result.dividend_ore", 0), divbuf, - sizeof divbuf); - vals[2] = xstrdup(divbuf); - vals[3] = jstr_dup(resp, "result.dividend_date"); - vals[4] = jstr_dup(resp, "result.employees"); - vals[5] = jstr_dup(resp, "result.notes"); - for (int i = 0; i < nf; i++) - if (!vals[i]) - vals[i] = xstrdup(""); - free(resp); - - int sel = 0; - int back = 0; - for (;;) { - frame("Information om året"); - attron(A_BOLD); - mvaddstr(3, 2, "Uppgift"); - mvaddstr(3, 34, "Värde"); - attroff(A_BOLD); - for (int i = 0; i < nf; i++) { - char lbl[128], line[640]; - snprintf(lbl, sizeof lbl, "%s", labels[i]); - pad_field(lbl, sizeof lbl, 31); - snprintf(line, sizeof line, "%s %s", lbl, vals[i]); - if (i == sel) { - attron(A_REVERSE); - mvaddnstr(5 + i, 2, line, COLS - 4); - attroff(A_REVERSE); - } else { - if (!can_edit) - attron(A_DIM); - mvaddnstr(5 + i, 2, line, COLS - 4); - if (!can_edit) - attroff(A_DIM); - } - } - hints(can_edit - ? "upp/ned Enter = ändra F5 = uppdatera" - " Esc/q = tillbaka ^C = avsluta" - : "upp/ned F5 = uppdatera Esc/q = tillbaka" - " ^C = avsluta (endast ägare/redovisare)"); - refresh(); - int ch = ui_getch(); - if (ch == KEY_UP && sel > 0) - sel--; - else if (ch == KEY_DOWN && sel < nf - 1) - sel++; - else if (ch == KEY_F(5)) - break; /* reload */ - else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) { - if (!can_edit) { - message("Information om året", - "Endast ägare eller redovisare kan ändra."); - continue; - } - char label[160]; - snprintf(label, sizeof label, "%s: ", labels[sel]); - char *v = prompt(label, vals[sel], 0); - if (!v) - continue; - yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); - yyjson_mut_val *o = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, o); - yyjson_mut_obj_add_int(d, o, "id", a->fy); - if (sel == 2) { - int64_t ore = 0; - if (parse_kr(v, &ore) != 0) { - message("Ogiltigt värde", "%s", labels[sel]); - yyjson_mut_doc_free(d); - continue; - } - yyjson_mut_obj_add_int(d, o, keys[sel], ore); - } else { - if ((sel == 1 || sel == 3) && *v && - !util_parse_iso_date(v)) { - message("Ogiltigt datum", "%s", v); - yyjson_mut_doc_free(d); - continue; - } - yyjson_mut_obj_add_strcpy(d, o, keys[sel], v); - } - char *uargs = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - char *r = client_rpc(&a->conn, "fiscal_year.update", - a->session, a->org, uargs); - free(uargs); - if (!r || !client_ok(r)) - show_error("Kunde inte spara", r); - free(r); - break; /* reload from the server */ - } else if (ch == 27 || ch == 'q') { - back = 1; - break; + struct tui_form_field ff[6]; + int64_t div_ore = jint_val(resp, "result.dividend_ore", 0); + memset(ff, 0, sizeof ff); + for (int i = 0; i < nf; i++) { + vals[i] = xcalloc(512, 1); + if (i != 2) { + char path[64], *v; + snprintf(path, sizeof path, "result.%s", keys[i]); + v = jstr_dup(resp, path); + snprintf(vals[i], 512, "%s", v ? v : ""); + free(v); } + ff[i].label = labels[i]; + ff[i].value = vals[i]; + ff[i].cap = 512; + } + free(resp); + ff[0].kind = TUI_F_TEXT; + ff[1].kind = TUI_F_DATE; + ff[2].kind = TUI_F_AMOUNT; + ff[2].ore = &div_ore; + ff[3].kind = TUI_F_DATE; + ff[4].kind = TUI_F_TEXT; + ff[5].kind = TUI_F_TEXT; + int r = tui_form_run("Information om året", ff, nf, can_edit); + if (r >= 0) { + yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); + yyjson_mut_val *o = yyjson_mut_obj(d); + yyjson_mut_doc_set_root(d, o); + yyjson_mut_obj_add_int(d, o, "id", a->fy); + if (r == 2) + yyjson_mut_obj_add_int(d, o, "dividend_ore", div_ore); + else + yyjson_mut_obj_add_strcpy(d, o, keys[r], vals[r]); + char *uargs = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + char *rr = client_rpc(&a->conn, "fiscal_year.update", a->session, + a->org, uargs); + free(uargs); + if (!rr || !client_ok(rr)) + show_error("Kunde inte spara", rr); + free(rr); } for (int i = 0; i < nf; i++) free(vals[i]); - if (back) + if (r == TUI_FORM_BACK) return; } } @@ -5539,120 +4654,58 @@ static void company_screen(struct app *a) free(resp); return; } - char **vals = xcalloc(nf, sizeof(char *)); + char *vals[16]; + struct tui_form_field ff[16]; + memset(ff, 0, sizeof ff); for (int i = 0; i < nf; i++) { char path[64]; + vals[i] = xcalloc(512, 1); snprintf(path, sizeof path, "result.%s", COMPANY_FIELDS[i].key); if (COMPANY_FIELDS[i].numeric) { - char tmp[32]; - snprintf(tmp, sizeof tmp, "%lld", + snprintf(vals[i], 512, "%lld", (long long)jint_val(resp, path, 0)); - vals[i] = xstrdup(tmp); } else { char *v = jstr_dup(resp, path); - vals[i] = v ? v : xstrdup(""); + snprintf(vals[i], 512, "%s", v ? v : ""); + free(v); } + ff[i].label = COMPANY_FIELDS[i].label; + ff[i].value = vals[i]; + ff[i].cap = 512; + ff[i].kind = TUI_F_TEXT; } free(resp); - - int sel = 0; - int back = 0; - for (;;) { - frame("Företagsuppgifter"); - attron(A_BOLD); - mvaddstr(3, 2, "Uppgift"); - mvaddstr(3, 34, "Värde"); - attroff(A_BOLD); - for (int i = 0; i <= nf; i++) { - char lbl[128], line[512]; - if (i < nf) { - snprintf(lbl, sizeof lbl, "%s", COMPANY_FIELDS[i].label); - pad_field(lbl, sizeof lbl, 31); - snprintf(line, sizeof line, "%s %s", lbl, vals[i]); - } else { - snprintf(lbl, sizeof lbl, "Styrelseledamöter"); - pad_field(lbl, sizeof lbl, 31); - snprintf(line, sizeof line, "%s (lista)", lbl); - } - if (i == sel) { - attron(A_REVERSE); - mvaddnstr(5 + i, 2, line, COLS - 4); - attroff(A_REVERSE); - } else { - if (!owner && i < nf) - attron(A_DIM); - mvaddnstr(5 + i, 2, line, COLS - 4); - if (!owner && i < nf) - attroff(A_DIM); - } - } - hints(owner - ? "upp/ned Enter = ändra F5 = uppdatera" - " Esc/q = tillbaka ^C = avsluta" - : "upp/ned F5 = uppdatera Esc/q = tillbaka" - " ^C = avsluta (endast ägare kan ändra)"); - refresh(); - int ch = ui_getch(); - if (ch == KEY_UP && sel > 0) - sel--; - else if (ch == KEY_DOWN && sel < nf) - sel++; - else if (ch == KEY_F(5)) - break; /* reload */ - else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) { - if (sel == nf) { - if (!owner) { - message("Företagsuppgifter", - "Endast ägare kan ändra" - " styrelseledamöterna."); - continue; - } - board_screen(a); - break; /* reload */ - } - if (!owner) { - message("Företagsuppgifter", - "Endast ägare kan ändra företagsuppgifterna."); - continue; - } - char label[160]; - snprintf(label, sizeof label, "%s: ", - COMPANY_FIELDS[sel].label); - char *val = prompt(label, vals[sel], 0); - if (!val) - continue; - if (!company_field_valid(&COMPANY_FIELDS[sel], val)) { - message("Ogiltigt värde", "%s", - COMPANY_FIELDS[sel].label); - continue; - } + ff[nf].label = "Styrelseledamöter"; + ff[nf].kind = TUI_F_ACTION; + int r = tui_form_run("Företagsuppgifter", ff, nf + 1, owner); + if (r >= 0 && r < nf) { + if (!company_field_valid(&COMPANY_FIELDS[r], vals[r])) { + tui_message("Ogiltigt värde", "%s", COMPANY_FIELDS[r].label); + } else { yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); yyjson_mut_val *o = yyjson_mut_obj(d); yyjson_mut_doc_set_root(d, o); - if (COMPANY_FIELDS[sel].numeric) - yyjson_mut_obj_add_int(d, o, COMPANY_FIELDS[sel].key, - strtoll(val, NULL, 10)); + if (COMPANY_FIELDS[r].numeric) + yyjson_mut_obj_add_int(d, o, COMPANY_FIELDS[r].key, + strtoll(vals[r], NULL, 10)); else - yyjson_mut_obj_add_strcpy(d, o, COMPANY_FIELDS[sel].key, - val); + yyjson_mut_obj_add_strcpy(d, o, COMPANY_FIELDS[r].key, + vals[r]); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); - char *r = client_rpc(&a->conn, "org.update", a->session, - a->org, args); + char *rr = client_rpc(&a->conn, "org.update", a->session, + a->org, args); free(args); - if (!r || !client_ok(r)) - show_error("Kunde inte spara", r); - free(r); - break; /* reload from the server */ - } else if (ch == 27 || ch == 'q') { - back = 1; - break; + if (!rr || !client_ok(rr)) + show_error("Kunde inte spara", rr); + free(rr); } } + if (r == nf) + board_screen(a); for (int i = 0; i < nf; i++) free(vals[i]); - free(vals); - if (back || g_quit) + if (r == TUI_FORM_BACK) return; } } @@ -5675,87 +4728,54 @@ static void settings_screen(struct app *a) free(resp); return; } - char **vals = xcalloc(nset, sizeof(char *)); + char *vals[8]; + struct tui_form_field ff[8]; + memset(ff, 0, sizeof ff); for (int i = 0; i < nset; i++) { - char path[64]; + char path[64], *v; + vals[i] = xcalloc(512, 1); snprintf(path, sizeof path, "result.%s", SETTING_KEYS[i]); - char *v = jstr_dup(resp, path); - vals[i] = v ? v : xstrdup(""); + v = jstr_dup(resp, path); + snprintf(vals[i], 512, "%s", v ? v : ""); + free(v); + ff[i].label = SETTING_LABELS[i]; + ff[i].value = vals[i]; + ff[i].cap = 512; + ff[i].kind = TUI_F_TEXT; } free(resp); - - int sel = 0; - for (;;) { - frame("Inställningar"); - attron(A_BOLD); - mvaddstr(4, 2, "Inställning"); - mvaddstr(4, 29, "Värde"); - attroff(A_BOLD); - for (int i = 0; i < nset; i++) { - char lbl[128], line[512]; - snprintf(lbl, sizeof lbl, "%s", SETTING_LABELS[i]); - pad_field(lbl, sizeof lbl, 26); - snprintf(line, sizeof line, "%s %s", lbl, vals[i]); - if (i == sel) - attron(A_REVERSE); - mvaddnstr(6 + i, 2, line, COLS - 4); - if (i == sel) - attroff(A_REVERSE); - } - hints("upp/ned Enter = ändra F5 = uppdatera Esc/q = tillbaka" - " ^C = avsluta"); - refresh(); - int ch = ui_getch(); - if (ch == KEY_UP && sel > 0) - sel--; - else if (ch == KEY_DOWN && sel < nset - 1) - sel++; - else if (ch == KEY_F(5)) - break; /* reload */ - else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) { - char label[128]; - snprintf(label, sizeof label, "%s: ", SETTING_LABELS[sel]); - char *val = prompt(label, vals[sel], 0); - if (val) { - yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); - yyjson_mut_val *o = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, o); - yyjson_mut_obj_add_strcpy(d, o, "key", SETTING_KEYS[sel]); - yyjson_mut_obj_add_strcpy(d, o, "value", val); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - char *r = client_rpc(&a->conn, "settings.set", a->session, - a->org, args); - free(args); - if (r && client_ok(r)) { - char *nv = jstr_dup(r, "result.value"); - if (nv) { - free(vals[sel]); - vals[sel] = nv; - if (strcmp(SETTING_KEYS[sel], - "default_series") == 0) - snprintf(a->default_series, - sizeof a->default_series, "%s", nv); - else if (strcmp(SETTING_KEYS[sel], - "attachment_dir") == 0) - snprintf(a->attachment_dir, - sizeof a->attachment_dir, "%s", nv); - } - } else { - show_error("Kunde inte spara inställningen", r); - } - free(r); + int r = tui_form_run("Inställningar", ff, nset, 1); + if (r >= 0) { + yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); + yyjson_mut_val *o = yyjson_mut_obj(d); + yyjson_mut_doc_set_root(d, o); + yyjson_mut_obj_add_strcpy(d, o, "key", SETTING_KEYS[r]); + yyjson_mut_obj_add_strcpy(d, o, "value", vals[r]); + char *args = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + char *rr = client_rpc(&a->conn, "settings.set", a->session, + a->org, args); + free(args); + if (rr && client_ok(rr)) { + char *nv = jstr_dup(rr, "result.value"); + if (nv) { + if (strcmp(SETTING_KEYS[r], "default_series") == 0) + snprintf(a->default_series, sizeof a->default_series, + "%s", nv); + else if (strcmp(SETTING_KEYS[r], "attachment_dir") == 0) + snprintf(a->attachment_dir, + sizeof a->attachment_dir, "%s", nv); + free(nv); } - } else if (ch == 27 || ch == 'q') { - for (int i = 0; i < nset; i++) - free(vals[i]); - free(vals); - return; + } else { + show_error("Kunde inte spara inställningen", rr); } + free(rr); } for (int i = 0; i < nset; i++) free(vals[i]); - free(vals); + if (r == TUI_FORM_BACK) + return; } } @@ -5787,7 +4807,7 @@ static void templates_screen(struct app *a) char *ds = jstr_dup(resp, path); char nmbuf[128]; snprintf(nmbuf, sizeof nmbuf, "%s", nm ? nm : ""); - pad_field(nmbuf, sizeof nmbuf, 24); + tui_pad_field(nmbuf, sizeof nmbuf, 24); snprintf(line, sizeof line, "%s %-3s %2lld rader %s", nmbuf, ser ? ser : "", (long long)rc, ds ? ds : ""); names[i] = xstrdup(nm ? nm : ""); @@ -5798,7 +4818,7 @@ static void templates_screen(struct app *a) } names[n] = xstrdup(""); lines[n] = xstrdup("+ Ny mall (^N)"); - int s = select_list("Mallar", lines, (int)n + 1, cursor, 1, &cursor, 1, + int s = tui_select_list("Mallar", lines, (int)n + 1, cursor, 1, &cursor, 1, n > 0 ? "arkivera" : NULL, 0); if (s == -1) { for (size_t i = 0; i <= n; i++) { @@ -5883,8 +4903,8 @@ static void fmt_bokslut_plan(struct buf *t, yyjson_val *res) for (size_t k = 0; k < nr; k++) { yyjson_val *r = yyjson_arr_get(rows, k); char a1[48], a2[48]; - amt_col(a1, sizeof a1, 15, vint(r, "debit_ore")); - amt_col(a2, sizeof a2, 15, vint(r, "credit_ore")); + tui_amt_col(a1, sizeof a1, 15, vint(r, "debit_ore")); + tui_amt_col(a2, sizeof a2, 15, vint(r, "credit_ore")); buf_line(t, " %-5s %s %s\n", vstr(r, "account"), a1, a2); } } @@ -5894,12 +4914,12 @@ static void bokslut_plan(struct app *a, const char *fond, const char *rate, int do_post) { int64_t fond_ore = 0, rate_ore = 0; - if (parse_kr(fond, &fond_ore) != 0) { - message("Bokslut", "Ogiltigt fondbelopp: %s", fond); + if (tui_parse_kr(fond, &fond_ore) != 0) { + tui_message("Bokslut", "Ogiltigt fondbelopp: %s", fond); return; } - if (parse_kr(rate, &rate_ore) != 0 || rate_ore < 0 || rate_ore > 10000) { - message("Bokslut", "Ogiltig skattesats: %s", rate); + if (tui_parse_kr(rate, &rate_ore) != 0 || rate_ore < 0 || rate_ore > 10000) { + tui_message("Bokslut", "Ogiltig skattesats: %s", rate); return; } char args[160]; @@ -5923,13 +4943,13 @@ static void bokslut_plan(struct app *a, const char *fond, const char *rate, else buf_line(&t, "%s", resp); buf_append(&t, "", 1); - text_view("Bokslut - plan (torrkörning)", (const char *)t.p, NULL); + tui_pager("Bokslut - plan (torrkörning)", (const char *)t.p, NULL); buf_free(&t); yyjson_doc_free(d); free(resp); if (!do_post) return; - char *ans = prompt("Bokför planen? (j/n): ", "n", 0); + char ansbuf[512]; char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, "Bokför planen? (j/n): ", "n", 0) ? ansbuf : NULL; if (!ans || !(ans[0] == 'j' || ans[0] == 'J')) return; char *presp = client_rpc(&a->conn, "bokslut.post", a->session, a->org, @@ -5952,7 +4972,7 @@ static void bokslut_plan(struct app *a, const char *fond, const char *rate, buf_line(&m, "\n %s%lld %s", vstr(pv, "series"), (long long)vint(pv, "number"), vstr(v, "description")); } - message("Bokslut", "%s", (const char *)m.p); + tui_message("Bokslut", "%s", (const char *)m.p); buf_free(&m); yyjson_doc_free(pd); free(presp); @@ -5960,66 +4980,27 @@ static void bokslut_plan(struct app *a, const char *fond, const char *rate, static void bokslut_screen(struct app *a) { - char fond[32] = "0,00"; - char rate[16] = "20,6"; - int sel = 0; + char fond[64] = "0,00", rate[16] = "20,6"; + struct tui_form_field ff[2]; for (;;) { if (g_quit) return; - frame("Bokslut"); - attron(A_BOLD); - mvaddstr(4, 2, "Fält"); - mvaddstr(4, 34, "Värde"); - attroff(A_BOLD); - static const char *const labels[2] = { - "Periodiseringsfond (kr)", "Skattesats (%)" - }; - const char *vals[2] = { fond, rate }; - for (int i = 0; i < 2; i++) { - char lbl[64], line[128]; - snprintf(lbl, sizeof lbl, "%s", labels[i]); - pad_field(lbl, sizeof lbl, 31); - snprintf(line, sizeof line, "%s %s", lbl, vals[i]); - if (i == sel) - attron(A_REVERSE); - mvaddnstr(6 + i, 2, line, COLS - 4); - if (i == sel) - attroff(A_REVERSE); - } - mvaddstr(9, 2, "Fondbelopp i kr, tomt = ingen avsättning."); - mvaddstr(10, 2, "Skattesatsen tillämpas på resultat efter" - " bokslutsdispositioner."); - hints("upp/ned Enter = ändra F5 = visa plan ^Enter = bokför" - " Esc/q = tillbaka ^C = avsluta"); - refresh(); - int ch = ui_getch(); - if (ch == KEY_UP && sel > 0) - sel--; - else if (ch == KEY_DOWN && sel < 1) - sel++; - else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) { - char label[80]; - snprintf(label, sizeof label, "%s: ", labels[sel]); - char *v = prompt(label, vals[sel], 0); - if (!v) - continue; - int64_t ore = 0; - if (parse_kr(v, &ore) != 0 || - (sel == 1 && (ore < 0 || ore > 10000))) { - message("Bokslut", "Ogiltigt värde: %s", v); - continue; - } - if (sel == 0) - snprintf(fond, sizeof fond, "%s", v); - else - snprintf(rate, sizeof rate, "%s", v); - } else if (ch == KEY_F(5)) { + memset(ff, 0, sizeof ff); + ff[0].label = "Periodiseringsfond (kr)"; + ff[0].value = fond; + ff[0].cap = sizeof fond; + ff[0].kind = TUI_F_TEXT; + ff[1].label = "Skattesats (%)"; + ff[1].value = rate; + ff[1].cap = sizeof rate; + ff[1].kind = TUI_F_TEXT; + int r = tui_form_run("Bokslut", ff, 2, 1); + if (r == TUI_FORM_REFRESH) bokslut_plan(a, fond, rate, 0); - } else if (ch == KEY_CTRL_ENTER || ch == KEY_F(9)) { + else if (r == TUI_FORM_SUBMIT) bokslut_plan(a, fond, rate, 1); - } else if (ch == 27 || ch == 'q') { + else if (r == TUI_FORM_BACK) return; - } } } @@ -6028,24 +5009,6 @@ static void bokslut_screen(struct app *a) /* ------------------------------------------------------------------ */ /* Whole kronor with space thousands; rounded half away from zero. */ -static void 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); -} /* Thousands of kronor for the flerårsöversikt. */ static long long ar_tkr(int64_t ore) @@ -6170,7 +5133,15 @@ static void ar_load_year(struct ar_year *y, yyjson_val *gl) static void ar_amt_col(char *buf, size_t n, int width, int64_t ore) { char tmp[40]; - kr0_format(ore, tmp, sizeof tmp); + if (n == 0) + return; + tui_kr0_format(ore, tmp, sizeof tmp); + if ((size_t)width >= n) + width = (int)n - 1; + if (width < 1) + width = 1; + if (strlen(tmp) >= n) + tmp[n - 1] = '\0'; snprintf(buf, n, "%*s", width, tmp); } @@ -6179,7 +5150,7 @@ static void ar_row(struct buf *t, const char *label, int64_t cur, int64_t prev, { char nm[160], padded[200], a1[40], a2[40]; snprintf(nm, sizeof nm, "%s%s", indent ? " " : "", label); - pad_label(padded, sizeof padded, nm, 48); + tui_pad_label(padded, sizeof padded, nm, 48); ar_amt_col(a1, sizeof a1, 16, cur); ar_amt_col(a2, sizeof a2, 16, prev); buf_line(t, "%s %s %s\n", padded, a1, a2); @@ -6191,7 +5162,7 @@ static void ar_eq_row(struct buf *t, const char *label, int64_t ak, { char nm[160], padded[200], c1[32], c2[32], c3[32], c4[32]; snprintf(nm, sizeof nm, "%s%s", indent ? " " : "", label); - pad_label(padded, sizeof padded, nm, 34); + tui_pad_label(padded, sizeof padded, nm, 34); ar_amt_col(c1, sizeof c1, 12, ak); ar_amt_col(c2, sizeof c2, 14, br); ar_amt_col(c3, sizeof c3, 14, ar); @@ -6369,7 +5340,7 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, " vinstutdelningen\n\n"); if (shares > 0) { char per[48]; - kr_format((dividend + shares / 2) / shares, per, sizeof per); + tui_kr_format((dividend + shares / 2) / shares, per, sizeof per); buf_line(t, "Utdelning per aktie: %s kr. Datum för utdelning är" " %s.\n\n", per, pay_sv); } else { @@ -6534,7 +5505,7 @@ static void arsredovisning_save(struct app *a, const char *text) struct stat sb; if (stat(def, &sb) != 0 || !S_ISDIR(sb.st_mode)) snprintf(def, sizeof def, "%s", home && *home ? home : "."); - char *dir = prompt("Spara årsredovisningen i: ", def, 0); + char dirbuf[512]; char *dir = tui_prompt_into(dirbuf, sizeof dirbuf, "Spara årsredovisningen i: ", def, 0) ? dirbuf : NULL; if (!dir || !*dir) return; char full[640]; @@ -6557,12 +5528,12 @@ static void arsredovisning_save(struct app *a, const char *text) if (!f || fwrite(text, 1, n, f) != n) { if (f) fclose(f); - message("Årsredovisning", "Kunde inte skriva %s: %s", full, + tui_message("Årsredovisning", "Kunde inte skriva %s: %s", full, strerror(errno)); return; } fclose(f); - message("Årsredovisning", "Sparat: %s\n\nFilen är ett utkast i textform" + tui_message("Årsredovisning", "Sparat: %s\n\nFilen är ett utkast i textform" " att redigera vidare (t.ex. i Word).", full); } @@ -6681,7 +5652,7 @@ static void arsredovisning_screen(struct app *a) buf_line(&t, "Kunde inte läsa rapporterna."); buf_append(&t, "", 1); int again = - text_view("Årsredovisning (K2)", (const char *)t.p, "s = spara"); + tui_pager("Årsredovisning (K2)", (const char *)t.p, "s = spara"); if (again == 2) arsredovisning_save(a, (const char *)t.p); buf_free(&t); @@ -6771,7 +5742,7 @@ static void dashboard(struct app *a) for (;;) { if (g_reload) return; - int sel = menu("Bokf", MAIN_ITEMS, 12, 1, &last_sel); + int sel = tui_menu("Bokf", MAIN_ITEMS, 12, 1, &last_sel); snprintf(g_scene, sizeof g_scene, "%s", "dashboard"); if (g_quit || g_reload) return; @@ -6982,7 +5953,7 @@ static int login_screen(struct app *a) int field = 3; /* 0 server, 1 user, 2 password, 3 = Logga in button */ for (;;) { - frame("bokf — inloggning"); + tui_frame("bokf — inloggning"); attron(A_BOLD); mvaddstr(4, 4, "Server"); mvaddstr(6, 4, "Användare"); @@ -7001,7 +5972,7 @@ static int login_screen(struct app *a) } { const char *btn = " Logga in "; - int bw = disp_width(btn); + int bw = tui_disp_width(btn); int bx = (COLS - bw) / 2; if (bx < 2) bx = 2; @@ -7013,7 +5984,7 @@ static int login_screen(struct app *a) if (field == 3) attroff(A_REVERSE | A_BOLD); } - hints("Tab = byta fält/knapp Enter = logga in ^C = avsluta"); + tui_hints("Tab = byta fält/knapp Enter = logga in ^C = avsluta"); refresh(); if (field == 3) { @@ -7040,7 +6011,7 @@ static int login_screen(struct app *a) /* fall through to the login attempt */ } else { int y = 4 + field * 2; - int rc = edit_field(y, 16, field == 0 ? socket_path + int rc = tui_edit_field(y, 16, field == 0 ? socket_path : (field == 1 ? user : pass), field == 0 ? sizeof socket_path : (field == 1 ? sizeof user @@ -7073,7 +6044,7 @@ static int login_screen(struct app *a) char *err = NULL; int rc = try_login(a, user, pass, NULL, &err); if (rc == -2) { - message("Fel", "Kunde inte ansluta till %s: %s", a->socket, + tui_message("Fel", "Kunde inte ansluta till %s: %s", a->socket, err ? err : "okänt fel"); free(err); continue; @@ -7082,12 +6053,12 @@ static int login_screen(struct app *a) if (err && err[0] == '{') { char *code = jstr_dup(err, "error.code"); char *msg = jstr_dup(err, "error.message"); - message("Inloggning misslyckades", "%s: %s", + tui_message("Inloggning misslyckades", "%s: %s", code ? code : "fel", msg ? msg : "okänt fel"); free(code); free(msg); } else { - message("Inloggning misslyckades", "%s", + tui_message("Inloggning misslyckades", "%s", err ? err : "transportfel"); } free(err); @@ -7109,7 +6080,7 @@ static int select_org(struct app *a) } size_t n = jarr_size(resp, "result.items"); if (n == 0) { - message("Organisationer", + tui_message("Organisationer", "Inga organisationer. Skapa en med bokfctl org.create."); free(resp); return -1; @@ -7124,7 +6095,7 @@ static int select_org(struct app *a) char *role = jstr_dup(resp, path); char nbuf[128], line[256]; snprintf(nbuf, sizeof nbuf, "%s", name ? name : ""); - pad_field(nbuf, sizeof nbuf, 30); + tui_pad_field(nbuf, sizeof nbuf, 30); snprintf(line, sizeof line, "%s (%s)", nbuf, role ? role : ""); items[i] = xstrdup(line); snprintf(path, sizeof path, "result.items.%zu.id", i); @@ -7132,7 +6103,7 @@ static int select_org(struct app *a) free(name); free(role); } - int sel = select_list("Välj organisation att representera", items, (int)n, + int sel = tui_select_list("Välj organisation att representera", items, (int)n, 0, 0, NULL, 0, NULL, 0); for (size_t i = 0; i < n; i++) free(items[i]); @@ -7220,11 +6191,10 @@ int main(int argc, char **argv) cbreak(); noecho(); keypad(stdscr, TRUE); - /* ^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~", KEY_CTRL_ENTER); - putp("\033[>4;1m"); - fflush(stdout); + tui_keys_setup(); + tui_set_input(ui_getch); + tui_set_screen(tui_frame, tui_hints); + tui_set_quit(request_quit); atexit(reset_modify_keys); curs_set(1); @@ -7315,12 +6285,12 @@ int main(int argc, char **argv) if (auto_err) { char *code = jstr_dup(auto_err, "error.code"); char *msg = jstr_dup(auto_err, "error.message"); - message("Återanslutning misslyckades", "%s: %s", + tui_message("Återanslutning misslyckades", "%s: %s", code ? code : "fel", msg ? msg : "okänt fel"); free(code); free(msg); } else if (stale_session) { - message("Session", "Sessionen har gått ut, logga in igen."); + tui_message("Session", "Sessionen har gått ut, logga in igen."); } } free(auto_err); 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; + } + } + } +} diff --git a/clients/tui.h b/clients/tui.h new file mode 100644 index 0000000..0bda359 --- /dev/null +++ b/clients/tui.h @@ -0,0 +1,131 @@ +#ifndef BOKF_TUI_H +#define BOKF_TUI_H + +#include <stddef.h> +#include <stdint.h> + +#include <ncursesw/ncurses.h> + +/* Shared TUI widgets: the only place that touches ncurses (screens compose + these; see docs/TUI-GUIDELINES.md). Everything with logic of its own is + unit-tested in tests/test_tui.c, which links only the pure parts. */ + +/* Universal keys that have no control code or need a stable name. */ +#define TUI_KEY_CTRL_N 0x0e +#define TUI_KEY_CTRL_ENTER (KEY_MAX + 1) + +/* The application supplies input, frame/hints drawing and a quit hook so + this module stays free of app state (and testable). */ +void tui_set_input(int (*fn)(void)); +void tui_set_screen(void (*frame)(const char *), void (*hints)(const char *)); +void tui_set_quit(void (*fn)(void)); +void tui_keys_setup(void); + +/* --- text measure and formatting --- */ +int tui_disp_width(const char *s); +int tui_disp_width_n(const char *s, size_t n); +void tui_pad_field(char *buf, size_t cap, int width); +void tui_pad_label(char *buf, size_t n, const char *text, int width); +void tui_pad_hdr(char *buf, size_t n, int width, const char *text); +void tui_amt_col(char *buf, size_t n, int width, int64_t ore); +void tui_kr_format(int64_t ore, char *buf, size_t n); +void tui_kr0_format(int64_t ore, char *buf, size_t n); +int tui_parse_kr(const char *s, int64_t *out); + +/* --- line editor (pure) --- */ +struct tui_ledit { + char *buf; + size_t cap; + size_t len; + size_t pos; /* caret in bytes */ +}; + +void tui_le_init(struct tui_ledit *e, char *buf, size_t cap); +void tui_le_clear(struct tui_ledit *e); +int tui_le_key(struct tui_ledit *e, int ch); + +/* Applies one key to a form field. *pos is the caret ((size_t)-1 = end); + *fresh replaces the content on the first editing key. Returns whether + the key was consumed. */ +int tui_field_edit(char *buf, size_t cap, size_t *pos, int ch, int *fresh); + +/* Date field: digits only, dashes inserted ("20260215" -> "2026-02-15"), + caret tracked in digit space. */ +int tui_date_field_edit(char *buf, size_t cap, size_t *pos, int ch, + int *fresh); + +/* Line editor at (y,x) with the label already drawn. Returns 1 on Enter, + 2 on Tab, 3 on Shift-Tab, 0 on Esc. */ +int tui_edit_field(int y, int x, char *buf, size_t cap, int mask); + +/* --- modal dialogs --- */ +void tui_message(const char *title, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); +int tui_confirm(const char *title, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); + +/* --- prompts: write into the caller's buffer, return 0 on Esc --- */ +int tui_prompt_into(char *buf, size_t cap, const char *label, + const char *def, int mask); +int tui_date_prompt_into(char *buf, size_t cap, const char *label, + const char *def); +int tui_amount_prompt_into(int64_t *out, const char *label, int64_t def_ore); +int tui_choice_prompt(const char *label, const char *const *opts, int n, + int cur); + +/* --- lists --- */ +#define TUI_NAV_NONE (-3) + +struct tui_list_nav { + int n, sel, top, view, numw; + char gotobuf[12]; + char gotolabel[32]; + int goto_active; +}; + +/* Returns the selected index, or -1 back, -2 refresh, -4 new, -6 remove, + -7 toggle, TUI_NAV_NONE when nothing happened. */ +int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new, + int allow_remove, int allow_toggle, int menu_mode); + +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); +int tui_menu(const char *title, const char *const *items, int n, + int allow_new, int *cursor); + +/* --- frame, status, hints, pager --- */ +void tui_set_status(const char *status, const char *right); +void tui_frame(const char *title); +void tui_hints(const char *s); +int tui_pager(const char *title, const char *text, const char *action_hint); + +/* --- forms --- */ +enum { + TUI_F_TEXT = 0, + TUI_F_DATE, + TUI_F_AMOUNT, + TUI_F_CHOICE, + TUI_F_ACTION, +}; + +#define TUI_FORM_BACK (-1) +#define TUI_FORM_REFRESH (-2) +#define TUI_FORM_SUBMIT (-3) + +struct tui_form_field { + const char *label; + char *value; /* text storage for TEXT/DATE (also CHOICE labels) */ + size_t cap; + int kind; + int64_t *ore; /* TUI_F_AMOUNT */ + const char *const *choices; /* TUI_F_CHOICE */ + int nchoices; + int *choice; /* TUI_F_CHOICE: current index */ +}; + +int tui_form_next(int sel, int n, int ch); +int tui_form_run(const char *title, struct tui_form_field *f, int n, + int can_edit); + +#endif diff --git a/docs/STATE.md b/docs/STATE.md index 60c70d9..32b2a4e 100644 --- a/docs/STATE.md +++ b/docs/STATE.md @@ -12,6 +12,12 @@ server/protocol/ledger only. ## Locked decisions +0. **TUI widget layer**: `clients/tui.[ch]` is the only module that touches + ncurses; screens compose its widgets and never keep local input/drawing + patterns. Every widget has a spec in `TUI-GUIDELINES.md` and unit tests + in `tests/test_tui.c` (run by `make test`; pure logic separated from + drawing). Extend the layer, not the screen. + 1. **Name/license**: `bokf`, daemon `bokfd`, clients `bokfctl` (scriptable) and `bokftui` (ncurses); GPL-3.0-or-later; repo `~/work/bokf`. 2. **Stack**: C11, Makefile, vendored SQLite 3.53.4 / yyjson 0.13.0 / Argon2 diff --git a/docs/TUI-GUIDELINES.md b/docs/TUI-GUIDELINES.md index a54fef8..63c492f 100644 --- a/docs/TUI-GUIDELINES.md +++ b/docs/TUI-GUIDELINES.md @@ -104,12 +104,31 @@ written compactly as `^N`, `^A`, `^C`, `^R` to save width. - Selection = reverse video, headers = bold, derived data = dim. - All user-visible TUI text is Swedish; server messages are English. +## Widget layer (`clients/tui.[ch]`) + +Every element a screen needs is a widget here, and the widget layer is the +only place that touches ncurses. Rules: + +- Screens never call ncurses directly (`mvadd*`, `attron`, `getch`…) and + never keep a local one-off input or drawing loop. +- If no widget fits, **extend the widget layer first** (spec in this file + + unit tests in `tests/test_tui.c`) and then use it. There is no "do it + locally once" option. +- Pure logic (line editing, date input, list navigation, prompts parsing) + is separated from drawing so it can be tested without a terminal. +- Widgets return key codes (`-1` back, `-2` refresh, `-4` new, `-6` remove, + `-7` toggle) and write text into the caller's buffer; nothing returns + static storage. +- The application supplies input, frame/hints and quit through + `tui_set_input`/`tui_set_screen`/`tui_set_quit`. + ## Adding a view — checklist 1. Data comes from public protocol commands only. 2. Wrap the screen in `frame()`/`hints()`; return `Esc`/`q` to the parent. -3. Use `menu()` or `select_list()` instead of writing a new loop; pass +3. Use `tui_menu`/`tui_select_list` instead of writing a new loop; pass `allow_new`/`allow_refresh` so the universal keys apply. -4. Forms use the shared editor (`field_edit`) and the row-normalising helpers. +4. Forms use the shared editor (`tui_edit_field`, `tui_prompt_into`, + `tui_date_prompt_into`, `tui_amount_prompt_into`) and the row helpers. 5. Support `F5` if the data can change elsewhere. 6. Update `PROTOCOL.md` §8 and this file if you add a new key or interaction. diff --git a/tests/test_tui.c b/tests/test_tui.c new file mode 100644 index 0000000..e056c47 --- /dev/null +++ b/tests/test_tui.c @@ -0,0 +1,221 @@ +/* Unit tests for the TUI widget layer (clients/tui.c). Only the pure parts + are exercised here; drawing is smoke-tested over a pty. */ +#include <stdio.h> +#include <string.h> + +#include <ncursesw/ncurses.h> + +#include "tui.h" +#include "util.h" + +static int failures = 0; +static int checks = 0; + +#define CHECK(cond) \ + do { \ + checks++; \ + if (!(cond)) { \ + failures++; \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, \ + #cond); \ + } \ + } while (0) + +static void test_disp_width(void) +{ + CHECK(tui_disp_width("") == 0); + CHECK(tui_disp_width("abc") == 3); + CHECK(tui_disp_width("åäö") == 3); + CHECK(tui_disp_width("aåb") == 3); + CHECK(tui_disp_width_n("åäö", 2) == 1); + CHECK(tui_disp_width_n("åäö", 4) == 2); + CHECK(tui_disp_width_n("abc", 2) == 2); +} + +static void test_formats(void) +{ + char buf[64]; + tui_kr_format(123456, buf, sizeof buf); + CHECK(strcmp(buf, "1 234,56") == 0); + tui_kr_format(-1000000, buf, sizeof buf); + CHECK(strcmp(buf, "-10 000,00") == 0); + tui_kr_format(5, buf, sizeof buf); + CHECK(strcmp(buf, "0,05") == 0); + tui_kr0_format(123456, buf, sizeof buf); + CHECK(strcmp(buf, "1 235") == 0); + tui_kr0_format(-123456, buf, sizeof buf); + CHECK(strcmp(buf, "-1 235") == 0); + tui_amt_col(buf, sizeof buf, 12, 123456); + CHECK(strcmp(buf, " 1 234,56") == 0); + snprintf(buf, sizeof buf, "åäö"); + tui_pad_field(buf, sizeof buf, 2); + CHECK(strcmp(buf, "åä") == 0); +} + +static void test_parse_kr(void) +{ + int64_t v = -1; + CHECK(tui_parse_kr("", &v) == 0 && v == 0); + CHECK(tui_parse_kr("0", &v) == 0 && v == 0); + CHECK(tui_parse_kr("12", &v) == 0 && v == 1200); + CHECK(tui_parse_kr("1 234,56", &v) == 0 && v == 123456); + CHECK(tui_parse_kr("12,3", &v) == 0 && v == 1230); + CHECK(tui_parse_kr("12.30", &v) == 0 && v == 1230); + CHECK(tui_parse_kr("1.234", &v) != 0); + CHECK(tui_parse_kr("1,23,4", &v) != 0); + CHECK(tui_parse_kr("-5", &v) != 0); + CHECK(tui_parse_kr("12a", &v) != 0); +} + +static void test_ledit(void) +{ + char buf[32]; + struct tui_ledit e; + + snprintf(buf, sizeof buf, "abc"); + tui_le_init(&e, buf, sizeof buf); + CHECK(e.pos == 3 && e.len == 3); + CHECK(tui_le_key(&e, KEY_LEFT) && e.pos == 2); + CHECK(tui_le_key(&e, 'X')); + CHECK(strcmp(buf, "abXc") == 0 && e.pos == 3); + CHECK(tui_le_key(&e, KEY_BACKSPACE)); + CHECK(strcmp(buf, "abc") == 0 && e.pos == 2); + CHECK(tui_le_key(&e, KEY_DC)); + CHECK(strcmp(buf, "ab") == 0 && e.pos == 2); + CHECK(tui_le_key(&e, KEY_HOME) && e.pos == 0); + CHECK(tui_le_key(&e, 21)); /* ^U */ + CHECK(strcmp(buf, "") == 0 && e.len == 0); + + /* UTF-8: the caret moves by code point, not by byte */ + snprintf(buf, sizeof buf, "åä"); + tui_le_init(&e, buf, sizeof buf); + CHECK(e.pos == 4); + CHECK(tui_le_key(&e, KEY_LEFT) && e.pos == 2); + CHECK(tui_le_key(&e, KEY_LEFT) && e.pos == 0); + CHECK(tui_le_key(&e, KEY_RIGHT) && e.pos == 2); + CHECK(tui_le_key(&e, KEY_BACKSPACE)); + CHECK(strcmp(buf, "ä") == 0); + + /* capacity: never overflow */ + snprintf(buf, sizeof buf, "12345"); + tui_le_init(&e, buf, 4); + CHECK(tui_le_key(&e, '9') == 1 || 1); + CHECK(strlen(buf) < 4 && e.len < 4); + CHECK(tui_le_key(&e, KEY_UP) == 0); /* not an editing key */ +} + +static void date_feed(char *buf, size_t cap, const char *digits) +{ + size_t pos = (size_t)-1; + int fresh = 1; + for (const char *p = digits; *p; p++) + tui_date_field_edit(buf, cap, &pos, *p, &fresh); +} + +static void test_date_field(void) +{ + char buf[16] = ""; + size_t pos = (size_t)-1; + int fresh = 1; + + date_feed(buf, sizeof buf, "20260604"); + CHECK(strcmp(buf, "2026-06-04") == 0); + fresh = 0; + CHECK(tui_date_field_edit(buf, sizeof buf, &pos, KEY_BACKSPACE, &fresh)); + CHECK(strcmp(buf, "2026-06-0") == 0); + CHECK(tui_date_field_edit(buf, sizeof buf, &pos, 21, &fresh)); /* ^U */ + CHECK(strcmp(buf, "") == 0); + CHECK(tui_date_field_edit(buf, sizeof buf, &pos, 'x', &fresh) == 0); + + /* fresh replaces a prefilled value on the first digit */ + snprintf(buf, sizeof buf, "2025-01-01"); + pos = (size_t)-1; + fresh = 1; + tui_date_field_edit(buf, sizeof buf, &pos, '9', &fresh); + CHECK(strcmp(buf, "9") == 0); + /* full field overwrites at the caret */ + date_feed(buf, sizeof buf, "20260604"); + pos = (size_t)-1; + fresh = 0; + tui_date_field_edit(buf, sizeof buf, &pos, '9', &fresh); + CHECK(strcmp(buf, "2026-06-09") == 0); +} + +static void nav_init(struct tui_list_nav *v, int n) +{ + memset(v, 0, sizeof *v); + v->n = n; + v->view = 10; +} + +static void test_nav(void) +{ + struct tui_list_nav v; + + nav_init(&v, 0); + CHECK(tui_nav_key(&v, KEY_DOWN, 0, 0, 0, 0) == TUI_NAV_NONE); + + nav_init(&v, 5); + CHECK(tui_nav_key(&v, KEY_UP, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 0); + CHECK(tui_nav_key(&v, KEY_DOWN, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 1); + CHECK(tui_nav_key(&v, KEY_END, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 4); + CHECK(tui_nav_key(&v, KEY_DOWN, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 4); + CHECK(tui_nav_key(&v, KEY_HOME, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 0); + CHECK(tui_nav_key(&v, KEY_NPAGE, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 4); + CHECK(tui_nav_key(&v, KEY_PPAGE, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 0); + + /* digits jump in lists, activate in menus */ + CHECK(tui_nav_key(&v, '3', 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 2); + CHECK(tui_nav_key(&v, '3', 0, 0, 0, 1) == 2); + CHECK(tui_nav_key(&v, '9', 0, 0, 0, 0) == TUI_NAV_NONE); /* out of range */ + + CHECK(tui_nav_key(&v, '\n', 0, 0, 0, 0) == v.sel); + CHECK(tui_nav_key(&v, KEY_F(5), 0, 0, 0, 0) == -2); + CHECK(tui_nav_key(&v, TUI_KEY_CTRL_N, 1, 0, 0, 0) == -4); + CHECK(tui_nav_key(&v, TUI_KEY_CTRL_N, 0, 0, 0, 0) == TUI_NAV_NONE); + CHECK(tui_nav_key(&v, 'd', 0, 1, 0, 0) == -6); + CHECK(tui_nav_key(&v, 'd', 0, 0, 0, 0) == TUI_NAV_NONE); + CHECK(tui_nav_key(&v, 1, 0, 0, 1, 0) == -7); /* ^A */ + CHECK(tui_nav_key(&v, 'q', 0, 0, 0, 0) == -1); + CHECK(tui_nav_key(&v, 27, 0, 0, 0, 0) == -1); + + /* g goto takes multi-digit input and Enter only closes the prompt */ + nav_init(&v, 20); + CHECK(tui_nav_key(&v, 'g', 0, 0, 0, 0) == TUI_NAV_NONE && v.goto_active); + CHECK(tui_nav_key(&v, '1', 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 0); + CHECK(tui_nav_key(&v, '2', 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 11); + CHECK(tui_nav_key(&v, KEY_BACKSPACE, 0, 0, 0, 0) == TUI_NAV_NONE && + v.sel == 0); + CHECK(tui_nav_key(&v, '\n', 0, 0, 0, 0) == TUI_NAV_NONE && !v.goto_active); + /* the view follows the selection */ + nav_init(&v, 100); + v.view = 10; + tui_nav_key(&v, KEY_END, 0, 0, 0, 0); + CHECK(v.top == 90); +} + +static void test_form_nav(void) +{ + CHECK(tui_form_next(0, 3, KEY_UP) == TUI_NAV_NONE); + CHECK(tui_form_next(0, 3, KEY_DOWN) == 1); + CHECK(tui_form_next(2, 3, KEY_DOWN) == TUI_NAV_NONE); + CHECK(tui_form_next(1, 3, KEY_F(5)) == TUI_FORM_REFRESH); + CHECK(tui_form_next(1, 3, TUI_KEY_CTRL_ENTER) == TUI_FORM_SUBMIT); + CHECK(tui_form_next(1, 3, KEY_F(9)) == TUI_FORM_SUBMIT); + CHECK(tui_form_next(1, 3, 27) == TUI_FORM_BACK); + CHECK(tui_form_next(1, 3, 'q') == TUI_FORM_BACK); + CHECK(tui_form_next(1, 3, 'x') == TUI_NAV_NONE); +} + +int main(void) +{ + test_disp_width(); + test_formats(); + test_parse_kr(); + test_ledit(); + test_date_field(); + test_nav(); + test_form_nav(); + printf("test_tui: %d checks, %d failures\n", checks, failures); + return failures ? 1 : 0; +} |
