#include "commands_native.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 cthulhu_i3_no_client_focus(display) ? 2 : 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;jbindingCount;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;idisplay,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;ibrowse) 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<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<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 (device->localKeyboard) { int local = next_local_key(device, &event); if (local < 0) return 3; if (!local) return 0; } 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;ibindingCount;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->remoteSymbol=key->symbol; // Remote keypad identities must follow Num Lock. Keep ordinary command // matching at level zero, and forward Shift separately like other keys. // Retain this identity in the capture owner until the physical release. if (IsKeypadKey(key->symbol)) { unsigned int consumed; KeySym translated; if (XkbLookupKeySym(device->display,event.xkey.keycode, event.xkey.state & device->numlockMask,&consumed,&translated)) key->remoteSymbol=translated; } 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) { release_local_keyboard(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;ibypass && 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 (device->bypass && device->capsCode) { XkbStateRec state; if (XkbGetState(device->display,XkbUseCoreKbd,&state)!=Success) return -7; device->capsLocked=(state.locked_mods & LockMask)!=0; } 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); } // Remote capture needs only the exclusive grab: no text translation or // compose state, because keys leave the reader as logical transitions. int acquire_remote_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; } return 0; } 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); }