165 lines
7.8 KiB
C
165 lines
7.8 KiB
C
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#include "commands_native.h"
|
|
#include <X11/extensions/XInput2.h>
|
|
#include <X11/Xatom.h>
|
|
#include <X11/XKBlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
|
|
// 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));
|
|
}
|
|
}
|