v0.4.0 b6be3a8

xkb.go RawHistoryFolder

1// Package xkb provides a cgo-free libxkbcommon keymap and keyboard state.
2package xkb
3
4import (
5	"fmt"
6	"runtime"
7	"time"
8	"unsafe"
9
10	"github.com/bnema/neferwl/internal/ports"
11	"github.com/bnema/purego"
12)
13
14const (
15	modsDepressed = 1 << iota
16	modsLatched
17	modsLocked
18	modsEffective
19	layoutEffective = 1 << 7
20)
21
22type RMLVO struct{ Rules, Model, Layout, Variant, Options string }
23
24// Keymap owns its native context, keymap and state. It belongs to one input goroutine.
25type Keymap struct {
26	ctx, mapPtr, state uintptr
27	lib, libc          uintptr
28	contextUnref       func(uintptr)
29	keymapUnref        func(uintptr)
30	stateUnref         func(uintptr)
31	stateNew           func(uintptr) uintptr
32	// Secure tracking belongs to the same producer goroutine as native state.
33	epoch             ports.SecurityState
34	epochSet          bool
35	held, quarantined map[uint32]bool
36	keymapString      func(uintptr, uint32) unsafe.Pointer
37	free              func(uintptr)
38	getSym            func(uintptr, uint32) uint32
39	// Buffers go to C as typed pointers: they escape to the heap, where a
40	// stack growth during the call cannot move them under C.
41	symName         func(uint32, *byte, uintptr) int32
42	updateKey       func(uintptr, uint32, uint32) uint32
43	serializeMods   func(uintptr, uint32) uint32
44	serializeLayout func(uintptr, uint32) uint32
45	modActive       func(uintptr, *byte, uint32) int32
46	minCode         func(uintptr) uint32
47	maxCode         func(uintptr) uint32
48	symsLevel       func(uintptr, uint32, uint32, uint32, *unsafe.Pointer) int32
49	fromName        func(*byte, uint32) uint32
50}
51
52type ruleNames struct{ rules, model, layout, variant, options uintptr }
53
54func New(names RMLVO) (_ *Keymap, err error) {
55	k := &Keymap{}
56	k.lib, err = purego.Dlopen("libxkbcommon.so.0", purego.RTLD_NOW|purego.RTLD_LOCAL)
57	if err != nil {
58		return nil, err
59	}
60	defer func() {
61		if err != nil {
62			k.Close()
63		}
64	}()
65	k.libc, err = purego.Dlopen("libc.so.6", purego.RTLD_NOW|purego.RTLD_LOCAL)
66	if err != nil {
67		return nil, err
68	}
69	var contextNew func(uint32) uintptr
70	var keymapNew func(uintptr, *ruleNames, uint32) uintptr
71	bind := func(fn any, name string) { purego.RegisterLibFunc(fn, k.lib, "xkb_"+name) }
72	bind(&contextNew, "context_new")
73	bind(&k.contextUnref, "context_unref")
74	bind(&keymapNew, "keymap_new_from_names")
75	bind(&k.keymapUnref, "keymap_unref")
76	bind(&k.stateNew, "state_new")
77	bind(&k.stateUnref, "state_unref")
78	bind(&k.keymapString, "keymap_get_as_string")
79	bind(&k.getSym, "state_key_get_one_sym")
80	bind(&k.symName, "keysym_get_name")
81	bind(&k.updateKey, "state_update_key")
82	bind(&k.serializeMods, "state_serialize_mods")
83	bind(&k.serializeLayout, "state_serialize_layout")
84	bind(&k.modActive, "state_mod_name_is_active")
85	bind(&k.minCode, "keymap_min_keycode")
86	bind(&k.maxCode, "keymap_max_keycode")
87	bind(&k.symsLevel, "keymap_key_get_syms_by_level")
88	bind(&k.fromName, "keysym_from_name")
89	purego.RegisterLibFunc(&k.free, k.libc, "free")
90	k.ctx = contextNew(0)
91	if k.ctx == 0 {
92		return nil, fmt.Errorf("xkb_context_new failed")
93	}
94	values := [...]string{names.Rules, names.Model, names.Layout, names.Variant, names.Options}
95	var buffers [5][]byte
96	var pins runtime.Pinner
97	defer pins.Unpin()
98	var pointers [5]uintptr
99	for i, v := range values {
100		if v == "" {
101			continue
102		}
103		buffers[i] = append([]byte(v), 0)
104		pins.Pin(&buffers[i][0])
105		pointers[i] = uintptr(unsafe.Pointer(&buffers[i][0]))
106	}
107	r := ruleNames{pointers[0], pointers[1], pointers[2], pointers[3], pointers[4]}
108	pins.Pin(&r)
109	k.mapPtr = keymapNew(k.ctx, &r, 0)
110	runtime.KeepAlive(buffers)
111	if k.mapPtr == 0 {
112		return nil, fmt.Errorf("xkb_keymap_new_from_names failed")
113	}
114	k.state = k.stateNew(k.mapPtr)
115	if k.state == 0 {
116		return nil, fmt.Errorf("xkb_state_new failed")
117	}
118	return k, nil
119}
120
121func (k *Keymap) String() string {
122	ptr := k.keymapString(k.mapPtr, 1)
123	if ptr == nil {
124		return ""
125	}
126	defer k.free(uintptr(ptr))
127	n := 0
128	for *(*byte)(unsafe.Add(ptr, n)) != 0 {
129		n++
130	}
131	return string(unsafe.Slice((*byte)(ptr), n))
132}
133
134func (k *Keymap) Key(evdevCode uint32, pressed bool, t time.Duration) ports.KeyEvent {
135	code := evdevCode + 8
136	sym := k.getSym(k.state, code)
137	var buf [64]byte
138	n := k.symName(sym, &buf[0], uintptr(len(buf)))
139	name := ""
140	if n > 0 && n < int32(len(buf)) {
141		name = string(buf[:n])
142	}
143	direction := uint32(0)
144	if pressed {
145		direction = 1
146	}
147	k.updateKey(k.state, code, direction)
148	event := ports.KeyEvent{Keysym: name, Pressed: pressed, Time: t, Keycode: evdevCode}
149	if pressed {
150		layout := k.serializeLayout(k.state, layoutEffective)
151		event.Base = k.levelName(code, layout, 0)
152	}
153	event.State = ports.ModState{
154		Depressed: k.serializeMods(k.state, modsDepressed), Latched: k.serializeMods(k.state, modsLatched),
155		Locked: k.serializeMods(k.state, modsLocked), Group: k.serializeLayout(k.state, layoutEffective),
156	}
157	for _, mod := range []struct {
158		name string
159		flag ports.Mods
160	}{{"Shift", ports.ModShift}, {"Control", ports.ModCtrl}, {"Mod1", ports.ModAlt}, {"Mod4", ports.ModSuper}} {
161		b := append([]byte(mod.name), 0)
162		if k.modActive(k.state, &b[0], modsEffective) > 0 {
163			event.Mods |= mod.flag
164		}
165		runtime.KeepAlive(b)
166	}
167	return event
168}
169
170// KeySecure resets native modifier/lock/layout state before translating in a
171// new security epoch. Keys physically held across a transition are suppressed
172// (including repeats and their eventual release), never replayed into XKB.
173// On reset failure no event is translated against the old native state.
174func (k *Keymap) KeySecure(code uint32, pressed bool, t time.Duration, epoch ports.SecurityState) (ports.KeyEvent, bool, error) {
175	if !k.epochSet || k.epoch != epoch {
176		next := k.stateNew(k.mapPtr)
177		if next == 0 {
178			// Even a failed translation must remember physical presses so a
179			// later successful reset cannot admit their repeats.
180			if k.held == nil {
181				k.held = make(map[uint32]bool)
182			}
183			if pressed {
184				k.held[code] = true
185			} else {
186				delete(k.held, code)
187			}
188			return ports.KeyEvent{}, false, fmt.Errorf("xkb_state_new failed during security reset")
189		}
190		old := k.state
191		k.state = next
192		k.stateUnref(old)
193		k.epoch, k.epochSet = epoch, true
194		if k.held == nil {
195			k.held = make(map[uint32]bool)
196		}
197		k.quarantined = make(map[uint32]bool, len(k.held))
198		for held := range k.held {
199			k.quarantined[held] = true
200		}
201	}
202	blocked := k.quarantined[code]
203	if pressed {
204		k.held[code] = true
205	} else {
206		delete(k.held, code)
207		delete(k.quarantined, code)
208	}
209	if blocked {
210		return ports.KeyEvent{}, false, nil
211	}
212	return k.Key(code, pressed, t), true, nil
213}
214
215// QuarantineFrom transfers physical held-key tracking when a producer replaces
216// its keymap. No old native state or key presses are replayed into the new map.
217// Call before closing previous; both maps belong to the calling goroutine.
218func (k *Keymap) QuarantineFrom(previous *Keymap) {
219	k.epoch, k.epochSet = previous.epoch, previous.epochSet
220	k.held = make(map[uint32]bool, len(previous.held))
221	k.quarantined = make(map[uint32]bool, len(previous.held))
222	for code := range previous.held {
223		k.held[code], k.quarantined[code] = true, true
224	}
225}
226
227// levelName is the first keysym name of a key at one shift level, or "".
228func (k *Keymap) levelName(code, layout, level uint32) string {
229	var ptr unsafe.Pointer
230	if k.symsLevel(k.mapPtr, code, layout, level, &ptr) < 1 {
231		return ""
232	}
233	var buf [64]byte
234	n := k.symName(*(*uint32)(ptr), &buf[0], uintptr(len(buf)))
235	if n <= 0 || n >= int32(len(buf)) {
236		return ""
237	}
238	return string(buf[:n])
239}
240
241func (k *Keymap) KeycodeFor(keysymName string) (evdev uint32, shift bool, ok bool) {
242	b := append([]byte(keysymName), 0)
243	sym := k.fromName(&b[0], 0)
244	runtime.KeepAlive(b)
245	if sym == 0 {
246		return 0, false, false
247	}
248	for code := k.minCode(k.mapPtr); code <= k.maxCode(k.mapPtr); code++ {
249		for level := uint32(0); level < 2; level++ {
250			var ptr unsafe.Pointer
251			count := k.symsLevel(k.mapPtr, code, 0, level, &ptr)
252			for i := int32(0); i < count; i++ {
253				if *(*uint32)(unsafe.Add(ptr, uintptr(i)*4)) == sym && code >= 8 {
254					return code - 8, level == 1, true
255				}
256			}
257		}
258	}
259	return 0, false, false
260}
261
262func (k *Keymap) Close() {
263	if k.state != 0 {
264		k.stateUnref(k.state)
265		k.state = 0
266	}
267	if k.mapPtr != 0 {
268		k.keymapUnref(k.mapPtr)
269		k.mapPtr = 0
270	}
271	if k.ctx != 0 {
272		k.contextUnref(k.ctx)
273		k.ctx = 0
274	}
275	if k.libc != 0 {
276		_ = purego.Dlclose(k.libc)
277		k.libc = 0
278	}
279	if k.lib != 0 {
280		_ = purego.Dlclose(k.lib)
281		k.lib = 0
282	}
283}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close