// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later #include "window_title.h" #include "commands_native.h" #include #include #include #include #include #include #include static unsigned long read_pid(Display *display, Window window); static int wine_process(Display *display, Window window) { unsigned long pid = read_pid(display, window); if (!pid) return 0; char path[64], executable[1024]; snprintf(path, sizeof(path), "/proc/%lu/exe", pid); ssize_t length = readlink(path, executable, sizeof(executable) - 1); if (length <= 0 || length >= (ssize_t)sizeof(executable) - 1) return 0; executable[length] = 0; const char *name = strrchr(executable, '/'); if (!name) return 0; ++name; return !strcmp(name, "wine") || !strcmp(name, "wine64") || !strcmp(name, "wine-preloader") || !strcmp(name, "wine64-preloader"); } static char *read_title(Display *display, Window window) { Atom actualType; int format; unsigned long items, remaining; unsigned char *data = NULL; Atom name = XInternAtom(display, "_NET_WM_NAME", False); if (XGetWindowProperty(display, window, name, 0, 1024, False, XInternAtom(display, "UTF8_STRING", False), &actualType, &format, &items, &remaining, &data) == Success && data && format == 8 && items) { char *title = strndup((char *)data, items); XFree(data); return title; } if (data) XFree(data); XTextProperty property = {0}; char *title = NULL; if (XGetWMName(display, window, &property) && property.value) { char **strings = NULL; int count = 0; if (Xutf8TextPropertyToTextList(display, &property, &strings, &count) >= Success && count > 0) title = strdup(strings[0]); if (strings) XFreeStringList(strings); XFree(property.value); } return title; } static int is_wine_desktop(const char *title) { return title && strcasestr(title, "Wine Desktop") != NULL; } static char *read_application(Display *display, Window window) { XClassHint hint = {0}; char *application = NULL; if (XGetClassHint(display, window, &hint)) { const char *name = hint.res_name && *hint.res_name ? hint.res_name : hint.res_class; if (name) application = strdup(name); } if (hint.res_name) XFree(hint.res_name); if (hint.res_class) XFree(hint.res_class); return application; } static Window parent_window(Display *display, Window window) { Window root, parent, *children = NULL; unsigned int count; if (!XQueryTree(display, window, &root, &parent, &children, &count)) return None; if (children) XFree(children); return parent; } static int is_descendant(Display *display, Window window, Window desktop) { for (int depth = 0; window && depth < 64; depth++) { if (window == desktop) return 1; Window parent = parent_window(display, window); if (parent == window) break; window = parent; } return 0; } static int usable_child(Display *display, Window window, WindowTitle *result) { XWindowAttributes attributes; if (!XGetWindowAttributes(display, window, &attributes) || attributes.map_state != IsViewable) return 0; char *title = read_title(display, window); if (!title || !*title || is_wine_desktop(title) || !strcasecmp(title, "Default IME") || !strcasecmp(title, "Program Manager")) { free(title); return 0; } char *application = read_application(display, window); XClassHint hint = {0}; XGetClassHint(display, window, &hint); const char *identifiers[] = {hint.res_name, hint.res_class}; int infrastructure = 0; for (unsigned int i = 0; i < 2; i++) { if (identifiers[i] && (!strcasecmp(identifiers[i], "wine-access.exe") || !strcasecmp(identifiers[i], "cthulhu-wine-access.exe") || (!strcasecmp(identifiers[i], "nvda.exe") && !strcasecmp(title, "NVDA")))) infrastructure = 1; } if (hint.res_name) XFree(hint.res_name); if (hint.res_class) XFree(hint.res_class); if (infrastructure) { free(title); free(application); return 0; } result->titleWindow = window; result->title = title; result->application = application; return 1; } // Prefer the focused child's titled ancestor, after proving that focus belongs // to this desktop. Fallback traversal is bounded and ignores hidden children. static void read_wine_title(Display *display, Window desktop, WindowTitle *result) { Window focus; int revert; XGetInputFocus(display, &focus, &revert); if (focus != None && focus != PointerRoot && is_descendant(display, focus, desktop)) { for (int depth = 0; focus && focus != desktop && depth < 64; depth++) { if (usable_child(display, focus, result)) { result->focused = 1; return; } focus = parent_window(display, focus); } } Window queue[200] = {desktop}; unsigned int queued = 1; for (unsigned int index = 0; index < queued; index++) { if (index && usable_child(display, queue[index], result)) return; Window root, parent, *children = NULL; unsigned int count; if (XQueryTree(display, queue[index], &root, &parent, &children, &count)) { for (unsigned int child = 0; child < count && queued < 200; child++) queue[queued++] = children[child]; } if (children) XFree(children); } } WindowTitle cthulhu_read_window_title(Display *display, Window window) { WindowTitle result = {0}; if (!display || !window) return result; cthulhu_x11_begin_read(display); XWindowAttributes attributes; if (XGetWindowAttributes(display, window, &attributes) && attributes.map_state == IsViewable) { result.window = window; char *title = read_title(display, window); if (is_wine_desktop(title)) { result.wine = 1; free(title); read_wine_title(display, window, &result); } else { result.titleWindow = window; result.title = title; result.application = read_application(display, window); XClassHint hint = {0}; if (XGetClassHint(display, window, &hint)) { result.wine = hint.res_class && !strcasecmp(hint.res_class, "Wine"); if (hint.res_name) XFree(hint.res_name); if (hint.res_class) XFree(hint.res_class); } // Current Wine uses the program's .exe for both WM_CLASS fields. // The actual client process distinguishes it from a Linux window // with the same title or class, without any prefix environment. result.wine = result.wine || wine_process(display, window); Window focus; int revert; XGetInputFocus(display, &focus, &revert); result.focused = focus != None && focus != PointerRoot && is_descendant(display, focus, window); } XWindowAttributes child; Window translated; if (result.titleWindow && XGetWindowAttributes(display, result.titleWindow, &child) && child.map_state == IsViewable && XTranslateCoordinates(display, result.titleWindow, DefaultRootWindow(display), 0, 0, &result.x, &result.y, &translated)) { result.width = child.width; result.height = child.height; } } if (cthulhu_x11_end_read(display)) { free(result.title); free(result.application); result = (WindowTitle){0}; } return result; } static unsigned long read_pid(Display *display, Window window) { Atom type; int format; unsigned long items, remaining, pid = 0; unsigned char *data = NULL; if (XGetWindowProperty(display, window, XInternAtom(display, "_NET_WM_PID", False), 0, 1, False, XA_CARDINAL, &type, &format, &items, &remaining, &data) == Success && data && type == XA_CARDINAL && format == 32 && items) pid = *(unsigned long *)data; if (data) XFree(data); return pid; } // Enumerate clients, then Wine desktop children, without relying on focus. // A bounded root traversal also serves window managers without an EWMH list. ApplicationWindow *cthulhu_list_application_windows(Display *display, int *count) { *count = 0; if (!display) return NULL; ApplicationWindow *result = calloc(4096, sizeof(*result)); if (!result) return NULL; cthulhu_x11_begin_read(display); Window queue[4096]; int wine[4096] = {0}; unsigned int queued = 0; Atom type; int format; unsigned long items, remaining; unsigned char *data = NULL; Window root = DefaultRootWindow(display); if (XGetWindowProperty(display, root, XInternAtom(display, "_NET_CLIENT_LIST", False), 0, 4096, False, XA_WINDOW, &type, &format, &items, &remaining, &data) == Success && data && type == XA_WINDOW && format == 32 && items) { Window *clients = (Window *)data; for (unsigned long i = 0; i < items && queued < 4096; i++) queue[queued++] = clients[i]; } else { queue[queued++] = root; } if (data) XFree(data); for (unsigned int i = 0; i < queued; i++) { char *title = read_title(display, queue[i]); int desktop = is_wine_desktop(title); free(title); char *application = read_application(display, queue[i]); int descend = !application || !*application || desktop || wine[i]; if (application && *application && !desktop) { // Skip Wine infrastructure using the same title discovery rules. WindowTitle child = {0}; if (!wine[i] || usable_child(display, queue[i], &child)) { result[*count] = (ApplicationWindow){application, read_pid(display, queue[i]), wine[i]}; (*count)++; application = NULL; } free(child.title); free(child.application); } free(application); if (descend) { Window treeRoot, parent, *children = NULL; unsigned int childCount; if (XQueryTree(display, queue[i], &treeRoot, &parent, &children, &childCount)) { for (unsigned int j = 0; j < childCount && queued < 4096; j++) { queue[queued] = children[j]; wine[queued++] = wine[i] || desktop; } } if (children) XFree(children); } } cthulhu_x11_end_read(display); return result; } void cthulhu_free_application_windows(ApplicationWindow *windows, int count) { for (int i = 0; i < count; i++) free(windows[i].application); free(windows); }