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}