From 662b3d414fc862efb66119181a1043e06d97e9be Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:13:07 +0200 Subject: [PATCH] X11: take modifier masks from the server's modifier map Right Alt (keycode 108) was hard-coded as Mod5/AltGr. On layouts where it is Mod1, the swallowed-release reconcile accepted Mod1 as held but rebuilt the state with Mod5, so the guest could see Option and Function together. Read the per-key masks from XGetModifierMapping (refreshed on MappingNotify) and use them everywhere; the old table is the fallback for unmapped keys. The probe now checks Right Alt against whichever mask the backend reports. Co-Authored-By: Claude Opus 5.5 --- patches/cocotron.patch | 93 ++++++++++++++++++++++++------------- tools/riot-modifier-check.m | 13 ++++-- 2 files changed, 71 insertions(+), 35 deletions(-) diff --git a/patches/cocotron.patch b/patches/cocotron.patch index ca2e81b..22a1fa3 100644 --- a/patches/cocotron.patch +++ b/patches/cocotron.patch @@ -2687,7 +2687,7 @@ index 18189404..12cde429 100644 +- (NSRect) frameForRootRect: (NSRect) rect; @end diff --git a/AppKit/X11.backend/X11Display.m b/AppKit/X11.backend/X11Display.m -index 5ae29bf4..a4a880b0 100644 +index 5ae29bf4..3a3c68f8 100644 --- a/AppKit/X11.backend/X11Display.m +++ b/AppKit/X11.backend/X11Display.m @@ -48,12 +48,271 @@ @@ -3575,7 +3575,7 @@ index 5ae29bf4..a4a880b0 100644 } - (NSEventModifierFlags) modifierFlagsForState: (unsigned int) state { -@@ -1010,17 +1625,136 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1010,17 +1625,157 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { return result; } @@ -3583,15 +3583,37 @@ index 5ae29bf4..a4a880b0 100644 + * carries the state AFTER it. Keep both sides separately so releasing one + * Shift/Command key does not clear the other, and reconcile missed releases + * when the compositor intercepts a shortcut or changes keyboard focus. */ ++static unsigned char rclolServerModifiers[256]; ++static BOOL rclolServerModifiersLoaded; ++ ++/* Which modifier each keycode sets depends on the layout: Right Alt is Mod5 ++ * (AltGr) on most European layouts and Mod1 (Alt) on US. Ask the server. */ ++static void rclolLoadServerModifiers(Display *display) { ++ if (rclolServerModifiersLoaded) return; ++ rclolServerModifiersLoaded = YES; ++ memset(rclolServerModifiers, 0, sizeof(rclolServerModifiers)); ++ XModifierKeymap *map = XGetModifierMapping(display); ++ if (!map) return; ++ for (int mod = 0; mod < 8; ++mod) ++ for (int i = 0; i < map->max_keypermod; ++i) { ++ KeyCode code = map->modifiermap[mod * map->max_keypermod + i]; ++ if (code) rclolServerModifiers[code] |= 1 << mod; ++ } ++ XFreeModifiermap(map); ++} ++ +static unsigned int rclolModifierMask(unsigned int key) { ++ unsigned int fallback; + switch (key) { -+ case 50: case 62: return ShiftMask; -+ case 37: case 105: return ControlMask; -+ case 64: return Mod1Mask; -+ case 108: return Mod5Mask; -+ case 133: case 134: return Mod4Mask; ++ case 50: case 62: fallback = ShiftMask; break; ++ case 37: case 105: fallback = ControlMask; break; ++ case 64: fallback = Mod1Mask; break; ++ case 108: fallback = Mod5Mask; break; ++ case 133: case 134: fallback = Mod4Mask; break; + default: return 0; + } ++ unsigned int server = rclolServerModifiers[key] & (Mod1Mask | Mod4Mask | Mod5Mask | ShiftMask | ControlMask); ++ return server ? server : fallback; +} + +- (void) postModifierKey:(unsigned int)key state:(unsigned int)state window:(id)delegate { @@ -3631,9 +3653,7 @@ index 5ae29bf4..a4a880b0 100644 + for (unsigned int i = 0; i < sizeof(keys); ++i) { + unsigned int key = keys[i]; + if (key == except || !_modifierKeysDown[key]) continue; -+ unsigned int held = rclolModifierMask(key); -+ if (key == 108) held |= Mod1Mask; /* Right Alt is Mod1 on layouts without AltGr. */ -+ if (state & held) continue; ++ if (state & rclolModifierMask(key)) continue; + _modifierKeysDown[key] = 0; + unsigned int now = state & ~(ShiftMask | ControlMask | Mod1Mask | Mod4Mask | Mod5Mask); + for (unsigned int j = 0; j < sizeof(keys); ++j) @@ -3648,6 +3668,7 @@ index 5ae29bf4..a4a880b0 100644 X11Window* window = (X11Window*) [self windowForID: ev->xany.window]; id delegate = [window delegate]; ++ rclolLoadServerModifiers(_display); + static int inputTrace = -1; + if (inputTrace < 0) inputTrace = getenv("RCLOL_INPUT_TRACE") != NULL; + if (inputTrace && (ev->type == KeyPress || ev->type == KeyRelease || ev->type == ButtonPress)) @@ -3712,7 +3733,7 @@ index 5ae29bf4..a4a880b0 100644 NSEventModifierFlags modifierFlags = [self modifierFlagsForState: ev->xkey.state]; char buf[20] = {0}; KeySym keySym; -@@ -1030,7 +1764,11 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1030,7 +1785,11 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { break; if (ev->type == KeyPress) { @@ -3725,7 +3746,7 @@ index 5ae29bf4..a4a880b0 100644 buf[strLen] = 0; } else { // Xutf8LookupString() may not be used with KeyRelease -@@ -1038,6 +1776,18 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1038,6 +1797,18 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { buf[strLen] = 0; } @@ -3744,7 +3765,7 @@ index 5ae29bf4..a4a880b0 100644 id str = [[NSString alloc] initWithCString: buf encoding: NSUTF8StringEncoding]; NSPoint pos = -@@ -1077,6 +1827,32 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1077,6 +1848,32 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { break; case ButtonPress:; @@ -3777,7 +3798,7 @@ index 5ae29bf4..a4a880b0 100644 NSTimeInterval now = [[NSDate date] timeIntervalSinceReferenceDate]; if (now - lastClickTimeStamp < [self doubleClickInterval]) { -@@ -1086,8 +1862,8 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1086,8 +1883,8 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { } lastClickTimeStamp = now; @@ -3788,7 +3809,7 @@ index 5ae29bf4..a4a880b0 100644 switch (ev->xbutton.button) { case Button1: -@@ -1096,10 +1872,6 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1096,10 +1893,6 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { case Button3: type = NSRightMouseDown; break; @@ -3799,7 +3820,7 @@ index 5ae29bf4..a4a880b0 100644 default: type = NSOtherMouseDown; } -@@ -1113,14 +1885,22 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1113,14 +1906,22 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { clickCount: clickCount deltaX: 0.0 deltaY: 0.0]; @@ -3826,7 +3847,7 @@ index 5ae29bf4..a4a880b0 100644 CGFloat deltaY = 0.0; switch (ev->xbutton.button) { -@@ -1130,14 +1910,6 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1130,14 +1931,6 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { case Button3: type = NSRightMouseUp; break; @@ -3841,7 +3862,7 @@ index 5ae29bf4..a4a880b0 100644 default: type = NSOtherMouseUp; } -@@ -1151,7 +1923,9 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1151,7 +1944,9 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { clickCount: clickCount deltaX: 0.0 deltaY: deltaY]; @@ -3852,7 +3873,7 @@ index 5ae29bf4..a4a880b0 100644 [self postEvent: event atStart: NO]; break; -@@ -1160,49 +1934,95 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1160,49 +1955,95 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { // ev->xmotion.x, ev->xmotion.y, ev->xmotion.x_root, // ev->xmotion.y_root); @@ -3974,7 +3995,7 @@ index 5ae29bf4..a4a880b0 100644 mouseEventWithType: type location: pos modifierFlags: -@@ -1211,28 +2031,58 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1211,28 +2052,58 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { clickCount: 1 deltaX: deltaX deltaY: deltaY]; @@ -4035,7 +4056,7 @@ index 5ae29bf4..a4a880b0 100644 NSLog(@"FocusIn"); if ([delegate attachedSheet]) { [[delegate attachedSheet] makeKeyAndOrderFront: delegate]; -@@ -1243,28 +2093,62 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1243,28 +2114,62 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { checkForAppDeactivation: NO]; lastFocusedWindow = nil; } @@ -4100,7 +4121,7 @@ index 5ae29bf4..a4a880b0 100644 O2Rect rect = NSMakeRect(ev->xexpose.x, ev->xexpose.y, ev->xexpose.width, ev->xexpose.height); -@@ -1301,11 +2185,23 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1301,11 +2206,23 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { break; case UnmapNotify: @@ -4128,7 +4149,7 @@ index 5ae29bf4..a4a880b0 100644 break; case MapRequest: -@@ -1317,10 +2213,21 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1317,10 +2234,21 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { break; case ConfigureNotify: @@ -4150,7 +4171,7 @@ index 5ae29bf4..a4a880b0 100644 break; case ConfigureRequest: -@@ -1344,6 +2251,8 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1344,6 +2272,8 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { break; case PropertyNotify: @@ -4159,7 +4180,17 @@ index 5ae29bf4..a4a880b0 100644 if ([window respondsToSelector: @selector(propertyNotify:)]) { [window propertyNotify: &ev->xproperty]; } -@@ -1399,7 +2308,9 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1392,6 +2322,9 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + + case MappingNotify: + NSLog(@"MappingNotify"); ++ XRefreshKeyboardMapping(&ev->xmapping); ++ if (ev->xmapping.request == MappingModifier) ++ rclolServerModifiersLoaded = NO; + break; + + case GenericEvent: +@@ -1399,7 +2332,9 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { break; default: @@ -4170,7 +4201,7 @@ index 5ae29bf4..a4a880b0 100644 [self _invalidateRRCache]; break; } -@@ -1433,6 +2344,10 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1433,6 +2368,10 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { NSLog(@"Request code: %d:%d", errorEvent->request_code, errorEvent->minor_code); NSLog(@"Error code: %d", errorEvent->error_code); @@ -4181,7 +4212,7 @@ index 5ae29bf4..a4a880b0 100644 return 0; } -@@ -1476,54 +2391,279 @@ void CGNativeBorderFrameWidthsForStyle(NSUInteger styleMask, CGFloat *top, +@@ -1476,54 +2415,279 @@ void CGNativeBorderFrameWidthsForStyle(NSUInteger styleMask, CGFloat *top, return frame; } @@ -4234,8 +4265,8 @@ index 5ae29bf4..a4a880b0 100644 - position.x, position.y); - XSync(_display, False); + [self warpPointerToRootX: x y: y]; - } - ++} ++ +/* Confinement preserves absolute motion and edge panning. Relative mode holds + * its own grab and is left alone while requested. */ +- (void) updateMouseConfinementForWindow:(X11Window *)window { @@ -4422,8 +4453,8 @@ index 5ae29bf4..a4a880b0 100644 + X11RelativeMouseEnd(&_relativeMouse); + _relativeWindow = None; + rclolPointerStamp = 0; -+} -+ + } + +/* CGAssociateMouseAndMouseCursorPosition(!doGrab). */ - (void) grabMouse: (BOOL) doGrab { + BOOL rclolRelativeBefore = _relativeMouse.active; diff --git a/tools/riot-modifier-check.m b/tools/riot-modifier-check.m index 116a45d..f0cfc81 100644 --- a/tools/riot-modifier-check.m +++ b/tools/riot-modifier-check.m @@ -63,14 +63,19 @@ int main(void) { key(display,KeyRelease,50,LockMask|ShiftMask|Mod4Mask); assert([events count]==before+1); expectModifier(0x38,NSAlphaShiftKeyMask|NSCommandKeyMask); - // Right Alt reports Mod1 on layouts without AltGr; that still counts as held. + // Right Alt is Mod1 (Option) on US layouts and Mod5 (AltGr) elsewhere; the + // backend reads the server map, so take whichever it reported and check it + // is still held, and reported once, when Command goes up. key(display,KeyPress,108,LockMask|Mod4Mask); + NSUInteger rightAlt=[[events lastObject] modifierFlags]&~(NSUInteger)(NSAlphaShiftKeyMask|NSCommandKeyMask); + assert(rightAlt==NSAlternateKeyMask || rightAlt==NSFunctionKeyMask); + unsigned int rightAltMask=rightAlt==NSAlternateKeyMask ? Mod1Mask : Mod5Mask; before=[events count]; - key(display,KeyRelease,133,LockMask|Mod4Mask|Mod1Mask); + key(display,KeyRelease,133,LockMask|Mod4Mask|rightAltMask); assert([events count]==before+1); - expectModifier(0x37,NSAlphaShiftKeyMask|NSAlternateKeyMask|NSFunctionKeyMask); + expectModifier(0x37,NSAlphaShiftKeyMask|rightAlt); method_setImplementation(method,original); - puts("PASS Super=Command, Alt=Option, both Command keys, post-transition flags, focus-loss release, missed-release reconciliation, Caps Lock, swallowed Super release"); + puts("PASS Super=Command, Alt=Option, both Command keys, post-transition flags, focus-loss release, missed-release reconciliation, Caps Lock, swallowed Super release, layout-mapped Right Alt"); [events release]; } return 0;