aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/gsr-x11-window-picker.c
diff options
context:
space:
mode:
authorTrung Lê <8@tle.id.au>2026-02-21 23:40:05 +1100
committerTrung Lê <8@tle.id.au>2026-02-21 23:47:49 +1100
commit3eed1b64af72dcb4bcb8ae28d45856767d4acff1 (patch)
tree0aeef768169d69c4ed24c3cc03398c63aef3ff59 /src/gsr-x11-window-picker.c
Initial commit
Diffstat (limited to 'src/gsr-x11-window-picker.c')
-rw-r--r--src/gsr-x11-window-picker.c329
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);
+}