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}