From 8da6a7e849513e1cb63410d95b121df729260d0c Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Sat, 19 Sep 2026 23:05:44 +0200 Subject: tui: themes, pager markup, menu sections; merge Bokslut and year info --- clients/bokftui.c | 442 ++++++++++++++++++++++++++++++------------------------ clients/tui.c | 339 +++++++++++++++++++++++++++++++++-------- clients/tui.h | 35 +++++ 3 files changed, 557 insertions(+), 259 deletions(-) (limited to 'clients') diff --git a/clients/bokftui.c b/clients/bokftui.c index e94faeb..64bed07 100644 --- a/clients/bokftui.c +++ b/clients/bokftui.c @@ -25,6 +25,12 @@ #define KEY_CTRL_X 0x18 /* ^Enter saves/posts the form; sent via xterm modifyOtherKeys */ #define KEY_CTRL_ENTER (KEY_MAX + 1) +/* Pager markup: headings, dimmed derived lines and full-width rules. */ +#define MK_HEAD "\001" +#define MK_DIM "\002" +#define MK_RULE "\003" +/* Menu item prefix for a non-selectable section header. */ +#define MK_SECTION MK_HEAD /* ^C quits the application; Esc is navigation only */ static int g_quit = 0; /* ^R re-execs the binary, keeping session, org, year and screen */ @@ -1916,13 +1922,14 @@ static void buf_line(struct buf *b, const char *fmt, ...) static void buf_rule(struct buf *b, int width) { char line[128]; - if (width > (int)sizeof line - 2) - width = (int)sizeof line - 2; + if (width > (int)sizeof line - 3) + width = (int)sizeof line - 3; if (width < 1) return; - memset(line, '-', (size_t)width); - line[width] = '\n'; - buf_append(b, line, (size_t)width + 1); + line[0] = MK_RULE[0]; + memset(line + 1, '-', (size_t)width); + line[width + 1] = '\n'; + buf_append(b, line, (size_t)width + 2); } /* prefix (8) + label (48) + amount columns */ @@ -1935,9 +1942,9 @@ static void fmt_head(struct buf *t, const struct app *a, const char *title, yyjson_val *fy = yyjson_obj_get(res, "fiscal_year"); const char *start = vstr(fy, "start_date"); const char *end = vstr(fy, "end_date"); - buf_line(t, "%s\n%s\n\n", title, a->org_name); - buf_line(t, "Räkenskapsår: %s - %s\n", start, end); - buf_line(t, "Period: %s - %s\n\n", start, end); + buf_line(t, MK_HEAD "%s\n%s\n\n", title, a->org_name); + buf_line(t, MK_DIM "Räkenskapsår: %s - %s\n", start, end); + buf_line(t, MK_DIM "Period: %s - %s\n\n", start, end); } static int acct_in(const char *number, int lo, int hi) @@ -2018,7 +2025,7 @@ static void bs_sum_line(struct buf *t, const char *label, 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); + buf_line(t, MK_DIM " %5s %s %s %s %s %s\n", "", lbl, a1, a2, a3, a4); } static int bs_group(struct buf *t, yyjson_val *res, const char *title, @@ -2027,7 +2034,7 @@ static int bs_group(struct buf *t, yyjson_val *res, const char *title, struct bs_sum probe = { 0, 0, 0 }; if (!bs_rows(NULL, res, lo, hi, &probe)) return 0; - buf_line(t, "%s\n", title); + buf_line(t, MK_HEAD "%s\n", title); struct bs_sum s = { 0, 0, 0 }; bs_rows(t, res, lo, hi, &s); bs_sum_line(t, sumlabel, &s); @@ -2056,12 +2063,12 @@ static void fmt_balans(struct buf *t, const struct app *a, yyjson_val *res) fmt_head(t, a, "Balansrapport", res); struct bs_sum anl = { 0, 0, 0 }, oms = { 0, 0, 0 }; struct bs_sum eget = { 0, 0, 0 }, skuld = { 0, 0, 0 }; - buf_line(t, "TILLGÅNGAR\n"); + buf_line(t, MK_HEAD "TILLGÅNGAR\n"); bs_col_head(t); - buf_line(t, "\nAnläggningstillgångar\n"); + buf_line(t, "\n" MK_HEAD "Anläggningstillgångar\n"); bs_rows(t, res, 1000, 1399, &anl); bs_sum_line(t, "Summa anläggningstillgångar", &anl); - buf_line(t, "\nOmsättningstillgångar\n"); + buf_line(t, "\n" MK_HEAD "Omsättningstillgångar\n"); int printed = 0; for (size_t i = 0; i < sizeof OMS / sizeof OMS[0]; i++) { struct bs_sum probe = { 0, 0, 0 }; @@ -2078,9 +2085,9 @@ static void fmt_balans(struct buf *t, const struct app *a, yyjson_val *res) buf_rule(t, BS_RULE_W); bs_sum_line(t, "Summa tillgångar", &as); - buf_line(t, "\nEGET KAPITAL OCH SKULDER\n"); + buf_line(t, "\n" MK_HEAD "EGET KAPITAL OCH SKULDER\n"); bs_col_head(t); - buf_line(t, "\nEget kapital\n"); + buf_line(t, "\n" MK_HEAD "Eget kapital\n"); bs_group(t, res, "Bundet eget kapital", "Summa bundet eget kapital", 2000, 2090, &eget); buf_line(t, "\n"); @@ -2106,7 +2113,7 @@ static void fmt_balans(struct buf *t, const struct app *a, yyjson_val *res) 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); + buf_line(t, MK_DIM " %5s %s %s %s %s %s\n", "", lbl, zero, zero, a1, a1); } /* ---------------- resultaträkning ---------------- */ @@ -2175,7 +2182,7 @@ static void is_summary(struct buf *t, const char *label, int64_t cur, 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); + buf_line(t, MK_DIM " %5s %s %s %s %s\n", "", lbl, a1, a1, a2); } static void is_print_group(struct buf *t, yyjson_val *res, yyjson_val *prev, @@ -2265,22 +2272,23 @@ static void fmt_resultat(struct buf *t, const struct app *a, yyjson_val *res, is_summary(t, "Summa rörelseintäkter, lagerförändringar m.m.", rev, rev_pre, 0); buf_line(t, "\n"); - buf_line(t, "%s\n", gr->section); + buf_line(t, MK_HEAD "%s\n", gr->section); } else { char lbl[80], h1[32], h2[32], h3[32]; 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_line(t, MK_HEAD " %5s %s %s %s %s\n", "", lbl, h1, h2, + h3); buf_rule(t, IS_RULE_W); buf_line(t, "\n"); - buf_line(t, "%s\n", gr->title); + buf_line(t, MK_HEAD "%s\n", gr->title); } sect = gr->section; } else { buf_line(t, "\n"); - buf_line(t, "%s\n", gr->title); + buf_line(t, MK_HEAD "%s\n", gr->title); } int64_t sc = 0, sp = 0; is_print_group(t, res, prev, gr, &sc, &sp); @@ -2300,7 +2308,7 @@ static void fmt_resultat(struct buf *t, const struct app *a, yyjson_val *res, int has_tax = is_range_has(res, 8900, 8989); int has_booked = is_range_has(res, 8999, 8999); if (is_range_has(res, 8000, 8599)) { - buf_line(t, "\nFinansiella poster\n"); + buf_line(t, "\n" MK_HEAD "Finansiella poster\n"); is_range_rows(t, res, prev, 8000, 8599, &fin, &fin_pre); is_summary(t, "Summa", fin, fin_pre, 0); } @@ -2311,14 +2319,14 @@ static void fmt_resultat(struct buf *t, const struct app *a, yyjson_val *res, is_summary(t, "Rörelseresultat", ror, ror_pre, 0); is_summary(t, "Resultat efter finansiella poster", after, after_pre, 1); if (has_disp) { - buf_line(t, "\nBokslutsdispositioner\n"); + buf_line(t, "\n" MK_HEAD "Bokslutsdispositioner\n"); is_range_rows(t, res, prev, 8800, 8899, &disp, &disp_pre); is_summary(t, "Summa", disp, disp_pre, 0); } int64_t before_tax = after + disp, before_tax_pre = after_pre + disp_pre; is_summary(t, "Resultat före skatt", before_tax, before_tax_pre, 1); if (has_tax) { - buf_line(t, "\nSkatt\n"); + buf_line(t, "\n" MK_HEAD "Skatt\n"); is_range_rows(t, res, prev, 8900, 8989, &tax, &tax_pre); is_summary(t, "Summa", tax, tax_pre, 0); } @@ -2327,7 +2335,7 @@ static void fmt_resultat(struct buf *t, const struct app *a, yyjson_val *res, buf_rule(t, IS_RULE_W); is_summary(t, "Årets resultat", ars, ars_pre, 0); if (has_booked) { - buf_line(t, "\nBokfört resultat\n"); + buf_line(t, "\n" MK_HEAD "Bokfört resultat\n"); is_range_rows(t, res, prev, 8999, 8999, &booked, &booked_pre); is_summary(t, "Summa", booked, booked_pre, 0); buf_line(t, "\n"); @@ -2353,7 +2361,8 @@ static void fmt_saldobalans(struct buf *t, const struct app *a, 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_line(t, MK_HEAD " %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"); size_t n = yyjson_arr_size(arr); @@ -2377,7 +2386,7 @@ static void fmt_saldobalans(struct buf *t, const struct app *a, 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); + buf_line(t, MK_DIM " %5s %s %s %s %s %s\n", "", lbl, a1, a2, a3, a4); } /* ---------------- momsdeklaration ---------------- */ @@ -2444,9 +2453,9 @@ static const char *vat_section_title(const char *s) static void fmt_moms(struct buf *t, const struct app *a, yyjson_val *res) { - buf_line(t, "Momsrapport\n\n%s\n\n", a->org_name); - buf_line(t, "Räkenskapsår: %s - %s\n", a->fy_start, a->fy_end); - buf_line(t, "Period: %s - %s\n", vstr(res, "from"), + buf_line(t, MK_HEAD "Momsrapport\n\n%s\n\n", a->org_name); + buf_line(t, MK_DIM "Räkenskapsår: %s - %s\n", a->fy_start, a->fy_end); + buf_line(t, MK_DIM "Period: %s - %s\n", vstr(res, "from"), vstr(res, "to")); const char *sect = NULL; yyjson_val *boxes = yyjson_obj_get(res, "boxes"); @@ -2457,7 +2466,7 @@ static void fmt_moms(struct buf *t, const struct app *a, yyjson_val *res) continue; /* last */ if (!sect || strcmp(sect, vr->section) != 0) { sect = vr->section; - buf_line(t, "\n%s %s\n", vr->section, + buf_line(t, "\n" MK_HEAD "%s %s\n", vr->section, vat_section_title(vr->section)); } int64_t ore = 0; @@ -2483,14 +2492,14 @@ static void fmt_moms(struct buf *t, const struct app *a, yyjson_val *res) break; } } - buf_line(t, "\nG. %s\n", vat_section_title("G.")); + buf_line(t, "\n" MK_HEAD "G. %s\n", vat_section_title("G.")); buf_rule(t, 87); char amt49[48], lbl49[192]; kr_whole(ore49, amt49, sizeof amt49); snprintf(lbl49, sizeof lbl49, "%s", "Moms att betala (+) eller att få tillbaka (-)"); pad_min(lbl49, sizeof lbl49, 66); - buf_line(t, "49. %s %14s\n", lbl49, amt49); + buf_line(t, MK_DIM "49. %s %14s\n", lbl49, amt49); } static int64_t prev_fy_id(struct app *a) @@ -2529,9 +2538,9 @@ static void fmt_huvudbok(struct buf *t, const struct app *a, yyjson_val *res) fmt_head(t, a, "Huvudbok", res); yyjson_val *lv = yyjson_obj_get(res, "last_voucher"); if (lv) - buf_line(t, "Senaste ver.: %s%lld\n\n", vstr(lv, "series"), + buf_line(t, MK_DIM "Senaste ver.: %s%lld\n\n", vstr(lv, "series"), (long long)vint(lv, "number")); - buf_line(t, "%-8s %-58s %14s %14s %16s\n", "Konto", "", "Debet", + buf_line(t, MK_HEAD "%-8s %-58s %14s %14s %16s\n", "Konto", "", "Debet", "Kredit", "Saldo"); buf_rule(t, 114); yyjson_val *accs = yyjson_obj_get(res, "accounts"); @@ -2541,14 +2550,15 @@ static void fmt_huvudbok(struct buf *t, const struct app *a, yyjson_val *res) int64_t ib = vint(ao, "ib_ore"); int64_t d = vint(ao, "debit_ore"); int64_t c = vint(ao, "credit_ore"); - buf_line(t, "\n%s %s\n\n", vstr(ao, "account"), vstr(ao, "name")); + buf_line(t, "\n" MK_HEAD "%s %s\n\n", vstr(ao, "account"), + vstr(ao, "name")); char a1[48]; tui_amt_col(a1, sizeof a1, 16, ib); char lbl[96]; tui_pad_label(lbl, sizeof lbl, "Ingående balans:", 59); - buf_line(t, " %s %14s %14s %16s\n", lbl, "", "", a1); + buf_line(t, MK_DIM " %s %14s %14s %16s\n", lbl, "", "", a1); tui_pad_label(lbl, sizeof lbl, "Ingående saldo:", 59); - buf_line(t, " %s %14s %14s %16s\n", lbl, "", "", a1); + buf_line(t, MK_DIM " %s %14s %14s %16s\n", lbl, "", "", a1); buf_line(t, "\n"); yyjson_val *rows = yyjson_obj_get(ao, "rows"); size_t nr = yyjson_arr_size(rows); @@ -2574,10 +2584,10 @@ static void fmt_huvudbok(struct buf *t, const struct app *a, yyjson_val *res) 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); + buf_line(t, MK_DIM " %s %14s %14s %16s\n", lbl, s1, s2, s3); 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); + buf_line(t, MK_DIM " %s %14s %14s %16s\n", lbl, s1, s2, s3); } } @@ -2589,12 +2599,12 @@ static void fmt_verifikationslista(struct buf *t, const struct app *a, fmt_head(t, a, "Verifikationslista", res); yyjson_val *lv = yyjson_obj_get(res, "last_voucher"); if (lv) - buf_line(t, "Senaste ver.: %s%lld\n\n", vstr(lv, "series"), + buf_line(t, MK_DIM "Senaste ver.: %s%lld\n\n", vstr(lv, "series"), (long long)vint(lv, "number")); char nmh[64]; 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_line(t, MK_HEAD "%-8s %-11s %s %14s %14s\n", "Ver.", "Datum/Konto", + nmh, "Debet", "Kredit"); buf_rule(t, 98); yyjson_val *vs = yyjson_obj_get(res, "vouchers"); size_t n = yyjson_arr_size(vs); @@ -2603,7 +2613,7 @@ static void fmt_verifikationslista(struct buf *t, const struct app *a, char ver[32]; snprintf(ver, sizeof ver, "%s%lld", vstr(vo, "series"), (long long)vint(vo, "number")); - buf_line(t, "%-8s %-11s %s\n", ver, vstr(vo, "date"), + buf_line(t, MK_HEAD "%-8s %-11s %s\n", ver, vstr(vo, "date"), vstr(vo, "description")); yyjson_val *rows = yyjson_obj_get(vo, "rows"); size_t nr = yyjson_arr_size(rows); @@ -2623,7 +2633,7 @@ static void fmt_verifikationslista(struct buf *t, const struct app *a, char s1[48], s2[48]; 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); + buf_line(t, "\n" MK_DIM "%-69s %14s %14s\n", "Omslutning:", s1, s2); } static void eskd_save(struct app *a, const char *from, const char *to) @@ -2856,23 +2866,23 @@ static void sru_rows(struct buf *t, yyjson_val *arr) static void fmt_ink2(struct buf *t, const struct app *a, yyjson_val *res) { - buf_line(t, "Inkomstdeklaration 2 (INK2)\n%s\n\n", a->org_name); - buf_line(t, "Räkenskapsår: %s - %s\n", vstr(res, "from"), + buf_line(t, MK_HEAD "Inkomstdeklaration 2 (INK2)\n%s\n\n", a->org_name); + buf_line(t, MK_DIM "Räkenskapsår: %s - %s\n", vstr(res, "from"), vstr(res, "to")); - buf_line(t, "Blankett: %s\n", vstr(res, "period")); - buf_line(t, "\nINK2 - huvudblankett\n"); + buf_line(t, MK_DIM "Blankett: %s\n", vstr(res, "period")); + buf_line(t, "\n" MK_HEAD "INK2 - huvudblankett\n"); buf_rule(t, 81); sru_rows(t, yyjson_obj_get(res, "ink2")); - buf_line(t, "\nINK2R - räkenskapsschema\n"); + buf_line(t, "\n" MK_HEAD "INK2R - räkenskapsschema\n"); buf_rule(t, 81); sru_rows(t, yyjson_obj_get(res, "ink2r")); - buf_line(t, "\nINK2S - skattemässiga justeringar\n"); + buf_line(t, "\n" MK_HEAD "INK2S - skattemässiga justeringar\n"); buf_rule(t, 81); sru_rows(t, yyjson_obj_get(res, "ink2s")); yyjson_val *un = yyjson_obj_get(res, "unmapped"); size_t nu = yyjson_arr_size(un); if (nu) { - buf_line(t, "\nKonton utan SRU-mappning (måste åtgärdas):\n"); + buf_line(t, "\n" MK_HEAD "Konton utan SRU-mappning (måste åtgärdas):\n"); for (size_t i = 0; i < nu; i++) { yyjson_val *row = yyjson_arr_get(un, i); char amt[48]; @@ -4126,87 +4136,6 @@ static void board_screen(struct app *a) } } -/* "Information om året": the per-year årsredovisning details. New years - inherit the stable fields, so only what changed needs editing. */ -static void yearinfo_screen(struct app *a) -{ - static const char *const labels[6] = { - "Väsentliga händelser", - "Årsstämma (YYYY-MM-DD)", - "Föreslagen utdelning (kr)", - "Utdelningens datum (YYYY-MM-DD)", - "Medelantal anställda", - "Övriga noter (t.ex. revisor)", - }; - static const char *const keys[6] = { "events", "agm_date", "dividend_ore", - "dividend_date", "employees", - "notes" }; - const int nf = 6; - int can_edit = strcmp(a->role, "owner") == 0 || - strcmp(a->role, "bookkeeper") == 0; - for (;;) { - if (g_quit) - return; - char args[48]; - snprintf(args, sizeof args, "{\"id\":%lld}", (long long)a->fy); - char *resp = client_rpc(&a->conn, "fiscal_year.get", a->session, - a->org, args); - if (!resp || !client_ok(resp)) { - show_error("Information om året", resp); - free(resp); - return; - } - char *vals[6]; - 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 (r == TUI_FORM_BACK) - return; - } -} - static void company_screen(struct app *a) { const int nf = (int)(sizeof COMPANY_FIELDS / sizeof COMPANY_FIELDS[0]); @@ -4441,7 +4370,7 @@ static void fmt_bokslut_plan(struct buf *t, yyjson_val *res) { yyjson_val *fy = yyjson_obj_get(res, "fiscal_year"); if (fy) - buf_line(t, "Bokslutsplan %s %s - %s\n", vstr(fy, "label"), + buf_line(t, MK_HEAD "Bokslutsplan %s %s - %s\n", vstr(fy, "label"), vstr(fy, "start_date"), vstr(fy, "end_date")); yyjson_val *vs = yyjson_obj_get(res, "vouchers"); size_t n = yyjson_arr_size(vs); @@ -4463,7 +4392,8 @@ static void fmt_bokslut_plan(struct buf *t, yyjson_val *res) num = pnum + 1; } pnum = num; - buf_line(t, "\n%s%lld %s\n", pser, num, vstr(v, "description")); + buf_line(t, "\n" MK_HEAD "%s%lld %s\n", pser, num, + vstr(v, "description")); yyjson_val *rows = yyjson_obj_get(v, "rows"); size_t nr = yyjson_arr_size(rows); for (size_t k = 0; k < nr; k++) { @@ -4544,23 +4474,92 @@ 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. */ static void bokslut_screen(struct app *a) { + static const char *const labels[6] = { + "Väsentliga händelser", + "Årsstämma (YYYY-MM-DD)", + "Föreslagen utdelning (kr)", + "Utdelningens datum (YYYY-MM-DD)", + "Medelantal anställda", + "Övriga noter (t.ex. revisor)", + }; + static const char *const keys[6] = { "events", "agm_date", "dividend_ore", + "dividend_date", "employees", + "notes" }; + const int nf = 6; char fond[64] = "0,00", rate[16] = "20,6"; - struct tui_form_field ff[2]; + int can_edit = strcmp(a->role, "owner") == 0 || + strcmp(a->role, "bookkeeper") == 0; for (;;) { if (g_quit) return; + char args[48]; + snprintf(args, sizeof args, "{\"id\":%lld}", (long long)a->fy); + char *resp = client_rpc(&a->conn, "fiscal_year.get", a->session, + a->org, args); + if (!resp || !client_ok(resp)) { + show_error("Bokslut", resp); + free(resp); + return; + } + char *vals[6]; + struct tui_form_field ff[8]; + int64_t div_ore = jint_val(resp, "result.dividend_ore", 0); memset(ff, 0, sizeof ff); - ff[0].label = "Periodiseringsfond (kr)"; - ff[0].value = fond; - ff[0].cap = sizeof fond; + 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].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); + 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; + ff[6].label = "Periodiseringsfond (kr)"; + ff[6].value = fond; + ff[6].cap = sizeof fond; + ff[6].kind = TUI_F_TEXT; + ff[7].label = "Skattesats (%)"; + ff[7].value = rate; + ff[7].cap = sizeof rate; + ff[7].kind = TUI_F_TEXT; + int r = tui_form_run("Bokslut", ff, nf + 2, can_edit); + if (r >= 0 && r < nf) { + 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 (r == TUI_FORM_REFRESH) bokslut_plan(a, fond, rate, 0); else if (r == TUI_FORM_SUBMIT) @@ -4711,6 +4710,17 @@ static void ar_amt_col(char *buf, size_t n, int width, int64_t ore) snprintf(buf, n, "%*s", width, tmp); } +/* Sum and balance rows are derived figures and read as secondary. */ +static int ar_dim_label(const char *label) +{ + return strncmp(label, "Summa", 5) == 0 || strcmp(label, "Totalt") == 0 || + strncmp(label, "Belopp vid", 10) == 0 || + strcmp(label, "Årets resultat") == 0 || + strcmp(label, "Resultat före skatt") == 0 || + strcmp(label, "Rörelseresultat") == 0 || + strcmp(label, "Resultat efter finansiella poster") == 0; +} + static void ar_row(struct buf *t, const char *label, int64_t cur, int64_t prev, int indent) { @@ -4719,7 +4729,8 @@ static void ar_row(struct buf *t, const char *label, int64_t cur, int64_t prev, 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); + buf_line(t, "%s%s %s %s\n", ar_dim_label(label) ? MK_DIM : "", padded, + a1, a2); } /* Equity table: four amount columns. */ @@ -4733,7 +4744,8 @@ static void ar_eq_row(struct buf *t, const char *label, int64_t ak, ar_amt_col(c2, sizeof c2, 14, br); ar_amt_col(c3, sizeof c3, 14, ar); ar_amt_col(c4, sizeof c4, 14, ak + br + ar); - buf_line(t, "%s %s %s %s %s\n", padded, c1, c2, c3, c4); + buf_line(t, "%s%s %s %s %s %s\n", ar_dim_label(label) ? MK_DIM : "", + padded, c1, c2, c3, c4); } static void ar_wrap(struct buf *t, const char *text) @@ -4801,14 +4813,15 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, ar_sv_date(vstr(fyj, "agm_date"), agm_sv, sizeof agm_sv); ar_sv_date(vstr(fyj, "dividend_date"), pay_sv, sizeof pay_sv); - buf_line(t, "ÅRSREDOVISNING\n\n%s\nOrg nr %s\n\n", a->org_name, org_nr); + buf_line(t, MK_HEAD "ÅRSREDOVISNING\n\n%s\nOrg nr %s\n\n", a->org_name, + org_nr); buf_line(t, "Årsredovisning för räkenskapsåret %s - %s\n\n", a->fy_start, a->fy_end); buf_line(t, "Styrelsen avger följande årsredovisning.\n\n"); buf_line(t, "Om inte annat särskilt anges, redovisas alla belopp i hela" " kronor.\n"); - buf_line(t, "\nFastställelseintyg\n\n"); + buf_line(t, "\n" MK_HEAD "Fastställelseintyg\n\n"); buf_line(t, "Undertecknad styrelseledamot i %s intygar, dels att denna" " kopia av\nårsredovisningen stämmer överens med originalet," " dels att resultaträkningen\noch balansräkningen har" @@ -4819,15 +4832,15 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, " ___________________________\n\nNamnförtydligande: %s\n", city, agm_sv, first_board); - buf_line(t, "\nFörvaltningsberättelse\n\nVerksamheten\n\nAllmänt om" - " verksamheten\n"); + buf_line(t, "\n" MK_HEAD "Förvaltningsberättelse\n\nVerksamheten\n\n" + "Allmänt om verksamheten\n"); if (description && *description) ar_wrap(t, description); else buf_line(t, "[Kort beskrivning av verksamheten saknas — fyll i under" " Företagsuppgifter.]\n"); buf_line(t, "\nBolaget har sitt säte i %s.\n", city); - buf_line(t, "\nVäsentliga händelser under räkenskapsåret\n"); + buf_line(t, "\n" MK_HEAD "Väsentliga händelser under räkenskapsåret\n"); if (events && *events) ar_wrap(t, events); else @@ -4839,7 +4852,7 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, for (int i = 0; i < nyears; i++) off += snprintf(row + off, sizeof row - (size_t)off, "%9s", years[i].label); - buf_line(t, "\n%s\n\n", row); + buf_line(t, "\n" MK_HEAD "%s\n\n", row); off = snprintf(row, sizeof row, "%-38s", "Nettoomsättning, tkr"); for (int i = 0; i < nyears; i++) off += snprintf(row + off, sizeof row - (size_t)off, "%9lld", @@ -4874,7 +4887,7 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, "Årets"); off += snprintf(row + off, sizeof row - (size_t)off, "%14s", "Totalt"); - buf_line(t, "\n%s\n", row); + buf_line(t, "\n" MK_HEAD "%s\n", row); buf_line(t, "%s\n", " resultat" " resultat"); int has_disp = movement != 0; @@ -4890,8 +4903,8 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, } int64_t dispon = br_ub + ars; - buf_line(t, "\nResultatdisposition\n\nTill årsstämmans förfogande står" - " följande vinstmedel:\n"); + buf_line(t, "\n" MK_HEAD "Resultatdisposition\n\nTill årsstämmans" + " förfogande står följande vinstmedel:\n"); ar_row(t, "Balanserat resultat", br_ub, 0, 1); ar_row(t, "Årets resultat", ars, 0, 1); ar_row(t, "Totalt", dispon, 0, 1); @@ -4902,7 +4915,7 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, ar_row(t, "Totalt", dispon, 0, 1); if (dividend > 0) { - buf_line(t, "\nStyrelsens yttrande över den föreslagna" + buf_line(t, "\n" MK_HEAD "Styrelsens yttrande över den föreslagna" " vinstutdelningen\n\n"); if (shares > 0) { char per[48]; @@ -4922,8 +4935,8 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, " 3 § 2 st. (försiktighetsregeln).\n"); } - buf_line(t, "\nResultaträkning\nSamtliga belopp anges i svenska" - " kronor.\n\n"); + buf_line(t, "\n" MK_HEAD "Resultaträkning\nSamtliga belopp anges i" + " svenska kronor.\n\n"); { char h1[40], h2[40], row[512]; snprintf(h1, sizeof h1, "%s - %s", cur->from, cur->to); @@ -4937,14 +4950,14 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, off += snprintf(row + off, sizeof row - (size_t)off, "%17s", h2); buf_line(t, "%s\n", row); } - buf_line(t, "Rörelseintäkter, lagerförändringar m.m.\n"); + buf_line(t, MK_HEAD "Rörelseintäkter, lagerförändringar m.m.\n"); ar_row(t, "Nettoomsättning", cur->groups[0], prev ? prev->groups[0] : 0, 1); int64_t rev_cur = cur->groups[0] + cur->groups[1]; int64_t rev_pre = prev ? prev->groups[0] + prev->groups[1] : 0; ar_row(t, "Summa Rörelseintäkter, lagerförändringar m.m.", rev_cur, rev_pre, 1); - buf_line(t, "\nRörelsekostnader\n"); + buf_line(t, "\n" MK_HEAD "Rörelsekostnader\n"); int64_t cost_cur = 0, cost_pre = 0; for (size_t g = 2; g < AR_GROUPS; g++) { int64_t c = cur->groups[g], p = prev ? prev->groups[g] : 0; @@ -4965,7 +4978,7 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, int64_t btax = cur->res_fin + cur->disp; int64_t btax_pre = prev ? prev->res_fin + prev->disp : 0; if (cur->disp || (prev && prev->disp)) { - buf_line(t, "\nBokslutsdispositioner\n"); + buf_line(t, "\n" MK_HEAD "Bokslutsdispositioner\n"); ar_row(t, "Summa", cur->disp, prev ? prev->disp : 0, 1); } ar_row(t, "Resultat före skatt", btax, btax_pre, 0); @@ -4975,7 +4988,7 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, buf_line(t, "\n"); ar_row(t, "Årets resultat", ars, prev_ars, 0); - buf_line(t, "\nBalansräkning\n\n"); + buf_line(t, "\n" MK_HEAD "Balansräkning\n\n"); { char h1[40], h2[40], row[256]; snprintf(h1, sizeof h1, "%s", cur->to); @@ -4989,10 +5002,10 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, off += snprintf(row + off, sizeof row - (size_t)off, "%17s", h2); buf_line(t, "%s\n", row); } - buf_line(t, "Tillgångar\nAnläggningstillgångar\n"); + buf_line(t, MK_HEAD "Tillgångar\nAnläggningstillgångar\n"); ar_row(t, "Summa anläggningstillgångar", ar_bs_sum(bs, 1000, 1399), ar_bs_ib_sum(bs, 1000, 1399), 1); - buf_line(t, "Omsättningstillgångar\n"); + buf_line(t, MK_HEAD "Omsättningstillgångar\n"); int64_t v = ar_bs_sum(bs, 1400, 1499), vp = ar_bs_ib_sum(bs, 1400, 1499); if (v || vp) ar_row(t, "Varulager", v, vp, 0); @@ -5006,11 +5019,12 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, ar_bs_ib_sum(bs, 1900, 1999), 0); ar_row(t, "Summa tillgångar", ar_bs_sum(bs, 1000, 1999), ar_bs_ib_sum(bs, 1000, 1999), 0); - buf_line(t, "\nEget kapital och skulder\nBundet eget kapital\n"); + buf_line(t, "\n" MK_HEAD "Eget kapital och skulder\nBundet eget" + " kapital\n"); ar_row(t, "Aktiekapital", ak_ub, ak_ib, 1); ar_row(t, "Summa bundet eget kapital", ar_bs_sum(bs, 2000, 2090), ar_bs_ib_sum(bs, 2000, 2090), 1); - buf_line(t, "Fritt eget kapital\n"); + buf_line(t, MK_HEAD "Fritt eget kapital\n"); ar_row(t, "Balanserat resultat", br_ub, br_ib, 1); ar_row(t, "Årets resultat", ars, prev_ars, 1); ar_row(t, "Summa fritt eget kapital", ar_bs_sum(bs, 2091, 2098) + ars, @@ -5025,7 +5039,7 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, ar_bs_ib_sum(bs, 2100, 2199), 0); ar_row(t, "Avsättningar", ar_bs_sum(bs, 2200, 2299), ar_bs_ib_sum(bs, 2200, 2299), 0); - buf_line(t, "Kortfristiga skulder\n"); + buf_line(t, MK_HEAD "Kortfristiga skulder\n"); ar_row(t, "Skatteskulder", ar_bs_sum(bs, 2500, 2599), ar_bs_ib_sum(bs, 2500, 2599), 1); ar_row(t, "Mervärdesskatt", ar_bs_sum(bs, 2600, 2699), @@ -5042,20 +5056,21 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, ar_bs_sum(bs, 2000, 2999) + ars, ar_bs_ib_sum(bs, 2000, 2999) + prev_ars, 0); - buf_line(t, "\nNoter\n\nNot 1 Redovisnings- och värderingsprinciper\n"); + buf_line(t, "\n" MK_HEAD "Noter\n\nNot 1 Redovisnings- och" + " värderingsprinciper\n"); buf_line(t, "Årsredovisningen är upprättad i enlighet med" " årsredovisningslagen och\nBFNAR 2016:10 Årsredovisning i" " mindre företag.\n"); - buf_line(t, "\nNot 2 Medelantal anställda\n"); + buf_line(t, "\n" MK_HEAD "Not 2 Medelantal anställda\n"); buf_line(t, "Medelantalet anställda under räkenskapsåret har uppgått" " till %s.\n", employees && *employees ? employees : "[antal]"); if (notes && *notes) { - buf_line(t, "\nNot 3 Övriga upplysningar\n"); + buf_line(t, "\n" MK_HEAD "Not 3 Övriga upplysningar\n"); ar_wrap(t, notes); } - buf_line(t, "\nUnderskrifter\n\n%s %s\n\n", city, agm_sv); + buf_line(t, "\n" MK_HEAD "Underskrifter\n\n%s %s\n\n", city, agm_sv); if (board_text && *board_text) buf_line(t, "%s", board_text); else @@ -5063,6 +5078,24 @@ static void fmt_arsredovisning(struct buf *t, const struct app *a, " Företagsuppgifter.]\n"); } +/* The pager text carries style markers that must not reach the saved file. */ +static void strip_markup(const char *in, struct buf *out) +{ + for (const char *p = in; *p;) { + const char *nl = strchr(p, '\n'); + size_t len = nl ? (size_t)(nl - p) : strlen(p); + if (len && p[0] >= TUI_MARK_HEADING && p[0] <= TUI_MARK_RULE) { + p++; + len--; + } + buf_append(out, p, len); + if (!nl) + break; + buf_append(out, "\n", 1); + p = nl + 1; + } +} + static void arsredovisning_save(struct app *a, const char *text) { const char *home = getenv("HOME"); @@ -5089,16 +5122,21 @@ static void arsredovisning_save(struct app *a, const char *text) *p = '-'; snprintf(full + strlen(full), sizeof full - strlen(full), "Årsredovisning %s.txt", *name ? name : "utkast"); + struct buf clean; + buf_init(&clean); + strip_markup(text, &clean); FILE *f = fopen(full, "wb"); - size_t n = strlen(text); - if (!f || fwrite(text, 1, n, f) != n) { + size_t n = clean.len; + if (!f || fwrite(clean.p, 1, n, f) != n) { if (f) fclose(f); + buf_free(&clean); tui_message("Årsredovisning", "Kunde inte skriva %s: %s", full, strerror(errno)); return; } fclose(f); + buf_free(&clean); tui_message("Årsredovisning", "Sparat: %s\n\nFilen är ett utkast i textform" " att redigera vidare (t.ex. i Word).", full); } @@ -5258,10 +5296,9 @@ static void open_scene(struct app *a, const char *scene) settings_screen(a); else if (strcmp(scene, "company") == 0) company_screen(a); - else if (strcmp(scene, "bokslut") == 0) + else if (strcmp(scene, "bokslut") == 0 || + strcmp(scene, "yearinfo") == 0) /* yearinfo: ^R compatibility */ bokslut_screen(a); - else if (strcmp(scene, "yearinfo") == 0) - yearinfo_screen(a); } static void do_reload(struct app *a, const char *self) @@ -5296,10 +5333,14 @@ static void do_reload(struct app *a, const char *self) } static const char *const MAIN_ITEMS[] = { - "Verifikat", "Underlag (inkorg)", "Ingående balans", - "Mallar", "Rapporter", "Revision", - "Inställningar", "Företagsuppgifter", "Bokslut", - "Information om året", "Räkenskapsår", "Logga ut / avsluta", + MK_SECTION "Bokföring", + "Verifikat", "Underlag (inkorg)", "Ingående balans", + MK_SECTION "Rapporter & bokslut", + "Rapporter", "Bokslut", + MK_SECTION "Register", + "Mallar", "Företagsuppgifter", "Inställningar", + MK_SECTION "Övrigt", + "Revision", "Räkenskapsår", "Logga ut / avsluta", }; static void dashboard(struct app *a) @@ -5308,7 +5349,9 @@ static void dashboard(struct app *a) for (;;) { if (g_reload) return; - int sel = tui_menu("Bokf", MAIN_ITEMS, 12, 1, &last_sel); + int sel = tui_menu("Bokf", MAIN_ITEMS, + (int)(sizeof MAIN_ITEMS / sizeof MAIN_ITEMS[0]), 1, + &last_sel); snprintf(g_scene, sizeof g_scene, "%s", "dashboard"); if (g_quit || g_reload) return; @@ -5321,41 +5364,40 @@ static void dashboard(struct app *a) if (sel < 0) continue; /* Esc/back at the top level never exits */ switch (sel) { - case 0: - open_scene(a, "vouchers"); - break; case 1: - open_scene(a, "inbox"); + open_scene(a, "vouchers"); break; case 2: - open_scene(a, "ib"); + open_scene(a, "inbox"); break; case 3: - open_scene(a, "templates"); - break; - case 4: - open_scene(a, "reports"); + open_scene(a, "ib"); break; case 5: - open_scene(a, "audit"); + open_scene(a, "reports"); break; case 6: - open_scene(a, "settings"); - break; - case 7: - open_scene(a, "company"); + open_scene(a, "bokslut"); break; case 8: - open_scene(a, "bokslut"); + open_scene(a, "templates"); break; case 9: - open_scene(a, "yearinfo"); + open_scene(a, "company"); break; case 10: + open_scene(a, "settings"); + break; + case 12: + open_scene(a, "audit"); + break; + case 13: select_fiscal_year(a); break; - case 11: + case 14: return; + default: + break; /* section header */ } } } @@ -5634,8 +5676,8 @@ static void usage(FILE *f) " --fy ID select fiscal year directly\n" " --session ID reuse an open session (or BOKFD_SESSION)\n" " --screen NAME start in a view: dashboard, vouchers, inbox,\n" - " ib, templates, reports, bokslut, yearinfo,\n" - " audit, settings, company\n" + " ib, templates, reports, bokslut (yearinfo\n" + " is an alias), audit, settings, company\n" " --version\n"); } diff --git a/clients/tui.c b/clients/tui.c index de9b9f9..473cc0e 100644 --- a/clients/tui.c +++ b/clients/tui.c @@ -39,6 +39,108 @@ static int in_key(void) return g_in(); } +/* ------------------------------------------------------------------ */ +/* theme: semantic styles, colour pairs and attribute fallback */ +/* ------------------------------------------------------------------ */ + +enum { + TUI_CP_HEAD = 1, + TUI_CP_ACCENT, + TUI_CP_POS, + TUI_CP_NEG, + TUI_CP_RULE, + TUI_CP_STATUS, +}; + +static int g_colours; + +int tui_style_attrs(enum tui_style s, int colours_ok, int no_color) +{ + int cp = 0; + if (colours_ok && !no_color) { + switch (s) { + case TUI_TITLE: + case TUI_HEADING: + case TUI_SUBHEADING: + cp = COLOR_PAIR(TUI_CP_HEAD); + break; + case TUI_ACCENT: + cp = COLOR_PAIR(TUI_CP_ACCENT); + break; + case TUI_POS: + cp = COLOR_PAIR(TUI_CP_POS); + break; + case TUI_NEG: + cp = COLOR_PAIR(TUI_CP_NEG); + break; + case TUI_RULE: + cp = COLOR_PAIR(TUI_CP_RULE); + break; + case TUI_STATUS: + cp = COLOR_PAIR(TUI_CP_STATUS); + break; + case TUI_DIM: + default: + break; + } + } + int a; + switch (s) { + case TUI_TITLE: + case TUI_HEADING: + a = A_BOLD; + break; + case TUI_SUBHEADING: + a = A_BOLD; + break; + case TUI_DIM: + case TUI_STATUS: + a = A_DIM; + break; + case TUI_ACCENT: + a = A_BOLD; + break; + case TUI_RULE: + a = colours_ok && !no_color ? 0 : A_DIM; + break; + case TUI_POS: + case TUI_NEG: + default: + a = 0; + break; + } + return a | (int)cp; +} + +void tui_style(enum tui_style s) +{ + attrset(tui_style_attrs(s, g_colours, 0)); +} + +void tui_style_reset(void) +{ + attrset(A_NORMAL); +} + +const char *tui_markup(const char *line, int *style) +{ + const char *p = line ? line : ""; + int st = -1; + if (*p == TUI_MARK_HEADING) { + st = TUI_HEADING; + p++; + } else if (*p == TUI_MARK_DIM) { + st = TUI_DIM; + p++; + } else if (*p == TUI_MARK_RULE) { + st = TUI_RULE; + p++; + } + if (style) + *style = st; + return p; +} + void tui_keys_setup(void) { /* ^Enter has no control code; enable xterm modifyOtherKeys and bind the @@ -48,9 +150,24 @@ void tui_keys_setup(void) define_key("\033[1~", KEY_HOME); define_key("\033OH", KEY_HOME); define_key("\033[H", KEY_HOME); + define_key("\033[7~", KEY_HOME); define_key("\033[4~", KEY_END); define_key("\033OF", KEY_END); define_key("\033[F", KEY_END); + define_key("\033[8~", KEY_END); + /* Colour only when the terminal has it and NO_COLOR is not set; without + it every style falls back to attributes. */ + g_colours = has_colors() && !getenv("NO_COLOR"); + if (g_colours) { + start_color(); + use_default_colors(); + init_pair(TUI_CP_HEAD, COLOR_CYAN, -1); + init_pair(TUI_CP_ACCENT, COLOR_YELLOW, -1); + init_pair(TUI_CP_POS, COLOR_GREEN, -1); + init_pair(TUI_CP_NEG, COLOR_RED, -1); + init_pair(TUI_CP_RULE, COLOR_BLUE, -1); + init_pair(TUI_CP_STATUS, COLOR_CYAN, -1); + } putp("\033[>4;1m"); fflush(stdout); } @@ -456,9 +573,9 @@ static WINDOW *dlg_open(const char *title, int h, int w) h = LINES - 2; WINDOW *win = newwin(h, w, (LINES - h) / 2, (COLS - w) / 2); box(win, 0, 0); - wattron(win, A_BOLD); + wattrset(win, tui_style_attrs(TUI_TITLE, g_colours, 0)); mvwaddnstr(win, 0, 2, title, w - 4); - wattroff(win, A_BOLD); + wattrset(win, A_NORMAL); wrefresh(win); nodelay(stdscr, FALSE); timeout(-1); @@ -505,7 +622,9 @@ void tui_message(const char *title, const char *fmt, ...) p = nl ? nl + 1 : NULL; } free(copy); + wattrset(win, tui_style_attrs(TUI_DIM, g_colours, 0)); mvwaddstr(win, h - 1, w - 13, " tryck Enter"); + wattrset(win, A_NORMAL); wrefresh(win); for (;;) { int ch = wgetch(win); @@ -555,7 +674,9 @@ int tui_confirm(const char *title, const char *fmt, ...) p = nl ? nl + 1 : NULL; } free(copy); + wattrset(win, tui_style_attrs(TUI_DIM, g_colours, 0)); mvwaddstr(win, h - 1, w - 25, " j = ja n = nej"); + wattrset(win, A_NORMAL); wrefresh(win); int yes = 0; for (;;) { @@ -590,9 +711,9 @@ int tui_prompt_into(char *buf, size_t cap, const char *label, int y = LINES - 3; move(y, 2); clrtoeol(); - attron(A_BOLD); + tui_style(TUI_HEADING); mvaddstr(y, 2, label); - attroff(A_BOLD); + tui_style_reset(); refresh(); int prev = curs_set(1); int rc = tui_edit_field(y, (int)strlen(label) + 3, buf, cap, mask); @@ -609,9 +730,9 @@ int tui_date_prompt_into(char *buf, size_t cap, const char *label, int y = LINES - 3; move(y, 2); clrtoeol(); - attron(A_BOLD); + tui_style(TUI_HEADING); mvaddstr(y, 2, label); - attroff(A_BOLD); + tui_style_reset(); size_t pos = (size_t)-1; int fresh = 1; int prev = curs_set(1); @@ -664,9 +785,9 @@ int tui_choice_prompt(const char *label, const char *const *opts, int n, clrtoeol(); char line[512]; snprintf(line, sizeof line, "%s< %s >", label, opts[cur]); - attron(A_BOLD); + tui_style(TUI_HEADING); mvaddnstr(y, 2, line, COLS - 4); - attroff(A_BOLD); + tui_style_reset(); char hint[256]; snprintf(hint, sizeof hint, "vänster/höger = välj Enter = OK " "Esc = avbryt"); @@ -691,11 +812,53 @@ int tui_choice_prompt(const char *label, const char *const *opts, int n, /* list navigation core (pure, unit-tested) */ /* ------------------------------------------------------------------ */ +static int nav_selectable(const struct tui_list_nav *v, int i) +{ + return i >= 0 && i < v->n && (!v->selectable || v->selectable[i]); +} + +/* First selectable row at or after `from` moving in `dir`. */ +static int nav_snap(const struct tui_list_nav *v, int from, int dir) +{ + for (int i = from; i >= 0 && i < v->n; i += dir) + if (nav_selectable(v, i)) + return i; + for (int i = from; i >= 0 && i < v->n; i -= dir) + if (nav_selectable(v, i)) + return i; + return from; +} + +/* Row index of the ord-th (0-based) selectable row, or -1. */ +static int nav_row_of(const struct tui_list_nav *v, int ord) +{ + int c = 0; + for (int i = 0; i < v->n; i++) { + if (!nav_selectable(v, i)) + continue; + if (c == ord) + return i; + c++; + } + return -1; +} + +static int nav_count(const struct tui_list_nav *v) +{ + int c = 0; + for (int i = 0; i < v->n; i++) + if (nav_selectable(v, i)) + c++; + return c; +} + /* Keys return the same codes the screens have always used: the selected index, or -1 back, -2 refresh, -4 new, -6 remove, -7 toggle. */ int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new, int allow_remove, int allow_toggle, int menu_mode) { + if (v->n > 0 && !nav_selectable(v, v->sel)) + v->sel = nav_snap(v, v->sel, 1); if (v->n <= 0) { /* an empty list is never a dead end: refresh, add and back work */ if (ch == KEY_F(5)) @@ -711,17 +874,17 @@ int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new, 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; + int r = nav_row_of(v, atoi(v->gotobuf) - 1); + if (r >= 0) + v->sel = r; } else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) { size_t gl = strlen(v->gotobuf); if (gl) v->gotobuf[gl - 1] = '\0'; if (v->gotobuf[0]) { - int x = atoi(v->gotobuf); - if (x >= 1 && x <= v->n) - v->sel = x - 1; + int r = nav_row_of(v, atoi(v->gotobuf) - 1); + if (r >= 0) + v->sel = r; } } else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER || ch == 27 || ch == 'g' || ch == 'G') { @@ -730,22 +893,33 @@ int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new, } 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 */ + if (ch == KEY_UP) { + if (v->sel > 0) + v->sel = nav_snap(v, v->sel - 1, -1); + } else if (ch == KEY_DOWN) { + if (v->sel < v->n - 1) + v->sel = nav_snap(v, v->sel + 1, 1); + } else if (ch == KEY_NPAGE) { + int x = v->sel + v->view; + v->sel = nav_snap(v, x < v->n ? x : v->n - 1, 1); + } else if (ch == KEY_PPAGE) { + int x = v->sel - v->view; + v->sel = nav_snap(v, x > 0 ? x : 0, -1); + } else if (ch == KEY_HOME) { + int r = nav_row_of(v, 0); + if (r >= 0) + v->sel = r; + } else if (ch == KEY_END) { + int r = nav_row_of(v, nav_count(v) - 1); + if (r >= 0) + v->sel = r; + } else if (ch >= '1' && ch <= '9') { + int r = nav_row_of(v, ch - '1'); + if (r >= 0) { + v->sel = r; + if (menu_mode) + return v->sel; /* menus activate directly */ + } } else if (ch == 'g' || ch == 'G') { v->goto_active = 1; v->gotobuf[0] = '\0'; @@ -795,9 +969,9 @@ static void list_draw(const char *title, char **items, int n, } if (v->goto_active) { move(LINES - 2, 2); - attron(A_BOLD); + tui_style(TUI_ACCENT); printw("%s%s", v->gotolabel, v->gotobuf); - attroff(A_BOLD); + tui_style_reset(); clrtoeol(); } else { move(LINES - 2, 2); @@ -883,7 +1057,27 @@ int tui_menu(const char *title, const char *const *items, int n, struct tui_list_nav v; memset(&v, 0, sizeof v); v.n = n; + unsigned char *flags = xcalloc(n > 0 ? (size_t)n : 1, 1); + int nsel = 0; + for (int i = 0; i < n; i++) { + if (items[i][0] == TUI_MARK_HEADING) + continue; + flags[i] = 1; + nsel++; + } + v.selectable = flags; + if (nsel == 0) { + free(flags); + return -1; + } v.sel = (cursor && *cursor >= 0 && *cursor < n) ? *cursor : 0; + if (!flags[v.sel]) + v.sel = nav_snap(&v, v.sel, 1); + v.numw = 1; + for (int tmp = nsel; tmp >= 10; tmp /= 10) + v.numw++; + if (v.numw < 2) + v.numw = 2; v.view = (LINES - 4) / 2; snprintf(v.gotolabel, sizeof v.gotolabel, "Gå till nummer: "); for (;;) { @@ -892,21 +1086,33 @@ int tui_menu(const char *title, const char *const *items, int n, 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]); + int num = 0; + for (int i = 0; i < v.top && i < n; i++) + if (v.selectable[i]) + num++; + for (int idx = v.top; idx < n && idx < v.top + v.view; idx++) { + char line[1024]; + if (!v.selectable[idx]) { + snprintf(line, sizeof line, "%*s %s", v.numw, "", + items[idx] + 1); + tui_style(TUI_DIM); + mvaddnstr(3 + (idx - v.top) * 2, 4, line, COLS - 6); + tui_style_reset(); + continue; + } + num++; + snprintf(line, sizeof line, "%*d. %s", v.numw, num, items[idx]); if (idx == v.sel) attron(A_REVERSE); - mvaddnstr(3 + i * 2, 4, line, COLS - 6); + mvaddnstr(3 + (idx - v.top) * 2, 4, line, COLS - 6); if (idx == v.sel) attroff(A_REVERSE); } if (v.goto_active) { move(LINES - 2, 2); - attron(A_BOLD); + tui_style(TUI_ACCENT); printw("%s%s", v.gotolabel, v.gotobuf); - attroff(A_BOLD); + tui_style_reset(); clrtoeol(); } else { move(LINES - 2, 2); @@ -924,6 +1130,7 @@ int tui_menu(const char *title, const char *const *items, int n, if (r != -3) { if (cursor) *cursor = v.sel; + free(flags); return r; } } @@ -945,20 +1152,20 @@ void tui_set_status(const char *status, const char *right) void tui_frame(const char *title) { erase(); - attron(A_BOLD); + tui_style(TUI_TITLE); box(stdscr, 0, 0); mvaddstr(0, 2, title); - attroff(A_BOLD); + tui_style_reset(); if (!g_status[0]) return; - attron(A_DIM); + tui_style(TUI_STATUS); mvaddnstr(1, 2, g_status, COLS - 4); if (g_right[0]) { int x = COLS - 3 - tui_disp_width(g_right); if (x > tui_disp_width(g_status) + 6) mvaddstr(1, x, g_right); } - attroff(A_DIM); + tui_style_reset(); } /* Flush a frame drawn by the application outside a widget loop. */ @@ -971,9 +1178,9 @@ void tui_hints(const char *s) { char line[512]; snprintf(line, sizeof line, "%s ^R = ladda om", s); - attron(A_DIM); + tui_style(TUI_STATUS); mvaddstr(LINES - 1, 2, line); - attroff(A_DIM); + tui_style_reset(); clrtoeol(); } @@ -1016,8 +1223,22 @@ int tui_pager_hook(const char *title, const char *text, 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); + if (top + i >= n) + continue; + int st = -1; + const char *s = tui_markup(lines[top + i], &st); + if (st == TUI_RULE) { + attrset(tui_style_attrs(TUI_RULE, g_colours, 0)); + for (int x = 2; x < COLS - 1; x++) + mvaddch(2 + i, x, ACS_HLINE); + attrset(A_NORMAL); + } else if (st >= 0) { + attrset(tui_style_attrs((enum tui_style)st, g_colours, 0)); + mvaddnstr(2 + i, 2, s, COLS - 4); + attrset(A_NORMAL); + } else { + mvaddnstr(2 + i, 2, s, COLS - 4); + } } char hint[512]; if (extra_hint && *extra_hint) @@ -1125,10 +1346,10 @@ int tui_form_run_hook(const char *title, struct tui_form_field *f, int n, if (sel < 0) sel = 0; tui_frame(title); - attron(A_BOLD); + tui_style(TUI_HEADING); mvaddstr(3, 2, "Uppgift"); mvaddstr(3, 34, "Värde"); - attroff(A_BOLD); + tui_style_reset(); for (int i = 0; i < n; i++) { char lbl[160], val[640], line[832]; snprintf(lbl, sizeof lbl, "%s", f[i].label); @@ -1157,12 +1378,12 @@ int tui_form_run_hook(const char *title, struct tui_form_field *f, int n, if (i == sel) attron(A_REVERSE); else if (!can_edit) - attron(A_DIM); + attron(tui_style_attrs(TUI_DIM, g_colours, 0)); mvaddnstr(5 + i, 2, line, COLS - 4); if (i == sel) attroff(A_REVERSE); else if (!can_edit) - attroff(A_DIM); + attroff(tui_style_attrs(TUI_DIM, g_colours, 0)); } tui_hints(can_edit ? "upp/ned Enter = ändra F5 = uppdatera" @@ -1454,10 +1675,10 @@ static void rt_draw_fields(const struct tui_rt *t) { if (t->nfields <= 0) return; - attron(A_BOLD); + tui_style(TUI_HEADING); mvaddstr(3, 2, "Uppgift"); mvaddstr(3, 34, "Värde"); - attroff(A_BOLD); + tui_style_reset(); for (int i = 0; i < t->nfields; i++) { char lbl[160], val[640], line[832]; snprintf(lbl, sizeof lbl, "%s", t->fields[i].label); @@ -1532,14 +1753,14 @@ int tui_rt_run(const char *title, struct tui_rt *t, const char *hint) rt_draw_fields(t); - attron(A_BOLD); + tui_style(TUI_HEADING); for (int i = 0; i < t->ncols; i++) { char hdr[64]; snprintf(hdr, sizeof hdr, "%s", t->cols[i].header); tui_pad_field(hdr, sizeof hdr, t->cols[i].width); mvaddstr(y - 1, t->cols[i].x, hdr); } - attroff(A_BOLD); + tui_style_reset(); int caret_y = -1, caret_x = -1; if (t->focus >= 0 && t->focus < t->nfields) { char val[640]; @@ -1592,9 +1813,9 @@ int tui_rt_run(const char *title, struct tui_rt *t, const char *hint) continue; t->info(t->ud, top + v, ibuf, sizeof ibuf); tui_pad_field(ibuf, sizeof ibuf, t->cols[i].width); - attron(A_DIM); + attron(tui_style_attrs(TUI_DIM, g_colours, 0)); mvaddstr(y + v, t->cols[i].x, ibuf); - attroff(A_DIM); + attroff(tui_style_attrs(TUI_DIM, g_colours, 0)); } } } @@ -1607,9 +1828,9 @@ int tui_rt_run(const char *title, struct tui_rt *t, const char *hint) move(fy, 2); clrtoeol(); if (foot[0]) { - attron(A_DIM); + tui_style(TUI_STATUS); addnstr(foot, COLS - 4); - attroff(A_DIM); + tui_style_reset(); } if (g_hints) g_hints(hint); diff --git a/clients/tui.h b/clients/tui.h index 061edbf..ecaa141 100644 --- a/clients/tui.h +++ b/clients/tui.h @@ -15,6 +15,35 @@ #define TUI_KEY_CTRL_X 0x18 #define TUI_KEY_CTRL_ENTER (KEY_MAX + 1) +/* Semantic styles: the widget layer maps them to colour pairs when the + terminal supports colour and NO_COLOR is unset, otherwise to plain + attributes. "Ordinary" drawing uses the terminal default. */ +enum tui_style { + TUI_TITLE = 0, + TUI_HEADING, + TUI_SUBHEADING, + TUI_DIM, + TUI_ACCENT, + TUI_POS, + TUI_NEG, + TUI_RULE, + TUI_STATUS, +}; + +/* Attribute bits for a style; pure so the colour/no-colour fallback is + unit-tested. colours_ok is the terminal capability, no_color the env. */ +int tui_style_attrs(enum tui_style s, int colours_ok, int no_color); +void tui_style(enum tui_style s); +void tui_style_reset(void); + +/* Pager line markup. A leading marker selects a style; \x03 is a rule and + is drawn full width as ACS_HLINE. tui_markup returns the text after the + marker and sets *style to the matching style, or -1 for plain text. */ +#define TUI_MARK_HEADING 0x01 +#define TUI_MARK_DIM 0x02 +#define TUI_MARK_RULE 0x03 +const char *tui_markup(const char *line, int *style); + /* Key hooks: widgets call the screen's hook for keys they do not handle. */ #define TUI_HOOK_NONE 0 /* not handled: process the key normally */ #define TUI_HOOK_STAY 1 /* handled: redraw and keep the widget up */ @@ -89,6 +118,9 @@ struct tui_list_nav { char gotobuf[12]; char gotolabel[32]; int goto_active; + /* Optional row flags: 0 marks a non-selectable row (a menu section + header). Navigation, numbering and goto skip them. */ + const unsigned char *selectable; }; /* Returns the selected index, or -1 back, -2 refresh, -4 new, -6 remove, @@ -103,6 +135,9 @@ int tui_select_list_hook(const char *title, char **items, int n, int start, int allow_refresh, int *cursor, int allow_new, const char *archive_action, int allow_toggle, int (*key)(void *ud, int ch), void *ud); +/* Menu items whose text starts with TUI_MARK_HEADING are non-selectable + section headers: drawn dim, not numbered and never highlighted. Numbering + and 1-9/g continue over the selectable items only. */ int tui_menu(const char *title, const char *const *items, int n, int allow_new, int *cursor); -- cgit v1.3