aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 00:05:51 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 00:05:51 +0200
commita61cfeefb0599e74fc9189b189b16dd0b6d36f0f (patch)
tree83e0934e190744605e91e69cd032e91c85eecf57
parent1f2d7a8190876e3d1c541251e079a11a2cc637f2 (diff)
downloadbokf-a61cfeefb0599e74fc9189b189b16dd0b6d36f0f.tar.gz
bokf-a61cfeefb0599e74fc9189b189b16dd0b6d36f0f.zip
tui: remember the focused row in forms and the bokslut hubv0.1.44
-rw-r--r--clients/bokftui.c15
-rw-r--r--clients/tui.c71
-rw-r--r--clients/tui.h17
-rw-r--r--docs/STATE.md5
-rw-r--r--docs/TUI-GUIDELINES.md13
-rw-r--r--tests/test_tui.c62
6 files changed, 154 insertions, 29 deletions
diff --git a/clients/bokftui.c b/clients/bokftui.c
index 3258f22..f7500bf 100644
--- a/clients/bokftui.c
+++ b/clients/bokftui.c
@@ -320,6 +320,7 @@ static void update_status(struct app *a)
/* Create a new fiscal year. Returns the new id (>0) or 0. */
static int64_t fy_new_form(struct app *a)
{
+ static int sel = 0;
char label[32] = "", start[16] = "", end[16] = "";
char *resp =
client_rpc(&a->conn, "fiscal_year.list", a->session, a->org, "{}");
@@ -372,7 +373,7 @@ static int64_t fy_new_form(struct app *a)
ff[2].value = end;
ff[2].cap = sizeof end;
ff[2].kind = TUI_F_DATE;
- int r = tui_form_run("Nytt räkenskapsår", ff, 3, 1);
+ int r = tui_form_run("Nytt räkenskapsår", ff, 3, 1, &sel);
if (r == TUI_FORM_BACK)
return 0;
if (r != TUI_FORM_REFRESH && r != TUI_FORM_SUBMIT)
@@ -4130,6 +4131,7 @@ static void board_screen(struct app *a)
static void company_screen(struct app *a)
{
+ static int sel = 0;
const int nf = (int)(sizeof COMPANY_FIELDS / sizeof COMPANY_FIELDS[0]);
int owner = a->role[0] && strcmp(a->role, "owner") == 0;
for (;;) {
@@ -4164,7 +4166,7 @@ static void company_screen(struct app *a)
free(resp);
ff[nf].label = "Styrelseledamöter";
ff[nf].kind = TUI_F_ACTION;
- int r = tui_form_run("Företagsuppgifter", ff, nf + 1, owner);
+ int r = tui_form_run("Företagsuppgifter", ff, nf + 1, owner, &sel);
if (r >= 0 && r < nf) {
if (!company_field_valid(&COMPANY_FIELDS[r], vals[r])) {
tui_message("Ogiltigt värde", "%s", COMPANY_FIELDS[r].label);
@@ -4204,6 +4206,7 @@ static const char *const SETTING_LABELS[] = { "Standardserie",
static void settings_screen(struct app *a)
{
+ static int sel = 0;
const int nset = (int)(sizeof SETTING_KEYS / sizeof SETTING_KEYS[0]);
for (;;) {
if (g_quit)
@@ -4231,7 +4234,7 @@ static void settings_screen(struct app *a)
ff[i].kind = TUI_F_TEXT;
}
free(resp);
- int r = tui_form_run("Inställningar", ff, nset, 1);
+ int r = tui_form_run("Inställningar", ff, nset, 1, &sel);
if (r >= 0) {
yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
yyjson_mut_val *o = yyjson_mut_obj(d);
@@ -4472,6 +4475,7 @@ static void bokslut_plan(struct app *a, const char *fond, const char *rate,
view. New years inherit the stable fields. */
static void bokslut_screen(struct app *a)
{
+ static int sel = 0;
static const char *const labels[6] = {
"Väsentliga händelser",
"Årsstämma (YYYY-MM-DD)",
@@ -4574,7 +4578,7 @@ static void bokslut_screen(struct app *a)
" bokslutsplan ^Enter = bokför planen (frågar"
" först) Esc/q = tillbaka ^C = avsluta");
int r = tui_form_run_actions("Bokslut", ff, nf + 2, can_edit, acts, 5,
- NULL);
+ NULL, &sel);
if (r >= 0 && r < nf) {
yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
yyjson_mut_val *o = yyjson_mut_obj(d);
@@ -5606,6 +5610,7 @@ static int try_login(struct app *a, const char *user, const char *pass,
static int login_screen(struct app *a)
{
+ static int sel = 0;
char socket_path[256], user[64], pass[128];
snprintf(socket_path, sizeof socket_path, "%s", a->socket);
const char *env_pass = getenv("BOKFD_PASSWORD");
@@ -5631,7 +5636,7 @@ static int login_screen(struct app *a)
ff[3].kind = TUI_F_ACTION;
for (;;) {
- int r = tui_form_run("bokf — inloggning", ff, 4, 1);
+ int r = tui_form_run("bokf — inloggning", ff, 4, 1, &sel);
if (r == TUI_FORM_BACK) {
if (g_quit) {
endwin();
diff --git a/clients/tui.c b/clients/tui.c
index a5b0cd2..c52538c 100644
--- a/clients/tui.c
+++ b/clients/tui.c
@@ -1346,6 +1346,36 @@ static int act_selectable(const struct tui_form_action *acts, int i, int na)
return acts && i >= 0 && i < na && acts[i].enabled != -1;
}
+int tui_form_focus_clamp(int focus, int nf,
+ const struct tui_form_action *acts, int na)
+{
+ if (nf < 0)
+ nf = 0;
+ if (na < 0)
+ na = 0;
+ int n = nf + na;
+ if (n <= 0)
+ return -1;
+ int pos = focus;
+ if (pos < 0)
+ pos = 0;
+ if (pos >= n)
+ pos = n - 1;
+ for (int i = 0; i < n; i++) {
+ if (pos < nf || act_selectable(acts, pos - nf, na))
+ return pos;
+ pos = (pos + 1) % n;
+ }
+ return -1;
+}
+
+int tui_form_focus_store(int *focus, int sel, int ret)
+{
+ if (focus)
+ *focus = sel;
+ return ret;
+}
+
int tui_form_act_step(int focus, int nf, const struct tui_form_action *acts,
int na, int dir)
{
@@ -1440,13 +1470,14 @@ static void form_draw_actions(const struct tui_form_action *acts, int na,
static int form_run(const char *title, struct tui_form_field *f, int nf,
int can_edit, const struct tui_form_action *acts, int na,
- const char *hint, int (*key)(void *ud, int ch), void *ud)
+ const char *hint, int (*key)(void *ud, int ch), void *ud,
+ int *focus)
{
- int sel = nf > 0 ? 0 : tui_form_act_first(0, acts, na);
+ int sel = tui_form_focus_clamp(focus ? *focus : -1, nf, acts, na);
for (;;) {
int ntot = nf + (na > 0 ? na : 0);
if (ntot <= 0)
- return TUI_FORM_BACK;
+ return tui_form_focus_store(focus, sel, TUI_FORM_BACK);
if (sel < 0)
sel = tui_form_act_first(nf, acts, na);
if (sel >= ntot)
@@ -1523,15 +1554,15 @@ static int form_run(const char *title, struct tui_form_field *f, int nf,
}
if (nxt == TUI_FORM_REFRESH || nxt == TUI_FORM_SUBMIT ||
nxt == TUI_FORM_BACK)
- return nxt;
+ return tui_form_focus_store(focus, sel, nxt);
if (key) {
int hk = key(ud, ch);
if (hk == TUI_HOOK_BACK)
- return TUI_FORM_BACK;
+ return tui_form_focus_store(focus, sel, TUI_FORM_BACK);
if (hk == TUI_HOOK_REFRESH)
- return TUI_FORM_REFRESH;
+ return tui_form_focus_store(focus, sel, TUI_FORM_REFRESH);
if (hk == TUI_HOOK_SUBMIT)
- return TUI_FORM_SUBMIT;
+ return tui_form_focus_store(focus, sel, TUI_FORM_SUBMIT);
if (hk != TUI_HOOK_NONE)
continue;
}
@@ -1550,7 +1581,8 @@ static int form_run(const char *title, struct tui_form_field *f, int nf,
}
if (act->enabled == -1)
continue;
- return TUI_FORM_ACTION + (sel - nf);
+ return tui_form_focus_store(focus, sel,
+ TUI_FORM_ACTION + (sel - nf));
}
if (!can_edit) {
tui_message(title, "Endast behöriga kan ändra.");
@@ -1558,49 +1590,50 @@ static int form_run(const char *title, struct tui_form_field *f, int nf,
}
struct tui_form_field *fl = &f[sel];
if (fl->kind == TUI_F_ACTION)
- return sel;
+ return tui_form_focus_store(focus, sel, sel);
char label[192];
snprintf(label, sizeof label, "%s: ", fl->label);
if (fl->kind == TUI_F_TEXT) {
if (fl->value &&
tui_prompt_into(fl->value, fl->cap, label, fl->value,
fl->mask))
- return sel;
+ return tui_form_focus_store(focus, sel, sel);
} else if (fl->kind == TUI_F_DATE) {
if (fl->value && tui_date_prompt_into(fl->value, fl->cap, label,
fl->value))
- return sel;
+ return tui_form_focus_store(focus, sel, sel);
} else if (fl->kind == TUI_F_AMOUNT) {
if (fl->ore && tui_amount_prompt_into(fl->ore, label, *fl->ore))
- return sel;
+ return tui_form_focus_store(focus, sel, sel);
} else if (fl->kind == TUI_F_CHOICE && fl->choice) {
int c = tui_choice_prompt(label, fl->choices, fl->nchoices,
*fl->choice);
if (c >= 0) {
*fl->choice = c;
- return sel;
+ return tui_form_focus_store(focus, sel, sel);
}
}
}
}
int tui_form_run(const char *title, struct tui_form_field *f, int n,
- int can_edit)
+ int can_edit, int *focus)
{
- return form_run(title, f, n, can_edit, NULL, 0, NULL, NULL, NULL);
+ return form_run(title, f, n, can_edit, NULL, 0, NULL, NULL, NULL, focus);
}
int tui_form_run_hook(const char *title, struct tui_form_field *f, int n,
- int can_edit, int (*key)(void *ud, int ch), void *ud)
+ int can_edit, int (*key)(void *ud, int ch), void *ud,
+ int *focus)
{
- return form_run(title, f, n, can_edit, NULL, 0, NULL, key, ud);
+ return form_run(title, f, n, can_edit, NULL, 0, NULL, key, ud, focus);
}
int tui_form_run_actions(const char *title, struct tui_form_field *f, int nf,
int can_edit, const struct tui_form_action *acts,
- int na, const char *hint)
+ int na, const char *hint, int *focus)
{
- return form_run(title, f, nf, can_edit, acts, na, hint, NULL, NULL);
+ return form_run(title, f, nf, can_edit, acts, na, hint, NULL, NULL, focus);
}
/* ------------------------------------------------------------------ */
diff --git a/clients/tui.h b/clients/tui.h
index d0656da..7250c0b 100644
--- a/clients/tui.h
+++ b/clients/tui.h
@@ -179,11 +179,13 @@ struct tui_form_field {
};
int tui_form_next(int sel, int n, int ch);
+
int tui_form_run(const char *title, struct tui_form_field *f, int n,
- int can_edit);
+ int can_edit, int *focus);
void tui_form_hint(const char *hint); /* overrides the footer for the next run */
int tui_form_run_hook(const char *title, struct tui_form_field *f, int n,
- int can_edit, int (*key)(void *ud, int ch), void *ud);
+ int can_edit, int (*key)(void *ud, int ch), void *ud,
+ int *focus);
/* --- action list in a form (tui_form_run_actions) --- */
/* enabled: 1 = selectable and runs; 0 = dimmed but selectable (Enter shows
@@ -197,6 +199,15 @@ struct tui_form_action {
#define TUI_FORM_ACTION 1000 /* action i is returned as TUI_FORM_ACTION + i */
+/* Focus memory: the screen owns an int and passes its address to
+ tui_form_run*; on entry *focus is clamped to a selectable row — out of
+ range snaps to the nearest end and a heading row snaps forward, wrapping
+ — and on every return the row the form stopped on is written back.
+ Passing NULL starts at the first focusable row and stores nothing. */
+int tui_form_focus_clamp(int focus, int nf,
+ const struct tui_form_action *acts, int na);
+int tui_form_focus_store(int *focus, int sel, int ret);
+
/* Focus ring over fields (0..nf-1) and actions (nf..nf+na-1); heading rows
are skipped and the ring wraps. dir +1 = Tab/Down, -1 = Shift-Tab/Up. */
int tui_form_act_step(int focus, int nf, const struct tui_form_action *acts,
@@ -218,7 +229,7 @@ void tui_form_act_label(const struct tui_form_action *a, char *buf, size_t n);
footer; tui_form_hint() still takes precedence. */
int tui_form_run_actions(const char *title, struct tui_form_field *f, int nf,
int can_edit, const struct tui_form_action *acts,
- int na, const char *hint);
+ int na, const char *hint, int *focus);
/* --- row table --- */
enum { TUI_RT_EDIT = 0, TUI_RT_INFO, TUI_RT_DATE };
diff --git a/docs/STATE.md b/docs/STATE.md
index a108a52..d75f3cb 100644
--- a/docs/STATE.md
+++ b/docs/STATE.md
@@ -102,6 +102,11 @@ server/protocol/ledger only.
closed or already posted year). Årsredovisning and INK2/SRU left the
Rapporter menu, and Ingående balans moved from Bokföring to the
dashboard's Räkenskapsår section next to Räkenskapsår.
+ The form widgets remember focus across runs (`int *focus` as the last
+ argument to `tui_form_run*`: clamped to a selectable row on entry and
+ written back on every return; screens own a `static int sel`), so a
+ sub-screen round-trip or a field save returns the highlight to the
+ same row.
## Pending decisions
diff --git a/docs/TUI-GUIDELINES.md b/docs/TUI-GUIDELINES.md
index 3bed983..3bd3c65 100644
--- a/docs/TUI-GUIDELINES.md
+++ b/docs/TUI-GUIDELINES.md
@@ -65,6 +65,14 @@ written compactly as `^N`, `^A`, `^C`, `^R` to save width.
- Field navigation: `Tab` / `Shift-Tab` forward/back, arrows Up/Down between
rows. `Enter` advances in simple forms; it never saves unless the screen is
a one-line prompt.
+- Focus memory: the screen keeps the focused row in an `int` and passes its
+ address as the last argument to `tui_form_run`, `tui_form_run_hook` and
+ `tui_form_run_actions` (like a list cursor). On entry `*focus` is clamped
+ to a selectable row — out of range snaps to the nearest end, a heading row
+ snaps forward and wraps — and on every return (`Esc`, `F5`, `^Enter`, a
+ chosen action, an edited field) the row is written back, so a re-run after
+ a save or a round-trip through another screen lands on the same row.
+ `NULL` starts at the first focusable row and stores nothing.
- The active field is drawn reverse-video and holds the hardware cursor at the
caret. The caret is a real position: ←/→/Home/End/Del work inside the field,
`Ctrl+U` clears it (see `field_edit`).
@@ -104,8 +112,9 @@ written compactly as `^N`, `^A`, `^C`, `^R` to save width.
`label (reason)`, `Enter` shows the reason in a message; `-1` is a dim
non-selectable heading/status row like a menu section. The call returns
the field index after an edit, `TUI_FORM_ACTION + i` for a chosen action,
- or `TUI_FORM_BACK`/`REFRESH`/`SUBMIT`; `tui_form_hint()` overrides the
- footer of the next run as usual.
+ or `TUI_FORM_BACK`/`REFRESH`/`SUBMIT`, and stores the row it stopped on in
+ the `int *focus` argument (see focus memory above); `tui_form_hint()`
+ overrides the footer of the next run as usual.
- `Bokslut` is the year-end hub in one view: the year fields, the status
heading `Bokslut bokfört: ja/nej` (derived from the `Skatt på årets
resultat` voucher that `bokslut.post` creates; a closed year counts as
diff --git a/tests/test_tui.c b/tests/test_tui.c
index 15a44c7..a024e27 100644
--- a/tests/test_tui.c
+++ b/tests/test_tui.c
@@ -433,6 +433,67 @@ static void test_form_actions(void)
CHECK(tui_form_act_step(1, 0, ACTS, 6, -1) == 5);
}
+static void test_form_focus(void)
+{
+ const int nf = 3;
+
+ /* start focus: a remembered row is kept, out of range clamps to the
+ nearest end, fields are always selectable */
+ CHECK(tui_form_focus_clamp(0, nf, ACTS, 6) == 0);
+ CHECK(tui_form_focus_clamp(2, nf, ACTS, 6) == 2);
+ CHECK(tui_form_focus_clamp(4, nf, ACTS, 6) == 4);
+ CHECK(tui_form_focus_clamp(6, nf, ACTS, 6) == 6); /* dimmed, selectable */
+ CHECK(tui_form_focus_clamp(8, nf, ACTS, 6) == 8);
+ CHECK(tui_form_focus_clamp(-1, nf, ACTS, 6) == 0);
+ CHECK(tui_form_focus_clamp(99, nf, ACTS, 6) == 8);
+
+ /* a heading row snaps forward (and wraps) to a selectable one */
+ CHECK(tui_form_focus_clamp(3, nf, ACTS, 6) == 4); /* action 0 is head */
+ CHECK(tui_form_focus_clamp(7, nf, ACTS, 6) == 8); /* action 4 is head */
+ CHECK(tui_form_focus_clamp(0, 0, ACTS, 6) == 1); /* fields gone */
+ static const struct tui_form_action WRAP[2] = {
+ { "Årsredovisning (K2)", 1, NULL },
+ { "rubrik", -1, NULL },
+ };
+ CHECK(tui_form_focus_clamp(1, 0, WRAP, 2) == 0); /* wraps to the top */
+ static const struct tui_form_action HEADS[2] = {
+ { "rubrik", -1, NULL },
+ { "rubrik", -1, NULL },
+ };
+ CHECK(tui_form_focus_clamp(0, 0, HEADS, 2) == -1);
+ CHECK(tui_form_focus_clamp(-1, 0, HEADS, 2) == -1);
+
+ /* every return writes the current row back; NULL stores nothing */
+ int focus = 0;
+ CHECK(tui_form_focus_store(&focus, 4, TUI_FORM_BACK) == TUI_FORM_BACK);
+ CHECK(focus == 4);
+ CHECK(tui_form_focus_store(&focus, 5, TUI_FORM_REFRESH) ==
+ TUI_FORM_REFRESH);
+ CHECK(focus == 5);
+ CHECK(tui_form_focus_store(&focus, 6, TUI_FORM_SUBMIT) == TUI_FORM_SUBMIT);
+ CHECK(focus == 6);
+ CHECK(tui_form_focus_store(&focus, 8, TUI_FORM_ACTION + 1) ==
+ TUI_FORM_ACTION + 1);
+ CHECK(focus == 8);
+ CHECK(tui_form_focus_store(&focus, 1, 1) == 1 && focus == 1);
+ CHECK(tui_form_focus_store(&focus, -1, TUI_FORM_BACK) == TUI_FORM_BACK);
+ CHECK(focus == -1);
+ CHECK(tui_form_focus_store(NULL, 4, TUI_FORM_BACK) == TUI_FORM_BACK);
+
+ /* two runs in a row, like the hub: walk to an action and choose it, the
+ next run starts on the same row */
+ focus = tui_form_focus_clamp(0, nf, ACTS, 6);
+ CHECK(focus == 0);
+ focus = tui_form_act_key(focus, nf, ACTS, 6, '\t');
+ focus = tui_form_act_key(focus, nf, ACTS, 6, '\t');
+ focus = tui_form_act_key(focus, nf, ACTS, 6, '\t');
+ CHECK(focus == 4); /* Årsredovisning (K2) */
+ CHECK(tui_form_focus_store(&focus, focus, TUI_FORM_ACTION + 1) ==
+ TUI_FORM_ACTION + 1);
+ CHECK(tui_form_focus_clamp(focus, nf, ACTS, 6) == 4);
+ CHECK(tui_form_focus_store(&focus, focus, TUI_FORM_BACK) == TUI_FORM_BACK);
+ CHECK(tui_form_focus_clamp(focus, nf, ACTS, 6) == 4);
+}
static char rt_cells[8][3][64];
static void rt_cb(void *ud, int row, int col, char **buf, size_t *cap)
@@ -617,6 +678,7 @@ int main(void)
test_nav_sections();
test_form_nav();
test_form_actions();
+ test_form_focus();
test_rowtable();
test_rt_fields();
test_rt_focus();