#!/usr/bin/env python3
"""Minimal X11 focus tool using ctypes + libX11.
Finds windows whose WM_NAME / _NET_WM_NAME contains a substring, raises them
and gives them input focus via _NET_ACTIVE_WINDOW (so the running WM cooperates)
plus a raw XSetInputFocus fallback.

Usage: xfocus.py <substring>
"""
import ctypes, sys

X = ctypes.CDLL("libX11.so.6")
X.XOpenDisplay.restype = ctypes.c_void_p
X.XDefaultRootWindow.restype = ctypes.c_ulong
X.XDefaultRootWindow.argtypes = [ctypes.c_void_p]
X.XInternAtom.restype = ctypes.c_ulong
X.XInternAtom.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int]
X.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)]
X.XFetchName.argtypes = [ctypes.c_void_p, ctypes.c_ulong,
    ctypes.POINTER(ctypes.c_char_p)]
X.XSetInputFocus.argtypes = [ctypes.c_void_p, ctypes.c_ulong, ctypes.c_int,
    ctypes.c_ulong]
X.XMapRaised.argtypes = [ctypes.c_void_p, ctypes.c_ulong]
X.XSync.argtypes = [ctypes.c_void_p, ctypes.c_int]
X.XFree.argtypes = [ctypes.c_void_p]
X.XGetWindowProperty.argtypes = [ctypes.c_void_p, ctypes.c_ulong, ctypes.c_ulong,
    ctypes.c_long, ctypes.c_long, ctypes.c_int, ctypes.c_ulong,
    ctypes.POINTER(ctypes.c_ulong), ctypes.POINTER(ctypes.c_int),
    ctypes.POINTER(ctypes.c_ulong), ctypes.POINTER(ctypes.c_ulong),
    ctypes.POINTER(ctypes.POINTER(ctypes.c_ubyte))]

class XClientMessageEvent(ctypes.Structure):
    _fields_ = [
        ("type", ctypes.c_int),
        ("serial", ctypes.c_ulong),
        ("send_event", ctypes.c_int),
        ("display", ctypes.c_void_p),
        ("window", ctypes.c_ulong),
        ("message_type", ctypes.c_ulong),
        ("format", ctypes.c_int),
        ("data", ctypes.c_long * 5),
    ]
class XEvent(ctypes.Union):
    _fields_ = [("xclient", XClientMessageEvent), ("pad", ctypes.c_long * 24)]
X.XSendEvent.argtypes = [ctypes.c_void_p, ctypes.c_ulong, ctypes.c_int,
    ctypes.c_long, ctypes.POINTER(XEvent)]

needle = sys.argv[1] if len(sys.argv) > 1 else ""
d = X.XOpenDisplay(None)
if not d:
    sys.exit("no display")
root = X.XDefaultRootWindow(d)
NET_ACTIVE = X.XInternAtom(d, b"_NET_ACTIVE_WINDOW", 0)
NET_WM_NAME = X.XInternAtom(d, b"_NET_WM_NAME", 0)
UTF8 = X.XInternAtom(d, b"UTF8_STRING", 0)

def get_net_name(w):
    t = ctypes.c_ulong(); fmt = ctypes.c_int()
    n = ctypes.c_ulong(); after = ctypes.c_ulong()
    data = ctypes.POINTER(ctypes.c_ubyte)()
    if X.XGetWindowProperty(d, w, NET_WM_NAME, 0, 1024, 0, UTF8,
            ctypes.byref(t), ctypes.byref(fmt), ctypes.byref(n),
            ctypes.byref(after), ctypes.byref(data)) == 0 and data:
        s = bytes(data[:n.value]).decode("utf-8", "replace")
        X.XFree(data)
        return s
    return ""

def get_name(w):
    name = ctypes.c_char_p()
    X.XFetchName(d, w, ctypes.byref(name))
    nm = name.value.decode("utf-8", "replace") if name.value else ""
    if name.value:
        X.XFree(name)
    return nm or get_net_name(w)

def walk(w):
    out = [(w, get_name(w))]
    rr = ctypes.c_ulong(); pp = ctypes.c_ulong()
    kids = ctypes.POINTER(ctypes.c_ulong)(); n = ctypes.c_uint()
    if X.XQueryTree(d, w, ctypes.byref(rr), ctypes.byref(pp),
                    ctypes.byref(kids), ctypes.byref(n)):
        for i in range(n.value):
            out += walk(kids[i])
        if kids:
            X.XFree(kids)
    return out

ClientMessage = 33
SubstructureRedirectMask = 1 << 20
SubstructureNotifyMask = 1 << 19

targets = [(w, nm) for (w, nm) in walk(root) if needle and needle in nm]
for w, nm in targets:
    print(f"activating 0x{w:x} '{nm}'")
    X.XMapRaised(d, w)
    ev = XEvent()
    ev.xclient.type = ClientMessage
    ev.xclient.send_event = 1
    ev.xclient.display = d
    ev.xclient.window = w
    ev.xclient.message_type = NET_ACTIVE
    ev.xclient.format = 32
    ev.xclient.data[0] = 2  # source indication: pager/tool
    ev.xclient.data[1] = 0  # timestamp: CurrentTime
    ev.xclient.data[2] = 0
    X.XSendEvent(d, root, 0,
        SubstructureRedirectMask | SubstructureNotifyMask, ctypes.byref(ev))
    X.XSetInputFocus(d, w, 2, 0)  # RevertToParent, CurrentTime
X.XSync(d, 0)
if not targets:
    sys.exit(1)
