aboutsummaryrefslogtreecommitdiffstats
path: root/packaging
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-13 20:54:29 +0200
committeruser <user@clank>2026-08-13 20:54:29 +0200
commite220bb7cb784140ba5c9cc1fe59b71fdb9dbf4b9 (patch)
tree2550d9edb930010b1766e697b839161188745910 /packaging
parenttest: run the status bar's tests in `make test` (diff)
downloadsl0ppi-e220bb7cb784140ba5c9cc1fe59b71fdb9dbf4b9.tar.gz
sl0ppi-e220bb7cb784140ba5c9cc1fe59b71fdb9dbf4b9.tar.xz
sl0ppi-e220bb7cb784140ba5c9cc1fe59b71fdb9dbf4b9.zip
sl0p(bar): draw the bar as a framed box, and a single line when collapsed
Given three rows the bar now draws the same rounded box the panes do, so the bottom of the screen reads as one more pane instead of a strip of text under them. Given one row it stays the line it always was, which is what a terminal narrow enough to collapse the layout should spend on chrome. The height is the LAYOUT's decision, because a plugin cannot resize its own pane: - default.kdl defines the bar as a pane_template `sl0pbar` and places it at size=3; - default.swap.kdl defines tab_template "mobile" placing the same template at size=1, and the `stacked` swap layout -- what responsive_layout collapses to below responsive_min_cols -- uses it. That reuse works because zellij parses a layout and its .swap.kdl with the same parser, on purpose, "so that we can reuse assets (eg. pane and tab templates)". Otherwise the collapsed template would have to duplicate the whole 100-line zjstatus configuration, and the two would drift within a week. ModuleConfig::viewport is the one definition of where the content line lives: three rows or more and it is framed, fewer and it is not, plus the column the content starts at. Rendering and click routing both read it, so the frame moves the drawing and the hit-testing together -- a click on the new border hits nothing rather than the widget it sits above. Clicks now check the LINE too, which a single-line bar never had to. status-bar-live.py grew the other half of the feature: the borders must span exactly the columns the pane frames span with rounded corners at their ends, a click on the border must not make a tab, and after shrinking the terminal below the responsive threshold the bar must be one line with no box -- with its button still working. The collapse is real: the bar lands on the last row, which only happens if the swap layout actually switched. One test bug worth naming: locating the button with str::find gave a BYTE offset, and the box glyphs are three bytes each, so the click landed two columns right of the thing it found. Same trap the pane frame tests document.
Diffstat (limited to 'packaging')
-rwxr-xr-xpackaging/checks/status-bar-live.py81
1 files changed, 77 insertions, 4 deletions
diff --git a/packaging/checks/status-bar-live.py b/packaging/checks/status-bar-live.py
index 5a63922..e5ad8af 100755
--- a/packaging/checks/status-bar-live.py
+++ b/packaging/checks/status-bar-live.py
@@ -150,14 +150,18 @@ def read_screen(fd, buffered="", seconds=8, until=None, quiet_for=0.7):
def bar_row(rows):
- """The bar is the last non-blank row: the tab strip and the clock are on it."""
+ """The bar's CONTENT row: the one with the tab strip on it.
+
+ Not "the last non-blank row" -- framed, that is the bottom border.
+ """
for y in range(len(rows) - 1, -1, -1):
- if rows[y].strip():
+ if "Tab #" in rows[y]:
return y, rows[y]
return None, ""
def main():
+ global COLS
for path in (BIN, WASM):
if not os.path.isfile(path):
print(f"FAIL: missing {path}")
@@ -229,7 +233,29 @@ def main():
if frame_span[0] is not None and bar_span != frame_span:
failures.append(f"bar spans {bar_span} but the pane frames span {frame_span}")
- # 2. the new-tab button makes a tab
+ # 2. framed: the bar is a box in the pane style, and its borders are
+ # part of it -- a box whose corners do not line up with the panes
+ # above is worse than no box
+ if y == 0 or y + 1 >= len(rows):
+ failures.append(f"the bar is not framed: content row {y} has no room for borders")
+ print("\n".join(f"{i:02d}|{r}" for i, r in enumerate(rows)))
+ return failures
+ top, bottom = rows[y - 1], rows[y + 1]
+ print(f"above: {top.strip()[:12]!r} below: {bottom.strip()[:12]!r}")
+ for label, line, corners in (
+ ("top", top, ("╭", "╮")),
+ ("bottom", bottom, ("╰", "╯")),
+ ):
+ span = painted_span(grid, y - 1 if label == "top" else y + 1)
+ if span != bar_span:
+ failures.append(f"the {label} border spans {span}, the bar spans {bar_span}")
+ if line[bar_span[0]] != corners[0] or line[bar_span[1]] != corners[1]:
+ failures.append(
+ f"the {label} border does not have rounded corners at its ends: "
+ f"{line[bar_span[0]]!r} {line[bar_span[1]]!r}"
+ )
+
+ # 3. the new-tab button makes a tab
def tabs():
out = subprocess.run(
[BIN, "-s", SESSION, "action", "list-tabs", "--json"],
@@ -257,7 +283,7 @@ def main():
if after != before + 1:
failures.append(f"clicking the new-tab button did not add a tab ({before} -> {after})")
- # 3. and a click just left of the tab strip must not: the button is a
+ # 4. and a click just left of the tab strip must not: the button is a
# button, not "anywhere on the right half of the bar"
before = tabs()
os.write(fd, f"\x1b[<0;{max(1, plus - 2)};{y + 1}M".encode())
@@ -265,6 +291,53 @@ def main():
time.sleep(1.5)
if tabs() != before:
failures.append("a click beside the button also made a tab")
+
+ # 5. a click on the box itself must not either: the frame is chrome
+ before = tabs()
+ os.write(fd, f"\x1b[<0;{button_col + 1};{y}M".encode())
+ os.write(fd, f"\x1b[<0;{button_col + 1};{y}m".encode())
+ time.sleep(1.5)
+ if tabs() != before:
+ failures.append("a click on the bar's top border made a tab")
+
+ # 6. collapsed ("mobile"): below responsive_min_cols the tab layout
+ # swaps to the stacked one, whose template gives the bar a single
+ # row -- and the plugin must then draw a line, not a broken box
+ narrow = 70
+ fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", ROWS, narrow, 0, 0))
+ time.sleep(2.5)
+ wide, COLS = COLS, narrow
+ try:
+ raw, rows = read_screen(fd, "", seconds=10)
+ grid = screen_cells(raw)
+ y, bar = bar_row(rows)
+ if y is None:
+ failures.append("no status bar after collapsing to the mobile layout")
+ return failures
+ above = rows[y - 1] if y > 0 else ""
+ below = rows[y + 1] if y + 1 < len(rows) else ""
+ print(f"collapsed: bar row {y}, above {above.strip()[:14]!r}")
+ if any(c in above or c in below for c in "╭╮╰╯"):
+ failures.append("the collapsed bar still draws a box")
+ span = painted_span(grid, y)
+ if span != (EXPECTED_MARGIN, narrow - 1 - EXPECTED_MARGIN):
+ failures.append(f"collapsed bar spans {span}, expected margins only")
+ plus = bar.rfind(" + ")
+ if plus < 0:
+ failures.append("no new-tab button on the collapsed bar")
+ else:
+ before = tabs()
+ os.write(fd, f"\x1b[<0;{plus + 2};{y + 1}M".encode())
+ os.write(fd, f"\x1b[<0;{plus + 2};{y + 1}m".encode())
+ time.sleep(2)
+ after = tabs()
+ print(f"collapsed tabs: {before} -> {after}")
+ if after != before + 1:
+ failures.append(
+ f"the collapsed bar's button did not make a tab ({before} -> {after})"
+ )
+ finally:
+ COLS = wide
return failures
finally:
subprocess.run([BIN, "-s", SESSION, "kill-session"], env=env, capture_output=True)