#21 WIP: feat(xwayland): add built-in Xwayland launcher

state
draft
branch
feat/xwayland-builtin → main
opened
bnema ·
updated
CommitSubjectAuthorDateSignatureStatus
8545a93Merge branch 'fix/hdr-sdr-plane' into feat/xwayland-builtinbriceGPG Verified
8342bf4Merge branch 'fix/x11-fullscreen-restore' into feat/xwayland-builtinbriceGPG Verified
7f7b429fix(drm): compose SDR buffers on HDR outputs until the plane pipeline is verifiedbriceGPG Verified
61f9ab3fix(core): restore fullscreen when focus returns to a window that left itbriceGPG Verified
41da69cfix(core): honour the fullscreen state a window maps withbriceGPG Verified
5eb9885test(xwm): make sinkPeer deterministicbriceGPG Verified
13ff6acfix(xwm): announce scale 1 to X11 clients while Xwayland runs at logical sizebriceGPG Verified
f2f4074test(xwm): measure the Wayland-to-X alloc path through onSelectionRequestbriceGPG Verified
04b38e5feat(xwm): answer TIMESTAMP and list it in TARGETSbriceGPG Verified
6168668fix(xwm): harden selection transfers on close, late replies and theme truncationbriceGPG Verified
8fb84bfrefactor(xwm): replace addFD/after function seams with a mockery-generated loop interfacebriceGPG Verified
07dab5dfix(xwm): never adopt the XSETTINGS window as a client windowbriceGPG Verified
a50e31efix(xwm): drop stale TARGETS replies after Wayland takes ownershipbriceGPG Verified
4afca7adocs(xwayland): clipboard bridge, XSETTINGS and root cursorbriceGPG Verified
def433btest(app): e2e clipboard bridge, XSETTINGS and root cursor on the built-in XWMbriceGPG Verified
3e19081Merge branch 'feat/x10' into feat/xwayland-builtinbriceGPG Verified
d7ef3dbMerge branch 'feat/x9' into feat/xwayland-builtinbriceGPG Verified
9bbf554feat(xwm): XSETTINGS manager, xwayland.xsettings key and root cursorbriceGPG Verified
46fd2a1Merge branch 'feat/x8' into feat/xwayland-builtinbriceGPG Verified
1f67695feat(wayland): Wayland to X text selection with INCR and X-owned clipboard sourcesbriceGPG Verified
3df83c3docs(xwayland): describe fullscreen, keyboard grab, output hint and steam_app rulesbriceGPG Verified
8c66911feat(xwm): X to Wayland text selection transfers with INCR, buffer pool and timeoutsbriceGPG Verified
ad55fe8feat(xwm): host timer seam After for selection timeoutsbriceGPG Verified
41edcc9feat(xwm): selection window, XFixes ownership events and TARGETS probebriceGPG Verified
d71e4a4test(app): cover X11 fullscreen, keyboard grab, output hint and class rules end to endbriceGPG Verified
43a1ce7test(wayland): wait for the output change before counting surface entersbriceGPG Verified
1b0bfc7fix(core): keep rule workspaces ahead of the X11 output hint and read the keyboard grab once per syncbriceGPG Verified
56a0abdMerge branch 'feat/x8-pointer' into feat/x8briceGPG Verified
b77ab13Merge branch 'feat/x8-grab' into feat/x8briceGPG Verified
224cf30feat(xwm): place output-sized X11 windows on their output, read bypass hint, steam_app rulesbriceGPG Verified
c5b7312docs(xwayland): state that builtin shows menus, popups and tooltipsbriceGPG Verified
864c005test(wayland): cover X11 pointer lock, confine, relative motion and single-output enterbriceGPG Verified
65eceabfeat(wayland): serve zwp_xwayland_keyboard_grab as a fullscreen shortcuts inhibitorbriceGPG Verified
47739datest(app): cover override-redirect X11 windows end to endbriceGPG Verified
dc722bbrefactor(xwm): share one Space with the wayland server and drop dead codebriceGPG Verified
8ab1d07fix(core): clear pointer and grab when an unmanaged window unmapsbriceGPG Verified
b3f6b3afeat(wayland): host override-redirect X11 windows as unmanaged windowsbriceGPG Verified
27b8b9eMerge branch 'feat/x7-xdgoutput' into feat/x7briceGPG Verified
83719a6Merge branch 'feat/x7-core' into feat/x7briceGPG Verified
7ef926ffeat(xwm): convert Place through Space and track override-redirect windowsbriceGPG Verified
5a832c8feat(xwm): add Space mapping layout coordinates to X coordinatesbriceGPG Verified
305c2c8feat(wayland): private X-space xdg-output manager for XwaylandbriceGPG Verified
bf24250feat(core): place, draw and hit-test unmanaged X11 windowsbriceGPG Verified
58a7346refactor(xwm): log counters on close, drop the test-only Slot lookup and state atoms in present tensebriceGPG Verified
80258b6docs(xwayland): describe the built-in window manager and the xwayland e2e checkbriceGPG Verified
b64a207test(app): add headless Xwayland end-to-end test with an x11probe clientbriceGPG Verified
ace7dd1fix(wayland): forward X11 pointer constraints and modal changes, remap on surface replacement, clear dangling parentsbriceGPG Verified
24f47ebfeat(wayland): integrate the built-in XWM with the serverbriceGPG Verified
e1de5a5test(xwm): replace the sinkPeer sleep with a peer lock barrierbriceGPG Verified
9439437fix(xwm): stop _NET_WM_STATE feedback, honour pre-ready geometry, drop stale map repliesbriceGPG Verified
3ccf649Merge branch 'feat/x6-configure' into feat/x6-xwmbriceGPG Verified
ce78abcfeat(xwm): add map flow, property decoding, serial pairing and reparent handlingbriceGPG Verified
5021c6ctest(xwm): fix staticcheck findings in configure testsbriceGPG Verified
50a2e95docs(xwm): state that Boot takes a component-scoped loggerbriceGPG Verified
f1a2966feat(xwm): add configure, focus, close and stacking lists for X11 windowsbriceGPG Verified
01f9a3dfix(xwm): check root ownership before claiming WM state and never adopt the check windowbriceGPG Verified
69b4382feat(xwm): add window table, boot and fd flush skeleton for the built-in X11 window managerbriceGPG Verified
3ed03aabuild(deps): add bnema.dev/xturbo v0.1.0briceGPG Verified
7646086Merge branch 'feat/x6-place' into feat/xwayland-builtinbriceGPG Verified
aa24d35Merge branch 'feat/x6-window-role' into feat/xwayland-builtinbriceGPG Verified
4eada3btest(xwayland): assert AttachWM is skipped when Xwayland exits before readinessbriceGPG Verified
04fea8dtest(vulkan): skip purego allocation budgets under the race runtimebriceGPG Verified
cac0ab6feat(core): send PlaceWindow for positioned windows on position changebriceGPG Verified
957ec9crefactor(wayland): route window access through role-neutral accessorsbriceGPG Verified
57e2ff1refactor(xwayland): split attachment into Wayland fd at start and WM fd after readinessbriceGPG Verified
db0a693docs(xwayland): state that the built-in backend does not manage X11 windowsbriceGPG Verified
e519f1fbuild(deps): bump purego-libwayland to v0.12.0 and purego to v0.13.0-bnema.2briceGPG Verified
a97cce0docs(xwayland): describe the built-in backend and drop the launcher handoff notebriceGPG Verified
e82a03efeat(xwayland): add experimental built-in launcher and client attachmentbriceGPG Verified

Checks 1

note: a failing or unfinished check blocks the merge.

107 files · +18450 -160
1/1
M .mockery.yml +30 -0
M Makefile +9 -2
M README.md +1 -1
M docs/config.md +9 -3
M docs/desktop.md +12 -1
M docs/dev/headless.md +25 -0
M go.mod +4 -3
M go.sum +8 -6
M internal/adapters/config/config.go +18 -4
M internal/adapters/config/config_test.go +36 -4
M internal/adapters/drm/colorop_match.go +11 -0
A internal/adapters/drm/sdrplane_test.go +30 -0
M internal/adapters/vulkan/capture_test.go +3 -0
A internal/adapters/vulkan/norace_test.go +5 -0
A internal/adapters/vulkan/race_test.go +8 -0
M internal/adapters/vulkan/scene_perf_test.go +6 -0
M internal/adapters/wayland/activation.go +3 -3
M internal/adapters/wayland/alloc_hotpath_test.go +9 -0
M internal/adapters/wayland/clipboard.go +5 -0
A internal/adapters/wayland/clipboard_xwm.go +93 -0
A internal/adapters/wayland/clipboard_xwm_test.go +147 -0
M internal/adapters/wayland/constraints.go +14 -13
M internal/adapters/wayland/decoration.go +1 -1
M internal/adapters/wayland/dialog.go +1 -1
M internal/adapters/wayland/dmabuf.go +3 -2
M internal/adapters/wayland/extforeigntoplevel.go +1 -1
M internal/adapters/wayland/foreigntoplevel.go +6 -6
M internal/adapters/wayland/frames.go +10 -6
M internal/adapters/wayland/globals.go +11 -1
M internal/adapters/wayland/imagecopy.go +1 -1
M internal/adapters/wayland/inhibit.go +5 -0
M internal/adapters/wayland/outputs.go +72 -1
M internal/adapters/wayland/popup.go +2 -2
M internal/adapters/wayland/scale.go +5 -3
M internal/adapters/wayland/screencast.go +1 -1
M internal/adapters/wayland/seat.go +11 -9
M internal/adapters/wayland/server.go +51 -16
M internal/adapters/wayland/surface.go +15 -2
A internal/adapters/wayland/window.go +112 -0
M internal/adapters/wayland/wire_client_test.go +6 -0
A internal/adapters/wayland/x11override_test.go +107 -0
A internal/adapters/wayland/x11peer_test.go +332 -0
A internal/adapters/wayland/x11window.go +458 -0
A internal/adapters/wayland/x11window_test.go +627 -0
M internal/adapters/wayland/xdg.go +32 -21
A internal/adapters/wayland/xdgoutputx_test.go +143 -0
A internal/adapters/wayland/xwayland.go +75 -0
A internal/adapters/wayland/xwayland_test.go +84 -0
A internal/adapters/wayland/xwaylandgrab.go +90 -0
A internal/adapters/wayland/xwm/atoms.go +118 -0
A internal/adapters/wayland/xwm/configure.go +317 -0
@@ -0,0 +1,317 @@
1+package xwm
2+
3+import (
4+ "math"
5+
6+ "bnema.dev/xturbo"
7+ "bnema.dev/xturbo/xproto"
8+ "github.com/bnema/neferwl/internal/ports"
9+)
10+
11+// maxClientSize bounds the size a floating window may ask for.
12+const maxClientSize = 16384
13+
14+// _NET_WM_STATE bits tracked in record.stateSent.
15+const (
16+ stateFullscreen uint8 = 1 << iota
17+ stateFocused
18+)
19+
20+// size is the size to send: the requested one, else the last known.
21+func (r *record) size() (w, h uint32) {
22+ w, h = r.pend.w, r.pend.h
23+ if w == 0 {
24+ w = r.w
25+ }
26+ if h == 0 {
27+ h = r.h
28+ }
29+ return w, h
30+}
31+
32+// differs reports whether the pending configure is not what the X server has.
33+func (r *record) differs() bool {
34+ w, h := r.size()
35+ p, s := &r.pend, &r.sent
36+ return p.x != s.x || p.y != s.y || w != s.w || h != s.h ||
37+ p.fullscreen != s.fullscreen || p.activated != s.activated
38+}
39+
40+// markDirty queues s for the next flush when its pending configure differs.
41+func (x *XWM) markDirty(s Slot, r *record) {
42+ if r.pend.dirty || !r.differs() {
43+ return
44+ }
45+ if len(x.dirty) == cap(x.dirty) {
46+ // Stale entries of freed and reused slots filled the queue: send
47+ // what is queued rather than grow it. r stays valid, nothing here
48+ // grows the table.
49+ x.flushConfigures()
50+ }
51+ r.pend.dirty = true
52+ x.dirty = append(x.dirty, s)
53+}
54+
55+// managedRec returns the record of a live managed window.
56+func (x *XWM) managedRec(s Slot) *record {
57+ if x.closed {
58+ return nil
59+ }
60+ if r := x.tab.rec(s); r != nil && r.kind == kindManaged {
61+ return r
62+ }
63+ return nil
64+}
65+
66+// Configure records the size and state core wants for s; sent at flush. A
67+// zero size keeps the current one.
68+func (x *XWM) Configure(s Slot, c ports.ConfigureWindow) {
69+ r := x.managedRec(s)
70+ if r == nil {
71+ return
72+ }
73+ if c.Width > 0 {
74+ r.pend.w = uint32(min(c.Width, maxClientSize))
75+ }
76+ if c.Height > 0 {
77+ r.pend.h = uint32(min(c.Height, maxClientSize))
78+ }
79+ r.pend.fullscreen, r.pend.activated = c.Fullscreen, c.Activated
80+ x.markDirty(s, r)
81+ x.wake()
82+}
83+
84+// Place records the layout position of the client rectangle of s.
85+func (x *XWM) Place(s Slot, px, py int) {
86+ r := x.managedRec(s)
87+ if r == nil {
88+ return
89+ }
90+ r.lx, r.ly, r.placed = int32(px), int32(py), true
91+ r.pend.x, r.pend.y = x.toX(r)
92+ x.markDirty(s, r)
93+ x.wake()
94+}
95+
96+// toX converts the layout position of r to X coordinates.
97+func (x *XWM) toX(r *record) (px, py int32) {
98+ ix, iy := x.space.ToX(int(r.lx), int(r.ly))
99+ return int32(ix), int32(iy)
100+}
101+
102+// SetLayout installs the output layout that X coordinates derive from. When it
103+// changes, placed managed windows are repositioned and mapped override windows
104+// are re-reported in layout coordinates. Display goroutine only.
105+func (x *XWM) SetLayout(l ports.Layout) {
106+ if x.closed {
107+ return
108+ }
109+ x.LayoutChanged(x.space.Set(l))
110+}
111+
112+// UseSpace makes sp the coordinate mapping, owned by the host, which calls
113+// Set on it and then LayoutChanged. Call before Attach.
114+func (x *XWM) UseSpace(sp *Space) { x.space = sp }
115+
116+// LayoutChanged repositions windows after the shared Space was set; changed
117+// is Set's result. Display goroutine only.
118+func (x *XWM) LayoutChanged(changed bool) {
119+ if x.closed {
120+ return
121+ }
122+ if n := x.space.Dropped(); n > 0 && !x.warnedSpace {
123+ x.warnedSpace = true
124+ x.log.Warn().Int("dropped", n).Int("max", maxRegions).Msg("more outputs than the X11 layout holds, extra outputs ignored")
125+ }
126+ if !changed {
127+ return
128+ }
129+ for i := range x.tab.recs {
130+ r, s := &x.tab.recs[i], Slot(i+1)
131+ switch {
132+ case r.kind == kindManaged && r.placed:
133+ r.pend.x, r.pend.y = x.toX(r)
134+ x.markDirty(s, r)
135+ case r.kind == kindUnmanaged && r.ovPaired:
136+ x.queueOverride(s, r)
137+ }
138+ }
139+ x.wake()
140+}
141+
142+// flushConfigures sends the pending configures, once per display iteration,
143+// then what focus left to do.
144+func (x *XWM) flushConfigures() {
145+ for _, s := range x.dirty {
146+ if r := x.tab.rec(s); r != nil && r.kind == kindManaged && r.pend.dirty {
147+ x.sendPending(s, r)
148+ }
149+ }
150+ x.dirty = x.dirty[:0]
151+ x.flushOverrides()
152+ x.flushFocus()
153+}
154+
155+// sendPending writes the geometry (with the synthetic ConfigureNotify ICCCM
156+// 4.1.5 asks for, since Xwayland may not move the window) and the
157+// _NET_WM_STATE bits that changed.
158+func (x *XWM) sendPending(s Slot, r *record) {
159+ r.pend.dirty = false
160+ p := &r.pend
161+ w, h := r.size()
162+ if w > 0 && h > 0 && (p.x != r.sent.x || p.y != r.sent.y || w != r.sent.w || h != r.sent.h) {
163+ var v xproto.ConfigureWindowValues
164+ v.SetX(p.x).SetY(p.y).SetWidth(w).SetHeight(h).SetBorderWidth(0)
165+ x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v), kindConfigure, x.tab.tag(s), 0)
166+ r.x, r.y, r.w, r.h = p.x, p.y, w, h
167+ x.sendConfigureNotify(r)
168+ r.sent.x, r.sent.y, r.sent.w, r.sent.h = p.x, p.y, w, h
169+ }
170+ var bits uint8
171+ if p.fullscreen {
172+ bits |= stateFullscreen
173+ }
174+ if p.activated {
175+ bits |= stateFocused
176+ }
177+ r.sent.fullscreen, r.sent.activated = p.fullscreen, p.activated
178+ r.fullscreen = p.fullscreen // ours now: the property is not read back
179+ if bits != r.stateSent {
180+ r.stateSent = bits
181+ x.writeState(r)
182+ }
183+}
184+
185+// writeState rewrites _NET_WM_STATE of r from its bits.
186+func (x *XWM) writeState(r *record) {
187+ var list [3]uint32
188+ n := 0
189+ if r.modal {
190+ list[n] = x.atoms[atomNetWMStateModal]
191+ n++
192+ }
193+ if r.stateSent&stateFullscreen != 0 {
194+ list[n] = x.atoms[atomNetWMStateFullscreen]
195+ n++
196+ }
197+ if r.stateSent&stateFocused != 0 {
198+ list[n] = x.atoms[atomNetWMStateFocused]
199+ n++
200+ }
201+ x.setProp32(r.xid, atomNetWMState, xproto.AtomAtom, list[:n])
202+}
203+
204+// sendConfigureNotify tells the client its geometry as the XWM sees it.
205+func (x *XWM) sendConfigureNotify(r *record) {
206+ var raw [32]byte
207+ d := xproto.ConfigureNotifyEventData{
208+ Event: xproto.Window(r.xid), Window: xproto.Window(r.xid),
209+ X: int16(min(max(r.x, math.MinInt16), math.MaxInt16)), Y: int16(min(max(r.y, math.MinInt16), math.MaxInt16)),
210+ Width: uint16(min(r.w, math.MaxUint16)), Height: uint16(min(r.h, math.MaxUint16)),
211+ }
212+ d.Put(&raw)
213+ xproto.SendEvent(x.conn.Buf(), false, xproto.Window(r.xid), xproto.EventMaskStructureNotify, &raw)
214+}
215+
216+// onConfigureRequest answers a client resizing itself. Tiled windows are told
217+// their current geometry; floating ones get the size they ask for; unmanaged
218+// and unknown windows are configured as requested.
219+func (x *XWM) onConfigureRequest(ev *xturbo.Event) {
220+ e := xproto.ConfigureRequestOf(ev)
221+ xid := uint32(e.Window())
222+ mask := e.ValueMask()
223+ s := x.tab.lookup(xid)
224+ r := x.managedRec(s)
225+ if r == nil {
226+ var v xproto.ConfigureWindowValues
227+ if mask&xproto.ConfigWindowX != 0 {
228+ v.SetX(int32(e.X()))
229+ }
230+ if mask&xproto.ConfigWindowY != 0 {
231+ v.SetY(int32(e.Y()))
232+ }
233+ if mask&xproto.ConfigWindowWidth != 0 {
234+ v.SetWidth(uint32(e.Width()))
235+ }
236+ if mask&xproto.ConfigWindowHeight != 0 {
237+ v.SetHeight(uint32(e.Height()))
238+ }
239+ if mask&xproto.ConfigWindowBorderWidth != 0 {
240+ v.SetBorderWidth(uint32(e.BorderWidth()))
241+ }
242+ if mask&xproto.ConfigWindowSibling != 0 {
243+ v.SetSibling(e.Sibling())
244+ }
245+ if mask&xproto.ConfigWindowStackMode != 0 {
246+ v.SetStackMode(e.StackMode())
247+ }
248+ xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(xid), &v)
249+ return
250+ }
251+ if !r.ready {
252+ x.configureBeforeReady(s, r, e)
253+ return
254+ }
255+ if r.floating && mask&(xproto.ConfigWindowWidth|xproto.ConfigWindowHeight) != 0 {
256+ w, h := r.w, r.h
257+ if mask&xproto.ConfigWindowWidth != 0 {
258+ w = uint32(min(max(int(e.Width()), 1), maxClientSize))
259+ }
260+ if mask&xproto.ConfigWindowHeight != 0 {
261+ h = uint32(min(max(int(e.Height()), 1), maxClientSize))
262+ }
263+ if w > 0 && h > 0 {
264+ var v xproto.ConfigureWindowValues
265+ v.SetWidth(w).SetHeight(h)
266+ x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(xid), &v), kindConfigure, x.tab.tag(s), 0)
267+ r.w, r.h = w, h
268+ // The size is the client's own: core learns it from the commit
269+ // and Configure only resends it when core wants another.
270+ r.pend.w, r.pend.h = w, h
271+ r.sent.w, r.sent.h = w, h
272+ }
273+ }
274+ x.sendConfigureNotify(r)
275+}
276+
277+// configureBeforeReady honours the geometry a window asks for before it is
278+// ready (XMoveResize before XMapWindow), floating or not: core only sees the
279+// window at Ready and the client must find the geometry it set.
280+func (x *XWM) configureBeforeReady(s Slot, r *record, e xproto.ConfigureRequestEvent) {
281+ mask := e.ValueMask()
282+ var v xproto.ConfigureWindowValues
283+ if mask&xproto.ConfigWindowX != 0 {
284+ r.x = int32(e.X())
285+ v.SetX(r.x)
286+ }
287+ if mask&xproto.ConfigWindowY != 0 {
288+ r.y = int32(e.Y())
289+ v.SetY(r.y)
290+ }
291+ if mask&xproto.ConfigWindowWidth != 0 {
292+ r.w, r.sized = uint32(min(max(int(e.Width()), 1), maxClientSize)), true
293+ v.SetWidth(r.w)
294+ }
295+ if mask&xproto.ConfigWindowHeight != 0 {
296+ r.h, r.sized = uint32(min(max(int(e.Height()), 1), maxClientSize)), true
297+ v.SetHeight(r.h)
298+ }
299+ if mask&(xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth|xproto.ConfigWindowHeight) != 0 {
300+ x.expect(xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v), kindConfigure, x.tab.tag(s), 0)
301+ }
302+ x.sendConfigureNotify(r)
303+}
304+
305+// onConfigureNotify keeps the known geometry of every top-level window.
306+func (x *XWM) onConfigureNotify(ev *xturbo.Event) {
307+ e := xproto.ConfigureNotifyOf(ev)
308+ if e.Event() != xproto.Window(x.root) {
309+ return
310+ }
311+ if r := x.tab.rec(x.tab.lookup(uint32(e.Window()))); r != nil && r.kind != kindFree {
312+ r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height())
313+ if r.kind == kindUnmanaged && r.ovPaired {
314+ x.queueOverride(x.tab.lookup(r.xid), r)
315+ }
316+ }
317+}
A internal/adapters/wayland/xwm/configure_test.go +591 -0
@@ -0,0 +1,591 @@
1+package xwm
2+
3+import (
4+ "encoding/binary"
5+ "runtime"
6+ "testing"
7+ "time"
8+
9+ "bnema.dev/xturbo/xproto"
10+ "github.com/bnema/neferwl/internal/ports"
11+ "github.com/stretchr/testify/mock"
12+ "github.com/stretchr/testify/require"
13+)
14+
15+// newWin makes a ready managed window known to the XWM and the mocked host.
16+func newWin(t *testing.T, x *XWM, p *xpeer, h *mockhost, xid uint32) Slot {
17+ t.Helper()
18+ h.EXPECT().Created(mock.Anything, xid).Return(ports.WindowID(xid)).Once()
19+ p.createNotify(xid, false)
20+ readUntil(t, x, func() bool { return x.tab.lookup(xid) != 0 })
21+ s := x.tab.lookup(xid)
22+ r := x.tab.rec(s)
23+ r.ready, r.w, r.h = true, 300, 200
24+ return s
25+}
26+
27+// settle flushes the XWM and waits until the peer has seen everything sent so
28+// far; it returns the number of requests received.
29+func settle(t *testing.T, x *XWM, p *xpeer) int {
30+ t.Helper()
31+ x.flush()
32+ n := len(p.requests())
33+ xproto.GetInputFocus(x.conn.Buf())
34+ require.NoError(t, x.conn.Flush())
35+ _, n = p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus })
36+ return n
37+}
38+
39+// since returns the requests of the given major opcode received after skip.
40+func since(p *xpeer, skip int, major uint8) []xpeerReq {
41+ var out []xpeerReq
42+ for _, r := range p.requests()[skip:] {
43+ if r.major == major {
44+ out = append(out, r)
45+ }
46+ }
47+ return out
48+}
49+
50+// sentEvents returns the 32-byte events of the SendEvent requests to dest.
51+func sentEvents(p *xpeer, skip int, dest uint32) [][]byte {
52+ var out [][]byte
53+ for _, r := range since(p, skip, opSendEvent) {
54+ if r.u32(4) == dest {
55+ out = append(out, r.raw[12:44])
56+ }
57+ }
58+ return out
59+}
60+
61+func propsTo(p *xpeer, skip int, win uint32, atom string) []xpeerReq {
62+ var out []xpeerReq
63+ a := p.atom(atom)
64+ for _, r := range since(p, skip, opChangeProperty) {
65+ if r.u32(4) == win && r.u32(8) == a {
66+ out = append(out, r)
67+ }
68+ }
69+ return out
70+}
71+
72+func (x *XWM) feed(t *testing.T, p *xpeer, send func()) {
73+ t.Helper()
74+ ev := x.stats.Events
75+ send()
76+ readUntil(t, x, func() bool { return x.stats.Events > ev })
77+}
78+
79+func TestConfigureCoalescesIntoOneRequest(t *testing.T) {
80+ x, p, h, _, runs := attached(t)
81+ const win = 0x500001
82+ s := newWin(t, x, p, h, win)
83+ n := settle(t, x, p)
84+
85+ x.Configure(s, ports.ConfigureWindow{Width: 640, Height: 480})
86+ x.Configure(s, ports.ConfigureWindow{Width: 800, Height: 600, Activated: true})
87+ x.Place(s, 10, 20)
88+ require.Len(t, *runs, 1, "one flush posted")
89+ n2 := settle(t, x, p)
90+
91+ cw := since(p, n, opConfigureWindow)
92+ require.Len(t, cw, 1)
93+ require.Equal(t, uint32(win), cw[0].u32(4))
94+ require.EqualValues(t, 0x1f, binary.LittleEndian.Uint16(cw[0].raw[8:]), "x y width height border")
95+ require.Equal(t, []uint32{10, 20, 800, 600, 0}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20), cw[0].u32(24), cw[0].u32(28)})
96+
97+ evs := sentEvents(p, n, win)
98+ require.Len(t, evs, 1, "one synthetic ConfigureNotify")
99+ require.EqualValues(t, 22, evs[0][0])
100+ require.EqualValues(t, win, binary.LittleEndian.Uint32(evs[0][4:]))
101+ require.EqualValues(t, win, binary.LittleEndian.Uint32(evs[0][8:]))
102+ require.EqualValues(t, 10, binary.LittleEndian.Uint16(evs[0][16:]))
103+ require.EqualValues(t, 20, binary.LittleEndian.Uint16(evs[0][18:]))
104+ require.EqualValues(t, 800, binary.LittleEndian.Uint16(evs[0][20:]))
105+ require.EqualValues(t, 600, binary.LittleEndian.Uint16(evs[0][22:]))
106+ require.Equal(t, xproto.EventMaskStructureNotify, xproto.EventMask(since(p, n, opSendEvent)[0].u32(8)))
107+
108+ // Unchanged: nothing is sent.
109+ x.Configure(s, ports.ConfigureWindow{Width: 800, Height: 600, Activated: true})
110+ x.Place(s, 10, 20)
111+ settle(t, x, p)
112+ require.Len(t, since(p, n2, opConfigureWindow), 0)
113+ require.Len(t, sentEvents(p, n2, win), 0)
114+}
115+
116+func TestConfigureWritesNetWMState(t *testing.T) {
117+ x, p, h, _, _ := attached(t)
118+ const win = 0x500001
119+ s := newWin(t, x, p, h, win)
120+ x.tab.rec(s).modal = true
121+ n := settle(t, x, p)
122+
123+ x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100})
124+ settle(t, x, p)
125+ require.Empty(t, propsTo(p, n, win, "_NET_WM_STATE"), "no state bit changed")
126+
127+ x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100, Fullscreen: true, Activated: true})
128+ settle(t, x, p)
129+ st, ok := p.prop(win, "_NET_WM_STATE")
130+ require.True(t, ok)
131+ require.ElementsMatch(t, []uint32{p.atom("_NET_WM_STATE_MODAL"), p.atom("_NET_WM_STATE_FULLSCREEN"), p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st))
132+
133+ x.Configure(s, ports.ConfigureWindow{Width: 100, Height: 100, Activated: true})
134+ settle(t, x, p)
135+ st, _ = p.prop(win, "_NET_WM_STATE")
136+ require.ElementsMatch(t, []uint32{p.atom("_NET_WM_STATE_MODAL"), p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st))
137+}
138+
139+func TestConfigureBadWindowIsAttributedAndIgnored(t *testing.T) {
140+ x, p, h, _, _ := attached(t)
141+ const win = 0x500001
142+ s := newWin(t, x, p, h, win)
143+ n := settle(t, x, p)
144+ x.Configure(s, ports.ConfigureWindow{Width: 640, Height: 480})
145+ x.flush()
146+ cw := []xpeerReq{}
147+ r, _ := p.waitReq(n, func(r xpeerReq) bool { return r.major == opConfigureWindow })
148+ cw = append(cw, r)
149+
150+ p.mu.Lock()
151+ e := make([]byte, 32)
152+ e[1] = xproto.BadWindow
153+ binary.LittleEndian.PutUint16(e[2:], cw[0].seq)
154+ binary.LittleEndian.PutUint32(e[4:], win)
155+ e[10] = opConfigureWindow
156+ p.write(e)
157+ p.mu.Unlock()
158+ readUntil(t, x, func() bool { return x.stats.Errors == 1 })
159+ require.Zero(t, x.stats.Unattributed)
160+ require.False(t, x.closed)
161+}
162+
163+func sendConfigureRequest(p *xpeer, win uint32, mask uint16, x, y int16, w, h uint16) {
164+ p.event(23, func(b *[32]byte) {
165+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
166+ binary.LittleEndian.PutUint32(b[8:], win)
167+ binary.LittleEndian.PutUint16(b[16:], uint16(x))
168+ binary.LittleEndian.PutUint16(b[18:], uint16(y))
169+ binary.LittleEndian.PutUint16(b[20:], w)
170+ binary.LittleEndian.PutUint16(b[22:], h)
171+ binary.LittleEndian.PutUint16(b[26:], mask)
172+ })
173+}
174+
175+func TestConfigureRequestTiledOnlyGetsNotify(t *testing.T) {
176+ x, p, h, _, _ := attached(t)
177+ const win = 0x500001
178+ newWin(t, x, p, h, win)
179+ n := settle(t, x, p)
180+
181+ x.feed(t, p, func() { sendConfigureRequest(p, win, 0xf, 5, 6, 1000, 900) })
182+ settle(t, x, p)
183+ require.Empty(t, since(p, n, opConfigureWindow), "a tiled window cannot resize itself")
184+ evs := sentEvents(p, n, win)
185+ require.Len(t, evs, 1)
186+ require.EqualValues(t, 22, evs[0][0])
187+ require.EqualValues(t, 300, binary.LittleEndian.Uint16(evs[0][20:]), "current geometry")
188+ require.EqualValues(t, 200, binary.LittleEndian.Uint16(evs[0][22:]))
189+}
190+
191+func TestConfigureRequestFloatingHonoursSize(t *testing.T) {
192+ x, p, h, _, _ := attached(t)
193+ const win = 0x500001
194+ s := newWin(t, x, p, h, win)
195+ x.tab.rec(s).floating = true
196+ n := settle(t, x, p)
197+
198+ x.feed(t, p, func() {
199+ sendConfigureRequest(p, win, uint16(xproto.ConfigWindowWidth|xproto.ConfigWindowHeight), 0, 0, 70000&0xffff, 20000)
200+ })
201+ settle(t, x, p)
202+ cw := since(p, n, opConfigureWindow)
203+ require.Len(t, cw, 1)
204+ require.EqualValues(t, xproto.ConfigWindowWidth|xproto.ConfigWindowHeight, binary.LittleEndian.Uint16(cw[0].raw[8:]))
205+ require.EqualValues(t, 4464, cw[0].u32(12))
206+ require.EqualValues(t, maxClientSize, cw[0].u32(16), "clamped")
207+ require.Len(t, sentEvents(p, n, win), 1)
208+}
209+
210+func TestConfigureRequestUnmanagedHonouredVerbatim(t *testing.T) {
211+ x, p, _, _, _ := attached(t)
212+ const win = 0x500009
213+ p.createNotify(win, true)
214+ readUntil(t, x, func() bool { return x.tab.lookup(win) != 0 })
215+ n := settle(t, x, p)
216+
217+ x.feed(t, p, func() {
218+ sendConfigureRequest(p, win, uint16(xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth), 30, 40, 50, 60)
219+ })
220+ settle(t, x, p)
221+ cw := since(p, n, opConfigureWindow)
222+ require.Len(t, cw, 1)
223+ require.EqualValues(t, 7, binary.LittleEndian.Uint16(cw[0].raw[8:]))
224+ require.Equal(t, []uint32{30, 40, 50}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20)})
225+ require.Empty(t, sentEvents(p, n, win))
226+}
227+
228+func TestConfigureStaleAndDuplicateDirtySlots(t *testing.T) {
229+ x, p, h, _, _ := attached(t)
230+ const a, b = 0x500001, 0x500002
231+ s := newWin(t, x, p, h, a)
232+ for i := 1; i <= 50; i++ {
233+ x.Configure(s, ports.ConfigureWindow{Width: 100 + i, Height: 100})
234+ }
235+ require.Len(t, x.dirty, 1, "queued once per false→true transition")
236+
237+ // The window dies with its entry queued; the slot is reused.
238+ h.EXPECT().Dissociate(s, true).Once()
239+ p.destroyNotify(a)
240+ readUntil(t, x, func() bool { return x.tab.lookup(a) == 0 })
241+ s2 := newWin(t, x, p, h, b)
242+ require.Equal(t, s, s2, "LIFO slot reuse")
243+ n := len(p.requests())
244+ x.Configure(s2, ports.ConfigureWindow{Width: 640, Height: 480})
245+ require.Len(t, x.dirty, 1)
246+ x.dirty = append(x.dirty, s, s) // stale and duplicate entries must be skipped
247+ settle(t, x, p)
248+ cw := since(p, n, opConfigureWindow)
249+ require.Len(t, cw, 1, "sent once despite duplicates")
250+ require.Equal(t, uint32(b), cw[0].u32(4))
251+ require.Empty(t, x.dirty)
252+ require.LessOrEqual(t, cap(x.dirty), len(x.tab.recs))
253+}
254+
255+func TestDirtyQueueNeverGrows(t *testing.T) {
256+ x, p, h, _, _ := attached(t)
257+ c0 := cap(x.dirty)
258+ const base = 0x600000
259+ for i := 0; i < 3*c0; i++ { // each round leaves a stale entry behind
260+ xid := uint32(base + i)
261+ h.EXPECT().Created(mock.Anything, xid).Return(ports.WindowID(1)).Once()
262+ p.createNotify(xid, false)
263+ readUntil(t, x, func() bool { return x.tab.lookup(xid) != 0 })
264+ s := x.tab.lookup(xid)
265+ x.tab.rec(s).ready = true
266+ x.Configure(s, ports.ConfigureWindow{Width: 10, Height: 10})
267+ h.EXPECT().Dissociate(s, true).Once()
268+ p.destroyNotify(xid)
269+ readUntil(t, x, func() bool { return x.tab.lookup(xid) == 0 })
270+ }
271+ require.Equal(t, c0, cap(x.dirty))
272+ require.LessOrEqual(t, len(x.dirty), c0)
273+}
274+
275+func TestPublicCommandsIgnoredWhenClosed(t *testing.T) {
276+ x, p, h, src, _ := attached(t)
277+ const win = 0x500001
278+ s := newWin(t, x, p, h, win)
279+ h.EXPECT().Dissociate(s, true).Once()
280+ src.EXPECT().Remove().Once()
281+ x.Close()
282+ require.NotPanics(t, func() {
283+ x.Configure(s, ports.ConfigureWindow{Width: 1, Height: 1})
284+ x.Place(s, 1, 2)
285+ x.Focus(s)
286+ x.CloseWindow(s)
287+ })
288+ require.Empty(t, x.dirty)
289+ require.Zero(t, x.active)
290+}
291+
292+// Focus.
293+
294+type focusModel struct {
295+ name string
296+ input, take bool
297+ setFocus bool // SetInputFocus on the window
298+ takeFocus bool // WM_TAKE_FOCUS sent
299+}
300+
301+func TestFocusInputModels(t *testing.T) {
302+ for _, m := range []focusModel{
303+ {"passive", true, false, true, false},
304+ {"locally active", true, true, true, true},
305+ {"globally active", false, true, false, true},
306+ {"no input", false, false, false, false},
307+ } {
308+ t.Run(m.name, func(t *testing.T) {
309+ x, p, h, _, _ := attached(t)
310+ const win = 0x500001
311+ s := newWin(t, x, p, h, win)
312+ r := x.tab.rec(s)
313+ r.input, r.takeFocus = m.input, m.take
314+ n := settle(t, x, p)
315+
316+ x.Focus(s)
317+ settle(t, x, p)
318+ var focuses []xpeerReq
319+ for _, r := range since(p, n, opSetInputFocus) {
320+ if r.u32(4) == win {
321+ focuses = append(focuses, r)
322+ }
323+ }
324+ if m.setFocus {
325+ require.Len(t, focuses, 1)
326+ require.EqualValues(t, xproto.InputFocusNone, focuses[0].raw[1], "never PointerRoot")
327+ require.Zero(t, focuses[0].u32(8), "CurrentTime")
328+ } else {
329+ require.Empty(t, focuses)
330+ }
331+ var takes int
332+ for _, ev := range sentEvents(p, n, win) {
333+ if ev[0] == 33 && binary.LittleEndian.Uint32(ev[8:]) == p.atom("WM_PROTOCOLS") {
334+ require.EqualValues(t, 32, ev[1])
335+ require.Equal(t, p.atom("WM_TAKE_FOCUS"), binary.LittleEndian.Uint32(ev[12:]))
336+ require.Zero(t, binary.LittleEndian.Uint32(ev[16:]), "CurrentTime")
337+ takes++
338+ }
339+ }
340+ require.Equal(t, map[bool]int{true: 1, false: 0}[m.takeFocus], takes)
341+
342+ act, ok := p.prop(peerRoot, "_NET_ACTIVE_WINDOW")
343+ require.True(t, ok)
344+ require.Equal(t, []uint32{win}, atomsOf(act), "named even for No-input windows")
345+ st, ok := p.prop(win, "_NET_WM_STATE")
346+ require.True(t, ok)
347+ require.Contains(t, atomsOf(st), p.atom("_NET_WM_STATE_FOCUSED"))
348+ })
349+ }
350+}
351+
352+func TestFocusActiveWindowWrittenOnlyOnChange(t *testing.T) {
353+ x, p, h, _, _ := attached(t)
354+ const a, b = 0x500001, 0x500002
355+ sa, sb := newWin(t, x, p, h, a), newWin(t, x, p, h, b)
356+ n := settle(t, x, p)
357+
358+ x.Focus(sa)
359+ x.Focus(sa)
360+ settle(t, x, p)
361+ require.Len(t, propsTo(p, n, peerRoot, "_NET_ACTIVE_WINDOW"), 1)
362+
363+ x.Focus(sb)
364+ settle(t, x, p)
365+ act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW")
366+ require.Equal(t, []uint32{b}, atomsOf(act))
367+ sta, _ := p.prop(a, "_NET_WM_STATE")
368+ require.NotContains(t, atomsOf(sta), p.atom("_NET_WM_STATE_FOCUSED"), "old window loses FOCUSED")
369+ stb, _ := p.prop(b, "_NET_WM_STATE")
370+ require.Contains(t, atomsOf(stb), p.atom("_NET_WM_STATE_FOCUSED"))
371+
372+ n = settle(t, x, p)
373+ x.Focus(0)
374+ settle(t, x, p)
375+ none := since(p, n, opSetInputFocus)
376+ require.Len(t, none, 1)
377+ require.Zero(t, none[0].u32(4), "focus None")
378+ require.EqualValues(t, xproto.InputFocusNone, none[0].raw[1])
379+ act, _ = p.prop(peerRoot, "_NET_ACTIVE_WINDOW")
380+ require.Equal(t, []uint32{0}, atomsOf(act))
381+ stb, _ = p.prop(b, "_NET_WM_STATE")
382+ require.NotContains(t, atomsOf(stb), p.atom("_NET_WM_STATE_FOCUSED"))
383+ require.Zero(t, x.active)
384+}
385+
386+func TestFocusStackingLists(t *testing.T) {
387+ x, p, h, _, _ := attached(t)
388+ const a, b, c = 0x500001, 0x500002, 0x500003
389+ sa := newWin(t, x, p, h, a)
390+ newWin(t, x, p, h, b)
391+ newWin(t, x, p, h, c)
392+ for _, w := range []uint32{a, b, c} {
393+ x.addClient(w)
394+ }
395+ settle(t, x, p)
396+ list, _ := p.prop(peerRoot, "_NET_CLIENT_LIST")
397+ require.Equal(t, []uint32{a, b, c}, atomsOf(list))
398+ n := settle(t, x, p)
399+
400+ x.Focus(sa)
401+ settle(t, x, p)
402+ stack, _ := p.prop(peerRoot, "_NET_CLIENT_LIST_STACKING")
403+ require.Equal(t, []uint32{b, c, a}, atomsOf(stack), "bottom to top")
404+ list, _ = p.prop(peerRoot, "_NET_CLIENT_LIST")
405+ require.Equal(t, []uint32{a, b, c}, atomsOf(list), "map order")
406+ require.Empty(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST"), "clients list untouched by focus")
407+ var raised bool
408+ for _, r := range since(p, n, opConfigureWindow) {
409+ if r.u32(4) == a && binary.LittleEndian.Uint16(r.raw[8:]) == uint16(xproto.ConfigWindowStackMode) {
410+ require.EqualValues(t, xproto.StackModeAbove, r.u32(12))
411+ raised = true
412+ }
413+ }
414+ require.True(t, raised)
415+
416+ // A burst costs one write per list.
417+ n = settle(t, x, p)
418+ x.removeClient(b)
419+ x.removeClient(c)
420+ settle(t, x, p)
421+ require.Len(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST"), 1)
422+ require.Len(t, propsTo(p, n, peerRoot, "_NET_CLIENT_LIST_STACKING"), 1)
423+ stack, _ = p.prop(peerRoot, "_NET_CLIENT_LIST_STACKING")
424+ require.Equal(t, []uint32{a}, atomsOf(stack))
425+}
426+
427+func TestDestroyedActiveWindowClearsActive(t *testing.T) {
428+ x, p, h, _, _ := attached(t)
429+ const win = 0x500001
430+ s := newWin(t, x, p, h, win)
431+ x.addClient(win)
432+ x.Focus(s)
433+ settle(t, x, p)
434+ h.EXPECT().Dissociate(s, true).Once()
435+ p.destroyNotify(win)
436+ readUntil(t, x, func() bool { return x.tab.lookup(win) == 0 })
437+ settle(t, x, p)
438+ require.Zero(t, x.active)
439+ act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW")
440+ require.Equal(t, []uint32{0}, atomsOf(act))
441+ list, _ := p.prop(peerRoot, "_NET_CLIENT_LIST")
442+ require.Empty(t, list.data)
443+}
444+
445+func TestFocusInDenied(t *testing.T) {
446+ x, p, h, _, _ := attached(t)
447+ const a, b = 0x500001, 0x500002
448+ sa := newWin(t, x, p, h, a)
449+ newWin(t, x, p, h, b)
450+ x.Focus(sa)
451+ n := settle(t, x, p)
452+
453+ focusIn := func(win uint32, detail byte) {
454+ p.event(9, func(e *[32]byte) {
455+ e[1] = detail
456+ binary.LittleEndian.PutUint32(e[4:], win)
457+ })
458+ }
459+ focusCount := func(skip int) int {
460+ c := 0
461+ for _, r := range since(p, skip, opSetInputFocus) {
462+ if r.u32(4) == a {
463+ c++
464+ }
465+ }
466+ return c
467+ }
468+ x.feed(t, p, func() { focusIn(a, 0) }) // the active window itself: fine
469+ x.feed(t, p, func() { focusIn(b, byte(xproto.NotifyDetailPointer)) })
470+ settle(t, x, p)
471+ require.Zero(t, focusCount(n))
472+
473+ x.feed(t, p, func() { focusIn(b, 0); focusIn(b, 0) })
474+ readUntil(t, x, func() bool { return x.stats.Events >= 5 })
475+ settle(t, x, p)
476+ require.Equal(t, 1, focusCount(n), "denied once per iteration")
477+ n = settle(t, x, p)
478+ settle(t, x, p)
479+ require.Zero(t, focusCount(n))
480+}
481+
482+func TestCloseWindow(t *testing.T) {
483+ x, p, h, _, _ := attached(t)
484+ const a, b = 0x500001, 0x500002
485+ sa, sb := newWin(t, x, p, h, a), newWin(t, x, p, h, b)
486+ x.tab.rec(sa).deleteWindow = true
487+ n := settle(t, x, p)
488+
489+ x.CloseWindow(sa)
490+ x.CloseWindow(sb)
491+ settle(t, x, p)
492+ evs := sentEvents(p, n, a)
493+ require.Len(t, evs, 1)
494+ require.EqualValues(t, 33, evs[0][0])
495+ require.EqualValues(t, 32, evs[0][1])
496+ require.Equal(t, p.atom("WM_PROTOCOLS"), binary.LittleEndian.Uint32(evs[0][8:]))
497+ require.Equal(t, p.atom("WM_DELETE_WINDOW"), binary.LittleEndian.Uint32(evs[0][12:]))
498+ require.Zero(t, binary.LittleEndian.Uint32(evs[0][16:]))
499+ require.Empty(t, sentEvents(p, n, b))
500+
501+ kills := since(p, n, opKillClient)
502+ require.Len(t, kills, 1)
503+ require.Equal(t, uint32(b), kills[0].u32(4))
504+}
505+
506+// Allocation guards. The peer sinks requests so the connection never blocks;
507+// its goroutine is idle in the sink loop before measuring.
508+
509+func sinkPeer(t *testing.T, x *XWM, p *xpeer) {
510+ t.Helper()
511+ // Everything written after sink.Store(true) is discarded, so one marker
512+ // request proves the peer loop is in sink mode and idle again.
513+ // A round trip first: the peer has consumed every earlier request, so the
514+ // marker is the only one it can see in sink mode.
515+ require.NoError(t, x.conn.Sync(&x.h))
516+ before := p.discarded.Load()
517+ p.sink.Store(true)
518+ xproto.GetInputFocus(x.conn.Buf())
519+ require.NoError(t, x.conn.Flush())
520+ deadline := time.Now().Add(30 * time.Second) // failure guard only: the wait is event driven
521+ for p.discarded.Load() == before {
522+ if time.Now().After(deadline) {
523+ t.Fatal("xpeer: sink marker never discarded")
524+ }
525+ runtime.Gosched()
526+ }
527+ x.src = nil // no source: write backpressure would not allocate either
528+ x.post = func(func()) {}
529+ x.warnedRing = true // the sink never answers: Expect slots stay used
530+}
531+
532+func TestConfigureFlushAllocations(t *testing.T) {
533+ x, p, h, _, _ := attached(t)
534+ s := newWin(t, x, p, h, 0x500001)
535+ sinkPeer(t, x, p)
536+ i := 0
537+ allocs := testing.AllocsPerRun(300, func() {
538+ i++
539+ x.Configure(s, ports.ConfigureWindow{Width: 100 + i%2, Height: 100, Fullscreen: i%3 == 0, Activated: i%2 == 0})
540+ x.Place(s, i%5, i%7)
541+ x.posted = false
542+ x.flush()
543+ })
544+ require.Zero(t, allocs)
545+ require.False(t, x.closed)
546+}
547+
548+func TestFocusAllocations(t *testing.T) {
549+ x, p, h, _, _ := attached(t)
550+ a, b := newWin(t, x, p, h, 0x500001), newWin(t, x, p, h, 0x500002)
551+ x.addClient(x.tab.rec(a).xid)
552+ x.addClient(x.tab.rec(b).xid)
553+ x.tab.rec(b).takeFocus = true
554+ x.tab.rec(a).input, x.tab.rec(b).input = true, true
555+ sinkPeer(t, x, p)
556+ i := 0
557+ allocs := testing.AllocsPerRun(150, func() {
558+ i++
559+ switch i % 3 {
560+ case 0:
561+ x.Focus(a)
562+ case 1:
563+ x.Focus(b)
564+ default:
565+ x.Focus(0)
566+ }
567+ x.refocus = i%5 == 0
568+ x.posted = false
569+ x.flush()
570+ })
571+ require.Zero(t, allocs)
572+ require.False(t, x.closed)
573+}
574+
575+// Repeated identical fullscreen configures cost nothing.
576+func TestFullscreenConfigureAllocations(t *testing.T) {
577+ x, p, h, _, _ := attached(t)
578+ s := newWin(t, x, p, h, 0x500001)
579+ sinkPeer(t, x, p)
580+ cfg := ports.ConfigureWindow{Width: 1920, Height: 1080, Fullscreen: true, Activated: true}
581+ x.Configure(s, cfg)
582+ x.posted = false
583+ x.flush()
584+ allocs := testing.AllocsPerRun(300, func() {
585+ x.Configure(s, cfg)
586+ x.Place(s, 0, 0)
587+ x.posted = false
588+ x.flush()
589+ })
590+ require.Zero(t, allocs)
591+}
A internal/adapters/wayland/xwm/cursor.go +109 -0
@@ -0,0 +1,109 @@
1+package xwm
2+
3+import (
4+ "bnema.dev/xturbo"
5+ "bnema.dev/xturbo/render"
6+ "bnema.dev/xturbo/xproto"
7+)
8+
9+// Root cursor: a themed arrow on the root window so the X background does not
10+// show Xwayland's cross. One-shot at boot; never fatal.
11+
12+const maxCursorBytes = 1 << 18 // one PutImage request, below the core limit
13+
14+// bootRootCursor sets the cursor of the root window.
15+func (x *XWM) bootRootCursor() error {
16+ if !x.cfg.RootCursor {
17+ return nil
18+ }
19+ img := x.cfg.Cursor
20+ usable := img.W > 0 && img.H > 0 && img.W <= 0xffff && img.H <= 0xffff &&
21+ len(img.Pixels) == img.W*img.H*4 && len(img.Pixels) <= maxCursorBytes
22+ if usable && x.conn.Ext().Info(xturbo.ExtRender).Present {
23+ b := x.conn.Buf()
24+ x.expect(render.QueryPictFormats(b), kindPictFormats, 0, 0)
25+ if err := x.conn.Sync(&x.h); err != nil {
26+ return err
27+ }
28+ if x.bootErr != nil {
29+ return x.bootErr
30+ }
31+ if x.xs.argb != 0 && x.argbCursor(img.W, img.H, img.HotX, img.HotY, img.Pixels) {
32+ return nil
33+ }
34+ }
35+ x.glyphCursor()
36+ return nil
37+}
38+
39+// onPictFormatsReply remembers the ARGB32 picture format (depth 32, direct,
40+// alpha mask 0xff at shift 24).
41+func (x *XWM) onPictFormatsReply(r *xturbo.Reply) {
42+ v, ok := render.ParseQueryPictFormatsReply(r.Raw)
43+ if !ok {
44+ return
45+ }
46+ l := v.Formats()
47+ for i := 0; i < l.Len(); i++ {
48+ f := l.At(i)
49+ if f.Type == render.PictTypeDirect && f.Depth == 32 && f.Direct.AlphaMask == 0xff && f.Direct.AlphaShift == 24 &&
50+ f.Direct.RedMask == 0xff && f.Direct.RedShift == 16 {
51+ x.xs.argb = uint32(f.Id)
52+ return
53+ }
54+ }
55+}
56+
57+func (x *XWM) newIDs(ids []uint32) bool {
58+ for i := range ids {
59+ id, err := x.conn.NewXID()
60+ if err != nil {
61+ return false
62+ }
63+ ids[i] = id
64+ }
65+ return true
66+}
67+
68+// argbCursor uploads the image and installs it on the root.
69+func (x *XWM) argbCursor(w, h, hotX, hotY int, px []byte) bool {
70+ var id [4]uint32 // pixmap, gc, picture, cursor
71+ if !x.newIDs(id[:]) {
72+ return false
73+ }
74+ b := x.conn.Buf()
75+ pm, gc, pic, cur := xproto.Pixmap(id[0]), xproto.Gcontext(id[1]), render.Picture(id[2]), xproto.Cursor(id[3])
76+ xproto.CreatePixmap(b, 32, pm, xproto.Drawable(x.root), uint16(w), uint16(h))
77+ var gv xproto.CreateGCValues
78+ xproto.CreateGC(b, gc, xproto.Drawable(pm), &gv)
79+ xproto.PutImage(b, xproto.ImageFormatZPixmap, xproto.Drawable(pm), gc, uint16(w), uint16(h), 0, 0, 0, 32, px)
80+ var pv render.CreatePictureValues
81+ render.CreatePicture(b, pic, xproto.Drawable(pm), render.Pictformat(x.xs.argb), &pv)
82+ render.CreateCursor(b, cur, pic, uint16(min(max(hotX, 0), w-1)), uint16(min(max(hotY, 0), h-1)))
83+ var wv xproto.ChangeWindowAttributesValues
84+ wv.SetCursor(cur)
85+ xproto.ChangeWindowAttributes(b, xproto.Window(x.root), &wv)
86+ render.FreePicture(b, pic)
87+ xproto.FreeGC(b, gc)
88+ xproto.FreePixmap(b, pm)
89+ xproto.FreeCursor(b, cur)
90+ return true
91+}
92+
93+// glyphCursor installs the "left_ptr" glyph of the X cursor font (glyph 68,
94+// mask 69).
95+func (x *XWM) glyphCursor() {
96+ var id [2]uint32 // font, cursor
97+ if !x.newIDs(id[:]) {
98+ return
99+ }
100+ b := x.conn.Buf()
101+ font, cur := xproto.Font(id[0]), xproto.Cursor(id[1])
102+ xproto.OpenFont(b, font, "cursor")
103+ xproto.CreateGlyphCursor(b, cur, font, font, 68, 69, 0, 0, 0, 0xffff, 0xffff, 0xffff)
104+ var wv xproto.ChangeWindowAttributesValues
105+ wv.SetCursor(cur)
106+ xproto.ChangeWindowAttributes(b, xproto.Window(x.root), &wv)
107+ xproto.CloseFont(b, font)
108+ xproto.FreeCursor(b, cur)
109+}
A internal/adapters/wayland/xwm/cursor_test.go +86 -0
@@ -0,0 +1,86 @@
1+package xwm
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func rootCursorExts() map[string]xpeerExt {
11+ e := fullPeerExts()
12+ e["RENDER"] = xpeerExt{139, 0, 0}
13+ return e
14+}
15+
16+func bootCursor(t *testing.T, exts map[string]xpeerExt, mutate func(*xpeer), cfg ports.XwaylandSettings) (*XWM, *xpeer) {
17+ t.Helper()
18+ fd, p := startXPeer(t, exts)
19+ if mutate != nil {
20+ p.set(mutate)
21+ }
22+ x, err := BootSettings(t.Context(), fd, testLog(), cfg)
23+ require.NoError(t, err)
24+ t.Cleanup(x.CloseConn)
25+ return x, p
26+}
27+
28+func cursorCfg() ports.XwaylandSettings {
29+ return ports.XwaylandSettings{RootCursor: true, Cursor: ports.CursorImage{W: 2, H: 2, HotX: 1, HotY: 0, Pixels: make([]byte, 16)}}
30+}
31+
32+func TestRootCursorRequests(t *testing.T) {
33+ _, p := bootCursor(t, rootCursorExts(), nil, cursorCfg())
34+ // QueryPictFormats (render minor 1) comes first; then the core requests.
35+ var got []string
36+ for _, r := range p.requests() {
37+ switch {
38+ case r.major == 139 && r.minor == 1:
39+ got = append(got, "QueryPictFormats")
40+ case r.major == 53:
41+ got = append(got, "CreatePixmap")
42+ case r.major == 72:
43+ got = append(got, "PutImage")
44+ case r.major == 139 && r.minor == 4:
45+ got = append(got, "CreatePicture")
46+ case r.major == 139 && r.minor == 27:
47+ got = append(got, "CreateCursor")
48+ case r.major == opChangeWindowAttributes && r.u32(4) == peerRoot && r.u32(8)&(1<<14) != 0:
49+ got = append(got, "ChangeWindowAttributes(root)")
50+ case r.major == 54:
51+ got = append(got, "FreePixmap")
52+ }
53+ }
54+ require.Equal(t, []string{"QueryPictFormats", "CreatePixmap", "PutImage", "CreatePicture", "CreateCursor",
55+ "ChangeWindowAttributes(root)", "FreePixmap"}, got)
56+ for _, r := range p.requests() {
57+ if r.major == 72 { // PutImage: ZPixmap, depth 32
58+ require.EqualValues(t, 2, r.raw[1])
59+ require.EqualValues(t, 32, r.raw[21])
60+ }
61+ }
62+}
63+
64+func TestRootCursorFallbackWithoutRender(t *testing.T) {
65+ _, p := bootCursor(t, fullPeerExts(), nil, cursorCfg())
66+ var open, glyph, set bool
67+ for _, r := range p.requests() {
68+ open = open || r.major == 45
69+ glyph = glyph || r.major == 94
70+ set = set || r.major == opChangeWindowAttributes && r.u32(4) == peerRoot && r.u32(8)&(1<<14) != 0
71+ }
72+ require.True(t, open && glyph && set, "OpenFont, CreateGlyphCursor, root cursor")
73+ require.Zero(t, p.count(53, nil), "no pixmap")
74+}
75+
76+func TestRootCursorFallbackWithoutARGB(t *testing.T) {
77+ _, p := bootCursor(t, rootCursorExts(), func(p *xpeer) { p.noARGB = true }, cursorCfg())
78+ require.NotZero(t, p.count(94, nil))
79+ require.Zero(t, p.count(72, nil))
80+}
81+
82+func TestRootCursorOff(t *testing.T) {
83+ _, p := bootCursor(t, rootCursorExts(), nil, ports.XwaylandSettings{})
84+ require.Zero(t, p.count(94, nil))
85+ require.Zero(t, p.count(53, nil))
86+}
A internal/adapters/wayland/xwm/events.go +303 -0
@@ -0,0 +1,303 @@
1+package xwm
2+
3+import (
4+ "bnema.dev/xturbo"
5+ "bnema.dev/xturbo/composite"
6+ "bnema.dev/xturbo/res"
7+ "bnema.dev/xturbo/xfixes"
8+ "bnema.dev/xturbo/xproto"
9+)
10+
11+// Reply kinds registered with Expect. The first argument is the window tag
12+// (slot | generation<<16, see table.tag), the second a property index.
13+const (
14+ kindAttr uint16 = 16 + iota
15+ kindProp
16+ kindGeom
17+ kindClientIds
18+ kindConfigure
19+ kindFocus
20+ kindRootMask
21+ kindVersion
22+ kindTree
23+ kindOverrideType
24+ kindSelTargets
25+ kindSelData
26+ kindSelOut
27+ kindSelOwner
28+ kindPictFormats
29+)
30+
31+// onEvent dispatches an X event. Before Attach there is no host: only the
32+// table is maintained, and Attach replays the result.
33+func (x *XWM) onEvent(ev *xturbo.Event) {
34+ x.stats.Events++
35+ if x.closed {
36+ return
37+ }
38+ switch ev.Kind {
39+ case xproto.KindPropertyNotify:
40+ x.noteTime(uint32(xproto.PropertyNotifyOf(ev).Time()))
41+ case xproto.KindSelectionRequest:
42+ x.noteTime(uint32(xproto.SelectionRequestOf(ev).Time()))
43+ }
44+ switch ev.Kind {
45+ case xproto.KindCreateNotify:
46+ x.onCreateNotify(ev)
47+ case xproto.KindDestroyNotify:
48+ x.onDestroyNotify(ev)
49+ case xproto.KindReparentNotify:
50+ x.onReparentNotify(ev)
51+ }
52+ if ev.Kind == xproto.KindSelectionClear {
53+ x.onXSettingsClear(ev)
54+ }
55+ if x.host == nil {
56+ x.onEventDetached(ev)
57+ return
58+ }
59+ switch ev.Kind {
60+ case xproto.KindMapRequest:
61+ x.onMapRequest(ev)
62+ case xproto.KindMapNotify:
63+ x.onMapNotify(ev)
64+ case xproto.KindUnmapNotify:
65+ x.onUnmapNotify(ev)
66+ case xproto.KindConfigureRequest:
67+ x.onConfigureRequest(ev)
68+ case xproto.KindConfigureNotify:
69+ x.onConfigureNotify(ev)
70+ case xproto.KindPropertyNotify:
71+ x.onPropertyNotify(ev)
72+ case xproto.KindClientMessage:
73+ x.onClientMessage(ev)
74+ case xproto.KindFocusIn:
75+ x.onFocusIn(ev)
76+ default:
77+ x.onSelectionEvent(ev)
78+ }
79+}
80+
81+// onEventDetached keeps what Attach replays: windows asked to map, and those
82+// withdrawn again, so Attach skips them; geometry requests are answered.
83+func (x *XWM) onEventDetached(ev *xturbo.Event) {
84+ switch ev.Kind {
85+ case xproto.KindMapRequest:
86+ if s := x.tab.lookup(uint32(xproto.MapRequestOf(ev).Window())); s != 0 {
87+ x.tab.rec(s).mapping = true
88+ }
89+ case xproto.KindUnmapNotify:
90+ if ev.SendEvent {
91+ return
92+ }
93+ if s := x.tab.lookup(uint32(xproto.UnmapNotifyOf(ev).Window())); s != 0 {
94+ r := x.tab.rec(s)
95+ r.mapped, r.mapping = false, false
96+ }
97+ case xproto.KindConfigureRequest:
98+ x.onConfigureRequest(ev)
99+ }
100+}
101+
102+func (x *XWM) onReply(r *xturbo.Reply) {
103+ x.stats.Replies++
104+ if x.closed {
105+ return
106+ }
107+ switch r.Kind {
108+ case kindVersion:
109+ x.onVersionReply(r)
110+ return
111+ case kindTree:
112+ x.onTreeReply(r)
113+ return
114+ case kindSelTargets:
115+ x.onTargetsReply(r)
116+ return
117+ case kindSelData:
118+ x.onSelDataReply(r)
119+ return
120+ case kindSelOwner:
121+ x.onSelOwnerReply(r)
122+ return
123+ case kindPictFormats:
124+ x.onPictFormatsReply(r)
125+ return
126+ }
127+ s, rec := x.tab.live(r.Args[0])
128+ if rec == nil {
129+ x.stats.Stale++
130+ return
131+ }
132+ switch r.Kind {
133+ case kindAttr:
134+ x.onAttrReply(rec, r)
135+ case kindProp:
136+ x.onPropReply(s, rec, r)
137+ case kindGeom:
138+ x.onGeomReply(s, rec, r)
139+ case kindClientIds:
140+ x.onClientIdsReply(s, rec, r)
141+ case kindOverrideType:
142+ x.onOverrideType(s, rec, r)
143+ }
144+}
145+
146+// onError never tears the XWM down: an error names a request, not the
147+// connection. Sticky Buf errors surface through Flush.
148+func (x *XWM) onError(e *xturbo.XError) {
149+ x.stats.Errors++
150+ if e.Kind == 0 {
151+ x.stats.Unattributed++
152+ x.log.Debug().Uint8("code", e.Code).Uint8("major", e.Major).Uint16("minor", e.Minor).
153+ Uint64("seq", uint64(e.Seq)).Msg("unattributed X error")
154+ return
155+ }
156+ switch e.Kind {
157+ case kindSelTargets, kindSelData, kindSelOut:
158+ x.onSelError(e)
159+ case kindRootMask:
160+ if e.Code == xproto.BadAccess {
161+ x.bootErr = ErrWMExists
162+ }
163+ case kindProp, kindGeom, kindClientIds, kindConfigure, kindFocus, kindOverrideType:
164+ // Usually BadWindow: the window died between request and reply and
165+ // DestroyNotify follows. Whatever the code, the awaited reply will
166+ // never come, so the map must not wait for it.
167+ s, rec := x.tab.live(e.Args[0])
168+ if rec == nil {
169+ x.stats.Stale++
170+ return
171+ }
172+ switch e.Kind {
173+ case kindProp:
174+ rec.pending &^= 1 << e.Args[1]
175+ case kindGeom:
176+ rec.pending &^= propGeom
177+ case kindClientIds:
178+ rec.pending &^= propClientIDs
179+ case kindOverrideType:
180+ if e.Code != xproto.BadWindow { // typed unknown: the window still shows
181+ x.overrideReady(s)
182+ }
183+ return
184+ default:
185+ return
186+ }
187+ if e.Code != xproto.BadWindow && e.Code != xproto.BadDrawable { // a dead window is never announced
188+ x.checkReady(s)
189+ }
190+ }
191+}
192+
193+// ownWindow reports whether xid is one of the XWM's own helper windows.
194+func (x *XWM) ownWindow(xid uint32) bool {
195+ return xid != 0 && (xid == x.wmWin || xid == x.selWin || xid == x.xs.win)
196+}
197+
198+// onCreateNotify records a new top-level window. Override-redirect windows
199+// are kept (their geometry and serial matter later) but never reach the host.
200+func (x *XWM) onCreateNotify(ev *xturbo.Event) {
201+ e := xproto.CreateNotifyOf(ev)
202+ xid := uint32(e.Window())
203+ if x.ownWindow(xid) || x.tab.lookup(xid) != 0 {
204+ return
205+ }
206+ s := x.insert(xid)
207+ if s == 0 {
208+ return
209+ }
210+ r := x.tab.rec(s)
211+ override := e.OverrideRedirect()
212+ r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height())
213+ if override {
214+ r.kind = kindUnmanaged
215+ x.stats.UnmanagedSeen++
216+ return
217+ }
218+ if x.host != nil {
219+ x.adopt(s)
220+ }
221+}
222+
223+// adopt announces a managed window to the host and starts watching it.
224+func (x *XWM) adopt(s Slot) {
225+ r := x.tab.rec(s)
226+ r.id = x.host.Created(s, r.xid)
227+ var v xproto.ChangeWindowAttributesValues
228+ v.SetEventMask(xproto.EventMaskPropertyChange | xproto.EventMaskFocusChange)
229+ x.expect(xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(r.xid), &v), kindAttr, x.tab.tag(s), 0)
230+ x.log.Debug().Uint32("xid", r.xid).Uint32("slot", uint32(s)).Msg("x11 window created")
231+}
232+
233+func (x *XWM) onDestroyNotify(ev *xturbo.Event) {
234+ xid := uint32(xproto.DestroyNotifyOf(ev).Window())
235+ s := x.tab.lookup(xid)
236+ if s == 0 {
237+ return
238+ }
239+ x.forgetWindow(s)
240+}
241+
242+// onTreeReply (boot): record every existing child and ask for its attributes.
243+func (x *XWM) onTreeReply(r *xturbo.Reply) {
244+ q, ok := xproto.ParseQueryTreeReply(r.Raw)
245+ if !ok {
246+ return
247+ }
248+ children := q.Children()
249+ for i := 0; i < children.Len(); i++ {
250+ xid := uint32(children.At(i))
251+ if x.ownWindow(xid) || x.tab.lookup(xid) != 0 {
252+ continue
253+ }
254+ s := x.insert(xid)
255+ if s == 0 {
256+ return
257+ }
258+ x.expect(xproto.GetWindowAttributes(x.conn.Buf(), xproto.Window(xid)), kindAttr, x.tab.tag(s), 0)
259+ }
260+}
261+
262+// onAttrReply (boot): classify an existing window; viewable ones go through
263+// the map flow once the host is attached.
264+func (x *XWM) onAttrReply(rec *record, r *xturbo.Reply) {
265+ a, ok := xproto.ParseGetWindowAttributesReply(r.Raw)
266+ if !ok || x.host != nil {
267+ return
268+ }
269+ if a.OverrideRedirect() {
270+ rec.kind = kindUnmanaged
271+ rec.mapped = a.MapState() == xproto.MapStateViewable
272+ x.stats.UnmanagedSeen++
273+ return
274+ }
275+ if a.MapState() == xproto.MapStateViewable {
276+ rec.mapped, rec.mapping = true, true
277+ }
278+}
279+
280+func (x *XWM) onVersionReply(r *xturbo.Reply) {
281+ switch r.Args[0] {
282+ case versionComposite:
283+ if v, ok := composite.ParseQueryVersionReply(r.Raw); ok {
284+ x.log.Info().Uint32("major", v.MajorVersion()).Uint32("minor", v.MinorVersion()).Msg("Composite")
285+ }
286+ case versionXFixes:
287+ if v, ok := xfixes.ParseQueryVersionReply(r.Raw); ok {
288+ x.log.Info().Uint32("major", v.MajorVersion()).Uint32("minor", v.MinorVersion()).Msg("XFixes")
289+ }
290+ case versionRes:
291+ if v, ok := res.ParseQueryVersionReply(r.Raw); ok {
292+ x.log.Info().Uint16("major", v.ServerMajor()).Uint16("minor", v.ServerMinor()).Msg("X-Resource")
293+ }
294+ }
295+}
296+
297+// noteTime records the server time of the latest timestamped event; 0 is
298+// CurrentTime and carries none.
299+func (x *XWM) noteTime(t uint32) {
300+ if t != 0 {
301+ x.lastTime = t
302+ }
303+}
A internal/adapters/wayland/xwm/fdsource_mock_test.go +129 -0
@@ -0,0 +1,129 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package xwm
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// newMockfdSource creates a new instance of mockfdSource. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func newMockfdSource(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *mockfdSource {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
21+ mock := &mockfdSource{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
30+
31+ return mock
32+}
33+
34+// mockfdSource is an autogenerated mock type for the fdSource type
35+type mockfdSource struct {
36+ mock.Mock
37+}
38+
39+type mockfdSource_Expecter struct {
40+ mock *mock.Mock
41+}
42+
43+func (_m *mockfdSource) EXPECT() *mockfdSource_Expecter {
44+ return &mockfdSource_Expecter{mock: &_m.Mock}
45+}
46+
47+// Remove provides a mock function for the type mockfdSource
48+func (_mock *mockfdSource) Remove() {
49+ _mock.Called()
50+ return
51+}
52+
53+// mockfdSource_Remove_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Remove'
54+type mockfdSource_Remove_Call struct {
55+ *mock.Call
56+}
57+
58+// Remove is a helper method to define mock.On call
59+func (_e *mockfdSource_Expecter) Remove() *mockfdSource_Remove_Call {
60+ return &mockfdSource_Remove_Call{Call: _e.mock.On("Remove")}
61+}
62+
63+func (_c *mockfdSource_Remove_Call) Run(run func()) *mockfdSource_Remove_Call {
64+ _c.Call.Run(func(args mock.Arguments) {
65+ run()
66+ })
67+ return _c
68+}
69+
70+func (_c *mockfdSource_Remove_Call) Return() *mockfdSource_Remove_Call {
71+ _c.Call.Return()
72+ return _c
73+}
74+
75+func (_c *mockfdSource_Remove_Call) RunAndReturn(run func()) *mockfdSource_Remove_Call {
76+ _c.Run(run)
77+ return _c
78+}
79+
80+// SetWritable provides a mock function for the type mockfdSource
81+func (_mock *mockfdSource) SetWritable(on bool) error {
82+ ret := _mock.Called(on)
83+
84+ if len(ret) == 0 {
85+ panic("no return value specified for SetWritable")
86+ }
87+
88+ var r0 error
89+ if returnFunc, ok := ret.Get(0).(func(bool) error); ok {
90+ r0 = returnFunc(on)
91+ } else {
92+ r0 = ret.Error(0)
93+ }
94+ return r0
95+}
96+
97+// mockfdSource_SetWritable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetWritable'
98+type mockfdSource_SetWritable_Call struct {
99+ *mock.Call
100+}
101+
102+// SetWritable is a helper method to define mock.On call
103+// - on bool
104+func (_e *mockfdSource_Expecter) SetWritable(on any) *mockfdSource_SetWritable_Call {
105+ return &mockfdSource_SetWritable_Call{Call: _e.mock.On("SetWritable", on)}
106+}
107+
108+func (_c *mockfdSource_SetWritable_Call) Run(run func(on bool)) *mockfdSource_SetWritable_Call {
109+ _c.Call.Run(func(args mock.Arguments) {
110+ var arg0 bool
111+ if args[0] != nil {
112+ arg0 = args[0].(bool)
113+ }
114+ run(
115+ arg0,
116+ )
117+ })
118+ return _c
119+}
120+
121+func (_c *mockfdSource_SetWritable_Call) Return(err error) *mockfdSource_SetWritable_Call {
122+ _c.Call.Return(err)
123+ return _c
124+}
125+
126+func (_c *mockfdSource_SetWritable_Call) RunAndReturn(run func(on bool) error) *mockfdSource_SetWritable_Call {
127+ _c.Call.Return(run)
128+ return _c
129+}
A internal/adapters/wayland/xwm/fixes_test.go +230 -0
@@ -0,0 +1,230 @@
1+package xwm
2+
3+import (
4+ "encoding/binary"
5+ "testing"
6+
7+ "bnema.dev/xturbo"
8+ "bnema.dev/xturbo/xproto"
9+ "github.com/bnema/neferwl/internal/ports"
10+ "github.com/stretchr/testify/mock"
11+ "github.com/stretchr/testify/require"
12+)
13+
14+func getProps(p *xpeer, skip int, win uint32) int {
15+ n := 0
16+ for _, r := range since(p, skip, opGetProperty) {
17+ if r.u32(4) == win {
18+ n++
19+ }
20+ }
21+ return n
22+}
23+
24+// Our own _NET_WM_STATE writes must not be read back once the window is ready.
25+func TestOwnNetWMStateWriteIsNotReadBack(t *testing.T) {
26+ hs := newHarness(t, nil)
27+ const win = 0x500001
28+ hs.classic(win)
29+ s := hs.mapAndWait(win)
30+
31+ hs.x.Configure(s, ports.ConfigureWindow{Width: 300, Height: 200, Fullscreen: true})
32+ n := settle(t, hs.x, hs.p)
33+ require.True(t, hs.x.tab.rec(s).fullscreen, "the written state is the known state")
34+ st, _ := hs.p.prop(win, "_NET_WM_STATE")
35+ require.Equal(t, []uint32{hs.p.atom("_NET_WM_STATE_FULLSCREEN")}, atomsOf(st))
36+
37+ ev := hs.x.Stats().Events
38+ hs.p.propertyNotify(win, hs.p.atom("_NET_WM_STATE"), false)
39+ readUntil(t, hs.x, func() bool { return hs.x.Stats().Events > ev })
40+ settle(t, hs.x, hs.p)
41+ require.Zero(t, getProps(hs.p, n, win), "no GetProperty")
42+
43+}
44+
45+// A pre-map XMoveResize is honoured, floating or not.
46+func TestConfigureRequestBeforeReadyIsHonoured(t *testing.T) {
47+ hs := newHarness(t, nil)
48+ const win = 0x500001
49+ hs.classic(win)
50+ hs.create(win)
51+ n := settle(t, hs.x, hs.p)
52+
53+ mask := uint16(xproto.ConfigWindowX | xproto.ConfigWindowY | xproto.ConfigWindowWidth | xproto.ConfigWindowHeight)
54+ hs.x.feed(t, hs.p, func() { sendConfigureRequest(hs.p, win, mask, 11, 22, 640, 70000&0xffff) })
55+ settle(t, hs.x, hs.p)
56+ cw := since(hs.p, n, opConfigureWindow)
57+ require.Len(t, cw, 1)
58+ require.Equal(t, []uint32{11, 22, 640, 4464}, []uint32{cw[0].u32(12), cw[0].u32(16), cw[0].u32(20), cw[0].u32(24)})
59+ require.Len(t, sentEvents(hs.p, n, win), 1)
60+
61+ hs.expectReady(win)
62+ hs.p.mapRequest(win)
63+ readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok })
64+ require.Equal(t, 640, hs.ready[win].Width)
65+ require.Equal(t, 4464, hs.ready[win].Height)
66+ require.False(t, hs.ready[win].Floating)
67+}
68+
69+// After a remap the state is rewritten from scratch, FOCUSED included.
70+func TestRemapRewritesStateWithFocused(t *testing.T) {
71+ hs := newHarness(t, nil)
72+ const win = 0x500001
73+ hs.classic(win)
74+ s := hs.mapAndWait(win)
75+ hs.x.Focus(s)
76+ settle(t, hs.x, hs.p)
77+ st, _ := hs.p.prop(win, "_NET_WM_STATE")
78+ require.Equal(t, []uint32{hs.p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st))
79+
80+ hs.h.EXPECT().Dissociate(s, false).Once()
81+ hs.p.unmapNotify(win, false)
82+ readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).mapping })
83+ delete(hs.ready, win)
84+ hs.expectReady(win)
85+ hs.p.mapRequest(win)
86+ readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok })
87+
88+ hs.x.Focus(s)
89+ settle(t, hs.x, hs.p)
90+ st, _ = hs.p.prop(win, "_NET_WM_STATE")
91+ require.Equal(t, []uint32{hs.p.atom("_NET_WM_STATE_FOCUSED")}, atomsOf(st))
92+}
93+
94+// Replies of a previous map never satisfy the next one (xturbo usually retires
95+// their slots first; the generation guards the rest).
96+func TestStaleRepliesOfPreviousMapAreDropped(t *testing.T) {
97+ hs := newHarness(t, nil)
98+ const win = 0x500001
99+ hs.classic(win)
100+ s := hs.create(win)
101+ hs.p.holdReplies() // the second map is never answered
102+ hs.p.mapRequest(win)
103+ readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).mapping })
104+ oldTag := hs.x.tab.tag(s)
105+ hs.h.EXPECT().Dissociate(s, false).Once()
106+ hs.p.unmapNotify(win, false)
107+ readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).mapping })
108+ hs.p.mapRequest(win)
109+ readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).mapping })
110+ require.NotEqual(t, oldTag, hs.x.tab.tag(s))
111+
112+ rep := make([]byte, 32)
113+ rep[0] = 1
114+ for idx := 0; idx < propCount; idx++ {
115+ hs.x.onReply(&xturbo.Reply{Kind: kindProp, Args: [4]uint32{oldTag, uint32(idx)}, Raw: rep})
116+ }
117+ hs.x.onReply(&xturbo.Reply{Kind: kindGeom, Args: [4]uint32{oldTag}, Raw: rep})
118+ require.EqualValues(t, propCount+1, hs.x.Stats().Stale)
119+ require.False(t, hs.x.tab.rec(s).ready)
120+ require.Equal(t, propMaskAll&^propClientIDs, hs.x.tab.rec(s).pending, "the new map still awaits its own replies")
121+}
122+
123+func TestGetGeometryBadDrawableIsNotReady(t *testing.T) {
124+ hs := newHarness(t, nil)
125+ const win = 0x500001
126+ hs.classic(win)
127+ s := hs.create(win)
128+ hs.p.set(func(p *xpeer) { p.bad[win] = true })
129+ hs.p.mapRequest(win)
130+ readUntil(t, hs.x, func() bool { return hs.x.Stats().Errors >= uint64(propCount)+1 })
131+ require.Zero(t, hs.x.Stats().Unattributed)
132+ require.False(t, hs.x.tab.rec(s).ready)
133+ hs.h.EXPECT().Dissociate(s, true).Once()
134+ hs.p.destroyNotify(win)
135+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 })
136+}
137+
138+// A host that refocuses the window while dissociating it must not leave a
139+// dangling active slot.
140+func TestForgetWindowClearsActiveSetByHost(t *testing.T) {
141+ x, p, h, _, _ := attached(t)
142+ const win = 0x500001
143+ s := newWin(t, x, p, h, win)
144+ h.EXPECT().Dissociate(s, true).Run(func(sl Slot, _ bool) { x.Focus(sl) }).Once()
145+ p.destroyNotify(win)
146+ readUntil(t, x, func() bool { return x.tab.lookup(win) == 0 })
147+ require.Zero(t, x.active)
148+ require.False(t, x.refocus)
149+ settle(t, x, p)
150+ act, _ := p.prop(peerRoot, "_NET_ACTIVE_WINDOW")
151+ require.Equal(t, []uint32{0}, atomsOf(act))
152+}
153+
154+func TestTitleAndAppIDAreValidUTF8(t *testing.T) {
155+ hs := newHarness(t, nil)
156+ const win = 0x500001
157+ hs.classic(win)
158+ hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_NAME"]) })
159+ hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("caf\xe9"))
160+ hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("i\x00C\xe9\x00"))
161+ s := hs.mapAndWait(win)
162+ require.Equal(t, "caf\uFFFD", hs.ready[win].Title)
163+ require.Equal(t, "C\uFFFD", hs.ready[win].AppID)
164+
165+ // Unchanged after sanitising: no host call.
166+ hs.notifyWMName(win)
167+ // A changed truncated multibyte title reaches the host sanitised.
168+ hs.h.EXPECT().Title(s, "\u00e9\uFFFD").Once()
169+ hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("\xc3\xa9\xe2\x82"))
170+ hs.notifyWMName(win)
171+}
172+
173+func TestSyntheticConfigureNotifyClampsCoordinates(t *testing.T) {
174+ x, p, h, _, _ := attached(t)
175+ const win = 0x500001
176+ s := newWin(t, x, p, h, win)
177+ n := settle(t, x, p)
178+ r := x.tab.rec(s)
179+ r.x, r.y, r.w, r.h = 70000, -70000, 70000, 5
180+ x.sendConfigureNotify(r)
181+ settle(t, x, p)
182+ ev := sentEvents(p, n, win)
183+ require.Len(t, ev, 1)
184+ require.EqualValues(t, 32767, int16(binary.LittleEndian.Uint16(ev[0][16:])))
185+ require.EqualValues(t, -32768, int16(binary.LittleEndian.Uint16(ev[0][18:])))
186+ require.EqualValues(t, 65535, binary.LittleEndian.Uint16(ev[0][20:]))
187+ require.EqualValues(t, 5, binary.LittleEndian.Uint16(ev[0][22:]))
188+}
189+
190+// A window withdrawn before Attach is not mapped by Attach; a pre-attach
191+// ConfigureRequest is honoured.
192+func TestUnmapAndConfigureBeforeAttach(t *testing.T) {
193+ x, p := bootPeer(t, nil)
194+ const gone, sized = 0x500001, 0x500002
195+ for _, w := range []uint32{gone, sized} {
196+ p.setProp(w, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("a\x00B\x00"))
197+ p.geoms[w] = xpeerGeom{w: 300, h: 200}
198+ p.createNotify(w, false)
199+ p.mapRequest(w)
200+ }
201+ p.unmapNotify(gone, false)
202+ mask := uint16(xproto.ConfigWindowWidth | xproto.ConfigWindowHeight)
203+ sendConfigureRequest(p, sized, mask, 0, 0, 500, 400)
204+ readUntil(t, x, func() bool { return x.Stats().Events >= 6 })
205+ n := len(p.requests())
206+
207+ h := newMockhost(t)
208+ src := newMockfdSource(t)
209+ src.EXPECT().Remove().Maybe()
210+ post, _ := newPost()
211+ var got WindowInfo
212+ done := false
213+ h.EXPECT().Created(mock.Anything, mock.Anything).Return(ports.WindowID(1)).Twice()
214+ h.EXPECT().Ready(mock.Anything, mock.Anything).Run(func(_ Slot, i WindowInfo) { got, done = i, true }).Once()
215+ x.attach(src, h, post)
216+ readUntil(t, x, func() bool { return done })
217+ settle(t, x, p)
218+ for _, r := range since(p, n, opMapWindow) {
219+ require.NotEqual(t, uint32(gone), r.u32(4), "withdrawn window must not be mapped")
220+ }
221+ require.Equal(t, 500, got.Width)
222+ require.Equal(t, 400, got.Height)
223+}
224+
225+func (hs *harness) notifyWMName(win uint32) {
226+ hs.t.Helper()
227+ before := hs.x.Stats().Replies
228+ hs.p.propertyNotify(win, uint32(xproto.AtomWmName), false)
229+ readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before })
230+}
A internal/adapters/wayland/xwm/focus.go +218 -0
@@ -0,0 +1,218 @@
1+package xwm
2+
3+import (
4+ "encoding/binary"
5+ "slices"
6+
7+ "bnema.dev/xturbo"
8+ "bnema.dev/xturbo/xproto"
9+)
10+
11+// Bits of XWM.listsDirty: the client lists to rewrite at flush.
12+const (
13+ listClients uint8 = 1 << iota
14+ listStacking
15+)
16+
17+// onFocusIn denies a focus change the XWM did not make: Wayland is the truth.
18+// The active window's focus is re-issued once at the next flush.
19+func (x *XWM) onFocusIn(ev *xturbo.Event) {
20+ e := xproto.FocusInOf(ev)
21+ if e.Detail() == xproto.NotifyDetailPointer {
22+ return
23+ }
24+ // Keyboard grabs are how menus steal focus legitimately (wlroots does
25+ // the same): fighting them would close every Steam menu.
26+ if e.Mode() == xproto.NotifyModeGrab || e.Mode() == xproto.NotifyModeUngrab {
27+ return
28+ }
29+ s := x.tab.lookup(uint32(e.Event()))
30+ if s == 0 || s == x.active {
31+ return
32+ }
33+ if r := x.tab.rec(s); r.kind == kindUnmanaged {
34+ return
35+ }
36+ x.refocus = true
37+ x.wake()
38+}
39+
40+// Focus gives keyboard focus to s; 0 means a native Wayland window or none.
41+// Focus never uses PointerRoot: without an X window to focus it is None.
42+func (x *XWM) Focus(s Slot) {
43+ if x.closed {
44+ return
45+ }
46+ r := x.managedRec(s)
47+ if r == nil {
48+ s = 0
49+ }
50+ if s == x.active {
51+ return
52+ }
53+ if old := x.managedRec(x.active); old != nil {
54+ old.pend.activated = false
55+ x.markDirty(x.active, old)
56+ }
57+ x.active = s
58+ if r == nil {
59+ x.issueFocus(0, nil)
60+ x.setActive(0)
61+ x.wake()
62+ return
63+ }
64+ r.pend.activated = true
65+ x.markDirty(s, r)
66+ x.issueFocus(s, r)
67+ x.setActive(r.xid)
68+ x.raise(r)
69+ x.wake()
70+}
71+
72+// issueFocus applies the ICCCM input model of r: Passive and Locally active
73+// windows get SetInputFocus, Locally and Globally active ones WM_TAKE_FOCUS,
74+// No-input windows nothing. A nil r clears the X focus.
75+func (x *XWM) issueFocus(s Slot, r *record) {
76+ b := x.conn.Buf()
77+ if r == nil {
78+ xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.WindowNone, 0)
79+ return
80+ }
81+ if r.input {
82+ x.expect(xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.Window(r.xid), 0), kindFocus, x.tab.tag(s), 0)
83+ }
84+ if r.takeFocus {
85+ x.sendProtocol(r.xid, atomWMTakeFocus)
86+ }
87+}
88+
89+// sendProtocol sends a WM_PROTOCOLS client message [proto, CurrentTime].
90+func (x *XWM) sendProtocol(xid uint32, proto int) {
91+ var raw [32]byte
92+ raw[0], raw[1] = 33, 32 // ClientMessage, format 32
93+ binary.LittleEndian.PutUint32(raw[4:], xid)
94+ binary.LittleEndian.PutUint32(raw[8:], x.atoms[atomWMProtocols])
95+ binary.LittleEndian.PutUint32(raw[12:], x.atoms[proto])
96+ xproto.SendEvent(x.conn.Buf(), false, xproto.Window(xid), xproto.EventMaskNoEvent, &raw)
97+}
98+
99+// setActive writes _NET_ACTIVE_WINDOW on the root.
100+func (x *XWM) setActive(xid uint32) {
101+ v := [1]uint32{xid}
102+ x.setProp32(x.root, atomNetActiveWindow, xproto.AtomWindow, v[:])
103+}
104+
105+// raise restacks r above its siblings and at the top of the stacking list.
106+func (x *XWM) raise(r *record) {
107+ var v xproto.ConfigureWindowValues
108+ v.SetStackMode(xproto.StackModeAbove)
109+ xproto.ConfigureWindow(x.conn.Buf(), xproto.Window(r.xid), &v)
110+ if i := slices.Index(x.stacking, r.xid); i >= 0 {
111+ x.stacking = append(x.stacking[:i], x.stacking[i+1:]...)
112+ x.stacking = append(x.stacking, r.xid)
113+ x.listsDirty |= listStacking
114+ }
115+}
116+
117+// CloseWindow asks s to close (WM_DELETE_WINDOW), or kills its client.
118+func (x *XWM) CloseWindow(s Slot) {
119+ r := x.managedRec(s)
120+ if r == nil {
121+ return
122+ }
123+ if r.deleteWindow {
124+ x.sendProtocol(r.xid, atomWMDeleteWindow)
125+ } else {
126+ xproto.KillClient(x.conn.Buf(), r.xid)
127+ }
128+ x.wake()
129+}
130+
131+// addClient puts a ready window at the end of both client lists.
132+func (x *XWM) addClient(xid uint32) {
133+ if !slices.Contains(x.clients, xid) {
134+ x.clients = append(x.clients, xid)
135+ x.listsDirty |= listClients
136+ }
137+ if !slices.Contains(x.stacking, xid) {
138+ x.stacking = append(x.stacking, xid)
139+ x.listsDirty |= listStacking
140+ }
141+ x.wake()
142+}
143+
144+// removeClient drops an unmapped or destroyed window from both client lists.
145+func (x *XWM) removeClient(xid uint32) {
146+ if i := slices.Index(x.clients, xid); i >= 0 {
147+ x.clients = slices.Delete(x.clients, i, i+1)
148+ x.listsDirty |= listClients
149+ }
150+ if i := slices.Index(x.stacking, xid); i >= 0 {
151+ x.stacking = slices.Delete(x.stacking, i, i+1)
152+ x.listsDirty |= listStacking
153+ }
154+ x.wake()
155+}
156+
157+// dropWindow takes s out of the client lists, the focus and the configure
158+// queue when it stops being mapped (unmap, destroy). It is idempotent.
159+func (x *XWM) dropWindow(s Slot) {
160+ r := x.tab.rec(s)
161+ if r != nil {
162+ r.ovQueued = false // a reused slot must not inherit a queued report
163+ }
164+ if r == nil || r.kind != kindManaged {
165+ return
166+ }
167+ x.removeClient(r.xid)
168+ r.pend.dirty = false // the queued slot is skipped at flush
169+ // A remap starts from a clean _NET_WM_STATE, so Focus writes FOCUSED again.
170+ r.pend.activated, r.sent.activated = false, false
171+ r.sent.fullscreen, r.stateSent = false, 0
172+ if x.active == s {
173+ x.active = 0
174+ x.refocus = false
175+ x.setActive(0)
176+ }
177+}
178+
179+// forgetWindow ends the life of s (destroy, reparent away): it is dropped,
180+// the host is told if it knew the window, and the record is freed.
181+func (x *XWM) forgetWindow(s Slot) {
182+ r := x.tab.rec(s)
183+ if r == nil || r.kind == kindFree {
184+ return
185+ }
186+ x.dropWindow(s)
187+ if r.kind == kindUnmanaged && r.ovPaired && x.host != nil {
188+ r.ovPaired = false
189+ x.host.OverrideGone(s)
190+ }
191+ if r.kind == kindManaged && r.id != 0 && x.host != nil {
192+ r.id = 0
193+ x.host.Dissociate(s, true)
194+ if x.active == s { // the host re-focused the window while dissociating it
195+ x.active, x.refocus = 0, false
196+ x.issueFocus(0, nil)
197+ x.setActive(0)
198+ x.wake()
199+ }
200+ }
201+ x.tab.remove(s)
202+}
203+
204+// flushFocus rewrites the client lists that changed and re-issues a denied
205+// focus, once per display iteration.
206+func (x *XWM) flushFocus() {
207+ if x.listsDirty&listClients != 0 {
208+ x.setProp32(x.root, atomNetClientList, xproto.AtomWindow, x.clients)
209+ }
210+ if x.listsDirty&listStacking != 0 {
211+ x.setProp32(x.root, atomNetClientListStacking, xproto.AtomWindow, x.stacking)
212+ }
213+ x.listsDirty = 0
214+ if x.refocus {
215+ x.refocus = false
216+ x.issueFocus(x.active, x.managedRec(x.active))
217+ }
218+}
A internal/adapters/wayland/xwm/host.go +82 -0
@@ -0,0 +1,82 @@
1+package xwm
2+
3+import (
4+ "time"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+// Slot is the XWM's window slot: table index + 1, stable until the window is
10+// destroyed. 0 means none.
11+type Slot uint32
12+
13+// WindowInfo is what the XWM learned about a managed window by the time it is
14+// ready to be mapped. The host copies it.
15+type WindowInfo struct {
16+ PID int
17+ Floating bool // dialog/utility/splash/fixed size
18+ Width, Height int // requested size, logical (scale 1)
19+ Parent Slot
20+ Modal bool // _NET_WM_STATE_MODAL
21+ Fullscreen bool // _NET_WM_STATE_FULLSCREEN at map
22+ Bypass uint8 // _NET_WM_BYPASS_COMPOSITOR (0 unset, 1 bypass, 2 do not)
23+ // HasAt: the client asked for a rect equal to a whole output; AtX, AtY
24+ // is that output's layout origin.
25+ HasAt bool
26+ AtX, AtY int
27+ Title, AppID string
28+}
29+
30+// host is the wayland server as the XWM sees it. Display goroutine only.
31+// All strings passed are owned by the XWM record and already deduplicated
32+// (only passed when changed).
33+type host interface {
34+ // Created: a managed (non-override-redirect) X window is known; the host
35+ // allocates a ports.WindowID and returns it. Not yet mapped.
36+ Created(slot Slot, xid uint32) ports.WindowID
37+ // Ready: properties read and MapWindow issued; the surface may still be
38+ // missing. Info is copied by the host.
39+ Ready(slot Slot, info WindowInfo)
40+ // Associate links the X window to the wl_surface with identity surfaceID.
41+ // The host maps the window on the surface's first buffer commit.
42+ Associate(slot Slot, surfaceID uint64)
43+ // Dissociate: UnmapNotify/DestroyNotify; the host emits WindowUnmapped if
44+ // mapped and drops the surface link. destroyed also frees the WindowID.
45+ Dissociate(slot Slot, destroyed bool)
46+ Title(slot Slot, title string) // changed only
47+ AppID(slot Slot, appID string) // changed only (WM_CLASS class)
48+ Parent(slot Slot, parent Slot) // 0 = none; after a WM_TRANSIENT_FOR change
49+ // Fullscreen forwards a fullscreen request (_NET_WM_STATE client message
50+ // or initial state); the host emits WindowFullscreenRequest.
51+ Fullscreen(slot Slot, on bool)
52+
53+ // OverrideMapped reports an override-redirect window that is mapped,
54+ // typed and paired with its wl_surface. Rect is in layout coordinates.
55+ OverrideMapped(slot Slot, xid uint32, surfaceID uint64, x, y, w, h int, noInput bool)
56+ // OverrideMoved reports a changed layout rect of a mapped override window.
57+ OverrideMoved(slot Slot, x, y, w, h int)
58+ // OverrideGone ends a mapped override window: unmapped or destroyed.
59+ OverrideGone(slot Slot)
60+
61+ // SelectionOwned: an X client owns kind (SelClipboard/SelPrimary) and
62+ // offers these MIME types, or none does any more (mimes nil). mimes is
63+ // scratch: the host copies it.
64+ SelectionOwned(kind int, mimes []string)
65+
66+ // SelectionRead asks for the data of the current Wayland selection kind
67+ // as mime: it returns a non-blocking read end the XWM closes, or -1 when
68+ // there is none, it is protected, or mime is not offered.
69+ SelectionRead(kind int, mime string) (fd int)
70+
71+ // After runs fn on the display goroutine once d has elapsed, unless the
72+ // returned cancel ran first. cancel is idempotent and display goroutine
73+ // only; fn may still run once after cancel when it was already queued,
74+ // so callers guard with their own generation.
75+ After(d time.Duration, fn func()) (cancel func())
76+}
77+
78+// fdSource is the display's event source for the X socket (*server.Source).
79+type fdSource interface {
80+ SetWritable(on bool) error
81+ Remove()
82+}
A internal/adapters/wayland/xwm/host_mock_test.go +775 -0
@@ -0,0 +1,775 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package xwm
6+
7+import (
8+ "time"
9+
10+ "github.com/bnema/neferwl/internal/ports"
11+ mock "github.com/stretchr/testify/mock"
12+)
13+
14+// newMockhost creates a new instance of mockhost. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
15+// The first argument is typically a *testing.T value.
16+func newMockhost(t interface {
17+ mock.TestingT
18+ Cleanup(func())
19+}) *mockhost {
20+ if helper, ok := t.(interface{ Helper() }); ok {
21+ helper.Helper()
22+ }
23+
24+ mock := &mockhost{}
25+ mock.Mock.Test(t)
26+
27+ t.Cleanup(func() {
28+ if helper, ok := t.(interface{ Helper() }); ok {
29+ helper.Helper()
30+ }
31+ mock.AssertExpectations(t)
32+ })
33+
34+ return mock
35+}
36+
37+// mockhost is an autogenerated mock type for the host type
38+type mockhost struct {
39+ mock.Mock
40+}
41+
42+type mockhost_Expecter struct {
43+ mock *mock.Mock
44+}
45+
46+func (_m *mockhost) EXPECT() *mockhost_Expecter {
47+ return &mockhost_Expecter{mock: &_m.Mock}
48+}
49+
50+// After provides a mock function for the type mockhost
51+func (_mock *mockhost) After(d time.Duration, fn func()) func() {
52+ ret := _mock.Called(d, fn)
53+
54+ if len(ret) == 0 {
55+ panic("no return value specified for After")
56+ }
57+
58+ var r0 func()
59+ if returnFunc, ok := ret.Get(0).(func(time.Duration, func()) func()); ok {
60+ r0 = returnFunc(d, fn)
61+ } else {
62+ if ret.Get(0) != nil {
63+ r0 = ret.Get(0).(func())
64+ }
65+ }
66+ return r0
67+}
68+
69+// mockhost_After_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'After'
70+type mockhost_After_Call struct {
71+ *mock.Call
72+}
73+
74+// After is a helper method to define mock.On call
75+// - d time.Duration
76+// - fn func()
77+func (_e *mockhost_Expecter) After(d any, fn any) *mockhost_After_Call {
78+ return &mockhost_After_Call{Call: _e.mock.On("After", d, fn)}
79+}
80+
81+func (_c *mockhost_After_Call) Run(run func(d time.Duration, fn func())) *mockhost_After_Call {
82+ _c.Call.Run(func(args mock.Arguments) {
83+ var arg0 time.Duration
84+ if args[0] != nil {
85+ arg0 = args[0].(time.Duration)
86+ }
87+ var arg1 func()
88+ if args[1] != nil {
89+ arg1 = args[1].(func())
90+ }
91+ run(
92+ arg0,
93+ arg1,
94+ )
95+ })
96+ return _c
97+}
98+
99+func (_c *mockhost_After_Call) Return(cancel func()) *mockhost_After_Call {
100+ _c.Call.Return(cancel)
101+ return _c
102+}
103+
104+func (_c *mockhost_After_Call) RunAndReturn(run func(d time.Duration, fn func()) func()) *mockhost_After_Call {
105+ _c.Call.Return(run)
106+ return _c
107+}
108+
109+// AppID provides a mock function for the type mockhost
110+func (_mock *mockhost) AppID(slot Slot, appID string) {
111+ _mock.Called(slot, appID)
112+ return
113+}
114+
115+// mockhost_AppID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AppID'
116+type mockhost_AppID_Call struct {
117+ *mock.Call
118+}
119+
120+// AppID is a helper method to define mock.On call
121+// - slot Slot
122+// - appID string
123+func (_e *mockhost_Expecter) AppID(slot any, appID any) *mockhost_AppID_Call {
124+ return &mockhost_AppID_Call{Call: _e.mock.On("AppID", slot, appID)}
125+}
126+
127+func (_c *mockhost_AppID_Call) Run(run func(slot Slot, appID string)) *mockhost_AppID_Call {
128+ _c.Call.Run(func(args mock.Arguments) {
129+ var arg0 Slot
130+ if args[0] != nil {
131+ arg0 = args[0].(Slot)
132+ }
133+ var arg1 string
134+ if args[1] != nil {
135+ arg1 = args[1].(string)
136+ }
137+ run(
138+ arg0,
139+ arg1,
140+ )
141+ })
142+ return _c
143+}
144+
145+func (_c *mockhost_AppID_Call) Return() *mockhost_AppID_Call {
146+ _c.Call.Return()
147+ return _c
148+}
149+
150+func (_c *mockhost_AppID_Call) RunAndReturn(run func(slot Slot, appID string)) *mockhost_AppID_Call {
151+ _c.Run(run)
152+ return _c
153+}
154+
155+// Associate provides a mock function for the type mockhost
156+func (_mock *mockhost) Associate(slot Slot, surfaceID uint64) {
157+ _mock.Called(slot, surfaceID)
158+ return
159+}
160+
161+// mockhost_Associate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Associate'
162+type mockhost_Associate_Call struct {
163+ *mock.Call
164+}
165+
166+// Associate is a helper method to define mock.On call
167+// - slot Slot
168+// - surfaceID uint64
169+func (_e *mockhost_Expecter) Associate(slot any, surfaceID any) *mockhost_Associate_Call {
170+ return &mockhost_Associate_Call{Call: _e.mock.On("Associate", slot, surfaceID)}
171+}
172+
173+func (_c *mockhost_Associate_Call) Run(run func(slot Slot, surfaceID uint64)) *mockhost_Associate_Call {
174+ _c.Call.Run(func(args mock.Arguments) {
175+ var arg0 Slot
176+ if args[0] != nil {
177+ arg0 = args[0].(Slot)
178+ }
179+ var arg1 uint64
180+ if args[1] != nil {
181+ arg1 = args[1].(uint64)
182+ }
183+ run(
184+ arg0,
185+ arg1,
186+ )
187+ })
188+ return _c
189+}
190+
191+func (_c *mockhost_Associate_Call) Return() *mockhost_Associate_Call {
192+ _c.Call.Return()
193+ return _c
194+}
195+
196+func (_c *mockhost_Associate_Call) RunAndReturn(run func(slot Slot, surfaceID uint64)) *mockhost_Associate_Call {
197+ _c.Run(run)
198+ return _c
199+}
200+
201+// Created provides a mock function for the type mockhost
202+func (_mock *mockhost) Created(slot Slot, xid uint32) ports.WindowID {
203+ ret := _mock.Called(slot, xid)
204+
205+ if len(ret) == 0 {
206+ panic("no return value specified for Created")
207+ }
208+
209+ var r0 ports.WindowID
210+ if returnFunc, ok := ret.Get(0).(func(Slot, uint32) ports.WindowID); ok {
211+ r0 = returnFunc(slot, xid)
212+ } else {
213+ r0 = ret.Get(0).(ports.WindowID)
214+ }
215+ return r0
216+}
217+
218+// mockhost_Created_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Created'
219+type mockhost_Created_Call struct {
220+ *mock.Call
221+}
222+
223+// Created is a helper method to define mock.On call
224+// - slot Slot
225+// - xid uint32
226+func (_e *mockhost_Expecter) Created(slot any, xid any) *mockhost_Created_Call {
227+ return &mockhost_Created_Call{Call: _e.mock.On("Created", slot, xid)}
228+}
229+
230+func (_c *mockhost_Created_Call) Run(run func(slot Slot, xid uint32)) *mockhost_Created_Call {
231+ _c.Call.Run(func(args mock.Arguments) {
232+ var arg0 Slot
233+ if args[0] != nil {
234+ arg0 = args[0].(Slot)
235+ }
236+ var arg1 uint32
237+ if args[1] != nil {
238+ arg1 = args[1].(uint32)
239+ }
240+ run(
241+ arg0,
242+ arg1,
243+ )
244+ })
245+ return _c
246+}
247+
248+func (_c *mockhost_Created_Call) Return(windowID ports.WindowID) *mockhost_Created_Call {
249+ _c.Call.Return(windowID)
250+ return _c
251+}
252+
253+func (_c *mockhost_Created_Call) RunAndReturn(run func(slot Slot, xid uint32) ports.WindowID) *mockhost_Created_Call {
254+ _c.Call.Return(run)
255+ return _c
256+}
257+
258+// Dissociate provides a mock function for the type mockhost
259+func (_mock *mockhost) Dissociate(slot Slot, destroyed bool) {
260+ _mock.Called(slot, destroyed)
261+ return
262+}
263+
264+// mockhost_Dissociate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Dissociate'
265+type mockhost_Dissociate_Call struct {
266+ *mock.Call
267+}
268+
269+// Dissociate is a helper method to define mock.On call
270+// - slot Slot
271+// - destroyed bool
272+func (_e *mockhost_Expecter) Dissociate(slot any, destroyed any) *mockhost_Dissociate_Call {
273+ return &mockhost_Dissociate_Call{Call: _e.mock.On("Dissociate", slot, destroyed)}
274+}
275+
276+func (_c *mockhost_Dissociate_Call) Run(run func(slot Slot, destroyed bool)) *mockhost_Dissociate_Call {
277+ _c.Call.Run(func(args mock.Arguments) {
278+ var arg0 Slot
279+ if args[0] != nil {
280+ arg0 = args[0].(Slot)
281+ }
282+ var arg1 bool
283+ if args[1] != nil {
284+ arg1 = args[1].(bool)
285+ }
286+ run(
287+ arg0,
288+ arg1,
289+ )
290+ })
291+ return _c
292+}
293+
294+func (_c *mockhost_Dissociate_Call) Return() *mockhost_Dissociate_Call {
295+ _c.Call.Return()
296+ return _c
297+}
298+
299+func (_c *mockhost_Dissociate_Call) RunAndReturn(run func(slot Slot, destroyed bool)) *mockhost_Dissociate_Call {
300+ _c.Run(run)
301+ return _c
302+}
303+
304+// Fullscreen provides a mock function for the type mockhost
305+func (_mock *mockhost) Fullscreen(slot Slot, on bool) {
306+ _mock.Called(slot, on)
307+ return
308+}
309+
310+// mockhost_Fullscreen_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Fullscreen'
311+type mockhost_Fullscreen_Call struct {
312+ *mock.Call
313+}
314+
315+// Fullscreen is a helper method to define mock.On call
316+// - slot Slot
317+// - on bool
318+func (_e *mockhost_Expecter) Fullscreen(slot any, on any) *mockhost_Fullscreen_Call {
319+ return &mockhost_Fullscreen_Call{Call: _e.mock.On("Fullscreen", slot, on)}
320+}
321+
322+func (_c *mockhost_Fullscreen_Call) Run(run func(slot Slot, on bool)) *mockhost_Fullscreen_Call {
323+ _c.Call.Run(func(args mock.Arguments) {
324+ var arg0 Slot
325+ if args[0] != nil {
326+ arg0 = args[0].(Slot)
327+ }
328+ var arg1 bool
329+ if args[1] != nil {
330+ arg1 = args[1].(bool)
331+ }
332+ run(
333+ arg0,
334+ arg1,
335+ )
336+ })
337+ return _c
338+}
339+
340+func (_c *mockhost_Fullscreen_Call) Return() *mockhost_Fullscreen_Call {
341+ _c.Call.Return()
342+ return _c
343+}
344+
345+func (_c *mockhost_Fullscreen_Call) RunAndReturn(run func(slot Slot, on bool)) *mockhost_Fullscreen_Call {
346+ _c.Run(run)
347+ return _c
348+}
349+
350+// OverrideGone provides a mock function for the type mockhost
351+func (_mock *mockhost) OverrideGone(slot Slot) {
352+ _mock.Called(slot)
353+ return
354+}
355+
356+// mockhost_OverrideGone_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideGone'
357+type mockhost_OverrideGone_Call struct {
358+ *mock.Call
359+}
360+
361+// OverrideGone is a helper method to define mock.On call
362+// - slot Slot
363+func (_e *mockhost_Expecter) OverrideGone(slot any) *mockhost_OverrideGone_Call {
364+ return &mockhost_OverrideGone_Call{Call: _e.mock.On("OverrideGone", slot)}
365+}
366+
367+func (_c *mockhost_OverrideGone_Call) Run(run func(slot Slot)) *mockhost_OverrideGone_Call {
368+ _c.Call.Run(func(args mock.Arguments) {
369+ var arg0 Slot
370+ if args[0] != nil {
371+ arg0 = args[0].(Slot)
372+ }
373+ run(
374+ arg0,
375+ )
376+ })
377+ return _c
378+}
379+
380+func (_c *mockhost_OverrideGone_Call) Return() *mockhost_OverrideGone_Call {
381+ _c.Call.Return()
382+ return _c
383+}
384+
385+func (_c *mockhost_OverrideGone_Call) RunAndReturn(run func(slot Slot)) *mockhost_OverrideGone_Call {
386+ _c.Run(run)
387+ return _c
388+}
389+
390+// OverrideMapped provides a mock function for the type mockhost
391+func (_mock *mockhost) OverrideMapped(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool) {
392+ _mock.Called(slot, xid, surfaceID, x, y, w, h, noInput)
393+ return
394+}
395+
396+// mockhost_OverrideMapped_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideMapped'
397+type mockhost_OverrideMapped_Call struct {
398+ *mock.Call
399+}
400+
401+// OverrideMapped is a helper method to define mock.On call
402+// - slot Slot
403+// - xid uint32
404+// - surfaceID uint64
405+// - x int
406+// - y int
407+// - w int
408+// - h int
409+// - noInput bool
410+func (_e *mockhost_Expecter) OverrideMapped(slot any, xid any, surfaceID any, x any, y any, w any, h any, noInput any) *mockhost_OverrideMapped_Call {
411+ return &mockhost_OverrideMapped_Call{Call: _e.mock.On("OverrideMapped", slot, xid, surfaceID, x, y, w, h, noInput)}
412+}
413+
414+func (_c *mockhost_OverrideMapped_Call) Run(run func(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool)) *mockhost_OverrideMapped_Call {
415+ _c.Call.Run(func(args mock.Arguments) {
416+ var arg0 Slot
417+ if args[0] != nil {
418+ arg0 = args[0].(Slot)
419+ }
420+ var arg1 uint32
421+ if args[1] != nil {
422+ arg1 = args[1].(uint32)
423+ }
424+ var arg2 uint64
425+ if args[2] != nil {
426+ arg2 = args[2].(uint64)
427+ }
428+ var arg3 int
429+ if args[3] != nil {
430+ arg3 = args[3].(int)
431+ }
432+ var arg4 int
433+ if args[4] != nil {
434+ arg4 = args[4].(int)
435+ }
436+ var arg5 int
437+ if args[5] != nil {
438+ arg5 = args[5].(int)
439+ }
440+ var arg6 int
441+ if args[6] != nil {
442+ arg6 = args[6].(int)
443+ }
444+ var arg7 bool
445+ if args[7] != nil {
446+ arg7 = args[7].(bool)
447+ }
448+ run(
449+ arg0,
450+ arg1,
451+ arg2,
452+ arg3,
453+ arg4,
454+ arg5,
455+ arg6,
456+ arg7,
457+ )
458+ })
459+ return _c
460+}
461+
462+func (_c *mockhost_OverrideMapped_Call) Return() *mockhost_OverrideMapped_Call {
463+ _c.Call.Return()
464+ return _c
465+}
466+
467+func (_c *mockhost_OverrideMapped_Call) RunAndReturn(run func(slot Slot, xid uint32, surfaceID uint64, x int, y int, w int, h int, noInput bool)) *mockhost_OverrideMapped_Call {
468+ _c.Run(run)
469+ return _c
470+}
471+
472+// OverrideMoved provides a mock function for the type mockhost
473+func (_mock *mockhost) OverrideMoved(slot Slot, x int, y int, w int, h int) {
474+ _mock.Called(slot, x, y, w, h)
475+ return
476+}
477+
478+// mockhost_OverrideMoved_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OverrideMoved'
479+type mockhost_OverrideMoved_Call struct {
480+ *mock.Call
481+}
482+
483+// OverrideMoved is a helper method to define mock.On call
484+// - slot Slot
485+// - x int
486+// - y int
487+// - w int
488+// - h int
489+func (_e *mockhost_Expecter) OverrideMoved(slot any, x any, y any, w any, h any) *mockhost_OverrideMoved_Call {
490+ return &mockhost_OverrideMoved_Call{Call: _e.mock.On("OverrideMoved", slot, x, y, w, h)}
491+}
492+
493+func (_c *mockhost_OverrideMoved_Call) Run(run func(slot Slot, x int, y int, w int, h int)) *mockhost_OverrideMoved_Call {
494+ _c.Call.Run(func(args mock.Arguments) {
495+ var arg0 Slot
496+ if args[0] != nil {
497+ arg0 = args[0].(Slot)
498+ }
499+ var arg1 int
500+ if args[1] != nil {
501+ arg1 = args[1].(int)
502+ }
503+ var arg2 int
504+ if args[2] != nil {
505+ arg2 = args[2].(int)
506+ }
507+ var arg3 int
508+ if args[3] != nil {
509+ arg3 = args[3].(int)
510+ }
511+ var arg4 int
512+ if args[4] != nil {
513+ arg4 = args[4].(int)
514+ }
515+ run(
516+ arg0,
517+ arg1,
518+ arg2,
519+ arg3,
520+ arg4,
521+ )
522+ })
523+ return _c
524+}
525+
526+func (_c *mockhost_OverrideMoved_Call) Return() *mockhost_OverrideMoved_Call {
527+ _c.Call.Return()
528+ return _c
529+}
530+
531+func (_c *mockhost_OverrideMoved_Call) RunAndReturn(run func(slot Slot, x int, y int, w int, h int)) *mockhost_OverrideMoved_Call {
532+ _c.Run(run)
533+ return _c
534+}
535+
536+// Parent provides a mock function for the type mockhost
537+func (_mock *mockhost) Parent(slot Slot, parent Slot) {
538+ _mock.Called(slot, parent)
539+ return
540+}
541+
542+// mockhost_Parent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Parent'
543+type mockhost_Parent_Call struct {
544+ *mock.Call
545+}
546+
547+// Parent is a helper method to define mock.On call
548+// - slot Slot
549+// - parent Slot
550+func (_e *mockhost_Expecter) Parent(slot any, parent any) *mockhost_Parent_Call {
551+ return &mockhost_Parent_Call{Call: _e.mock.On("Parent", slot, parent)}
552+}
553+
554+func (_c *mockhost_Parent_Call) Run(run func(slot Slot, parent Slot)) *mockhost_Parent_Call {
555+ _c.Call.Run(func(args mock.Arguments) {
556+ var arg0 Slot
557+ if args[0] != nil {
558+ arg0 = args[0].(Slot)
559+ }
560+ var arg1 Slot
561+ if args[1] != nil {
562+ arg1 = args[1].(Slot)
563+ }
564+ run(
565+ arg0,
566+ arg1,
567+ )
568+ })
569+ return _c
570+}
571+
572+func (_c *mockhost_Parent_Call) Return() *mockhost_Parent_Call {
573+ _c.Call.Return()
574+ return _c
575+}
576+
577+func (_c *mockhost_Parent_Call) RunAndReturn(run func(slot Slot, parent Slot)) *mockhost_Parent_Call {
578+ _c.Run(run)
579+ return _c
580+}
581+
582+// Ready provides a mock function for the type mockhost
583+func (_mock *mockhost) Ready(slot Slot, info WindowInfo) {
584+ _mock.Called(slot, info)
585+ return
586+}
587+
588+// mockhost_Ready_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ready'
589+type mockhost_Ready_Call struct {
590+ *mock.Call
591+}
592+
593+// Ready is a helper method to define mock.On call
594+// - slot Slot
595+// - info WindowInfo
596+func (_e *mockhost_Expecter) Ready(slot any, info any) *mockhost_Ready_Call {
597+ return &mockhost_Ready_Call{Call: _e.mock.On("Ready", slot, info)}
598+}
599+
600+func (_c *mockhost_Ready_Call) Run(run func(slot Slot, info WindowInfo)) *mockhost_Ready_Call {
601+ _c.Call.Run(func(args mock.Arguments) {
602+ var arg0 Slot
603+ if args[0] != nil {
604+ arg0 = args[0].(Slot)
605+ }
606+ var arg1 WindowInfo
607+ if args[1] != nil {
608+ arg1 = args[1].(WindowInfo)
609+ }
610+ run(
611+ arg0,
612+ arg1,
613+ )
614+ })
615+ return _c
616+}
617+
618+func (_c *mockhost_Ready_Call) Return() *mockhost_Ready_Call {
619+ _c.Call.Return()
620+ return _c
621+}
622+
623+func (_c *mockhost_Ready_Call) RunAndReturn(run func(slot Slot, info WindowInfo)) *mockhost_Ready_Call {
624+ _c.Run(run)
625+ return _c
626+}
627+
628+// SelectionOwned provides a mock function for the type mockhost
629+func (_mock *mockhost) SelectionOwned(kind int, mimes []string) {
630+ _mock.Called(kind, mimes)
631+ return
632+}
633+
634+// mockhost_SelectionOwned_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectionOwned'
635+type mockhost_SelectionOwned_Call struct {
636+ *mock.Call
637+}
638+
639+// SelectionOwned is a helper method to define mock.On call
640+// - kind int
641+// - mimes []string
642+func (_e *mockhost_Expecter) SelectionOwned(kind any, mimes any) *mockhost_SelectionOwned_Call {
643+ return &mockhost_SelectionOwned_Call{Call: _e.mock.On("SelectionOwned", kind, mimes)}
644+}
645+
646+func (_c *mockhost_SelectionOwned_Call) Run(run func(kind int, mimes []string)) *mockhost_SelectionOwned_Call {
647+ _c.Call.Run(func(args mock.Arguments) {
648+ var arg0 int
649+ if args[0] != nil {
650+ arg0 = args[0].(int)
651+ }
652+ var arg1 []string
653+ if args[1] != nil {
654+ arg1 = args[1].([]string)
655+ }
656+ run(
657+ arg0,
658+ arg1,
659+ )
660+ })
661+ return _c
662+}
663+
664+func (_c *mockhost_SelectionOwned_Call) Return() *mockhost_SelectionOwned_Call {
665+ _c.Call.Return()
666+ return _c
667+}
668+
669+func (_c *mockhost_SelectionOwned_Call) RunAndReturn(run func(kind int, mimes []string)) *mockhost_SelectionOwned_Call {
670+ _c.Run(run)
671+ return _c
672+}
673+
674+// SelectionRead provides a mock function for the type mockhost
675+func (_mock *mockhost) SelectionRead(kind int, mime string) int {
676+ ret := _mock.Called(kind, mime)
677+
678+ if len(ret) == 0 {
679+ panic("no return value specified for SelectionRead")
680+ }
681+
682+ var r0 int
683+ if returnFunc, ok := ret.Get(0).(func(int, string) int); ok {
684+ r0 = returnFunc(kind, mime)
685+ } else {
686+ r0 = ret.Get(0).(int)
687+ }
688+ return r0
689+}
690+
691+// mockhost_SelectionRead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectionRead'
692+type mockhost_SelectionRead_Call struct {
693+ *mock.Call
694+}
695+
696+// SelectionRead is a helper method to define mock.On call
697+// - kind int
698+// - mime string
699+func (_e *mockhost_Expecter) SelectionRead(kind any, mime any) *mockhost_SelectionRead_Call {
700+ return &mockhost_SelectionRead_Call{Call: _e.mock.On("SelectionRead", kind, mime)}
701+}
702+
703+func (_c *mockhost_SelectionRead_Call) Run(run func(kind int, mime string)) *mockhost_SelectionRead_Call {
704+ _c.Call.Run(func(args mock.Arguments) {
705+ var arg0 int
706+ if args[0] != nil {
707+ arg0 = args[0].(int)
708+ }
709+ var arg1 string
710+ if args[1] != nil {
711+ arg1 = args[1].(string)
712+ }
713+ run(
714+ arg0,
715+ arg1,
716+ )
717+ })
718+ return _c
719+}
720+
721+func (_c *mockhost_SelectionRead_Call) Return(fd int) *mockhost_SelectionRead_Call {
722+ _c.Call.Return(fd)
723+ return _c
724+}
725+
726+func (_c *mockhost_SelectionRead_Call) RunAndReturn(run func(kind int, mime string) int) *mockhost_SelectionRead_Call {
727+ _c.Call.Return(run)
728+ return _c
729+}
730+
731+// Title provides a mock function for the type mockhost
732+func (_mock *mockhost) Title(slot Slot, title string) {
733+ _mock.Called(slot, title)
734+ return
735+}
736+
737+// mockhost_Title_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Title'
738+type mockhost_Title_Call struct {
739+ *mock.Call
740+}
741+
742+// Title is a helper method to define mock.On call
743+// - slot Slot
744+// - title string
745+func (_e *mockhost_Expecter) Title(slot any, title any) *mockhost_Title_Call {
746+ return &mockhost_Title_Call{Call: _e.mock.On("Title", slot, title)}
747+}
748+
749+func (_c *mockhost_Title_Call) Run(run func(slot Slot, title string)) *mockhost_Title_Call {
750+ _c.Call.Run(func(args mock.Arguments) {
751+ var arg0 Slot
752+ if args[0] != nil {
753+ arg0 = args[0].(Slot)
754+ }
755+ var arg1 string
756+ if args[1] != nil {
757+ arg1 = args[1].(string)
758+ }
759+ run(
760+ arg0,
761+ arg1,
762+ )
763+ })
764+ return _c
765+}
766+
767+func (_c *mockhost_Title_Call) Return() *mockhost_Title_Call {
768+ _c.Call.Return()
769+ return _c
770+}
771+
772+func (_c *mockhost_Title_Call) RunAndReturn(run func(slot Slot, title string)) *mockhost_Title_Call {
773+ _c.Run(run)
774+ return _c
775+}
A internal/adapters/wayland/xwm/loop_mock_test.go +180 -0
@@ -0,0 +1,180 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package xwm
6+
7+import (
8+ "time"
9+
10+ mock "github.com/stretchr/testify/mock"
11+)
12+
13+// newMockloop creates a new instance of mockloop. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14+// The first argument is typically a *testing.T value.
15+func newMockloop(t interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}) *mockloop {
19+ if helper, ok := t.(interface{ Helper() }); ok {
20+ helper.Helper()
21+ }
22+
23+ mock := &mockloop{}
24+ mock.Mock.Test(t)
25+
26+ t.Cleanup(func() {
27+ if helper, ok := t.(interface{ Helper() }); ok {
28+ helper.Helper()
29+ }
30+ mock.AssertExpectations(t)
31+ })
32+
33+ return mock
34+}
35+
36+// mockloop is an autogenerated mock type for the loop type
37+type mockloop struct {
38+ mock.Mock
39+}
40+
41+type mockloop_Expecter struct {
42+ mock *mock.Mock
43+}
44+
45+func (_m *mockloop) EXPECT() *mockloop_Expecter {
46+ return &mockloop_Expecter{mock: &_m.Mock}
47+}
48+
49+// AddFD provides a mock function for the type mockloop
50+func (_mock *mockloop) AddFD(fd int, onReadable func(), onWritable func()) (fdSource, error) {
51+ ret := _mock.Called(fd, onReadable, onWritable)
52+
53+ if len(ret) == 0 {
54+ panic("no return value specified for AddFD")
55+ }
56+
57+ var r0 fdSource
58+ var r1 error
59+ if returnFunc, ok := ret.Get(0).(func(int, func(), func()) (fdSource, error)); ok {
60+ return returnFunc(fd, onReadable, onWritable)
61+ }
62+ if returnFunc, ok := ret.Get(0).(func(int, func(), func()) fdSource); ok {
63+ r0 = returnFunc(fd, onReadable, onWritable)
64+ } else {
65+ if ret.Get(0) != nil {
66+ r0 = ret.Get(0).(fdSource)
67+ }
68+ }
69+ if returnFunc, ok := ret.Get(1).(func(int, func(), func()) error); ok {
70+ r1 = returnFunc(fd, onReadable, onWritable)
71+ } else {
72+ r1 = ret.Error(1)
73+ }
74+ return r0, r1
75+}
76+
77+// mockloop_AddFD_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddFD'
78+type mockloop_AddFD_Call struct {
79+ *mock.Call
80+}
81+
82+// AddFD is a helper method to define mock.On call
83+// - fd int
84+// - onReadable func()
85+// - onWritable func()
86+func (_e *mockloop_Expecter) AddFD(fd any, onReadable any, onWritable any) *mockloop_AddFD_Call {
87+ return &mockloop_AddFD_Call{Call: _e.mock.On("AddFD", fd, onReadable, onWritable)}
88+}
89+
90+func (_c *mockloop_AddFD_Call) Run(run func(fd int, onReadable func(), onWritable func())) *mockloop_AddFD_Call {
91+ _c.Call.Run(func(args mock.Arguments) {
92+ var arg0 int
93+ if args[0] != nil {
94+ arg0 = args[0].(int)
95+ }
96+ var arg1 func()
97+ if args[1] != nil {
98+ arg1 = args[1].(func())
99+ }
100+ var arg2 func()
101+ if args[2] != nil {
102+ arg2 = args[2].(func())
103+ }
104+ run(
105+ arg0,
106+ arg1,
107+ arg2,
108+ )
109+ })
110+ return _c
111+}
112+
113+func (_c *mockloop_AddFD_Call) Return(fdSourceMoqParam fdSource, err error) *mockloop_AddFD_Call {
114+ _c.Call.Return(fdSourceMoqParam, err)
115+ return _c
116+}
117+
118+func (_c *mockloop_AddFD_Call) RunAndReturn(run func(fd int, onReadable func(), onWritable func()) (fdSource, error)) *mockloop_AddFD_Call {
119+ _c.Call.Return(run)
120+ return _c
121+}
122+
123+// After provides a mock function for the type mockloop
124+func (_mock *mockloop) After(d time.Duration, fn func()) func() {
125+ ret := _mock.Called(d, fn)
126+
127+ if len(ret) == 0 {
128+ panic("no return value specified for After")
129+ }
130+
131+ var r0 func()
132+ if returnFunc, ok := ret.Get(0).(func(time.Duration, func()) func()); ok {
133+ r0 = returnFunc(d, fn)
134+ } else {
135+ if ret.Get(0) != nil {
136+ r0 = ret.Get(0).(func())
137+ }
138+ }
139+ return r0
140+}
141+
142+// mockloop_After_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'After'
143+type mockloop_After_Call struct {
144+ *mock.Call
145+}
146+
147+// After is a helper method to define mock.On call
148+// - d time.Duration
149+// - fn func()
150+func (_e *mockloop_Expecter) After(d any, fn any) *mockloop_After_Call {
151+ return &mockloop_After_Call{Call: _e.mock.On("After", d, fn)}
152+}
153+
154+func (_c *mockloop_After_Call) Run(run func(d time.Duration, fn func())) *mockloop_After_Call {
155+ _c.Call.Run(func(args mock.Arguments) {
156+ var arg0 time.Duration
157+ if args[0] != nil {
158+ arg0 = args[0].(time.Duration)
159+ }
160+ var arg1 func()
161+ if args[1] != nil {
162+ arg1 = args[1].(func())
163+ }
164+ run(
165+ arg0,
166+ arg1,
167+ )
168+ })
169+ return _c
170+}
171+
172+func (_c *mockloop_After_Call) Return(cancel func()) *mockloop_After_Call {
173+ _c.Call.Return(cancel)
174+ return _c
175+}
176+
177+func (_c *mockloop_After_Call) RunAndReturn(run func(d time.Duration, fn func()) func()) *mockloop_After_Call {
178+ _c.Call.Return(run)
179+ return _c
180+}
A internal/adapters/wayland/xwm/mapflow.go +359 -0
@@ -0,0 +1,359 @@
1+package xwm
2+
3+import (
4+ "bnema.dev/xturbo"
5+ "bnema.dev/xturbo/xproto"
6+)
7+
8+// ICCCM WM_STATE values.
9+const (
10+ wmStateWithdrawn uint32 = 0
11+ wmStateNormal uint32 = 1
12+)
13+
14+// _NET_WM_STATE client message actions.
15+const (
16+ netStateRemove = 0
17+ netStateAdd = 1
18+ netStateToggle = 2
19+)
20+
21+// beginMap starts the map flow of a managed window: every property, the
22+// geometry and MapWindow go out in one batch (wlroots order). Also called by
23+// Attach for windows mapped before it.
24+func (x *XWM) beginMap(s Slot) {
25+ rec := x.tab.rec(s)
26+ if rec.kind != kindManaged || rec.mapping {
27+ return
28+ }
29+ rec.mapping, rec.ready = true, false
30+ rec.hasNetName = false
31+ rec.pending = propGeom
32+ xid := rec.xid
33+ for idx := 0; idx < propCount; idx++ {
34+ x.requestProp(s, idx)
35+ }
36+ b := x.conn.Buf()
37+ x.expect(xproto.GetGeometry(b, xproto.Drawable(xid)), kindGeom, x.tab.tag(s), 0)
38+ xproto.MapWindow(b, xproto.Window(xid))
39+}
40+
41+func (x *XWM) onMapRequest(ev *xturbo.Event) {
42+ if s := x.tab.lookup(uint32(xproto.MapRequestOf(ev).Window())); s != 0 {
43+ x.beginMap(s)
44+ }
45+}
46+
47+func (x *XWM) onMapNotify(ev *xturbo.Event) {
48+ s := x.tab.lookup(uint32(xproto.MapNotifyOf(ev).Window()))
49+ if s == 0 {
50+ return
51+ }
52+ rec := x.tab.rec(s)
53+ again := rec.mapped
54+ rec.mapped = true
55+ if rec.kind == kindUnmanaged && !again {
56+ x.requestOverrideType(s)
57+ }
58+}
59+
60+// onUnmapNotify ends the mapped life of a managed window; a later MapRequest
61+// starts over and re-reads the properties. Synthetic events (ICCCM unmap
62+// requests) are not an unmap.
63+func (x *XWM) onUnmapNotify(ev *xturbo.Event) {
64+ if ev.SendEvent {
65+ return
66+ }
67+ s := x.tab.lookup(uint32(xproto.UnmapNotifyOf(ev).Window()))
68+ if s == 0 {
69+ return
70+ }
71+ rec := x.tab.rec(s)
72+ rec.mapped = false
73+ if rec.kind == kindUnmanaged {
74+ x.overrideUnmapped(s, rec)
75+ return
76+ }
77+ if rec.kind != kindManaged {
78+ return
79+ }
80+ wasMapping := rec.mapping
81+ rec.ready, rec.mapping, rec.pending, rec.sized = false, false, 0, false
82+ rec.gen++ // replies still in flight for this map name a dead tag
83+ if rec.gen == 0 {
84+ rec.gen = 1
85+ }
86+ rec.pend.dirty = false
87+ rec.serial, rec.surface = 0, 0
88+ if wasMapping {
89+ xid := rec.xid
90+ x.setProp32Typed(xid, atomWMState, x.atoms[atomWMState], []uint32{wmStateWithdrawn, 0})
91+ xproto.DeleteProperty(x.conn.Buf(), xproto.Window(xid), xproto.Atom(x.atoms[atomNetWMState]))
92+ }
93+ x.dropWindow(s)
94+ if rec.id != 0 {
95+ x.host.Dissociate(s, false)
96+ }
97+}
98+
99+// setProp32Typed is setProp32 with an explicit property type atom.
100+func (x *XWM) setProp32Typed(win uint32, prop int, typ uint32, vals []uint32) {
101+ x.setProp32(win, prop, xproto.Atom(typ), vals)
102+}
103+
104+// checkReady fires Ready once every awaited reply is in.
105+func (x *XWM) checkReady(s Slot) {
106+ rec := x.tab.rec(s)
107+ if rec == nil || rec.pending != 0 || rec.ready || !rec.mapping || rec.kind != kindManaged || x.host == nil {
108+ return
109+ }
110+ rec.ready = true
111+ x.classify(rec)
112+ info := x.info(s)
113+ xid := rec.xid
114+
115+ var state [2]uint32
116+ n := 0
117+ if rec.modal {
118+ state[n] = x.atoms[atomNetWMStateModal]
119+ n++
120+ }
121+ if rec.fullscreen {
122+ state[n] = x.atoms[atomNetWMStateFullscreen]
123+ n++
124+ }
125+ x.setProp32(xid, atomNetWMState, xproto.AtomAtom, state[:n])
126+ x.setProp32Typed(xid, atomWMState, x.atoms[atomWMState], []uint32{wmStateNormal, 0})
127+ x.addClient(xid)
128+
129+ x.host.Ready(s, info)
130+ x.tryPair(s)
131+}
132+
133+// info is what the host learns when a window is ready.
134+func (x *XWM) info(s Slot) WindowInfo {
135+ rec := x.tab.rec(s)
136+ parent := x.managedSlot(rec.transient)
137+ info := WindowInfo{
138+ PID: int(rec.pid),
139+ Floating: rec.floating,
140+ Parent: parent,
141+ Modal: rec.modal,
142+ Fullscreen: rec.fullscreen,
143+ Title: rec.title,
144+ AppID: rec.class,
145+ Bypass: rec.bypass,
146+ }
147+ if rec.sized && !info.Floating {
148+ info.AtX, info.AtY, info.HasAt = x.space.WholeOutput(int(rec.x), int(rec.y), int(rec.w), int(rec.h))
149+ }
150+ if info.Floating || rec.sized {
151+ info.Width, info.Height = int(rec.w), int(rec.h)
152+ }
153+ return info
154+}
155+
156+func (x *XWM) onGeomReply(s Slot, rec *record, r *xturbo.Reply) {
157+ if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok {
158+ rec.x, rec.y, rec.w, rec.h = int32(g.X()), int32(g.Y()), uint32(g.Width()), uint32(g.Height())
159+ }
160+ rec.pending &^= propGeom
161+ x.checkReady(s)
162+}
163+
164+// onClientMessage handles WL_SURFACE_SERIAL (pairing), the legacy
165+// WL_SURFACE_ID (counted, ignored) and _NET_WM_STATE requests.
166+func (x *XWM) onClientMessage(ev *xturbo.Event) {
167+ e := xproto.ClientMessageOf(ev)
168+ if e.Format() != 32 {
169+ return
170+ }
171+ typ := uint32(e.Type())
172+ switch typ {
173+ case x.atoms[atomWLSurfaceSerial]:
174+ s := x.tab.lookup(uint32(e.Window()))
175+ if s == 0 {
176+ return
177+ }
178+ rec := x.tab.rec(s)
179+ if rec.serial != 0 {
180+ x.log.Debug().Uint32("xid", rec.xid).Msg("duplicate WL_SURFACE_SERIAL ignored")
181+ return
182+ }
183+ d := e.Data().Data32()
184+ rec.serial = uint64(d[1])<<32 | uint64(d[0])
185+ x.tryPair(s)
186+ case x.atoms[atomWLSurfaceID]:
187+ x.stats.LegacyID++
188+ if x.stats.LegacyID == 1 {
189+ x.log.Warn().Msg("WL_SURFACE_ID received: Xwayland did not bind xwayland_shell_v1, window ignored")
190+ }
191+ case x.atoms[atomNetWMState]:
192+ x.onNetWMState(e)
193+ case x.atoms[atomWMChangeState]:
194+ // Iconify requests are ignored: windows stay mapped.
195+ }
196+}
197+
198+// onNetWMState forwards fullscreen requests to the host and records them; the
199+// property itself is written when core answers with a configure.
200+func (x *XWM) onNetWMState(e xproto.ClientMessageEvent) {
201+ s := x.tab.lookup(uint32(e.Window()))
202+ if s == 0 {
203+ return
204+ }
205+ rec := x.tab.rec(s)
206+ if rec.kind != kindManaged {
207+ return
208+ }
209+ d := e.Data().Data32()
210+ apply := func(cur bool) bool {
211+ switch d[0] {
212+ case netStateRemove:
213+ return false
214+ case netStateAdd:
215+ return true
216+ case netStateToggle:
217+ return !cur
218+ }
219+ return cur
220+ }
221+ for _, a := range d[1:3] {
222+ switch {
223+ case a == 0:
224+ case a == x.atoms[atomNetWMStateModal]:
225+ rec.modal = apply(rec.modal)
226+ case a == x.atoms[atomNetWMStateFullscreen]:
227+ on := apply(rec.fullscreen)
228+ if on == rec.fullscreen {
229+ continue
230+ }
231+ rec.fullscreen = on
232+ if rec.ready {
233+ x.host.Fullscreen(s, on)
234+ rec = x.tab.rec(s)
235+ }
236+ }
237+ }
238+}
239+
240+// onReparentNotify keeps the table to root's direct children: a window moved
241+// away is forgotten, one moved into root is treated as newly created.
242+func (x *XWM) onReparentNotify(ev *xturbo.Event) {
243+ e := xproto.ReparentNotifyOf(ev)
244+ xid := uint32(e.Window())
245+ if xid == x.wmWin {
246+ return
247+ }
248+ s := x.tab.lookup(xid)
249+ if uint32(e.Parent()) != x.root {
250+ if s != 0 {
251+ x.forgetWindow(s)
252+ }
253+ return
254+ }
255+ if s != 0 {
256+ return
257+ }
258+ s = x.insert(xid)
259+ if s == 0 {
260+ return
261+ }
262+ rec := x.tab.rec(s)
263+ rec.x, rec.y = int32(e.X()), int32(e.Y())
264+ if e.OverrideRedirect() {
265+ rec.kind = kindUnmanaged
266+ x.stats.UnmanagedSeen++
267+ return
268+ }
269+ if x.host != nil {
270+ x.adopt(s)
271+ }
272+}
273+
274+// tryPair links the window to the wl_surface carrying its serial, once both
275+// are known and the window is ready (the host maps on the surface's first
276+// buffer, so it must know the window's info by then). Unmanaged windows keep
277+// the surface but are never associated.
278+func (x *XWM) tryPair(s Slot) {
279+ rec := x.tab.rec(s)
280+ if rec.serial == 0 || rec.surface != 0 {
281+ return
282+ }
283+ if rec.kind == kindManaged && !rec.ready {
284+ return
285+ }
286+ for i := 0; i < x.unpairedN; i++ {
287+ if x.unpaired[i].serial != rec.serial {
288+ continue
289+ }
290+ rec.surface = x.unpaired[i].surface
291+ x.unpairedN--
292+ x.unpaired[i] = x.unpaired[x.unpairedN]
293+ if rec.kind == kindManaged {
294+ x.log.Debug().Uint32("xid", rec.xid).Uint64("surface", rec.surface).Msg("x11 window paired")
295+ x.host.Associate(s, rec.surface)
296+ } else {
297+ x.overrideMapped(s, rec)
298+ }
299+ return
300+ }
301+}
302+
303+// SetSerial records the serial of a wl_surface (xwayland_surface_v1.set_serial)
304+// and pairs it with the window announcing the same serial.
305+func (x *XWM) SetSerial(surfaceID uint64, serial uint64) {
306+ if serial == 0 || x.closed {
307+ return
308+ }
309+ for i := 0; i < x.unpairedN; i++ {
310+ if x.unpaired[i].serial == serial {
311+ x.unpaired[i].surface = surfaceID
312+ x.pairSerial(serial)
313+ return
314+ }
315+ }
316+ if x.unpairedN == unpairedCap {
317+ copy(x.unpaired[:], x.unpaired[1:])
318+ x.unpairedN--
319+ if !x.warnedUnpaired {
320+ x.warnedUnpaired = true
321+ x.log.Warn().Msg("too many unpaired surfaces, dropping the oldest")
322+ }
323+ }
324+ x.unpaired[x.unpairedN] = struct{ serial, surface uint64 }{serial, surfaceID}
325+ x.unpairedN++
326+ x.pairSerial(serial)
327+}
328+
329+// pairSerial offers a newly known surface serial to the windows.
330+func (x *XWM) pairSerial(serial uint64) {
331+ for i := range x.tab.recs {
332+ if r := &x.tab.recs[i]; r.kind != kindFree && r.serial == serial && r.surface == 0 {
333+ x.tryPair(Slot(i + 1))
334+ break
335+ }
336+ }
337+ x.wake()
338+}
339+
340+// SurfaceGone forgets a destroyed wl_surface.
341+func (x *XWM) SurfaceGone(surfaceID uint64) {
342+ for i := 0; i < x.unpairedN; i++ {
343+ if x.unpaired[i].surface == surfaceID {
344+ x.unpairedN--
345+ x.unpaired[i] = x.unpaired[x.unpairedN]
346+ break
347+ }
348+ }
349+ for i := range x.tab.recs {
350+ if r := &x.tab.recs[i]; r.kind != kindFree && r.surface == surfaceID {
351+ r.surface = 0
352+ if r.kind == kindUnmanaged && r.ovPaired {
353+ r.ovPaired = false
354+ x.host.OverrideGone(Slot(i + 1))
355+ }
356+ }
357+ }
358+ x.wake()
359+}
A internal/adapters/wayland/xwm/mapflow_test.go +806 -0
@@ -0,0 +1,806 @@
1+package xwm
2+
3+import (
4+ "context"
5+ "encoding/binary"
6+ "testing"
7+
8+ "bnema.dev/xturbo"
9+ "bnema.dev/xturbo/xproto"
10+ "github.com/bnema/neferwl/internal/ports"
11+ "github.com/stretchr/testify/mock"
12+ "github.com/stretchr/testify/require"
13+)
14+
15+// harness bundles an attached XWM, its peer and the host mock.
16+type harness struct {
17+ t *testing.T
18+ x *XWM
19+ p *xpeer
20+ h *mockhost
21+
22+ ready map[uint32]WindowInfo // by xid, filled by Ready
23+ next ports.WindowID
24+}
25+
26+func newHarness(t *testing.T, mutate func(p *xpeer)) *harness {
27+ t.Helper()
28+ x, p := bootPeer(t, mutate)
29+ hs := &harness{t: t, x: x, p: p, h: newMockhost(t), ready: map[uint32]WindowInfo{}}
30+ src := newMockfdSource(t)
31+ src.EXPECT().Remove().Maybe()
32+ src.EXPECT().SetWritable(mock.Anything).Return(nil).Maybe()
33+ post, _ := newPost()
34+ x.attach(src, hs.h, post)
35+ return hs
36+}
37+
38+func (hs *harness) expectCreated(win uint32) {
39+ hs.next++
40+ hs.h.EXPECT().Created(mock.Anything, win).Return(hs.next).Once()
41+}
42+
43+func (hs *harness) expectReady(win uint32) {
44+ hs.h.EXPECT().Ready(mock.Anything, mock.Anything).Run(func(_ Slot, info WindowInfo) {
45+ hs.ready[win] = info
46+ }).Once()
47+}
48+
49+func (hs *harness) create(win uint32) Slot {
50+ hs.t.Helper()
51+ hs.expectCreated(win)
52+ hs.p.createNotify(win, false)
53+ readUntil(hs.t, hs.x, func() bool { return hs.x.tab.lookup(win) != 0 })
54+ return hs.x.tab.lookup(win)
55+}
56+
57+// mapAndWait creates and maps win and waits for Ready.
58+func (hs *harness) mapAndWait(win uint32) Slot {
59+ hs.t.Helper()
60+ s := hs.create(win)
61+ hs.expectReady(win)
62+ hs.p.mapRequest(win)
63+ readUntil(hs.t, hs.x, func() bool { _, ok := hs.ready[win]; return ok })
64+ return s
65+}
66+
67+func (hs *harness) str(win uint32, atom string, typ uint32, s string) {
68+ hs.p.setProp(win, hs.p.atom(atom), typ, 8, []byte(s))
69+}
70+
71+func (hs *harness) words(win uint32, atom string, typ uint32, w ...uint32) {
72+ hs.p.setProp(win, hs.p.atom(atom), typ, 32, le32(w...))
73+}
74+
75+func (hs *harness) atoms(win uint32, atom string, names ...string) {
76+ var w []uint32
77+ for _, n := range names {
78+ w = append(w, hs.p.atom(n))
79+ }
80+ hs.words(win, atom, uint32(xproto.AtomAtom), w...)
81+}
82+
83+func (hs *harness) classic(win uint32) {
84+ hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("inst\x00Class\x00"))
85+ hs.str(win, "_NET_WM_NAME", hs.p.atom("UTF8_STRING"), "net title")
86+ hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("old title"))
87+ hs.words(win, "_NET_WM_PID", uint32(xproto.AtomCardinal), 4242)
88+ hs.p.set(func(p *xpeer) { p.geoms[win] = xpeerGeom{w: 300, h: 200} })
89+}
90+
91+func (hs *harness) count(match func(r xpeerReq) bool) int {
92+ n := 0
93+ for _, r := range hs.p.requests() {
94+ if match(r) {
95+ n++
96+ }
97+ }
98+ return n
99+}
100+
101+func (hs *harness) waitMap(win uint32) {
102+ hs.t.Helper()
103+ readUntil(hs.t, hs.x, func() bool {
104+ return hs.count(func(r xpeerReq) bool { return r.major == opMapWindow && r.u32(4) == win }) > 0
105+ })
106+}
107+
108+func TestMapFlowBatchesRequestsAndReadyFiresOnce(t *testing.T) {
109+ hs := newHarness(t, nil)
110+ const win = 0x500001
111+ hs.classic(win)
112+ hs.create(win)
113+ base := len(hs.p.requests())
114+
115+ hs.p.holdReplies()
116+ hs.p.mapRequest(win)
117+ hs.expectReady(win)
118+ hs.waitMap(win)
119+
120+ reqs := hs.p.requests()[base:]
121+ want := []uint32{
122+ uint32(xproto.AtomWmClass), uint32(xproto.AtomWmName), hs.p.atom("_NET_WM_NAME"), uint32(xproto.AtomWmHints),
123+ uint32(xproto.AtomWmNormalHints), hs.p.atom("WM_PROTOCOLS"), uint32(xproto.AtomWmTransientFor),
124+ hs.p.atom("_NET_WM_WINDOW_TYPE"), hs.p.atom("_NET_WM_STATE"), hs.p.atom("_NET_WM_PID"), hs.p.atom("_NET_WM_BYPASS_COMPOSITOR"),
125+ }
126+ require.GreaterOrEqual(t, len(reqs), len(want)+2)
127+ for i, a := range want {
128+ require.EqualValues(t, opGetProperty, reqs[i].major, i)
129+ require.Equal(t, uint32(win), reqs[i].u32(4))
130+ require.Equal(t, a, reqs[i].u32(8), i)
131+ require.EqualValues(t, 1024, reqs[i].u32(20), "long length: one 4 KiB read")
132+ }
133+ require.EqualValues(t, opGetGeometry, reqs[len(want)].major)
134+ require.EqualValues(t, opMapWindow, reqs[len(want)+1].major, "MapWindow in the same batch")
135+
136+ hs.p.release() // every reply arrives after MapWindow went out
137+ readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok })
138+ info := hs.ready[win]
139+ require.Equal(t, WindowInfo{PID: 4242, Title: "net title", AppID: "Class"}, info, "_NET_WM_NAME wins, class is the second string")
140+
141+ state, ok := hs.p.prop(win, "WM_STATE")
142+ require.True(t, ok)
143+ require.Equal(t, []uint32{1, 0}, atomsOf(state))
144+ list, ok := hs.p.prop(peerRoot, "_NET_CLIENT_LIST")
145+ require.True(t, ok)
146+ require.Equal(t, []uint32{win}, atomsOf(list))
147+}
148+
149+func TestMapFlowFloatingAndModal(t *testing.T) {
150+ hint := func(minw, minh, maxw, maxh uint32) []byte {
151+ return le32(hintMinSize|hintMaxSize, 0, 0, 0, 0, minw, minh, maxw, maxh, 0, 0, 0, 0, 0, 0, 0, 0, 0)
152+ }
153+ cases := []struct {
154+ name string
155+ setup func(hs *harness, win uint32)
156+ want func(info WindowInfo)
157+ }{
158+ {"dialog type", func(hs *harness, w uint32) {
159+ hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_DIALOG")
160+ }, func(i WindowInfo) {
161+ require.True(t, i.Floating)
162+ require.Equal(t, 300, i.Width)
163+ require.Equal(t, 200, i.Height)
164+ }},
165+ {"utility type", func(hs *harness, w uint32) {
166+ hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_UTILITY")
167+ }, func(i WindowInfo) { require.True(t, i.Floating) }},
168+ {"splash type", func(hs *harness, w uint32) {
169+ hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_SPLASH")
170+ }, func(i WindowInfo) { require.True(t, i.Floating) }},
171+ {"normal type", func(hs *harness, w uint32) {
172+ hs.atoms(w, "_NET_WM_WINDOW_TYPE", "_NET_WM_WINDOW_TYPE_NORMAL")
173+ }, func(i WindowInfo) { require.False(t, i.Floating); require.Zero(t, i.Width) }},
174+ {"fixed size", func(hs *harness, w uint32) {
175+ hs.p.setProp(w, uint32(xproto.AtomWmNormalHints), uint32(xproto.AtomWmSizeHints), 32, hint(200, 100, 200, 100))
176+ }, func(i WindowInfo) { require.True(t, i.Floating) }},
177+ {"resizable", func(hs *harness, w uint32) {
178+ hs.p.setProp(w, uint32(xproto.AtomWmNormalHints), uint32(xproto.AtomWmSizeHints), 32, hint(100, 100, 800, 600))
179+ }, func(i WindowInfo) { require.False(t, i.Floating) }},
180+ {"modal", func(hs *harness, w uint32) {
181+ hs.atoms(w, "_NET_WM_STATE", "_NET_WM_STATE_MODAL")
182+ }, func(i WindowInfo) { require.True(t, i.Modal); require.False(t, i.Fullscreen) }},
183+ {"bypass 1", func(hs *harness, w uint32) {
184+ hs.words(w, "_NET_WM_BYPASS_COMPOSITOR", uint32(xproto.AtomCardinal), 1)
185+ }, func(i WindowInfo) { require.EqualValues(t, 1, i.Bypass) }},
186+ {"bypass 2 kept", func(hs *harness, w uint32) {
187+ hs.words(w, "_NET_WM_BYPASS_COMPOSITOR", uint32(xproto.AtomCardinal), 2)
188+ }, func(i WindowInfo) { require.EqualValues(t, 2, i.Bypass) }},
189+ {"bypass absent", func(hs *harness, w uint32) {}, func(i WindowInfo) { require.Zero(t, i.Bypass) }},
190+ {"class steam_app", func(hs *harness, w uint32) {
191+ hs.p.setProp(w, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("steam_app_892970\x00steam_app_892970\x00"))
192+ }, func(i WindowInfo) { require.Equal(t, "steam_app_892970", i.AppID) }},
193+ {"fullscreen at map", func(hs *harness, w uint32) {
194+ hs.atoms(w, "_NET_WM_STATE", "_NET_WM_STATE_FULLSCREEN")
195+ }, func(i WindowInfo) { require.True(t, i.Fullscreen) }},
196+ }
197+ for _, c := range cases {
198+ t.Run(c.name, func(t *testing.T) {
199+ hs := newHarness(t, nil)
200+ const win = 0x500001
201+ hs.classic(win)
202+ c.setup(hs, win)
203+ hs.mapAndWait(win)
204+ c.want(hs.ready[win])
205+ })
206+ }
207+}
208+
209+func TestMapFlowTransientForManagedWindowFloats(t *testing.T) {
210+ hs := newHarness(t, nil)
211+ const parent, child = 0x500001, 0x500002
212+ hs.classic(parent)
213+ ps := hs.mapAndWait(parent)
214+ hs.classic(child)
215+ hs.p.setProp(child, uint32(xproto.AtomWmTransientFor), uint32(xproto.AtomWindow), 32, le32(parent))
216+ hs.mapAndWait(child)
217+ require.True(t, hs.ready[child].Floating)
218+ require.Equal(t, ps, hs.ready[child].Parent)
219+}
220+
221+func TestMapFlowTransientForUnknownWindowIsNotAParent(t *testing.T) {
222+ hs := newHarness(t, nil)
223+ const win = 0x500001
224+ hs.classic(win)
225+ hs.p.setProp(win, uint32(xproto.AtomWmTransientFor), uint32(xproto.AtomWindow), 32, le32(0x777777))
226+ hs.mapAndWait(win)
227+ require.Zero(t, hs.ready[win].Parent)
228+ require.False(t, hs.ready[win].Floating)
229+}
230+
231+func TestMapFlowClassFallsBackToInstance(t *testing.T) {
232+ hs := newHarness(t, nil)
233+ const win = 0x500001
234+ hs.classic(win)
235+ hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("onlyinstance\x00"))
236+ hs.mapAndWait(win)
237+ require.Equal(t, "onlyinstance", hs.ready[win].AppID)
238+}
239+
240+func TestMapFlowTitleFallsBackToWMName(t *testing.T) {
241+ hs := newHarness(t, nil)
242+ const win = 0x500001
243+ hs.classic(win)
244+ hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_NAME"]) })
245+ hs.mapAndWait(win)
246+ require.Equal(t, "old title", hs.ready[win].Title)
247+}
248+
249+func TestMapFlowPIDFromXRes(t *testing.T) {
250+ hs := newHarness(t, func(p *xpeer) { p.pids = map[uint32]uint32{0x500001: 777} })
251+ const win = 0x500001
252+ hs.classic(win)
253+ hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_PID"]) })
254+ hs.create(win)
255+ hs.expectReady(win)
256+ hs.p.mapRequest(win)
257+ readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok })
258+ require.Equal(t, 777, hs.ready[win].PID, "Ready waited for the QueryClientIds reply")
259+ require.Equal(t, 1, hs.count(func(r xpeerReq) bool { return r.major == 144 && r.minor == 4 }))
260+ require.Zero(t, hs.x.tab.rec(hs.x.tab.lookup(win)).pending)
261+}
262+
263+func TestMapFlowPIDUnknownStillReady(t *testing.T) {
264+ hs := newHarness(t, nil)
265+ const win = 0x500001
266+ hs.classic(win)
267+ hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_PID"]) })
268+ hs.mapAndWait(win)
269+ require.Zero(t, hs.ready[win].PID)
270+}
271+
272+func TestMapFlowNoXResAsksNothing(t *testing.T) {
273+ exts := fullPeerExts()
274+ delete(exts, "X-Resource")
275+ fd, p := startXPeer(t, exts)
276+ x, err := Boot(context.Background(), fd, testLog())
277+ require.NoError(t, err)
278+ t.Cleanup(x.CloseConn)
279+ h := newMockhost(t)
280+ src := newMockfdSource(t)
281+ src.EXPECT().Remove().Maybe()
282+ post, _ := newPost()
283+ x.attach(src, h, post)
284+ hs := &harness{t: t, x: x, p: p, h: h, ready: map[uint32]WindowInfo{}}
285+ const win = 0x500001
286+ hs.classic(win)
287+ p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_PID"]) })
288+ hs.mapAndWait(win)
289+ require.Zero(t, hs.count(func(r xpeerReq) bool { return r.major == 144 }))
290+}
291+
292+func TestMapFlowBadWindowOnPropertiesNeverReady(t *testing.T) {
293+ hs := newHarness(t, nil)
294+ const win = 0x500001
295+ hs.classic(win)
296+ hs.p.set(func(p *xpeer) { p.bad[win] = true })
297+ s := hs.create(win)
298+ hs.p.mapRequest(win)
299+ readUntil(t, hs.x, func() bool { return hs.x.Stats().Errors >= uint64(propCount)+1 })
300+ require.Zero(t, hs.x.tab.rec(s).pending, "failed replies are no longer awaited")
301+ require.False(t, hs.x.tab.rec(s).ready)
302+ require.Zero(t, hs.x.Stats().Unattributed)
303+
304+ hs.h.EXPECT().Dissociate(s, true).Once()
305+ hs.p.destroyNotify(win)
306+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 })
307+}
308+
309+func TestMapRequestTwiceBeforeReadyAsksOnce(t *testing.T) {
310+ hs := newHarness(t, nil)
311+ const win = 0x500001
312+ hs.classic(win)
313+ hs.create(win)
314+ hs.p.holdReplies()
315+ hs.p.mapRequest(win)
316+ hs.p.mapRequest(win)
317+ hs.expectReady(win)
318+ hs.waitMap(win)
319+ hs.p.release()
320+ readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok })
321+ require.Equal(t, 1, hs.count(func(r xpeerReq) bool { return r.major == opMapWindow && r.u32(4) == win }))
322+}
323+
324+func TestMapNotifyMarksMapped(t *testing.T) {
325+ hs := newHarness(t, nil)
326+ const win = 0x500001
327+ hs.classic(win)
328+ s := hs.mapAndWait(win)
329+ hs.p.mapNotify(win)
330+ readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).mapped })
331+}
332+
333+// Property changes after Ready.
334+
335+func (hs *harness) notify(win uint32, atom string, deleted bool) {
336+ hs.t.Helper()
337+ before := hs.x.Stats().Replies
338+ hs.p.propertyNotify(win, hs.p.atom(atom), deleted)
339+ if deleted {
340+ hs.p.propertyNotify(win, hs.p.atom("WM_PROTOCOLS"), false) // sentinel: one reply follows
341+ readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before })
342+ return
343+ }
344+ readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before })
345+}
346+
347+func TestPropertyChangedTitleReachesHostOnce(t *testing.T) {
348+ hs := newHarness(t, nil)
349+ const win = 0x500001
350+ hs.classic(win)
351+ s := hs.mapAndWait(win)
352+
353+ hs.h.EXPECT().Title(s, "renamed").Once()
354+ hs.str(win, "_NET_WM_NAME", hs.p.atom("UTF8_STRING"), "renamed")
355+ hs.notify(win, "_NET_WM_NAME", false)
356+ require.Equal(t, "renamed", hs.x.tab.rec(s).title)
357+
358+ // Same value again: no host call (the mock fails on an unexpected one).
359+ hs.notify(win, "_NET_WM_NAME", false)
360+
361+ // WM_NAME is ignored while _NET_WM_NAME is set.
362+ hs.p.setProp(win, uint32(xproto.AtomWmName), uint32(xproto.AtomString), 8, []byte("legacy"))
363+ before := hs.x.Stats().Events
364+ hs.p.propertyNotify(win, uint32(xproto.AtomWmName), false)
365+ readUntil(t, hs.x, func() bool { return hs.x.Stats().Events > before })
366+ require.Equal(t, "renamed", hs.x.tab.rec(s).title)
367+}
368+
369+func TestPropertyNetNameDeletedFallsBackToWMName(t *testing.T) {
370+ hs := newHarness(t, nil)
371+ const win = 0x500001
372+ hs.classic(win)
373+ s := hs.mapAndWait(win)
374+ hs.p.set(func(p *xpeer) { delete(p.props[win], p.atoms["_NET_WM_NAME"]) })
375+ hs.h.EXPECT().Title(s, "old title").Once()
376+ hs.p.propertyNotify(win, hs.p.atom("_NET_WM_NAME"), true)
377+ readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).title == "old title" })
378+}
379+
380+func TestPropertyClassChangeReachesHost(t *testing.T) {
381+ hs := newHarness(t, nil)
382+ const win = 0x500001
383+ hs.classic(win)
384+ s := hs.mapAndWait(win)
385+ hs.h.EXPECT().AppID(s, "Other").Once()
386+ hs.p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("i\x00Other\x00"))
387+ hs.p.propertyNotify(win, uint32(xproto.AtomWmClass), false)
388+ readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).class == "Other" })
389+}
390+
391+func TestPropertyTransientChangeReachesHost(t *testing.T) {
392+ hs := newHarness(t, nil)
393+ const a, b = 0x500001, 0x500002
394+ hs.classic(a)
395+ as := hs.mapAndWait(a)
396+ hs.classic(b)
397+ bs := hs.mapAndWait(b)
398+ hs.h.EXPECT().Parent(bs, as).Once()
399+ hs.p.setProp(b, uint32(xproto.AtomWmTransientFor), uint32(xproto.AtomWindow), 32, le32(a))
400+ hs.p.propertyNotify(b, uint32(xproto.AtomWmTransientFor), false)
401+ readUntil(t, hs.x, func() bool { return hs.x.tab.rec(bs).transient == a })
402+}
403+
404+func TestPropertyInputProtocolsAndHints(t *testing.T) {
405+ hs := newHarness(t, nil)
406+ const win = 0x500001
407+ hs.classic(win)
408+ hs.atoms(win, "WM_PROTOCOLS", "WM_DELETE_WINDOW", "WM_TAKE_FOCUS")
409+ hs.p.setProp(win, uint32(xproto.AtomWmHints), uint32(xproto.AtomWmHints), 32, le32(hintInput, 0, 0, 0, 0, 0, 0, 0, 0))
410+ s := hs.mapAndWait(win)
411+ r := hs.x.tab.rec(s)
412+ require.True(t, r.deleteWindow)
413+ require.True(t, r.takeFocus)
414+ require.False(t, r.input, "WM_HINTS input = false")
415+
416+ // No WM_HINTS: input defaults to true.
417+ const other = 0x500002
418+ hs.classic(other)
419+ o := hs.mapAndWait(other)
420+ require.True(t, hs.x.tab.rec(o).input)
421+ require.False(t, hs.x.tab.rec(o).deleteWindow)
422+}
423+
424+func TestPropertyUnwatchedAtomAsksNothing(t *testing.T) {
425+ hs := newHarness(t, nil)
426+ const win = 0x500001
427+ hs.classic(win)
428+ hs.mapAndWait(win)
429+ n := len(hs.p.requests())
430+ hs.p.propertyNotify(win, hs.p.atom("_MOTIF_WM_HINTS"), false)
431+ hs.p.propertyNotify(0x999999, hs.p.atom("_NET_WM_NAME"), false) // unknown window
432+ readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 2 })
433+ require.Len(t, hs.p.requests(), n)
434+}
435+
436+// Pairing.
437+
438+func (hs *harness) serialMsg(win uint32, serial uint64) {
439+ hs.p.clientMessage(win, hs.p.atom("WL_SURFACE_SERIAL"), uint32(serial), uint32(serial>>32))
440+ hs.t.Helper()
441+ before := hs.x.Stats().Events
442+ readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Events > before })
443+}
444+
445+func TestPairingSerialBeforeSetSerial(t *testing.T) {
446+ hs := newHarness(t, nil)
447+ const win = 0x500001
448+ hs.classic(win)
449+ s := hs.mapAndWait(win)
450+ hs.serialMsg(win, 0x1_0000_0002)
451+ require.EqualValues(t, 0x1_0000_0002, hs.x.tab.rec(s).serial, "high word in data32[1]")
452+ hs.h.EXPECT().Associate(s, uint64(99)).Once()
453+ hs.x.SetSerial(99, 0x1_0000_0002)
454+ require.EqualValues(t, 99, hs.x.tab.rec(s).surface)
455+ require.Zero(t, hs.x.unpairedN)
456+}
457+
458+func TestPairingSetSerialBeforeSerial(t *testing.T) {
459+ hs := newHarness(t, nil)
460+ const win = 0x500001
461+ hs.classic(win)
462+ s := hs.mapAndWait(win)
463+ hs.x.SetSerial(99, 7)
464+ require.Equal(t, 1, hs.x.unpairedN)
465+ hs.h.EXPECT().Associate(s, uint64(99)).Once()
466+ hs.serialMsg(win, 7)
467+ require.Zero(t, hs.x.unpairedN)
468+}
469+
470+func TestPairingWaitsForReady(t *testing.T) {
471+ hs := newHarness(t, nil)
472+ const win = 0x500001
473+ hs.classic(win)
474+ s := hs.create(win)
475+ hs.x.SetSerial(5, 3)
476+ hs.serialMsg(win, 3) // not mapped yet: no Associate (the mock fails on one)
477+ require.Zero(t, hs.x.tab.rec(s).surface)
478+
479+ hs.h.EXPECT().Associate(s, uint64(5)).Once()
480+ hs.expectReady(win)
481+ hs.p.mapRequest(win)
482+ readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).surface != 0 })
483+}
484+
485+func TestPairingSerialForUnknownWindowIgnored(t *testing.T) {
486+ hs := newHarness(t, nil)
487+ hs.x.SetSerial(5, 3)
488+ hs.serialMsg(0x888888, 3)
489+ require.Equal(t, 1, hs.x.unpairedN, "stays unpaired")
490+}
491+
492+func TestPairingSurfaceGoneBeforePair(t *testing.T) {
493+ hs := newHarness(t, nil)
494+ const win = 0x500001
495+ hs.classic(win)
496+ s := hs.mapAndWait(win)
497+ hs.x.SetSerial(5, 3)
498+ hs.x.SurfaceGone(5)
499+ require.Zero(t, hs.x.unpairedN)
500+ hs.serialMsg(win, 3) // the surface is gone: nothing to pair with
501+ require.Zero(t, hs.x.tab.rec(s).surface)
502+}
503+
504+func TestPairingSurfaceGoneAfterPairClearsLink(t *testing.T) {
505+ hs := newHarness(t, nil)
506+ const win = 0x500001
507+ hs.classic(win)
508+ s := hs.mapAndWait(win)
509+ hs.serialMsg(win, 3)
510+ hs.h.EXPECT().Associate(s, uint64(5)).Once()
511+ hs.x.SetSerial(5, 3)
512+ hs.x.SurfaceGone(5)
513+ require.Zero(t, hs.x.tab.rec(s).surface)
514+}
515+
516+func TestPairingDuplicateSerialIgnored(t *testing.T) {
517+ hs := newHarness(t, nil)
518+ const win = 0x500001
519+ hs.classic(win)
520+ s := hs.mapAndWait(win)
521+ hs.serialMsg(win, 3)
522+ hs.serialMsg(win, 4)
523+ require.EqualValues(t, 3, hs.x.tab.rec(s).serial)
524+ hs.x.SetSerial(8, 4) // nobody announces 4: stays unpaired
525+ require.Equal(t, 1, hs.x.unpairedN)
526+}
527+
528+func TestPairingUnmanagedKeepsSurfaceWithoutAssociate(t *testing.T) {
529+ hs := newHarness(t, nil)
530+ hs.p.createNotify(0x500009, true)
531+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) != 0 })
532+ hs.serialMsg(0x500009, 3)
533+ hs.x.SetSerial(5, 3) // the mock fails on an Associate
534+ require.EqualValues(t, 5, hs.x.tab.rec(hs.x.tab.lookup(0x500009)).surface)
535+}
536+
537+func TestPairingTableOverflowDropsOldest(t *testing.T) {
538+ hs := newHarness(t, nil)
539+ for i := uint64(1); i <= unpairedCap+1; i++ {
540+ hs.x.SetSerial(100+i, i)
541+ }
542+ require.Equal(t, unpairedCap, hs.x.unpairedN)
543+ for i := 0; i < hs.x.unpairedN; i++ {
544+ require.NotEqual(t, uint64(1), hs.x.unpaired[i].serial, "oldest dropped")
545+ }
546+}
547+
548+func TestPairingZeroSerialIgnored(t *testing.T) {
549+ hs := newHarness(t, nil)
550+ hs.x.SetSerial(5, 0)
551+ require.Zero(t, hs.x.unpairedN)
552+}
553+
554+func TestLegacySurfaceIDCountedAndIgnored(t *testing.T) {
555+ hs := newHarness(t, nil)
556+ const win = 0x500001
557+ hs.classic(win)
558+ s := hs.mapAndWait(win)
559+ hs.p.clientMessage(win, hs.p.atom("WL_SURFACE_ID"), 12)
560+ readUntil(t, hs.x, func() bool { return hs.x.Stats().LegacyID == 1 })
561+ require.Zero(t, hs.x.tab.rec(s).serial)
562+}
563+
564+func TestNetWMStateClientMessageForwardsFullscreen(t *testing.T) {
565+ hs := newHarness(t, nil)
566+ const win = 0x500001
567+ hs.classic(win)
568+ s := hs.mapAndWait(win)
569+ hs.h.EXPECT().Fullscreen(s, true).Once()
570+ hs.p.clientMessage(win, hs.p.atom("_NET_WM_STATE"), 1, hs.p.atom("_NET_WM_STATE_FULLSCREEN"))
571+ readUntil(t, hs.x, func() bool { return hs.x.tab.rec(s).fullscreen })
572+ hs.h.EXPECT().Fullscreen(s, false).Once() // toggle
573+ hs.p.clientMessage(win, hs.p.atom("_NET_WM_STATE"), 2, hs.p.atom("_NET_WM_STATE_FULLSCREEN"))
574+ readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).fullscreen })
575+ // Not written back by the message itself.
576+ st, ok := hs.p.prop(win, "_NET_WM_STATE")
577+ require.True(t, ok)
578+ require.Empty(t, st.data)
579+}
580+
581+// Unmap, destroy, remap.
582+
583+func TestUnmapDissociatesAndRemapRereadsProperties(t *testing.T) {
584+ hs := newHarness(t, nil)
585+ const win = 0x500001
586+ hs.classic(win)
587+ s := hs.mapAndWait(win)
588+
589+ // A synthetic UnmapNotify (ICCCM withdraw request) is not an unmap.
590+ hs.p.unmapNotify(win, true)
591+ readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 2 })
592+ require.True(t, hs.x.tab.rec(s).ready)
593+
594+ hs.h.EXPECT().Dissociate(s, false).Once()
595+ hs.p.unmapNotify(win, false)
596+ readUntil(t, hs.x, func() bool { return !hs.x.tab.rec(s).ready })
597+ r := hs.x.tab.rec(s)
598+ require.False(t, r.mapping)
599+ require.Zero(t, r.serial)
600+ hs.p.waitReq(0, func(q xpeerReq) bool {
601+ return q.major == opSetInputFocus || q.major == opChangeProperty && q.u32(4) == win
602+ })
603+ readUntil(t, hs.x, func() bool {
604+ v, ok := hs.p.prop(win, "WM_STATE")
605+ return ok && len(v.data) == 8 && binary.LittleEndian.Uint32(v.data) == 0
606+ })
607+ _, ok := hs.p.prop(win, "_NET_WM_STATE")
608+ require.False(t, ok, "_NET_WM_STATE deleted")
609+ list, _ := hs.p.prop(peerRoot, "_NET_CLIENT_LIST")
610+ require.Empty(t, list.data)
611+
612+ // Remap starts over.
613+ delete(hs.ready, win)
614+ hs.expectReady(win)
615+ n := hs.count(func(q xpeerReq) bool { return q.major == opGetProperty && q.u32(4) == win })
616+ hs.p.mapRequest(win)
617+ readUntil(t, hs.x, func() bool { _, ok := hs.ready[win]; return ok })
618+ require.Equal(t, n+propCount, hs.count(func(q xpeerReq) bool { return q.major == opGetProperty && q.u32(4) == win }))
619+ list, _ = hs.p.prop(peerRoot, "_NET_CLIENT_LIST")
620+ require.Equal(t, []uint32{win}, atomsOf(list))
621+}
622+
623+func TestDestroyWhileMappingNeverReady(t *testing.T) {
624+ hs := newHarness(t, nil)
625+ const win = 0x500001
626+ hs.classic(win)
627+ s := hs.create(win)
628+ hs.p.holdReplies()
629+ hs.p.mapRequest(win)
630+ hs.waitMap(win)
631+ hs.h.EXPECT().Dissociate(s, true).Once()
632+ hs.p.destroyNotify(win)
633+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 })
634+ replies := hs.x.Stats().Replies
635+ hs.p.release() // replies for a window that is gone: xturbo retired their slots
636+ readUntil(t, hs.x, func() bool { return hs.x.conn.Stats().DroppedReplies >= uint64(propCount) })
637+ require.Equal(t, replies, hs.x.Stats().Replies, "none reaches the XWM")
638+}
639+
640+func TestStaleRepliesAfterSlotReuseAreIgnored(t *testing.T) {
641+ hs := newHarness(t, nil)
642+ const old, fresh = 0x500001, 0x500002
643+ hs.classic(old)
644+ hs.classic(fresh)
645+ s := hs.create(old)
646+ hs.p.holdReplies()
647+ hs.p.mapRequest(old)
648+ hs.waitMap(old)
649+ hs.h.EXPECT().Dissociate(s, true).Once()
650+ hs.p.destroyNotify(old)
651+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(old) == 0 })
652+
653+ hs.expectCreated(fresh)
654+ hs.p.createNotify(fresh, false)
655+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(fresh) != 0 })
656+ require.Equal(t, s, hs.x.tab.lookup(fresh), "the freed slot is reused")
657+
658+ hs.p.release()
659+ readUntil(t, hs.x, func() bool { return hs.x.conn.Stats().DroppedReplies >= uint64(propCount) })
660+
661+ // A reply that does get through with the old tag (slot reused, generation
662+ // bumped) is ignored.
663+ oldTag := s | Slot(hs.x.tab.rec(s).gen-1)<<16
664+ rep := make([]byte, 32)
665+ rep[0] = 1
666+ hs.x.onReply(&xturbo.Reply{Kind: kindProp, Args: [4]uint32{uint32(oldTag), propClass}, Raw: rep})
667+ hs.x.onReply(&xturbo.Reply{Kind: kindGeom, Args: [4]uint32{uint32(oldTag)}, Raw: rep})
668+ require.EqualValues(t, 2, hs.x.Stats().Stale)
669+ require.False(t, hs.x.tab.rec(s).ready, "stale replies must not complete the new window")
670+ require.Zero(t, hs.x.tab.rec(s).pending)
671+}
672+
673+// Reparenting.
674+
675+func TestReparentAwayFromRootRemovesManagedWindow(t *testing.T) {
676+ hs := newHarness(t, nil)
677+ const win = 0x500001
678+ hs.classic(win)
679+ s := hs.mapAndWait(win)
680+ hs.h.EXPECT().Dissociate(s, true).Once()
681+ hs.p.reparentNotify(win, 0x600000)
682+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) == 0 })
683+ list, _ := hs.p.prop(peerRoot, "_NET_CLIENT_LIST")
684+ require.Empty(t, list.data)
685+ require.Zero(t, hs.x.tab.n)
686+}
687+
688+func TestReparentAwayOfUnmanagedWindowRemovesIt(t *testing.T) {
689+ hs := newHarness(t, nil)
690+ hs.p.createNotify(0x500009, true)
691+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) != 0 })
692+ hs.p.reparentNotify(0x500009, 0x600000)
693+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) == 0 })
694+}
695+
696+func TestReparentIntoRootAdoptsWindow(t *testing.T) {
697+ hs := newHarness(t, nil)
698+ const win = 0x500001
699+ hs.expectCreated(win)
700+ hs.p.reparentNotify(win, peerRoot)
701+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(win) != 0 })
702+ require.Equal(t, kindManaged, hs.x.tab.rec(hs.x.tab.lookup(win)).kind)
703+
704+ // Already known: no second Created.
705+ hs.p.reparentNotify(win, peerRoot)
706+ readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 2 })
707+}
708+
709+func TestReparentIntoRootOfOverrideRedirectWindowStaysUnmanaged(t *testing.T) {
710+ hs := newHarness(t, nil)
711+ hs.p.event(21, func(b *[32]byte) {
712+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
713+ binary.LittleEndian.PutUint32(b[8:], 0x500009)
714+ binary.LittleEndian.PutUint32(b[12:], peerRoot)
715+ b[20] = 1
716+ })
717+ readUntil(t, hs.x, func() bool { return hs.x.tab.lookup(0x500009) != 0 })
718+ require.Equal(t, kindUnmanaged, hs.x.tab.rec(hs.x.tab.lookup(0x500009)).kind)
719+}
720+
721+func TestReparentOfWMWindowIgnored(t *testing.T) {
722+ hs := newHarness(t, nil)
723+ hs.p.reparentNotify(hs.x.wmWin, peerRoot)
724+ readUntil(t, hs.x, func() bool { return hs.x.Stats().Events >= 1 })
725+ require.Zero(t, hs.x.tab.lookup(hs.x.wmWin))
726+}
727+
728+// Window known only before Attach: Attach replays the map flow.
729+func TestAttachMapsWindowsRequestedBeforeAttach(t *testing.T) {
730+ x, p := bootPeer(t, nil)
731+ const win = 0x500001
732+ p.setProp(win, uint32(xproto.AtomWmClass), uint32(xproto.AtomString), 8, []byte("a\x00B\x00"))
733+ p.createNotify(win, false)
734+ p.mapRequest(win)
735+ readUntil(t, x, func() bool { return x.Stats().Events >= 2 })
736+ h := newMockhost(t)
737+ src := newMockfdSource(t)
738+ src.EXPECT().Remove().Maybe()
739+ post, _ := newPost()
740+ var got WindowInfo
741+ done := false
742+ h.EXPECT().Created(mock.Anything, uint32(win)).Return(ports.WindowID(1)).Once()
743+ h.EXPECT().Ready(mock.Anything, mock.Anything).Run(func(_ Slot, i WindowInfo) { got, done = i, true }).Once()
744+ x.attach(src, h, post)
745+ readUntil(t, x, func() bool { return done })
746+ require.Equal(t, "B", got.AppID)
747+}
748+
749+// Replies complete a map in any order (a server may answer the geometry
750+// before the properties it was asked for first): Ready fires on the last one.
751+func TestMapFlowReadyFiresOnLastReplyInAnyOrder(t *testing.T) {
752+ hs := newHarness(t, nil)
753+ const win = 0x500001
754+ hs.classic(win)
755+ s := hs.create(win)
756+ r := hs.x.tab.rec(s)
757+ r.mapping, r.pending = true, propMaskAll&^propClientIDs
758+ hs.expectReady(win)
759+ order := []int{propCount, propPID, propBypass, propClass, propNetName, propName, propHints, propNormalHints, propProtocols, propTransientFor, propWindowType, propState}
760+ for i, bit := range order {
761+ require.False(t, hs.x.tab.rec(s).ready, i)
762+ if bit == propCount { // the geometry
763+ hs.x.tab.rec(s).w, hs.x.tab.rec(s).h = 300, 200
764+ hs.x.tab.rec(s).pending &^= propGeom
765+ hs.x.checkReady(s)
766+ continue
767+ }
768+ var val []byte
769+ var format uint8 = 8
770+ switch bit {
771+ case propClass:
772+ val = []byte("a\x00B\x00")
773+ case propNetName:
774+ val = []byte("t")
775+ case propPID:
776+ val, format = le32(5), 32
777+ default:
778+ format = 32
779+ }
780+ x := hs.x
781+ x.applyProp(s, bit, format, val)
782+ x.tab.rec(s).pending &^= 1 << bit
783+ x.checkReady(s)
784+ }
785+ require.True(t, hs.x.tab.rec(s).ready)
786+ require.Equal(t, "B", hs.ready[win].AppID)
787+ require.Equal(t, "t", hs.ready[win].Title)
788+ require.Equal(t, 5, hs.ready[win].PID)
789+}
790+
791+func TestMapFlowWholeOutputRectSetsHint(t *testing.T) {
792+ hs := newHarness(t, nil)
793+ hs.x.space.Set(ports.Layout{out(0, 0, 1920, 1080), out(1920, 0, 1280, 1024)})
794+ const win = 0x500001
795+ hs.classic(win)
796+ s := hs.create(win)
797+ r := hs.x.tab.rec(s)
798+ r.x, r.y, r.w, r.h, r.sized = 1920, 0, 1280, 1024, true
799+ info := hs.x.info(s)
800+ require.True(t, info.HasAt)
801+ require.Equal(t, 1920, info.AtX)
802+ r.h = 1000
803+ require.False(t, hs.x.info(s).HasAt)
804+ r.h, r.sized = 1024, false
805+ require.False(t, hs.x.info(s).HasAt)
806+}
A internal/adapters/wayland/xwm/props.go +376 -0
@@ -0,0 +1,376 @@
1+package xwm
2+
3+import (
4+ "bytes"
5+ "encoding/binary"
6+ "strings"
7+
8+ "bnema.dev/xturbo"
9+ "bnema.dev/xturbo/res"
10+ "bnema.dev/xturbo/xproto"
11+)
12+
13+// Property indices (second Expect argument of kindProp) and their bits in
14+// record.pending. propGeom is the GetGeometry bit; propClientIDs is raised
15+// while a QueryClientIds reply stands in for a missing _NET_WM_PID.
16+const (
17+ propClass = iota
18+ propName
19+ propNetName
20+ propHints
21+ propNormalHints
22+ propProtocols
23+ propTransientFor
24+ propWindowType
25+ propState
26+ propPID
27+ propBypass
28+ propCount
29+)
30+
31+const (
32+ propGeom uint16 = 1 << propCount
33+ propClientIDs uint16 = propGeom << 1
34+ // propMaskAll is what a map waits for: every property and the geometry.
35+ propMaskAll uint16 = propGeom<<1 - 1
36+)
37+
38+// WM_NORMAL_HINTS flags and WM_HINTS flags.
39+const (
40+ hintInput = 1 << 0
41+ hintMinSize = 1 << 4
42+ hintMaxSize = 1 << 5
43+)
44+
45+// maxPropWords bounds one GetProperty (4 KiB), so titles are truncated there.
46+const maxPropWords = 1024
47+
48+// propIndex maps a property atom to its index, or -1 when it is not watched.
49+func (x *XWM) propIndex(a uint32) int {
50+ switch a {
51+ case uint32(xproto.AtomWmClass):
52+ return propClass
53+ case uint32(xproto.AtomWmName):
54+ return propName
55+ case uint32(xproto.AtomWmHints):
56+ return propHints
57+ case uint32(xproto.AtomWmNormalHints):
58+ return propNormalHints
59+ case uint32(xproto.AtomWmTransientFor):
60+ return propTransientFor
61+ case x.atoms[atomNetWMName]:
62+ return propNetName
63+ case x.atoms[atomWMProtocols]:
64+ return propProtocols
65+ case x.atoms[atomNetWMWindowType]:
66+ return propWindowType
67+ case x.atoms[atomNetWMState]:
68+ return propState
69+ case x.atoms[atomNetWMPID]:
70+ return propPID
71+ case x.atoms[atomNetWMBypassCompositor]:
72+ return propBypass
73+ }
74+ return -1
75+}
76+
77+func (x *XWM) propAtom(idx int) xproto.Atom {
78+ switch idx {
79+ case propClass:
80+ return xproto.AtomWmClass
81+ case propName:
82+ return xproto.AtomWmName
83+ case propHints:
84+ return xproto.AtomWmHints
85+ case propNormalHints:
86+ return xproto.AtomWmNormalHints
87+ case propTransientFor:
88+ return xproto.AtomWmTransientFor
89+ case propNetName:
90+ return xproto.Atom(x.atoms[atomNetWMName])
91+ case propProtocols:
92+ return xproto.Atom(x.atoms[atomWMProtocols])
93+ case propWindowType:
94+ return xproto.Atom(x.atoms[atomNetWMWindowType])
95+ case propState:
96+ return xproto.Atom(x.atoms[atomNetWMState])
97+ case propBypass:
98+ return xproto.Atom(x.atoms[atomNetWMBypassCompositor])
99+ default:
100+ return xproto.Atom(x.atoms[atomNetWMPID])
101+ }
102+}
103+
104+// requestProp queues a GetProperty for property idx of s.
105+func (x *XWM) requestProp(s Slot, idx int) {
106+ rec := x.tab.rec(s)
107+ seq := xproto.GetProperty(x.conn.Buf(), false, xproto.Window(rec.xid), x.propAtom(idx), xproto.AtomAny, 0, maxPropWords)
108+ rec.pending |= 1 << idx
109+ x.expect(seq, kindProp, x.tab.tag(s), uint32(idx))
110+}
111+
112+// onPropertyNotify handles a changed property of a managed window that is
113+// being mapped or is mapped; unwatched atoms cost nothing.
114+func (x *XWM) onPropertyNotify(ev *xturbo.Event) {
115+ e := xproto.PropertyNotifyOf(ev)
116+ if uint32(e.Window()) == x.selWin && x.selWin != 0 {
117+ x.onSelProperty(ev)
118+ return
119+ }
120+ if x.onRequestorProperty(e) {
121+ return
122+ }
123+ s := x.tab.lookup(uint32(e.Window()))
124+ if s == 0 {
125+ return
126+ }
127+ if rec := x.tab.rec(s); rec.kind != kindManaged || !rec.mapping {
128+ return
129+ } else if rec.ready && x.propIndex(uint32(e.Atom())) == propState {
130+ // The XWM owns _NET_WM_STATE of a mapped window: our own writes
131+ // would feed back, and clients change it with ClientMessages.
132+ return
133+ }
134+ idx := x.propIndex(uint32(e.Atom()))
135+ if idx < 0 {
136+ return
137+ }
138+ if e.State() == xproto.PropertyDelete {
139+ x.applyProp(s, idx, 0, nil)
140+ return
141+ }
142+ x.requestProp(s, idx)
143+}
144+
145+// onPropReply decodes one GetProperty reply into the record. An absent
146+// property (format 0) clears its fields.
147+func (x *XWM) onPropReply(s Slot, _ *record, r *xturbo.Reply) {
148+ idx := int(r.Args[1])
149+ if idx < 0 || idx >= propCount {
150+ return
151+ }
152+ var format uint8
153+ var val []byte
154+ if rep, ok := xproto.ParseGetPropertyReply(r.Raw); ok {
155+ format, val = rep.Format(), rep.Value()
156+ }
157+ x.applyProp(s, idx, format, val)
158+ x.tab.rec(s).pending &^= 1 << idx
159+ x.checkReady(s)
160+}
161+
162+func word(b []byte, i int) uint32 { return binary.LittleEndian.Uint32(b[4*i:]) }
163+
164+// applyProp stores one decoded property. It does not allocate unless a string
165+// changed. Host calls come last: the record is not touched after them.
166+func (x *XWM) applyProp(s Slot, idx int, format uint8, val []byte) {
167+ rec := x.tab.rec(s)
168+ if idx == propClass || idx == propName || idx == propNetName {
169+ if format != 8 {
170+ val = nil
171+ }
172+ } else if format != 32 {
173+ val = nil
174+ }
175+ switch idx {
176+ case propClass:
177+ inst, class := val, []byte(nil)
178+ if i := bytes.IndexByte(val, 0); i >= 0 {
179+ inst, class = val[:i], val[i+1:]
180+ if j := bytes.IndexByte(class, 0); j >= 0 {
181+ class = class[:j]
182+ }
183+ }
184+ app := class
185+ if len(app) == 0 {
186+ app = inst
187+ }
188+ if rec.class == string(app) {
189+ return
190+ }
191+ cls := strings.ToValidUTF8(string(app), "\uFFFD")
192+ if rec.class == cls {
193+ return
194+ }
195+ rec.class = cls
196+ if rec.ready {
197+ x.host.AppID(s, rec.class)
198+ }
199+ case propName:
200+ if !rec.hasNetName {
201+ x.setTitle(s, val)
202+ }
203+ case propNetName:
204+ had := rec.hasNetName
205+ rec.hasNetName = len(val) > 0
206+ if rec.hasNetName {
207+ x.setTitle(s, val)
208+ } else if had && rec.mapping {
209+ x.requestProp(s, propName)
210+ }
211+ case propHints:
212+ rec.input = true
213+ if len(val) >= 8 && word(val, 0)&hintInput != 0 {
214+ rec.input = word(val, 1) != 0
215+ }
216+ case propNormalHints:
217+ rec.minW, rec.minH, rec.maxW, rec.maxH = 0, 0, 0, 0
218+ if len(val) >= 36 {
219+ fl := word(val, 0)
220+ if fl&hintMinSize != 0 {
221+ rec.minW, rec.minH = int32(word(val, 5)), int32(word(val, 6))
222+ }
223+ if fl&hintMaxSize != 0 {
224+ rec.maxW, rec.maxH = int32(word(val, 7)), int32(word(val, 8))
225+ }
226+ }
227+ x.classify(rec)
228+ case propProtocols:
229+ rec.deleteWindow, rec.takeFocus = false, false
230+ for i := 0; i+4 <= len(val); i += 4 {
231+ switch binary.LittleEndian.Uint32(val[i:]) {
232+ case x.atoms[atomWMDeleteWindow]:
233+ rec.deleteWindow = true
234+ case x.atoms[atomWMTakeFocus]:
235+ rec.takeFocus = true
236+ }
237+ }
238+ case propTransientFor:
239+ var t uint32
240+ if len(val) >= 4 {
241+ t = word(val, 0)
242+ }
243+ if t == rec.transient {
244+ return
245+ }
246+ rec.transient = t
247+ x.classify(rec)
248+ if rec.ready {
249+ x.host.Parent(s, x.managedSlot(t))
250+ }
251+ case propWindowType:
252+ rec.wtype = 0
253+ for i := 0; i+4 <= len(val); i += 4 {
254+ if t := x.windowType(binary.LittleEndian.Uint32(val[i:])); t != 0 {
255+ rec.wtype = t
256+ break
257+ }
258+ }
259+ x.classify(rec)
260+ case propState:
261+ var modal, fs bool
262+ for i := 0; i+4 <= len(val); i += 4 {
263+ switch binary.LittleEndian.Uint32(val[i:]) {
264+ case x.atoms[atomNetWMStateModal]:
265+ modal = true
266+ case x.atoms[atomNetWMStateFullscreen]:
267+ fs = true
268+ }
269+ }
270+ changed := fs != rec.fullscreen
271+ rec.modal, rec.fullscreen = modal, fs
272+ if changed && rec.ready {
273+ x.host.Fullscreen(s, fs)
274+ }
275+ case propBypass:
276+ // Kept for a later VRR/tearing policy; direct scanout and tearing
277+ // do not depend on it. 2 ("do not bypass") is ignored on purpose.
278+ rec.bypass = 0
279+ if len(val) >= 4 {
280+ rec.bypass = uint8(min(word(val, 0), 2))
281+ }
282+ x.log.Debug().Uint32("xid", rec.xid).Uint8("bypass", rec.bypass).Msg("_NET_WM_BYPASS_COMPOSITOR")
283+ case propPID:
284+ if len(val) >= 4 {
285+ rec.pid = int32(word(val, 0))
286+ return
287+ }
288+ rec.pid = 0
289+ if !rec.ready && rec.pending&propClientIDs == 0 && x.conn.Ext().Info(xturbo.ExtRes).Present {
290+ x.queryClientIDs(s)
291+ }
292+ }
293+}
294+
295+// setTitle stores a title when it differs; only a change allocates.
296+func (x *XWM) setTitle(s Slot, val []byte) {
297+ rec := x.tab.rec(s)
298+ if rec.title == string(val) {
299+ return
300+ }
301+ t := strings.ToValidUTF8(string(val), "\uFFFD")
302+ if rec.title == t {
303+ return
304+ }
305+ rec.title = t
306+ if rec.ready {
307+ x.host.Title(s, rec.title)
308+ }
309+}
310+
311+// windowType returns the atom enum of a recognised _NET_WM_WINDOW_TYPE atom,
312+// or 0.
313+func (x *XWM) windowType(a uint32) uint8 {
314+ for t := atomNetWMWindowTypeNormal; t <= atomNetWMWindowTypeDND; t++ {
315+ if x.atoms[t] == a {
316+ return uint8(t)
317+ }
318+ }
319+ return 0
320+}
321+
322+// floatingType reports whether a window type floats by itself.
323+func floatingType(t uint8) bool {
324+ switch int(t) {
325+ case atomNetWMWindowTypeDialog, atomNetWMWindowTypeUtility, atomNetWMWindowTypeSplash, atomNetWMWindowTypeToolbar:
326+ return true
327+ }
328+ return false
329+}
330+
331+// classify sets rec.floating, the single source of truth: a dialog-like
332+// window type, a transient for a managed window, or a fixed size.
333+func (x *XWM) classify(rec *record) {
334+ fixed := rec.minW > 0 && rec.minH > 0 && rec.minW == rec.maxW && rec.minH == rec.maxH
335+ rec.floating = floatingType(rec.wtype) || x.managedSlot(rec.transient) != 0 || fixed
336+}
337+
338+// managedSlot returns the slot of the managed window xid, or 0.
339+func (x *XWM) managedSlot(xid uint32) Slot {
340+ if xid == 0 {
341+ return 0
342+ }
343+ if s := x.tab.lookup(xid); s != 0 && x.tab.rec(s).kind == kindManaged {
344+ return s
345+ }
346+ return 0
347+}
348+
349+// queryClientIDs asks XRes for the PID of the client owning s, which stands
350+// in for a missing _NET_WM_PID: Ready waits for it (propClientIDs).
351+func (x *XWM) queryClientIDs(s Slot) {
352+ rec := x.tab.rec(s)
353+ specs := [1]res.ClientIdSpec{{Client: rec.xid, Mask: res.ClientIdMaskLocalClientPID}}
354+ seq := res.QueryClientIds(x.conn.Buf(), specs[:])
355+ if seq == 0 {
356+ return
357+ }
358+ rec.pending |= propClientIDs
359+ x.expect(seq, kindClientIds, x.tab.tag(s), 0)
360+}
361+
362+// onClientIdsReply stores the PID XRes found and releases the map.
363+func (x *XWM) onClientIdsReply(s Slot, rec *record, r *xturbo.Reply) {
364+ if q, ok := res.ParseQueryClientIdsReply(r.Raw); ok {
365+ it := q.Ids()
366+ for it.Next() {
367+ v := it.Value()
368+ if v.Spec().Mask == res.ClientIdMaskLocalClientPID && v.Value().Len() > 0 {
369+ rec.pid = int32(v.Value().At(0))
370+ break
371+ }
372+ }
373+ }
374+ rec.pending &^= propClientIDs
375+ x.checkReady(s)
376+}
A internal/adapters/wayland/xwm/selection.go +1050 -0
@@ -0,0 +1,1050 @@
1+package xwm
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+ "time"
7+
8+ "bnema.dev/xturbo"
9+ "bnema.dev/xturbo/xfixes"
10+ "bnema.dev/xturbo/xproto"
11+ "golang.org/x/sys/unix"
12+)
13+
14+// The selection bridge: text-only CLIPBOARD and PRIMARY between X clients and
15+// the Wayland clipboard. Display goroutine only; nothing here is locked.
16+
17+// Selection kinds, in the order clipboard.go uses.
18+const (
19+ SelClipboard = iota
20+ SelPrimary
21+ selKinds
22+)
23+
24+// textMimes are the MIME types the bridge offers, in the order of preference;
25+// mimeTarget gives the atom an X owner must advertise for each.
26+var (
27+ textMimes = [5]string{"text/plain;charset=utf-8", "text/plain", "UTF8_STRING", "STRING", "TEXT"}
28+ mimeTarget = [5]int{atomUTF8String, atomString, atomUTF8String, atomString, atomUTF8String}
29+)
30+
31+// selState is one selection.
32+type selState struct {
33+ atom uint32
34+ gen uint32 // bumped on every ownership change; stales TARGETS probes
35+ probe uint32 // gen when the TARGETS ConvertSelection was issued
36+ owner uint32 // X owner window other than selWin, 0 none
37+ ownTime uint32 // lastTime when selWin took ownership, answered for TIMESTAMP
38+ ours bool // selWin owns the selection (Wayland -> X)
39+ // wl is what the Wayland side offers while ours: raw text availability.
40+ wlUTF8, wlPlain bool
41+ wlMime [len(textMimes)]string // offered text MIMEs, in textMimes order, "" absent
42+}
43+
44+const (
45+ chunk = 65536 // every transfer step moves at most this many bytes
46+ nXfer = 4 // concurrent transfers per direction, and pool buffers
47+ tickEvery = time.Second
48+ xIdleMax = 10 // ticks without X-side progress
49+ rIdleMax = 30 // ticks of a reader that does not drain its pipe
50+)
51+
52+// bufPool is the fixed set of transfer buffers.
53+type bufPool struct {
54+ bufs [nXfer][chunk]byte
55+ free [nXfer]int8
56+ n int
57+}
58+
59+func (p *bufPool) init() {
60+ for i := range p.free {
61+ p.free[i] = int8(i)
62+ }
63+ p.n = nXfer
64+}
65+
66+// get returns a free buffer index, -1 when the pool is exhausted.
67+func (p *bufPool) get() int {
68+ if p.n == 0 {
69+ return -1
70+ }
71+ p.n--
72+ return int(p.free[p.n])
73+}
74+
75+func (p *bufPool) put(i int) {
76+ p.free[p.n] = int8(i)
77+ p.n++
78+}
79+
80+// X -> Wayland transfer states.
81+const (
82+ inFree uint8 = iota
83+ inQueued // waiting for the conversion property to be free
84+ inConverting
85+ inWaitIncr // waiting for the next INCR chunk (PropertyNotify)
86+ inWriting // a buffer is pending on the Wayland reader's pipe
87+ inReading // a GetProperty is outstanding
88+)
89+
90+// xin is one X -> Wayland transfer.
91+type xin struct {
92+ x *XWM
93+ idx int
94+ state uint8
95+ kind int
96+ fd int
97+ target uint32
98+ order uint64
99+ gen uint32 // stales replies of an aborted transfer
100+ incr bool
101+ buf int
102+ off, n int
103+ words uint32 // long offset of the next piece of a long property
104+ more bool // the last piece left bytes in the property
105+ ready bool // the owner wrote the next INCR chunk while a buffer was pending
106+ idle int
107+ src fdSource
108+ onR func() // pipe hangup
109+ onW func() // pipe writable
110+}
111+
112+// selBridge is the XWM's selection state.
113+type selBridge struct {
114+ st [selKinds]selState
115+ mimes [len(textMimes)]string // scratch handed to SelectionOwned
116+ pool bufPool
117+ in [nXfer]xin
118+ out [nXfer]xout
119+ order uint64
120+ // active is the transfer that owns the conversion property, -1 none.
121+ active int
122+ timerOn bool
123+ closing bool // selClose ran: no transfer may start
124+ cancel func()
125+ tickFn func()
126+}
127+
128+func (x *XWM) initSelection() {
129+ b := &x.sel
130+ b.pool.init()
131+ b.active = -1
132+ b.tickFn = x.selTick
133+ for i := range b.in {
134+ t := &b.in[i]
135+ t.x, t.idx, t.buf = x, i, -1
136+ t.onR = func() { t.x.inAbort(t.idx, true) }
137+ t.onW = func() { t.x.inWritable(t.idx) }
138+ }
139+ for i := range b.out {
140+ o := &b.out[i]
141+ o.x, o.idx, o.buf, o.fd = x, i, -1, -1
142+ o.onR = func() { o.x.outReadable(o.idx) }
143+ }
144+}
145+
146+// bootSelection creates the selection window and asks for ownership events.
147+// Without XFixes the bridge stays off.
148+func (x *XWM) bootSelection(root xproto.Window) {
149+ if !x.conn.Ext().Info(xturbo.ExtXFixes).Present {
150+ x.log.Warn().Msg("X server has no XFixes: clipboard bridge disabled")
151+ return
152+ }
153+ xid := x.selWin
154+ b := x.conn.Buf()
155+ var wv xproto.CreateWindowValues
156+ wv.SetEventMask(xproto.EventMaskPropertyChange)
157+ xproto.CreateWindow(b, 0, xproto.Window(xid), root, -1, -1, 1, 1, 0, xproto.WindowClassInputOnly, 0, &wv)
158+ x.sel.st[SelClipboard].atom = x.atoms[atomClipboard]
159+ x.sel.st[SelPrimary].atom = x.atoms[atomPrimary]
160+ const mask = xfixes.SelectionEventMaskSetSelectionOwner | xfixes.SelectionEventMaskSelectionWindowDestroy | xfixes.SelectionEventMaskSelectionClientClose
161+ for k := range x.sel.st {
162+ xfixes.SelectSelectionInput(b, root, xproto.Atom(x.sel.st[k].atom), mask)
163+ }
164+}
165+
166+// selKind maps a selection atom to its kind, -1 when it is neither.
167+func (x *XWM) selKind(atom uint32) int {
168+ for k := range x.sel.st {
169+ if x.sel.st[k].atom == atom && atom != 0 {
170+ return k
171+ }
172+ }
173+ return -1
174+}
175+
176+// onSelectionEvent handles the selection events; others are ignored.
177+func (x *XWM) onSelectionEvent(ev *xturbo.Event) {
178+ if x.selWin == 0 {
179+ return
180+ }
181+ switch ev.Kind {
182+ case xfixes.KindSelectionNotify:
183+ x.onXFixesSelection(ev)
184+ case xproto.KindSelectionNotify:
185+ x.onSelectionNotify(ev)
186+ case xproto.KindSelectionRequest:
187+ x.onSelectionRequest(ev)
188+ case xproto.KindSelectionClear:
189+ x.onSelectionClear(ev)
190+ }
191+}
192+
193+// onXFixesSelection: the owner of a selection changed.
194+func (x *XWM) onXFixesSelection(ev *xturbo.Event) {
195+ e := xfixes.SelectionNotifyOf(ev)
196+ k := x.selKind(uint32(e.Selection()))
197+ if k < 0 {
198+ return
199+ }
200+ st := &x.sel.st[k]
201+ owner := uint32(e.Owner())
202+ if owner == x.selWin { // our own SetSelectionOwner echoed back
203+ return
204+ }
205+ st.gen++
206+ st.owner = owner
207+ if owner == 0 {
208+ x.log.Debug().Int("kind", k).Msg("selection: no X owner")
209+ x.host.SelectionOwned(k, nil)
210+ return
211+ }
212+ st.probe = st.gen
213+ x.log.Debug().Int("kind", k).Uint32("owner", owner).Msg("selection: X owner")
214+ xproto.ConvertSelection(x.conn.Buf(), xproto.Window(x.selWin), xproto.Atom(st.atom),
215+ xproto.Atom(x.atoms[atomTargets]), xproto.Atom(x.atoms[atomNeferwlTargets]), 0)
216+ x.wake()
217+}
218+
219+// onSelectionNotify: a ConvertSelection we issued was answered.
220+func (x *XWM) onSelectionNotify(ev *xturbo.Event) {
221+ e := xproto.SelectionNotifyOf(ev)
222+ if uint32(e.Requestor()) != x.selWin {
223+ return
224+ }
225+ k := x.selKind(uint32(e.Selection()))
226+ if k < 0 {
227+ return
228+ }
229+ if uint32(e.Target()) != x.atoms[atomTargets] {
230+ x.onDataNotify(e)
231+ return
232+ }
233+ {
234+ st := &x.sel.st[k]
235+ if st.ours || st.owner == 0 || st.probe != st.gen { // stale probe
236+ return
237+ }
238+ if e.Property() == 0 {
239+ x.host.SelectionOwned(k, nil)
240+ return
241+ }
242+ seq := xproto.GetProperty(x.conn.Buf(), true, xproto.Window(x.selWin), xproto.Atom(x.atoms[atomNeferwlTargets]), 0, 0, 256)
243+ x.expect(seq, kindSelTargets, uint32(k), x.sel.st[k].gen)
244+ x.wake()
245+ return
246+ }
247+}
248+
249+// onTargetsReply turns the owner's TARGETS list into the MIME types offered
250+// to Wayland: the text subset, empty for images, files and the like.
251+func (x *XWM) onTargetsReply(r *xturbo.Reply) {
252+ k := int(r.Args[0])
253+ if k < 0 || k >= selKinds || r.Args[1] != x.sel.st[k].gen || x.sel.st[k].ours || x.sel.st[k].owner == 0 || x.sel.st[k].probe != r.Args[1] {
254+ return
255+ }
256+ g, ok := xproto.ParseGetPropertyReply(r.Raw)
257+ if !ok {
258+ return
259+ }
260+ var have [atomCount]bool
261+ if g.Format() == 32 {
262+ v := g.Value()
263+ for i := 0; i+4 <= len(v); i += 4 {
264+ a := binary.LittleEndian.Uint32(v[i:])
265+ for _, t := range [...]int{atomUTF8String, atomString, atomText} {
266+ if a == x.atoms[t] {
267+ have[t] = true
268+ }
269+ }
270+ }
271+ }
272+ n := 0
273+ for i, m := range textMimes {
274+ if have[mimeTarget[i]] {
275+ x.sel.mimes[n] = m
276+ n++
277+ }
278+ }
279+ if n == 0 {
280+ x.host.SelectionOwned(k, nil)
281+ return
282+ }
283+ x.host.SelectionOwned(k, x.sel.mimes[:n])
284+}
285+
286+// ---- X -> Wayland ----
287+
288+// SelectionReceive: a Wayland reader wants the X selection kind as mime. The
289+// XWM takes fd (a write end it now owns) and closes it when done.
290+func (x *XWM) SelectionReceive(kind int, mime string, fd int) {
291+ target := uint32(0)
292+ for i, m := range textMimes {
293+ if m == mime {
294+ target = x.atoms[mimeTarget[i]]
295+ break
296+ }
297+ }
298+ if x.selWin == 0 || x.closed || kind < 0 || kind >= selKinds || target == 0 || x.sel.st[kind].owner == 0 {
299+ _ = unix.Close(fd)
300+ return
301+ }
302+ if unix.SetNonblock(fd, true) != nil {
303+ _ = unix.Close(fd)
304+ return
305+ }
306+ for i := range x.sel.in {
307+ t := &x.sel.in[i]
308+ if t.state != inFree {
309+ continue
310+ }
311+ x.sel.order++
312+ t.state, t.kind, t.fd, t.target, t.order = inQueued, kind, fd, target, x.sel.order
313+ t.incr, t.buf, t.off, t.n, t.words, t.more, t.ready, t.idle, t.src = false, -1, 0, 0, 0, false, false, 0, nil
314+ x.selEnsureTimer()
315+ x.inStartNext()
316+ return
317+ }
318+ x.log.Warn().Msg("selection: too many transfers, paste refused")
319+ _ = unix.Close(fd)
320+}
321+
322+// inStartNext starts the oldest queued transfer when none is converting.
323+func (x *XWM) inStartNext() {
324+ b := &x.sel
325+ if b.active >= 0 || b.closing {
326+ return
327+ }
328+ best := -1
329+ for i := range b.in {
330+ if b.in[i].state == inQueued && (best < 0 || b.in[i].order < b.in[best].order) {
331+ best = i
332+ }
333+ }
334+ if best < 0 {
335+ return
336+ }
337+ t := &b.in[best]
338+ t.state, t.idle = inConverting, 0
339+ b.active = best
340+ xproto.ConvertSelection(x.conn.Buf(), xproto.Window(x.selWin), xproto.Atom(b.st[t.kind].atom),
341+ xproto.Atom(t.target), xproto.Atom(x.atoms[atomNeferwlData]), 0)
342+ x.wake()
343+}
344+
345+// inFinish ends transfer i and starts the next one.
346+func (x *XWM) inFinish(i int) {
347+ b := &x.sel
348+ t := &b.in[i]
349+ if t.src != nil {
350+ t.src.Remove()
351+ t.src = nil
352+ }
353+ if t.fd >= 0 {
354+ _ = unix.Close(t.fd)
355+ }
356+ if t.buf >= 0 {
357+ b.pool.put(t.buf)
358+ t.buf = -1
359+ }
360+ t.gen++
361+ t.state, t.fd = inFree, -1
362+ if b.active == i {
363+ b.active = -1
364+ }
365+ x.inStartNext()
366+}
367+
368+// inAbort drops transfer i, deleting the conversion property when X may still
369+// be writing it.
370+func (x *XWM) inAbort(i int, del bool) {
371+ t := &x.sel.in[i]
372+ if t.state == inFree {
373+ return
374+ }
375+ if del && x.sel.active == i && !x.closed {
376+ xproto.DeleteProperty(x.conn.Buf(), xproto.Window(x.selWin), xproto.Atom(x.atoms[atomNeferwlData]))
377+ x.wake()
378+ }
379+ x.inFinish(i)
380+}
381+
382+// onDataNotify: the owner answered our data ConvertSelection.
383+func (x *XWM) onDataNotify(e xproto.SelectionNotifyEvent) {
384+ i := x.sel.active
385+ if i < 0 {
386+ return
387+ }
388+ t := &x.sel.in[i]
389+ if t.state != inConverting || uint32(e.Target()) != t.target || uint32(e.Selection()) != x.sel.st[t.kind].atom {
390+ return
391+ }
392+ if p := uint32(e.Property()); p != 0 && p != x.atoms[atomNeferwlData] {
393+ return
394+ }
395+ if e.Property() == 0 {
396+ x.inFinish(i)
397+ return
398+ }
399+ t.words = 0
400+ x.inRequest(i)
401+}
402+
403+// inRequest asks for the next piece of the property; delete makes INCR and
404+// whole-value reads advance.
405+func (x *XWM) inRequest(i int) {
406+ t := &x.sel.in[i]
407+ t.state = inReading
408+ seq := xproto.GetProperty(x.conn.Buf(), true, xproto.Window(x.selWin), xproto.Atom(x.atoms[atomNeferwlData]),
409+ 0, t.words, chunk/4)
410+ if seq != 0 {
411+ if err := x.conn.Expect(seq, kindSelData, uint32(i), t.gen, 0, 0); err != nil {
412+ x.stats.RingFull++
413+ }
414+ }
415+ x.wake()
416+}
417+
418+// onSelDataReply handles one piece of the converted property.
419+func (x *XWM) onSelDataReply(r *xturbo.Reply) {
420+ i := int(r.Args[0])
421+ if i < 0 || i >= nXfer {
422+ return
423+ }
424+ t := &x.sel.in[i]
425+ if t.state != inReading || t.gen != r.Args[1] {
426+ return
427+ }
428+ g, ok := xproto.ParseGetPropertyReply(r.Raw)
429+ if !ok || g.Format() == 0 {
430+ x.inAbort(i, false)
431+ return
432+ }
433+ t.idle = 0
434+ if uint32(g.Type()) == x.atoms[atomIncr] {
435+ t.incr, t.state, t.words, t.more = true, inWaitIncr, 0, false
436+ return
437+ }
438+ v := g.Value()
439+ if g.Format() != 8 || len(v) > chunk {
440+ x.inAbort(i, true)
441+ return
442+ }
443+ t.more = g.BytesAfter() > 0
444+ if len(v) == 0 {
445+ if t.more { // cannot happen: bytes remain only after a non-empty read
446+ x.inAbort(i, true)
447+ return
448+ }
449+ x.inNext(i)
450+ return
451+ }
452+ b := x.sel.pool.get()
453+ if b < 0 {
454+ x.log.Warn().Msg("selection: no free buffer, paste aborted")
455+ x.inAbort(i, true)
456+ return
457+ }
458+ t.buf = b
459+ t.n = copy(x.sel.pool.bufs[b][:], v)
460+ t.off = 0
461+ t.words += uint32(t.n) / 4
462+ t.state = inWriting
463+ x.inDrain(i)
464+}
465+
466+// inDrain writes the pending buffer to the reader's pipe.
467+func (x *XWM) inDrain(i int) {
468+ t := &x.sel.in[i]
469+ buf := &x.sel.pool.bufs[t.buf]
470+ for t.off < t.n {
471+ n, err := unix.Write(t.fd, buf[t.off:t.n])
472+ switch {
473+ case err == nil:
474+ t.off += n
475+ t.idle = 0
476+ case errors.Is(err, unix.EINTR):
477+ case errors.Is(err, unix.EAGAIN):
478+ x.inWatch(i, true)
479+ return
480+ default: // EPIPE: the reader went away
481+ x.inAbort(i, true)
482+ return
483+ }
484+ }
485+ x.sel.pool.put(t.buf)
486+ t.buf = -1
487+ if t.src != nil {
488+ x.inWatch(i, false)
489+ }
490+ x.inNext(i)
491+}
492+
493+// inWatch arms or disarms writable interest on the reader's pipe.
494+func (x *XWM) inWatch(i int, on bool) {
495+ t := &x.sel.in[i]
496+ if t.src == nil {
497+ if !on || x.lp == nil {
498+ if on {
499+ x.inAbort(i, true)
500+ }
501+ return
502+ }
503+ src, err := x.lp.AddFD(t.fd, t.onR, t.onW)
504+ if err != nil {
505+ x.inAbort(i, true)
506+ return
507+ }
508+ t.src = src
509+ }
510+ if err := t.src.SetWritable(on); err != nil {
511+ x.inAbort(i, true)
512+ }
513+}
514+
515+// inWritable: the pipe has room again.
516+func (x *XWM) inWritable(i int) {
517+ t := &x.sel.in[i]
518+ if t.state == inWriting && t.buf >= 0 {
519+ x.inDrain(i)
520+ }
521+}
522+
523+// inNext decides what follows a drained (or empty) piece.
524+func (x *XWM) inNext(i int) {
525+ t := &x.sel.in[i]
526+ switch {
527+ case t.more: // the rest of this property
528+ x.inRequest(i)
529+ case t.incr && t.n > 0:
530+ t.state, t.n, t.words = inWaitIncr, 0, 0
531+ if t.ready {
532+ t.ready = false
533+ x.inRequest(i)
534+ }
535+ default: // single value done, or the empty INCR terminator
536+ x.inFinish(i)
537+ }
538+}
539+
540+// onSelProperty: PropertyNotify on the selection window.
541+func (x *XWM) onSelProperty(ev *xturbo.Event) {
542+ e := xproto.PropertyNotifyOf(ev)
543+ if e.State() != xproto.PropertyNewValue || uint32(e.Atom()) != x.atoms[atomNeferwlData] {
544+ return
545+ }
546+ // (a Deleted state on a requestor is handled by onRequestorProperty)
547+ i := x.sel.active
548+ if i < 0 || !x.sel.in[i].incr {
549+ return
550+ }
551+ t := &x.sel.in[i]
552+ switch t.state {
553+ case inWaitIncr:
554+ t.idle = 0
555+ t.n = 0
556+ x.inRequest(i)
557+ case inWriting, inReading:
558+ t.ready = true // no request while a buffer is pending: backpressure
559+ }
560+}
561+
562+// onSelError: a selection request failed; the awaited reply never comes.
563+func (x *XWM) onSelError(e *xturbo.XError) {
564+ if e.Kind == kindSelOut {
565+ if i := int(e.Args[0]); i >= 0 && i < nXfer && x.sel.out[i].gen == e.Args[1] && x.sel.out[i].state != outFree {
566+ x.outAbort(i)
567+ }
568+ return
569+ }
570+ if e.Kind == kindSelData {
571+ if i := int(e.Args[0]); i >= 0 && i < nXfer && x.sel.in[i].gen == e.Args[1] && x.sel.in[i].state == inReading {
572+ x.inAbort(i, false)
573+ }
574+ }
575+}
576+
577+// ---- timeouts ----
578+
579+func (x *XWM) selEnsureTimer() {
580+ b := &x.sel
581+ if b.timerOn || x.lp == nil {
582+ return
583+ }
584+ b.timerOn = true
585+ b.cancel = x.lp.After(tickEvery, b.tickFn)
586+}
587+
588+// selTick ages every transfer by one second and aborts the stalled ones.
589+func (x *XWM) selTick() {
590+ b := &x.sel
591+ b.timerOn, b.cancel = false, nil
592+ if x.closed {
593+ return
594+ }
595+ live := false
596+ for i := range b.in {
597+ t := &b.in[i]
598+ if t.state == inFree {
599+ continue
600+ }
601+ t.idle++
602+ limit := xIdleMax
603+ if t.state == inWriting {
604+ limit = rIdleMax
605+ }
606+ if t.state != inQueued && t.idle >= limit {
607+ x.log.Warn().Int("kind", t.kind).Msg("selection: transfer timed out")
608+ x.inAbort(i, true)
609+ continue
610+ }
611+ live = true
612+ }
613+ for i := range b.out {
614+ o := &b.out[i]
615+ if o.state == outFree {
616+ continue
617+ }
618+ o.idle++
619+ limit := rIdleMax
620+ if o.incr && o.n > 0 && !o.credit {
621+ limit = xIdleMax // the requestor does not delete the property
622+ }
623+ if o.idle >= limit {
624+ x.log.Warn().Int("kind", o.kind).Msg("selection: transfer timed out")
625+ x.outAbort(i)
626+ continue
627+ }
628+ live = true
629+ }
630+ if live {
631+ x.selEnsureTimer()
632+ }
633+}
634+
635+// selClose aborts every transfer.
636+func (x *XWM) selClose() {
637+ x.sel.closing = true
638+ for i := range x.sel.out {
639+ if x.sel.out[i].state != outFree {
640+ x.outAbort(i)
641+ }
642+ }
643+ for i := range x.sel.in {
644+ if x.sel.in[i].state != inFree {
645+ x.inAbort(i, false)
646+ }
647+ }
648+ if x.sel.cancel != nil {
649+ x.sel.cancel()
650+ x.sel.cancel = nil
651+ }
652+ x.sel.timerOn = false
653+}
654+
655+// ---- Wayland -> X ----
656+
657+// Wayland -> X transfer states.
658+const (
659+ outFree uint8 = iota
660+ outActive
661+)
662+
663+// xout is one Wayland -> X transfer.
664+type xout struct {
665+ x *XWM
666+ idx int
667+ state uint8
668+ kind int
669+ requestor uint32
670+ property uint32
671+ target uint32
672+ time uint32
673+ fd int
674+ buf int
675+ n int
676+ eof bool
677+ incr bool // INCR announced
678+ credit bool // the requestor deleted the property: the next chunk may go
679+ notified bool // the requestor got its SelectionNotify
680+ idle int
681+ gen uint32
682+ src fdSource
683+ onR func()
684+}
685+
686+// WaylandSelection: a Wayland client owns kind with these MIME types, or none
687+// does (mimes nil). The XWM takes X ownership when text is offered.
688+func (x *XWM) WaylandSelection(kind int, mimes []string) {
689+ if x.selWin == 0 || x.closed || kind < 0 || kind >= selKinds || x.host == nil {
690+ return
691+ }
692+ st := &x.sel.st[kind]
693+ st.wlUTF8, st.wlPlain = false, false
694+ st.wlMime = [len(textMimes)]string{}
695+ for _, m := range mimes {
696+ for i, tm := range textMimes {
697+ if m != tm {
698+ continue
699+ }
700+ st.wlMime[i] = m
701+ if mimeTarget[i] == atomUTF8String {
702+ st.wlUTF8 = true
703+ } else {
704+ st.wlPlain = true
705+ }
706+ }
707+ }
708+ for i := range x.sel.out { // the data being served is stale
709+ if x.sel.out[i].state != outFree && x.sel.out[i].kind == kind {
710+ x.outAbort(i)
711+ }
712+ }
713+ b := x.conn.Buf()
714+ if !st.wlUTF8 && !st.wlPlain {
715+ if st.ours {
716+ st.ours = false
717+ xproto.SetSelectionOwner(b, 0, xproto.Atom(st.atom), 0)
718+ x.wake()
719+ }
720+ return
721+ }
722+ st.ours = true
723+ st.ownTime = x.lastTime
724+ st.gen++
725+ st.owner = 0
726+ xproto.SetSelectionOwner(b, xproto.Window(x.selWin), xproto.Atom(st.atom), 0)
727+ x.log.Debug().Int("kind", kind).Msg("selection: Wayland owner")
728+ x.wake()
729+}
730+
731+// onSelectionClear: someone else took a selection from selWin.
732+func (x *XWM) onSelectionClear(ev *xturbo.Event) {
733+ e := xproto.SelectionClearOf(ev)
734+ k := x.selKind(uint32(e.Selection()))
735+ if k < 0 || uint32(e.Owner()) != x.selWin {
736+ return
737+ }
738+ x.sel.st[k].ours = false
739+ for i := range x.sel.out {
740+ if x.sel.out[i].state != outFree && x.sel.out[i].kind == k {
741+ x.outAbort(i)
742+ }
743+ }
744+}
745+
746+// notify sends a SelectionNotify to the requestor; property 0 refuses.
747+func (x *XWM) notify(requestor, selection, target, property, time uint32) {
748+ d := xproto.SelectionNotifyEventData{
749+ Time: xproto.Timestamp(time), Requestor: xproto.Window(requestor),
750+ Selection: xproto.Atom(selection), Target: xproto.Atom(target), Property: xproto.Atom(property),
751+ }
752+ var ev [32]byte
753+ d.Put(&ev)
754+ xproto.SendEvent(x.conn.Buf(), false, xproto.Window(requestor), 0, &ev)
755+ x.wake()
756+}
757+
758+// onSelectionRequest: an X client asks selWin for the Wayland selection.
759+func (x *XWM) onSelectionRequest(ev *xturbo.Event) {
760+ e := xproto.SelectionRequestOf(ev)
761+ if uint32(e.Owner()) != x.selWin {
762+ return
763+ }
764+ sel, req, target, prop, time := uint32(e.Selection()), uint32(e.Requestor()), uint32(e.Target()), uint32(e.Property()), uint32(e.Time())
765+ k := x.selKind(sel)
766+ if k < 0 || !x.sel.st[k].ours {
767+ x.notify(req, sel, target, 0, time)
768+ return
769+ }
770+ if prop == 0 { // obsolete client
771+ prop = target
772+ }
773+ st := &x.sel.st[k]
774+ switch target {
775+ case x.atoms[atomTargets]:
776+ var list [6]uint32
777+ n := 0
778+ list[n], n = x.atoms[atomTargets], n+1
779+ list[n], n = x.atoms[atomTimestamp], n+1
780+ if st.wlUTF8 {
781+ list[n], n = x.atoms[atomUTF8String], n+1
782+ list[n], n = x.atoms[atomText], n+1
783+ }
784+ list[n], n = x.atoms[atomString], n+1
785+ xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(req), xproto.Atom(prop),
786+ xproto.AtomAtom, 32, uint32(n), x.encode32(list[:n]))
787+ x.notify(req, sel, target, prop, time)
788+ return
789+ case x.atoms[atomTimestamp]:
790+ xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(req), xproto.Atom(prop),
791+ xproto.AtomInteger, 32, 1, x.encode32([]uint32{st.ownTime}))
792+ x.notify(req, sel, target, prop, time)
793+ return
794+ case x.atoms[atomUTF8String], x.atoms[atomText], x.atoms[atomString]:
795+ default:
796+ x.notify(req, sel, target, 0, time)
797+ return
798+ }
799+ // Choose the Wayland MIME: UTF-8 targets prefer the UTF-8 types; STRING
800+ // prefers plain text and falls back to the UTF-8 bytes unchanged.
801+ mime := ""
802+ order := [...]int{0, 2, 4, 1, 3} // utf8 types, then plain
803+ if target == x.atoms[atomString] {
804+ order = [...]int{1, 3, 0, 2, 4}
805+ }
806+ for _, i := range order {
807+ if st.wlMime[i] != "" && (target == x.atoms[atomString] || mimeTarget[i] == atomUTF8String) {
808+ mime = st.wlMime[i]
809+ break
810+ }
811+ }
812+ if mime == "" {
813+ x.notify(req, sel, target, 0, time)
814+ return
815+ }
816+ for i := range x.sel.out { // one transfer per (requestor, property)
817+ o := &x.sel.out[i]
818+ if o.state != outFree && o.requestor == req && o.property == prop {
819+ x.notify(req, sel, target, 0, time)
820+ return
821+ }
822+ }
823+ slot := -1
824+ for i := range x.sel.out {
825+ if x.sel.out[i].state == outFree {
826+ slot = i
827+ break
828+ }
829+ }
830+ if slot < 0 || x.sel.pool.n == 0 {
831+ x.log.Warn().Msg("selection: too many transfers, request refused")
832+ x.notify(req, sel, target, 0, time)
833+ return
834+ }
835+ fd := x.host.SelectionRead(k, mime)
836+ if fd < 0 {
837+ x.notify(req, sel, target, 0, time)
838+ return
839+ }
840+ x.startOut(slot, k, req, prop, target, time, fd)
841+}
842+
843+// startOut begins serving the data on fd (a non-blocking read end it now
844+// owns) to requestor.
845+func (x *XWM) startOut(slot, k int, req, prop, target, time uint32, fd int) {
846+ o := &x.sel.out[slot]
847+ o.state, o.kind, o.requestor, o.property, o.target, o.time = outActive, k, req, prop, target, time
848+ o.fd, o.buf, o.n, o.eof, o.incr, o.credit, o.notified, o.idle, o.src = fd, x.sel.pool.get(), 0, false, false, false, false, 0, nil
849+ x.selEnsureTimer()
850+ x.outRead(slot) // data may already be waiting
851+}
852+
853+// encode32 packs vals into the shared scratch.
854+func (x *XWM) encode32(vals []uint32) []byte {
855+ n := 4 * len(vals)
856+ if cap(x.scratch) < n {
857+ x.scratch = make([]byte, 0, n)
858+ }
859+ p := x.scratch[:n]
860+ for i, v := range vals {
861+ binary.LittleEndian.PutUint32(p[4*i:], v)
862+ }
863+ return p
864+}
865+
866+// outWatch registers the pipe's read end with the display.
867+func (x *XWM) outWatch(i int) {
868+ o := &x.sel.out[i]
869+ if o.src != nil {
870+ return
871+ }
872+ if x.lp == nil {
873+ x.outAbort(i)
874+ return
875+ }
876+ src, err := x.lp.AddFD(o.fd, o.onR, nil)
877+ if err != nil {
878+ x.outAbort(i)
879+ return
880+ }
881+ o.src = src
882+}
883+
884+// outUnwatch drops read interest: the buffer is full and nothing may be read
885+// until the requestor consumed the previous chunk.
886+func (x *XWM) outUnwatch(i int) {
887+ o := &x.sel.out[i]
888+ if o.src != nil {
889+ o.src.Remove()
890+ o.src = nil
891+ }
892+}
893+
894+// outReadable is the pipe's read callback.
895+func (x *XWM) outReadable(i int) {
896+ if x.sel.out[i].state == outFree {
897+ return
898+ }
899+ x.outRead(i)
900+}
901+
902+// outRead fills the buffer from the pipe and advances the transfer.
903+func (x *XWM) outRead(i int) {
904+ o := &x.sel.out[i]
905+ buf := &x.sel.pool.bufs[o.buf]
906+ for o.n < chunk && !o.eof {
907+ n, err := unix.Read(o.fd, buf[o.n:])
908+ switch {
909+ case err == nil && n == 0:
910+ o.eof = true
911+ case err == nil:
912+ o.n += n
913+ o.idle = 0
914+ case errors.Is(err, unix.EINTR):
915+ continue
916+ case errors.Is(err, unix.EAGAIN):
917+ x.outPump(i)
918+ return
919+ default:
920+ x.outAbort(i)
921+ return
922+ }
923+ }
924+ x.outPump(i)
925+}
926+
927+// outPump sends what the state allows.
928+func (x *XWM) outPump(i int) {
929+ o := &x.sel.out[i]
930+ if o.state == outFree {
931+ return
932+ }
933+ full := o.n == chunk
934+ if !o.incr {
935+ switch {
936+ case o.eof: // whole value
937+ x.outSetProp(i, o.n)
938+ x.notify(o.requestor, x.sel.st[o.kind].atom, o.target, o.property, o.time)
939+ x.outFinish(i)
940+ case full: // does not fit one request: INCR
941+ o.incr, o.notified = true, true
942+ var v xproto.ChangeWindowAttributesValues
943+ v.SetEventMask(xproto.EventMaskPropertyChange)
944+ xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(o.requestor), &v)
945+ xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(o.requestor), xproto.Atom(o.property),
946+ xproto.Atom(x.atoms[atomIncr]), 32, 1, x.encode32([]uint32{0}))
947+ x.notify(o.requestor, x.sel.st[o.kind].atom, o.target, o.property, o.time)
948+ x.outUnwatch(i)
949+ default:
950+ x.outWatch(i)
951+ }
952+ return
953+ }
954+ if o.credit && (full || o.eof) {
955+ o.credit = false
956+ o.idle = 0
957+ x.outSetProp(i, o.n)
958+ if o.n == 0 { // the empty chunk ends the transfer
959+ x.outFinish(i)
960+ return
961+ }
962+ o.n = 0
963+ }
964+ if o.n < chunk && !o.eof {
965+ x.outWatch(i)
966+ } else {
967+ x.outUnwatch(i)
968+ }
969+}
970+
971+// outSetProp writes the first n buffered bytes as the property value.
972+func (x *XWM) outSetProp(i, n int) {
973+ o := &x.sel.out[i]
974+ seq := xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(o.requestor), xproto.Atom(o.property),
975+ xproto.Atom(o.target), 8, uint32(n), x.sel.pool.bufs[o.buf][:n])
976+ if seq != 0 {
977+ if err := x.conn.Expect(seq, kindSelOut, uint32(i), o.gen, 0, 0); err != nil {
978+ x.stats.RingFull++
979+ }
980+ }
981+ x.wake()
982+}
983+
984+// outFinish ends a completed transfer.
985+func (x *XWM) outFinish(i int) {
986+ o := &x.sel.out[i]
987+ if o.incr && !x.closed {
988+ var v xproto.ChangeWindowAttributesValues
989+ v.SetEventMask(0)
990+ xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(o.requestor), &v)
991+ x.wake()
992+ }
993+ x.outRelease(i)
994+}
995+
996+// outRelease frees the transfer's resources.
997+func (x *XWM) outRelease(i int) {
998+ o := &x.sel.out[i]
999+ x.outUnwatch(i)
1000+ if o.fd >= 0 {
1001+ _ = unix.Close(o.fd)
1002+ }
1003+ if o.buf >= 0 {
1004+ x.sel.pool.put(o.buf)
1005+ }
1006+ o.fd, o.buf = -1, -1
1007+ o.gen++
1008+ o.state = outFree
1009+}
1010+
1011+// outAbort ends a transfer early: the requestor is refused (before its
1012+// notification) or released (INCR: mask dropped, property deleted).
1013+func (x *XWM) outAbort(i int) {
1014+ o := &x.sel.out[i]
1015+ if o.state == outFree {
1016+ return
1017+ }
1018+ if !x.closed {
1019+ switch {
1020+ case !o.notified:
1021+ x.notify(o.requestor, x.sel.st[o.kind].atom, o.target, 0, o.time)
1022+ case o.incr:
1023+ var v xproto.ChangeWindowAttributesValues
1024+ v.SetEventMask(0)
1025+ xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(o.requestor), &v)
1026+ xproto.DeleteProperty(x.conn.Buf(), xproto.Window(o.requestor), xproto.Atom(o.property))
1027+ x.wake()
1028+ }
1029+ }
1030+ x.outRelease(i)
1031+}
1032+
1033+// onRequestorProperty: PropertyNotify on a window we serve by INCR.
1034+func (x *XWM) onRequestorProperty(e xproto.PropertyNotifyEvent) bool {
1035+ if e.State() != xproto.PropertyDelete {
1036+ return false
1037+ }
1038+ hit := false
1039+ for i := range x.sel.out {
1040+ o := &x.sel.out[i]
1041+ if o.state == outFree || !o.incr || o.requestor != uint32(e.Window()) || o.property != uint32(e.Atom()) {
1042+ continue
1043+ }
1044+ hit = true
1045+ o.credit = true
1046+ o.idle = 0
1047+ x.outPump(i)
1048+ }
1049+ return hit
1050+}
A internal/adapters/wayland/xwm/selection_test.go +801 -0
@@ -0,0 +1,801 @@
1+package xwm
2+
3+import (
4+ "bytes"
5+ "encoding/binary"
6+ "io"
7+ "os"
8+ "testing"
9+ "time"
10+
11+ "bnema.dev/xturbo"
12+ "bnema.dev/xturbo/xproto"
13+ "github.com/stretchr/testify/mock"
14+ "github.com/stretchr/testify/require"
15+ "golang.org/x/sys/unix"
16+)
17+
18+const peerXFixesFirstEvent = 87
19+
20+// xfixesSelection sends an XFixesSelectionNotify.
21+func (p *xpeer) xfixesSelection(owner, selection uint32) {
22+ p.event(peerXFixesFirstEvent, func(b *[32]byte) {
23+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
24+ binary.LittleEndian.PutUint32(b[8:], owner)
25+ binary.LittleEndian.PutUint32(b[12:], selection)
26+ })
27+}
28+
29+// selectionNotify sends a core SelectionNotify to the requestor.
30+func (p *xpeer) selectionNotify(requestor, selection, target, property uint32) {
31+ p.event(31, func(b *[32]byte) {
32+ binary.LittleEndian.PutUint32(b[8:], requestor)
33+ binary.LittleEndian.PutUint32(b[12:], selection)
34+ binary.LittleEndian.PutUint32(b[16:], target)
35+ binary.LittleEndian.PutUint32(b[20:], property)
36+ })
37+}
38+
39+// pumpReq dispatches X events until a request matching match was sent after
40+// the first skip requests.
41+func pumpReq(t *testing.T, x *XWM, p *xpeer, skip int, match func(xpeerReq) bool) (xpeerReq, int) {
42+ t.Helper()
43+ var found xpeerReq
44+ next := skip
45+ readUntil(t, x, func() bool {
46+ x.flush()
47+ reqs := p.requests()
48+ for i := skip; i < len(reqs); i++ {
49+ if match(reqs[i]) {
50+ found, next = reqs[i], i+1
51+ return true
52+ }
53+ }
54+ return false
55+ })
56+ return found, next
57+}
58+
59+func TestSelectionBoot(t *testing.T) {
60+ x, p := bootPeer(t, nil)
61+ require.NotZero(t, x.selWin)
62+ reqs := p.requests()
63+ var created, selected int
64+ for _, r := range reqs {
65+ switch {
66+ case r.major == opCreateWindow && r.u32(4) == x.selWin:
67+ created++
68+ require.EqualValues(t, 2, binary.LittleEndian.Uint16(r.raw[22:]), "InputOnly")
69+ case r.major == 138 && r.minor == 2: // SelectSelectionInput
70+ selected++
71+ require.EqualValues(t, peerRoot, r.u32(4))
72+ require.Contains(t, []uint32{p.atom("CLIPBOARD"), p.atom("PRIMARY")}, r.u32(8))
73+ require.EqualValues(t, 7, r.u32(12))
74+ }
75+ }
76+ require.Equal(t, 1, created)
77+ require.Equal(t, 2, selected)
78+}
79+
80+func TestSelectionXOwnsAnnounces(t *testing.T) {
81+ x, p, h, _, _ := attached(t)
82+ const owner = 0x500010
83+ clip, prim := p.atom("CLIPBOARD"), p.atom("PRIMARY")
84+ tgt := p.atom("TARGETS")
85+
86+ // Owner with text targets: TARGETS probe, then the intersection.
87+ p.xfixesSelection(owner, clip)
88+ req, n := pumpReq(t, x, p, 0, func(r xpeerReq) bool { return r.major == 24 }) // ConvertSelection
89+ require.Equal(t, x.selWin, req.u32(4))
90+ require.Equal(t, clip, req.u32(8))
91+ require.Equal(t, tgt, req.u32(12))
92+ prop := req.u32(16)
93+ require.Equal(t, p.atom("_NEFERWL_TARGETS"), prop)
94+ p.setProp(x.selWin, prop, 4, 32, le32(tgt, p.atom("UTF8_STRING"), p.atom("image/png"), p.atom("TEXT")))
95+ got := make(chan []string, 8)
96+ h.EXPECT().SelectionOwned(SelClipboard, []string{"text/plain;charset=utf-8", "UTF8_STRING", "TEXT"}).Run(func(int, []string) { got <- nil }).Once()
97+ p.selectionNotify(x.selWin, clip, tgt, prop)
98+ readUntil(t, x, func() bool { return len(got) == 1 })
99+ pumpReq(t, x, p, n, func(r xpeerReq) bool { return r.major == opGetProperty && r.u32(8) == prop })
100+
101+ // Only image/png: nothing offered.
102+ p.xfixesSelection(owner, prim)
103+ pumpReq(t, x, p, n, func(r xpeerReq) bool { return r.major == 24 && r.u32(8) == prim })
104+ p.setProp(x.selWin, prop, 4, 32, le32(tgt, p.atom("image/png")))
105+ h.EXPECT().SelectionOwned(SelPrimary, []string(nil)).Run(func(int, []string) { got <- nil }).Once()
106+ p.selectionNotify(x.selWin, prim, tgt, prop)
107+ readUntil(t, x, func() bool { return len(got) == 2 })
108+
109+ // Owner None.
110+ h.EXPECT().SelectionOwned(SelClipboard, []string(nil)).Run(func(int, []string) { got <- nil }).Once()
111+ p.xfixesSelection(0, clip)
112+ readUntil(t, x, func() bool { return len(got) == 3 })
113+
114+ // Our own window: ignored (the mock fails on any host call).
115+ before := len(p.requests())
116+ p.xfixesSelection(x.selWin, clip)
117+ ev := x.Stats().Events
118+ readUntil(t, x, func() bool { return x.Stats().Events > ev })
119+ x.flush()
120+ require.Equal(t, before, len(p.requests()))
121+}
122+
123+// ---- X -> Wayland ----
124+
125+type selRig struct {
126+ x *XWM
127+ p *xpeer
128+ h *mockhost
129+ clip uint32
130+ utf8 uint32
131+ data uint32
132+ skip int
133+ timer func()
134+}
135+
136+// ownedRig attaches an XWM whose CLIPBOARD is owned by an X client offering text.
137+func ownedRig(t *testing.T) *selRig {
138+ t.Helper()
139+ x, p, h, _, _ := attached(t)
140+ r := &selRig{x: x, p: p, h: h, clip: p.atom("CLIPBOARD"), utf8: p.atom("UTF8_STRING"), data: p.atom("_NEFERWL_DATA")}
141+ h.EXPECT().After(mock.Anything, mock.Anything).Run(func(_ time.Duration, fn func()) { r.timer = fn }).Return(func() {}).Maybe()
142+ got := 0
143+ h.EXPECT().SelectionOwned(SelClipboard, mock.Anything).Run(func(int, []string) { got++ }).Once()
144+ p.xfixesSelection(0x500010, r.clip)
145+ _, n := pumpReq(t, x, p, 0, func(q xpeerReq) bool { return q.major == 24 })
146+ tgt := p.atom("TARGETS")
147+ p.setProp(x.selWin, p.atom("_NEFERWL_TARGETS"), 4, 32, le32(tgt, r.utf8))
148+ p.selectionNotify(x.selWin, r.clip, tgt, p.atom("_NEFERWL_TARGETS"))
149+ readUntil(t, x, func() bool { return got == 1 })
150+ r.skip = len(p.requests())
151+ _ = n
152+ return r
153+}
154+
155+// receive starts a paste and returns the read end of its pipe.
156+func (r *selRig) receive(t *testing.T, size int) *os.File {
157+ t.Helper()
158+ fds := make([]int, 2)
159+ require.NoError(t, unix.Pipe2(fds, unix.O_CLOEXEC))
160+ if size > 0 {
161+ _, err := unix.FcntlInt(uintptr(fds[0]), unix.F_SETPIPE_SZ, size)
162+ require.NoError(t, err)
163+ }
164+ r.x.SelectionReceive(SelClipboard, "text/plain;charset=utf-8", fds[1])
165+ r.x.flush()
166+ f := os.NewFile(uintptr(fds[0]), "pipe-r")
167+ t.Cleanup(func() { f.Close() })
168+ return f
169+}
170+
171+func (r *selRig) waitGet(t *testing.T) {
172+ t.Helper()
173+ _, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == opGetProperty && q.u32(8) == r.data })
174+ r.skip = n
175+}
176+
177+func pattern(n int) []byte {
178+ b := make([]byte, n)
179+ for i := range b {
180+ b[i] = byte(i*7 + i/251)
181+ }
182+ return b
183+}
184+
185+func TestSelectionXToWaylandSmall(t *testing.T) {
186+ r := ownedRig(t)
187+ f := r.receive(t, 0)
188+ req, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == 24 })
189+ require.Equal(t, r.x.selWin, req.u32(4))
190+ require.Equal(t, r.clip, req.u32(8))
191+ require.Equal(t, r.utf8, req.u32(12))
192+ require.Equal(t, r.data, req.u32(16))
193+ r.skip = n
194+ r.p.setProp(r.x.selWin, r.data, r.utf8, 8, []byte("hello clipboard"))
195+ r.p.selectionNotify(r.x.selWin, r.clip, r.utf8, r.data)
196+ r.waitGet(t)
197+ buf := make([]byte, 64)
198+ var got []byte
199+ readUntil(t, r.x, func() bool {
200+ m, _ := unix.Read(int(f.Fd()), buf[:0:0])
201+ _ = m
202+ return r.x.sel.in[0].state == inFree
203+ })
204+ m, err := f.Read(buf)
205+ require.NoError(t, err)
206+ got = buf[:m]
207+ require.Equal(t, "hello clipboard", string(got))
208+ _, ok := r.p.prop(r.x.selWin, "_NEFERWL_DATA")
209+ require.False(t, ok, "property deleted by the read")
210+ _, err = f.Read(buf)
211+ require.ErrorIs(t, err, io.EOF, "write end closed")
212+}
213+
214+func TestSelectionXToWaylandRefused(t *testing.T) {
215+ r := ownedRig(t)
216+ f := r.receive(t, 0)
217+ _, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == 24 })
218+ r.skip = n
219+ r.p.selectionNotify(r.x.selWin, r.clip, r.utf8, 0)
220+ readUntil(t, r.x, func() bool { return r.x.sel.in[0].state == inFree })
221+ _, err := f.Read(make([]byte, 8))
222+ require.ErrorIs(t, err, io.EOF)
223+}
224+
225+func TestSelectionXToWaylandINCR(t *testing.T) {
226+ r := ownedRig(t)
227+ const total = 1 << 20
228+ want := pattern(total)
229+ f := r.receive(t, 1<<20)
230+ _, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == 24 })
231+ r.skip = n
232+ r.p.setProp(r.x.selWin, r.data, r.p.atom("INCR"), 32, le32(total))
233+ r.p.selectionNotify(r.x.selWin, r.clip, r.utf8, r.data)
234+ r.waitGet(t) // reads and deletes the INCR marker
235+ gets := 1
236+ for off := 0; off <= total; off += chunk {
237+ end := min(off+chunk, total)
238+ r.p.setProp(r.x.selWin, r.data, r.utf8, 8, want[off:end])
239+ r.p.propertyNotify(r.x.selWin, r.data, false)
240+ r.waitGet(t)
241+ gets++
242+ }
243+ readUntil(t, r.x, func() bool { return r.x.sel.in[0].state == inFree })
244+ got, err := io.ReadAll(f)
245+ require.NoError(t, err)
246+ require.True(t, bytes.Equal(want, got), "byte-exact, got %d", len(got))
247+ require.Equal(t, 1+total/chunk+1, gets, "marker, 16 chunks, empty terminator")
248+}
249+
250+// A reader that does not drain: no further GetProperty until the pipe has room.
251+func TestSelectionXToWaylandBackpressure(t *testing.T) {
252+ r := ownedRig(t)
253+ src := newMockfdSource(t)
254+ var onW func()
255+ r.x.lp = newFDLoop(t, r, src, nil, &onW)
256+ src.EXPECT().SetWritable(true).Return(nil).Once()
257+ src.EXPECT().SetWritable(false).Return(nil).Once()
258+ src.EXPECT().Remove().Return().Once()
259+ f := r.receive(t, 0) // default 64 KiB pipe
260+ _, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == 24 })
261+ r.skip = n
262+ r.p.setProp(r.x.selWin, r.data, r.p.atom("INCR"), 32, le32(3*chunk))
263+ r.p.selectionNotify(r.x.selWin, r.clip, r.utf8, r.data)
264+ r.waitGet(t)
265+ want := pattern(3 * chunk)
266+ chunkN := func(k int) {
267+ r.p.setProp(r.x.selWin, r.data, r.utf8, 8, want[k*chunk:(k+1)*chunk])
268+ r.p.propertyNotify(r.x.selWin, r.data, false)
269+ }
270+ chunkN(0) // fills the pipe exactly
271+ r.waitGet(t)
272+ readUntil(t, r.x, func() bool { return r.x.sel.in[0].state == inWaitIncr })
273+ chunkN(1) // nobody reads: EAGAIN, the buffer stays pending
274+ r.waitGet(t)
275+ readUntil(t, r.x, func() bool { return r.x.sel.in[0].state == inWriting })
276+ require.NotNil(t, onW)
277+
278+ gets := getProps(r.p, 0, r.x.selWin)
279+ chunkN(2) // announced while a buffer is pending
280+ ev := r.x.Stats().Events
281+ readUntil(t, r.x, func() bool { return r.x.Stats().Events > ev })
282+ r.x.flush()
283+ require.Equal(t, gets, getProps(r.p, 0, r.x.selWin), "no GetProperty while a buffer is pending")
284+
285+ got := make([]byte, 0, 3*chunk)
286+ buf := make([]byte, chunk)
287+ m, err := f.Read(buf) // the reader wakes: room again
288+ require.NoError(t, err)
289+ got = append(got, buf[:m]...)
290+ onW()
291+ require.Equal(t, inReading, r.x.sel.in[0].state, "the pending announcement is served once drained")
292+ r.waitGet(t)
293+ _ = got
294+ r.x.selClose()
295+}
296+
297+func TestSelectionTimeouts(t *testing.T) {
298+ r := ownedRig(t)
299+ f := r.receive(t, 0)
300+ _, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == 24 })
301+ r.skip = n
302+ require.NotNil(t, r.timer)
303+ // X stays silent after ConvertSelection: nine ticks keep it alive, the tenth aborts.
304+ for i := 0; i < xIdleMax-1; i++ {
305+ fn := r.timer
306+ fn()
307+ require.Equal(t, inConverting, r.x.sel.in[0].state, i)
308+ }
309+ r.timer()
310+ require.Equal(t, inFree, r.x.sel.in[0].state)
311+ _, err := f.Read(make([]byte, 8))
312+ require.ErrorIs(t, err, io.EOF, "fd closed")
313+ pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == opDeleteProperty && q.u32(8) == r.data })
314+ require.Equal(t, nXfer, r.x.sel.pool.n)
315+}
316+
317+func TestSelectionPoolExhausted(t *testing.T) {
318+ r := ownedRig(t)
319+ var files []*os.File
320+ for i := 0; i < nXfer; i++ {
321+ files = append(files, r.receive(t, 0))
322+ }
323+ fifth := r.receive(t, 0)
324+ _, err := fifth.Read(make([]byte, 8))
325+ require.ErrorIs(t, err, io.EOF, "the fifth paste is refused with its pipe closed")
326+ for i := range r.x.sel.in {
327+ require.NotEqual(t, inFree, r.x.sel.in[i].state, i)
328+ }
329+ _ = files
330+ r.x.selClose()
331+ for i := range r.x.sel.in {
332+ require.Equal(t, inFree, r.x.sel.in[i].state, i)
333+ }
334+}
335+
336+// allocRig is a rig whose peer sinks requests and whose timer seam is inert.
337+func allocRig(t *testing.T) *selRig {
338+ r := ownedRig(t)
339+ sinkPeer(t, r.x, r.p)
340+ r.x.lp = newTimerLoop(t)
341+ r.x.sel.timerOn = true // no timer re-arming in the measured loop
342+ return r
343+}
344+
345+func dataReply(r *selRig, typ uint32, payload []byte) *xturbo.Reply {
346+ raw := make([]byte, 32+(len(payload)+3)&^3)
347+ raw[0], raw[1] = 1, 8
348+ binary.LittleEndian.PutUint32(raw[8:], typ)
349+ binary.LittleEndian.PutUint32(raw[16:], uint32(len(payload)))
350+ copy(raw[32:], payload)
351+ return &xturbo.Reply{Kind: kindSelData, Raw: raw}
352+}
353+
354+func TestSelectionCopyAllocations(t *testing.T) {
355+ r := allocRig(t)
356+ x := r.x
357+ fds := make([]int, 2)
358+ require.NoError(t, unix.Pipe2(fds, unix.O_CLOEXEC))
359+ t.Cleanup(func() { unix.Close(fds[0]); unix.Close(fds[1]) })
360+ payload := pattern(4096)
361+ rep := dataReply(r, r.utf8, payload)
362+ var note xturbo.Event
363+ note.Kind = xproto.KindSelectionNotify
364+ binary.LittleEndian.PutUint32(note.Raw[8:], x.selWin)
365+ binary.LittleEndian.PutUint32(note.Raw[12:], r.clip)
366+ binary.LittleEndian.PutUint32(note.Raw[16:], r.utf8)
367+ binary.LittleEndian.PutUint32(note.Raw[20:], r.data)
368+ sink := make([]byte, 8192)
369+ mime := textMimes[0]
370+ round := func() {
371+ fd, err := unix.FcntlInt(uintptr(fds[1]), unix.F_DUPFD_CLOEXEC, 0)
372+ if err != nil {
373+ t.Fatal("dup")
374+ }
375+ x.SelectionReceive(SelClipboard, mime, fd)
376+ x.onSelectionNotify(&note)
377+ rep.Args = [4]uint32{0, x.sel.in[0].gen}
378+ x.onSelDataReply(rep)
379+ if m, err := unix.Read(fds[0], sink); err != nil || m != 4096 {
380+ t.Fatal("read")
381+ }
382+ x.posted = false
383+ x.flush()
384+ }
385+ round()
386+ require.Zero(t, testing.AllocsPerRun(100, round))
387+}
388+
389+func TestSelectionINCRChunkAllocations(t *testing.T) {
390+ r := allocRig(t)
391+ x := r.x
392+ fds := make([]int, 2)
393+ require.NoError(t, unix.Pipe2(fds, unix.O_CLOEXEC))
394+ t.Cleanup(func() { unix.Close(fds[0]); unix.Close(fds[1]) })
395+ x.SelectionReceive(SelClipboard, textMimes[0], fds[1])
396+ var note xturbo.Event
397+ note.Kind = xproto.KindSelectionNotify
398+ binary.LittleEndian.PutUint32(note.Raw[8:], x.selWin)
399+ binary.LittleEndian.PutUint32(note.Raw[12:], r.clip)
400+ binary.LittleEndian.PutUint32(note.Raw[16:], r.utf8)
401+ binary.LittleEndian.PutUint32(note.Raw[20:], r.data)
402+ x.onSelectionNotify(&note)
403+ x.onSelDataReply(dataReply(r, r.p.atom("INCR"), le32(1<<20)))
404+ require.Equal(t, inWaitIncr, x.sel.in[0].state)
405+ rep := dataReply(r, r.utf8, pattern(chunk))
406+ var pn xturbo.Event
407+ pn.Kind = xproto.KindPropertyNotify
408+ binary.LittleEndian.PutUint32(pn.Raw[4:], x.selWin)
409+ binary.LittleEndian.PutUint32(pn.Raw[8:], r.data)
410+ sink := make([]byte, chunk)
411+ step := func() {
412+ x.onSelProperty(&pn)
413+ rep.Args = [4]uint32{0, x.sel.in[0].gen}
414+ x.onSelDataReply(rep)
415+ if m, err := unix.Read(fds[0], sink); err != nil || m != chunk {
416+ t.Fatal("read")
417+ }
418+ x.posted = false
419+ x.flush()
420+ }
421+ step()
422+ require.Zero(t, testing.AllocsPerRun(100, step))
423+ require.Equal(t, inWaitIncr, x.sel.in[0].state)
424+}
425+
426+// ---- Wayland -> X ----
427+
428+const reqWin = 0x500020
429+
430+func (p *xpeer) selectionRequest(owner, requestor, selection, target, property uint32) {
431+ p.event(30, func(b *[32]byte) {
432+ binary.LittleEndian.PutUint32(b[8:], owner)
433+ binary.LittleEndian.PutUint32(b[12:], requestor)
434+ binary.LittleEndian.PutUint32(b[16:], selection)
435+ binary.LittleEndian.PutUint32(b[20:], target)
436+ binary.LittleEndian.PutUint32(b[24:], property)
437+ })
438+}
439+
440+func (p *xpeer) selectionClear(owner, selection uint32) {
441+ p.event(29, func(b *[32]byte) {
442+ binary.LittleEndian.PutUint32(b[8:], owner)
443+ binary.LittleEndian.PutUint32(b[12:], selection)
444+ })
445+}
446+
447+// sentNotify returns the property of the SelectionNotify SendEvent after skip.
448+func sentNotify(t *testing.T, r *selRig) (property uint32) {
449+ t.Helper()
450+ req, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == opSendEvent && q.raw[12] == 31 })
451+ r.skip = n
452+ require.EqualValues(t, reqWin, req.u32(4))
453+ require.Equal(t, r.clip, binary.LittleEndian.Uint32(req.raw[12+12:]))
454+ return binary.LittleEndian.Uint32(req.raw[12+20:])
455+}
456+
457+// ownedByWayland attaches an XWM and gives CLIPBOARD to a Wayland client offering mimes.
458+func ownedByWayland(t *testing.T, mimes ...string) *selRig {
459+ t.Helper()
460+ x, p, h, _, _ := attached(t)
461+ r := &selRig{x: x, p: p, h: h, clip: p.atom("CLIPBOARD"), utf8: p.atom("UTF8_STRING"), data: p.atom("_NEFERWL_DATA")}
462+ h.EXPECT().After(mock.Anything, mock.Anything).Run(func(_ time.Duration, fn func()) { r.timer = fn }).Return(func() {}).Maybe()
463+ x.WaylandSelection(SelClipboard, mimes)
464+ x.flush()
465+ req, n := pumpReq(t, x, p, 0, func(q xpeerReq) bool { return q.major == opSetSelectionOwner && q.u32(8) == r.clip })
466+ require.Equal(t, x.selWin, req.u32(4))
467+ r.skip = n
468+ return r
469+}
470+
471+// serve queues a SelectionRequest; SelectionRead hands out the read end of a
472+// pipe whose write end is returned.
473+func (r *selRig) serve(t *testing.T, target uint32) (w *os.File) {
474+ t.Helper()
475+ fds := make([]int, 2)
476+ require.NoError(t, unix.Pipe2(fds, unix.O_CLOEXEC|unix.O_NONBLOCK))
477+ r.h.EXPECT().SelectionRead(SelClipboard, mock.Anything).Return(fds[0]).Once()
478+ r.p.selectionRequest(r.x.selWin, reqWin, r.clip, target, r.data)
479+ w = os.NewFile(uintptr(fds[1]), "pipe-w")
480+ t.Cleanup(func() { w.Close() })
481+ return w
482+}
483+
484+func TestSelectionWaylandToXTargets(t *testing.T) {
485+ r := ownedByWayland(t, "text/plain")
486+ r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.p.atom("TARGETS"), r.data)
487+ require.Equal(t, r.data, sentNotify(t, r))
488+ v, ok := r.p.prop(reqWin, "_NEFERWL_DATA")
489+ require.True(t, ok)
490+ require.Equal(t, []uint32{r.p.atom("TARGETS"), r.p.atom("TIMESTAMP"), r.p.atom("STRING")}, atomsOf(v), "no UTF8 target for text/plain only")
491+ r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.p.atom("image/png"), r.data)
492+ require.Zero(t, sentNotify(t, r), "unknown target refused")
493+ // UTF8_STRING is not offered by a text/plain-only source, but STRING reads it.
494+ r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.utf8, r.data)
495+ require.Zero(t, sentNotify(t, r), "no UTF-8 text offered")
496+}
497+
498+func TestSelectionWaylandToXSmall(t *testing.T) {
499+ r := ownedByWayland(t, "text/plain;charset=utf-8")
500+ w := r.serve(t, r.utf8)
501+ data := pattern(4096)
502+ _, err := w.Write(data)
503+ require.NoError(t, err)
504+ require.NoError(t, w.Close())
505+ require.Equal(t, r.data, sentNotify(t, r))
506+ v, ok := r.p.prop(reqWin, "_NEFERWL_DATA")
507+ require.True(t, ok)
508+ require.Equal(t, r.utf8, v.typ)
509+ require.True(t, bytes.Equal(data, v.data))
510+ require.Equal(t, nXfer, r.x.sel.pool.n)
511+}
512+
513+func TestSelectionWaylandToXINCR(t *testing.T) {
514+ r := ownedByWayland(t, "text/plain;charset=utf-8")
515+ src := newMockfdSource(t)
516+ var onR func()
517+ removes := 0
518+ r.x.lp = newFDLoop(t, r, src, &onR, nil)
519+ src.EXPECT().Remove().Run(func() { removes++ }).Return().Maybe()
520+ const total = 1<<20 + 123
521+ data := pattern(total)
522+ w := r.serve(t, r.utf8)
523+ require.NoError(t, unix.SetNonblock(int(w.Fd()), false))
524+ go func() { _, _ = w.Write(data); w.Close() }()
525+
526+ // The first fill is a whole chunk: INCR is announced.
527+ var got []byte
528+ deadline := time.Now().Add(5 * time.Second)
529+ for !r.x.sel.out[0].incr {
530+ require.True(t, time.Now().Before(deadline), "INCR never announced")
531+ r.x.onReadable()
532+ if onR != nil && r.x.sel.out[0].state == outActive {
533+ onR()
534+ }
535+ time.Sleep(time.Millisecond)
536+ }
537+ require.Equal(t, r.data, sentNotify(t, r))
538+ v, _ := r.p.prop(reqWin, "_NEFERWL_DATA")
539+ require.Equal(t, r.p.atom("INCR"), v.typ)
540+ mask, _ := pumpReq(t, r.x, r.p, 0, func(q xpeerReq) bool {
541+ return q.major == opChangeWindowAttributes && q.u32(4) == reqWin && q.u32(12) == uint32(xproto.EventMaskPropertyChange)
542+ })
543+ require.NotZero(t, mask.raw)
544+
545+ chunks := 0
546+ for {
547+ // The requestor deletes the property: the next chunk goes out.
548+ r.p.propertyNotify(reqWin, r.data, true)
549+ before := len(r.p.requests())
550+ if onR != nil {
551+ time.Sleep(time.Millisecond)
552+ }
553+ var cp xpeerProp
554+ readUntil(t, r.x, func() bool {
555+ if onR != nil && r.x.sel.out[0].state != outFree && r.x.sel.out[0].n < chunk && !r.x.sel.out[0].eof {
556+ onR()
557+ }
558+ r.x.flush()
559+ for _, q := range r.p.requests()[before:] {
560+ if q.major == opChangeProperty && q.u32(4) == reqWin {
561+ return true
562+ }
563+ }
564+ return false
565+ })
566+ cp, _ = r.p.prop(reqWin, "_NEFERWL_DATA")
567+ chunks++
568+ if len(cp.data) == 0 {
569+ break
570+ }
571+ require.Equal(t, r.utf8, cp.typ)
572+ got = append(got, cp.data...)
573+ }
574+ require.True(t, bytes.Equal(data, got), "byte-exact: %d of %d", len(got), total)
575+ require.Equal(t, total/chunk+2, chunks, "16 full chunks, the tail, then the empty terminator")
576+ readUntil(t, r.x, func() bool { return r.x.sel.out[0].state == outFree })
577+ // The mask is removed at the end.
578+ reqs := r.p.requests()
579+ last := reqs[len(reqs)-1]
580+ for i := len(reqs) - 1; i >= 0; i-- {
581+ if reqs[i].major == opChangeWindowAttributes && reqs[i].u32(4) == reqWin {
582+ last = reqs[i]
583+ break
584+ }
585+ }
586+ require.Zero(t, last.u32(12), "event mask dropped")
587+ require.Equal(t, nXfer, r.x.sel.pool.n)
588+}
589+
590+func TestSelectionWaylandToXTimeoutAndClear(t *testing.T) {
591+ r := ownedByWayland(t, "text/plain;charset=utf-8")
592+ src := newMockfdSource(t)
593+ src.EXPECT().Remove().Return().Maybe()
594+ r.x.lp = newFDLoop(t, r, src, nil, nil)
595+ w := r.serve(t, r.utf8)
596+ _, err := w.Write(pattern(chunk)) // a full chunk: INCR
597+ require.NoError(t, err)
598+ readUntil(t, r.x, func() bool { return r.x.sel.out[0].incr })
599+ require.Equal(t, r.data, sentNotify(t, r))
600+ require.NotNil(t, r.timer)
601+ for i := 0; i < xIdleMax; i++ {
602+ require.Equal(t, outActive, r.x.sel.out[0].state, i)
603+ r.timer()
604+ }
605+ require.Equal(t, outFree, r.x.sel.out[0].state, "no Deleted for 10 s aborts")
606+ pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == opDeleteProperty && q.u32(4) == reqWin })
607+
608+ // SelectionClear aborts a running transfer and refuses further requests.
609+ w2 := r.serve(t, r.utf8)
610+ _, err = w2.Write(pattern(chunk))
611+ require.NoError(t, err)
612+ readUntil(t, r.x, func() bool { return r.x.sel.out[0].state == outActive })
613+ r.p.selectionClear(r.x.selWin, r.clip)
614+ readUntil(t, r.x, func() bool { return r.x.sel.out[0].state == outFree })
615+ r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.utf8, r.data)
616+ require.Equal(t, r.data, sentNotify(t, r), "INCR announcement of the aborted transfer")
617+ require.Zero(t, sentNotify(t, r), "request after the clear is refused")
618+}
619+
620+// Replacing the Wayland selection mid-transfer aborts it; nil releases X ownership.
621+func TestSelectionClearAndRace(t *testing.T) {
622+ r := ownedByWayland(t, "text/plain;charset=utf-8")
623+ src := newMockfdSource(t)
624+ src.EXPECT().Remove().Return().Maybe()
625+ r.x.lp = newFDLoop(t, r, src, nil, nil)
626+ w := r.serve(t, r.utf8)
627+ _, err := w.Write(pattern(chunk))
628+ require.NoError(t, err)
629+ readUntil(t, r.x, func() bool { return r.x.sel.out[0].incr })
630+ r.x.WaylandSelection(SelClipboard, []string{"text/plain"})
631+ require.Equal(t, outFree, r.x.sel.out[0].state)
632+ r.x.WaylandSelection(SelClipboard, nil) // protected or cleared
633+ r.x.flush()
634+ req, _ := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == opSetSelectionOwner && q.u32(4) == 0 })
635+ require.Equal(t, r.clip, req.u32(8), "ownership released")
636+ require.False(t, r.x.sel.st[SelClipboard].ours)
637+}
638+
639+func TestSelectionCopyAllocationsWaylandToX(t *testing.T) {
640+ r := ownedByWayland(t, "text/plain;charset=utf-8")
641+ x := r.x
642+ sinkPeer(t, x, r.p)
643+ x.lp = newTimerLoop(t)
644+ x.sel.timerOn = true
645+ data := pattern(4096)
646+ var ev xturbo.Event
647+ ev.Kind = xproto.KindSelectionRequest
648+ binary.LittleEndian.PutUint32(ev.Raw[8:], x.selWin)
649+ binary.LittleEndian.PutUint32(ev.Raw[12:], reqWin)
650+ binary.LittleEndian.PutUint32(ev.Raw[16:], r.clip)
651+ binary.LittleEndian.PutUint32(ev.Raw[20:], r.utf8)
652+ binary.LittleEndian.PutUint32(ev.Raw[24:], r.data)
653+ fds := make([]int, 2)
654+ x.sel.st[SelClipboard].wlMime = [len(textMimes)]string{"text/plain;charset=utf-8"}
655+ r.h.EXPECT().SelectionRead(SelClipboard, mock.Anything).RunAndReturn(func(int, string) int { return fds[0] }).Maybe()
656+ // The host mock allocates per call (call recording); its cost is measured
657+ // alone and subtracted, so the request path itself must add nothing.
658+ x.post = nil // the test post queue grows by append
659+ base := testing.AllocsPerRun(100, func() { r.h.Calls = r.h.Calls[:0]; x.host.SelectionRead(SelClipboard, "text/plain;charset=utf-8") })
660+ allocs := testing.AllocsPerRun(100, func() {
661+ if unix.Pipe2(fds, unix.O_CLOEXEC|unix.O_NONBLOCK) != nil {
662+ t.Fatal("pipe")
663+ }
664+ if _, err := unix.Write(fds[1], data); err != nil {
665+ t.Fatal("write")
666+ }
667+ unix.Close(fds[1])
668+ r.h.Calls = r.h.Calls[:0] // the mock's call log would grow
669+ x.onSelectionRequest(&ev)
670+ x.posted = false
671+ x.flush()
672+ })
673+ // Pipe2 itself does not allocate; the measured path is read + ChangeProperty + SendEvent.
674+ // The slack covers the in-process X peer goroutine reading the flushed bytes.
675+ require.LessOrEqual(t, allocs, base+8, "request path allocates beyond the host call")
676+ require.Equal(t, outFree, x.sel.out[0].state)
677+}
678+
679+// A TARGETS reply that arrives after Wayland took the selection is dropped.
680+func TestSelectionStaleTargetsAfterWaylandOwner(t *testing.T) {
681+ x, p, h, _, _ := attached(t)
682+ const owner = 0x500010
683+ clip := p.atom("CLIPBOARD")
684+ tgt := p.atom("TARGETS")
685+ p.xfixesSelection(owner, clip)
686+ req, _ := pumpReq(t, x, p, 0, func(r xpeerReq) bool { return r.major == 24 })
687+ prop := req.u32(16)
688+ x.WaylandSelection(SelClipboard, []string{"text/plain"})
689+ p.setProp(x.selWin, prop, 4, 32, le32(tgt, p.atom("UTF8_STRING")))
690+ p.selectionNotify(x.selWin, clip, tgt, prop) // the mock fails on SelectionOwned
691+ ev := x.Stats().Events
692+ readUntil(t, x, func() bool { return x.Stats().Events > ev })
693+ x.flush()
694+ require.True(t, x.sel.st[SelClipboard].ours)
695+ for i := 0; i < 20; i++ { // let any (wrong) GetProperty round trip finish
696+ x.onReadable()
697+ x.flush()
698+ time.Sleep(time.Millisecond)
699+ }
700+ require.Zero(t, getProps(p, 0, x.selWin), "the stale TARGETS reply is not fetched")
701+ _ = h
702+}
703+
704+// newFDLoop is a loop whose AddFD hands out src and captures the callbacks;
705+// timers are inert.
706+func newFDLoop(t *testing.T, rig *selRig, src fdSource, onR, onW *func()) *mockloop {
707+ l := newMockloop(t)
708+ l.EXPECT().AddFD(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(
709+ func(_ int, r, w func()) (fdSource, error) {
710+ if onR != nil {
711+ *onR = r
712+ }
713+ if onW != nil {
714+ *onW = w
715+ }
716+ return src, nil
717+ }).Maybe()
718+ l.EXPECT().After(mock.Anything, mock.Anything).RunAndReturn(
719+ func(_ time.Duration, fn func()) func() { rig.timer = fn; return func() {} }).Maybe()
720+ return l
721+}
722+
723+// newTimerLoop is a loop with inert timers.
724+func newTimerLoop(t *testing.T) *mockloop {
725+ l := newMockloop(t)
726+ l.EXPECT().After(mock.Anything, mock.Anything).RunAndReturn(
727+ func(time.Duration, func()) func() { return func() {} }).Maybe()
728+ return l
729+}
730+
731+// A late SelectionNotify of an aborted transfer (other selection or property)
732+// is not taken by the next transfer.
733+func TestSelectionLateReplyIgnored(t *testing.T) {
734+ r := ownedRig(t)
735+ _ = r.receive(t, 0)
736+ _, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == 24 })
737+ r.skip = n
738+ for i := 0; i < xIdleMax; i++ {
739+ r.timer()
740+ }
741+ require.Equal(t, inFree, r.x.sel.in[0].state)
742+ _ = r.receive(t, 0)
743+ require.Equal(t, inConverting, r.x.sel.in[0].state)
744+ prim := r.p.atom("PRIMARY")
745+ r.p.selectionNotify(r.x.selWin, prim, r.utf8, r.data)
746+ r.p.selectionNotify(r.x.selWin, r.clip, r.utf8, r.p.atom("_NEFERWL_TARGETS"))
747+ ev := r.x.Stats().Events
748+ readUntil(t, r.x, func() bool { return r.x.Stats().Events >= ev+2 })
749+ require.Equal(t, inConverting, r.x.sel.in[0].state)
750+}
751+
752+// CloseConn closes the fds of in-flight transfers.
753+func TestSelectionCloseConnClosesFDs(t *testing.T) {
754+ r := ownedRig(t)
755+ f := r.receive(t, 0)
756+ r.x.CloseConn()
757+ _, err := f.Read(make([]byte, 8))
758+ require.ErrorIs(t, err, io.EOF)
759+}
760+
761+// Closing never starts a queued transfer's ConvertSelection.
762+func TestSelectionCloseDoesNotStartQueued(t *testing.T) {
763+ r := ownedRig(t)
764+ _ = r.receive(t, 0)
765+ _ = r.receive(t, 0)
766+ _, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == 24 })
767+ r.x.selClose()
768+ r.x.flush()
769+ time.Sleep(50 * time.Millisecond)
770+ cs := 0
771+ for _, q := range since(r.p, r.skip, 24) {
772+ cs++
773+ _ = q
774+ }
775+ _ = n
776+ require.Equal(t, 1, cs)
777+ require.False(t, r.x.sel.timerOn)
778+}
779+
780+func TestSelectionWaylandToXTimestamp(t *testing.T) {
781+ r := ownedByWayland(t, "text/plain")
782+ r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.p.atom("TIMESTAMP"), r.data)
783+ require.Equal(t, r.data, sentNotify(t, r))
784+ v, ok := r.p.prop(reqWin, "_NEFERWL_DATA")
785+ require.True(t, ok)
786+ require.EqualValues(t, 32, v.format)
787+ require.EqualValues(t, 19, v.typ, "INTEGER")
788+ require.Len(t, v.data, 4)
789+}
790+
791+func TestSelectionOwnershipTimeFromLastEvent(t *testing.T) {
792+ x, p, _, _, _ := attached(t)
793+ p.event(28, func(b *[32]byte) { // PropertyNotify
794+ binary.LittleEndian.PutUint32(b[4:], x.selWin)
795+ binary.LittleEndian.PutUint32(b[8:], 1)
796+ binary.LittleEndian.PutUint32(b[12:], 4242)
797+ })
798+ readUntil(t, x, func() bool { return x.lastTime == 4242 })
799+ x.WaylandSelection(SelClipboard, []string{"text/plain"})
800+ require.EqualValues(t, 4242, x.sel.st[SelClipboard].ownTime)
801+}
A internal/adapters/wayland/xwm/selection_xsettings.go +198 -0
@@ -0,0 +1,198 @@
1+package xwm
2+
3+import (
4+ "encoding/binary"
5+ "unicode/utf8"
6+
7+ "bnema.dev/xturbo"
8+ "bnema.dev/xturbo/xproto"
9+)
10+
11+// XSETTINGS 0.5 manager (selection _XSETTINGS_S0). The window is the XWM's
12+// own, independent of any selection bridge window.
13+
14+const (
15+ xsBufSize = 1024
16+ xsMaxString = 256 // longest theme name published
17+ xsInt = 0
18+ xsString = 1
19+)
20+
21+// xsettings is the manager state. Settings are fixed, sorted by name.
22+type xsettings struct {
23+ win uint32
24+ active bool // we own _XSETTINGS_S0
25+ taken bool // another manager owns it (boot check)
26+ ownerNo uint32
27+ argb uint32 // ARGB32 picture format, 0 unknown
28+
29+ serial uint32
30+ // Last published values and the serial of their last change.
31+ theme string
32+ size int32
33+ fixedSerial uint32 // serial of the constant settings (scale 1, 96 dpi)
34+ themeSerial uint32
35+ sizeSerial uint32
36+
37+ buf [xsBufSize]byte
38+}
39+
40+// X11 clients see outputs at their logical size and the compositor upscales
41+// their windows, so the scale announced is always 1 at 96 dpi.
42+const xsDPI = 96 * 1024
43+
44+func (x *XWM) cursorThemeName() string {
45+ t := x.cfg.CursorTheme
46+ if t == "" {
47+ t = "default"
48+ }
49+ if len(t) > xsMaxString {
50+ n := xsMaxString
51+ for n > 0 && !utf8.RuneStart(t[n]) { // back off to a rune boundary
52+ n--
53+ }
54+ t = t[:n]
55+ }
56+ return t
57+}
58+
59+func (x *XWM) cursorThemeSize() int32 {
60+ if s := x.cfg.CursorSize; s >= 8 && s <= 256 {
61+ return int32(s)
62+ }
63+ return 24
64+}
65+
66+// bootSettings runs at the end of boot: the XSETTINGS manager and the root
67+// cursor. Failures are never fatal.
68+func (x *XWM) bootSettings() error {
69+ if err := x.bootXSettings(); err != nil {
70+ return err
71+ }
72+ return x.bootRootCursor()
73+}
74+
75+func (x *XWM) bootXSettings() error {
76+ if !x.cfg.XSettings {
77+ return nil
78+ }
79+ conn, b := x.conn, x.conn.Buf()
80+ win, err := conn.NewXID()
81+ if err != nil {
82+ return err
83+ }
84+ x.xs.win = win
85+ var wv xproto.CreateWindowValues
86+ wv.SetEventMask(0)
87+ xproto.CreateWindow(b, 0, xproto.Window(win), xproto.Window(x.root), -1, -1, 1, 1, 0, xproto.WindowClassInputOnly, 0, &wv)
88+ x.expect(xproto.GetSelectionOwner(b, xproto.Atom(x.atoms[atomXSettingsS0])), kindSelOwner, 0, 0)
89+ if err := conn.Sync(&x.h); err != nil {
90+ return err
91+ }
92+ if x.bootErr != nil {
93+ return x.bootErr
94+ }
95+ if x.xs.taken {
96+ x.log.Warn().Uint32("owner", x.xs.ownerNo).Msg("another XSETTINGS manager runs, not taking _XSETTINGS_S0")
97+ xproto.DestroyWindow(b, xproto.Window(win))
98+ x.xs.win = 0
99+ return nil
100+ }
101+ x.publishXSettings(true)
102+ sel := x.atoms[atomXSettingsS0]
103+ xproto.SetSelectionOwner(b, xproto.Window(win), xproto.Atom(sel), 0)
104+ // ICCCM §2.8: announce the new manager to the root.
105+ var ev [32]byte
106+ ev[0], ev[1] = 33, 32 // ClientMessage, format 32
107+ binary.LittleEndian.PutUint32(ev[4:], x.root)
108+ binary.LittleEndian.PutUint32(ev[8:], x.atoms[atomManager])
109+ binary.LittleEndian.PutUint32(ev[16:], sel)
110+ binary.LittleEndian.PutUint32(ev[20:], win)
111+ xproto.SendEvent(b, false, xproto.Window(x.root), xproto.EventMaskStructureNotify, &ev)
112+ x.xs.active = true
113+ return nil
114+}
115+
116+// onSelOwnerReply: the boot GetSelectionOwner of _XSETTINGS_S0.
117+func (x *XWM) onSelOwnerReply(r *xturbo.Reply) {
118+ if v, ok := xproto.ParseGetSelectionOwnerReply(r.Raw); ok && v.Owner() != 0 && uint32(v.Owner()) != x.xs.win {
119+ x.xs.taken, x.xs.ownerNo = true, uint32(v.Owner())
120+ }
121+}
122+
123+// onXSettingsClear: another client took _XSETTINGS_S0.
124+func (x *XWM) onXSettingsClear(ev *xturbo.Event) {
125+ e := xproto.SelectionClearEvent(xproto.SelectionClearOf(ev))
126+ if x.xs.active && uint32(e.Owner()) == x.xs.win && uint32(e.Selection()) == x.atoms[atomXSettingsS0] {
127+ x.xs.active = false
128+ x.log.Warn().Msg("XSETTINGS manager replaced by another client, settings no longer updated")
129+ }
130+}
131+
132+// publishXSettings encodes and writes the property when a value changed
133+// (force: the first time). It reports whether it wrote. Unchanged values cost
134+// no allocation and no request.
135+func (x *XWM) publishXSettings(force bool) bool {
136+ s := &x.xs
137+ theme, size := x.cursorThemeName(), x.cursorThemeSize()
138+ if !force && s.serial != 0 && theme == s.theme && size == s.size {
139+ return false
140+ }
141+ next := s.serial + 1 // wraps
142+ if force || s.serial == 0 {
143+ s.fixedSerial = next
144+ }
145+ if force || s.serial == 0 || theme != s.theme {
146+ s.themeSerial = next
147+ }
148+ if force || s.serial == 0 || size != s.size {
149+ s.sizeSerial = next
150+ }
151+ s.serial, s.theme, s.size = next, theme, size
152+ n := s.encode()
153+ xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(s.win),
154+ xproto.Atom(x.atoms[atomXSettingsSettings]), xproto.Atom(x.atoms[atomXSettingsSettings]), 8, uint32(n), s.buf[:n])
155+ return true
156+}
157+
158+func pad4(n int) int { return (n + 3) &^ 3 }
159+
160+// encode writes the settings, sorted by name, into s.buf.
161+func (s *xsettings) encode() int {
162+ le := binary.LittleEndian
163+ b := s.buf[:]
164+ clear(b[:16])
165+ b[0] = 0 // LSBFirst
166+ le.PutUint32(b[4:], s.serial)
167+ le.PutUint32(b[8:], 5)
168+ n := 12
169+ putName := func(typ byte, name string, serial uint32) {
170+ b[n], b[n+1] = typ, 0
171+ le.PutUint16(b[n+2:], uint16(len(name)))
172+ n += 4
173+ copy(b[n:], name)
174+ clear(b[n+len(name) : n+pad4(len(name))])
175+ n += pad4(len(name))
176+ le.PutUint32(b[n:], serial)
177+ n += 4
178+ }
179+ putName(xsInt, "Gdk/UnscaledDPI", s.fixedSerial)
180+ le.PutUint32(b[n:], xsDPI)
181+ n += 4
182+ putName(xsInt, "Gdk/WindowScalingFactor", s.fixedSerial)
183+ le.PutUint32(b[n:], 1)
184+ n += 4
185+ putName(xsString, "Gtk/CursorThemeName", s.themeSerial)
186+ le.PutUint32(b[n:], uint32(len(s.theme)))
187+ n += 4
188+ copy(b[n:], s.theme)
189+ clear(b[n+len(s.theme) : n+pad4(len(s.theme))])
190+ n += pad4(len(s.theme))
191+ putName(xsInt, "Gtk/CursorThemeSize", s.sizeSerial)
192+ le.PutUint32(b[n:], uint32(s.size))
193+ n += 4
194+ putName(xsInt, "Xft/DPI", s.fixedSerial)
195+ le.PutUint32(b[n:], xsDPI)
196+ n += 4
197+ return n
198+}
A internal/adapters/wayland/xwm/space.go +158 -0
@@ -0,0 +1,158 @@
1+package xwm
2+
3+import "github.com/bnema/neferwl/internal/ports"
4+
5+const maxRegions = 16
6+
7+// Region is one output's part of the layout and where it is in X space.
8+// X = layout - Origin.
9+type Region struct {
10+ Layout, X ports.Rect // logical layout rect; X rect in X pixels
11+}
12+
13+// Space maps layout coordinates (which may be negative) to X coordinates
14+// (non-negative: the layout top-left is X (0,0)). Fixed storage, no allocs.
15+type Space struct {
16+ regions [maxRegions]Region
17+ n int
18+ dropped int // outputs beyond maxRegions in the last Set
19+ ox, oy int // layout point that is X (0,0)
20+ w, h int // X screen size: the extents
21+ scale float64 // scale of the first output; not part of the change test
22+}
23+
24+// Origin is the layout point that is X (0,0): the top-left of the layout
25+// extents.
26+func (s *Space) Origin() (ox, oy int) { return s.ox, s.oy }
27+
28+// Dropped is how many outputs of the last Set did not fit.
29+func (s *Space) Dropped() int { return s.dropped }
30+
31+// Set recomputes the mapping and reports whether it changed.
32+func (s *Space) Set(l ports.Layout) bool {
33+ var next Space
34+ n := len(l)
35+ if n > maxRegions {
36+ next.dropped = n - maxRegions
37+ n = maxRegions
38+ }
39+ next.n = n
40+ var maxX, maxY int
41+ for i := 0; i < n; i++ {
42+ o := l[i]
43+ if i == 0 || o.X < next.ox {
44+ next.ox = o.X
45+ }
46+ if i == 0 || o.Y < next.oy {
47+ next.oy = o.Y
48+ }
49+ if i == 0 || o.X+o.Width > maxX {
50+ maxX = o.X + o.Width
51+ }
52+ if i == 0 || o.Y+o.Height > maxY {
53+ maxY = o.Y + o.Height
54+ }
55+ }
56+ for i := 0; i < n; i++ {
57+ o := l[i]
58+ next.regions[i] = Region{
59+ Layout: ports.Rect{X: o.X, Y: o.Y, W: o.Width, H: o.Height},
60+ X: ports.Rect{X: o.X - next.ox, Y: o.Y - next.oy, W: o.Width, H: o.Height},
61+ }
62+ }
63+ next.w, next.h = maxX-next.ox, maxY-next.oy
64+ if n > 0 {
65+ next.scale = l[0].Scale
66+ }
67+ if next.regions == s.regions && next.n == s.n && next.ox == s.ox && next.oy == s.oy &&
68+ next.w == s.w && next.h == s.h {
69+ s.dropped, s.scale = next.dropped, next.scale
70+ return false
71+ }
72+ *s = next
73+ return true
74+}
75+
76+// Scale is the scale of the first output of the layout, 1 when unset.
77+func (s *Space) Scale() float64 {
78+ if s.scale <= 0 {
79+ return 1
80+ }
81+ return s.scale
82+}
83+
84+// Size is the X screen size.
85+func (s *Space) Size() (w, h int) { return s.w, s.h }
86+
87+// nearest returns the index of the region containing the layout point, or
88+// the closest one by squared distance when it is in a hole. -1 when empty.
89+func (s *Space) nearest(lx, ly int) int {
90+ best, bestD := -1, 0
91+ for i := 0; i < s.n; i++ {
92+ r := s.regions[i].Layout
93+ dx := axisGap(lx, r.X, r.X+r.W)
94+ dy := axisGap(ly, r.Y, r.Y+r.H)
95+ d := dx*dx + dy*dy
96+ if best < 0 || d < bestD {
97+ best, bestD = i, d
98+ }
99+ }
100+ return best
101+}
102+
103+// nearestX is nearest in X space.
104+func (s *Space) nearestX(x, y int) int {
105+ best, bestD := -1, 0
106+ for i := 0; i < s.n; i++ {
107+ r := s.regions[i].X
108+ dx := axisGap(x, r.X, r.X+r.W)
109+ dy := axisGap(y, r.Y, r.Y+r.H)
110+ d := dx*dx + dy*dy
111+ if best < 0 || d < bestD {
112+ best, bestD = i, d
113+ }
114+ }
115+ return best
116+}
117+
118+// axisGap is the distance from v to [lo, hi): 0 inside.
119+func axisGap(v, lo, hi int) int {
120+ if v < lo {
121+ return lo - v
122+ }
123+ if v >= hi {
124+ return v - hi + 1
125+ }
126+ return 0
127+}
128+
129+// WholeOutput reports whether the X rect is exactly one output's, and that
130+// output's layout origin.
131+func (s *Space) WholeOutput(x, y, w, h int) (lx, ly int, ok bool) {
132+ for i := 0; i < s.n; i++ {
133+ if r := s.regions[i]; r.X.X == x && r.X.Y == y && r.X.W == w && r.X.H == h {
134+ return r.Layout.X, r.Layout.Y, true
135+ }
136+ }
137+ return 0, 0, false
138+}
139+
140+// ToX converts a layout point to X, through the nearest region.
141+func (s *Space) ToX(lx, ly int) (x, y int) {
142+ i := s.nearest(lx, ly)
143+ if i < 0 {
144+ return lx, ly
145+ }
146+ r := &s.regions[i]
147+ return r.X.X + lx - r.Layout.X, r.X.Y + ly - r.Layout.Y
148+}
149+
150+// ToLayout converts an X point to layout coordinates.
151+func (s *Space) ToLayout(x, y int) (lx, ly int) {
152+ i := s.nearestX(x, y)
153+ if i < 0 {
154+ return x, y
155+ }
156+ r := &s.regions[i]
157+ return r.Layout.X + x - r.X.X, r.Layout.Y + y - r.X.Y
158+}
A internal/adapters/wayland/xwm/space_test.go +110 -0
@@ -0,0 +1,110 @@
1+package xwm
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+func out(x, y, w, h int) ports.OutputPlacement {
10+ return ports.OutputPlacement{X: x, Y: y, Width: w, Height: h, Scale: 1}
11+}
12+
13+func TestSpaceLayouts(t *testing.T) {
14+ tests := []struct {
15+ name string
16+ l ports.Layout
17+ w, h int
18+ lx, ly, x, y int
19+ }{
20+ {"single", ports.Layout{out(0, 0, 1920, 1080)}, 1920, 1080, 10, 20, 10, 20},
21+ {"side by side", ports.Layout{out(0, 0, 1920, 1080), out(1920, 0, 1280, 1024)}, 3200, 1080, 2000, 5, 2000, 5},
22+ {"left-of negative origin", ports.Layout{out(-640, 0, 640, 480), out(0, 0, 1920, 1080)}, 2560, 1080, -630, 20, 10, 20},
23+ {"above", ports.Layout{out(0, -500, 800, 500), out(0, 0, 800, 600)}, 800, 1100, 3, -400, 3, 100},
24+ {"below", ports.Layout{out(0, 0, 800, 600), out(0, 600, 800, 400)}, 800, 1000, 3, 700, 3, 700},
25+ {"hole nearest", ports.Layout{out(0, 0, 100, 100), out(300, 0, 100, 100)}, 400, 100, 120, 10, 120, 10},
26+ }
27+ for _, tc := range tests {
28+ t.Run(tc.name, func(t *testing.T) {
29+ var s Space
30+ if !s.Set(tc.l) {
31+ t.Fatal("first Set not changed")
32+ }
33+ if w, h := s.Size(); w != tc.w || h != tc.h {
34+ t.Fatalf("size %dx%d, want %dx%d", w, h, tc.w, tc.h)
35+ }
36+ x, y := s.ToX(tc.lx, tc.ly)
37+ if x != tc.x || y != tc.y {
38+ t.Fatalf("ToX = %d,%d want %d,%d", x, y, tc.x, tc.y)
39+ }
40+ if lx, ly := s.ToLayout(x, y); lx != tc.lx || ly != tc.ly {
41+ t.Fatalf("round trip = %d,%d want %d,%d", lx, ly, tc.lx, tc.ly)
42+ }
43+ if s.Set(tc.l) {
44+ t.Fatal("identical Set reported changed")
45+ }
46+ })
47+ }
48+}
49+
50+func TestSpaceEmptyAndOrigin(t *testing.T) {
51+ var s Space
52+ if x, y := s.ToX(5, 6); x != 5 || y != 6 {
53+ t.Fatalf("empty ToX = %d,%d", x, y)
54+ }
55+ s.Set(ports.Layout{out(-640, -20, 640, 480), out(0, 0, 1920, 1080)})
56+ if ox, oy := s.Origin(); ox != -640 || oy != -20 {
57+ t.Fatalf("origin = %d,%d", ox, oy)
58+ }
59+}
60+
61+func TestSpaceSeventeenOutputs(t *testing.T) {
62+ l := make(ports.Layout, 17)
63+ for i := range l {
64+ l[i] = out(i*100, 0, 100, 100)
65+ }
66+ var s Space
67+ s.Set(l)
68+ if s.Dropped() != 1 {
69+ t.Fatalf("dropped = %d", s.Dropped())
70+ }
71+ if w, _ := s.Size(); w != 1600 {
72+ t.Fatalf("width = %d", w)
73+ }
74+ if x, _ := s.ToX(1550, 5); x != 1550 {
75+ t.Fatalf("ToX = %d", x)
76+ }
77+}
78+
79+func TestSpaceAllocations(t *testing.T) {
80+ l := ports.Layout{out(-640, 0, 640, 480), out(0, 0, 1920, 1080)}
81+ var s Space
82+ s.Set(l)
83+ n := testing.AllocsPerRun(100, func() {
84+ s.Set(l)
85+ x, y := s.ToX(-600, 10)
86+ x, y = s.ToLayout(x, y)
87+ _, _ = x, y
88+ _, _ = s.Origin()
89+ _, _ = s.Size()
90+ })
91+ if n != 0 {
92+ t.Fatalf("allocs = %v", n)
93+ }
94+}
95+
96+func TestSpaceWholeOutputMatchesPlace(t *testing.T) {
97+ var s Space
98+ s.Set(ports.Layout{out(-640, 0, 640, 480), out(0, 0, 1920, 1080)})
99+ lx, ly, ok := s.WholeOutput(0, 0, 640, 480)
100+ if !ok || lx != -640 || ly != 0 {
101+ t.Fatalf("WholeOutput = %d,%d,%v", lx, ly, ok)
102+ }
103+ // Placing the window at that origin lands on the same X point.
104+ if x, y := s.ToX(lx, ly); x != 0 || y != 0 {
105+ t.Fatalf("ToX = %d,%d", x, y)
106+ }
107+ if _, _, ok := s.WholeOutput(0, 0, 640, 481); ok {
108+ t.Fatal("partial rect matched")
109+ }
110+}
A internal/adapters/wayland/xwm/streams_test.go +197 -0
@@ -0,0 +1,197 @@
1+package xwm
2+
3+import (
4+ "encoding/hex"
5+ "os"
6+ "path/filepath"
7+ "runtime"
8+ "runtime/debug"
9+ "strings"
10+ "testing"
11+ "time"
12+
13+ "bnema.dev/xturbo/xproto"
14+ "github.com/stretchr/testify/require"
15+)
16+
17+// The recorded streams are the bytes an X server wrote to the XWM during a
18+// steady-state session of one mapped window, captured from the scripted xpeer
19+// (real encoded X packets: events, GetProperty replies). The scenario is
20+// deterministic, so a replay meets the request sequence numbers it recorded.
21+// They are not xtrace captures of a real Xwayland: xpeer speaks the same wire
22+// protocol and keeps the fixture reproducible. Regenerate with
23+//
24+// XWM_UPDATE_STREAMS=1 go test ./internal/adapters/wayland/xwm -run TestRecordedStreams
25+const (
26+ streamCycles = 400
27+ streamWarm = 20 // cycles replayed before measuring
28+)
29+
30+const streamWin = 0x500001
31+
32+type streamKind struct {
33+ name string
34+ // perCycle is Events+Replies the XWM sees per cycle.
35+ perCycle int
36+ cycle func(hs *harness)
37+}
38+
39+var streamKinds = []streamKind{
40+ // A title notification the XWM answers with one GetProperty whose reply
41+ // equals the stored title.
42+ {"property_unchanged", 2, func(hs *harness) {
43+ before := hs.x.Stats().Replies
44+ hs.p.propertyNotify(streamWin, hs.p.atom("_NET_WM_NAME"), false)
45+ readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before })
46+ }},
47+ // The same plus ConfigureNotify, FocusIn and MapNotify.
48+ {"steady", 5, func(hs *harness) {
49+ before := hs.x.Stats().Replies
50+ hs.p.propertyNotify(streamWin, hs.p.atom("_NET_WM_NAME"), false)
51+ readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Replies > before })
52+ events := hs.x.Stats().Events
53+ hs.p.configureNotify(streamWin)
54+ hs.p.focusIn(streamWin)
55+ hs.p.mapNotify(streamWin)
56+ readUntil(hs.t, hs.x, func() bool { return hs.x.Stats().Events >= events+3 })
57+ }},
58+}
59+
60+// streamScenario boots, maps one window and parks the peer behind a poke so
61+// nothing but the test writes to the XWM afterwards (an allocation count is
62+// process-wide). The poke is part of the scenario, recorded or replayed.
63+func streamScenario(t *testing.T) *harness {
64+ t.Helper()
65+ hs := newHarness(t, nil)
66+ hs.classic(streamWin)
67+ hs.mapAndWait(streamWin)
68+ // The steady state is a focused window: a FocusIn on it is not denied,
69+ // so no request depends on how events are batched into flushes. The
70+ // poke makes sure the focus requests are out before the peer pauses.
71+ hs.x.Focus(hs.x.tab.lookup(streamWin))
72+ poke(hs)
73+ return hs
74+}
75+
76+func poke(hs *harness) {
77+ hs.t.Helper()
78+ n := len(hs.p.requests())
79+ xproto.GetInputFocus(hs.x.conn.Buf())
80+ hs.x.flush()
81+ hs.p.waitReq(n, func(r xpeerReq) bool { return r.major == opGetInputFocus })
82+}
83+
84+func recordStream(t *testing.T, k streamKind) []byte {
85+ hs := streamScenario(t)
86+ poke(hs)
87+ hs.p.mu.Lock()
88+ hs.p.record = true
89+ hs.p.mu.Unlock()
90+ for i := 0; i < streamCycles; i++ {
91+ k.cycle(hs)
92+ }
93+ hs.p.mu.Lock()
94+ defer hs.p.mu.Unlock()
95+ hs.p.record = false
96+ return append([]byte(nil), hs.p.tap...)
97+}
98+
99+func streamPath(k streamKind) string {
100+ return filepath.Join("testdata", "streams", k.name+".hex")
101+}
102+
103+func loadStream(t *testing.T, k streamKind) []byte {
104+ t.Helper()
105+ text, err := os.ReadFile(streamPath(k))
106+ require.NoError(t, err)
107+ b, err := hex.DecodeString(strings.Join(strings.Fields(string(text)), ""))
108+ require.NoError(t, err)
109+ return b
110+}
111+
112+func encodeHex(b []byte) string {
113+ var sb strings.Builder
114+ for len(b) > 0 {
115+ n := min(32, len(b))
116+ sb.WriteString(hex.EncodeToString(b[:n]))
117+ sb.WriteByte('\n')
118+ b = b[n:]
119+ }
120+ return sb.String()
121+}
122+
123+func TestRecordedStreams(t *testing.T) {
124+ for _, k := range streamKinds {
125+ t.Run(k.name, func(t *testing.T) {
126+ got := recordStream(t, k)
127+ if os.Getenv("XWM_UPDATE_STREAMS") != "" {
128+ require.NoError(t, os.MkdirAll(filepath.Dir(streamPath(k)), 0o755))
129+ require.NoError(t, os.WriteFile(streamPath(k), []byte(encodeHex(got)), 0o644))
130+ }
131+ require.Equal(t, loadStream(t, k), got, "the committed stream no longer matches the protocol the XWM speaks")
132+ })
133+ }
134+}
135+
136+// replay feeds the recorded stream and reports the mallocs of the part after
137+// the warm-up cycles.
138+func replay(t *testing.T, k streamKind) (mallocs uint64, hs *harness) {
139+ t.Helper()
140+ stream := loadStream(t, k)
141+ hs = streamScenario(t)
142+ hs.p.pause()
143+ // A peer blocked in its read consumes one more request before it parks
144+ // (this one, or an earlier one still in flight); a parked peer leaves it
145+ // unread. Either way the peer ends up parked, with nothing half read.
146+ xproto.GetInputFocus(hs.x.conn.Buf())
147+ hs.x.flush()
148+ hs.p.waitParked()
149+ _, err := hs.p.f.Write(stream)
150+ require.NoError(t, err)
151+
152+ x := hs.x
153+ // The harness post queue grows by append, at batching-dependent moments.
154+ x.post = func(func()) {}
155+ base := x.Stats().Events + x.Stats().Replies
156+ drive := func(until uint64) {
157+ deadline := time.Now().Add(10 * time.Second)
158+ for x.Stats().Events+x.Stats().Replies < until {
159+ if time.Now().After(deadline) { // no formatting arguments: boxing them would count as mallocs
160+ t.Fatal("stream stalled")
161+ }
162+ before := x.Stats().Events + x.Stats().Replies
163+ x.onReadable()
164+ if x.Stats().Events+x.Stats().Replies == before {
165+ runtime.Gosched()
166+ }
167+ }
168+ }
169+ drive(base + uint64(streamWarm*k.perCycle))
170+ target := base + uint64(streamCycles*k.perCycle)
171+
172+ var m0, m1 runtime.MemStats
173+ // A GC cycle finishing inside the window allocates runtime-internal
174+ // objects (sudogs), which Mallocs counts: keep the collector off.
175+ defer debug.SetGCPercent(debug.SetGCPercent(-1))
176+ // Like testing.AllocsPerRun: one P, so no other goroutine's runtime
177+ // bookkeeping (timers, netpoll) lands in the window.
178+ defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
179+ runtime.GC()
180+ runtime.ReadMemStats(&m0)
181+ drive(target)
182+ runtime.ReadMemStats(&m1)
183+ require.Equal(t, target, x.Stats().Events+x.Stats().Replies)
184+ return m1.Mallocs - m0.Mallocs, hs
185+}
186+
187+func TestPropertyUnchangedAllocations(t *testing.T) {
188+ m, hs := replay(t, streamKinds[0])
189+ require.Zero(t, m, "unchanged property cycles must not allocate")
190+ require.Equal(t, "net title", hs.x.tab.rec(hs.x.tab.lookup(streamWin)).title)
191+}
192+
193+func TestDispatchAllocations(t *testing.T) {
194+ m, hs := replay(t, streamKinds[1])
195+ require.Zero(t, m, "steady-state event dispatch must not allocate")
196+ require.True(t, hs.x.tab.rec(hs.x.tab.lookup(streamWin)).mapped)
197+}
A internal/adapters/wayland/xwm/table.go +242 -0
@@ -0,0 +1,242 @@
1+package xwm
2+
3+import (
4+ "math/bits"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+)
8+
9+// Record kinds.
10+const (
11+ kindFree uint8 = iota
12+ kindManaged
13+ kindUnmanaged
14+)
15+
16+const (
17+ initialSlots = 256
18+ // maxSlots bounds the table: a Slot travels in the low 16 bits of an
19+ // Expect argument.
20+ maxSlots = 1<<16 - 1
21+ indexGone = 0xFFFFFFFF // tombstone in table.index
22+)
23+
24+// pending is the configure state not yet (or last) sent to the X server.
25+type pending struct {
26+ x, y int32
27+ w, h uint32
28+ fullscreen, activated, dirty bool
29+}
30+
31+// ovRect is a layout rectangle.
32+type ovRect struct{ x, y, w, h int32 }
33+
34+// record is one X window known to the XWM. Strings are the only per-record
35+// heap data.
36+type record struct {
37+ xid uint32
38+ id ports.WindowID
39+ kind uint8
40+ gen uint16 // bumped on insert; stale Expect replies are told apart by it
41+
42+ // X state
43+ mapped, mapping, ready bool
44+ x, y int32
45+ w, h uint32
46+
47+ // properties
48+ pending uint16 // propMask bits still awaited
49+ input, deleteWindow, takeFocus bool
50+ wtype uint8
51+ modal, fullscreen bool
52+ transient uint32 // xid
53+ pid int32
54+ minW, minH, maxW, maxH int32
55+ title, class string // owned, replaced only on change
56+ hasNetName bool
57+ floating bool // dialog-like type; set with the properties
58+ bypass uint8 // _NET_WM_BYPASS_COMPOSITOR, 0..2
59+ sized bool // the client asked for its size before Ready
60+
61+ // pairing
62+ serial uint64
63+ surface uint64
64+
65+ // layout position of a managed window, from Place
66+ lx, ly int32
67+ placed bool
68+
69+ // unmanaged (override-redirect) windows
70+ noInput bool // tooltip, DND, notification: never takes the pointer
71+ ovPaired bool // OverrideMapped sent, OverrideGone owed
72+ ovQueued bool // slot is in XWM.ovDirty
73+ ov ovRect // layout rect last sent to the host
74+
75+ // configure
76+ pend, sent pending
77+ stateSent uint8 // _NET_WM_STATE bits last written
78+}
79+
80+// table is a slab of records with an open-addressing index by X id. No Go map.
81+type table struct {
82+ recs []record // slab, len == cap, power of two
83+ free []uint32 // free slot indices (LIFO)
84+ index []uint32 // slot+1, 0 empty, indexGone tombstone; len == 2*len(recs)
85+ shift uint8
86+ n int
87+ tombs int
88+}
89+
90+func newTable(size int) *table {
91+ t := &table{}
92+ t.resize(size)
93+ return t
94+}
95+
96+// resize reallocates the slab, free list and index for size records and
97+// reindexes the live ones.
98+func (t *table) resize(size int) {
99+ old := t.recs
100+ t.recs = make([]record, size)
101+ copy(t.recs, old)
102+ t.free = make([]uint32, 0, size)
103+ for i := min(size, maxSlots) - 1; i >= 0; i-- {
104+ if i >= len(old) || old[i].kind == kindFree {
105+ t.free = append(t.free, uint32(i))
106+ }
107+ }
108+ t.index = make([]uint32, 2*size)
109+ t.shift = uint8(32 - bits.TrailingZeros(uint(len(t.index))))
110+ t.reindex()
111+}
112+
113+// reindex rebuilds the index from the live records, dropping tombstones.
114+// It does not allocate.
115+func (t *table) reindex() {
116+ clear(t.index)
117+ t.tombs = 0
118+ for i := range t.recs {
119+ if t.recs[i].kind != kindFree {
120+ t.indexAdd(t.recs[i].xid, uint32(i)+1)
121+ }
122+ }
123+}
124+
125+func (t *table) hash(xid uint32) uint32 { return (xid * 0x9E3779B1) >> t.shift }
126+
127+// indexAdd places slot for xid, which must be absent, in the first empty or
128+// tombstone position of its probe sequence.
129+func (t *table) indexAdd(xid, slot uint32) {
130+ mask := uint32(len(t.index) - 1)
131+ for i := t.hash(xid); ; i = (i + 1) & mask {
132+ switch t.index[i] {
133+ case 0:
134+ t.index[i] = slot
135+ return
136+ case indexGone:
137+ t.index[i] = slot
138+ t.tombs--
139+ return
140+ }
141+ }
142+}
143+
144+// grow doubles the table. It reports false at maxSlots.
145+func (t *table) grow() bool {
146+ if len(t.recs) >= 1<<16 {
147+ return false
148+ }
149+ t.resize(2 * len(t.recs))
150+ return len(t.free) > 0
151+}
152+
153+// insert returns the slot of xid, creating a managed record (generation
154+// bumped) when absent. It returns 0 when the table is full. Growth is the
155+// only allocation.
156+func (t *table) insert(xid uint32) Slot {
157+ if s := t.lookup(xid); s != 0 {
158+ return s
159+ }
160+ if len(t.free) == 0 && !t.grow() {
161+ return 0
162+ }
163+ i := t.free[len(t.free)-1]
164+ t.free = t.free[:len(t.free)-1]
165+ if t.n+t.tombs+1 > len(t.index)/4*3 { // keep an empty index cell for lookup to stop at
166+ t.reindex()
167+ }
168+ r := &t.recs[i]
169+ gen := r.gen + 1
170+ if gen == 0 {
171+ gen = 1
172+ }
173+ *r = record{xid: xid, kind: kindManaged, gen: gen, input: true}
174+ t.indexAdd(xid, i+1)
175+ t.n++
176+ return Slot(i + 1)
177+}
178+
179+// remove frees s. The record is zeroed except its generation.
180+func (t *table) remove(s Slot) {
181+ r := t.rec(s)
182+ if r == nil || r.kind == kindFree {
183+ return
184+ }
185+ mask := uint32(len(t.index) - 1)
186+ for i := t.hash(r.xid); ; i = (i + 1) & mask {
187+ v := t.index[i]
188+ if v == 0 {
189+ break
190+ }
191+ if v == uint32(s) {
192+ t.index[i] = indexGone
193+ t.tombs++
194+ break
195+ }
196+ }
197+ *r = record{gen: r.gen}
198+ t.free = append(t.free, uint32(s)-1)
199+ t.n--
200+ if t.n+t.tombs > len(t.index)/4*3 {
201+ t.reindex()
202+ }
203+}
204+
205+// lookup returns the slot of xid, or 0.
206+func (t *table) lookup(xid uint32) Slot {
207+ mask := uint32(len(t.index) - 1)
208+ for i := t.hash(xid); ; i = (i + 1) & mask {
209+ v := t.index[i]
210+ if v == 0 {
211+ return 0
212+ }
213+ if v != indexGone && t.recs[v-1].xid == xid {
214+ return Slot(v)
215+ }
216+ }
217+}
218+
219+// rec returns the record of s, or nil when s is out of range. The pointer is
220+// valid only until the next insert, host call or anything else that can grow
221+// the table or free s: never hold it across those, fetch it again with rec(s).
222+func (t *table) rec(s Slot) *record {
223+ if s == 0 || int(s) > len(t.recs) {
224+ return nil
225+ }
226+ return &t.recs[s-1]
227+}
228+
229+// tag is the Expect argument naming s: slot in the low 16 bits, generation
230+// in the high 16.
231+func (t *table) tag(s Slot) uint32 { return uint32(s) | uint32(t.recs[s-1].gen)<<16 }
232+
233+// live resolves an Expect tag. It returns nil when the slot was freed or
234+// reused since the request was sent.
235+func (t *table) live(tag uint32) (Slot, *record) {
236+ s := Slot(tag & 0xFFFF)
237+ r := t.rec(s)
238+ if r == nil || r.kind == kindFree || r.gen != uint16(tag>>16) {
239+ return 0, nil
240+ }
241+ return s, r
242+}
A internal/adapters/wayland/xwm/table_test.go +138 -0
@@ -0,0 +1,138 @@
1+package xwm
2+
3+import (
4+ "math/rand/v2"
5+ "testing"
6+
7+ "github.com/stretchr/testify/require"
8+)
9+
10+func TestTableAgainstMap(t *testing.T) {
11+ rng := rand.New(rand.NewPCG(1, 2))
12+ tab := newTable(initialSlots)
13+ model := map[uint32]Slot{}
14+ var xids []uint32
15+ for i := 0; i < 10000; i++ {
16+ switch op := rng.IntN(3); {
17+ case op < 2 || len(xids) == 0:
18+ xid := rng.Uint32()&0x1fffff | 0x200000
19+ if _, ok := model[xid]; ok {
20+ continue
21+ }
22+ s := tab.insert(xid)
23+ require.NotZero(t, s)
24+ model[xid] = s
25+ xids = append(xids, xid)
26+ default:
27+ k := rng.IntN(len(xids))
28+ xid := xids[k]
29+ xids[k] = xids[len(xids)-1]
30+ xids = xids[:len(xids)-1]
31+ tab.remove(model[xid])
32+ delete(model, xid)
33+ }
34+ if i%500 == 0 {
35+ for xid, s := range model {
36+ require.Equal(t, s, tab.lookup(xid))
37+ }
38+ require.Zero(t, tab.lookup(rng.Uint32()|1<<31))
39+ }
40+ }
41+ require.Equal(t, len(model), tab.n)
42+ for xid, s := range model {
43+ require.Equal(t, s, tab.lookup(xid))
44+ require.Equal(t, xid, tab.rec(s).xid)
45+ }
46+}
47+
48+func TestTableGrowth(t *testing.T) {
49+ tab := newTable(initialSlots)
50+ slots := make([]Slot, 0, initialSlots+1)
51+ for i := 0; i < initialSlots; i++ {
52+ slots = append(slots, tab.insert(uint32(0x200000+i)))
53+ }
54+ require.Len(t, tab.recs, initialSlots)
55+ tab.rec(slots[7]).title = "kept"
56+ slots = append(slots, tab.insert(0x300000))
57+ require.Len(t, tab.recs, 2*initialSlots)
58+ require.Len(t, tab.index, 4*initialSlots)
59+ for i, s := range slots[:initialSlots] {
60+ require.Equal(t, s, tab.lookup(uint32(0x200000+i)))
61+ }
62+ require.Equal(t, "kept", tab.rec(slots[7]).title, "growth keeps records")
63+ require.Equal(t, slots[initialSlots], tab.lookup(0x300000))
64+}
65+
66+func TestTableTombstoneReuse(t *testing.T) {
67+ tab := newTable(initialSlots)
68+ for i := 0; i < 5000; i++ { // far more churn than slots: tombstones must be recycled
69+ s := tab.insert(uint32(0x200000 + i))
70+ require.NotZero(t, s)
71+ require.Equal(t, s, tab.lookup(uint32(0x200000+i)))
72+ tab.remove(s)
73+ require.Zero(t, tab.lookup(uint32(0x200000+i)))
74+ }
75+ require.Len(t, tab.recs, initialSlots)
76+ require.Zero(t, tab.n)
77+}
78+
79+func TestTableGenerationAndTag(t *testing.T) {
80+ tab := newTable(initialSlots)
81+ s := tab.insert(0x200001)
82+ tag := tab.tag(s)
83+ got, rec := tab.live(tag)
84+ require.Equal(t, s, got)
85+ require.NotNil(t, rec)
86+ tab.remove(s)
87+ _, rec = tab.live(tag)
88+ require.Nil(t, rec, "freed slot")
89+ s2 := tab.insert(0x200002)
90+ require.Equal(t, s, s2, "LIFO free list reuses the slot")
91+ _, rec = tab.live(tag)
92+ require.Nil(t, rec, "stale tag after slot reuse")
93+ _, rec = tab.live(tab.tag(s2))
94+ require.NotNil(t, rec)
95+}
96+
97+func TestTableFull(t *testing.T) {
98+ tab := newTable(initialSlots)
99+ for i := 0; i < maxSlots; i++ {
100+ require.NotZero(t, tab.insert(uint32(i+1)), i)
101+ }
102+ require.Zero(t, tab.insert(0xFFFFFF), "table is bounded by the 16-bit slot in Expect tags")
103+ require.Equal(t, Slot(maxSlots), tab.lookup(maxSlots))
104+}
105+
106+func TestTableInsertExisting(t *testing.T) {
107+ tab := newTable(initialSlots)
108+ s := tab.insert(5)
109+ require.Equal(t, s, tab.insert(5))
110+ require.Equal(t, 1, tab.n)
111+}
112+
113+func TestTableLookupAllocations(t *testing.T) {
114+ tab := newTable(initialSlots)
115+ for i := 0; i < 200; i++ {
116+ tab.insert(uint32(0x200000 + i))
117+ }
118+ var sink Slot
119+ n := testing.AllocsPerRun(1000, func() {
120+ sink += tab.lookup(0x200010)
121+ sink += tab.lookup(0x7fffffff) // absent
122+ _, r := tab.live(tab.tag(3))
123+ if r != nil {
124+ sink++
125+ }
126+ })
127+ require.Zero(t, n)
128+ require.NotZero(t, sink)
129+}
130+
131+func TestTableInsertRemoveAllocations(t *testing.T) {
132+ tab := newTable(initialSlots)
133+ n := testing.AllocsPerRun(1000, func() {
134+ s := tab.insert(0x200001)
135+ tab.remove(s)
136+ })
137+ require.Zero(t, n)
138+}
A internal/adapters/wayland/xwm/testdata/streams/property_unchanged.hex +950 -0
@@ -0,0 +1,950 @@
1+1c00740001005000f30300000000000000000000000000000000000000000000
2+0108750003000000ef0300000000000009000000000000000000000000000000
3+6e6574207469746c650000001c00750001005000f30300000000000000000000
4+0000000000000000000000000108760003000000ef0300000000000009000000
5+0000000000000000000000006e6574207469746c650000001c00760001005000
6+f303000000000000000000000000000000000000000000000108770003000000
7+ef03000000000000090000000000000000000000000000006e6574207469746c
8+650000001c00770001005000f303000000000000000000000000000000000000
9+000000000108780003000000ef03000000000000090000000000000000000000
10+000000006e6574207469746c650000001c00780001005000f303000000000000
11+000000000000000000000000000000000108790003000000ef03000000000000
12+090000000000000000000000000000006e6574207469746c650000001c007900
13+01005000f3030000000000000000000000000000000000000000000001087a00
14+03000000ef03000000000000090000000000000000000000000000006e657420
15+7469746c650000001c007a0001005000f3030000000000000000000000000000
16+000000000000000001087b0003000000ef030000000000000900000000000000
17+00000000000000006e6574207469746c650000001c007b0001005000f3030000
18+000000000000000000000000000000000000000001087c0003000000ef030000
19+00000000090000000000000000000000000000006e6574207469746c65000000
20+1c007c0001005000f30300000000000000000000000000000000000000000000
21+01087d0003000000ef0300000000000009000000000000000000000000000000
22+6e6574207469746c650000001c007d0001005000f30300000000000000000000
23+00000000000000000000000001087e0003000000ef0300000000000009000000
24+0000000000000000000000006e6574207469746c650000001c007e0001005000
25+f3030000000000000000000000000000000000000000000001087f0003000000
26+ef03000000000000090000000000000000000000000000006e6574207469746c
27+650000001c007f0001005000f303000000000000000000000000000000000000
28+000000000108800003000000ef03000000000000090000000000000000000000
29+000000006e6574207469746c650000001c00800001005000f303000000000000
30+000000000000000000000000000000000108810003000000ef03000000000000
31+090000000000000000000000000000006e6574207469746c650000001c008100
32+01005000f3030000000000000000000000000000000000000000000001088200
33+03000000ef03000000000000090000000000000000000000000000006e657420
34+7469746c650000001c00820001005000f3030000000000000000000000000000
35+00000000000000000108830003000000ef030000000000000900000000000000
36+00000000000000006e6574207469746c650000001c00830001005000f3030000
37+00000000000000000000000000000000000000000108840003000000ef030000
38+00000000090000000000000000000000000000006e6574207469746c65000000
39+1c00840001005000f30300000000000000000000000000000000000000000000
40+0108850003000000ef0300000000000009000000000000000000000000000000
41+6e6574207469746c650000001c00850001005000f30300000000000000000000
42+0000000000000000000000000108860003000000ef0300000000000009000000
43+0000000000000000000000006e6574207469746c650000001c00860001005000
44+f303000000000000000000000000000000000000000000000108870003000000
45+ef03000000000000090000000000000000000000000000006e6574207469746c
46+650000001c00870001005000f303000000000000000000000000000000000000
47+000000000108880003000000ef03000000000000090000000000000000000000
48+000000006e6574207469746c650000001c00880001005000f303000000000000
49+000000000000000000000000000000000108890003000000ef03000000000000
50+090000000000000000000000000000006e6574207469746c650000001c008900
51+01005000f3030000000000000000000000000000000000000000000001088a00
52+03000000ef03000000000000090000000000000000000000000000006e657420
53+7469746c650000001c008a0001005000f3030000000000000000000000000000
54+000000000000000001088b0003000000ef030000000000000900000000000000
55+00000000000000006e6574207469746c650000001c008b0001005000f3030000
56+000000000000000000000000000000000000000001088c0003000000ef030000
57+00000000090000000000000000000000000000006e6574207469746c65000000
58+1c008c0001005000f30300000000000000000000000000000000000000000000
59+01088d0003000000ef0300000000000009000000000000000000000000000000
60+6e6574207469746c650000001c008d0001005000f30300000000000000000000
61+00000000000000000000000001088e0003000000ef0300000000000009000000
62+0000000000000000000000006e6574207469746c650000001c008e0001005000
63+f3030000000000000000000000000000000000000000000001088f0003000000
64+ef03000000000000090000000000000000000000000000006e6574207469746c
65+650000001c008f0001005000f303000000000000000000000000000000000000
66+000000000108900003000000ef03000000000000090000000000000000000000
67+000000006e6574207469746c650000001c00900001005000f303000000000000
68+000000000000000000000000000000000108910003000000ef03000000000000
69+090000000000000000000000000000006e6574207469746c650000001c009100
70+01005000f3030000000000000000000000000000000000000000000001089200
71+03000000ef03000000000000090000000000000000000000000000006e657420
72+7469746c650000001c00920001005000f3030000000000000000000000000000
73+00000000000000000108930003000000ef030000000000000900000000000000
74+00000000000000006e6574207469746c650000001c00930001005000f3030000
75+00000000000000000000000000000000000000000108940003000000ef030000
76+00000000090000000000000000000000000000006e6574207469746c65000000
77+1c00940001005000f30300000000000000000000000000000000000000000000
78+0108950003000000ef0300000000000009000000000000000000000000000000
79+6e6574207469746c650000001c00950001005000f30300000000000000000000
80+0000000000000000000000000108960003000000ef0300000000000009000000
81+0000000000000000000000006e6574207469746c650000001c00960001005000
82+f303000000000000000000000000000000000000000000000108970003000000
83+ef03000000000000090000000000000000000000000000006e6574207469746c
84+650000001c00970001005000f303000000000000000000000000000000000000
85+000000000108980003000000ef03000000000000090000000000000000000000
86+000000006e6574207469746c650000001c00980001005000f303000000000000
87+000000000000000000000000000000000108990003000000ef03000000000000
88+090000000000000000000000000000006e6574207469746c650000001c009900
89+01005000f3030000000000000000000000000000000000000000000001089a00
90+03000000ef03000000000000090000000000000000000000000000006e657420
91+7469746c650000001c009a0001005000f3030000000000000000000000000000
92+000000000000000001089b0003000000ef030000000000000900000000000000
93+00000000000000006e6574207469746c650000001c009b0001005000f3030000
94+000000000000000000000000000000000000000001089c0003000000ef030000
95+00000000090000000000000000000000000000006e6574207469746c65000000
96+1c009c0001005000f30300000000000000000000000000000000000000000000
97+01089d0003000000ef0300000000000009000000000000000000000000000000
98+6e6574207469746c650000001c009d0001005000f30300000000000000000000
99+00000000000000000000000001089e0003000000ef0300000000000009000000
100+0000000000000000000000006e6574207469746c650000001c009e0001005000
101+f3030000000000000000000000000000000000000000000001089f0003000000
102+ef03000000000000090000000000000000000000000000006e6574207469746c
103+650000001c009f0001005000f303000000000000000000000000000000000000
104+000000000108a00003000000ef03000000000000090000000000000000000000
105+000000006e6574207469746c650000001c00a00001005000f303000000000000
106+000000000000000000000000000000000108a10003000000ef03000000000000
107+090000000000000000000000000000006e6574207469746c650000001c00a100
108+01005000f303000000000000000000000000000000000000000000000108a200
109+03000000ef03000000000000090000000000000000000000000000006e657420
110+7469746c650000001c00a20001005000f3030000000000000000000000000000
111+00000000000000000108a30003000000ef030000000000000900000000000000
112+00000000000000006e6574207469746c650000001c00a30001005000f3030000
113+00000000000000000000000000000000000000000108a40003000000ef030000
114+00000000090000000000000000000000000000006e6574207469746c65000000
115+1c00a40001005000f30300000000000000000000000000000000000000000000
116+0108a50003000000ef0300000000000009000000000000000000000000000000
117+6e6574207469746c650000001c00a50001005000f30300000000000000000000
118+0000000000000000000000000108a60003000000ef0300000000000009000000
119+0000000000000000000000006e6574207469746c650000001c00a60001005000
120+f303000000000000000000000000000000000000000000000108a70003000000
121+ef03000000000000090000000000000000000000000000006e6574207469746c
122+650000001c00a70001005000f303000000000000000000000000000000000000
123+000000000108a80003000000ef03000000000000090000000000000000000000
124+000000006e6574207469746c650000001c00a80001005000f303000000000000
125+000000000000000000000000000000000108a90003000000ef03000000000000
126+090000000000000000000000000000006e6574207469746c650000001c00a900
127+01005000f303000000000000000000000000000000000000000000000108aa00
128+03000000ef03000000000000090000000000000000000000000000006e657420
129+7469746c650000001c00aa0001005000f3030000000000000000000000000000
130+00000000000000000108ab0003000000ef030000000000000900000000000000
131+00000000000000006e6574207469746c650000001c00ab0001005000f3030000
132+00000000000000000000000000000000000000000108ac0003000000ef030000
133+00000000090000000000000000000000000000006e6574207469746c65000000
134+1c00ac0001005000f30300000000000000000000000000000000000000000000
135+0108ad0003000000ef0300000000000009000000000000000000000000000000
136+6e6574207469746c650000001c00ad0001005000f30300000000000000000000
137+0000000000000000000000000108ae0003000000ef0300000000000009000000
138+0000000000000000000000006e6574207469746c650000001c00ae0001005000
139+f303000000000000000000000000000000000000000000000108af0003000000
140+ef03000000000000090000000000000000000000000000006e6574207469746c
141+650000001c00af0001005000f303000000000000000000000000000000000000
142+000000000108b00003000000ef03000000000000090000000000000000000000
143+000000006e6574207469746c650000001c00b00001005000f303000000000000
144+000000000000000000000000000000000108b10003000000ef03000000000000
145+090000000000000000000000000000006e6574207469746c650000001c00b100
146+01005000f303000000000000000000000000000000000000000000000108b200
147+03000000ef03000000000000090000000000000000000000000000006e657420
148+7469746c650000001c00b20001005000f3030000000000000000000000000000
149+00000000000000000108b30003000000ef030000000000000900000000000000
150+00000000000000006e6574207469746c650000001c00b30001005000f3030000
151+00000000000000000000000000000000000000000108b40003000000ef030000
152+00000000090000000000000000000000000000006e6574207469746c65000000
153+1c00b40001005000f30300000000000000000000000000000000000000000000
154+0108b50003000000ef0300000000000009000000000000000000000000000000
155+6e6574207469746c650000001c00b50001005000f30300000000000000000000
156+0000000000000000000000000108b60003000000ef0300000000000009000000
157+0000000000000000000000006e6574207469746c650000001c00b60001005000
158+f303000000000000000000000000000000000000000000000108b70003000000
159+ef03000000000000090000000000000000000000000000006e6574207469746c
160+650000001c00b70001005000f303000000000000000000000000000000000000
161+000000000108b80003000000ef03000000000000090000000000000000000000
162+000000006e6574207469746c650000001c00b80001005000f303000000000000
163+000000000000000000000000000000000108b90003000000ef03000000000000
164+090000000000000000000000000000006e6574207469746c650000001c00b900
165+01005000f303000000000000000000000000000000000000000000000108ba00
166+03000000ef03000000000000090000000000000000000000000000006e657420
167+7469746c650000001c00ba0001005000f3030000000000000000000000000000
168+00000000000000000108bb0003000000ef030000000000000900000000000000
169+00000000000000006e6574207469746c650000001c00bb0001005000f3030000
170+00000000000000000000000000000000000000000108bc0003000000ef030000
171+00000000090000000000000000000000000000006e6574207469746c65000000
172+1c00bc0001005000f30300000000000000000000000000000000000000000000
173+0108bd0003000000ef0300000000000009000000000000000000000000000000
174+6e6574207469746c650000001c00bd0001005000f30300000000000000000000
175+0000000000000000000000000108be0003000000ef0300000000000009000000
176+0000000000000000000000006e6574207469746c650000001c00be0001005000
177+f303000000000000000000000000000000000000000000000108bf0003000000
178+ef03000000000000090000000000000000000000000000006e6574207469746c
179+650000001c00bf0001005000f303000000000000000000000000000000000000
180+000000000108c00003000000ef03000000000000090000000000000000000000
181+000000006e6574207469746c650000001c00c00001005000f303000000000000
182+000000000000000000000000000000000108c10003000000ef03000000000000
183+090000000000000000000000000000006e6574207469746c650000001c00c100
184+01005000f303000000000000000000000000000000000000000000000108c200
185+03000000ef03000000000000090000000000000000000000000000006e657420
186+7469746c650000001c00c20001005000f3030000000000000000000000000000
187+00000000000000000108c30003000000ef030000000000000900000000000000
188+00000000000000006e6574207469746c650000001c00c30001005000f3030000
189+00000000000000000000000000000000000000000108c40003000000ef030000
190+00000000090000000000000000000000000000006e6574207469746c65000000
191+1c00c40001005000f30300000000000000000000000000000000000000000000
192+0108c50003000000ef0300000000000009000000000000000000000000000000
193+6e6574207469746c650000001c00c50001005000f30300000000000000000000
194+0000000000000000000000000108c60003000000ef0300000000000009000000
195+0000000000000000000000006e6574207469746c650000001c00c60001005000
196+f303000000000000000000000000000000000000000000000108c70003000000
197+ef03000000000000090000000000000000000000000000006e6574207469746c
198+650000001c00c70001005000f303000000000000000000000000000000000000
199+000000000108c80003000000ef03000000000000090000000000000000000000
200+000000006e6574207469746c650000001c00c80001005000f303000000000000
201+000000000000000000000000000000000108c90003000000ef03000000000000
202+090000000000000000000000000000006e6574207469746c650000001c00c900
203+01005000f303000000000000000000000000000000000000000000000108ca00
204+03000000ef03000000000000090000000000000000000000000000006e657420
205+7469746c650000001c00ca0001005000f3030000000000000000000000000000
206+00000000000000000108cb0003000000ef030000000000000900000000000000
207+00000000000000006e6574207469746c650000001c00cb0001005000f3030000
208+00000000000000000000000000000000000000000108cc0003000000ef030000
209+00000000090000000000000000000000000000006e6574207469746c65000000
210+1c00cc0001005000f30300000000000000000000000000000000000000000000
211+0108cd0003000000ef0300000000000009000000000000000000000000000000
212+6e6574207469746c650000001c00cd0001005000f30300000000000000000000
213+0000000000000000000000000108ce0003000000ef0300000000000009000000
214+0000000000000000000000006e6574207469746c650000001c00ce0001005000
215+f303000000000000000000000000000000000000000000000108cf0003000000
216+ef03000000000000090000000000000000000000000000006e6574207469746c
217+650000001c00cf0001005000f303000000000000000000000000000000000000
218+000000000108d00003000000ef03000000000000090000000000000000000000
219+000000006e6574207469746c650000001c00d00001005000f303000000000000
220+000000000000000000000000000000000108d10003000000ef03000000000000
221+090000000000000000000000000000006e6574207469746c650000001c00d100
222+01005000f303000000000000000000000000000000000000000000000108d200
223+03000000ef03000000000000090000000000000000000000000000006e657420
224+7469746c650000001c00d20001005000f3030000000000000000000000000000
225+00000000000000000108d30003000000ef030000000000000900000000000000
226+00000000000000006e6574207469746c650000001c00d30001005000f3030000
227+00000000000000000000000000000000000000000108d40003000000ef030000
228+00000000090000000000000000000000000000006e6574207469746c65000000
229+1c00d40001005000f30300000000000000000000000000000000000000000000
230+0108d50003000000ef0300000000000009000000000000000000000000000000
231+6e6574207469746c650000001c00d50001005000f30300000000000000000000
232+0000000000000000000000000108d60003000000ef0300000000000009000000
233+0000000000000000000000006e6574207469746c650000001c00d60001005000
234+f303000000000000000000000000000000000000000000000108d70003000000
235+ef03000000000000090000000000000000000000000000006e6574207469746c
236+650000001c00d70001005000f303000000000000000000000000000000000000
237+000000000108d80003000000ef03000000000000090000000000000000000000
238+000000006e6574207469746c650000001c00d80001005000f303000000000000
239+000000000000000000000000000000000108d90003000000ef03000000000000
240+090000000000000000000000000000006e6574207469746c650000001c00d900
241+01005000f303000000000000000000000000000000000000000000000108da00
242+03000000ef03000000000000090000000000000000000000000000006e657420
243+7469746c650000001c00da0001005000f3030000000000000000000000000000
244+00000000000000000108db0003000000ef030000000000000900000000000000
245+00000000000000006e6574207469746c650000001c00db0001005000f3030000
246+00000000000000000000000000000000000000000108dc0003000000ef030000
247+00000000090000000000000000000000000000006e6574207469746c65000000
248+1c00dc0001005000f30300000000000000000000000000000000000000000000
249+0108dd0003000000ef0300000000000009000000000000000000000000000000
250+6e6574207469746c650000001c00dd0001005000f30300000000000000000000
251+0000000000000000000000000108de0003000000ef0300000000000009000000
252+0000000000000000000000006e6574207469746c650000001c00de0001005000
253+f303000000000000000000000000000000000000000000000108df0003000000
254+ef03000000000000090000000000000000000000000000006e6574207469746c
255+650000001c00df0001005000f303000000000000000000000000000000000000
256+000000000108e00003000000ef03000000000000090000000000000000000000
257+000000006e6574207469746c650000001c00e00001005000f303000000000000
258+000000000000000000000000000000000108e10003000000ef03000000000000
259+090000000000000000000000000000006e6574207469746c650000001c00e100
260+01005000f303000000000000000000000000000000000000000000000108e200
261+03000000ef03000000000000090000000000000000000000000000006e657420
262+7469746c650000001c00e20001005000f3030000000000000000000000000000
263+00000000000000000108e30003000000ef030000000000000900000000000000
264+00000000000000006e6574207469746c650000001c00e30001005000f3030000
265+00000000000000000000000000000000000000000108e40003000000ef030000
266+00000000090000000000000000000000000000006e6574207469746c65000000
267+1c00e40001005000f30300000000000000000000000000000000000000000000
268+0108e50003000000ef0300000000000009000000000000000000000000000000
269+6e6574207469746c650000001c00e50001005000f30300000000000000000000
270+0000000000000000000000000108e60003000000ef0300000000000009000000
271+0000000000000000000000006e6574207469746c650000001c00e60001005000
272+f303000000000000000000000000000000000000000000000108e70003000000
273+ef03000000000000090000000000000000000000000000006e6574207469746c
274+650000001c00e70001005000f303000000000000000000000000000000000000
275+000000000108e80003000000ef03000000000000090000000000000000000000
276+000000006e6574207469746c650000001c00e80001005000f303000000000000
277+000000000000000000000000000000000108e90003000000ef03000000000000
278+090000000000000000000000000000006e6574207469746c650000001c00e900
279+01005000f303000000000000000000000000000000000000000000000108ea00
280+03000000ef03000000000000090000000000000000000000000000006e657420
281+7469746c650000001c00ea0001005000f3030000000000000000000000000000
282+00000000000000000108eb0003000000ef030000000000000900000000000000
283+00000000000000006e6574207469746c650000001c00eb0001005000f3030000
284+00000000000000000000000000000000000000000108ec0003000000ef030000
285+00000000090000000000000000000000000000006e6574207469746c65000000
286+1c00ec0001005000f30300000000000000000000000000000000000000000000
287+0108ed0003000000ef0300000000000009000000000000000000000000000000
288+6e6574207469746c650000001c00ed0001005000f30300000000000000000000
289+0000000000000000000000000108ee0003000000ef0300000000000009000000
290+0000000000000000000000006e6574207469746c650000001c00ee0001005000
291+f303000000000000000000000000000000000000000000000108ef0003000000
292+ef03000000000000090000000000000000000000000000006e6574207469746c
293+650000001c00ef0001005000f303000000000000000000000000000000000000
294+000000000108f00003000000ef03000000000000090000000000000000000000
295+000000006e6574207469746c650000001c00f00001005000f303000000000000
296+000000000000000000000000000000000108f10003000000ef03000000000000
297+090000000000000000000000000000006e6574207469746c650000001c00f100
298+01005000f303000000000000000000000000000000000000000000000108f200
299+03000000ef03000000000000090000000000000000000000000000006e657420
300+7469746c650000001c00f20001005000f3030000000000000000000000000000
301+00000000000000000108f30003000000ef030000000000000900000000000000
302+00000000000000006e6574207469746c650000001c00f30001005000f3030000
303+00000000000000000000000000000000000000000108f40003000000ef030000
304+00000000090000000000000000000000000000006e6574207469746c65000000
305+1c00f40001005000f30300000000000000000000000000000000000000000000
306+0108f50003000000ef0300000000000009000000000000000000000000000000
307+6e6574207469746c650000001c00f50001005000f30300000000000000000000
308+0000000000000000000000000108f60003000000ef0300000000000009000000
309+0000000000000000000000006e6574207469746c650000001c00f60001005000
310+f303000000000000000000000000000000000000000000000108f70003000000
311+ef03000000000000090000000000000000000000000000006e6574207469746c
312+650000001c00f70001005000f303000000000000000000000000000000000000
313+000000000108f80003000000ef03000000000000090000000000000000000000
314+000000006e6574207469746c650000001c00f80001005000f303000000000000
315+000000000000000000000000000000000108f90003000000ef03000000000000
316+090000000000000000000000000000006e6574207469746c650000001c00f900
317+01005000f303000000000000000000000000000000000000000000000108fa00
318+03000000ef03000000000000090000000000000000000000000000006e657420
319+7469746c650000001c00fa0001005000f3030000000000000000000000000000
320+00000000000000000108fb0003000000ef030000000000000900000000000000
321+00000000000000006e6574207469746c650000001c00fb0001005000f3030000
322+00000000000000000000000000000000000000000108fc0003000000ef030000
323+00000000090000000000000000000000000000006e6574207469746c65000000
324+1c00fc0001005000f30300000000000000000000000000000000000000000000
325+0108fd0003000000ef0300000000000009000000000000000000000000000000
326+6e6574207469746c650000001c00fd0001005000f30300000000000000000000
327+0000000000000000000000000108fe0003000000ef0300000000000009000000
328+0000000000000000000000006e6574207469746c650000001c00fe0001005000
329+f303000000000000000000000000000000000000000000000108ff0003000000
330+ef03000000000000090000000000000000000000000000006e6574207469746c
331+650000001c00ff0001005000f303000000000000000000000000000000000000
332+000000000108000103000000ef03000000000000090000000000000000000000
333+000000006e6574207469746c650000001c00000101005000f303000000000000
334+000000000000000000000000000000000108010103000000ef03000000000000
335+090000000000000000000000000000006e6574207469746c650000001c000101
336+01005000f3030000000000000000000000000000000000000000000001080201
337+03000000ef03000000000000090000000000000000000000000000006e657420
338+7469746c650000001c00020101005000f3030000000000000000000000000000
339+00000000000000000108030103000000ef030000000000000900000000000000
340+00000000000000006e6574207469746c650000001c00030101005000f3030000
341+00000000000000000000000000000000000000000108040103000000ef030000
342+00000000090000000000000000000000000000006e6574207469746c65000000
343+1c00040101005000f30300000000000000000000000000000000000000000000
344+0108050103000000ef0300000000000009000000000000000000000000000000
345+6e6574207469746c650000001c00050101005000f30300000000000000000000
346+0000000000000000000000000108060103000000ef0300000000000009000000
347+0000000000000000000000006e6574207469746c650000001c00060101005000
348+f303000000000000000000000000000000000000000000000108070103000000
349+ef03000000000000090000000000000000000000000000006e6574207469746c
350+650000001c00070101005000f303000000000000000000000000000000000000
351+000000000108080103000000ef03000000000000090000000000000000000000
352+000000006e6574207469746c650000001c00080101005000f303000000000000
353+000000000000000000000000000000000108090103000000ef03000000000000
354+090000000000000000000000000000006e6574207469746c650000001c000901
355+01005000f3030000000000000000000000000000000000000000000001080a01
356+03000000ef03000000000000090000000000000000000000000000006e657420
357+7469746c650000001c000a0101005000f3030000000000000000000000000000
358+000000000000000001080b0103000000ef030000000000000900000000000000
359+00000000000000006e6574207469746c650000001c000b0101005000f3030000
360+000000000000000000000000000000000000000001080c0103000000ef030000
361+00000000090000000000000000000000000000006e6574207469746c65000000
362+1c000c0101005000f30300000000000000000000000000000000000000000000
363+01080d0103000000ef0300000000000009000000000000000000000000000000
364+6e6574207469746c650000001c000d0101005000f30300000000000000000000
365+00000000000000000000000001080e0103000000ef0300000000000009000000
366+0000000000000000000000006e6574207469746c650000001c000e0101005000
367+f3030000000000000000000000000000000000000000000001080f0103000000
368+ef03000000000000090000000000000000000000000000006e6574207469746c
369+650000001c000f0101005000f303000000000000000000000000000000000000
370+000000000108100103000000ef03000000000000090000000000000000000000
371+000000006e6574207469746c650000001c00100101005000f303000000000000
372+000000000000000000000000000000000108110103000000ef03000000000000
373+090000000000000000000000000000006e6574207469746c650000001c001101
374+01005000f3030000000000000000000000000000000000000000000001081201
375+03000000ef03000000000000090000000000000000000000000000006e657420
376+7469746c650000001c00120101005000f3030000000000000000000000000000
377+00000000000000000108130103000000ef030000000000000900000000000000
378+00000000000000006e6574207469746c650000001c00130101005000f3030000
379+00000000000000000000000000000000000000000108140103000000ef030000
380+00000000090000000000000000000000000000006e6574207469746c65000000
381+1c00140101005000f30300000000000000000000000000000000000000000000
382+0108150103000000ef0300000000000009000000000000000000000000000000
383+6e6574207469746c650000001c00150101005000f30300000000000000000000
384+0000000000000000000000000108160103000000ef0300000000000009000000
385+0000000000000000000000006e6574207469746c650000001c00160101005000
386+f303000000000000000000000000000000000000000000000108170103000000
387+ef03000000000000090000000000000000000000000000006e6574207469746c
388+650000001c00170101005000f303000000000000000000000000000000000000
389+000000000108180103000000ef03000000000000090000000000000000000000
390+000000006e6574207469746c650000001c00180101005000f303000000000000
391+000000000000000000000000000000000108190103000000ef03000000000000
392+090000000000000000000000000000006e6574207469746c650000001c001901
393+01005000f3030000000000000000000000000000000000000000000001081a01
394+03000000ef03000000000000090000000000000000000000000000006e657420
395+7469746c650000001c001a0101005000f3030000000000000000000000000000
396+000000000000000001081b0103000000ef030000000000000900000000000000
397+00000000000000006e6574207469746c650000001c001b0101005000f3030000
398+000000000000000000000000000000000000000001081c0103000000ef030000
399+00000000090000000000000000000000000000006e6574207469746c65000000
400+1c001c0101005000f30300000000000000000000000000000000000000000000
401+01081d0103000000ef0300000000000009000000000000000000000000000000
402+6e6574207469746c650000001c001d0101005000f30300000000000000000000
403+00000000000000000000000001081e0103000000ef0300000000000009000000
404+0000000000000000000000006e6574207469746c650000001c001e0101005000
405+f3030000000000000000000000000000000000000000000001081f0103000000
406+ef03000000000000090000000000000000000000000000006e6574207469746c
407+650000001c001f0101005000f303000000000000000000000000000000000000
408+000000000108200103000000ef03000000000000090000000000000000000000
409+000000006e6574207469746c650000001c00200101005000f303000000000000
410+000000000000000000000000000000000108210103000000ef03000000000000
411+090000000000000000000000000000006e6574207469746c650000001c002101
412+01005000f3030000000000000000000000000000000000000000000001082201
413+03000000ef03000000000000090000000000000000000000000000006e657420
414+7469746c650000001c00220101005000f3030000000000000000000000000000
415+00000000000000000108230103000000ef030000000000000900000000000000
416+00000000000000006e6574207469746c650000001c00230101005000f3030000
417+00000000000000000000000000000000000000000108240103000000ef030000
418+00000000090000000000000000000000000000006e6574207469746c65000000
419+1c00240101005000f30300000000000000000000000000000000000000000000
420+0108250103000000ef0300000000000009000000000000000000000000000000
421+6e6574207469746c650000001c00250101005000f30300000000000000000000
422+0000000000000000000000000108260103000000ef0300000000000009000000
423+0000000000000000000000006e6574207469746c650000001c00260101005000
424+f303000000000000000000000000000000000000000000000108270103000000
425+ef03000000000000090000000000000000000000000000006e6574207469746c
426+650000001c00270101005000f303000000000000000000000000000000000000
427+000000000108280103000000ef03000000000000090000000000000000000000
428+000000006e6574207469746c650000001c00280101005000f303000000000000
429+000000000000000000000000000000000108290103000000ef03000000000000
430+090000000000000000000000000000006e6574207469746c650000001c002901
431+01005000f3030000000000000000000000000000000000000000000001082a01
432+03000000ef03000000000000090000000000000000000000000000006e657420
433+7469746c650000001c002a0101005000f3030000000000000000000000000000
434+000000000000000001082b0103000000ef030000000000000900000000000000
435+00000000000000006e6574207469746c650000001c002b0101005000f3030000
436+000000000000000000000000000000000000000001082c0103000000ef030000
437+00000000090000000000000000000000000000006e6574207469746c65000000
438+1c002c0101005000f30300000000000000000000000000000000000000000000
439+01082d0103000000ef0300000000000009000000000000000000000000000000
440+6e6574207469746c650000001c002d0101005000f30300000000000000000000
441+00000000000000000000000001082e0103000000ef0300000000000009000000
442+0000000000000000000000006e6574207469746c650000001c002e0101005000
443+f3030000000000000000000000000000000000000000000001082f0103000000
444+ef03000000000000090000000000000000000000000000006e6574207469746c
445+650000001c002f0101005000f303000000000000000000000000000000000000
446+000000000108300103000000ef03000000000000090000000000000000000000
447+000000006e6574207469746c650000001c00300101005000f303000000000000
448+000000000000000000000000000000000108310103000000ef03000000000000
449+090000000000000000000000000000006e6574207469746c650000001c003101
450+01005000f3030000000000000000000000000000000000000000000001083201
451+03000000ef03000000000000090000000000000000000000000000006e657420
452+7469746c650000001c00320101005000f3030000000000000000000000000000
453+00000000000000000108330103000000ef030000000000000900000000000000
454+00000000000000006e6574207469746c650000001c00330101005000f3030000
455+00000000000000000000000000000000000000000108340103000000ef030000
456+00000000090000000000000000000000000000006e6574207469746c65000000
457+1c00340101005000f30300000000000000000000000000000000000000000000
458+0108350103000000ef0300000000000009000000000000000000000000000000
459+6e6574207469746c650000001c00350101005000f30300000000000000000000
460+0000000000000000000000000108360103000000ef0300000000000009000000
461+0000000000000000000000006e6574207469746c650000001c00360101005000
462+f303000000000000000000000000000000000000000000000108370103000000
463+ef03000000000000090000000000000000000000000000006e6574207469746c
464+650000001c00370101005000f303000000000000000000000000000000000000
465+000000000108380103000000ef03000000000000090000000000000000000000
466+000000006e6574207469746c650000001c00380101005000f303000000000000
467+000000000000000000000000000000000108390103000000ef03000000000000
468+090000000000000000000000000000006e6574207469746c650000001c003901
469+01005000f3030000000000000000000000000000000000000000000001083a01
470+03000000ef03000000000000090000000000000000000000000000006e657420
471+7469746c650000001c003a0101005000f3030000000000000000000000000000
472+000000000000000001083b0103000000ef030000000000000900000000000000
473+00000000000000006e6574207469746c650000001c003b0101005000f3030000
474+000000000000000000000000000000000000000001083c0103000000ef030000
475+00000000090000000000000000000000000000006e6574207469746c65000000
476+1c003c0101005000f30300000000000000000000000000000000000000000000
477+01083d0103000000ef0300000000000009000000000000000000000000000000
478+6e6574207469746c650000001c003d0101005000f30300000000000000000000
479+00000000000000000000000001083e0103000000ef0300000000000009000000
480+0000000000000000000000006e6574207469746c650000001c003e0101005000
481+f3030000000000000000000000000000000000000000000001083f0103000000
482+ef03000000000000090000000000000000000000000000006e6574207469746c
483+650000001c003f0101005000f303000000000000000000000000000000000000
484+000000000108400103000000ef03000000000000090000000000000000000000
485+000000006e6574207469746c650000001c00400101005000f303000000000000
486+000000000000000000000000000000000108410103000000ef03000000000000
487+090000000000000000000000000000006e6574207469746c650000001c004101
488+01005000f3030000000000000000000000000000000000000000000001084201
489+03000000ef03000000000000090000000000000000000000000000006e657420
490+7469746c650000001c00420101005000f3030000000000000000000000000000
491+00000000000000000108430103000000ef030000000000000900000000000000
492+00000000000000006e6574207469746c650000001c00430101005000f3030000
493+00000000000000000000000000000000000000000108440103000000ef030000
494+00000000090000000000000000000000000000006e6574207469746c65000000
495+1c00440101005000f30300000000000000000000000000000000000000000000
496+0108450103000000ef0300000000000009000000000000000000000000000000
497+6e6574207469746c650000001c00450101005000f30300000000000000000000
498+0000000000000000000000000108460103000000ef0300000000000009000000
499+0000000000000000000000006e6574207469746c650000001c00460101005000
500+f303000000000000000000000000000000000000000000000108470103000000
501+ef03000000000000090000000000000000000000000000006e6574207469746c
502+650000001c00470101005000f303000000000000000000000000000000000000
503+000000000108480103000000ef03000000000000090000000000000000000000
504+000000006e6574207469746c650000001c00480101005000f303000000000000
505+000000000000000000000000000000000108490103000000ef03000000000000
506+090000000000000000000000000000006e6574207469746c650000001c004901
507+01005000f3030000000000000000000000000000000000000000000001084a01
508+03000000ef03000000000000090000000000000000000000000000006e657420
509+7469746c650000001c004a0101005000f3030000000000000000000000000000
510+000000000000000001084b0103000000ef030000000000000900000000000000
511+00000000000000006e6574207469746c650000001c004b0101005000f3030000
512+000000000000000000000000000000000000000001084c0103000000ef030000
513+00000000090000000000000000000000000000006e6574207469746c65000000
514+1c004c0101005000f30300000000000000000000000000000000000000000000
515+01084d0103000000ef0300000000000009000000000000000000000000000000
516+6e6574207469746c650000001c004d0101005000f30300000000000000000000
517+00000000000000000000000001084e0103000000ef0300000000000009000000
518+0000000000000000000000006e6574207469746c650000001c004e0101005000
519+f3030000000000000000000000000000000000000000000001084f0103000000
520+ef03000000000000090000000000000000000000000000006e6574207469746c
521+650000001c004f0101005000f303000000000000000000000000000000000000
522+000000000108500103000000ef03000000000000090000000000000000000000
523+000000006e6574207469746c650000001c00500101005000f303000000000000
524+000000000000000000000000000000000108510103000000ef03000000000000
525+090000000000000000000000000000006e6574207469746c650000001c005101
526+01005000f3030000000000000000000000000000000000000000000001085201
527+03000000ef03000000000000090000000000000000000000000000006e657420
528+7469746c650000001c00520101005000f3030000000000000000000000000000
529+00000000000000000108530103000000ef030000000000000900000000000000
530+00000000000000006e6574207469746c650000001c00530101005000f3030000
531+00000000000000000000000000000000000000000108540103000000ef030000
532+00000000090000000000000000000000000000006e6574207469746c65000000
533+1c00540101005000f30300000000000000000000000000000000000000000000
534+0108550103000000ef0300000000000009000000000000000000000000000000
535+6e6574207469746c650000001c00550101005000f30300000000000000000000
536+0000000000000000000000000108560103000000ef0300000000000009000000
537+0000000000000000000000006e6574207469746c650000001c00560101005000
538+f303000000000000000000000000000000000000000000000108570103000000
539+ef03000000000000090000000000000000000000000000006e6574207469746c
540+650000001c00570101005000f303000000000000000000000000000000000000
541+000000000108580103000000ef03000000000000090000000000000000000000
542+000000006e6574207469746c650000001c00580101005000f303000000000000
543+000000000000000000000000000000000108590103000000ef03000000000000
544+090000000000000000000000000000006e6574207469746c650000001c005901
545+01005000f3030000000000000000000000000000000000000000000001085a01
546+03000000ef03000000000000090000000000000000000000000000006e657420
547+7469746c650000001c005a0101005000f3030000000000000000000000000000
548+000000000000000001085b0103000000ef030000000000000900000000000000
549+00000000000000006e6574207469746c650000001c005b0101005000f3030000
550+000000000000000000000000000000000000000001085c0103000000ef030000
551+00000000090000000000000000000000000000006e6574207469746c65000000
552+1c005c0101005000f30300000000000000000000000000000000000000000000
553+01085d0103000000ef0300000000000009000000000000000000000000000000
554+6e6574207469746c650000001c005d0101005000f30300000000000000000000
555+00000000000000000000000001085e0103000000ef0300000000000009000000
556+0000000000000000000000006e6574207469746c650000001c005e0101005000
557+f3030000000000000000000000000000000000000000000001085f0103000000
558+ef03000000000000090000000000000000000000000000006e6574207469746c
559+650000001c005f0101005000f303000000000000000000000000000000000000
560+000000000108600103000000ef03000000000000090000000000000000000000
561+000000006e6574207469746c650000001c00600101005000f303000000000000
562+000000000000000000000000000000000108610103000000ef03000000000000
563+090000000000000000000000000000006e6574207469746c650000001c006101
564+01005000f3030000000000000000000000000000000000000000000001086201
565+03000000ef03000000000000090000000000000000000000000000006e657420
566+7469746c650000001c00620101005000f3030000000000000000000000000000
567+00000000000000000108630103000000ef030000000000000900000000000000
568+00000000000000006e6574207469746c650000001c00630101005000f3030000
569+00000000000000000000000000000000000000000108640103000000ef030000
570+00000000090000000000000000000000000000006e6574207469746c65000000
571+1c00640101005000f30300000000000000000000000000000000000000000000
572+0108650103000000ef0300000000000009000000000000000000000000000000
573+6e6574207469746c650000001c00650101005000f30300000000000000000000
574+0000000000000000000000000108660103000000ef0300000000000009000000
575+0000000000000000000000006e6574207469746c650000001c00660101005000
576+f303000000000000000000000000000000000000000000000108670103000000
577+ef03000000000000090000000000000000000000000000006e6574207469746c
578+650000001c00670101005000f303000000000000000000000000000000000000
579+000000000108680103000000ef03000000000000090000000000000000000000
580+000000006e6574207469746c650000001c00680101005000f303000000000000
581+000000000000000000000000000000000108690103000000ef03000000000000
582+090000000000000000000000000000006e6574207469746c650000001c006901
583+01005000f3030000000000000000000000000000000000000000000001086a01
584+03000000ef03000000000000090000000000000000000000000000006e657420
585+7469746c650000001c006a0101005000f3030000000000000000000000000000
586+000000000000000001086b0103000000ef030000000000000900000000000000
587+00000000000000006e6574207469746c650000001c006b0101005000f3030000
588+000000000000000000000000000000000000000001086c0103000000ef030000
589+00000000090000000000000000000000000000006e6574207469746c65000000
590+1c006c0101005000f30300000000000000000000000000000000000000000000
591+01086d0103000000ef0300000000000009000000000000000000000000000000
592+6e6574207469746c650000001c006d0101005000f30300000000000000000000
593+00000000000000000000000001086e0103000000ef0300000000000009000000
594+0000000000000000000000006e6574207469746c650000001c006e0101005000
595+f3030000000000000000000000000000000000000000000001086f0103000000
596+ef03000000000000090000000000000000000000000000006e6574207469746c
597+650000001c006f0101005000f303000000000000000000000000000000000000
598+000000000108700103000000ef03000000000000090000000000000000000000
599+000000006e6574207469746c650000001c00700101005000f303000000000000
600+000000000000000000000000000000000108710103000000ef03000000000000
601+090000000000000000000000000000006e6574207469746c650000001c007101
602+01005000f3030000000000000000000000000000000000000000000001087201
603+03000000ef03000000000000090000000000000000000000000000006e657420
604+7469746c650000001c00720101005000f3030000000000000000000000000000
605+00000000000000000108730103000000ef030000000000000900000000000000
606+00000000000000006e6574207469746c650000001c00730101005000f3030000
607+00000000000000000000000000000000000000000108740103000000ef030000
608+00000000090000000000000000000000000000006e6574207469746c65000000
609+1c00740101005000f30300000000000000000000000000000000000000000000
610+0108750103000000ef0300000000000009000000000000000000000000000000
611+6e6574207469746c650000001c00750101005000f30300000000000000000000
612+0000000000000000000000000108760103000000ef0300000000000009000000
613+0000000000000000000000006e6574207469746c650000001c00760101005000
614+f303000000000000000000000000000000000000000000000108770103000000
615+ef03000000000000090000000000000000000000000000006e6574207469746c
616+650000001c00770101005000f303000000000000000000000000000000000000
617+000000000108780103000000ef03000000000000090000000000000000000000
618+000000006e6574207469746c650000001c00780101005000f303000000000000
619+000000000000000000000000000000000108790103000000ef03000000000000
620+090000000000000000000000000000006e6574207469746c650000001c007901
621+01005000f3030000000000000000000000000000000000000000000001087a01
622+03000000ef03000000000000090000000000000000000000000000006e657420
623+7469746c650000001c007a0101005000f3030000000000000000000000000000
624+000000000000000001087b0103000000ef030000000000000900000000000000
625+00000000000000006e6574207469746c650000001c007b0101005000f3030000
626+000000000000000000000000000000000000000001087c0103000000ef030000
627+00000000090000000000000000000000000000006e6574207469746c65000000
628+1c007c0101005000f30300000000000000000000000000000000000000000000
629+01087d0103000000ef0300000000000009000000000000000000000000000000
630+6e6574207469746c650000001c007d0101005000f30300000000000000000000
631+00000000000000000000000001087e0103000000ef0300000000000009000000
632+0000000000000000000000006e6574207469746c650000001c007e0101005000
633+f3030000000000000000000000000000000000000000000001087f0103000000
634+ef03000000000000090000000000000000000000000000006e6574207469746c
635+650000001c007f0101005000f303000000000000000000000000000000000000
636+000000000108800103000000ef03000000000000090000000000000000000000
637+000000006e6574207469746c650000001c00800101005000f303000000000000
638+000000000000000000000000000000000108810103000000ef03000000000000
639+090000000000000000000000000000006e6574207469746c650000001c008101
640+01005000f3030000000000000000000000000000000000000000000001088201
641+03000000ef03000000000000090000000000000000000000000000006e657420
642+7469746c650000001c00820101005000f3030000000000000000000000000000
643+00000000000000000108830103000000ef030000000000000900000000000000
644+00000000000000006e6574207469746c650000001c00830101005000f3030000
645+00000000000000000000000000000000000000000108840103000000ef030000
646+00000000090000000000000000000000000000006e6574207469746c65000000
647+1c00840101005000f30300000000000000000000000000000000000000000000
648+0108850103000000ef0300000000000009000000000000000000000000000000
649+6e6574207469746c650000001c00850101005000f30300000000000000000000
650+0000000000000000000000000108860103000000ef0300000000000009000000
651+0000000000000000000000006e6574207469746c650000001c00860101005000
652+f303000000000000000000000000000000000000000000000108870103000000
653+ef03000000000000090000000000000000000000000000006e6574207469746c
654+650000001c00870101005000f303000000000000000000000000000000000000
655+000000000108880103000000ef03000000000000090000000000000000000000
656+000000006e6574207469746c650000001c00880101005000f303000000000000
657+000000000000000000000000000000000108890103000000ef03000000000000
658+090000000000000000000000000000006e6574207469746c650000001c008901
659+01005000f3030000000000000000000000000000000000000000000001088a01
660+03000000ef03000000000000090000000000000000000000000000006e657420
661+7469746c650000001c008a0101005000f3030000000000000000000000000000
662+000000000000000001088b0103000000ef030000000000000900000000000000
663+00000000000000006e6574207469746c650000001c008b0101005000f3030000
664+000000000000000000000000000000000000000001088c0103000000ef030000
665+00000000090000000000000000000000000000006e6574207469746c65000000
666+1c008c0101005000f30300000000000000000000000000000000000000000000
667+01088d0103000000ef0300000000000009000000000000000000000000000000
668+6e6574207469746c650000001c008d0101005000f30300000000000000000000
669+00000000000000000000000001088e0103000000ef0300000000000009000000
670+0000000000000000000000006e6574207469746c650000001c008e0101005000
671+f3030000000000000000000000000000000000000000000001088f0103000000
672+ef03000000000000090000000000000000000000000000006e6574207469746c
673+650000001c008f0101005000f303000000000000000000000000000000000000
674+000000000108900103000000ef03000000000000090000000000000000000000
675+000000006e6574207469746c650000001c00900101005000f303000000000000
676+000000000000000000000000000000000108910103000000ef03000000000000
677+090000000000000000000000000000006e6574207469746c650000001c009101
678+01005000f3030000000000000000000000000000000000000000000001089201
679+03000000ef03000000000000090000000000000000000000000000006e657420
680+7469746c650000001c00920101005000f3030000000000000000000000000000
681+00000000000000000108930103000000ef030000000000000900000000000000
682+00000000000000006e6574207469746c650000001c00930101005000f3030000
683+00000000000000000000000000000000000000000108940103000000ef030000
684+00000000090000000000000000000000000000006e6574207469746c65000000
685+1c00940101005000f30300000000000000000000000000000000000000000000
686+0108950103000000ef0300000000000009000000000000000000000000000000
687+6e6574207469746c650000001c00950101005000f30300000000000000000000
688+0000000000000000000000000108960103000000ef0300000000000009000000
689+0000000000000000000000006e6574207469746c650000001c00960101005000
690+f303000000000000000000000000000000000000000000000108970103000000
691+ef03000000000000090000000000000000000000000000006e6574207469746c
692+650000001c00970101005000f303000000000000000000000000000000000000
693+000000000108980103000000ef03000000000000090000000000000000000000
694+000000006e6574207469746c650000001c00980101005000f303000000000000
695+000000000000000000000000000000000108990103000000ef03000000000000
696+090000000000000000000000000000006e6574207469746c650000001c009901
697+01005000f3030000000000000000000000000000000000000000000001089a01
698+03000000ef03000000000000090000000000000000000000000000006e657420
699+7469746c650000001c009a0101005000f3030000000000000000000000000000
700+000000000000000001089b0103000000ef030000000000000900000000000000
701+00000000000000006e6574207469746c650000001c009b0101005000f3030000
702+000000000000000000000000000000000000000001089c0103000000ef030000
703+00000000090000000000000000000000000000006e6574207469746c65000000
704+1c009c0101005000f30300000000000000000000000000000000000000000000
705+01089d0103000000ef0300000000000009000000000000000000000000000000
706+6e6574207469746c650000001c009d0101005000f30300000000000000000000
707+00000000000000000000000001089e0103000000ef0300000000000009000000
708+0000000000000000000000006e6574207469746c650000001c009e0101005000
709+f3030000000000000000000000000000000000000000000001089f0103000000
710+ef03000000000000090000000000000000000000000000006e6574207469746c
711+650000001c009f0101005000f303000000000000000000000000000000000000
712+000000000108a00103000000ef03000000000000090000000000000000000000
713+000000006e6574207469746c650000001c00a00101005000f303000000000000
714+000000000000000000000000000000000108a10103000000ef03000000000000
715+090000000000000000000000000000006e6574207469746c650000001c00a101
716+01005000f303000000000000000000000000000000000000000000000108a201
717+03000000ef03000000000000090000000000000000000000000000006e657420
718+7469746c650000001c00a20101005000f3030000000000000000000000000000
719+00000000000000000108a30103000000ef030000000000000900000000000000
720+00000000000000006e6574207469746c650000001c00a30101005000f3030000
721+00000000000000000000000000000000000000000108a40103000000ef030000
722+00000000090000000000000000000000000000006e6574207469746c65000000
723+1c00a40101005000f30300000000000000000000000000000000000000000000
724+0108a50103000000ef0300000000000009000000000000000000000000000000
725+6e6574207469746c650000001c00a50101005000f30300000000000000000000
726+0000000000000000000000000108a60103000000ef0300000000000009000000
727+0000000000000000000000006e6574207469746c650000001c00a60101005000
728+f303000000000000000000000000000000000000000000000108a70103000000
729+ef03000000000000090000000000000000000000000000006e6574207469746c
730+650000001c00a70101005000f303000000000000000000000000000000000000
731+000000000108a80103000000ef03000000000000090000000000000000000000
732+000000006e6574207469746c650000001c00a80101005000f303000000000000
733+000000000000000000000000000000000108a90103000000ef03000000000000
734+090000000000000000000000000000006e6574207469746c650000001c00a901
735+01005000f303000000000000000000000000000000000000000000000108aa01
736+03000000ef03000000000000090000000000000000000000000000006e657420
737+7469746c650000001c00aa0101005000f3030000000000000000000000000000
738+00000000000000000108ab0103000000ef030000000000000900000000000000
739+00000000000000006e6574207469746c650000001c00ab0101005000f3030000
740+00000000000000000000000000000000000000000108ac0103000000ef030000
741+00000000090000000000000000000000000000006e6574207469746c65000000
742+1c00ac0101005000f30300000000000000000000000000000000000000000000
743+0108ad0103000000ef0300000000000009000000000000000000000000000000
744+6e6574207469746c650000001c00ad0101005000f30300000000000000000000
745+0000000000000000000000000108ae0103000000ef0300000000000009000000
746+0000000000000000000000006e6574207469746c650000001c00ae0101005000
747+f303000000000000000000000000000000000000000000000108af0103000000
748+ef03000000000000090000000000000000000000000000006e6574207469746c
749+650000001c00af0101005000f303000000000000000000000000000000000000
750+000000000108b00103000000ef03000000000000090000000000000000000000
751+000000006e6574207469746c650000001c00b00101005000f303000000000000
752+000000000000000000000000000000000108b10103000000ef03000000000000
753+090000000000000000000000000000006e6574207469746c650000001c00b101
754+01005000f303000000000000000000000000000000000000000000000108b201
755+03000000ef03000000000000090000000000000000000000000000006e657420
756+7469746c650000001c00b20101005000f3030000000000000000000000000000
757+00000000000000000108b30103000000ef030000000000000900000000000000
758+00000000000000006e6574207469746c650000001c00b30101005000f3030000
759+00000000000000000000000000000000000000000108b40103000000ef030000
760+00000000090000000000000000000000000000006e6574207469746c65000000
761+1c00b40101005000f30300000000000000000000000000000000000000000000
762+0108b50103000000ef0300000000000009000000000000000000000000000000
763+6e6574207469746c650000001c00b50101005000f30300000000000000000000
764+0000000000000000000000000108b60103000000ef0300000000000009000000
765+0000000000000000000000006e6574207469746c650000001c00b60101005000
766+f303000000000000000000000000000000000000000000000108b70103000000
767+ef03000000000000090000000000000000000000000000006e6574207469746c
768+650000001c00b70101005000f303000000000000000000000000000000000000
769+000000000108b80103000000ef03000000000000090000000000000000000000
770+000000006e6574207469746c650000001c00b80101005000f303000000000000
771+000000000000000000000000000000000108b90103000000ef03000000000000
772+090000000000000000000000000000006e6574207469746c650000001c00b901
773+01005000f303000000000000000000000000000000000000000000000108ba01
774+03000000ef03000000000000090000000000000000000000000000006e657420
775+7469746c650000001c00ba0101005000f3030000000000000000000000000000
776+00000000000000000108bb0103000000ef030000000000000900000000000000
777+00000000000000006e6574207469746c650000001c00bb0101005000f3030000
778+00000000000000000000000000000000000000000108bc0103000000ef030000
779+00000000090000000000000000000000000000006e6574207469746c65000000
780+1c00bc0101005000f30300000000000000000000000000000000000000000000
781+0108bd0103000000ef0300000000000009000000000000000000000000000000
782+6e6574207469746c650000001c00bd0101005000f30300000000000000000000
783+0000000000000000000000000108be0103000000ef0300000000000009000000
784+0000000000000000000000006e6574207469746c650000001c00be0101005000
785+f303000000000000000000000000000000000000000000000108bf0103000000
786+ef03000000000000090000000000000000000000000000006e6574207469746c
787+650000001c00bf0101005000f303000000000000000000000000000000000000
788+000000000108c00103000000ef03000000000000090000000000000000000000
789+000000006e6574207469746c650000001c00c00101005000f303000000000000
790+000000000000000000000000000000000108c10103000000ef03000000000000
791+090000000000000000000000000000006e6574207469746c650000001c00c101
792+01005000f303000000000000000000000000000000000000000000000108c201
793+03000000ef03000000000000090000000000000000000000000000006e657420
794+7469746c650000001c00c20101005000f3030000000000000000000000000000
795+00000000000000000108c30103000000ef030000000000000900000000000000
796+00000000000000006e6574207469746c650000001c00c30101005000f3030000
797+00000000000000000000000000000000000000000108c40103000000ef030000
798+00000000090000000000000000000000000000006e6574207469746c65000000
799+1c00c40101005000f30300000000000000000000000000000000000000000000
800+0108c50103000000ef0300000000000009000000000000000000000000000000
801+6e6574207469746c650000001c00c50101005000f30300000000000000000000
802+0000000000000000000000000108c60103000000ef0300000000000009000000
803+0000000000000000000000006e6574207469746c650000001c00c60101005000
804+f303000000000000000000000000000000000000000000000108c70103000000
805+ef03000000000000090000000000000000000000000000006e6574207469746c
806+650000001c00c70101005000f303000000000000000000000000000000000000
807+000000000108c80103000000ef03000000000000090000000000000000000000
808+000000006e6574207469746c650000001c00c80101005000f303000000000000
809+000000000000000000000000000000000108c90103000000ef03000000000000
810+090000000000000000000000000000006e6574207469746c650000001c00c901
811+01005000f303000000000000000000000000000000000000000000000108ca01
812+03000000ef03000000000000090000000000000000000000000000006e657420
813+7469746c650000001c00ca0101005000f3030000000000000000000000000000
814+00000000000000000108cb0103000000ef030000000000000900000000000000
815+00000000000000006e6574207469746c650000001c00cb0101005000f3030000
816+00000000000000000000000000000000000000000108cc0103000000ef030000
817+00000000090000000000000000000000000000006e6574207469746c65000000
818+1c00cc0101005000f30300000000000000000000000000000000000000000000
819+0108cd0103000000ef0300000000000009000000000000000000000000000000
820+6e6574207469746c650000001c00cd0101005000f30300000000000000000000
821+0000000000000000000000000108ce0103000000ef0300000000000009000000
822+0000000000000000000000006e6574207469746c650000001c00ce0101005000
823+f303000000000000000000000000000000000000000000000108cf0103000000
824+ef03000000000000090000000000000000000000000000006e6574207469746c
825+650000001c00cf0101005000f303000000000000000000000000000000000000
826+000000000108d00103000000ef03000000000000090000000000000000000000
827+000000006e6574207469746c650000001c00d00101005000f303000000000000
828+000000000000000000000000000000000108d10103000000ef03000000000000
829+090000000000000000000000000000006e6574207469746c650000001c00d101
830+01005000f303000000000000000000000000000000000000000000000108d201
831+03000000ef03000000000000090000000000000000000000000000006e657420
832+7469746c650000001c00d20101005000f3030000000000000000000000000000
833+00000000000000000108d30103000000ef030000000000000900000000000000
834+00000000000000006e6574207469746c650000001c00d30101005000f3030000
835+00000000000000000000000000000000000000000108d40103000000ef030000
836+00000000090000000000000000000000000000006e6574207469746c65000000
837+1c00d40101005000f30300000000000000000000000000000000000000000000
838+0108d50103000000ef0300000000000009000000000000000000000000000000
839+6e6574207469746c650000001c00d50101005000f30300000000000000000000
840+0000000000000000000000000108d60103000000ef0300000000000009000000
841+0000000000000000000000006e6574207469746c650000001c00d60101005000
842+f303000000000000000000000000000000000000000000000108d70103000000
843+ef03000000000000090000000000000000000000000000006e6574207469746c
844+650000001c00d70101005000f303000000000000000000000000000000000000
845+000000000108d80103000000ef03000000000000090000000000000000000000
846+000000006e6574207469746c650000001c00d80101005000f303000000000000
847+000000000000000000000000000000000108d90103000000ef03000000000000
848+090000000000000000000000000000006e6574207469746c650000001c00d901
849+01005000f303000000000000000000000000000000000000000000000108da01
850+03000000ef03000000000000090000000000000000000000000000006e657420
851+7469746c650000001c00da0101005000f3030000000000000000000000000000
852+00000000000000000108db0103000000ef030000000000000900000000000000
853+00000000000000006e6574207469746c650000001c00db0101005000f3030000
854+00000000000000000000000000000000000000000108dc0103000000ef030000
855+00000000090000000000000000000000000000006e6574207469746c65000000
856+1c00dc0101005000f30300000000000000000000000000000000000000000000
857+0108dd0103000000ef0300000000000009000000000000000000000000000000
858+6e6574207469746c650000001c00dd0101005000f30300000000000000000000
859+0000000000000000000000000108de0103000000ef0300000000000009000000
860+0000000000000000000000006e6574207469746c650000001c00de0101005000
861+f303000000000000000000000000000000000000000000000108df0103000000
862+ef03000000000000090000000000000000000000000000006e6574207469746c
863+650000001c00df0101005000f303000000000000000000000000000000000000
864+000000000108e00103000000ef03000000000000090000000000000000000000
865+000000006e6574207469746c650000001c00e00101005000f303000000000000
866+000000000000000000000000000000000108e10103000000ef03000000000000
867+090000000000000000000000000000006e6574207469746c650000001c00e101
868+01005000f303000000000000000000000000000000000000000000000108e201
869+03000000ef03000000000000090000000000000000000000000000006e657420
870+7469746c650000001c00e20101005000f3030000000000000000000000000000
871+00000000000000000108e30103000000ef030000000000000900000000000000
872+00000000000000006e6574207469746c650000001c00e30101005000f3030000
873+00000000000000000000000000000000000000000108e40103000000ef030000
874+00000000090000000000000000000000000000006e6574207469746c65000000
875+1c00e40101005000f30300000000000000000000000000000000000000000000
876+0108e50103000000ef0300000000000009000000000000000000000000000000
877+6e6574207469746c650000001c00e50101005000f30300000000000000000000
878+0000000000000000000000000108e60103000000ef0300000000000009000000
879+0000000000000000000000006e6574207469746c650000001c00e60101005000
880+f303000000000000000000000000000000000000000000000108e70103000000
881+ef03000000000000090000000000000000000000000000006e6574207469746c
882+650000001c00e70101005000f303000000000000000000000000000000000000
883+000000000108e80103000000ef03000000000000090000000000000000000000
884+000000006e6574207469746c650000001c00e80101005000f303000000000000
885+000000000000000000000000000000000108e90103000000ef03000000000000
886+090000000000000000000000000000006e6574207469746c650000001c00e901
887+01005000f303000000000000000000000000000000000000000000000108ea01
888+03000000ef03000000000000090000000000000000000000000000006e657420
889+7469746c650000001c00ea0101005000f3030000000000000000000000000000
890+00000000000000000108eb0103000000ef030000000000000900000000000000
891+00000000000000006e6574207469746c650000001c00eb0101005000f3030000
892+00000000000000000000000000000000000000000108ec0103000000ef030000
893+00000000090000000000000000000000000000006e6574207469746c65000000
894+1c00ec0101005000f30300000000000000000000000000000000000000000000
895+0108ed0103000000ef0300000000000009000000000000000000000000000000
896+6e6574207469746c650000001c00ed0101005000f30300000000000000000000
897+0000000000000000000000000108ee0103000000ef0300000000000009000000
898+0000000000000000000000006e6574207469746c650000001c00ee0101005000
899+f303000000000000000000000000000000000000000000000108ef0103000000
900+ef03000000000000090000000000000000000000000000006e6574207469746c
901+650000001c00ef0101005000f303000000000000000000000000000000000000
902+000000000108f00103000000ef03000000000000090000000000000000000000
903+000000006e6574207469746c650000001c00f00101005000f303000000000000
904+000000000000000000000000000000000108f10103000000ef03000000000000
905+090000000000000000000000000000006e6574207469746c650000001c00f101
906+01005000f303000000000000000000000000000000000000000000000108f201
907+03000000ef03000000000000090000000000000000000000000000006e657420
908+7469746c650000001c00f20101005000f3030000000000000000000000000000
909+00000000000000000108f30103000000ef030000000000000900000000000000
910+00000000000000006e6574207469746c650000001c00f30101005000f3030000
911+00000000000000000000000000000000000000000108f40103000000ef030000
912+00000000090000000000000000000000000000006e6574207469746c65000000
913+1c00f40101005000f30300000000000000000000000000000000000000000000
914+0108f50103000000ef0300000000000009000000000000000000000000000000
915+6e6574207469746c650000001c00f50101005000f30300000000000000000000
916+0000000000000000000000000108f60103000000ef0300000000000009000000
917+0000000000000000000000006e6574207469746c650000001c00f60101005000
918+f303000000000000000000000000000000000000000000000108f70103000000
919+ef03000000000000090000000000000000000000000000006e6574207469746c
920+650000001c00f70101005000f303000000000000000000000000000000000000
921+000000000108f80103000000ef03000000000000090000000000000000000000
922+000000006e6574207469746c650000001c00f80101005000f303000000000000
923+000000000000000000000000000000000108f90103000000ef03000000000000
924+090000000000000000000000000000006e6574207469746c650000001c00f901
925+01005000f303000000000000000000000000000000000000000000000108fa01
926+03000000ef03000000000000090000000000000000000000000000006e657420
927+7469746c650000001c00fa0101005000f3030000000000000000000000000000
928+00000000000000000108fb0103000000ef030000000000000900000000000000
929+00000000000000006e6574207469746c650000001c00fb0101005000f3030000
930+00000000000000000000000000000000000000000108fc0103000000ef030000
931+00000000090000000000000000000000000000006e6574207469746c65000000
932+1c00fc0101005000f30300000000000000000000000000000000000000000000
933+0108fd0103000000ef0300000000000009000000000000000000000000000000
934+6e6574207469746c650000001c00fd0101005000f30300000000000000000000
935+0000000000000000000000000108fe0103000000ef0300000000000009000000
936+0000000000000000000000006e6574207469746c650000001c00fe0101005000
937+f303000000000000000000000000000000000000000000000108ff0103000000
938+ef03000000000000090000000000000000000000000000006e6574207469746c
939+650000001c00ff0101005000f303000000000000000000000000000000000000
940+000000000108000203000000ef03000000000000090000000000000000000000
941+000000006e6574207469746c650000001c00000201005000f303000000000000
942+000000000000000000000000000000000108010203000000ef03000000000000
943+090000000000000000000000000000006e6574207469746c650000001c000102
944+01005000f3030000000000000000000000000000000000000000000001080202
945+03000000ef03000000000000090000000000000000000000000000006e657420
946+7469746c650000001c00020201005000f3030000000000000000000000000000
947+00000000000000000108030203000000ef030000000000000900000000000000
948+00000000000000006e6574207469746c650000001c00030201005000f3030000
949+00000000000000000000000000000000000000000108040203000000ef030000
950+00000000090000000000000000000000000000006e6574207469746c65000000
A internal/adapters/wayland/xwm/testdata/streams/steady.hex +2150 -0 large diff (2150 lines changed): collapsed
A internal/adapters/wayland/xwm/unmanaged.go +109 -0
@@ -0,0 +1,109 @@
1+package xwm
2+
3+import (
4+ "bnema.dev/xturbo"
5+ "bnema.dev/xturbo/xproto"
6+)
7+
8+// Override-redirect windows are tracked, never managed: the XWM reads their
9+// window type once at map, pairs them with their wl_surface and reports the
10+// mapped rect to the host in layout coordinates.
11+
12+// maxTypeWords bounds the _NET_WM_WINDOW_TYPE read.
13+const maxTypeWords = 32
14+
15+// requestOverrideType asks for the window type of a freshly mapped override
16+// window; the reply completes its readiness.
17+func (x *XWM) requestOverrideType(s Slot) {
18+ rec := x.tab.rec(s)
19+ seq := xproto.GetProperty(x.conn.Buf(), false, xproto.Window(rec.xid),
20+ xproto.Atom(x.atoms[atomNetWMWindowType]), xproto.AtomAny, 0, maxTypeWords)
21+ x.expect(seq, kindOverrideType, x.tab.tag(s), 0)
22+ x.wake()
23+}
24+
25+func (x *XWM) onOverrideType(s Slot, rec *record, r *xturbo.Reply) {
26+ rec.noInput = false
27+ if rep, ok := xproto.ParseGetPropertyReply(r.Raw); ok && rep.Format() == 32 {
28+ val := rep.Value()
29+ for i := 0; i+4 <= len(val); i += 4 {
30+ switch x.windowType(word(val, i/4)) {
31+ case atomNetWMWindowTypeTooltip, atomNetWMWindowTypeNotification, atomNetWMWindowTypeDND:
32+ rec.noInput = true
33+ }
34+ }
35+ }
36+ x.overrideReady(s)
37+}
38+
39+// overrideReady marks a mapped override window typed and pairs it.
40+func (x *XWM) overrideReady(s Slot) {
41+ rec := x.tab.rec(s)
42+ if rec == nil || rec.kind != kindUnmanaged || !rec.mapped {
43+ return
44+ }
45+ rec.ready = true
46+ x.tryPair(s)
47+ x.overrideMapped(s, rec)
48+}
49+
50+// overrideMapped reports the window once it is mapped, typed and paired.
51+func (x *XWM) overrideMapped(s Slot, rec *record) {
52+ if x.host == nil || rec.ovPaired || !rec.mapped || !rec.ready || rec.surface == 0 {
53+ return
54+ }
55+ rec.ovPaired = true
56+ rec.ov = x.layoutRect(rec)
57+ o := rec.ov
58+ x.host.OverrideMapped(s, rec.xid, rec.surface, int(o.x), int(o.y), int(o.w), int(o.h), rec.noInput)
59+}
60+
61+// overrideUnmapped ends the mapped life of an override window; a remap reads
62+// the type again and pairs with a new surface.
63+func (x *XWM) overrideUnmapped(s Slot, rec *record) {
64+ rec.ready, rec.serial, rec.surface, rec.noInput = false, 0, 0, false
65+ rec.gen++ // a type reply still in flight names a dead tag
66+ if rec.gen == 0 {
67+ rec.gen = 1
68+ }
69+ if rec.ovPaired {
70+ rec.ovPaired = false
71+ x.host.OverrideGone(s)
72+ }
73+}
74+
75+func (x *XWM) layoutRect(rec *record) ovRect {
76+ lx, ly := x.space.ToLayout(int(rec.x), int(rec.y))
77+ return ovRect{int32(lx), int32(ly), int32(rec.w), int32(rec.h)}
78+}
79+
80+// queueOverride schedules a geometry report for s; flushOverrides sends it
81+// once per iteration, only if the layout rect changed.
82+func (x *XWM) queueOverride(s Slot, rec *record) {
83+ if rec.ovQueued || s == 0 {
84+ return
85+ }
86+ if len(x.ovDirty) == cap(x.ovDirty) {
87+ x.flushOverrides()
88+ }
89+ rec.ovQueued = true
90+ x.ovDirty = append(x.ovDirty, s)
91+}
92+
93+func (x *XWM) flushOverrides() {
94+ for _, s := range x.ovDirty {
95+ rec := x.tab.rec(s)
96+ if rec == nil || rec.kind != kindUnmanaged || !rec.ovQueued {
97+ continue
98+ }
99+ rec.ovQueued = false
100+ if !rec.ovPaired {
101+ continue
102+ }
103+ if o := x.layoutRect(rec); o != rec.ov {
104+ rec.ov = o
105+ x.host.OverrideMoved(s, int(o.x), int(o.y), int(o.w), int(o.h))
106+ }
107+ }
108+ x.ovDirty = x.ovDirty[:0]
109+}
A internal/adapters/wayland/xwm/unmanaged_test.go +247 -0
A internal/adapters/wayland/xwm/xpeer_test.go +655 -0
A internal/adapters/wayland/xwm/xsettings_test.go +244 -0
A internal/adapters/wayland/xwm/xwm.go +509 -0
A internal/adapters/wayland/xwm/xwm_test.go +387 -0
A internal/adapters/wayland/xwm_selection_mock_test.go +143 -0
A internal/adapters/xwayland/builtin.go +216 -0
A internal/adapters/xwayland/starter_mock_test.go +110 -0
M internal/adapters/xwayland/xwayland.go +46 -18
M internal/adapters/xwayland/xwayland_test.go +207 -0
M internal/app/app.go +13 -6
M internal/app/cursor.go +18 -0
M internal/app/session.go +2 -1
A internal/app/testdata/x11probe/main.go +692 -0
A internal/app/xwayland_test.go +808 -0
M internal/core/client.go +8 -0
M internal/core/core.go +16 -5
M internal/core/core_test.go +52 -3
M internal/core/datadrag.go +1 -1
M internal/core/fullscreen_left_core_test.go +1 -6
M internal/core/fullscreen_left_test.go +29 -0
M internal/core/layout.go +7 -1
A internal/core/place_test.go +80 -0
M internal/core/rules_test.go +44 -0
M internal/core/scene.go +12 -1
M internal/core/spawn_placement.go +9 -1
M internal/core/transition.go +20 -0
A internal/core/unmanaged.go +117 -0
A internal/core/unmanaged_test.go +145 -0
M internal/ports/client_commands.go +10 -0
M internal/ports/client_events.go +37 -0
M internal/ports/config.go +35 -3

Comments0 open / 0

no comments yet.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
l
merge request: the list of files (j k move, enter opens, esc back)
M
merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges
R
merge request: mark the head read
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close