test/native_selection_follow.py
Ref: Size: 10.4 KiB History
#!/usr/bin/env python3
"""Real-PTY acceptance for completed local selections that follow terminal edits.
The source panes deliberately contain duplicate text. The surrounding numbered
rows and framebuffer location make a copied duplicate insufficient evidence:
the selected *occurrence* must move with the terminal's own tracked pins.
"""
import shlex
import os
import sys
import time
sys.dont_write_bytecode = True
from native_lifecycle import start_workspace
from native_resize import by_id
from native_selection import SelectionRig, cell_background
from native_tiling import eventually, require
SHIFT = 1
def pty_output(rig, pane, data):
"""Write fixture-owned terminal output without changing GUI focus."""
fd = os.open(rig.tty_paths[pane], os.O_WRONLY | os.O_NOCTTY)
try:
os.write(fd, data)
finally:
os.close(fd)
def row_of(state, pane, prefix):
for row, line in enumerate(by_id(state)[pane]['painted_text'].splitlines()):
if line.startswith(prefix):
return row
return None
def mouse_cell(rig, state, pane, col, row, kind, mods=0):
point = rig.cell_point(state, pane, col, row)
x, y = point.split(',')
rig.send(f'mouse:{kind},{x},{y},0,{mods}')
def local_shift_select(rig, pane, start, finish):
"""Use the ordinary SDL mouse hook: an app may be reporting mouse input."""
state = rig.state()
mouse_cell(rig, state, pane, *start, 'down', SHIFT)
mouse_cell(rig, state, pane, *finish, 'move', SHIFT)
mouse_cell(rig, state, pane, *finish, 'up', SHIFT)
return state
def shell_follow_program(rig, pane):
trigger = rig.root / 'shell-follow-go'
source = rig.root / 'shell-follow.py'
rows = by_id(rig.state())[pane]['rows']
require(rows >= 9, 'shell follow fixture needs nine terminal rows')
source.write_text(
'import os, time\nfrom pathlib import Path\n'
f'trigger=Path({str(trigger)!r})\n'
f'rows={rows}\n'
'body=["SHELL-%03d" % n for n in range(rows)]\n'
'body[rows-8]="SHELL-DUPLICATE"; body[rows-7]="SHELL-BEFORE-003"\n'
'body[rows-5]="SHELL-DUPLICATE"; body[rows-4]="SHELL-AFTER-006"; body[-1]="SHELL-READY"\n'
'os.write(1, ("\\033[0m\\033[2J\\033[H"+"\\r\\n".join(body)).encode())\n'
'while not trigger.exists(): time.sleep(.01)\n'
'for n in range(3):\n'
' os.write(1, ("\\r\\nSHELL-TAIL-%03d" % n).encode()); time.sleep(.03)\n'
'while True: time.sleep(60)\n')
rig.focus(pane)
rig.shell('python3 ' + shlex.quote(str(source)))
state = rig.wait_state(lambda s: row_of(s, pane, 'SHELL-READY') is not None)
return trigger, state
def shell_primary_scroll(rig, target, neighbour):
trigger, state = shell_follow_program(rig, target)
after = row_of(state, target, 'SHELL-AFTER-006')
start = None if after is None else after - 1
require(start is not None and start + 1 < by_id(state)[target]['rows'],
'shell fixture did not leave a two-row selection in the viewport')
background = cell_background(rig, state, target, 2, start)
# Select a duplicate plus its distinct successor. Matching the word alone
# would let a stale range select a different identical line.
local_shift_select(rig, target, (0, start), (16, start + 1))
rig.copied('SHELL-DUPLICATE\nSHELL-AFTER-006')
eventually(lambda: cell_background(rig, rig.state(), target, 2, start) != background,
'completed shell selection was not visibly painted')
# Unselected output must not move or invalidate this pane's occurrence.
pty_output(rig, neighbour, b'\033[0mNEIGHBOUR-OUTPUT\r\n')
rig.wait_state(lambda s: 'NEIGHBOUR-OUTPUT' in by_id(s)[neighbour]['painted_text'])
rig.key('copy')
rig.copied('SHELL-DUPLICATE\nSHELL-AFTER-006')
trigger.touch()
moved = rig.wait_state(lambda s: row_of(s, target, 'SHELL-TAIL-002') is not None)
after = row_of(moved, target, 'SHELL-AFTER-006')
new_row = None if after is None else after - 1
require(new_row == start - 3,
f'shell occurrence moved to row {new_row}, expected {start - 3}')
eventually(lambda: cell_background(rig, rig.state(), target, 2, new_row) != background,
'shell highlight did not follow its selected occurrence upward')
require(cell_background(rig, rig.state(), target, 2, start) == background,
'shell highlight remained at the old viewport row')
pty_output(rig, neighbour, b'\033]52;c;Q09QWS1TRU5USU5FTA==\a')
rig.copied('COPY-SENTINEL')
rig.key('copy')
rig.copied('SHELL-DUPLICATE\nSHELL-AFTER-006')
rig.ok('completed local selection follows primary-screen append through duplicate rows; neighbouring output is independent')
# Return the target pane to its shell before the alternate-screen fixture
# starts; otherwise its launch command would be consumed by the old app.
rig.key('interrupt')
time.sleep(.12)
def alt_reporter(rig, pane):
control = rig.root / 'alt-follow-control'
received = rig.root / 'alt-follow-received.bin'
source = rig.root / 'alt-follow.py'
rows = by_id(rig.state())[pane]['rows']
require(rows >= 9, 'alt follow fixture needs nine terminal rows')
source.write_text(
'import os, select, sys, termios, tty, time\nfrom pathlib import Path\n'
f'control=Path({str(control)!r}); received=Path({str(received)!r})\n'
'fd=0; old=termios.tcgetattr(fd); tty.setraw(fd); seen=0\n'
f'rows={rows}\n'
'body=["ALT-%03d" % n for n in range(rows)]\n'
'body[rows-8]="ALT-DUPLICATE"; body[rows-7]="ALT-BEFORE-003"\n'
'body[rows-5]="ALT-DUPLICATE"; body[rows-4]="ALT-AFTER-006"; body[-1]="ALT-READY"\n'
'try:\n'
' os.write(1, ("\\033[?1049h\\033[?25l\\033[2J\\033[H"+"\\r\\n".join(body)+"\\033[?1002h\\033[?1006h").encode())\n'
' while True:\n'
' if control.exists():\n'
' data=control.read_text()[seen:]; seen += len(data)\n'
' for command in data.splitlines():\n'
' if command == "scroll": os.write(1, ("\\033[2;%dr\\033[2S\\033[r" % rows).encode())\n'
' if command == "reset": os.write(1, b"\\033c")\n'
' if select.select([fd], [], [], .02)[0]:\n'
' with received.open("ab") as out: out.write(os.read(fd, 4096))\n'
'finally: termios.tcsetattr(fd, termios.TCSADRAIN, old)\n')
rig.focus(pane)
rig.shell('python3 ' + shlex.quote(str(source)))
state = rig.wait_state(lambda s: row_of(s, pane, 'ALT-READY') is not None)
return control, received, state
def alt_region_scroll(rig, target, neighbour):
control, received, state = alt_reporter(rig, target)
after = row_of(state, target, 'ALT-AFTER-006')
start = None if after is None else after - 1
require(start is not None and start + 1 < by_id(state)[target]['rows'], 'alt fixture rows missing')
background = cell_background(rig, state, target, 2, start)
before_bytes = received.stat().st_size if received.exists() else 0
local_shift_select(rig, target, (0, start), (14, start + 1))
rig.copied('ALT-DUPLICATE\nALT-AFTER-006')
time.sleep(.12)
require((received.stat().st_size if received.exists() else 0) == before_bytes,
'Shift local selection leaked mouse reports into the reporting app')
with control.open('a') as out:
out.write('scroll\n')
moved = rig.wait_state(lambda s: row_of(s, target, 'ALT-AFTER-006') == start - 1)
after = row_of(moved, target, 'ALT-AFTER-006')
new_row = None if after is None else after - 1
eventually(lambda: cell_background(rig, rig.state(), target, 2, new_row) != background,
'alt-screen region scroll did not move the highlight')
require(cell_background(rig, rig.state(), target, 2, start) == background,
'alt-screen region scroll left highlight at its old row')
# Ctrl+Shift+C must ask the daemon for the tracked occurrence after the
# scroll, rather than reusing the old client coordinates.
pty_output(rig, neighbour, b'\033]52;c;Q09QWS1TRU5USU5FTA==\a')
rig.copied('COPY-SENTINEL')
rig.key('copy')
rig.copied('ALT-DUPLICATE\nALT-AFTER-006')
rig.ok('Shift selection in a mouse-reporting alt screen follows vertical-region scrolling and Ctrl+Shift+C copies it')
return control
def invalidation(rig, target, control, neighbour):
stable = rig.clipboard()
require(stable == 'ALT-DUPLICATE\nALT-AFTER-006', 'missing selection-copy baseline')
# A real pane geometry change makes terminal screen coordinates ambiguous.
state = rig.state()
rig.drag('beside', dx=-2 * state['cell_w'])
state = rig.wait_state(lambda s: row_of(s, target, 'ALT-AFTER-006') is not None)
rig.unchanged(stable)
pty_output(rig, neighbour, b'\033]52;c;Q09QWS1TRU5USU5FTA==\a')
rig.copied('COPY-SENTINEL')
rig.key('copy')
rig.unchanged('COPY-SENTINEL')
after = row_of(state, target, 'ALT-AFTER-006')
start = None if after is None else after - 1
require(start is not None and start + 1 < by_id(state)[target]['rows'],
'resized alt fixture lost its selection markers')
background = cell_background(rig, state, target, 2, start)
local_shift_select(rig, target, (0, start), (14, start + 1))
rig.copied(stable)
eventually(lambda: cell_background(rig, rig.state(), target, 2, start) != background,
'selection could not be recreated after resize')
# RIS resets the terminal's pages and makes Ghostty's tracked pins garbage.
with control.open('a') as out:
out.write('reset\n')
rig.wait_state(lambda s: row_of(s, target, 'ALT-DUPLICATE') is None)
rig.unchanged(stable)
pty_output(rig, neighbour, b'\033]52;c;Q09QWS1TRU5USU5FTA==\a')
rig.copied('COPY-SENTINEL')
rig.key('copy')
rig.unchanged('COPY-SENTINEL')
rig.ok('resize and terminal reset invalidate a followed selection without changing the desktop clipboard')
def exercise(rig):
refs = start_workspace(rig)
panes = list(refs)
target, neighbour = panes[1], panes[2]
shell_primary_scroll(rig, target, neighbour)
control = alt_region_scroll(rig, target, neighbour)
invalidation(rig, target, control, neighbour)
rig.assert_cli_untouched()
def main():
require(len(sys.argv) == 3, 'usage: native_selection_follow.py MUX MUXG')
rig = SelectionRig(*sys.argv[1:])
try:
exercise(rig)
rig.quit()
print('Native selection-follow acceptance passed', flush=True)
except BaseException:
rig.failure_artifacts()
raise
finally:
rig.close()
if __name__ == '__main__':
main()