Files

274 lines
11 KiB
C

// 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 <X11/Xatom.h>
#include <X11/Xutil.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <stdio.h>
#include <unistd.h>
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);
}