1package wayland
2
3import (
4 "errors"
5 "testing"
6 "time"
7
8 "github.com/bnema/go-wayland-bindings/server/extdatacontrol"
9 "github.com/bnema/go-wayland-bindings/server/primaryselection"
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// clipEvents records the selection offers sent to a data device.
17type clipEvents struct {
18 wlturbo.BaseProxy
19 c *wlturbo.Display
20 offers chan uint32 // selection offer ids; 0 clears
21}
22
23func (p *clipEvents) Dispatch(e *wlturbo.Event) {
24 switch e.Opcode {
25 case uint16(wayland.DataDeviceEventDataOffer):
26 // The new offer: register it so its offer events are accepted.
27 registerOffer(p.c, e.Uint32())
28 case uint16(wayland.DataDeviceEventSelection):
29 p.offers <- e.Uint32()
30 }
31}
32
33type sourceEvents struct {
34 wlturbo.BaseProxy
35 sends chan int
36 cancelled chan struct{}
37}
38
39func (p *sourceEvents) Dispatch(e *wlturbo.Event) {
40 switch e.Opcode {
41 case uint16(wayland.DataSourceEventSend):
42 _ = e.String()
43 fd := int(e.Fd())
44 _, _ = unix.Write(fd, []byte("pasted"))
45 _ = unix.Close(fd)
46 p.sends <- fd
47 case uint16(wayland.DataSourceEventCancelled):
48 p.cancelled <- struct{}{}
49 }
50}
51
52func registerOffer(c *wlturbo.Display, id uint32) {
53 p := &protocolProxy{}
54 p.SetID(id)
55 registerWireProxy(c, p)
56}
57
58// clipClient is a client with a toplevel, a data device and a pointer to
59// its clipboard globals.
60type clipClient struct {
61 c *wlturbo.Display
62 manager uint32
63 device *clipEvents
64 window ports.WindowID
65}
66
67func newClipClient(t *testing.T, s *Server, dir string, events chan ports.ClientEvent) *clipClient {
68 t.Helper()
69 c := protocolClient(t, s, dir)
70 seat := bindProtocol(t, c, "wl_seat")
71 registerProtocol(t, c, seat)
72 g, _ := c.Registry().FindGlobal("wl_data_device_manager")
73 manager, err := bindWireID(c, g.Name, g.Interface, 3)
74 if err != nil {
75 t.Fatal(err)
76 }
77 device := &clipEvents{c: c, offers: make(chan uint32, 8)}
78 device.SetID(c.AllocateID())
79 registerWireProxy(c, device)
80 requestProtocol(t, c, manager, wayland.DataDeviceManagerRequestGetDataDevice, device.ID(), seat)
81 w := toplevelMapper(t, c, events)()
82 return &clipClient{c: c, manager: manager, device: device, window: w.ID}
83}
84
85// selection returns the next selection offer id sent to the client.
86func (cc *clipClient) selection(t *testing.T) (uint32, bool) {
87 t.Helper()
88 if err := cc.c.Roundtrip(); err != nil {
89 t.Fatal(err)
90 }
91 select {
92 case id := <-cc.device.offers:
93 return id, true
94 case <-time.After(100 * time.Millisecond):
95 return 0, false
96 }
97}
98
99func (cc *clipClient) copy(t *testing.T) *sourceEvents {
100 t.Helper()
101 src := &sourceEvents{sends: make(chan int, 4), cancelled: make(chan struct{}, 4)}
102 src.SetID(cc.c.AllocateID())
103 registerWireProxy(cc.c, src)
104 requestProtocol(t, cc.c, cc.manager, wayland.DataDeviceManagerRequestCreateDataSource, src.ID())
105 requestProtocol(t, cc.c, src.ID(), wayland.DataSourceRequestOffer, "text/plain")
106 requestProtocol(t, cc.c, cc.device.ID(), wayland.DataDeviceRequestSetSelection, src.ID(), uint32(0))
107 if err := cc.c.Roundtrip(); err != nil {
108 t.Fatal(err)
109 }
110 return src
111}
112
113// focus gives cc keyboard focus and waits for the selection it gets then.
114func (cc *clipClient) focus(t *testing.T, commands chan<- ports.ClientCommand) uint32 {
115 t.Helper()
116 commands <- ports.FocusWindow{ID: cc.window}
117 deadline := time.After(2 * time.Second)
118 for {
119 if id, ok := cc.selection(t); ok {
120 return id
121 }
122 select {
123 case <-deadline:
124 t.Fatal("no selection on focus")
125 default:
126 }
127 }
128}
129
130// The clipboard follows keyboard focus: only the focused client may set
131// it, and a client gets it when it gains focus; pasting reaches the source.
132func TestClipboardFollowsFocus(t *testing.T) {
133 s, events, commands, dir := keyboardServer(t)
134 a := newClipClient(t, s, dir, events)
135 b := newClipClient(t, s, dir, events)
136 a.focus(t, commands)
137
138 // An unfocused client cannot set the selection: its source is cancelled.
139 stolen := b.copy(t)
140 select {
141 case <-stolen.cancelled:
142 case <-time.After(2 * time.Second):
143 t.Fatal("unfocused source not cancelled")
144 }
145
146 src := a.copy(t)
147 offer, ok := a.selection(t)
148 if !ok || offer == 0 {
149 t.Fatalf("focused client got no offer: %d %v", offer, ok)
150 }
151 if id, ok := b.selection(t); ok {
152 t.Fatalf("unfocused client got selection %d", id)
153 }
154
155 // Focus moves to b: b gets the offer and can paste from a.
156 if offer = b.focus(t, commands); offer == 0 {
157 t.Fatal("no offer on focus")
158 }
159 var fds [2]int
160 if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil {
161 t.Fatal(err)
162 }
163 defer unix.Close(fds[0])
164 if err := wireRequest(b.c, offer, uint16(wayland.DataOfferRequestReceive), []int{fds[1]}, "text/plain"); err != nil {
165 t.Fatal(err)
166 }
167 _ = unix.Close(fds[1])
168 if err := b.c.Roundtrip(); err != nil {
169 t.Fatal(err)
170 }
171 // a answers the send event.
172 deadline := time.After(2 * time.Second)
173 for len(src.sends) == 0 {
174 if err := a.c.Roundtrip(); err != nil {
175 t.Fatal(err)
176 }
177 select {
178 case <-deadline:
179 t.Fatal("source never asked to send")
180 case <-time.After(5 * time.Millisecond):
181 }
182 }
183 buf := make([]byte, 16)
184 // Go runtime signals (preemption) can interrupt the raw read: retry.
185 n, err := unix.Read(fds[0], buf)
186 for errors.Is(err, unix.EINTR) {
187 n, err = unix.Read(fds[0], buf)
188 }
189 if err != nil {
190 t.Fatal(err)
191 }
192 if string(buf[:n]) != "pasted" {
193 t.Fatalf("pasted %q", buf[:n])
194 }
195}
196
197// A clipboard manager (ext_data_control_v1) sees the selection without
198// focus, and a destroyed source clears it.
199func TestDataControlSeesSelection(t *testing.T) {
200 s, events, commands, dir := keyboardServer(t)
201 a := newClipClient(t, s, dir, events)
202 a.focus(t, commands)
203
204 m := protocolClient(t, s, dir)
205 seat := bindProtocol(t, m, "wl_seat")
206 registerProtocol(t, m, seat)
207 manager := bindProtocol(t, m, "ext_data_control_manager_v1")
208 offers := make(chan uint32, 8)
209 dev := &controlEvents{c: m, offers: offers}
210 dev.SetID(m.AllocateID())
211 registerWireProxy(m, dev)
212 requestProtocol(t, m, manager, extdatacontrol.ExtDataControlManagerV1RequestGetDataDevice, dev.ID(), seat)
213 next := func() uint32 {
214 t.Helper()
215 deadline := time.After(2 * time.Second)
216 for {
217 if err := m.Roundtrip(); err != nil {
218 t.Fatal(err)
219 }
220 select {
221 case id := <-offers:
222 return id
223 case <-deadline:
224 t.Fatal("no selection event")
225 case <-time.After(5 * time.Millisecond):
226 }
227 }
228 }
229 if id := next(); id != 0 {
230 t.Fatalf("initial selection %d, want none", id)
231 }
232 src := a.copy(t)
233 if id := next(); id == 0 {
234 t.Fatal("manager did not see the copy")
235 }
236 requestProtocol(t, a.c, src.ID(), wayland.DataSourceRequestDestroy)
237 if err := a.c.Roundtrip(); err != nil {
238 t.Fatal(err)
239 }
240 if id := next(); id != 0 {
241 t.Fatalf("selection %d after its source died", id)
242 }
243}
244
245type controlEvents struct {
246 wlturbo.BaseProxy
247 c *wlturbo.Display
248 offers chan uint32
249}
250
251func (p *controlEvents) Dispatch(e *wlturbo.Event) {
252 switch e.Opcode {
253 case uint16(extdatacontrol.ExtDataControlDeviceV1EventDataOffer):
254 registerOffer(p.c, e.Uint32())
255 case uint16(extdatacontrol.ExtDataControlDeviceV1EventSelection):
256 p.offers <- e.Uint32()
257 }
258}
259
260// A new copy cancels the previous source; setting the current source
261// again changes nothing.
262func TestClipboardReplaceCancelsOldSource(t *testing.T) {
263 s, events, commands, dir := keyboardServer(t)
264 a := newClipClient(t, s, dir, events)
265 a.focus(t, commands)
266 first := a.copy(t)
267 if id, ok := a.selection(t); !ok || id == 0 {
268 t.Fatal("no offer for the first copy")
269 }
270 requestProtocol(t, a.c, a.device.ID(), wayland.DataDeviceRequestSetSelection, first.ID(), uint32(0))
271 if id, ok := a.selection(t); ok {
272 t.Fatalf("setting the same source again sent offer %d", id)
273 }
274 if len(first.cancelled) != 0 {
275 t.Fatal("current source cancelled when set again")
276 }
277 a.copy(t)
278 if err := a.c.Roundtrip(); err != nil {
279 t.Fatal(err)
280 }
281 select {
282 case <-first.cancelled:
283 case <-time.After(2 * time.Second):
284 t.Fatal("replaced source not cancelled")
285 }
286}
287
288// A data-control source can be set once.
289func TestDataControlUsedSource(t *testing.T) {
290 s, _, _, dir := keyboardServer(t)
291 m := protocolClient(t, s, dir)
292 seat := bindProtocol(t, m, "wl_seat")
293 registerProtocol(t, m, seat)
294 manager := bindProtocol(t, m, "ext_data_control_manager_v1")
295 dev := &controlEvents{c: m, offers: make(chan uint32, 8)}
296 dev.SetID(m.AllocateID())
297 registerWireProxy(m, dev)
298 requestProtocol(t, m, manager, extdatacontrol.ExtDataControlManagerV1RequestGetDataDevice, dev.ID(), seat)
299 src := m.AllocateID()
300 registerProtocol(t, m, src)
301 requestProtocol(t, m, manager, extdatacontrol.ExtDataControlManagerV1RequestCreateDataSource, src)
302 requestProtocol(t, m, src, extdatacontrol.ExtDataControlSourceV1RequestOffer, "text/plain")
303 requestProtocol(t, m, dev.ID(), extdatacontrol.ExtDataControlDeviceV1RequestSetSelection, src)
304 requestProtocol(t, m, dev.ID(), extdatacontrol.ExtDataControlDeviceV1RequestSetPrimarySelection, src)
305 expectProtocolError(t, m, dev.ID(), uint32(extdatacontrol.ExtDataControlDeviceV1ErrorUsedSource))
306}
307
308type primaryEvents struct {
309 wlturbo.BaseProxy
310 c *wlturbo.Display
311 offers chan uint32
312}
313
314func (p *primaryEvents) Dispatch(e *wlturbo.Event) {
315 switch e.Opcode {
316 case uint16(primaryselection.ZwpPrimarySelectionDeviceV1EventDataOffer):
317 registerOffer(p.c, e.Uint32())
318 case uint16(primaryselection.ZwpPrimarySelectionDeviceV1EventSelection):
319 p.offers <- e.Uint32()
320 }
321}
322
323// A clipboard manager setting the primary selection reaches the focused
324// client's primary selection device.
325func TestPrimarySelectionFromManager(t *testing.T) {
326 s, events, commands, dir := keyboardServer(t)
327 a := newClipClient(t, s, dir, events)
328 pm := bindProtocol(t, a.c, "zwp_primary_selection_device_manager_v1")
329 seat := bindProtocol(t, a.c, "wl_seat")
330 registerProtocol(t, a.c, seat)
331 pdev := &primaryEvents{c: a.c, offers: make(chan uint32, 8)}
332 pdev.SetID(a.c.AllocateID())
333 registerWireProxy(a.c, pdev)
334 requestProtocol(t, a.c, pm, primaryselection.ZwpPrimarySelectionDeviceManagerV1RequestGetDevice, pdev.ID(), seat)
335 a.focus(t, commands)
336
337 m := protocolClient(t, s, dir)
338 mseat := bindProtocol(t, m, "wl_seat")
339 registerProtocol(t, m, mseat)
340 manager := bindProtocol(t, m, "ext_data_control_manager_v1")
341 dev := &controlEvents{c: m, offers: make(chan uint32, 8)}
342 dev.SetID(m.AllocateID())
343 registerWireProxy(m, dev)
344 requestProtocol(t, m, manager, extdatacontrol.ExtDataControlManagerV1RequestGetDataDevice, dev.ID(), mseat)
345 src := m.AllocateID()
346 registerProtocol(t, m, src)
347 requestProtocol(t, m, manager, extdatacontrol.ExtDataControlManagerV1RequestCreateDataSource, src)
348 requestProtocol(t, m, src, extdatacontrol.ExtDataControlSourceV1RequestOffer, "text/plain")
349 requestProtocol(t, m, dev.ID(), extdatacontrol.ExtDataControlDeviceV1RequestSetPrimarySelection, src)
350 if err := m.Roundtrip(); err != nil {
351 t.Fatal(err)
352 }
353 deadline := time.After(2 * time.Second)
354 for {
355 if err := a.c.Roundtrip(); err != nil {
356 t.Fatal(err)
357 }
358 select {
359 case id := <-pdev.offers:
360 if id != 0 {
361 return
362 }
363 case <-deadline:
364 t.Fatal("focused client got no primary selection")
365 case <-time.After(5 * time.Millisecond):
366 }
367 }
368}