diff --git a/core/internal/clipboard/pastekey.go b/core/internal/clipboard/pastekey.go index d097308be..aff24b250 100644 --- a/core/internal/clipboard/pastekey.go +++ b/core/internal/clipboard/pastekey.go @@ -2,28 +2,19 @@ package clipboard import ( "fmt" - "regexp" - "strconv" - "strings" + "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/keymap" wlclient "github.com/AvengeMedia/dankgo/wayland/client" "golang.org/x/sys/unix" ) const ( - xkbKeymapFormatV1 = 1 - keyStateReleased = 0 keyStatePressed = 1 // xkb real modifier bit positions are fixed: Shift=0, Lock=1, Control=2 shiftModMask = 1 << 0 ctrlModMask = 1 << 2 - - // evdev fallbacks for a standard pc105 map - fallbackCtrlKey = 29 // KEY_LEFTCTRL - fallbackShiftKey = 42 // KEY_LEFTSHIFT - fallbackVKey = 47 // KEY_V ) // SendPasteKeystroke emulates a paste shortcut via zwp_virtual_keyboard_v1 @@ -50,25 +41,25 @@ func SendPasteKeystroke(withShift bool) (err error) { } defer keyboard.Release() - var keymap *wlclient.KeyboardKeymapEvent + var seatKeymap *wlclient.KeyboardKeymapEvent keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) { - if keymap == nil { - keymap = &e + if seatKeymap == nil { + seatKeymap = &e } }) s.display.Roundtrip() - if keymap == nil || keymap.Format != xkbKeymapFormatV1 { + if seatKeymap == nil || seatKeymap.Format != keymap.FormatXkbV1 { return fmt.Errorf("no xkb keymap from seat") } - defer unix.Close(keymap.Fd) + defer unix.Close(seatKeymap.Fd) - keymapText, err := readKeymap(keymap.Fd, keymap.Size) + keymapText, err := keymap.Read(seatKeymap.Fd, seatKeymap.Size) if err != nil { return fmt.Errorf("read keymap: %w", err) } - keys := resolveKeycodes(keymapText) + keys := keymap.Parse(keymapText) vk, err := s.virtualKeyboardMgr.CreateVirtualKeyboard(s.seat) if err != nil { @@ -76,15 +67,15 @@ func SendPasteKeystroke(withShift bool) (err error) { } defer vk.Destroy() - if err := vk.Keymap(xkbKeymapFormatV1, keymap.Fd, keymap.Size); err != nil { + if err := vk.Keymap(keymap.FormatXkbV1, seatKeymap.Fd, seatKeymap.Size); err != nil { return fmt.Errorf("set keymap: %w", err) } mods := uint32(ctrlModMask) - held := []uint32{keys.ctrl} + held := []uint32{keys.Keycode("Control_L")} if withShift { mods |= shiftModMask - held = append(held, keys.shift) + held = append(held, keys.Keycode("Shift_L")) } t := uint32(0) @@ -121,93 +112,8 @@ func SendPasteKeystroke(withShift bool) (err error) { if err := vk.Modifiers(mods, 0, 0, 0); err != nil { return fmt.Errorf("set modifiers: %w", err) } - if err := press(keys.v); err != nil { + if err := press(keys.Keycode("v")); err != nil { return fmt.Errorf("key press: %w", err) } return nil } - -func readKeymap(fd int, size uint32) (string, error) { - data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ, unix.MAP_PRIVATE) - if err != nil { - return "", err - } - text := strings.TrimRight(string(data), "\x00") - return text, unix.Munmap(data) -} - -type pasteKeycodes struct { - ctrl uint32 - shift uint32 - v uint32 -} - -var ( - keycodeDefRe = regexp.MustCompile(`<([A-Za-z0-9+_-]+)>\s*=\s*(\d+)`) - keySymbolsRe = regexp.MustCompile(`key\s*<([A-Za-z0-9+_-]+)>\s*\{([^}]*)\}`) - groupIndexRe = regexp.MustCompile(`\w+\[\d+\]\s*=`) - symbolListRe = regexp.MustCompile(`\[([^\]]*)\]`) -) - -// xkbcommon may serialize keysyms as hex escapes instead of names -// (e.g. "0x76" for v, "0xffe3" for Control_L). -var keysymNames = map[uint32]string{ - 0x76: "v", - 0xffe3: "Control_L", - 0xffe1: "Shift_L", -} - -func canonicalKeysym(sym string) string { - if !strings.HasPrefix(sym, "0x") && !strings.HasPrefix(sym, "0X") { - return sym - } - value, err := strconv.ParseUint(sym[2:], 16, 32) - if err != nil { - return sym - } - if name, ok := keysymNames[uint32(value)]; ok { - return name - } - return sym -} - -// resolveKeycodes finds the evdev keycodes producing the keysyms we need in -// the seat keymap's first group, falling back to pc105 positions. -func resolveKeycodes(keymap string) pasteKeycodes { - codes := map[string]uint32{} - for _, m := range keycodeDefRe.FindAllStringSubmatch(keymap, -1) { - if code, err := strconv.Atoi(m[2]); err == nil { - codes[m[1]] = uint32(code) - } - } - - keys := pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey} - want := map[string]*uint32{ - "Control_L": &keys.ctrl, - "Shift_L": &keys.shift, - "v": &keys.v, - } - - for _, m := range keySymbolsRe.FindAllStringSubmatch(keymap, -1) { - group := symbolListRe.FindStringSubmatch(groupIndexRe.ReplaceAllString(m[2], "")) - if group == nil { - continue - } - xkbCode, ok := codes[m[1]] - if !ok || xkbCode < 8 { - continue - } - level1 := canonicalKeysym(strings.TrimSpace(strings.Split(group[1], ",")[0])) - target, wanted := want[level1] - if !wanted { - continue - } - *target = xkbCode - 8 - delete(want, level1) - if len(want) == 0 { - break - } - } - - return keys -} diff --git a/core/internal/clipboard/pastekey_live_test.go b/core/internal/clipboard/pastekey_live_test.go index 51bfc967d..b2d75dcfb 100644 --- a/core/internal/clipboard/pastekey_live_test.go +++ b/core/internal/clipboard/pastekey_live_test.go @@ -4,6 +4,7 @@ import ( "os" "testing" + "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/keymap" wlclient "github.com/AvengeMedia/dankgo/wayland/client" "golang.org/x/sys/unix" ) @@ -32,20 +33,20 @@ func TestLiveSeatKeymapResolution(t *testing.T) { } defer keyboard.Release() - var keymap *wlclient.KeyboardKeymapEvent + var seatKeymap *wlclient.KeyboardKeymapEvent keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) { - if keymap == nil { - keymap = &e + if seatKeymap == nil { + seatKeymap = &e } }) s.display.Roundtrip() - if keymap == nil { + if seatKeymap == nil { t.Fatal("no keymap event") } - defer unix.Close(keymap.Fd) + defer unix.Close(seatKeymap.Fd) - text, err := readKeymap(keymap.Fd, keymap.Size) + text, err := keymap.Read(seatKeymap.Fd, seatKeymap.Size) if err != nil { t.Fatalf("read keymap: %v", err) } @@ -56,10 +57,7 @@ func TestLiveSeatKeymapResolution(t *testing.T) { } } - keys := resolveKeycodes(text) - t.Logf("keymap size=%d resolved ctrl=%d shift=%d v=%d", keymap.Size, keys.ctrl, keys.shift, keys.v) - - if keys.ctrl == fallbackCtrlKey && keys.shift == fallbackShiftKey && keys.v == fallbackVKey { - t.Log("all keycodes are fallbacks - parsing may not have matched the live keymap") - } + keys := keymap.Parse(text) + t.Logf("keymap size=%d resolved ctrl=%d shift=%d v=%d", seatKeymap.Size, + keys.Keycode("Control_L"), keys.Keycode("Shift_L"), keys.Keycode("v")) } diff --git a/core/internal/clipboard/pastekey_test.go b/core/internal/clipboard/pastekey_test.go deleted file mode 100644 index c57126959..000000000 --- a/core/internal/clipboard/pastekey_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package clipboard - -import "testing" - -const qwertyKeymap = `xkb_keymap { -xkb_keycodes "(unnamed)" { - minimum = 8; - maximum = 708; - = 9; - = 55; - = 37; - = 50; - alias = ; - indicator 1 = "Caps Lock"; -}; -xkb_types "(unnamed)" { - type "ALPHABETIC" { - modifiers = Shift+Lock; - map[Shift] = Level2; - level_name[Level1] = "Base"; - }; -}; -xkb_symbols "(unnamed)" { - key { [ Escape ] }; - key { type= "ALPHABETIC", [ v, V ] }; - key { [ Control_L ] }; - key { [ Shift_L ] }; -}; -};` - -const hexKeymap = `xkb_keymap { -xkb_keycodes "(unnamed)" { - = 56; - = 38; - = 51; -}; -xkb_symbols "(unnamed)" { - key { [ 0xffe3 ] }; - key { - type= "PC_ALT_LEVEL2", - symbols[1]= [ 0xffe1, 0xfe08 ] - }; - key { [ 0x76, 0x56 ] }; -}; -};` - -const dvorakKeymap = `xkb_keymap { -xkb_keycodes "(unnamed)" { - = 60; - = 37; - = 50; -}; -xkb_symbols "(unnamed)" { - key { [ v, V ] }; - key { [ Control_L ] }; - key { [ Shift_L ] }; -}; -};` - -func TestResolveKeycodes(t *testing.T) { - tests := []struct { - name string - keymap string - want pasteKeycodes - }{ - {"qwerty", qwertyKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 47}}, - {"dvorak", dvorakKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 52}}, - {"hex keysyms", hexKeymap, pasteKeycodes{ctrl: 30, shift: 43, v: 48}}, - {"empty falls back", "", pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := resolveKeycodes(tt.keymap) - if got != tt.want { - t.Errorf("resolveKeycodes() = %+v, want %+v", got, tt.want) - } - }) - } -} diff --git a/core/internal/colorpicker/picker.go b/core/internal/colorpicker/picker.go index 383f8ac19..0b9c01834 100644 --- a/core/internal/colorpicker/picker.go +++ b/core/internal/colorpicker/picker.go @@ -12,6 +12,7 @@ import ( "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter" "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client" + "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/keymap" "github.com/AvengeMedia/dankgo/wayland/client" ) @@ -64,6 +65,7 @@ type Picker struct { seat *client.Seat pointer *client.Pointer keyboard *client.Keyboard + keymap *keymap.Keymap layerShell *wlr_layer_shell.ZwlrLayerShellV1 screencopy *wlr_screencopy.ZwlrScreencopyManagerV1 viewporter *wp_viewporter.WpViewporter @@ -738,9 +740,14 @@ func (p *Picker) setupPointerHandlers() { } func (p *Picker) setupKeyboardHandlers() { + p.keyboard.SetKeymapHandler(func(e client.KeyboardKeymapEvent) { + p.keymap = keymap.FromEvent(e) + }) + p.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) { + sym := p.keymap.Keysym(e.Key) for _, ls := range p.surfaces { - ls.state.OnKey(e.Key, e.State) + ls.state.OnKey(sym, e.State) } }) } diff --git a/core/internal/colorpicker/state.go b/core/internal/colorpicker/state.go index ee3ebd091..8bb3a3631 100644 --- a/core/internal/colorpicker/state.go +++ b/core/internal/colorpicker/state.go @@ -266,7 +266,7 @@ func (s *SurfaceState) OnPointerButton(button, state uint32) { } } -func (s *SurfaceState) OnKey(key, state uint32) { +func (s *SurfaceState) OnKey(sym string, state uint32) { if state != 1 { return } @@ -274,10 +274,10 @@ func (s *SurfaceState) OnKey(key, state uint32) { s.mu.Lock() defer s.mu.Unlock() - switch key { - case 1: // KEY_ESC + switch sym { + case "Escape": s.cancelled = true - case 28: // KEY_ENTER + case "Return", "KP_Enter": if s.readyForDisplay && s.screenBuf != nil { s.colorPicked = true } diff --git a/core/internal/colorpicker/state_test.go b/core/internal/colorpicker/state_test.go index a6ba27a60..d46722c88 100644 --- a/core/internal/colorpicker/state_test.go +++ b/core/internal/colorpicker/state_test.go @@ -104,7 +104,7 @@ func TestSurfaceState_ConcurrentIsDone(t *testing.T) { for range goroutines / 3 { wg.Go(func() { for range iterations { - s.OnKey(1, 1) + s.OnKey("Escape", 1) } }) } diff --git a/core/internal/screenshot/region.go b/core/internal/screenshot/region.go index 2137498fd..9ccf78dcc 100644 --- a/core/internal/screenshot/region.go +++ b/core/internal/screenshot/region.go @@ -12,6 +12,7 @@ import ( "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_cursor_shape" "github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter" wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client" + "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/keymap" "github.com/AvengeMedia/dankgo/wayland/client" ) @@ -108,6 +109,7 @@ type RegionSelector struct { seat *client.Seat pointer *client.Pointer keyboard *client.Keyboard + keymap *keymap.Keymap layerShell *wlr_layer_shell.ZwlrLayerShellV1 screencopy *wlr_screencopy.ZwlrScreencopyManagerV1 viewporter *wp_viewporter.WpViewporter diff --git a/core/internal/screenshot/region_input.go b/core/internal/screenshot/region_input.go index 5e6a66ba3..ca776a53b 100644 --- a/core/internal/screenshot/region_input.go +++ b/core/internal/screenshot/region_input.go @@ -3,6 +3,7 @@ package screenshot import ( "math" + "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/keymap" "github.com/AvengeMedia/dankgo/wayland/client" ) @@ -432,52 +433,60 @@ func (r *RegionSelector) setupKeyboardHandlers() { } }) + r.keyboard.SetKeymapHandler(func(e client.KeyboardKeymapEvent) { + r.keymap = keymap.FromEvent(e) + }) + r.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) { - switch e.Key { - case 29, 97: // Ctrl left/right - ctrl := e.State != 0 - if ctrl != r.ctrlHeld { - r.ctrlHeld = ctrl - r.refreshCursor() - if r.selection.hasSelection { - for _, os := range r.surfaces { - r.redrawSurface(os) - } + r.handleKey(r.keymap.Keysym(e.Key), e.State) + }) +} + +func (r *RegionSelector) handleKey(sym string, state uint32) { + held := state != 0 + switch sym { + case "Control_L", "Control_R": + if held != r.ctrlHeld { + r.ctrlHeld = held + r.refreshCursor() + if r.selection.hasSelection { + for _, os := range r.surfaces { + r.redrawSurface(os) } } - case 56, 100: // Alt left/right - r.altHeld = e.State != 0 - } - if e.State != 1 { - return } + case "Alt_L", "Alt_R": + r.altHeld = held + } + if state != 1 { + return + } - if r.phase == phaseScroll { - switch e.Key { - case 1: - r.cancelled = true - r.running = false - case 28, 96: - r.finishScroll() - } - return - } - - switch e.Key { - case 1: + if r.phase == phaseScroll { + switch sym { + case "Escape": r.cancelled = true r.running = false - case 25: - r.showCapturedCursor = !r.showCapturedCursor - for _, os := range r.surfaces { - r.redrawSurface(os) - } - case 28, 57, 96: - if r.selection.hasSelection { - r.finishSelection() - } + case "Return", "KP_Enter": + r.finishScroll() } - }) + return + } + + switch sym { + case "Escape": + r.cancelled = true + r.running = false + case "p": + r.showCapturedCursor = !r.showCapturedCursor + for _, os := range r.surfaces { + r.redrawSurface(os) + } + case "Return", "space", "KP_Enter": + if r.selection.hasSelection { + r.finishSelection() + } + } } func (r *RegionSelector) selectionDeviceRect() (*OutputSurface, int, int, int, int) { diff --git a/core/internal/screenshot/region_key_test.go b/core/internal/screenshot/region_key_test.go new file mode 100644 index 000000000..9695a3bcf --- /dev/null +++ b/core/internal/screenshot/region_key_test.go @@ -0,0 +1,48 @@ +package screenshot + +import "testing" + +func TestHandleKeyEscapeCancels(t *testing.T) { + for _, phase := range []selectorPhase{phaseSelect, phaseScroll} { + r := &RegionSelector{running: true, phase: phase} + r.handleKey("Escape", 1) + if !r.cancelled || r.running { + t.Errorf("phase %v: cancelled=%v running=%v after Escape", phase, r.cancelled, r.running) + } + } +} + +func TestHandleKeyIgnoresReleaseAndCapsLock(t *testing.T) { + r := &RegionSelector{running: true} + r.handleKey("Escape", 0) + r.handleKey("Caps_Lock", 1) + if r.cancelled || !r.running { + t.Errorf("cancelled=%v running=%v, want untouched", r.cancelled, r.running) + } +} + +func TestHandleKeyTogglesCapturedCursor(t *testing.T) { + r := &RegionSelector{running: true, showCapturedCursor: true} + r.handleKey("p", 1) + if r.showCapturedCursor { + t.Error("p did not toggle captured cursor off") + } + r.handleKey("p", 1) + if !r.showCapturedCursor { + t.Error("p did not toggle captured cursor back on") + } +} + +func TestHandleKeyTracksModifiers(t *testing.T) { + r := &RegionSelector{running: true} + r.handleKey("Control_R", 1) + r.handleKey("Alt_L", 1) + if !r.ctrlHeld || !r.altHeld { + t.Errorf("ctrl=%v alt=%v after press, want both held", r.ctrlHeld, r.altHeld) + } + r.handleKey("Control_R", 0) + r.handleKey("Alt_L", 0) + if r.ctrlHeld || r.altHeld { + t.Errorf("ctrl=%v alt=%v after release, want both released", r.ctrlHeld, r.altHeld) + } +} diff --git a/core/internal/wayland/keymap/keymap.go b/core/internal/wayland/keymap/keymap.go new file mode 100644 index 000000000..c7e96b900 --- /dev/null +++ b/core/internal/wayland/keymap/keymap.go @@ -0,0 +1,155 @@ +package keymap + +import ( + "regexp" + "strconv" + "strings" + + "github.com/AvengeMedia/dankgo/wayland/client" + "golang.org/x/sys/unix" +) + +const FormatXkbV1 = 1 + +// wl_keyboard.key carries evdev scancodes; xkb keycodes sit 8 above them. +const evdevOffset = 8 + +var pc105Codes = map[string]uint32{ + "Escape": 1, + "p": 25, + "Return": 28, + "Control_L": 29, + "Shift_L": 42, + "v": 47, + "Alt_L": 56, + "space": 57, + "KP_Enter": 96, + "Control_R": 97, + "Alt_R": 100, +} + +var pc105Syms = invert(pc105Codes) + +// xkbcommon may serialize keysyms as hex instead of names. +var keysymNames = map[uint32]string{ + 0x20: "space", + 0x70: "p", + 0x76: "v", + 0xff0d: "Return", + 0xff1b: "Escape", + 0xff8d: "KP_Enter", + 0xffe1: "Shift_L", + 0xffe3: "Control_L", + 0xffe4: "Control_R", + 0xffe9: "Alt_L", + 0xffea: "Alt_R", +} + +var ( + keycodeDefRe = regexp.MustCompile(`<([A-Za-z0-9+_-]+)>\s*=\s*(\d+)`) + keySymbolsRe = regexp.MustCompile(`key\s*<([A-Za-z0-9+_-]+)>\s*\{([^}]*)\}`) + groupIndexRe = regexp.MustCompile(`\w+\[\d+\]\s*=`) + symbolListRe = regexp.MustCompile(`\[([^\]]*)\]`) +) + +// Keymap maps evdev scancodes to their first-group, first-level keysym names. +// A nil Keymap answers with pc105 positions. +type Keymap struct { + symByCode map[uint32]string + codeBySym map[string]uint32 +} + +// FromEvent owns and closes the keymap fd; nil means no usable xkb keymap. +func FromEvent(e client.KeyboardKeymapEvent) *Keymap { + defer unix.Close(e.Fd) + if e.Format != FormatXkbV1 { + return nil + } + text, err := Read(e.Fd, e.Size) + if err != nil { + return nil + } + return Parse(text) +} + +func Read(fd int, size uint32) (string, error) { + data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ, unix.MAP_PRIVATE) + if err != nil { + return "", err + } + text := strings.TrimRight(string(data), "\x00") + return text, unix.Munmap(data) +} + +func Parse(text string) *Keymap { + xkbCodes := map[string]uint32{} + for _, m := range keycodeDefRe.FindAllStringSubmatch(text, -1) { + if code, err := strconv.Atoi(m[2]); err == nil { + xkbCodes[m[1]] = uint32(code) + } + } + + k := &Keymap{symByCode: map[uint32]string{}, codeBySym: map[string]uint32{}} + for _, m := range keySymbolsRe.FindAllStringSubmatch(text, -1) { + group := symbolListRe.FindStringSubmatch(groupIndexRe.ReplaceAllString(m[2], "")) + if group == nil { + continue + } + xkbCode, ok := xkbCodes[m[1]] + if !ok || xkbCode < evdevOffset { + continue + } + sym := canonicalKeysym(strings.TrimSpace(strings.Split(group[1], ",")[0])) + if sym == "" { + continue + } + code := xkbCode - evdevOffset + k.symByCode[code] = sym + if _, seen := k.codeBySym[sym]; !seen { + k.codeBySym[sym] = code + } + } + return k +} + +func (k *Keymap) Keysym(code uint32) string { + if k == nil { + return pc105Syms[code] + } + if sym, ok := k.symByCode[code]; ok { + return sym + } + return pc105Syms[code] +} + +func (k *Keymap) Keycode(sym string) uint32 { + if k == nil { + return pc105Codes[sym] + } + if code, ok := k.codeBySym[sym]; ok { + return code + } + return pc105Codes[sym] +} + +func canonicalKeysym(sym string) string { + if !strings.HasPrefix(sym, "0x") && !strings.HasPrefix(sym, "0X") { + return sym + } + value, err := strconv.ParseUint(sym[2:], 16, 32) + if err != nil { + return sym + } + if name, ok := keysymNames[uint32(value)]; ok { + return name + } + return sym +} + +func invert(m map[string]uint32) map[uint32]string { + out := make(map[uint32]string, len(m)) + for sym, code := range m { + out[code] = sym + } + return out +} diff --git a/core/internal/wayland/keymap/keymap_test.go b/core/internal/wayland/keymap/keymap_test.go new file mode 100644 index 000000000..d81888918 --- /dev/null +++ b/core/internal/wayland/keymap/keymap_test.go @@ -0,0 +1,140 @@ +package keymap + +import "testing" + +const qwertyKeymap = `xkb_keymap { +xkb_keycodes "(unnamed)" { + minimum = 8; + maximum = 708; + = 9; + = 55; + = 37; + = 50; + alias = ; + indicator 1 = "Caps Lock"; +}; +xkb_types "(unnamed)" { + type "ALPHABETIC" { + modifiers = Shift+Lock; + map[Shift] = Level2; + level_name[Level1] = "Base"; + }; +}; +xkb_symbols "(unnamed)" { + key { [ Escape ] }; + key { type= "ALPHABETIC", [ v, V ] }; + key { [ Control_L ] }; + key { [ Shift_L ] }; +}; +};` + +const hexKeymap = `xkb_keymap { +xkb_keycodes "(unnamed)" { + = 56; + = 38; + = 51; +}; +xkb_symbols "(unnamed)" { + key { [ 0xffe3 ] }; + key { + type= "PC_ALT_LEVEL2", + symbols[1]= [ 0xffe1, 0xfe08 ] + }; + key { [ 0x76, 0x56 ] }; +}; +};` + +const dvorakKeymap = `xkb_keymap { +xkb_keycodes "(unnamed)" { + = 60; + = 37; + = 50; +}; +xkb_symbols "(unnamed)" { + key { [ v, V ] }; + key { [ Control_L ] }; + key { [ Shift_L ] }; +}; +};` + +const swapEscapeKeymap = `xkb_keymap { +xkb_keycodes "(unnamed)" { + = 9; + = 36; + = 66; + = 104; +}; +xkb_symbols "(unnamed)" { + key { [ Caps_Lock ] }; + key { [ Return ] }; + key { + type= "ONE_LEVEL", + symbols[1]= [ Escape ] + }; + key { [ KP_Enter ] }; +}; +};` + +func TestKeycode(t *testing.T) { + tests := []struct { + name string + keymap string + sym string + want uint32 + }{ + {"qwerty v", qwertyKeymap, "v", 47}, + {"qwerty ctrl", qwertyKeymap, "Control_L", 29}, + {"qwerty shift", qwertyKeymap, "Shift_L", 42}, + {"dvorak v", dvorakKeymap, "v", 52}, + {"hex v", hexKeymap, "v", 48}, + {"hex ctrl", hexKeymap, "Control_L", 30}, + {"hex shift", hexKeymap, "Shift_L", 43}, + {"swapescape escape", swapEscapeKeymap, "Escape", 58}, + {"missing falls back to pc105", dvorakKeymap, "Escape", 1}, + {"empty falls back to pc105", "", "v", 47}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Parse(tt.keymap).Keycode(tt.sym); got != tt.want { + t.Errorf("Keycode(%q) = %d, want %d", tt.sym, got, tt.want) + } + }) + } +} + +func TestKeysym(t *testing.T) { + tests := []struct { + name string + keymap string + code uint32 + want string + }{ + {"swapescape caps yields Escape", swapEscapeKeymap, 58, "Escape"}, + {"swapescape esc yields Caps_Lock", swapEscapeKeymap, 1, "Caps_Lock"}, + {"swapescape return", swapEscapeKeymap, 28, "Return"}, + {"swapescape keypad enter", swapEscapeKeymap, 96, "KP_Enter"}, + {"qwerty escape", qwertyKeymap, 1, "Escape"}, + {"hex ctrl", hexKeymap, 30, "Control_L"}, + {"unmapped code falls back to pc105", qwertyKeymap, 28, "Return"}, + {"unknown code is empty", qwertyKeymap, 200, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Parse(tt.keymap).Keysym(tt.code); got != tt.want { + t.Errorf("Keysym(%d) = %q, want %q", tt.code, got, tt.want) + } + }) + } +} + +func TestNilKeymapUsesPc105(t *testing.T) { + var k *Keymap + if got := k.Keysym(1); got != "Escape" { + t.Errorf("Keysym(1) = %q, want Escape", got) + } + if got := k.Keycode("Control_L"); got != 29 { + t.Errorf("Keycode(Control_L) = %d, want 29", got) + } +}