summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--clients/tui.c218
-rw-r--r--clients/tui.h33
-rw-r--r--tests/test_tui.c56
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;
}