#!/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)