diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-19 23:53:09 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-19 23:53:09 +0200 |
| commit | 1f2d7a8190876e3d1c541251e079a11a2cc637f2 (patch) | |
| tree | 284427385e9c140dddb0c97df318fc85852fe8e1 | |
| parent | 86c0ef0e30f6cb24db04f20a326cf5fb4b616c3c (diff) | |
| download | bokf-1f2d7a8190876e3d1c541251e079a11a2cc637f2.tar.gz bokf-1f2d7a8190876e3d1c541251e079a11a2cc637f2.zip | |
tui: bokslut hub with year fields, reports and the posting actionv0.1.43
| -rw-r--r-- | clients/bokftui.c | 168 | ||||
| -rw-r--r-- | clients/tui.c | 192 | ||||
| -rw-r--r-- | clients/tui.h | 35 | ||||
| -rw-r--r-- | docs/STATE.md | 21 | ||||
| -rw-r--r-- | docs/TUI-GUIDELINES.md | 16 | ||||
| -rw-r--r-- | tests/test_tui.c | 83 |
6 files changed, 427 insertions, 88 deletions
diff --git a/clients/bokftui.c b/clients/bokftui.c index aa7233b..3258f22 100644 --- a/clients/bokftui.c +++ b/clients/bokftui.c @@ -2950,6 +2950,38 @@ static void sru_save(struct app *a) saved[0], saved[1]); } +static void ink2_screen(struct app *a) +{ + char sargs[64]; + snprintf(sargs, sizeof sargs, "{\"fiscal_year\":%lld}", (long long)a->fy); + for (;;) { + char *resp = client_rpc(&a->conn, "sru.export", a->session, a->org, + sargs); + if (!resp || !client_ok(resp)) { + show_error("Inkomstdeklaration", resp); + free(resp); + break; + } + yyjson_doc *sd = parse(resp); + yyjson_val *sres = sd ? jget(yyjson_doc_get_root(sd), "result") : NULL; + struct buf out; + buf_init(&out); + if (sres) + fmt_ink2(&out, a, sres); + else + buf_line(&out, "%s", resp); + buf_append(&out, "", 1); + int sagain = tui_pager("Rapport", (const char *)out.p, "s = spara SRU"); + buf_free(&out); + yyjson_doc_free(sd); + free(resp); + if (sagain == 2) + sru_save(a); + if (!sagain) + break; + } +} + static void reports_screen(struct app *a) { static const char *const report_items[] = { @@ -2959,56 +2991,16 @@ static void reports_screen(struct app *a) "Huvudbok", "Verifikationslista", "Momsdeklaration", - "Inkomstdeklaration (INK2/SRU)", - "Årsredovisning (K2)", "Kontolista (alla konton)", }; static int last_sel = 0; for (;;) { if (g_quit) return; - int sel = tui_menu("Rapporter", report_items, 9, 0, &last_sel); + int sel = tui_menu("Rapporter", report_items, 7, 0, &last_sel); if (sel < 0) return; if (sel == 6) { - char sargs[64]; - snprintf(sargs, sizeof sargs, "{\"fiscal_year\":%lld}", - (long long)a->fy); - for (;;) { - char *resp = client_rpc(&a->conn, "sru.export", a->session, - a->org, sargs); - if (!resp || !client_ok(resp)) { - show_error("Inkomstdeklaration", resp); - free(resp); - break; - } - yyjson_doc *sd = parse(resp); - yyjson_val *sres = - sd ? jget(yyjson_doc_get_root(sd), "result") : NULL; - struct buf out; - buf_init(&out); - if (sres) - fmt_ink2(&out, a, sres); - else - buf_line(&out, "%s", resp); - buf_append(&out, "", 1); - int sagain = tui_pager("Rapport", (const char *)out.p, - "s = spara SRU"); - buf_free(&out); - yyjson_doc_free(sd); - free(resp); - if (sagain == 2) - sru_save(a); - if (!sagain) - break; - } - continue; - } - if (sel == 7) { - arsredovisning_screen(a); - continue; - } - if (sel == 8) { accounts_report(a); continue; } @@ -4474,9 +4466,10 @@ static void bokslut_plan(struct app *a, const char *fond, const char *rate, free(presp); } -/* Bokslut: the per-year årsredovisning details (fields 0-5, saved one by - one with fiscal_year.update) plus the local periodiseringsfond and tax - rate inputs. New years inherit the stable fields. */ +/* Bokslut: the year-end hub. The year's årsredovisning details (fields 0-5, + saved one by one with fiscal_year.update) and the local periodiseringsfond + and tax rate inputs plus a status row and the year-end actions, all in one + view. New years inherit the stable fields. */ static void bokslut_screen(struct app *a) { static const char *const labels[6] = { @@ -4506,6 +4499,9 @@ static void bokslut_screen(struct app *a) free(resp); return; } + char *fystat = jstr_dup(resp, "result.status"); + int closed = fystat && strcmp(fystat, "closed") == 0; + free(fystat); char *vals[6]; struct tui_form_field ff[8]; int64_t div_ore = jint_val(resp, "result.dividend_ore", 0); @@ -4524,6 +4520,41 @@ static void bokslut_screen(struct app *a) ff[i].cap = 512; } free(resp); + + /* A closed year counts as posted; an open one only when the tax + voucher that bokslut.post creates is already in the books. */ + int posted = closed; + if (!closed) { + char vargs[160]; + snprintf(vargs, sizeof vargs, + "{\"fiscal_year\":%lld," + "\"text\":\"Skatt på årets resultat\",\"limit\":1}", + (long long)a->fy); + char *vresp = client_rpc(&a->conn, "voucher.list", a->session, + a->org, vargs); + if (vresp && client_ok(vresp)) + posted = jarr_size(vresp, "result.items") > 0; + free(vresp); + } + + char status_line[96]; + snprintf(status_line, sizeof status_line, "Bokslut bokfört: %s", + posted ? "ja" : "nej"); + const char *reason = NULL; + if (closed) + reason = "året är stängt"; + else if (posted) + reason = "bokslutet är redan bokfört"; + else if (!can_edit) + reason = "du har inte behörighet"; + struct tui_form_action acts[5] = { + { status_line, -1, NULL }, + { "Årsredovisning (K2)", 1, NULL }, + { "Inkomstdeklaration (INK2/SRU)", 1, NULL }, + { "Bokslutsplan (torrkörning)", 1, NULL }, + { "Bokför bokslut", reason ? 0 : 1, reason }, + }; + ff[0].kind = TUI_F_TEXT; ff[1].kind = TUI_F_DATE; ff[2].kind = TUI_F_AMOUNT; @@ -4539,10 +4570,11 @@ static void bokslut_screen(struct app *a) ff[7].value = rate; ff[7].cap = sizeof rate; ff[7].kind = TUI_F_TEXT; - tui_form_hint("upp/ned/Home/End Enter = ändra F5 = visa" + tui_form_hint("upp/ned/Tab = flytta Enter = ändra/utför F5 = visa" " bokslutsplan ^Enter = bokför planen (frågar" " först) Esc/q = tillbaka ^C = avsluta"); - int r = tui_form_run("Bokslut", ff, nf + 2, can_edit); + int r = tui_form_run_actions("Bokslut", ff, nf + 2, can_edit, acts, 5, + NULL); if (r >= 0 && r < nf) { yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); yyjson_mut_val *o = yyjson_mut_obj(d); @@ -4563,6 +4595,22 @@ static void bokslut_screen(struct app *a) } for (int i = 0; i < nf; i++) free(vals[i]); + if (r == TUI_FORM_ACTION + 1) { + arsredovisning_screen(a); + continue; + } + if (r == TUI_FORM_ACTION + 2) { + ink2_screen(a); + continue; + } + if (r == TUI_FORM_ACTION + 3) { + bokslut_plan(a, fond, rate, 0); + continue; + } + if (r == TUI_FORM_ACTION + 4) { + bokslut_plan(a, fond, rate, 1); + continue; + } if (r == TUI_FORM_REFRESH) bokslut_plan(a, fond, rate, 0); else if (r == TUI_FORM_SUBMIT) @@ -5337,13 +5385,15 @@ static void do_reload(struct app *a, const char *self) static const char *const MAIN_ITEMS[] = { MK_SECTION "Bokföring", - "Verifikat", "Underlag (inkorg)", "Ingående balans", + "Verifikat", "Underlag (inkorg)", MK_SECTION "Rapporter & bokslut", "Rapporter", "Bokslut", MK_SECTION "Register", "Mallar", "Företagsuppgifter", "Inställningar", + MK_SECTION "Räkenskapsår", + "Ingående balans", "Räkenskapsår", MK_SECTION "Övrigt", - "Revision", "Räkenskapsår", "Logga ut / avsluta", + "Revision", "Logga ut / avsluta", }; static void dashboard(struct app *a) @@ -5373,31 +5423,31 @@ static void dashboard(struct app *a) case 2: open_scene(a, "inbox"); break; - case 3: - open_scene(a, "ib"); - break; - case 5: + case 4: open_scene(a, "reports"); break; - case 6: + case 5: open_scene(a, "bokslut"); break; - case 8: + case 7: open_scene(a, "templates"); break; - case 9: + case 8: open_scene(a, "company"); break; - case 10: + case 9: open_scene(a, "settings"); break; - case 12: - open_scene(a, "audit"); + case 11: + open_scene(a, "ib"); break; - case 13: + case 12: select_fiscal_year(a); break; case 14: + open_scene(a, "audit"); + break; + case 15: return; default: break; /* section header */ diff --git a/clients/tui.c b/clients/tui.c index a964f65..a5b0cd2 100644 --- a/clients/tui.c +++ b/clients/tui.c @@ -1341,27 +1341,122 @@ int tui_form_next(int sel, int n, int ch) return TUI_NAV_NONE; } -int tui_form_run(const char *title, struct tui_form_field *f, int n, - int can_edit) +static int act_selectable(const struct tui_form_action *acts, int i, int na) { - return tui_form_run_hook(title, f, n, can_edit, NULL, NULL); + return acts && i >= 0 && i < na && acts[i].enabled != -1; } -int tui_form_run_hook(const char *title, struct tui_form_field *f, int n, - int can_edit, int (*key)(void *ud, int ch), void *ud) +int tui_form_act_step(int focus, int nf, const struct tui_form_action *acts, + int na, int dir) +{ + if (nf < 0) + nf = 0; + if (na < 0) + na = 0; + int n = nf + na; + if (n <= 0) + return -1; + int pos = focus >= 0 && focus < n ? focus : 0; + for (int i = 0; i < n; i++) { + pos = (pos + (dir >= 0 ? 1 : -1) + n) % n; + if (pos < nf || act_selectable(acts, pos - nf, na)) + return pos; + } + return focus; +} + +int tui_form_act_first(int nf, const struct tui_form_action *acts, int na) +{ + if (nf > 0) + return 0; + for (int i = 0; i < na; i++) + if (act_selectable(acts, i, na)) + return i; + return -1; +} + +int tui_form_act_key(int sel, int nf, const struct tui_form_action *acts, + int na, int ch) +{ + int n = (nf > 0 ? nf : 0) + (na > 0 ? na : 0); + if (ch == KEY_UP || ch == KEY_DOWN || ch == '\t' || ch == KEY_BTAB) { + int dir = ch == KEY_UP || ch == KEY_BTAB ? -1 : 1; + return tui_form_act_step(sel, nf, acts, na, dir); + } + if (ch == KEY_HOME) + return tui_form_act_first(nf, acts, na); + if (ch == KEY_END) { + for (int i = n - 1; i >= 0; i--) { + if (i < nf || act_selectable(acts, i - nf, na)) + return i; + } + return tui_form_act_first(nf, acts, na); + } + if (ch == KEY_NPAGE || ch == KEY_PPAGE) { + int dir = ch == KEY_NPAGE ? 1 : -1; + int pos = sel; + for (int i = 0; i < TUI_FORM_PAGE; i++) + pos = tui_form_act_step(pos, nf, acts, na, dir); + return pos; + } + return tui_form_next(sel, nf, ch); +} + +void tui_form_act_label(const struct tui_form_action *a, char *buf, size_t n) +{ + const char *label = a && a->label ? a->label : ""; + if (a && a->enabled == 0) { + const char *reason = a->disabled_reason && *a->disabled_reason + ? a->disabled_reason + : "otillgänglig"; + snprintf(buf, n, "%s (%s)", label, reason); + } else { + snprintf(buf, n, "%s", label); + } +} + +/* Action rows sit under the fields, one blank line below them. */ +static void form_draw_actions(const struct tui_form_action *acts, int na, + int nf, int sel) +{ + int y = 5 + nf + 1; + for (int i = 0; i < na; i++) { + if (y + i >= LINES - 2) + break; + char line[832]; + tui_form_act_label(&acts[i], line, sizeof line); + int dim = acts[i].enabled != 1; + if (dim) + attron(tui_style_attrs(TUI_DIM, g_colours, 0)); + if (sel == nf + i) + attron(A_REVERSE); + mvaddnstr(y + i, 2, line, COLS - 4); + if (sel == nf + i) + attroff(A_REVERSE); + if (dim) + attroff(tui_style_attrs(TUI_DIM, g_colours, 0)); + } +} + +static int form_run(const char *title, struct tui_form_field *f, int nf, + int can_edit, const struct tui_form_action *acts, int na, + const char *hint, int (*key)(void *ud, int ch), void *ud) { - int sel = 0; + int sel = nf > 0 ? 0 : tui_form_act_first(0, acts, na); for (;;) { - if (sel >= n) - sel = n - 1; + int ntot = nf + (na > 0 ? na : 0); + if (ntot <= 0) + return TUI_FORM_BACK; if (sel < 0) - sel = 0; + sel = tui_form_act_first(nf, acts, na); + if (sel >= ntot) + sel = ntot - 1; tui_frame(title); tui_style(TUI_HEADING); mvaddstr(3, 2, "Uppgift"); mvaddstr(3, 34, "Värde"); tui_style_reset(); - for (int i = 0; i < n; i++) { + for (int i = 0; i < nf; i++) { char lbl[160], val[640], line[832]; snprintf(lbl, sizeof lbl, "%s", f[i].label); tui_pad_field(lbl, sizeof lbl, 31); @@ -1396,19 +1491,32 @@ int tui_form_run_hook(const char *title, struct tui_form_field *f, int n, else if (!can_edit) attroff(tui_style_attrs(TUI_DIM, g_colours, 0)); } - tui_hints(g_form_hint - ? g_form_hint - : (can_edit - ? "upp/ned/Home/End Enter = ändra" - " F5 = uppdatera ^Enter = spara" - " Esc/q = tillbaka ^C = avsluta" - : "upp/ned/Home/End F5 = uppdatera" - " Esc/q = tillbaka ^C = avsluta" - " (endast behöriga kan ändra)")); + if (na > 0) + form_draw_actions(acts, na, nf, sel); + const char *h = g_form_hint ? g_form_hint : hint; + if (!h) + h = na > 0 + ? (can_edit + ? "upp/ned/Tab = flytta Enter = ändra/utför" + " F5 = uppdatera ^Enter = spara" + " Esc/q = tillbaka ^C = avsluta" + : "upp/ned/Tab = flytta Enter = utför" + " F5 = uppdatera Esc/q = tillbaka" + " ^C = avsluta" + " (endast behöriga kan ändra)") + : (can_edit + ? "upp/ned/Home/End Enter = ändra" + " F5 = uppdatera ^Enter = spara" + " Esc/q = tillbaka ^C = avsluta" + : "upp/ned/Home/End F5 = uppdatera" + " Esc/q = tillbaka ^C = avsluta" + " (endast behöriga kan ändra)"); + tui_hints(h); g_form_hint = NULL; refresh(); int ch = in_key(); - int nxt = tui_form_next(sel, n, ch); + int nxt = na > 0 ? tui_form_act_key(sel, nf, acts, na, ch) + : tui_form_next(sel, nf, ch); if (nxt >= 0) { sel = nxt; continue; @@ -1417,18 +1525,33 @@ int tui_form_run_hook(const char *title, struct tui_form_field *f, int n, nxt == TUI_FORM_BACK) return nxt; if (key) { - int h = key(ud, ch); - if (h == TUI_HOOK_BACK) + int hk = key(ud, ch); + if (hk == TUI_HOOK_BACK) return TUI_FORM_BACK; - if (h == TUI_HOOK_REFRESH) + if (hk == TUI_HOOK_REFRESH) return TUI_FORM_REFRESH; - if (h == TUI_HOOK_SUBMIT) + if (hk == TUI_HOOK_SUBMIT) return TUI_FORM_SUBMIT; - if (h != TUI_HOOK_NONE) + if (hk != TUI_HOOK_NONE) continue; } if (ch != '\n' && ch != '\r' && ch != KEY_ENTER) continue; + if (sel < 0) + continue; /* no focusable row: only the return keys work */ + if (sel >= nf) { + const struct tui_form_action *act = &acts[sel - nf]; + if (act->enabled == 0) { + tui_message(title, "%s", + act->disabled_reason && *act->disabled_reason + ? act->disabled_reason + : "Otillgänglig."); + continue; + } + if (act->enabled == -1) + continue; + return TUI_FORM_ACTION + (sel - nf); + } if (!can_edit) { tui_message(title, "Endast behöriga kan ändra."); continue; @@ -1461,6 +1584,25 @@ int tui_form_run_hook(const char *title, struct tui_form_field *f, int n, } } +int tui_form_run(const char *title, struct tui_form_field *f, int n, + int can_edit) +{ + return form_run(title, f, n, can_edit, NULL, 0, NULL, NULL, NULL); +} + +int tui_form_run_hook(const char *title, struct tui_form_field *f, int n, + int can_edit, int (*key)(void *ud, int ch), void *ud) +{ + return form_run(title, f, n, can_edit, NULL, 0, NULL, key, ud); +} + +int tui_form_run_actions(const char *title, struct tui_form_field *f, int nf, + int can_edit, const struct tui_form_action *acts, + int na, const char *hint) +{ + return form_run(title, f, nf, can_edit, acts, na, hint, NULL, NULL); +} + /* ------------------------------------------------------------------ */ /* row table (dynamic multi-column editor with a trailing blank row) */ /* ------------------------------------------------------------------ */ diff --git a/clients/tui.h b/clients/tui.h index 0950eac..d0656da 100644 --- a/clients/tui.h +++ b/clients/tui.h @@ -185,6 +185,41 @@ void tui_form_hint(const char *hint); /* overrides the footer for the next run * int tui_form_run_hook(const char *title, struct tui_form_field *f, int n, int can_edit, int (*key)(void *ud, int ch), void *ud); +/* --- action list in a form (tui_form_run_actions) --- */ +/* enabled: 1 = selectable and runs; 0 = dimmed but selectable (Enter shows + disabled_reason in a message); -1 = non-selectable heading/status row, + drawn dim like a menu section and skipped by navigation. */ +struct tui_form_action { + const char *label; + int enabled; + const char *disabled_reason; +}; + +#define TUI_FORM_ACTION 1000 /* action i is returned as TUI_FORM_ACTION + i */ + +/* Focus ring over fields (0..nf-1) and actions (nf..nf+na-1); heading rows + are skipped and the ring wraps. dir +1 = Tab/Down, -1 = Shift-Tab/Up. */ +int tui_form_act_step(int focus, int nf, const struct tui_form_action *acts, + int na, int dir); +/* Same ring for one key, plus Home/End/PgUp/PgDn and the form return keys + (F5/^Enter/F9/Esc/q). Returns the new focus, a TUI_FORM_* code or + TUI_NAV_NONE. */ +int tui_form_act_key(int sel, int nf, const struct tui_form_action *acts, + int na, int ch); +/* Index of the first focusable row (field 0 when fields exist, else the + first selectable action), or -1. */ +int tui_form_act_first(int nf, const struct tui_form_action *acts, int na); +/* "label", or "label (reason)" for a disabled selectable action. */ +void tui_form_act_label(const struct tui_form_action *a, char *buf, size_t n); + +/* Form with fields and an action list below them (see TUI-GUIDELINES.md). + Returns a field index after an edit, TUI_FORM_ACTION + i for a chosen + action, or TUI_FORM_BACK/REFRESH/SUBMIT. `hint` overrides the default + footer; tui_form_hint() still takes precedence. */ +int tui_form_run_actions(const char *title, struct tui_form_field *f, int nf, + int can_edit, const struct tui_form_action *acts, + int na, const char *hint); + /* --- row table --- */ enum { TUI_RT_EDIT = 0, TUI_RT_INFO, TUI_RT_DATE }; diff --git a/docs/STATE.md b/docs/STATE.md index 5112455..a108a52 100644 --- a/docs/STATE.md +++ b/docs/STATE.md @@ -59,10 +59,11 @@ server/protocol/ledger only. 11. **UI keys**: Ctrl+N = add, F5 = refresh, Esc/q = back (never exits), Ctrl+C = quit, 1–9/g = jump, F7 = clear row in editors, F9 = save. Shared line editor and date field; see TUI-GUIDELINES.md. The dashboard - is sectioned (Bokföring / Rapporter & bokslut / Register / Övrigt) with - non-selectable headers; theme/styles and pager markup live in the widget - layer (`tui_style`, `tui_markup`). Year-end **Bokslut** and the former - "Information om året" are one screen (`yearinfo` is an alias). + is sectioned (Bokföring / Rapporter & bokslut / Register / Räkenskapsår + / Övrigt) with non-selectable headers; theme/styles and pager markup + live in the widget layer (`tui_style`, `tui_markup`). Year-end + **Bokslut** and the former "Information om året" are one screen + (`yearinfo` is an alias). 12. **Login**: two steps — credentials, then org picker ("Välj organisation att representera"). No org switch in the dashboard; fiscal year switch is on the dashboard. `--org ID` bypasses the picker. @@ -89,6 +90,18 @@ server/protocol/ledger only. 20/21, reverse-charge output VAT 2614/2624/2634 in 30/31/32, and box 48 signed negative. Rules may share a box and are summed; box 49 is the sum of the moms boxes only. v3 migrates existing databases. +17. **Year-end hub and menu IA (2026-09-19)**: the forms widget layer gained + an action list (`tui_form_run_actions`; fields and actions in one focus + ring, `TUI_FORM_ACTION + i` for a chosen action, dimmed-but-selectable + rows with a `disabled_reason`, `-1` heading/status rows). The **Bokslut** + screen is the year-end hub: the eight year fields, the derived status + row `Bokslut bokfört: ja/nej` (`voucher.list` with + `text:"Skatt på årets resultat"`; a closed year counts as posted) and + the actions Årsredovisning (K2), Inkomstdeklaration (INK2/SRU), + Bokslutsplan (torrkörning) and Bokför bokslut (dimmed with a reason on a + closed or already posted year). Årsredovisning and INK2/SRU left the + Rapporter menu, and Ingående balans moved from Bokföring to the + dashboard's Räkenskapsår section next to Räkenskapsår. ## Pending decisions diff --git a/docs/TUI-GUIDELINES.md b/docs/TUI-GUIDELINES.md index decca09..3bed983 100644 --- a/docs/TUI-GUIDELINES.md +++ b/docs/TUI-GUIDELINES.md @@ -97,6 +97,22 @@ written compactly as `^N`, `^A`, `^C`, `^R` to save width. changing anything. Typing a printable character hands the focus to the first table cell and inserts it there, so rows can be entered without touching the header. `F5`/`Ctrl+Enter`/`Esc` work the same from both. +- A form may carry an action list under its fields (`tui_form_run_actions`): + fields first, then action rows, one focus ring (`Tab`/`Shift-Tab`/ + up/down, `Home`/`End`; the ring wraps and heading rows are skipped). + `enabled: 1` runs on `Enter`; `0` is dimmed but selectable and shown as + `label (reason)`, `Enter` shows the reason in a message; `-1` is a dim + non-selectable heading/status row like a menu section. The call returns + the field index after an edit, `TUI_FORM_ACTION + i` for a chosen action, + or `TUI_FORM_BACK`/`REFRESH`/`SUBMIT`; `tui_form_hint()` overrides the + footer of the next run as usual. +- `Bokslut` is the year-end hub in one view: the year fields, the status + heading `Bokslut bokfört: ja/nej` (derived from the `Skatt på årets + resultat` voucher that `bokslut.post` creates; a closed year counts as + posted) and the actions Årsredovisning (K2), Inkomstdeklaration + (INK2/SRU), Bokslutsplan (torrkörning) and Bokför bokslut — the last + dimmed with a reason when the year is closed or already posted. `F5` + shows the plan, `Ctrl+Enter` posts it. - Validation: `F5` validates without writing and reports exactly what is wrong (field, row number). Server `dry_run` is used where available. - Saving: `Ctrl+Enter` writes, shows a confirmation message, and returns to diff --git a/tests/test_tui.c b/tests/test_tui.c index 93aece0..15a44c7 100644 --- a/tests/test_tui.c +++ b/tests/test_tui.c @@ -351,6 +351,88 @@ static void test_form_nav(void) CHECK(tui_form_next(1, 3, 'x') == TUI_NAV_NONE); } +/* Fields 0..2, then six action rows: 0 heading, 1/2 selectable, 3 disabled + but selectable, 4 heading, 5 selectable. Focus 3..8 is actions 0..5. */ +static const struct tui_form_action ACTS[6] = { + { "Bokslut bokfört: nej", -1, NULL }, + { "Årsredovisning (K2)", 1, NULL }, + { "Inkomstdeklaration (INK2/SRU)", 1, NULL }, + { "Bokför bokslut", 0, "året är stängt" }, + { "rubrik igen", -1, NULL }, + { "Bokslutsplan (torrkörning)", 1, NULL }, +}; + +static void test_form_actions(void) +{ + const int nf = 3; + + CHECK(TUI_FORM_ACTION > 0); + CHECK(TUI_FORM_ACTION != TUI_FORM_BACK); + CHECK(TUI_FORM_ACTION != TUI_FORM_REFRESH); + CHECK(TUI_FORM_ACTION != TUI_FORM_SUBMIT); + CHECK(TUI_FORM_ACTION + 5 > nf); + + /* initial focus: first field, or the first selectable action */ + CHECK(tui_form_act_first(nf, ACTS, 6) == 0); + CHECK(tui_form_act_first(0, ACTS, 6) == 1); + static const struct tui_form_action HEADS[2] = { + { "rubrik", -1, NULL }, + { "rubrik", -1, NULL }, + }; + CHECK(tui_form_act_first(0, HEADS, 2) == -1); + CHECK(tui_form_act_first(4, ACTS, 6) == 0); + + /* the ring runs fields -> actions -> back, skipping headings */ + CHECK(tui_form_act_step(0, nf, ACTS, 6, 1) == 1); + CHECK(tui_form_act_step(1, nf, ACTS, 6, 1) == 2); + CHECK(tui_form_act_step(2, nf, ACTS, 6, 1) == 4); /* skip focus 3 */ + CHECK(tui_form_act_step(4, nf, ACTS, 6, 1) == 5); + CHECK(tui_form_act_step(5, nf, ACTS, 6, 1) == 6); /* disabled, selectable */ + CHECK(tui_form_act_step(6, nf, ACTS, 6, 1) == 8); /* skip focus 7 */ + CHECK(tui_form_act_step(8, nf, ACTS, 6, 1) == 0); /* wrap */ + CHECK(tui_form_act_step(0, nf, ACTS, 6, -1) == 8); /* wrap back */ + CHECK(tui_form_act_step(8, nf, ACTS, 6, -1) == 6); + CHECK(tui_form_act_step(6, nf, ACTS, 6, -1) == 5); + CHECK(tui_form_act_step(5, nf, ACTS, 6, -1) == 4); + CHECK(tui_form_act_step(4, nf, ACTS, 6, -1) == 2); /* skip focus 3 */ + /* an out-of-range focus snaps back into the ring */ + CHECK(tui_form_act_step(99, nf, ACTS, 6, 1) == 1); + + /* keys: Tab/Shift-Tab/arrows walk the ring, Home/End the ends */ + CHECK(tui_form_act_key(0, nf, ACTS, 6, '\t') == 1); + CHECK(tui_form_act_key(0, nf, ACTS, 6, KEY_BTAB) == 8); + CHECK(tui_form_act_key(0, nf, ACTS, 6, KEY_UP) == 8); + CHECK(tui_form_act_key(8, nf, ACTS, 6, KEY_DOWN) == 0); + CHECK(tui_form_act_key(4, nf, ACTS, 6, KEY_HOME) == 0); + CHECK(tui_form_act_key(0, nf, ACTS, 6, KEY_END) == 8); + CHECK(tui_form_act_key(0, nf, ACTS, 6, KEY_NPAGE) == 4); + CHECK(tui_form_act_key(8, nf, ACTS, 6, KEY_PPAGE) == 4); + CHECK(tui_form_act_key(0, nf, ACTS, 6, KEY_F(5)) == TUI_FORM_REFRESH); + CHECK(tui_form_act_key(5, nf, ACTS, 6, TUI_KEY_CTRL_ENTER) == + TUI_FORM_SUBMIT); + CHECK(tui_form_act_key(5, nf, ACTS, 6, 27) == TUI_FORM_BACK); + CHECK(tui_form_act_key(5, nf, ACTS, 6, 'x') == TUI_NAV_NONE); + + /* labels: plain, disabled with reason, heading, fallback reason */ + char buf[128]; + tui_form_act_label(&ACTS[1], buf, sizeof buf); + CHECK(strcmp(buf, "Årsredovisning (K2)") == 0); + tui_form_act_label(&ACTS[3], buf, sizeof buf); + CHECK(strcmp(buf, "Bokför bokslut (året är stängt)") == 0); + tui_form_act_label(&ACTS[0], buf, sizeof buf); + CHECK(strcmp(buf, "Bokslut bokfört: nej") == 0); + const struct tui_form_action no_reason = { "Stängd", 0, NULL }; + tui_form_act_label(&no_reason, buf, sizeof buf); + CHECK(strcmp(buf, "Stängd (otillgänglig)") == 0); + tui_form_act_label(NULL, buf, sizeof buf); + CHECK(buf[0] == '\0'); + + /* without fields the ring cycles the selectable actions only */ + CHECK(tui_form_act_step(0, 0, ACTS, 6, 1) == 1); + CHECK(tui_form_act_step(5, 0, ACTS, 6, 1) == 1); /* wraps past the head */ + CHECK(tui_form_act_step(1, 0, ACTS, 6, -1) == 5); +} + static char rt_cells[8][3][64]; static void rt_cb(void *ud, int row, int col, char **buf, size_t *cap) @@ -534,6 +616,7 @@ int main(void) test_nav(); test_nav_sections(); test_form_nav(); + test_form_actions(); test_rowtable(); test_rt_fields(); test_rt_focus(); |
