main 908d8c0

xkbcommon.go RawHistoryFolder

1// Package xkbcommon interprets Wayland keyboard maps with libxkbcommon,
2// loaded at runtime without cgo. Close owned values when they are no longer used.
3package xkbcommon
4
5import (
6	"errors"
7	"fmt"
8	"io"
9	"runtime"
10	"strings"
11	"sync"
12	"syscall"
13	"unsafe"
14
15	"github.com/bnema/purego-xkbcommon/raw"
16)
17
18var ErrClosed = errors.New("xkbcommon: closed handle")
19
20func cstr(s string) []byte { return append([]byte(s), 0) }
21
22// ptr returns the address of b's first byte, or nil. The result is only valid
23// as an argument to a raw.Funcs method, whose uintptr conversion keeps the
24// backing array alive and (via //go:uintptrescapes) on the heap for the call.
25func ptr(b []byte) unsafe.Pointer {
26	if len(b) == 0 {
27		return nil
28	}
29	return unsafe.Pointer(&b[0])
30}
31func validString(s string) error {
32	if strings.IndexByte(s, 0) >= 0 {
33		return errors.New("xkbcommon: embedded NUL")
34	}
35	return nil
36}
37
38// Keycodes from wl_keyboard are evdev codes; add 8 before calling State.KeySym or State.UTF8.
39func WaylandKeycode(evdev uint32) uint32 { return evdev + 8 }
40
41type Context struct {
42	mu sync.Mutex
43	h  uintptr
44}
45
46func NewContext() (*Context, error) {
47	if err := Available(); err != nil {
48		return nil, err
49	}
50	h := loader.f.Xkb_context_new(0)
51	if h == 0 {
52		return nil, errors.New("xkbcommon: create context failed")
53	}
54	return &Context{h: h}, nil
55}
56func (c *Context) Close() error {
57	if c == nil {
58		return nil
59	}
60	c.mu.Lock()
61	defer c.mu.Unlock()
62	if c.h != 0 {
63		loader.f.Xkb_context_unref(c.h)
64		c.h = 0
65	}
66	return nil
67}
68func (c *Context) retain() (uintptr, error) {
69	if c == nil {
70		return 0, ErrClosed
71	}
72	c.mu.Lock()
73	defer c.mu.Unlock()
74	if c.h == 0 {
75		return 0, ErrClosed
76	}
77	loader.f.Xkb_context_ref(c.h)
78	return c.h, nil
79}
80func releaseContext(h uintptr) { loader.f.Xkb_context_unref(h) }
81
82type Keymap struct {
83	mu         sync.Mutex
84	h, context uintptr
85}
86
87func newKeymap(ctx *Context, build func(uintptr) uintptr) (*Keymap, error) {
88	h, e := ctx.retain()
89	if e != nil {
90		return nil, e
91	}
92	k := build(h)
93	if k == 0 {
94		releaseContext(h)
95		return nil, errors.New("xkbcommon: invalid keymap")
96	}
97	return &Keymap{h: k, context: h}, nil
98}
99
100// NewKeymapString compiles a NUL-free xkb_v1 text keymap.
101func (c *Context) NewKeymapString(text string) (*Keymap, error) {
102	if e := validString(text); e != nil {
103		return nil, e
104	}
105	b := cstr(text)
106	return newKeymap(c, func(h uintptr) uintptr {
107		v := loader.f.Xkb_keymap_new_from_string(h, ptr(b), raw.XKB_KEYMAP_FORMAT_TEXT_V1, 0)
108		runtime.KeepAlive(b)
109		return v
110	})
111}
112
113// NewKeymapRules compiles an evdev rules keymap. Empty rules/model/variant/options
114// use libxkbcommon defaults; layout is e.g. "us", "fr", "de", or "us,de".
115func (c *Context) NewKeymapRules(rules, model, layout, variant, options string) (*Keymap, error) {
116	args := []string{rules, model, layout, variant, options}
117	var b [5][]byte
118	var n raw.RuleNames
119	p := []*unsafe.Pointer{&n.Rules, &n.Model, &n.Layout, &n.Variant, &n.Options}
120	for i, s := range args {
121		if e := validString(s); e != nil {
122			return nil, e
123		}
124		if s != "" {
125			b[i] = cstr(s)
126			*p[i] = ptr(b[i])
127		}
128	}
129	return newKeymap(c, func(h uintptr) uintptr {
130		v := loader.f.Xkb_keymap_new_from_names(h, unsafe.Pointer(&n), 0)
131		runtime.KeepAlive(b)
132		runtime.KeepAlive(n)
133		return v
134	})
135}
136
137// NewKeymapFD copies exactly size bytes from offset zero of a Wayland xkb_v1
138// keymap FD (at most 16 MiB). A trailing Wayland NUL, if present, is excluded
139// from the compiled text. The caller retains ownership of fd, even on error.
140func (c *Context) NewKeymapFD(fd int, size int) (*Keymap, error) {
141	if fd < 0 || size < 1 || size > 16<<20 {
142		return nil, errors.New("xkbcommon: invalid keymap FD or size (limit 16 MiB)")
143	}
144	var stat syscall.Stat_t
145	if e := syscall.Fstat(fd, &stat); e != nil {
146		return nil, fmt.Errorf("xkbcommon: stat keymap FD: %w", e)
147	}
148	if stat.Mode&syscall.S_IFMT != syscall.S_IFREG {
149		return nil, errors.New("xkbcommon: keymap FD is not a regular file")
150	}
151	b := make([]byte, size)
152	for offset := 0; offset < size; {
153		n, e := syscall.Pread(fd, b[offset:], int64(offset))
154		offset += n
155		if e == syscall.EINTR {
156			continue
157		}
158		if e != nil {
159			return nil, fmt.Errorf("xkbcommon: read keymap FD: %w", e)
160		}
161		if n == 0 {
162			return nil, fmt.Errorf("xkbcommon: read keymap FD: %w", errors.New("short read"))
163		}
164	}
165	if b[len(b)-1] == 0 {
166		b = b[:len(b)-1]
167	}
168	return newKeymap(c, func(h uintptr) uintptr {
169		v := loader.f.Xkb_keymap_new_from_buffer(h, ptr(b), uintptr(len(b)), raw.XKB_KEYMAP_FORMAT_TEXT_V1, 0)
170		runtime.KeepAlive(b)
171		return v
172	})
173}
174func (k *Keymap) Close() error {
175	if k == nil {
176		return nil
177	}
178	k.mu.Lock()
179	h, ctx := k.h, k.context
180	k.h = 0
181	k.context = 0
182	if h != 0 {
183		loader.f.Xkb_keymap_unref(h)
184	}
185	k.mu.Unlock()
186	if ctx != 0 {
187		releaseContext(ctx)
188	}
189	return nil
190}
191func (k *Keymap) retain() (uintptr, error) {
192	if k == nil {
193		return 0, ErrClosed
194	}
195	k.mu.Lock()
196	defer k.mu.Unlock()
197	if k.h == 0 {
198		return 0, ErrClosed
199	}
200	loader.f.Xkb_keymap_ref(k.h)
201	return k.h, nil
202}
203func (k *Keymap) ModIndex(name string) (uint32, error) {
204	if e := validString(name); e != nil {
205		return 0, e
206	}
207	if k == nil {
208		return 0, ErrClosed
209	}
210	b := cstr(name)
211	k.mu.Lock()
212	defer k.mu.Unlock()
213	if k.h == 0 {
214		return 0, ErrClosed
215	}
216	v := loader.f.Xkb_keymap_mod_get_index(k.h, ptr(b))
217	runtime.KeepAlive(b)
218	return v, nil
219}
220
221// KeyRepeats reports whether the key repeats when held. It does not allocate.
222func (k *Keymap) KeyRepeats(keycode uint32) (bool, error) {
223	if k == nil {
224		return false, ErrClosed
225	}
226	k.mu.Lock()
227	defer k.mu.Unlock()
228	if k.h == 0 {
229		return false, ErrClosed
230	}
231	return loader.f.Xkb_keymap_key_repeats(k.h, keycode) > 0, nil
232}
233func (k *Keymap) LayoutCount() (uint32, error) {
234	if k == nil {
235		return 0, ErrClosed
236	}
237	k.mu.Lock()
238	defer k.mu.Unlock()
239	if k.h == 0 {
240		return 0, ErrClosed
241	}
242	return loader.f.Xkb_keymap_num_layouts(k.h), nil
243}
244func (k *Keymap) NewState() (*State, error) {
245	h, e := k.retain()
246	if e != nil {
247		return nil, e
248	}
249	s := loader.f.Xkb_state_new(h)
250	if s == 0 {
251		loader.f.Xkb_keymap_unref(h)
252		return nil, errors.New("xkbcommon: create state failed")
253	}
254	return &State{h: s, keymap: h}, nil
255}
256
257type State struct {
258	mu        sync.Mutex
259	h, keymap uintptr
260	name      [64]byte // NUL-terminated modifier-name scratch, guarded by mu
261}
262
263func (s *State) Close() error {
264	if s == nil {
265		return nil
266	}
267	s.mu.Lock()
268	h, k := s.h, s.keymap
269	s.h = 0
270	s.keymap = 0
271	if h != 0 {
272		loader.f.Xkb_state_unref(h)
273	}
274	s.mu.Unlock()
275	if k != 0 {
276		loader.f.Xkb_keymap_unref(k)
277	}
278	return nil
279}
280
281// UpdateMask accepts depressed/latched/locked modifier masks and layout group indices
282// in the order supplied by wl_keyboard.modifiers. It returns changed components.
283func (s *State) UpdateMask(depressed, latched, locked, groupDepressed, groupLatched, groupLocked uint32) (uint32, error) {
284	if s == nil {
285		return 0, ErrClosed
286	}
287	s.mu.Lock()
288	defer s.mu.Unlock()
289	if s.h == 0 {
290		return 0, ErrClosed
291	}
292	return loader.f.Xkb_state_update_mask(s.h, depressed, latched, locked, groupDepressed, groupLatched, groupLocked), nil
293}
294func (s *State) Layout() (uint32, error) {
295	if s == nil {
296		return 0, ErrClosed
297	}
298	s.mu.Lock()
299	defer s.mu.Unlock()
300	if s.h == 0 {
301		return 0, ErrClosed
302	}
303	return loader.f.Xkb_state_serialize_layout(s.h, raw.XKB_STATE_LAYOUT_EFFECTIVE), nil
304}
305
306// ModIndexActive reports whether the modifier at idx is effectively active.
307// It does not allocate. An unknown index reports false.
308func (s *State) ModIndexActive(idx uint32) (bool, error) {
309	if s == nil {
310		return false, ErrClosed
311	}
312	s.mu.Lock()
313	defer s.mu.Unlock()
314	if s.h == 0 {
315		return false, ErrClosed
316	}
317	return loader.f.Xkb_state_mod_index_is_active(s.h, idx, raw.XKB_STATE_MODS_EFFECTIVE) > 0, nil
318}
319
320// ModNameActive reports whether the named modifier (for example
321// raw.XKB_MOD_NAME_CTRL) is effectively active. Names up to 63 bytes do not
322// allocate. An unknown name reports false.
323func (s *State) ModNameActive(name string) (bool, error) {
324	if e := validString(name); e != nil {
325		return false, e
326	}
327	if s == nil {
328		return false, ErrClosed
329	}
330	s.mu.Lock()
331	defer s.mu.Unlock()
332	if s.h == 0 {
333		return false, ErrClosed
334	}
335	var b []byte
336	if len(name) < len(s.name) {
337		// Heap-resident scratch owned by State and guarded by s.mu.
338		b = s.name[:len(name)+1]
339		copy(b, name)
340		b[len(name)] = 0
341	} else {
342		b = cstr(name)
343	}
344	v := loader.f.Xkb_state_mod_name_is_active(s.h, ptr(b), raw.XKB_STATE_MODS_EFFECTIVE)
345	runtime.KeepAlive(b)
346	return v > 0, nil
347}
348
349func (s *State) Mods() (uint32, error) {
350	if s == nil {
351		return 0, ErrClosed
352	}
353	s.mu.Lock()
354	defer s.mu.Unlock()
355	if s.h == 0 {
356		return 0, ErrClosed
357	}
358	return loader.f.Xkb_state_serialize_mods(s.h, raw.XKB_STATE_MODS_EFFECTIVE), nil
359}
360func (s *State) KeySym(keycode uint32) (uint32, error) {
361	if s == nil {
362		return 0, ErrClosed
363	}
364	s.mu.Lock()
365	defer s.mu.Unlock()
366	if s.h == 0 {
367		return 0, ErrClosed
368	}
369	return loader.f.Xkb_state_key_get_one_sym(s.h, keycode), nil
370}
371
372// sizedUTF8 probes the required byte count (excluding NUL) and retries if it grows.
373func sizedUTF8(call func(unsafe.Pointer, uintptr) int32) (string, error) {
374	n := call(nil, 0)
375	if n < 0 {
376		return "", errors.New("xkbcommon: UTF-8 conversion failed")
377	}
378	if n == 0 {
379		return "", nil
380	}
381	for attempt := 0; attempt < 3; attempt++ {
382		if n > 1<<20 {
383			return "", errors.New("xkbcommon: UTF-8 result exceeds 1 MiB")
384		}
385		b := make([]byte, int(n)+1)
386		got := call(ptr(b), uintptr(len(b)))
387		runtime.KeepAlive(b)
388		if got < 0 {
389			return "", errors.New("xkbcommon: UTF-8 conversion failed")
390		}
391		if int(got) < len(b) {
392			return string(b[:got]), nil
393		}
394		n = got
395	}
396	return "", errors.New("xkbcommon: UTF-8 result changed during sizing")
397}
398
399// MaxUTF8Buffer is the byte API's maximum scratch length, including the
400// native trailing NUL. Thus a full result can contain at most 511 payload bytes.
401const MaxUTF8Buffer = 512
402
403// ErrUTF8Buffer reports a destination buffer whose length is outside
404// 1..MaxUTF8Buffer.
405var ErrUTF8Buffer = fmt.Errorf("xkbcommon: UTF-8 buffer length must be 1..%d", MaxUTF8Buffer)
406
407// utf8Into never creates a string or keeps the caller's storage. Native writes
408// are synchronous; the Syscall uintptr conversion keeps dst's backing array
409// alive and unmoved for the duration of the call (go:uintptrescapes).
410// Short buffers are wiped rather than exposing truncated UTF-8 or credentials.
411// Callers clear dst before any early return.
412func utf8Into(dst []byte, call func(unsafe.Pointer, uintptr) int32) (int, error) {
413	if len(dst) < 1 || len(dst) > MaxUTF8Buffer {
414		return 0, ErrUTF8Buffer
415	}
416	n := call(ptr(dst), uintptr(len(dst)))
417	runtime.KeepAlive(dst)
418	if n < 0 {
419		clear(dst)
420		return 0, errors.New("xkbcommon: UTF-8 conversion failed")
421	}
422	if int(n) >= len(dst) {
423		clear(dst)
424		return int(n), io.ErrShortBuffer
425	}
426	clear(dst[n:])
427	return int(n), nil
428}
429
430// UTF8Into writes into caller-owned mutable storage without constructing a
431// string. dst is passed to C through a uintptr-escaping call, so its backing
432// array lives on the heap (a stack array sliced into dst is moved there once). n excludes NUL; on io.ErrShortBuffer n is the required payload size,
433// all of dst is cleared, and the caller needs n+1 bytes. dst must have length
434// 1..MaxUTF8Buffer. No storage is retained; the caller owns wiping after use.
435func (s *State) UTF8Into(keycode uint32, dst []byte) (int, error) {
436	clear(dst)
437	if s == nil {
438		return 0, ErrClosed
439	}
440	s.mu.Lock()
441	defer s.mu.Unlock()
442	if s.h == 0 {
443		return 0, ErrClosed
444	}
445	return utf8Into(dst, func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_state_key_get_utf8(s.h, keycode, p, n) })
446}
447
448func (s *State) UTF8(keycode uint32) (string, error) {
449	if s == nil {
450		return "", ErrClosed
451	}
452	s.mu.Lock()
453	defer s.mu.Unlock()
454	if s.h == 0 {
455		return "", ErrClosed
456	}
457	return sizedUTF8(func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_state_key_get_utf8(s.h, keycode, p, n) })
458}
459func KeysymName(sym uint32) (string, error) {
460	if e := Available(); e != nil {
461		return "", e
462	}
463	return sizedUTF8(func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) })
464}
465
466// KeysymNameInto writes the keysym name into dst without allocating, with the
467// same bounds and short-buffer semantics as State.UTF8Into.
468func KeysymNameInto(sym uint32, dst []byte) (int, error) {
469	clear(dst)
470	if e := Available(); e != nil {
471		return 0, e
472	}
473	return utf8Into(dst, func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) })
474}
475func KeysymFromName(name string) (uint32, error) {
476	if e := Available(); e != nil {
477		return 0, e
478	}
479	if e := validString(name); e != nil {
480		return 0, e
481	}
482	b := cstr(name)
483	v := loader.f.Xkb_keysym_from_name(ptr(b), 0)
484	runtime.KeepAlive(b)
485	return v, nil
486}
487
488type ComposeTable struct {
489	mu         sync.Mutex
490	h, context uintptr
491}
492
493func (c *Context) NewComposeTable(locale string) (*ComposeTable, error) {
494	if e := validString(locale); e != nil {
495		return nil, e
496	}
497	ctx, e := c.retain()
498	if e != nil {
499		return nil, e
500	}
501	b := cstr(locale)
502	h := loader.f.Xkb_compose_table_new_from_locale(ctx, ptr(b), 0)
503	runtime.KeepAlive(b)
504	if h == 0 {
505		releaseContext(ctx)
506		return nil, fmt.Errorf("xkbcommon: no compose table for %q", locale)
507	}
508	return &ComposeTable{h: h, context: ctx}, nil
509}
510func (t *ComposeTable) Close() error {
511	if t == nil {
512		return nil
513	}
514	t.mu.Lock()
515	h, ctx := t.h, t.context
516	t.h = 0
517	t.context = 0
518	if h != 0 {
519		loader.f.Xkb_compose_table_unref(h)
520	}
521	t.mu.Unlock()
522	if ctx != 0 {
523		releaseContext(ctx)
524	}
525	return nil
526}
527func (t *ComposeTable) NewState() (*ComposeState, error) {
528	if t == nil {
529		return nil, ErrClosed
530	}
531	t.mu.Lock()
532	if t.h == 0 {
533		t.mu.Unlock()
534		return nil, ErrClosed
535	}
536	table := loader.f.Xkb_compose_table_ref(t.h)
537	t.mu.Unlock()
538	h := loader.f.Xkb_compose_state_new(table, 0)
539	if h == 0 {
540		loader.f.Xkb_compose_table_unref(table)
541		return nil, errors.New("xkbcommon: create compose state failed")
542	}
543	return &ComposeState{h: h, table: table}, nil
544}
545
546type ComposeState struct {
547	mu       sync.Mutex
548	h, table uintptr
549}
550
551func (s *ComposeState) Close() error {
552	if s == nil {
553		return nil
554	}
555	s.mu.Lock()
556	h, t := s.h, s.table
557	s.h = 0
558	s.table = 0
559	if h != 0 {
560		loader.f.Xkb_compose_state_unref(h)
561	}
562	s.mu.Unlock()
563	if t != 0 {
564		loader.f.Xkb_compose_table_unref(t)
565	}
566	return nil
567}
568func (s *ComposeState) Feed(sym uint32) (uint32, error) {
569	if s == nil {
570		return 0, ErrClosed
571	}
572	s.mu.Lock()
573	defer s.mu.Unlock()
574	if s.h == 0 {
575		return 0, ErrClosed
576	}
577	return loader.f.Xkb_compose_state_feed(s.h, sym), nil
578}
579func (s *ComposeState) Status() (uint32, error) {
580	if s == nil {
581		return 0, ErrClosed
582	}
583	s.mu.Lock()
584	defer s.mu.Unlock()
585	if s.h == 0 {
586		return 0, ErrClosed
587	}
588	return loader.f.Xkb_compose_state_get_status(s.h), nil
589}
590
591// UTF8Into is the compose counterpart of State.UTF8Into, with identical
592// bounds, ownership, NUL and short-buffer semantics. It does not reset compose.
593func (s *ComposeState) UTF8Into(dst []byte) (int, error) {
594	clear(dst)
595	if s == nil {
596		return 0, ErrClosed
597	}
598	s.mu.Lock()
599	defer s.mu.Unlock()
600	if s.h == 0 {
601		return 0, ErrClosed
602	}
603	return utf8Into(dst, func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_compose_state_get_utf8(s.h, p, n) })
604}
605
606func (s *ComposeState) UTF8() (string, error) {
607	if s == nil {
608		return "", ErrClosed
609	}
610	s.mu.Lock()
611	defer s.mu.Unlock()
612	if s.h == 0 {
613		return "", ErrClosed
614	}
615	return sizedUTF8(func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_compose_state_get_utf8(s.h, p, n) })
616}
617func (s *ComposeState) KeySym() (uint32, error) {
618	if s == nil {
619		return 0, ErrClosed
620	}
621	s.mu.Lock()
622	defer s.mu.Unlock()
623	if s.h == 0 {
624		return 0, ErrClosed
625	}
626	return loader.f.Xkb_compose_state_get_one_sym(s.h), nil
627}
628func (s *ComposeState) Reset() error {
629	if s == nil {
630		return ErrClosed
631	}
632	s.mu.Lock()
633	defer s.mu.Unlock()
634	if s.h == 0 {
635		return ErrClosed
636	}
637	loader.f.Xkb_compose_state_reset(s.h)
638	return nil
639}

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 1

Tags 2

↑↓ moveenter openesc close