feat/xdg-foreign fdbb95e

xkb.go RawHistoryFolder

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

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