aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-19 23:05:44 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-19 23:05:44 +0200
commit8da6a7e849513e1cb63410d95b121df729260d0c (patch)
tree77de7f5b3952d153e072cda8cc7c4f4bcd5f0c87
parent730d7159e3b9b3adfba2c3779893fdb4e107647a (diff)
downloadbokf-0.1.40.tar.gz
bokf-0.1.40.zip
tui: themes, pager markup, menu sections; merge Bokslut and year infov0.1.40
-rw-r--r--clients/bokftui.c442
-rw-r--r--clients/tui.c339
-rw-r--r--clients/tui.h35
-rw-r--r--docs/STATE.md16
-rw-r--r--docs/TUI-GUIDELINES.md55
-rw-r--r--tests/test_tui.c85
6 files changed, 705 insertions, 267 deletions
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);
diff --git a/docs/STATE.md b/docs/STATE.md
index 32b2a4e..5112455 100644
--- a/docs/STATE.md
+++ b/docs/STATE.md
@@ -58,7 +58,11 @@ server/protocol/ledger only.
size from `meta` first. Setting `attachment_dir` (tilde expanded).
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.
+ 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).
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.
@@ -99,10 +103,12 @@ server/protocol/ledger only.
verksamhetsbeskrivning, flerårsöversikt and changes-in-equity from the
books, resultatdisposition, styrelsens yttrande, fastställelseintyg,
board signatures) and needs no prompting: everything comes from the
- books, the org record (Företagsuppgifter) and the per-year
- **Information om året** (`fiscal_year.update`, schema v6: events, AGM
- and payment dates, proposed dividend, employees, notes — inherited from
- the previous year when a year is opened). Board members are a list per
+ books, the org record (Företagsuppgifter) and the per-year year booklet
+ now edited on the **Bokslut** screen (`fiscal_year.update`, schema v6:
+ events, AGM and payment dates, proposed dividend, employees, notes —
+ inherited from the previous year when a year is opened; fields 0–5 save
+ per field, the periodiseringsfond/tax rate fields are local). Board
+ members are a list per
org (`board.*`, edited under Företagsuppgifter) and the stämma decision
is posted with a `Utdelning` template (D 2099/K 2898). Imported history
years are read with their "Stäng" closings skipped, so comparisons and
diff --git a/docs/TUI-GUIDELINES.md b/docs/TUI-GUIDELINES.md
index e02fade..631a516 100644
--- a/docs/TUI-GUIDELINES.md
+++ b/docs/TUI-GUIDELINES.md
@@ -51,6 +51,12 @@ written compactly as `^N`, `^A`, `^C`, `^R` to save width.
- Digits move the highlight; only Enter activates. Menus are the exception:
digits activate directly (they are shortcuts).
- Empty lists are never a dead end: show a message or keep the add-row.
+- Menus may be split into sections: an item whose text starts with
+ `\x01` (`TUI_MARK_HEADING`) is a non-selectable header, drawn dim and
+ without a number. Numbering, `1`–`9` and `g` count only the selectable
+ items and continue across sections; arrows/PgUp/PgDn/Home/End skip the
+ headers. A cursor that points at a header snaps to the next selectable
+ item.
## Forms
@@ -122,8 +128,48 @@ written compactly as `^N`, `^A`, `^C`, `^R` to save width.
- Columns are padded with `pad_field()` (UTF-8 display width) and truncated to
their column; amounts right-aligned via `kr_format()`; never pad with `%s`
widths directly on user text.
-- Selection = reverse video, headers = bold, derived data = dim.
-- All user-visible TUI text is Swedish; server messages are English.
+- Selection = reverse video, headers = bold, derived data = dim. Drawing uses
+ the semantic styles below, never raw `A_BOLD`/`A_DIM`.
+
+## Theme and styles
+
+`clients/tui.c` owns the theme. The widget layer (and only the widget layer)
+calls `tui_style(enum tui_style)` to set a semantic style and
+`tui_style_reset()` to return to the terminal default:
+
+| Style | Meaning |
+|---|---|
+| `TUI_TITLE` | screen title in the frame |
+| `TUI_HEADING` | headings, column headers, form labels |
+| `TUI_SUBHEADING` | secondary headings |
+| `TUI_DIM` | derived/secondary data, section headers |
+| `TUI_ACCENT` | interactive prompts (goto) |
+| `TUI_POS` / `TUI_NEG` | positive/negative amounts |
+| `TUI_RULE` | full-width rules |
+| `TUI_STATUS` | status line, hints, footers |
+
+Colours are initialised in `tui_keys_setup()`: a small set of pairs (cyan
+headings, yellow accent, green/red amounts, blue rules) on the default
+background. If the terminal has no colour support or `NO_COLOR` is set in the
+environment, every style falls back to attributes (bold/dim) via the pure
+`tui_style_attrs(style, colours_ok, no_color)`; reverse video for the
+selection is kept in both modes.
+
+## Pager markup
+
+Pager text may carry a one-byte leading marker per line (set by the report
+formatters, interpreted by `tui_markup`):
+
+| Marker | Effect |
+|---|---|
+| `\x01` | line is a heading (`TUI_HEADING`) |
+| `\x02` | line is dimmed (`TUI_DIM`) |
+| `\x03` | full-width `ACS_HLINE` rule (`TUI_RULE`); the rest of the line is ignored |
+
+Report formatters mark headings, sums/derived lines and rules before the
+text reaches `tui_pager`; the markers never change column widths. Anything
+that saves pager text to a file strips the markers first
+(`strip_markup()` in `bokftui.c`).
## Widget layer (`clients/tui.[ch]`)
@@ -143,7 +189,10 @@ only place that touches ncurses. Rules:
`TUI_HOOK_*` codes are all distinct.
- `tui_pager_hook` takes `TUI_PAGER_SAVE` to enable the `s` save action; the
`extra_hint` is shown in the footer. Plain `tui_pager` keeps `s` on when a
- save hint is passed.
+ save hint is passed. Pager lines go through `tui_markup` (see below).
+- Styles come from `tui_style`/`tui_style_attrs`; report markup and menu
+ sections from `tui_markup`/`TUI_MARK_HEADING`. Their fallbacks and the
+ navigation over section headers are unit-tested in `tests/test_tui.c`.
- `tui_redraw` flushes a `tui_frame`/`tui_set_status` pair drawn by the
application outside a widget loop (startup/reconnect messages).
- The application supplies input, frame/hints and quit through
diff --git a/tests/test_tui.c b/tests/test_tui.c
index 9c47c43..f4405b4 100644
--- a/tests/test_tui.c
+++ b/tests/test_tui.c
@@ -199,6 +199,88 @@ static void test_nav(void)
CHECK(v.top == 90);
}
+static void test_styles(void)
+{
+ /* without colour every style falls back to attributes */
+ CHECK(tui_style_attrs(TUI_TITLE, 0, 0) == (int)A_BOLD);
+ CHECK(tui_style_attrs(TUI_HEADING, 0, 0) == (int)A_BOLD);
+ CHECK(tui_style_attrs(TUI_SUBHEADING, 0, 0) == (int)A_BOLD);
+ CHECK(tui_style_attrs(TUI_DIM, 0, 0) == (int)A_DIM);
+ CHECK(tui_style_attrs(TUI_STATUS, 0, 0) == (int)A_DIM);
+ CHECK(tui_style_attrs(TUI_RULE, 0, 0) == (int)A_DIM);
+ CHECK(tui_style_attrs(TUI_POS, 0, 0) == 0);
+ CHECK(tui_style_attrs(TUI_NEG, 0, 0) == 0);
+ /* NO_COLOR forces the same fallback even when colours are available */
+ CHECK(tui_style_attrs(TUI_POS, 1, 1) == tui_style_attrs(TUI_POS, 0, 0));
+ CHECK(tui_style_attrs(TUI_RULE, 1, 1) == (int)A_DIM);
+ CHECK(tui_style_attrs(TUI_TITLE, 1, 1) == (int)A_BOLD);
+ /* with colour the amount styles get distinct colour pairs */
+ int pos = tui_style_attrs(TUI_POS, 1, 0);
+ int neg = tui_style_attrs(TUI_NEG, 1, 0);
+ CHECK((pos & (int)A_COLOR) != 0);
+ CHECK((neg & (int)A_COLOR) != 0);
+ CHECK((pos & (int)A_COLOR) != (neg & (int)A_COLOR));
+ CHECK((tui_style_attrs(TUI_HEADING, 1, 0) & (int)A_BOLD) != 0);
+ CHECK((tui_style_attrs(TUI_STATUS, 1, 0) & (int)A_DIM) != 0);
+}
+
+static void test_markup(void)
+{
+ int st = 123;
+ const char *s = tui_markup("vanlig text", &st);
+ CHECK(strcmp(s, "vanlig text") == 0 && st == -1);
+ s = tui_markup("\001Rubrik", &st);
+ CHECK(strcmp(s, "Rubrik") == 0 && st == TUI_HEADING);
+ s = tui_markup("\002dimmad rad", &st);
+ CHECK(strcmp(s, "dimmad rad") == 0 && st == TUI_DIM);
+ s = tui_markup("\003----------------", &st);
+ CHECK(strcmp(s, "----------------") == 0 && st == TUI_RULE);
+ s = tui_markup("", &st);
+ CHECK(s && *s == '\0' && st == -1);
+ s = tui_markup(NULL, &st);
+ CHECK(s && *s == '\0' && st == -1);
+ /* only a leading marker counts; the marker may be NULL-checked away */
+ s = tui_markup("a\001b", &st);
+ CHECK(strcmp(s, "a\001b") == 0 && st == -1);
+ s = tui_markup("\004text", &st);
+ CHECK(strcmp(s, "\004text") == 0 && st == -1);
+ s = tui_markup("\001x", NULL);
+ CHECK(strcmp(s, "x") == 0);
+}
+
+static const unsigned char SECTION_FLAGS[5] = { 0, 1, 1, 0, 1 };
+
+static void test_nav_sections(void)
+{
+ struct tui_list_nav v;
+ nav_init(&v, 5);
+ v.selectable = SECTION_FLAGS;
+ v.sel = 0; /* a header: snaps to the first selectable row */
+ CHECK(tui_nav_key(&v, KEY_DOWN, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 2);
+ CHECK(tui_nav_key(&v, KEY_DOWN, 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_UP, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 2);
+ CHECK(tui_nav_key(&v, KEY_HOME, 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);
+ /* digits and goto count selectable rows only */
+ CHECK(tui_nav_key(&v, '1', 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 1);
+ CHECK(tui_nav_key(&v, '2', 0, 0, 0, 1) == 2 && v.sel == 2);
+ CHECK(tui_nav_key(&v, '3', 0, 0, 0, 1) == 4 && v.sel == 4);
+ CHECK(tui_nav_key(&v, '9', 0, 0, 0, 1) == TUI_NAV_NONE && v.sel == 4);
+ v.sel = 1;
+ CHECK(tui_nav_key(&v, 'g', 0, 0, 0, 0) == TUI_NAV_NONE && v.goto_active);
+ CHECK(tui_nav_key(&v, '2', 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 2);
+ CHECK(tui_nav_key(&v, KEY_BACKSPACE, 0, 0, 0, 0) == TUI_NAV_NONE &&
+ v.sel == 2);
+ CHECK(tui_nav_key(&v, '\n', 0, 0, 0, 0) == TUI_NAV_NONE && !v.goto_active);
+ /* paging lands on selectable rows */
+ nav_init(&v, 5);
+ v.selectable = SECTION_FLAGS;
+ v.view = 2;
+ 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 == 2);
+}
+
static void test_form_nav(void)
{
CHECK(TUI_NAV_NONE != TUI_FORM_SUBMIT);
@@ -398,7 +480,10 @@ int main(void)
test_parse_kr();
test_ledit();
test_date_field();
+ test_styles();
+ test_markup();
test_nav();
+ test_nav_sections();
test_form_nav();
test_rowtable();
test_rt_fields();