amharc 0.6.0

A cinematic GTK4 and GStreamer desktop video player with a floating widget mode
Documentation
#!/usr/bin/env python3
"""Small dependency-free X11 driver for KnottVideo visual regression captures."""

from __future__ import annotations

import argparse
import ctypes
import ctypes.util
import sys
import time


Window = ctypes.c_ulong
Display = ctypes.c_void_p


class X11Driver:
    def __init__(self, title: str) -> None:
        x11_name = ctypes.util.find_library("X11")
        xtst_name = ctypes.util.find_library("Xtst")
        if not x11_name or not xtst_name:
            raise RuntimeError("X11 test libraries are unavailable")
        self.x11 = ctypes.CDLL(x11_name)
        self.xtst = ctypes.CDLL(xtst_name)
        self._declare_functions()
        self.display = self.x11.XOpenDisplay(None)
        if not self.display:
            raise RuntimeError("could not open the current X11 display")
        self.root = self.x11.XDefaultRootWindow(self.display)
        self.window = self._find_window(self.root, title)
        if not self.window:
            raise RuntimeError(f"could not find an X11 window named {title!r}")

    def _declare_functions(self) -> None:
        self.x11.XOpenDisplay.argtypes = [ctypes.c_char_p]
        self.x11.XOpenDisplay.restype = Display
        self.x11.XDefaultRootWindow.argtypes = [Display]
        self.x11.XDefaultRootWindow.restype = Window
        self.x11.XQueryTree.argtypes = [
            Display,
            Window,
            ctypes.POINTER(Window),
            ctypes.POINTER(Window),
            ctypes.POINTER(ctypes.POINTER(Window)),
            ctypes.POINTER(ctypes.c_uint),
        ]
        self.x11.XQueryTree.restype = ctypes.c_int
        self.x11.XFetchName.argtypes = [Display, Window, ctypes.POINTER(ctypes.c_char_p)]
        self.x11.XFetchName.restype = ctypes.c_int
        self.x11.XFree.argtypes = [ctypes.c_void_p]
        self.x11.XRaiseWindow.argtypes = [Display, Window]
        self.x11.XSetInputFocus.argtypes = [Display, Window, ctypes.c_int, ctypes.c_ulong]
        self.x11.XResizeWindow.argtypes = [Display, Window, ctypes.c_uint, ctypes.c_uint]
        self.x11.XWarpPointer.argtypes = [
            Display,
            Window,
            Window,
            ctypes.c_int,
            ctypes.c_int,
            ctypes.c_uint,
            ctypes.c_uint,
            ctypes.c_int,
            ctypes.c_int,
        ]
        self.x11.XStringToKeysym.argtypes = [ctypes.c_char_p]
        self.x11.XStringToKeysym.restype = ctypes.c_ulong
        self.x11.XKeysymToKeycode.argtypes = [Display, ctypes.c_ulong]
        self.x11.XKeysymToKeycode.restype = ctypes.c_ubyte
        self.x11.XFlush.argtypes = [Display]
        self.x11.XCloseDisplay.argtypes = [Display]
        self.xtst.XTestFakeKeyEvent.argtypes = [
            Display,
            ctypes.c_uint,
            ctypes.c_int,
            ctypes.c_ulong,
        ]
        self.xtst.XTestFakeKeyEvent.restype = ctypes.c_int
        self.xtst.XTestFakeButtonEvent.argtypes = [
            Display,
            ctypes.c_uint,
            ctypes.c_int,
            ctypes.c_ulong,
        ]
        self.xtst.XTestFakeButtonEvent.restype = ctypes.c_int

    def _title(self, window: int) -> str:
        name = ctypes.c_char_p()
        if not self.x11.XFetchName(self.display, window, ctypes.byref(name)) or not name.value:
            return ""
        try:
            return name.value.decode("utf-8", errors="replace")
        finally:
            self.x11.XFree(name)

    def _find_window(self, parent: int, title: str) -> int:
        if self._title(parent) == title:
            return parent
        root = Window()
        returned_parent = Window()
        children = ctypes.POINTER(Window)()
        count = ctypes.c_uint()
        if not self.x11.XQueryTree(
            self.display,
            parent,
            ctypes.byref(root),
            ctypes.byref(returned_parent),
            ctypes.byref(children),
            ctypes.byref(count),
        ):
            return 0
        try:
            for index in range(count.value):
                match = self._find_window(children[index], title)
                if match:
                    return match
        finally:
            if children:
                self.x11.XFree(children)
        return 0

    def activate(self) -> None:
        self.x11.XRaiseWindow(self.display, self.window)
        self.x11.XSetInputFocus(self.display, self.window, 1, 0)
        self.x11.XFlush(self.display)

    def resize(self, width: int, height: int) -> None:
        self.x11.XResizeWindow(self.display, self.window, width, height)
        self.x11.XFlush(self.display)

    def key(self, chord: str) -> None:
        names = [part.strip() for part in chord.split("+") if part.strip()]
        if not names:
            raise ValueError("key chord is empty")
        aliases = {
            "ctrl": "Control_L",
            "control": "Control_L",
            "shift": "Shift_L",
            "alt": "Alt_L",
            "super": "Super_L",
            "escape": "Escape",
            "esc": "Escape",
            "space": "space",
        }
        keycodes = []
        for name in names:
            keysym_name = aliases.get(name.lower(), name)
            keysym = self.x11.XStringToKeysym(keysym_name.encode())
            keycode = self.x11.XKeysymToKeycode(self.display, keysym)
            if not keycode:
                raise ValueError(f"unknown X11 key {name!r}")
            keycodes.append(keycode)
        self.activate()
        for keycode in keycodes:
            self.xtst.XTestFakeKeyEvent(self.display, keycode, True, 0)
        for keycode in reversed(keycodes):
            self.xtst.XTestFakeKeyEvent(self.display, keycode, False, 0)
        self.x11.XFlush(self.display)

    def click(self, x: int, y: int) -> None:
        self.activate()
        self.x11.XWarpPointer(self.display, 0, self.window, 0, 0, 0, 0, x, y)
        self.xtst.XTestFakeButtonEvent(self.display, 1, True, 0)
        self.xtst.XTestFakeButtonEvent(self.display, 1, False, 0)
        self.x11.XFlush(self.display)

    def scroll(self, x: int, y: int, steps: int) -> None:
        """Scroll at a window-relative point; positive steps move downward."""
        self.activate()
        self.x11.XWarpPointer(self.display, 0, self.window, 0, 0, 0, 0, x, y)
        button = 5 if steps > 0 else 4
        for _ in range(abs(steps)):
            self.xtst.XTestFakeButtonEvent(self.display, button, True, 0)
            self.xtst.XTestFakeButtonEvent(self.display, button, False, 0)
        self.x11.XFlush(self.display)

    def close(self) -> None:
        if self.display:
            self.x11.XCloseDisplay(self.display)
            self.display = None


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--title", default="KnottVideo")
    parser.add_argument("--activate", action="store_true")
    parser.add_argument("--resize", nargs=2, type=int, metavar=("WIDTH", "HEIGHT"))
    parser.add_argument("--key", action="append", default=[])
    parser.add_argument("--click", nargs=2, type=int, action="append", default=[])
    parser.add_argument(
        "--scroll",
        nargs=3,
        type=int,
        action="append",
        default=[],
        metavar=("X", "Y", "STEPS"),
    )
    parser.add_argument("--settle", type=float, default=0.35)
    args = parser.parse_args()

    driver = X11Driver(args.title)
    try:
        if args.activate:
            driver.activate()
        if args.resize:
            driver.resize(*args.resize)
        for chord in args.key:
            driver.key(chord)
            time.sleep(0.08)
        for x, y in args.click:
            driver.click(x, y)
            time.sleep(0.08)
        for x, y, steps in args.scroll:
            driver.scroll(x, y, steps)
            time.sleep(0.08)
        time.sleep(max(args.settle, 0.0))
        print(f"visual gate target: {args.title} ({driver.window:#x})")
    finally:
        driver.close()
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except (RuntimeError, ValueError) as error:
        print(f"visual gate error: {error}", file=sys.stderr)
        raise SystemExit(1)