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}
134
135func (p *eventsProxy) Dispatch(e *wlturbo.Event) {
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 }
144 if e.Opcode != uint16(wayland.KeyboardEventKeymap) {
145 return
146 }
147 e.Uint32()
148 fd := int(e.Fd())
149 size := e.Uint32()
150 defer unix.Close(fd)
151 data := make([]byte, size)
152 if _, err := unix.Pread(fd, data, 0); err == nil {
153 p.keymaps = append(p.keymaps, strings.TrimRight(string(data), "\x00"))
154 }
155}
156
157func TestSetKeymap(t *testing.T) {
158 s, _, commands, dir := keyboardServer(t)
159 c := protocolClient(t, s, dir)
160 // Version 4 so the keyboard receives repeat_info.
161 g, ok := c.Registry().FindGlobal("wl_seat")
162 if !ok {
163 t.Fatal("missing wl_seat")
164 }
165 seat, err := bindWireID(c, g.Name, g.Interface, 4)
166 if err != nil {
167 t.Fatal(err)
168 }
169 registerProtocol(t, c, seat)
170 id := c.AllocateID()
171 proxy := &eventsProxy{}
172 proxy.SetID(id)
173 registerWireProxy(c, proxy)
174 requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, id)
175 if err := c.Roundtrip(); err != nil {
176 t.Fatal(err)
177 }
178 proxy.opcodes, proxy.keymaps = nil, nil
179 sync := func() {
180 t.Helper()
181 // Commands apply on the display goroutine; wait until it has run them.
182 deadline := time.Now().Add(time.Second)
183 for len(commands) > 0 && time.Now().Before(deadline) {
184 time.Sleep(5 * time.Millisecond)
185 }
186 time.Sleep(20 * time.Millisecond)
187 if err := c.Roundtrip(); err != nil {
188 t.Fatal(err)
189 }
190 }
191
192 // Repeat-only: one repeat_info, no keymap.
193 commands <- ports.SetKeymap{RepeatRate: 40, RepeatDelay: 300}
194 sync()
195 if len(proxy.opcodes) != 1 || proxy.opcodes[0] != uint16(wayland.KeyboardEventRepeatInfo) {
196 t.Fatalf("repeat-only events %v", proxy.opcodes)
197 }
198
199 // New keymap: keymap then repeat_info, and the new content.
200 proxy.opcodes = nil
201 next := strings.Replace(keymapText(t), "xkb_keymap", "xkb_keymap ", 1)
202 commands <- ports.SetKeymap{Keymap: next, RepeatRate: 40, RepeatDelay: 300}
203 sync()
204 want := []uint16{uint16(wayland.KeyboardEventKeymap), uint16(wayland.KeyboardEventRepeatInfo)}
205 if !slices.Equal(proxy.opcodes, want) {
206 t.Fatalf("keymap events %v, want %v", proxy.opcodes, want)
207 }
208 if len(proxy.keymaps) != 1 || proxy.keymaps[0] != next {
209 t.Fatalf("keymap content not replaced (%d keymaps)", len(proxy.keymaps))
210 }
211 // A keyboard created afterwards gets the new keymap.
212 id2 := c.AllocateID()
213 late := &eventsProxy{}
214 late.SetID(id2)
215 registerWireProxy(c, late)
216 requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, id2)
217 if err := c.Roundtrip(); err != nil {
218 t.Fatal(err)
219 }
220 if len(late.keymaps) != 1 || late.keymaps[0] != next {
221 t.Fatal("late keyboard got the old keymap")
222 }
223}
224
225func TestFootKeyboard(t *testing.T) {
226 tool, err := exec.LookPath("foot")
227 if err != nil {
228 t.Skip("foot not installed")
229 }
230 s, events, commands, dir := keyboardServer(t)
231 _, done, output := lifecycleClient(t, tool, s, dir, []string{"HOME=" + t.TempDir(), "WAYLAND_DEBUG=client"}, "-c", "/dev/null", "sh", "-c", "sleep 10")
232 w := mapped(t, events, 10*time.Second)
233 commands <- ports.FocusWindow{ID: w.ID}
234 for _, code := range []uint32{18, 46, 35, 24, 57, 35, 23, 28} {
235 commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Pressed: true, Time: 1 * time.Millisecond}}
236 commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: code, Time: 2 * time.Millisecond}}
237 }
238 enter := regexp.MustCompile(`wl_keyboard#[0-9]+\.enter\(`)
239 key := regexp.MustCompile(`wl_keyboard#[0-9]+\.key\(`)
240 deadline := time.After(4 * time.Second)
241 tick := time.NewTicker(10 * time.Millisecond)
242 defer tick.Stop()
243 for {
244 select {
245 case err := <-done:
246 t.Fatalf("foot exited: %v: %s", err, output.String())
247 case <-deadline:
248 t.Fatalf("keyboard events missing: %s", output.String())
249 case <-tick.C:
250 text := output.String()
251 if enter.MatchString(text) && key.MatchString(text) {
252 return
253 }
254 }
255 }
256}