1package wayland
2
3import (
4 "context"
5 "os"
6 "os/exec"
7 "regexp"
8 "slices"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/bnema/go-wayland-bindings/server/wayland"
14 "github.com/bnema/neferwl/internal/adapters/logging"
15 "github.com/bnema/neferwl/internal/ports"
16 "github.com/bnema/wlturbo"
17 "golang.org/x/sys/unix"
18)
19
20func keymapText(t *testing.T) string {
21 t.Helper()
22 data, err := os.ReadFile("testdata/us.xkb")
23 if err != nil {
24 t.Fatal(err)
25 }
26 return string(data)
27}
28
29// keyboardServer runs a server; setup, if any, runs before Run starts.
30func keyboardServer(t *testing.T, setup ...func(*Server)) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, string) {
31 t.Helper()
32 dir := t.TempDir()
33 events := make(chan ports.ClientEvent, 16)
34 commands := make(chan ports.ClientCommand, 32)
35 s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, Keymap: keymapText(t), RepeatRate: 25, RepeatDelay: 600}, Channels{Events: events, Commands: commands}, logging.For(context.Background(), "wayland"))
36 if err != nil {
37 t.Fatal(err)
38 }
39 for _, f := range setup {
40 f(s)
41 }
42 ctx, cancel := context.WithCancel(context.Background())
43 done := make(chan error, 1)
44 go func() { done <- s.Run(ctx) }()
45 t.Cleanup(func() {
46 cancel()
47 select {
48 case err := <-done:
49 if err != nil {
50 t.Errorf("Run: %v", err)
51 }
52 case <-time.After(2 * time.Second):
53 t.Error("Run hung")
54 }
55 })
56 return s, events, commands, dir
57}
58
59func TestKeyboardInfo(t *testing.T) {
60 tool, err := exec.LookPath("wayland-info")
61 if err != nil {
62 t.Skip("wayland-info not installed")
63 }
64 s, _, _, dir := keyboardServer(t)
65 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
66 defer cancel()
67 cmd := exec.CommandContext(ctx, tool)
68 cmd.Env = append(os.Environ(), "XDG_RUNTIME_DIR="+dir, "WAYLAND_DISPLAY="+s.SocketName())
69 out, err := cmd.CombinedOutput()
70 if err != nil {
71 t.Fatalf("wayland-info: %v: %s", err, out)
72 }
73 for _, want := range []string{"capabilities: pointer keyboard", "keyboard repeat rate: 25", "keyboard repeat delay: 600"} {
74 if !strings.Contains(string(out), want) {
75 t.Errorf("missing %q: %s", want, out)
76 }
77 }
78}
79
80type keymapProxy struct {
81 wlturbo.BaseProxy
82 result chan error
83}
84
85func (p *keymapProxy) Dispatch(e *wlturbo.Event) {
86 if e.Opcode != uint16(wayland.KeyboardEventKeymap) {
87 return
88 }
89 format := e.Uint32()
90 fd := int(e.Fd())
91 size := e.Uint32()
92 if fd <= 0 || size == 0 || format != uint32(wayland.KeyboardKeymapFormatXkbV1) {
93 p.result <- os.ErrInvalid
94 return
95 }
96 defer unix.Close(fd)
97 data := make([]byte, size)
98 _, err := unix.Pread(fd, data, 0)
99 if err == nil && !strings.HasPrefix(string(data), "xkb_keymap") {
100 err = os.ErrInvalid
101 }
102 p.result <- err
103}
104func TestKeyboardKeymapProtocol(t *testing.T) {
105 s, _, _, dir := keyboardServer(t)
106 c := protocolClient(t, s, dir)
107 seat := bindProtocol(t, c, "wl_seat")
108 registerProtocol(t, c, seat)
109 id := c.AllocateID()
110 proxy := &keymapProxy{result: make(chan error, 1)}
111 proxy.SetID(id)
112 registerWireProxy(c, proxy)
113 requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, id)
114 if err := c.Roundtrip(); err != nil {
115 t.Fatal(err)
116 }
117 select {
118 case err := <-proxy.result:
119 if err != nil {
120 t.Fatal(err)
121 }
122 default:
123 t.Fatal("missing keymap event")
124 }
125}
126
127// events records keyboard event opcodes; a keymap event also checks its content.
128type eventsProxy struct {
129 wlturbo.BaseProxy
130 opcodes []uint16
131 keymaps []string
132 locked []uint32 // locked modifiers of each modifiers event
133 depress []uint32 // depressed modifiers of each modifiers event
134 repeat chan struct{} // optional protocol barrier notification
135}
136
137func (p *eventsProxy) Dispatch(e *wlturbo.Event) {
138 p.opcodes = append(p.opcodes, e.Opcode)
139 if e.Opcode == uint16(wayland.KeyboardEventRepeatInfo) && p.repeat != nil {
140 p.repeat <- struct{}{}
141 }
142 if e.Opcode == uint16(wayland.KeyboardEventModifiers) {
143 e.Uint32() // serial
144 p.depress = append(p.depress, e.Uint32())
145 e.Uint32() // latched
146 p.locked = append(p.locked, e.Uint32())
147 return
148 }
149 if e.Opcode != uint16(wayland.KeyboardEventKeymap) {
150 return
151 }
152 e.Uint32()
153 fd := int(e.Fd())
154 size := e.Uint32()
155 defer unix.Close(fd)
156 data := make([]byte, size)
157 if _, err := unix.Pread(fd, data, 0); err == nil {
158 p.keymaps = append(p.keymaps, strings.TrimRight(string(data), "\x00"))
159 }
160}
161
162func TestSetKeymap(t *testing.T) {
163 s, _, commands, dir := keyboardServer(t)
164 c := protocolClient(t, s, dir)
165 // Version 4 so the keyboard receives repeat_info.
166 g, ok := c.Registry().FindGlobal("wl_seat")
167 if !ok {
168 t.Fatal("missing wl_seat")
169 }
170 seat, err := bindWireID(c, g.Name, g.Interface, 4)
171 if err != nil {
172 t.Fatal(err)
173 }
174 registerProtocol(t, c, seat)
175 id := c.AllocateID()
176 proxy := &eventsProxy{}
177 proxy.SetID(id)
178 registerWireProxy(c, proxy)
179 requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, id)
180 if err := c.Roundtrip(); err != nil {
181 t.Fatal(err)
182 }
183 proxy.opcodes, proxy.keymaps = nil, nil
184 sync := func() {
185 t.Helper()
186 // Commands apply on the display goroutine; wait until it has run them.
187 deadline := time.Now().Add(time.Second)
188 for len(commands) > 0 && time.Now().Before(deadline) {
189 time.Sleep(5 * time.Millisecond)
190 }
191 time.Sleep(20 * time.Millisecond)
192 if err := c.Roundtrip(); err != nil {
193 t.Fatal(err)
194 }
195 }
196
197 // Repeat-only: one repeat_info, no keymap.
198 commands <- ports.SetKeymap{RepeatRate: 40, RepeatDelay: 300}
199 sync()
200 if len(proxy.opcodes) != 1 || proxy.opcodes[0] != uint16(wayland.KeyboardEventRepeatInfo) {
201 t.Fatalf("repeat-only events %v", proxy.opcodes)
202 }
203
204 // New keymap: keymap then repeat_info, and the new content.
205 proxy.opcodes = nil
206 next := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap ", 1)
207 commands <- ports.SetKeymap{Keymap: next, RepeatRate: 40, RepeatDelay: 300}
208 sync()
209 want := []uint16{uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventRepeatInfo)}
210 if !slices.Equal(proxy.opcodes, want) {
211 t.Fatalf("keymap events %v, want %v", proxy.opcodes, want)
212 }
213 if len(proxy.keymaps) != 1 || proxy.keymaps[0] != next {
214 t.Fatalf("keymap content not replaced (%d keymaps)", len(proxy.keymaps))
215 }
216 // A keyboard created afterwards gets the new keymap.
217 id2 := c.AllocateID()
218 late := &eventsProxy{}
219 late.SetID(id2)
220 registerWireProxy(c, late)
221 requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, id2)
222 if err := c.Roundtrip(); err != nil {
223 t.Fatal(err)
224 }
225 if len(late.keymaps) != 1 || late.keymaps[0] != next {
226 t.Fatal("late keyboard got the old keymap")
227 }
228}
229
230func TestFootKeyboard(t *testing.T) {
231 tool, err := exec.LookPath("foot")
232 if err != nil {
233 t.Skip("foot not installed")
234 }
235 s, events, commands, dir := keyboardServer(t)
236 _, done, output := lifecycleClient(t, tool, s, dir, []string{"HOME=" + t.TempDir(), "WAYLAND_DEBUG=client"}, "-c", "/dev/null", "sh", "-c", "sleep 10")
237 w := mapped(t, events, 10*time.Second)
238 commands <- ports.FocusWindow{ID: w.ID}
239 for _, code := range []uint32{18, 46, 35, 24, 57, 35, 23, 28} {
240 commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Pressed: true, Time: 1 * time.Millisecond}}
241 commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Time: 2 * time.Millisecond}}
242 }
243 enter := regexp.MustCompile(`wl_keyboard#[0-9]+\.enter\(`)
244 key := regexp.MustCompile(`wl_keyboard#[0-9]+\.key\(`)
245 deadline := time.After(4 * time.Second)
246 tick := time.NewTicker(10 * time.Millisecond)
247 defer tick.Stop()
248 for {
249 select {
250 case err := <-done:
251 t.Fatalf("foot exited: %v: %s", err, output.String())
252 case <-deadline:
253 t.Fatalf("keyboard events missing: %s", output.String())
254 case <-tick.C:
255 text := output.String()
256 if enter.MatchString(text) && key.MatchString(text) {
257 return
258 }
259 }
260 }
261}
262
263// SetModifiers sends the requested depressed mask with the real latched and
264// locked state, and Restore sends the real state back; the seat's own state
265// is untouched. A window that is not focused is ignored.
266func TestSetModifiersOverrideAndRestore(t *testing.T) {
267 s, events, commands, dir := keyboardServer(t)
268 target, proxy := focusedTarget(t, s, events, commands, dir)
269 const capsLocked = 2
270 var id ports.WindowID
271 set := make(chan struct{})
272 s.display.Do(func() { s.seat.modState = ports.ModState{Locked: capsLocked}; id = s.seat.focused; close(set) })
273 <-set
274 wait := func() {
275 t.Helper()
276 syncCommands(t, s, commands, target)
277 }
278
279 proxy.depress, proxy.locked = nil, nil
280 commands <- ports.SetModifiers{ID: id + 100, Mods: ports.ModCtrl}
281 wait()
282 if len(proxy.depress) != 0 {
283 t.Fatalf("unfocused window got modifiers %v", proxy.depress)
284 }
285 commands <- ports.SetModifiers{ID: id, Mods: ports.ModCtrl | ports.ModShift}
286 wait()
287 if !slices.Equal(proxy.depress, []uint32{xkbControl | xkbShift}) || !slices.Equal(proxy.locked, []uint32{capsLocked}) {
288 t.Fatalf("override depressed %v locked %v", proxy.depress, proxy.locked)
289 }
290 done := make(chan ports.ModState, 1)
291 commands <- ports.SetModifiers{ID: id, Restore: true, State: ports.ModState{Locked: capsLocked}}
292 wait()
293 if !slices.Equal(proxy.depress, []uint32{xkbControl | xkbShift, 0}) || !slices.Equal(proxy.locked, []uint32{capsLocked, capsLocked}) {
294 t.Fatalf("restore depressed %v locked %v", proxy.depress, proxy.locked)
295 }
296 s.display.Do(func() { done <- s.seat.modState })
297 if m := <-done; m != (ports.ModState{Locked: capsLocked}) {
298 t.Fatalf("seat state changed: %+v", m)
299 }
300}
301
302// Restore carries the real state core saw, which swallowed keys (a latch a
303// hint label key consumed) never gave the seat: it is sent and adopted.
304func TestSetModifiersRestoreAdoptsCoreState(t *testing.T) {
305 s, events, commands, dir := keyboardServer(t)
306 target, proxy := focusedTarget(t, s, events, commands, dir)
307 const shiftLatched, capsLocked = 1, 2
308 var id ports.WindowID
309 set := make(chan struct{})
310 s.display.Do(func() { s.seat.modState = ports.ModState{Latched: shiftLatched}; id = s.seat.focused; close(set) })
311 <-set
312 proxy.depress, proxy.locked = nil, nil
313 want := ports.ModState{Locked: capsLocked} // the latch was consumed
314 commands <- ports.SetModifiers{ID: id, Restore: true, State: want}
315 var got ports.ModState
316 deadline := time.Now().Add(2 * time.Second)
317 for got != want {
318 if time.Now().After(deadline) {
319 t.Fatalf("seat state %+v, want %+v", got, want)
320 }
321 s.display.Do(func() { got = s.seat.modState })
322 time.Sleep(time.Millisecond)
323 }
324 roundtrip(t, target)
325 if !slices.Equal(proxy.locked, []uint32{capsLocked}) || !slices.Equal(proxy.depress, []uint32{0}) {
326 t.Fatalf("restore depressed %v locked %v", proxy.depress, proxy.locked)
327 }
328}
329
330// syncCommands observes an existing command through the real protocol. A
331// repeat_info emitted by SetKeymap means all preceding commands were applied;
332// merely emptying the channel or posting a display callback cannot prove that.
333func syncCommands(t *testing.T, s *Server, commands chan<- ports.ClientCommand, c *wlturbo.Display) {
334 t.Helper()
335 var needsKeymap bool
336 if !s.display.Do(func() { needsKeymap = s.seat.keymapFD < 0 }) {
337 t.Fatal("display stopped")
338 }
339 if needsKeymap {
340 keymap := keymapText(t)
341 if !s.display.Do(func() { s.setKeymap(ports.SetKeymap{Keymap: keymap}) }) {
342 t.Fatal("display stopped")
343 }
344 }
345 seat := bindVersion(t, c, "wl_seat", 4)
346 registerProtocol(t, c, seat)
347 id := c.AllocateID()
348 p := &eventsProxy{repeat: make(chan struct{}, 2)}
349 p.SetID(id)
350 registerWireProxy(c, p)
351 requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, id)
352 roundtrip(t, c)
353 select {
354 case <-p.repeat:
355 default:
356 t.Fatal("missing initial repeat_info")
357 }
358 var rate, delay int
359 if !s.display.Do(func() { rate, delay = s.seat.repeatRate, s.seat.repeatDelay }) {
360 t.Fatal("display stopped")
361 }
362 commands <- ports.SetKeymap{RepeatRate: rate, RepeatDelay: delay}
363 // Dispatch blocks on the protocol socket; close it on timeout to bound
364 // failure without polling the display or sleeping for command delivery.
365 timeout := time.AfterFunc(2*time.Second, func() { _ = c.Close() })
366 defer timeout.Stop()
367 for {
368 select {
369 case <-p.repeat:
370 requestProtocol(t, c, id, wayland.KeyboardRequestRelease)
371 roundtrip(t, c)
372 return
373 default:
374 }
375 if err := c.Dispatch(); err != nil {
376 t.Fatal(err)
377 }
378 }
379}