aboutsummaryrefslogtreecommitdiffstats
path: root/src/app_draw.c
diff options
context:
space:
mode:
authoruser <user@clank>2026-09-08 16:58:24 +0200
committeruser <user@clank>2026-09-08 16:58:24 +0200
commit935fb7e93726dcb8ab0529c2dbcb6b5e75656794 (patch)
tree824b46e4cff68b7d4fa0c333717684ce273c9b10 /src/app_draw.c
parentrelease: v0.1.6 paperwork -- formula and PKGBUILD pinned (diff)
downloadslosh-935fb7e93726dcb8ab0529c2dbcb6b5e75656794.tar.gz
slosh-935fb7e93726dcb8ab0529c2dbcb6b5e75656794.tar.xz
slosh-935fb7e93726dcb8ab0529c2dbcb6b5e75656794.zip
search: a scrollback search bar on /, matches highlighted
C-a / opens an inline search bar on the focused pane's status row, on lib-vt's GhosttySearch (D4's pattern: the scrollback walk, reflow and alt-screen survival are the library's, the UI and the wiring are ours). Type to search live; the viewport follows the current match; up/down (or C-n/C-p) step through them with wraparound; Enter keeps your place, Escape snaps back. A k-of-n counter reads on the right, or "no matches". Every match on screen is tinted, and the one the bar is on is tinted apart -- warm gold by default, painted after the shader pass so the scrolled wash does not dilute a match you are hunting for. Four themeable colours (search_fg/bg, search_cur_fg/bg). Bare / took the key from help, which keeps ?; that needed the legacy decoder to read the shift out of a punctuation byte the way it already did for capitals -- without it ? and / decode identically (SLASH, no mods) and cannot be two keys on a client without the kitty protocol. tests/test_search.py drives it end to end (39 checks): the bar, the counter, stepping and wrap, all-match vs current highlighting, Enter/ Escape, new output updating the count, case-insensitivity, the keyboard belonging to the bar, click-away, and that / and ? no longer collide.
Diffstat (limited to 'src/app_draw.c')
-rw-r--r--src/app_draw.c95
1 files changed, 95 insertions, 0 deletions
diff --git a/src/app_draw.c b/src/app_draw.c
index 700b9b6..a3221e8 100644
--- a/src/app_draw.c
+++ b/src/app_draw.c
@@ -194,8 +194,97 @@ struct row_btn {
char action[48];
};
+/* The search bar, on the row the epitaph and the OSC 5577 status use: the
+ * bottom border on a classic frame, the last content row in compact. It wins
+ * the whole row while it is up -- a query and a status would be two sentences
+ * on one line -- and comes back the moment the search closes.
+ *
+ * Left, the field: `/ query█`, in the rename editor's colours, because it is
+ * the same kind of thing (typing that is not going to the program) and should
+ * announce itself the same way. Right, the answer: `k of n`, or `no matches`,
+ * which is the difference between "narrow it" and "it is not there". */
+static bool draw_search_bar(app_t *a, screen_t *s, node_t *leaf) {
+ if (!a->searching || a->search_id != leaf->id) return false;
+ rect_t r = leaf->rect;
+ if (r.w < 8 || r.h < 3) return true; /* too small to draw, still claimed */
+ uint16_t y = (uint16_t)(r.y + r.h - 1);
+ uint16_t left = (uint16_t)(r.x + 1), right = (uint16_t)(r.x + r.w - 1);
+
+ /* The counter first, so the field knows what is left. Only when there is a
+ * needle: `0 of 0` under an empty field would be an answer to a question
+ * nobody asked yet. */
+ uint16_t cx = right;
+ if (a->search_buf[0]) {
+ size_t idx = 0, total = 0;
+ bool selected = pane_search_pos(leaf->pane, &idx, &total);
+ char count[48];
+ if (selected)
+ snprintf(count, sizeof count, " %zu of %zu ", idx + 1, total);
+ else if (total)
+ snprintf(count, sizeof count, " %zu ", total);
+ else
+ snprintf(count, sizeof count, " no matches ");
+ uint16_t cw = cells(count);
+ if (left + cw + 6 < right) { /* only when a usable field remains */
+ cx = (uint16_t)(right - cw);
+ screen_text(s, cx, y, count, total ? HINT_C : DEAD_C, NO_COLOR,
+ total ? 0 : ATTR_BOLD);
+ }
+ }
+
+ /* The field: head scrolled off when too long, whole characters only --
+ * the cursor is the one thing that must stay on screen while typing. */
+ char buf[160];
+ int len = snprintf(buf, sizeof buf, " / %s\u2588 ", a->search_buf);
+ if (len < 0) len = 0;
+ if ((size_t)len >= sizeof buf) len = (int)strlen(buf);
+ uint16_t avail = (uint16_t)(cx > left ? cx - left : 0);
+ if (len > (int)avail) {
+ size_t off = (size_t)(len - (int)avail);
+ while (off < (size_t)len && ((unsigned char)buf[off] & 0xC0) == 0x80) off++;
+ memmove(buf, buf + off, (size_t)len - off + 1);
+ len = (int)((size_t)len - off);
+ }
+ uint16_t drawn = screen_text(s, left, y, buf, RENAME_FG, RENAME_BG, 0);
+ /* Claims its cells, like the epitaph: not a button, but owning the row is
+ * what keeps the armed split guide from ruling a line through the query --
+ * and what lets a click on the bar be "not away", so it does not commit. */
+ if (drawn) {
+ char action[48];
+ snprintf(action, sizeof action, "search:%u", leaf->id);
+ hit_add(&s->hits, left, y, drawn, 1, action);
+ }
+ return true;
+}
+
+/* Tint the pane's on-screen search matches. The current match is painted in
+ * its own colour and made bold; the rest share the plain match colour. Our
+ * own inverse is cleared on the cells we touch, so a match landing on selected
+ * text reads as a match rather than a double-negative. */
+static void draw_search_marks(app_t *a, screen_t *s, node_t *n) {
+ if (!a->searching || a->search_id != n->id || !n->pane) return;
+ pane_mark_t marks[256];
+ size_t nm = pane_search_marks(n->pane, marks, 256);
+ for (size_t i = 0; i < nm; i++) {
+ if (marks[i].row >= n->content.h) continue;
+ uint16_t y = (uint16_t)(n->content.y + marks[i].row);
+ color_t fg = marks[i].current ? SEARCH_CUR_FG : SEARCH_FG;
+ color_t bg = marks[i].current ? SEARCH_CUR_BG : SEARCH_BG;
+ for (uint16_t x = marks[i].x0; x <= marks[i].x1 && x < n->content.w; x++) {
+ cell_t *c = screen_at(s, (uint16_t)(n->content.x + x), y);
+ if (!c) continue;
+ c->fg = fg;
+ c->bg = bg;
+ c->attrs &= (uint16_t)~ATTR_INVERSE;
+ if (marks[i].current) c->attrs |= ATTR_BOLD;
+ }
+ }
+}
+
static void draw_pane_status(app_t *a, screen_t *s, node_t *leaf, color_t fg,
bool focused) {
+ if (draw_search_bar(a, s, leaf)) return;
+
struct row_btn row[8];
size_t nbtn = 0;
char status[256] = {0};
@@ -1647,6 +1736,12 @@ static void draw_cb(node_t *n, void *ud) {
* is not decoration and must not be dimmed along with the thing it is
* offered on. */
shade_leaf(d->a, d->s, n);
+ /* Search match highlights, after the shader pass rather than before it: a
+ * match is on screen because it was searched for, so it wants to be crisp,
+ * not washed by the `scrolled` fade over the very history it sits in. Every
+ * on-screen match in `search`; the one the bar is on in `search_cur`, which
+ * is what tells "this one" from "all of them". */
+ draw_search_marks(d->a, d->s, n);
/* A compact pane's chrome is the ring of shared line it sits inside — one
* cell out on every side — rather than the rect's own edge; the state
* passes (a bell's flash, a theme's chrome chain) land there instead. The