🔒 Repository is read-only – file editing is disabled.

PaganDE/tools/x11_probe.py main

213 linii Raw ← Powrót
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
#!/usr/bin/env python3
"""Narzędzie DEV: podglądanie okien X11 (geometria, nazwa, stan).

PO CO: kompozytor w trybie `winit` jest zwykłym oknem X11. Żeby automatycznie
przetestować renderowanie kursora albo interakcje, trzeba wiedzieć GDZIE to okno
jest na ekranie i przeliczyć współrzędne ekranu → współrzędne kompozytora.
`xwininfo`/`xdotool` bywają niedostępne, a ten skrypt korzysta tylko z libX11.

Użycie:

    python3 tools/x11_probe.py list            # wypisz widoczne okna + geometrię
    python3 tools/x11_probe.py find 1280 800    # znajdź okno o dokładnym rozmiarze

Skrypt szuka wyłącznie okien potomnych korzenia (top-level), co w praktyce
wystarcza: okno `winit` jest właśnie takie.
"""

import ctypes
import ctypes.util

x11 = ctypes.CDLL(ctypes.util.find_library("X11"))
# WAŻNE: dla każdej funkcji ustawiamy `argtypes`/`restype`. Bez tego ctypes
# potraktuje wskaźnik Display i 64-bitowe id okna jak zwykłe inty i obetnie je
# do 32 bitów — dostaniemy segfault albo pustą listę okien.
x11.XOpenDisplay.restype = ctypes.c_void_p
x11.XOpenDisplay.argtypes = [ctypes.c_char_p]
x11.XDefaultRootWindow.restype = ctypes.c_ulong
x11.XDefaultRootWindow.argtypes = [ctypes.c_void_p]
x11.XQueryTree.argtypes = [
    ctypes.c_void_p,
    ctypes.c_ulong,
    ctypes.POINTER(ctypes.c_ulong),
    ctypes.POINTER(ctypes.c_ulong),
    ctypes.POINTER(ctypes.POINTER(ctypes.c_ulong)),
    ctypes.POINTER(ctypes.c_uint),
]
x11.XFetchName.argtypes = [ctypes.c_void_p, ctypes.c_ulong, ctypes.POINTER(ctypes.c_char_p)]
x11.XFree.argtypes = [ctypes.c_void_p]

# XWindowAttributes — bierzemy tylko pola, których potrzebujemy. Układ musi
# odpowiadać nagłówkowi Xlib.h; dopełniamy buforem, bo interesuje nas początek
# struktury (x, y, width, height, border_width, depth, ...).
class Attr(ctypes.Structure):
    _fields_ = [
        ("x", ctypes.c_int),
        ("y", ctypes.c_int),
        ("width", ctypes.c_int),
        ("height", ctypes.c_int),
        ("border_width", ctypes.c_int),
        ("depth", ctypes.c_int),
        ("visual", ctypes.c_void_p),
        ("root", ctypes.c_ulong),
        ("class_", ctypes.c_int),
        ("bit_gravity", ctypes.c_int),
        ("win_gravity", ctypes.c_int),
        ("backing_store", ctypes.c_int),
        ("backing_planes", ctypes.c_ulong),
        ("backing_pixel", ctypes.c_ulong),
        ("save_under", ctypes.c_int),
        ("colormap", ctypes.c_ulong),
        ("map_installed", ctypes.c_int),
        ("map_state", ctypes.c_int),
        ("all_event_masks", ctypes.c_long),
        ("your_event_mask", ctypes.c_long),
        ("do_not_propagate_mask", ctypes.c_long),
        ("override_redirect", ctypes.c_int),
        ("screen", ctypes.c_void_p),
    ]


_display = None
_root = None


def open_display():
    global _display, _root
    _display = x11.XOpenDisplay(None)
    if not _display:
        raise SystemExit("nie mogę otworzyć DISPLAY (ustaw DISPLAY=:N)")
    _root = x11.XDefaultRootWindow(_display)


def children(win):
    """Zwraca listę okien potomnych w kolejności stosu (od spodu do wierzchu)."""
    root_return = ctypes.c_ulong()
    parent_return = ctypes.c_ulong()
    arr = ctypes.POINTER(ctypes.c_ulong)()
    count = ctypes.c_uint()
    x11.XQueryTree(
        _display,
        ctypes.c_ulong(win),
        ctypes.byref(root_return),
        ctypes.byref(parent_return),
        ctypes.byref(arr),
        ctypes.byref(count),
    )
    result = [arr[i] for i in range(count.value)]
    if arr:
        x11.XFree(arr)
    return result


