a73x

test/wayland_pointer.py

Ref:   Size: 2.6 KiB   History

"""Drive the retained virtual-pointer helper on an isolated Sway output.

MUXG_TEST_POINTER names the helper binary (line protocol: move x y w h,
button 0/1, wheel NOTCHES; each command returns ok). A real input serial is required for
Wayland clipboard ownership; SDL-injected events cannot establish it.
"""
import json
import select
import subprocess


class Pointer:
    def __init__(self, binary, env, pid):
        self.env, self.pid = env, pid
        self.proc = subprocess.Popen([binary], env=env, stdin=subprocess.PIPE,
                                     stdout=subprocess.PIPE, text=True)

    def command(self, line):
        self.proc.stdin.write(line + '\n')
        self.proc.stdin.flush()
        if not select.select([self.proc.stdout], [], [], 3)[0]:
            raise RuntimeError('virtual pointer command timed out')
        if self.proc.stdout.readline().strip() != 'ok':
            raise RuntimeError('virtual pointer command failed')

    def query(self, kind):
        return json.loads(subprocess.check_output(['swaymsg', '-r', '-t', kind],
                                                 env=self.env, timeout=3))

    def move(self, x, y):
        def find(node):
            if node.get('pid') == self.pid:
                return node
            for child in node.get('nodes', []) + node.get('floating_nodes', []):
                found = find(child)
                if found:
                    return found
        node = find(self.query('get_tree'))
        if node is None:
            raise RuntimeError('owned native window is not mapped')
        outputs = self.query('get_outputs')
        if len(outputs) != 1 or not outputs[0]['name'].startswith('HEADLESS-'):
            raise RuntimeError('pointer fixture requires one isolated headless output')
        rect, screen = node['rect'], outputs[0]['rect']
        self.command(f"move {int(rect['x'] + x - screen['x'])} "
                     f"{int(rect['y'] + y - screen['y'])} {screen['width']} {screen['height']}")

    def event(self, kind, x, y):
        self.move(x, y)
        if kind in ('mousedown', 'click'):
            self.command('button 1')
        if kind in ('mouseup', 'click'):
            self.command('button 0')

    def wheel(self, x, y, notches):
        self.move(x, y)
        self.command(f'wheel {notches}')

    def close(self):
        self.proc.stdin.close()
        try:
            if self.proc.wait(timeout=3):
                raise RuntimeError('virtual pointer failed on shutdown')
        except subprocess.TimeoutExpired:
            self.proc.kill()
            self.proc.wait(timeout=3)
            raise
        finally:
            self.proc.stdout.close()