summaryrefslogtreecommitdiff
path: root/clients
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-23 10:58:17 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-23 10:58:17 +0200
commit71a702f375750829c634b552217c9925d549828b (patch)
tree2b963da1d0110ed43920722ba4193ee9c79f2521 /clients
parent8ccb6d58d8e2f1c65dfd7ab5f0eb7d93f1f19def (diff)
downloadbokf-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.c12
-rw-r--r--clients/screens_attachments.c5
-rw-r--r--clients/screens_bank.c9
-rw-r--r--clients/screens_bokslut.c13
-rw-r--r--clients/screens_dashboard.c9
-rw-r--r--clients/screens_ib.c19
-rw-r--r--clients/screens_invoices.c125
-rw-r--r--clients/screens_payroll.c27
-rw-r--r--clients/screens_rules.c10
-rw-r--r--clients/screens_settings.c6
-rw-r--r--clients/screens_templates.c15
-rw-r--r--clients/screens_vouchers.c49
-rw-r--r--clients/tui.c522
-rw-r--r--clients/tui.h39
-rw-r--r--clients/ui.h4
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"