diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-24 09:45:50 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | a39fb20cb770c3adb911cec7d1f3d033669ed1bb (patch) | |
| tree | e7bb88af15990e502b5f4deac5493ea2957e8b24 /test/column_grip.py | |
| parent | 39cdba439c1ae47dbc7c3243a9273aa011c6affa (diff) | |
| download | pardes-a39fb20cb770c3adb911cec7d1f3d033669ed1bb.tar.gz pardes-a39fb20cb770c3adb911cec7d1f3d033669ed1bb.zip | |
Light a held column grip, and rail its landing beside the rule where the GUI shows it
The column grip worked in the GUI but showed nothing anyone could see.
Driving the real SDL window with pixel mouse events, press, drag and
release all reach the core and reorder or resize the column; the feedback
is what failed. The grip kept its muted color while held, and a reorder's
insertion rail was drawn in the gutter cell at the destination edge. The
GUI paints that cell as the column's scroll rail, and at the window's left
edge, the common case of moving the right column first, the dashes were
lost in it. From the leftmost column, a drag short of a neighbour's middle
shows no rail, rightly, since nothing would move, but nothing said the
grip was even held.
A held grip now lights in column_box, the accent its muted color is mixed
from, and the rail takes that color. The rail runs in the seam cell left of
where the column's rule will land, where a border drag's rail runs, for a
reorder and a left-edge move alike; at the window's left edge, which has no
seam, it runs just past the scroll rail, leaving column 0's grip and pane
boxes whole. A carried column's pointer aims at a place, not a word, so the
column tags no longer light the word under it, and a release snaps the
layout as a border drag's does rather than animating it. The preview and the release both
ask layout.columnDrop where the column goes, so the rail stands where the
release puts the edge, and the drag no longer carries its unused column
index.
The GUI test feed takes ESC]777;mouse;<down|up|motion>;<button>;<x>;<y> in
window pixels and dispatches it as an SDL event. test/column_grip.py uses
it to drive the grip through the same pixel-to-cell and tag hit path as a
hand on the mouse, and checks the grip, the rail and the result against
GPU captures and the 9P grid.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Diffstat (limited to 'test/column_grip.py')
| -rw-r--r-- | test/column_grip.py | 132 |
1 files changed, 132 insertions, 0 deletions
diff --git a/test/column_grip.py b/test/column_grip.py new file mode 100644 index 00000000..ac0341c3 --- /dev/null +++ b/test/column_grip.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Drag a column grip in the real SDL window with pixel mouse events. + +WAYLAND_DISPLAY=/run/user/1000/wayland-1 \\ + python3 -B test/column_grip.py zig-out/bin/pardes-gui /tmp/pardes-column-grip [--pad 7] + +The window is hidden (PARDES_TEST). Mouse events enter as ESC]777;mouse +sequences on stdin, which the GUI builds into SDL events and dispatches like +its own, so pixel-to-cell and tag hit testing run as they do under a hand. +9P observes the grid; each step leaves a PNG of the GPU frame in the output +directory: grip-rest, grip-resize-drag, grip-reorder-drag, grip-done. +""" +import argparse +import os +from pathlib import Path +import shutil +import subprocess +import time + +from fs import session +from ui_review import require, wait_for + + +def rows(client): + frame = client.screen() + cols = frame['cols'] + return [''.join(cell[0] for cell in frame['cells'][at:at + cols]) for at in range(0, len(frame['cells']), cols)] + + +def rail(client): + """The one column the drag preview's rail runs down, if any.""" + bottom = rows(client)[-1] + found = [x for x, ch in enumerate(bottom) if ch == '╎'] + require(len(found) <= 1, f'more than one rail: {found}') + return found[0] if found else None + + +def grab(root, label): + time.sleep(.5) + def convert(): + shutil.copyfile(root / 'latest.ppm', root / (label + '.ppm')) + return subprocess.run(['magick', str(root / (label + '.ppm')), str(root / (label + '.png'))], + stderr=subprocess.DEVNULL).returncode == 0 + require(wait_for(convert, 8), 'GPU capture did not contain a complete image') + return root / (label + '.png') + + +def pixel(path, x, y): + out = subprocess.run(['magick', str(path), '-format', f'%[pixel:p{{{x},{y}}}]', 'info:'], + capture_output=True, text=True, check=True) + return out.stdout.strip() + + +def run(args): + root = args.output.resolve() + shutil.rmtree(root, ignore_errors=True) + root.mkdir(parents=True, mode=0o700) + (root / 'config/pardes').mkdir(parents=True) + (root / 'config/pardes/init').write_text('Shell /bin/sh\n') + (root / 'w.txt').write_text('alpha\nbeta\n') + name = 'column-grip-' + str(os.getpid()) + launch = ['-c', 'stty cols 100 rows 30; exec "$@"', 'column-grip', + str(args.binary.resolve()), '--9p=' + name, str(root / 'w.txt')] + env = {'PARDES_TEST': '1', 'PARDES_TEST_CAPTURE_DIR': str(root), 'PARDES_TEST_COLS': '100', + 'PARDES_TEST_ROWS': '30', 'PARDES_TEST_PAD': str(args.pad)} + handles = {} + with session('/bin/sh', root, name, socket_name=name, tty=True, launch=launch, + inherited=env, terminal=handles) as (client, _): + def send(data): + os.write(handles['input_fd'], data.encode()) + + def mouse(kind, button, col, row): + # the middle of a grid cell, in window pixels + send(f'\x1b]777;mouse;{kind};{button};{(col + .5) * cw};{(row + .5) * ch}\x07') + time.sleep(.15) + + require(wait_for(lambda: 'Newcol' in rows(client)[0]), 'workspace tag never drew') + send('\x1bn') + time.sleep(.3) + send('\x1bc') + require(wait_for(lambda: rows(client)[1].count('New Tty') == 2), 'no second column') + require(wait_for(lambda: (root / 'latest.ppm').exists(), 8), 'GPU capture missing') + time.sleep(.5) + with open(root / 'latest.ppm', 'rb') as ppm: + ppm.readline() + width, height = map(int, ppm.readline().split()) + frame = client.screen() + cw = (width - args.pad) // frame['cols'] + ch = (height - args.pad) // frame['rows'] + edge = rows(client)[1].index('New Tty', 10) - 3 + + rest = grab(root, 'grip-rest') + mouse('down', 1, edge, 1) + held = grab(root, 'grip-held') + require(pixel(held, int((edge + 1) * cw), int(1.5 * ch)) != pixel(rest, int((edge + 1) * cw), int(1.5 * ch)), + 'a grabbed grip looks the same as a resting one') + require(rail(client) is None, 'a grip held in place previews a move') + + # Short of the left column's middle: the left edge follows, and the + # rail is the seam cell left of where the rule will be. + for col in range(edge, edge - 11, -1): + mouse('motion', 1, col, 1) + require(rail(client) == edge - 11, f'resize rail at {rail(client)}, wanted {edge - 11}') + grab(root, 'grip-resize-drag') + mouse('up', 1, edge - 10, 1) + require(wait_for(lambda: rows(client)[1].index('New Tty', 10) - 3 == edge - 10), 'left edge did not follow the grip') + edge -= 10 + + # Over the left column's left half: it goes first, and the rail runs + # just past the left window edge's scroll rail. + mouse('down', 1, edge, 1) + for col in range(edge, 4, -5): + mouse('motion', 1, col, 10) + require(rail(client) == 1, f'reorder rail at {rail(client)}, wanted 1') + grab(root, 'grip-reorder-drag') + mouse('up', 1, 5, 10) + require(wait_for(lambda: 'alpha' in rows(client)[3][50:]), 'the column did not move first') + require(rail(client) is None, 'the rail outlived the drag') + grab(root, 'grip-done') + print('ok', root) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('binary', type=Path) + parser.add_argument('output', type=Path) + parser.add_argument('--pad', type=int, default=0, help='leftover pixels past the grid (PARDES_TEST_PAD)') + run(parser.parse_args()) + + +if __name__ == '__main__': + main() |
