screenshot: resolve region and picker keys through the seat keymap

fixes #3311
port 1.6

(cherry picked from commit 3674d0f480)
This commit is contained in:
bbedward
2026-09-05 15:19:16 +00:00
committed by dms-ci[bot]
parent 95b2750e5f
commit 71e8a85cd1
11 changed files with 427 additions and 242 deletions
+12 -106
View File
@@ -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
}
+10 -12
View File
@@ -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"))
}
-80
View File
@@ -1,80 +0,0 @@
package clipboard
import "testing"
const qwertyKeymap = `xkb_keymap {
xkb_keycodes "(unnamed)" {
minimum = 8;
maximum = 708;
<ESC> = 9;
<AB04> = 55;
<LCTL> = 37;
<LFSH> = 50;
alias <AL01> = <AC01>;
indicator 1 = "Caps Lock";
};
xkb_types "(unnamed)" {
type "ALPHABETIC" {
modifiers = Shift+Lock;
map[Shift] = Level2;
level_name[Level1] = "Base";
};
};
xkb_symbols "(unnamed)" {
key <ESC> { [ Escape ] };
key <AB04> { type= "ALPHABETIC", [ v, V ] };
key <LCTL> { [ Control_L ] };
key <LFSH> { [ Shift_L ] };
};
};`
const hexKeymap = `xkb_keymap {
xkb_keycodes "(unnamed)" {
<AB04> = 56;
<LCTL> = 38;
<LFSH> = 51;
};
xkb_symbols "(unnamed)" {
key <LCTL> { [ 0xffe3 ] };
key <LFSH> {
type= "PC_ALT_LEVEL2",
symbols[1]= [ 0xffe1, 0xfe08 ]
};
key <AB04> { [ 0x76, 0x56 ] };
};
};`
const dvorakKeymap = `xkb_keymap {
xkb_keycodes "(unnamed)" {
<AB09> = 60;
<LCTL> = 37;
<LFSH> = 50;
};
xkb_symbols "(unnamed)" {
key <AB09> { [ v, V ] };
key <LCTL> { [ Control_L ] };
key <LFSH> { [ 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)
}
})
}
}
+8 -1
View File
@@ -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)
}
})
}
+4 -4
View File
@@ -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
}
+1 -1
View File
@@ -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)
}
})
}
+2
View File
@@ -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
+47 -38
View File
@@ -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) {
@@ -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)
}
}
+155
View File
@@ -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
}
+140
View File
@@ -0,0 +1,140 @@
package keymap
import "testing"
const qwertyKeymap = `xkb_keymap {
xkb_keycodes "(unnamed)" {
minimum = 8;
maximum = 708;
<ESC> = 9;
<AB04> = 55;
<LCTL> = 37;
<LFSH> = 50;
alias <AL01> = <AC01>;
indicator 1 = "Caps Lock";
};
xkb_types "(unnamed)" {
type "ALPHABETIC" {
modifiers = Shift+Lock;
map[Shift] = Level2;
level_name[Level1] = "Base";
};
};
xkb_symbols "(unnamed)" {
key <ESC> { [ Escape ] };
key <AB04> { type= "ALPHABETIC", [ v, V ] };
key <LCTL> { [ Control_L ] };
key <LFSH> { [ Shift_L ] };
};
};`
const hexKeymap = `xkb_keymap {
xkb_keycodes "(unnamed)" {
<AB04> = 56;
<LCTL> = 38;
<LFSH> = 51;
};
xkb_symbols "(unnamed)" {
key <LCTL> { [ 0xffe3 ] };
key <LFSH> {
type= "PC_ALT_LEVEL2",
symbols[1]= [ 0xffe1, 0xfe08 ]
};
key <AB04> { [ 0x76, 0x56 ] };
};
};`
const dvorakKeymap = `xkb_keymap {
xkb_keycodes "(unnamed)" {
<AB09> = 60;
<LCTL> = 37;
<LFSH> = 50;
};
xkb_symbols "(unnamed)" {
key <AB09> { [ v, V ] };
key <LCTL> { [ Control_L ] };
key <LFSH> { [ Shift_L ] };
};
};`
const swapEscapeKeymap = `xkb_keymap {
xkb_keycodes "(unnamed)" {
<ESC> = 9;
<RTRN> = 36;
<CAPS> = 66;
<KPEN> = 104;
};
xkb_symbols "(unnamed)" {
key <ESC> { [ Caps_Lock ] };
key <RTRN> { [ Return ] };
key <CAPS> {
type= "ONE_LEVEL",
symbols[1]= [ Escape ]
};
key <KPEN> { [ 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)
}
}