302 lines
9.9 KiB
Go
302 lines
9.9 KiB
Go
// 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 <stdlib.h>
|
|
#include <xkbcommon/xkbcommon.h>
|
|
*/
|
|
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<<shiftIndex, 0, 0, 0, 0, group)
|
|
shifted := uint32(C.xkb_state_key_get_one_sym(state, code))
|
|
if !shift_pair_base(base) || !shift_pair_partner(shifted, base) {
|
|
continue
|
|
}
|
|
found := false
|
|
for _, partner := range pairs.shifted[base] {
|
|
found = found || partner == shifted
|
|
}
|
|
if !found {
|
|
pairs.shifted[base] = append(pairs.shifted[base], shifted)
|
|
}
|
|
if previous, ok := pairs.unshifted[shifted]; ok && previous != base {
|
|
delete(pairs.unshifted, shifted)
|
|
ambiguous[shifted] = true
|
|
} else if !ambiguous[shifted] {
|
|
pairs.unshifted[shifted] = base
|
|
}
|
|
}
|
|
}
|
|
return pairs
|
|
}
|
|
|
|
func normalize_symbol(symbol uint32) uint32 {
|
|
symbol = lower_symbol(symbol)
|
|
if symbol == 0xfe20 {
|
|
return 0xff09
|
|
} // ISO_Left_Tab.
|
|
return symbol
|
|
}
|
|
|
|
func make_grabs(configuration keyboardConfiguration, policy keyboardPolicy) ([]uint32, []keyGrab, input.CommandBindings) {
|
|
bindings, _ := input.ResolveCommandBindings(configuration.layout, policy.browse, configuration.bindings)
|
|
if policy.button {
|
|
for chord, command := range bindings {
|
|
if command == input.DocumentActivate && !chord.CthulhuModifier {
|
|
delete(bindings, chord)
|
|
}
|
|
}
|
|
}
|
|
if !policy.enabled || policy.secure {
|
|
return []uint32{}, []keyGrab{}, bindings
|
|
}
|
|
symbols, _ := modifier_symbols(configuration.modifiers)
|
|
unique := make(map[keyGrab]bool)
|
|
for chord := range bindings {
|
|
if chord.CthulhuModifier {
|
|
continue
|
|
}
|
|
for _, locks := range []uint32{0, 2, 16, 18} {
|
|
mask := uint32(chord.Modifiers) | locks
|
|
unique[keyGrab{chord.Symbol, mask}] = true
|
|
// Implementations differ in whether a shifted/capitalized keysym is
|
|
// emitted; both forms resolve to the same semantic command.
|
|
if mask&3 != 0 {
|
|
unique[keyGrab{shifted_symbol(chord.Symbol), mask}] = true
|
|
}
|
|
if mask&1 != 0 {
|
|
pairs := loadShiftPairs()
|
|
for _, symbol := range pairs.shifted[chord.Symbol] {
|
|
if pairs.unshifted[symbol] == chord.Symbol {
|
|
unique[keyGrab{symbol, mask}] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
grabs := make([]keyGrab, 0, len(unique))
|
|
for grab := range unique {
|
|
grabs = append(grabs, grab)
|
|
}
|
|
sort.Slice(grabs, func(i, j int) bool {
|
|
if grabs[i].Symbol == grabs[j].Symbol {
|
|
return grabs[i].Modifiers < grabs[j].Modifiers
|
|
}
|
|
return grabs[i].Symbol < grabs[j].Symbol
|
|
})
|
|
return symbols, grabs, bindings
|
|
}
|
|
|
|
type keyEvent struct {
|
|
released bool
|
|
state, symbol, character uint32
|
|
code uint16
|
|
}
|
|
|
|
func (owner *keyboardOwner) handle_key(key keyEvent) error {
|
|
keyboard := owner.keyboard
|
|
keyboard.secureMutex.Lock()
|
|
defer keyboard.secureMutex.Unlock()
|
|
if keyboard.secure.Load() || owner.policy.secure {
|
|
return nil
|
|
}
|
|
// A fresh signal can win the select before the policy wakeup. Reset at
|
|
// delivery as well so it cannot inherit a modifier from before locking.
|
|
owner.retire_secure_generation()
|
|
owner.sequence = max(owner.sequence+1, uint32(time.Since(owner.started).Milliseconds())+1)
|
|
timestamp := owner.sequence // One local clock shared by monitoring and commands.
|
|
symbol := normalize_symbol(key.symbol)
|
|
// Monitoring below keeps the raw symbol, so typing "#" still echoes as
|
|
// numbersign; only command lookup sees the unshifted binding symbol.
|
|
if key.state&1 != 0 {
|
|
if base, ok := loadShiftPairs().unshifted[symbol]; ok {
|
|
symbol = base
|
|
}
|
|
}
|
|
if key.symbol == 0xffb0 && owner.router.IsModifier(0xff9e) {
|
|
symbol = 0xff9e
|
|
}
|
|
previous, repeated := owner.held[key.code]
|
|
if previous != 0 {
|
|
symbol = previous
|
|
} // Releases may arrive after Shift/locks change.
|
|
if key.released {
|
|
delete(owner.held, key.code)
|
|
} else {
|
|
owner.held[key.code] = symbol
|
|
}
|
|
chord := owner.router.Chord(symbol, uint16(key.state))
|
|
consumed, command := false, input.Command("")
|
|
if owner.policy.enabled && (key.released || key.state <= 0xffff) {
|
|
consumed, command = owner.router.Handle(symbol, key.code, uint16(key.state), !key.released)
|
|
}
|
|
if key.released {
|
|
delete(owner.captured, key.code)
|
|
} else if consumed || owner.learning {
|
|
owner.captured[key.code] = true
|
|
}
|
|
keyboard.active.Store(len(owner.captured) != 0 || owner.learning)
|
|
if !key.released && !repeated && owner.learning {
|
|
command = input.LearnKey
|
|
switch {
|
|
case symbol == 0xff1b || owner.bindings[chord] == input.ToggleLearnMode:
|
|
command = input.ToggleLearnMode
|
|
case symbol == 0xffbe && chord.Modifiers == 0 && !chord.CthulhuModifier:
|
|
command = input.LearnDocumentation
|
|
case symbol == 0xffbf && chord.Modifiers == 0 && !chord.CthulhuModifier:
|
|
command = input.LearnDefaultShortcuts
|
|
case symbol == 0xffc0 && chord.Modifiers == 0 && !chord.CthulhuModifier:
|
|
command = input.LearnApplicationShortcuts
|
|
}
|
|
if command != input.LearnKey {
|
|
owner.learning = false
|
|
keyboard.learn.Store(false)
|
|
}
|
|
} else if command == input.ToggleLearnMode {
|
|
owner.learning = true
|
|
keyboard.learn.Store(true)
|
|
keyboard.learnEcho.Store(true)
|
|
}
|
|
if command != "" && !repeated {
|
|
event := input.CommandEvent{Command: command, Timestamp: timestamp, Chord: chord, KeyName: key_name(symbol), LearnModeStarted: command == input.ToggleLearnMode && owner.learning}
|
|
select {
|
|
case keyboard.commands <- event:
|
|
default:
|
|
return errors.New("Wayland command queue overflow")
|
|
}
|
|
}
|
|
// Command presses must not race their own speech through the independent
|
|
// monitoring worker. Modifier observation still tracks normal input intent.
|
|
if !consumed && !owner.policy.learn && command == "" && key.code <= 255 {
|
|
event := input.NewKeyEvent(uint8(key.code), !key.released, key_name(key.symbol))
|
|
event.Timestamp, event.Repeat = timestamp, !key.released && repeated
|
|
keyboard.intent.Observe(event)
|
|
select {
|
|
case keyboard.events <- event:
|
|
default:
|
|
keyboard.overflow.Add(1)
|
|
}
|
|
} else if !consumed && key.code > 255 {
|
|
keyboard.overflow.Add(1)
|
|
}
|
|
return nil
|
|
}
|