feat(xkb): add libxkbcommon keymap and state via purego
- commit
- 476955059f64544205d331441f2963a1d7a41912
- author
- brice · · 2026-09-23 15:42 UTC
- parent
- 80c9df5
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +255 -0
A internal/adapters/xkb/xkb.go +201 -0
| @@ -0,0 +1,201 @@ | |||
| 1 | + | // Package xkb provides a cgo-free libxkbcommon keymap and keyboard state. | |
| 2 | + | package xkb | |
| 3 | + | ||
| 4 | + | import ( | |
| 5 | + | "fmt" | |
| 6 | + | "runtime" | |
| 7 | + | "unsafe" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/nefertty/internal/ports" | |
| 10 | + | "github.com/bnema/purego" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | const ( | |
| 14 | + | modsDepressed = 1 << iota | |
| 15 | + | modsLatched | |
| 16 | + | modsLocked | |
| 17 | + | modsEffective | |
| 18 | + | layoutEffective = 1 << 7 | |
| 19 | + | ) | |
| 20 | + | ||
| 21 | + | type RMLVO struct{ Rules, Model, Layout, Variant, Options string } | |
| 22 | + | ||
| 23 | + | // Keymap owns its native context, keymap and state. It belongs to one input goroutine. | |
| 24 | + | type Keymap struct { | |
| 25 | + | ctx, mapPtr, state uintptr | |
| 26 | + | lib, libc uintptr | |
| 27 | + | contextUnref func(uintptr) | |
| 28 | + | keymapUnref func(uintptr) | |
| 29 | + | stateUnref func(uintptr) | |
| 30 | + | keymapString func(uintptr, uint32) unsafe.Pointer | |
| 31 | + | free func(uintptr) | |
| 32 | + | getSym func(uintptr, uint32) uint32 | |
| 33 | + | symName func(uint32, uintptr, uintptr) int32 | |
| 34 | + | updateKey func(uintptr, uint32, uint32) uint32 | |
| 35 | + | serializeMods func(uintptr, uint32) uint32 | |
| 36 | + | serializeLayout func(uintptr, uint32) uint32 | |
| 37 | + | modActive func(uintptr, uintptr, uint32) int32 | |
| 38 | + | minCode func(uintptr) uint32 | |
| 39 | + | maxCode func(uintptr) uint32 | |
| 40 | + | symsLevel func(uintptr, uint32, uint32, uint32, *unsafe.Pointer) int32 | |
| 41 | + | fromName func(uintptr, uint32) uint32 | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | type ruleNames struct{ rules, model, layout, variant, options uintptr } | |
| 45 | + | ||
| 46 | + | func New(names RMLVO) (_ *Keymap, err error) { | |
| 47 | + | k := &Keymap{} | |
| 48 | + | k.lib, err = purego.Dlopen("libxkbcommon.so.0", purego.RTLD_NOW|purego.RTLD_LOCAL) | |
| 49 | + | if err != nil { | |
| 50 | + | return nil, err | |
| 51 | + | } | |
| 52 | + | defer func() { | |
| 53 | + | if err != nil { | |
| 54 | + | k.Close() | |
| 55 | + | } | |
| 56 | + | }() | |
| 57 | + | k.libc, err = purego.Dlopen("libc.so.6", purego.RTLD_NOW|purego.RTLD_LOCAL) | |
| 58 | + | if err != nil { | |
| 59 | + | return nil, err | |
| 60 | + | } | |
| 61 | + | var contextNew func(uint32) uintptr | |
| 62 | + | var keymapNew func(uintptr, *ruleNames, uint32) uintptr | |
| 63 | + | var stateNew func(uintptr) uintptr | |
| 64 | + | bind := func(fn any, name string) { purego.RegisterLibFunc(fn, k.lib, "xkb_"+name) } | |
| 65 | + | bind(&contextNew, "context_new") | |
| 66 | + | bind(&k.contextUnref, "context_unref") | |
| 67 | + | bind(&keymapNew, "keymap_new_from_names") | |
| 68 | + | bind(&k.keymapUnref, "keymap_unref") | |
| 69 | + | bind(&stateNew, "state_new") | |
| 70 | + | bind(&k.stateUnref, "state_unref") | |
| 71 | + | bind(&k.keymapString, "keymap_get_as_string") | |
| 72 | + | bind(&k.getSym, "state_key_get_one_sym") | |
| 73 | + | bind(&k.symName, "keysym_get_name") | |
| 74 | + | bind(&k.updateKey, "state_update_key") | |
| 75 | + | bind(&k.serializeMods, "state_serialize_mods") | |
| 76 | + | bind(&k.serializeLayout, "state_serialize_layout") | |
| 77 | + | bind(&k.modActive, "state_mod_name_is_active") | |
| 78 | + | bind(&k.minCode, "keymap_min_keycode") | |
| 79 | + | bind(&k.maxCode, "keymap_max_keycode") | |
| 80 | + | bind(&k.symsLevel, "keymap_key_get_syms_by_level") | |
| 81 | + | bind(&k.fromName, "keysym_from_name") | |
| 82 | + | purego.RegisterLibFunc(&k.free, k.libc, "free") | |
| 83 | + | k.ctx = contextNew(0) | |
| 84 | + | if k.ctx == 0 { | |
| 85 | + | return nil, fmt.Errorf("xkb_context_new failed") | |
| 86 | + | } | |
| 87 | + | values := [...]string{names.Rules, names.Model, names.Layout, names.Variant, names.Options} | |
| 88 | + | var buffers [5][]byte | |
| 89 | + | var pins runtime.Pinner | |
| 90 | + | defer pins.Unpin() | |
| 91 | + | var pointers [5]uintptr | |
| 92 | + | for i, v := range values { | |
| 93 | + | if v == "" { | |
| 94 | + | continue | |
| 95 | + | } | |
| 96 | + | buffers[i] = append([]byte(v), 0) | |
| 97 | + | pins.Pin(&buffers[i][0]) | |
| 98 | + | pointers[i] = uintptr(unsafe.Pointer(&buffers[i][0])) | |
| 99 | + | } | |
| 100 | + | r := ruleNames{pointers[0], pointers[1], pointers[2], pointers[3], pointers[4]} | |
| 101 | + | pins.Pin(&r) | |
| 102 | + | k.mapPtr = keymapNew(k.ctx, &r, 0) | |
| 103 | + | runtime.KeepAlive(buffers) | |
| 104 | + | if k.mapPtr == 0 { | |
| 105 | + | return nil, fmt.Errorf("xkb_keymap_new_from_names failed") | |
| 106 | + | } | |
| 107 | + | k.state = stateNew(k.mapPtr) | |
| 108 | + | if k.state == 0 { | |
| 109 | + | return nil, fmt.Errorf("xkb_state_new failed") | |
| 110 | + | } | |
| 111 | + | return k, nil | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | func (k *Keymap) String() string { | |
| 115 | + | ptr := k.keymapString(k.mapPtr, 1) | |
| 116 | + | if ptr == nil { | |
| 117 | + | return "" | |
| 118 | + | } | |
| 119 | + | defer k.free(uintptr(ptr)) | |
| 120 | + | n := 0 | |
| 121 | + | for *(*byte)(unsafe.Add(ptr, n)) != 0 { | |
| 122 | + | n++ | |
| 123 | + | } | |
| 124 | + | return string(unsafe.Slice((*byte)(ptr), n)) | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | func (k *Keymap) Key(evdevCode uint32, pressed bool, timeMsec uint32) ports.KeyEvent { | |
| 128 | + | code := evdevCode + 8 | |
| 129 | + | sym := k.getSym(k.state, code) | |
| 130 | + | var buf [64]byte | |
| 131 | + | n := k.symName(sym, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf))) | |
| 132 | + | name := "" | |
| 133 | + | if n > 0 && n < int32(len(buf)) { | |
| 134 | + | name = string(buf[:n]) | |
| 135 | + | } | |
| 136 | + | direction := uint32(0) | |
| 137 | + | if pressed { | |
| 138 | + | direction = 1 | |
| 139 | + | } | |
| 140 | + | k.updateKey(k.state, code, direction) | |
| 141 | + | event := ports.KeyEvent{Keysym: name, Pressed: pressed, TimeMsec: timeMsec, Keycode: evdevCode} | |
| 142 | + | event.State = ports.ModState{ | |
| 143 | + | Depressed: k.serializeMods(k.state, modsDepressed), Latched: k.serializeMods(k.state, modsLatched), | |
| 144 | + | Locked: k.serializeMods(k.state, modsLocked), Group: k.serializeLayout(k.state, layoutEffective), | |
| 145 | + | } | |
| 146 | + | for _, mod := range []struct { | |
| 147 | + | name string | |
| 148 | + | flag ports.Mods | |
| 149 | + | }{{"Shift", ports.ModShift}, {"Control", ports.ModCtrl}, {"Mod1", ports.ModAlt}, {"Mod4", ports.ModSuper}} { | |
| 150 | + | b := append([]byte(mod.name), 0) | |
| 151 | + | if k.modActive(k.state, uintptr(unsafe.Pointer(&b[0])), modsEffective) > 0 { | |
| 152 | + | event.Mods |= mod.flag | |
| 153 | + | } | |
| 154 | + | runtime.KeepAlive(b) | |
| 155 | + | } | |
| 156 | + | return event | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | func (k *Keymap) KeycodeFor(keysymName string) (evdev uint32, shift bool, ok bool) { | |
| 160 | + | b := append([]byte(keysymName), 0) | |
| 161 | + | sym := k.fromName(uintptr(unsafe.Pointer(&b[0])), 0) | |
| 162 | + | runtime.KeepAlive(b) | |
| 163 | + | if sym == 0 { | |
| 164 | + | return 0, false, false | |
| 165 | + | } | |
| 166 | + | for code := k.minCode(k.mapPtr); code <= k.maxCode(k.mapPtr); code++ { | |
| 167 | + | for level := uint32(0); level < 2; level++ { | |
| 168 | + | var ptr unsafe.Pointer | |
| 169 | + | count := k.symsLevel(k.mapPtr, code, 0, level, &ptr) | |
| 170 | + | for i := int32(0); i < count; i++ { | |
| 171 | + | if *(*uint32)(unsafe.Add(ptr, uintptr(i)*4)) == sym && code >= 8 { | |
| 172 | + | return code - 8, level == 1, true | |
| 173 | + | } | |
| 174 | + | } | |
| 175 | + | } | |
| 176 | + | } | |
| 177 | + | return 0, false, false | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | func (k *Keymap) Close() { | |
| 181 | + | if k.state != 0 { | |
| 182 | + | k.stateUnref(k.state) | |
| 183 | + | k.state = 0 | |
| 184 | + | } | |
| 185 | + | if k.mapPtr != 0 { | |
| 186 | + | k.keymapUnref(k.mapPtr) | |
| 187 | + | k.mapPtr = 0 | |
| 188 | + | } | |
| 189 | + | if k.ctx != 0 { | |
| 190 | + | k.contextUnref(k.ctx) | |
| 191 | + | k.ctx = 0 | |
| 192 | + | } | |
| 193 | + | if k.libc != 0 { | |
| 194 | + | _ = purego.Dlclose(k.libc) | |
| 195 | + | k.libc = 0 | |
| 196 | + | } | |
| 197 | + | if k.lib != 0 { | |
| 198 | + | _ = purego.Dlclose(k.lib) | |
| 199 | + | k.lib = 0 | |
| 200 | + | } | |
| 201 | + | } | |
| @@ -0,0 +1,201 @@ | |||||
| 1 | + | // Package xkb provides a cgo-free libxkbcommon keymap and keyboard state. | |||
| 2 | + | package xkb | |||
| 3 | + | ||||
| 4 | + | import ( | |||
| 5 | + | "fmt" | |||
| 6 | + | "runtime" | |||
| 7 | + | "unsafe" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 10 | + | "github.com/bnema/purego" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | const ( | |||
| 14 | + | modsDepressed = 1 << iota | |||
| 15 | + | modsLatched | |||
| 16 | + | modsLocked | |||
| 17 | + | modsEffective | |||
| 18 | + | layoutEffective = 1 << 7 | |||
| 19 | + | ) | |||
| 20 | + | ||||
| 21 | + | type RMLVO struct{ Rules, Model, Layout, Variant, Options string } | |||
| 22 | + | ||||
| 23 | + | // Keymap owns its native context, keymap and state. It belongs to one input goroutine. | |||
| 24 | + | type Keymap struct { | |||
| 25 | + | ctx, mapPtr, state uintptr | |||
| 26 | + | lib, libc uintptr | |||
| 27 | + | contextUnref func(uintptr) | |||
| 28 | + | keymapUnref func(uintptr) | |||
| 29 | + | stateUnref func(uintptr) | |||
| 30 | + | keymapString func(uintptr, uint32) unsafe.Pointer | |||
| 31 | + | free func(uintptr) | |||
| 32 | + | getSym func(uintptr, uint32) uint32 | |||
| 33 | + | symName func(uint32, uintptr, uintptr) int32 | |||
| 34 | + | updateKey func(uintptr, uint32, uint32) uint32 | |||
| 35 | + | serializeMods func(uintptr, uint32) uint32 | |||
| 36 | + | serializeLayout func(uintptr, uint32) uint32 | |||
| 37 | + | modActive func(uintptr, uintptr, uint32) int32 | |||
| 38 | + | minCode func(uintptr) uint32 | |||
| 39 | + | maxCode func(uintptr) uint32 | |||
| 40 | + | symsLevel func(uintptr, uint32, uint32, uint32, *unsafe.Pointer) int32 | |||
| 41 | + | fromName func(uintptr, uint32) uint32 | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | type ruleNames struct{ rules, model, layout, variant, options uintptr } | |||
| 45 | + | ||||
| 46 | + | func New(names RMLVO) (_ *Keymap, err error) { | |||
| 47 | + | k := &Keymap{} | |||
| 48 | + | k.lib, err = purego.Dlopen("libxkbcommon.so.0", purego.RTLD_NOW|purego.RTLD_LOCAL) | |||
| 49 | + | if err != nil { | |||
| 50 | + | return nil, err | |||
| 51 | + | } | |||
| 52 | + | defer func() { | |||
| 53 | + | if err != nil { | |||
| 54 | + | k.Close() | |||
| 55 | + | } | |||
| 56 | + | }() | |||
| 57 | + | k.libc, err = purego.Dlopen("libc.so.6", purego.RTLD_NOW|purego.RTLD_LOCAL) | |||
| 58 | + | if err != nil { | |||
| 59 | + | return nil, err | |||
| 60 | + | } | |||
| 61 | + | var contextNew func(uint32) uintptr | |||
| 62 | + | var keymapNew func(uintptr, *ruleNames, uint32) uintptr | |||
| 63 | + | var stateNew func(uintptr) uintptr | |||
| 64 | + | bind := func(fn any, name string) { purego.RegisterLibFunc(fn, k.lib, "xkb_"+name) } | |||
| 65 | + | bind(&contextNew, "context_new") | |||
| 66 | + | bind(&k.contextUnref, "context_unref") | |||
| 67 | + | bind(&keymapNew, "keymap_new_from_names") | |||
| 68 | + | bind(&k.keymapUnref, "keymap_unref") | |||
| 69 | + | bind(&stateNew, "state_new") | |||
| 70 | + | bind(&k.stateUnref, "state_unref") | |||
| 71 | + | bind(&k.keymapString, "keymap_get_as_string") | |||
| 72 | + | bind(&k.getSym, "state_key_get_one_sym") | |||
| 73 | + | bind(&k.symName, "keysym_get_name") | |||
| 74 | + | bind(&k.updateKey, "state_update_key") | |||
| 75 | + | bind(&k.serializeMods, "state_serialize_mods") | |||
| 76 | + | bind(&k.serializeLayout, "state_serialize_layout") | |||
| 77 | + | bind(&k.modActive, "state_mod_name_is_active") | |||
| 78 | + | bind(&k.minCode, "keymap_min_keycode") | |||
| 79 | + | bind(&k.maxCode, "keymap_max_keycode") | |||
| 80 | + | bind(&k.symsLevel, "keymap_key_get_syms_by_level") | |||
| 81 | + | bind(&k.fromName, "keysym_from_name") | |||
| 82 | + | purego.RegisterLibFunc(&k.free, k.libc, "free") | |||
| 83 | + | k.ctx = contextNew(0) | |||
| 84 | + | if k.ctx == 0 { | |||
| 85 | + | return nil, fmt.Errorf("xkb_context_new failed") | |||
| 86 | + | } | |||
| 87 | + | values := [...]string{names.Rules, names.Model, names.Layout, names.Variant, names.Options} | |||
| 88 | + | var buffers [5][]byte | |||
| 89 | + | var pins runtime.Pinner | |||
| 90 | + | defer pins.Unpin() | |||
| 91 | + | var pointers [5]uintptr | |||
| 92 | + | for i, v := range values { | |||
| 93 | + | if v == "" { | |||
| 94 | + | continue | |||
| 95 | + | } | |||
| 96 | + | buffers[i] = append([]byte(v), 0) | |||
| 97 | + | pins.Pin(&buffers[i][0]) | |||
| 98 | + | pointers[i] = uintptr(unsafe.Pointer(&buffers[i][0])) | |||
| 99 | + | } | |||
| 100 | + | r := ruleNames{pointers[0], pointers[1], pointers[2], pointers[3], pointers[4]} | |||
| 101 | + | pins.Pin(&r) | |||
| 102 | + | k.mapPtr = keymapNew(k.ctx, &r, 0) | |||
| 103 | + | runtime.KeepAlive(buffers) | |||
| 104 | + | if k.mapPtr == 0 { | |||
| 105 | + | return nil, fmt.Errorf("xkb_keymap_new_from_names failed") | |||
| 106 | + | } | |||
| 107 | + | k.state = stateNew(k.mapPtr) | |||
| 108 | + | if k.state == 0 { | |||
| 109 | + | return nil, fmt.Errorf("xkb_state_new failed") | |||
| 110 | + | } | |||
| 111 | + | return k, nil | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | func (k *Keymap) String() string { | |||
| 115 | + | ptr := k.keymapString(k.mapPtr, 1) | |||
| 116 | + | if ptr == nil { | |||
| 117 | + | return "" | |||
| 118 | + | } | |||
| 119 | + | defer k.free(uintptr(ptr)) | |||
| 120 | + | n := 0 | |||
| 121 | + | for *(*byte)(unsafe.Add(ptr, n)) != 0 { | |||
| 122 | + | n++ | |||
| 123 | + | } | |||
| 124 | + | return string(unsafe.Slice((*byte)(ptr), n)) | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | func (k *Keymap) Key(evdevCode uint32, pressed bool, timeMsec uint32) ports.KeyEvent { | |||
| 128 | + | code := evdevCode + 8 | |||
| 129 | + | sym := k.getSym(k.state, code) | |||
| 130 | + | var buf [64]byte | |||
| 131 | + | n := k.symName(sym, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf))) | |||
| 132 | + | name := "" | |||
| 133 | + | if n > 0 && n < int32(len(buf)) { | |||
| 134 | + | name = string(buf[:n]) | |||
| 135 | + | } | |||
| 136 | + | direction := uint32(0) | |||
| 137 | + | if pressed { | |||
| 138 | + | direction = 1 | |||
| 139 | + | } | |||
| 140 | + | k.updateKey(k.state, code, direction) | |||
| 141 | + | event := ports.KeyEvent{Keysym: name, Pressed: pressed, TimeMsec: timeMsec, Keycode: evdevCode} | |||
| 142 | + | event.State = ports.ModState{ | |||
| 143 | + | Depressed: k.serializeMods(k.state, modsDepressed), Latched: k.serializeMods(k.state, modsLatched), | |||
| 144 | + | Locked: k.serializeMods(k.state, modsLocked), Group: k.serializeLayout(k.state, layoutEffective), | |||
| 145 | + | } | |||
| 146 | + | for _, mod := range []struct { | |||
| 147 | + | name string | |||
| 148 | + | flag ports.Mods | |||
| 149 | + | }{{"Shift", ports.ModShift}, {"Control", ports.ModCtrl}, {"Mod1", ports.ModAlt}, {"Mod4", ports.ModSuper}} { | |||
| 150 | + | b := append([]byte(mod.name), 0) | |||
| 151 | + | if k.modActive(k.state, uintptr(unsafe.Pointer(&b[0])), modsEffective) > 0 { | |||
| 152 | + | event.Mods |= mod.flag | |||
| 153 | + | } | |||
| 154 | + | runtime.KeepAlive(b) | |||
| 155 | + | } | |||
| 156 | + | return event | |||
| 157 | + | } | |||
| 158 | + | ||||
| 159 | + | func (k *Keymap) KeycodeFor(keysymName string) (evdev uint32, shift bool, ok bool) { | |||
| 160 | + | b := append([]byte(keysymName), 0) | |||
| 161 | + | sym := k.fromName(uintptr(unsafe.Pointer(&b[0])), 0) | |||
| 162 | + | runtime.KeepAlive(b) | |||
| 163 | + | if sym == 0 { | |||
| 164 | + | return 0, false, false | |||
| 165 | + | } | |||
| 166 | + | for code := k.minCode(k.mapPtr); code <= k.maxCode(k.mapPtr); code++ { | |||
| 167 | + | for level := uint32(0); level < 2; level++ { | |||
| 168 | + | var ptr unsafe.Pointer | |||
| 169 | + | count := k.symsLevel(k.mapPtr, code, 0, level, &ptr) | |||
| 170 | + | for i := int32(0); i < count; i++ { | |||
| 171 | + | if *(*uint32)(unsafe.Add(ptr, uintptr(i)*4)) == sym && code >= 8 { | |||
| 172 | + | return code - 8, level == 1, true | |||
| 173 | + | } | |||
| 174 | + | } | |||
| 175 | + | } | |||
| 176 | + | } | |||
| 177 | + | return 0, false, false | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | func (k *Keymap) Close() { | |||
| 181 | + | if k.state != 0 { | |||
| 182 | + | k.stateUnref(k.state) | |||
| 183 | + | k.state = 0 | |||
| 184 | + | } | |||
| 185 | + | if k.mapPtr != 0 { | |||
| 186 | + | k.keymapUnref(k.mapPtr) | |||
| 187 | + | k.mapPtr = 0 | |||
| 188 | + | } | |||
| 189 | + | if k.ctx != 0 { | |||
| 190 | + | k.contextUnref(k.ctx) | |||
| 191 | + | k.ctx = 0 | |||
| 192 | + | } | |||
| 193 | + | if k.libc != 0 { | |||
| 194 | + | _ = purego.Dlclose(k.libc) | |||
| 195 | + | k.libc = 0 | |||
| 196 | + | } | |||
| 197 | + | if k.lib != 0 { | |||
| 198 | + | _ = purego.Dlclose(k.lib) | |||
| 199 | + | k.lib = 0 | |||
| 200 | + | } | |||
| 201 | + | } | |||
A internal/adapters/xkb/xkb_test.go +54 -0
| @@ -0,0 +1,54 @@ | |||
| 1 | + | package xkb | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "strings" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func newTest(t *testing.T, layout string) *Keymap { | |
| 11 | + | t.Helper() | |
| 12 | + | k, err := New(RMLVO{Layout: layout}) | |
| 13 | + | if err != nil { | |
| 14 | + | if strings.Contains(err.Error(), "libxkbcommon.so.0") { | |
| 15 | + | t.Skipf("xkbcommon unavailable: %v", err) | |
| 16 | + | } | |
| 17 | + | t.Fatal(err) | |
| 18 | + | } | |
| 19 | + | t.Cleanup(k.Close) | |
| 20 | + | return k | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | func TestKeymap(t *testing.T) { | |
| 24 | + | k := newTest(t, "us") | |
| 25 | + | if !strings.Contains(k.String(), "xkb_keymap") { | |
| 26 | + | t.Fatal("missing keymap text") | |
| 27 | + | } | |
| 28 | + | if ev := k.Key(30, true, 123); ev.Keysym != "a" || ev.Keycode != 30 || !ev.Pressed || ev.TimeMsec != 123 { | |
| 29 | + | t.Fatalf("A: %+v", ev) | |
| 30 | + | } | |
| 31 | + | k.Key(30, false, 0) | |
| 32 | + | k.Key(42, true, 0) | |
| 33 | + | if ev := k.Key(30, true, 0); ev.Keysym != "A" || ev.Mods&ports.ModShift == 0 || ev.State.Depressed == 0 { | |
| 34 | + | t.Fatalf("shift A: %+v", ev) | |
| 35 | + | } | |
| 36 | + | k.Key(42, false, 0) | |
| 37 | + | if ev := k.Key(30, false, 0); ev.Mods&ports.ModShift != 0 { | |
| 38 | + | t.Fatalf("shift release: %+v", ev) | |
| 39 | + | } | |
| 40 | + | for _, tt := range []struct { | |
| 41 | + | name string | |
| 42 | + | code uint32 | |
| 43 | + | shift, ok bool | |
| 44 | + | }{{"a", 30, false, true}, {"A", 30, true, true}, {"Return", 28, false, true}, {"nope", 0, false, false}} { | |
| 45 | + | code, shift, ok := k.KeycodeFor(tt.name) | |
| 46 | + | if code != tt.code || shift != tt.shift || ok != tt.ok { | |
| 47 | + | t.Errorf("%s: %d %v %v", tt.name, code, shift, ok) | |
| 48 | + | } | |
| 49 | + | } | |
| 50 | + | fr := newTest(t, "fr") | |
| 51 | + | if code, _, ok := fr.KeycodeFor("a"); !ok || code != 16 { | |
| 52 | + | t.Errorf("fr a: %d %v", code, ok) | |
| 53 | + | } | |
| 54 | + | } | |
| @@ -0,0 +1,54 @@ | |||||
| 1 | + | package xkb | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "strings" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/nefertty/internal/ports" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func newTest(t *testing.T, layout string) *Keymap { | |||
| 11 | + | t.Helper() | |||
| 12 | + | k, err := New(RMLVO{Layout: layout}) | |||
| 13 | + | if err != nil { | |||
| 14 | + | if strings.Contains(err.Error(), "libxkbcommon.so.0") { | |||
| 15 | + | t.Skipf("xkbcommon unavailable: %v", err) | |||
| 16 | + | } | |||
| 17 | + | t.Fatal(err) | |||
| 18 | + | } | |||
| 19 | + | t.Cleanup(k.Close) | |||
| 20 | + | return k | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | func TestKeymap(t *testing.T) { | |||
| 24 | + | k := newTest(t, "us") | |||
| 25 | + | if !strings.Contains(k.String(), "xkb_keymap") { | |||
| 26 | + | t.Fatal("missing keymap text") | |||
| 27 | + | } | |||
| 28 | + | if ev := k.Key(30, true, 123); ev.Keysym != "a" || ev.Keycode != 30 || !ev.Pressed || ev.TimeMsec != 123 { | |||
| 29 | + | t.Fatalf("A: %+v", ev) | |||
| 30 | + | } | |||
| 31 | + | k.Key(30, false, 0) | |||
| 32 | + | k.Key(42, true, 0) | |||
| 33 | + | if ev := k.Key(30, true, 0); ev.Keysym != "A" || ev.Mods&ports.ModShift == 0 || ev.State.Depressed == 0 { | |||
| 34 | + | t.Fatalf("shift A: %+v", ev) | |||
| 35 | + | } | |||
| 36 | + | k.Key(42, false, 0) | |||
| 37 | + | if ev := k.Key(30, false, 0); ev.Mods&ports.ModShift != 0 { | |||
| 38 | + | t.Fatalf("shift release: %+v", ev) | |||
| 39 | + | } | |||
| 40 | + | for _, tt := range []struct { | |||
| 41 | + | name string | |||
| 42 | + | code uint32 | |||
| 43 | + | shift, ok bool | |||
| 44 | + | }{{"a", 30, false, true}, {"A", 30, true, true}, {"Return", 28, false, true}, {"nope", 0, false, false}} { | |||
| 45 | + | code, shift, ok := k.KeycodeFor(tt.name) | |||
| 46 | + | if code != tt.code || shift != tt.shift || ok != tt.ok { | |||
| 47 | + | t.Errorf("%s: %d %v %v", tt.name, code, shift, ok) | |||
| 48 | + | } | |||
| 49 | + | } | |||
| 50 | + | fr := newTest(t, "fr") | |||
| 51 | + | if code, _, ok := fr.KeycodeFor("a"); !ok || code != 16 { | |||
| 52 | + | t.Errorf("fr a: %d %v", code, ok) | |||
| 53 | + | } | |||
| 54 | + | } | |||