From 2ca284619125af0d5c12e9210ff1bfc8f9122739 Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Wed, 23 Sep 2026 08:56:49 +0200 Subject: 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 --- clients/tui.c | 933 ++++++++++++++++++++++++++++++++-------------------------- 1 file changed, 516 insertions(+), 417 deletions(-) (limited to 'clients/tui.c') 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; } -- cgit v1.3