mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-17 00:34:01 -04:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user