#!/usr/bin/env python3 """Live check: the status bar lines up with the panes, and its new-tab button makes a tab. Both are things you can only see on a real session. The bar is a plugin rendering into a full-width pane, so nothing in the layout file says where its first column lands -- and the button is a click surface whose only definition of "where" is the string it drew. Usage: status-bar-live.py [path-to-sl0ppij] [path-to-zjstatus.wasm] """ import fcntl import json import os import pty import re import shutil import struct import subprocess import sys import tempfile import termios import time HERE = os.path.dirname(os.path.abspath(__file__)) REPO = os.path.abspath(os.path.join(HERE, "../..")) BIN = os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else f"{REPO}/sl0ppij/target/fastdev/sl0ppij") WASM = os.path.abspath( sys.argv[2] if len(sys.argv) > 2 else f"{REPO}/sl0pstatus/target/wasm32-wasip1/release/zjstatus.wasm" ) ROWS, COLS = 30, 100 SESSION = f"sl0ptest-bar-{os.getpid()}" # the horizontal pane gap the bar is supposed to match: `gap` x `gap_aspect` EXPECTED_MARGIN = 2 def screen_cells(raw): """Very small VT: absolute moves, erases, printable runs, and whether each cell has a background colour set. The background matters. A bar segment usually starts with a padding space inside its coloured pill, so "where does the bar begin" is a question about painted cells, not about ink -- strip the colour and the answer is off by one, which is exactly the size of the thing being measured. """ grid, row, col, i, bg = {}, 0, 0, 0, False while i < len(raw): ch = raw[i] if ch == "\x1b": m = re.match(r"\x1b\[([0-9;?]*)([a-zA-Z])", raw[i:]) if m: nums = [int(n) for n in m.group(1).replace("?", "").split(";") if n != ""] verb, first = m.group(2), (nums[0] if nums else 0) if verb == "m": # 0 = reset, 49 = default background, 40-47/100-107 = set, # 48 = extended (48;5;n or 48;2;r;g;b) if not nums or 0 in nums or 49 in nums: bg = False if any(n == 48 or 40 <= n <= 47 or 100 <= n <= 107 for n in nums): bg = True if verb in "Hf": row = (nums[0] - 1) if nums else 0 col = (nums[1] - 1) if len(nums) > 1 else 0 elif verb == "G": col = max(0, first - 1) elif verb == "J": if first == 2: grid.clear() else: for c in range(col, COLS): grid.pop((row, c), None) for r in range(row + 1, ROWS): for c in range(COLS): grid.pop((r, c), None) elif verb == "K": span = range(col, COLS) if first == 0 else range(0, COLS) for c in span: grid.pop((row, c), None) i += m.end() continue m = re.match(r"\x1b][^\x07\x1b]*(\x07|\x1b\\)", raw[i:]) if m: i += m.end() continue i += 1 continue if ch == "\r": col = 0 elif ch == "\n": row, col = row + 1, 0 elif ch >= " ": grid[(row, col)] = (ch, bg) col += 1 i += 1 return grid def screen_rows(raw): grid = screen_cells(raw) return [ "".join(grid.get((r, c), (" ", False))[0] for c in range(COLS)) for r in range(ROWS) ] def painted_span(grid, row): """(first, last) column of `row` that is drawn on: ink or background.""" painted = [ c for c in range(COLS) if (lambda cell: cell[0] not in (" ", "") or cell[1])(grid.get((row, c), (" ", False))) ] return (painted[0], painted[-1]) if painted else (None, None) def repaint(fd): """Force a full redraw, discard the intermediate one, read only the final.""" fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", ROWS, COLS - 1, 0, 0)) time.sleep(0.8) try: while os.read(fd, 1 << 20): pass except (BlockingIOError, OSError): pass fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", ROWS, COLS, 0, 0)) return "" def read_screen(fd, buffered="", seconds=8, until=None, quiet_for=0.7): deadline, last = time.time() + seconds, time.time() while time.time() < deadline: time.sleep(0.15) try: chunk = os.read(fd, 1 << 20).decode("utf8", "replace") except BlockingIOError: chunk = "" except OSError: break if chunk: buffered, last = buffered + chunk, time.time() continue if until and until(screen_rows(buffered)): break if buffered and time.time() - last >= quiet_for: break return buffered, screen_rows(buffered) def bar_row(rows): """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 "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}") return 1 tmp = tempfile.mkdtemp(prefix="sl0p-bar-") config_dir = os.path.join(tmp, "config") shutil.copytree(os.path.join(REPO, "config"), config_dir) plugins = os.path.join(config_dir, "plugins") os.makedirs(plugins, exist_ok=True) shutil.copy(WASM, os.path.join(plugins, "zjstatus.wasm")) # the shipped config points at ~/.config/sl0ppij; this run is isolated for name in ("config.kdl", "layouts/default.kdl", "layouts/default.swap.kdl"): path = os.path.join(config_dir, name) if not os.path.isfile(path): continue with open(path) as f: text = f.read() with open(path, "w") as f: f.write(text.replace("~/.config/sl0ppij", config_dir)) env = dict(os.environ) env.update( { "SL0PPIJ_SOCKET_DIR": os.path.join(tmp, "sock"), "SL0PPIJ_CONFIG_FILE": os.path.join(config_dir, "config.kdl"), "SL0PPIJ_CONFIG_DIR": config_dir, "SL0PPIJ_CACHE_DIR": os.path.join(tmp, "cache"), "XDG_CACHE_HOME": os.path.join(tmp, "cache"), "TERM": "xterm-256color", "SHELL": "/bin/sh", } ) pid, fd = pty.fork() if pid == 0: os.execvpe(BIN, [BIN, "-s", SESSION], env) fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", ROWS, COLS, 0, 0)) os.set_blocking(fd, False) failures = [] try: _, rows = read_screen(fd, "", seconds=25, until=lambda r: any("Tab #1" in x for x in r)) raw, rows = read_screen(fd, repaint(fd), seconds=10) grid = screen_cells(raw) y, bar = bar_row(rows) if y is None or "Tab #1" not in bar: failures.append("no status bar on screen") print("\n".join(f"{i:02d}|{r}" for i, r in enumerate(rows))) return failures print(f"bar row {y}: {bar.rstrip()!r}") # 1. the bar occupies exactly the columns the pane frames do bar_span = painted_span(grid, y) frame_rows = [i for i, r in enumerate(rows) if r.lstrip().startswith(("╭", "┌"))] frame_spans = [ (r.index(r.lstrip()[0]), len(r.rstrip()) - 1) for i, r in enumerate(rows) if i in frame_rows ] frame_span = ( min(s[0] for s in frame_spans), max(s[1] for s in frame_spans), ) if frame_spans else (None, None) print(f"bar spans columns {bar_span}, pane frames span {frame_span}") if bar_span != (EXPECTED_MARGIN, COLS - 1 - EXPECTED_MARGIN): failures.append( f"bar spans {bar_span}, expected " f"{(EXPECTED_MARGIN, COLS - 1 - EXPECTED_MARGIN)}" ) 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. 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"], env=env, capture_output=True, text=True, ).stdout try: return len(json.loads(out)) except Exception: return out.count('"name"') or len(out.strip().splitlines()) plus = bar.rfind(" + ") if plus < 0: failures.append("no new-tab button drawn on the bar") return failures button_col = plus + 1 print(f"new-tab button at column {button_col}") before = tabs() os.write(fd, f"\x1b[<0;{button_col + 1};{y + 1}M".encode()) os.write(fd, f"\x1b[<0;{button_col + 1};{y + 1}m".encode()) time.sleep(2) after = tabs() print(f"tabs: {before} -> {after}") if after != before + 1: failures.append(f"clicking the new-tab button did not add a tab ({before} -> {after})") # 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()) os.write(fd, f"\x1b[<0;{max(1, plus - 2)};{y + 1}m".encode()) 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, "kill-session", SESSION], env=env, capture_output=True) # `kill-session NAME`, not `-s NAME kill-session`: the latter parses but # leaves the session running. Delete too: killed sessions linger in # `list-sessions` as resurrectable. subprocess.run([BIN, "delete-session", SESSION], env=env, capture_output=True) try: os.close(fd) except OSError: pass os.waitpid(pid, os.WNOHANG) time.sleep(1) ps = subprocess.run(["ps", "-eo", "pid,comm,args"], capture_output=True, text=True).stdout for line in ps.splitlines(): fields = line.split(None, 2) if len(fields) == 3 and fields[1] == os.path.basename(BIN) and tmp in fields[2]: try: os.kill(int(fields[0]), 15) except (ProcessLookupError, ValueError, PermissionError): pass shutil.rmtree(tmp, ignore_errors=True) if __name__ == "__main__": problems = main() if problems == 1: sys.exit(1) for p in problems: print(f"FAIL: {p}") if not problems: print("ok: the bar lines up with the panes and its + makes a tab") sys.exit(1 if problems else 0)