v0.1.0 0e37717

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

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