summaryrefslogtreecommitdiff
path: root/clients
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-23 09:24:40 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-23 09:24:40 +0200
commit6159042d0cb3870e6a7c6f8d9a97e175d59e6f80 (patch)
treec6422654298a744b0a791de8f1e8ccb8e1c55071 /clients
parent53071bb69f5d21b35b8a61c2b1dd18832ce781d4 (diff)
downloadbokf-6159042d0cb3870e6a7c6f8d9a97e175d59e6f80.tar.gz
bokf-6159042d0cb3870e6a7c6f8d9a97e175d59e6f80.zip
tui: 'g' + a letter searches list rows
The goto prompt decides on its first key: digits jump to a row number as before, any other printable key searches the row texts of the list or menu (case-insensitive, also åäö), with up/down stepping through the matches. Keys typed into the prompt no longer reach the screen's key hook, and Enter in the action menu closes an open prompt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'clients')
-rw-r--r--clients/tui.c192
-rw-r--r--clients/tui.h11
2 files changed, 152 insertions, 51 deletions
diff --git a/clients/tui.c b/clients/tui.c
index aa859e2..4bc1d66 100644
--- a/clients/tui.c
+++ b/clients/tui.c
@@ -991,6 +991,69 @@ static int nav_count(const struct tui_list_nav *v)
return c;
}
+/* Lower-cases ASCII and the two-byte Latin-1 capitals (U+00C0-U+00DE except
+ U+00D7) into dst. */
+static void fold_copy(char *dst, size_t n, const char *src)
+{
+ size_t o = 0;
+ for (const unsigned char *p = (const unsigned char *)src;
+ *p && o + 1 < n; p++) {
+ unsigned char c = *p;
+ if (c >= 'A' && c <= 'Z') {
+ c = (unsigned char)(c + 32);
+ } else if (c == 0xC3 && p[1] >= 0x80 && p[1] <= 0x9E &&
+ p[1] != 0x97 && o + 2 < n) {
+ dst[o++] = (char)c;
+ c = (unsigned char)(*++p + 0x20);
+ }
+ dst[o++] = (char)c;
+ }
+ if (n)
+ dst[o] = '\0';
+}
+
+int tui_text_match(const char *hay, const char *needle)
+{
+ if (!needle || !*needle)
+ return 1;
+ if (!hay)
+ return 0;
+ char h[1024], nd[128];
+ fold_copy(h, sizeof h, hay);
+ fold_copy(nd, sizeof nd, needle);
+ return strstr(h, nd) != NULL;
+}
+
+/* Next selectable row matching the search, starting at `from` and moving in
+ dir (wrapping); `from` itself counts. -1 when nothing matches. */
+static int nav_find(const struct tui_list_nav *v, int from, int dir)
+{
+ if (!v->items || v->n <= 0)
+ return -1;
+ for (int k = 0; k < v->n; k++) {
+ int i = ((from + k * dir) % v->n + v->n) % v->n;
+ if (nav_selectable(v, i) && tui_text_match(v->items[i], v->gotobuf))
+ return i;
+ }
+ return -1;
+}
+
+static void nav_search_to(struct tui_list_nav *v, int from, int dir)
+{
+ int r = nav_find(v, from, dir);
+ v->goto_miss = r < 0;
+ if (r >= 0)
+ v->sel = r;
+}
+
+static void nav_goto_close(struct tui_list_nav *v)
+{
+ v->goto_active = 0;
+ v->goto_search = 0;
+ v->goto_miss = 0;
+ v->gotobuf[0] = '\0';
+}
+
/* Keys return the same codes the screens have always used: the selected
index, or -1 back, -2 refresh, -4 new, -6 remove, -7 toggle. */
int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new,
@@ -1009,26 +1072,51 @@ int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new,
return TUI_NAV_NONE;
}
if (v->goto_active) {
- if (ch >= '0' && ch <= '9' && strlen(v->gotobuf) < 9) {
- size_t gl = strlen(v->gotobuf);
- v->gotobuf[gl] = (char)ch;
- v->gotobuf[gl + 1] = '\0';
- int r = nav_row_of(v, atoi(v->gotobuf) - 1);
- if (r >= 0)
- v->sel = r;
+ /* the first key decides: digits jump to a row number, anything
+ else printable searches the row texts */
+ size_t gl = strlen(v->gotobuf);
+ int printable = ch >= 32 && ch < 256 && ch != 127;
+ if (!v->goto_search && ch >= '0' && ch <= '9') {
+ if (gl < 9) {
+ v->gotobuf[gl] = (char)ch;
+ v->gotobuf[gl + 1] = '\0';
+ int r = nav_row_of(v, atoi(v->gotobuf) - 1);
+ if (r >= 0)
+ v->sel = r;
+ }
+ } else if (printable && v->items &&
+ (v->goto_search || (gl == 0 && ch != ' '))) {
+ if (gl + 1 < sizeof v->gotobuf) {
+ v->gotobuf[gl] = (char)ch;
+ v->gotobuf[gl + 1] = '\0';
+ }
+ v->goto_search = 1;
+ /* search once the UTF-8 character is complete */
+ if ((ch & 0xC0) != 0xC0)
+ nav_search_to(v, 0, 1);
+ } else if (v->goto_search && (ch == KEY_DOWN || ch == KEY_UP)) {
+ int dir = ch == KEY_DOWN ? 1 : -1;
+ nav_search_to(v, v->sel + dir, dir);
} else if (ch == KEY_BACKSPACE || ch == 127 || ch == 8) {
- size_t gl = strlen(v->gotobuf);
+ while (gl > 0 &&
+ ((unsigned char)v->gotobuf[gl - 1] & 0xC0) == 0x80)
+ gl--;
if (gl)
- v->gotobuf[gl - 1] = '\0';
- if (v->gotobuf[0]) {
+ gl--;
+ v->gotobuf[gl] = '\0';
+ if (!gl) {
+ v->goto_search = 0;
+ v->goto_miss = 0;
+ } else if (v->goto_search) {
+ nav_search_to(v, 0, 1);
+ } else {
int r = nav_row_of(v, atoi(v->gotobuf) - 1);
if (r >= 0)
v->sel = r;
}
} else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER || ch == 27 ||
- ch == 'g' || ch == 'G') {
- v->goto_active = 0;
- v->gotobuf[0] = '\0';
+ (!v->goto_search && (ch == 'g' || ch == 'G'))) {
+ nav_goto_close(v);
}
return -3;
}
@@ -1060,8 +1148,8 @@ int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new,
return v->sel; /* menus activate directly */
}
} else if (ch == 'g' || ch == 'G') {
+ nav_goto_close(v);
v->goto_active = 1;
- v->gotobuf[0] = '\0';
} else if (ch == '\n' || ch == '\r' || ch == KEY_ENTER)
return v->sel;
else if (ch == KEY_F(5))
@@ -1087,6 +1175,33 @@ int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new,
/* list and menu rendering */
/* ------------------------------------------------------------------ */
+/* The goto/search prompt on the line above the hints (blank when closed). */
+static void nav_goto_draw(const struct tui_list_nav *v)
+{
+ move(LINES - 2, 2);
+ clrtoeol();
+ if (!v->goto_active)
+ return;
+ char label[64];
+ size_t ll = strlen(v->gotolabel);
+ if (v->goto_search)
+ snprintf(label, sizeof label, "Sök: ");
+ else if (!v->gotobuf[0] && v->items && ll > 2)
+ snprintf(label, sizeof label, "%.*s eller sök (bokstav): ",
+ (int)(ll - 2), v->gotolabel);
+ else
+ snprintf(label, sizeof label, "%s", v->gotolabel);
+ tui_style(TUI_ACCENT);
+ printw("%s%s", label, v->gotobuf);
+ tui_style_reset();
+ tui_style(TUI_DIM);
+ if (v->goto_miss)
+ addstr(" (ingen träff)");
+ else if (v->goto_search)
+ addstr(" upp/ned = fler träffar Enter/Esc = stäng");
+ tui_style_reset();
+}
+
static void list_draw(const char *title, char **items, int n,
const struct tui_list_nav *v, const char *hint)
{
@@ -1106,16 +1221,7 @@ static void list_draw(const char *title, char **items, int n,
if (idx == v->sel)
attroff(A_REVERSE);
}
- if (v->goto_active) {
- move(LINES - 2, 2);
- tui_style(TUI_ACCENT);
- printw("%s%s", v->gotolabel, v->gotobuf);
- tui_style_reset();
- clrtoeol();
- } else {
- move(LINES - 2, 2);
- clrtoeol();
- }
+ nav_goto_draw(v);
if (g_hints)
g_hints(hint);
refresh();
@@ -1155,6 +1261,7 @@ int tui_select_list_hook(const char *title, char **items, int n, int start,
v.sel = n - 1;
v.view = LINES - 4;
v.numw = tui_num_width(n);
+ v.items = (const char *const *)items;
snprintf(v.gotolabel, sizeof v.gotolabel, "Gå till rad: ");
for (;;) {
if (cursor)
@@ -1164,7 +1271,7 @@ int tui_select_list_hook(const char *title, char **items, int n, int start,
if (archive_action)
snprintf(dbuf, sizeof dbuf, " d = %s", archive_action);
snprintf(hint, sizeof hint,
- "upp/ned, 1-9 = hoppa, g = gå till, PgUp/PgDn, Home/End,"
+ "upp/ned, 1-9 = hoppa, g = gå till/sök, PgUp/PgDn, Home/End,"
" Enter%s%s%s%s Esc/q = tillbaka ^C = avsluta",
allow_refresh ? " F5 = uppdatera" : "",
allow_new ? " ^N = ny" : "", dbuf,
@@ -1175,9 +1282,10 @@ int tui_select_list_hook(const char *title, char **items, int n, int start,
}
list_draw(title, items, n, &v, hint);
int ch = in_key();
+ int in_goto = v.goto_active;
int r = tui_nav_key(&v, ch, allow_new, archive_action != NULL,
allow_toggle, 0);
- if (r == TUI_NAV_NONE && key) {
+ if (r == TUI_NAV_NONE && key && !in_goto && !v.goto_active) {
int h = key(ud, ch);
if (h == TUI_HOOK_BACK)
return -1;
@@ -1219,6 +1327,7 @@ int tui_menu(const char *title, const char *const *items, int n,
if (v.numw < 2)
v.numw = 2;
v.view = (LINES - 4) / 2;
+ v.items = items;
snprintf(v.gotolabel, sizeof v.gotolabel, "Gå till nummer: ");
for (;;) {
if (cursor)
@@ -1248,22 +1357,13 @@ int tui_menu(const char *title, const char *const *items, int n,
if (idx == v.sel)
attroff(A_REVERSE);
}
- if (v.goto_active) {
- move(LINES - 2, 2);
- tui_style(TUI_ACCENT);
- printw("%s%s", v.gotolabel, v.gotobuf);
- tui_style_reset();
- clrtoeol();
- } else {
- move(LINES - 2, 2);
- clrtoeol();
- }
+ nav_goto_draw(&v);
if (g_hints)
g_hints(allow_new
- ? "upp/ned, 1-9 = snabbval, g = gå till, PgUp/PgDn,"
+ ? "upp/ned, 1-9 = snabbval, g = gå till/sök, PgUp/PgDn,"
" Home/End, Enter ^N = ny Esc/q = tillbaka"
" ^C = avsluta"
- : "upp/ned, 1-9 = snabbval, g = gå till, PgUp/PgDn,"
+ : "upp/ned, 1-9 = snabbval, g = gå till/sök, PgUp/PgDn,"
" Home/End, Enter Esc/q = tillbaka ^C = avsluta");
refresh();
int r = tui_nav_key(&v, in_key(), allow_new, 0, 0, 1);
@@ -1723,6 +1823,7 @@ int tui_action_menu(const char *title, const struct tui_action *acts, int n,
if (v.numw < 2)
v.numw = 2;
v.view = LINES - 4;
+ v.items = (const char *const *)items;
snprintf(v.gotolabel, sizeof v.gotolabel, "Gå till nummer: ");
for (;;) {
int num = 0;
@@ -1762,22 +1863,13 @@ int tui_action_menu(const char *title, const struct tui_action *acts, int n,
if (acts[idx].enabled != 1)
attroff(tui_style_attrs(TUI_DIM, g_colours, 0));
}
- if (v.goto_active) {
- move(LINES - 2, 2);
- tui_style(TUI_ACCENT);
- printw("%s%s", v.gotolabel, v.gotobuf);
- tui_style_reset();
- clrtoeol();
- } else {
- move(LINES - 2, 2);
- clrtoeol();
- }
+ nav_goto_draw(&v);
if (g_hints)
- g_hints("upp/ned, 1-9 = hoppa, g = gå till, PgUp/PgDn, Home/End,"
+ g_hints("upp/ned, 1-9 = hoppa, g = gå till/sök, PgUp/PgDn, Home/End,"
" Enter = utför Esc/q = avbryt");
refresh();
int ch = in_key();
- if (ch == '\n' || ch == '\r' || ch == KEY_ENTER) {
+ if (!v.goto_active && (ch == '\n' || ch == '\r' || ch == KEY_ENTER)) {
if (!ok[v.sel])
continue;
if (acts[v.sel].enabled != 1) {
diff --git a/clients/tui.h b/clients/tui.h
index c4fe116..4ae0da6 100644
--- a/clients/tui.h
+++ b/clients/tui.h
@@ -130,14 +130,23 @@ int tui_choice_prompt(const char *label, const char *const *opts, int n,
struct tui_list_nav {
int n, sel, top, view, numw;
- char gotobuf[12];
+ char gotobuf[64];
char gotolabel[32];
int goto_active;
+ /* The goto prompt became a search: its first key was not a digit. */
+ int goto_search;
+ int goto_miss; /* the search text matches no row */
/* Optional row flags: 0 marks a non-selectable row (a menu section
header). Navigation, numbering and goto skip them. */
const unsigned char *selectable;
+ /* Optional row texts for the goto search; NULL: digits only. */
+ const char *const *items;
};
+/* Whether needle occurs in hay, ignoring case for ASCII and the Latin-1
+ letters (Å/Ä/Ö/É…). An empty needle matches. Pure. */
+int tui_text_match(const char *hay, const char *needle);
+
/* Returns the selected index, or -1 back, -2 refresh, -4 new, -6 remove,
-7 toggle, TUI_NAV_NONE when nothing happened. */
int tui_nav_key(struct tui_list_nav *v, int ch, int allow_new,