feat: allocation-free modifier, key-repeat and keysym-name helpers
Adds State.ModNameActive, State.ModIndexActive, Keymap.KeyRepeats and KeysymNameInto, plus AllocsPerRun tests asserting 0 allocations on the per-key hot path. Existing API is unchanged.
- commit
- e01c3110a9bde6a713910707630480f160a2786b
- author
- brice · · 2026-10-01 18:44 UTC
- parent
- 26c26f2
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +192 -2
M README.md +6 -2
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | # purego-xkbcommon | |
| 2 | 2 | ||
| 3 | - | Go 1.27 bindings for Linux `libxkbcommon.so.0`, without cgo or X11. The safe package owns native references; the `raw` package exposes generated ABI constants, types, and function registrations. Install libxkbcommon at runtime and xkeyboard-config for rules-based keymaps. The loader reports missing libraries and required symbols via `Available`. | |
| 3 | + | Go 1.27 bindings for Linux `libxkbcommon.so.0`, without cgo or X11. The safe package owns native references; the `raw` package exposes generated ABI constants, types, and function bindings. Install libxkbcommon at runtime and xkeyboard-config for rules-based keymaps. The loader reports missing libraries and required symbols via `Available`. | |
| 4 | 4 | ||
| 5 | 5 | ```go | |
| 6 | 6 | ctx, err := xkbcommon.NewContext() | |
| @@ -19,4 +19,8 @@ Use `NewKeymapFD(fd, size)` for Wayland `xkb_v1` keymaps; it copies exactly `siz | |||
| 19 | 19 | ||
| 20 | 20 | `State.UTF8Into` and `ComposeState.UTF8Into` write into a caller-owned buffer of 1..`MaxUTF8Buffer` bytes and create no Go string. On `io.ErrShortBuffer` the whole buffer is wiped and n+1 bytes are needed; the caller wipes the buffer after use. | |
| 21 | 21 | ||
| 22 | - | Pinned upstream headers and licenses are in `upstream/`; `upstream/METADATA` and `SHA256SUMS` identify their exact source. Run `GOWORK=off go generate ./...` to regenerate `raw/generated.go`; consumers do not run generation. The generator checks the selected ABI declarations against the headers. `github.com/bnema/purego v0.13.0-bnema.1` matches purego-vulkan and supports `Dlopen`, `Dlsym`, and `RegisterFunc` with cgo disabled. | |
| 22 | + | Pinned upstream headers and licenses are in `upstream/`; `upstream/METADATA` and `SHA256SUMS` identify their exact source. Run `GOWORK=off go generate ./...` to regenerate `raw/generated.go`; consumers do not run generation. The generator checks the selected ABI declarations against the headers. `github.com/bnema/purego v0.13.0-bnema.1` matches purego-vulkan and supports `Dlopen`, `Dlsym`, and the fixed-arity `Syscall6`/`Syscall15` with cgo disabled. | |
| 23 | + | ||
| 24 | + | ## Allocation-free hot path | |
| 25 | + | ||
| 26 | + | `raw.Funcs` holds symbol addresses resolved once by `Available`; every generated method calls `purego.Syscall6` (or `Syscall15` for more than six arguments) directly, so no reflection or return boxing happens per call. Per-key calls — `State.KeySym`, `State.UTF8Into`, `State.UpdateMask`, `State.Layout`, `State.Mods`, `State.ModIndexActive`, `State.ModNameActive`, `Keymap.KeyRepeats`, `KeysymNameInto` and the `ComposeState` `Feed`/`Status`/`KeySym`/`UTF8Into`/`Reset` methods — perform 0 allocations (asserted by `TestHotPathAllocs`). The string-returning `UTF8` and `KeysymName` still allocate their result. | |
| @@ -1,6 +1,6 @@ | |||||
| 1 | # purego-xkbcommon | 1 | # purego-xkbcommon | ||
| 2 | 2 | ||||
| 3 | - | Go 1.27 bindings for Linux `libxkbcommon.so.0`, without cgo or X11. The safe package owns native references; the `raw` package exposes generated ABI constants, types, and function registrations. Install libxkbcommon at runtime and xkeyboard-config for rules-based keymaps. The loader reports missing libraries and required symbols via `Available`. | 3 | + | Go 1.27 bindings for Linux `libxkbcommon.so.0`, without cgo or X11. The safe package owns native references; the `raw` package exposes generated ABI constants, types, and function bindings. Install libxkbcommon at runtime and xkeyboard-config for rules-based keymaps. The loader reports missing libraries and required symbols via `Available`. |
| 4 | 4 | ||||
| 5 | ```go | 5 | ```go | ||
| 6 | ctx, err := xkbcommon.NewContext() | 6 | ctx, err := xkbcommon.NewContext() | ||
| @@ -19,4 +19,8 @@ Use `NewKeymapFD(fd, size)` for Wayland `xkb_v1` keymaps; it copies exactly `siz | |||||
| 19 | 19 | ||||
| 20 | `State.UTF8Into` and `ComposeState.UTF8Into` write into a caller-owned buffer of 1..`MaxUTF8Buffer` bytes and create no Go string. On `io.ErrShortBuffer` the whole buffer is wiped and n+1 bytes are needed; the caller wipes the buffer after use. | 20 | `State.UTF8Into` and `ComposeState.UTF8Into` write into a caller-owned buffer of 1..`MaxUTF8Buffer` bytes and create no Go string. On `io.ErrShortBuffer` the whole buffer is wiped and n+1 bytes are needed; the caller wipes the buffer after use. | ||
| 21 | 21 | ||||
| 22 | - | Pinned upstream headers and licenses are in `upstream/`; `upstream/METADATA` and `SHA256SUMS` identify their exact source. Run `GOWORK=off go generate ./...` to regenerate `raw/generated.go`; consumers do not run generation. The generator checks the selected ABI declarations against the headers. `github.com/bnema/purego v0.13.0-bnema.1` matches purego-vulkan and supports `Dlopen`, `Dlsym`, and `RegisterFunc` with cgo disabled. | 22 | + | Pinned upstream headers and licenses are in `upstream/`; `upstream/METADATA` and `SHA256SUMS` identify their exact source. Run `GOWORK=off go generate ./...` to regenerate `raw/generated.go`; consumers do not run generation. The generator checks the selected ABI declarations against the headers. `github.com/bnema/purego v0.13.0-bnema.1` matches purego-vulkan and supports `Dlopen`, `Dlsym`, and the fixed-arity `Syscall6`/`Syscall15` with cgo disabled. |
| 23 | + | ||||
| 24 | + | ## Allocation-free hot path | |||
| 25 | + | ||||
| 26 | + | `raw.Funcs` holds symbol addresses resolved once by `Available`; every generated method calls `purego.Syscall6` (or `Syscall15` for more than six arguments) directly, so no reflection or return boxing happens per call. Per-key calls — `State.KeySym`, `State.UTF8Into`, `State.UpdateMask`, `State.Layout`, `State.Mods`, `State.ModIndexActive`, `State.ModNameActive`, `Keymap.KeyRepeats`, `KeysymNameInto` and the `ComposeState` `Feed`/`Status`/`KeySym`/`UTF8Into`/`Reset` methods — perform 0 allocations (asserted by `TestHotPathAllocs`). The string-returning `UTF8` and `KeysymName` still allocate their result. | |||
A alloc_test.go +120 -0
| @@ -0,0 +1,120 @@ | |||
| 1 | + | package xkbcommon | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "os" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/purego-xkbcommon/raw" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // fdKeymap compiles a pinned Wayland-format keymap from testdata. | |
| 11 | + | func fdKeymap(t *testing.T, c *Context, layout string) *Keymap { | |
| 12 | + | t.Helper() | |
| 13 | + | text, e := os.ReadFile("testdata/keymaps/" + layout + ".xkb") | |
| 14 | + | if e != nil { | |
| 15 | + | t.Fatal(e) | |
| 16 | + | } | |
| 17 | + | f := keymapFile(t, append(text, 0)) | |
| 18 | + | k, e := c.NewKeymapFD(int(f.Fd()), len(text)+1) | |
| 19 | + | if e != nil { | |
| 20 | + | t.Fatal(e) | |
| 21 | + | } | |
| 22 | + | t.Cleanup(func() { k.Close() }) | |
| 23 | + | return k | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | func assertNoAllocs(t *testing.T, name string, fn func()) { | |
| 27 | + | t.Helper() | |
| 28 | + | fn() // warm up lazy runtime state | |
| 29 | + | if n := testing.AllocsPerRun(200, fn); n != 0 { | |
| 30 | + | t.Errorf("%s: %v allocs/op, want 0", name, n) | |
| 31 | + | } else { | |
| 32 | + | t.Logf("%s: 0 allocs/op", name) | |
| 33 | + | } | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func TestHotPathAllocs(t *testing.T) { | |
| 37 | + | c := live(t) | |
| 38 | + | k := fdKeymap(t, c, "fr") | |
| 39 | + | s := state(t, k) | |
| 40 | + | code := WaylandKeycode(16) | |
| 41 | + | var buf [MaxUTF8Buffer]byte | |
| 42 | + | var fail error | |
| 43 | + | check := func(e error) { | |
| 44 | + | if e != nil { | |
| 45 | + | fail = e | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | table, e := c.NewComposeTable("en_US.UTF-8") | |
| 50 | + | if e != nil { | |
| 51 | + | t.Fatal(e) | |
| 52 | + | } | |
| 53 | + | defer table.Close() | |
| 54 | + | compose, e := table.NewState() | |
| 55 | + | if e != nil { | |
| 56 | + | t.Fatal(e) | |
| 57 | + | } | |
| 58 | + | defer compose.Close() | |
| 59 | + | ||
| 60 | + | assertNoAllocs(t, "State.KeySym", func() { _, e := s.KeySym(code); check(e) }) | |
| 61 | + | assertNoAllocs(t, "State.UTF8Into", func() { _, e := s.UTF8Into(code, buf[:]); check(e) }) | |
| 62 | + | assertNoAllocs(t, "State.UpdateMask", func() { _, e := s.UpdateMask(1, 0, 2, 0, 0, 0); check(e) }) | |
| 63 | + | assertNoAllocs(t, "State.Layout", func() { _, e := s.Layout(); check(e) }) | |
| 64 | + | assertNoAllocs(t, "State.Mods", func() { _, e := s.Mods(); check(e) }) | |
| 65 | + | assertNoAllocs(t, "State.ModIndexActive", func() { _, e := s.ModIndexActive(0); check(e) }) | |
| 66 | + | assertNoAllocs(t, "State.ModNameActive", func() { _, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); check(e) }) | |
| 67 | + | assertNoAllocs(t, "Keymap.KeyRepeats", func() { _, e := k.KeyRepeats(code); check(e) }) | |
| 68 | + | assertNoAllocs(t, "KeysymNameInto", func() { _, e := KeysymNameInto(raw.XKB_KEY_a, buf[:]); check(e) }) | |
| 69 | + | assertNoAllocs(t, "compose key sequence", func() { | |
| 70 | + | _, e := compose.Feed(raw.XKB_KEY_dead_circumflex) | |
| 71 | + | check(e) | |
| 72 | + | _, e = compose.Feed(raw.XKB_KEY_e) | |
| 73 | + | check(e) | |
| 74 | + | _, e = compose.Status() | |
| 75 | + | check(e) | |
| 76 | + | _, e = compose.KeySym() | |
| 77 | + | check(e) | |
| 78 | + | _, e = compose.UTF8Into(buf[:]) | |
| 79 | + | check(e) | |
| 80 | + | check(compose.Reset()) | |
| 81 | + | }) | |
| 82 | + | if fail != nil { | |
| 83 | + | t.Fatal(fail) | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | // TestHotPathValues guards the fixed-arity dispatch against argument/return | |
| 88 | + | // conversion mistakes. | |
| 89 | + | func TestHotPathValues(t *testing.T) { | |
| 90 | + | c := live(t) | |
| 91 | + | k := fdKeymap(t, c, "us") | |
| 92 | + | s := state(t, k) | |
| 93 | + | if r, e := k.KeyRepeats(WaylandKeycode(30)); e != nil || !r { | |
| 94 | + | t.Fatalf("KeyRepeats(a) = %v, %v", r, e) | |
| 95 | + | } | |
| 96 | + | ctrl, e := k.ModIndex(raw.XKB_MOD_NAME_CTRL) | |
| 97 | + | if e != nil { | |
| 98 | + | t.Fatal(e) | |
| 99 | + | } | |
| 100 | + | if a, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); e != nil || a { | |
| 101 | + | t.Fatalf("ctrl active before update: %v %v", a, e) | |
| 102 | + | } | |
| 103 | + | if _, e := s.UpdateMask(1<<ctrl, 0, 0, 0, 0, 0); e != nil { | |
| 104 | + | t.Fatal(e) | |
| 105 | + | } | |
| 106 | + | if a, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); e != nil || !a { | |
| 107 | + | t.Fatalf("ctrl name active: %v %v", a, e) | |
| 108 | + | } | |
| 109 | + | if a, e := s.ModIndexActive(ctrl); e != nil || !a { | |
| 110 | + | t.Fatalf("ctrl index active: %v %v", a, e) | |
| 111 | + | } | |
| 112 | + | if a, e := s.ModNameActive("NoSuchModifier"); e != nil || a { | |
| 113 | + | t.Fatalf("unknown modifier active: %v %v", a, e) | |
| 114 | + | } | |
| 115 | + | var buf [16]byte | |
| 116 | + | n, e := KeysymNameInto(raw.XKB_KEY_EuroSign, buf[:]) | |
| 117 | + | if e != nil || string(buf[:n]) != "EuroSign" { | |
| 118 | + | t.Fatalf("KeysymNameInto = %q %v", buf[:n], e) | |
| 119 | + | } | |
| 120 | + | } | |
| @@ -0,0 +1,120 @@ | |||||
| 1 | + | package xkbcommon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "os" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/purego-xkbcommon/raw" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // fdKeymap compiles a pinned Wayland-format keymap from testdata. | |||
| 11 | + | func fdKeymap(t *testing.T, c *Context, layout string) *Keymap { | |||
| 12 | + | t.Helper() | |||
| 13 | + | text, e := os.ReadFile("testdata/keymaps/" + layout + ".xkb") | |||
| 14 | + | if e != nil { | |||
| 15 | + | t.Fatal(e) | |||
| 16 | + | } | |||
| 17 | + | f := keymapFile(t, append(text, 0)) | |||
| 18 | + | k, e := c.NewKeymapFD(int(f.Fd()), len(text)+1) | |||
| 19 | + | if e != nil { | |||
| 20 | + | t.Fatal(e) | |||
| 21 | + | } | |||
| 22 | + | t.Cleanup(func() { k.Close() }) | |||
| 23 | + | return k | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | func assertNoAllocs(t *testing.T, name string, fn func()) { | |||
| 27 | + | t.Helper() | |||
| 28 | + | fn() // warm up lazy runtime state | |||
| 29 | + | if n := testing.AllocsPerRun(200, fn); n != 0 { | |||
| 30 | + | t.Errorf("%s: %v allocs/op, want 0", name, n) | |||
| 31 | + | } else { | |||
| 32 | + | t.Logf("%s: 0 allocs/op", name) | |||
| 33 | + | } | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | func TestHotPathAllocs(t *testing.T) { | |||
| 37 | + | c := live(t) | |||
| 38 | + | k := fdKeymap(t, c, "fr") | |||
| 39 | + | s := state(t, k) | |||
| 40 | + | code := WaylandKeycode(16) | |||
| 41 | + | var buf [MaxUTF8Buffer]byte | |||
| 42 | + | var fail error | |||
| 43 | + | check := func(e error) { | |||
| 44 | + | if e != nil { | |||
| 45 | + | fail = e | |||
| 46 | + | } | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | table, e := c.NewComposeTable("en_US.UTF-8") | |||
| 50 | + | if e != nil { | |||
| 51 | + | t.Fatal(e) | |||
| 52 | + | } | |||
| 53 | + | defer table.Close() | |||
| 54 | + | compose, e := table.NewState() | |||
| 55 | + | if e != nil { | |||
| 56 | + | t.Fatal(e) | |||
| 57 | + | } | |||
| 58 | + | defer compose.Close() | |||
| 59 | + | ||||
| 60 | + | assertNoAllocs(t, "State.KeySym", func() { _, e := s.KeySym(code); check(e) }) | |||
| 61 | + | assertNoAllocs(t, "State.UTF8Into", func() { _, e := s.UTF8Into(code, buf[:]); check(e) }) | |||
| 62 | + | assertNoAllocs(t, "State.UpdateMask", func() { _, e := s.UpdateMask(1, 0, 2, 0, 0, 0); check(e) }) | |||
| 63 | + | assertNoAllocs(t, "State.Layout", func() { _, e := s.Layout(); check(e) }) | |||
| 64 | + | assertNoAllocs(t, "State.Mods", func() { _, e := s.Mods(); check(e) }) | |||
| 65 | + | assertNoAllocs(t, "State.ModIndexActive", func() { _, e := s.ModIndexActive(0); check(e) }) | |||
| 66 | + | assertNoAllocs(t, "State.ModNameActive", func() { _, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); check(e) }) | |||
| 67 | + | assertNoAllocs(t, "Keymap.KeyRepeats", func() { _, e := k.KeyRepeats(code); check(e) }) | |||
| 68 | + | assertNoAllocs(t, "KeysymNameInto", func() { _, e := KeysymNameInto(raw.XKB_KEY_a, buf[:]); check(e) }) | |||
| 69 | + | assertNoAllocs(t, "compose key sequence", func() { | |||
| 70 | + | _, e := compose.Feed(raw.XKB_KEY_dead_circumflex) | |||
| 71 | + | check(e) | |||
| 72 | + | _, e = compose.Feed(raw.XKB_KEY_e) | |||
| 73 | + | check(e) | |||
| 74 | + | _, e = compose.Status() | |||
| 75 | + | check(e) | |||
| 76 | + | _, e = compose.KeySym() | |||
| 77 | + | check(e) | |||
| 78 | + | _, e = compose.UTF8Into(buf[:]) | |||
| 79 | + | check(e) | |||
| 80 | + | check(compose.Reset()) | |||
| 81 | + | }) | |||
| 82 | + | if fail != nil { | |||
| 83 | + | t.Fatal(fail) | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | // TestHotPathValues guards the fixed-arity dispatch against argument/return | |||
| 88 | + | // conversion mistakes. | |||
| 89 | + | func TestHotPathValues(t *testing.T) { | |||
| 90 | + | c := live(t) | |||
| 91 | + | k := fdKeymap(t, c, "us") | |||
| 92 | + | s := state(t, k) | |||
| 93 | + | if r, e := k.KeyRepeats(WaylandKeycode(30)); e != nil || !r { | |||
| 94 | + | t.Fatalf("KeyRepeats(a) = %v, %v", r, e) | |||
| 95 | + | } | |||
| 96 | + | ctrl, e := k.ModIndex(raw.XKB_MOD_NAME_CTRL) | |||
| 97 | + | if e != nil { | |||
| 98 | + | t.Fatal(e) | |||
| 99 | + | } | |||
| 100 | + | if a, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); e != nil || a { | |||
| 101 | + | t.Fatalf("ctrl active before update: %v %v", a, e) | |||
| 102 | + | } | |||
| 103 | + | if _, e := s.UpdateMask(1<<ctrl, 0, 0, 0, 0, 0); e != nil { | |||
| 104 | + | t.Fatal(e) | |||
| 105 | + | } | |||
| 106 | + | if a, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); e != nil || !a { | |||
| 107 | + | t.Fatalf("ctrl name active: %v %v", a, e) | |||
| 108 | + | } | |||
| 109 | + | if a, e := s.ModIndexActive(ctrl); e != nil || !a { | |||
| 110 | + | t.Fatalf("ctrl index active: %v %v", a, e) | |||
| 111 | + | } | |||
| 112 | + | if a, e := s.ModNameActive("NoSuchModifier"); e != nil || a { | |||
| 113 | + | t.Fatalf("unknown modifier active: %v %v", a, e) | |||
| 114 | + | } | |||
| 115 | + | var buf [16]byte | |||
| 116 | + | n, e := KeysymNameInto(raw.XKB_KEY_EuroSign, buf[:]) | |||
| 117 | + | if e != nil || string(buf[:n]) != "EuroSign" { | |||
| 118 | + | t.Fatalf("KeysymNameInto = %q %v", buf[:n], e) | |||
| 119 | + | } | |||
| 120 | + | } | |||
M xkbcommon.go +66 -0
| @@ -213,6 +213,19 @@ func (k *Keymap) ModIndex(name string) (uint32, error) { | |||
| 213 | 213 | runtime.KeepAlive(b) | |
| 214 | 214 | return v, nil | |
| 215 | 215 | } | |
| 216 | + | ||
| 217 | + | // KeyRepeats reports whether the key repeats when held. It does not allocate. | |
| 218 | + | func (k *Keymap) KeyRepeats(keycode uint32) (bool, error) { | |
| 219 | + | if k == nil { | |
| 220 | + | return false, ErrClosed | |
| 221 | + | } | |
| 222 | + | k.mu.Lock() | |
| 223 | + | defer k.mu.Unlock() | |
| 224 | + | if k.h == 0 { | |
| 225 | + | return false, ErrClosed | |
| 226 | + | } | |
| 227 | + | return loader.f.Xkb_keymap_key_repeats(k.h, keycode) > 0, nil | |
| 228 | + | } | |
| 216 | 229 | func (k *Keymap) LayoutCount() (uint32, error) { | |
| 217 | 230 | if k == nil { | |
| 218 | 231 | return 0, ErrClosed | |
| @@ -284,6 +297,49 @@ func (s *State) Layout() (uint32, error) { | |||
| 284 | 297 | } | |
| 285 | 298 | return loader.f.Xkb_state_serialize_layout(s.h, raw.XKB_STATE_LAYOUT_EFFECTIVE), nil | |
| 286 | 299 | } | |
| 300 | + | ||
| 301 | + | // ModIndexActive reports whether the modifier at idx is effectively active. | |
| 302 | + | // It does not allocate. An unknown index reports false. | |
| 303 | + | func (s *State) ModIndexActive(idx uint32) (bool, error) { | |
| 304 | + | if s == nil { | |
| 305 | + | return false, ErrClosed | |
| 306 | + | } | |
| 307 | + | s.mu.Lock() | |
| 308 | + | defer s.mu.Unlock() | |
| 309 | + | if s.h == 0 { | |
| 310 | + | return false, ErrClosed | |
| 311 | + | } | |
| 312 | + | return loader.f.Xkb_state_mod_index_is_active(s.h, idx, raw.XKB_STATE_MODS_EFFECTIVE) > 0, nil | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | // ModNameActive reports whether the named modifier (for example | |
| 316 | + | // raw.XKB_MOD_NAME_CTRL) is effectively active. Names up to 63 bytes do not | |
| 317 | + | // allocate. An unknown name reports false. | |
| 318 | + | func (s *State) ModNameActive(name string) (bool, error) { | |
| 319 | + | if e := validString(name); e != nil { | |
| 320 | + | return false, e | |
| 321 | + | } | |
| 322 | + | if s == nil { | |
| 323 | + | return false, ErrClosed | |
| 324 | + | } | |
| 325 | + | var small [64]byte | |
| 326 | + | var b []byte | |
| 327 | + | if len(name) < len(small) { | |
| 328 | + | copy(small[:], name) | |
| 329 | + | b = small[:len(name)+1] | |
| 330 | + | } else { | |
| 331 | + | b = cstr(name) | |
| 332 | + | } | |
| 333 | + | s.mu.Lock() | |
| 334 | + | defer s.mu.Unlock() | |
| 335 | + | if s.h == 0 { | |
| 336 | + | return false, ErrClosed | |
| 337 | + | } | |
| 338 | + | v := loader.f.Xkb_state_mod_name_is_active(s.h, ptr(b), raw.XKB_STATE_MODS_EFFECTIVE) | |
| 339 | + | runtime.KeepAlive(b) | |
| 340 | + | return v > 0, nil | |
| 341 | + | } | |
| 342 | + | ||
| 287 | 343 | func (s *State) Mods() (uint32, error) { | |
| 288 | 344 | if s == nil { | |
| 289 | 345 | return 0, ErrClosed | |
| @@ -401,6 +457,16 @@ func KeysymName(sym uint32) (string, error) { | |||
| 401 | 457 | } | |
| 402 | 458 | return sizedUTF8(func(p, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) }) | |
| 403 | 459 | } | |
| 460 | + | ||
| 461 | + | // KeysymNameInto writes the keysym name into dst without allocating, with the | |
| 462 | + | // same bounds and short-buffer semantics as State.UTF8Into. | |
| 463 | + | func KeysymNameInto(sym uint32, dst []byte) (int, error) { | |
| 464 | + | clear(dst) | |
| 465 | + | if e := Available(); e != nil { | |
| 466 | + | return 0, e | |
| 467 | + | } | |
| 468 | + | return utf8Into(dst, func(p, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) }) | |
| 469 | + | } | |
| 404 | 470 | func KeysymFromName(name string) (uint32, error) { | |
| 405 | 471 | if e := Available(); e != nil { | |
| 406 | 472 | return 0, e | |
| @@ -213,6 +213,19 @@ func (k *Keymap) ModIndex(name string) (uint32, error) { | |||||
| 213 | runtime.KeepAlive(b) | 213 | runtime.KeepAlive(b) | ||
| 214 | return v, nil | 214 | return v, nil | ||
| 215 | } | 215 | } | ||
| 216 | + | ||||
| 217 | + | // KeyRepeats reports whether the key repeats when held. It does not allocate. | |||
| 218 | + | func (k *Keymap) KeyRepeats(keycode uint32) (bool, error) { | |||
| 219 | + | if k == nil { | |||
| 220 | + | return false, ErrClosed | |||
| 221 | + | } | |||
| 222 | + | k.mu.Lock() | |||
| 223 | + | defer k.mu.Unlock() | |||
| 224 | + | if k.h == 0 { | |||
| 225 | + | return false, ErrClosed | |||
| 226 | + | } | |||
| 227 | + | return loader.f.Xkb_keymap_key_repeats(k.h, keycode) > 0, nil | |||
| 228 | + | } | |||
| 216 | func (k *Keymap) LayoutCount() (uint32, error) { | 229 | func (k *Keymap) LayoutCount() (uint32, error) { | ||
| 217 | if k == nil { | 230 | if k == nil { | ||
| 218 | return 0, ErrClosed | 231 | return 0, ErrClosed | ||
| @@ -284,6 +297,49 @@ func (s *State) Layout() (uint32, error) { | |||||
| 284 | } | 297 | } | ||
| 285 | return loader.f.Xkb_state_serialize_layout(s.h, raw.XKB_STATE_LAYOUT_EFFECTIVE), nil | 298 | return loader.f.Xkb_state_serialize_layout(s.h, raw.XKB_STATE_LAYOUT_EFFECTIVE), nil | ||
| 286 | } | 299 | } | ||
| 300 | + | ||||
| 301 | + | // ModIndexActive reports whether the modifier at idx is effectively active. | |||
| 302 | + | // It does not allocate. An unknown index reports false. | |||
| 303 | + | func (s *State) ModIndexActive(idx uint32) (bool, error) { | |||
| 304 | + | if s == nil { | |||
| 305 | + | return false, ErrClosed | |||
| 306 | + | } | |||
| 307 | + | s.mu.Lock() | |||
| 308 | + | defer s.mu.Unlock() | |||
| 309 | + | if s.h == 0 { | |||
| 310 | + | return false, ErrClosed | |||
| 311 | + | } | |||
| 312 | + | return loader.f.Xkb_state_mod_index_is_active(s.h, idx, raw.XKB_STATE_MODS_EFFECTIVE) > 0, nil | |||
| 313 | + | } | |||
| 314 | + | ||||
| 315 | + | // ModNameActive reports whether the named modifier (for example | |||
| 316 | + | // raw.XKB_MOD_NAME_CTRL) is effectively active. Names up to 63 bytes do not | |||
| 317 | + | // allocate. An unknown name reports false. | |||
| 318 | + | func (s *State) ModNameActive(name string) (bool, error) { | |||
| 319 | + | if e := validString(name); e != nil { | |||
| 320 | + | return false, e | |||
| 321 | + | } | |||
| 322 | + | if s == nil { | |||
| 323 | + | return false, ErrClosed | |||
| 324 | + | } | |||
| 325 | + | var small [64]byte | |||
| 326 | + | var b []byte | |||
| 327 | + | if len(name) < len(small) { | |||
| 328 | + | copy(small[:], name) | |||
| 329 | + | b = small[:len(name)+1] | |||
| 330 | + | } else { | |||
| 331 | + | b = cstr(name) | |||
| 332 | + | } | |||
| 333 | + | s.mu.Lock() | |||
| 334 | + | defer s.mu.Unlock() | |||
| 335 | + | if s.h == 0 { | |||
| 336 | + | return false, ErrClosed | |||
| 337 | + | } | |||
| 338 | + | v := loader.f.Xkb_state_mod_name_is_active(s.h, ptr(b), raw.XKB_STATE_MODS_EFFECTIVE) | |||
| 339 | + | runtime.KeepAlive(b) | |||
| 340 | + | return v > 0, nil | |||
| 341 | + | } | |||
| 342 | + | ||||
| 287 | func (s *State) Mods() (uint32, error) { | 343 | func (s *State) Mods() (uint32, error) { | ||
| 288 | if s == nil { | 344 | if s == nil { | ||
| 289 | return 0, ErrClosed | 345 | return 0, ErrClosed | ||
| @@ -401,6 +457,16 @@ func KeysymName(sym uint32) (string, error) { | |||||
| 401 | } | 457 | } | ||
| 402 | return sizedUTF8(func(p, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) }) | 458 | return sizedUTF8(func(p, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) }) | ||
| 403 | } | 459 | } | ||
| 460 | + | ||||
| 461 | + | // KeysymNameInto writes the keysym name into dst without allocating, with the | |||
| 462 | + | // same bounds and short-buffer semantics as State.UTF8Into. | |||
| 463 | + | func KeysymNameInto(sym uint32, dst []byte) (int, error) { | |||
| 464 | + | clear(dst) | |||
| 465 | + | if e := Available(); e != nil { | |||
| 466 | + | return 0, e | |||
| 467 | + | } | |||
| 468 | + | return utf8Into(dst, func(p, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) }) | |||
| 469 | + | } | |||
| 404 | func KeysymFromName(name string) (uint32, error) { | 470 | func KeysymFromName(name string) (uint32, error) { | ||
| 405 | if e := Available(); e != nil { | 471 | if e := Available(); e != nil { | ||
| 406 | return 0, e | 472 | return 0, e | ||