715 lines
34 KiB
C
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);
|
|
}
|