summaryrefslogtreecommitdiff
path: root/clients
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-23 08:56:49 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-23 08:56:49 +0200
commit2ca284619125af0d5c12e9210ff1bfc8f9122739 (patch)
treea304096c2b58492d345e71f205571b913b555a23 /clients
parent278d89825dcb25c4cc55891b84b96b6ca9b84354 (diff)
downloadbokf-2ca284619125af0d5c12e9210ff1bfc8f9122739.tar.gz
bokf-2ca284619125af0d5c12e9210ff1bfc8f9122739.zip
tui: one shared field editor, in-place form editing, value-only focus
Forms edit fields in place in the value column instead of a bottom prompt, highlight only the focused value and scroll when taller than the screen; long text scrolls horizontally instead of wrapping. Fixes: typing after Backspace/Del no longer wipes the field, 'q' is text in row tables, the caret resets between cells, dialogs decode arrow keys, full buffers never take half a UTF-8 character, an invalid amount stays editable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'clients')
-rw-r--r--clients/tui.c933
-rw-r--r--clients/tui.h12
2 files changed, 527 insertions, 418 deletions
diff --git a/clients/tui.c b/clients/tui.c
index 13da593..211df40 100644
--- a/clients/tui.c
+++ b/clients/tui.c
@@ -261,9 +261,40 @@ void tui_le_clear(struct tui_ledit *e)
e->buf[0] = '\0';
}
+static size_t utf8_seq_len(unsigned char c)
+{
+ if ((c & 0xE0) == 0xC0)
+ return 2;
+ if ((c & 0xF0) == 0xE0)
+ return 3;
+ if ((c & 0xF8) == 0xF0)
+ return 4;
+ return 1;
+}
+
+/* Keys arrive one byte at a time: a lead byte is only accepted when its
+ whole sequence fits, and a continuation byte only completes an open
+ sequence, so a full buffer never ends in half a character. */
+static int le_accepts(const struct tui_ledit *e, unsigned char ch)
+{
+ if ((ch & 0xC0) != 0x80)
+ return e->len + utf8_seq_len(ch) < e->cap;
+ size_t start = e->pos, cont = 0;
+ while (start > 0 && cont < 3 &&
+ ((unsigned char)e->buf[start - 1] & 0xC0) == 0x80) {
+ start--;
+ cont++;
+ }
+ if (start == 0)
+ return 0;
+ unsigned char lead = (unsigned char)e->buf[start - 1];
+ return (lead & 0xC0) == 0xC0 && cont + 1 < utf8_seq_len(lead) &&
+ e->len + 1 < e->cap;
+}
+
static void le_insert(struct tui_ledit *e, unsigned char ch)
{
- if (e->len + 1 >= e->cap)
+ if (!le_accepts(e, ch))
return;
memmove(e->buf + e->pos + 1, e->buf + e->pos, e->len - e->pos + 1);
e->buf[e->pos] = (char)ch;
@@ -358,17 +389,9 @@ int tui_date_field_edit(char *buf, size_t cap, size_t *pos, int ch,
ci = seen;
}
- if (*fresh) {
- int editing = ch >= '0' && ch <= '9';
- int moving = ch == KEY_LEFT || ch == KEY_RIGHT || ch == KEY_HOME ||
- ch == KEY_END || ch == 1 || ch == 5;
- if (editing || moving) {
- if (editing) {
- n = 0;
- ci = 0;
- }
- *fresh = 0; /* movement keeps the date and moves the caret */
- }
+ if (*fresh && ch >= '0' && ch <= '9') {
+ n = 0;
+ ci = 0;
}
if (ch >= '0' && ch <= '9') {
@@ -412,10 +435,10 @@ int tui_date_field_edit(char *buf, size_t cap, size_t *pos, int ch,
} else if (ch == 21) {
n = 0;
ci = 0;
- *fresh = 0;
} else {
return 0; /* not a date editing key */
}
+ *fresh = 0; /* any edit or caret move ends the replace-on-type state */
/* rebuild "YYYY-MM-DD" (partial while typing) */
char out[16];
@@ -445,50 +468,147 @@ int tui_field_edit(char *buf, size_t cap, size_t *pos, int ch, int *fresh)
tui_le_init(&e, buf, cap);
if (*pos != (size_t)-1 && *pos <= e.len)
e.pos = *pos;
- if (*fresh) {
- int editing = ch >= 32 && ch < 256;
- int moving = ch == KEY_LEFT || ch == KEY_RIGHT || ch == KEY_HOME ||
- ch == KEY_END || ch == 1 || ch == 5;
- if (editing || moving) {
- if (editing)
- tui_le_clear(&e);
- *fresh = 0; /* movement keeps the content and moves the caret */
- }
- }
+ if (*fresh && ch >= 32 && ch < 256 && ch != 127)
+ tui_le_clear(&e); /* the first printable replaces the value */
int handled = tui_le_key(&e, ch);
+ if (handled)
+ *fresh = 0; /* Backspace/Del/^U/moves edit what is there */
*pos = e.pos;
return handled;
}
-int tui_edit_field(int y, int x, char *buf, size_t cap, int mask)
+size_t tui_field_scroll(const char *buf, size_t pos, int width, size_t start)
{
- size_t pos = (size_t)-1;
- int fresh = 1;
- for (;;) {
- move(y, x);
- if (mask) {
+ size_t len = strlen(buf);
+ if (width < 1)
+ width = 1;
+ if (pos > len)
+ pos = len;
+ if (start > len)
+ start = 0;
+ while (start > 0 && ((unsigned char)buf[start] & 0xC0) == 0x80)
+ start--;
+ if (start > pos)
+ start = pos;
+ /* the caret needs a column of its own, also after the last character */
+ while (tui_disp_width_n(buf + start, pos - start) > width - 1) {
+ start++;
+ while (start < pos && ((unsigned char)buf[start] & 0xC0) == 0x80)
+ start++;
+ }
+ /* scroll back when text was removed and more of it fits again */
+ while (start > 0) {
+ size_t p = start - 1;
+ while (p > 0 && ((unsigned char)buf[p] & 0xC0) == 0x80)
+ p--;
+ if (tui_disp_width(buf + p) > width - 1)
+ break;
+ start = p;
+ }
+ return start;
+}
+
+/* Paints width columns at (y,x) in attr: buf from byte `start`, clipped and
+ padded, '*' per character when masked. Returns the caret column. */
+static int field_paint(int y, int x, int width, const char *buf, size_t pos,
+ int mask, size_t start, int attr)
+{
+ char vis[1024];
+ if (mask) {
+ int n = tui_disp_width(buf + start);
+ if (n > (int)sizeof vis - 1)
+ n = (int)sizeof vis - 1;
+ memset(vis, '*', (size_t)n);
+ vis[n] = '\0';
+ } else {
+ snprintf(vis, sizeof vis, "%s", buf + start);
+ }
+ tui_pad_field(vis, sizeof vis, width);
+ attrset(attr);
+ mvaddstr(y, x, vis);
+ attrset(A_NORMAL);
+ size_t len = strlen(buf);
+ if (pos > len)
+ pos = len;
+ int cx = pos > start ? tui_disp_width_n(buf + start, pos - start) : 0;
+ return x + (cx < width ? cx : width - 1);
+}
+
+#define FIELD_EDIT_HINT \
+ "Enter = klar Esc = ångra vänster/höger/Home/End = flytta" \
+ " ^U = töm"
+
+/* The one field editor: edits buf in place inside width columns at (y,x),
+ scrolling horizontally so the caret stays visible. `first` (0 = none) is
+ applied before the first paint, so typing on a focused field starts
+ editing with that key. Returns 1 on Enter, 2 on Tab, 3 on Shift-Tab (only
+ when `tabs`), 0 on Esc. The caller owns commit/restore. */
+static int field_loop(int y, int x, int width, char *buf, size_t cap,
+ int mask, int date, int tabs, int fresh, int first)
+{
+ size_t pos = (size_t)-1, start = 0;
+ int prev = curs_set(1);
+ int rc = 0;
+ if (g_hints)
+ g_hints(FIELD_EDIT_HINT);
+ for (int ch = first;; ch = 0) {
+ if (!ch) {
size_t len = strlen(buf);
- for (size_t i = 0; i < len; i++)
- addch('*');
- } else {
- addnstr(buf, (int)cap);
+ size_t cp = pos == (size_t)-1 || pos > len ? len : pos;
+ start = tui_field_scroll(buf, cp, width, start);
+ int cx = field_paint(y, x, width, buf, cp, mask, start,
+ (int)A_REVERSE);
+ move(y, cx);
+ refresh();
+ ch = in_key();
}
- clrtoeol();
- size_t len = strlen(buf);
- size_t cp = pos == (size_t)-1 || pos > len ? len : pos;
- move(y, x + (mask ? (int)cp : tui_disp_width_n(buf, cp)));
- refresh();
- int ch = in_key();
- if (ch == '\n' || ch == '\r' || ch == KEY_ENTER)
- return 1;
- if (ch == 27)
- return 0;
- if (ch == '\t')
- return 2;
- if (ch == KEY_BTAB)
- return 3;
- tui_field_edit(buf, cap, &pos, ch, &fresh);
+ if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
+ rc = 1;
+ break;
+ }
+ if (ch == 27) {
+ rc = 0;
+ break;
+ }
+ if (tabs && (ch == '\t' || ch == KEY_BTAB)) {
+ rc = ch == '\t' ? 2 : 3;
+ break;
+ }
+ if (date)
+ tui_date_field_edit(buf, cap, &pos, ch, &fresh);
+ else
+ tui_field_edit(buf, cap, &pos, ch, &fresh);
}
+ curs_set(prev == ERR ? 0 : prev);
+ return rc;
+}
+
+/* Edits a copy of buf; only Enter/Tab write it back, Esc leaves buf as it
+ was. Same return codes as field_loop. */
+static int field_scratch(int y, int x, int width, char *buf, size_t cap,
+ const char *def, int mask, int date, int tabs,
+ int first)
+{
+ if (!cap)
+ return 0;
+ char *scratch = xcalloc(cap, 1);
+ snprintf(scratch, cap, "%s", def ? def : buf);
+ int rc = field_loop(y, x, width, scratch, cap, mask, date, tabs, 1, first);
+ if (rc)
+ snprintf(buf, cap, "%s", scratch);
+ free(scratch);
+ return rc;
+}
+
+static int field_width(int x)
+{
+ int w = COLS - 2 - x;
+ return w < 1 ? 1 : w;
+}
+
+int tui_edit_field(int y, int x, char *buf, size_t cap, int mask)
+{
+ return field_loop(y, x, field_width(x), buf, cap, mask, 0, 1, 1, 0);
}
int tui_parse_kr(const char *s, int64_t *out)
@@ -613,20 +733,55 @@ void tui_amt_col(char *buf, size_t n, int width, int64_t ore)
/* modal dialogs */
/* ------------------------------------------------------------------ */
-/* Center a box for a fixed number of lines and draw the frame. */
-static WINDOW *dlg_open(const char *title, int h, int w)
+/* Centered box with the text and a dim key footer. Keys are read through
+ the application's input hook (keypad decoding, ^C/^R), so an arrow key
+ cannot leak escape-sequence bytes into the next view. */
+static WINDOW *dlg_open(const char *title, const char *text,
+ const char *footer)
{
+ int lines = 1, maxw = 0, cur = 0;
+ for (const unsigned char *p = (const unsigned char *)text; *p; p++) {
+ if (*p == '\n') {
+ lines++;
+ cur = 0;
+ } else if ((*p & 0xC0) != 0x80 && ++cur > maxw) {
+ maxw = cur;
+ }
+ }
+ int fw = tui_disp_width(footer) + 4, tw = tui_disp_width(title) + 6;
+ int h = lines + 4, w = maxw + 4;
+ if (w < fw)
+ w = fw;
+ if (w < tw)
+ w = tw;
if (w > COLS - 4)
w = COLS - 4;
if (w < 30)
w = 30;
if (h > LINES - 2)
h = LINES - 2;
+ refresh(); /* flush the view first so reading a key cannot repaint it */
WINDOW *win = newwin(h, w, (LINES - h) / 2, (COLS - w) / 2);
box(win, 0, 0);
wattrset(win, tui_style_attrs(TUI_TITLE, g_colours, 0));
mvwaddnstr(win, 0, 2, title, w - 4);
wattrset(win, A_NORMAL);
+ int row = 2;
+ char *copy = xstrdup(text);
+ for (char *p = copy; p && *p && row < h - 1; row++) {
+ char *nl = strchr(p, '\n');
+ if (nl)
+ *nl = '\0';
+ char line[1024];
+ snprintf(line, sizeof line, "%s", p);
+ tui_pad_field(line, sizeof line, w - 4);
+ mvwaddstr(win, row, 2, line);
+ p = nl ? nl + 1 : NULL;
+ }
+ free(copy);
+ wattrset(win, tui_style_attrs(TUI_DIM, g_colours, 0));
+ mvwaddstr(win, h - 1, w - 2 - tui_disp_width(footer), footer);
+ wattrset(win, A_NORMAL);
wrefresh(win);
nodelay(stdscr, FALSE);
timeout(-1);
@@ -640,54 +795,33 @@ static void dlg_close(WINDOW *win)
refresh();
}
-void tui_message(const char *title, const char *fmt, ...)
+/* Waits for a dialog key; returns 1 for a "yes" key, 0 for a "no" key. */
+static int dlg_wait(int yes_no)
{
- char text[2048];
- va_list ap;
- va_start(ap, fmt);
- vsnprintf(text, sizeof text, fmt, ap);
- va_end(ap);
- int lines = 1;
- size_t maxw = 0, cur = 0;
- for (const char *p = text; *p; p++) {
- if (*p == '\n') {
- lines++;
- if (cur > maxw)
- maxw = cur;
- cur = 0;
- } else {
- cur++;
- }
- }
- if (cur > maxw)
- maxw = cur;
- int h = lines + 4, w = (int)maxw + 4;
- WINDOW *win = dlg_open(title, h, w);
- int row = 2;
- char *copy = xstrdup(text);
- for (char *p = copy; p && *p; row++) {
- char *nl = strchr(p, '\n');
- if (nl)
- *nl = '\0';
- mvwaddnstr(win, row, 2, p, w - 4);
- p = nl ? nl + 1 : NULL;
- }
- free(copy);
- wattrset(win, tui_style_attrs(TUI_DIM, g_colours, 0));
- mvwaddstr(win, h - 1, w - 13, " tryck Enter");
- wattrset(win, A_NORMAL);
- wrefresh(win);
for (;;) {
- int ch = wgetch(win);
+ int ch = in_key();
if (ch == 3) {
if (g_quit)
g_quit();
- break;
+ return 0;
}
- if (ch == '\n' || ch == '\r' || ch == ' ' || ch == 27 ||
- ch == KEY_ENTER)
- break;
+ if (ch == '\n' || ch == '\r' || ch == KEY_ENTER ||
+ (yes_no && (ch == 'j' || ch == 'J')) || (!yes_no && ch == ' '))
+ return 1;
+ if (ch == 27 || (yes_no && (ch == 'n' || ch == 'N')))
+ return 0;
}
+}
+
+void tui_message(const char *title, const char *fmt, ...)
+{
+ char text[2048];
+ va_list ap;
+ va_start(ap, fmt);
+ vsnprintf(text, sizeof text, fmt, ap);
+ va_end(ap);
+ WINDOW *win = dlg_open(title, text, " tryck Enter ");
+ dlg_wait(0);
dlg_close(win);
}
@@ -699,52 +833,8 @@ int tui_confirm(const char *title, const char *fmt, ...)
va_start(ap, fmt);
vsnprintf(text, sizeof text, fmt, ap);
va_end(ap);
- int lines = 1;
- size_t maxw = 0, cur = 0;
- for (const char *p = text; *p; p++) {
- if (*p == '\n') {
- lines++;
- if (cur > maxw)
- maxw = cur;
- cur = 0;
- } else {
- cur++;
- }
- }
- if (cur > maxw)
- maxw = cur;
- int h = lines + 4, w = (int)maxw + 4;
- WINDOW *win = dlg_open(title, h, w);
- int row = 2;
- char *copy = xstrdup(text);
- for (char *p = copy; p && *p; row++) {
- char *nl = strchr(p, '\n');
- if (nl)
- *nl = '\0';
- mvwaddnstr(win, row, 2, p, w - 4);
- p = nl ? nl + 1 : NULL;
- }
- free(copy);
- wattrset(win, tui_style_attrs(TUI_DIM, g_colours, 0));
- mvwaddstr(win, h - 1, w - 25, " j = ja n = nej");
- wattrset(win, A_NORMAL);
- wrefresh(win);
- int yes = 0;
- for (;;) {
- int ch = wgetch(win);
- if (ch == 3) {
- if (g_quit)
- g_quit();
- break;
- }
- if (ch == 'j' || ch == 'J' || ch == '\n' || ch == '\r' ||
- ch == KEY_ENTER) {
- yes = 1;
- break;
- }
- if (ch == 'n' || ch == 'N' || ch == 27)
- break;
- }
+ WINDOW *win = dlg_open(title, text, " j = ja n = nej ");
+ int yes = dlg_wait(1);
dlg_close(win);
return yes;
}
@@ -753,117 +843,108 @@ int tui_confirm(const char *title, const char *fmt, ...)
/* prompts (write into the caller's buffer, 0 on Esc) */
/* ------------------------------------------------------------------ */
-int tui_prompt_into(char *buf, size_t cap, const char *label,
- const char *def, int mask)
+/* Draws the prompt label on the prompt line and returns where input starts. */
+static int prompt_label(const char *label, int gap)
{
- char scratch[512];
- size_t scap = cap < sizeof scratch ? cap : sizeof scratch;
- snprintf(scratch, scap, "%s", def ? def : buf);
-
int y = LINES - 3;
- move(y, 2);
+ move(y, 1);
clrtoeol();
tui_style(TUI_HEADING);
- mvaddstr(y, 2, label);
+ mvaddnstr(y, 2, label, COLS - 4);
tui_style_reset();
- refresh();
- int prev = curs_set(1);
- int rc = tui_edit_field(y, (int)strlen(label) + 3, scratch, scap, mask);
- curs_set(prev == ERR ? 0 : prev);
- if (rc != 0)
- snprintf(buf, cap, "%s", scratch);
- return rc != 0;
+ int x = 2 + tui_disp_width(label) + gap;
+ return x < COLS - 3 ? x : COLS - 3;
+}
+
+int tui_prompt_into(char *buf, size_t cap, const char *label,
+ const char *def, int mask)
+{
+ int x = prompt_label(label, 1);
+ return field_scratch(LINES - 3, x, field_width(x), buf, cap, def, mask, 0,
+ 1, 0) != 0;
}
int tui_date_prompt_into(char *buf, size_t cap, const char *label,
const char *def)
{
- char scratch[16];
- size_t scap = cap < sizeof scratch ? cap : sizeof scratch;
- snprintf(scratch, scap, "%s", def ? def : buf);
+ int x = prompt_label(label, 1);
+ return field_scratch(LINES - 3, x, field_width(x), buf, cap, def, 0, 1, 0,
+ 0) != 0;
+}
- int y = LINES - 3;
- move(y, 2);
- clrtoeol();
- tui_style(TUI_HEADING);
- mvaddstr(y, 2, label);
- tui_style_reset();
- size_t pos = (size_t)-1;
+/* Amount editor at (y,x): an unparsable amount is reported and the typed
+ text stays in the field for correction. Returns 1..3 like field_loop. */
+static int amount_loop(int y, int x, int width, int64_t *out, int64_t def_ore,
+ int tabs, int first)
+{
+ char buf[48];
+ tui_kr_format(def_ore, buf, sizeof buf);
int fresh = 1;
- int prev = curs_set(1);
- int rc = 0;
for (;;) {
- move(y, (int)strlen(label) + 3);
- clrtoeol();
- addnstr(scratch, (int)scap);
- clrtoeol();
- size_t slen = strlen(scratch);
- size_t cp = pos == (size_t)-1 || pos > slen ? slen : pos;
- move(y, (int)strlen(label) + 3 + (int)tui_disp_width_n(scratch, cp));
- refresh();
- int ch = in_key();
- if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
- rc = 1;
- break;
+ int rc = field_loop(y, x, width, buf, sizeof buf, 0, 0, tabs, fresh,
+ first);
+ if (!rc)
+ return 0;
+ int64_t v;
+ if (tui_parse_kr(buf, &v) == 0) {
+ *out = v;
+ return rc;
}
- if (ch == 27)
- break;
- tui_date_field_edit(scratch, scap, &pos, ch, &fresh);
+ tui_message("Ogiltigt belopp", "%s", buf);
+ fresh = 0;
+ first = 0;
}
- curs_set(prev == ERR ? 0 : prev);
- if (rc)
- snprintf(buf, cap, "%s", scratch);
- return rc;
}
int tui_amount_prompt_into(int64_t *out, const char *label, int64_t def_ore)
{
- char def[48], buf[48];
- tui_kr_format(def_ore, def, sizeof def);
- if (!tui_prompt_into(buf, sizeof buf, label, def, 0))
- return 0;
- if (tui_parse_kr(buf, out) != 0) {
- tui_message("Ogiltigt belopp", "%s", buf);
- return 0;
- }
- return 1;
+ int x = prompt_label(label, 1);
+ return amount_loop(LINES - 3, x, field_width(x), out, def_ore, 0, 0) != 0;
}
-int tui_choice_prompt(const char *label, const char *const *opts, int n,
- int cur)
+/* "< value >" at (y,x); left/right cycle, Enter picks, Esc cancels.
+ Returns the chosen index or -1 (Tab/Shift-Tab pick too when `tabs`, with
+ *how set to 2/3; Enter sets 1). */
+static int choice_loop(int y, int x, int width, const char *const *opts,
+ int n, int cur, int tabs, int *how)
{
if (n <= 0)
return -1;
if (cur < 0 || cur >= n)
cur = 0;
- int y = LINES - 3;
int prev = curs_set(0);
+ int ret = -1;
+ if (g_hints)
+ g_hints("vänster/höger = välj Enter = OK Esc = avbryt");
for (;;) {
- move(y, 2);
- clrtoeol();
char line[512];
- snprintf(line, sizeof line, "%s< %s >", label, opts[cur]);
- tui_style(TUI_HEADING);
- mvaddnstr(y, 2, line, COLS - 4);
- tui_style_reset();
- char hint[256];
- snprintf(hint, sizeof hint, "vänster/höger = välj Enter = OK "
- "Esc = avbryt");
- g_hints ? g_hints(hint) : (void)0;
+ snprintf(line, sizeof line, "< %s >", opts[cur]);
+ field_paint(y, x, width, line, 0, 0, 0, (int)A_REVERSE);
refresh();
int ch = in_key();
- if (ch == KEY_LEFT)
+ if (ch == KEY_LEFT) {
cur = cur > 0 ? cur - 1 : n - 1;
- else if (ch == KEY_RIGHT)
+ } else if (ch == KEY_RIGHT || ch == ' ') {
cur = cur + 1 < n ? cur + 1 : 0;
- else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
- curs_set(prev == ERR ? 0 : prev);
- return cur;
+ } else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER ||
+ (tabs && (ch == '\t' || ch == KEY_BTAB))) {
+ if (how)
+ *how = ch == '\t' ? 2 : ch == KEY_BTAB ? 3 : 1;
+ ret = cur;
+ break;
} else if (ch == 27) {
- curs_set(prev == ERR ? 0 : prev);
- return -1;
+ break;
}
}
+ curs_set(prev == ERR ? 0 : prev);
+ return ret;
+}
+
+int tui_choice_prompt(const char *label, const char *const *opts, int n,
+ int cur)
+{
+ int x = prompt_label(label, 0);
+ return choice_loop(LINES - 3, x, field_width(x), opts, n, cur, 0, NULL);
}
/* ------------------------------------------------------------------ */
@@ -1715,85 +1796,178 @@ int tui_action_menu(const char *title, const struct tui_action *acts, int n,
return ret;
}
-/* Action rows sit under the fields, one blank line below them. */
-static void form_draw_actions(const struct tui_form_action *acts, int na,
- int nf, int sel)
+/* 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
+#define FORM_LABEL_W 31
+#define FORM_VALUE_X 34
+#define FORM_FOCUS_MIN_W 24
+
+static void field_value(const struct tui_form_field *f, char *val, size_t n)
{
- int y = 5 + nf + 1;
- for (int i = 0; i < na; i++) {
- if (y + i >= LINES - 2)
- break;
- char line[832];
- tui_form_act_label(&acts[i], line, sizeof line);
- int dim = acts[i].enabled != 1;
- if (dim)
- attron(tui_style_attrs(TUI_DIM, g_colours, 0));
- if (sel == nf + i)
- attron(A_REVERSE);
- mvaddnstr(y + i, 2, line, COLS - 4);
- if (sel == nf + i)
- attroff(A_REVERSE);
- if (dim)
- attroff(tui_style_attrs(TUI_DIM, g_colours, 0));
+ if (f->kind == TUI_F_ACTION) {
+ snprintf(val, n, "%s", f->value ? f->value : "(lista)");
+ } else if (f->kind == TUI_F_AMOUNT && f->ore) {
+ tui_kr_format(*f->ore, val, n);
+ } else if (f->kind == TUI_F_CHOICE && f->choice && f->choices) {
+ int c = *f->choice;
+ snprintf(val, n, "%s",
+ c >= 0 && c < f->nchoices ? f->choices[c] : "");
+ } else if (f->mask && f->kind == TUI_F_TEXT) {
+ size_t vl = f->value ? (size_t)tui_disp_width(f->value) : 0;
+ if (vl >= n)
+ vl = n - 1;
+ memset(val, '*', vl);
+ val[vl] = '\0';
+ } else {
+ snprintf(val, n, "%s", f->value ? f->value : "");
+ }
+}
+
+static void form_header(void)
+{
+ tui_style(TUI_HEADING);
+ mvaddstr(3, 2, "Uppgift");
+ mvaddstr(3, FORM_VALUE_X, "Värde");
+ tui_style_reset();
+}
+
+/* One field row. The focused row has a bold label and a reverse-video
+ value box (at least FORM_FOCUS_MIN_W wide so an empty field shows);
+ the label itself is never highlighted. */
+static void form_field_row(int y, const struct tui_form_field *f, int focused,
+ int dim)
+{
+ char lbl[160], val[640];
+ snprintf(lbl, sizeof lbl, "%s", f->label);
+ tui_pad_field(lbl, sizeof lbl, FORM_LABEL_W);
+ int plain = dim ? tui_style_attrs(TUI_DIM, g_colours, 0) : (int)A_NORMAL;
+ attrset(focused ? tui_style_attrs(TUI_ACCENT, g_colours, 0) : plain);
+ mvaddstr(y, 2, lbl);
+ attrset(A_NORMAL);
+ field_value(f, val, sizeof val);
+ int vw = field_width(FORM_VALUE_X);
+ if (focused) {
+ int w = tui_disp_width(val) + 1;
+ if (w < FORM_FOCUS_MIN_W)
+ w = FORM_FOCUS_MIN_W;
+ field_paint(y, FORM_VALUE_X, w < vw ? w : vw, val, 0, 0, 0,
+ (int)A_REVERSE);
+ } else {
+ field_paint(y, FORM_VALUE_X, vw, val, 0, 0, 0, plain);
}
}
+/* Edits field f in place on row y (the value column). `first` is a key
+ that starts the edit (0 = none). Returns 0 when cancelled, else 1 for
+ Enter, 2 for Tab, 3 for Shift-Tab; the field holds the new value. */
+static int form_field_edit(struct tui_form_field *f, int y, int first)
+{
+ int x = FORM_VALUE_X, w = field_width(x);
+ attrset(tui_style_attrs(TUI_ACCENT, g_colours, 0));
+ char lbl[160];
+ snprintf(lbl, sizeof lbl, "%s", f->label);
+ tui_pad_field(lbl, sizeof lbl, FORM_LABEL_W);
+ mvaddstr(y, 2, lbl);
+ attrset(A_NORMAL);
+ if (f->kind == TUI_F_TEXT || f->kind == TUI_F_DATE) {
+ if (!f->value)
+ return 0;
+ return field_scratch(y, x, w, f->value, f->cap, NULL, f->mask,
+ f->kind == TUI_F_DATE, 1, first);
+ }
+ if (f->kind == TUI_F_AMOUNT && f->ore) {
+ int64_t v = *f->ore;
+ int rc = amount_loop(y, x, w, &v, *f->ore, 1, first);
+ if (rc)
+ *f->ore = v;
+ return rc;
+ }
+ if (f->kind == TUI_F_CHOICE && f->choice && !first) {
+ int how = 0;
+ int c = choice_loop(y, x, w, f->choices, f->nchoices, *f->choice, 1,
+ &how);
+ if (c < 0)
+ return 0;
+ *f->choice = c;
+ return how;
+ }
+ return 0;
+}
+
+/* Whether typing a printable key on the focused field starts editing it. */
+static int field_types(const struct tui_form_field *f)
+{
+ return (f->kind == TUI_F_TEXT || f->kind == TUI_F_DATE) ? f->value != NULL
+ : f->kind == TUI_F_AMOUNT ? f->ore != NULL
+ : 0;
+}
+
+/* Scroll offset (in form lines) that keeps line `sl` of `nlines` visible
+ in a view of `view` lines. Pure. */
+int tui_form_scroll(int top, int sl, int nlines, int view)
+{
+ if (view < 1)
+ view = 1;
+ if (sl < top)
+ top = sl;
+ if (sl >= top + view)
+ top = sl - view + 1;
+ if (top + view > nlines)
+ top = nlines - view;
+ return top < 0 ? 0 : top;
+}
+
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,
int *focus)
{
int sel = tui_form_focus_clamp(focus ? *focus : -1, nf, acts, na);
+ int top = 0;
+ if (na < 0)
+ na = 0;
for (;;) {
- int ntot = nf + (na > 0 ? na : 0);
+ int ntot = nf + na;
if (ntot <= 0)
return tui_form_focus_store(focus, sel, TUI_FORM_BACK);
if (sel < 0)
sel = tui_form_act_first(nf, acts, na);
if (sel >= ntot)
sel = ntot - 1;
+ /* form lines: the fields, a blank line, then the action rows */
+ int nlines = nf + (na > 0 ? na + 1 : 0);
+ int view = LINES - 3 - FORM_Y;
+ if (view < 1)
+ view = 1;
+ int sl = sel < 0 ? 0 : sel < nf ? sel : sel + 1;
+ top = tui_form_scroll(top, sl, nlines, view);
tui_frame(title);
- tui_style(TUI_HEADING);
- mvaddstr(3, 2, "Uppgift");
- mvaddstr(3, 34, "Värde");
- tui_style_reset();
- for (int i = 0; i < nf; i++) {
- char lbl[160], val[640], line[832];
- snprintf(lbl, sizeof lbl, "%s", f[i].label);
- tui_pad_field(lbl, sizeof lbl, 31);
- if (f[i].kind == TUI_F_ACTION) {
- snprintf(val, sizeof val, "%s",
- f[i].value ? f[i].value : "(lista)");
- } else if (f[i].kind == TUI_F_AMOUNT && f[i].ore) {
- tui_kr_format(*f[i].ore, val, sizeof val);
- } else if (f[i].kind == TUI_F_CHOICE && f[i].choice &&
- f[i].choices) {
- int c = *f[i].choice;
- snprintf(val, sizeof val, "%s",
- c >= 0 && c < f[i].nchoices ? f[i].choices[c] : "");
- } else if (f[i].mask && f[i].kind == TUI_F_TEXT) {
- size_t vl = f[i].value ? strlen(f[i].value) : 0;
- if (vl >= sizeof val)
- vl = sizeof val - 1;
- memset(val, '*', vl);
- val[vl] = '\0';
- } else {
- snprintf(val, sizeof val, "%s",
- f[i].value ? f[i].value : "");
+ form_header();
+ for (int l = top; l < nlines && l < top + view; l++) {
+ int y = FORM_Y + l - top;
+ if (l < nf) {
+ form_field_row(y, &f[l], l == sel, !can_edit && l != sel);
+ } else if (l > nf) {
+ const struct tui_form_action *a = &acts[l - nf - 1];
+ char line[832];
+ tui_form_act_label(a, line, sizeof line);
+ int attr = a->enabled != 1
+ ? tui_style_attrs(TUI_DIM, g_colours, 0)
+ : (int)A_NORMAL;
+ if (sel == l - 1)
+ attr |= A_REVERSE;
+ attrset(attr);
+ mvaddnstr(y, 2, line, COLS - 4);
+ attrset(A_NORMAL);
}
- snprintf(line, sizeof line, "%s %s", lbl, val);
- if (i == sel)
- attron(A_REVERSE);
- else if (!can_edit)
- attron(tui_style_attrs(TUI_DIM, g_colours, 0));
- mvaddnstr(5 + i, 2, line, COLS - 4);
- if (i == sel)
- attroff(A_REVERSE);
- else if (!can_edit)
- attroff(tui_style_attrs(TUI_DIM, g_colours, 0));
}
- if (na > 0)
- form_draw_actions(acts, na, nf, sel);
+ tui_style(TUI_DIM);
+ if (top > 0)
+ mvaddstr(FORM_Y - 1, 2, "↑ fler");
+ if (top + view < nlines)
+ mvaddstr(FORM_Y + view, 2, "↓ fler");
+ tui_style_reset();
const char *h = g_form_hint ? g_form_hint : hint;
if (!h)
h = na > 0
@@ -1863,31 +2037,18 @@ static int form_run(const char *title, struct tui_form_field *f, int nf,
tui_message(title, "Endast behöriga kan ändra.");
continue;
}
- struct tui_form_field *fl = &f[sel];
- if (fl->kind == TUI_F_ACTION)
+ if (f[sel].kind == TUI_F_ACTION)
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 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 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 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 tui_form_focus_store(focus, sel, sel);
- }
- }
+ int rc = form_field_edit(&f[sel], FORM_Y + sel - top, 0);
+ if (!rc)
+ continue;
+ /* Tab/Shift-Tab commit and move on; Enter stays on the field */
+ int next = rc == 1 ? sel
+ : tui_form_act_step(sel, nf, acts, na,
+ rc == 2 ? 1 : -1);
+ if (focus)
+ *focus = next;
+ return sel;
}
}
@@ -2080,6 +2241,8 @@ int tui_rt_move(struct tui_rt *t, int dr, int dc)
moved = 1;
}
t->fresh = 1;
+ t->pos = (size_t)-1; /* a new cell starts with the caret at its end */
+ t->hscroll = 0;
return moved;
}
@@ -2112,87 +2275,34 @@ int tui_rt_clear(struct tui_rt *t)
tui_rt_normalize(t);
t->fresh = 1;
t->pos = (size_t)-1;
+ t->hscroll = 0;
return 1;
}
-static void rt_field_value(const struct tui_form_field *f, char *val,
- size_t n)
-{
- if (f->kind == TUI_F_ACTION) {
- snprintf(val, n, "%s", f->value ? f->value : "(lista)");
- } else if (f->kind == TUI_F_AMOUNT && f->ore) {
- tui_kr_format(*f->ore, val, n);
- } else if (f->kind == TUI_F_CHOICE && f->choice && f->choices) {
- int c = *f->choice;
- snprintf(val, n, "%s",
- c >= 0 && c < f->nchoices ? f->choices[c] : "");
- } else if (f->mask && f->kind == TUI_F_TEXT) {
- size_t vl = f->value ? strlen(f->value) : 0;
- if (vl >= n)
- vl = n - 1;
- memset(val, '*', vl);
- val[vl] = '\0';
- } else {
- snprintf(val, n, "%s", f->value ? f->value : "");
- }
-}
-
static void rt_draw_fields(const struct tui_rt *t)
{
if (t->nfields <= 0)
return;
- tui_style(TUI_HEADING);
- mvaddstr(3, 2, "Uppgift");
- mvaddstr(3, 34, "Värde");
- tui_style_reset();
- for (int i = 0; i < t->nfields; i++) {
- char lbl[160], val[640], line[832];
- snprintf(lbl, sizeof lbl, "%s", t->fields[i].label);
- tui_pad_field(lbl, sizeof lbl, 31);
- rt_field_value(&t->fields[i], val, sizeof val);
- snprintf(line, sizeof line, "%s %s", lbl, val);
- if (i == t->focus)
- attron(A_REVERSE);
- mvaddnstr(5 + i, 2, line, COLS - 4);
- if (i == t->focus)
- attroff(A_REVERSE);
- }
+ form_header();
+ for (int i = 0; i < t->nfields; i++)
+ form_field_row(FORM_Y + i, &t->fields[i], i == t->focus, 0);
}
-/* Opens the editor for header field i; on success focus advances to the
- next field (or the table). Esc cancels and keeps the focus. */
-static void rt_edit_field(struct tui_rt *t, int i)
+/* Edits header field i in place (`first` = the key that started it, or 0).
+ On a commit the focus moves on (Shift-Tab: back); Esc keeps it. */
+static void rt_edit_field(struct tui_rt *t, int i, int first)
{
if (i < 0 || i >= t->nfields)
return;
- struct tui_form_field *fl = &t->fields[i];
- char label[192];
- snprintf(label, sizeof label, "%s: ", fl->label);
- int ok = 0;
- if (fl->kind == TUI_F_TEXT) {
- if (fl->value && tui_prompt_into(fl->value, fl->cap, label, fl->value,
- fl->mask))
- ok = 1;
- } else if (fl->kind == TUI_F_DATE) {
- if (fl->value && tui_date_prompt_into(fl->value, fl->cap, label,
- fl->value))
- ok = 1;
- } else if (fl->kind == TUI_F_AMOUNT) {
- if (fl->ore && tui_amount_prompt_into(fl->ore, label, *fl->ore))
- ok = 1;
- } 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;
- ok = 1;
- }
- }
- if (!ok)
+ int rc = form_field_edit(&t->fields[i], FORM_Y + i, first);
+ if (!rc)
return;
tui_rt_normalize(t);
t->focus = tui_rt_focus_step(i, t->nfields, &t->row, &t->col, t->nrows,
- t->neditable, 1);
+ t->neditable, rc == 3 ? -1 : 1);
+ t->fresh = 1;
+ t->pos = (size_t)-1;
+ t->hscroll = 0;
}
/* Returns -1 back, -2 refresh, -3 submit; otherwise an edited key was
@@ -2200,13 +2310,14 @@ static void rt_edit_field(struct tui_rt *t, int i)
int tui_rt_run(const char *title, struct tui_rt *t, const char *hint)
{
int top = 0;
- int prev_cursor = curs_set(1);
+ int prev_cursor = curs_set(0);
+ int ret;
for (;;) {
if (g_frame)
g_frame(title);
int y = t->y;
- if (t->nfields > 0 && y < t->nfields + 6)
- y = t->nfields + 6; /* fields at 5.., column header, then rows */
+ if (t->nfields > 0 && y < t->nfields + FORM_Y + 1)
+ y = t->nfields + FORM_Y + 1; /* fields, column header, rows */
int view = LINES - 2 - y;
if (view < 1)
view = 1;
@@ -2228,47 +2339,33 @@ int tui_rt_run(const char *title, struct tui_rt *t, const char *hint)
}
tui_style_reset();
int caret_y = -1, caret_x = -1;
- if (t->focus >= 0 && t->focus < t->nfields) {
- char val[640];
- rt_field_value(&t->fields[t->focus], val, sizeof val);
- caret_y = 5 + t->focus;
- caret_x = 34 + (int)tui_disp_width(val);
- if (caret_x > COLS - 1)
- caret_x = COLS - 1;
- }
int ei = 0;
for (int i = 0; i < t->ncols; i++) {
- int focused_col = (t->focus < 0 &&
- t->cols[i].kind != TUI_RT_INFO && ei == t->col);
- if (t->cols[i].kind != TUI_RT_INFO)
- ei++;
- if (t->cols[i].kind == TUI_RT_INFO) {
+ if (t->cols[i].kind == TUI_RT_INFO)
continue;
- }
+ int focused_col = t->focus < 0 && ei == t->col;
+ ei++;
for (int v = 0; v < view && top + v < t->nrows; v++) {
int row = top + v;
char *buf = NULL;
size_t cap = 0;
rt_get(t, row, i, &buf, &cap);
- char padded[512];
- snprintf(padded, sizeof padded, "%s", buf ? buf : "");
- tui_pad_field(padded, sizeof padded, t->cols[i].width);
- int vy = y + v;
+ const char *text = buf ? buf : "";
if (focused_col && row == t->row) {
- attron(A_REVERSE);
- caret_y = vy;
- size_t slen = strlen(buf ? buf : "");
- size_t cp = (t->fresh || t->pos == (size_t)-1 ||
- t->pos > slen)
+ size_t slen = strlen(text);
+ size_t cp = t->pos == (size_t)-1 || t->pos > slen
? slen
: t->pos;
- int cw = tui_disp_width_n(buf ? buf : "", cp);
- caret_x = t->cols[i].x +
- (cw > t->cols[i].width ? t->cols[i].width : cw);
+ t->hscroll = tui_field_scroll(text, cp, t->cols[i].width,
+ t->hscroll);
+ caret_y = y + v;
+ caret_x = field_paint(y + v, t->cols[i].x,
+ t->cols[i].width, text, cp, 0,
+ t->hscroll, (int)A_REVERSE);
+ } else {
+ field_paint(y + v, t->cols[i].x, t->cols[i].width, text,
+ 0, 0, 0, (int)A_NORMAL);
}
- mvaddstr(vy, t->cols[i].x, padded);
- if (focused_col && row == t->row)
- attroff(A_REVERSE);
}
}
if (t->info) {
@@ -2300,35 +2397,39 @@ int tui_rt_run(const char *title, struct tui_rt *t, const char *hint)
}
if (g_hints)
g_hints(hint);
+ /* the caret only shows in a table cell; a focused header field is
+ marked by its highlighted value until it is edited */
+ curs_set(caret_y >= 0 ? 1 : 0);
if (caret_y >= 0)
move(caret_y, caret_x);
refresh();
int ch = in_key();
- if (ch == 27 || ch == 'q') {
- curs_set(prev_cursor == ERR ? 0 : prev_cursor);
- return -1;
+ /* no 'q' here: every printable key is text in a cell or field */
+ if (ch == 27) {
+ ret = -1;
+ break;
}
if (ch == KEY_F(5)) {
- curs_set(prev_cursor == ERR ? 0 : prev_cursor);
- return -2;
+ ret = -2;
+ break;
}
if (ch == TUI_KEY_CTRL_ENTER || ch == KEY_F(9)) {
- curs_set(prev_cursor == ERR ? 0 : prev_cursor);
- return -3;
+ ret = -3;
+ break;
}
if (t->key) {
int h = t->key(t->ud, ch);
if (h == TUI_HOOK_BACK) {
- curs_set(prev_cursor == ERR ? 0 : prev_cursor);
- return -1;
+ ret = -1;
+ break;
}
if (h == TUI_HOOK_REFRESH) {
- curs_set(prev_cursor == ERR ? 0 : prev_cursor);
- return -2;
+ ret = -2;
+ break;
}
if (h == TUI_HOOK_SUBMIT) {
- curs_set(prev_cursor == ERR ? 0 : prev_cursor);
- return -3;
+ ret = -3;
+ break;
}
if (h == TUI_HOOK_STAY)
continue;
@@ -2340,6 +2441,7 @@ int tui_rt_run(const char *title, struct tui_rt *t, const char *hint)
ch == '\t' ? 1 : -1);
t->fresh = 1;
t->pos = (size_t)-1;
+ t->hscroll = 0;
continue;
}
if (ch == KEY_UP || ch == KEY_DOWN) {
@@ -2354,17 +2456,12 @@ int tui_rt_run(const char *title, struct tui_rt *t, const char *hint)
continue;
}
if (t->focus >= 0) {
- if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
- rt_edit_field(t, t->focus);
- continue;
- }
- if (ch < 32 || ch >= 256)
- continue;
- /* typing is meant for the rows: hand the focus to the table */
- t->focus = -1;
- t->col = 0;
- t->fresh = 1;
- t->pos = (size_t)-1;
+ if (ch == '\n' || ch == '\r' || ch == KEY_ENTER)
+ rt_edit_field(t, t->focus, 0);
+ else if (ch >= 32 && ch < 256 && ch != 127 &&
+ field_types(&t->fields[t->focus]))
+ rt_edit_field(t, t->focus, ch);
+ continue;
}
if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
tui_rt_move(t, 1, 0);
@@ -2394,4 +2491,6 @@ int tui_rt_run(const char *title, struct tui_rt *t, const char *hint)
tui_field_edit(buf, cap, &t->pos, ch, &t->fresh);
tui_rt_normalize(t);
}
+ curs_set(prev_cursor == ERR ? 0 : prev_cursor);
+ return ret;
}
diff --git a/clients/tui.h b/clients/tui.h
index 1785a32..5ede574 100644
--- a/clients/tui.h
+++ b/clients/tui.h
@@ -99,7 +99,14 @@ int tui_field_edit(char *buf, size_t cap, size_t *pos, int ch, int *fresh);
int tui_date_field_edit(char *buf, size_t cap, size_t *pos, int ch,
int *fresh);
-/* Line editor at (y,x) with the label already drawn. Returns 1 on Enter,
+/* First byte to show so the caret at byte pos is visible in a field of
+ width columns (one column is kept for the caret after the last
+ character). `start` is the previous value: it is kept while the caret
+ stays visible, and moved back when removed text makes room. Pure. */
+size_t tui_field_scroll(const char *buf, size_t pos, int width, size_t start);
+
+/* Line editor at (y,x) up to the right border, scrolling horizontally,
+ with the label already drawn. Edits buf in place. Returns 1 on Enter,
2 on Tab, 3 on Shift-Tab, 0 on Esc. */
int tui_edit_field(int y, int x, char *buf, size_t cap, int mask);
@@ -187,6 +194,8 @@ struct tui_form_field {
};
int tui_form_next(int sel, int n, int ch);
+/* First visible form line so line sl of nlines shows in view lines. Pure. */
+int tui_form_scroll(int top, int sl, int nlines, int view);
int tui_form_run(const char *title, struct tui_form_field *f, int n,
int can_edit, int *focus);
@@ -283,6 +292,7 @@ struct tui_rt {
int row, col;
int fresh;
size_t pos;
+ size_t hscroll; /* first byte shown in the focused cell */
void (*cell)(void *, int, int, char **, size_t *);
void (*info)(void *, int, char *, size_t);
void *ud;