#!/usr/bin/env python3 """Check narrow-window first-document Look placement in owned SDL/TTY sessions. python3 -B test/look_placement.py zig-out/column-review/bin/pardes-gui python3 -B test/look_placement.py zig-out/column-review/bin/pardes --tty Starts the classic three-terminal layout, then opens the first document from the lower-left or right terminal. Screen-grid geometry must split only that terminal, immediately below it, and a repeated Look must reuse the file pane. Use --output DIR to retain grid observations and SDL GPU screenshots. """ import argparse from contextlib import nullcontext from pathlib import Path import tempfile from fs import session from ui_review import capture, panes, require, wait_for def geometry(client, markers): frame = client.screen() width = frame['cols'] lines = [''.join(cell[0] for cell in frame['cells'][row * width:(row + 1) * width]) for row in range(frame['rows'])] positions = {} for serial, marker in markers.items(): hits = [(row, line.index(marker)) for row, line in enumerate(lines) if marker in line] if len(hits) != 1: return None row, offset = hits[0] column_x = 0 if offset < width // 2 else width // 2 positions[serial] = (column_x, row) rectangles = {} for serial, (column_x, row) in positions.items(): following = [other_y for other_x, other_y in positions.values() if other_x == column_x and other_y > row] end = min(following, default=frame['rows']) rectangles[serial] = (column_x, row, width // 2, end - row) return frame, rectangles def rectangle_text(frame, rect): x, y, width, height = rect return tuple(tuple(cell[0] for cell in frame['cells'][row * frame['cols'] + x: row * frame['cols'] + x + width]) for row in range(y, y + height)) def run_case(args, root, source_index): root.mkdir(parents=True, exist_ok=True, mode=0o700) root.chmod(0o700) config = root / 'config/pardes' config.mkdir(parents=True) (config / 'init').write_text('Shell /bin/sh\nTheme forge\n') target = root / 'opened.txt' contents = b'First document opened from this terminal.\n' * 40 target.write_bytes(contents) name = 'look-placement' inherited = {} if args.tty else { 'PARDES_TEST': '1', 'PARDES_TEST_COLS': '150', 'PARDES_TEST_ROWS': '48', 'PARDES_TEST_CAPTURE_DIR': str(root), 'SDL_VIDEODRIVER': 'x11', } # No document argument: the regression concerns the very first document. launch = ['-c', 'stty cols 150 rows 48; exec "$@"', name, str(args.binary.resolve()), '--9p=' + name, '-n', '3'] with session('/bin/sh', root, name, socket_name=name, tty=True, launch=launch, inherited=inherited) as (client, _): serials = list(panes(client)) require(len(serials) == 3, 'classic fixture did not boot three terminals') markers = {} for index, serial in enumerate(serials): pane = f'/pane/{serial}' require('pty' in client.list(pane), 'fixture contains an existing document') marker = f'LP_{index}' markers[serial] = marker client.write(pane + '/tag', b'', truncate=True) client.write(pane + '/tag', f' {marker} Tty Del Collapse'.encode()) # Establish non-pristine shells and a stable prompt before measuring. client.write(pane + '/pty/data', f"printf 'ready-{index}\\n'\r".encode()) require(wait_for(lambda: f'\nready-{index}\n'.encode() in client.read(pane + '/body').replace(b'\r', b'')), 'terminal did not execute readiness command') before_frame, before = require(wait_for(lambda: geometry(client, markers)), 'terminal tags not visible in fixture') source = serials[source_index] source_rect = before[source] require(source_rect[3] >= 8, 'source terminal too short for a local split') require(before[serials[0]][:2] == (0, 2) and before[serials[1]][0] == 0 and before[serials[1]][1] > 2 and before[serials[2]][:2] == (75, 2), f'unexpected initial classic layout: {before}') client.write(f'/pane/{source}/look', f'{target}\n'.encode()) opened = require(wait_for(lambda: next((serial for serial in panes(client) if serial not in serials), None)), 'Look did not open the first document') markers[opened] = 'opened.txt' after_frame, after = require(wait_for(lambda: geometry(client, markers)), 'Look pane tags not visible') require(len(panes(client)) == 4, 'Look removed a terminal or added extra panes') x, y, width, height = source_rect require(after[source][:3] == (x, y, width), f'Look moved its originating terminal: {source_rect} -> {after[source]}') require(after[opened][:3] == (x, y + after[source][3], width) and after[source][3] + after[opened][3] == height, f'Look did not split directly below its caller: {after}') for serial in serials: if serial == source: continue require(after[serial] == before[serial], f'Look resized or moved unrelated terminal {serial}: {before} -> {after}') require(rectangle_text(before_frame, before[serial]) == rectangle_text(after_frame, after[serial]), f'Look changed unrelated terminal {serial} rendered text') require(client.read(f'/pane/{opened}/body') == contents, 'opened document contents differ') client.write(f'/pane/{source}/look', f'{target}:2\n'.encode()) require(list(panes(client)) == serials + [opened], 'repeated Look created a duplicate pane') require(wait_for(lambda: geometry(client, markers)[1] == after), 'repeated Look changed layout') require(target.read_bytes() == contents, 'Look modified the fixture on disk') if args.output: capture(client, root, 'look-below-caller', not args.tty) print(f'{"TTY" if args.tty else "SDL"} source {"lower-left" if source_index == 1 else "right"}: ' 'local split, unchanged other terminals, first-file contents and re-Look reuse passed', flush=True) def run(args): # Cache-rooted fixtures avoid relying on /tmp quota in developer sessions. cache = Path.home() / '.cache' cache.mkdir(exist_ok=True) context = nullcontext(str(args.output.resolve())) if args.output else tempfile.TemporaryDirectory( prefix='lp-', dir=cache) with context as directory: for source_index, label in [(1, 'left'), (2, 'right')]: run_case(args, Path(directory) / label, source_index) if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('binary', type=Path) parser.add_argument('--tty', action='store_true') parser.add_argument('--output', type=Path) run(parser.parse_args())