// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package wayland /* #cgo pkg-config: xkbcommon #include #include */ import "C" import ( "errors" "fmt" "sort" "sync" "time" "unsafe" "git.stormux.org/storm/cthulhu/internal/input" ) type keyGrab struct{ Symbol, Modifiers uint32 } type keyboardPolicy struct{ enabled, browse, button, secure, learn bool } func modifier_symbols(names []string) ([]uint32, error) { var symbols []uint32 for _, name := range names { switch name { case "Insert": symbols = append(symbols, 0xff63) case "KP_Insert": symbols = append(symbols, 0xff9e, 0xffb0) // NumLock alias. case "Caps_Lock": symbols = append(symbols, 0xffe5) default: return nil, fmt.Errorf("command modifier %q is unsupported; use Insert, KP_Insert, or Caps_Lock", name) } } return symbols, nil } func key_name(symbol uint32) string { var buffer [128]C.char length := C.xkb_keysym_get_name(C.xkb_keysym_t(symbol), &buffer[0], C.size_t(len(buffer))) if length <= 0 || int(length) >= len(buffer) { return "" } return C.GoString((*C.char)(unsafe.Pointer(&buffer[0]))) } 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, 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; // shifted digit and punctuation chords therefore require these defaults to // match the compositor's layout. The protocol cannot verify that match. 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< 255 { keyboard.overflow.Add(1) } return nil }