gtk: avoid physical fallback for XKB modifiers (#13967)

Hi, I noticed that this XKB config line was causing problems for
Ghosttty:

```
    key <DELE> {   [ ISO_Level3_Shift ] };
```

That's a valid reconfiguration of the "delete key" as a modifier, and
the same worked find in other terminal emulators (like Alacritty). With
Ghosttty, I was getting actual `<delete>` behaviour whenever I pressed
the modifier key (although the modifier engaged after that).

This patch fixes it. In line with your AI disclose policy: I made the
patch with Codex Sol. The logical fix is very succinct, and the bulk of
the patch works around an underlying issue to do with GDK not
recognising some modifiers as modifiers. I've tested the implementation
and the patch definitely resolves it!
This commit is contained in:
Jeffrey C. Ollie
2026-08-22 03:55:34 -05:00
committed by GitHub
2 changed files with 93 additions and 13 deletions

View File

@@ -1378,19 +1378,11 @@ pub const Surface = extern struct {
if (entry.native == keycode) break :w3c entry.key;
} else .unidentified;
// Consult the pre-remapped XKB keyval/keysym to get the (possibly)
// remapped key. If the W3C key or the remapped key
// is eligible for remapping, we use it.
//
// See the docs for `shouldBeRemappable` for why we even have to
// do this in the first place.
if (gtk_key.keyFromKeyval(keyval)) |remapped| {
if (w3c_key.shouldBeRemappable() or remapped.shouldBeRemappable())
break :keycode remapped;
}
// Return the original physical key
break :keycode w3c_key;
break :keycode gtk_key.remapKey(
w3c_key,
keyval,
key_event.isModifier() != 0,
);
};
// Get our modifier for the event

View File

@@ -227,6 +227,94 @@ pub fn keyFromKeyval(keyval: c_uint) ?input.Key {
return null;
}
/// Returns the logical key after applying any eligible XKB remapping.
///
/// Modifier keyvals that Ghostty doesn't represent must not fall back to the
/// physical key. XKB can turn any physical key into a modifier, and using the
/// original key in that case would encode an unrelated key press.
pub fn remapKey(
physical_key: input.Key,
keyval: c_uint,
is_modifier: bool,
) input.Key {
if (keyFromKeyval(keyval)) |remapped| {
if (physical_key.shouldBeRemappable() or remapped.shouldBeRemappable())
return remapped;
}
if (is_modifier or isModifierKeyval(keyval)) return .unidentified;
return physical_key;
}
/// Returns whether a keyval has modifier semantics that GDK may not expose
/// through `gdk_key_event_is_modifier`.
///
/// In particular, XKB can report ISO level and group modifiers as keyvals
/// such as `Mode_switch` while GDK reports that the event is not a modifier.
fn isModifierKeyval(keyval: c_uint) bool {
return switch (keyval) {
gdk.KEY_Shift_L,
gdk.KEY_Shift_R,
gdk.KEY_Shift_Lock,
gdk.KEY_Control_L,
gdk.KEY_Control_R,
gdk.KEY_Caps_Lock,
gdk.KEY_Meta_L,
gdk.KEY_Meta_R,
gdk.KEY_Alt_L,
gdk.KEY_Alt_R,
gdk.KEY_Super_L,
gdk.KEY_Super_R,
gdk.KEY_Hyper_L,
gdk.KEY_Hyper_R,
gdk.KEY_Num_Lock,
gdk.KEY_Mode_switch,
gdk.KEY_ISO_Lock,
gdk.KEY_ISO_Level2_Latch,
gdk.KEY_ISO_Level3_Shift,
gdk.KEY_ISO_Level3_Latch,
gdk.KEY_ISO_Level3_Lock,
gdk.KEY_ISO_Level5_Shift,
gdk.KEY_ISO_Level5_Latch,
gdk.KEY_ISO_Level5_Lock,
gdk.KEY_ISO_Group_Latch,
gdk.KEY_ISO_Group_Lock,
=> true,
else => false,
};
}
test "remap key" {
const testing = std.testing;
// XKB remaps between non-writing-system keys are honored.
try testing.expectEqual(
input.Key.escape,
remapKey(.caps_lock, gdk.KEY_Escape, false),
);
// Writing-system keys retain their physical identity.
try testing.expectEqual(
input.Key.key_a,
remapKey(.key_a, gdk.KEY_b, false),
);
// GDK doesn't identify Mode_switch as a modifier, but it must not fall
// back to the physical key.
try testing.expectEqual(
input.Key.unidentified,
remapKey(.delete, gdk.KEY_Mode_switch, false),
);
// GDK's modifier flag also covers custom modifier actions whose keyval
// Ghostty doesn't represent.
try testing.expectEqual(
input.Key.unidentified,
remapKey(.delete, gdk.KEY_VoidSymbol, true),
);
}
/// Returns a keyval from an input key or null if we don't have a mapping.
pub fn keyvalFromKey(key: input.Key) ?c_uint {
switch (key) {