detached e80b5ff

fix(wayland): send the typing keyboard's modifiers when the virtual keymap switch skips the keymap

commit
e80b5ff7437a8f0eacb6f0752cdba2ba7b61db6f
author
brice · · 2026-09-26 14:28 UTC
parent
a589526
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +70 -9
M internal/adapters/wayland/keyboard_test.go +8 -0
@@ -129,10 +129,18 @@ type eventsProxy struct {
129129 wlturbo.BaseProxy
130130 opcodes []uint16
131131 keymaps []string
132+ locked []uint32 // locked modifiers of each modifiers event
132133 }
133134
134135 func (p *eventsProxy) Dispatch(e *wlturbo.Event) {
135136 p.opcodes = append(p.opcodes, e.Opcode)
137+ if e.Opcode == uint16(wayland.KeyboardEventModifiers) {
138+ e.Uint32() // serial
139+ e.Uint32() // depressed
140+ e.Uint32() // latched
141+ p.locked = append(p.locked, e.Uint32())
142+ return
143+ }
136144 if e.Opcode != uint16(wayland.KeyboardEventKeymap) {
137145 return
138146 }
@@ -129,10 +129,18 @@ type eventsProxy struct {
129 wlturbo.BaseProxy129 wlturbo.BaseProxy
130 opcodes []uint16130 opcodes []uint16
131 keymaps []string131 keymaps []string
132+ locked []uint32 // locked modifiers of each modifiers event
132 }133 }
133 134
134 func (p *eventsProxy) Dispatch(e *wlturbo.Event) {135 func (p *eventsProxy) Dispatch(e *wlturbo.Event) {
135 p.opcodes = append(p.opcodes, e.Opcode)136 p.opcodes = append(p.opcodes, e.Opcode)
137+ if e.Opcode == uint16(wayland.KeyboardEventModifiers) {
138+ e.Uint32() // serial
139+ e.Uint32() // depressed
140+ e.Uint32() // latched
141+ p.locked = append(p.locked, e.Uint32())
142+ return
143+ }
136 if e.Opcode != uint16(wayland.KeyboardEventKeymap) {144 if e.Opcode != uint16(wayland.KeyboardEventKeymap) {
137 return145 return
138 }146 }
M internal/adapters/wayland/server.go +1 -1
@@ -101,7 +101,7 @@ type Server struct {
101101 heldKeys map[uint32]bool
102102 keymapFD int
103103 keymapSize uint32
104- keymapText string // the seat keymap, to compare virtual keymaps with
104+ keymapText string // the seat keymap, to compare virtual keymaps with
105105 keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap
106106 repeatRate, repeatDelay int
107107 // eventMu protects only the notification queue, not display-owned window state.
@@ -101,7 +101,7 @@ type Server struct {
101 heldKeys map[uint32]bool101 heldKeys map[uint32]bool
102 keymapFD int102 keymapFD int
103 keymapSize uint32103 keymapSize uint32
104- keymapText string // the seat keymap, to compare virtual keymaps with104+ keymapText string // the seat keymap, to compare virtual keymaps with
105 keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap105 keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap
106 repeatRate, repeatDelay int106 repeatRate, repeatDelay int
107 // eventMu protects only the notification queue, not display-owned window state.107 // eventMu protects only the notification queue, not display-owned window state.
M internal/adapters/wayland/virtualkeyboard.go +13 -6
@@ -22,7 +22,8 @@ type virtualKeyboard struct {
2222 server *Server
2323 fd int // sealed copy of the client keymap, -1 before one
2424 size uint32
25- text string // the keymap, compared with the active one
25+ text string // the keymap, compared with the active one
26+ mods ports.ModState // last modifiers request, zero before one
2627 // pressed are keys held on focus; a focus change releases them by leave.
2728 pressed map[uint32]bool
2829 focus ports.WindowID
@@ -131,6 +132,7 @@ func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key
131132 }
132133
133134 func (k *virtualKeyboard) Modifiers(r *virtualkeyboard.ZwpVirtualKeyboardV1, depressed, latched, locked, group uint32) {
135+ k.mods = ports.ModState{Depressed: depressed, Latched: latched, Locked: locked, Group: group}
134136 for _, kb := range k.keyboards(r) {
135137 kb.SendModifiers(k.server.serial, depressed, latched, locked, group)
136138 }
@@ -176,11 +178,16 @@ func (s *Server) useKeymap(k *virtualKeyboard) {
176178 if s.activeKeymapText() != old {
177179 s.sendKeymapAll(s.currentKeymap())
178180 }
179- if k == nil {
180- s.serial++
181- for _, kb := range s.clientKeyboards(s.focusClient()) {
182- s.sendModifiers(kb)
183- }
181+ // Modifiers follow the keyboard that types: a skipped keymap no longer
182+ // resets them, so a virtual keyboard never types through seat Shift or
183+ // Caps Lock, and the seat gets its own back.
184+ m := s.modState
185+ if k != nil {
186+ m = k.mods
187+ }
188+ s.serial++
189+ for _, kb := range s.clientKeyboards(s.focusClient()) {
190+ kb.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
184191 }
185192 s.log.Debug().Bool("virtual", k != nil).Msg("keymap switched")
186193 }
@@ -22,7 +22,8 @@ type virtualKeyboard struct {
22 server *Server22 server *Server
23 fd int // sealed copy of the client keymap, -1 before one23 fd int // sealed copy of the client keymap, -1 before one
24 size uint3224 size uint32
25- text string // the keymap, compared with the active one25+ text string // the keymap, compared with the active one
26+ mods ports.ModState // last modifiers request, zero before one
26 // pressed are keys held on focus; a focus change releases them by leave.27 // pressed are keys held on focus; a focus change releases them by leave.
27 pressed map[uint32]bool28 pressed map[uint32]bool
28 focus ports.WindowID29 focus ports.WindowID
@@ -131,6 +132,7 @@ func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key
131 }132 }
132 133
133 func (k *virtualKeyboard) Modifiers(r *virtualkeyboard.ZwpVirtualKeyboardV1, depressed, latched, locked, group uint32) {134 func (k *virtualKeyboard) Modifiers(r *virtualkeyboard.ZwpVirtualKeyboardV1, depressed, latched, locked, group uint32) {
135+ k.mods = ports.ModState{Depressed: depressed, Latched: latched, Locked: locked, Group: group}
134 for _, kb := range k.keyboards(r) {136 for _, kb := range k.keyboards(r) {
135 kb.SendModifiers(k.server.serial, depressed, latched, locked, group)137 kb.SendModifiers(k.server.serial, depressed, latched, locked, group)
136 }138 }
@@ -176,11 +178,16 @@ func (s *Server) useKeymap(k *virtualKeyboard) {
176 if s.activeKeymapText() != old {178 if s.activeKeymapText() != old {
177 s.sendKeymapAll(s.currentKeymap())179 s.sendKeymapAll(s.currentKeymap())
178 }180 }
179- if k == nil {181+ // Modifiers follow the keyboard that types: a skipped keymap no longer
180- s.serial++182+ // resets them, so a virtual keyboard never types through seat Shift or
181- for _, kb := range s.clientKeyboards(s.focusClient()) {183+ // Caps Lock, and the seat gets its own back.
182- s.sendModifiers(kb)184+ m := s.modState
183- }185+ if k != nil {
186+ m = k.mods
187+ }
188+ s.serial++
189+ for _, kb := range s.clientKeyboards(s.focusClient()) {
190+ kb.SendModifiers(s.serial, m.Depressed, m.Latched, m.Locked, m.Group)
184 }191 }
185 s.log.Debug().Bool("virtual", k != nil).Msg("keymap switched")192 s.log.Debug().Bool("virtual", k != nil).Msg("keymap switched")
186 }193 }
M internal/adapters/wayland/virtualkeyboard_test.go +48 -2
@@ -69,7 +69,7 @@ func TestVirtualKeyboardTypesIntoFocus(t *testing.T) {
6969 if err := target.Roundtrip(); err != nil {
7070 t.Fatal(err)
7171 }
72- want := []uint16{uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventKey), uint16(wayland.KeyboardEventKey)}
72+ want := []uint16{uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventModifiers), uint16(wayland.KeyboardEventKey), uint16(wayland.KeyboardEventKey)}
7373 if !slices.Equal(proxy.opcodes, want) {
7474 t.Fatalf("virtual events %v, want %v", proxy.opcodes, want)
7575 }
@@ -142,7 +142,8 @@ func TestVirtualKeyboardDisconnectReleases(t *testing.T) {
142142 }
143143 k, m := uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventModifiers)
144144 key := uint16(wayland.KeyboardEventKey)
145- want := []uint16{k, m, key, key, k, m}
145+ // The Modifiers request comes after the switch's own modifiers event.
146+ want := []uint16{k, m, m, key, key, k, m}
146147 if !slices.Equal(proxy.opcodes, want) {
147148 t.Fatalf("events %v, want %v", proxy.opcodes, want)
148149 }
@@ -212,3 +213,48 @@ func TestVirtualKeyboardSameKeymapNotResent(t *testing.T) {
212213 t.Fatalf("keys %d, want 4 (events %v)", keys, proxy.opcodes)
213214 }
214215 }
216+
217+// A virtual keyboard with the seat keymap types without the seat's
218+// modifiers: its switch sends its own (none), and the seat's come back
219+// with the next real key.
220+func TestVirtualKeyboardSameKeymapOwnModifiers(t *testing.T) {
221+ s, events, commands, dir := keyboardServer(t)
222+ target, proxy := focusedTarget(t, s, events, commands, dir)
223+ const capsLocked = 2
224+ set := make(chan struct{})
225+ s.display.Do(func() { s.modState = ports.ModState{Locked: capsLocked}; close(set) })
226+ <-set
227+ proxy.locked = nil
228+ typer, kb := virtualTyper(t, s, dir)
229+ sendVirtualKeymap(t, typer, kb, keymapText(t))
230+ requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
231+ roundtrip(t, typer, target)
232+ m, key := uint16(wayland.KeyboardEventModifiers), uint16(wayland.KeyboardEventKey)
233+ if !slices.Equal(proxy.opcodes, []uint16{m, key}) || !slices.Equal(proxy.locked, []uint32{0}) {
234+ t.Fatalf("events %v locked %v, want modifiers with nothing locked, then the key", proxy.opcodes, proxy.locked)
235+ }
236+}
237+
238+// A virtual keymap that differs from the seat's is sent on switch, on
239+// update while it owns the keyboards, and the seat's again on switch back.
240+func TestVirtualKeyboardOtherKeymapSent(t *testing.T) {
241+ s, events, commands, dir := keyboardServer(t)
242+ target, proxy := focusedTarget(t, s, events, commands, dir)
243+ typer, kb := virtualTyper(t, s, dir)
244+ first := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap ", 1)
245+ second := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap ", 1)
246+ sendVirtualKeymap(t, typer, kb, first)
247+ requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
248+ sendVirtualKeymap(t, typer, kb, second)
249+ requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(2), uint32(30), uint32(0))
250+ roundtrip(t, typer, target)
251+ _ = typer.Close()
252+ deadline := time.Now().Add(2 * time.Second)
253+ for len(proxy.keymaps) < 3 && time.Now().Before(deadline) {
254+ roundtrip(t, target)
255+ time.Sleep(5 * time.Millisecond)
256+ }
257+ if !slices.Equal(proxy.keymaps, []string{first, second, keymapText(t)}) {
258+ t.Fatalf("keymaps %d, want virtual, updated virtual, seat", len(proxy.keymaps))
259+ }
260+}
@@ -69,7 +69,7 @@ func TestVirtualKeyboardTypesIntoFocus(t *testing.T) {
69 if err := target.Roundtrip(); err != nil {69 if err := target.Roundtrip(); err != nil {
70 t.Fatal(err)70 t.Fatal(err)
71 }71 }
72- want := []uint16{uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventKey), uint16(wayland.KeyboardEventKey)}72+ want := []uint16{uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventModifiers), uint16(wayland.KeyboardEventKey), uint16(wayland.KeyboardEventKey)}
73 if !slices.Equal(proxy.opcodes, want) {73 if !slices.Equal(proxy.opcodes, want) {
74 t.Fatalf("virtual events %v, want %v", proxy.opcodes, want)74 t.Fatalf("virtual events %v, want %v", proxy.opcodes, want)
75 }75 }
@@ -142,7 +142,8 @@ func TestVirtualKeyboardDisconnectReleases(t *testing.T) {
142 }142 }
143 k, m := uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventModifiers)143 k, m := uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventModifiers)
144 key := uint16(wayland.KeyboardEventKey)144 key := uint16(wayland.KeyboardEventKey)
145- want := []uint16{k, m, key, key, k, m}145+ // The Modifiers request comes after the switch's own modifiers event.
146+ want := []uint16{k, m, m, key, key, k, m}
146 if !slices.Equal(proxy.opcodes, want) {147 if !slices.Equal(proxy.opcodes, want) {
147 t.Fatalf("events %v, want %v", proxy.opcodes, want)148 t.Fatalf("events %v, want %v", proxy.opcodes, want)
148 }149 }
@@ -212,3 +213,48 @@ func TestVirtualKeyboardSameKeymapNotResent(t *testing.T) {
212 t.Fatalf("keys %d, want 4 (events %v)", keys, proxy.opcodes)213 t.Fatalf("keys %d, want 4 (events %v)", keys, proxy.opcodes)
213 }214 }
214 }215 }
216+
217+// A virtual keyboard with the seat keymap types without the seat's
218+// modifiers: its switch sends its own (none), and the seat's come back
219+// with the next real key.
220+func TestVirtualKeyboardSameKeymapOwnModifiers(t *testing.T) {
221+ s, events, commands, dir := keyboardServer(t)
222+ target, proxy := focusedTarget(t, s, events, commands, dir)
223+ const capsLocked = 2
224+ set := make(chan struct{})
225+ s.display.Do(func() { s.modState = ports.ModState{Locked: capsLocked}; close(set) })
226+ <-set
227+ proxy.locked = nil
228+ typer, kb := virtualTyper(t, s, dir)
229+ sendVirtualKeymap(t, typer, kb, keymapText(t))
230+ requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
231+ roundtrip(t, typer, target)
232+ m, key := uint16(wayland.KeyboardEventModifiers), uint16(wayland.KeyboardEventKey)
233+ if !slices.Equal(proxy.opcodes, []uint16{m, key}) || !slices.Equal(proxy.locked, []uint32{0}) {
234+ t.Fatalf("events %v locked %v, want modifiers with nothing locked, then the key", proxy.opcodes, proxy.locked)
235+ }
236+}
237+
238+// A virtual keymap that differs from the seat's is sent on switch, on
239+// update while it owns the keyboards, and the seat's again on switch back.
240+func TestVirtualKeyboardOtherKeymapSent(t *testing.T) {
241+ s, events, commands, dir := keyboardServer(t)
242+ target, proxy := focusedTarget(t, s, events, commands, dir)
243+ typer, kb := virtualTyper(t, s, dir)
244+ first := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap ", 1)
245+ second := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap ", 1)
246+ sendVirtualKeymap(t, typer, kb, first)
247+ requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
248+ sendVirtualKeymap(t, typer, kb, second)
249+ requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(2), uint32(30), uint32(0))
250+ roundtrip(t, typer, target)
251+ _ = typer.Close()
252+ deadline := time.Now().Add(2 * time.Second)
253+ for len(proxy.keymaps) < 3 && time.Now().Before(deadline) {
254+ roundtrip(t, target)
255+ time.Sleep(5 * time.Millisecond)
256+ }
257+ if !slices.Equal(proxy.keymaps, []string{first, second, keymapText(t)}) {
258+ t.Fatalf("keymaps %d, want virtual, updated virtual, seat", len(proxy.keymaps))
259+ }
260+}

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 23

Tags 10

↑↓ moveenter openesc close