Files

715 lines
34 KiB
C

#include "commands_native.h"
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/XKBlib.h>
#include <X11/extensions/XTest.h>
#include <X11/keysym.h>
#include <X11/Xutil.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-compose.h>
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <errno.h>
#include <stdlib.h>
#include <pthread.h>
static pthread_mutex_t error_mutex = PTHREAD_MUTEX_INITIALIZER;
static Display *error_display;
static int grab_error;
static int (*previous_error_handler)(Display *, XErrorEvent *);
static int command_error(Display *display, XErrorEvent *event) {
if (display == error_display) { grab_error = event->error_code; return 0; }
return previous_error_handler ? previous_error_handler(display, event) : 0;
}
// Metadata readers share the keyboard owner's guard because Xlib's error
// handler is process-wide. Each begin/end pair runs in one native call.
void cthulhu_x11_begin_read(Display *display) {
pthread_mutex_lock(&error_mutex);
XSync(display, False);
error_display = display;
grab_error = 0;
previous_error_handler = XSetErrorHandler(command_error);
}
int cthulhu_x11_end_read(Display *display) {
XSync(display, False);
int failed = grab_error;
XSetErrorHandler(previous_error_handler);
error_display = NULL;
pthread_mutex_unlock(&error_mutex);
return failed;
}
static int cthulhu_identifier_is_xterm(const char *identifier) {
if (identifier == NULL) return 0;
char lower[256];
size_t length = strlen(identifier);
if (length == 0 || length >= sizeof(lower)) return 0;
for (size_t index = 0; index <= length; index++) {
lower[index] = (char)tolower((unsigned char)identifier[index]);
}
const char *base = strrchr(lower, '/');
base = base != NULL ? base + 1 : lower;
return strcmp(base, "xterm") == 0;
}
static unsigned char *cthulhu_window_property(
Display *display,
Window window,
Atom property,
Atom *type,
int *format,
unsigned long *items
) {
unsigned char *data = NULL;
unsigned long bytesAfter = 0;
if (XGetWindowProperty(
display,
window,
property,
0,
1024,
False,
AnyPropertyType,
type,
format,
items,
&bytesAfter,
&data) != Success || data == NULL) {
if (data != NULL) XFree(data);
return NULL;
}
return data;
}
static unsigned long cthulhu_single_window_property(Display *display, Window window,
Atom property, Atom expectedType) {
Atom type = None;
int format = 0;
unsigned long items = 0, value = 0;
unsigned char *data = cthulhu_window_property(display, window, property, &type, &format, &items);
if (data != NULL && type == expectedType && format == 32 && items == 1)
value = ((unsigned long *)data)[0];
if (data != NULL) XFree(data);
return value;
}
// A minimal window manager can focus GTK's input-only keyboard grab window
// when a combo popup appears. Resolve only a mapped transient popup in that
// grab's explicit window group, with matching process metadata throughout.
// The caller holds the X11 error guard; disappearing windows fail closed.
static Window cthulhu_input_grab_owner(Display *display, Window focus) {
Window root = DefaultRootWindow(display);
XWindowAttributes attributes;
if (focus == None || focus == PointerRoot || focus == root ||
!XGetWindowAttributes(display, focus, &attributes) ||
attributes.class != InputOnly || !attributes.override_redirect ||
attributes.map_state != IsViewable)
return focus;
Atom leaderAtom = XInternAtom(display, "WM_CLIENT_LEADER", False);
Atom pidAtom = XInternAtom(display, "_NET_WM_PID", False);
Atom typeAtom = XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
Window leader = cthulhu_single_window_property(display, focus, leaderAtom, XA_WINDOW);
unsigned long pid = cthulhu_single_window_property(display, focus, pidAtom, XA_CARDINAL);
if (leader == None || pid == 0) return focus;
Window treeRoot, parent, *children = NULL;
unsigned int count = 0;
if (!XQueryTree(display, root, &treeRoot, &parent, &children, &count)) return focus;
Window result = focus;
if (count <= 256) {
Atom comboType = XInternAtom(display, "_NET_WM_WINDOW_TYPE_COMBO", False);
Atom popupType = XInternAtom(display, "_NET_WM_WINDOW_TYPE_POPUP_MENU", False);
Atom dropdownType = XInternAtom(display, "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU", False);
// XQueryTree returns stacking order, bottom to top.
for (unsigned int index = count; index > 0; index--) {
Window candidate = children[index - 1], owner = None;
if (candidate == focus || !XGetWindowAttributes(display, candidate, &attributes) ||
attributes.class != InputOutput || !attributes.override_redirect ||
attributes.map_state != IsViewable)
continue;
Atom type = cthulhu_single_window_property(display, candidate, typeAtom, XA_ATOM);
if (type != comboType && type != popupType && type != dropdownType) continue;
if (cthulhu_single_window_property(display, candidate, leaderAtom, XA_WINDOW) != leader ||
cthulhu_single_window_property(display, candidate, pidAtom, XA_CARDINAL) != pid ||
!XGetTransientForHint(display, candidate, &owner) || owner == None ||
owner == root || owner == candidate ||
!XGetWindowAttributes(display, owner, &attributes) ||
attributes.class != InputOutput || attributes.override_redirect ||
attributes.map_state != IsViewable ||
cthulhu_single_window_property(display, owner, leaderAtom, XA_WINDOW) != leader ||
cthulhu_single_window_property(display, owner, pidAtom, XA_CARDINAL) != pid)
continue;
result = owner;
break;
}
}
if (children != NULL) XFree(children);
return result;
}
// Follow the mapped child under the pointer. The client PID is usually on a
// descendant of the window-manager frame; keep the first PID found on that path.
PointerPosition cthulhu_read_pointer(Display *display) {
PointerPosition result = {0};
cthulhu_x11_begin_read(display);
Window root = DefaultRootWindow(display), queryRoot, child;
int rootX, rootY, winX, winY; unsigned int mask;
if (XQueryPointer(display, root, &queryRoot, &child, &result.x, &result.y,
&winX, &winY, &mask)) {
result.known = 1;
Atom pidAtom = XInternAtom(display, "_NET_WM_PID", False);
for (int depth = 0; child != None && depth < 32; depth++) {
Window current = child;
Atom type = None; int format = 0; unsigned long items = 0;
unsigned char *data = cthulhu_window_property(display, current, pidAtom,
&type, &format, &items);
if (data && type == XA_CARDINAL && format == 32 && items == 1) {
result.pid = (int)((unsigned long *)data)[0];
result.window = current;
}
if (data) XFree(data);
if (!XQueryPointer(display, current, &queryRoot, &child,
&rootX, &rootY, &winX, &winY, &mask)) break;
}
}
if (cthulhu_x11_end_read(display)) result = (PointerPosition){0};
return result;
}
// Resolve only the client's immediate enclosing frame. Some WMs omit or leave
// stale _NET_FRAME_EXTENTS, while GTK reports the real reparenting frame.
// A container with another managed client is not this client's decoration.
// The caller holds the Xlib error guard throughout these read-only queries.
static void cthulhu_read_frame_bounds(Display *display, Window window, Foreground *result) {
Window root, parent, *children = NULL;
unsigned int count = 0;
if (!XQueryTree(display, window, &root, &parent, &children, &count)) return;
if (children) XFree(children);
if (parent == None || parent == root) return;
XWindowAttributes attributes;
if (!XGetWindowAttributes(display, parent, &attributes) ||
attributes.class != InputOutput ||
attributes.map_state != IsViewable) return;
// i3's decoration windows are override-redirect: that flag bypasses WM
// management, not foreground ownership. Validate the enclosing frame by
// its tree, managed children and bounds below instead.
Atom stateAtom = XInternAtom(display, "WM_STATE", False);
Atom type = None; int format = 0; unsigned long items = 0;
unsigned char *data = cthulhu_window_property(display, parent, stateAtom, &type, &format, &items);
if (data) XFree(data);
if (type != None) return;
Window treeRoot, frameParent;
children = NULL;
if (!XQueryTree(display, parent, &treeRoot, &frameParent, &children, &count)) return;
int unambiguous = count <= 64, foundClient = 0;
for (unsigned int index = 0; unambiguous && index < count; index++) {
if (children[index] == window) { foundClient = 1; continue; }
data = cthulhu_window_property(display, children[index], stateAtom, &type, &format, &items);
if (data) XFree(data);
if (type != None) unambiguous = 0;
}
if (children) XFree(children);
if (!unambiguous || !foundClient) return;
int x, y;
Window child;
if (!XTranslateCoordinates(display, parent, root, 0, 0, &x, &y, &child)) return;
int border = attributes.border_width;
x -= border; y -= border;
int width = attributes.width + 2 * border, height = attributes.height + 2 * border;
// Decorations must enclose the client and remain bounded like the EWMH
// extents. Never turn an unrelated workspace into a frame match.
int left = result->x - x, top = result->y - y;
int right = x + width - result->x - result->width;
int bottom = y + height - result->y - result->height;
if (left < 0 || top < 0 || right < 0 || bottom < 0 ||
left >= 1024 || top >= 1024 || right >= 1024 || bottom >= 1024) return;
result->frameX = x; result->frameY = y;
result->frameWidth = width; result->frameHeight = height;
}
// Read-only metadata connection, owned by the caller's native event thread.
// All requests which can race window destruction share the Xlib error guard.
Foreground read_foreground(Display *display) {
Foreground result = {0};
pthread_mutex_lock(&error_mutex);
XSync(display,False);
error_display=display; grab_error=0;
previous_error_handler=XSetErrorHandler(command_error);
Window root=DefaultRootWindow(display), window=None;
Atom type=None; int format=0; unsigned long items=0;
unsigned char *data=cthulhu_window_property(display,root,
XInternAtom(display,"_NET_ACTIVE_WINDOW",False),&type,&format,&items);
if (data && type==XA_WINDOW && format==32 && items==1) {
result.known=1; window=((Window *)data)[0];
} else {
// Non-EWMH X11 sessions (including the native fixture): resolve
// input focus to its client ancestor and resolve input-only popup grabs.
int revert; XGetInputFocus(display,&window,&revert);
result.known=1;
for (int depth=0; window!=None && window!=PointerRoot && window!=root && depth<64; depth++) {
Atom stateType=None; int stateFormat=0; unsigned long stateItems=0;
unsigned char *state=cthulhu_window_property(display,window,
XInternAtom(display,"WM_STATE",False),&stateType,&stateFormat,&stateItems);
if (state) XFree(state);
if (stateType!=None) break;
Window treeRoot,parent,*children=NULL; unsigned int count=0;
if (!XQueryTree(display,window,&treeRoot,&parent,&children,&count)) break;
if (children) XFree(children);
if (parent==root || parent==None) break;
window=parent;
}
window=cthulhu_input_grab_owner(display,window);
}
if (data) XFree(data);
if (window!=None && window!=PointerRoot && window!=root) {
XWindowAttributes attributes;
if (XGetWindowAttributes(display,window,&attributes) && attributes.map_state==IsViewable) {
result.window=window;
Window child;
XTranslateCoordinates(display,window,root,0,0,&result.x,&result.y,&child);
result.width=attributes.width; result.height=attributes.height;
result.outerX=result.x; result.outerY=result.y;
result.outerWidth=result.width; result.outerHeight=result.height;
data=cthulhu_window_property(display,window,XInternAtom(display,"_NET_FRAME_EXTENTS",False),&type,&format,&items);
if (data && type==XA_CARDINAL && format==32 && items==4) {
unsigned long *edges=(unsigned long *)data;
if (edges[0]<1024 && edges[1]<1024 && edges[2]<1024 && edges[3]<1024) {
result.outerX-=edges[0]; result.outerY-=edges[2];
result.outerWidth+=edges[0]+edges[1]; result.outerHeight+=edges[2]+edges[3];
}
}
if (data) XFree(data);
cthulhu_read_frame_bounds(display, window, &result);
data=cthulhu_window_property(display,window,XInternAtom(display,"_NET_WM_PID",False),&type,&format,&items);
if (data && type==XA_CARDINAL && format==32 && items==1) result.pid=(int)((unsigned long *)data)[0];
if (data) XFree(data);
}
}
XSync(display,False);
// A disappearing client is an observed loss, not a process-fatal BadWindow.
if (grab_error) { result.window=0; result.pid=0; }
XSetErrorHandler(previous_error_handler); error_display=NULL;
pthread_mutex_unlock(&error_mutex);
return result;
}
int cthulhu_focus_metadata_unchecked(Display *display, SteamFocusSample *steam) {
Window root = DefaultRootWindow(display);
Atom activeAtom = XInternAtom(display, "_NET_ACTIVE_WINDOW", False);
Atom type = None;
int format = 0;
unsigned long items = 0;
unsigned char *data = cthulhu_window_property(display, root, activeAtom, &type, &format, &items);
if (data == NULL || type != XA_WINDOW || format != 32 || items < 1) {
if (data != NULL) XFree(data);
return 0;
}
Window window = ((Window *)data)[0];
XFree(data);
if (steam != NULL) {
steam->known = 1;
steam->window = window;
}
if (window == None || window == root) {
return 0;
}
Atom classAtom = XInternAtom(display, "WM_CLASS", False);
data = cthulhu_window_property(display, window, classAtom, &type, &format, &items);
int matched = 0;
int identified = 0;
if (data != NULL && type == XA_STRING && format == 8 && items > 0) {
identified = 1;
char *identifier = (char *)data;
if (cthulhu_identifier_is_xterm(identifier)) matched = 1;
char *classIdentifier = identifier + strlen(identifier) + 1;
if (steam != NULL && (unsigned char *)classIdentifier < data + items) {
steam->steam = strcmp(identifier, "steamwebhelper") == 0 &&
strcmp(classIdentifier, "steam") == 0;
}
if (!matched && (unsigned char *)classIdentifier < data + items) {
matched = cthulhu_identifier_is_xterm(classIdentifier);
}
}
if (data != NULL) XFree(data);
if (matched) {
return 1;
}
if (identified) return 2;
Atom pidAtom = XInternAtom(display, "_NET_WM_PID", False);
data = cthulhu_window_property(display, window, pidAtom, &type, &format, &items);
long pid = -1;
if (data != NULL && type == XA_CARDINAL && format == 32 && items > 0) {
pid = (long)((unsigned long *)data)[0];
identified = 1;
}
if (data != NULL) XFree(data);
if (pid > 0) {
char path[64];
snprintf(path, sizeof(path), "/proc/%ld/cmdline", pid);
FILE *cmdline = fopen(path, "rb");
if (cmdline != NULL) {
char executable[256];
size_t length = fread(executable, 1, sizeof(executable) - 1, cmdline);
fclose(cmdline);
executable[length] = '\0';
matched = cthulhu_identifier_is_xterm(executable);
identified = 1;
}
}
if (matched) return 1;
return identified ? 2 : 0;
}
int cthulhu_display_active_window_xterm_match(Display *display) {
pthread_mutex_lock(&error_mutex);
XSync(display,False);
error_display=display; grab_error=0;
previous_error_handler=XSetErrorHandler(command_error);
int match=cthulhu_focus_metadata_unchecked(display, NULL);
XSync(display,False);
if (grab_error) match=0;
XSetErrorHandler(previous_error_handler); error_display=NULL;
pthread_mutex_unlock(&error_mutex);
return match;
}
int cthulhu_active_window_is_xterm(void) {
Display *display = XOpenDisplay(NULL);
if (display == NULL) return 0;
int match = cthulhu_display_active_window_xterm_match(display);
XCloseDisplay(display);
return match == 1;
}
// Ignore Scroll Lock only when its modifier slot has no other meaning. In
// particular, a slot shared with AltGr/level switching must remain significant.
static unsigned int scroll_lock_modifiers(Display *display) {
unsigned int mask = XkbKeysymToModifiers(display, XK_Scroll_Lock) &
~(ShiftMask | ControlMask | Mod1Mask | Mod4Mask);
if (!mask) return 0;
XModifierKeymap *mapping = XGetModifierMapping(display);
if (mapping == NULL) return 0;
int minCode, maxCode, symbolsPerCode;
XDisplayKeycodes(display, &minCode, &maxCode);
KeySym *symbols = XGetKeyboardMapping(display, minCode,
maxCode - minCode + 1, &symbolsPerCode);
if (symbols == NULL) { XFreeModifiermap(mapping); return 0; }
for (int slot = 0; slot < 8; slot++) {
unsigned int bit = 1u << slot;
if (!(mask & bit)) continue;
for (int index = 0; index < mapping->max_keypermod; index++) {
int code = mapping->modifiermap[slot * mapping->max_keypermod + index];
if (!code) continue;
if (code < minCode || code > maxCode) { mask &= ~bit; break; }
for (int symbolIndex = 0; symbolIndex < symbolsPerCode; symbolIndex++) {
KeySym symbol = symbols[(code - minCode) * symbolsPerCode + symbolIndex];
if (symbol != NoSymbol && symbol != XK_Scroll_Lock) mask &= ~bit;
}
}
}
XFree(symbols);
XFreeModifiermap(mapping);
return mask;
}
static void grab_command_keys(CommandDevice *device) {
// Modifier-first chords must be observed even before the command key is
// known. Lock modifiers are transparent; NumLock preserves keypad typing.
unsigned int allowedModifiers = device->ignoredModifierMask |
ControlMask | ShiftMask | device->extraModifierMask;
for (unsigned int mask=0;mask<256;mask++) {
if (mask & ~allowedModifiers) continue;
if (device->capsCode) XGrabKey(device->display,device->capsCode,mask,device->root,False,GrabModeAsync,GrabModeSync);
if (device->insertCode) XGrabKey(device->display,device->insertCode,mask,device->root,False,GrabModeAsync,GrabModeSync);
if (device->keypadCode && !(mask & device->numlockMask)) XGrabKey(device->display,device->keypadCode,mask,device->root,False,GrabModeAsync,GrabModeSync);
}
for (int j=0;j<device->bindingCount;j++) {
NativeBinding binding=device->bindings[j];
if (binding.browseOnly && !device->browse && !(device->probeBindings && binding.probe)) continue;
int code=XKeysymToKeycode(device->display,binding.symbol);
if (!code) continue;
for (unsigned int mask=0;mask<256;mask++) {
if ((mask & ~device->ignoredModifierMask) != binding.modifiers) continue;
// Non-numeric keypad navigation has a different meaning with NumLock.
if (binding.symbol>=XK_KP_Home && binding.symbol<=XK_KP_Delete && (mask & device->numlockMask)) continue;
XGrabKey(device->display,code,mask,device->root,False,GrabModeAsync,GrabModeSync);
}
}
}
int set_command_keys(CommandDevice *device, int enabled) {
// Another reader may have changed Caps Lock while command input was
// delegated. Adopt that state before treating Caps as our modifier again.
if (enabled && !device->commands && device->capsCode) {
XkbStateRec state;
if (XkbGetState(device->display, XkbUseCoreKbd, &state) != Success) return -7;
device->capsLocked = (state.locked_mods & LockMask) != 0;
}
pthread_mutex_lock(&error_mutex);
XSync(device->display,False);
error_display=device->display; grab_error=0;
previous_error_handler=XSetErrorHandler(command_error);
XUngrabKey(device->display,AnyKey,AnyModifier,device->root);
if (!enabled) {
// A synchronous passive grab may already have activated before Go
// consumes its press. XUngrabKey only removes future registrations.
XUngrabKeyboard(device->display,CurrentTime);
device->active=0;
}
if (enabled) grab_command_keys(device);
XSync(device->display,False);
int error=grab_error;
if (error && enabled) {
XUngrabKey(device->display,AnyKey,AnyModifier,device->root);
XSync(device->display,False);
}
XSetErrorHandler(previous_error_handler); error_display=NULL;
pthread_mutex_unlock(&error_mutex);
device->commands=enabled && !error;
return error;
}
// Replace passive grabs transactionally. A conflicting grab restores the old
// configuration so preferences can report the failure without losing controls.
int configure_command_bindings(CommandDevice *device, NativeBinding *bindings, int count, unsigned int extraModifierMask, int insertCode, int keypadCode, int capsCode) {
NativeBinding *copy=count ? malloc(sizeof(NativeBinding)*count) : NULL;
if (count && !copy) return -3;
if (count) memcpy(copy,bindings,sizeof(NativeBinding)*count);
for (int i=0;i<count;i++) {
int code=XKeysymToKeycode(device->display,copy[i].symbol);
if (!code || XkbKeycodeToKeysym(device->display,code,0,0)!=copy[i].symbol) { free(copy); return -2; }
}
int previousInsert=device->insertCode, previousKeypad=device->keypadCode, previousCaps=device->capsCode;
NativeBinding *previous=device->bindings;
int previousCount=device->bindingCount, enabled=device->commands;
unsigned int previousMask=device->extraModifierMask;
device->bindings=copy; device->bindingCount=count; device->extraModifierMask=extraModifierMask;
device->insertCode=insertCode; device->keypadCode=keypadCode; device->capsCode=capsCode;
// Probe newly customized browse-only chords even outside browse mode, so
// Apply can reject a conflict instead of failing on the next mode change.
for (int i=0;i<count;i++) if (copy[i].browseOnly && copy[i].probe && !device->browse) device->probeBindings=1;
int probing=device->probeBindings;
int error=set_command_keys(device,enabled);
device->probeBindings=0;
if (!error && probing) error=set_command_keys(device,enabled);
if (error) {
device->bindings=previous; device->bindingCount=previousCount; device->extraModifierMask=previousMask;
device->insertCode=previousInsert; device->keypadCode=previousKeypad; device->capsCode=previousCaps;
int restoreError=set_command_keys(device,enabled);
free(copy);
return restoreError ? -6 : error;
}
free(previous);
if (capsCode && !previousCaps) {
device->capsLocked=0;
XkbLockModifiers(device->display,XkbUseCoreKbd,LockMask,0);
XSync(device->display,False);
}
return 0;
}
int set_browse_keys(CommandDevice *device, int enabled) {
int previous=device->browse;
device->browse=enabled;
int error=set_command_keys(device,device->commands);
if (error) {
device->browse=previous;
set_command_keys(device,1);
}
device->browseWindow=read_foreground(device->display).window;
return error;
}
CommandDevice *open_commands(int useInsert, int useKeypad, int useCaps, int *error) {
CommandDevice *device = calloc(1,sizeof(CommandDevice));
if (!device) { *error=-3; return NULL; }
device->display = XOpenDisplay(NULL);
if (!device->display) { free(device); *error = -1; return NULL; }
int eventBase, errorBase, majorVersion, minorVersion;
if (!XTestQueryExtension(device->display,&eventBase,&errorBase,&majorVersion,&minorVersion)) {
XCloseDisplay(device->display); free(device); *error=-5; return NULL;
}
Bool repeatSupported=False;
if (!XkbSetDetectableAutoRepeat(device->display,True,&repeatSupported) || !repeatSupported) {
XCloseDisplay(device->display); free(device); *error=-4; return NULL;
}
device->root = DefaultRootWindow(device->display);
int xkbOpcode, xkbError, xkbMajor=XkbMajorVersion, xkbMinor=XkbMinorVersion;
if (!XkbQueryExtension(device->display,&xkbOpcode,&device->xkbEvent,&xkbError,&xkbMajor,&xkbMinor)) {
XCloseDisplay(device->display); free(device); *error=-7; return NULL;
}
const char *lockNames[]={"Caps Lock","Num Lock","Scroll Lock"};
for (int i=0;i<3;i++) {
int index; Bool state;
if (XkbGetNamedIndicator(device->display,XInternAtom(device->display,lockNames[i],False),&index,&state,NULL,NULL)) {
device->lockIndicators[i]=1u<<index;
}
}
XkbSelectEvents(device->display,XkbUseCoreKbd,XkbIndicatorStateNotifyMask,XkbIndicatorStateNotifyMask);
device->insertCode = useInsert ? XKeysymToKeycode(device->display,XK_Insert) : 0;
device->keypadCode = useKeypad ? XKeysymToKeycode(device->display,XK_KP_Insert) : 0;
device->capsCode = useCaps ? XKeysymToKeycode(device->display,XK_Caps_Lock) : 0;
device->numlockMask = XkbKeysymToModifiers(device->display,XK_Num_Lock);
device->ignoredModifierMask = LockMask | device->numlockMask |
scroll_lock_modifiers(device->display);
if ((useInsert && !device->insertCode) || (useKeypad && !device->keypadCode) || (useCaps && !device->capsCode)) {
XCloseDisplay(device->display); free(device); *error=-2; return NULL;
}
*error=set_command_keys(device,1);
if (*error) { XCloseDisplay(device->display); free(device); return NULL; }
if (device->capsCode) {
device->capsLocked=0;
XkbLockModifiers(device->display,XkbUseCoreKbd,LockMask,0);
XSync(device->display,False);
}
return device;
}
int command_lock_state(CommandDevice *device) {
unsigned int indicators=0;
XkbGetIndicatorState(device->display,XkbUseCoreKbd,&indicators);
int locks=0;
for (int i=0;i<3;i++) if (indicators & device->lockIndicators[i]) locks |= 1<<i;
// Ignore the transient XKB lock action on a grabbed reader modifier.
if (device->commands && device->capsCode) locks=(locks & ~1) | device->capsLocked;
return locks;
}
void toggle_command_caps_lock(CommandDevice *device) {
device->capsLocked=!device->capsLocked;
XkbLockModifiers(device->display,XkbUseCoreKbd,LockMask,device->capsLocked ? LockMask:0);
XSync(device->display,False);
}
int next_command_key(CommandDevice *device, CommandKey *key) {
if (!XPending(device->display)) {
struct pollfd fd={ConnectionNumber(device->display),POLLIN,0};
int result=poll(&fd,1,20);
if (result<0 && errno==EINTR) return 0;
if (result<0 || (fd.revents & (POLLERR|POLLHUP|POLLNVAL))) return -1;
if (result==0) return 0;
}
XEvent event;
XNextEvent(device->display,&event);
if (event.type==device->xkbEvent && ((XkbEvent *)&event)->any.xkb_type==XkbIndicatorStateNotify) {
key->locks=command_lock_state(device);
key->timestamp=((XkbEvent *)&event)->indicators.time;
return 2;
}
if (event.type!=KeyPress && event.type!=KeyRelease) return 0;
// Events captured before suspension can still be queued in Xlib after
// the grabs are released. Discard that old reader input; it must neither
// dispatch commands nor acquire a new explicit grab in the delegated app.
if (!device->commands) return 0;
// Preserve the intentional capitalization state on both modifier edges.
if (device->capsCode && event.xkey.keycode==(unsigned)device->capsCode) {
XkbLockModifiers(device->display,XkbUseCoreKbd,LockMask,device->capsLocked ? LockMask:0);
XSync(device->display,False);
}
if (!device->active && event.type==KeyPress) {
KeySym symbol=XLookupKeysym(&event.xkey,0);
unsigned int modifiers=event.xkey.state & ~device->ignoredModifierMask;
int browseOnly=0, activate=0;
for (int i=0;i<device->bindingCount;i++) {
NativeBinding binding=device->bindings[i];
if (binding.symbol==symbol && binding.modifiers==modifiers) {
browseOnly=binding.browseOnly; activate=binding.activate; break;
}
}
if (browseOnly) {
Foreground foreground=read_foreground(device->display);
if (!device->browse || !foreground.known || foreground.window==0 ||
foreground.window!=device->browseWindow) {
XAllowEvents(device->display,ReplayKeyboard,event.xkey.time); XFlush(device->display); return 0;
}
}
// Python leaves Return on a natively focused standard button to the
// application. Replay the real press before taking an explicit grab.
if (symbol==XK_Return && activate && device->browse && device->nativeButtonFocus &&
!(event.xkey.state & (ControlMask|ShiftMask))) {
XAllowEvents(device->display,ReplayKeyboard,event.xkey.time); XFlush(device->display); return 0;
}
// Upgrade our passive Insert grab to an explicit grab. Unlike a passive
// grab, this stays active if Insert is released before the command key.
int status=XGrabKeyboard(device->display,device->root,False,GrabModeAsync,GrabModeAsync,event.xkey.time);
if (status!=GrabSuccess) { XAllowEvents(device->display,ReplayKeyboard,event.xkey.time); XFlush(device->display); return -2; }
device->active=1;
}
key->pressed=(event.type==KeyPress); key->code=event.xkey.keycode;
key->symbol=XLookupKeysym(&event.xkey,0);
key->modifiers=event.xkey.state & ~device->ignoredModifierMask;
key->text[0]=0; key->textSymbol=0;
if (device->internalUI && event.type==KeyPress) {
char ignored[64]; KeySym translated;
XLookupString(&event.xkey,ignored,sizeof(ignored),&translated,NULL);
key->textSymbol=translated;
if (!(event.xkey.state & (ControlMask|Mod1Mask|Mod4Mask))) {
if (device->composeState) xkb_compose_state_feed(device->composeState,translated);
enum xkb_compose_status status=device->composeState ? xkb_compose_state_get_status(device->composeState) : XKB_COMPOSE_NOTHING;
if (status==XKB_COMPOSE_COMPOSED) {
xkb_compose_state_get_utf8(device->composeState,key->text,sizeof(key->text));
xkb_compose_state_reset(device->composeState);
} else if (status==XKB_COMPOSE_CANCELLED) {
xkb_compose_state_reset(device->composeState);
} else if (status==XKB_COMPOSE_NOTHING) {
xkb_keysym_to_utf8(translated,key->text,sizeof(key->text));
}
} else {
xkb_keysym_to_utf8(XLookupKeysym(&event.xkey,0),key->text,sizeof(key->text));
}
}
key->timestamp=event.xkey.time;
return 1;
}
void release_command_keyboard(CommandDevice *device) {
XUngrabKeyboard(device->display,CurrentTime); XFlush(device->display); device->active=0;
}
int replay_command_keys(CommandDevice *device, CommandKey *keys, int count) {
pthread_mutex_lock(&error_mutex);
XSync(device->display,False);
error_display=device->display; grab_error=0;
previous_error_handler=XSetErrorHandler(command_error);
XUngrabKeyboard(device->display,CurrentTime);
XUngrabKey(device->display,AnyKey,AnyModifier,device->root);
device->active=0;
int injected=1;
for (int i=0;i<count;i++) {
// Replaying an unbound chord must not restore Caps Lock's lock action
// while that key is configured as the reader modifier.
if (device->capsCode && keys[i].code==device->capsCode) continue;
if (!XTestFakeKeyEvent(device->display,keys[i].code,keys[i].pressed,CurrentTime)) injected=0;
}
if (device->commands) grab_command_keys(device);
XSync(device->display,False);
XSetErrorHandler(previous_error_handler);
int error=grab_error;
error_display=NULL;
pthread_mutex_unlock(&error_mutex);
if (!injected) return -1;
return error;
}
void close_commands(CommandDevice *device) {
if (device->composeState) xkb_compose_state_unref(device->composeState);
if (device->composeTable) xkb_compose_table_unref(device->composeTable);
if (device->textContext) xkb_context_unref(device->textContext);
if (device->active) release_command_keyboard(device);
XUngrabKey(device->display,AnyKey,AnyModifier,device->root);
XSync(device->display,False); XCloseDisplay(device->display); free(device->bindings); free(device);
}
int acquire_internal_keyboard(CommandDevice *device) {
if (!device->active) {
int status=XGrabKeyboard(device->display,device->root,False,GrabModeAsync,GrabModeAsync,CurrentTime);
if (status!=GrabSuccess) return status;
device->active=1;
}
if (!device->textContext) {
device->textContext=xkb_context_new(XKB_CONTEXT_NO_FLAGS);
const char *locale=getenv("LC_ALL");
if (!locale || !*locale) locale=getenv("LC_CTYPE");
if (!locale || !*locale) locale=getenv("LANG");
if (!locale || !*locale) locale="C";
if (device->textContext) device->composeTable=xkb_compose_table_new_from_locale(device->textContext,locale,XKB_COMPOSE_COMPILE_NO_FLAGS);
if (device->composeTable) device->composeState=xkb_compose_state_new(device->composeTable,XKB_COMPOSE_STATE_NO_FLAGS);
}
if (device->composeState) xkb_compose_state_reset(device->composeState);
device->internalUI=1;
return 0;
}
void reset_internal_compose(CommandDevice *device) {
device->internalUI=0;
if (device->composeState) xkb_compose_state_reset(device->composeState);
}