detached 3338d18

merge: vk-keymap (virtual keymap dedupe, relative motion deltas)

commit
3338d18e1c965dddc191e65a040b359701617e32
author
brice · · 2026-09-26 14:31 UTC
merge of
1e24fe6 + e80b5ff · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +136 -16
M internal/adapters/wayland/commands_test.go +5 -4
@@ -12,16 +12,17 @@ import (
1212
1313 func TestDrainCommandsCollapsesMotion(t *testing.T) {
1414 cmds := make(chan ports.ClientCommand, 16)
15- cmds <- ports.PointerMotionTo{ID: 1, X: 2}
16- cmds <- ports.PointerMotionTo{ID: 1, X: 3}
15+ cmds <- ports.PointerMotionTo{ID: 1, X: 2, DX: 0.25, UnaccelDX: 0.5}
16+ cmds <- ports.PointerMotionTo{ID: 1, X: 3, DX: 0.25, DY: -0.5, UnaccelDX: 0.5, UnaccelDY: -1}
1717 cmds <- ports.PointerButtonTo{ID: 1, Button: 272, Pressed: true}
1818 cmds <- ports.PointerMotionTo{ID: 1, X: 4}
1919 cmds <- ports.PointerFocus{ID: 2}
2020 cmds <- ports.PointerMotionTo{ID: 2, X: 5}
2121 cmds <- ports.PointerMotionTo{ID: 2, X: 6}
22- batch, open := drainCommands(ports.PointerMotionTo{ID: 1, X: 1}, cmds)
22+ batch, open := drainCommands(ports.PointerMotionTo{ID: 1, X: 1, DX: 0.25, UnaccelDX: 0.5}, cmds)
2323 want := []ports.ClientCommand{
24- ports.PointerMotionTo{ID: 1, X: 3},
24+ // Slow motions add up: none of the sub-pixel deltas is lost.
25+ ports.PointerMotionTo{ID: 1, X: 3, DX: 0.75, DY: -0.5, UnaccelDX: 1.5, UnaccelDY: -1},
2526 ports.PointerButtonTo{ID: 1, Button: 272, Pressed: true},
2627 ports.PointerMotionTo{ID: 1, X: 4},
2728 ports.PointerFocus{ID: 2},
@@ -12,16 +12,17 @@ import (
12 12
13 func TestDrainCommandsCollapsesMotion(t *testing.T) {13 func TestDrainCommandsCollapsesMotion(t *testing.T) {
14 cmds := make(chan ports.ClientCommand, 16)14 cmds := make(chan ports.ClientCommand, 16)
15- cmds <- ports.PointerMotionTo{ID: 1, X: 2}15+ cmds <- ports.PointerMotionTo{ID: 1, X: 2, DX: 0.25, UnaccelDX: 0.5}
16- cmds <- ports.PointerMotionTo{ID: 1, X: 3}16+ cmds <- ports.PointerMotionTo{ID: 1, X: 3, DX: 0.25, DY: -0.5, UnaccelDX: 0.5, UnaccelDY: -1}
17 cmds <- ports.PointerButtonTo{ID: 1, Button: 272, Pressed: true}17 cmds <- ports.PointerButtonTo{ID: 1, Button: 272, Pressed: true}
18 cmds <- ports.PointerMotionTo{ID: 1, X: 4}18 cmds <- ports.PointerMotionTo{ID: 1, X: 4}
19 cmds <- ports.PointerFocus{ID: 2}19 cmds <- ports.PointerFocus{ID: 2}
20 cmds <- ports.PointerMotionTo{ID: 2, X: 5}20 cmds <- ports.PointerMotionTo{ID: 2, X: 5}
21 cmds <- ports.PointerMotionTo{ID: 2, X: 6}21 cmds <- ports.PointerMotionTo{ID: 2, X: 6}
22- batch, open := drainCommands(ports.PointerMotionTo{ID: 1, X: 1}, cmds)22+ batch, open := drainCommands(ports.PointerMotionTo{ID: 1, X: 1, DX: 0.25, UnaccelDX: 0.5}, cmds)
23 want := []ports.ClientCommand{23 want := []ports.ClientCommand{
24- ports.PointerMotionTo{ID: 1, X: 3},24+ // Slow motions add up: none of the sub-pixel deltas is lost.
25+ ports.PointerMotionTo{ID: 1, X: 3, DX: 0.75, DY: -0.5, UnaccelDX: 1.5, UnaccelDY: -1},
25 ports.PointerButtonTo{ID: 1, Button: 272, Pressed: true},26 ports.PointerButtonTo{ID: 1, Button: 272, Pressed: true},
26 ports.PointerMotionTo{ID: 1, X: 4},27 ports.PointerMotionTo{ID: 1, X: 4},
27 ports.PointerFocus{ID: 2},28 ports.PointerFocus{ID: 2},
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 +11 -3
@@ -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 {
@@ -431,7 +432,8 @@ func (s *Server) forwardContents(ctx context.Context) {
431432 const maxCommandBatch = 256
432433
433434 // drainCommands returns first plus the commands already queued, with runs of
434-// pointer motion for the same window collapsed into the latest one. open is
435+// pointer motion for the same window collapsed into the latest one, their
436+// relative deltas summed. open is
435437 // false once the channel is closed.
436438 func drainCommands(first ports.ClientCommand, cmds <-chan ports.ClientCommand) (batch []ports.ClientCommand, open bool) {
437439 batch = append(batch, first)
@@ -443,6 +445,12 @@ func drainCommands(first ports.ClientCommand, cmds <-chan ports.ClientCommand) (
443445 }
444446 if m, isMotion := c.(ports.PointerMotionTo); isMotion {
445447 if last, lastMotion := batch[len(batch)-1].(ports.PointerMotionTo); lastMotion && last.ID == m.ID {
448+ // The position is the latest; relative deltas add up so
449+ // locked pointers (games) lose no movement.
450+ m.DX += last.DX
451+ m.DY += last.DY
452+ m.UnaccelDX += last.UnaccelDX
453+ m.UnaccelDY += last.UnaccelDY
446454 batch[len(batch)-1] = m
447455 continue
448456 }
@@ -702,7 +710,7 @@ func (s *Server) setKeymap(c ports.SetKeymap) {
702710 if s.keymapFD >= 0 {
703711 unix.Close(s.keymapFD)
704712 }
705- s.keymapFD, s.keymapSize = fd, size
713+ s.keymapFD, s.keymapSize, s.keymapText = fd, size, c.Keymap
706714 s.keymapOwner = nil
707715 focused := s.focused
708716 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 {
@@ -431,7 +432,8 @@ func (s *Server) forwardContents(ctx context.Context) {
431 const maxCommandBatch = 256432 const maxCommandBatch = 256
432 433
433 // drainCommands returns first plus the commands already queued, with runs of434 // drainCommands returns first plus the commands already queued, with runs of
434-// pointer motion for the same window collapsed into the latest one. open is435+// pointer motion for the same window collapsed into the latest one, their
436+// relative deltas summed. open is
435 // false once the channel is closed.437 // false once the channel is closed.
436 func drainCommands(first ports.ClientCommand, cmds <-chan ports.ClientCommand) (batch []ports.ClientCommand, open bool) {438 func drainCommands(first ports.ClientCommand, cmds <-chan ports.ClientCommand) (batch []ports.ClientCommand, open bool) {
437 batch = append(batch, first)439 batch = append(batch, first)
@@ -443,6 +445,12 @@ func drainCommands(first ports.ClientCommand, cmds <-chan ports.ClientCommand) (
443 }445 }
444 if m, isMotion := c.(ports.PointerMotionTo); isMotion {446 if m, isMotion := c.(ports.PointerMotionTo); isMotion {
445 if last, lastMotion := batch[len(batch)-1].(ports.PointerMotionTo); lastMotion && last.ID == m.ID {447 if last, lastMotion := batch[len(batch)-1].(ports.PointerMotionTo); lastMotion && last.ID == m.ID {
448+ // The position is the latest; relative deltas add up so
449+ // locked pointers (games) lose no movement.
450+ m.DX += last.DX
451+ m.DY += last.DY
452+ m.UnaccelDX += last.UnaccelDX
453+ m.UnaccelDY += last.UnaccelDY
446 batch[len(batch)-1] = m454 batch[len(batch)-1] = m
447 continue455 continue
448 }456 }
@@ -702,7 +710,7 @@ func (s *Server) setKeymap(c ports.SetKeymap) {
702 if s.keymapFD >= 0 {710 if s.keymapFD >= 0 {
703 unix.Close(s.keymapFD)711 unix.Close(s.keymapFD)
704 }712 }
705- s.keymapFD, s.keymapSize = fd, size713+ s.keymapFD, s.keymapSize, s.keymapText = fd, size, c.Keymap
706 s.keymapOwner = nil714 s.keymapOwner = nil
707 focused := s.focused715 focused := s.focused
708 s.changeFocus(0)716 s.changeFocus(0)
M internal/adapters/wayland/virtualkeyboard.go +32 -7
@@ -22,6 +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
26+ mods ports.ModState // last modifiers request, zero before one
2527 // pressed are keys held on focus; a focus change releases them by leave.
2628 pressed map[uint32]bool
2729 focus ports.WindowID
@@ -65,7 +67,9 @@ func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format
6567 unix.Close(k.fd)
6668 }
6769 k.fd, k.size = copyFD, copySize
68- if s := k.server; s.keymapOwner == k {
70+ old := k.server.activeKeymapText()
71+ k.text = text
72+ if s := k.server; s.keymapOwner == k && text != old {
6973 // Clients must see the new layout before the next key.
7074 s.sendKeymapAll(k.fd, k.size)
7175 }
@@ -128,6 +132,7 @@ func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key
128132 }
129133
130134 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}
131136 for _, kb := range k.keyboards(r) {
132137 kb.SendModifiers(k.server.serial, depressed, latched, locked, group)
133138 }
@@ -164,13 +169,25 @@ func (s *Server) useKeymap(k *virtualKeyboard) {
164169 if s.keymapOwner == k {
165170 return
166171 }
172+ old := s.activeKeymapText()
167173 s.keymapOwner = k
168- s.sendKeymapAll(s.currentKeymap())
169- if k == nil {
170- s.serial++
171- for _, kb := range s.clientKeyboards(s.focusClient()) {
172- s.sendModifiers(kb)
173- }
174+ // Like wlroots and smithay, a keymap equal to the active one is not
175+ // sent again: tools such as dictation apps create a virtual keyboard
176+ // with the seat layout per use, and each resend makes every client
177+ // (Xwayland included) recompile its keymap.
178+ if s.activeKeymapText() != old {
179+ s.sendKeymapAll(s.currentKeymap())
180+ }
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)
174191 }
175192 s.log.Debug().Bool("virtual", k != nil).Msg("keymap switched")
176193 }
@@ -185,6 +202,14 @@ func (s *Server) sendKeymapAll(fd int, size uint32) {
185202 }
186203 }
187204
205+// activeKeymapText is the text of the keymap keyboards carry.
206+func (s *Server) activeKeymapText() string {
207+ if k := s.keymapOwner; k != nil {
208+ return k.text
209+ }
210+ return s.keymapText
211+}
212+
188213 // currentKeymap is the keymap a new keyboard must start with.
189214 func (s *Server) currentKeymap() (int, uint32) {
190215 if k := s.keymapOwner; k != nil {
@@ -22,6 +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 one
26+ mods ports.ModState // last modifiers request, zero before one
25 // 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.
26 pressed map[uint32]bool28 pressed map[uint32]bool
27 focus ports.WindowID29 focus ports.WindowID
@@ -65,7 +67,9 @@ func (k *virtualKeyboard) Keymap(_ *virtualkeyboard.ZwpVirtualKeyboardV1, format
65 unix.Close(k.fd)67 unix.Close(k.fd)
66 }68 }
67 k.fd, k.size = copyFD, copySize69 k.fd, k.size = copyFD, copySize
68- if s := k.server; s.keymapOwner == k {70+ old := k.server.activeKeymapText()
71+ k.text = text
72+ if s := k.server; s.keymapOwner == k && text != old {
69 // Clients must see the new layout before the next key.73 // Clients must see the new layout before the next key.
70 s.sendKeymapAll(k.fd, k.size)74 s.sendKeymapAll(k.fd, k.size)
71 }75 }
@@ -128,6 +132,7 @@ func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key
128 }132 }
129 133
130 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}
131 for _, kb := range k.keyboards(r) {136 for _, kb := range k.keyboards(r) {
132 kb.SendModifiers(k.server.serial, depressed, latched, locked, group)137 kb.SendModifiers(k.server.serial, depressed, latched, locked, group)
133 }138 }
@@ -164,13 +169,25 @@ func (s *Server) useKeymap(k *virtualKeyboard) {
164 if s.keymapOwner == k {169 if s.keymapOwner == k {
165 return170 return
166 }171 }
172+ old := s.activeKeymapText()
167 s.keymapOwner = k173 s.keymapOwner = k
168- s.sendKeymapAll(s.currentKeymap())174+ // Like wlroots and smithay, a keymap equal to the active one is not
169- if k == nil {175+ // sent again: tools such as dictation apps create a virtual keyboard
170- s.serial++176+ // with the seat layout per use, and each resend makes every client
171- for _, kb := range s.clientKeyboards(s.focusClient()) {177+ // (Xwayland included) recompile its keymap.
172- s.sendModifiers(kb)178+ if s.activeKeymapText() != old {
173- }179+ s.sendKeymapAll(s.currentKeymap())
180+ }
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)
174 }191 }
175 s.log.Debug().Bool("virtual", k != nil).Msg("keymap switched")192 s.log.Debug().Bool("virtual", k != nil).Msg("keymap switched")
176 }193 }
@@ -185,6 +202,14 @@ func (s *Server) sendKeymapAll(fd int, size uint32) {
185 }202 }
186 }203 }
187 204
205+// activeKeymapText is the text of the keymap keyboards carry.
206+func (s *Server) activeKeymapText() string {
207+ if k := s.keymapOwner; k != nil {
208+ return k.text
209+ }
210+ return s.keymapText
211+}
212+
188 // currentKeymap is the keymap a new keyboard must start with.213 // currentKeymap is the keymap a new keyboard must start with.
189 func (s *Server) currentKeymap() (int, uint32) {214 func (s *Server) currentKeymap() (int, uint32) {
190 if k := s.keymapOwner; k != nil {215 if k := s.keymapOwner; k != nil {
M internal/adapters/wayland/virtualkeyboard_test.go +80 -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 }
@@ -180,3 +181,80 @@ func TestVirtualKeyBeforeKeymap(t *testing.T) {
180181 requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
181182 expectProtocolError(t, typer, kb, uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap))
182183 }
184+
185+// A virtual keyboard with the seat keymap (dictation tools, wtype) sends
186+// no keymap to clients: only its keys, then no keymap on the switch back.
187+func TestVirtualKeyboardSameKeymapNotResent(t *testing.T) {
188+ s, events, commands, dir := keyboardServer(t)
189+ target, proxy := focusedTarget(t, s, events, commands, dir)
190+ for range 2 { // one typer per use, like a dictation app
191+ typer, kb := virtualTyper(t, s, dir)
192+ sendVirtualKeymap(t, typer, kb, keymapText(t))
193+ requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
194+ requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(2), uint32(30), uint32(0))
195+ roundtrip(t, typer, target)
196+ _ = typer.Close()
197+ }
198+ deadline := time.Now().Add(200 * time.Millisecond)
199+ for time.Now().Before(deadline) {
200+ roundtrip(t, target)
201+ time.Sleep(5 * time.Millisecond)
202+ }
203+ if len(proxy.keymaps) != 0 {
204+ t.Fatalf("%d keymaps sent for a virtual keyboard with the seat keymap", len(proxy.keymaps))
205+ }
206+ keys := 0
207+ for _, o := range proxy.opcodes {
208+ if o == uint16(wayland.KeyboardEventKey) {
209+ keys++
210+ }
211+ }
212+ if keys != 4 {
213+ t.Fatalf("keys %d, want 4 (events %v)", keys, proxy.opcodes)
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+}
@@ -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 }
@@ -180,3 +181,80 @@ func TestVirtualKeyBeforeKeymap(t *testing.T) {
180 requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))181 requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
181 expectProtocolError(t, typer, kb, uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap))182 expectProtocolError(t, typer, kb, uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap))
182 }183 }
184+
185+// A virtual keyboard with the seat keymap (dictation tools, wtype) sends
186+// no keymap to clients: only its keys, then no keymap on the switch back.
187+func TestVirtualKeyboardSameKeymapNotResent(t *testing.T) {
188+ s, events, commands, dir := keyboardServer(t)
189+ target, proxy := focusedTarget(t, s, events, commands, dir)
190+ for range 2 { // one typer per use, like a dictation app
191+ typer, kb := virtualTyper(t, s, dir)
192+ sendVirtualKeymap(t, typer, kb, keymapText(t))
193+ requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
194+ requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(2), uint32(30), uint32(0))
195+ roundtrip(t, typer, target)
196+ _ = typer.Close()
197+ }
198+ deadline := time.Now().Add(200 * time.Millisecond)
199+ for time.Now().Before(deadline) {
200+ roundtrip(t, target)
201+ time.Sleep(5 * time.Millisecond)
202+ }
203+ if len(proxy.keymaps) != 0 {
204+ t.Fatalf("%d keymaps sent for a virtual keyboard with the seat keymap", len(proxy.keymaps))
205+ }
206+ keys := 0
207+ for _, o := range proxy.opcodes {
208+ if o == uint16(wayland.KeyboardEventKey) {
209+ keys++
210+ }
211+ }
212+ if keys != 4 {
213+ t.Fatalf("keys %d, want 4 (events %v)", keys, proxy.opcodes)
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 26

Tags 10

↑↓ moveenter openesc close