diff options
Diffstat (limited to 'src/gsr-x11-window-picker.c')
| -rw-r--r-- | src/gsr-x11-window-picker.c | 329 |
1 files changed, 329 insertions, 0 deletions
diff --git a/src/gsr-x11-window-picker.c b/src/gsr-x11-window-picker.c new file mode 100644 index 0000000..83f1aa2 --- /dev/null +++ b/src/gsr-x11-window-picker.c @@ -0,0 +1,329 @@ +#include "gsr-x11-window-picker.h" + +#include <stdlib.h> +#include <string.h> +#include <X11/Xutil.h> +#include <X11/cursorfont.h> +#include <X11/keysym.h> +#include <X11/Xatom.h> + +#include <glib.h> +#include <glib-unix.h> + +/* ── Internal struct ─────────────────────────────────────────────── */ + +struct _GsrX11WindowPicker { + Display *display; /* own connection */ + Window root; + Cursor crosshair; + GsrX11WindowPickCallback callback; + void *userdata; + GSource *source; + guint source_id; + bool finished; /* prevents double callback */ +}; + +/* ── X11 tree walker ─────────────────────────────────────────────── */ + +/** + * Check if a window has a given atom property. + */ +static bool +window_has_atom(Display *display, Window window, Atom atom) +{ + Atom type_ret; + int format_ret; + unsigned long nitems, bytes_after; + unsigned char *data = NULL; + + int rc = XGetWindowProperty(display, window, atom, + 0, 0, False, AnyPropertyType, + &type_ret, &format_ret, + &nitems, &bytes_after, &data); + if (data) + XFree(data); + + return rc == Success && type_ret != None; +} + +/** + * Walk the window tree looking for the first child with _NET_WM_STATE, + * which is the "real" toplevel window managed by the WM. + */ +static Window +find_toplevel_window(Display *display, Window window) +{ + if (window == None) + return None; + + Atom wm_state = XInternAtom(display, "_NET_WM_STATE", False); + if (!wm_state) + return None; + + if (window_has_atom(display, window, wm_state)) + return window; + + Window root, parent; + Window *children = NULL; + unsigned int n_children = 0; + + if (!XQueryTree(display, window, &root, &parent, &children, &n_children) + || !children) + return None; + + Window found = None; + + /* First pass: direct children with _NET_WM_STATE */ + for (int i = (int)n_children - 1; i >= 0; i--) { + if (children[i] && window_has_atom(display, children[i], wm_state)) { + found = children[i]; + goto done; + } + } + /* Second pass: recurse */ + for (int i = (int)n_children - 1; i >= 0; i--) { + if (children[i]) { + Window w = find_toplevel_window(display, children[i]); + if (w != None) { + found = w; + goto done; + } + } + } + +done: + XFree(children); + return found; +} + +/** + * Get the WM_NAME of a window. Returns a g_malloc'd string, or NULL. + */ +static char * +get_window_name(Display *display, Window window) +{ + if (window == None) + return NULL; + + /* Try _NET_WM_NAME first (UTF-8) */ + Atom net_wm_name = XInternAtom(display, "_NET_WM_NAME", False); + Atom utf8_string = XInternAtom(display, "UTF8_STRING", False); + + if (net_wm_name && utf8_string) { + Atom type_ret; + int format_ret; + unsigned long nitems, bytes_after; + unsigned char *data = NULL; + + int rc = XGetWindowProperty(display, window, net_wm_name, + 0, 1024, False, utf8_string, + &type_ret, &format_ret, + &nitems, &bytes_after, &data); + if (rc == Success && data && nitems > 0) { + char *name = g_strdup((const char *)data); + XFree(data); + return name; + } + if (data) + XFree(data); + } + + /* Fallback: XGetWMName */ + XTextProperty wm_name; + if (XGetWMName(display, window, &wm_name) && wm_name.nitems > 0) { + char **list = NULL; + int count = 0; + char *result = NULL; + + if (Xutf8TextPropertyToTextList(display, &wm_name, &list, &count) >= 0 + && list && count > 0 && list[0]) + { + result = g_strdup(list[0]); + } else { + /* Last resort: just copy the raw value */ + result = g_strndup((const char *)wm_name.value, wm_name.nitems); + } + + if (list) + XFreeStringList(list); + if (wm_name.value) + XFree(wm_name.value); + + return result; + } + + return NULL; +} + +/* ── Finish the pick ─────────────────────────────────────────────── */ + +static void +finish_pick(GsrX11WindowPicker *self, Window window, char *name) +{ + if (self->finished) + return; + self->finished = true; + + XUngrabPointer(self->display, CurrentTime); + XUngrabKeyboard(self->display, CurrentTime); + XSync(self->display, False); + + GsrX11WindowPickResult result = { + .window = window, + .name = name, + }; + self->callback(&result, self->userdata); + + /* Self-destroy after callback */ + gsr_x11_window_picker_free(self); +} + +/* ── GSource dispatch — poll X events ────────────────────────────── */ + +static gboolean +picker_source_prepare(GSource *source G_GNUC_UNUSED, gint *timeout) +{ + *timeout = -1; + return FALSE; +} + +static gboolean +picker_source_check(GSource *source G_GNUC_UNUSED) +{ + return TRUE; +} + +static gboolean +picker_source_dispatch(GSource *source G_GNUC_UNUSED, + GSourceFunc callback G_GNUC_UNUSED, + gpointer user_data) +{ + GsrX11WindowPicker *self = user_data; + + if (self->finished) + return G_SOURCE_REMOVE; + + while (XPending(self->display)) { + XEvent ev; + XNextEvent(self->display, &ev); + + if (ev.type == ButtonPress) { + Window clicked = ev.xbutton.subwindow; + if (clicked == None) + clicked = ev.xbutton.window; + + /* Walk tree to find the real toplevel */ + Window toplevel = find_toplevel_window(self->display, clicked); + if (toplevel != None) + clicked = toplevel; + + if (clicked == None || clicked == self->root) { + /* Clicked root / empty space — cancel */ + finish_pick(self, None, NULL); + return G_SOURCE_REMOVE; + } + + char *name = get_window_name(self->display, clicked); + if (!name) + name = g_strdup("(no name)"); + + finish_pick(self, clicked, name); + return G_SOURCE_REMOVE; + } + + if (ev.type == KeyPress) { + KeySym ks = XLookupKeysym(&ev.xkey, 0); + if (ks == XK_Escape) { + finish_pick(self, None, NULL); + return G_SOURCE_REMOVE; + } + } + } + + return G_SOURCE_CONTINUE; +} + +static GSourceFuncs picker_source_funcs = { + .prepare = picker_source_prepare, + .check = picker_source_check, + .dispatch = picker_source_dispatch, + .finalize = NULL, +}; + +/* ── Public API ──────────────────────────────────────────────────── */ + +GsrX11WindowPicker * +gsr_x11_window_picker_new(GsrX11WindowPickCallback callback, + void *userdata) +{ + if (!callback) + return NULL; + + /* Open our own X connection so we don't conflict with GDK */ + Display *dpy = XOpenDisplay(NULL); + if (!dpy) { + g_warning("gsr_x11_window_picker: failed to open X display"); + return NULL; + } + + GsrX11WindowPicker *self = g_new0(GsrX11WindowPicker, 1); + self->display = dpy; + self->root = DefaultRootWindow(dpy); + self->callback = callback; + self->userdata = userdata; + self->finished = false; + + self->crosshair = XCreateFontCursor(dpy, XC_crosshair); + + /* Grab pointer with crosshair */ + int status = XGrabPointer(dpy, self->root, False, + ButtonPressMask | ButtonReleaseMask, + GrabModeAsync, GrabModeAsync, + self->root, self->crosshair, CurrentTime); + if (status != GrabSuccess) { + g_warning("gsr_x11_window_picker: XGrabPointer failed (%d)", status); + XFreeCursor(dpy, self->crosshair); + XCloseDisplay(dpy); + g_free(self); + return NULL; + } + + /* Also grab keyboard so we can detect Escape */ + XGrabKeyboard(dpy, self->root, False, + GrabModeAsync, GrabModeAsync, CurrentTime); + + XSync(dpy, False); + + /* Set up GSource to poll X events */ + int x_fd = ConnectionNumber(dpy); + self->source = g_source_new(&picker_source_funcs, sizeof(GSource)); + g_source_set_callback(self->source, NULL, self, NULL); + g_source_add_unix_fd(self->source, x_fd, G_IO_IN | G_IO_HUP | G_IO_ERR); + self->source_id = g_source_attach(self->source, NULL); + + return self; +} + +void +gsr_x11_window_picker_free(GsrX11WindowPicker *self) +{ + if (!self) + return; + + if (self->source) { + g_source_destroy(self->source); + g_source_unref(self->source); + self->source = NULL; + } + + if (self->display) { + XUngrabPointer(self->display, CurrentTime); + XUngrabKeyboard(self->display, CurrentTime); + if (self->crosshair) + XFreeCursor(self->display, self->crosshair); + XCloseDisplay(self->display); + self->display = NULL; + } + + g_free(self); +} |