def attributes(win):
    x11.XGetWindowAttributes.argtypes = [ctypes.c_void_p, ctypes.c_ulong, ctypes.POINTER(Attr)]
    x11.XGetWindowAttributes.restype = ctypes.c_int
    attr = Attr()
    ok = x11.XGetWindowAttributes(_display, ctypes.c_ulong(win), ctypes.byref(attr))
    return attr if ok else None


def name(win):
    ptr = ctypes.c_char_p()
    if x11.XFetchName(_display, ctypes.c_ulong(win), ctypes.byref(ptr)) and ptr.value:
        title = ptr.value.decode("utf-8", "replace")
        x11.XFree(ptr)
        return title
    return ""


def main(argv):
    open_display()
    cmd = argv[1] if len(argv) > 1 else "list"

    if cmd == "list":
        for win in children(_root):
            attr = attributes(win)
            if not attr or attr.map_state != 2:  # 2 = IsViewable
                continue
            # Interesują nas okna o sensownym rozmiarze (pomijamy 1×1 pomocnicze).
            if attr.width < 50 or attr.height < 50:
                continue
            print(
                f"0x{win:x} {attr.width}x{attr.height}+{attr.x}+{attr.y} "
                f"title={name(win)!r}"
            )
    elif cmd == "find":
        want_w, want_h = int(argv[2]), int(argv[3])

        def walk(win, ox, oy):
            attr = attributes(win)
            if not attr or attr.map_state != 2:
                return None
            ax, ay = ox + attr.x, oy + attr.y
            if attr.width == want_w and attr.height == want_h:
                return (ax, ay)
            # Schodzimy w głąb — okno klienta bywa dzieckiem ramki menedżera okien.
            for child in children(win):
                found = walk(child, ax, ay)
                if found:
                    return found
            return None

        result = walk(_root, 0, 0)
        if result:
            print(f"{result[0]} {result[1]}")
            return
        raise SystemExit(f"nie znaleziono widocznego okna {want_w}x{want_h}")
    elif cmd == "tree":
        def dump(win, ox, oy, depth):
            attr = attributes(win)
            if not attr or attr.map_state != 2:
                return
            ax, ay = ox + attr.x, oy + attr.y
            print(
                "  " * depth
                + f"0x{win:x} {attr.width}x{attr.height}+{ax}+{ay} title={name(win)!r}"
            )
            for child in children(win):
                dump(child, ax, ay, depth + 1)

        for top in children(_root):
            dump(top, 0, 0, 0)
    elif cmd == "pointer":
        # Bieżąca pozycja kursora X (bezwzględna). Przydaje się, by sprawdzić,
        # czy wstrzyknięcie myszy przez XTest faktycznie zadziałało.
        root_ret = ctypes.c_ulong()
        child_ret = ctypes.c_ulong()
        rx = ctypes.c_int()
        ry = ctypes.c_int()
        wx = ctypes.c_int()
        wy = ctypes.c_int()
        mask = ctypes.c_uint()
        x11.XQueryPointer.argtypes = [
            ctypes.c_void_p,
            ctypes.c_ulong,
            ctypes.POINTER(ctypes.c_ulong),
            ctypes.POINTER(ctypes.c_ulong),
            ctypes.POINTER(ctypes.c_int),
            ctypes.POINTER(ctypes.c_int),
            ctypes.POINTER(ctypes.c_int),
            ctypes.POINTER(ctypes.c_int),
            ctypes.POINTER(ctypes.c_uint),
        ]
        x11.XQueryPointer(
            _display,
            ctypes.c_ulong(_root),
            ctypes.byref(root_ret),
            ctypes.byref(child_ret),
            ctypes.byref(rx),
            ctypes.byref(ry),
            ctypes.byref(wx),
            ctypes.byref(wy),
            ctypes.byref(mask),
        )
        print(f"{rx.value} {ry.value} child=0x{child_ret.value:x}")
    else:
        raise SystemExit(__doc__)


if __name__ == "__main__":
    import sys

    main(sys.argv)