1package wayland
2
3import (
4 "slices"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/bnema/go-wayland-bindings/server/virtualkeyboard"
10 "github.com/bnema/go-wayland-bindings/server/wayland"
11 "github.com/bnema/neferwl/internal/ports"
12 "github.com/bnema/wlturbo"
13 "golang.org/x/sys/unix"
14)
15
16// virtualTyper binds a virtual keyboard on its own connection, as wtype does.
17func virtualTyper(t *testing.T, s *Server, dir string) (*wlturbo.Display, uint32) {
18 t.Helper()
19 c := protocolClient(t, s, dir)
20 seat := bindProtocol(t, c, "wl_seat")
21 registerProtocol(t, c, seat)
22 manager := bindProtocol(t, c, "zwp_virtual_keyboard_manager_v1")
23 registerProtocol(t, c, manager)
24 kb := c.AllocateID()
25 registerProtocol(t, c, kb)
26 requestProtocol(t, c, manager, virtualkeyboard.ZwpVirtualKeyboardManagerV1RequestCreateVirtualKeyboard, seat, kb)
27 return c, kb
28}
29
30func sendVirtualKeymap(t *testing.T, c *wlturbo.Display, kb uint32, keymap string) {
31 t.Helper()
32 fd, size, err := keymapFile(keymap)
33 if err != nil {
34 t.Fatal(err)
35 }
36 defer unix.Close(fd)
37 if err := wireRequest(c, kb, uint16(virtualkeyboard.ZwpVirtualKeyboardV1RequestKeymap), []int{fd}, uint32(wayland.KeyboardKeymapFormatXkbV1), size); err != nil {
38 t.Fatal(err)
39 }
40}
41
42// Virtual keys reach the focused client under the virtual keymap; the next
43// real key switches it back to the seat keymap.
44func TestVirtualKeyboardTypesIntoFocus(t *testing.T) {
45 s, events, commands, dir := keyboardServer(t)
46 target := protocolClient(t, s, dir)
47 seat := bindProtocol(t, target, "wl_seat")
48 registerProtocol(t, target, seat)
49 proxy := &eventsProxy{}
50 proxy.SetID(target.AllocateID())
51 registerWireProxy(target, proxy)
52 requestProtocol(t, target, seat, wayland.SeatRequestGetKeyboard, proxy.ID())
53 w := toplevelMapper(t, target, events)()
54 commands <- ports.FocusWindow{ID: w.ID}
55 waitFocus(t, s, w.ID)
56 if err := target.Roundtrip(); err != nil {
57 t.Fatal(err)
58 }
59 proxy.opcodes, proxy.keymaps = nil, nil
60
61 typer, kb := virtualTyper(t, s, dir)
62 virtual := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap ", 1)
63 sendVirtualKeymap(t, typer, kb, virtual)
64 requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
65 requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(2), uint32(30), uint32(0))
66 if err := typer.Roundtrip(); err != nil {
67 t.Fatal(err)
68 }
69 if err := target.Roundtrip(); err != nil {
70 t.Fatal(err)
71 }
72 want := []uint16{uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventModifiers), uint16(wayland.KeyboardEventKey), uint16(wayland.KeyboardEventKey)}
73 if !slices.Equal(proxy.opcodes, want) {
74 t.Fatalf("virtual events %v, want %v", proxy.opcodes, want)
75 }
76 if len(proxy.keymaps) != 1 || proxy.keymaps[0] != virtual {
77 t.Fatal("focused client did not get the virtual keymap")
78 }
79
80 proxy.opcodes, proxy.keymaps = nil, nil
81 commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, TimeMsec: 3}}
82 deadline := time.Now().Add(2 * time.Second)
83 for !slices.Contains(proxy.opcodes, uint16(wayland.KeyboardEventKey)) && time.Now().Before(deadline) {
84 if err := target.Roundtrip(); err != nil {
85 t.Fatal(err)
86 }
87 time.Sleep(5 * time.Millisecond)
88 }
89 want = []uint16{uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventModifiers), uint16(wayland.KeyboardEventKey)}
90 if !slices.Equal(proxy.opcodes, want) {
91 t.Fatalf("real key events %v, want %v", proxy.opcodes, want)
92 }
93 if len(proxy.keymaps) != 1 || proxy.keymaps[0] != keymapText(t) {
94 t.Fatal("seat keymap not restored")
95 }
96}
97
98// focusedTarget maps a focused toplevel with one keyboard and returns its
99// connection and recorder, drained of setup events.
100func focusedTarget(t *testing.T, s *Server, events chan ports.ClientEvent, commands chan ports.ClientCommand, dir string) (*wlturbo.Display, *eventsProxy) {
101 t.Helper()
102 target := protocolClient(t, s, dir)
103 seat := bindProtocol(t, target, "wl_seat")
104 registerProtocol(t, target, seat)
105 proxy := &eventsProxy{}
106 proxy.SetID(target.AllocateID())
107 registerWireProxy(target, proxy)
108 requestProtocol(t, target, seat, wayland.SeatRequestGetKeyboard, proxy.ID())
109 w := toplevelMapper(t, target, events)()
110 commands <- ports.FocusWindow{ID: w.ID}
111 waitFocus(t, s, w.ID)
112 if err := target.Roundtrip(); err != nil {
113 t.Fatal(err)
114 }
115 proxy.opcodes, proxy.keymaps = nil, nil
116 return target, proxy
117}
118
119func roundtrip(t *testing.T, cs ...*wlturbo.Display) {
120 t.Helper()
121 for _, c := range cs {
122 if err := c.Roundtrip(); err != nil {
123 t.Fatal(err)
124 }
125 }
126}
127
128// A typer dying mid-key releases the key and restores the seat keymap.
129func TestVirtualKeyboardDisconnectReleases(t *testing.T) {
130 s, events, commands, dir := keyboardServer(t)
131 target, proxy := focusedTarget(t, s, events, commands, dir)
132 typer, kb := virtualTyper(t, s, dir)
133 sendVirtualKeymap(t, typer, kb, strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap ", 1))
134 requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestModifiers, uint32(1), uint32(0), uint32(0), uint32(0))
135 requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
136 roundtrip(t, typer)
137 _ = typer.Close()
138 deadline := time.Now().Add(2 * time.Second)
139 for len(proxy.keymaps) < 2 && time.Now().Before(deadline) {
140 roundtrip(t, target)
141 time.Sleep(5 * time.Millisecond)
142 }
143 k, m := uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventModifiers)
144 key := uint16(wayland.KeyboardEventKey)
145 // The Modifiers request comes after the switch's own modifiers event.
146 want := []uint16{k, m, m, key, key, k, m}
147 if !slices.Equal(proxy.opcodes, want) {
148 t.Fatalf("events %v, want %v", proxy.opcodes, want)
149 }
150 if proxy.keymaps[1] != keymapText(t) {
151 t.Fatal("seat keymap not restored")
152 }
153}
154
155// A keyboard created while a typer owns the keymap starts with it; a
156// malformed keymap is ignored and the last good one stays.
157func TestVirtualKeymapLateKeyboardAndRejected(t *testing.T) {
158 s, events, commands, dir := keyboardServer(t)
159 target, _ := focusedTarget(t, s, events, commands, dir)
160 typer, kb := virtualTyper(t, s, dir)
161 virtual := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap ", 1)
162 sendVirtualKeymap(t, typer, kb, virtual)
163 sendVirtualKeymap(t, typer, kb, "not a keymap")
164 requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
165 roundtrip(t, typer, target)
166 seat := bindProtocol(t, target, "wl_seat")
167 registerProtocol(t, target, seat)
168 late := &eventsProxy{}
169 late.SetID(target.AllocateID())
170 registerWireProxy(target, late)
171 requestProtocol(t, target, seat, wayland.SeatRequestGetKeyboard, late.ID())
172 roundtrip(t, target)
173 if len(late.keymaps) != 1 || late.keymaps[0] != virtual {
174 t.Fatal("late keyboard did not get the virtual keymap")
175 }
176}
177
178func TestVirtualKeyBeforeKeymap(t *testing.T) {
179 s, _, _, dir := keyboardServer(t)
180 typer, kb := virtualTyper(t, s, dir)
181 requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(30), uint32(1))
182 expectProtocolError(t, typer, kb, uint32(virtualkeyboard.ZwpVirtualKeyboardV1ErrorNoKeymap))
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.
187func 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.
220func 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.seat.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.
240func 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}
261
262// smallKeymap is a dictation tool keymap: only the keys it types.
263const smallKeymap = `xkb_keymap {
264xkb_keycodes "(unnamed)" {
265minimum = 8;
266maximum = 26;
267<K1> = 9;
268};
269xkb_types "(unnamed)" { include "complete" };
270xkb_compatibility "(unnamed)" { include "complete" };
271xkb_symbols "(unnamed)" {
272key <K1> {[D]};
273};
274};
275`
276
277func TestWidenKeycodes(t *testing.T) {
278 const tail = "\nxkb_symbols { key <K1> {[D]}; };\n};\n"
279 cases := []struct {
280 name, in, want string
281 from int
282 }{
283 {"small maximum", smallKeymap, strings.Replace(smallKeymap, "maximum = 26;", "maximum = 255;", 1), 26},
284 {"maximum 127..254", "xkb_keymap {\nxkb_keycodes { minimum = 8; maximum = 200; <K1> = 9; };" + tail,
285 "xkb_keymap {\nxkb_keycodes { minimum = 8; maximum = 255; <K1> = 9; };" + tail, 200},
286 {"no maximum", "xkb_keymap {\nxkb_keycodes \"x\" { minimum = 8; <K1> = 9; <K2> = 25; };" + tail,
287 "xkb_keymap {\nxkb_keycodes \"x\" { maximum = 255; minimum = 8; <K1> = 9; <K2> = 25; };" + tail, 25},
288 {"mixed case", "XKB_Keymap {\nXkb_KeyCodes { MINIMUM = 8; Maximum = 26; <K1> = 9; };" + tail,
289 "XKB_Keymap {\nXkb_KeyCodes { MINIMUM = 8; Maximum = 255; <K1> = 9; };" + tail, 26},
290 {"brace in comment", "xkb_keymap {\nxkb_keycodes {\n// ends here }\n# and } here\nminimum = 8;\nmaximum = 26;\n<K1> = 9;\n};" + tail,
291 "xkb_keymap {\nxkb_keycodes {\n// ends here }\n# and } here\nminimum = 8;\nmaximum = 255;\n<K1> = 9;\n};" + tail, 26},
292 {"commented maximum", "xkb_keymap {\nxkb_keycodes {\n// maximum = 26;\nminimum = 8;\n<K1> = 9;\n};" + tail,
293 "xkb_keymap {\nxkb_keycodes { maximum = 255;\n// maximum = 26;\nminimum = 8;\n<K1> = 9;\n};" + tail, 9},
294 {"maximum after comment", "xkb_keymap {\nxkb_keycodes {\nminimum = 8;\nmaximum = // why\n 26;\n<K1> = 9;\n};" + tail,
295 "xkb_keymap {\nxkb_keycodes {\nminimum = 8;\nmaximum = // why\n 255;\n<K1> = 9;\n};" + tail, 26},
296 }
297 for _, c := range cases {
298 got, from := widenKeycodes(c.in)
299 if got != c.want || from != c.from {
300 t.Errorf("%s: from %d want %d\n%s", c.name, from, c.from, got)
301 }
302 }
303 for name, keep := range map[string]string{
304 "seat keymap": keymapText(t),
305 "maximum 255": "xkb_keymap { xkb_keycodes { maximum = 255; <K1> = 9; }; };",
306 "maximum above 255": "xkb_keymap { xkb_keycodes { minimum = 8; maximum = 708; <K1> = 9; }; };",
307 "no keycodes": "xkb_keymap { xkb_symbols { }; };",
308 "includes": "xkb_keymap { xkb_keycodes { include \"evdev+aliases(qwerty)\" }; };",
309 "maximum in string": "xkb_keymap { xkb_keycodes \"maximum = 26;\" { minimum = 8; <K1> = 300; }; };",
310 } {
311 if got, from := widenKeycodes(keep); from != 0 || got != keep {
312 t.Errorf("%s changed:\n%s", name, got)
313 }
314 }
315}
316
317// Xwayland overflows its key maps on a keymap whose maximum keycode is
318// small: clients get the virtual keymap with the full keycode range.
319func TestVirtualKeymapWidenedForClients(t *testing.T) {
320 s, events, commands, dir := keyboardServer(t)
321 target, proxy := focusedTarget(t, s, events, commands, dir)
322 typer, kb := virtualTyper(t, s, dir)
323 sendVirtualKeymap(t, typer, kb, smallKeymap)
324 requestProtocol(t, typer, kb, virtualkeyboard.ZwpVirtualKeyboardV1RequestKey, uint32(1), uint32(1), uint32(1))
325 roundtrip(t, typer, target)
326 deadline := time.Now().Add(2 * time.Second)
327 for len(proxy.keymaps) < 1 && time.Now().Before(deadline) {
328 roundtrip(t, target)
329 time.Sleep(5 * time.Millisecond)
330 }
331 if n := len(proxy.keymaps); n == 0 || !strings.Contains(proxy.keymaps[n-1], "maximum = 255;") || strings.Contains(proxy.keymaps[n-1], "maximum = 26;") {
332 t.Fatalf("keymaps %q", proxy.keymaps)
333 }
334}