// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later #include "commands_native.h" #include #include #include #include #include // Xorg marks XTEST devices with a non-deletable, read-only property. Device // names and the core send_event flag are not provenance. Unknown/non-device // sources stay ineligible; no timing match against XRecord is involved. static int local_source(CommandDevice *device, int source) { if (source <= 0 || source == device->localKeyboard) return 0; int count = 0, allowed = 0; cthulhu_x11_begin_read(device->display); XIDeviceInfo *info = XIQueryDevice(device->display, source, &count); if (info && count == 1 && info->use == XISlaveKeyboard && info->enabled && info->attachment == device->localKeyboard) { Atom type = None; int format = 0; unsigned long items = 0, remaining = 0; unsigned char *data = NULL; Atom xtest = XInternAtom(device->display, "XTEST Device", True); int result = xtest == None ? BadAtom : XIGetProperty(device->display, source, xtest, 0, 1, False, AnyPropertyType, &type, &format, &items, &remaining, &data); // The atom must exist on the server, but not on this source device. int synthetic = result != Success || type != None; if (data) XFree(data); data = NULL; Atom node = XInternAtom(device->display, "Device Node", True); if (!synthetic && node != None && XIGetProperty(device->display, source, node, 0, 64, False, XA_STRING, &type, &format, &items, &remaining, &data) == Success && type == XA_STRING && format == 8 && items > 16 && items < 256 && remaining == 0 && data && memcmp(data, "/dev/input/event", 16) == 0) { int valid = 1; for (unsigned long i = 16; i < items; i++) if (data[i] < '0' || data[i] > '9') valid = 0; char path[256]; memcpy(path, data, items); path[items] = 0; struct stat status; if (valid && stat(path, &status) == 0 && S_ISCHR(status.st_mode)) allowed = 1; } if (data) XFree(data); } if (info) XIFreeDeviceInfo(info); if (cthulhu_x11_end_read(device->display)) allowed = 0; return allowed; } static int acquire_local_keyboard_unchecked(CommandDevice *device) { int eventBase, errorBase, major = 2, minor = 0, pointer = 0; if (device->active || !XQueryExtension(device->display, "XInputExtension", &device->xiOpcode, &eventBase, &errorBase) || XIQueryVersion(device->display, &major, &minor) != Success || major < 2 || !XIGetClientPointer(device->display, None, &pointer)) return -1; int count = 0, keyboard = 0; XIDeviceInfo *info = XIQueryDevice(device->display, pointer, &count); if (info && count == 1 && info->use == XIMasterPointer && info->enabled) keyboard = info->attachment; if (info) XIFreeDeviceInfo(info); if (!keyboard) return -1; // A key held before the dialog cannot submit it on repeat or provide an // inherited modifier. The caller may retry after releasing that chord. char held[32]; XQueryKeymap(device->display, held); for (int i = 0; i < 32; i++) if (held[i]) return -1; unsigned char bits[XIMaskLen(XI_LASTEVENT)] = {0}; XISetMask(bits, XI_KeyPress); XISetMask(bits, XI_KeyRelease); XIEventMask mask = {keyboard, sizeof(bits), bits}; int result = XIGrabDevice(device->display, keyboard, device->root, CurrentTime, None, GrabModeAsync, GrabModeAsync, False, &mask); if (result != GrabSuccess) return result ? result : -1; unsigned char changes[XIMaskLen(XI_LASTEVENT)] = {0}; XISetMask(changes, XI_HierarchyChanged); XIEventMask hierarchy = {XIAllDevices, sizeof(changes), changes}; XISelectEvents(device->display, device->root, &hierarchy, 1); device->localKeyboard = keyboard; memset(device->localHeld, 0, sizeof(device->localHeld)); // Initialize the ordinary text/compose renderer after acquiring XI2. Its // core-grab branch is skipped because the device already owns capture. device->active = 1; acquire_internal_keyboard(device); XFlush(device->display); return 0; } int acquire_local_keyboard(CommandDevice *device) { // The selected master can disappear between query and grab. Like source // validation, acquisition must turn BadDevice into denial, not reader exit. cthulhu_x11_begin_read(device->display); int result = acquire_local_keyboard_unchecked(device); int error = cthulhu_x11_end_read(device->display); if (error) { release_local_keyboard(device); device->active = 0; reset_internal_compose(device); return -1; } return result; } // Convert the very same device event to the existing text renderer. Core and // SendEvent traffic is discarded in this mode, as are XTEST and unknown sources. int next_local_key(CommandDevice *device, XEvent *event) { if (event->type != GenericEvent || event->xcookie.extension != device->xiOpcode || !XGetEventData(device->display, &event->xcookie)) return 0; int accepted = 0; XKeyEvent key = {0}; // Device removal/disable/reattachment invalidates this capture, including // keys whose release can no longer arrive. Reopen against a fresh hierarchy. if (!event->xcookie.send_event && event->xcookie.evtype == XI_HierarchyChanged) accepted = -1; if (event->xcookie.evtype == XI_KeyPress || event->xcookie.evtype == XI_KeyRelease) { XIDeviceEvent *input = event->xcookie.data; if (!event->xcookie.send_event && !input->send_event && input->deviceid == device->localKeyboard && input->detail >= 8 && input->detail <= 255 && !(input->flags & XIKeyRepeat) && local_source(device, input->sourceid)) { int code = input->detail; // Duplicate downs and releases without a down in this capture // lifetime cannot navigate or activate a local form. if ((input->evtype == XI_KeyPress) != !!device->localHeld[code]) { key.type = input->evtype == XI_KeyPress ? KeyPress : KeyRelease; key.display = device->display; key.window = input->event; key.root = input->root; key.time = input->time; key.keycode = code; key.same_screen = True; // Ignore modifiers held by an injected source. Build state // solely from keys admitted during this local capture. for (int i = 8; i < 256; i++) if (device->localHeld[i]) key.state |= XkbKeysymToModifiers(device->display, XkbKeycodeToKeysym(device->display, i, 0, 0)); device->localHeld[code] = key.type == KeyPress; accepted = 1; } } } XFreeEventData(device->display, &event->xcookie); if (accepted > 0) { memset(event, 0, sizeof(*event)); event->xkey = key; } return accepted; } void release_local_keyboard(CommandDevice *device) { if (device->localKeyboard) { cthulhu_x11_begin_read(device->display); XIUngrabDevice(device->display, device->localKeyboard, CurrentTime); unsigned char bits[XIMaskLen(XI_LASTEVENT)] = {0}; XIEventMask mask = {XIAllDevices, sizeof(bits), bits}; XISelectEvents(device->display, device->root, &mask, 1); // A removed master has already lost its grab. Still clear our local // ownership and root event selection when XIUngrab reports BadDevice. cthulhu_x11_end_read(device->display); device->localKeyboard = 0; memset(device->localHeld, 0, sizeof(device->localHeld)); } }