274 lines
11 KiB
C
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);
|
|
}
|