diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-23 10:58:17 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-23 10:58:17 +0200 |
| commit | 71a702f375750829c634b552217c9925d549828b (patch) | |
| tree | 2b963da1d0110ed43920722ba4193ee9c79f2521 /clients | |
| parent | 8ccb6d58d8e2f1c65dfd7ab5f0eb7d93f1f19def (diff) | |
| download | bokf-71a702f375750829c634b552217c9925d549828b.tar.gz bokf-71a702f375750829c634b552217c9925d549828b.zip | |
tui: context menu on → / ^O; no F-keys, ^N or ^Enter
→ (where no caret uses it) and ^O (everywhere, also in table cells)
open "Åtgärder": a box at the right edge with every action of the view
and its key. Screens declare their actions with tui_set_actions();
choosing one feeds its key to the widget, so the menu and accelerators
share one path. Function keys, Ctrl+N (now n), Ctrl+Enter and Ctrl+F are
gone and no extended keyboard protocol is enabled, so the TUI works in a
browser terminal; save/post/validate/preview/template/attach are menu
actions. make check rejects F-keys, ^N and ^Enter in clients/.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'clients')
| -rw-r--r-- | clients/bokftui.c | 12 | ||||
| -rw-r--r-- | clients/screens_attachments.c | 5 | ||||
| -rw-r--r-- | clients/screens_bank.c | 9 | ||||
| -rw-r--r-- | clients/screens_bokslut.c | 13 | ||||
| -rw-r--r-- | clients/screens_dashboard.c | 9 | ||||
| -rw-r--r-- | clients/screens_ib.c | 19 | ||||
| -rw-r--r-- | clients/screens_invoices.c | 125 | ||||
| -rw-r--r-- | clients/screens_payroll.c | 27 | ||||
| -rw-r--r-- | clients/screens_rules.c | 10 | ||||
| -rw-r--r-- | clients/screens_settings.c | 6 | ||||
| -rw-r--r-- | clients/screens_templates.c | 15 | ||||
| -rw-r--r-- | clients/screens_vouchers.c | 49 | ||||
| -rw-r--r-- | clients/tui.c | 522 | ||||
| -rw-r--r-- | clients/tui.h | 39 | ||||
| -rw-r--r-- | clients/ui.h | 4 |
15 files changed, 616 insertions, 248 deletions
diff --git a/clients/bokftui.c b/clients/bokftui.c index 19a63cf..46b94cd 100644 --- a/clients/bokftui.c +++ b/clients/bokftui.c @@ -244,6 +244,10 @@ static int login_screen(struct app *a) ff[3].kind = TUI_F_ACTION; for (;;) { + static const struct tui_action acts[] = { + { "login", "Logga in", TUI_KEY_SUBMIT, 1, NULL }, + }; + tui_set_actions(acts, 1); int r = tui_form_run("bokf — inloggning", ff, 4, 1, &sel); if (r == TUI_FORM_BACK) { if (g_quit) { @@ -354,13 +358,6 @@ static void usage(FILE *f) " --version\n"); } -static void reset_modify_keys(void) -{ - putp("\033[<u"); - putp("\033[>4m"); - fflush(stdout); -} - int main(int argc, char **argv) { struct app app; @@ -419,7 +416,6 @@ int main(int argc, char **argv) tui_set_input(ui_getch); tui_set_screen(tui_frame, tui_hints); tui_set_quit(request_quit); - atexit(reset_modify_keys); curs_set(1); const char *env_session = getenv("BOKFD_SESSION"); diff --git a/clients/screens_attachments.c b/clients/screens_attachments.c index c72ee67..b2e9ad8 100644 --- a/clients/screens_attachments.c +++ b/clients/screens_attachments.c @@ -139,6 +139,11 @@ void inbox_screen(struct app *a) free(fn); } struct inboxctx ctx = { a, ids, n, cursor }; + static const struct tui_action iacts[] = { + { "inbox.upload", "Ladda upp fil…", 'a', 1, NULL }, + { "inbox.link", "Koppla till verifikat…", 'k', 1, NULL }, + }; + tui_set_actions(iacts, 2); int sel = tui_select_list_hook("Underlag (inkorg)", items, (int)n, cursor, 1, &cursor, 1, NULL, 0, inbox_key, &ctx); diff --git a/clients/screens_bank.c b/clients/screens_bank.c index 5131055..c5b101e 100644 --- a/clients/screens_bank.c +++ b/clients/screens_bank.c @@ -163,7 +163,7 @@ static void bank_match_tx(struct app *a, const struct bank_tx *t) items[i] = xstrdup(line); } items[t->nsuggestions] = xstrdup("Välj annat verifikat…"); - items[t->nsuggestions + 1] = xstrdup("Skapa nytt verifikat… (^N)"); + items[t->nsuggestions + 1] = xstrdup("Skapa nytt verifikat… (n)"); int sel = tui_select_list("Matcha transaktion", items, (int)nact, 0, 0, NULL, 0, NULL, 0); for (size_t i = 0; i < nact; i++) @@ -395,6 +395,13 @@ void bank_screen(struct app *a) } } struct bankctx ctx = { a, txs, n, start }; + static const struct tui_action bacts[] = { + { "bank.new_voucher", "Nytt verifikat från transaktionen", 'n', 1, + NULL }, + { "bank.unmatch", "Ta bort matchning", 'u', 1, NULL }, + { "bank.import", "Importera bankfil…", 'a', 1, NULL }, + }; + tui_set_actions(bacts, 3); int sel = tui_select_list_hook(title, lines, (int)rows, start, 1, &ctx.cursor, 1, NULL, 0, bank_key, &ctx); diff --git a/clients/screens_bokslut.c b/clients/screens_bokslut.c index a4776c9..2328c01 100644 --- a/clients/screens_bokslut.c +++ b/clients/screens_bokslut.c @@ -241,9 +241,16 @@ void bokslut_screen(struct app *a) ff[7].value = rate; ff[7].cap = sizeof rate; ff[7].kind = TUI_F_TEXT; - tui_form_hint("upp/ned/Tab = flytta Enter = ändra/utför F5 = visa" - " bokslutsplan F9 = bokför planen (frågar" - " först) Esc/q = tillbaka ^C = avsluta"); + static const struct tui_action bacts[] = { + { "bokslut.plan", "Visa bokslutsplan (torrkörning)", + TUI_KEY_REFRESH, 1, NULL }, + { "bokslut.post", "Bokför planen… (frågar först)", + TUI_KEY_SUBMIT, 1, NULL }, + }; + tui_set_actions(bacts, can_edit ? 2 : 1); + tui_form_hint("upp/ned/Tab = flytta Enter = ändra/utför" + " → = åtgärder (bokslutsplan, bokför)" + " Esc/q = tillbaka"); int r = tui_form_run_actions("Bokslut", ff, nf + 2, can_edit, acts, 5, NULL, &sel); if (r >= 0 && r < nf) { diff --git a/clients/screens_dashboard.c b/clients/screens_dashboard.c index 45f0aed..d34e960 100644 --- a/clients/screens_dashboard.c +++ b/clients/screens_dashboard.c @@ -90,6 +90,11 @@ static int64_t fy_new_form(struct app *a) ff[2].value = end; ff[2].cap = sizeof end; ff[2].kind = TUI_F_DATE; + static const struct tui_action acts[] = { + { "fy.create", "Skapa räkenskapsåret", TUI_KEY_SUBMIT, 1, NULL }, + { "fy.validate", "Validera", TUI_KEY_REFRESH, 1, NULL }, + }; + tui_set_actions(acts, 2); int r = tui_form_run("Nytt räkenskapsår", ff, 3, 1, &sel); if (r == TUI_FORM_BACK) return 0; @@ -364,6 +369,10 @@ void dashboard(struct app *a) for (;;) { if (g_reload) return; + static const struct tui_action acts[] = { + { "voucher.new", "Nytt verifikat", 'n', 1, NULL }, + }; + tui_set_actions(acts, 1); int sel = tui_menu("Bokf", MAIN_ITEMS, (int)(sizeof MAIN_ITEMS / sizeof MAIN_ITEMS[0]), 1, &last_sel); diff --git a/clients/screens_ib.c b/clients/screens_ib.c index 2d62297..34cac20 100644 --- a/clients/screens_ib.c +++ b/clients/screens_ib.c @@ -270,9 +270,14 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) tui_rt_set_footer(&ib.rt, ib_footer); for (;;) { + static const struct tui_action acts[] = { + { "ib.save", "Spara", TUI_KEY_SUBMIT, 1, NULL }, + { "ib.validate", "Validera", TUI_KEY_REFRESH, 1, NULL }, + }; + tui_set_actions(acts, 2); int rr = tui_rt_run("Ingående balans", &ib.rt, - "Tab = byta fält F5 = validera ^X = rensa rad" - " F9 = spara Esc = avbryt"); + "Tab = byta fält ^O = åtgärder (spara," + " validera) ^X = rensa rad Esc = avbryt"); if (rr == -1) return 0; char msg[256]; @@ -301,7 +306,7 @@ struct ibpager { static int ib_view_key(void *ud, int ch) { struct ibpager *p = ud; - if (ch == 'e' || ch == 'E' || ch == TUI_KEY_CTRL_N) { + if (ch == 'e' || ch == 'E') { if (ib_form(p->a, p->accs, p->amts, p->n)) return TUI_HOOK_REFRESH; return TUI_HOOK_STAY; @@ -310,7 +315,7 @@ static int ib_view_key(void *ud, int ch) } /* 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. */ + columns and the sum as plain text, and the e key runs the editor. */ void ib_screen(struct app *a) { for (;;) { @@ -341,8 +346,12 @@ void ib_screen(struct app *a) buf_line(&t, "\nSumma: %s\n", sumbuf); buf_append(&t, "\0", 1); struct ibpager p = { a, accs, amts, n }; + static const struct tui_action acts[] = { + { "ib.edit", "Redigera", 'e', 1, NULL }, + }; + tui_set_actions(acts, 1); int ret = tui_pager_hook("Ingående balans", (const char *)t.p, - "e/^N = redigera", 0, ib_view_key, &p); + "e = redigera", 0, ib_view_key, &p); buf_free(&t); for (int i = 0; i < n; i++) free(accs[i]); diff --git a/clients/screens_invoices.c b/clients/screens_invoices.c index 712a8a8..46d2406 100644 --- a/clients/screens_invoices.c +++ b/clients/screens_invoices.c @@ -240,6 +240,7 @@ struct cform_ctx { int64_t id; const char *draft_id; int deleted; + struct tui_action acts[3]; char name[256]; char address[512]; char postal[32]; @@ -302,28 +303,23 @@ static void customer_load_json(const char *json, struct cform_ctx *c) yyjson_doc_free(doc); } +#define CFORM_DRAFT_DELETE TUI_KEY_ACTION(0) + static int cform_key(void *ud, int ch) { struct cform_ctx *c = ud; - if (ch != KEY_F(2)) + if (ch == TUI_KEY_MENU) { + c->acts[2].enabled = + drafts_have(kdrafts(), c->a->org, "customer", c->draft_id); return TUI_HOOK_NONE; - int have = drafts_have(kdrafts(), c->a->org, "customer", c->draft_id); - struct tui_action acts[] = { - { "customer.save", "Spara", 0, 1, NULL }, - { "customer.draft.delete", "Radera utkast", 0, have, "inget utkast" }, - }; - static int afocus; - int r = tui_action_menu("Åtgärder", acts, 2, &afocus); - if (r == 0) - return TUI_HOOK_SUBMIT; - if (r == 1) { - if (tui_confirm("Kund", "Radera utkastet?")) { - drafts_del(kdrafts(), c->a->org, "customer", c->draft_id); - c->deleted = 1; - return TUI_HOOK_BACK; - } - return TUI_HOOK_STAY; + } + if (ch != CFORM_DRAFT_DELETE) + return TUI_HOOK_NONE; + if (tui_confirm("Kund", "Radera utkastet?")) { + drafts_del(kdrafts(), c->a->org, "customer", c->draft_id); + c->deleted = 1; + return TUI_HOOK_BACK; } return TUI_HOOK_STAY; } @@ -404,13 +400,19 @@ static void customer_form(struct app *a, int64_t id, const char *draft_id) ff[9].kind = TUI_F_ACTION; static int sel = 0; - tui_form_hint_extra("F2 = åtgärder"); + c.acts[0] = (struct tui_action){ "customer.save", "Spara", + TUI_KEY_SUBMIT, 1, NULL }; + c.acts[1] = (struct tui_action){ "customer.validate", "Validera", + TUI_KEY_REFRESH, 1, NULL }; + c.acts[2] = (struct tui_action){ "customer.draft.delete", "Radera utkast", + CFORM_DRAFT_DELETE, 0, "inget utkast" }; for (;;) { char title[64]; snprintf(title, sizeof title, "%s%s", id > 0 ? "Kund" : "Ny kund", drafts_have(kdrafts(), a->org, "customer", draft_id) ? " <UTKAST>" : ""); + tui_set_actions(c.acts, 3); int r = tui_form_run_hook(title, ff, 10, 1, cform_key, &c, &sel); if (c.deleted) break; @@ -474,6 +476,7 @@ struct clist { unsigned char *active; size_t nserver; size_t nrows; /* server rows + temp drafts; the add row is at nrows */ + struct tui_action act; /* 'd' for the current row (see clist_key) */ }; static void clist_clear(struct clist *l) @@ -547,45 +550,27 @@ static void clist_collect_temp(void *ud, const char *id, const char *fields) l->nrows++; } +/* The row action behind 'd' depends on the row: a draft is deleted, a + saved customer archived or reactivated. The menu shows the right one. */ static int clist_key(void *ud, int ch) { struct clist *l = ud; - static int afocus; - if (ch != KEY_F(2) || !l->cur) + if (ch != TUI_KEY_MENU || !l->cur) return TUI_HOOK_NONE; int i = *l->cur; - if (i < 0 || (size_t)i >= l->nrows) - return TUI_HOOK_STAY; - struct tui_action acts[] = { - { "customer.open", "Öppna", 0, 1, NULL }, - { "customer.draft.delete", "Radera utkast", 0, l->dkeys[i] != NULL, - "inget utkast" }, - { "customer.archive", l->active[i] ? "Arkivera" : "Återaktivera", 0, - l->ids[i] != 0, "gäller inte utkast" }, - }; - int r = tui_action_menu("Åtgärder", acts, 3, &afocus); - if (r == 0) { - char idbuf[32]; - const char *dk = l->dkeys[i]; - if (!dk) { - snprintf(idbuf, sizeof idbuf, "%lld", (long long)l->ids[i]); - dk = idbuf; - } - customer_form(l->a, l->ids[i], dk); - return TUI_HOOK_REFRESH; + struct tui_action *a = &l->act; + *a = (struct tui_action){ "customer.remove", "Arkivera", 'd', 1, NULL }; + if (i < 0 || (size_t)i >= l->nrows) { + a->enabled = 0; + a->reason = "välj en kund"; + } else if (l->dkeys[i] && l->ids[i] == 0) { + a->label = "Radera utkast"; + a->id = "customer.draft.delete"; + } else if (!l->active[i]) { + a->label = "Återaktivera"; } - if (r == 1) { - if (tui_confirm("Kund", "Radera utkastet?")) - drafts_del(kdrafts(), l->a->org, "customer", l->dkeys[i]); - return TUI_HOOK_REFRESH; - } - if (r == 2) { - customer_archive_toggle(l->a, l->ids[i], l->names[i], - l->active[i] != 0); - return TUI_HOOK_REFRESH; - } - return TUI_HOOK_STAY; + return TUI_HOOK_NONE; } void customers_screen(struct app *a) @@ -665,7 +650,7 @@ void customers_screen(struct app *a) free(resp); drafts_foreach(kdrafts(), a->org, "customer", clist_collect_temp, &l); - l.items[l.nrows] = xstrdup("+ Ny kund (^N)"); + l.items[l.nrows] = xstrdup("+ Ny kund (n)"); l.names[l.nrows] = xstrdup(""); l.ids[l.nrows] = 0; l.active[l.nrows] = 1; @@ -679,12 +664,13 @@ void customers_screen(struct app *a) } } cur = start; - tui_list_hint_extra("F2 = åtgärder"); + l.act = (struct tui_action){ "customer.remove", "Arkivera", 'd', 1, + NULL }; + tui_set_actions(&l.act, 1); int sel = tui_select_list_hook("Kunder", l.items, (int)l.nrows + 1, start, 1, &cur, 1, "arkivera/återaktivera", 0, clist_key, &l); - tui_list_hint_extra(NULL); if (cur >= 0 && (size_t)cur < l.nserver) a->customer_sel = l.ids[cur]; if (sel == -1) { @@ -697,7 +683,8 @@ void customers_screen(struct app *a) } if (sel == -6 && cur >= 0 && (size_t)cur < l.nrows) { if (l.ids[cur] == 0) { - tui_message("Kund", "Utkast raderas med F2."); + if (l.dkeys[cur] && tui_confirm("Kund", "Radera utkastet?")) + drafts_del(kdrafts(), a->org, "customer", l.dkeys[cur]); } else { customer_archive_toggle(a, l.ids[cur], l.names[cur], l.active[cur] != 0); @@ -1020,6 +1007,15 @@ static void invoices_detail(struct app *a, int64_t id) snprintf(ahint, sizeof ahint, "p = visa PDF s = skicka u = duplicera%s", ctx.paid_date[0] ? "" : " b = kvittera betalning"); + struct tui_action acts[] = { + { "invoice.pdf", "Visa PDF", 'p', 1, NULL }, + { "invoice.send", "Skicka…", 's', 1, NULL }, + { "invoice.duplicate", "Duplicera till nytt utkast", 'u', 1, + NULL }, + { "invoice.pay", "Kvittera betalning…", 'b', + ctx.paid_date[0] ? 0 : 1, "redan betald" }, + }; + tui_set_actions(acts, 4); int ret = tui_pager_hook("Faktura", (const char *)t.p, ahint, 0, inv_key, &ctx); free(cust); @@ -1457,10 +1453,15 @@ static int64_t invoices_form_run(struct iform *f) tui_rt_set_fields(&f->rt, ff, 7); tui_rt_normalize(&f->rt); const char *hint = - "Enter = välj/ändra Tab = byta fält F5 = förhandsvisa" - " F9 = utfärda Esc = avbryt"; + "Enter = välj/ändra Tab = byta fält" + " ^O = åtgärder (utfärda, förhandsvisa) Esc = avbryt"; + static const struct tui_action acts[] = { + { "invoice.issue", "Utfärda", TUI_KEY_SUBMIT, 1, NULL }, + { "invoice.preview", "Förhandsvisa PDF", TUI_KEY_REFRESH, 1, NULL }, + }; int64_t out = 0; for (;;) { + tui_set_actions(acts, 2); int rr = tui_rt_run("Ny faktura", &f->rt, hint); if (rr == -1) break; @@ -1600,10 +1601,12 @@ static int64_t invoices_new_from(struct app *a, int64_t id) return out; } +#define INVOICES_NEXT_NUMBER TUI_KEY_ACTION(0) + static int invoices_key(void *ud, int ch) { struct app *a = ud; - if (ch != 'n') + if (ch != INVOICES_NEXT_NUMBER) return TUI_HOOK_NONE; char *resp = client_rpc(&a->conn, "invoice.sequence_get", a->session, a->org, "{}"); @@ -1696,7 +1699,7 @@ void invoices_screen(struct app *a) free(paid); } free(resp); - items[n] = xstrdup("+ Ny faktura (^N)"); + items[n] = xstrdup("+ Ny faktura (n)"); } int start = 0; if (a->invoice_sel) { @@ -1707,6 +1710,12 @@ void invoices_screen(struct app *a) } } int cur = start; + static const struct tui_action lacts[] = { + { "invoice.new", "Ny faktura", 'n', 1, NULL }, + { "invoice.next_number", "Ändra nästa fakturanummer…", + INVOICES_NEXT_NUMBER, 1, NULL }, + }; + tui_set_actions(lacts, 2); int sel = tui_select_list_hook("Fakturor", items, (int)n + 1, start, 1, &cur, 1, NULL, 0, invoices_key, a); if (cur >= 0 && (size_t)cur < n) diff --git a/clients/screens_payroll.c b/clients/screens_payroll.c index 8a27ec8..ecec962 100644 --- a/clients/screens_payroll.c +++ b/clients/screens_payroll.c @@ -291,6 +291,11 @@ static void employee_form(struct app *a, int64_t id) ff[i].kind = TUI_F_TEXT; i++; const char *title = f.is_new ? "Ny anställd" : "Anställd"; + static const struct tui_action eacts[] = { + { "employee.save", "Spara", TUI_KEY_SUBMIT, 1, NULL }, + { "employee.validate", "Validera", TUI_KEY_REFRESH, 1, NULL }, + }; + tui_set_actions(eacts, 2); int r = tui_form_run(title, ff, i, 1, &sel); if (r == TUI_FORM_BACK) return; @@ -404,7 +409,7 @@ void employees_screen(struct app *a) start = i; break; } - lines[n] = xstrdup("+ Ny anställd (^N)"); + lines[n] = xstrdup("+ Ny anställd (n)"); int cur = (int)start; int sel = tui_select_list("Anställda", lines, (int)n + 1, cur, 1, &cur, 1, "arkivera/återaktivera", 0); @@ -597,8 +602,8 @@ void payroll_tax_tables_screen(struct app *a) acts[2].disabled_reason = reason; int r = tui_form_run_actions( "Skattetabeller", ff, 4, 0, acts, 3, - "upp/ned/Tab = flytta Enter = utför F5 = uppdatera" - " Esc/q = tillbaka ^C = avsluta", + "upp/ned/Tab = flytta Enter = utför → = åtgärder" + " Esc/q = tillbaka", &sel); if (r == TUI_FORM_ACTION + 1) tax_tables_fetch(a, current_year); @@ -1100,9 +1105,8 @@ static int64_t payroll_run_screen(struct app *a, int64_t id) struct tui_form_action acts[7] = { { status, -1, NULL }, { totals, -1, NULL }, - { "Förhandsvisa (F5)", 1, NULL }, - { "Bokför körning (F9)", post_reason ? 0 : 1, - post_reason }, + { "Förhandsvisa", 1, NULL }, + { "Bokför körning", post_reason ? 0 : 1, post_reason }, { "Lönebesked (PDF)", payslip_reason ? 0 : 1, payslip_reason }, { "AGI-underlag", agi_reason ? 0 : 1, agi_reason }, { "Betala skatt & avgifter", pay_reason ? 0 : 1, pay_reason }, @@ -1123,9 +1127,14 @@ static int64_t payroll_run_screen(struct app *a, int64_t id) else snprintf(title, sizeof title, "Ny lönekörning"); int editable = can_write && !posted; + struct tui_action macts[] = { + { "payroll.preview", "Förhandsvisa", TUI_KEY_REFRESH, 1, NULL }, + { "payroll.post", "Bokför körning…", TUI_KEY_SUBMIT, + post_reason ? 0 : 1, post_reason }, + }; + tui_set_actions(macts, 2); tui_form_hint("upp/ned/Tab = flytta Enter = ändra/utför" - " F5 = förhandsvisa F9 = bokför" - " Esc/q = tillbaka ^C = avsluta"); + " → = åtgärder Esc/q = tillbaka"); int ret = tui_form_run_actions(title, ff, 2, editable, acts, 7, NULL, &focus); if (ret == TUI_FORM_BACK) @@ -1207,7 +1216,7 @@ void payroll_screen(struct app *a) start = i; break; } - lines[n] = xstrdup("+ Ny lönekörning (^N)"); + lines[n] = xstrdup("+ Ny lönekörning (n)"); char title[160]; if (stale) snprintf(title, sizeof title, diff --git a/clients/screens_rules.c b/clients/screens_rules.c index 19f898d..4109f80 100644 --- a/clients/screens_rules.c +++ b/clients/screens_rules.c @@ -39,7 +39,8 @@ struct rule { int64_t sort_order; }; -/* F5 dry-runs; Ctrl+Enter creates/updates. Returns 1 when saved. */ +/* dry=1 validates only (the "Validera" action); otherwise creates/updates. + Returns 1 when saved. */ static int rule_save(struct app *a, int64_t id, const char *box, int mti, const char *pattern, int signi, const char *sort, int dry) { @@ -120,7 +121,12 @@ static int rule_form(struct app *a, const struct rule *r) ff[4].cap = sizeof sort; ff[4].kind = TUI_F_TEXT; const char *title = r ? "Redigera momsregel" : "Ny momsregel"; + static const struct tui_action acts[] = { + { "rule.save", "Spara", TUI_KEY_SUBMIT, 1, NULL }, + { "rule.validate", "Validera", TUI_KEY_REFRESH, 1, NULL }, + }; for (;;) { + tui_set_actions(acts, 2); int rr = tui_form_run(title, ff, 5, 1, &sel); if (rr == TUI_FORM_BACK) return 0; @@ -202,7 +208,7 @@ void rules_screen(struct app *a) (long long)rules[i].sort_order); items[i] = xstrdup(line); } - items[n] = xstrdup("+ Ny regel (^N)"); + items[n] = xstrdup("+ Ny regel (n)"); int s = tui_select_list("Momsregler", items, (int)n + 1, cursor, 1, &cursor, 1, n > 0 ? "ta bort" : NULL, 0); for (size_t i = 0; i <= n; i++) diff --git a/clients/screens_settings.c b/clients/screens_settings.c index 46ba201..f47a51b 100644 --- a/clients/screens_settings.c +++ b/clients/screens_settings.c @@ -95,7 +95,7 @@ static void board_screen(struct app *a) lines[i] = xstrdup(line); } if (owner) - lines[n] = xstrdup("+ Ny ledamot (^N)"); + lines[n] = xstrdup("+ Ny ledamot (n)"); int s = tui_select_list("Styrelseledamöter", lines, rows, cursor, 1, &cursor, owner ? 1 : 0, owner && n > 0 ? "ta bort" : NULL, 0); @@ -296,8 +296,8 @@ static void settings_form(struct app *a, const char *title, (long long)jint_val(seq, "result.next_number", 1)); free(seq); } - tui_form_hint("upp/ned/Home/End Enter = ändra F5 = uppdatera" - " Esc/q = tillbaka ^C = avsluta"); + tui_form_hint("upp/ned = flytta Enter = ändra → = åtgärder" + " Esc/q = tillbaka"); int r = tui_form_run_actions(title, ff, n, can_edit, acts, show_password_status ? 1 : 0, NULL, focus); diff --git a/clients/screens_templates.c b/clients/screens_templates.c index e55028d..430a126 100644 --- a/clients/screens_templates.c +++ b/clients/screens_templates.c @@ -68,7 +68,8 @@ static void tpl_footer(void *ud, char *buf, size_t n) " kredit."); } -/* F5 dry-runs; Ctrl+Enter creates/updates. Returns 1 when saved. */ +/* dry=1 validates only (the "Validera" action); otherwise creates/updates. + Returns 1 when saved. */ static int tpl_save(struct tplform *tf, int dry) { const char *problem = NULL; @@ -233,9 +234,15 @@ static int template_form(struct app *a, const char *load_name) ff[2].kind = TUI_F_TEXT; tui_rt_set_fields(&tf.rt, ff, 3); const char *title = load_name ? "Redigera mall" : "Ny mall"; - const char *hint = "Enter = ändra fält Tab = byta fält F5 = validera" - " ^X = rensa rad F9 = spara Esc = avbryt"; + const char *hint = "Tab = byta fält Enter = ändra fält" + " ^O = åtgärder (spara, validera) ^X = rensa rad" + " Esc = avbryt"; + static const struct tui_action acts[] = { + { "template.save", "Spara", TUI_KEY_SUBMIT, 1, NULL }, + { "template.validate", "Validera", TUI_KEY_REFRESH, 1, NULL }, + }; for (;;) { + tui_set_actions(acts, 2); int rr = tui_rt_run(title, &tf.rt, hint); if (rr == -1) return 0; @@ -309,7 +316,7 @@ void templates_screen(struct app *a) free(ds); } names[n] = xstrdup(""); - lines[n] = xstrdup("+ Ny mall (^N)"); + lines[n] = xstrdup("+ Ny mall (n)"); int s = tui_select_list("Mallar", lines, (int)n + 1, cursor, 1, &cursor, 1, n > 0 ? "arkivera" : NULL, 0); if (s == -1) { diff --git a/clients/screens_vouchers.c b/clients/screens_vouchers.c index 3c33990..454486e 100644 --- a/clients/screens_vouchers.c +++ b/clients/screens_vouchers.c @@ -46,7 +46,7 @@ static int vd_key(void *ud, int ch) ctx->nav = dir; return TUI_HOOK_BACK; } - if (ch == TUI_KEY_CTRL_N) { + if (ch == 'n') { int64_t nid = vouchers_new(a); if (nid > 0) { ctx->posted_id = nid; @@ -88,7 +88,7 @@ static int vd_key(void *ud, int ch) } return TUI_HOOK_BACK; } - if (ch == 6) { /* ^F: attach a file to this voucher */ + if (ch == 'a') { /* attach a file to this voucher */ char *path = file_browser(a, a->attachment_dir); if (path) { struct stat sb; @@ -323,9 +323,23 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new, ctx.can_next = pos >= 0 && pos + 1 < total; char hint[256], title[96]; snprintf(hint, sizeof hint, - "%s^N = nytt verifikat c = rätta ^F = bifoga%s", + "%sn = nytt c = rätta a = bifoga%s", pos >= 0 ? "upp/ned = föregående/nästa " : "", natts ? " f = underlag" : ""); + struct tui_action acts[] = { + { "voucher.prev", "Föregående verifikat", KEY_UP, + ctx.can_prev, "första i listan" }, + { "voucher.next", "Nästa verifikat", KEY_DOWN, ctx.can_next, + "sista i listan" }, + { "voucher.correct", "Rätta (ändringsverifikat)…", 'c', 1, NULL }, + { "voucher.attach", "Bifoga fil…", 'a', 1, NULL }, + { "voucher.files", "Underlag…", 'f', natts ? 1 : 0, + "inga underlag" }, + { "voucher.new", "Nytt verifikat", 'n', 1, NULL }, + }; + /* stepping needs a place in the list order */ + tui_set_actions(pos >= 0 ? acts : acts + 2, + pos >= 0 ? 6 : 4); if (pos >= 0) snprintf(title, sizeof title, "Verifikat %s%lld (%d av %d)", series ? series : "", (long long)number, pos + 1, total); @@ -836,14 +850,25 @@ static int64_t vform_post(struct vform *vf, int dry) return 0; } +#define VFORM_TEMPLATE TUI_KEY_ACTION(0) +#define VFORM_ATTACH TUI_KEY_ACTION(1) + +static const struct tui_action VFORM_ACTS[] = { + { "voucher.post", "Bokför", TUI_KEY_SUBMIT, 1, NULL }, + { "voucher.validate", "Validera (torrkörning)", TUI_KEY_REFRESH, 1, + NULL }, + { "voucher.template", "Hämta mall…", VFORM_TEMPLATE, 1, NULL }, + { "voucher.attach", "Bifoga fil…", VFORM_ATTACH, 1, NULL }, +}; + static int vform_key(void *ud, int ch) { struct vform *vf = ud; - if (ch == KEY_F(4)) { + if (ch == VFORM_TEMPLATE) { vform_template(vf); return TUI_HOOK_STAY; } - if (ch == 6) { + if (ch == VFORM_ATTACH) { vform_attach(vf); return TUI_HOOK_STAY; } @@ -929,10 +954,11 @@ int64_t vouchers_new_prefill(struct app *a, const struct voucher_prefill *p) ff[2].kind = TUI_F_TEXT; tui_rt_set_fields(&vf.rt, ff, 3); tui_rt_normalize(&vf.rt); - const char *hint = "Enter = ändra fält Tab = byta fält F4 = mall" - " ^F = bifoga fil F5 = validera ^X = rensa rad" - " F9 = bokför Esc = avbryt"; + const char *hint = "Tab = byta fält Enter = ändra fält" + " ^O = åtgärder (bokför, validera, mall, bifoga)" + " ^X = rensa rad Esc = avbryt"; for (;;) { + tui_set_actions(VFORM_ACTS, 4); int rr = tui_rt_run("Nytt verifikat", &vf.rt, hint); if (rr == -1) return 0; @@ -1050,7 +1076,7 @@ static char **vlist_lines(const struct vlist_item *v, size_t n) v[i].desc ? v[i].desc : ""); items[i] = xstrdup(line); } - items[n] = xstrdup("+ Nytt verifikat (^N)"); + items[n] = xstrdup("+ Nytt verifikat (n)"); return items; } @@ -1098,6 +1124,11 @@ void vouchers_screen(struct app *a) vlist_sort_label(a->voucher_sort)); struct vlctx lctx = { a, 0 }; int cur = start; + struct tui_action lacts[] = { + { "voucher.new", "Nytt verifikat", 'n', 1, NULL }, + { "voucher.sort", "Byt sortering", 's', 1, NULL }, + }; + tui_set_actions(lacts, 2); tui_list_hint_extra("s = sortera"); int sel = tui_select_list_hook(title, items, (int)n + 1, start, 1, &cur, 1, NULL, 0, vlist_key, &lctx); diff --git a/clients/tui.c b/clients/tui.c index 43a3d27..bad019b 100644 --- a/clients/tui.c +++ b/clients/tui.c @@ -45,6 +45,11 @@ static int in_key(void) return g_in(); } +static void acts_take(const struct tui_action **acts, int *n); +static int ctx_menu(const struct tui_action *scr, int ns, + const struct tui_action *own, int no, int anchor_y); +static int ctx_key(int ch, int arrow_ok); + /* ------------------------------------------------------------------ */ /* theme: semantic styles, colour pairs and attribute fallback */ /* ------------------------------------------------------------------ */ @@ -165,14 +170,9 @@ int tui_sticky_split(char **lines, int n, char **head, int *nhead) void tui_keys_setup(void) { - /* ^Enter has no control code; enable xterm modifyOtherKeys level 2 (level - 1 leaves control+Enter alone) and the Kitty keyboard protocol, then bind - the sequences they produce. Terminals with neither keep F9 working. */ - putp("\033[>4;2m"); - putp("\033[>1u"); - fflush(stdout); - define_key("\033[27;5;13~", TUI_KEY_CTRL_ENTER); - define_key("\033[13;5u", TUI_KEY_CTRL_ENTER); + /* No extended keyboard protocol is enabled: every key the TUI uses has a + legacy encoding, which a browser terminal sends too. The CSI forms + below are still bound in case a terminal sends them anyway. */ define_key("\033[27;2;9~", KEY_BTAB); define_key("\033[9;2u", KEY_BTAB); define_key("\033[27u", 27); @@ -187,6 +187,8 @@ void tui_keys_setup(void) define_key("\033[102;5u", 6); define_key("\033[27;5;110~", 14); define_key("\033[110;5u", 14); + define_key("\033[27;5;111~", TUI_KEY_CTRL_O); + define_key("\033[111;5u", TUI_KEY_CTRL_O); define_key("\033[27;5;114~", 18); define_key("\033[114;5u", 18); define_key("\033[27;5;117~", 21); @@ -1065,9 +1067,9 @@ int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new, v->sel = nav_snap(v, v->sel, 1); if (v->n <= 0) { /* an empty list is never a dead end: refresh, add and back work */ - if (ch == KEY_F(5)) + if (ch == 'r' || ch == TUI_KEY_REFRESH) return -2; - if (ch == TUI_KEY_CTRL_N && allow_new) + if (ch == 'n' && allow_new) return -4; if (ch == 27 || ch == 'q') return -1; @@ -1154,9 +1156,9 @@ int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new, v->goto_active = 1; } else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) return v->sel; - else if (ch == KEY_F(5)) + else if (ch == 'r' || ch == TUI_KEY_REFRESH) return -2; - else if (ch == TUI_KEY_CTRL_N && allow_new) + else if (ch == 'n' && allow_new) return -4; else if ((ch == 'd' || ch == 'D') && allow_remove) return -6; @@ -1265,25 +1267,54 @@ int tui_select_list_hook(const char *title, char **items, int n, int start, v.numw = tui_num_width(n); v.items = (const char *const *)items; snprintf(v.gotolabel, sizeof v.gotolabel, "Gå till rad: "); + const struct tui_action *sa; + int sn; + acts_take(&sa, &sn); + int pending = 0; for (;;) { if (cursor) *cursor = v.sel; list_view_sync(&v); - char hint[320], dbuf[64] = ""; - if (archive_action) - snprintf(dbuf, sizeof dbuf, " d = %s", archive_action); + char hint[320]; snprintf(hint, sizeof hint, - "upp/ned, 1-9 = hoppa, g = gå till/sök, PgUp/PgDn, Home/End," - " Enter%s%s%s%s Esc/q = tillbaka ^C = avsluta", - allow_refresh ? " F5 = uppdatera" : "", - allow_new ? " ^N = ny" : "", dbuf, - allow_toggle ? " ^A = öppna/stäng" : ""); + "Enter = öppna → = åtgärder%s g = gå till/sök" + " Esc/q = tillbaka", + allow_new ? " n = ny" : ""); if (g_list_hint_extra && *g_list_hint_extra) { size_t hl = strlen(hint); snprintf(hint + hl, sizeof hint - hl, " %s", g_list_hint_extra); } - list_draw(title, items, n, &v, hint); - int ch = in_key(); + if (!pending) + list_draw(title, items, n, &v, hint); + int ch = pending ? pending : in_key(); + pending = 0; + if (!v.goto_active && ctx_key(ch, 1)) { + if (key) + key(ud, TUI_KEY_MENU); + struct tui_action own[8]; + int no = 0; + if (n > 0) + own[no++] = (struct tui_action){ "list.open", "Öppna", '\n', + 1, NULL }; + if (allow_new) + own[no++] = (struct tui_action){ "list.new", "Ny", 'n', 1, + NULL }; + if (archive_action && n > 0) + own[no++] = (struct tui_action){ "list.remove", archive_action, + 'd', 1, NULL }; + if (allow_toggle && n > 0) + own[no++] = (struct tui_action){ "list.toggle", + "Stäng/öppna", 1, 1, NULL }; + own[no++] = (struct tui_action){ "list.goto", "Gå till rad / sök", + 'g', 1, NULL }; + if (allow_refresh) + own[no++] = (struct tui_action){ "list.refresh", "Uppdatera", + 'r', 1, NULL }; + own[no++] = (struct tui_action){ "list.back", "Tillbaka", 27, 1, + NULL }; + pending = ctx_menu(sa, sn, own, no, 2 + v.sel - v.top); + continue; + } int in_goto = v.goto_active; int r = tui_nav_key(&v, ch, allow_new, archive_action != NULL, allow_toggle, 0); @@ -1331,10 +1362,25 @@ int tui_menu(const char *title, const char *const *items, int n, v.view = (LINES - 4) / 2; v.items = items; snprintf(v.gotolabel, sizeof v.gotolabel, "Gå till nummer: "); + const struct tui_action *sa; + int sn; + acts_take(&sa, &sn); + int pending = 0; for (;;) { if (cursor) *cursor = v.sel; list_view_sync(&v); + if (pending) { + int r = tui_nav_key(&v, pending, allow_new, 0, 0, 1); + pending = 0; + if (r != -3) { + if (cursor) + *cursor = v.sel; + free(flags); + return r; + } + continue; + } if (g_frame) g_frame(title); int num = 0; @@ -1362,13 +1408,28 @@ int tui_menu(const char *title, const char *const *items, int n, nav_goto_draw(&v); if (g_hints) g_hints(allow_new - ? "upp/ned, 1-9 = snabbval, g = gå till/sök, PgUp/PgDn," - " Home/End, Enter ^N = ny Esc/q = tillbaka" - " ^C = avsluta" - : "upp/ned, 1-9 = snabbval, g = gå till/sök, PgUp/PgDn," - " Home/End, Enter Esc/q = tillbaka ^C = avsluta"); + ? "Enter = öppna 1-9 = snabbval → = åtgärder" + " n = ny g = gå till/sök Esc/q = tillbaka" + : "Enter = öppna 1-9 = snabbval → = åtgärder" + " g = gå till/sök Esc/q = tillbaka"); refresh(); - int r = tui_nav_key(&v, in_key(), allow_new, 0, 0, 1); + int ch = in_key(); + if (!v.goto_active && ctx_key(ch, 1)) { + struct tui_action own[4]; + int no = 0; + own[no++] = (struct tui_action){ "menu.open", "Öppna", '\n', 1, + NULL }; + if (allow_new) + own[no++] = (struct tui_action){ "menu.new", "Ny", 'n', 1, + NULL }; + own[no++] = (struct tui_action){ "menu.goto", "Gå till / sök", + 'g', 1, NULL }; + own[no++] = (struct tui_action){ "menu.back", "Tillbaka", 27, 1, + NULL }; + pending = ctx_menu(sa, sn, own, no, 3 + (v.sel - v.top) * 2); + continue; + } + int r = tui_nav_key(&v, ch, allow_new, 0, 0, 1); if (r != -3) { if (cursor) *cursor = v.sel; @@ -1500,6 +1561,9 @@ int tui_pager_hook(const char *title, const char *text, int vbody = view - nhead; if (vbody < 1) vbody = 1; + const struct tui_action *sa; + int sn; + acts_take(&sa, &sn); for (;;) { tui_frame(title); int row = 0; @@ -1514,20 +1578,36 @@ int tui_pager_hook(const char *title, const char *text, char hint[512]; const char *scroll = (flags & TUI_PAGER_NO_ARROWS) ? "PgUp/PgDn/Home/End rullar" - : "piltangenter/PgUp/PgDn rullar"; + : "upp/ned/PgUp/PgDn rullar"; if (extra_hint && *extra_hint) snprintf(hint, sizeof hint, - "%s F5 = uppdatera %s q = tillbaka", scroll, + "%s → = åtgärder %s q = tillbaka", scroll, extra_hint); else - snprintf(hint, sizeof hint, "%s F5 = uppdatera q = tillbaka", + snprintf(hint, sizeof hint, "%s → = åtgärder q = tillbaka", scroll); tui_hints(hint); refresh(); int ch = in_key(); + if (ctx_key(ch, 1)) { + if (key) + key(ud, TUI_KEY_MENU); + struct tui_action own[4]; + int no = 0; + own[no++] = (struct tui_action){ "pager.refresh", "Uppdatera", 'r', + 1, NULL }; + if (flags & TUI_PAGER_SAVE) + own[no++] = (struct tui_action){ "pager.save", "Spara", 's', 1, + NULL }; + own[no++] = (struct tui_action){ "pager.back", "Tillbaka", 27, 1, + NULL }; + ch = ctx_menu(sa, sn, own, no, 2); + if (!ch) + continue; + } if (ch == 'q' || ch == 27) break; - if (ch == KEY_F(5)) { + if (ch == 'r' || ch == TUI_KEY_REFRESH) { ret = 1; break; } @@ -1597,9 +1677,9 @@ int tui_form_next(int sel, int n, int ch) int p = sel - TUI_FORM_PAGE; return p < 0 ? 0 : p; } - if (ch == KEY_F(5)) + if (ch == TUI_KEY_REFRESH) return TUI_FORM_REFRESH; - if (ch == TUI_KEY_CTRL_ENTER || ch == KEY_F(9)) + if (ch == TUI_KEY_SUBMIT) return TUI_FORM_SUBMIT; if (ch == 27 || ch == 'q') return TUI_FORM_BACK; @@ -1734,22 +1814,24 @@ void tui_form_act_label(const struct tui_form_action *a, char *buf, size_t n) } } -static void action_key_name(int key, char *buf, size_t n) +void tui_key_name(int key, char *buf, size_t n) { - if (key >= KEY_F(1) && key <= KEY_F(12)) - snprintf(buf, n, "F%d", key - KEY_F(0)); - else if (key == TUI_KEY_CTRL_N) - snprintf(buf, n, "^N"); - else if (key == TUI_KEY_CTRL_X) - snprintf(buf, n, "^X"); - else if (key == 27) + if (key == 27) snprintf(buf, n, "Esc"); else if (key == '\n' || key == '\r' || key == KEY_ENTER) snprintf(buf, n, "Enter"); + else if (key == ' ') + snprintf(buf, n, "mellanslag"); + else if (key == KEY_UP) + snprintf(buf, n, "↑"); + else if (key == KEY_DOWN) + snprintf(buf, n, "↓"); else if (key > 0 && key < 27) snprintf(buf, n, "^%c", 'A' + key - 1); + else if (key > 32 && key < 127) + snprintf(buf, n, "%c", key); else - snprintf(buf, n, "?"); + buf[0] = '\0'; /* virtual: menu only */ } void tui_action_label(const struct tui_action *a, char *buf, size_t n) @@ -1778,9 +1860,11 @@ void tui_action_hint(const struct tui_action *acts, int n, int max, char *buf, char key[16], item[200]; size_t len; - if (acts[i].enabled != 1 || !acts[i].key) + if (acts[i].enabled != 1) + continue; + tui_key_name(acts[i].key, key, sizeof key); + if (!key[0]) continue; - action_key_name(acts[i].key, key, sizeof key); snprintf(item, sizeof item, "%s%s = %s", shown ? " " : "", key, acts[i].label ? acts[i].label : ""); len = strlen(item); @@ -1790,110 +1874,224 @@ void tui_action_hint(const struct tui_action *acts, int n, int max, char *buf, o += len; shown++; } - const char *more = "F2 = fler"; + const char *more = "→ = åtgärder"; size_t ml = strlen(more); if (o + ml + (o ? 3 : 0) + 1 <= cap) snprintf(buf + o, cap - o, "%s%s", o ? " " : "", more); } -int tui_action_menu(const char *title, const struct tui_action *acts, int n, - int *focus) +int tui_ctx_merge(const struct tui_action *scr, int ns, + const struct tui_action *own, int no, + struct tui_action *out, int cap) { - struct tui_list_nav v; - unsigned char *ok; - char **items; - int ret = -1; + int n = 0; + for (int i = 0; scr && i < ns && n < cap; i++) + out[n++] = scr[i]; + int sep = n > 0; + for (int i = 0; own && i < no && n < cap; i++) { + int taken = 0; + for (int j = 0; scr && j < ns; j++) + if (own[i].key && scr[j].key == own[i].key) + taken = 1; + if (taken) + continue; + if (sep && n + 1 < cap) { + memset(&out[n], 0, sizeof out[n]); + out[n].label = ""; + out[n].enabled = -1; + n++; + } + sep = 0; + out[n++] = own[i]; + } + return n; +} +static int menu_step(const struct tui_action *acts, int n, int from, int dir) +{ + for (int k = 1; k <= n; k++) { + int i = ((from + k * dir) % n + n) % n; + if (acts[i].enabled != -1) + return i; + } + return from; +} + +int tui_action_menu_at(const char *title, const struct tui_action *acts, + int n, int anchor_y, int *focus) +{ if (!acts || n <= 0) return -1; - memset(&v, 0, sizeof v); - v.n = n; - ok = xcalloc((size_t)n, 1); - items = xcalloc((size_t)n, sizeof(char *)); + int lw = 0, kw = 0; for (int i = 0; i < n; i++) { - char line[192]; - - tui_action_label(&acts[i], line, sizeof line); - items[i] = xstrdup(line); - ok[i] = acts[i].enabled != -1; + char lab[256], key[16]; + tui_action_label(&acts[i], lab, sizeof lab); + tui_key_name(acts[i].key, key, sizeof key); + if (tui_disp_width(lab) > lw) + lw = tui_disp_width(lab); + if (tui_disp_width(key) > kw) + kw = tui_disp_width(key); } - v.selectable = ok; - v.sel = focus && *focus >= 0 && *focus < n ? *focus : 0; - if (!ok[v.sel]) - v.sel = nav_snap(&v, v.sel, 1); - v.numw = tui_num_width(n); - if (v.numw < 2) - v.numw = 2; - v.view = LINES - 4; - v.items = (const char *const *)items; - snprintf(v.gotolabel, sizeof v.gotolabel, "Gå till nummer: "); + int w = lw + (kw ? kw + 3 : 0) + 4; + if (w < tui_disp_width(title) + 6) + w = tui_disp_width(title) + 6; + if (w < 24) + w = 24; + if (w > COLS - 4) + w = COLS - 4; + int h = n + 2; + if (h > LINES - 2) + h = LINES - 2; + int view = h - 2; + int x = COLS - w - 2; + if (x < 1) + x = 1; + int y = anchor_y < 0 ? (LINES - h) / 2 : anchor_y - 1; + if (y + h > LINES - 1) + y = LINES - 1 - h; + if (y < 1) + y = 1; + int sel = focus && *focus >= 0 && *focus < n ? *focus : 0; + if (acts[sel].enabled == -1) + sel = menu_step(acts, n, sel, 1); + if (acts[sel].enabled == -1) + return -1; /* only headings */ + refresh(); + WINDOW *win = newwin(h, w, y, x); + int top = 0, ret = -1, prev = curs_set(0); for (;;) { - int num = 0; - - if (v.sel < 0 || v.sel >= n) { - v.sel = nav_snap(&v, 0, 1); - if (v.sel < 0) - break; - } - if (focus) - *focus = v.sel; - list_view_sync(&v); - if (g_frame) - g_frame(title); - for (int i = 0; i < v.top && i < n; i++) - if (ok[i]) - num++; - for (int idx = v.top; idx < n && idx < v.top + v.view; idx++) { - char line[832]; - if (!ok[idx]) { - snprintf(line, sizeof line, "%*s %s", v.numw, "", - items[idx]); - attron(tui_style_attrs(TUI_DIM, g_colours, 0)); - mvaddnstr(3 + (idx - v.top), 4, line, COLS - 6); - attroff(tui_style_attrs(TUI_DIM, g_colours, 0)); + if (sel < top) + top = sel; + if (sel >= top + view) + top = sel - view + 1; + werase(win); + wattrset(win, tui_style_attrs(TUI_TITLE, g_colours, 0)); + box(win, 0, 0); + mvwaddnstr(win, 0, 2, title, w - 4); + wattrset(win, A_NORMAL); + for (int r = 0; r < view && top + r < n; r++) { + const struct tui_action *a = &acts[top + r]; + if (a->enabled == -1 && !(a->label && *a->label)) { + wattrset(win, tui_style_attrs(TUI_RULE, g_colours, 0)); + mvwhline(win, 1 + r, 1, ACS_HLINE, w - 2); + wattrset(win, A_NORMAL); continue; } - num++; - snprintf(line, sizeof line, "%*d. %s", v.numw, num, items[idx]); - if (acts[idx].enabled != 1) - attron(tui_style_attrs(TUI_DIM, g_colours, 0)); - if (idx == v.sel) - attron(A_REVERSE); - mvaddnstr(3 + (idx - v.top), 4, line, COLS - 6); - if (idx == v.sel) - attroff(A_REVERSE); - if (acts[idx].enabled != 1) - attroff(tui_style_attrs(TUI_DIM, g_colours, 0)); + char lab[256], key[16], line[512]; + tui_action_label(a, lab, sizeof lab); + tui_key_name(a->key, key, sizeof key); + tui_pad_field(lab, sizeof lab, w - 4 - (kw ? kw + 1 : 0)); + snprintf(line, sizeof line, " %s%s%*s ", lab, kw ? " " : "", kw, + ""); + tui_pad_field(line, sizeof line, w - 2); + int attr = a->enabled == 1 ? (int)A_NORMAL + : a->enabled == 0 ? tui_style_attrs(TUI_DIM, g_colours, 0) + : tui_style_attrs(TUI_HEADING, + g_colours, 0); + if (top + r == sel) + attr |= A_REVERSE; + wattrset(win, attr); + mvwaddstr(win, 1 + r, 1, line); + /* the key, right-aligned in its column */ + if (key[0]) { + int kx = w - 2 - tui_disp_width(key); + wattrset(win, (top + r == sel ? A_REVERSE : A_NORMAL) | + tui_style_attrs(TUI_ACCENT, g_colours, 0)); + mvwaddstr(win, 1 + r, kx, key); + } + wattrset(win, A_NORMAL); } - nav_goto_draw(&v); if (g_hints) - g_hints("upp/ned, 1-9 = hoppa, g = gå till/sök, PgUp/PgDn, Home/End," - " Enter = utför Esc/q = avbryt"); + g_hints("upp/ned = välj Enter = utför Esc/vänster = stäng"); refresh(); + touchwin(win); + wrefresh(win); int ch = in_key(); - if (!v.goto_active && (ch == '\n' || ch == '\r' || ch == KEY_ENTER)) { - if (!ok[v.sel]) - continue; - if (acts[v.sel].enabled != 1) { - tui_message(title, "%s", - acts[v.sel].reason && *acts[v.sel].reason - ? acts[v.sel].reason - : "Otillgänglig."); + if (ch == KEY_UP || ch == KEY_BTAB) { + sel = menu_step(acts, n, sel, -1); + } else if (ch == KEY_DOWN || ch == '\t') { + sel = menu_step(acts, n, sel, 1); + } else if (ch == KEY_HOME) { + sel = menu_step(acts, n, n - 1, 1); + } else if (ch == KEY_END) { + sel = menu_step(acts, n, 0, -1); + } else if (ch == 27 || ch == KEY_LEFT || ch == 'q' || + ch == TUI_KEY_CTRL_O) { + break; + } else { + int pick = -1; + if (ch == '\n' || ch == '\r' || ch == KEY_ENTER || ch == KEY_RIGHT) + pick = sel; + else + for (int i = 0; i < n; i++) + if (acts[i].enabled != -1 && acts[i].key == ch) + pick = i; + if (pick < 0) continue; + sel = pick; + if (acts[pick].enabled == 1) { + ret = pick; + break; } - ret = v.sel; - break; + tui_message(title, "%s", + acts[pick].reason && *acts[pick].reason + ? acts[pick].reason + : "Otillgänglig."); } - if (tui_nav_key(&v, ch, 0, 0, 0, 0) == -1) - break; } - for (int i = 0; i < n; i++) - free(items[i]); - free(items); - free(ok); + if (focus) + *focus = sel; + curs_set(prev == ERR ? 0 : prev); + delwin(win); + touchwin(stdscr); + refresh(); return ret; } +int tui_action_menu(const char *title, const struct tui_action *acts, int n, + int *focus) +{ + return tui_action_menu_at(title, acts, n, -1, focus); +} + +/* Screen actions registered for the next widget run. */ +static const struct tui_action *g_acts; +static int g_nacts; + +void tui_set_actions(const struct tui_action *acts, int n) +{ + g_acts = acts; + g_nacts = acts && n > 0 ? n : 0; +} + +static void acts_take(const struct tui_action **acts, int *n) +{ + *acts = g_acts; + *n = g_nacts; + g_acts = NULL; + g_nacts = 0; +} + +#define CTX_MAX 64 + +/* Opens the context menu over screen + own actions; returns the key of the + chosen action (fed back to the widget) or 0. */ +static int ctx_menu(const struct tui_action *scr, int ns, + const struct tui_action *own, int no, int anchor_y) +{ + struct tui_action all[CTX_MAX]; + int n = tui_ctx_merge(scr, ns, own, no, all, CTX_MAX); + int i = tui_action_menu_at("Åtgärder", all, n, anchor_y, NULL); + return i >= 0 ? all[i].key : 0; +} + +/* Whether ch opens the context menu here: ^O always, → where no caret + uses it. */ +static int ctx_key(int ch, int arrow_ok) +{ + return ch == TUI_KEY_CTRL_O || (arrow_ok && ch == KEY_RIGHT); +} + /* Form layout: labels at column 2, values at FORM_VALUE_X, first row at FORM_Y. Shared by tui_form_run* and the header fields of tui_rt. */ #define FORM_Y 5 @@ -2023,6 +2221,10 @@ static int form_run(const char *title, struct tui_form_field *f, int nf, { int sel = tui_form_focus_clamp(focus ? *focus : -1, nf, acts, na); int top = 0; + const struct tui_action *sa; + int sn; + acts_take(&sa, &sn); + int pending = 0; if (na < 0) na = 0; for (;;) { @@ -2071,18 +2273,15 @@ static int form_run(const char *title, struct tui_form_field *f, int nf, h = na > 0 ? (can_edit ? "upp/ned/Tab = flytta Enter = ändra/utför" - " F5 = uppdatera F9 = spara" - " Esc/q = tillbaka ^C = avsluta" + " → = åtgärder Esc/q = tillbaka" : "upp/ned/Tab = flytta Enter = utför" - " F5 = uppdatera Esc/q = tillbaka" - " ^C = avsluta" + " → = åtgärder Esc/q = tillbaka" " (endast behöriga kan ändra)") : (can_edit - ? "upp/ned/Home/End Enter = ändra" - " F5 = uppdatera F9 = spara" - " Esc/q = tillbaka ^C = avsluta" - : "upp/ned/Home/End F5 = uppdatera" - " Esc/q = tillbaka ^C = avsluta" + ? "upp/ned = flytta Enter = ändra" + " → = åtgärder Esc/q = tillbaka" + : "upp/ned = flytta → = åtgärder" + " Esc/q = tillbaka" " (endast behöriga kan ändra)"); char hbuf[640]; if (g_form_hint_extra && *g_form_hint_extra && @@ -2092,7 +2291,28 @@ static int form_run(const char *title, struct tui_form_field *f, int nf, tui_hints(h); g_form_hint = NULL; refresh(); - int ch = in_key(); + int ch = pending ? pending : in_key(); + pending = 0; + if (ctx_key(ch, 1)) { + if (key) + key(ud, TUI_KEY_MENU); + struct tui_action own[5]; + int no = 0; + if (sel >= 0 && sel < nf) + own[no++] = (struct tui_action){ + "form.edit", f[sel].kind == TUI_F_ACTION ? "Öppna" : "Ändra", + '\n', can_edit ? 1 : 0, "endast behöriga kan ändra" }; + else if (sel >= nf) + own[no++] = (struct tui_action){ "form.run", "Utför", '\n', 1, + NULL }; + own[no++] = (struct tui_action){ "form.refresh", "Uppdatera", + TUI_KEY_REFRESH, 1, NULL }; + own[no++] = (struct tui_action){ "form.back", "Tillbaka", 27, 1, + NULL }; + int line = sel < 0 ? 0 : sel < nf ? sel : sel + 1; + pending = ctx_menu(sa, sn, own, no, FORM_Y + line - top); + continue; + } int nxt = na > 0 ? tui_form_act_key(sel, nf, acts, na, ch) : tui_form_next(sel, nf, ch); if (nxt >= 0) { @@ -2410,6 +2630,10 @@ int tui_rt_run(const char *title, struct tui_rt *t, const char *hint) int top = 0; int prev_cursor = curs_set(0); int ret; + const struct tui_action *sa; + int sn; + acts_take(&sa, &sn); + int pending = 0; for (;;) { if (g_frame) g_frame(title); @@ -2501,17 +2725,41 @@ int tui_rt_run(const char *title, struct tui_rt *t, const char *hint) if (caret_y >= 0) move(caret_y, caret_x); refresh(); - int ch = in_key(); + int ch = pending ? pending : in_key(); + pending = 0; + /* ^O everywhere; → only on a header field, in a cell it moves the + caret */ + if (ctx_key(ch, t->focus >= 0)) { + if (t->key) + t->key(t->ud, TUI_KEY_MENU); + struct tui_action own[5]; + int no = 0; + if (t->focus >= 0) + own[no++] = (struct tui_action){ "rt.edit", "Ändra fält", '\n', + 1, NULL }; + else + own[no++] = (struct tui_action){ "rt.clear", "Rensa raden", + TUI_KEY_CTRL_X, 1, NULL }; + own[no++] = (struct tui_action){ "rt.refresh", "Validera", + TUI_KEY_REFRESH, 1, NULL }; + own[no++] = (struct tui_action){ "rt.submit", "Spara", + TUI_KEY_SUBMIT, 1, NULL }; + own[no++] = (struct tui_action){ "rt.back", "Avbryt", 27, 1, + NULL }; + int ay = caret_y >= 0 ? caret_y : FORM_Y + t->focus; + pending = ctx_menu(sa, sn, own, no, ay); + continue; + } /* no 'q' here: every printable key is text in a cell or field */ if (ch == 27) { ret = -1; break; } - if (ch == KEY_F(5)) { + if (ch == TUI_KEY_REFRESH) { ret = -2; break; } - if (ch == TUI_KEY_CTRL_ENTER || ch == KEY_F(9)) { + if (ch == TUI_KEY_SUBMIT) { ret = -3; break; } diff --git a/clients/tui.h b/clients/tui.h index 4ae0da6..58b3ff5 100644 --- a/clients/tui.h +++ b/clients/tui.h @@ -12,8 +12,15 @@ /* Universal keys that have no control code or need a stable name. */ #define TUI_KEY_CTRL_N 0x0e +#define TUI_KEY_CTRL_O 0x0f /* context menu, also while typing in a cell */ #define TUI_KEY_CTRL_X 0x18 #define TUI_KEY_CTRL_ENTER (KEY_MAX + 1) +/* Virtual keys: they have no physical key and only come from the context + menu (docs/TUI-GUIDELINES.md "Keys and the web"). No widget uses F-keys. */ +#define TUI_KEY_REFRESH (KEY_MAX + 2) /* uppdatera / validera / förhandsvisa */ +#define TUI_KEY_SUBMIT (KEY_MAX + 3) /* spara / bokför / utfärda */ +#define TUI_KEY_MENU (KEY_MAX + 4) /* to the key hook: the menu opens */ +#define TUI_KEY_ACTION(n) (KEY_MAX + 16 + (n)) /* menu-only screen action */ /* Semantic styles: the widget layer maps them to colour pairs when the terminal supports colour and NO_COLOR is unset, otherwise to plain @@ -230,14 +237,36 @@ struct tui_action { /* "label", or "label (reason)" when the action is disabled. */ void tui_action_label(const struct tui_action *a, char *buf, size_t n); -/* Builds "F9 = spara F2 = fler", showing at most max keyed actions. */ +/* Builds "a = bifoga n = ny → = åtgärder", showing at most max actions + whose key is a visible key (virtual keys are menu-only). */ void tui_action_hint(const struct tui_action *acts, int n, int max, char *buf, size_t cap); -/* Modal action list: arrows/Tab/Home/End/PgUp/PgDn/1-9/g navigate, Enter - runs (a disabled action shows its reason), Esc cancels. Returns the - chosen index or -1. *focus remembers the row. */ +/* Display name of an action key: "Enter", "Esc", "^X", "n", "↑"; "" for a + virtual key (menu only). */ +void tui_key_name(int key, char *buf, size_t n); +/* Context menu ("Åtgärder"): a box at the right edge next to row anchor_y + (-1: centred) listing label and key per action. Up/down/Home/End move, + Enter or the action's own key runs it (a disabled action shows its + reason), Esc/←/q/^O close. Returns the chosen index or -1; *focus (may + be NULL) remembers the row. */ +int tui_action_menu_at(const char *title, const struct tui_action *acts, + int n, int anchor_y, int *focus); int tui_action_menu(const char *title, const struct tui_action *acts, int n, int *focus); +/* The screen's actions for the next widget run (lists, menus, pager, + forms, row table); the widget takes them at entry, so they never leak + into a widget opened from a hook. → (or ^O) opens the context menu with + these first, then the widget's own; choosing one feeds its key to the + widget as if it was typed, so accelerators and the menu share one path. + The key hook gets TUI_KEY_MENU right before the menu opens, so a screen + can update enabled/reason for the current row in its array. */ +void tui_set_actions(const struct tui_action *acts, int n); +/* Screen actions first, then own actions whose key is not taken, with a + separator row (enabled -1, empty label) between the groups. Returns the + count written to out. Pure. */ +int tui_ctx_merge(const struct tui_action *scr, int ns, + const struct tui_action *own, int no, + struct tui_action *out, int cap); /* --- action list in a form (tui_form_run_actions) --- */ /* enabled: 1 = selectable and runs; 0 = dimmed but selectable (Enter shows @@ -269,7 +298,7 @@ int tui_form_act_step(int focus, int nf, const struct tui_form_action *acts, int tui_form_act_page(int focus, int nf, const struct tui_form_action *acts, int na, int dir); /* Same ring for one key, plus Home/End/PgUp/PgDn and the form return keys - (F5/^Enter/F9/Esc/q). Returns the new focus, a TUI_FORM_* code or + (TUI_KEY_REFRESH/TUI_KEY_SUBMIT from the menu, Esc/q). Returns the new focus, a TUI_FORM_* code or TUI_NAV_NONE. */ int tui_form_act_key(int sel, int nf, const struct tui_form_action *acts, int na, int ch); diff --git a/clients/ui.h b/clients/ui.h index 884adbf..0fb533b 100644 --- a/clients/ui.h +++ b/clients/ui.h @@ -12,12 +12,8 @@ /* Shared UI state and helpers for the bokftui screens (screens_<group>.c). The scene registry and main() live in bokftui.c. */ -/* universal "new/add" hotkey: ^N (0x0e), the de-facto Linux convention */ -#define KEY_CTRL_N 0x0e /* ^X clears the focused row in row editors */ #define KEY_CTRL_X 0x18 -/* ^Enter saves/posts the form; sent via xterm modifyOtherKeys */ -#define KEY_CTRL_ENTER (KEY_MAX + 1) /* Pager markup: headings, dimmed derived lines and full-width rules. */ #define MK_HEAD "\001" #define MK_DIM "\002" |
