From 68f6c1c9eb45235740d68a877ae238a7db331a8b Mon Sep 17 00:00:00 2001 From: Christoph Lumme Date: Sat, 12 Sep 2026 00:49:44 +0200 Subject: [PATCH] xquartz: expose the Option layer as a real XKB level On a non-US Mac keyboard layout, Option+key characters (the AltGr / level-3 layer, e.g. @ on Option+G, # on Option+3 on Swiss German) do not reach XKB-native clients (GTK, Qt) over X11. xev reports the correct keysym, but GTK apps get the base glyph. DarwinBuildModifierMaps rewrites the Option key to XK_Mode_switch when !XQuartzOptionSendsAlt, and the four glyphs per key are consumed as two core-protocol groups. Core clients reach the Option layer as group 2 via Mode_switch, but XKB-native clients resolve levels through the XKB state, where the key types built from the width-4 core map are two-level; level 3 is unreachable and the app falls back to the base glyph. Map the Option key to XK_ISO_Level3_Shift on Mod5 and, after XkbApplyMappingChange, install a FOUR_LEVEL key type and switch each Option-bearing key to one group of it via XkbChangeTypesOfKey, which resizes the symbol map and keeps width/offset/ group_info consistent. XKB-native clients then reach the Option layer as level 3. The new type slot is memset and its level_names/preserve populated before use: XkbAllocClientMap does not clear a slot that fits existing slack, and an uninitialised level_names pointer crashed XkbCopyKeymap (memmove) during device-class copy on the next key event. Assigning types with XkbChangeTypesOfKey rather than hand-editing width/offset is what keeps the keymap copy-safe. Tested on Apple Silicon (2.8.6 SDK, -Dxcsecurity=true): Option+G yields @ and Option+3 yields # in GTK apps, verified via xev and xkbcomp; ssh -X works. Signed-off-by: Christoph Lumme --- hw/xquartz/quartzKeyboard.c | 133 ++++++++++++++++++++++++++++++++++-- 1 file changed, 127 insertions(+), 6 deletions(-) diff --git a/hw/xquartz/quartzKeyboard.c b/hw/xquartz/quartzKeyboard.c index c35a2d15d1..c08aa0f337 100644 --- a/hw/xquartz/quartzKeyboard.c +++ b/hw/xquartz/quartzKeyboard.c @@ -262,9 +262,16 @@ DarwinBuildModifierMaps(darwinKeyboardInfo *info) case XK_Alt_L: info->modifierKeycodes[NX_MODIFIERKEY_ALTERNATE][0] = i; - info->modMap[MIN_KEYCODE + i] = Mod1Mask; - if (!XQuartzOptionSendsAlt) - *k = XK_Mode_switch; // Yes, this is ugly. This needs to be cleaned up when we integrate quartzKeyboard with this code and refactor. + if (!XQuartzOptionSendsAlt) { + /* Make Option the level-3 chooser so the Option layer is a real + * XKB level, reachable by XKB-native clients. See + * QuartzInstallFourLevel(). */ + *k = XK_ISO_Level3_Shift; + info->modMap[MIN_KEYCODE + i] = Mod5Mask; + } + else { + info->modMap[MIN_KEYCODE + i] = Mod1Mask; + } break; case XK_Alt_R: @@ -273,9 +280,13 @@ DarwinBuildModifierMaps(darwinKeyboardInfo *info) #else info->modifierKeycodes[NX_MODIFIERKEY_ALTERNATE][0] = i; #endif - if (!XQuartzOptionSendsAlt) - *k = XK_Mode_switch; // Yes, this is ugly. This needs to be cleaned up when we integrate quartzKeyboard with this code and refactor. - info->modMap[MIN_KEYCODE + i] = Mod1Mask; + if (!XQuartzOptionSendsAlt) { + *k = XK_ISO_Level3_Shift; + info->modMap[MIN_KEYCODE + i] = Mod5Mask; + } + else { + info->modMap[MIN_KEYCODE + i] = Mod1Mask; + } break; case XK_Mode_switch: @@ -388,6 +399,114 @@ DarwinKeyboardSetRepeat(DeviceIntPtr pDev, int initialKeyRepeatValue, } } +/* + * QuartzInstallFourLevel + * + * XkbApplyMappingChange builds the keymap from the flat 4-glyph core map + * (k[0]=base, k[1]=shift, k[2]=option, k[3]=option+shift). With Option rewritten + * to Mode_switch, the core protocol reaches k[2]/k[3] as group 2, but XKB-native + * clients (GTK/Qt) never do, so Option+ falls back to the base glyph. + * + * This rewrites every key that carries an Option-layer glyph into ONE group of + * four levels governed by a FOUR_LEVEL key type, with the Option keycodes bound to + * the LevelThree virtual modifier. XKB clients then reach the Option layer as + * level 3, and the core-protocol result is unchanged (same glyphs, same order). + * + * Runs only when Option is the layer chooser (!XQuartzOptionSendsAlt). + */ +static int +QuartzFourLevelType(XkbDescPtr xkb, unsigned int lvl3mask) +{ + XkbClientMapPtr map = xkb->map; + int ndx = map->num_types; + XkbKeyTypePtr type; + + /* Grow the types array, then ZERO the new slot before resizing it. + * XkbAllocClientMap does not clear a slot that fits in existing slack, which + * left level_names/map as garbage pointers and crashed XkbCopyKeymap in + * memmove during device-class copy on a key event. */ + if (XkbAllocClientMap(xkb, XkbKeyTypesMask, ndx + 1) != Success) + return -1; + if (map->size_types <= ndx) + return -1; + map->num_types = ndx + 1; /* required: ResizeKeyType checks < num_types */ + memset(&map->types[ndx], 0, sizeof(XkbKeyTypeRec)); + if (XkbResizeKeyType(xkb, ndx, 4 /*map entries*/, TRUE, 4 /*levels*/) != Success) + return -1; + type = &map->types[ndx]; + type->num_levels = 4; + type->mods.mask = ShiftMask | lvl3mask; + type->mods.real_mods = ShiftMask | lvl3mask; + type->mods.vmods = 0; + /* None -> L1, Shift -> L2, LevelThree -> L3, Shift+LevelThree -> L4 */ + type->map[0].active = TRUE; type->map[0].mods.mask = 0; type->map[0].level = 0; + type->map[1].active = TRUE; type->map[1].mods.mask = ShiftMask; type->map[1].level = 1; + type->map[2].active = TRUE; type->map[2].mods.mask = lvl3mask; type->map[2].level = 2; + type->map[3].active = TRUE; type->map[3].mods.mask = ShiftMask|lvl3mask; type->map[3].level = 3; + /* preserve[] and level_names[] were (re)allocated by XkbResizeKeyType; populate + * them so XkbCopyKeymap copies valid data instead of uninitialised memory. */ + if (type->preserve) + memset(type->preserve, 0, 4 * sizeof(XkbModsRec)); + if (type->level_names) { + type->level_names[0] = MakeAtom("Base", 4, TRUE); + type->level_names[1] = MakeAtom("Shift", 5, TRUE); + type->level_names[2] = MakeAtom("Level3", 6, TRUE); + type->level_names[3] = MakeAtom("Shift Level3", 12, TRUE); + } + type->name = MakeAtom("XQUARTZ_FOUR_LEVEL", 18, TRUE); + return ndx; +} + +static void +QuartzInstallFourLevel(DeviceIntPtr pDev, darwinKeyboardInfo *info) +{ + XkbDescPtr xkb; + int kc, fourlevel = -1; + /* LevelThree is conventionally Mod5; DarwinBuildModifierMaps puts the Option + * keys on Mod5 as ISO_Level3_Shift. */ + const unsigned int lvl3mask = Mod5Mask; + + if (XQuartzOptionSendsAlt) + return; + if (!pDev->key || !pDev->key->xkbInfo) + return; + xkb = pDev->key->xkbInfo->desc; + if (!xkb || !xkb->map) + return; + + for (kc = xkb->min_key_code; kc <= xkb->max_key_code; kc++) { + int idx = (kc - MIN_KEYCODE); + KeySym *g, l1, l2, l3, l4, *syms; + int newType; + + if (idx < 0 || idx >= NUM_KEYCODES) + continue; + g = info->keyMap + idx * GLYPHS_PER_KEY; + l1 = g[0]; l2 = g[1]; l3 = g[2]; l4 = g[3]; + + /* Only touch keys that actually carry an Option-layer glyph. */ + if (l3 == NoSymbol && l4 == NoSymbol) + continue; + + if (fourlevel < 0) { + fourlevel = QuartzFourLevelType(xkb, lvl3mask); + if (fourlevel < 0) + return; + } + + /* Use the supported API to switch the key to one group of the FOUR_LEVEL + * type: it resizes the symbol map and keeps width/offset/group_info + * consistent, which hand-editing did not. */ + newType = fourlevel; + if (XkbChangeTypesOfKey(xkb, kc, 1, XkbGroup1Mask, &newType, NULL) != Success) + continue; + syms = XkbKeySymsPtr(xkb, kc); + if (!syms) + continue; + syms[0] = l1; syms[1] = l2; syms[2] = l3; syms[3] = l4; + } +} + void DarwinKeyboardReloadHandler(void) { @@ -434,6 +553,7 @@ DarwinKeyboardReloadHandler(void) keyInfo.modMap, serverClient); DarwinKeyboardSetRepeat(darwinKeyboard, initialKeyRepeatValue, keyRepeatValue); + QuartzInstallFourLevel(darwinKeyboard, &keyInfo); /* Apply the mappings to the core keyboard */ for (pDev = inputInfo.devices; pDev; pDev = pDev->next) { @@ -446,6 +566,7 @@ DarwinKeyboardReloadHandler(void) keyInfo.modMap, serverClient); DarwinKeyboardSetRepeat(pDev, initialKeyRepeatValue, keyRepeatValue); + QuartzInstallFourLevel(pDev, &keyInfo); } } } pthread_mutex_unlock(&keyInfo_mutex);