aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-19 22:33:33 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-19 22:33:33 +0200
commite352cd34d30acc1955c62027c779ac98004ac201 (patch)
tree8a94163b837fdc16e334c159d91a9f622d42763d
parent84c4fdcf138ad6ef4f7edfea76f2318b7a7a5e3e (diff)
downloadbokf-0.1.38.tar.gz
bokf-0.1.38.zip
tui: migrate the remaining screens to the widget layerv0.1.38
-rw-r--r--clients/bokftui.c2744
-rw-r--r--clients/tui.c212
-rw-r--r--clients/tui.h31
-rw-r--r--docs/TUI-GUIDELINES.md23
-rw-r--r--tests/test_tui.c44
5 files changed, 1433 insertions, 1621 deletions
diff --git a/clients/bokftui.c b/clients/bokftui.c
index 0967f51..af6e284 100644
--- a/clients/bokftui.c
+++ b/clients/bokftui.c
@@ -806,6 +806,160 @@ done:
free(resp);
}
+struct vdctx {
+ struct app *a;
+ int64_t id;
+ size_t natts;
+ const char *resp;
+ int corrected;
+ int want_refresh;
+ int posted_new;
+ int64_t posted_id;
+};
+
+static int vd_key(void *ud, int ch)
+{
+ struct vdctx *ctx = ud;
+ struct app *a = ctx->a;
+ if (ch == TUI_KEY_CTRL_N) {
+ int64_t nid = vouchers_new(a);
+ if (nid > 0) {
+ ctx->posted_id = nid;
+ ctx->posted_new = 1;
+ return TUI_HOOK_BACK;
+ }
+ return TUI_HOOK_STAY;
+ }
+ if (ch == 'c') {
+ char reasonbuf[512];
+ char *reason =
+ tui_prompt_into(reasonbuf, sizeof reasonbuf, "Beskriv rättelsen: ",
+ "", 0)
+ ? reasonbuf
+ : NULL;
+ if (reason && *reason) {
+ yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val *o = yyjson_mut_obj(d);
+ yyjson_mut_doc_set_root(d, o);
+ yyjson_mut_obj_add_int(d, o, "voucher", ctx->id);
+ yyjson_mut_obj_add_strcpy(d, o, "description", reason);
+ char *ref = util_random_id("tui-", 8);
+ yyjson_mut_obj_add_strcpy(d, o, "client_ref", ref);
+ char *cargs = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ char *r = client_rpc(&a->conn, "voucher.correct", a->session,
+ a->org, cargs);
+ if (r && client_ok(r)) {
+ int64_t new_id = jint_val(r, "result.id", 0);
+ tui_message("Rättat", "Ändringsverifikat %lld skapat",
+ (long long)new_id);
+ ctx->corrected = 1;
+ } else {
+ show_error("Kunde inte rätta", r);
+ }
+ free(r);
+ free(cargs);
+ free(ref);
+ }
+ return TUI_HOOK_BACK;
+ }
+ if (ch == 6) { /* ^F: attach a file to this voucher */
+ char *path = file_browser(a, a->attachment_dir);
+ if (path) {
+ struct stat sb;
+ if (stat(path, &sb) == 0 &&
+ (long)sb.st_size > a->max_attachment_bytes) {
+ tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.",
+ (double)sb.st_size / (1024 * 1024),
+ a->max_attachment_bytes / (1024 * 1024));
+ free(path);
+ } else {
+ char *b64 = read_file_b64(path);
+ if (!b64) {
+ tui_message("Fel", "Kunde inte läsa %s", path);
+ free(path);
+ } else {
+ const char *base = strrchr(path, '/');
+ base = base ? base + 1 : path;
+ yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val *o = yyjson_mut_obj(d);
+ yyjson_mut_doc_set_root(d, o);
+ yyjson_mut_obj_add_strcpy(d, o, "filename", base);
+ yyjson_mut_obj_add_strcpy(d, o, "content_base64", b64);
+ yyjson_mut_obj_add_int(d, o, "voucher_id", ctx->id);
+ char *fargs = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ free(b64);
+ free(path);
+ char *r = client_rpc(&a->conn, "attachment.put", a->session,
+ a->org, fargs);
+ if (r && client_ok(r))
+ tui_message("Underlag", "Bifogat.");
+ else
+ show_error("Kunde inte bifoga", r);
+ free(r);
+ free(fargs);
+ ctx->want_refresh = 1;
+ return TUI_HOOK_BACK;
+ }
+ }
+ }
+ return TUI_HOOK_STAY;
+ }
+ if (ch == 'f' && ctx->natts > 0) {
+ char **alines = xcalloc(ctx->natts, sizeof(char *));
+ int64_t *aids = xcalloc(ctx->natts, sizeof(int64_t));
+ for (size_t i = 0; i < ctx->natts; i++) {
+ char path[64];
+ snprintf(path, sizeof path, "result.attachments.%zu.id", i);
+ aids[i] = jint_val(ctx->resp, path, 0);
+ snprintf(path, sizeof path, "result.attachments.%zu.filename", i);
+ char *fn = jstr_dup(ctx->resp, path);
+ alines[i] = xstrdup(fn ? fn : "(namnlös)");
+ free(fn);
+ }
+ int acursor = 0;
+ int asel = tui_select_list("Underlag", alines, (int)ctx->natts, 0, 0,
+ &acursor, 0, "ta bort", 0);
+ if (asel >= 0) {
+ attachment_download(a, aids[asel]);
+ } else if (asel == -6 && acursor >= 0 &&
+ (size_t)acursor < ctx->natts) {
+ char q[320];
+ snprintf(q, sizeof q, "Ta bort '%s' från verifikatet? (j/n): ",
+ alines[acursor]);
+ char ansbuf[512];
+ char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, q, "n", 0)
+ ? ansbuf
+ : NULL;
+ if (ans && (ans[0] == 'j' || ans[0] == 'J')) {
+ char uargs[64];
+ snprintf(uargs, sizeof uargs, "{\"id\":%lld,\"voucher_id\":%lld}",
+ (long long)aids[acursor], (long long)ctx->id);
+ char *r = client_rpc(&a->conn, "attachment.unlink", a->session,
+ a->org, uargs);
+ if (r && client_ok(r)) {
+ tui_message("Underlag", "Länken borttagen.");
+ ctx->want_refresh = 1;
+ for (size_t i = 0; i < ctx->natts; i++)
+ free(alines[i]);
+ free(alines);
+ free(aids);
+ return TUI_HOOK_REFRESH;
+ }
+ show_error("Kunde inte ta bort", r);
+ free(r);
+ }
+ }
+ for (size_t i = 0; i < ctx->natts; i++)
+ free(alines[i]);
+ free(alines);
+ free(aids);
+ return TUI_HOOK_STAY;
+ }
+ return TUI_HOOK_NONE;
+}
+
static int voucher_detail(struct app *a, int64_t id, int64_t *out_new)
{
if (out_new)
@@ -882,181 +1036,26 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new)
}
buf_append(&text, "\0", 1);
- int nlines = 0;
- for (size_t i = 0; i < text.len && text.p[i]; i++)
- if (text.p[i] == '\n')
- nlines++;
- char **lines = xcalloc(nlines + 2, sizeof(char *));
- int n = 0;
- char *copy = xstrdup((char *)text.p);
- for (char *p = copy;;) {
- char *nl = strchr(p, '\n');
- if (nl)
- *nl = '\0';
- lines[n++] = p;
- if (!nl)
- break;
- p = nl + 1;
- }
- int top = 0, view = LINES - 4;
- int want_refresh = 0, corrected = 0, stop = 0, posted_new = 0;
- int64_t posted_id = 0;
- while (!stop) {
- char hint[256];
- snprintf(hint, sizeof hint,
- "upp/ned F5 = uppdatera ^N = nytt verifikat c ="
- " rätta ^F = bifoga%s Esc/q = tillbaka",
- natts ? " f = underlag" : "");
- tui_frame("Verifikat");
- for (int i = 0; i < view && top + i < n; i++)
- mvaddnstr(2 + i, 2, lines[top + i], COLS - 4);
- tui_hints(hint);
- refresh();
- int ch = ui_getch();
- if (ch == 'q' || ch == 27) {
- stop = 1;
- } else if (ch == KEY_F(5)) {
- want_refresh = 1;
- stop = 1;
- } else if (ch == KEY_CTRL_N) {
- int64_t nid = vouchers_new(a);
- if (nid > 0) {
- posted_id = nid;
- posted_new = 1;
- stop = 1;
- }
- } else if (ch == 'c') {
- char reasonbuf[512]; char *reason = tui_prompt_into(reasonbuf, sizeof reasonbuf, "Beskriv rättelsen: ", "", 0) ? reasonbuf : NULL;
- if (reason && *reason) {
- yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
- yyjson_mut_val *o = yyjson_mut_obj(d);
- yyjson_mut_doc_set_root(d, o);
- yyjson_mut_obj_add_int(d, o, "voucher", id);
- yyjson_mut_obj_add_strcpy(d, o, "description", reason);
- char *ref = util_random_id("tui-", 8);
- yyjson_mut_obj_add_strcpy(d, o, "client_ref", ref);
- char *cargs = yyjson_mut_write(d, 0, NULL);
- yyjson_mut_doc_free(d);
- char *r = client_rpc(&a->conn, "voucher.correct", a->session,
- a->org, cargs);
- if (r && client_ok(r)) {
- int64_t new_id = jint_val(r, "result.id", 0);
- tui_message("Rättat", "Ändringsverifikat %lld skapat",
- (long long)new_id);
- corrected = 1;
- } else {
- show_error("Kunde inte rätta", r);
- }
- free(r);
- free(cargs);
- free(ref);
- }
- stop = 1;
- } else if (ch == 6) { /* ^F: attach a file to this voucher */
- char *path = file_browser(a, a->attachment_dir);
- if (path) {
- struct stat sb;
- if (stat(path, &sb) == 0 &&
- (long)sb.st_size > a->max_attachment_bytes) {
- tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.",
- (double)sb.st_size / (1024 * 1024),
- a->max_attachment_bytes / (1024 * 1024));
- free(path);
- } else {
- char *b64 = read_file_b64(path);
- if (!b64) {
- tui_message("Fel", "Kunde inte läsa %s", path);
- free(path);
- } else {
- const char *base = strrchr(path, '/');
- base = base ? base + 1 : path;
- yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
- yyjson_mut_val *o = yyjson_mut_obj(d);
- yyjson_mut_doc_set_root(d, o);
- yyjson_mut_obj_add_strcpy(d, o, "filename", base);
- yyjson_mut_obj_add_strcpy(d, o, "content_base64",
- b64);
- yyjson_mut_obj_add_int(d, o, "voucher_id", id);
- char *fargs = yyjson_mut_write(d, 0, NULL);
- yyjson_mut_doc_free(d);
- free(b64);
- free(path);
- char *r = client_rpc(&a->conn, "attachment.put",
- a->session, a->org, fargs);
- if (r && client_ok(r))
- tui_message("Underlag", "Bifogat.");
- else
- show_error("Kunde inte bifoga", r);
- free(r);
- free(fargs);
- want_refresh = 1;
- stop = 1;
- }
- }
- }
- } else if (ch == 'f' && natts > 0) {
- char **alines = xcalloc(natts, sizeof(char *));
- int64_t *aids = xcalloc(natts, sizeof(int64_t));
- for (size_t i = 0; i < natts; i++) {
- char path[64];
- snprintf(path, sizeof path, "result.attachments.%zu.id",
- i);
- aids[i] = jint_val(resp, path, 0);
- snprintf(path, sizeof path,
- "result.attachments.%zu.filename", i);
- char *fn = jstr_dup(resp, path);
- alines[i] = xstrdup(fn ? fn : "(namnlös)");
- free(fn);
- }
- int acursor = 0;
- int asel = tui_select_list("Underlag", alines, (int)natts, 0, 0,
- &acursor, 0, "ta bort", 0);
- if (asel >= 0)
- attachment_download(a, aids[asel]);
- else if (asel == -6 && acursor >= 0 &&
- (size_t)acursor < natts) {
- char q[320];
- snprintf(q, sizeof q,
- "Ta bort '%s' från verifikatet? (j/n): ",
- alines[acursor]);
- char ansbuf[512]; char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, q, "n", 0) ? ansbuf : NULL;
- if (ans && (ans[0] == 'j' || ans[0] == 'J')) {
- char uargs[64];
- snprintf(uargs, sizeof uargs,
- "{\"id\":%lld,\"voucher_id\":%lld}",
- (long long)aids[acursor], (long long)id);
- char *r = client_rpc(&a->conn, "attachment.unlink",
- a->session, a->org, uargs);
- if (r && client_ok(r)) {
- tui_message("Underlag", "Länken borttagen.");
- want_refresh = 1;
- stop = 1;
- } else {
- show_error("Kunde inte ta bort", r);
- }
- free(r);
- }
- }
- for (size_t i = 0; i < natts; i++)
- free(alines[i]);
- free(alines);
- free(aids);
- } else if (ch == KEY_DOWN && top + view < n) {
- top++;
- } else if (ch == KEY_UP && top > 0) {
- top--;
- } else if (ch == KEY_NPAGE) {
- top += view;
- } else if (ch == KEY_PPAGE) {
- top -= view;
- }
- if (top + view > n)
- top = n > view ? n - view : 0;
- if (top < 0)
- top = 0;
- }
- free(copy);
- free(lines);
+ struct vdctx ctx;
+ memset(&ctx, 0, sizeof ctx);
+ ctx.a = a;
+ ctx.id = id;
+ ctx.natts = natts;
+ ctx.resp = resp;
+ char hint[256];
+ snprintf(hint, sizeof hint,
+ "^N = nytt verifikat c = rätta ^F = bifoga%s",
+ natts ? " f = underlag" : "");
+ int want_refresh = 0, corrected = 0;
+ int ret = tui_pager_hook("Verifikat", (const char *)text.p, hint, 0,
+ vd_key, &ctx);
+ if (ret == 1)
+ want_refresh = 1;
+ if (ctx.want_refresh)
+ want_refresh = 1;
+ corrected = ctx.corrected;
+ int posted_new = ctx.posted_new;
+ int64_t posted_id = ctx.posted_id;
free(series);
free(date);
free(desc);
@@ -1081,43 +1080,6 @@ struct tui_vrow {
char text[64];
};
-static int row_has_content(const struct tui_vrow *r)
-{
- return r->account[0] || r->debit[0] || r->credit[0] || r->text[0];
-}
-
-/* Keeps exactly one empty trailing row: appends one when the last row has
- content, and collapses an empty row that is followed by another empty
- row (the row the user just cleared). Adjusts *field so the selection
- stays on the same logical cell. */
-static void normalize_rows(struct tui_vrow *rows, int *nrows, int *field)
-{
- while (*nrows < 64 && row_has_content(&rows[*nrows - 1])) {
- memset(&rows[*nrows], 0, sizeof rows[0]);
- (*nrows)++;
- }
- int i = 0;
- while (i < *nrows - 1) {
- if (!row_has_content(&rows[i]) && !row_has_content(&rows[i + 1])) {
- int fr = *field >= 3 ? (*field - 3) / 4 : -1;
- if (fr > i)
- *field -= 4;
- memmove(&rows[i], &rows[i + 1],
- (size_t)(*nrows - i - 1) * sizeof rows[0]);
- memset(&rows[*nrows - 1], 0, sizeof rows[0]);
- (*nrows)--;
- continue;
- }
- i++;
- }
- if (*nrows < 1) {
- *nrows = 1;
- memset(&rows[0], 0, sizeof rows[0]);
- if (*field >= 3)
- *field = 3;
- }
-}
-
struct acct_cache {
int loaded;
int64_t org;
@@ -1190,546 +1152,470 @@ static int acct_exists(struct app *a, const char *number)
return 0;
}
-static int64_t vouchers_new(struct app *a)
-{
- static struct tui_vrow rows[64];
- char date[16], desc[256], series[16], client_ref[64];
+
+struct vform {
+ struct app *a;
+ struct tui_rt rt;
+ struct tui_vrow rows[64];
int64_t att_ids[32];
char att_names[32][80];
- int natt = 0;
- curs_set(1);
- memset(rows, 0, sizeof rows);
- snprintf(date, sizeof date, "%s",
- a->fy_start[0] ? a->fy_start : "2026-01-01");
- desc[0] = '\0';
- snprintf(series, sizeof series, "%s", a->default_series);
- snprintf(client_ref, sizeof client_ref, "%s", util_random_id("tui-", 8));
- int nrows = 1;
- int field = 0;
- int field_fresh = 1;
- size_t field_pos = (size_t)-1;
- int nfields = 3 + 4 * nrows;
- const int x_account = 2;
- const int name_w = 24;
- const int x_name = 9;
- const int x_debit = 34;
- const int x_credit = 46;
- const int x_text = 58;
- int text_w = COLS - x_text - 2;
- if (text_w < 8)
- text_w = 8;
+ int natt;
+ char date[16];
+ char series[16];
+ char desc[256];
+ char client_ref[64];
+};
- for (;;) {
- tui_frame("Nytt verifikat");
- int caret_y = -1, caret_x = -1;
- attron(A_BOLD);
- mvaddstr(2, 2, "Datum");
- mvaddstr(2, 22, "Serie");
- mvaddstr(4, 2, "Text");
- attroff(A_BOLD);
- {
- /* field order is 0 = date, 1 = series, 2 = text */
- const char *hvals[3] = { date, series, desc };
- int hx[3] = { 9, 29, 9 };
- int hy[3] = { 2, 2, 4 };
- int hw[3] = { 10, 8, COLS - 12 };
- for (int k = 0; k < 3; k++) {
- int w = hw[k] > 1 ? hw[k] : 1;
- char padded[512];
- snprintf(padded, sizeof padded, "%s", hvals[k]);
- tui_pad_field(padded, sizeof padded, w);
- if (field == k) {
- attron(A_REVERSE);
- caret_y = hy[k];
- size_t slen = strlen(hvals[k]);
- size_t cp = (field_fresh || field_pos == (size_t)-1 ||
- field_pos > slen)
- ? slen
- : field_pos;
- int cw = (int)tui_disp_width_n(hvals[k], cp);
- caret_x = hx[k] + (cw > w ? w : cw);
- }
- mvaddstr(hy[k], hx[k], padded);
- if (field == k)
- attroff(A_REVERSE);
- }
- }
- attron(A_BOLD);
- mvaddstr(6, x_account, "Konto");
- mvaddstr(6, x_name, "Namn");
- mvaddstr(6, x_debit, "Debet");
- mvaddstr(6, x_credit, "Kredit");
- mvaddstr(6, x_text, "Text");
- attroff(A_BOLD);
+static void vform_cell(void *ud, int row, int col, char **buf, size_t *cap)
+{
+ struct vform *vf = ud;
+ struct tui_vrow *r = &vf->rows[row];
+ if (col == 0) {
+ *buf = r->account;
+ *cap = sizeof r->account;
+ } else if (col == 2) {
+ *buf = r->debit;
+ *cap = sizeof r->debit;
+ } else if (col == 3) {
+ *buf = r->credit;
+ *cap = sizeof r->credit;
+ } else {
+ *buf = r->text;
+ *cap = sizeof r->text;
+ }
+}
- int64_t sum_d = 0, sum_c = 0;
- for (int i = 0; i < nrows; i++) {
- int y = 7 + i;
- int64_t v;
- if (tui_parse_kr(rows[i].debit, &v) == 0)
- sum_d += v;
- if (tui_parse_kr(rows[i].credit, &v) == 0)
- sum_c += v;
- int sel_ci = -1;
- if (field >= 3) {
- int ri = (field - 3) / 4, ci = (field - 3) % 4;
- if (ri == i)
- sel_ci = ci;
- }
- const char *vals[4] = { rows[i].account, rows[i].debit,
- rows[i].credit, rows[i].text };
- int xs[4] = { x_account, x_debit, x_credit, x_text };
- int ws[4] = { 6, 11, 11, text_w };
- for (int k = 0; k < 4; k++) {
- int w = ws[k] > 1 ? ws[k] : 1;
- char padded[512];
- snprintf(padded, sizeof padded, "%s", vals[k]);
- tui_pad_field(padded, sizeof padded, w);
- if (k == sel_ci) {
- attron(A_REVERSE);
- caret_y = y;
- size_t slen = strlen(vals[k]);
- size_t cp = (field_fresh || field_pos == (size_t)-1 ||
- field_pos > slen)
- ? slen
- : field_pos;
- int cw = (int)tui_disp_width_n(vals[k], cp);
- caret_x = xs[k] + (cw > w ? w : cw);
- }
- mvaddstr(y, xs[k], padded);
- if (k == sel_ci)
- attroff(A_REVERSE);
- }
- {
- char nbuf[512];
- const char *nm = acct_name(a, rows[i].account);
- snprintf(nbuf, sizeof nbuf, "%s", nm ? nm : "");
- tui_pad_field(nbuf, sizeof nbuf, name_w);
- attron(A_DIM);
- mvaddstr(y, x_name, nbuf);
- attroff(A_DIM);
- }
+static void vform_info(void *ud, int row, char *buf, size_t n)
+{
+ struct vform *vf = ud;
+ const char *nm = acct_name(vf->a, vf->rows[row].account);
+ snprintf(buf, n, "%s", nm ? nm : "");
+}
+
+static void vform_footer(void *ud, char *buf, size_t n)
+{
+ struct vform *vf = ud;
+ int64_t sum_d = 0, sum_c = 0;
+ for (int i = 0; i < vf->rt.nrows; i++) {
+ int64_t v;
+ if (tui_parse_kr(vf->rows[i].debit, &v) == 0)
+ sum_d += v;
+ if (tui_parse_kr(vf->rows[i].credit, &v) == 0)
+ sum_c += v;
+ }
+ char d1[32], c1[32], diff[32], line[1024];
+ tui_kr_format(sum_d, d1, sizeof d1);
+ tui_kr_format(sum_c, c1, sizeof c1);
+ tui_kr_format(sum_d - sum_c, diff, sizeof diff);
+ int balanced = sum_d == sum_c;
+ struct buf t;
+ buf_init(&t);
+ if (vf->natt > 0) {
+ snprintf(line, sizeof line, "Bilagor: ");
+ for (int i = 0; i < vf->natt; i++) {
+ size_t used = strlen(line);
+ if (used + 1 >= sizeof line)
+ break;
+ snprintf(line + used, sizeof line - used, "%.79s%s",
+ vf->att_names[i], i + 1 < vf->natt ? ", " : "");
}
- char d1[32], c1[32], diff[32];
- tui_kr_format(sum_d, d1, sizeof d1);
- tui_kr_format(sum_c, c1, sizeof c1);
- tui_kr_format(sum_d - sum_c, diff, sizeof diff);
- int balanced = sum_d == sum_c;
- move(7 + nrows, 2);
- clrtoeol();
- if (natt > 0) {
- char aline[4096] = "Bilagor: ";
- for (int i = 0; i < natt; i++) {
- size_t used = strlen(aline);
- if (used + 1 >= sizeof aline)
- break;
- snprintf(aline + used, sizeof aline - used, "%.79s%s",
- att_names[i], i + 1 < natt ? ", " : "");
+ buf_append(&t, line, strlen(line));
+ buf_append(&t, "\n", 1);
+ }
+ snprintf(line, sizeof line, "Summa debet %12s kredit %12s %s", d1, c1,
+ balanced ? "I BALANS" : "DIFF");
+ buf_append(&t, line, strlen(line));
+ if (!balanced) {
+ snprintf(line, sizeof line, "\nDifferens: %s", diff);
+ buf_append(&t, line, strlen(line));
+ }
+ buf_append(&t, "\0", 1);
+ snprintf(buf, n, "%s", (char *)t.p);
+ buf_free(&t);
+}
+
+static void vform_attach(struct vform *vf)
+{
+ struct app *a = vf->a;
+ char *path = file_browser(a, a->attachment_dir);
+ if (!path)
+ return;
+ struct stat sb;
+ if (stat(path, &sb) == 0 &&
+ (long)sb.st_size > a->max_attachment_bytes) {
+ tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.",
+ (double)sb.st_size / (1024 * 1024),
+ a->max_attachment_bytes / (1024 * 1024));
+ free(path);
+ return;
+ }
+ char *b64 = read_file_b64(path);
+ if (!b64) {
+ tui_message("Bilaga", "Kunde inte läsa %s", path);
+ free(path);
+ return;
+ }
+ if (vf->natt >= 32) {
+ tui_message("Bilaga", "Max 32 bilagor per verifikat.");
+ free(b64);
+ free(path);
+ return;
+ }
+ const char *base = strrchr(path, '/');
+ base = base ? base + 1 : path;
+ yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val *o = yyjson_mut_obj(d);
+ yyjson_mut_doc_set_root(d, o);
+ yyjson_mut_obj_add_strcpy(d, o, "filename", base);
+ yyjson_mut_obj_add_strcpy(d, o, "content_base64", b64);
+ char *args = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ size_t need = strlen(args) + 256;
+ char *raw = xmalloc(need);
+ snprintf(raw, need,
+ "{\"v\":1,\"id\":\"tui\",\"cmd\":\"attachment.put\","
+ "\"session\":\"%s\",\"org\":%lld,\"args\":%s}",
+ a->session, (long long)a->org, args);
+ free(args);
+ char *r = NULL;
+ if (client_send_line(&a->conn, raw) == 0)
+ r = client_read_line(&a->conn);
+ free(raw);
+ if (r && client_ok(r)) {
+ vf->att_ids[vf->natt] = jint_val(r, "result.id", 0);
+ snprintf(vf->att_names[vf->natt], sizeof vf->att_names[vf->natt],
+ "%s", base);
+ vf->natt++;
+ } else {
+ show_error("Kunde inte bifoga filen", r);
+ }
+ free(r);
+ free(b64);
+ free(path);
+}
+
+static void vform_template(struct vform *vf)
+{
+ struct app *a = vf->a;
+ char *lresp = client_rpc(&a->conn, "template.list", a->session, a->org,
+ "{\"active_only\":true}");
+ if (!lresp || !client_ok(lresp)) {
+ show_error("Mallar", lresp);
+ free(lresp);
+ return;
+ }
+ size_t ln = jarr_size(lresp, "result.items");
+ if (ln == 0) {
+ tui_message("Mallar",
+ "Inga mallar ännu. Skapa en under Mallar först.");
+ free(lresp);
+ return;
+ }
+ char **names = xcalloc(ln, sizeof(char *));
+ char **lines = xcalloc(ln, sizeof(char *));
+ for (size_t i = 0; i < ln; i++) {
+ char path[64], line[512];
+ snprintf(path, sizeof path, "result.items.%zu.name", i);
+ char *nm = jstr_dup(lresp, path);
+ snprintf(path, sizeof path, "result.items.%zu.series", i);
+ char *ser = jstr_dup(lresp, path);
+ snprintf(path, sizeof path, "result.items.%zu.row_count", i);
+ int64_t rc = jint_val(lresp, path, 0);
+ snprintf(path, sizeof path, "result.items.%zu.description", i);
+ char *ds = jstr_dup(lresp, path);
+ char nmbuf[128];
+ snprintf(nmbuf, sizeof nmbuf, "%s", nm ? nm : "");
+ tui_pad_field(nmbuf, sizeof nmbuf, 24);
+ snprintf(line, sizeof line, "%s %-3s %2lld rader %s", nmbuf,
+ ser ? ser : "", (long long)rc, ds ? ds : "");
+ names[i] = xstrdup(nm ? nm : "");
+ lines[i] = xstrdup(line);
+ free(nm);
+ free(ser);
+ free(ds);
+ }
+ int tsel = tui_select_list("Välj mall", lines, (int)ln, 0, 1, NULL, 0,
+ NULL, 0);
+ char tname[128] = "";
+ if (tsel >= 0)
+ snprintf(tname, sizeof tname, "%s", names[tsel]);
+ for (size_t i = 0; i < ln; i++) {
+ free(names[i]);
+ free(lines[i]);
+ }
+ free(names);
+ free(lines);
+ free(lresp);
+ if (tsel < 0)
+ return;
+ yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val *to = yyjson_mut_obj(d);
+ yyjson_mut_doc_set_root(d, to);
+ yyjson_mut_obj_add_strcpy(d, to, "name", tname);
+ char *targs = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ char *resp =
+ client_rpc(&a->conn, "template.get", a->session, a->org, targs);
+ free(targs);
+ if (!resp || !client_ok(resp)) {
+ show_error("Mall", resp);
+ free(resp);
+ return;
+ }
+ char xsbuf[512];
+ char *xs = tui_prompt_into(xsbuf, sizeof xsbuf, "Belopp (x, kr): ", "",
+ 0)
+ ? xsbuf
+ : NULL;
+ double xv = 0;
+ if (xs && *xs && !parse_x_double(xs, &xv)) {
+ tui_message("Fel", "Ogiltigt belopp.");
+ free(resp);
+ return;
+ }
+ size_t n = jarr_size(resp, "result.rows");
+ char **accts = xcalloc(n ? n : 1, sizeof(char *));
+ char **forms = xcalloc(n ? n : 1, sizeof(char *));
+ char **descs = xcalloc(n ? n : 1, sizeof(char *));
+ struct template_row *in = xcalloc(n ? n : 1, sizeof *in);
+ for (size_t i = 0; i < n; i++) {
+ char path[64];
+ snprintf(path, sizeof path, "result.rows.%zu.account", i);
+ accts[i] = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.rows.%zu.formula", i);
+ forms[i] = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.rows.%zu.description", i);
+ descs[i] = jstr_dup(resp, path);
+ in[i].account = accts[i] ? accts[i] : "";
+ in[i].formula = forms[i] ? forms[i] : "";
+ in[i].description = descs[i] && *descs[i] ? descs[i] : NULL;
+ }
+ struct resolved_row *out = xcalloc(n ? n : 1, sizeof *out);
+ size_t on = 0;
+ char ferr[256] = "";
+ if (formula_resolve_rows(in, n, xv, out, &on, ferr, sizeof ferr) != 0) {
+ tui_message("Mall", "%s",
+ ferr[0] ? ferr : "kunde inte lösa mallen");
+ } else {
+ memset(vf->rows, 0, sizeof vf->rows);
+ int use = (int)(on > 64 ? 64 : on);
+ for (int i = 0; i < use; i++) {
+ snprintf(vf->rows[i].account, sizeof vf->rows[i].account, "%s",
+ out[i].account);
+ char tmp[32];
+ if (out[i].debit_ore) {
+ tui_kr_format(out[i].debit_ore, tmp, sizeof tmp);
+ snprintf(vf->rows[i].debit, sizeof vf->rows[i].debit, "%s",
+ tmp);
+ } else {
+ tui_kr_format(out[i].credit_ore, tmp, sizeof tmp);
+ snprintf(vf->rows[i].credit, sizeof vf->rows[i].credit, "%s",
+ tmp);
}
- attron(A_DIM);
- mvaddnstr(7 + nrows, 2, aline, COLS - 4);
- attroff(A_DIM);
+ if (out[i].description[0])
+ snprintf(vf->rows[i].text, sizeof vf->rows[i].text, "%s",
+ out[i].description);
}
- attron(A_BOLD);
- mvprintw(7 + nrows + 1, 2, "Summa debet %12s kredit %12s ", d1, c1);
- mvprintw(7 + nrows + 1, 58, "%s", balanced ? "I BALANS" : "DIFF");
- attroff(A_BOLD);
- if (!balanced)
- mvprintw(7 + nrows + 2, 2, "Differens: %s", diff);
- tui_hints("F4 = mall ^F = bifoga fil Tab = byta fält F5 = validera"
- " ^X = rensa rad ^Enter = bokför Esc = avbryt");
- if (caret_y >= 0)
- move(caret_y, caret_x);
- refresh();
+ vf->rt.nrows = use > 0 ? use : 1;
+ char *tds = jstr_dup(resp, "result.description");
+ char *tser = jstr_dup(resp, "result.series");
+ if (!vf->desc[0] && tds && *tds)
+ replace_x_into(tds, xv, vf->desc, sizeof vf->desc);
+ if (tser && *tser)
+ snprintf(vf->series, sizeof vf->series, "%s", tser);
+ free(tds);
+ free(tser);
+ vf->rt.row = 0;
+ vf->rt.col = 0;
+ vf->rt.fresh = 1;
+ vf->rt.pos = (size_t)-1;
+ tui_rt_normalize(&vf->rt);
+ }
+ free(out);
+ for (size_t i = 0; i < n; i++) {
+ free(accts[i]);
+ free(forms[i]);
+ free(descs[i]);
+ }
+ free(accts);
+ free(forms);
+ free(descs);
+ free(in);
+ free(resp);
+}
- int ch = ui_getch();
- if (ch == 27) {
- curs_set(0);
- return 0;
- }
- if (ch == '\t') {
- field = (field + 1) % nfields;
- field_fresh = 1;
- continue;
- }
- if (ch == KEY_BTAB) {
- field = (field + nfields - 1) % nfields;
- field_fresh = 1;
+/* Returns the posted voucher id, or 0 to stay in the form. */
+static int64_t vform_post(struct vform *vf, int dry)
+{
+ struct app *a = vf->a;
+ yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val *o = yyjson_mut_obj(d);
+ yyjson_mut_doc_set_root(d, o);
+ yyjson_mut_obj_add_strcpy(d, o, "date", vf->date);
+ yyjson_mut_obj_add_strcpy(d, o, "description", vf->desc);
+ yyjson_mut_obj_add_strcpy(d, o, "series", vf->series);
+ yyjson_mut_obj_add_strcpy(d, o, "client_ref", vf->client_ref);
+ yyjson_mut_val *arr = yyjson_mut_arr(d);
+ for (int i = 0; i < vf->rt.nrows; i++) {
+ if (!vf->rows[i].account[0])
continue;
+ int64_t dv = 0, cv = 0;
+ if (tui_parse_kr(vf->rows[i].debit, &dv) != 0 ||
+ tui_parse_kr(vf->rows[i].credit, &cv) != 0) {
+ tui_message("Fel", "Ogiltigt belopp på rad %d", i + 1);
+ yyjson_mut_doc_free(d);
+ return 0;
}
- if (ch == KEY_UP) {
- if (field >= 3) {
- int ci = (field - 3) % 4;
- int ri = (field - 3) / 4;
- if (ri > 0) {
- field = 3 + (ri - 1) * 4 + ci;
- field_fresh = 1;
- }
+ yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr);
+ yyjson_mut_obj_add_strcpy(d, ro, "account", vf->rows[i].account);
+ yyjson_mut_obj_add_int(d, ro, "debit_ore", dv);
+ yyjson_mut_obj_add_int(d, ro, "credit_ore", cv);
+ if (vf->rows[i].text[0])
+ yyjson_mut_obj_add_strcpy(d, ro, "description",
+ vf->rows[i].text);
+ }
+ yyjson_mut_obj_add_val(d, o, "rows", arr);
+ if (vf->natt > 0) {
+ yyjson_mut_val *aa = yyjson_mut_arr(d);
+ for (int i = 0; i < vf->natt; i++)
+ yyjson_mut_arr_add_int(d, aa, vf->att_ids[i]);
+ yyjson_mut_obj_add_val(d, o, "attachment_ids", aa);
+ }
+ char *args = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ if (dry) {
+ char *raw = xmalloc(strlen(args) + 256);
+ sprintf(raw,
+ "{\"v\":1,\"id\":\"tui\",\"cmd\":\"voucher.post\","
+ "\"session\":\"%s\",\"org\":%lld,\"dry_run\":true,"
+ "\"args\":%s}",
+ a->session, (long long)a->org, args);
+ if (client_send_line(&a->conn, raw) == 0) {
+ char *r = client_read_line(&a->conn);
+ if (r && client_ok(r)) {
+ int64_t num = jint_val(r, "result.number", 0);
+ tui_message("Validering OK",
+ "Nummer %lld skulle tilldelas.", (long long)num);
+ } else {
+ show_error("Validering misslyckades", r);
}
- continue;
+ free(r);
+ } else {
+ tui_message("Fel", "Kunde inte nå servern");
}
- if (ch == KEY_DOWN) {
- if (field >= 3) {
- int ci = (field - 3) % 4;
- int ri = (field - 3) / 4;
- if (ri + 1 < nrows) {
- field = 3 + (ri + 1) * 4 + ci;
- field_fresh = 1;
- }
- }
- continue;
+ free(raw);
+ } else {
+ char *r = client_rpc(&a->conn, "voucher.post", a->session, a->org,
+ args);
+ if (r && client_ok(r)) {
+ int64_t id = jint_val(r, "result.id", 0);
+ int64_t num = jint_val(r, "result.number", 0);
+ char *ser = jstr_dup(r, "result.series");
+ int replayed = jbool_val(r, "result.replayed", 0);
+ tui_message("Bokfört", "%s%lld (id %lld)%s", ser ? ser : "",
+ (long long)num, (long long)id,
+ replayed ? " — redan bokfört (idempotent)" : "");
+ free(ser);
+ free(r);
+ free(args);
+ return id;
}
- if (ch == KEY_F(4)) {
- char tname[128] = "";
- char *lresp = client_rpc(&a->conn, "template.list", a->session,
- a->org, "{\"active_only\":true}");
- if (!lresp || !client_ok(lresp)) {
- show_error("Mallar", lresp);
- free(lresp);
+ show_error("Kunde inte bokföra", r);
+ free(r);
+ }
+ free(args);
+ return 0;
+}
+
+static int vform_key(void *ud, int ch)
+{
+ struct vform *vf = ud;
+ if (ch == KEY_F(4)) {
+ vform_template(vf);
+ return TUI_HOOK_STAY;
+ }
+ if (ch == 6) {
+ vform_attach(vf);
+ return TUI_HOOK_STAY;
+ }
+ return TUI_HOOK_NONE;
+}
+
+static int64_t vouchers_new(struct app *a)
+{
+ struct vform vf;
+ memset(&vf, 0, sizeof vf);
+ vf.a = a;
+ snprintf(vf.date, sizeof vf.date, "%s",
+ a->fy_start[0] ? a->fy_start : "2026-01-01");
+ snprintf(vf.series, sizeof vf.series, "%s", a->default_series);
+ char *ref = util_random_id("tui-", 8);
+ snprintf(vf.client_ref, sizeof vf.client_ref, "%s", ref);
+ free(ref);
+ int text_w = COLS - 60;
+ if (text_w < 8)
+ text_w = 8;
+ struct tui_rt_col cols[5] = {
+ { "Konto", 2, 6, TUI_RT_EDIT },
+ { "Namn", 9, 24, TUI_RT_INFO },
+ { "Debet", 34, 11, TUI_RT_EDIT },
+ { "Kredit", 46, 11, TUI_RT_EDIT },
+ { "Text", 58, text_w, TUI_RT_EDIT },
+ };
+ tui_rt_init(&vf.rt, 1, 64, 5, cols, 7, vform_cell, vform_info, &vf);
+ tui_rt_set_footer(&vf.rt, vform_footer);
+ tui_rt_set_key(&vf.rt, vform_key);
+ struct tui_form_field ff[3];
+ memset(ff, 0, sizeof ff);
+ ff[0].label = "Datum";
+ ff[0].value = vf.date;
+ ff[0].cap = sizeof vf.date;
+ ff[0].kind = TUI_F_DATE;
+ ff[1].label = "Serie";
+ ff[1].value = vf.series;
+ ff[1].cap = sizeof vf.series;
+ ff[1].kind = TUI_F_TEXT;
+ ff[2].label = "Text";
+ ff[2].value = vf.desc;
+ ff[2].cap = sizeof vf.desc;
+ ff[2].kind = TUI_F_TEXT;
+ const char *hint = "F4 = mall ^F = bifoga fil Tab = byta fält F5 ="
+ " validera ^X = rensa rad ^Enter = bokför Esc ="
+ " avbryt";
+ int stage = 0;
+ for (;;) {
+ if (stage == 0) {
+ int r = tui_form_run_hook("Nytt verifikat", ff, 3, 1, vform_key,
+ &vf);
+ if (r == TUI_FORM_BACK)
+ return 0;
+ if (r == TUI_FORM_REFRESH) {
+ vform_post(&vf, 1);
continue;
}
- size_t ln = jarr_size(lresp, "result.items");
- if (ln == 0) {
- tui_message("Mallar",
- "Inga mallar ännu. Skapa en under Mallar först.");
- free(lresp);
+ if (r == TUI_FORM_SUBMIT) {
+ int64_t id = vform_post(&vf, 0);
+ if (id > 0)
+ return id;
continue;
}
- char **names = xcalloc(ln, sizeof(char *));
- char **lines = xcalloc(ln, sizeof(char *));
- for (size_t i = 0; i < ln; i++) {
- char path[64], line[512];
- snprintf(path, sizeof path, "result.items.%zu.name", i);
- char *nm = jstr_dup(lresp, path);
- snprintf(path, sizeof path, "result.items.%zu.series", i);
- char *ser = jstr_dup(lresp, path);
- snprintf(path, sizeof path, "result.items.%zu.row_count", i);
- int64_t rc = jint_val(lresp, path, 0);
- snprintf(path, sizeof path, "result.items.%zu.description",
- i);
- char *ds = jstr_dup(lresp, path);
- char nmbuf[128];
- snprintf(nmbuf, sizeof nmbuf, "%s", nm ? nm : "");
- tui_pad_field(nmbuf, sizeof nmbuf, 24);
- snprintf(line, sizeof line, "%s %-3s %2lld rader %s", nmbuf,
- ser ? ser : "", (long long)rc, ds ? ds : "");
- names[i] = xstrdup(nm ? nm : "");
- lines[i] = xstrdup(line);
- free(nm);
- free(ser);
- free(ds);
- }
- int tsel = tui_select_list("Välj mall", lines, (int)ln, 0, 1, NULL, 0, NULL, 0);
- if (tsel >= 0)
- snprintf(tname, sizeof tname, "%s", names[tsel]);
- for (size_t i = 0; i < ln; i++) {
- free(names[i]);
- free(lines[i]);
- }
- free(names);
- free(lines);
- free(lresp);
- if (tsel < 0)
+ stage = 1;
+ } else {
+ int rr = tui_rt_run("Nytt verifikat", &vf.rt, hint);
+ if (rr == -1) {
+ stage = 0;
continue;
- {
- char tbuf[128];
- snprintf(tbuf, sizeof tbuf, "%s", tname);
- yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
- yyjson_mut_val *to = yyjson_mut_obj(d);
- yyjson_mut_doc_set_root(d, to);
- yyjson_mut_obj_add_strcpy(d, to, "name", tbuf);
- char *targs = yyjson_mut_write(d, 0, NULL);
- yyjson_mut_doc_free(d);
- char *resp = client_rpc(&a->conn, "template.get", a->session,
- a->org, targs);
- free(targs);
- if (!resp || !client_ok(resp)) {
- show_error("Mall", resp);
- free(resp);
- } else {
- char xsbuf[512]; char *xs = tui_prompt_into(xsbuf, sizeof xsbuf, "Belopp (x, kr): ", "", 0) ? xsbuf : NULL;
- double xv = 0;
- if (xs && *xs && !parse_x_double(xs, &xv)) {
- tui_message("Fel", "Ogiltigt belopp.");
- free(resp);
- continue;
- }
- size_t n = jarr_size(resp, "result.rows");
- char **accts = xcalloc(n ? n : 1, sizeof(char *));
- char **forms = xcalloc(n ? n : 1, sizeof(char *));
- char **descs = xcalloc(n ? n : 1, sizeof(char *));
- struct template_row *in = xcalloc(n ? n : 1, sizeof *in);
- for (size_t i = 0; i < n; i++) {
- char path[64];
- snprintf(path, sizeof path, "result.rows.%zu.account",
- i);
- accts[i] = jstr_dup(resp, path);
- snprintf(path, sizeof path, "result.rows.%zu.formula",
- i);
- forms[i] = jstr_dup(resp, path);
- snprintf(path, sizeof path,
- "result.rows.%zu.description", i);
- descs[i] = jstr_dup(resp, path);
- in[i].account = accts[i] ? accts[i] : "";
- in[i].formula = forms[i] ? forms[i] : "";
- in[i].description =
- descs[i] && *descs[i] ? descs[i] : NULL;
- }
- struct resolved_row *out =
- xcalloc(n ? n : 1, sizeof *out);
- size_t on = 0;
- char ferr[256] = "";
- if (formula_resolve_rows(in, n, xv, out, &on, ferr,
- sizeof ferr) != 0) {
- tui_message("Mall", "%s",
- ferr[0] ? ferr : "kunde inte lösa mallen");
- } else {
- memset(rows, 0, sizeof rows);
- int use = (int)(on > 64 ? 64 : on);
- for (int i = 0; i < use; i++) {
- snprintf(rows[i].account, sizeof rows[i].account,
- "%s", out[i].account);
- char tmp[32];
- if (out[i].debit_ore) {
- tui_kr_format(out[i].debit_ore, tmp, sizeof tmp);
- snprintf(rows[i].debit, sizeof rows[i].debit,
- "%s", tmp);
- } else {
- tui_kr_format(out[i].credit_ore, tmp,
- sizeof tmp);
- snprintf(rows[i].credit,
- sizeof rows[i].credit, "%s", tmp);
- }
- if (out[i].description[0])
- snprintf(rows[i].text, sizeof rows[i].text,
- "%s", out[i].description);
- }
- nrows = use > 0 ? use : 1;
- char *tds = jstr_dup(resp, "result.description");
- char *tser = jstr_dup(resp, "result.series");
- if (!desc[0] && tds && *tds)
- replace_x_into(tds, xv, desc, sizeof desc);
- if (tser && *tser)
- snprintf(series, sizeof series, "%s", tser);
- free(tds);
- free(tser);
- field = 3;
- field_fresh = 1;
- normalize_rows(rows, &nrows, &field);
- nfields = 3 + 4 * nrows;
- }
- free(out);
- for (size_t i = 0; i < n; i++) {
- free(accts[i]);
- free(forms[i]);
- free(descs[i]);
- }
- free(accts);
- free(forms);
- free(descs);
- free(in);
- free(resp);
- }
- }
- continue;
- }
- if (ch == 6) { /* ^F: attach a file */
- char *path = file_browser(a, a->attachment_dir);
- if (path) {
- struct stat sb;
- if (stat(path, &sb) == 0 &&
- (long)sb.st_size > a->max_attachment_bytes) {
- tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.",
- (double)sb.st_size / (1024 * 1024),
- a->max_attachment_bytes / (1024 * 1024));
- free(path);
- continue;
- }
- char *b64 = read_file_b64(path);
- if (!b64) {
- tui_message("Bilaga", "Kunde inte läsa %s", path);
- } else if (natt >= 32) {
- tui_message("Bilaga", "Max 32 bilagor per verifikat.");
- } else {
- const char *base = strrchr(path, '/');
- base = base ? base + 1 : path;
- yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
- yyjson_mut_val *o = yyjson_mut_obj(d);
- yyjson_mut_doc_set_root(d, o);
- yyjson_mut_obj_add_strcpy(d, o, "filename", base);
- yyjson_mut_obj_add_strcpy(d, o, "content_base64", b64);
- char *args = yyjson_mut_write(d, 0, NULL);
- yyjson_mut_doc_free(d);
- size_t need = strlen(args) + 256;
- char *raw = xmalloc(need);
- snprintf(raw, need,
- "{\"v\":1,\"id\":\"tui\",\"cmd\":"
- "\"attachment.put\",\"session\":\"%s\","
- "\"org\":%lld,\"args\":%s}",
- a->session, (long long)a->org, args);
- free(args);
- char *r = NULL;
- if (client_send_line(&a->conn, raw) == 0)
- r = client_read_line(&a->conn);
- free(raw);
- if (r && client_ok(r)) {
- att_ids[natt] = jint_val(r, "result.id", 0);
- snprintf(att_names[natt], sizeof att_names[natt], "%s",
- base);
- natt++;
- } else {
- show_error("Kunde inte bifoga filen", r);
- }
- free(r);
- }
- free(b64);
- free(path);
- }
- continue;
- }
- if (ch == KEY_CTRL_X) {
- if (field >= 3) {
- int ri = (field - 3) / 4;
- if (ri < nrows) {
- memset(&rows[ri], 0, sizeof rows[ri]);
- normalize_rows(rows, &nrows, &field);
- nfields = 3 + 4 * nrows;
- field_fresh = 1;
- }
- }
- continue;
- }
-
- /* build args for validate/post */
- int do_post = (ch == KEY_CTRL_ENTER || ch == KEY_F(9));
- int do_check = (ch == KEY_F(5));
- if (do_post || do_check) {
- yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
- yyjson_mut_val *o = yyjson_mut_obj(d);
- yyjson_mut_doc_set_root(d, o);
- yyjson_mut_obj_add_strcpy(d, o, "date", date);
- yyjson_mut_obj_add_strcpy(d, o, "description", desc);
- yyjson_mut_obj_add_strcpy(d, o, "series", series);
- yyjson_mut_obj_add_strcpy(d, o, "client_ref", client_ref);
- yyjson_mut_val *arr = yyjson_mut_arr(d);
- for (int i = 0; i < nrows; i++) {
- if (!rows[i].account[0])
- continue;
- int64_t dv = 0, cv = 0;
- if (tui_parse_kr(rows[i].debit, &dv) != 0 ||
- tui_parse_kr(rows[i].credit, &cv) != 0) {
- tui_message("Fel", "Ogiltigt belopp på rad %d", i + 1);
- yyjson_mut_doc_free(d);
- goto next_key;
- }
- yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr);
- yyjson_mut_obj_add_strcpy(d, ro, "account", rows[i].account);
- yyjson_mut_obj_add_int(d, ro, "debit_ore", dv);
- yyjson_mut_obj_add_int(d, ro, "credit_ore", cv);
- if (rows[i].text[0])
- yyjson_mut_obj_add_strcpy(d, ro, "description",
- rows[i].text);
}
- yyjson_mut_obj_add_val(d, o, "rows", arr);
- if (natt > 0) {
- yyjson_mut_val *aa = yyjson_mut_arr(d);
- for (int i = 0; i < natt; i++)
- yyjson_mut_arr_add_int(d, aa, att_ids[i]);
- yyjson_mut_obj_add_val(d, o, "attachment_ids", aa);
+ if (rr == -2) {
+ vform_post(&vf, 1);
+ continue;
}
- char *args = yyjson_mut_write(d, 0, NULL);
- yyjson_mut_doc_free(d);
- if (do_check) {
- char *raw = xmalloc(strlen(args) + 256);
- sprintf(raw,
- "{\"v\":1,\"id\":\"tui\",\"cmd\":\"voucher.post\","
- "\"session\":\"%s\",\"org\":%lld,\"dry_run\":true,"
- "\"args\":%s}",
- a->session, (long long)a->org, args);
- if (client_send_line(&a->conn, raw) == 0) {
- char *r = client_read_line(&a->conn);
- if (r && client_ok(r)) {
- int64_t num = jint_val(r, "result.number", 0);
- tui_message("Validering OK",
- "Nummer %lld skulle tilldelas.", (long long)num);
- } else {
- show_error("Validering misslyckades", r);
- }
- free(r);
- } else {
- tui_message("Fel", "Kunde inte nå servern");
- }
- free(raw);
- } else {
- char *r = client_rpc(&a->conn, "voucher.post", a->session, a->org,
- args);
- if (r && client_ok(r)) {
- int64_t id = jint_val(r, "result.id", 0);
- int64_t num = jint_val(r, "result.number", 0);
- char *ser = jstr_dup(r, "result.series");
- int replayed = jbool_val(r, "result.replayed", 0);
- tui_message("Bokfört", "%s%lld (id %lld)%s", ser ? ser : "",
- (long long)num, (long long)id,
- replayed ? " — redan bokfört (idempotent)" : "");
- free(ser);
- free(r);
- free(args);
- curs_set(0);
+ if (rr == -3) {
+ int64_t id = vform_post(&vf, 0);
+ if (id > 0)
return id;
- }
- show_error("Kunde inte bokföra", r);
- free(r);
}
- free(args);
- next_key:
- continue;
}
-
- /* text input into current field */
- char *buf = NULL;
- size_t cap = 0;
- if (field == 0) {
- buf = date;
- cap = sizeof date;
- } else if (field == 1) {
- buf = series;
- cap = sizeof series;
- } else if (field == 2) {
- buf = desc;
- cap = sizeof desc;
- } else {
- int ri = (field - 3) / 4, ci = (field - 3) % 4;
- if (ri < nrows) {
- switch (ci) {
- case 0:
- buf = rows[ri].account;
- cap = sizeof rows[ri].account;
- break;
- case 1:
- buf = rows[ri].debit;
- cap = sizeof rows[ri].debit;
- break;
- case 2:
- buf = rows[ri].credit;
- cap = sizeof rows[ri].credit;
- break;
- default:
- buf = rows[ri].text;
- cap = sizeof rows[ri].text;
- break;
- }
- }
- }
- if (!buf)
- continue;
- /* first keystroke in a freshly focused field replaces its content
- (so a prefilled date can be typed over directly) */
- if (field == 0)
- tui_date_field_edit(buf, cap, &field_pos, ch, &field_fresh);
- else
- tui_field_edit(buf, cap, &field_pos, ch, &field_fresh);
- normalize_rows(rows, &nrows, &field);
- nfields = 3 + 4 * nrows;
}
}
@@ -3296,9 +3182,94 @@ static int64_t pick_voucher(struct app *a)
return out;
}
+static void inbox_upload(struct app *a)
+{
+ char *path = file_browser(a, a->attachment_dir);
+ if (!path)
+ return;
+ struct stat sb;
+ if (stat(path, &sb) == 0 && (long)sb.st_size > a->max_attachment_bytes) {
+ tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.",
+ (double)sb.st_size / (1024 * 1024),
+ a->max_attachment_bytes / (1024 * 1024));
+ free(path);
+ return;
+ }
+ char *b64 = read_file_b64(path);
+ if (!b64) {
+ tui_message("Fel", "Kunde inte läsa %s", path);
+ free(path);
+ return;
+ }
+ const char *base = strrchr(path, '/');
+ base = base ? base + 1 : path;
+ yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val *o = yyjson_mut_obj(d);
+ yyjson_mut_doc_set_root(d, o);
+ yyjson_mut_obj_add_strcpy(d, o, "filename", base);
+ yyjson_mut_obj_add_strcpy(d, o, "content_base64", b64);
+ char *args = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ free(b64);
+ size_t reqlen = strlen(args) + 256;
+ char *raw = xmalloc(reqlen);
+ snprintf(raw, reqlen,
+ "{\"v\":1,\"id\":\"tui\",\"cmd\":\"attachment.put\","
+ "\"session\":\"%s\",\"org\":%lld,\"args\":%s}",
+ a->session, (long long)a->org, args);
+ free(args);
+ char *r = NULL;
+ if (client_send_line(&a->conn, raw) == 0)
+ r = client_read_line(&a->conn);
+ free(raw);
+ if (r && client_ok(r))
+ tui_message("Underlag", "Sparat.");
+ else
+ show_error("Kunde inte spara", r);
+ free(r);
+ free(path);
+}
+
+struct inboxctx {
+ struct app *a;
+ int64_t *ids;
+ size_t n;
+ int cursor;
+};
+
+static int inbox_key(void *ud, int ch)
+{
+ struct inboxctx *ctx = ud;
+ struct app *a = ctx->a;
+ int64_t sel_id = (ctx->cursor >= 0 && (size_t)ctx->cursor < ctx->n)
+ ? ctx->ids[ctx->cursor]
+ : 0;
+ if (ch == 'k' && sel_id) {
+ int64_t vid = pick_voucher(a);
+ if (vid) {
+ char kargs[64];
+ snprintf(kargs, sizeof kargs, "{\"id\":%lld,\"voucher_id\":%lld}",
+ (long long)sel_id, (long long)vid);
+ char *r = client_rpc(&a->conn, "attachment.link", a->session,
+ a->org, kargs);
+ if (r && client_ok(r))
+ tui_message("Underlag", "Kopplat till verifikatet.");
+ else
+ show_error("Kunde inte koppla", r);
+ free(r);
+ }
+ return TUI_HOOK_REFRESH;
+ }
+ if (ch == 'a' || ch == 'A') {
+ inbox_upload(a);
+ return TUI_HOOK_REFRESH;
+ }
+ return TUI_HOOK_NONE;
+}
+
static void inbox_screen(struct app *a)
{
- int top = 0, sel = 0;
+ int cursor = 0;
for (;;) {
if (g_quit)
return;
@@ -3328,118 +3299,27 @@ static void inbox_screen(struct app *a)
items[i] = xstrdup(line);
free(fn);
}
- int view = LINES - 4;
- if (n == 0)
- sel = 0;
- else if (sel >= (int)n)
- sel = (int)n - 1;
- if (sel < top)
- top = sel;
- if (sel >= top + view)
- top = sel - view + 1;
- if (top > (int)n - view)
- top = (int)n - view;
- if (top < 0)
- top = 0;
- tui_frame("Underlag (inkorg)");
- for (size_t i = (size_t)top; i < n && (int)(i - (size_t)top) < view;
- i++) {
- int y = 2 + (int)(i - (size_t)top);
- if ((int)i == sel)
- attron(A_REVERSE);
- mvaddnstr(y, 2, items[i], COLS - 4);
- if ((int)i == sel)
- attroff(A_REVERSE);
- }
- if (n == 0)
- mvaddstr(2, 2, "Inga obokförda underlag.");
- tui_hints("Enter = hämta k = koppla till verifikat a/^N = lägg till"
- " fil PgUp/PgDn, Home/End rullar F5 = uppdatera Esc/q ="
- " tillbaka");
- refresh();
- int ch = ui_getch();
- int64_t sel_id = (sel >= 0 && (size_t)sel < n) ? ids[sel] : 0;
+ struct inboxctx ctx = { a, ids, n, cursor };
+ int sel = tui_select_list_hook("Underlag (inkorg)", items, (int)n,
+ cursor, 1, &cursor, 1, NULL, 0,
+ inbox_key, &ctx);
+ int64_t sel_id =
+ (sel >= 0 && (size_t)sel < n) ? ids[sel] : 0;
for (size_t i = 0; i < n; i++)
free(items[i]);
free(items);
free(ids);
free(resp);
- if (ch == 27 || ch == 'q')
+ if (sel == -1)
return;
- if ((ch == '\n' || ch == '\r' || ch == KEY_ENTER) && sel_id)
- attachment_download(a, sel_id);
- else if (ch == KEY_DOWN && sel + 1 < (int)n)
- sel++;
- else if (ch == KEY_UP && sel > 0)
- sel--;
- else if (ch == KEY_NPAGE)
- sel = sel + view < (int)n ? sel + view : (int)n - 1;
- else if (ch == KEY_PPAGE)
- sel = sel - view > 0 ? sel - view : 0;
- else if (ch == KEY_HOME)
- sel = 0;
- else if (ch == KEY_END)
- sel = n > 0 ? (int)n - 1 : 0;
- else if (ch == 'k' && sel_id) {
- int64_t vid = pick_voucher(a);
- if (vid) {
- char kargs[64];
- snprintf(kargs, sizeof kargs,
- "{\"id\":%lld,\"voucher_id\":%lld}",
- (long long)sel_id, (long long)vid);
- char *r = client_rpc(&a->conn, "attachment.link", a->session,
- a->org, kargs);
- if (r && client_ok(r))
- tui_message("Underlag", "Kopplat till verifikatet.");
- else
- show_error("Kunde inte koppla", r);
- free(r);
- }
- } else if (ch == 'a' || ch == KEY_CTRL_N) {
- char *path = file_browser(a, a->attachment_dir);
- if (!path)
- continue;
- struct stat sb;
- if (stat(path, &sb) == 0 &&
- (long)sb.st_size > a->max_attachment_bytes) {
- tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.",
- (double)sb.st_size / (1024 * 1024),
- a->max_attachment_bytes / (1024 * 1024));
- free(path);
- continue;
- }
- char *b64 = read_file_b64(path);
- if (!b64) {
- tui_message("Fel", "Kunde inte läsa %s", path);
- continue;
- }
- const char *base = strrchr(path, '/');
- base = base ? base + 1 : path;
- yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
- yyjson_mut_val *o = yyjson_mut_obj(d);
- yyjson_mut_doc_set_root(d, o);
- yyjson_mut_obj_add_strcpy(d, o, "filename", base);
- yyjson_mut_obj_add_strcpy(d, o, "content_base64", b64);
- char *args = yyjson_mut_write(d, 0, NULL);
- yyjson_mut_doc_free(d);
- free(b64);
- size_t reqlen = strlen(args) + 256;
- char *raw = xmalloc(reqlen);
- snprintf(raw, reqlen,
- "{\"v\":1,\"id\":\"tui\",\"cmd\":\"attachment.put\","
- "\"session\":\"%s\",\"org\":%lld,\"args\":%s}",
- a->session, (long long)a->org, args);
- free(args);
- char *r = NULL;
- if (client_send_line(&a->conn, raw) == 0)
- r = client_read_line(&a->conn);
- free(raw);
- if (r && client_ok(r))
- tui_message("Underlag", "Sparat.");
- else
- show_error("Kunde inte spara", r);
- free(r);
+ if (sel == -2)
+ continue;
+ if (sel == -4) {
+ inbox_upload(a);
+ continue;
}
+ if (sel_id)
+ attachment_download(a, sel_id);
}
}
@@ -3517,53 +3397,132 @@ struct tui_trow {
char text[64];
};
-static int trow_has_content(const struct tui_trow *r)
+struct tplform {
+ struct app *a;
+ struct tui_rt rt;
+ struct tui_trow rows[64];
+ int nrows;
+ char name[128];
+ char desc[256];
+ char series[16];
+ int64_t id;
+};
+
+static void tpl_cell(void *ud, int row, int col, char **buf, size_t *cap)
{
- return r->account[0] || r->formula[0] || r->text[0];
+ struct tplform *tf = ud;
+ struct tui_trow *r = &tf->rows[row];
+ if (col == 0) {
+ *buf = r->account;
+ *cap = sizeof r->account;
+ } else if (col == 2) {
+ *buf = r->formula;
+ *cap = sizeof r->formula;
+ } else {
+ *buf = r->text;
+ *cap = sizeof r->text;
+ }
}
-static void trow_normalize(struct tui_trow *rows, int *nrows, int *field)
+static void tpl_info(void *ud, int row, char *buf, size_t n)
{
- while (*nrows < 64 && trow_has_content(&rows[*nrows - 1])) {
- memset(&rows[*nrows], 0, sizeof rows[0]);
- (*nrows)++;
- }
- int i = 0;
- while (i < *nrows - 1) {
- if (!trow_has_content(&rows[i]) && !trow_has_content(&rows[i + 1])) {
- int fr = *field >= 3 ? (*field - 3) / 3 : -1;
- if (fr > i)
- *field -= 3;
- memmove(&rows[i], &rows[i + 1],
- (size_t)(*nrows - i - 1) * sizeof rows[0]);
- memset(&rows[*nrows - 1], 0, sizeof rows[0]);
- (*nrows)--;
+ struct tplform *tf = ud;
+ const char *nm = acct_name(tf->a, tf->rows[row].account);
+ snprintf(buf, n, "%s", nm ? nm : "");
+}
+
+static void tpl_footer(void *ud, char *buf, size_t n)
+{
+ (void)ud;
+ snprintf(buf, n, "x = belopp i kronor. Positivt blir debet, negativt"
+ " kredit.");
+}
+
+/* F5 dry-runs; Ctrl+Enter creates/updates. Returns 1 when saved. */
+static int tpl_save(struct tplform *tf, int dry)
+{
+ const char *problem = NULL;
+ char msg[256] = "";
+ if (!tf->name[0])
+ problem = "Namn saknas.";
+ int used = 0;
+ for (int i = 0; i < tf->rt.nrows && !problem; i++) {
+ if (!tf->rows[i].account[0])
continue;
+ if (!acct_exists(tf->a, tf->rows[i].account)) {
+ snprintf(msg, sizeof msg, "Konto %.15s finns inte (rad %d).",
+ tf->rows[i].account, i + 1);
+ problem = msg;
+ break;
}
- i++;
+ if (!tf->rows[i].formula[0] || !formula_valid(tf->rows[i].formula)) {
+ snprintf(msg, sizeof msg,
+ "Ogiltig formel på rad %d: '%.60s'.", i + 1,
+ tf->rows[i].formula);
+ problem = msg;
+ break;
+ }
+ used++;
+ }
+ if (!problem && used == 0)
+ problem = "Mallen behöver minst en rad.";
+ if (problem) {
+ tui_message("Mall", "%s", problem);
+ return 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);
+ if (tf->id)
+ yyjson_mut_obj_add_int(d, o, "id", tf->id);
+ yyjson_mut_obj_add_strcpy(d, o, "name", tf->name);
+ yyjson_mut_obj_add_strcpy(d, o, "series", tf->series);
+ yyjson_mut_obj_add_strcpy(d, o, "description", tf->desc);
+ yyjson_mut_val *arr = yyjson_mut_arr(d);
+ for (int i = 0; i < tf->rt.nrows; i++) {
+ if (!tf->rows[i].account[0])
+ continue;
+ yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr);
+ yyjson_mut_obj_add_strcpy(d, ro, "account", tf->rows[i].account);
+ yyjson_mut_obj_add_strcpy(d, ro, "formula", tf->rows[i].formula);
+ if (tf->rows[i].text[0])
+ yyjson_mut_obj_add_strcpy(d, ro, "description",
+ tf->rows[i].text);
}
- if (*nrows < 1) {
- *nrows = 1;
- memset(&rows[0], 0, sizeof rows[0]);
- if (*field >= 3)
- *field = 3;
+ yyjson_mut_obj_add_val(d, o, "rows", arr);
+ char *args = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ const char *cmd = tf->id ? "template.update" : "template.create";
+ char *resp = dry ? rpc_dry(tf->a, cmd, args)
+ : client_rpc(&tf->a->conn, cmd, tf->a->session,
+ tf->a->org, args);
+ free(args);
+ if (resp && client_ok(resp)) {
+ if (dry) {
+ tui_message("Validering OK", "Mallen är korrekt (%d rader).",
+ used);
+ } else {
+ tui_message("Mall", "Mallen '%s' sparad.", tf->name);
+ free(resp);
+ return 1;
+ }
+ } else {
+ show_error("Kunde inte spara mallen", resp);
}
+ free(resp);
+ return 0;
}
-/* Template editor: a form, not a wizard. name == NULL creates a new
+/* Template editor: a form, not a wizard. load_name == NULL creates a new
template, otherwise the named template is loaded and saved via
template.update. */
static int template_form(struct app *a, const char *load_name)
{
- static struct tui_trow rows[64];
- char tname[128] = "", tdesc[256] = "";
- char series[16];
- snprintf(series, sizeof series, "%s", a->default_series);
- int64_t tpl_id = 0;
- curs_set(1);
- memset(rows, 0, sizeof rows);
- int nrows = 1;
-
+ struct tplform tf;
+ memset(&tf, 0, sizeof tf);
+ tf.a = a;
+ snprintf(tf.series, sizeof tf.series, "%s", a->default_series);
if (load_name && *load_name) {
yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
yyjson_mut_val *o = yyjson_mut_obj(d);
@@ -3577,19 +3536,18 @@ static int template_form(struct app *a, const char *load_name)
if (!resp || !client_ok(resp)) {
show_error("Mall", resp);
free(resp);
- curs_set(0);
return 0;
}
- tpl_id = jint_val(resp, "result.id", 0);
+ tf.id = jint_val(resp, "result.id", 0);
char *nm = jstr_dup(resp, "result.name");
char *ser = jstr_dup(resp, "result.series");
char *ds = jstr_dup(resp, "result.description");
if (nm)
- snprintf(tname, sizeof tname, "%s", nm);
+ snprintf(tf.name, sizeof tf.name, "%s", nm);
if (ser && *ser)
- snprintf(series, sizeof series, "%s", ser);
+ snprintf(tf.series, sizeof tf.series, "%s", ser);
if (ds)
- snprintf(tdesc, sizeof tdesc, "%s", ds);
+ snprintf(tf.desc, sizeof tf.desc, "%s", ds);
free(nm);
free(ser);
free(ds);
@@ -3604,292 +3562,77 @@ static int template_form(struct app *a, const char *load_name)
char *form = jstr_dup(resp, path);
snprintf(path, sizeof path, "result.rows.%zu.description", i);
char *rd = jstr_dup(resp, path);
- snprintf(rows[i].account, sizeof rows[i].account, "%s",
+ snprintf(tf.rows[i].account, sizeof tf.rows[i].account, "%s",
acc ? acc : "");
- snprintf(rows[i].formula, sizeof rows[i].formula, "%s",
+ snprintf(tf.rows[i].formula, sizeof tf.rows[i].formula, "%s",
form ? form : "");
- snprintf(rows[i].text, sizeof rows[i].text, "%s", rd ? rd : "");
+ snprintf(tf.rows[i].text, sizeof tf.rows[i].text, "%s",
+ rd ? rd : "");
free(acc);
free(form);
free(rd);
}
- nrows = n > 0 ? (int)n : 1;
+ tf.nrows = n > 0 ? (int)n : 1;
free(resp);
}
-
- int field = 0;
- int field_fresh = 1;
- size_t field_pos = (size_t)-1;
- int nfields = 3 + 3 * nrows;
- const int x_account = 2;
- const int name_w = 24;
- const int x_name = 9;
- const int x_formula = 34;
- const int form_w = 20;
- const int x_text = 55;
- int text_w = COLS - x_text - 2;
+ int text_w = COLS - 57;
if (text_w < 8)
text_w = 8;
-
+ struct tui_rt_col cols[4] = {
+ { "Konto", 2, 6, TUI_RT_EDIT },
+ { "Namn", 9, 24, TUI_RT_INFO },
+ { "Formel", 34, 20, TUI_RT_EDIT },
+ { "Radtext", 55, text_w, TUI_RT_EDIT },
+ };
+ tui_rt_init(&tf.rt, tf.nrows > 0 ? tf.nrows : 1, 64, 4, cols, 7, tpl_cell,
+ tpl_info, &tf);
+ tui_rt_set_footer(&tf.rt, tpl_footer);
+ struct tui_form_field ff[3];
+ memset(ff, 0, sizeof ff);
+ ff[0].label = "Namn";
+ ff[0].value = tf.name;
+ ff[0].cap = sizeof tf.name;
+ ff[0].kind = TUI_F_TEXT;
+ ff[1].label = "Serie";
+ ff[1].value = tf.series;
+ ff[1].cap = sizeof tf.series;
+ ff[1].kind = TUI_F_TEXT;
+ ff[2].label = "Text";
+ ff[2].value = tf.desc;
+ ff[2].cap = sizeof tf.desc;
+ ff[2].kind = TUI_F_TEXT;
+ const char *title = load_name ? "Redigera mall" : "Ny mall";
+ const char *hint = "Tab = byta fält F5 = validera ^X = rensa rad"
+ " ^Enter = spara Esc = avbryt";
+ int stage = 0;
for (;;) {
- tui_frame(load_name ? "Redigera mall" : "Ny mall");
- int caret_y = -1, caret_x = -1;
- attron(A_BOLD);
- mvaddstr(2, 2, "Namn");
- mvaddstr(2, 46, "Serie");
- mvaddstr(4, 2, "Text");
- attroff(A_BOLD);
- {
- const char *hvals[3] = { tname, series, tdesc };
- int hx[3] = { 9, 53, 9 };
- int hy[3] = { 2, 2, 4 };
- int hw[3] = { 34, 8, COLS - 12 };
- for (int k = 0; k < 3; k++) {
- int w = hw[k] > 1 ? hw[k] : 1;
- char padded[512];
- snprintf(padded, sizeof padded, "%s", hvals[k]);
- tui_pad_field(padded, sizeof padded, w);
- if (field == k) {
- attron(A_REVERSE);
- caret_y = hy[k];
- size_t slen = strlen(hvals[k]);
- size_t cp = (field_fresh || field_pos == (size_t)-1 ||
- field_pos > slen)
- ? slen
- : field_pos;
- int cw = (int)tui_disp_width_n(hvals[k], cp);
- caret_x = hx[k] + (cw > w ? w : cw);
- }
- mvaddstr(hy[k], hx[k], padded);
- if (field == k)
- attroff(A_REVERSE);
- }
- }
- attron(A_BOLD);
- mvaddstr(6, x_account, "Konto");
- mvaddstr(6, x_name, "Namn");
- mvaddstr(6, x_formula, "Formel");
- mvaddstr(6, x_text, "Radtext");
- attroff(A_BOLD);
-
- for (int i = 0; i < nrows; i++) {
- int y = 7 + i;
- int sel_ci = -1;
- if (field >= 3) {
- int ri = (field - 3) / 3, ci = (field - 3) % 3;
- if (ri == i)
- sel_ci = ci;
- }
- const char *vals[3] = { rows[i].account, rows[i].formula,
- rows[i].text };
- int xs[3] = { x_account, x_formula, x_text };
- int ws[3] = { 6, form_w, text_w };
- for (int k = 0; k < 3; k++) {
- int w = ws[k] > 1 ? ws[k] : 1;
- char padded[512];
- snprintf(padded, sizeof padded, "%s", vals[k]);
- tui_pad_field(padded, sizeof padded, w);
- if (k == sel_ci) {
- attron(A_REVERSE);
- caret_y = y;
- size_t slen = strlen(vals[k]);
- size_t cp = (field_fresh || field_pos == (size_t)-1 ||
- field_pos > slen)
- ? slen
- : field_pos;
- int cw = (int)tui_disp_width_n(vals[k], cp);
- caret_x = xs[k] + (cw > w ? w : cw);
- }
- mvaddstr(y, xs[k], padded);
- if (k == sel_ci)
- attroff(A_REVERSE);
- }
- {
- char nbuf[512];
- const char *nm = acct_name(a, rows[i].account);
- snprintf(nbuf, sizeof nbuf, "%s", nm ? nm : "");
- tui_pad_field(nbuf, sizeof nbuf, name_w);
- attron(A_DIM);
- mvaddstr(y, x_name, nbuf);
- attroff(A_DIM);
- }
- }
- move(7 + nrows, 2);
- clrtoeol();
- attron(A_DIM);
- mvaddnstr(7 + nrows, 2,
- "x = belopp i kronor. Positivt blir debet, negativt kredit.",
- COLS - 4);
- attroff(A_DIM);
- tui_hints("Tab = byta fält F5 = validera ^X = rensa rad ^Enter = spara Esc = avbryt");
- if (caret_y >= 0)
- move(caret_y, caret_x);
- refresh();
-
- int ch = ui_getch();
- if (ch == 27) {
- curs_set(0);
- return 0;
- }
- if (ch == ' ') {
- field = (field + 1) % nfields;
- field_fresh = 1;
- continue;
- }
- if (ch == KEY_BTAB) {
- field = (field + nfields - 1) % nfields;
- field_fresh = 1;
- continue;
- }
- if (ch == KEY_UP) {
- if (field >= 3) {
- int ci = (field - 3) % 3;
- int ri = (field - 3) / 3;
- if (ri > 0) {
- field = 3 + (ri - 1) * 3 + ci;
- field_fresh = 1;
- }
- }
- continue;
- }
- if (ch == KEY_DOWN) {
- if (field >= 3) {
- int ci = (field - 3) % 3;
- int ri = (field - 3) / 3;
- if (ri + 1 < nrows) {
- field = 3 + (ri + 1) * 3 + ci;
- field_fresh = 1;
- }
- }
- continue;
- }
- if (ch == KEY_CTRL_X) {
- if (field >= 3) {
- int ri = (field - 3) / 3;
- if (ri < nrows) {
- memset(&rows[ri], 0, sizeof rows[ri]);
- trow_normalize(rows, &nrows, &field);
- nfields = 3 + 3 * nrows;
- field_fresh = 1;
- }
- }
- continue;
- }
-
- /* validation and save */
- if (ch == KEY_F(5) || ch == KEY_CTRL_ENTER || ch == KEY_F(9)) {
- const char *problem = NULL;
- char msg[256] = "";
- if (!tname[0]) {
- problem = "Namn saknas.";
- }
- int used = 0;
- for (int i = 0; i < nrows && !problem; i++) {
- if (!rows[i].account[0])
- continue;
- if (!acct_exists(a, rows[i].account)) {
- snprintf(msg, sizeof msg,
- "Konto %.15s finns inte (rad %d).",
- rows[i].account, i + 1);
- problem = msg;
- break;
- }
- if (!rows[i].formula[0] || !formula_valid(rows[i].formula)) {
- snprintf(msg, sizeof msg,
- "Ogiltig formel på rad %d: '%.60s'.", i + 1,
- rows[i].formula);
- problem = msg;
- break;
- }
- used++;
- }
- if (!problem && used == 0)
- problem = "Mallen behöver minst en rad.";
- if (problem) {
- tui_message("Mall", "%s", problem);
+ if (stage == 0) {
+ int r = tui_form_run(title, ff, 3, 1);
+ if (r == TUI_FORM_BACK)
+ return 0;
+ if (r == TUI_FORM_REFRESH) {
+ tpl_save(&tf, 1);
continue;
}
-
- yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
- yyjson_mut_val *o = yyjson_mut_obj(d);
- yyjson_mut_doc_set_root(d, o);
- if (tpl_id)
- yyjson_mut_obj_add_int(d, o, "id", tpl_id);
- yyjson_mut_obj_add_strcpy(d, o, "name", tname);
- yyjson_mut_obj_add_strcpy(d, o, "series", series);
- yyjson_mut_obj_add_strcpy(d, o, "description", tdesc);
- yyjson_mut_val *arr = yyjson_mut_arr(d);
- for (int i = 0; i < nrows; i++) {
- if (!rows[i].account[0])
- continue;
- yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr);
- yyjson_mut_obj_add_strcpy(d, ro, "account", rows[i].account);
- yyjson_mut_obj_add_strcpy(d, ro, "formula", rows[i].formula);
- if (rows[i].text[0])
- yyjson_mut_obj_add_strcpy(d, ro, "description",
- rows[i].text);
- }
- yyjson_mut_obj_add_val(d, o, "rows", arr);
- char *args = yyjson_mut_write(d, 0, NULL);
- yyjson_mut_doc_free(d);
- const char *cmd = tpl_id ? "template.update" : "template.create";
- char *resp = ch == KEY_F(5)
- ? rpc_dry(a, cmd, args)
- : client_rpc(&a->conn, cmd, a->session, a->org,
- args);
- free(args);
- if (resp && client_ok(resp)) {
- if (ch == KEY_F(5)) {
- tui_message("Validering OK",
- "Mallen är korrekt (%d rader).", used);
- } else {
- tui_message("Mall", "Mallen '%s' sparad.", tname);
- free(resp);
- curs_set(0);
+ if (r == TUI_FORM_SUBMIT) {
+ if (tpl_save(&tf, 0))
return 1;
- }
- } else {
- show_error("Kunde inte spara mallen", resp);
+ continue;
}
- free(resp);
- continue;
- }
-
- char *buf = NULL;
- size_t cap = 0;
- if (field == 0) {
- buf = tname;
- cap = sizeof tname;
- } else if (field == 1) {
- buf = series;
- cap = sizeof series;
- } else if (field == 2) {
- buf = tdesc;
- cap = sizeof tdesc;
+ stage = 1;
} else {
- int ri = (field - 3) / 3, ci = (field - 3) % 3;
- if (ri < nrows) {
- switch (ci) {
- case 0:
- buf = rows[ri].account;
- cap = sizeof rows[ri].account;
- break;
- case 1:
- buf = rows[ri].formula;
- cap = sizeof rows[ri].formula;
- break;
- default:
- buf = rows[ri].text;
- cap = sizeof rows[ri].text;
- break;
- }
+ int rr = tui_rt_run(title, &tf.rt, hint);
+ if (rr == -1) {
+ stage = 0;
+ continue;
+ }
+ if (rr == -2) {
+ tpl_save(&tf, 1);
+ continue;
}
+ if (rr == -3 && tpl_save(&tf, 0))
+ return 1;
}
- if (!buf)
- continue;
- tui_field_edit(buf, cap, &field_pos, ch, &field_fresh);
- trow_normalize(rows, &nrows, &field);
- nfields = 3 + 3 * nrows;
}
}
@@ -3926,39 +3669,6 @@ struct tui_ibrow {
char text[64];
};
-static int ibrow_has_content(const struct tui_ibrow *r)
-{
- return r->account[0] || r->amount[0] || r->text[0];
-}
-
-static void ibrow_normalize(struct tui_ibrow *rows, int *nrows, int *field)
-{
- while (*nrows < 64 && ibrow_has_content(&rows[*nrows - 1])) {
- memset(&rows[*nrows], 0, sizeof rows[0]);
- (*nrows)++;
- }
- int i = 0;
- while (i < *nrows - 1) {
- if (!ibrow_has_content(&rows[i]) && !ibrow_has_content(&rows[i + 1])) {
- int fr = *field / 3;
- if (fr > i)
- *field -= 3;
- memmove(&rows[i], &rows[i + 1],
- (size_t)(*nrows - i - 1) * sizeof rows[0]);
- memset(&rows[*nrows - 1], 0, sizeof rows[0]);
- (*nrows)--;
- continue;
- }
- i++;
- }
- if (*nrows < 1) {
- *nrows = 1;
- memset(&rows[0], 0, sizeof rows[0]);
- if (*field >= 3)
- *field = 0;
- }
-}
-
/* Loads the net IB per account for the active fiscal year. */
/* Effective opening balances of the selected year: the year's IB vouchers
plus all earlier movements, exactly what the reports show. */
@@ -4013,378 +3723,271 @@ static int64_t ib_old_lookup(char **accs, int64_t *amts, int n,
return 0;
}
+struct ibform {
+ struct app *a;
+ struct tui_rt rt;
+ struct tui_ibrow rows[64];
+ char **old_acc;
+ int64_t *old_amt;
+ int nold;
+};
+
+static void ib_cell(void *ud, int row, int col, char **buf, size_t *cap)
+{
+ struct ibform *ib = ud;
+ struct tui_ibrow *r = &ib->rows[row];
+ if (col == 0) {
+ *buf = r->account;
+ *cap = sizeof r->account;
+ } else if (col == 2) {
+ *buf = r->amount;
+ *cap = sizeof r->amount;
+ } else {
+ *buf = r->text;
+ *cap = sizeof r->text;
+ }
+}
+
+static void ib_info(void *ud, int row, char *buf, size_t n)
+{
+ struct ibform *ib = ud;
+ const char *nm = acct_name(ib->a, ib->rows[row].account);
+ snprintf(buf, n, "%s", nm ? nm : "");
+}
+
+static void ib_footer(void *ud, char *buf, size_t n)
+{
+ (void)ud;
+ snprintf(buf, n, "Positiva belopp = debet, negativa = kredit. Summan ska"
+ " bli noll.");
+}
+
+static const char *ib_check(struct ibform *ib, int *used_out, char *msg,
+ size_t msglen)
+{
+ int64_t sum = 0;
+ int used = 0;
+ for (int i = 0; i < ib->rt.nrows; i++) {
+ if (!ib->rows[i].account[0])
+ continue;
+ if (!acct_exists(ib->a, ib->rows[i].account)) {
+ snprintf(msg, msglen, "Konto %.15s finns inte (rad %d).",
+ ib->rows[i].account, i + 1);
+ return msg;
+ }
+ double kr = 0;
+ if (ib->rows[i].amount[0] &&
+ !parse_x_double(ib->rows[i].amount, &kr)) {
+ snprintf(msg, msglen, "Ogiltigt belopp på rad %d: '%.20s'.", i + 1,
+ ib->rows[i].amount);
+ return msg;
+ }
+ sum += (int64_t)llround(kr * 100.0);
+ used++;
+ }
+ if (used == 0)
+ return "Ange minst ett konto.";
+ if (sum != 0) {
+ snprintf(msg, msglen, "Summan måste bli noll (nu %lld öre).",
+ (long long)sum);
+ return msg;
+ }
+ *used_out = used;
+ return NULL;
+}
+
+/* Posts the delta against the existing IB so nothing is ever edited. */
+static int ib_post_delta(struct ibform *ib)
+{
+ struct app *a = ib->a;
+ yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val *o = yyjson_mut_obj(d);
+ yyjson_mut_doc_set_root(d, o);
+ yyjson_mut_obj_add_strcpy(d, o, "date", a->fy_start);
+ yyjson_mut_obj_add_strcpy(d, o, "description", "Ingående balans");
+ yyjson_mut_obj_add_strcpy(d, o, "series", "IB");
+ char *ref = util_random_id("tui-", 8);
+ yyjson_mut_obj_add_strcpy(d, o, "client_ref", ref);
+ free(ref);
+ yyjson_mut_val *arr = yyjson_mut_arr(d);
+ int posted = 0;
+ for (int i = 0; i < ib->rt.nrows; i++) {
+ if (!ib->rows[i].account[0])
+ continue;
+ double kr = 0;
+ if (ib->rows[i].amount[0] &&
+ !parse_x_double(ib->rows[i].amount, &kr))
+ kr = 0;
+ int64_t new_amt = (int64_t)llround(kr * 100.0);
+ int64_t prev_amt =
+ ib_old_lookup(ib->old_acc, ib->old_amt, ib->nold,
+ ib->rows[i].account);
+ int64_t delta = new_amt - prev_amt;
+ if (delta == 0)
+ continue;
+ yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr);
+ yyjson_mut_obj_add_strcpy(d, ro, "account", ib->rows[i].account);
+ yyjson_mut_obj_add_int(d, ro, "debit_ore", delta > 0 ? delta : 0);
+ yyjson_mut_obj_add_int(d, ro, "credit_ore", delta < 0 ? -delta : 0);
+ if (ib->rows[i].text[0])
+ yyjson_mut_obj_add_strcpy(d, ro, "description",
+ ib->rows[i].text);
+ posted++;
+ }
+ for (int i = 0; i < ib->nold; i++) {
+ int still = 0;
+ for (int j = 0; j < ib->rt.nrows; j++)
+ if (strcmp(ib->rows[j].account, ib->old_acc[i]) == 0) {
+ still = 1;
+ break;
+ }
+ if (!still && ib->old_amt[i] != 0) {
+ yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr);
+ yyjson_mut_obj_add_strcpy(d, ro, "account", ib->old_acc[i]);
+ yyjson_mut_obj_add_int(d, ro, "debit_ore",
+ ib->old_amt[i] < 0 ? -ib->old_amt[i] : 0);
+ yyjson_mut_obj_add_int(d, ro, "credit_ore",
+ ib->old_amt[i] > 0 ? ib->old_amt[i] : 0);
+ posted++;
+ }
+ }
+ if (posted == 0) {
+ yyjson_mut_doc_free(d);
+ tui_message("Ingående balans", "Ingen ändring.");
+ return 0;
+ }
+ yyjson_mut_obj_add_val(d, o, "rows", arr);
+ char *args = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ char *resp =
+ client_rpc(&a->conn, "voucher.post", a->session, a->org, args);
+ free(args);
+ if (resp && client_ok(resp)) {
+ int64_t num = jint_val(resp, "result.number", 0);
+ tui_message("Ingående balans",
+ "Sparat som IB %lld (%d justerade rader).",
+ (long long)num, posted);
+ free(resp);
+ return 1;
+ }
+ show_error("Kunde inte spara ingående balans", resp);
+ free(resp);
+ return 0;
+}
+
static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold)
{
- static struct tui_ibrow rows[64];
+ struct ibform ib;
+ memset(&ib, 0, sizeof ib);
+ ib.a = a;
+ ib.old_acc = old_acc;
+ ib.old_amt = old_amt;
+ ib.nold = nold;
if (nold > 63) {
tui_message("Ingående balans",
"För många konton (%d) för att redigera här.", nold);
return 0;
}
- memset(rows, 0, sizeof rows);
for (int i = 0; i < nold && i < 63; i++) {
- snprintf(rows[i].account, sizeof rows[i].account, "%s", old_acc[i]);
- tui_kr_format(old_amt[i], rows[i].amount, sizeof rows[i].amount);
+ snprintf(ib.rows[i].account, sizeof ib.rows[i].account, "%s",
+ old_acc[i]);
+ tui_kr_format(old_amt[i], ib.rows[i].amount,
+ sizeof ib.rows[i].amount);
}
- int nrows = nold > 0 ? nold : 1;
- int field = 0;
- int field_fresh = 1;
- size_t field_pos = (size_t)-1;
- int nfields = 3 * nrows;
- const int x_account = 2;
- const int name_w = 24;
- const int x_name = 9;
- const int x_amount = 34;
- const int amount_w = 14;
- const int x_text = 49;
- int text_w = COLS - x_text - 2;
+ int text_w = COLS - 51;
if (text_w < 6)
text_w = 6;
- curs_set(1);
+ struct tui_rt_col cols[4] = {
+ { "Konto", 2, 6, TUI_RT_EDIT },
+ { "Namn", 9, 24, TUI_RT_INFO },
+ { "Belopp", 34, 14, TUI_RT_EDIT },
+ { "Text", 49, text_w, TUI_RT_EDIT },
+ };
+ tui_rt_init(&ib.rt, nold > 0 ? nold : 1, 64, 4, cols, 5, ib_cell, ib_info,
+ &ib);
+ tui_rt_set_footer(&ib.rt, ib_footer);
for (;;) {
- tui_frame("Ingående balans");
- int caret_y = -1, caret_x = -1;
- attron(A_BOLD);
- mvaddstr(2, 2,
- "Positiva belopp = debet, negativa = kredit. Summan ska bli"
- " noll.");
- attroff(A_BOLD);
- attron(A_BOLD);
- mvaddstr(4, x_account, "Konto");
- mvaddstr(4, x_name, "Namn");
- mvaddstr(4, x_amount, "Belopp");
- mvaddstr(4, x_text, "Text");
- attroff(A_BOLD);
- for (int i = 0; i < nrows; i++) {
- int y = 5 + i;
- int sel_ci = -1;
- if (field >= 0 && field < nfields) {
- int ri = field / 3, ci = field % 3;
- if (ri == i)
- sel_ci = ci;
- }
- const char *vals[3] = { rows[i].account, rows[i].amount,
- rows[i].text };
- int xs[3] = { x_account, x_amount, x_text };
- int ws[3] = { 6, amount_w, text_w };
- for (int k = 0; k < 3; k++) {
- int w = ws[k] > 1 ? ws[k] : 1;
- char padded[512];
- snprintf(padded, sizeof padded, "%s", vals[k]);
- tui_pad_field(padded, sizeof padded, w);
- if (k == sel_ci) {
- attron(A_REVERSE);
- caret_y = y;
- size_t slen = strlen(vals[k]);
- size_t cp = (field_fresh || field_pos == (size_t)-1 ||
- field_pos > slen)
- ? slen
- : field_pos;
- int cw = (int)tui_disp_width_n(vals[k], cp);
- caret_x = xs[k] + (cw > w ? w : cw);
- }
- mvaddstr(y, xs[k], padded);
- if (k == sel_ci)
- attroff(A_REVERSE);
- }
- {
- char nbuf[512];
- const char *nm = acct_name(a, rows[i].account);
- snprintf(nbuf, sizeof nbuf, "%s", nm ? nm : "");
- tui_pad_field(nbuf, sizeof nbuf, name_w);
- attron(A_DIM);
- mvaddstr(y, x_name, nbuf);
- attroff(A_DIM);
- }
- }
- tui_hints("Tab = byta fält F5 = validera ^X = rensa rad ^Enter = spara Esc = avbryt");
- if (caret_y >= 0)
- move(caret_y, caret_x);
- refresh();
-
- int ch = ui_getch();
- if (ch == 27) {
- curs_set(0);
+ int rr = tui_rt_run("Ingående balans", &ib.rt,
+ "Tab = byta fält F5 = validera ^X = rensa rad"
+ " ^Enter = spara Esc = avbryt");
+ if (rr == -1)
return 0;
- }
- if (ch == '\t') {
- field = (field + 1) % nfields;
- field_fresh = 1;
+ char msg[256];
+ int used = 0;
+ const char *problem = ib_check(&ib, &used, msg, sizeof msg);
+ if (problem) {
+ tui_message("Ingående balans", "%s", problem);
continue;
}
- if (ch == KEY_BTAB) {
- field = (field + nfields - 1) % nfields;
- field_fresh = 1;
+ if (rr == -2) {
+ tui_message("Validering OK", "%d konton, summan är noll.", used);
continue;
}
- if (ch == KEY_UP) {
- int ci = field % 3, ri = field / 3;
- if (ri > 0) {
- field = (ri - 1) * 3 + ci;
- field_fresh = 1;
- }
- continue;
- }
- if (ch == KEY_DOWN) {
- int ci = field % 3, ri = field / 3;
- if (ri + 1 < nrows) {
- field = (ri + 1) * 3 + ci;
- field_fresh = 1;
- }
- continue;
- }
- if (ch == KEY_CTRL_X) {
- int ri = field / 3;
- if (ri < nrows) {
- memset(&rows[ri], 0, sizeof rows[ri]);
- ibrow_normalize(rows, &nrows, &field);
- nfields = 3 * nrows;
- field_fresh = 1;
- }
- continue;
- }
-
- if (ch == KEY_F(5) || ch == KEY_CTRL_ENTER || ch == KEY_F(9)) {
- const char *problem = NULL;
- char msg[256] = "";
- int64_t sum = 0;
- int used = 0;
- for (int i = 0; i < nrows && !problem; i++) {
- if (!rows[i].account[0])
- continue;
- if (!acct_exists(a, rows[i].account)) {
- snprintf(msg, sizeof msg,
- "Konto %.15s finns inte (rad %d).",
- rows[i].account, i + 1);
- problem = msg;
- break;
- }
- double kr = 0;
- if (rows[i].amount[0] && !parse_x_double(rows[i].amount, &kr)) {
- snprintf(msg, sizeof msg,
- "Ogiltigt belopp på rad %d: '%.20s'.", i + 1,
- rows[i].amount);
- problem = msg;
- break;
- }
- sum += (int64_t)llround(kr * 100.0);
- used++;
- }
- if (!problem && used == 0)
- problem = "Ange minst ett konto.";
- if (!problem && sum != 0) {
- snprintf(msg, sizeof msg,
- "Summan måste bli noll (nu %lld öre).",
- (long long)sum);
- problem = msg;
- }
- if (problem) {
- tui_message("Ingående balans", "%s", problem);
- continue;
- }
-
- if (ch == KEY_F(5)) {
- tui_message("Validering OK", "%d konton, summan är noll.", used);
- continue;
- }
+ if (ib_post_delta(&ib))
+ return 1;
+ }
+}
- /* delta against the existing IB so nothing is ever edited */
- yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
- yyjson_mut_val *o = yyjson_mut_obj(d);
- yyjson_mut_doc_set_root(d, o);
- char datebuf[16];
- snprintf(datebuf, sizeof datebuf, "%s", a->fy_start);
- yyjson_mut_obj_add_strcpy(d, o, "date", datebuf);
- yyjson_mut_obj_add_strcpy(d, o, "description",
- "Ingående balans");
- yyjson_mut_obj_add_strcpy(d, o, "series", "IB");
- char *ref = util_random_id("tui-", 8);
- yyjson_mut_obj_add_strcpy(d, o, "client_ref", ref);
- free(ref);
- yyjson_mut_val *arr = yyjson_mut_arr(d);
- int posted = 0;
- for (int i = 0; i < nrows; i++) {
- if (!rows[i].account[0])
- continue;
- double kr = 0;
- if (rows[i].amount[0] && !parse_x_double(rows[i].amount, &kr))
- kr = 0;
- int64_t new_amt = (int64_t)llround(kr * 100.0);
- int64_t prev_amt = ib_old_lookup(old_acc, old_amt, nold,
- rows[i].account);
- int64_t delta = new_amt - prev_amt;
- if (delta == 0)
- continue;
- yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr);
- yyjson_mut_obj_add_strcpy(d, ro, "account", rows[i].account);
- yyjson_mut_obj_add_int(d, ro, "debit_ore",
- delta > 0 ? delta : 0);
- yyjson_mut_obj_add_int(d, ro, "credit_ore",
- delta < 0 ? -delta : 0);
- if (rows[i].text[0])
- yyjson_mut_obj_add_strcpy(d, ro, "description",
- rows[i].text);
- posted++;
- }
- /* accounts that were in the old IB but removed now */
- for (int i = 0; i < nold; i++) {
- int still = 0;
- for (int j = 0; j < nrows; j++)
- if (strcmp(rows[j].account, old_acc[i]) == 0) {
- still = 1;
- break;
- }
- if (!still && old_amt[i] != 0) {
- yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr);
- yyjson_mut_obj_add_strcpy(d, ro, "account", old_acc[i]);
- yyjson_mut_obj_add_int(d, ro, "debit_ore",
- old_amt[i] < 0 ? -old_amt[i] : 0);
- yyjson_mut_obj_add_int(d, ro, "credit_ore",
- old_amt[i] > 0 ? old_amt[i] : 0);
- posted++;
- }
- }
- if (posted == 0) {
- yyjson_mut_doc_free(d);
- tui_message("Ingående balans", "Ingen ändring.");
- continue;
- }
- yyjson_mut_obj_add_val(d, o, "rows", arr);
- char *args = yyjson_mut_write(d, 0, NULL);
- yyjson_mut_doc_free(d);
- char *resp =
- client_rpc(&a->conn, "voucher.post", a->session, a->org, args);
- free(args);
- if (resp && client_ok(resp)) {
- int64_t num = jint_val(resp, "result.number", 0);
- tui_message("Ingående balans",
- "Sparat som IB %lld (%d justerade rader).",
- (long long)num, posted);
- free(resp);
- curs_set(0);
- return 1;
- }
- show_error("Kunde inte spara ingående balans", resp);
- free(resp);
- continue;
- }
+struct ibpager {
+ struct app *a;
+ char **accs;
+ int64_t *amts;
+ int n;
+};
- char *buf = NULL;
- size_t cap = 0;
- int ri = field / 3, ci = field % 3;
- if (ri < nrows) {
- switch (ci) {
- case 0:
- buf = rows[ri].account;
- cap = sizeof rows[ri].account;
- break;
- case 1:
- buf = rows[ri].amount;
- cap = sizeof rows[ri].amount;
- break;
- default:
- buf = rows[ri].text;
- cap = sizeof rows[ri].text;
- break;
- }
- }
- if (!buf)
- continue;
- tui_field_edit(buf, cap, &field_pos, ch, &field_fresh);
- ibrow_normalize(rows, &nrows, &field);
- nfields = 3 * nrows;
+static int ib_view_key(void *ud, int ch)
+{
+ struct ibpager *p = ud;
+ if (ch == 'e' || ch == 'E' || ch == TUI_KEY_CTRL_N) {
+ if (ib_form(p->a, p->accs, p->amts, p->n))
+ return TUI_HOOK_REFRESH;
+ return TUI_HOOK_STAY;
}
+ return TUI_HOOK_NONE;
}
+/* The list is a read-only table with no selection: tui_pager shows the
+ columns and the sum as plain text, and the e/^N keys run the editor. */
static void ib_screen(struct app *a)
{
- char **accs = NULL;
- int64_t *amts = NULL;
- int n = 0;
- int reload = 1;
- int top = 0;
for (;;) {
- if (reload) {
- for (int i = 0; i < n; i++)
- free(accs[i]);
- free(accs);
- free(amts);
- accs = NULL;
- amts = NULL;
- n = 0;
- top = 0;
- reload = 0;
- if (ib_load(a, &accs, &amts, &n) != 0)
- return;
- }
- int view = LINES - 10;
- if (view < 1)
- view = 1;
- if (top > n - view)
- top = n - view;
- if (top < 0)
- top = 0;
- tui_frame("Ingående balans");
- attron(A_BOLD);
- mvprintw(2, 2, "Räkenskapsår %s ingående balans (effektiv)",
+ char **accs = NULL;
+ int64_t *amts = NULL;
+ int n = 0;
+ if (ib_load(a, &accs, &amts, &n) != 0)
+ return;
+ struct buf t;
+ buf_init(&t);
+ buf_line(&t, "Räkenskapsår %s ingående balans (effektiv)\n\n",
a->fy_label);
- mvaddstr(4, 2, "Konto");
- mvaddstr(4, 9, "Namn");
- mvaddstr(4, 62, "Belopp");
- attroff(A_BOLD);
+ buf_line(&t, "%-7s%-53s%s\n\n", "Konto", "Namn", "Belopp");
int64_t sum = 0;
- for (int i = 0; i < n; i++)
+ for (int i = 0; i < n; i++) {
sum += amts[i];
- if (n == 0) {
- mvaddstr(6, 2, "Ingen ingående balans registrerad ännu.");
- }
- for (int i = 0; i < view && top + i < n; i++) {
- int idx = top + i;
- char amount[32];
- tui_kr_format(amts[idx], amount, sizeof amount);
- const char *nm = acct_name(a, accs[idx]);
- char nbuf[128];
+ char amount[32], nbuf[128];
+ tui_kr_format(amts[i], amount, sizeof amount);
+ const char *nm = acct_name(a, accs[i]);
snprintf(nbuf, sizeof nbuf, "%s", nm ? nm : "");
tui_pad_field(nbuf, sizeof nbuf, 50);
- mvprintw(6 + i, 2, "%-6s %s %14s", accs[idx], nbuf, amount);
+ buf_line(&t, "%-6s %s %14s\n", accs[i], nbuf, amount);
}
+ if (n == 0)
+ buf_line(&t, "Ingen ingående balans registrerad ännu.\n");
char sumbuf[32];
tui_kr_format(sum, sumbuf, sizeof sumbuf);
- attron(A_BOLD);
- mvprintw(LINES - 4, 2, "Summa: %s", sumbuf);
- attroff(A_BOLD);
- tui_hints("e/^N = redigera PgUp/PgDn, Home/End rullar F5 ="
- " uppdatera Esc/q = tillbaka");
- refresh();
- int ch = ui_getch();
- if (ch == 'e' || ch == 'E' || ch == KEY_CTRL_N) {
- if (ib_form(a, accs, amts, n))
- reload = 1;
- continue;
- }
- if (ch == KEY_DOWN && top + view < n)
- top++;
- else if (ch == KEY_UP && top > 0)
- top--;
- else if (ch == KEY_NPAGE)
- top += view;
- else if (ch == KEY_PPAGE)
- top -= view;
- else if (ch == KEY_HOME)
- top = 0;
- else if (ch == KEY_END)
- top = n > view ? n - view : 0;
- else if (ch == KEY_F(5)) {
- reload = 1;
- continue;
- } else if (ch == 27 || ch == 'q') {
- for (int i = 0; i < n; i++)
- free(accs[i]);
- free(accs);
- free(amts);
+ buf_line(&t, "\nSumma: %s\n", sumbuf);
+ buf_append(&t, "\0", 1);
+ struct ibpager p = { a, accs, amts, n };
+ int ret = tui_pager_hook("Ingående balans", (const char *)t.p,
+ "e/^N = redigera", 0, ib_view_key, &p);
+ buf_free(&t);
+ for (int i = 0; i < n; i++)
+ free(accs[i]);
+ free(accs);
+ free(amts);
+ if (ret == 0)
return;
- }
}
}
@@ -5950,94 +5553,36 @@ static int login_screen(struct app *a)
const char *env_pass = getenv("BOKFD_PASSWORD");
snprintf(user, sizeof user, "%s", a->username);
snprintf(pass, sizeof pass, "%s", env_pass ? env_pass : "");
- int field = 3; /* 0 server, 1 user, 2 password, 3 = Logga in button */
+ struct tui_form_field ff[4];
+ memset(ff, 0, sizeof ff);
+ ff[0].label = "Server";
+ ff[0].value = socket_path;
+ ff[0].cap = sizeof socket_path;
+ ff[0].kind = TUI_F_TEXT;
+ ff[1].label = "Användare";
+ ff[1].value = user;
+ ff[1].cap = sizeof user;
+ ff[1].kind = TUI_F_TEXT;
+ ff[2].label = "Lösenord";
+ ff[2].value = pass;
+ ff[2].cap = sizeof pass;
+ ff[2].kind = TUI_F_TEXT;
+ ff[2].mask = 1;
+ ff[3].label = "";
+ ff[3].value = "Logga in";
+ ff[3].kind = TUI_F_ACTION;
for (;;) {
- tui_frame("bokf — inloggning");
- attron(A_BOLD);
- mvaddstr(4, 4, "Server");
- mvaddstr(6, 4, "Användare");
- mvaddstr(8, 4, "Lösenord");
- attroff(A_BOLD);
- mvaddstr(4, 16, socket_path);
- mvaddstr(6, 16, user);
- {
- char stars[128];
- size_t n = strlen(pass);
- if (n > sizeof stars - 1)
- n = sizeof stars - 1;
- memset(stars, '*', n);
- stars[n] = '\0';
- mvaddstr(8, 16, stars);
- }
- {
- const char *btn = " Logga in ";
- int bw = tui_disp_width(btn);
- int bx = (COLS - bw) / 2;
- if (bx < 2)
- bx = 2;
- if (field == 3) {
- attron(A_REVERSE | A_BOLD);
- curs_set(0);
+ int r = tui_form_run("bokf — inloggning", ff, 4, 1);
+ if (r == TUI_FORM_BACK) {
+ if (g_quit) {
+ endwin();
+ return -1;
}
- mvaddstr(11, bx, btn);
- if (field == 3)
- attroff(A_REVERSE | A_BOLD);
- }
- tui_hints("Tab = byta fält/knapp Enter = logga in ^C = avsluta");
- refresh();
-
- if (field == 3) {
- int ch = ui_getch();
- if (ch == 27) {
- if (g_quit) {
- endwin();
- return -1;
- }
- continue; /* Esc never exits here */
- }
- if (ch == '\t') {
- field = 0;
- curs_set(1);
- continue;
- }
- if (ch == KEY_BTAB) {
- field = 2;
- curs_set(1);
- continue;
- }
- if (ch != '\n' && ch != '\r' && ch != KEY_ENTER && ch != ' ')
- continue;
- /* fall through to the login attempt */
- } else {
- int y = 4 + field * 2;
- int rc = tui_edit_field(y, 16, field == 0 ? socket_path
- : (field == 1 ? user : pass),
- field == 0 ? sizeof socket_path
- : (field == 1 ? sizeof user
- : sizeof pass),
- field == 2);
- if (rc == 0) {
- if (g_quit) {
- endwin();
- return -1;
- }
- continue;
- }
- if (rc == 2) {
- field = (field + 1) % 4;
- continue;
- }
- if (rc == 3) {
- field = (field + 3) % 4;
- continue;
- }
- if (field < 2) {
- field++;
- continue;
- }
- /* field 2: Enter submits */
+ continue; /* Esc never exits here */
}
+ if (r != TUI_FORM_SUBMIT && r != 3)
+ continue;
/* attempt login */
snprintf(a->socket, sizeof a->socket, "%s", socket_path);
@@ -6212,9 +5757,12 @@ int main(int argc, char **argv)
app.username[0] && env_pass && *env_pass ? 1 : 0);
for (int i = 0; i < 60 && !app.session[0]; i++) {
if (i > 0) {
- mvprintw(LINES / 2, 2, "Återansluter till servern... (%ds)",
- i);
- refresh();
+ char msg[128];
+ snprintf(msg, sizeof msg,
+ "Återansluter till servern... (%ds)", i);
+ tui_set_status(msg, NULL);
+ tui_frame("bokf — ansluter");
+ tui_redraw();
sleep(1);
}
if (session_arg && *session_arg) {
diff --git a/clients/tui.c b/clients/tui.c
index d19bd17..164b47b 100644
--- a/clients/tui.c
+++ b/clients/tui.c
@@ -689,8 +689,16 @@ int tui_choice_prompt(const char *label, const char *const *opts, int n,
int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new,
int allow_remove, int allow_toggle, int menu_mode)
{
- if (v->n <= 0)
- return -3;
+ if (v->n <= 0) {
+ /* an empty list is never a dead end: refresh, add and back work */
+ if (ch == KEY_F(5))
+ return -2;
+ if (ch == TUI_KEY_CTRL_N && allow_new)
+ return -4;
+ if (ch == 27 || ch == 'q')
+ return -1;
+ return TUI_NAV_NONE;
+ }
if (v->goto_active) {
if (ch >= '0' && ch <= '9' && strlen(v->gotobuf) < 9) {
size_t gl = strlen(v->gotobuf);
@@ -807,6 +815,16 @@ int tui_select_list(const char *title, char **items, int n, int start,
int allow_refresh, int *cursor, int allow_new,
const char *archive_action, int allow_toggle)
{
+ return tui_select_list_hook(title, items, n, start, allow_refresh, cursor,
+ allow_new, archive_action, allow_toggle, NULL,
+ NULL);
+}
+
+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)
+{
struct tui_list_nav v;
memset(&v, 0, sizeof v);
v.n = n;
@@ -833,11 +851,21 @@ int tui_select_list(const char *title, char **items, int n, int start,
allow_new ? " ^N = ny" : "", dbuf,
allow_toggle ? " ^A = öppna/stäng" : "");
list_draw(title, items, n, &v, hint);
- int r = tui_nav_key(&v, in_key(), allow_new,
- archive_action != NULL, allow_toggle, 0);
+ int ch = in_key();
+ int r = tui_nav_key(&v, ch, allow_new, archive_action != NULL,
+ allow_toggle, 0);
+ if (r == TUI_NAV_NONE && key) {
+ int h = key(ud, ch);
+ if (h == TUI_HOOK_BACK)
+ return -1;
+ if (h == TUI_HOOK_REFRESH)
+ return -2;
+ if (h != TUI_HOOK_NONE)
+ continue;
+ }
if (r == -2 && !allow_refresh)
continue;
- if (r != -3)
+ if (r != TUI_NAV_NONE)
return r;
}
}
@@ -926,6 +954,12 @@ void tui_frame(const char *title)
attroff(A_DIM);
}
+/* Flush a frame drawn by the application outside a widget loop. */
+void tui_redraw(void)
+{
+ refresh();
+}
+
void tui_hints(const char *s)
{
char line[512];
@@ -942,6 +976,14 @@ void tui_hints(const char *s)
int tui_pager(const char *title, const char *text, const char *action_hint)
{
+ return tui_pager_hook(title, text, action_hint,
+ action_hint ? TUI_PAGER_SAVE : 0, NULL, NULL);
+}
+
+int tui_pager_hook(const char *title, const char *text,
+ const char *extra_hint, unsigned flags,
+ int (*key)(void *ud, int ch), void *ud)
+{
int ret = 0;
int nlines = 0;
for (const char *p = text; *p; p++)
@@ -970,12 +1012,12 @@ int tui_pager(const char *title, const char *text, const char *action_hint)
if (top + i < n)
mvaddnstr(2 + i, 2, lines[top + i], COLS - 4);
}
- char hint[256];
- if (action_hint)
+ char hint[512];
+ if (extra_hint && *extra_hint)
snprintf(hint, sizeof hint,
"piltangenter/PgUp/PgDn rullar F5 = uppdatera %s "
"q = tillbaka",
- action_hint);
+ extra_hint);
else
snprintf(hint, sizeof hint,
"piltangenter/PgUp/PgDn rullar F5 = uppdatera "
@@ -989,10 +1031,21 @@ int tui_pager(const char *title, const char *text, const char *action_hint)
ret = 1;
break;
}
- if (ch == 's' && action_hint) {
+ if ((flags & TUI_PAGER_SAVE) && ch == 's') {
ret = 2;
break;
}
+ if (key) {
+ int h = key(ud, ch);
+ if (h == TUI_HOOK_BACK)
+ break;
+ if (h == TUI_HOOK_REFRESH) {
+ ret = 1;
+ break;
+ }
+ if (h == TUI_HOOK_STAY)
+ continue;
+ }
if (ch == KEY_DOWN && top + view < n)
top++;
else if (ch == KEY_UP && top > 0)
@@ -1040,6 +1093,12 @@ int tui_form_next(int sel, int n, int ch)
int tui_form_run(const char *title, struct tui_form_field *f, int n,
int can_edit)
{
+ return tui_form_run_hook(title, f, n, can_edit, NULL, NULL);
+}
+
+int tui_form_run_hook(const char *title, struct tui_form_field *f, int n,
+ int can_edit, int (*key)(void *ud, int ch), void *ud)
+{
int sel = 0;
for (;;) {
if (sel >= n)
@@ -1056,7 +1115,8 @@ int tui_form_run(const char *title, struct tui_form_field *f, int n,
snprintf(lbl, sizeof lbl, "%s", f[i].label);
tui_pad_field(lbl, sizeof lbl, 31);
if (f[i].kind == TUI_F_ACTION) {
- snprintf(val, sizeof val, "(lista)");
+ snprintf(val, sizeof val, "%s",
+ f[i].value ? f[i].value : "(lista)");
} else if (f[i].kind == TUI_F_AMOUNT && f[i].ore) {
tui_kr_format(*f[i].ore, val, sizeof val);
} else if (f[i].kind == TUI_F_CHOICE && f[i].choice &&
@@ -1064,6 +1124,12 @@ int tui_form_run(const char *title, struct tui_form_field *f, int n,
int c = *f[i].choice;
snprintf(val, sizeof val, "%s",
c >= 0 && c < f[i].nchoices ? f[i].choices[c] : "");
+ } else if (f[i].mask && f[i].kind == TUI_F_TEXT) {
+ size_t vl = f[i].value ? strlen(f[i].value) : 0;
+ if (vl >= sizeof val)
+ vl = sizeof val - 1;
+ memset(val, '*', vl);
+ val[vl] = '\0';
} else {
snprintf(val, sizeof val, "%s",
f[i].value ? f[i].value : "");
@@ -1094,6 +1160,17 @@ int tui_form_run(const char *title, struct tui_form_field *f, int n,
if (nxt == TUI_FORM_REFRESH || nxt == TUI_FORM_SUBMIT ||
nxt == TUI_FORM_BACK)
return nxt;
+ if (key) {
+ int h = key(ud, ch);
+ if (h == TUI_HOOK_BACK)
+ return TUI_FORM_BACK;
+ if (h == TUI_HOOK_REFRESH)
+ return TUI_FORM_REFRESH;
+ if (h == TUI_HOOK_SUBMIT)
+ return TUI_FORM_SUBMIT;
+ if (h != TUI_HOOK_NONE)
+ continue;
+ }
if (ch != '\n' && ch != '\r' && ch != KEY_ENTER)
continue;
if (!can_edit) {
@@ -1107,7 +1184,8 @@ int tui_form_run(const char *title, struct tui_form_field *f, int n,
snprintf(label, sizeof label, "%s: ", fl->label);
if (fl->kind == TUI_F_TEXT) {
if (fl->value &&
- tui_prompt_into(fl->value, fl->cap, label, fl->value, 0))
+ tui_prompt_into(fl->value, fl->cap, label, fl->value,
+ fl->mask))
return sel;
} else if (fl->kind == TUI_F_DATE) {
if (fl->value && tui_date_prompt_into(fl->value, fl->cap, label,
@@ -1153,6 +1231,32 @@ void tui_rt_init(struct tui_rt *t, int nrows, int maxrows, int ncols,
t->pos = (size_t)-1;
}
+void tui_rt_set_key(struct tui_rt *t, int (*key)(void *ud, int ch))
+{
+ t->key = key;
+}
+
+void tui_rt_set_footer(struct tui_rt *t,
+ void (*footer)(void *ud, char *buf, size_t n))
+{
+ t->footer = footer;
+}
+
+/* Actual column index of the ecol-th editable column. Cell callbacks always
+ receive the actual column index, also when INFO columns are interleaved. */
+int tui_rt_col_of(const struct tui_rt *t, int ecol)
+{
+ int ei = 0;
+ for (int i = 0; i < t->ncols; i++) {
+ if (t->cols[i].kind == TUI_RT_INFO)
+ continue;
+ if (ei == ecol)
+ return i;
+ ei++;
+ }
+ return 0;
+}
+
static void rt_get(struct tui_rt *t, int row, int col, char **buf, size_t *cap)
{
t->cell(t->ud, row, col, buf, cap);
@@ -1208,6 +1312,19 @@ int tui_rt_move(struct tui_rt *t, int dr, int dc)
return moved;
}
+/* Tab/Shift-Tab: next/previous editable cell, wrapping to the next row. */
+int tui_rt_tab(struct tui_rt *t, int dir)
+{
+ if (t->neditable <= 0)
+ return 0;
+ int moved = tui_rt_move(t, 0, dir);
+ if (dir > 0 && t->col == 0)
+ moved |= tui_rt_move(t, 1, 0);
+ else if (dir < 0 && t->col == t->neditable - 1)
+ moved |= tui_rt_move(t, -1, 0);
+ return moved;
+}
+
int tui_rt_clear(struct tui_rt *t)
{
if (t->row < 0 || t->row >= t->nrows)
@@ -1232,10 +1349,24 @@ int tui_rt_clear(struct tui_rt *t)
int tui_rt_run(const char *title, struct tui_rt *t, const char *hint)
{
int top = 0;
+ int prev_cursor = curs_set(1);
for (;;) {
if (g_frame)
g_frame(title);
- int view = LINES - 4 - t->y;
+ char foot[2048];
+ int nfoot = 0;
+ foot[0] = '\0';
+ if (t->footer) {
+ t->footer(t->ud, foot, sizeof foot);
+ for (char *p = foot; *p;) {
+ nfoot++;
+ char *nl = strchr(p, '\n');
+ if (!nl)
+ break;
+ p = nl + 1;
+ }
+ }
+ int view = LINES - 4 - t->y - nfoot;
if (view < 1)
view = 1;
if (t->row < top)
@@ -1300,24 +1431,66 @@ int tui_rt_run(const char *title, struct tui_rt *t, const char *hint)
}
}
}
+ if (nfoot > 0) {
+ int fy = t->y + (t->nrows - top < view ? t->nrows - top : view);
+ if (fy + nfoot > LINES - 2)
+ fy = LINES - 2 - nfoot;
+ if (fy < t->y)
+ fy = t->y;
+ char *p = foot;
+ for (int i = 0; i < nfoot && fy + i < LINES - 2; i++) {
+ char *nl = strchr(p, '\n');
+ if (nl)
+ *nl = '\0';
+ move(fy + i, 2);
+ clrtoeol();
+ attron(A_DIM);
+ addnstr(p, COLS - 4);
+ attroff(A_DIM);
+ p = nl ? nl + 1 : p + strlen(p);
+ }
+ }
if (g_hints)
g_hints(hint);
if (caret_y >= 0)
move(caret_y, caret_x);
refresh();
int ch = in_key();
- if (ch == 27 || ch == 'q')
+ if (ch == 27 || ch == 'q') {
+ curs_set(prev_cursor == ERR ? 0 : prev_cursor);
return -1;
- if (ch == KEY_F(5))
+ }
+ if (ch == KEY_F(5)) {
+ curs_set(prev_cursor == ERR ? 0 : prev_cursor);
return -2;
- if (ch == TUI_KEY_CTRL_ENTER || ch == KEY_F(9))
+ }
+ if (ch == TUI_KEY_CTRL_ENTER || ch == KEY_F(9)) {
+ curs_set(prev_cursor == ERR ? 0 : prev_cursor);
return -3;
+ }
+ if (t->key) {
+ int h = t->key(t->ud, ch);
+ if (h == TUI_HOOK_BACK) {
+ curs_set(prev_cursor == ERR ? 0 : prev_cursor);
+ return -1;
+ }
+ if (h == TUI_HOOK_REFRESH) {
+ curs_set(prev_cursor == ERR ? 0 : prev_cursor);
+ return -2;
+ }
+ if (h == TUI_HOOK_SUBMIT) {
+ curs_set(prev_cursor == ERR ? 0 : prev_cursor);
+ return -3;
+ }
+ if (h == TUI_HOOK_STAY)
+ continue;
+ }
if (ch == '\t') {
- tui_rt_move(t, 0, 1);
+ tui_rt_tab(t, 1);
continue;
}
if (ch == KEY_BTAB) {
- tui_rt_move(t, 0, -1);
+ tui_rt_tab(t, -1);
continue;
}
if (ch == KEY_UP) {
@@ -1332,12 +1505,13 @@ int tui_rt_run(const char *title, struct tui_rt *t, const char *hint)
tui_rt_clear(t);
continue;
}
+ int ci = tui_rt_col_of(t, t->col);
char *buf = NULL;
size_t cap = 0;
- rt_get(t, t->row, t->col, &buf, &cap);
+ rt_get(t, t->row, ci, &buf, &cap);
if (!buf)
continue;
- if (t->cols[t->col].kind == TUI_RT_DATE)
+ if (t->cols[ci].kind == TUI_RT_DATE)
tui_date_field_edit(buf, cap, &t->pos, ch, &t->fresh);
else
tui_field_edit(buf, cap, &t->pos, ch, &t->fresh);
diff --git a/clients/tui.h b/clients/tui.h
index d41d58c..39ea913 100644
--- a/clients/tui.h
+++ b/clients/tui.h
@@ -15,6 +15,13 @@
#define TUI_KEY_CTRL_X 0x18
#define TUI_KEY_CTRL_ENTER (KEY_MAX + 1)
+/* 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 */
+#define TUI_HOOK_BACK 2 /* handled: return "back" */
+#define TUI_HOOK_REFRESH 3 /* handled: return "refresh" */
+#define TUI_HOOK_SUBMIT 4 /* handled: return "submit" (forms only) */
+
/* The application supplies input, frame/hints drawing and a quit hook so
this module stays free of app state (and testable). */
void tui_set_input(int (*fn)(void));
@@ -92,6 +99,10 @@ int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new,
int tui_select_list(const char *title, char **items, int n, int start,
int allow_refresh, int *cursor, int allow_new,
const char *archive_action, int allow_toggle);
+int tui_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);
int tui_menu(const char *title, const char *const *items, int n,
int allow_new, int *cursor);
@@ -99,7 +110,12 @@ int tui_menu(const char *title, const char *const *items, int n,
void tui_set_status(const char *status, const char *right);
void tui_frame(const char *title);
void tui_hints(const char *s);
+void tui_redraw(void);
+#define TUI_PAGER_SAVE 0x1 /* 's' returns 2 (the save action) */
int tui_pager(const char *title, const char *text, const char *action_hint);
+int tui_pager_hook(const char *title, const char *text,
+ const char *extra_hint, unsigned flags,
+ int (*key)(void *ud, int ch), void *ud);
/* --- forms --- */
enum {
@@ -112,13 +128,14 @@ enum {
#define TUI_FORM_BACK (-1)
#define TUI_FORM_REFRESH (-2)
-#define TUI_FORM_SUBMIT (-3)
+#define TUI_FORM_SUBMIT (-8)
struct tui_form_field {
const char *label;
- char *value; /* text storage for TEXT/DATE (also CHOICE labels) */
+ char *value; /* text storage for TEXT/DATE (also CHOICE labels/ACTION) */
size_t cap;
int kind;
+ int mask; /* TEXT: '*' instead of the content */
int64_t *ore; /* TUI_F_AMOUNT */
const char *const *choices; /* TUI_F_CHOICE */
int nchoices;
@@ -128,6 +145,8 @@ struct tui_form_field {
int tui_form_next(int sel, int n, int ch);
int tui_form_run(const char *title, struct tui_form_field *f, int n,
int can_edit);
+int tui_form_run_hook(const char *title, struct tui_form_field *f, int n,
+ int can_edit, int (*key)(void *ud, int ch), void *ud);
/* --- row table --- */
enum { TUI_RT_EDIT = 0, TUI_RT_INFO, TUI_RT_DATE };
@@ -149,16 +168,24 @@ struct tui_rt {
void (*cell)(void *, int, int, char **, size_t *);
void (*info)(void *, int, char *, size_t);
void *ud;
+ int (*key)(void *, int);
+ /* Writes the dim status lines shown under the table ('\n' separated). */
+ void (*footer)(void *, char *, size_t);
};
void tui_rt_init(struct tui_rt *t, int nrows, int maxrows, int ncols,
const struct tui_rt_col *cols, int y,
void (*cell)(void *, int, int, char **, size_t *),
void (*info)(void *, int, char *, size_t), void *ud);
+void tui_rt_set_key(struct tui_rt *t, int (*key)(void *ud, int ch));
+void tui_rt_set_footer(struct tui_rt *t,
+ void (*footer)(void *ud, char *buf, size_t n));
int tui_rt_blank_row(struct tui_rt *t, int row);
int tui_rt_normalize(struct tui_rt *t);
int tui_rt_move(struct tui_rt *t, int dr, int dc);
+int tui_rt_tab(struct tui_rt *t, int dir);
int tui_rt_clear(struct tui_rt *t);
+int tui_rt_col_of(const struct tui_rt *t, int ecol);
int tui_rt_run(const char *title, struct tui_rt *t, const char *hint);
#endif
diff --git a/docs/TUI-GUIDELINES.md b/docs/TUI-GUIDELINES.md
index 63c492f..276cfd9 100644
--- a/docs/TUI-GUIDELINES.md
+++ b/docs/TUI-GUIDELINES.md
@@ -72,11 +72,24 @@ written compactly as `^N`, `^A`, `^C`, `^R` to save width.
- Derived values (account names, balances) are dim and non-editable.
- Row tables: always exactly one empty trailing row; entering data appends a
new empty row; an empty row followed by another empty row collapses.
- `Ctrl+X` clears the selected row.
+ `Ctrl+X` clears the selected row. `Tab` walks the editable cells and wraps
+ to the next row's first cell (`Shift-Tab` back). A dim footer callback may
+ show derived lines (balance, attachment list) directly under the table.
+- Fields with `mask` set are shown as `*` (`tui_form_field.mask`); used for
+ passwords. ACTION fields show their `value` when set, else `(lista)`.
+- Header fields plus rows: the header is a `tui_form_run`, the rows a
+ `tui_rt_run`. Editing a header field hands over to the row table; `Esc` in
+ the table goes back to the header. `F5`/`Ctrl+Enter` work in both stages.
- Validation: `F5` validates without writing and reports exactly what is wrong
(field, row number). Server `dry_run` is used where available.
- Saving: `Ctrl+Enter` writes, shows a confirmation message, and returns to
the previous view. `Esc` cancels without saving.
+- Screen-specific keys (template picker, attach, link, download) are handled
+ in a key hook, never with a local loop. The hook runs for keys the widget
+ itself does not handle and returns `TUI_HOOK_*`: `STAY` (consumed, redraw),
+ `BACK`, `REFRESH` or `SUBMIT` (the widget returns as on `Esc`/`F5`/
+ `Ctrl+Enter`). Hooks exist on `tui_form_run_hook`, `tui_rt_set_key`,
+ `tui_pager_hook` and `tui_select_list_hook`.
- File browser (`file_browser`): starts in the org's `attachment_dir` (or
`$HOME`; a leading `~` is expanded to `$HOME`), `.. (uppåt)` is the first row, directories sort first with a
trailing `/`, hidden files are skipped. Enter enters a directory or picks a
@@ -118,7 +131,13 @@ only place that touches ncurses. Rules:
is separated from drawing so it can be tested without a terminal.
- Widgets return key codes (`-1` back, `-2` refresh, `-4` new, `-6` remove,
`-7` toggle) and write text into the caller's buffer; nothing returns
- static storage.
+ static storage. `TUI_NAV_NONE`, `TUI_FORM_BACK/REFRESH/SUBMIT` and the
+ `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.
+- `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
`tui_set_input`/`tui_set_screen`/`tui_set_quit`.
diff --git a/tests/test_tui.c b/tests/test_tui.c
index b2462e8..98a1ac2 100644
--- a/tests/test_tui.c
+++ b/tests/test_tui.c
@@ -154,6 +154,11 @@ static void test_nav(void)
nav_init(&v, 0);
CHECK(tui_nav_key(&v, KEY_DOWN, 0, 0, 0, 0) == TUI_NAV_NONE);
+ CHECK(tui_nav_key(&v, KEY_F(5), 0, 0, 0, 0) == -2);
+ CHECK(tui_nav_key(&v, TUI_KEY_CTRL_N, 1, 0, 0, 0) == -4);
+ CHECK(tui_nav_key(&v, TUI_KEY_CTRL_N, 0, 0, 0, 0) == TUI_NAV_NONE);
+ CHECK(tui_nav_key(&v, 'q', 0, 0, 0, 0) == -1);
+ CHECK(tui_nav_key(&v, 27, 0, 0, 0, 0) == -1);
nav_init(&v, 5);
CHECK(tui_nav_key(&v, KEY_UP, 0, 0, 0, 0) == TUI_NAV_NONE && v.sel == 0);
@@ -196,6 +201,8 @@ static void test_nav(void)
static void test_form_nav(void)
{
+ CHECK(TUI_NAV_NONE != TUI_FORM_SUBMIT);
+ CHECK(tui_form_next(1, 3, 'x') == TUI_NAV_NONE);
CHECK(tui_form_next(0, 3, KEY_UP) == TUI_NAV_NONE);
CHECK(tui_form_next(0, 3, KEY_DOWN) == 1);
CHECK(tui_form_next(2, 3, KEY_DOWN) == TUI_NAV_NONE);
@@ -226,19 +233,56 @@ static const struct tui_rt_col RT_COLS[3] = {
{ "B", 22, 8, TUI_RT_EDIT },
};
+static int rt_key_cb(void *ud, int ch)
+{
+ (void)ud;
+ (void)ch;
+ return TUI_HOOK_NONE;
+}
+static void rt_footer_cb(void *ud, char *buf, size_t n)
+{
+ (void)ud;
+ snprintf(buf, n, "footer");
+}
+
static void test_rowtable(void)
{
struct tui_rt t;
memset(rt_cells, 0, sizeof rt_cells);
tui_rt_init(&t, 1, 8, 3, RT_COLS, 6, rt_cb, rt_info_cb, NULL);
CHECK(t.nrows == 1 && t.neditable == 2 && t.row == 0 && t.col == 0);
+ CHECK(t.key == NULL && t.footer == NULL);
+ tui_rt_set_key(&t, rt_key_cb);
+ tui_rt_set_footer(&t, rt_footer_cb);
+ CHECK(t.key == rt_key_cb && t.footer == rt_footer_cb);
+ /* editable index -> actual column index (INFO columns are skipped) */
+ CHECK(tui_rt_col_of(&t, 0) == 0);
+ CHECK(tui_rt_col_of(&t, 1) == 2);
CHECK(tui_rt_blank_row(&t, 0));
CHECK(tui_rt_move(&t, 0, 1) && t.col == 1);
CHECK(tui_rt_move(&t, 0, 1) && t.col == 0);
CHECK(tui_rt_move(&t, -1, 0) == 0);
CHECK(tui_rt_move(&t, 1, 0) == 0);
+ /* Tab walks the row and wraps to the next row */
+ t.nrows = 3;
+ t.row = 0;
+ t.col = 0;
+ CHECK(tui_rt_tab(&t, 1) && t.row == 0 && t.col == 1);
+ CHECK(tui_rt_tab(&t, 1) && t.row == 1 && t.col == 0);
+ CHECK(tui_rt_tab(&t, -1) && t.row == 0 && t.col == 1);
+ CHECK(tui_rt_tab(&t, -1) && t.row == 0 && t.col == 0);
+ /* with a single editable column every Tab is a row move */
+ struct tui_rt one;
+ tui_rt_init(&one, 3, 8, 1, RT_COLS, 6, rt_cb, rt_info_cb, NULL);
+ CHECK(one.neditable == 1);
+ CHECK(tui_rt_tab(&one, 1) && one.row == 1 && one.col == 0);
+ CHECK(tui_rt_tab(&one, -1) && one.row == 0 && one.col == 0);
+
/* data in the blank row appends a new blank row */
+ tui_rt_normalize(&t);
+ t.row = 0;
+ t.col = 0;
snprintf(rt_cells[0][0], 64, "1930");
CHECK(tui_rt_normalize(&t) == 2);
snprintf(rt_cells[1][0], 64, "x");