gtk-ng: allow XKB remaps for non-writing-system keys (#8330)

Compromise solution to #7356

XKB is naughty. It's really really naughty. I don't understand why we
didn't just kill XKB with hammers during the Wayland migration and
change it for something much better. I don't understand why we're
content with what amounts to an OS-level software key remapper that
completely jumbles information about original physical key codes in
order to fake keyboard layouts, and not just let users who really want
to remap keys use some sort of evdev or udev-based mapper program.

In a sane system like macOS, the "c" key is always the "c" key, but it's
understood to produce the Unicode character "ц" when using a Russian
layout. XKB defies sanity, and just pretends that your "c" key is
actually a "ц" key instead, and so when you ask for the keybind "Ctrl+C"
it just shrugs in apathy (#7309). And so, we took matters into our own
hands and interpreted hardware keycodes ourselves.

But then, a *lot* of people have the ingrained muscle memory of swapping
Escape with Caps Lock so that it is easier to hit. We respect that. In a
sane system, they would use a remapper that actually makes the system
think you've hit the Escape key when in reality you've hit the Caps Lock
key, so in all intents and purposes to the OS and any app developer,
these two just have their wires swapped. But not on Linux. Somehow this
and the aforementioned case should be treated by the same key transform
algorithm, which is completely diabolical.

As a result, we have to settle for a compromise that truly satisfies
neither party — by allowing XKB remaps for keys that don't really change
depending on the layout.

The Linux input stack besets all hopes and aspirations.
This commit is contained in:
Mitchell Hashimoto
2025-08-21 11:45:48 -07:00
committed by GitHub
2 changed files with 97 additions and 4 deletions

View File

@@ -838,7 +838,7 @@ pub const Surface = extern struct {
// such as single quote on a US international keyboard layout.
if (priv.im_composing) return true;
// If we were composing and now we're not it means that we committed
// If we were composing and now we're not, it means that we committed
// the text. We also don't want to encode a key event for this.
// Example: enable Japanese input method, press "konn" and then
// press enter. The final enter should not be encoded and "konn"
@@ -878,9 +878,24 @@ pub const Surface = extern struct {
// We want to get the physical unmapped key to process physical keybinds.
// (These are keybinds explicitly marked as requesting physical mapping).
const physical_key = keycode: for (input.keycodes.entries) |entry| {
if (entry.native == keycode) break :keycode entry.key;
} else .unidentified;
const physical_key = keycode: {
const w3c_key: input.Key = w3c: for (input.keycodes.entries) |entry| {
if (entry.native == keycode) break :w3c entry.key;
} else .unidentified;
// If the key should be remappable, then consult the pre-remapped
// XKB keyval/keysym to get the (possibly) remapped key.
//
// See the docs for `shouldBeRemappable` for why we even have to
// do this in the first place.
if (w3c_key.shouldBeRemappable()) {
if (gtk_key.keyFromKeyval(keyval)) |remapped|
break :keycode remapped;
}
// Return the original physical key
break :keycode w3c_key;
};
// Get our modifier for the event
const mods: input.Mods = gtk_key.eventMods(

View File

@@ -589,6 +589,84 @@ pub const Key = enum(c_int) {
};
}
/// Whether this key should be remappable by the operating system.
///
/// On certain OSes (namely Linux and the BSDs) certain keys like the
/// functional keys are expected to be remappable by the user, such as
/// in the very common use case of swapping the Caps Lock key with the
/// Escape key with the XKB option `caps:swapescape`.
///
/// However, the way XKB implements this is by essentially acting as a
/// software key remapper that destroys all information about the original
/// physical key, leading to very annoying bugs like #7309 where the
/// physical key `XKB_KEY_c` gets remapped into `XKB_KEY_Cyrillic_tse`,
/// which causes all of our physical key handling to completely break down.
/// _Very naughty._
///
/// As a compromise, given that writing system keys (§3.1.1) comprise the
/// majority of keys that "change meaning [...] based on the current locale
/// and keyboard layout", we allow all other keys to be remapped by default
/// since they should be fairly harmless. We might consider making this
/// configurable, but for now this should at least placate most people.
pub fn shouldBeRemappable(self: Key) bool {
return switch (self) {
// "Writing System Keys" § 3.1.1
.backquote,
.backslash,
.bracket_left,
.bracket_right,
.comma,
.digit_0,
.digit_1,
.digit_2,
.digit_3,
.digit_4,
.digit_5,
.digit_6,
.digit_7,
.digit_8,
.digit_9,
.equal,
.intl_backslash,
.intl_ro,
.intl_yen,
.key_a,
.key_b,
.key_c,
.key_d,
.key_e,
.key_f,
.key_g,
.key_h,
.key_i,
.key_j,
.key_k,
.key_l,
.key_m,
.key_n,
.key_o,
.key_p,
.key_q,
.key_r,
.key_s,
.key_t,
.key_u,
.key_v,
.key_w,
.key_x,
.key_y,
.key_z,
.minus,
.period,
.quote,
.semicolon,
.slash,
=> false,
else => true,
};
}
/// Returns true if this is a keypad key.
pub fn keypad(self: Key) bool {
return switch (self) {