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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 { | |||
| 129 | 129 | wlturbo.BaseProxy | |
| 130 | 130 | opcodes []uint16 | |
| 131 | 131 | keymaps []string | |
| 132 | + | locked []uint32 // locked modifiers of each modifiers event | |
| 132 | 133 | } | |
| 133 | 134 | ||
| 134 | 135 | func (p *eventsProxy) Dispatch(e *wlturbo.Event) { | |
| 135 | 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 | 144 | if e.Opcode != uint16(wayland.KeyboardEventKeymap) { | |
| 137 | 145 | return | |
| 138 | 146 | } | |
| @@ -129,10 +129,18 @@ type eventsProxy struct { | |||||
| 129 | wlturbo.BaseProxy | 129 | wlturbo.BaseProxy | ||
| 130 | opcodes []uint16 | 130 | opcodes []uint16 | ||
| 131 | keymaps []string | 131 | 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 | return | 145 | return | ||
| 138 | } | 146 | } | ||
M internal/adapters/wayland/server.go +1 -1
| @@ -101,7 +101,7 @@ type Server struct { | |||
| 101 | 101 | heldKeys map[uint32]bool | |
| 102 | 102 | keymapFD int | |
| 103 | 103 | keymapSize uint32 | |
| 104 | - | keymapText string // the seat keymap, to compare virtual keymaps with | |
| 104 | + | keymapText string // the seat keymap, to compare virtual keymaps with | |
| 105 | 105 | keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap | |
| 106 | 106 | repeatRate, repeatDelay int | |
| 107 | 107 | // eventMu protects only the notification queue, not display-owned window state. | |
| @@ -101,7 +101,7 @@ type Server struct { | |||||
| 101 | heldKeys map[uint32]bool | 101 | heldKeys map[uint32]bool | ||
| 102 | keymapFD int | 102 | keymapFD int | ||
| 103 | keymapSize uint32 | 103 | keymapSize uint32 | ||
| 104 | - | keymapText string // the seat keymap, to compare virtual keymaps with | 104 | + | keymapText string // the seat keymap, to compare virtual keymaps with |
| 105 | keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap | 105 | keymapOwner *virtualKeyboard // nil: keyboards carry the seat keymap | ||
| 106 | repeatRate, repeatDelay int | 106 | 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 { | |||
| 22 | 22 | server *Server | |
| 23 | 23 | fd int // sealed copy of the client keymap, -1 before one | |
| 24 | 24 | 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 | |
| 26 | 27 | // pressed are keys held on focus; a focus change releases them by leave. | |
| 27 | 28 | pressed map[uint32]bool | |
| 28 | 29 | focus ports.WindowID | |
| @@ -131,6 +132,7 @@ func (k *virtualKeyboard) Key(r *virtualkeyboard.ZwpVirtualKeyboardV1, time, key | |||
| 131 | 132 | } | |
| 132 | 133 | ||
| 133 | 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 | 136 | for _, kb := range k.keyboards(r) { | |
| 135 | 137 | kb.SendModifiers(k.server.serial, depressed, latched, locked, group) | |
| 136 | 138 | } | |
| @@ -176,11 +178,16 @@ func (s *Server) useKeymap(k *virtualKeyboard) { | |||
| 176 | 178 | if s.activeKeymapText() != old { | |
| 177 | 179 | s.sendKeymapAll(s.currentKeymap()) | |
| 178 | 180 | } | |
| 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) | |
| 184 | 191 | } | |
| 185 | 192 | s.log.Debug().Bool("virtual", k != nil).Msg("keymap switched") | |
| 186 | 193 | } | |
| @@ -22,7 +22,8 @@ type virtualKeyboard struct { | |||||
| 22 | server *Server | 22 | server *Server | ||
| 23 | fd int // sealed copy of the client keymap, -1 before one | 23 | fd int // sealed copy of the client keymap, -1 before one | ||
| 24 | size uint32 | 24 | 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 | |||
| 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]bool | 28 | pressed map[uint32]bool | ||
| 28 | focus ports.WindowID | 29 | 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) { | |||
| 69 | 69 | if err := target.Roundtrip(); err != nil { | |
| 70 | 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 | 73 | if !slices.Equal(proxy.opcodes, want) { | |
| 74 | 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 | 143 | k, m := uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventModifiers) | |
| 144 | 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 | 147 | if !slices.Equal(proxy.opcodes, want) { | |
| 147 | 148 | t.Fatalf("events %v, want %v", proxy.opcodes, want) | |
| 148 | 149 | } | |
| @@ -212,3 +213,48 @@ func TestVirtualKeyboardSameKeymapNotResent(t *testing.T) { | |||
| 212 | 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 | + | } | |
| @@ -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 | + | } | |||