aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-14 20:21:12 +0200
committeruser <user@clank>2026-08-14 20:21:12 +0200
commitbb41317523ca7c7bf1faba42fd9b843561e8878a (patch)
tree8d109475448a3bc22c9a58373a4919fc71fe7d8a
parentsl0ppty: minimised panes get one row between them, and ASCII marks (diff)
downloadslosh-bb41317523ca7c7bf1faba42fd9b843561e8878a.tar.gz
slosh-bb41317523ca7c7bf1faba42fd9b843561e8878a.tar.xz
slosh-bb41317523ca7c7bf1faba42fd9b843561e8878a.zip
sl0ppty: a pane rings on the bar it is put away in
A minimised pane is not on screen at all, which is precisely the case a bell exists for, and it had nowhere to say so. Its entry on the bar carries the mark now. The mark keeps its own colour there rather than taking the row's, and the same in the tab strip, where it had been drawn in whatever colour the tab was — an indicator in the same dim grey as the label beside it is one you have to already be looking for. It stays in the bell colour whether the entry is hovered or not, because it is not reporting the pointer. Testing it needed the pane to ring itself on a delay: `raw` writes to the focused pane, and focusing a minimised one is what restores it, so there is no way to ring one from outside without also answering it. The test waits for the mark rather than for a duration. Three copies of "count the columns in this short string" became one helper on the way past. The third was about to be a fourth.
-rw-r--r--src/app.c67
-rw-r--r--tests/test_bell.py56
2 files changed, 102 insertions, 21 deletions
diff --git a/src/app.c b/src/app.c
index f608d1f..08461dd 100644
--- a/src/app.c
+++ b/src/app.c
@@ -239,6 +239,16 @@ static tab_t *cur(app_t *a) { return &a->tabs[a->cur]; }
* Always asked with the rect that is about to be registered as the hit, so a
* thing that lights up and the thing that would be clicked cannot drift apart.
* A pointer already carrying something is busy and lights nothing. */
+/* Display columns of a short chrome string. Counts codepoints, which is exact
+ * for what chrome draws: every glyph here is one column, and a mark wide
+ * enough not to be is the thing the config warns you about. */
+static uint16_t cells(const char *str) {
+ uint16_t n = 0;
+ for (const char *q = str; q && *q; q++)
+ if (((unsigned char)*q & 0xC0) != 0x80) n++;
+ return n;
+}
+
static bool ptr_on(const app_t *a, uint16_t x, uint16_t y, uint16_t w,
uint16_t h) {
return a->ptr_valid && a->drag.kind == DRAG_NONE && a->ptr_x >= x &&
@@ -359,19 +369,31 @@ static void draw_min_bar(app_t *a, screen_t *s) {
for (size_t i = 0; i < nmin && x < right; i++) {
const char *nm = pane_title(mins[i]->pane);
+ /* A pane you cannot see is exactly the one a bell is for, so the mark
+ * comes along and keeps its own colour rather than the row's. */
+ bool rang = CFG.bell_indicator && pane_bell(mins[i]->pane);
+
char label[64];
- snprintf(label, sizeof label, " %s ", nm && *nm ? nm : "pane");
- /* Drawn, measured, then redrawn lit if the pointer turns out to be on it:
- * the same trick the tab strip uses, and for the same reason — the cells
- * that light up are exactly the ones registered. */
- uint16_t w = screen_text(s, x, bar.y, label, MINBAR, NO_COLOR, 0);
- if ((uint16_t)(x + w) > right) break;
- if (ptr_on(a, x, bar.y, w, 1))
+ snprintf(label, sizeof label, " %s", nm && *nm ? nm : "pane");
+ uint16_t lw = cells(label);
+ uint16_t bw = rang ? (uint16_t)(1 + cells(CFG.bell_mark)) : 0;
+ uint16_t total = (uint16_t)(lw + bw + 1); /* and the gap to the next one */
+ if ((uint16_t)(x + total) > right) break;
+
+ screen_text(s, x, bar.y, label, MINBAR, NO_COLOR, 0);
+ /* Lit as one target, since the whole entry is one target. */
+ if (ptr_on(a, x, bar.y, total, 1))
screen_text(s, x, bar.y, label, MINBAR_HOVER, NO_COLOR, ATTR_BOLD);
+ if (rang) {
+ char mark[24];
+ snprintf(mark, sizeof mark, " %s", CFG.bell_mark);
+ screen_text(s, (uint16_t)(x + lw), bar.y, mark, BELL_C, NO_COLOR,
+ ATTR_BOLD);
+ }
char action[48];
snprintf(action, sizeof action, "focus:%u", mins[i]->id);
- hit_add(&s->hits, x, bar.y, w, 1, action);
- x = (uint16_t)(x + w);
+ hit_add(&s->hits, x, bar.y, total, 1, action);
+ x = (uint16_t)(x + total);
}
}
@@ -437,9 +459,7 @@ static void draw_status_line(app_t *a, screen_t *s) {
/* Columns, not bytes: the only non-ASCII here is the arrow, three bytes
* wide and one column, and strlen would hold the whole indicator two
* columns further from the edge than it needed to be. */
- uint16_t iw = 0;
- for (const char *q = ind; *q; q++)
- if (((unsigned char)*q & 0xC0) != 0x80) iw++;
+ uint16_t iw = cells(ind);
if (right > x + iw + 2) {
screen_text(s, (uint16_t)(right - iw), y, ind, STATUS_STATE, NO_COLOR,
ATTR_BOLD);
@@ -1895,9 +1915,7 @@ static void draw_frame(app_t *a, screen_t *s, node_t *leaf) {
* may be more than one character. Booking three cells for a
* two-character mark would draw it over its neighbour and hand the
* neighbour's hit a cell it does not own. */
- uint16_t mw = 0;
- for (const char *q = btns[i].mark; *q; q++)
- if (((unsigned char)*q & 0xC0) != 0x80) mw++;
+ uint16_t mw = cells(btns[i].mark);
if (!mw) mw = 1;
uint16_t bw = (uint16_t)(mw + 2);
if (bx < r.x + 4 + bw) break;
@@ -2272,12 +2290,9 @@ static void draw_tab_strip(app_t *a, screen_t *s) {
const char *nm = t->name[0] ? t->name : (t->purpose[0] ? t->purpose : "");
/* A pane that rang in a tab you are not looking at is invisible without
* this, and that is the case the whole indicator exists for. */
- const char *bell = (CFG.bell_indicator && tab_has_bell(t)) ? CFG.bell_mark : "";
- if (nm[0])
- snprintf(label, sizeof label, " %zu:%s%s%s ", i + 1, nm,
- bell[0] ? " " : "", bell);
- else
- snprintf(label, sizeof label, " %zu%s%s ", i + 1, bell[0] ? " " : "", bell);
+ bool rang = CFG.bell_indicator && tab_has_bell(t);
+ if (nm[0]) snprintf(label, sizeof label, " %zu:%s ", i + 1, nm);
+ else snprintf(label, sizeof label, " %zu ", i + 1);
bool active = i == a->cur;
uint16_t attrs = active ? ATTR_BOLD : 0;
@@ -2292,6 +2307,16 @@ static void draw_tab_strip(app_t *a, screen_t *s) {
/* Hovering keeps the active tab's fill — it is still the tab you are in —
* so its feedback lands on the text instead. An inactive tab has no fill
* to keep, and brightens. */
+ /* The bell keeps its own colour here too, rather than taking the tab's —
+ * an indicator drawn in the same dim grey as the label it sits next to is
+ * an indicator you have to already be looking for. */
+ if (rang) {
+ char mark[24];
+ snprintf(mark, sizeof mark, "%s ", CFG.bell_mark);
+ w = (uint16_t)(w + screen_text(s, (uint16_t)(x + w), y, mark, BELL_C,
+ active ? TAB_ACTIVE_BG : NO_COLOR,
+ ATTR_BOLD));
+ }
if (ptr_on(a, x, y, w, 1))
screen_text(s, x, y, label, active ? TAB_ACTIVE_HOVER_FG : TAB_HOVER,
active ? TAB_ACTIVE_BG : NO_COLOR, attrs | ATTR_BOLD);
diff --git a/tests/test_bell.py b/tests/test_bell.py
index 27307fc..15f72d9 100644
--- a/tests/test_bell.py
+++ b/tests/test_bell.py
@@ -179,10 +179,66 @@ def test_it_can_be_turned_off_and_the_mark_chosen():
os.unlink(l)
+# ---- a pane you have put away ----------------------------------------------
+
+RINGER = ["/bin/sh", "-c",
+ 'printf "\\033]2;ringer\\007"; sleep 1; printf "\\007"; '
+ 'stty raw -echo; cat']
+
+
+def bar_row(s):
+ open_panes = [p for p in s.panes() if p["w"] > 1]
+ return max(p["y"] + p["h"] for p in open_panes)
+
+
+def test_a_minimised_pane_rings_on_the_bar():
+ """The pane is not on screen at all, which is the case a bell is for.
+
+ It has to ring itself on a delay: `raw` writes to the focused pane, and
+ focusing a minimised one is what restores it.
+ """
+ with Session(RINGER, cols=90, rows=20) as s:
+ s.settle(20)
+ s.api("split", dir="cols")
+ s.settle(20)
+ gone = s.panes()[0]["id"]
+ s.api("focus", id=gone)
+ s.settle(10)
+ s.send(r"\x01m") # put it away before it rings
+ s.settle(20)
+ check("it is on the bar and not on screen",
+ [p["w"] for p in s.panes() if p["id"] == gone] == [0],
+ str(s.panes()))
+ row = s.snapshot().text[bar_row(s)]
+ check("with no mark yet", MARK not in row, repr(row))
+
+ for _ in range(60): # gate on the mark rather than guess at it
+ s.settle(30)
+ if MARK in s.snapshot().text[bar_row(s)]:
+ break
+ row = s.snapshot().text[bar_row(s)]
+ check("ringing while put away marks its entry", MARK in row, repr(row))
+ check("and the tab it lives in", MARK in s.snapshot().line(1),
+ repr(s.snapshot().line(1)))
+
+ entry = [h for h in s.snapshot().hits
+ if h["action"] == f"focus:{gone}" and h["h"] == 1]
+ check("the entry is still one target", len(entry) == 1, str(entry))
+ if not entry:
+ return
+ s.click(entry[0]["x"] + 1, entry[0]["y"])
+ s.settle(30)
+ check("restoring it answers the bell everywhere",
+ MARK not in s.snapshot().line(1)
+ and [p["w"] for p in s.panes() if p["id"] == gone] != [0],
+ repr(s.snapshot().line(1)))
+
+
if __name__ == "__main__":
test_a_bel_marks_the_pane_that_rang()
test_looking_at_it_is_what_puts_it_out()
test_a_bell_in_another_tab_shows_on_the_strip()
test_the_strip_and_the_pane_clear_together()
test_it_can_be_turned_off_and_the_mark_chosen()
+ test_a_minimised_pane_rings_on_the_bar()
sys.exit(report())