diff --git a/internal/platform/wayland/protocol_test.go b/internal/platform/wayland/protocol_test.go index 53e7253..0c41f75 100644 --- a/internal/platform/wayland/protocol_test.go +++ b/internal/platform/wayland/protocol_test.go @@ -242,6 +242,11 @@ func TestNativeKeyboardBrowseSecureAndLearnTransitions(t *testing.T) { monitor.key(t, 'h', 43, 0, false) monitor.key(t, 'h', 43, 0, true) await_command(t, keyboard, input.DocumentNextHeading) + if loadShiftPairs().unshifted['#'] == '3' { + monitor.key(t, '#', 12, 1, false) + monitor.key(t, '3', 12, 0, true) + await_command(t, keyboard, input.DocumentPreviousHeadingAtLevel3) + } keyboard.SetNativeButtonFocus(true) wait_snapshot(t, monitor, func(state MonitorSnapshot) bool { for _, grab := range state.grabs { diff --git a/internal/platform/wayland/routing.go b/internal/platform/wayland/routing.go index 101488c..10b47d4 100644 --- a/internal/platform/wayland/routing.go +++ b/internal/platform/wayland/routing.go @@ -6,6 +6,7 @@ package wayland /* #cgo pkg-config: xkbcommon +#include #include */ import "C" @@ -14,6 +15,7 @@ import ( "errors" "fmt" "sort" + "sync" "time" "unsafe" @@ -51,12 +53,101 @@ func key_name(symbol uint32) string { func lower_symbol(symbol uint32) uint32 { return uint32(C.xkb_keysym_to_lower(C.xkb_keysym_t(symbol))) } -// KeyboardMonitor sends keysyms rather than a keymap. Letter case is defined -// by XKB; punctuation's physical Shift pairing cannot be inferred from it. +// KeyboardMonitor sends keysyms rather than a keymap, and the symbol for a +// shifted key is the shifted keysym: Shift+3 arrives as numbersign on a US +// layout. Letter case is defined by XKB alone, but a digit or punctuation +// key's Shift partner depends on the layout. Bindings name the unshifted +// symbol plus the Shift bit, as delivered by the Xorg path, so both grab +// registration and event handling translate through shift pairs taken from +// the default XKB keymap (the XKB_DEFAULT_* environment, else libxkbcommon +// defaults). The monitor interface does not expose the compositor's keymap; +// when its layout differs from that keymap, shifted digit and punctuation +// chords keep the previous pass-through behavior instead of misfiring. func shifted_symbol(symbol uint32) uint32 { return uint32(C.xkb_keysym_to_upper(C.xkb_keysym_t(symbol))) } +type shiftPairs struct { + shifted map[uint32][]uint32 // Unshifted symbol to its shifted partners. + unshifted map[uint32]uint32 // Shifted symbol to its one unshifted partner. +} + +var loadShiftPairs = sync.OnceValue(func() shiftPairs { return build_shift_pairs("") }) + +func shift_pair_base(symbol uint32) bool { + if symbol < '!' || symbol > '~' { + return false + } + return (symbol < 'A' || symbol > 'Z') && (symbol < 'a' || symbol > 'z') +} + +func shift_pair_partner(symbol, base uint32) bool { + if symbol == 0 || symbol == base || symbol >= 0xfe00 { + return false // NoSymbol, unchanged, or dead/function/modifier ranges. + } + return (symbol < 'A' || symbol > 'Z') && (symbol < 'a' || symbol > 'z') +} + +// An empty layout selects the default keymap; tests name layouts explicitly. +func build_shift_pairs(layout string) shiftPairs { + pairs := shiftPairs{shifted: make(map[uint32][]uint32), unshifted: make(map[uint32]uint32)} + context := C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS) + if context == nil { + return pairs + } + C.xkb_context_set_log_level(context, C.XKB_LOG_LEVEL_CRITICAL) // Failure means empty pairs, not stderr noise. + defer C.xkb_context_unref(context) + var names C.struct_xkb_rule_names + if layout != "" { + clayout := C.CString(layout) + defer C.free(unsafe.Pointer(clayout)) + names.layout = clayout + } + keymap := C.xkb_keymap_new_from_names(context, &names, C.XKB_KEYMAP_COMPILE_NO_FLAGS) + if keymap == nil { + return pairs + } + defer C.xkb_keymap_unref(keymap) + cshift := C.CString("Shift") + defer C.free(unsafe.Pointer(cshift)) + shiftIndex := C.xkb_keymap_mod_get_index(keymap, cshift) + if shiftIndex == C.XKB_MOD_INVALID { + return pairs + } + state := C.xkb_state_new(keymap) + if state == nil { + return pairs + } + defer C.xkb_state_unref(state) + ambiguous := make(map[uint32]bool) + groups := C.xkb_keymap_num_layouts(keymap) + for group := C.xkb_layout_index_t(0); group < groups; group++ { + for code := C.xkb_keymap_min_keycode(keymap); code <= C.xkb_keymap_max_keycode(keymap); code++ { + C.xkb_state_update_mask(state, 0, 0, 0, 0, 0, group) + base := uint32(C.xkb_state_key_get_one_sym(state, code)) + C.xkb_state_update_mask(state, 1<