a73x

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()