diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-19 22:03:06 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-19 22:03:06 +0200 |
| commit | 84c4fdcf138ad6ef4f7edfea76f2318b7a7a5e3e (patch) | |
| tree | dbae3b9fe87503297dc551393add7f0b97a0a543 | |
| parent | 4329ffb98d7eeffe2b3874d2b31cb0857d33d944 (diff) | |
| download | bokf-84c4fdcf138ad6ef4f7edfea76f2318b7a7a5e3e.tar.gz bokf-84c4fdcf138ad6ef4f7edfea76f2318b7a7a5e3e.zip | |
tui: row table widget (dynamic rows, trailing blank row) + testsv0.1.37
| -rw-r--r-- | clients/tui.c | 218 | ||||
| -rw-r--r-- | clients/tui.h | 33 | ||||
| -rw-r--r-- | tests/test_tui.c | 56 |
3 files changed, 307 insertions, 0 deletions
diff --git a/clients/tui.c b/clients/tui.c index e8508db..d19bd17 100644 --- a/clients/tui.c +++ b/clients/tui.c @@ -1126,3 +1126,221 @@ int tui_form_run(const char *title, struct tui_form_field *f, int n, } } } + +/* ------------------------------------------------------------------ */ +/* row table (dynamic multi-column editor with a trailing blank row) */ +/* ------------------------------------------------------------------ */ + +void tui_rt_init(struct tui_rt *t, int nrows, int maxrows, int ncols, + const struct tui_rt_col *cols, int y, + void (*cell)(void *, int, int, char **, size_t *), + void (*info)(void *, int, char *, size_t), void *ud) +{ + memset(t, 0, sizeof *t); + t->nrows = nrows > 0 ? nrows : 1; + t->maxrows = maxrows; + t->ncols = ncols; + /* only the editable columns take part in Tab/field navigation */ + for (int i = 0; i < ncols; i++) + if (cols[i].kind != TUI_RT_INFO) + t->neditable++; + t->cols = cols; + t->y = y; + t->cell = cell; + t->info = info; + t->ud = ud; + t->fresh = 1; + t->pos = (size_t)-1; +} + +static void rt_get(struct tui_rt *t, int row, int col, char **buf, size_t *cap) +{ + t->cell(t->ud, row, col, buf, cap); +} + +int tui_rt_blank_row(struct tui_rt *t, int row) +{ + for (int i = 0; i < t->ncols; i++) { + if (t->cols[i].kind == TUI_RT_INFO) + continue; + char *buf = NULL; + size_t cap = 0; + rt_get(t, row, i, &buf, &cap); + if (buf && buf[0]) + return 0; + } + return 1; +} + +int tui_rt_normalize(struct tui_rt *t) +{ + if (t->nrows < 1) + t->nrows = 1; + while (t->nrows > 1 && tui_rt_blank_row(t, t->nrows - 1) && + tui_rt_blank_row(t, t->nrows - 2)) + t->nrows--; + if (!tui_rt_blank_row(t, t->nrows - 1) && t->nrows < t->maxrows) + t->nrows++; + if (t->row >= t->nrows) + t->row = t->nrows - 1; + if (t->col >= t->neditable) + t->col = t->neditable - 1; + if (t->col < 0) + t->col = 0; + return t->nrows; +} + +int tui_rt_move(struct tui_rt *t, int dr, int dc) +{ + int moved = 0; + if (dr < 0 && t->row > 0) { + t->row--; + moved = 1; + } else if (dr > 0 && t->row + 1 < t->nrows) { + t->row++; + moved = 1; + } + if (dc && t->neditable > 0) { + t->col = (t->col + dc + t->neditable) % t->neditable; + moved = 1; + } + t->fresh = 1; + return moved; +} + +int tui_rt_clear(struct tui_rt *t) +{ + if (t->row < 0 || t->row >= t->nrows) + return 0; + for (int i = 0; i < t->ncols; i++) { + if (t->cols[i].kind == TUI_RT_INFO) + continue; + char *buf = NULL; + size_t cap = 0; + rt_get(t, t->row, i, &buf, &cap); + if (buf && cap) + buf[0] = '\0'; + } + tui_rt_normalize(t); + t->fresh = 1; + t->pos = (size_t)-1; + return 1; +} + +/* Returns -1 back, -2 refresh, -3 submit; otherwise an edited key was + consumed. */ +int tui_rt_run(const char *title, struct tui_rt *t, const char *hint) +{ + int top = 0; + for (;;) { + if (g_frame) + g_frame(title); + int view = LINES - 4 - t->y; + if (view < 1) + view = 1; + if (t->row < top) + top = t->row; + if (t->row >= top + view) + top = t->row - view + 1; + attron(A_BOLD); + for (int i = 0; i < t->ncols; i++) { + char hdr[64]; + snprintf(hdr, sizeof hdr, "%s", t->cols[i].header); + tui_pad_field(hdr, sizeof hdr, t->cols[i].width); + mvaddstr(t->y - 1, t->cols[i].x, hdr); + } + attroff(A_BOLD); + int caret_y = -1, caret_x = -1; + int ei = 0; + for (int i = 0; i < t->ncols; i++) { + int focused_col = (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) { + continue; + } + 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 y = t->y + v; + if (focused_col && row == t->row) { + attron(A_REVERSE); + caret_y = y; + size_t slen = strlen(buf ? buf : ""); + size_t cp = (t->fresh || 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); + } + mvaddstr(y, t->cols[i].x, padded); + if (focused_col && row == t->row) + attroff(A_REVERSE); + } + } + if (t->info) { + for (int v = 0; v < view && top + v < t->nrows; v++) { + char ibuf[512]; + for (int i = 0; i < t->ncols; i++) { + if (t->cols[i].kind != TUI_RT_INFO) + continue; + t->info(t->ud, top + v, ibuf, sizeof ibuf); + tui_pad_field(ibuf, sizeof ibuf, t->cols[i].width); + attron(A_DIM); + mvaddstr(t->y + v, t->cols[i].x, ibuf); + attroff(A_DIM); + } + } + } + if (g_hints) + g_hints(hint); + if (caret_y >= 0) + move(caret_y, caret_x); + refresh(); + int ch = in_key(); + if (ch == 27 || ch == 'q') + return -1; + if (ch == KEY_F(5)) + return -2; + if (ch == TUI_KEY_CTRL_ENTER || ch == KEY_F(9)) + return -3; + if (ch == '\t') { + tui_rt_move(t, 0, 1); + continue; + } + if (ch == KEY_BTAB) { + tui_rt_move(t, 0, -1); + continue; + } + if (ch == KEY_UP) { + tui_rt_move(t, -1, 0); + continue; + } + if (ch == KEY_DOWN) { + tui_rt_move(t, 1, 0); + continue; + } + if (ch == TUI_KEY_CTRL_X) { + tui_rt_clear(t); + continue; + } + char *buf = NULL; + size_t cap = 0; + rt_get(t, t->row, t->col, &buf, &cap); + if (!buf) + continue; + if (t->cols[t->col].kind == TUI_RT_DATE) + tui_date_field_edit(buf, cap, &t->pos, ch, &t->fresh); + else + tui_field_edit(buf, cap, &t->pos, ch, &t->fresh); + tui_rt_normalize(t); + } +} diff --git a/clients/tui.h b/clients/tui.h index 0bda359..d41d58c 100644 --- a/clients/tui.h +++ b/clients/tui.h @@ -12,6 +12,7 @@ /* Universal keys that have no control code or need a stable name. */ #define TUI_KEY_CTRL_N 0x0e +#define TUI_KEY_CTRL_X 0x18 #define TUI_KEY_CTRL_ENTER (KEY_MAX + 1) /* The application supplies input, frame/hints drawing and a quit hook so @@ -128,4 +129,36 @@ 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); +/* --- row table --- */ +enum { TUI_RT_EDIT = 0, TUI_RT_INFO, TUI_RT_DATE }; + +struct tui_rt_col { + const char *header; + int x; + int width; + int kind; +}; + +struct tui_rt { + int nrows, maxrows, ncols, neditable; + const struct tui_rt_col *cols; + int y; + int row, col; + int fresh; + size_t pos; + void (*cell)(void *, int, int, char **, size_t *); + void (*info)(void *, int, char *, size_t); + void *ud; +}; + +void tui_rt_init(struct tui_rt *t, int nrows, int maxrows, int ncols, + const struct tui_rt_col *cols, int y, + void (*cell)(void *, int, int, char **, size_t *), + void (*info)(void *, int, char *, size_t), void *ud); +int tui_rt_blank_row(struct tui_rt *t, int row); +int tui_rt_normalize(struct tui_rt *t); +int tui_rt_move(struct tui_rt *t, int dr, int dc); +int tui_rt_clear(struct tui_rt *t); +int tui_rt_run(const char *title, struct tui_rt *t, const char *hint); + #endif diff --git a/tests/test_tui.c b/tests/test_tui.c index e056c47..b2462e8 100644 --- a/tests/test_tui.c +++ b/tests/test_tui.c @@ -207,6 +207,61 @@ static void test_form_nav(void) CHECK(tui_form_next(1, 3, 'x') == TUI_NAV_NONE); } + +static char rt_cells[8][3][64]; +static void rt_cb(void *ud, int row, int col, char **buf, size_t *cap) +{ + (void)ud; + *buf = rt_cells[row][col]; + *cap = sizeof rt_cells[row][col]; +} +static void rt_info_cb(void *ud, int row, char *buf, size_t n) +{ + (void)ud; + snprintf(buf, n, "info%d", row); +} +static const struct tui_rt_col RT_COLS[3] = { + { "A", 2, 8, TUI_RT_EDIT }, + { "N", 12, 8, TUI_RT_INFO }, + { "B", 22, 8, TUI_RT_EDIT }, +}; + +static void test_rowtable(void) +{ + struct tui_rt t; + memset(rt_cells, 0, sizeof rt_cells); + tui_rt_init(&t, 1, 8, 3, RT_COLS, 6, rt_cb, rt_info_cb, NULL); + CHECK(t.nrows == 1 && t.neditable == 2 && t.row == 0 && t.col == 0); + CHECK(tui_rt_blank_row(&t, 0)); + CHECK(tui_rt_move(&t, 0, 1) && t.col == 1); + CHECK(tui_rt_move(&t, 0, 1) && t.col == 0); + CHECK(tui_rt_move(&t, -1, 0) == 0); + CHECK(tui_rt_move(&t, 1, 0) == 0); + + /* data in the blank row appends a new blank row */ + snprintf(rt_cells[0][0], 64, "1930"); + CHECK(tui_rt_normalize(&t) == 2); + snprintf(rt_cells[1][0], 64, "x"); + CHECK(tui_rt_normalize(&t) == 3); + /* two trailing blank rows collapse into one */ + t.nrows = 5; + CHECK(tui_rt_normalize(&t) == 3); + + /* ^X clears the row and re-normalizes */ + t.row = 1; + t.col = 0; + CHECK(tui_rt_clear(&t)); + CHECK(t.nrows == 2); + CHECK(rt_cells[1][0][0] == '\0'); + CHECK(tui_rt_clear(&t)); + CHECK(t.nrows == 2); /* the second clear only hit the blank row */ + t.row = 0; + CHECK(tui_rt_clear(&t)); + CHECK(t.nrows == 1); + tui_rt_clear(&t); + CHECK(t.nrows == 1); /* never below one row */ +} + int main(void) { test_disp_width(); @@ -216,6 +271,7 @@ int main(void) test_date_field(); test_nav(); test_form_nav(); + test_rowtable(); printf("test_tui: %d checks, %d failures\n", checks, failures); return failures ? 1 : 0; } |
