detached e01c311

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +192 -2
M README.md +6 -2
@@ -1,6 +1,6 @@
11 # purego-xkbcommon
22
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`.
44
55 ```go
66 ctx, err := xkbcommon.NewContext()
@@ -19,4 +19,8 @@ Use `NewKeymapFD(fd, size)` for Wayland `xkb_v1` keymaps; it copies exactly `siz
1919
2020 `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.
2121
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-xkbcommon1 # 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 ```go5 ```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) {
213213 runtime.KeepAlive(b)
214214 return v, nil
215215 }
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+}
216229 func (k *Keymap) LayoutCount() (uint32, error) {
217230 if k == nil {
218231 return 0, ErrClosed
@@ -284,6 +297,49 @@ func (s *State) Layout() (uint32, error) {
284297 }
285298 return loader.f.Xkb_state_serialize_layout(s.h, raw.XKB_STATE_LAYOUT_EFFECTIVE), nil
286299 }
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+
287343 func (s *State) Mods() (uint32, error) {
288344 if s == nil {
289345 return 0, ErrClosed
@@ -401,6 +457,16 @@ func KeysymName(sym uint32) (string, error) {
401457 }
402458 return sizedUTF8(func(p, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) })
403459 }
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+}
404470 func KeysymFromName(name string) (uint32, error) {
405471 if e := Available(); e != nil {
406472 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, nil214 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, ErrClosed231 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), nil298 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, ErrClosed345 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, e472 return 0, e

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