detached 7219ece

perf: cache modifiers and layout per mask change; nil-safe Seat

commit
7219ecee67ffe05dcf67936c6fb43fd78edc0982
author
brice · · 2026-10-01 18:37 UTC
parent
fe9795c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +25 -14
M keyboard.go +15 -7
@@ -54,6 +54,8 @@ type keyboard struct {
5454 compose *xkb.ComposeState
5555
5656 modBits [len(modNames)]uint32 // xkb mask per Modifiers bit; 0 when absent
57+ mods Modifiers // effective modifiers and layout, refreshed when the mask changes
58+ group uint32
5759 lastMask [6]uint32
5860 haveMask bool
5961
@@ -173,6 +175,7 @@ func (k *keyboard) replace(km *xkb.Keymap) error {
173175 if k.haveMask { // the compositor's modifiers survive a keymap change
174176 _, _ = s.UpdateMask(k.lastMask[0], k.lastMask[1], k.lastMask[2], k.lastMask[3], k.lastMask[4], k.lastMask[5])
175177 }
178+ k.refresh()
176179 return nil
177180 }
178181
@@ -183,9 +186,17 @@ func (k *keyboard) setMask(depressed, latched, locked, group uint32) error {
183186 return nil
184187 }
185188 _, err := k.state.UpdateMask(depressed, latched, locked, 0, 0, group)
189+ k.refresh()
186190 return err
187191 }
188192
193+// refresh recomputes the cached modifiers and layout from the xkb state. Both
194+// only change when the mask does, so keys do not query them.
195+func (k *keyboard) refresh() {
196+ k.mods, k.group = k.queryModifiers(), 0
197+ k.group, _ = k.state.Layout()
198+}
199+
189200 // reset drops held keys, repeat and compose state. It keeps the keymap, the
190201 // modifier mask and the focus flag.
191202 func (k *keyboard) reset() {
@@ -239,7 +250,7 @@ func (k *keyboard) expired() bool {
239250 return err == nil && n == 8
240251 }
241252
242-func (k *keyboard) modifiers() Modifiers {
253+func (k *keyboard) queryModifiers() Modifiers {
243254 mods, err := k.state.Mods()
244255 if err != nil {
245256 return 0
@@ -312,10 +323,8 @@ func (k *keyboard) release(evdev uint32, ev *KeyEvent) bool {
312323 k.disarm()
313324 }
314325 ev.Keycode, ev.Keysym, ev.Pressed = evdev, h.sym, false
315- if k.state != nil {
316- ev.Modifiers = k.modifiers()
317- ev.Group, _ = k.state.Layout()
318- }
326+ k.changed = false
327+ ev.Modifiers, ev.Group = k.mods, k.group
319328 return true
320329 }
321330
@@ -353,8 +362,7 @@ func (k *keyboard) repeat(ev *KeyEvent) (bool, error) {
353362 // the SecretBuffer, if one is set. In secret mode ev.Text stays nil.
354363 func (k *keyboard) translate(evdev, sym uint32, repeat bool, ev *KeyEvent) (textKind, error) {
355364 ev.Keycode, ev.Keysym, ev.Pressed, ev.Repeat = evdev, sym, true, repeat
356- ev.Modifiers = k.modifiers()
357- ev.Group, _ = k.state.Layout()
365+ ev.Modifiers, ev.Group = k.mods, k.group
358366 k.changed = false
359367 if k.secret != nil {
360368 w := &k.w
@@ -54,6 +54,8 @@ type keyboard struct {
54 compose *xkb.ComposeState54 compose *xkb.ComposeState
55 55
56 modBits [len(modNames)]uint32 // xkb mask per Modifiers bit; 0 when absent56 modBits [len(modNames)]uint32 // xkb mask per Modifiers bit; 0 when absent
57+ mods Modifiers // effective modifiers and layout, refreshed when the mask changes
58+ group uint32
57 lastMask [6]uint3259 lastMask [6]uint32
58 haveMask bool60 haveMask bool
59 61
@@ -173,6 +175,7 @@ func (k *keyboard) replace(km *xkb.Keymap) error {
173 if k.haveMask { // the compositor's modifiers survive a keymap change175 if k.haveMask { // the compositor's modifiers survive a keymap change
174 _, _ = s.UpdateMask(k.lastMask[0], k.lastMask[1], k.lastMask[2], k.lastMask[3], k.lastMask[4], k.lastMask[5])176 _, _ = s.UpdateMask(k.lastMask[0], k.lastMask[1], k.lastMask[2], k.lastMask[3], k.lastMask[4], k.lastMask[5])
175 }177 }
178+ k.refresh()
176 return nil179 return nil
177 }180 }
178 181
@@ -183,9 +186,17 @@ func (k *keyboard) setMask(depressed, latched, locked, group uint32) error {
183 return nil186 return nil
184 }187 }
185 _, err := k.state.UpdateMask(depressed, latched, locked, 0, 0, group)188 _, err := k.state.UpdateMask(depressed, latched, locked, 0, 0, group)
189+ k.refresh()
186 return err190 return err
187 }191 }
188 192
193+// refresh recomputes the cached modifiers and layout from the xkb state. Both
194+// only change when the mask does, so keys do not query them.
195+func (k *keyboard) refresh() {
196+ k.mods, k.group = k.queryModifiers(), 0
197+ k.group, _ = k.state.Layout()
198+}
199+
189 // reset drops held keys, repeat and compose state. It keeps the keymap, the200 // reset drops held keys, repeat and compose state. It keeps the keymap, the
190 // modifier mask and the focus flag.201 // modifier mask and the focus flag.
191 func (k *keyboard) reset() {202 func (k *keyboard) reset() {
@@ -239,7 +250,7 @@ func (k *keyboard) expired() bool {
239 return err == nil && n == 8250 return err == nil && n == 8
240 }251 }
241 252
242-func (k *keyboard) modifiers() Modifiers {253+func (k *keyboard) queryModifiers() Modifiers {
243 mods, err := k.state.Mods()254 mods, err := k.state.Mods()
244 if err != nil {255 if err != nil {
245 return 0256 return 0
@@ -312,10 +323,8 @@ func (k *keyboard) release(evdev uint32, ev *KeyEvent) bool {
312 k.disarm()323 k.disarm()
313 }324 }
314 ev.Keycode, ev.Keysym, ev.Pressed = evdev, h.sym, false325 ev.Keycode, ev.Keysym, ev.Pressed = evdev, h.sym, false
315- if k.state != nil {326+ k.changed = false
316- ev.Modifiers = k.modifiers()327+ ev.Modifiers, ev.Group = k.mods, k.group
317- ev.Group, _ = k.state.Layout()
318- }
319 return true328 return true
320 }329 }
321 330
@@ -353,8 +362,7 @@ func (k *keyboard) repeat(ev *KeyEvent) (bool, error) {
353 // the SecretBuffer, if one is set. In secret mode ev.Text stays nil.362 // the SecretBuffer, if one is set. In secret mode ev.Text stays nil.
354 func (k *keyboard) translate(evdev, sym uint32, repeat bool, ev *KeyEvent) (textKind, error) {363 func (k *keyboard) translate(evdev, sym uint32, repeat bool, ev *KeyEvent) (textKind, error) {
355 ev.Keycode, ev.Keysym, ev.Pressed, ev.Repeat = evdev, sym, true, repeat364 ev.Keycode, ev.Keysym, ev.Pressed, ev.Repeat = evdev, sym, true, repeat
356- ev.Modifiers = k.modifiers()365+ ev.Modifiers, ev.Group = k.mods, k.group
357- ev.Group, _ = k.state.Layout()
358 k.changed = false366 k.changed = false
359 if k.secret != nil {367 if k.secret != nil {
360 w := &k.w368 w := &k.w
M keyboard_test.go +5 -5
@@ -94,7 +94,7 @@ func TestKeyboardKeymapFD(t *testing.T) {
9494 _, err = f.Write(append(data, 0))
9595 require.NoError(t, err)
9696
97- k := testKeyboard(t, "", "us")
97+ k := testKeyboard(t, "C", "us")
9898 fd, err := unix.Dup(int(f.Fd()))
9999 require.NoError(t, err)
100100 require.NoError(t, k.replaceFD(fd, len(data)+1))
@@ -140,7 +140,7 @@ func pressKey2(k *keyboard, code uint32) (KeyEvent, bool) {
140140 }
141141
142142 func TestKeyboardRepeatTimer(t *testing.T) {
143- k := testKeyboard(t, "", "us")
143+ k := testKeyboard(t, "C", "us")
144144 k.setRepeat(20, 50)
145145 _, ok := pressKey(t, k, keyQ)
146146 require.True(t, ok)
@@ -167,7 +167,7 @@ func TestKeyboardRepeatTimer(t *testing.T) {
167167 }
168168
169169 func TestKeyboardRepeatTracksCurrentMask(t *testing.T) {
170- k := testKeyboard(t, "", "us")
170+ k := testKeyboard(t, "C", "us")
171171 k.setRepeat(100, 1)
172172 pressKey(t, k, keyQ)
173173 idx, err := k.keymap.ModIndex("Shift")
@@ -237,7 +237,7 @@ func TestSecretKeysNeverReachText(t *testing.T) {
237237 }
238238
239239 func TestSecretChangedFlag(t *testing.T) {
240- k := testKeyboard(t, "", "us")
240+ k := testKeyboard(t, "C", "us")
241241 buf := NewSecretBuffer(2)
242242 k.setSecret(buf)
243243 pressKey(t, k, keyQ)
@@ -251,7 +251,7 @@ func TestSecretChangedFlag(t *testing.T) {
251251 }
252252
253253 func TestSecretRepeat(t *testing.T) {
254- k := testKeyboard(t, "", "us")
254+ k := testKeyboard(t, "C", "us")
255255 buf := NewSecretBuffer(16)
256256 k.setSecret(buf)
257257 k.setRepeat(100, 1)
@@ -94,7 +94,7 @@ func TestKeyboardKeymapFD(t *testing.T) {
94 _, err = f.Write(append(data, 0))94 _, err = f.Write(append(data, 0))
95 require.NoError(t, err)95 require.NoError(t, err)
96 96
97- k := testKeyboard(t, "", "us")97+ k := testKeyboard(t, "C", "us")
98 fd, err := unix.Dup(int(f.Fd()))98 fd, err := unix.Dup(int(f.Fd()))
99 require.NoError(t, err)99 require.NoError(t, err)
100 require.NoError(t, k.replaceFD(fd, len(data)+1))100 require.NoError(t, k.replaceFD(fd, len(data)+1))
@@ -140,7 +140,7 @@ func pressKey2(k *keyboard, code uint32) (KeyEvent, bool) {
140 }140 }
141 141
142 func TestKeyboardRepeatTimer(t *testing.T) {142 func TestKeyboardRepeatTimer(t *testing.T) {
143- k := testKeyboard(t, "", "us")143+ k := testKeyboard(t, "C", "us")
144 k.setRepeat(20, 50)144 k.setRepeat(20, 50)
145 _, ok := pressKey(t, k, keyQ)145 _, ok := pressKey(t, k, keyQ)
146 require.True(t, ok)146 require.True(t, ok)
@@ -167,7 +167,7 @@ func TestKeyboardRepeatTimer(t *testing.T) {
167 }167 }
168 168
169 func TestKeyboardRepeatTracksCurrentMask(t *testing.T) {169 func TestKeyboardRepeatTracksCurrentMask(t *testing.T) {
170- k := testKeyboard(t, "", "us")170+ k := testKeyboard(t, "C", "us")
171 k.setRepeat(100, 1)171 k.setRepeat(100, 1)
172 pressKey(t, k, keyQ)172 pressKey(t, k, keyQ)
173 idx, err := k.keymap.ModIndex("Shift")173 idx, err := k.keymap.ModIndex("Shift")
@@ -237,7 +237,7 @@ func TestSecretKeysNeverReachText(t *testing.T) {
237 }237 }
238 238
239 func TestSecretChangedFlag(t *testing.T) {239 func TestSecretChangedFlag(t *testing.T) {
240- k := testKeyboard(t, "", "us")240+ k := testKeyboard(t, "C", "us")
241 buf := NewSecretBuffer(2)241 buf := NewSecretBuffer(2)
242 k.setSecret(buf)242 k.setSecret(buf)
243 pressKey(t, k, keyQ)243 pressKey(t, k, keyQ)
@@ -251,7 +251,7 @@ func TestSecretChangedFlag(t *testing.T) {
251 }251 }
252 252
253 func TestSecretRepeat(t *testing.T) {253 func TestSecretRepeat(t *testing.T) {
254- k := testKeyboard(t, "", "us")254+ k := testKeyboard(t, "C", "us")
255 buf := NewSecretBuffer(16)255 buf := NewSecretBuffer(16)
256 k.setSecret(buf)256 k.setSecret(buf)
257 k.setRepeat(100, 1)257 k.setRepeat(100, 1)
M seat.go +5 -2
@@ -137,7 +137,7 @@ type Seat struct {
137137 shape CursorShape
138138 }
139139
140-// Seat returns the connection's seat, binding wl_seat on first use. The seat
140+// Seat returns the connection's seat (nil after Close), binding wl_seat on first use. The seat
141141 // is also bound when the first surface is created. Input starts flowing to the
142142 // Handler once the compositor announces the capabilities.
143143 func (c *Conn) Seat() *Seat {
@@ -187,6 +187,9 @@ func (c *Conn) ensureSeat() {
187187 // KeyEvent without text. Installing or removing cancels held keys and repeat.
188188 // The previous buffer is not wiped: that is the caller's call.
189189 func (s *Seat) SetSecret(b *SecretBuffer) {
190+ if s == nil {
191+ return
192+ }
190193 s.secret = b
191194 if s.kb != nil {
192195 s.kb.setSecret(b)
@@ -197,7 +200,7 @@ func (s *Seat) SetSecret(b *SecretBuffer) {
197200 // when the compositor has no cursor-shape support, the pointer is outside the
198201 // connection's surfaces, or shape is unchanged since the last enter.
199202 func (s *Seat) SetCursor(shape CursorShape) error {
200- if s.c.closed {
203+ if s == nil || s.c.closed {
201204 return ErrClosed
202205 }
203206 limit := CursorZoomOut
@@ -137,7 +137,7 @@ type Seat struct {
137 shape CursorShape137 shape CursorShape
138 }138 }
139 139
140-// Seat returns the connection's seat, binding wl_seat on first use. The seat140+// Seat returns the connection's seat (nil after Close), binding wl_seat on first use. The seat
141 // is also bound when the first surface is created. Input starts flowing to the141 // is also bound when the first surface is created. Input starts flowing to the
142 // Handler once the compositor announces the capabilities.142 // Handler once the compositor announces the capabilities.
143 func (c *Conn) Seat() *Seat {143 func (c *Conn) Seat() *Seat {
@@ -187,6 +187,9 @@ func (c *Conn) ensureSeat() {
187 // KeyEvent without text. Installing or removing cancels held keys and repeat.187 // KeyEvent without text. Installing or removing cancels held keys and repeat.
188 // The previous buffer is not wiped: that is the caller's call.188 // The previous buffer is not wiped: that is the caller's call.
189 func (s *Seat) SetSecret(b *SecretBuffer) {189 func (s *Seat) SetSecret(b *SecretBuffer) {
190+ if s == nil {
191+ return
192+ }
190 s.secret = b193 s.secret = b
191 if s.kb != nil {194 if s.kb != nil {
192 s.kb.setSecret(b)195 s.kb.setSecret(b)
@@ -197,7 +200,7 @@ func (s *Seat) SetSecret(b *SecretBuffer) {
197 // when the compositor has no cursor-shape support, the pointer is outside the200 // when the compositor has no cursor-shape support, the pointer is outside the
198 // connection's surfaces, or shape is unchanged since the last enter.201 // connection's surfaces, or shape is unchanged since the last enter.
199 func (s *Seat) SetCursor(shape CursorShape) error {202 func (s *Seat) SetCursor(shape CursorShape) error {
200- if s.c.closed {203+ if s == nil || s.c.closed {
201 return ErrClosed204 return ErrClosed
202 }205 }
203 limit := CursorZoomOut206 limit := CursorZoomOut

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 4

↑↓ moveenter openesc close