detached bc84cb3

fix(wayland): skip resending a virtual keymap equal to the active one

commit
bc84cb3e4018ff7a89ae5e43b7bb31a46dd0d773
author
brice · · 2026-09-26 14:23 UTC
parent
1e24fe6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +55 -4
M internal/adapters/wayland/server.go +3 -2
@@ -101,6 +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
104105 keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap
105106 repeatRate, repeatDelay int
106107 // eventMu protects only the notification queue, not display-owned window state.
@@ -216,7 +217,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
216217 d.Close()
217218 return nil, e
218219 }
219- s.keymapFD, s.keymapSize = fd, size
220+ s.keymapFD, s.keymapSize, s.keymapText = fd, size, opts.Keymap
220221 }
221222 s.cleanup = func() {
222223 if s.keymapFD >= 0 {
@@ -702,7 +703,7 @@ func (s *Server) setKeymap(c ports.SetKeymap) {
702703 if s.keymapFD >= 0 {
703704 unix.Close(s.keymapFD)
704705 }
705- s.keymapFD, s.keymapSize = fd, size
706+ s.keymapFD, s.keymapSize, s.keymapText = fd, size, c.Keymap
706707 s.keymapOwner = nil
707708 focused := s.focused
708709 s.changeFocus(0)
@@ -101,6 +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 with
104 keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap105 keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap
105 repeatRate, repeatDelay int106 repeatRate, repeatDelay int
106 // eventMu protects only the notification queue, not display-owned window state.107 // eventMu protects only the notification queue, not display-owned window state.
@@ -216,7 +217,7 @@ func New(opts Options, ch Channels, log zerowrap.Logger) (*Server, error) {
216 d.Close()217 d.Close()
217 return nil, e218 return nil, e
218 }219 }
219- s.keymapFD, s.keymapSize = fd, size220+ s.keymapFD, s.keymapSize, s.keymapText = fd, size, opts.Keymap
220 }221 }
221 s.cleanup = func() {222 s.cleanup = func() {
222 if s.keymapFD >= 0 {223 if s.keymapFD >= 0 {
@@ -702,7 +703,7 @@ func (s *Server) setKeymap(c ports.SetKeymap) {
702 if s.keymapFD >= 0 {703 if s.keymapFD >= 0 {
703 unix.Close(s.keymapFD)704 unix.Close(s.keymapFD)
704 }705 }
705- s.keymapFD, s.keymapSize = fd, size706+ s.keymapFD, s.keymapSize, s.keymapText = fd, size, c.Keymap
706 s.keymapOwner = nil707 s.keymapOwner = nil
707 focused := s.focused708 focused := s.focused
708 s.changeFocus(0)709 s.changeFocus(0)
M internal/adapters/wayland/virtualkeyboard.go +20 -2
@@ -22,6 +22,7 @@ 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
2526 // pressed are keys held on focus; a focus change releases them by leave.
2627 pressed map[uint32]bool
2728 focus ports.WindowID
@@ -65,7 +66,9 @@ func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format
6566 unix.Close(k.fd)
6667 }
6768 k.fd, k.size = copyFD, copySize
68- if s := k.server; s.keymapOwner == k {
69+ old := k.server.activeKeymapText()
70+ k.text = text
71+ if s := k.server; s.keymapOwner == k && text != old {
6972 // Clients must see the new layout before the next key.
7073 s.sendKeymapAll(k.fd, k.size)
7174 }
@@ -164,8 +167,15 @@ func (s *Server) useKeymap(k *virtualKeyboard) {
164167 if s.keymapOwner == k {
165168 return
166169 }
170+ old := s.activeKeymapText()
167171 s.keymapOwner = k
168- s.sendKeymapAll(s.currentKeymap())
172+ // Like wlroots and smithay, a keymap equal to the active one is not
173+ // sent again: tools such as dictation apps create a virtual keyboard
174+ // with the seat layout per use, and each resend makes every client
175+ // (Xwayland included) recompile its keymap.
176+ if s.activeKeymapText() != old {
177+ s.sendKeymapAll(s.currentKeymap())
178+ }
169179 if k == nil {
170180 s.serial++
171181 for _, kb := range s.clientKeyboards(s.focusClient()) {
@@ -185,6 +195,14 @@ func (s *Server) sendKeymapAll(fd int, size uint32) {
185195 }
186196 }
187197
198+// activeKeymapText is the text of the keymap keyboards carry.
199+func (s *Server) activeKeymapText() string {
200+ if k := s.keymapOwner; k != nil {
201+ return k.text
202+ }
203+ return s.keymapText
204+}
205+
188206 // currentKeymap is the keymap a new keyboard must start with.
189207 func (s *Server) currentKeymap() (int, uint32) {
190208 if k := s.keymapOwner; k != nil {
@@ -22,6 +22,7 @@ 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 one
25 // pressed are keys held on focus; a focus change releases them by leave.26 // pressed are keys held on focus; a focus change releases them by leave.
26 pressed map[uint32]bool27 pressed map[uint32]bool
27 focus ports.WindowID28 focus ports.WindowID
@@ -65,7 +66,9 @@ func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format
65 unix.Close(k.fd)66 unix.Close(k.fd)
66 }67 }
67 k.fd, k.size = copyFD, copySize68 k.fd, k.size = copyFD, copySize
68- if s := k.server; s.keymapOwner == k {69+ old := k.server.activeKeymapText()
70+ k.text = text
71+ if s := k.server; s.keymapOwner == k && text != old {
69 // Clients must see the new layout before the next key.72 // Clients must see the new layout before the next key.
70 s.sendKeymapAll(k.fd, k.size)73 s.sendKeymapAll(k.fd, k.size)
71 }74 }
@@ -164,8 +167,15 @@ func (s *Server) useKeymap(k *virtualKeyboard) {
164 if s.keymapOwner == k {167 if s.keymapOwner == k {
165 return168 return
166 }169 }
170+ old := s.activeKeymapText()
167 s.keymapOwner = k171 s.keymapOwner = k
168- s.sendKeymapAll(s.currentKeymap())172+ // Like wlroots and smithay, a keymap equal to the active one is not
173+ // sent again: tools such as dictation apps create a virtual keyboard
174+ // with the seat layout per use, and each resend makes every client
175+ // (Xwayland included) recompile its keymap.
176+ if s.activeKeymapText() != old {
177+ s.sendKeymapAll(s.currentKeymap())
178+ }
169 if k == nil {179 if k == nil {
170 s.serial++180 s.serial++
171 for _, kb := range s.clientKeyboards(s.focusClient()) {181 for _, kb := range s.clientKeyboards(s.focusClient()) {
@@ -185,6 +195,14 @@ func (s *Server) sendKeymapAll(fd int, size uint32) {
185 }195 }
186 }196 }
187 197
198+// activeKeymapText is the text of the keymap keyboards carry.
199+func (s *Server) activeKeymapText() string {
200+ if k := s.keymapOwner; k != nil {
201+ return k.text
202+ }
203+ return s.keymapText
204+}
205+
188 // currentKeymap is the keymap a new keyboard must start with.206 // currentKeymap is the keymap a new keyboard must start with.
189 func (s *Server) currentKeymap() (int, uint32) {207 func (s *Server) currentKeymap() (int, uint32) {
190 if k := s.keymapOwner; k != nil {208 if k := s.keymapOwner; k != nil {
M internal/adapters/wayland/virtualkeyboard_test.go +32 -0
@@ -180,3 +180,35 @@ func TestVirtualKeyBeforeKeymap(t *testing.T) {
180180 requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
181181 expectProtocolError(t, typer, kb, uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap))
182182 }
183+
184+// A virtual keyboard with the seat keymap (dictation tools, wtype) sends
185+// no keymap to clients: only its keys, then no keymap on the switch back.
186+func TestVirtualKeyboardSameKeymapNotResent(t *testing.T) {
187+ s, events, commands, dir := keyboardServer(t)
188+ target, proxy := focusedTarget(t, s, events, commands, dir)
189+ for range 2 { // one typer per use, like a dictation app
190+ typer, kb := virtualTyper(t, s, dir)
191+ sendVirtualKeymap(t, typer, kb, keymapText(t))
192+ requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
193+ requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(2), uint32(30), uint32(0))
194+ roundtrip(t, typer, target)
195+ _ = typer.Close()
196+ }
197+ deadline := time.Now().Add(200 * time.Millisecond)
198+ for time.Now().Before(deadline) {
199+ roundtrip(t, target)
200+ time.Sleep(5 * time.Millisecond)
201+ }
202+ if len(proxy.keymaps) != 0 {
203+ t.Fatalf("%d keymaps sent for a virtual keyboard with the seat keymap", len(proxy.keymaps))
204+ }
205+ keys := 0
206+ for _, o := range proxy.opcodes {
207+ if o == uint16(wayland.KeyboardEventKey) {
208+ keys++
209+ }
210+ }
211+ if keys != 4 {
212+ t.Fatalf("keys %d, want 4 (events %v)", keys, proxy.opcodes)
213+ }
214+}
@@ -180,3 +180,35 @@ func TestVirtualKeyBeforeKeymap(t *testing.T) {
180 requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))180 requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
181 expectProtocolError(t, typer, kb, uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap))181 expectProtocolError(t, typer, kb, uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap))
182 }182 }
183+
184+// A virtual keyboard with the seat keymap (dictation tools, wtype) sends
185+// no keymap to clients: only its keys, then no keymap on the switch back.
186+func TestVirtualKeyboardSameKeymapNotResent(t *testing.T) {
187+ s, events, commands, dir := keyboardServer(t)
188+ target, proxy := focusedTarget(t, s, events, commands, dir)
189+ for range 2 { // one typer per use, like a dictation app
190+ typer, kb := virtualTyper(t, s, dir)
191+ sendVirtualKeymap(t, typer, kb, keymapText(t))
192+ requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
193+ requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(2), uint32(30), uint32(0))
194+ roundtrip(t, typer, target)
195+ _ = typer.Close()
196+ }
197+ deadline := time.Now().Add(200 * time.Millisecond)
198+ for time.Now().Before(deadline) {
199+ roundtrip(t, target)
200+ time.Sleep(5 * time.Millisecond)
201+ }
202+ if len(proxy.keymaps) != 0 {
203+ t.Fatalf("%d keymaps sent for a virtual keyboard with the seat keymap", len(proxy.keymaps))
204+ }
205+ keys := 0
206+ for _, o := range proxy.opcodes {
207+ if o == uint16(wayland.KeyboardEventKey) {
208+ keys++
209+ }
210+ }
211+ if keys != 4 {
212+ t.Fatalf("keys %d, want 4 (events %v)", keys, proxy.opcodes)
213+ }
214+}

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