detached 69b4382

feat(xwm): add window table, boot and fd flush skeleton for the built-in X11 window manager

Table with open-addressing index, atoms, root setup, Attach/Close, coalesced flush with write backpressure, host/fdSource seams with mockery mocks, scripted xpeer, and table allocation checks in perf-check.
commit
69b4382cc14937312b7a3f12fc0477e5c00de3c5
author
brice · · 2026-10-22 12:02 UTC
committer
brice · · 2026-10-22 17:54 UTC
parent
3ed03aa
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
16 files · +2549 -1
M .mockery.yml +12 -0
@@ -195,3 +195,15 @@ packages:
195195 dir: internal/adapters/xwayland
196196 pkgname: xwayland
197197 filename: starter_mock_test.go
198+ github.com/bnema/neferwl/internal/adapters/wayland/xwm:
199+ interfaces:
200+ host:
201+ config:
202+ dir: internal/adapters/wayland/xwm
203+ pkgname: xwm
204+ filename: host_mock_test.go
205+ fdSource:
206+ config:
207+ dir: internal/adapters/wayland/xwm
208+ pkgname: xwm
209+ filename: fdsource_mock_test.go
@@ -195,3 +195,15 @@ packages:
195 dir: internal/adapters/xwayland195 dir: internal/adapters/xwayland
196 pkgname: xwayland196 pkgname: xwayland
197 filename: starter_mock_test.go197 filename: starter_mock_test.go
198+ github.com/bnema/neferwl/internal/adapters/wayland/xwm:
199+ interfaces:
200+ host:
201+ config:
202+ dir: internal/adapters/wayland/xwm
203+ pkgname: xwm
204+ filename: host_mock_test.go
205+ fdSource:
206+ config:
207+ dir: internal/adapters/wayland/xwm
208+ pkgname: xwm
209+ filename: fdsource_mock_test.go
M Makefile +2 -1
@@ -69,7 +69,8 @@ adapter-imports:
6969 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
7070 # Fast, mandatory guards for tiled content publications and SHM copies.
7171 perf-check:
72- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing)$$' -count=1
72+ CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations)$$' -count=1
73+ CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1
7374 check: vet test arch fakes-check log-check perf-check
7475
7576 # On-demand SDR/HDR colour check with examples/testpattern on a headless
@@ -69,7 +69,8 @@ adapter-imports:
69 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }69 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
70 # Fast, mandatory guards for tiled content publications and SHM copies.70 # Fast, mandatory guards for tiled content publications and SHM copies.
71 perf-check:71 perf-check:
72- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing)$$' -count=172+ CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland ./internal/adapters/outputkit/capture ./internal/adapters/screencast ./internal/adapters/wayland/xwm -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestRenderSteadyStateDMABufAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations|TestWriteMappedAllocations|TestCapturedForwardAllocations|TestCommandForwardAllocations|TestOutboxQueuedDeliveryAllocations|TestCastFrameAllocations|TestCastProducerAllocations|TestCaptureCycleAllocations|TestLogScenesAllocations|TestOverviewNamedRowsAllocations|TestBufferContentAllocations|TestDueFramesFireScratch|TestRetireSemaphoreAllocatesNothing|TestTableLookupAllocations|TestTableInsertRemoveAllocations)$$' -count=1
73+ CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1
73 check: vet test arch fakes-check log-check perf-check74 check: vet test arch fakes-check log-check perf-check
74 75
75 # On-demand SDR/HDR colour check with examples/testpattern on a headless76 # On-demand SDR/HDR colour check with examples/testpattern on a headless
A internal/adapters/wayland/xwm/atoms.go +100 -0
@@ -0,0 +1,100 @@
1+package xwm
2+
3+// Atoms interned at boot, in this fixed order. Clipboard and selection atoms
4+// are interned now so later phases add no round trip.
5+const (
6+ atomWMProtocols = iota
7+ atomWMDeleteWindow
8+ atomWMTakeFocus
9+ atomWMState
10+ atomWMChangeState
11+ atomWMS0
12+ atomUTF8String
13+ atomNetWMCMS0
14+ atomNetSupported
15+ atomNetSupportingWMCheck
16+ atomNetWMName
17+ atomNetWMPID
18+ atomNetWMWindowType
19+ atomNetWMWindowTypeNormal
20+ atomNetWMWindowTypeDialog
21+ atomNetWMWindowTypeUtility
22+ atomNetWMWindowTypeSplash
23+ atomNetWMWindowTypeToolbar
24+ atomNetWMWindowTypeMenu
25+ atomNetWMWindowTypeDropdownMenu
26+ atomNetWMWindowTypePopupMenu
27+ atomNetWMWindowTypeTooltip
28+ atomNetWMWindowTypeNotification
29+ atomNetWMWindowTypeCombo
30+ atomNetWMWindowTypeDND
31+ atomNetWMState
32+ atomNetWMStateModal
33+ atomNetWMStateFullscreen
34+ atomNetWMStateFocused
35+ atomNetWMStateHidden
36+ atomNetWMStateMaximizedVert
37+ atomNetWMStateMaximizedHorz
38+ atomNetActiveWindow
39+ atomNetCloseWindow
40+ atomNetClientList
41+ atomNetClientListStacking
42+ atomNetWMMoveresize
43+ atomNetWMBypassCompositor
44+ atomWLSurfaceID
45+ atomWLSurfaceSerial
46+ atomNetWMIcon
47+ atomMotifWMHints
48+ atomClipboard
49+ atomPrimary
50+ atomTargets
51+ atomCount
52+)
53+
54+var atomNames = [atomCount]string{
55+ atomWMProtocols: "WM_PROTOCOLS",
56+ atomWMDeleteWindow: "WM_DELETE_WINDOW",
57+ atomWMTakeFocus: "WM_TAKE_FOCUS",
58+ atomWMState: "WM_STATE",
59+ atomWMChangeState: "WM_CHANGE_STATE",
60+ atomWMS0: "WM_S0",
61+ atomUTF8String: "UTF8_STRING",
62+ atomNetWMCMS0: "_NET_WM_CM_S0",
63+ atomNetSupported: "_NET_SUPPORTED",
64+ atomNetSupportingWMCheck: "_NET_SUPPORTING_WM_CHECK",
65+ atomNetWMName: "_NET_WM_NAME",
66+ atomNetWMPID: "_NET_WM_PID",
67+ atomNetWMWindowType: "_NET_WM_WINDOW_TYPE",
68+ atomNetWMWindowTypeNormal: "_NET_WM_WINDOW_TYPE_NORMAL",
69+ atomNetWMWindowTypeDialog: "_NET_WM_WINDOW_TYPE_DIALOG",
70+ atomNetWMWindowTypeUtility: "_NET_WM_WINDOW_TYPE_UTILITY",
71+ atomNetWMWindowTypeSplash: "_NET_WM_WINDOW_TYPE_SPLASH",
72+ atomNetWMWindowTypeToolbar: "_NET_WM_WINDOW_TYPE_TOOLBAR",
73+ atomNetWMWindowTypeMenu: "_NET_WM_WINDOW_TYPE_MENU",
74+ atomNetWMWindowTypeDropdownMenu: "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU",
75+ atomNetWMWindowTypePopupMenu: "_NET_WM_WINDOW_TYPE_POPUP_MENU",
76+ atomNetWMWindowTypeTooltip: "_NET_WM_WINDOW_TYPE_TOOLTIP",
77+ atomNetWMWindowTypeNotification: "_NET_WM_WINDOW_TYPE_NOTIFICATION",
78+ atomNetWMWindowTypeCombo: "_NET_WM_WINDOW_TYPE_COMBO",
79+ atomNetWMWindowTypeDND: "_NET_WM_WINDOW_TYPE_DND",
80+ atomNetWMState: "_NET_WM_STATE",
81+ atomNetWMStateModal: "_NET_WM_STATE_MODAL",
82+ atomNetWMStateFullscreen: "_NET_WM_STATE_FULLSCREEN",
83+ atomNetWMStateFocused: "_NET_WM_STATE_FOCUSED",
84+ atomNetWMStateHidden: "_NET_WM_STATE_HIDDEN",
85+ atomNetWMStateMaximizedVert: "_NET_WM_STATE_MAXIMIZED_VERT",
86+ atomNetWMStateMaximizedHorz: "_NET_WM_STATE_MAXIMIZED_HORZ",
87+ atomNetActiveWindow: "_NET_ACTIVE_WINDOW",
88+ atomNetCloseWindow: "_NET_CLOSE_WINDOW",
89+ atomNetClientList: "_NET_CLIENT_LIST",
90+ atomNetClientListStacking: "_NET_CLIENT_LIST_STACKING",
91+ atomNetWMMoveresize: "_NET_WM_MOVERESIZE",
92+ atomNetWMBypassCompositor: "_NET_WM_BYPASS_COMPOSITOR",
93+ atomWLSurfaceID: "WL_SURFACE_ID",
94+ atomWLSurfaceSerial: "WL_SURFACE_SERIAL",
95+ atomNetWMIcon: "_NET_WM_ICON",
96+ atomMotifWMHints: "_MOTIF_WM_HINTS",
97+ atomClipboard: "CLIPBOARD",
98+ atomPrimary: "PRIMARY",
99+ atomTargets: "TARGETS",
100+}
@@ -0,0 +1,100 @@
1+package xwm
2+
3+// Atoms interned at boot, in this fixed order. Clipboard and selection atoms
4+// are interned now so later phases add no round trip.
5+const (
6+ atomWMProtocols = iota
7+ atomWMDeleteWindow
8+ atomWMTakeFocus
9+ atomWMState
10+ atomWMChangeState
11+ atomWMS0
12+ atomUTF8String
13+ atomNetWMCMS0
14+ atomNetSupported
15+ atomNetSupportingWMCheck
16+ atomNetWMName
17+ atomNetWMPID
18+ atomNetWMWindowType
19+ atomNetWMWindowTypeNormal
20+ atomNetWMWindowTypeDialog
21+ atomNetWMWindowTypeUtility
22+ atomNetWMWindowTypeSplash
23+ atomNetWMWindowTypeToolbar
24+ atomNetWMWindowTypeMenu
25+ atomNetWMWindowTypeDropdownMenu
26+ atomNetWMWindowTypePopupMenu
27+ atomNetWMWindowTypeTooltip
28+ atomNetWMWindowTypeNotification
29+ atomNetWMWindowTypeCombo
30+ atomNetWMWindowTypeDND
31+ atomNetWMState
32+ atomNetWMStateModal
33+ atomNetWMStateFullscreen
34+ atomNetWMStateFocused
35+ atomNetWMStateHidden
36+ atomNetWMStateMaximizedVert
37+ atomNetWMStateMaximizedHorz
38+ atomNetActiveWindow
39+ atomNetCloseWindow
40+ atomNetClientList
41+ atomNetClientListStacking
42+ atomNetWMMoveresize
43+ atomNetWMBypassCompositor
44+ atomWLSurfaceID
45+ atomWLSurfaceSerial
46+ atomNetWMIcon
47+ atomMotifWMHints
48+ atomClipboard
49+ atomPrimary
50+ atomTargets
51+ atomCount
52+)
53+
54+var atomNames = [atomCount]string{
55+ atomWMProtocols: "WM_PROTOCOLS",
56+ atomWMDeleteWindow: "WM_DELETE_WINDOW",
57+ atomWMTakeFocus: "WM_TAKE_FOCUS",
58+ atomWMState: "WM_STATE",
59+ atomWMChangeState: "WM_CHANGE_STATE",
60+ atomWMS0: "WM_S0",
61+ atomUTF8String: "UTF8_STRING",
62+ atomNetWMCMS0: "_NET_WM_CM_S0",
63+ atomNetSupported: "_NET_SUPPORTED",
64+ atomNetSupportingWMCheck: "_NET_SUPPORTING_WM_CHECK",
65+ atomNetWMName: "_NET_WM_NAME",
66+ atomNetWMPID: "_NET_WM_PID",
67+ atomNetWMWindowType: "_NET_WM_WINDOW_TYPE",
68+ atomNetWMWindowTypeNormal: "_NET_WM_WINDOW_TYPE_NORMAL",
69+ atomNetWMWindowTypeDialog: "_NET_WM_WINDOW_TYPE_DIALOG",
70+ atomNetWMWindowTypeUtility: "_NET_WM_WINDOW_TYPE_UTILITY",
71+ atomNetWMWindowTypeSplash: "_NET_WM_WINDOW_TYPE_SPLASH",
72+ atomNetWMWindowTypeToolbar: "_NET_WM_WINDOW_TYPE_TOOLBAR",
73+ atomNetWMWindowTypeMenu: "_NET_WM_WINDOW_TYPE_MENU",
74+ atomNetWMWindowTypeDropdownMenu: "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU",
75+ atomNetWMWindowTypePopupMenu: "_NET_WM_WINDOW_TYPE_POPUP_MENU",
76+ atomNetWMWindowTypeTooltip: "_NET_WM_WINDOW_TYPE_TOOLTIP",
77+ atomNetWMWindowTypeNotification: "_NET_WM_WINDOW_TYPE_NOTIFICATION",
78+ atomNetWMWindowTypeCombo: "_NET_WM_WINDOW_TYPE_COMBO",
79+ atomNetWMWindowTypeDND: "_NET_WM_WINDOW_TYPE_DND",
80+ atomNetWMState: "_NET_WM_STATE",
81+ atomNetWMStateModal: "_NET_WM_STATE_MODAL",
82+ atomNetWMStateFullscreen: "_NET_WM_STATE_FULLSCREEN",
83+ atomNetWMStateFocused: "_NET_WM_STATE_FOCUSED",
84+ atomNetWMStateHidden: "_NET_WM_STATE_HIDDEN",
85+ atomNetWMStateMaximizedVert: "_NET_WM_STATE_MAXIMIZED_VERT",
86+ atomNetWMStateMaximizedHorz: "_NET_WM_STATE_MAXIMIZED_HORZ",
87+ atomNetActiveWindow: "_NET_ACTIVE_WINDOW",
88+ atomNetCloseWindow: "_NET_CLOSE_WINDOW",
89+ atomNetClientList: "_NET_CLIENT_LIST",
90+ atomNetClientListStacking: "_NET_CLIENT_LIST_STACKING",
91+ atomNetWMMoveresize: "_NET_WM_MOVERESIZE",
92+ atomNetWMBypassCompositor: "_NET_WM_BYPASS_COMPOSITOR",
93+ atomWLSurfaceID: "WL_SURFACE_ID",
94+ atomWLSurfaceSerial: "WL_SURFACE_SERIAL",
95+ atomNetWMIcon: "_NET_WM_ICON",
96+ atomMotifWMHints: "_MOTIF_WM_HINTS",
97+ atomClipboard: "CLIPBOARD",
98+ atomPrimary: "PRIMARY",
99+ atomTargets: "TARGETS",
100+}
A internal/adapters/wayland/xwm/configure.go +18 -0
@@ -0,0 +1,18 @@
1+package xwm
2+
3+import (
4+ "bnema.dev/xturbo"
5+ "github.com/bnema/neferwl/internal/ports"
6+)
7+
8+func (x *XWM) onConfigureRequest(ev *xturbo.Event) { _ = ev }
9+func (x *XWM) onConfigureNotify(ev *xturbo.Event) { _ = ev }
10+
11+// Configure records the size and state core wants for s; sent at flush.
12+func (x *XWM) Configure(s Slot, c ports.ConfigureWindow) { _, _ = s, c }
13+
14+// Place records the layout position of the client rectangle of s.
15+func (x *XWM) Place(s Slot, px, py int) { _, _, _ = s, px, py }
16+
17+// flushConfigures sends the pending configures, once per display iteration.
18+func (x *XWM) flushConfigures() {}
@@ -0,0 +1,18 @@
1+package xwm
2+
3+import (
4+ "bnema.dev/xturbo"
5+ "github.com/bnema/neferwl/internal/ports"
6+)
7+
8+func (x *XWM) onConfigureRequest(ev *xturbo.Event) { _ = ev }
9+func (x *XWM) onConfigureNotify(ev *xturbo.Event) { _ = ev }
10+
11+// Configure records the size and state core wants for s; sent at flush.
12+func (x *XWM) Configure(s Slot, c ports.ConfigureWindow) { _, _ = s, c }
13+
14+// Place records the layout position of the client rectangle of s.
15+func (x *XWM) Place(s Slot, px, py int) { _, _, _ = s, px, py }
16+
17+// flushConfigures sends the pending configures, once per display iteration.
18+func (x *XWM) flushConfigures() {}
A internal/adapters/wayland/xwm/events.go +231 -0
@@ -0,0 +1,231 @@
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+)
24+
25+// onEvent dispatches an X event. Before Attach there is no host: only the
26+// table is maintained, and Attach replays the result.
27+func (x *XWM) onEvent(ev *xturbo.Event) {
28+ x.stats.Events++
29+ if x.closed {
30+ return
31+ }
32+ switch ev.Kind {
33+ case xproto.KindCreateNotify:
34+ x.onCreateNotify(ev)
35+ case xproto.KindDestroyNotify:
36+ x.onDestroyNotify(ev)
37+ }
38+ if x.host == nil {
39+ if ev.Kind == xproto.KindMapRequest {
40+ if s := x.tab.lookup(uint32(xproto.MapRequestOf(ev).Window())); s != 0 {
41+ x.tab.rec(s).mapping = true
42+ }
43+ }
44+ return
45+ }
46+ switch ev.Kind {
47+ case xproto.KindMapRequest:
48+ x.onMapRequest(ev)
49+ case xproto.KindMapNotify:
50+ x.onMapNotify(ev)
51+ case xproto.KindUnmapNotify:
52+ x.onUnmapNotify(ev)
53+ case xproto.KindConfigureRequest:
54+ x.onConfigureRequest(ev)
55+ case xproto.KindConfigureNotify:
56+ x.onConfigureNotify(ev)
57+ case xproto.KindPropertyNotify:
58+ x.onPropertyNotify(ev)
59+ case xproto.KindClientMessage:
60+ x.onClientMessage(ev)
61+ case xproto.KindFocusIn:
62+ x.onFocusIn(ev)
63+ }
64+}
65+
66+func (x *XWM) onReply(r *xturbo.Reply) {
67+ x.stats.Replies++
68+ if x.closed {
69+ return
70+ }
71+ switch r.Kind {
72+ case kindVersion:
73+ x.onVersionReply(r)
74+ return
75+ case kindTree:
76+ x.onTreeReply(r)
77+ return
78+ }
79+ s, rec := x.tab.live(r.Args[0])
80+ if rec == nil {
81+ x.stats.Stale++
82+ return
83+ }
84+ switch r.Kind {
85+ case kindAttr:
86+ x.onAttrReply(rec, r)
87+ case kindProp:
88+ x.onPropReply(s, rec, r)
89+ case kindGeom:
90+ x.onGeomReply(s, rec, r)
91+ case kindClientIds:
92+ x.onClientIdsReply(s, rec, r)
93+ }
94+}
95+
96+// onError never tears the XWM down: an error names a request, not the
97+// connection. Sticky Buf errors surface through Flush.
98+func (x *XWM) onError(e *xturbo.XError) {
99+ x.stats.Errors++
100+ if e.Kind == 0 {
101+ x.stats.Unattributed++
102+ x.log.Debug().Uint8("code", e.Code).Uint8("major", e.Major).Uint16("minor", e.Minor).
103+ Uint64("seq", uint64(e.Seq)).Msg("unattributed X error")
104+ return
105+ }
106+ switch e.Kind {
107+ case kindRootMask:
108+ if e.Code == xproto.BadAccess {
109+ x.bootErr = ErrWMExists
110+ }
111+ case kindProp, kindGeom, kindConfigure, kindFocus:
112+ if e.Code != xproto.BadWindow {
113+ return
114+ }
115+ // The window died between request and reply; DestroyNotify follows.
116+ _, rec := x.tab.live(e.Args[0])
117+ if rec == nil {
118+ x.stats.Stale++
119+ return
120+ }
121+ switch e.Kind {
122+ case kindProp:
123+ rec.pending &^= 1 << e.Args[1]
124+ case kindGeom:
125+ rec.pending &^= propGeom
126+ }
127+ }
128+}
129+
130+// onCreateNotify records a new top-level window. Override-redirect windows
131+// are kept (their geometry and serial matter later) but never reach the host.
132+func (x *XWM) onCreateNotify(ev *xturbo.Event) {
133+ e := xproto.CreateNotifyOf(ev)
134+ xid := uint32(e.Window())
135+ if x.tab.lookup(xid) != 0 {
136+ return
137+ }
138+ s := x.insert(xid)
139+ if s == 0 {
140+ return
141+ }
142+ r := x.tab.rec(s)
143+ r.override = e.OverrideRedirect()
144+ r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height())
145+ if r.override {
146+ r.kind = kindUnmanaged
147+ x.stats.UnmanagedSeen++
148+ return
149+ }
150+ if x.host != nil {
151+ x.adopt(s)
152+ }
153+}
154+
155+// adopt announces a managed window to the host and starts watching it.
156+func (x *XWM) adopt(s Slot) {
157+ r := x.tab.rec(s)
158+ r.id = x.host.Created(s, r.xid)
159+ var v xproto.ChangeWindowAttributesValues
160+ v.SetEventMask(xproto.EventMaskPropertyChange | xproto.EventMaskFocusChange)
161+ x.expect(xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(r.xid), &v), kindAttr, x.tab.tag(s), 0)
162+ x.log.Debug().Uint32("xid", r.xid).Uint32("slot", uint32(s)).Msg("x11 window created")
163+}
164+
165+func (x *XWM) onDestroyNotify(ev *xturbo.Event) {
166+ xid := uint32(xproto.DestroyNotifyOf(ev).Window())
167+ s := x.tab.lookup(xid)
168+ if s == 0 {
169+ return
170+ }
171+ if r := x.tab.rec(s); r.kind == kindManaged && r.id != 0 && x.host != nil {
172+ r.id = 0
173+ x.host.Dissociate(s, true)
174+ }
175+ x.tab.remove(s)
176+}
177+
178+// onTreeReply (boot): record every existing child and ask for its attributes.
179+func (x *XWM) onTreeReply(r *xturbo.Reply) {
180+ q, ok := xproto.ParseQueryTreeReply(r.Raw)
181+ if !ok {
182+ return
183+ }
184+ children := q.Children()
185+ for i := 0; i < children.Len(); i++ {
186+ xid := uint32(children.At(i))
187+ if x.tab.lookup(xid) != 0 {
188+ continue
189+ }
190+ s := x.insert(xid)
191+ if s == 0 {
192+ return
193+ }
194+ x.expect(xproto.GetWindowAttributes(x.conn.Buf(), xproto.Window(xid)), kindAttr, x.tab.tag(s), 0)
195+ }
196+}
197+
198+// onAttrReply (boot): classify an existing window; viewable ones go through
199+// the map flow once the host is attached.
200+func (x *XWM) onAttrReply(rec *record, r *xturbo.Reply) {
201+ a, ok := xproto.ParseGetWindowAttributesReply(r.Raw)
202+ if !ok || x.host != nil {
203+ return
204+ }
205+ if a.OverrideRedirect() {
206+ rec.override = true
207+ rec.kind = kindUnmanaged
208+ x.stats.UnmanagedSeen++
209+ return
210+ }
211+ if a.MapState() == xproto.MapStateViewable {
212+ rec.mapped, rec.mapping = true, true
213+ }
214+}
215+
216+func (x *XWM) onVersionReply(r *xturbo.Reply) {
217+ switch r.Args[0] {
218+ case versionComposite:
219+ if v, ok := composite.ParseQueryVersionReply(r.Raw); ok {
220+ x.log.Info().Uint32("major", v.MajorVersion()).Uint32("minor", v.MinorVersion()).Msg("Composite")
221+ }
222+ case versionXFixes:
223+ if v, ok := xfixes.ParseQueryVersionReply(r.Raw); ok {
224+ x.log.Info().Uint32("major", v.MajorVersion()).Uint32("minor", v.MinorVersion()).Msg("XFixes")
225+ }
226+ case versionRes:
227+ if v, ok := res.ParseQueryVersionReply(r.Raw); ok {
228+ x.log.Info().Uint16("major", v.ServerMajor()).Uint16("minor", v.ServerMinor()).Msg("X-Resource")
229+ }
230+ }
231+}
@@ -0,0 +1,231 @@
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+)
24+
25+// onEvent dispatches an X event. Before Attach there is no host: only the
26+// table is maintained, and Attach replays the result.
27+func (x *XWM) onEvent(ev *xturbo.Event) {
28+ x.stats.Events++
29+ if x.closed {
30+ return
31+ }
32+ switch ev.Kind {
33+ case xproto.KindCreateNotify:
34+ x.onCreateNotify(ev)
35+ case xproto.KindDestroyNotify:
36+ x.onDestroyNotify(ev)
37+ }
38+ if x.host == nil {
39+ if ev.Kind == xproto.KindMapRequest {
40+ if s := x.tab.lookup(uint32(xproto.MapRequestOf(ev).Window())); s != 0 {
41+ x.tab.rec(s).mapping = true
42+ }
43+ }
44+ return
45+ }
46+ switch ev.Kind {
47+ case xproto.KindMapRequest:
48+ x.onMapRequest(ev)
49+ case xproto.KindMapNotify:
50+ x.onMapNotify(ev)
51+ case xproto.KindUnmapNotify:
52+ x.onUnmapNotify(ev)
53+ case xproto.KindConfigureRequest:
54+ x.onConfigureRequest(ev)
55+ case xproto.KindConfigureNotify:
56+ x.onConfigureNotify(ev)
57+ case xproto.KindPropertyNotify:
58+ x.onPropertyNotify(ev)
59+ case xproto.KindClientMessage:
60+ x.onClientMessage(ev)
61+ case xproto.KindFocusIn:
62+ x.onFocusIn(ev)
63+ }
64+}
65+
66+func (x *XWM) onReply(r *xturbo.Reply) {
67+ x.stats.Replies++
68+ if x.closed {
69+ return
70+ }
71+ switch r.Kind {
72+ case kindVersion:
73+ x.onVersionReply(r)
74+ return
75+ case kindTree:
76+ x.onTreeReply(r)
77+ return
78+ }
79+ s, rec := x.tab.live(r.Args[0])
80+ if rec == nil {
81+ x.stats.Stale++
82+ return
83+ }
84+ switch r.Kind {
85+ case kindAttr:
86+ x.onAttrReply(rec, r)
87+ case kindProp:
88+ x.onPropReply(s, rec, r)
89+ case kindGeom:
90+ x.onGeomReply(s, rec, r)
91+ case kindClientIds:
92+ x.onClientIdsReply(s, rec, r)
93+ }
94+}
95+
96+// onError never tears the XWM down: an error names a request, not the
97+// connection. Sticky Buf errors surface through Flush.
98+func (x *XWM) onError(e *xturbo.XError) {
99+ x.stats.Errors++
100+ if e.Kind == 0 {
101+ x.stats.Unattributed++
102+ x.log.Debug().Uint8("code", e.Code).Uint8("major", e.Major).Uint16("minor", e.Minor).
103+ Uint64("seq", uint64(e.Seq)).Msg("unattributed X error")
104+ return
105+ }
106+ switch e.Kind {
107+ case kindRootMask:
108+ if e.Code == xproto.BadAccess {
109+ x.bootErr = ErrWMExists
110+ }
111+ case kindProp, kindGeom, kindConfigure, kindFocus:
112+ if e.Code != xproto.BadWindow {
113+ return
114+ }
115+ // The window died between request and reply; DestroyNotify follows.
116+ _, rec := x.tab.live(e.Args[0])
117+ if rec == nil {
118+ x.stats.Stale++
119+ return
120+ }
121+ switch e.Kind {
122+ case kindProp:
123+ rec.pending &^= 1 << e.Args[1]
124+ case kindGeom:
125+ rec.pending &^= propGeom
126+ }
127+ }
128+}
129+
130+// onCreateNotify records a new top-level window. Override-redirect windows
131+// are kept (their geometry and serial matter later) but never reach the host.
132+func (x *XWM) onCreateNotify(ev *xturbo.Event) {
133+ e := xproto.CreateNotifyOf(ev)
134+ xid := uint32(e.Window())
135+ if x.tab.lookup(xid) != 0 {
136+ return
137+ }
138+ s := x.insert(xid)
139+ if s == 0 {
140+ return
141+ }
142+ r := x.tab.rec(s)
143+ r.override = e.OverrideRedirect()
144+ r.x, r.y, r.w, r.h = int32(e.X()), int32(e.Y()), uint32(e.Width()), uint32(e.Height())
145+ if r.override {
146+ r.kind = kindUnmanaged
147+ x.stats.UnmanagedSeen++
148+ return
149+ }
150+ if x.host != nil {
151+ x.adopt(s)
152+ }
153+}
154+
155+// adopt announces a managed window to the host and starts watching it.
156+func (x *XWM) adopt(s Slot) {
157+ r := x.tab.rec(s)
158+ r.id = x.host.Created(s, r.xid)
159+ var v xproto.ChangeWindowAttributesValues
160+ v.SetEventMask(xproto.EventMaskPropertyChange | xproto.EventMaskFocusChange)
161+ x.expect(xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(r.xid), &v), kindAttr, x.tab.tag(s), 0)
162+ x.log.Debug().Uint32("xid", r.xid).Uint32("slot", uint32(s)).Msg("x11 window created")
163+}
164+
165+func (x *XWM) onDestroyNotify(ev *xturbo.Event) {
166+ xid := uint32(xproto.DestroyNotifyOf(ev).Window())
167+ s := x.tab.lookup(xid)
168+ if s == 0 {
169+ return
170+ }
171+ if r := x.tab.rec(s); r.kind == kindManaged && r.id != 0 && x.host != nil {
172+ r.id = 0
173+ x.host.Dissociate(s, true)
174+ }
175+ x.tab.remove(s)
176+}
177+
178+// onTreeReply (boot): record every existing child and ask for its attributes.
179+func (x *XWM) onTreeReply(r *xturbo.Reply) {
180+ q, ok := xproto.ParseQueryTreeReply(r.Raw)
181+ if !ok {
182+ return
183+ }
184+ children := q.Children()
185+ for i := 0; i < children.Len(); i++ {
186+ xid := uint32(children.At(i))
187+ if x.tab.lookup(xid) != 0 {
188+ continue
189+ }
190+ s := x.insert(xid)
191+ if s == 0 {
192+ return
193+ }
194+ x.expect(xproto.GetWindowAttributes(x.conn.Buf(), xproto.Window(xid)), kindAttr, x.tab.tag(s), 0)
195+ }
196+}
197+
198+// onAttrReply (boot): classify an existing window; viewable ones go through
199+// the map flow once the host is attached.
200+func (x *XWM) onAttrReply(rec *record, r *xturbo.Reply) {
201+ a, ok := xproto.ParseGetWindowAttributesReply(r.Raw)
202+ if !ok || x.host != nil {
203+ return
204+ }
205+ if a.OverrideRedirect() {
206+ rec.override = true
207+ rec.kind = kindUnmanaged
208+ x.stats.UnmanagedSeen++
209+ return
210+ }
211+ if a.MapState() == xproto.MapStateViewable {
212+ rec.mapped, rec.mapping = true, true
213+ }
214+}
215+
216+func (x *XWM) onVersionReply(r *xturbo.Reply) {
217+ switch r.Args[0] {
218+ case versionComposite:
219+ if v, ok := composite.ParseQueryVersionReply(r.Raw); ok {
220+ x.log.Info().Uint32("major", v.MajorVersion()).Uint32("minor", v.MinorVersion()).Msg("Composite")
221+ }
222+ case versionXFixes:
223+ if v, ok := xfixes.ParseQueryVersionReply(r.Raw); ok {
224+ x.log.Info().Uint32("major", v.MajorVersion()).Uint32("minor", v.MinorVersion()).Msg("XFixes")
225+ }
226+ case versionRes:
227+ if v, ok := res.ParseQueryVersionReply(r.Raw); ok {
228+ x.log.Info().Uint16("major", v.ServerMajor()).Uint16("minor", v.ServerMinor()).Msg("X-Resource")
229+ }
230+ }
231+}
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+}
@@ -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/focus.go +11 -0
@@ -0,0 +1,11 @@
1+package xwm
2+
3+import "bnema.dev/xturbo"
4+
5+func (x *XWM) onFocusIn(ev *xturbo.Event) { _ = ev }
6+
7+// Focus gives keyboard focus to s; 0 means a native Wayland window or none.
8+func (x *XWM) Focus(s Slot) { _ = s }
9+
10+// CloseWindow asks s to close (WM_DELETE_WINDOW), or kills its client.
11+func (x *XWM) CloseWindow(s Slot) { _ = s }
@@ -0,0 +1,11 @@
1+package xwm
2+
3+import "bnema.dev/xturbo"
4+
5+func (x *XWM) onFocusIn(ev *xturbo.Event) { _ = ev }
6+
7+// Focus gives keyboard focus to s; 0 means a native Wayland window or none.
8+func (x *XWM) Focus(s Slot) { _ = s }
9+
10+// CloseWindow asks s to close (WM_DELETE_WINDOW), or kills its client.
11+func (x *XWM) CloseWindow(s Slot) { _ = s }
A internal/adapters/wayland/xwm/host.go +49 -0
@@ -0,0 +1,49 @@
1+package xwm
2+
3+import "github.com/bnema/neferwl/internal/ports"
4+
5+// Slot is the XWM's window slot: table index + 1, stable until the window is
6+// destroyed. 0 means none.
7+type Slot uint32
8+
9+// WindowInfo is what the XWM learned about a managed window by the time it is
10+// ready to be mapped. The host copies it.
11+type WindowInfo struct {
12+ PID int
13+ Floating bool // dialog/utility/splash/fixed size
14+ Width, Height int // requested size, logical (scale 1)
15+ Parent Slot
16+ Modal bool // _NET_WM_STATE_MODAL
17+ Fullscreen bool // _NET_WM_STATE_FULLSCREEN at map
18+ Title, AppID string
19+}
20+
21+// host is the wayland server as the XWM sees it. Display goroutine only.
22+// All strings passed are owned by the XWM record and already deduplicated
23+// (only passed when changed).
24+type host interface {
25+ // Created: a managed (non-override-redirect) X window is known; the host
26+ // allocates a ports.WindowID and returns it. Not yet mapped.
27+ Created(slot Slot, xid uint32) ports.WindowID
28+ // Ready: properties read and MapWindow issued; the surface may still be
29+ // missing. Info is copied by the host.
30+ Ready(slot Slot, info WindowInfo)
31+ // Associate links the X window to the wl_surface with identity surfaceID.
32+ // The host maps the window on the surface's first buffer commit.
33+ Associate(slot Slot, surfaceID uint64)
34+ // Dissociate: UnmapNotify/DestroyNotify; the host emits WindowUnmapped if
35+ // mapped and drops the surface link. destroyed also frees the WindowID.
36+ Dissociate(slot Slot, destroyed bool)
37+ Title(slot Slot, title string) // changed only
38+ AppID(slot Slot, appID string) // changed only (WM_CLASS class)
39+ Parent(slot Slot, parent Slot) // 0 = none; after a WM_TRANSIENT_FOR change
40+ // Fullscreen forwards a fullscreen request (_NET_WM_STATE client message
41+ // or initial state); the host emits WindowFullscreenRequest.
42+ Fullscreen(slot Slot, on bool)
43+}
44+
45+// fdSource is the display's event source for the X socket (*server.Source).
46+type fdSource interface {
47+ SetWritable(on bool) error
48+ Remove()
49+}
@@ -0,0 +1,49 @@
1+package xwm
2+
3+import "github.com/bnema/neferwl/internal/ports"
4+
5+// Slot is the XWM's window slot: table index + 1, stable until the window is
6+// destroyed. 0 means none.
7+type Slot uint32
8+
9+// WindowInfo is what the XWM learned about a managed window by the time it is
10+// ready to be mapped. The host copies it.
11+type WindowInfo struct {
12+ PID int
13+ Floating bool // dialog/utility/splash/fixed size
14+ Width, Height int // requested size, logical (scale 1)
15+ Parent Slot
16+ Modal bool // _NET_WM_STATE_MODAL
17+ Fullscreen bool // _NET_WM_STATE_FULLSCREEN at map
18+ Title, AppID string
19+}
20+
21+// host is the wayland server as the XWM sees it. Display goroutine only.
22+// All strings passed are owned by the XWM record and already deduplicated
23+// (only passed when changed).
24+type host interface {
25+ // Created: a managed (non-override-redirect) X window is known; the host
26+ // allocates a ports.WindowID and returns it. Not yet mapped.
27+ Created(slot Slot, xid uint32) ports.WindowID
28+ // Ready: properties read and MapWindow issued; the surface may still be
29+ // missing. Info is copied by the host.
30+ Ready(slot Slot, info WindowInfo)
31+ // Associate links the X window to the wl_surface with identity surfaceID.
32+ // The host maps the window on the surface's first buffer commit.
33+ Associate(slot Slot, surfaceID uint64)
34+ // Dissociate: UnmapNotify/DestroyNotify; the host emits WindowUnmapped if
35+ // mapped and drops the surface link. destroyed also frees the WindowID.
36+ Dissociate(slot Slot, destroyed bool)
37+ Title(slot Slot, title string) // changed only
38+ AppID(slot Slot, appID string) // changed only (WM_CLASS class)
39+ Parent(slot Slot, parent Slot) // 0 = none; after a WM_TRANSIENT_FOR change
40+ // Fullscreen forwards a fullscreen request (_NET_WM_STATE client message
41+ // or initial state); the host emits WindowFullscreenRequest.
42+ Fullscreen(slot Slot, on bool)
43+}
44+
45+// fdSource is the display's event source for the X socket (*server.Source).
46+type fdSource interface {
47+ SetWritable(on bool) error
48+ Remove()
49+}
A internal/adapters/wayland/xwm/host_mock_test.go +425 -0
@@ -0,0 +1,425 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package xwm
6+
7+import (
8+ "github.com/bnema/neferwl/internal/ports"
9+ mock "github.com/stretchr/testify/mock"
10+)
11+
12+// 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.
13+// The first argument is typically a *testing.T value.
14+func newMockhost(t interface {
15+ mock.TestingT
16+ Cleanup(func())
17+}) *mockhost {
18+ if helper, ok := t.(interface{ Helper() }); ok {
19+ helper.Helper()
20+ }
21+
22+ mock := &mockhost{}
23+ mock.Mock.Test(t)
24+
25+ t.Cleanup(func() {
26+ if helper, ok := t.(interface{ Helper() }); ok {
27+ helper.Helper()
28+ }
29+ mock.AssertExpectations(t)
30+ })
31+
32+ return mock
33+}
34+
35+// mockhost is an autogenerated mock type for the host type
36+type mockhost struct {
37+ mock.Mock
38+}
39+
40+type mockhost_Expecter struct {
41+ mock *mock.Mock
42+}
43+
44+func (_m *mockhost) EXPECT() *mockhost_Expecter {
45+ return &mockhost_Expecter{mock: &_m.Mock}
46+}
47+
48+// AppID provides a mock function for the type mockhost
49+func (_mock *mockhost) AppID(slot Slot, appID string) {
50+ _mock.Called(slot, appID)
51+ return
52+}
53+
54+// mockhost_AppID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AppID'
55+type mockhost_AppID_Call struct {
56+ *mock.Call
57+}
58+
59+// AppID is a helper method to define mock.On call
60+// - slot Slot
61+// - appID string
62+func (_e *mockhost_Expecter) AppID(slot any, appID any) *mockhost_AppID_Call {
63+ return &mockhost_AppID_Call{Call: _e.mock.On("AppID", slot, appID)}
64+}
65+
66+func (_c *mockhost_AppID_Call) Run(run func(slot Slot, appID string)) *mockhost_AppID_Call {
67+ _c.Call.Run(func(args mock.Arguments) {
68+ var arg0 Slot
69+ if args[0] != nil {
70+ arg0 = args[0].(Slot)
71+ }
72+ var arg1 string
73+ if args[1] != nil {
74+ arg1 = args[1].(string)
75+ }
76+ run(
77+ arg0,
78+ arg1,
79+ )
80+ })
81+ return _c
82+}
83+
84+func (_c *mockhost_AppID_Call) Return() *mockhost_AppID_Call {
85+ _c.Call.Return()
86+ return _c
87+}
88+
89+func (_c *mockhost_AppID_Call) RunAndReturn(run func(slot Slot, appID string)) *mockhost_AppID_Call {
90+ _c.Run(run)
91+ return _c
92+}
93+
94+// Associate provides a mock function for the type mockhost
95+func (_mock *mockhost) Associate(slot Slot, surfaceID uint64) {
96+ _mock.Called(slot, surfaceID)
97+ return
98+}
99+
100+// mockhost_Associate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Associate'
101+type mockhost_Associate_Call struct {
102+ *mock.Call
103+}
104+
105+// Associate is a helper method to define mock.On call
106+// - slot Slot
107+// - surfaceID uint64
108+func (_e *mockhost_Expecter) Associate(slot any, surfaceID any) *mockhost_Associate_Call {
109+ return &mockhost_Associate_Call{Call: _e.mock.On("Associate", slot, surfaceID)}
110+}
111+
112+func (_c *mockhost_Associate_Call) Run(run func(slot Slot, surfaceID uint64)) *mockhost_Associate_Call {
113+ _c.Call.Run(func(args mock.Arguments) {
114+ var arg0 Slot
115+ if args[0] != nil {
116+ arg0 = args[0].(Slot)
117+ }
118+ var arg1 uint64
119+ if args[1] != nil {
120+ arg1 = args[1].(uint64)
121+ }
122+ run(
123+ arg0,
124+ arg1,
125+ )
126+ })
127+ return _c
128+}
129+
130+func (_c *mockhost_Associate_Call) Return() *mockhost_Associate_Call {
131+ _c.Call.Return()
132+ return _c
133+}
134+
135+func (_c *mockhost_Associate_Call) RunAndReturn(run func(slot Slot, surfaceID uint64)) *mockhost_Associate_Call {
136+ _c.Run(run)
137+ return _c
138+}
139+
140+// Created provides a mock function for the type mockhost
141+func (_mock *mockhost) Created(slot Slot, xid uint32) ports.WindowID {
142+ ret := _mock.Called(slot, xid)
143+
144+ if len(ret) == 0 {
145+ panic("no return value specified for Created")
146+ }
147+
148+ var r0 ports.WindowID
149+ if returnFunc, ok := ret.Get(0).(func(Slot, uint32) ports.WindowID); ok {
150+ r0 = returnFunc(slot, xid)
151+ } else {
152+ r0 = ret.Get(0).(ports.WindowID)
153+ }
154+ return r0
155+}
156+
157+// mockhost_Created_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Created'
158+type mockhost_Created_Call struct {
159+ *mock.Call
160+}
161+
162+// Created is a helper method to define mock.On call
163+// - slot Slot
164+// - xid uint32
165+func (_e *mockhost_Expecter) Created(slot any, xid any) *mockhost_Created_Call {
166+ return &mockhost_Created_Call{Call: _e.mock.On("Created", slot, xid)}
167+}
168+
169+func (_c *mockhost_Created_Call) Run(run func(slot Slot, xid uint32)) *mockhost_Created_Call {
170+ _c.Call.Run(func(args mock.Arguments) {
171+ var arg0 Slot
172+ if args[0] != nil {
173+ arg0 = args[0].(Slot)
174+ }
175+ var arg1 uint32
176+ if args[1] != nil {
177+ arg1 = args[1].(uint32)
178+ }
179+ run(
180+ arg0,
181+ arg1,
182+ )
183+ })
184+ return _c
185+}
186+
187+func (_c *mockhost_Created_Call) Return(windowID ports.WindowID) *mockhost_Created_Call {
188+ _c.Call.Return(windowID)
189+ return _c
190+}
191+
192+func (_c *mockhost_Created_Call) RunAndReturn(run func(slot Slot, xid uint32) ports.WindowID) *mockhost_Created_Call {
193+ _c.Call.Return(run)
194+ return _c
195+}
196+
197+// Dissociate provides a mock function for the type mockhost
198+func (_mock *mockhost) Dissociate(slot Slot, destroyed bool) {
199+ _mock.Called(slot, destroyed)
200+ return
201+}
202+
203+// mockhost_Dissociate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Dissociate'
204+type mockhost_Dissociate_Call struct {
205+ *mock.Call
206+}
207+
208+// Dissociate is a helper method to define mock.On call
209+// - slot Slot
210+// - destroyed bool
211+func (_e *mockhost_Expecter) Dissociate(slot any, destroyed any) *mockhost_Dissociate_Call {
212+ return &mockhost_Dissociate_Call{Call: _e.mock.On("Dissociate", slot, destroyed)}
213+}
214+
215+func (_c *mockhost_Dissociate_Call) Run(run func(slot Slot, destroyed bool)) *mockhost_Dissociate_Call {
216+ _c.Call.Run(func(args mock.Arguments) {
217+ var arg0 Slot
218+ if args[0] != nil {
219+ arg0 = args[0].(Slot)
220+ }
221+ var arg1 bool
222+ if args[1] != nil {
223+ arg1 = args[1].(bool)
224+ }
225+ run(
226+ arg0,
227+ arg1,
228+ )
229+ })
230+ return _c
231+}
232+
233+func (_c *mockhost_Dissociate_Call) Return() *mockhost_Dissociate_Call {
234+ _c.Call.Return()
235+ return _c
236+}
237+
238+func (_c *mockhost_Dissociate_Call) RunAndReturn(run func(slot Slot, destroyed bool)) *mockhost_Dissociate_Call {
239+ _c.Run(run)
240+ return _c
241+}
242+
243+// Fullscreen provides a mock function for the type mockhost
244+func (_mock *mockhost) Fullscreen(slot Slot, on bool) {
245+ _mock.Called(slot, on)
246+ return
247+}
248+
249+// mockhost_Fullscreen_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Fullscreen'
250+type mockhost_Fullscreen_Call struct {
251+ *mock.Call
252+}
253+
254+// Fullscreen is a helper method to define mock.On call
255+// - slot Slot
256+// - on bool
257+func (_e *mockhost_Expecter) Fullscreen(slot any, on any) *mockhost_Fullscreen_Call {
258+ return &mockhost_Fullscreen_Call{Call: _e.mock.On("Fullscreen", slot, on)}
259+}
260+
261+func (_c *mockhost_Fullscreen_Call) Run(run func(slot Slot, on bool)) *mockhost_Fullscreen_Call {
262+ _c.Call.Run(func(args mock.Arguments) {
263+ var arg0 Slot
264+ if args[0] != nil {
265+ arg0 = args[0].(Slot)
266+ }
267+ var arg1 bool
268+ if args[1] != nil {
269+ arg1 = args[1].(bool)
270+ }
271+ run(
272+ arg0,
273+ arg1,
274+ )
275+ })
276+ return _c
277+}
278+
279+func (_c *mockhost_Fullscreen_Call) Return() *mockhost_Fullscreen_Call {
280+ _c.Call.Return()
281+ return _c
282+}
283+
284+func (_c *mockhost_Fullscreen_Call) RunAndReturn(run func(slot Slot, on bool)) *mockhost_Fullscreen_Call {
285+ _c.Run(run)
286+ return _c
287+}
288+
289+// Parent provides a mock function for the type mockhost
290+func (_mock *mockhost) Parent(slot Slot, parent Slot) {
291+ _mock.Called(slot, parent)
292+ return
293+}
294+
295+// mockhost_Parent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Parent'
296+type mockhost_Parent_Call struct {
297+ *mock.Call
298+}
299+
300+// Parent is a helper method to define mock.On call
301+// - slot Slot
302+// - parent Slot
303+func (_e *mockhost_Expecter) Parent(slot any, parent any) *mockhost_Parent_Call {
304+ return &mockhost_Parent_Call{Call: _e.mock.On("Parent", slot, parent)}
305+}
306+
307+func (_c *mockhost_Parent_Call) Run(run func(slot Slot, parent Slot)) *mockhost_Parent_Call {
308+ _c.Call.Run(func(args mock.Arguments) {
309+ var arg0 Slot
310+ if args[0] != nil {
311+ arg0 = args[0].(Slot)
312+ }
313+ var arg1 Slot
314+ if args[1] != nil {
315+ arg1 = args[1].(Slot)
316+ }
317+ run(
318+ arg0,
319+ arg1,
320+ )
321+ })
322+ return _c
323+}
324+
325+func (_c *mockhost_Parent_Call) Return() *mockhost_Parent_Call {
326+ _c.Call.Return()
327+ return _c
328+}
329+
330+func (_c *mockhost_Parent_Call) RunAndReturn(run func(slot Slot, parent Slot)) *mockhost_Parent_Call {
331+ _c.Run(run)
332+ return _c
333+}
334+
335+// Ready provides a mock function for the type mockhost
336+func (_mock *mockhost) Ready(slot Slot, info WindowInfo) {
337+ _mock.Called(slot, info)
338+ return
339+}
340+
341+// mockhost_Ready_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ready'
342+type mockhost_Ready_Call struct {
343+ *mock.Call
344+}
345+
346+// Ready is a helper method to define mock.On call
347+// - slot Slot
348+// - info WindowInfo
349+func (_e *mockhost_Expecter) Ready(slot any, info any) *mockhost_Ready_Call {
350+ return &mockhost_Ready_Call{Call: _e.mock.On("Ready", slot, info)}
351+}
352+
353+func (_c *mockhost_Ready_Call) Run(run func(slot Slot, info WindowInfo)) *mockhost_Ready_Call {
354+ _c.Call.Run(func(args mock.Arguments) {
355+ var arg0 Slot
356+ if args[0] != nil {
357+ arg0 = args[0].(Slot)
358+ }
359+ var arg1 WindowInfo
360+ if args[1] != nil {
361+ arg1 = args[1].(WindowInfo)
362+ }
363+ run(
364+ arg0,
365+ arg1,
366+ )
367+ })
368+ return _c
369+}
370+
371+func (_c *mockhost_Ready_Call) Return() *mockhost_Ready_Call {
372+ _c.Call.Return()
373+ return _c
374+}
375+
376+func (_c *mockhost_Ready_Call) RunAndReturn(run func(slot Slot, info WindowInfo)) *mockhost_Ready_Call {
377+ _c.Run(run)
378+ return _c
379+}
380+
381+// Title provides a mock function for the type mockhost
382+func (_mock *mockhost) Title(slot Slot, title string) {
383+ _mock.Called(slot, title)
384+ return
385+}
386+
387+// mockhost_Title_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Title'
388+type mockhost_Title_Call struct {
389+ *mock.Call
390+}
391+
392+// Title is a helper method to define mock.On call
393+// - slot Slot
394+// - title string
395+func (_e *mockhost_Expecter) Title(slot any, title any) *mockhost_Title_Call {
396+ return &mockhost_Title_Call{Call: _e.mock.On("Title", slot, title)}
397+}
398+
399+func (_c *mockhost_Title_Call) Run(run func(slot Slot, title string)) *mockhost_Title_Call {
400+ _c.Call.Run(func(args mock.Arguments) {
401+ var arg0 Slot
402+ if args[0] != nil {
403+ arg0 = args[0].(Slot)
404+ }
405+ var arg1 string
406+ if args[1] != nil {
407+ arg1 = args[1].(string)
408+ }
409+ run(
410+ arg0,
411+ arg1,
412+ )
413+ })
414+ return _c
415+}
416+
417+func (_c *mockhost_Title_Call) Return() *mockhost_Title_Call {
418+ _c.Call.Return()
419+ return _c
420+}
421+
422+func (_c *mockhost_Title_Call) RunAndReturn(run func(slot Slot, title string)) *mockhost_Title_Call {
423+ _c.Run(run)
424+ return _c
425+}
@@ -0,0 +1,425 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package xwm
6+
7+import (
8+ "github.com/bnema/neferwl/internal/ports"
9+ mock "github.com/stretchr/testify/mock"
10+)
11+
12+// 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.
13+// The first argument is typically a *testing.T value.
14+func newMockhost(t interface {
15+ mock.TestingT
16+ Cleanup(func())
17+}) *mockhost {
18+ if helper, ok := t.(interface{ Helper() }); ok {
19+ helper.Helper()
20+ }
21+
22+ mock := &mockhost{}
23+ mock.Mock.Test(t)
24+
25+ t.Cleanup(func() {
26+ if helper, ok := t.(interface{ Helper() }); ok {
27+ helper.Helper()
28+ }
29+ mock.AssertExpectations(t)
30+ })
31+
32+ return mock
33+}
34+
35+// mockhost is an autogenerated mock type for the host type
36+type mockhost struct {
37+ mock.Mock
38+}
39+
40+type mockhost_Expecter struct {
41+ mock *mock.Mock
42+}
43+
44+func (_m *mockhost) EXPECT() *mockhost_Expecter {
45+ return &mockhost_Expecter{mock: &_m.Mock}
46+}
47+
48+// AppID provides a mock function for the type mockhost
49+func (_mock *mockhost) AppID(slot Slot, appID string) {
50+ _mock.Called(slot, appID)
51+ return
52+}
53+
54+// mockhost_AppID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AppID'
55+type mockhost_AppID_Call struct {
56+ *mock.Call
57+}
58+
59+// AppID is a helper method to define mock.On call
60+// - slot Slot
61+// - appID string
62+func (_e *mockhost_Expecter) AppID(slot any, appID any) *mockhost_AppID_Call {
63+ return &mockhost_AppID_Call{Call: _e.mock.On("AppID", slot, appID)}
64+}
65+
66+func (_c *mockhost_AppID_Call) Run(run func(slot Slot, appID string)) *mockhost_AppID_Call {
67+ _c.Call.Run(func(args mock.Arguments) {
68+ var arg0 Slot
69+ if args[0] != nil {
70+ arg0 = args[0].(Slot)
71+ }
72+ var arg1 string
73+ if args[1] != nil {
74+ arg1 = args[1].(string)
75+ }
76+ run(
77+ arg0,
78+ arg1,
79+ )
80+ })
81+ return _c
82+}
83+
84+func (_c *mockhost_AppID_Call) Return() *mockhost_AppID_Call {
85+ _c.Call.Return()
86+ return _c
87+}
88+
89+func (_c *mockhost_AppID_Call) RunAndReturn(run func(slot Slot, appID string)) *mockhost_AppID_Call {
90+ _c.Run(run)
91+ return _c
92+}
93+
94+// Associate provides a mock function for the type mockhost
95+func (_mock *mockhost) Associate(slot Slot, surfaceID uint64) {
96+ _mock.Called(slot, surfaceID)
97+ return
98+}
99+
100+// mockhost_Associate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Associate'
101+type mockhost_Associate_Call struct {
102+ *mock.Call
103+}
104+
105+// Associate is a helper method to define mock.On call
106+// - slot Slot
107+// - surfaceID uint64
108+func (_e *mockhost_Expecter) Associate(slot any, surfaceID any) *mockhost_Associate_Call {
109+ return &mockhost_Associate_Call{Call: _e.mock.On("Associate", slot, surfaceID)}
110+}
111+
112+func (_c *mockhost_Associate_Call) Run(run func(slot Slot, surfaceID uint64)) *mockhost_Associate_Call {
113+ _c.Call.Run(func(args mock.Arguments) {
114+ var arg0 Slot
115+ if args[0] != nil {
116+ arg0 = args[0].(Slot)
117+ }
118+ var arg1 uint64
119+ if args[1] != nil {
120+ arg1 = args[1].(uint64)
121+ }
122+ run(
123+ arg0,
124+ arg1,
125+ )
126+ })
127+ return _c
128+}
129+
130+func (_c *mockhost_Associate_Call) Return() *mockhost_Associate_Call {
131+ _c.Call.Return()
132+ return _c
133+}
134+
135+func (_c *mockhost_Associate_Call) RunAndReturn(run func(slot Slot, surfaceID uint64)) *mockhost_Associate_Call {
136+ _c.Run(run)
137+ return _c
138+}
139+
140+// Created provides a mock function for the type mockhost
141+func (_mock *mockhost) Created(slot Slot, xid uint32) ports.WindowID {
142+ ret := _mock.Called(slot, xid)
143+
144+ if len(ret) == 0 {
145+ panic("no return value specified for Created")
146+ }
147+
148+ var r0 ports.WindowID
149+ if returnFunc, ok := ret.Get(0).(func(Slot, uint32) ports.WindowID); ok {
150+ r0 = returnFunc(slot, xid)
151+ } else {
152+ r0 = ret.Get(0).(ports.WindowID)
153+ }
154+ return r0
155+}
156+
157+// mockhost_Created_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Created'
158+type mockhost_Created_Call struct {
159+ *mock.Call
160+}
161+
162+// Created is a helper method to define mock.On call
163+// - slot Slot
164+// - xid uint32
165+func (_e *mockhost_Expecter) Created(slot any, xid any) *mockhost_Created_Call {
166+ return &mockhost_Created_Call{Call: _e.mock.On("Created", slot, xid)}
167+}
168+
169+func (_c *mockhost_Created_Call) Run(run func(slot Slot, xid uint32)) *mockhost_Created_Call {
170+ _c.Call.Run(func(args mock.Arguments) {
171+ var arg0 Slot
172+ if args[0] != nil {
173+ arg0 = args[0].(Slot)
174+ }
175+ var arg1 uint32
176+ if args[1] != nil {
177+ arg1 = args[1].(uint32)
178+ }
179+ run(
180+ arg0,
181+ arg1,
182+ )
183+ })
184+ return _c
185+}
186+
187+func (_c *mockhost_Created_Call) Return(windowID ports.WindowID) *mockhost_Created_Call {
188+ _c.Call.Return(windowID)
189+ return _c
190+}
191+
192+func (_c *mockhost_Created_Call) RunAndReturn(run func(slot Slot, xid uint32) ports.WindowID) *mockhost_Created_Call {
193+ _c.Call.Return(run)
194+ return _c
195+}
196+
197+// Dissociate provides a mock function for the type mockhost
198+func (_mock *mockhost) Dissociate(slot Slot, destroyed bool) {
199+ _mock.Called(slot, destroyed)
200+ return
201+}
202+
203+// mockhost_Dissociate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Dissociate'
204+type mockhost_Dissociate_Call struct {
205+ *mock.Call
206+}
207+
208+// Dissociate is a helper method to define mock.On call
209+// - slot Slot
210+// - destroyed bool
211+func (_e *mockhost_Expecter) Dissociate(slot any, destroyed any) *mockhost_Dissociate_Call {
212+ return &mockhost_Dissociate_Call{Call: _e.mock.On("Dissociate", slot, destroyed)}
213+}
214+
215+func (_c *mockhost_Dissociate_Call) Run(run func(slot Slot, destroyed bool)) *mockhost_Dissociate_Call {
216+ _c.Call.Run(func(args mock.Arguments) {
217+ var arg0 Slot
218+ if args[0] != nil {
219+ arg0 = args[0].(Slot)
220+ }
221+ var arg1 bool
222+ if args[1] != nil {
223+ arg1 = args[1].(bool)
224+ }
225+ run(
226+ arg0,
227+ arg1,
228+ )
229+ })
230+ return _c
231+}
232+
233+func (_c *mockhost_Dissociate_Call) Return() *mockhost_Dissociate_Call {
234+ _c.Call.Return()
235+ return _c
236+}
237+
238+func (_c *mockhost_Dissociate_Call) RunAndReturn(run func(slot Slot, destroyed bool)) *mockhost_Dissociate_Call {
239+ _c.Run(run)
240+ return _c
241+}
242+
243+// Fullscreen provides a mock function for the type mockhost
244+func (_mock *mockhost) Fullscreen(slot Slot, on bool) {
245+ _mock.Called(slot, on)
246+ return
247+}
248+
249+// mockhost_Fullscreen_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Fullscreen'
250+type mockhost_Fullscreen_Call struct {
251+ *mock.Call
252+}
253+
254+// Fullscreen is a helper method to define mock.On call
255+// - slot Slot
256+// - on bool
257+func (_e *mockhost_Expecter) Fullscreen(slot any, on any) *mockhost_Fullscreen_Call {
258+ return &mockhost_Fullscreen_Call{Call: _e.mock.On("Fullscreen", slot, on)}
259+}
260+
261+func (_c *mockhost_Fullscreen_Call) Run(run func(slot Slot, on bool)) *mockhost_Fullscreen_Call {
262+ _c.Call.Run(func(args mock.Arguments) {
263+ var arg0 Slot
264+ if args[0] != nil {
265+ arg0 = args[0].(Slot)
266+ }
267+ var arg1 bool
268+ if args[1] != nil {
269+ arg1 = args[1].(bool)
270+ }
271+ run(
272+ arg0,
273+ arg1,
274+ )
275+ })
276+ return _c
277+}
278+
279+func (_c *mockhost_Fullscreen_Call) Return() *mockhost_Fullscreen_Call {
280+ _c.Call.Return()
281+ return _c
282+}
283+
284+func (_c *mockhost_Fullscreen_Call) RunAndReturn(run func(slot Slot, on bool)) *mockhost_Fullscreen_Call {
285+ _c.Run(run)
286+ return _c
287+}
288+
289+// Parent provides a mock function for the type mockhost
290+func (_mock *mockhost) Parent(slot Slot, parent Slot) {
291+ _mock.Called(slot, parent)
292+ return
293+}
294+
295+// mockhost_Parent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Parent'
296+type mockhost_Parent_Call struct {
297+ *mock.Call
298+}
299+
300+// Parent is a helper method to define mock.On call
301+// - slot Slot
302+// - parent Slot
303+func (_e *mockhost_Expecter) Parent(slot any, parent any) *mockhost_Parent_Call {
304+ return &mockhost_Parent_Call{Call: _e.mock.On("Parent", slot, parent)}
305+}
306+
307+func (_c *mockhost_Parent_Call) Run(run func(slot Slot, parent Slot)) *mockhost_Parent_Call {
308+ _c.Call.Run(func(args mock.Arguments) {
309+ var arg0 Slot
310+ if args[0] != nil {
311+ arg0 = args[0].(Slot)
312+ }
313+ var arg1 Slot
314+ if args[1] != nil {
315+ arg1 = args[1].(Slot)
316+ }
317+ run(
318+ arg0,
319+ arg1,
320+ )
321+ })
322+ return _c
323+}
324+
325+func (_c *mockhost_Parent_Call) Return() *mockhost_Parent_Call {
326+ _c.Call.Return()
327+ return _c
328+}
329+
330+func (_c *mockhost_Parent_Call) RunAndReturn(run func(slot Slot, parent Slot)) *mockhost_Parent_Call {
331+ _c.Run(run)
332+ return _c
333+}
334+
335+// Ready provides a mock function for the type mockhost
336+func (_mock *mockhost) Ready(slot Slot, info WindowInfo) {
337+ _mock.Called(slot, info)
338+ return
339+}
340+
341+// mockhost_Ready_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ready'
342+type mockhost_Ready_Call struct {
343+ *mock.Call
344+}
345+
346+// Ready is a helper method to define mock.On call
347+// - slot Slot
348+// - info WindowInfo
349+func (_e *mockhost_Expecter) Ready(slot any, info any) *mockhost_Ready_Call {
350+ return &mockhost_Ready_Call{Call: _e.mock.On("Ready", slot, info)}
351+}
352+
353+func (_c *mockhost_Ready_Call) Run(run func(slot Slot, info WindowInfo)) *mockhost_Ready_Call {
354+ _c.Call.Run(func(args mock.Arguments) {
355+ var arg0 Slot
356+ if args[0] != nil {
357+ arg0 = args[0].(Slot)
358+ }
359+ var arg1 WindowInfo
360+ if args[1] != nil {
361+ arg1 = args[1].(WindowInfo)
362+ }
363+ run(
364+ arg0,
365+ arg1,
366+ )
367+ })
368+ return _c
369+}
370+
371+func (_c *mockhost_Ready_Call) Return() *mockhost_Ready_Call {
372+ _c.Call.Return()
373+ return _c
374+}
375+
376+func (_c *mockhost_Ready_Call) RunAndReturn(run func(slot Slot, info WindowInfo)) *mockhost_Ready_Call {
377+ _c.Run(run)
378+ return _c
379+}
380+
381+// Title provides a mock function for the type mockhost
382+func (_mock *mockhost) Title(slot Slot, title string) {
383+ _mock.Called(slot, title)
384+ return
385+}
386+
387+// mockhost_Title_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Title'
388+type mockhost_Title_Call struct {
389+ *mock.Call
390+}
391+
392+// Title is a helper method to define mock.On call
393+// - slot Slot
394+// - title string
395+func (_e *mockhost_Expecter) Title(slot any, title any) *mockhost_Title_Call {
396+ return &mockhost_Title_Call{Call: _e.mock.On("Title", slot, title)}
397+}
398+
399+func (_c *mockhost_Title_Call) Run(run func(slot Slot, title string)) *mockhost_Title_Call {
400+ _c.Call.Run(func(args mock.Arguments) {
401+ var arg0 Slot
402+ if args[0] != nil {
403+ arg0 = args[0].(Slot)
404+ }
405+ var arg1 string
406+ if args[1] != nil {
407+ arg1 = args[1].(string)
408+ }
409+ run(
410+ arg0,
411+ arg1,
412+ )
413+ })
414+ return _c
415+}
416+
417+func (_c *mockhost_Title_Call) Return() *mockhost_Title_Call {
418+ _c.Call.Return()
419+ return _c
420+}
421+
422+func (_c *mockhost_Title_Call) RunAndReturn(run func(slot Slot, title string)) *mockhost_Title_Call {
423+ _c.Run(run)
424+ return _c
425+}
A internal/adapters/wayland/xwm/mapflow.go +21 -0
@@ -0,0 +1,21 @@
1+package xwm
2+
3+import "bnema.dev/xturbo"
4+
5+// beginMap starts the map flow of a managed window: properties, geometry,
6+// MapWindow. Also called by Attach for windows mapped before it.
7+func (x *XWM) beginMap(s Slot) { _ = s }
8+
9+func (x *XWM) onMapRequest(ev *xturbo.Event) { _ = ev }
10+func (x *XWM) onMapNotify(ev *xturbo.Event) { _ = ev }
11+func (x *XWM) onUnmapNotify(ev *xturbo.Event) { _ = ev }
12+func (x *XWM) onClientMessage(ev *xturbo.Event) { _ = ev }
13+
14+func (x *XWM) onGeomReply(s Slot, rec *record, r *xturbo.Reply) { _, _, _ = s, rec, r }
15+func (x *XWM) onClientIdsReply(s Slot, rec *record, r *xturbo.Reply) { _, _, _ = s, rec, r }
16+
17+// SetSerial records the serial of a wl_surface (xwayland_surface_v1.set_serial).
18+func (x *XWM) SetSerial(surfaceID uint64, serial uint64) { _, _ = surfaceID, serial }
19+
20+// SurfaceGone forgets a destroyed wl_surface.
21+func (x *XWM) SurfaceGone(surfaceID uint64) { _ = surfaceID }
@@ -0,0 +1,21 @@
1+package xwm
2+
3+import "bnema.dev/xturbo"
4+
5+// beginMap starts the map flow of a managed window: properties, geometry,
6+// MapWindow. Also called by Attach for windows mapped before it.
7+func (x *XWM) beginMap(s Slot) { _ = s }
8+
9+func (x *XWM) onMapRequest(ev *xturbo.Event) { _ = ev }
10+func (x *XWM) onMapNotify(ev *xturbo.Event) { _ = ev }
11+func (x *XWM) onUnmapNotify(ev *xturbo.Event) { _ = ev }
12+func (x *XWM) onClientMessage(ev *xturbo.Event) { _ = ev }
13+
14+func (x *XWM) onGeomReply(s Slot, rec *record, r *xturbo.Reply) { _, _, _ = s, rec, r }
15+func (x *XWM) onClientIdsReply(s Slot, rec *record, r *xturbo.Reply) { _, _, _ = s, rec, r }
16+
17+// SetSerial records the serial of a wl_surface (xwayland_surface_v1.set_serial).
18+func (x *XWM) SetSerial(surfaceID uint64, serial uint64) { _, _ = surfaceID, serial }
19+
20+// SurfaceGone forgets a destroyed wl_surface.
21+func (x *XWM) SurfaceGone(surfaceID uint64) { _ = surfaceID }
A internal/adapters/wayland/xwm/props.go +35 -0
@@ -0,0 +1,35 @@
1+package xwm
2+
3+import (
4+ "bnema.dev/xturbo"
5+ "bnema.dev/xturbo/xproto"
6+)
7+
8+// Property indices (second Expect argument of kindProp) and their bits in
9+// record.pending. propGeom is the GetGeometry bit.
10+const (
11+ propClass = iota
12+ propName
13+ propNetName
14+ propHints
15+ propNormalHints
16+ propProtocols
17+ propTransientFor
18+ propWindowType
19+ propState
20+ propPID
21+ propCount
22+)
23+
24+const (
25+ propGeom uint16 = 1 << propCount
26+ propMaskAll uint16 = propGeom<<1 - 1
27+)
28+
29+// onPropertyNotify handles a changed property of a managed window.
30+func (x *XWM) onPropertyNotify(ev *xturbo.Event) { _ = ev }
31+
32+// onPropReply decodes one GetProperty reply into the record.
33+func (x *XWM) onPropReply(s Slot, rec *record, r *xturbo.Reply) { _, _, _ = s, rec, r }
34+
35+var _ = xproto.AtomWmClass
@@ -0,0 +1,35 @@
1+package xwm
2+
3+import (
4+ "bnema.dev/xturbo"
5+ "bnema.dev/xturbo/xproto"
6+)
7+
8+// Property indices (second Expect argument of kindProp) and their bits in
9+// record.pending. propGeom is the GetGeometry bit.
10+const (
11+ propClass = iota
12+ propName
13+ propNetName
14+ propHints
15+ propNormalHints
16+ propProtocols
17+ propTransientFor
18+ propWindowType
19+ propState
20+ propPID
21+ propCount
22+)
23+
24+const (
25+ propGeom uint16 = 1 << propCount
26+ propMaskAll uint16 = propGeom<<1 - 1
27+)
28+
29+// onPropertyNotify handles a changed property of a managed window.
30+func (x *XWM) onPropertyNotify(ev *xturbo.Event) { _ = ev }
31+
32+// onPropReply decodes one GetProperty reply into the record.
33+func (x *XWM) onPropReply(s Slot, rec *record, r *xturbo.Reply) { _, _, _ = s, rec, r }
34+
35+var _ = xproto.AtomWmClass
A internal/adapters/wayland/xwm/table.go +226 -0
@@ -0,0 +1,226 @@
1+//lint:file-ignore U1000 the record fields below are filled by the map, property, configure and focus flows that build on this skeleton
2+
3+package xwm
4+
5+import (
6+ "math/bits"
7+
8+ "github.com/bnema/neferwl/internal/ports"
9+)
10+
11+// Record kinds.
12+const (
13+ kindFree uint8 = iota
14+ kindManaged
15+ kindUnmanaged
16+)
17+
18+const (
19+ initialSlots = 256
20+ // maxSlots bounds the table: a Slot travels in the low 16 bits of an
21+ // Expect argument.
22+ maxSlots = 1<<16 - 1
23+ indexGone = 0xFFFFFFFF // tombstone in table.index
24+)
25+
26+// pending is the configure state not yet (or last) sent to the X server.
27+type pending struct {
28+ x, y int32
29+ w, h uint32
30+ fullscreen, activated, dirty bool
31+}
32+
33+// record is one X window known to the XWM. Strings are the only per-record
34+// heap data.
35+type record struct {
36+ xid uint32
37+ id ports.WindowID
38+ kind uint8
39+ gen uint16 // bumped on insert; stale Expect replies are told apart by it
40+
41+ // X state
42+ override, mapped, mapping, ready bool
43+ x, y int32
44+ w, h uint32
45+
46+ // properties
47+ pending uint16 // propMask bits still awaited
48+ input, deleteWindow, takeFocus bool
49+ wtype uint8
50+ modal, fullscreen bool
51+ transient uint32 // xid
52+ pid int32
53+ minW, minH, maxW, maxH int32
54+ title, class string // owned, replaced only on change
55+ hasNetName bool
56+
57+ // pairing
58+ serial uint64
59+ surface uint64
60+
61+ // configure
62+ pend, sent pending
63+ stateSent uint8 // _NET_WM_STATE bits last written
64+}
65+
66+// table is a slab of records with an open-addressing index by X id. No Go map.
67+type table struct {
68+ recs []record // slab, len == cap, power of two
69+ free []uint32 // free slot indices (LIFO)
70+ index []uint32 // slot+1, 0 empty, indexGone tombstone; len == 2*len(recs)
71+ shift uint8
72+ n int
73+ tombs int
74+}
75+
76+func newTable(size int) *table {
77+ t := &table{}
78+ t.resize(size)
79+ return t
80+}
81+
82+// resize reallocates the slab, free list and index for size records and
83+// reindexes the live ones.
84+func (t *table) resize(size int) {
85+ old := t.recs
86+ t.recs = make([]record, size)
87+ copy(t.recs, old)
88+ t.free = make([]uint32, 0, size)
89+ for i := min(size, maxSlots) - 1; i >= 0; i-- {
90+ if i >= len(old) || old[i].kind == kindFree {
91+ t.free = append(t.free, uint32(i))
92+ }
93+ }
94+ t.index = make([]uint32, 2*size)
95+ t.shift = uint8(32 - bits.TrailingZeros(uint(len(t.index))))
96+ t.reindex()
97+}
98+
99+// reindex rebuilds the index from the live records, dropping tombstones.
100+// It does not allocate.
101+func (t *table) reindex() {
102+ clear(t.index)
103+ t.tombs = 0
104+ for i := range t.recs {
105+ if t.recs[i].kind != kindFree {
106+ t.indexAdd(t.recs[i].xid, uint32(i)+1)
107+ }
108+ }
109+}
110+
111+func (t *table) hash(xid uint32) uint32 { return (xid * 0x9E3779B1) >> t.shift }
112+
113+// indexAdd places slot for xid, which must be absent, in the first empty or
114+// tombstone position of its probe sequence.
115+func (t *table) indexAdd(xid, slot uint32) {
116+ mask := uint32(len(t.index) - 1)
117+ for i := t.hash(xid); ; i = (i + 1) & mask {
118+ switch t.index[i] {
119+ case 0:
120+ t.index[i] = slot
121+ return
122+ case indexGone:
123+ t.index[i] = slot
124+ t.tombs--
125+ return
126+ }
127+ }
128+}
129+
130+// grow doubles the table. It reports false at maxSlots.
131+func (t *table) grow() bool {
132+ if len(t.recs) >= 1<<16 {
133+ return false
134+ }
135+ t.resize(2 * len(t.recs))
136+ return len(t.free) > 0
137+}
138+
139+// insert returns the slot of xid, creating a managed record (generation
140+// bumped) when absent. It returns 0 when the table is full. Growth is the
141+// only allocation.
142+func (t *table) insert(xid uint32) Slot {
143+ if s := t.lookup(xid); s != 0 {
144+ return s
145+ }
146+ if len(t.free) == 0 && !t.grow() {
147+ return 0
148+ }
149+ i := t.free[len(t.free)-1]
150+ t.free = t.free[:len(t.free)-1]
151+ if t.n+t.tombs+1 > len(t.index)/4*3 { // keep an empty index cell for lookup to stop at
152+ t.reindex()
153+ }
154+ r := &t.recs[i]
155+ gen := r.gen + 1
156+ if gen == 0 {
157+ gen = 1
158+ }
159+ *r = record{xid: xid, kind: kindManaged, gen: gen, input: true}
160+ t.indexAdd(xid, i+1)
161+ t.n++
162+ return Slot(i + 1)
163+}
164+
165+// remove frees s. The record is zeroed except its generation.
166+func (t *table) remove(s Slot) {
167+ r := t.rec(s)
168+ if r == nil || r.kind == kindFree {
169+ return
170+ }
171+ mask := uint32(len(t.index) - 1)
172+ for i := t.hash(r.xid); ; i = (i + 1) & mask {
173+ v := t.index[i]
174+ if v == 0 {
175+ break
176+ }
177+ if v == uint32(s) {
178+ t.index[i] = indexGone
179+ t.tombs++
180+ break
181+ }
182+ }
183+ *r = record{gen: r.gen}
184+ t.free = append(t.free, uint32(s)-1)
185+ t.n--
186+ if t.n+t.tombs > len(t.index)/4*3 {
187+ t.reindex()
188+ }
189+}
190+
191+// lookup returns the slot of xid, or 0.
192+func (t *table) lookup(xid uint32) Slot {
193+ mask := uint32(len(t.index) - 1)
194+ for i := t.hash(xid); ; i = (i + 1) & mask {
195+ v := t.index[i]
196+ if v == 0 {
197+ return 0
198+ }
199+ if v != indexGone && t.recs[v-1].xid == xid {
200+ return Slot(v)
201+ }
202+ }
203+}
204+
205+// rec returns the record of s, or nil when s is out of range.
206+func (t *table) rec(s Slot) *record {
207+ if s == 0 || int(s) > len(t.recs) {
208+ return nil
209+ }
210+ return &t.recs[s-1]
211+}
212+
213+// tag is the Expect argument naming s: slot in the low 16 bits, generation
214+// in the high 16.
215+func (t *table) tag(s Slot) uint32 { return uint32(s) | uint32(t.recs[s-1].gen)<<16 }
216+
217+// live resolves an Expect tag. It returns nil when the slot was freed or
218+// reused since the request was sent.
219+func (t *table) live(tag uint32) (Slot, *record) {
220+ s := Slot(tag & 0xFFFF)
221+ r := t.rec(s)
222+ if r == nil || r.kind == kindFree || r.gen != uint16(tag>>16) {
223+ return 0, nil
224+ }
225+ return s, r
226+}
@@ -0,0 +1,226 @@
1+//lint:file-ignore U1000 the record fields below are filled by the map, property, configure and focus flows that build on this skeleton
2+
3+package xwm
4+
5+import (
6+ "math/bits"
7+
8+ "github.com/bnema/neferwl/internal/ports"
9+)
10+
11+// Record kinds.
12+const (
13+ kindFree uint8 = iota
14+ kindManaged
15+ kindUnmanaged
16+)
17+
18+const (
19+ initialSlots = 256
20+ // maxSlots bounds the table: a Slot travels in the low 16 bits of an
21+ // Expect argument.
22+ maxSlots = 1<<16 - 1
23+ indexGone = 0xFFFFFFFF // tombstone in table.index
24+)
25+
26+// pending is the configure state not yet (or last) sent to the X server.
27+type pending struct {
28+ x, y int32
29+ w, h uint32
30+ fullscreen, activated, dirty bool
31+}
32+
33+// record is one X window known to the XWM. Strings are the only per-record
34+// heap data.
35+type record struct {
36+ xid uint32
37+ id ports.WindowID
38+ kind uint8
39+ gen uint16 // bumped on insert; stale Expect replies are told apart by it
40+
41+ // X state
42+ override, mapped, mapping, ready bool
43+ x, y int32
44+ w, h uint32
45+
46+ // properties
47+ pending uint16 // propMask bits still awaited
48+ input, deleteWindow, takeFocus bool
49+ wtype uint8
50+ modal, fullscreen bool
51+ transient uint32 // xid
52+ pid int32
53+ minW, minH, maxW, maxH int32
54+ title, class string // owned, replaced only on change
55+ hasNetName bool
56+
57+ // pairing
58+ serial uint64
59+ surface uint64
60+
61+ // configure
62+ pend, sent pending
63+ stateSent uint8 // _NET_WM_STATE bits last written
64+}
65+
66+// table is a slab of records with an open-addressing index by X id. No Go map.
67+type table struct {
68+ recs []record // slab, len == cap, power of two
69+ free []uint32 // free slot indices (LIFO)
70+ index []uint32 // slot+1, 0 empty, indexGone tombstone; len == 2*len(recs)
71+ shift uint8
72+ n int
73+ tombs int
74+}
75+
76+func newTable(size int) *table {
77+ t := &table{}
78+ t.resize(size)
79+ return t
80+}
81+
82+// resize reallocates the slab, free list and index for size records and
83+// reindexes the live ones.
84+func (t *table) resize(size int) {
85+ old := t.recs
86+ t.recs = make([]record, size)
87+ copy(t.recs, old)
88+ t.free = make([]uint32, 0, size)
89+ for i := min(size, maxSlots) - 1; i >= 0; i-- {
90+ if i >= len(old) || old[i].kind == kindFree {
91+ t.free = append(t.free, uint32(i))
92+ }
93+ }
94+ t.index = make([]uint32, 2*size)
95+ t.shift = uint8(32 - bits.TrailingZeros(uint(len(t.index))))
96+ t.reindex()
97+}
98+
99+// reindex rebuilds the index from the live records, dropping tombstones.
100+// It does not allocate.
101+func (t *table) reindex() {
102+ clear(t.index)
103+ t.tombs = 0
104+ for i := range t.recs {
105+ if t.recs[i].kind != kindFree {
106+ t.indexAdd(t.recs[i].xid, uint32(i)+1)
107+ }
108+ }
109+}
110+
111+func (t *table) hash(xid uint32) uint32 { return (xid * 0x9E3779B1) >> t.shift }
112+
113+// indexAdd places slot for xid, which must be absent, in the first empty or
114+// tombstone position of its probe sequence.
115+func (t *table) indexAdd(xid, slot uint32) {
116+ mask := uint32(len(t.index) - 1)
117+ for i := t.hash(xid); ; i = (i + 1) & mask {
118+ switch t.index[i] {
119+ case 0:
120+ t.index[i] = slot
121+ return
122+ case indexGone:
123+ t.index[i] = slot
124+ t.tombs--
125+ return
126+ }
127+ }
128+}
129+
130+// grow doubles the table. It reports false at maxSlots.
131+func (t *table) grow() bool {
132+ if len(t.recs) >= 1<<16 {
133+ return false
134+ }
135+ t.resize(2 * len(t.recs))
136+ return len(t.free) > 0
137+}
138+
139+// insert returns the slot of xid, creating a managed record (generation
140+// bumped) when absent. It returns 0 when the table is full. Growth is the
141+// only allocation.
142+func (t *table) insert(xid uint32) Slot {
143+ if s := t.lookup(xid); s != 0 {
144+ return s
145+ }
146+ if len(t.free) == 0 && !t.grow() {
147+ return 0
148+ }
149+ i := t.free[len(t.free)-1]
150+ t.free = t.free[:len(t.free)-1]
151+ if t.n+t.tombs+1 > len(t.index)/4*3 { // keep an empty index cell for lookup to stop at
152+ t.reindex()
153+ }
154+ r := &t.recs[i]
155+ gen := r.gen + 1
156+ if gen == 0 {
157+ gen = 1
158+ }
159+ *r = record{xid: xid, kind: kindManaged, gen: gen, input: true}
160+ t.indexAdd(xid, i+1)
161+ t.n++
162+ return Slot(i + 1)
163+}
164+
165+// remove frees s. The record is zeroed except its generation.
166+func (t *table) remove(s Slot) {
167+ r := t.rec(s)
168+ if r == nil || r.kind == kindFree {
169+ return
170+ }
171+ mask := uint32(len(t.index) - 1)
172+ for i := t.hash(r.xid); ; i = (i + 1) & mask {
173+ v := t.index[i]
174+ if v == 0 {
175+ break
176+ }
177+ if v == uint32(s) {
178+ t.index[i] = indexGone
179+ t.tombs++
180+ break
181+ }
182+ }
183+ *r = record{gen: r.gen}
184+ t.free = append(t.free, uint32(s)-1)
185+ t.n--
186+ if t.n+t.tombs > len(t.index)/4*3 {
187+ t.reindex()
188+ }
189+}
190+
191+// lookup returns the slot of xid, or 0.
192+func (t *table) lookup(xid uint32) Slot {
193+ mask := uint32(len(t.index) - 1)
194+ for i := t.hash(xid); ; i = (i + 1) & mask {
195+ v := t.index[i]
196+ if v == 0 {
197+ return 0
198+ }
199+ if v != indexGone && t.recs[v-1].xid == xid {
200+ return Slot(v)
201+ }
202+ }
203+}
204+
205+// rec returns the record of s, or nil when s is out of range.
206+func (t *table) rec(s Slot) *record {
207+ if s == 0 || int(s) > len(t.recs) {
208+ return nil
209+ }
210+ return &t.recs[s-1]
211+}
212+
213+// tag is the Expect argument naming s: slot in the low 16 bits, generation
214+// in the high 16.
215+func (t *table) tag(s Slot) uint32 { return uint32(s) | uint32(t.recs[s-1].gen)<<16 }
216+
217+// live resolves an Expect tag. It returns nil when the slot was freed or
218+// reused since the request was sent.
219+func (t *table) live(tag uint32) (Slot, *record) {
220+ s := Slot(tag & 0xFFFF)
221+ r := t.rec(s)
222+ if r == nil || r.kind == kindFree || r.gen != uint16(tag>>16) {
223+ return 0, nil
224+ }
225+ return s, r
226+}
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+}
@@ -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/xpeer_test.go +411 -0
@@ -0,0 +1,411 @@
1+package xwm
2+
3+import (
4+ "encoding/binary"
5+ "io"
6+ "os"
7+ "sync"
8+ "sync/atomic"
9+ "testing"
10+ "time"
11+
12+ "golang.org/x/sys/unix"
13+)
14+
15+// Opcodes the peer understands.
16+const (
17+ opCreateWindow = 1
18+ opChangeWindowAttributes = 2
19+ opGetWindowAttributes = 3
20+ opMapWindow = 8
21+ opConfigureWindow = 12
22+ opGetGeometry = 14
23+ opQueryTree = 15
24+ opInternAtom = 16
25+ opChangeProperty = 18
26+ opGetProperty = 20
27+ opSetSelectionOwner = 22
28+ opSendEvent = 25
29+ opSetInputFocus = 42
30+ opGetInputFocus = 43
31+ opQueryExtension = 98
32+ opKillClient = 113
33+)
34+
35+const (
36+ peerRoot = 0x100
37+ peerXIDBase = 0x200000
38+ peerXIDMask = 0x1fffff
39+ peerMaxWords = 0x100000
40+)
41+
42+type xpeerExt struct{ major, firstEvent, firstError uint8 }
43+
44+// xpeerReq is one request the peer received.
45+type xpeerReq struct {
46+ seq uint16
47+ major uint8
48+ minor uint8
49+ raw []byte // whole request, header included
50+}
51+
52+func (r xpeerReq) u32(off int) uint32 { return binary.LittleEndian.Uint32(r.raw[off:]) }
53+
54+type xpeerProp struct {
55+ typ uint32
56+ format uint8
57+ data []byte
58+}
59+
60+type xpeerGeom struct {
61+ x, y int16
62+ w, h, border uint16
63+}
64+
65+// xpeer is a scripted X server speaking the real wire protocol over a
66+// socketpair: setup, extension queries, atoms, properties, geometry, window
67+// tree. It records every request. It is a protocol peer, not a test double of
68+// a project interface.
69+type xpeer struct {
70+ t *testing.T
71+ f *os.File
72+
73+ mu sync.Mutex
74+ seq uint16
75+ reqs []xpeerReq
76+ exts map[string]xpeerExt
77+ atoms map[string]uint32
78+ nextAtom uint32
79+ props map[uint32]map[uint32]xpeerProp
80+ geoms map[uint32]xpeerGeom
81+ attrs map[uint32][2]byte // window -> {overrideRedirect, mapState}
82+ children []uint32
83+ denyRoot bool // answer the root event-mask request with BadAccess
84+
85+ gate atomic.Pointer[chan struct{}] // closed = reading allowed
86+ done chan struct{}
87+}
88+
89+func fullPeerExts() map[string]xpeerExt {
90+ return map[string]xpeerExt{
91+ "BIG-REQUESTS": {133, 0, 0},
92+ "XC-MISC": {136, 0, 0},
93+ "Composite": {142, 0, 0},
94+ "XFIXES": {138, 87, 140},
95+ "X-Resource": {144, 0, 0},
96+ }
97+}
98+
99+// startXPeer returns the client end of a socketpair, to give to Boot, and the
100+// peer serving the other end.
101+func startXPeer(t *testing.T, exts map[string]xpeerExt) (int, *xpeer) {
102+ t.Helper()
103+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
104+ if err != nil {
105+ t.Fatal(err)
106+ }
107+ p := &xpeer{
108+ t: t, f: os.NewFile(uintptr(fds[1]), "xpeer"),
109+ exts: exts, atoms: map[string]uint32{}, nextAtom: 1000,
110+ props: map[uint32]map[uint32]xpeerProp{}, geoms: map[uint32]xpeerGeom{}, attrs: map[uint32][2]byte{},
111+ done: make(chan struct{}),
112+ }
113+ open := make(chan struct{})
114+ close(open)
115+ p.gate.Store(&open)
116+ t.Cleanup(func() {
117+ p.f.Close()
118+ <-p.done
119+ })
120+ go p.serve()
121+ return fds[0], p
122+}
123+
124+func (p *xpeer) setupReply() []byte {
125+ vendor := []byte("xpeer\x00\x00\x00")
126+ b := make([]byte, 40+len(vendor)+40)
127+ b[0], b[2] = 1, 11
128+ binary.LittleEndian.PutUint16(b[6:], uint16((len(b)-8)/4))
129+ binary.LittleEndian.PutUint32(b[12:], peerXIDBase)
130+ binary.LittleEndian.PutUint32(b[16:], peerXIDMask)
131+ binary.LittleEndian.PutUint16(b[24:], 5)
132+ b[28] = 1
133+ copy(b[40:], vendor)
134+ scr := b[40+len(vendor):]
135+ binary.LittleEndian.PutUint32(scr, peerRoot)
136+ binary.LittleEndian.PutUint32(scr[32:], 0x22) // root visual
137+ scr[38] = 24 // root depth
138+ return b
139+}
140+
141+func (p *xpeer) serve() {
142+ defer close(p.done)
143+ var hdr [12]byte
144+ if _, err := io.ReadFull(p.f, hdr[:]); err != nil {
145+ return
146+ }
147+ // Auth name and data are empty (NewConn sends none).
148+ if _, err := p.f.Write(p.setupReply()); err != nil {
149+ return
150+ }
151+ for {
152+ <-*p.gate.Load()
153+ var h [4]byte
154+ if _, err := io.ReadFull(p.f, h[:]); err != nil {
155+ return
156+ }
157+ raw := h[:]
158+ n := int(binary.LittleEndian.Uint16(h[2:])) * 4
159+ if n == 0 { // BIG-REQUESTS header
160+ var l [4]byte
161+ if _, err := io.ReadFull(p.f, l[:]); err != nil {
162+ return
163+ }
164+ raw = append(raw, l[:]...)
165+ n = int(binary.LittleEndian.Uint32(l[:])) * 4
166+ }
167+ rest := make([]byte, n-len(raw))
168+ if _, err := io.ReadFull(p.f, rest); err != nil {
169+ return
170+ }
171+ raw = append(append([]byte(nil), raw...), rest...)
172+ p.mu.Lock()
173+ p.seq++
174+ r := xpeerReq{p.seq, h[0], h[1], raw}
175+ p.reqs = append(p.reqs, r)
176+ p.answer(r, raw[len(raw)-len(rest):])
177+ p.mu.Unlock()
178+ }
179+}
180+
181+// write sends bytes to the client. The caller holds p.mu.
182+func (p *xpeer) write(b []byte) { _, _ = p.f.Write(b) }
183+
184+func (p *xpeer) reply(data uint8, body []byte) {
185+ words := (max(len(body), 24) - 24 + 3) / 4
186+ b := make([]byte, 32+4*words)
187+ b[0], b[1] = 1, data
188+ binary.LittleEndian.PutUint16(b[2:], p.seq)
189+ binary.LittleEndian.PutUint32(b[4:], uint32(words))
190+ copy(b[8:], body)
191+ p.write(b)
192+}
193+
194+func (p *xpeer) errorPacket(code, major uint8, bad uint32) {
195+ b := make([]byte, 32)
196+ b[1] = code
197+ binary.LittleEndian.PutUint16(b[2:], p.seq)
198+ binary.LittleEndian.PutUint32(b[4:], bad)
199+ b[10] = major
200+ p.write(b)
201+}
202+
203+func (p *xpeer) extName(major uint8) string {
204+ for name, e := range p.exts {
205+ if e.major == major {
206+ return name
207+ }
208+ }
209+ return ""
210+}
211+
212+func le32(v ...uint32) []byte {
213+ b := make([]byte, 0, 4*len(v))
214+ for _, x := range v {
215+ b = binary.LittleEndian.AppendUint32(b, x)
216+ }
217+ return b
218+}
219+
220+// answer runs with p.mu held.
221+func (p *xpeer) answer(r xpeerReq, body []byte) {
222+ switch {
223+ case r.major == opQueryExtension:
224+ e, ok := p.exts[string(body[4:4+binary.LittleEndian.Uint16(body)])]
225+ var present uint8
226+ if ok {
227+ present = 1
228+ }
229+ p.reply(0, []byte{present, e.major, e.firstEvent, e.firstError})
230+ case r.major == opInternAtom:
231+ name := string(body[4 : 4+binary.LittleEndian.Uint16(body)])
232+ a, ok := p.atoms[name]
233+ if !ok {
234+ p.nextAtom++
235+ a = p.nextAtom
236+ p.atoms[name] = a
237+ }
238+ p.reply(0, le32(a))
239+ case r.major == opGetInputFocus:
240+ p.reply(1, le32(0))
241+ case r.major == opQueryTree:
242+ b := make([]byte, 24+4*len(p.children))
243+ binary.LittleEndian.PutUint32(b[0:], peerRoot)
244+ binary.LittleEndian.PutUint16(b[8:], uint16(len(p.children)))
245+ for i, c := range p.children {
246+ binary.LittleEndian.PutUint32(b[24+4*i:], c)
247+ }
248+ p.reply(0, b)
249+ case r.major == opGetWindowAttributes:
250+ a := p.attrs[binary.LittleEndian.Uint32(body)]
251+ b := make([]byte, 36) // reply length 3
252+ b[18] = a[1] // map_state is byte 26 of the reply
253+ b[19] = a[0] // override_redirect, byte 27
254+ p.reply(0, b)
255+ case r.major == opGetGeometry:
256+ g := p.geoms[binary.LittleEndian.Uint32(body)]
257+ b := make([]byte, 24)
258+ binary.LittleEndian.PutUint32(b[0:], peerRoot)
259+ binary.LittleEndian.PutUint16(b[4:], uint16(g.x))
260+ binary.LittleEndian.PutUint16(b[6:], uint16(g.y))
261+ binary.LittleEndian.PutUint16(b[8:], g.w)
262+ binary.LittleEndian.PutUint16(b[10:], g.h)
263+ binary.LittleEndian.PutUint16(b[12:], g.border)
264+ p.reply(24, b)
265+ case r.major == opChangeProperty:
266+ win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:])
267+ format, n := body[12], int(binary.LittleEndian.Uint32(body[16:]))
268+ if p.props[win] == nil {
269+ p.props[win] = map[uint32]xpeerProp{}
270+ }
271+ p.props[win][prop] = xpeerProp{
272+ typ: binary.LittleEndian.Uint32(body[8:]), format: format,
273+ data: append([]byte(nil), body[20:20+n*int(format)/8]...),
274+ }
275+ case r.major == opGetProperty:
276+ win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:])
277+ v, ok := p.props[win][prop]
278+ b := make([]byte, 24+(len(v.data)+3)&^3)
279+ if ok && v.format != 0 {
280+ binary.LittleEndian.PutUint32(b[0:], v.typ)
281+ binary.LittleEndian.PutUint32(b[8:], uint32(len(v.data)/(int(v.format)/8)))
282+ copy(b[24:], v.data)
283+ }
284+ p.reply(v.format, b)
285+ case r.major == opChangeWindowAttributes:
286+ const cwEventMask = 1 << 11
287+ if p.denyRoot && binary.LittleEndian.Uint32(body) == peerRoot && binary.LittleEndian.Uint32(body[4:])&cwEventMask != 0 {
288+ p.errorPacket(10, opChangeWindowAttributes, peerRoot) // BadAccess
289+ }
290+ case r.major >= 128:
291+ switch p.extName(r.major) {
292+ case "BIG-REQUESTS":
293+ p.reply(0, le32(peerMaxWords))
294+ case "XC-MISC":
295+ if r.minor == 1 {
296+ p.reply(0, le32(0, 1))
297+ }
298+ case "Composite", "XFIXES":
299+ if r.minor == 0 {
300+ p.reply(0, le32(5, 4))
301+ }
302+ case "X-Resource":
303+ if r.minor == 0 {
304+ p.reply(0, []byte{1, 0, 2, 0})
305+ }
306+ }
307+ }
308+}
309+
310+// requests returns a snapshot of every request received after setup.
311+func (p *xpeer) requests() []xpeerReq {
312+ p.mu.Lock()
313+ defer p.mu.Unlock()
314+ return append([]xpeerReq(nil), p.reqs...)
315+}
316+
317+// waitReq waits for a request satisfying match among those after the first
318+// skip requests, and returns it with the new count.
319+func (p *xpeer) waitReq(skip int, match func(xpeerReq) bool) (xpeerReq, int) {
320+ p.t.Helper()
321+ deadline := time.Now().Add(5 * time.Second)
322+ for time.Now().Before(deadline) {
323+ reqs := p.requests()
324+ for i := skip; i < len(reqs); i++ {
325+ if match(reqs[i]) {
326+ return reqs[i], i + 1
327+ }
328+ }
329+ time.Sleep(time.Millisecond)
330+ }
331+ p.t.Fatal("xpeer: expected request never arrived")
332+ return xpeerReq{}, 0
333+}
334+
335+func (p *xpeer) atom(name string) uint32 {
336+ p.mu.Lock()
337+ defer p.mu.Unlock()
338+ return p.atoms[name]
339+}
340+
341+func (p *xpeer) prop(win uint32, atom string) (xpeerProp, bool) {
342+ p.mu.Lock()
343+ defer p.mu.Unlock()
344+ v, ok := p.props[win][p.atoms[atom]]
345+ return v, ok
346+}
347+
348+// event sends a 32-byte core event stamped with the current sequence number.
349+func (p *xpeer) event(code uint8, fill func(b *[32]byte)) {
350+ p.mu.Lock()
351+ defer p.mu.Unlock()
352+ var b [32]byte
353+ b[0] = code
354+ binary.LittleEndian.PutUint16(b[2:], p.seq)
355+ if fill != nil {
356+ fill(&b)
357+ }
358+ p.write(b[:])
359+}
360+
361+func (p *xpeer) createNotify(win uint32, override bool) {
362+ p.event(16, func(b *[32]byte) {
363+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
364+ binary.LittleEndian.PutUint32(b[8:], win)
365+ binary.LittleEndian.PutUint16(b[16:], 100)
366+ binary.LittleEndian.PutUint16(b[18:], 50)
367+ if override {
368+ b[22] = 1
369+ }
370+ })
371+}
372+
373+func (p *xpeer) destroyNotify(win uint32) {
374+ p.event(17, func(b *[32]byte) {
375+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
376+ binary.LittleEndian.PutUint32(b[8:], win)
377+ })
378+}
379+
380+// pause makes the peer stop reading after the request it is serving.
381+func (p *xpeer) pause() {
382+ g := make(chan struct{})
383+ p.gate.Store(&g)
384+}
385+
386+// resume lets the peer read again.
387+func (p *xpeer) resume() {
388+ g := *p.gate.Load()
389+ open := make(chan struct{})
390+ close(open)
391+ p.gate.Store(&open)
392+ select {
393+ case <-g:
394+ default:
395+ close(g)
396+ }
397+}
398+
399+// set mutates the peer's script under its lock.
400+func (p *xpeer) set(fn func(p *xpeer)) {
401+ p.mu.Lock()
402+ defer p.mu.Unlock()
403+ fn(p)
404+}
405+
406+// hangup closes the server end; the client reads EOF.
407+func (p *xpeer) hangup() {
408+ if rc, err := p.f.SyscallConn(); err == nil {
409+ _ = rc.Control(func(fd uintptr) { _ = unix.Shutdown(int(fd), unix.SHUT_RDWR) })
410+ }
411+}
@@ -0,0 +1,411 @@
1+package xwm
2+
3+import (
4+ "encoding/binary"
5+ "io"
6+ "os"
7+ "sync"
8+ "sync/atomic"
9+ "testing"
10+ "time"
11+
12+ "golang.org/x/sys/unix"
13+)
14+
15+// Opcodes the peer understands.
16+const (
17+ opCreateWindow = 1
18+ opChangeWindowAttributes = 2
19+ opGetWindowAttributes = 3
20+ opMapWindow = 8
21+ opConfigureWindow = 12
22+ opGetGeometry = 14
23+ opQueryTree = 15
24+ opInternAtom = 16
25+ opChangeProperty = 18
26+ opGetProperty = 20
27+ opSetSelectionOwner = 22
28+ opSendEvent = 25
29+ opSetInputFocus = 42
30+ opGetInputFocus = 43
31+ opQueryExtension = 98
32+ opKillClient = 113
33+)
34+
35+const (
36+ peerRoot = 0x100
37+ peerXIDBase = 0x200000
38+ peerXIDMask = 0x1fffff
39+ peerMaxWords = 0x100000
40+)
41+
42+type xpeerExt struct{ major, firstEvent, firstError uint8 }
43+
44+// xpeerReq is one request the peer received.
45+type xpeerReq struct {
46+ seq uint16
47+ major uint8
48+ minor uint8
49+ raw []byte // whole request, header included
50+}
51+
52+func (r xpeerReq) u32(off int) uint32 { return binary.LittleEndian.Uint32(r.raw[off:]) }
53+
54+type xpeerProp struct {
55+ typ uint32
56+ format uint8
57+ data []byte
58+}
59+
60+type xpeerGeom struct {
61+ x, y int16
62+ w, h, border uint16
63+}
64+
65+// xpeer is a scripted X server speaking the real wire protocol over a
66+// socketpair: setup, extension queries, atoms, properties, geometry, window
67+// tree. It records every request. It is a protocol peer, not a test double of
68+// a project interface.
69+type xpeer struct {
70+ t *testing.T
71+ f *os.File
72+
73+ mu sync.Mutex
74+ seq uint16
75+ reqs []xpeerReq
76+ exts map[string]xpeerExt
77+ atoms map[string]uint32
78+ nextAtom uint32
79+ props map[uint32]map[uint32]xpeerProp
80+ geoms map[uint32]xpeerGeom
81+ attrs map[uint32][2]byte // window -> {overrideRedirect, mapState}
82+ children []uint32
83+ denyRoot bool // answer the root event-mask request with BadAccess
84+
85+ gate atomic.Pointer[chan struct{}] // closed = reading allowed
86+ done chan struct{}
87+}
88+
89+func fullPeerExts() map[string]xpeerExt {
90+ return map[string]xpeerExt{
91+ "BIG-REQUESTS": {133, 0, 0},
92+ "XC-MISC": {136, 0, 0},
93+ "Composite": {142, 0, 0},
94+ "XFIXES": {138, 87, 140},
95+ "X-Resource": {144, 0, 0},
96+ }
97+}
98+
99+// startXPeer returns the client end of a socketpair, to give to Boot, and the
100+// peer serving the other end.
101+func startXPeer(t *testing.T, exts map[string]xpeerExt) (int, *xpeer) {
102+ t.Helper()
103+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
104+ if err != nil {
105+ t.Fatal(err)
106+ }
107+ p := &xpeer{
108+ t: t, f: os.NewFile(uintptr(fds[1]), "xpeer"),
109+ exts: exts, atoms: map[string]uint32{}, nextAtom: 1000,
110+ props: map[uint32]map[uint32]xpeerProp{}, geoms: map[uint32]xpeerGeom{}, attrs: map[uint32][2]byte{},
111+ done: make(chan struct{}),
112+ }
113+ open := make(chan struct{})
114+ close(open)
115+ p.gate.Store(&open)
116+ t.Cleanup(func() {
117+ p.f.Close()
118+ <-p.done
119+ })
120+ go p.serve()
121+ return fds[0], p
122+}
123+
124+func (p *xpeer) setupReply() []byte {
125+ vendor := []byte("xpeer\x00\x00\x00")
126+ b := make([]byte, 40+len(vendor)+40)
127+ b[0], b[2] = 1, 11
128+ binary.LittleEndian.PutUint16(b[6:], uint16((len(b)-8)/4))
129+ binary.LittleEndian.PutUint32(b[12:], peerXIDBase)
130+ binary.LittleEndian.PutUint32(b[16:], peerXIDMask)
131+ binary.LittleEndian.PutUint16(b[24:], 5)
132+ b[28] = 1
133+ copy(b[40:], vendor)
134+ scr := b[40+len(vendor):]
135+ binary.LittleEndian.PutUint32(scr, peerRoot)
136+ binary.LittleEndian.PutUint32(scr[32:], 0x22) // root visual
137+ scr[38] = 24 // root depth
138+ return b
139+}
140+
141+func (p *xpeer) serve() {
142+ defer close(p.done)
143+ var hdr [12]byte
144+ if _, err := io.ReadFull(p.f, hdr[:]); err != nil {
145+ return
146+ }
147+ // Auth name and data are empty (NewConn sends none).
148+ if _, err := p.f.Write(p.setupReply()); err != nil {
149+ return
150+ }
151+ for {
152+ <-*p.gate.Load()
153+ var h [4]byte
154+ if _, err := io.ReadFull(p.f, h[:]); err != nil {
155+ return
156+ }
157+ raw := h[:]
158+ n := int(binary.LittleEndian.Uint16(h[2:])) * 4
159+ if n == 0 { // BIG-REQUESTS header
160+ var l [4]byte
161+ if _, err := io.ReadFull(p.f, l[:]); err != nil {
162+ return
163+ }
164+ raw = append(raw, l[:]...)
165+ n = int(binary.LittleEndian.Uint32(l[:])) * 4
166+ }
167+ rest := make([]byte, n-len(raw))
168+ if _, err := io.ReadFull(p.f, rest); err != nil {
169+ return
170+ }
171+ raw = append(append([]byte(nil), raw...), rest...)
172+ p.mu.Lock()
173+ p.seq++
174+ r := xpeerReq{p.seq, h[0], h[1], raw}
175+ p.reqs = append(p.reqs, r)
176+ p.answer(r, raw[len(raw)-len(rest):])
177+ p.mu.Unlock()
178+ }
179+}
180+
181+// write sends bytes to the client. The caller holds p.mu.
182+func (p *xpeer) write(b []byte) { _, _ = p.f.Write(b) }
183+
184+func (p *xpeer) reply(data uint8, body []byte) {
185+ words := (max(len(body), 24) - 24 + 3) / 4
186+ b := make([]byte, 32+4*words)
187+ b[0], b[1] = 1, data
188+ binary.LittleEndian.PutUint16(b[2:], p.seq)
189+ binary.LittleEndian.PutUint32(b[4:], uint32(words))
190+ copy(b[8:], body)
191+ p.write(b)
192+}
193+
194+func (p *xpeer) errorPacket(code, major uint8, bad uint32) {
195+ b := make([]byte, 32)
196+ b[1] = code
197+ binary.LittleEndian.PutUint16(b[2:], p.seq)
198+ binary.LittleEndian.PutUint32(b[4:], bad)
199+ b[10] = major
200+ p.write(b)
201+}
202+
203+func (p *xpeer) extName(major uint8) string {
204+ for name, e := range p.exts {
205+ if e.major == major {
206+ return name
207+ }
208+ }
209+ return ""
210+}
211+
212+func le32(v ...uint32) []byte {
213+ b := make([]byte, 0, 4*len(v))
214+ for _, x := range v {
215+ b = binary.LittleEndian.AppendUint32(b, x)
216+ }
217+ return b
218+}
219+
220+// answer runs with p.mu held.
221+func (p *xpeer) answer(r xpeerReq, body []byte) {
222+ switch {
223+ case r.major == opQueryExtension:
224+ e, ok := p.exts[string(body[4:4+binary.LittleEndian.Uint16(body)])]
225+ var present uint8
226+ if ok {
227+ present = 1
228+ }
229+ p.reply(0, []byte{present, e.major, e.firstEvent, e.firstError})
230+ case r.major == opInternAtom:
231+ name := string(body[4 : 4+binary.LittleEndian.Uint16(body)])
232+ a, ok := p.atoms[name]
233+ if !ok {
234+ p.nextAtom++
235+ a = p.nextAtom
236+ p.atoms[name] = a
237+ }
238+ p.reply(0, le32(a))
239+ case r.major == opGetInputFocus:
240+ p.reply(1, le32(0))
241+ case r.major == opQueryTree:
242+ b := make([]byte, 24+4*len(p.children))
243+ binary.LittleEndian.PutUint32(b[0:], peerRoot)
244+ binary.LittleEndian.PutUint16(b[8:], uint16(len(p.children)))
245+ for i, c := range p.children {
246+ binary.LittleEndian.PutUint32(b[24+4*i:], c)
247+ }
248+ p.reply(0, b)
249+ case r.major == opGetWindowAttributes:
250+ a := p.attrs[binary.LittleEndian.Uint32(body)]
251+ b := make([]byte, 36) // reply length 3
252+ b[18] = a[1] // map_state is byte 26 of the reply
253+ b[19] = a[0] // override_redirect, byte 27
254+ p.reply(0, b)
255+ case r.major == opGetGeometry:
256+ g := p.geoms[binary.LittleEndian.Uint32(body)]
257+ b := make([]byte, 24)
258+ binary.LittleEndian.PutUint32(b[0:], peerRoot)
259+ binary.LittleEndian.PutUint16(b[4:], uint16(g.x))
260+ binary.LittleEndian.PutUint16(b[6:], uint16(g.y))
261+ binary.LittleEndian.PutUint16(b[8:], g.w)
262+ binary.LittleEndian.PutUint16(b[10:], g.h)
263+ binary.LittleEndian.PutUint16(b[12:], g.border)
264+ p.reply(24, b)
265+ case r.major == opChangeProperty:
266+ win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:])
267+ format, n := body[12], int(binary.LittleEndian.Uint32(body[16:]))
268+ if p.props[win] == nil {
269+ p.props[win] = map[uint32]xpeerProp{}
270+ }
271+ p.props[win][prop] = xpeerProp{
272+ typ: binary.LittleEndian.Uint32(body[8:]), format: format,
273+ data: append([]byte(nil), body[20:20+n*int(format)/8]...),
274+ }
275+ case r.major == opGetProperty:
276+ win, prop := binary.LittleEndian.Uint32(body), binary.LittleEndian.Uint32(body[4:])
277+ v, ok := p.props[win][prop]
278+ b := make([]byte, 24+(len(v.data)+3)&^3)
279+ if ok && v.format != 0 {
280+ binary.LittleEndian.PutUint32(b[0:], v.typ)
281+ binary.LittleEndian.PutUint32(b[8:], uint32(len(v.data)/(int(v.format)/8)))
282+ copy(b[24:], v.data)
283+ }
284+ p.reply(v.format, b)
285+ case r.major == opChangeWindowAttributes:
286+ const cwEventMask = 1 << 11
287+ if p.denyRoot && binary.LittleEndian.Uint32(body) == peerRoot && binary.LittleEndian.Uint32(body[4:])&cwEventMask != 0 {
288+ p.errorPacket(10, opChangeWindowAttributes, peerRoot) // BadAccess
289+ }
290+ case r.major >= 128:
291+ switch p.extName(r.major) {
292+ case "BIG-REQUESTS":
293+ p.reply(0, le32(peerMaxWords))
294+ case "XC-MISC":
295+ if r.minor == 1 {
296+ p.reply(0, le32(0, 1))
297+ }
298+ case "Composite", "XFIXES":
299+ if r.minor == 0 {
300+ p.reply(0, le32(5, 4))
301+ }
302+ case "X-Resource":
303+ if r.minor == 0 {
304+ p.reply(0, []byte{1, 0, 2, 0})
305+ }
306+ }
307+ }
308+}
309+
310+// requests returns a snapshot of every request received after setup.
311+func (p *xpeer) requests() []xpeerReq {
312+ p.mu.Lock()
313+ defer p.mu.Unlock()
314+ return append([]xpeerReq(nil), p.reqs...)
315+}
316+
317+// waitReq waits for a request satisfying match among those after the first
318+// skip requests, and returns it with the new count.
319+func (p *xpeer) waitReq(skip int, match func(xpeerReq) bool) (xpeerReq, int) {
320+ p.t.Helper()
321+ deadline := time.Now().Add(5 * time.Second)
322+ for time.Now().Before(deadline) {
323+ reqs := p.requests()
324+ for i := skip; i < len(reqs); i++ {
325+ if match(reqs[i]) {
326+ return reqs[i], i + 1
327+ }
328+ }
329+ time.Sleep(time.Millisecond)
330+ }
331+ p.t.Fatal("xpeer: expected request never arrived")
332+ return xpeerReq{}, 0
333+}
334+
335+func (p *xpeer) atom(name string) uint32 {
336+ p.mu.Lock()
337+ defer p.mu.Unlock()
338+ return p.atoms[name]
339+}
340+
341+func (p *xpeer) prop(win uint32, atom string) (xpeerProp, bool) {
342+ p.mu.Lock()
343+ defer p.mu.Unlock()
344+ v, ok := p.props[win][p.atoms[atom]]
345+ return v, ok
346+}
347+
348+// event sends a 32-byte core event stamped with the current sequence number.
349+func (p *xpeer) event(code uint8, fill func(b *[32]byte)) {
350+ p.mu.Lock()
351+ defer p.mu.Unlock()
352+ var b [32]byte
353+ b[0] = code
354+ binary.LittleEndian.PutUint16(b[2:], p.seq)
355+ if fill != nil {
356+ fill(&b)
357+ }
358+ p.write(b[:])
359+}
360+
361+func (p *xpeer) createNotify(win uint32, override bool) {
362+ p.event(16, func(b *[32]byte) {
363+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
364+ binary.LittleEndian.PutUint32(b[8:], win)
365+ binary.LittleEndian.PutUint16(b[16:], 100)
366+ binary.LittleEndian.PutUint16(b[18:], 50)
367+ if override {
368+ b[22] = 1
369+ }
370+ })
371+}
372+
373+func (p *xpeer) destroyNotify(win uint32) {
374+ p.event(17, func(b *[32]byte) {
375+ binary.LittleEndian.PutUint32(b[4:], peerRoot)
376+ binary.LittleEndian.PutUint32(b[8:], win)
377+ })
378+}
379+
380+// pause makes the peer stop reading after the request it is serving.
381+func (p *xpeer) pause() {
382+ g := make(chan struct{})
383+ p.gate.Store(&g)
384+}
385+
386+// resume lets the peer read again.
387+func (p *xpeer) resume() {
388+ g := *p.gate.Load()
389+ open := make(chan struct{})
390+ close(open)
391+ p.gate.Store(&open)
392+ select {
393+ case <-g:
394+ default:
395+ close(g)
396+ }
397+}
398+
399+// set mutates the peer's script under its lock.
400+func (p *xpeer) set(fn func(p *xpeer)) {
401+ p.mu.Lock()
402+ defer p.mu.Unlock()
403+ fn(p)
404+}
405+
406+// hangup closes the server end; the client reads EOF.
407+func (p *xpeer) hangup() {
408+ if rc, err := p.f.SyscallConn(); err == nil {
409+ _ = rc.Control(func(fd uintptr) { _ = unix.Shutdown(int(fd), unix.SHUT_RDWR) })
410+ }
411+}
A internal/adapters/wayland/xwm/xwm.go +379 -0
@@ -0,0 +1,379 @@
1+//lint:file-ignore U1000 pairing, focus and stacking state is used by the flows that build on this skeleton
2+
3+// Package xwm is the built-in X11 window manager for Xwayland: it speaks the
4+// X protocol over the -wm socket with xturbo and reports managed windows to
5+// the Wayland server through the unexported host interface.
6+//
7+// Threading: Boot runs on the starter goroutine and blocks. The XWM it returns
8+// is then handed to the display goroutine, which owns it exclusively; nothing
9+// in this package is locked.
10+package xwm
11+
12+import (
13+ "encoding/binary"
14+ "errors"
15+ "fmt"
16+
17+ "bnema.dev/xturbo"
18+ "bnema.dev/xturbo/composite"
19+ "bnema.dev/xturbo/res"
20+ "bnema.dev/xturbo/xfixes"
21+ "bnema.dev/xturbo/xproto"
22+ "github.com/bnema/purego-libwayland/server"
23+ "github.com/bnema/zerowrap"
24+)
25+
26+var (
27+ // ErrWMExists is returned by Boot when another client already redirects
28+ // the root window's children.
29+ ErrWMExists = errors.New("xwm: another window manager owns the root window")
30+ // ErrNoComposite is returned by Boot when the X server lacks Composite.
31+ ErrNoComposite = errors.New("xwm: X server has no Composite extension")
32+)
33+
34+// Stats are connection and event counters, for diagnostics.
35+type Stats struct {
36+ Events, Replies, Errors, Unattributed, Stale, RingFull, LegacyID, UnmanagedSeen uint64
37+}
38+
39+// Version request ids, carried in the first Expect argument of kindVersion.
40+const (
41+ versionComposite uint32 = iota + 1
42+ versionXFixes
43+ versionRes
44+)
45+
46+const unpairedCap = 64
47+
48+// XWM manages the windows of one Xwayland. Display goroutine only, after Boot.
49+type XWM struct {
50+ conn *xturbo.Conn
51+ log zerowrap.Logger
52+ root uint32
53+ wmWin uint32
54+ atoms [atomCount]uint32
55+ tab *table
56+ h xturbo.Handler
57+ stats Stats
58+
59+ // attachment
60+ host host
61+ src fdSource
62+ post func(func())
63+ flushFn func()
64+
65+ posted, writable, closed bool
66+ bootErr error
67+ warnedRing, warnedFull bool
68+
69+ scratch []byte // encode buffer for 32-bit property values, 4 bytes per slot
70+
71+ // Shared by the map, configure and focus flows.
72+ dirty []Slot // slots with a pending configure, capacity == table size
73+ clients []uint32 // _NET_CLIENT_LIST, map order
74+ stacking []uint32 // _NET_CLIENT_LIST_STACKING, bottom to top
75+ active Slot // slot named by _NET_ACTIVE_WINDOW, 0 = none
76+ refocus bool // a FocusIn was denied; focus is re-issued in flush
77+ listsDirty uint8 // client lists to rewrite in flush
78+ unpaired [unpairedCap]struct{ serial, surface uint64 }
79+ unpairedN int
80+}
81+
82+func newXWM(conn *xturbo.Conn, log zerowrap.Logger) *XWM {
83+ x := &XWM{conn: conn, log: log, root: conn.Root(), tab: newTable(initialSlots)}
84+ x.h = xturbo.Handler{OnEvent: x.onEvent, OnReply: x.onReply, OnError: x.onError}
85+ x.fit()
86+ return x
87+}
88+
89+// fit sizes the per-slot buffers to the table; called after every insert.
90+func (x *XWM) fit() {
91+ n := len(x.tab.recs)
92+ if cap(x.dirty) < n {
93+ x.dirty = append(make([]Slot, 0, n), x.dirty...)
94+ x.clients = append(make([]uint32, 0, n), x.clients...)
95+ x.stacking = append(make([]uint32, 0, n), x.stacking...)
96+ x.scratch = make([]byte, 0, 4*n)
97+ }
98+}
99+
100+// insert adds a record for xid; 0 means the table is full.
101+func (x *XWM) insert(xid uint32) Slot {
102+ s := x.tab.insert(xid)
103+ if s == 0 {
104+ if !x.warnedFull {
105+ x.warnedFull = true
106+ x.log.Warn().Int("windows", x.tab.n).Msg("X11 window table full, window ignored")
107+ }
108+ return 0
109+ }
110+ x.fit()
111+ return s
112+}
113+
114+// expect registers seq, tolerating a full ring: the reply then arrives
115+// unattributed and is dropped.
116+func (x *XWM) expect(seq xturbo.Seq, kind uint16, a0, a1 uint32) {
117+ if seq == 0 {
118+ return
119+ }
120+ if err := x.conn.Expect(seq, kind, a0, a1, 0, 0); err != nil {
121+ x.stats.RingFull++
122+ if !x.warnedRing {
123+ x.warnedRing = true
124+ x.log.Warn().Err(err).Msg("X11 request not tracked")
125+ }
126+ }
127+}
128+
129+// setProp32 queues ChangeProperty (replace, format 32) for vals.
130+func (x *XWM) setProp32(win uint32, prop int, typ xproto.Atom, vals []uint32) {
131+ n := 4 * len(vals)
132+ if cap(x.scratch) < n {
133+ x.scratch = make([]byte, 0, n)
134+ }
135+ p := x.scratch[:n]
136+ for i, v := range vals {
137+ binary.LittleEndian.PutUint32(p[4*i:], v)
138+ }
139+ xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(win),
140+ xproto.Atom(x.atoms[prop]), typ, 32, uint32(len(vals)), p)
141+}
142+
143+// Boot connects to the X server on wmFD and becomes its window manager. It
144+// blocks (about 25 s at worst) and must not run on the display goroutine: the
145+// handshake needs Xwayland, which needs the display goroutine to serve its
146+// Wayland requests. It takes ownership of wmFD and closes it on failure.
147+func Boot(wmFD int, log zerowrap.Logger) (*XWM, error) {
148+ conn, err := xturbo.NewConn(wmFD)
149+ if err != nil {
150+ return nil, fmt.Errorf("xwm: connect: %w", err)
151+ }
152+ x := newXWM(conn, log)
153+ if err := x.boot(); err != nil {
154+ _ = conn.Close()
155+ return nil, err
156+ }
157+ return x, nil
158+}
159+
160+func (x *XWM) boot() error {
161+ conn := x.conn
162+ ext := conn.Ext()
163+ if !ext.Info(xturbo.ExtComposite).Present {
164+ return ErrNoComposite
165+ }
166+ var visual, depth uint32
167+ if it := xproto.NewSetupIter(conn.SetupBytes(), 1); it.Next() {
168+ if roots := it.Value().Roots(); roots.Next() {
169+ visual, depth = uint32(roots.Value().RootVisual()), uint32(roots.Value().RootDepth())
170+ }
171+ }
172+ x.log.Info().Uint32("root", x.root).Uint32("visual", visual).Uint32("depth", depth).
173+ Bool("xfixes", ext.Info(xturbo.ExtXFixes).Present).
174+ Bool("res", ext.Info(xturbo.ExtRes).Present).Msg("X server")
175+
176+ if err := conn.InternAtoms(atomNames[:], false, x.atoms[:]); err != nil {
177+ return fmt.Errorf("xwm: intern atoms: %w", err)
178+ }
179+ xid, err := conn.NewXID()
180+ if err != nil {
181+ return fmt.Errorf("xwm: allocate window id: %w", err)
182+ }
183+ x.wmWin = xid
184+
185+ b := conn.Buf()
186+ x.expect(composite.QueryVersion(b, 0, 4), kindVersion, versionComposite, 0)
187+ if ext.Info(xturbo.ExtXFixes).Present {
188+ x.expect(xfixes.QueryVersion(b, 6, 0), kindVersion, versionXFixes, 0)
189+ }
190+ if ext.Info(xturbo.ExtRes).Present {
191+ x.expect(res.QueryVersion(b, 1, 2), kindVersion, versionRes, 0)
192+ }
193+
194+ root := xproto.Window(x.root)
195+ var rootMask xproto.ChangeWindowAttributesValues
196+ rootMask.SetEventMask(xproto.EventMaskSubstructureRedirect | xproto.EventMaskSubstructureNotify | xproto.EventMaskPropertyChange)
197+ x.expect(xproto.ChangeWindowAttributes(b, root, &rootMask), kindRootMask, 0, 0)
198+ composite.RedirectSubwindows(b, root, composite.RedirectManual)
199+
200+ var wv xproto.CreateWindowValues
201+ wv.SetEventMask(0)
202+ xproto.CreateWindow(b, 0, xproto.Window(x.wmWin), root, -1, -1, 1, 1, 0, xproto.WindowClassInputOutput, 0, &wv)
203+ name := []byte("neferwl")
204+ xproto.ChangeProperty(b, xproto.PropModeReplace, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomNetWMName]),
205+ xproto.Atom(x.atoms[atomUTF8String]), 8, uint32(len(name)), name)
206+ x.setProp32(x.root, atomNetSupportingWMCheck, xproto.AtomWindow, []uint32{x.wmWin})
207+ x.setProp32(x.wmWin, atomNetSupportingWMCheck, xproto.AtomWindow, []uint32{x.wmWin})
208+ xproto.SetSelectionOwner(b, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomWMS0]), 0)
209+ xproto.SetSelectionOwner(b, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomNetWMCMS0]), 0)
210+
211+ supported := [...]int{
212+ atomNetWMState, atomNetActiveWindow, atomNetCloseWindow, atomNetWMStateFocused,
213+ atomNetWMStateModal, atomNetWMStateFullscreen, atomNetWMStateHidden, atomNetClientList,
214+ atomNetClientListStacking, atomNetSupportingWMCheck, atomNetWMName, atomNetWMPID,
215+ atomNetWMWindowType,
216+ }
217+ var list [len(supported)]uint32
218+ for i, a := range supported {
219+ list[i] = x.atoms[a]
220+ }
221+ x.setProp32(x.root, atomNetSupported, xproto.AtomAtom, list[:])
222+ x.setProp32(x.root, atomNetActiveWindow, xproto.AtomWindow, []uint32{0})
223+ x.setProp32(x.root, atomNetClientList, xproto.AtomWindow, nil)
224+ x.setProp32(x.root, atomNetClientListStacking, xproto.AtomWindow, nil)
225+ xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.WindowNone, 0)
226+ if err := b.Err(); err != nil {
227+ return fmt.Errorf("xwm: encode setup requests: %w", err)
228+ }
229+ if err := conn.Sync(&x.h); err != nil {
230+ return fmt.Errorf("xwm: root setup: %w", err)
231+ }
232+ if x.bootErr != nil {
233+ return x.bootErr
234+ }
235+
236+ // Windows that exist already (Xwayland restarted under a live client set).
237+ x.expect(xproto.QueryTree(b, root), kindTree, 0, 0)
238+ if err := conn.Sync(&x.h); err != nil {
239+ return fmt.Errorf("xwm: query tree: %w", err)
240+ }
241+ return x.bootErr
242+}
243+
244+// Attach registers the X socket with the display and hands the windows found
245+// at boot to h. Display goroutine only.
246+func (x *XWM) Attach(d *server.Display, h host) error {
247+ src, err := d.AddFDRW(x.conn.Fd(), x.onReadable, x.onWritable)
248+ if err != nil {
249+ return fmt.Errorf("xwm: watch X socket: %w", err)
250+ }
251+ x.attach(src, h, d.Post)
252+ return nil
253+}
254+
255+// attach is Attach over its seams: the event source and the display's Post.
256+func (x *XWM) attach(src fdSource, h host, post func(func())) {
257+ x.src, x.host, x.post = src, h, post
258+ x.flushFn = func() {
259+ x.posted = false
260+ x.flush()
261+ }
262+ for i := range x.tab.recs {
263+ r := &x.tab.recs[i]
264+ if r.kind != kindManaged || r.id != 0 {
265+ continue
266+ }
267+ s := Slot(i + 1)
268+ x.adopt(s)
269+ if r.mapping { // MapRequest or viewable before Attach
270+ r.mapping = false
271+ x.beginMap(s)
272+ }
273+ }
274+ x.flush()
275+}
276+
277+// Close detaches from the display, drops every window from the host and closes
278+// the X connection. Idempotent.
279+func (x *XWM) Close() {
280+ if x.closed {
281+ return
282+ }
283+ x.closed = true
284+ if x.src != nil {
285+ x.src.Remove()
286+ x.src = nil
287+ }
288+ if x.host != nil {
289+ for i := range x.tab.recs {
290+ if r := &x.tab.recs[i]; r.kind == kindManaged && r.id != 0 {
291+ r.id = 0
292+ x.host.Dissociate(Slot(i+1), true)
293+ }
294+ }
295+ }
296+ x.unpairedN = 0
297+ _ = x.conn.Close()
298+}
299+
300+// CloseConn closes the connection of an XWM that was never attached.
301+func (x *XWM) CloseConn() {
302+ if !x.closed {
303+ x.closed = true
304+ _ = x.conn.Close()
305+ }
306+}
307+
308+// fail ends window management after an unrecoverable connection error. Xwayland
309+// keeps running; its windows no longer map.
310+func (x *XWM) fail(err error) {
311+ if x.closed {
312+ return
313+ }
314+ x.log.Warn().Err(err).Msg("X11 window management stopped")
315+ x.Close()
316+}
317+
318+// Stats returns the counters.
319+func (x *XWM) Stats() Stats { return x.stats }
320+
321+// Slot returns the slot of the X window xid, or 0.
322+func (x *XWM) Slot(xid uint32) Slot { return x.tab.lookup(xid) }
323+
324+func (x *XWM) onReadable() {
325+ if x.closed {
326+ return
327+ }
328+ if err := x.conn.Dispatch(&x.h); err != nil {
329+ x.fail(err)
330+ return
331+ }
332+ x.flush()
333+}
334+
335+func (x *XWM) onWritable() { x.flush() }
336+
337+// wake schedules one flush at the end of the current display iteration.
338+func (x *XWM) wake() {
339+ if x.posted || x.closed || x.post == nil {
340+ return
341+ }
342+ x.posted = true
343+ x.post(x.flushFn)
344+}
345+
346+// flush sends what the handlers queued: pending configures, an XID prefetch,
347+// then the socket, arming POLLOUT only while it is full.
348+func (x *XWM) flush() {
349+ if x.closed {
350+ return
351+ }
352+ x.flushConfigures()
353+ if x.conn.XIDsLow() {
354+ _ = x.conn.PrefetchXIDs()
355+ }
356+ err := x.conn.Flush()
357+ switch {
358+ case err == nil:
359+ if x.writable {
360+ x.setWritable(false)
361+ }
362+ case errors.Is(err, xturbo.ErrWouldBlock):
363+ if !x.writable {
364+ x.setWritable(true)
365+ }
366+ default:
367+ x.fail(err)
368+ }
369+}
370+
371+func (x *XWM) setWritable(on bool) {
372+ x.writable = on
373+ if x.src == nil {
374+ return
375+ }
376+ if err := x.src.SetWritable(on); err != nil {
377+ x.fail(err)
378+ }
379+}
@@ -0,0 +1,379 @@
1+//lint:file-ignore U1000 pairing, focus and stacking state is used by the flows that build on this skeleton
2+
3+// Package xwm is the built-in X11 window manager for Xwayland: it speaks the
4+// X protocol over the -wm socket with xturbo and reports managed windows to
5+// the Wayland server through the unexported host interface.
6+//
7+// Threading: Boot runs on the starter goroutine and blocks. The XWM it returns
8+// is then handed to the display goroutine, which owns it exclusively; nothing
9+// in this package is locked.
10+package xwm
11+
12+import (
13+ "encoding/binary"
14+ "errors"
15+ "fmt"
16+
17+ "bnema.dev/xturbo"
18+ "bnema.dev/xturbo/composite"
19+ "bnema.dev/xturbo/res"
20+ "bnema.dev/xturbo/xfixes"
21+ "bnema.dev/xturbo/xproto"
22+ "github.com/bnema/purego-libwayland/server"
23+ "github.com/bnema/zerowrap"
24+)
25+
26+var (
27+ // ErrWMExists is returned by Boot when another client already redirects
28+ // the root window's children.
29+ ErrWMExists = errors.New("xwm: another window manager owns the root window")
30+ // ErrNoComposite is returned by Boot when the X server lacks Composite.
31+ ErrNoComposite = errors.New("xwm: X server has no Composite extension")
32+)
33+
34+// Stats are connection and event counters, for diagnostics.
35+type Stats struct {
36+ Events, Replies, Errors, Unattributed, Stale, RingFull, LegacyID, UnmanagedSeen uint64
37+}
38+
39+// Version request ids, carried in the first Expect argument of kindVersion.
40+const (
41+ versionComposite uint32 = iota + 1
42+ versionXFixes
43+ versionRes
44+)
45+
46+const unpairedCap = 64
47+
48+// XWM manages the windows of one Xwayland. Display goroutine only, after Boot.
49+type XWM struct {
50+ conn *xturbo.Conn
51+ log zerowrap.Logger
52+ root uint32
53+ wmWin uint32
54+ atoms [atomCount]uint32
55+ tab *table
56+ h xturbo.Handler
57+ stats Stats
58+
59+ // attachment
60+ host host
61+ src fdSource
62+ post func(func())
63+ flushFn func()
64+
65+ posted, writable, closed bool
66+ bootErr error
67+ warnedRing, warnedFull bool
68+
69+ scratch []byte // encode buffer for 32-bit property values, 4 bytes per slot
70+
71+ // Shared by the map, configure and focus flows.
72+ dirty []Slot // slots with a pending configure, capacity == table size
73+ clients []uint32 // _NET_CLIENT_LIST, map order
74+ stacking []uint32 // _NET_CLIENT_LIST_STACKING, bottom to top
75+ active Slot // slot named by _NET_ACTIVE_WINDOW, 0 = none
76+ refocus bool // a FocusIn was denied; focus is re-issued in flush
77+ listsDirty uint8 // client lists to rewrite in flush
78+ unpaired [unpairedCap]struct{ serial, surface uint64 }
79+ unpairedN int
80+}
81+
82+func newXWM(conn *xturbo.Conn, log zerowrap.Logger) *XWM {
83+ x := &XWM{conn: conn, log: log, root: conn.Root(), tab: newTable(initialSlots)}
84+ x.h = xturbo.Handler{OnEvent: x.onEvent, OnReply: x.onReply, OnError: x.onError}
85+ x.fit()
86+ return x
87+}
88+
89+// fit sizes the per-slot buffers to the table; called after every insert.
90+func (x *XWM) fit() {
91+ n := len(x.tab.recs)
92+ if cap(x.dirty) < n {
93+ x.dirty = append(make([]Slot, 0, n), x.dirty...)
94+ x.clients = append(make([]uint32, 0, n), x.clients...)
95+ x.stacking = append(make([]uint32, 0, n), x.stacking...)
96+ x.scratch = make([]byte, 0, 4*n)
97+ }
98+}
99+
100+// insert adds a record for xid; 0 means the table is full.
101+func (x *XWM) insert(xid uint32) Slot {
102+ s := x.tab.insert(xid)
103+ if s == 0 {
104+ if !x.warnedFull {
105+ x.warnedFull = true
106+ x.log.Warn().Int("windows", x.tab.n).Msg("X11 window table full, window ignored")
107+ }
108+ return 0
109+ }
110+ x.fit()
111+ return s
112+}
113+
114+// expect registers seq, tolerating a full ring: the reply then arrives
115+// unattributed and is dropped.
116+func (x *XWM) expect(seq xturbo.Seq, kind uint16, a0, a1 uint32) {
117+ if seq == 0 {
118+ return
119+ }
120+ if err := x.conn.Expect(seq, kind, a0, a1, 0, 0); err != nil {
121+ x.stats.RingFull++
122+ if !x.warnedRing {
123+ x.warnedRing = true
124+ x.log.Warn().Err(err).Msg("X11 request not tracked")
125+ }
126+ }
127+}
128+
129+// setProp32 queues ChangeProperty (replace, format 32) for vals.
130+func (x *XWM) setProp32(win uint32, prop int, typ xproto.Atom, vals []uint32) {
131+ n := 4 * len(vals)
132+ if cap(x.scratch) < n {
133+ x.scratch = make([]byte, 0, n)
134+ }
135+ p := x.scratch[:n]
136+ for i, v := range vals {
137+ binary.LittleEndian.PutUint32(p[4*i:], v)
138+ }
139+ xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(win),
140+ xproto.Atom(x.atoms[prop]), typ, 32, uint32(len(vals)), p)
141+}
142+
143+// Boot connects to the X server on wmFD and becomes its window manager. It
144+// blocks (about 25 s at worst) and must not run on the display goroutine: the
145+// handshake needs Xwayland, which needs the display goroutine to serve its
146+// Wayland requests. It takes ownership of wmFD and closes it on failure.
147+func Boot(wmFD int, log zerowrap.Logger) (*XWM, error) {
148+ conn, err := xturbo.NewConn(wmFD)
149+ if err != nil {
150+ return nil, fmt.Errorf("xwm: connect: %w", err)
151+ }
152+ x := newXWM(conn, log)
153+ if err := x.boot(); err != nil {
154+ _ = conn.Close()
155+ return nil, err
156+ }
157+ return x, nil
158+}
159+
160+func (x *XWM) boot() error {
161+ conn := x.conn
162+ ext := conn.Ext()
163+ if !ext.Info(xturbo.ExtComposite).Present {
164+ return ErrNoComposite
165+ }
166+ var visual, depth uint32
167+ if it := xproto.NewSetupIter(conn.SetupBytes(), 1); it.Next() {
168+ if roots := it.Value().Roots(); roots.Next() {
169+ visual, depth = uint32(roots.Value().RootVisual()), uint32(roots.Value().RootDepth())
170+ }
171+ }
172+ x.log.Info().Uint32("root", x.root).Uint32("visual", visual).Uint32("depth", depth).
173+ Bool("xfixes", ext.Info(xturbo.ExtXFixes).Present).
174+ Bool("res", ext.Info(xturbo.ExtRes).Present).Msg("X server")
175+
176+ if err := conn.InternAtoms(atomNames[:], false, x.atoms[:]); err != nil {
177+ return fmt.Errorf("xwm: intern atoms: %w", err)
178+ }
179+ xid, err := conn.NewXID()
180+ if err != nil {
181+ return fmt.Errorf("xwm: allocate window id: %w", err)
182+ }
183+ x.wmWin = xid
184+
185+ b := conn.Buf()
186+ x.expect(composite.QueryVersion(b, 0, 4), kindVersion, versionComposite, 0)
187+ if ext.Info(xturbo.ExtXFixes).Present {
188+ x.expect(xfixes.QueryVersion(b, 6, 0), kindVersion, versionXFixes, 0)
189+ }
190+ if ext.Info(xturbo.ExtRes).Present {
191+ x.expect(res.QueryVersion(b, 1, 2), kindVersion, versionRes, 0)
192+ }
193+
194+ root := xproto.Window(x.root)
195+ var rootMask xproto.ChangeWindowAttributesValues
196+ rootMask.SetEventMask(xproto.EventMaskSubstructureRedirect | xproto.EventMaskSubstructureNotify | xproto.EventMaskPropertyChange)
197+ x.expect(xproto.ChangeWindowAttributes(b, root, &rootMask), kindRootMask, 0, 0)
198+ composite.RedirectSubwindows(b, root, composite.RedirectManual)
199+
200+ var wv xproto.CreateWindowValues
201+ wv.SetEventMask(0)
202+ xproto.CreateWindow(b, 0, xproto.Window(x.wmWin), root, -1, -1, 1, 1, 0, xproto.WindowClassInputOutput, 0, &wv)
203+ name := []byte("neferwl")
204+ xproto.ChangeProperty(b, xproto.PropModeReplace, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomNetWMName]),
205+ xproto.Atom(x.atoms[atomUTF8String]), 8, uint32(len(name)), name)
206+ x.setProp32(x.root, atomNetSupportingWMCheck, xproto.AtomWindow, []uint32{x.wmWin})
207+ x.setProp32(x.wmWin, atomNetSupportingWMCheck, xproto.AtomWindow, []uint32{x.wmWin})
208+ xproto.SetSelectionOwner(b, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomWMS0]), 0)
209+ xproto.SetSelectionOwner(b, xproto.Window(x.wmWin), xproto.Atom(x.atoms[atomNetWMCMS0]), 0)
210+
211+ supported := [...]int{
212+ atomNetWMState, atomNetActiveWindow, atomNetCloseWindow, atomNetWMStateFocused,
213+ atomNetWMStateModal, atomNetWMStateFullscreen, atomNetWMStateHidden, atomNetClientList,
214+ atomNetClientListStacking, atomNetSupportingWMCheck, atomNetWMName, atomNetWMPID,
215+ atomNetWMWindowType,
216+ }
217+ var list [len(supported)]uint32
218+ for i, a := range supported {
219+ list[i] = x.atoms[a]
220+ }
221+ x.setProp32(x.root, atomNetSupported, xproto.AtomAtom, list[:])
222+ x.setProp32(x.root, atomNetActiveWindow, xproto.AtomWindow, []uint32{0})
223+ x.setProp32(x.root, atomNetClientList, xproto.AtomWindow, nil)
224+ x.setProp32(x.root, atomNetClientListStacking, xproto.AtomWindow, nil)
225+ xproto.SetInputFocus(b, xproto.InputFocusNone, xproto.WindowNone, 0)
226+ if err := b.Err(); err != nil {
227+ return fmt.Errorf("xwm: encode setup requests: %w", err)
228+ }
229+ if err := conn.Sync(&x.h); err != nil {
230+ return fmt.Errorf("xwm: root setup: %w", err)
231+ }
232+ if x.bootErr != nil {
233+ return x.bootErr
234+ }
235+
236+ // Windows that exist already (Xwayland restarted under a live client set).
237+ x.expect(xproto.QueryTree(b, root), kindTree, 0, 0)
238+ if err := conn.Sync(&x.h); err != nil {
239+ return fmt.Errorf("xwm: query tree: %w", err)
240+ }
241+ return x.bootErr
242+}
243+
244+// Attach registers the X socket with the display and hands the windows found
245+// at boot to h. Display goroutine only.
246+func (x *XWM) Attach(d *server.Display, h host) error {
247+ src, err := d.AddFDRW(x.conn.Fd(), x.onReadable, x.onWritable)
248+ if err != nil {
249+ return fmt.Errorf("xwm: watch X socket: %w", err)
250+ }
251+ x.attach(src, h, d.Post)
252+ return nil
253+}
254+
255+// attach is Attach over its seams: the event source and the display's Post.
256+func (x *XWM) attach(src fdSource, h host, post func(func())) {
257+ x.src, x.host, x.post = src, h, post
258+ x.flushFn = func() {
259+ x.posted = false
260+ x.flush()
261+ }
262+ for i := range x.tab.recs {
263+ r := &x.tab.recs[i]
264+ if r.kind != kindManaged || r.id != 0 {
265+ continue
266+ }
267+ s := Slot(i + 1)
268+ x.adopt(s)
269+ if r.mapping { // MapRequest or viewable before Attach
270+ r.mapping = false
271+ x.beginMap(s)
272+ }
273+ }
274+ x.flush()
275+}
276+
277+// Close detaches from the display, drops every window from the host and closes
278+// the X connection. Idempotent.
279+func (x *XWM) Close() {
280+ if x.closed {
281+ return
282+ }
283+ x.closed = true
284+ if x.src != nil {
285+ x.src.Remove()
286+ x.src = nil
287+ }
288+ if x.host != nil {
289+ for i := range x.tab.recs {
290+ if r := &x.tab.recs[i]; r.kind == kindManaged && r.id != 0 {
291+ r.id = 0
292+ x.host.Dissociate(Slot(i+1), true)
293+ }
294+ }
295+ }
296+ x.unpairedN = 0
297+ _ = x.conn.Close()
298+}
299+
300+// CloseConn closes the connection of an XWM that was never attached.
301+func (x *XWM) CloseConn() {
302+ if !x.closed {
303+ x.closed = true
304+ _ = x.conn.Close()
305+ }
306+}
307+
308+// fail ends window management after an unrecoverable connection error. Xwayland
309+// keeps running; its windows no longer map.
310+func (x *XWM) fail(err error) {
311+ if x.closed {
312+ return
313+ }
314+ x.log.Warn().Err(err).Msg("X11 window management stopped")
315+ x.Close()
316+}
317+
318+// Stats returns the counters.
319+func (x *XWM) Stats() Stats { return x.stats }
320+
321+// Slot returns the slot of the X window xid, or 0.
322+func (x *XWM) Slot(xid uint32) Slot { return x.tab.lookup(xid) }
323+
324+func (x *XWM) onReadable() {
325+ if x.closed {
326+ return
327+ }
328+ if err := x.conn.Dispatch(&x.h); err != nil {
329+ x.fail(err)
330+ return
331+ }
332+ x.flush()
333+}
334+
335+func (x *XWM) onWritable() { x.flush() }
336+
337+// wake schedules one flush at the end of the current display iteration.
338+func (x *XWM) wake() {
339+ if x.posted || x.closed || x.post == nil {
340+ return
341+ }
342+ x.posted = true
343+ x.post(x.flushFn)
344+}
345+
346+// flush sends what the handlers queued: pending configures, an XID prefetch,
347+// then the socket, arming POLLOUT only while it is full.
348+func (x *XWM) flush() {
349+ if x.closed {
350+ return
351+ }
352+ x.flushConfigures()
353+ if x.conn.XIDsLow() {
354+ _ = x.conn.PrefetchXIDs()
355+ }
356+ err := x.conn.Flush()
357+ switch {
358+ case err == nil:
359+ if x.writable {
360+ x.setWritable(false)
361+ }
362+ case errors.Is(err, xturbo.ErrWouldBlock):
363+ if !x.writable {
364+ x.setWritable(true)
365+ }
366+ default:
367+ x.fail(err)
368+ }
369+}
370+
371+func (x *XWM) setWritable(on bool) {
372+ x.writable = on
373+ if x.src == nil {
374+ return
375+ }
376+ if err := x.src.SetWritable(on); err != nil {
377+ x.fail(err)
378+ }
379+}
A internal/adapters/wayland/xwm/xwm_test.go +362 -0
@@ -0,0 +1,362 @@
1+package xwm
2+
3+import (
4+ "encoding/binary"
5+ "os"
6+ "strings"
7+ "testing"
8+ "time"
9+
10+ "bnema.dev/xturbo/xproto"
11+ "github.com/bnema/neferwl/internal/ports"
12+ "github.com/bnema/zerowrap"
13+ "github.com/rs/zerolog"
14+ "github.com/stretchr/testify/mock"
15+ "github.com/stretchr/testify/require"
16+)
17+
18+func testLog() zerowrap.Logger { return zerowrap.Logger{Logger: zerolog.Nop()} }
19+
20+func bootPeer(t *testing.T, mutate func(p *xpeer)) (*XWM, *xpeer) {
21+ t.Helper()
22+ fd, p := startXPeer(t, fullPeerExts())
23+ if mutate != nil {
24+ p.set(mutate)
25+ }
26+ x, err := Boot(fd, testLog())
27+ require.NoError(t, err)
28+ t.Cleanup(x.CloseConn)
29+ return x, p
30+}
31+
32+func atomsOf(p xpeerProp) []uint32 {
33+ out := make([]uint32, len(p.data)/4)
34+ for i := range out {
35+ out[i] = binary.LittleEndian.Uint32(p.data[4*i:])
36+ }
37+ return out
38+}
39+
40+func TestBootRequests(t *testing.T) {
41+ x, p := bootPeer(t, nil)
42+ reqs := p.requests()
43+
44+ first := -1
45+ for i, r := range reqs {
46+ if r.major == opInternAtom {
47+ first = i
48+ break
49+ }
50+ }
51+ require.NotEqual(t, -1, first)
52+ for i := 0; i < atomCount; i++ {
53+ r := reqs[first+i]
54+ require.EqualValues(t, opInternAtom, r.major, i)
55+ n := int(binary.LittleEndian.Uint16(r.raw[4:]))
56+ require.Equal(t, atomNames[i], string(r.raw[8:8+n]), i)
57+ require.Equal(t, p.atom(atomNames[i]), x.atoms[i])
58+ }
59+
60+ // Root mask, then composite redirect, then the WM window and ownership.
61+ var rootMask, redirect, create, s0, cm, focus int
62+ rootMask, redirect, create, s0, cm, focus = -1, -1, -1, -1, -1, -1
63+ wm := x.wmWin
64+ for i := first + atomCount; i < len(reqs); i++ {
65+ r := reqs[i]
66+ switch {
67+ case r.major == opChangeWindowAttributes && r.u32(4) == peerRoot:
68+ rootMask = i
69+ require.EqualValues(t, 1<<11, r.u32(8))
70+ want := uint32(xproto.EventMaskSubstructureRedirect | xproto.EventMaskSubstructureNotify | xproto.EventMaskPropertyChange)
71+ require.Equal(t, want, r.u32(12))
72+ case r.major == 142 && r.minor == 2:
73+ redirect = i
74+ require.EqualValues(t, peerRoot, r.u32(4))
75+ require.EqualValues(t, 1, r.raw[8], "manual")
76+ case r.major == opCreateWindow:
77+ create = i
78+ require.Equal(t, wm, r.u32(4))
79+ case r.major == opSetSelectionOwner && r.u32(8) == p.atom("WM_S0"):
80+ s0 = i
81+ require.Equal(t, wm, r.u32(4))
82+ case r.major == opSetSelectionOwner && r.u32(8) == p.atom("_NET_WM_CM_S0"):
83+ cm = i
84+ case r.major == opSetInputFocus:
85+ focus = i
86+ require.EqualValues(t, 0, r.u32(4), "focus None")
87+ }
88+ }
89+ for name, i := range map[string]int{"rootMask": rootMask, "redirect": redirect, "create": create, "WM_S0": s0, "CM_S0": cm, "focus": focus} {
90+ require.NotEqual(t, -1, i, name)
91+ }
92+ require.Less(t, rootMask, redirect)
93+ require.Less(t, redirect, create)
94+ require.Less(t, create, s0)
95+
96+ sup, ok := p.prop(peerRoot, "_NET_SUPPORTED")
97+ require.True(t, ok)
98+ var names []string
99+ byAtom := map[uint32]string{}
100+ for n, a := range p.atoms {
101+ byAtom[a] = n
102+ }
103+ for _, a := range atomsOf(sup) {
104+ names = append(names, byAtom[a])
105+ }
106+ require.Equal(t, []string{
107+ "_NET_WM_STATE", "_NET_ACTIVE_WINDOW", "_NET_CLOSE_WINDOW", "_NET_WM_STATE_FOCUSED",
108+ "_NET_WM_STATE_MODAL", "_NET_WM_STATE_FULLSCREEN", "_NET_WM_STATE_HIDDEN", "_NET_CLIENT_LIST",
109+ "_NET_CLIENT_LIST_STACKING", "_NET_SUPPORTING_WM_CHECK", "_NET_WM_NAME", "_NET_WM_PID",
110+ "_NET_WM_WINDOW_TYPE",
111+ }, names)
112+ require.NotContains(t, names, "_NET_WM_MOVERESIZE")
113+ require.NotContains(t, names, "_NET_WM_STATE_MAXIMIZED_VERT")
114+
115+ check, ok := p.prop(peerRoot, "_NET_SUPPORTING_WM_CHECK")
116+ require.True(t, ok)
117+ require.Equal(t, []uint32{wm}, atomsOf(check))
118+ check, ok = p.prop(wm, "_NET_SUPPORTING_WM_CHECK")
119+ require.True(t, ok)
120+ require.Equal(t, []uint32{wm}, atomsOf(check))
121+ name, ok := p.prop(wm, "_NET_WM_NAME")
122+ require.True(t, ok)
123+ require.Equal(t, "neferwl", string(name.data))
124+ active, ok := p.prop(peerRoot, "_NET_ACTIVE_WINDOW")
125+ require.True(t, ok)
126+ require.Equal(t, []uint32{0}, atomsOf(active))
127+ for _, l := range []string{"_NET_CLIENT_LIST", "_NET_CLIENT_LIST_STACKING"} {
128+ v, ok := p.prop(peerRoot, l)
129+ require.True(t, ok, l)
130+ require.Empty(t, v.data, l)
131+ }
132+}
133+
134+func TestBootWMExists(t *testing.T) {
135+ fd, p := startXPeer(t, fullPeerExts())
136+ p.set(func(p *xpeer) { p.denyRoot = true })
137+ x, err := Boot(fd, testLog())
138+ require.ErrorIs(t, err, ErrWMExists)
139+ require.Nil(t, x)
140+}
141+
142+func TestBootNoComposite(t *testing.T) {
143+ exts := fullPeerExts()
144+ delete(exts, "Composite")
145+ fd, _ := startXPeer(t, exts)
146+ _, err := Boot(fd, testLog())
147+ require.ErrorIs(t, err, ErrNoComposite)
148+}
149+
150+func TestBootOptionalExtensions(t *testing.T) {
151+ exts := fullPeerExts()
152+ delete(exts, "XFIXES")
153+ delete(exts, "X-Resource")
154+ fd, _ := startXPeer(t, exts)
155+ x, err := Boot(fd, testLog())
156+ require.NoError(t, err)
157+ x.CloseConn()
158+}
159+
160+func TestBootConnectFailure(t *testing.T) {
161+ fd, p := startXPeer(t, fullPeerExts())
162+ p.hangup()
163+ _, err := Boot(fd, testLog())
164+ require.Error(t, err)
165+}
166+
167+func TestBootExistingWindows(t *testing.T) {
168+ x, p := bootPeer(t, func(p *xpeer) {
169+ p.children = []uint32{0x400001, 0x400002}
170+ p.attrs[0x400002] = [2]byte{1, 2} // override-redirect, viewable
171+ })
172+ _ = p
173+ require.NotZero(t, x.Slot(0x400001))
174+ require.NotZero(t, x.Slot(0x400002))
175+ require.Equal(t, kindManaged, x.tab.rec(x.Slot(0x400001)).kind)
176+ require.Equal(t, kindUnmanaged, x.tab.rec(x.Slot(0x400002)).kind)
177+}
178+
179+// readUntil dispatches the socket until cond holds.
180+func readUntil(t *testing.T, x *XWM, cond func() bool) {
181+ t.Helper()
182+ deadline := time.Now().Add(5 * time.Second)
183+ for !cond() {
184+ if time.Now().After(deadline) {
185+ t.Fatal("condition never held")
186+ }
187+ x.onReadable()
188+ time.Sleep(time.Millisecond)
189+ }
190+}
191+
192+func newPost() (post func(func()), runs *[]func()) {
193+ var q []func()
194+ return func(f func()) { q = append(q, f) }, &q
195+}
196+
197+func TestAttachAnnouncesWindowsFoundAtBoot(t *testing.T) {
198+ x, _ := bootPeer(t, func(p *xpeer) {
199+ p.children = []uint32{0x400001, 0x400002}
200+ p.attrs[0x400002] = [2]byte{1, 2}
201+ })
202+ h := newMockhost(t)
203+ src := newMockfdSource(t)
204+ h.EXPECT().Created(x.Slot(0x400001), uint32(0x400001)).Return(ports.WindowID(7)).Once()
205+ post, _ := newPost()
206+ x.attach(src, h, post)
207+ require.Equal(t, ports.WindowID(7), x.tab.rec(x.Slot(0x400001)).id)
208+}
209+
210+func attached(t *testing.T) (*XWM, *xpeer, *mockhost, *mockfdSource, *[]func()) {
211+ t.Helper()
212+ x, p := bootPeer(t, nil)
213+ h := newMockhost(t)
214+ src := newMockfdSource(t)
215+ post, runs := newPost()
216+ x.attach(src, h, post)
217+ return x, p, h, src, runs
218+}
219+
220+func TestCreateAndDestroyManaged(t *testing.T) {
221+ x, p, h, src, _ := attached(t)
222+ const win = 0x500001
223+ h.EXPECT().Created(mock.Anything, uint32(win)).Return(ports.WindowID(3)).Once()
224+ p.createNotify(win, false)
225+ readUntil(t, x, func() bool { return x.Slot(win) != 0 })
226+ s := x.Slot(win)
227+ require.Equal(t, kindManaged, x.tab.rec(s).kind)
228+ require.Equal(t, ports.WindowID(3), x.tab.rec(s).id)
229+
230+ // The window is watched for property and focus changes.
231+ req, _ := p.waitReq(0, func(r xpeerReq) bool { return r.major == opChangeWindowAttributes && r.u32(4) == win })
232+ require.EqualValues(t, xproto.EventMaskPropertyChange|xproto.EventMaskFocusChange, req.u32(12))
233+
234+ h.EXPECT().Dissociate(s, true).Once()
235+ p.destroyNotify(win)
236+ readUntil(t, x, func() bool { return x.Slot(win) == 0 })
237+ require.Zero(t, x.tab.n)
238+ _ = src
239+}
240+
241+func TestOverrideRedirectNeverReachesHost(t *testing.T) {
242+ x, p, _, _, _ := attached(t) // the mock fails the test on any host call
243+ p.createNotify(0x500002, true)
244+ readUntil(t, x, func() bool { return x.Slot(0x500002) != 0 })
245+ require.Equal(t, kindUnmanaged, x.tab.rec(x.Slot(0x500002)).kind)
246+ require.EqualValues(t, 1, x.Stats().UnmanagedSeen)
247+ p.destroyNotify(0x500002)
248+ readUntil(t, x, func() bool { return x.Slot(0x500002) == 0 })
249+}
250+
251+func TestCloseDissociatesEverything(t *testing.T) {
252+ x, p, h, src, _ := attached(t)
253+ for _, win := range []uint32{0x500001, 0x500002} {
254+ h.EXPECT().Created(mock.Anything, win).Return(ports.WindowID(win)).Once()
255+ p.createNotify(win, false)
256+ }
257+ p.createNotify(0x500003, true)
258+ readUntil(t, x, func() bool { return x.Slot(0x500003) != 0 })
259+ h.EXPECT().Dissociate(x.Slot(0x500001), true).Once()
260+ h.EXPECT().Dissociate(x.Slot(0x500002), true).Once()
261+ src.EXPECT().Remove().Once()
262+ x.Close()
263+ x.Close() // idempotent
264+ require.True(t, x.closed)
265+ require.Equal(t, -1, x.conn.Fd())
266+}
267+
268+func TestEOFClosesXWM(t *testing.T) {
269+ x, p, h, src, _ := attached(t)
270+ h.EXPECT().Created(mock.Anything, uint32(0x500001)).Return(ports.WindowID(1)).Once()
271+ p.createNotify(0x500001, false)
272+ readUntil(t, x, func() bool { return x.Slot(0x500001) != 0 })
273+ h.EXPECT().Dissociate(x.Slot(0x500001), true).Once()
274+ src.EXPECT().Remove().Once()
275+ p.hangup()
276+ readUntil(t, x, func() bool { return x.closed })
277+}
278+
279+func TestWakeCoalescesFlush(t *testing.T) {
280+ x, _, _, _, runs := attached(t)
281+ x.wake()
282+ x.wake()
283+ x.wake()
284+ require.Len(t, *runs, 1)
285+ (*runs)[0]()
286+ x.wake()
287+ require.Len(t, *runs, 2)
288+}
289+
290+func TestWriteBackpressure(t *testing.T) {
291+ x, p, _, src, _ := attached(t)
292+ p.pause()
293+ b := x.conn.Buf()
294+ big := make([]byte, 64<<10)
295+ for i := 0; i < 64; i++ { // 4 MiB: more than any socket buffer
296+ xproto.ChangeProperty(b, xproto.PropModeReplace, xproto.Window(x.root), xproto.AtomWmName, xproto.AtomString, 8, uint32(len(big)), big)
297+ }
298+ src.EXPECT().SetWritable(true).Return(nil).Once()
299+ x.flush()
300+ require.True(t, x.writable)
301+ x.flush() // still blocked: SetWritable is not called again
302+ x.onWritable()
303+ require.True(t, x.writable)
304+
305+ drained := false
306+ src.EXPECT().SetWritable(false).RunAndReturn(func(bool) error { drained = true; return nil }).Once()
307+ p.resume()
308+ deadline := time.Now().Add(10 * time.Second)
309+ for !drained {
310+ require.True(t, time.Now().Before(deadline), "never drained")
311+ x.onWritable()
312+ time.Sleep(time.Millisecond)
313+ }
314+ require.False(t, x.writable)
315+}
316+
317+func TestFlushWithNothingQueuedSendsNothing(t *testing.T) {
318+ x, p, _, _, _ := attached(t)
319+ n := len(p.requests())
320+ x.flush()
321+ time.Sleep(20 * time.Millisecond)
322+ require.Len(t, p.requests(), n, "nothing queued, nothing sent")
323+}
324+
325+// The XWM never blocks after Boot: NewXID, Sync and InternAtoms are startup
326+// only, so they may appear in production code only inside boot().
327+func TestNoBlockingCallsOutsideBoot(t *testing.T) {
328+ files, err := os.ReadDir(".")
329+ require.NoError(t, err)
330+ for _, f := range files {
331+ name := f.Name()
332+ if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
333+ continue
334+ }
335+ src, err := os.ReadFile(name)
336+ require.NoError(t, err)
337+ text := string(src)
338+ lo, hi := 0, 0
339+ if name == "xwm.go" {
340+ lo = strings.Index(text, "func (x *XWM) boot() error")
341+ hi = strings.Index(text, "// Attach registers")
342+ require.Greater(t, hi, lo)
343+ }
344+ for _, call := range []string{".NewXID(", ".Sync(", ".InternAtoms("} {
345+ for _, at := range allIndex(text, call) {
346+ require.True(t, name == "xwm.go" && at > lo && at < hi, "%s: %s outside boot", name, call)
347+ }
348+ }
349+ }
350+}
351+
352+func allIndex(s, sub string) []int {
353+ var out []int
354+ for off := 0; ; {
355+ i := strings.Index(s[off:], sub)
356+ if i < 0 {
357+ return out
358+ }
359+ out = append(out, off+i)
360+ off += i + len(sub)
361+ }
362+}
@@ -0,0 +1,362 @@
1+package xwm
2+
3+import (
4+ "encoding/binary"
5+ "os"
6+ "strings"
7+ "testing"
8+ "time"
9+
10+ "bnema.dev/xturbo/xproto"
11+ "github.com/bnema/neferwl/internal/ports"
12+ "github.com/bnema/zerowrap"
13+ "github.com/rs/zerolog"
14+ "github.com/stretchr/testify/mock"
15+ "github.com/stretchr/testify/require"
16+)
17+
18+func testLog() zerowrap.Logger { return zerowrap.Logger{Logger: zerolog.Nop()} }
19+
20+func bootPeer(t *testing.T, mutate func(p *xpeer)) (*XWM, *xpeer) {
21+ t.Helper()
22+ fd, p := startXPeer(t, fullPeerExts())
23+ if mutate != nil {
24+ p.set(mutate)
25+ }
26+ x, err := Boot(fd, testLog())
27+ require.NoError(t, err)
28+ t.Cleanup(x.CloseConn)
29+ return x, p
30+}
31+
32+func atomsOf(p xpeerProp) []uint32 {
33+ out := make([]uint32, len(p.data)/4)
34+ for i := range out {
35+ out[i] = binary.LittleEndian.Uint32(p.data[4*i:])
36+ }
37+ return out
38+}
39+
40+func TestBootRequests(t *testing.T) {
41+ x, p := bootPeer(t, nil)
42+ reqs := p.requests()
43+
44+ first := -1
45+ for i, r := range reqs {
46+ if r.major == opInternAtom {
47+ first = i
48+ break
49+ }
50+ }
51+ require.NotEqual(t, -1, first)
52+ for i := 0; i < atomCount; i++ {
53+ r := reqs[first+i]
54+ require.EqualValues(t, opInternAtom, r.major, i)
55+ n := int(binary.LittleEndian.Uint16(r.raw[4:]))
56+ require.Equal(t, atomNames[i], string(r.raw[8:8+n]), i)
57+ require.Equal(t, p.atom(atomNames[i]), x.atoms[i])
58+ }
59+
60+ // Root mask, then composite redirect, then the WM window and ownership.
61+ var rootMask, redirect, create, s0, cm, focus int
62+ rootMask, redirect, create, s0, cm, focus = -1, -1, -1, -1, -1, -1
63+ wm := x.wmWin
64+ for i := first + atomCount; i < len(reqs); i++ {
65+ r := reqs[i]
66+ switch {
67+ case r.major == opChangeWindowAttributes && r.u32(4) == peerRoot:
68+ rootMask = i
69+ require.EqualValues(t, 1<<11, r.u32(8))
70+ want := uint32(xproto.EventMaskSubstructureRedirect | xproto.EventMaskSubstructureNotify | xproto.EventMaskPropertyChange)
71+ require.Equal(t, want, r.u32(12))
72+ case r.major == 142 && r.minor == 2:
73+ redirect = i
74+ require.EqualValues(t, peerRoot, r.u32(4))
75+ require.EqualValues(t, 1, r.raw[8], "manual")
76+ case r.major == opCreateWindow:
77+ create = i
78+ require.Equal(t, wm, r.u32(4))
79+ case r.major == opSetSelectionOwner && r.u32(8) == p.atom("WM_S0"):
80+ s0 = i
81+ require.Equal(t, wm, r.u32(4))
82+ case r.major == opSetSelectionOwner && r.u32(8) == p.atom("_NET_WM_CM_S0"):
83+ cm = i
84+ case r.major == opSetInputFocus:
85+ focus = i
86+ require.EqualValues(t, 0, r.u32(4), "focus None")
87+ }
88+ }
89+ for name, i := range map[string]int{"rootMask": rootMask, "redirect": redirect, "create": create, "WM_S0": s0, "CM_S0": cm, "focus": focus} {
90+ require.NotEqual(t, -1, i, name)
91+ }
92+ require.Less(t, rootMask, redirect)
93+ require.Less(t, redirect, create)
94+ require.Less(t, create, s0)
95+
96+ sup, ok := p.prop(peerRoot, "_NET_SUPPORTED")
97+ require.True(t, ok)
98+ var names []string
99+ byAtom := map[uint32]string{}
100+ for n, a := range p.atoms {
101+ byAtom[a] = n
102+ }
103+ for _, a := range atomsOf(sup) {
104+ names = append(names, byAtom[a])
105+ }
106+ require.Equal(t, []string{
107+ "_NET_WM_STATE", "_NET_ACTIVE_WINDOW", "_NET_CLOSE_WINDOW", "_NET_WM_STATE_FOCUSED",
108+ "_NET_WM_STATE_MODAL", "_NET_WM_STATE_FULLSCREEN", "_NET_WM_STATE_HIDDEN", "_NET_CLIENT_LIST",
109+ "_NET_CLIENT_LIST_STACKING", "_NET_SUPPORTING_WM_CHECK", "_NET_WM_NAME", "_NET_WM_PID",
110+ "_NET_WM_WINDOW_TYPE",
111+ }, names)
112+ require.NotContains(t, names, "_NET_WM_MOVERESIZE")
113+ require.NotContains(t, names, "_NET_WM_STATE_MAXIMIZED_VERT")
114+
115+ check, ok := p.prop(peerRoot, "_NET_SUPPORTING_WM_CHECK")
116+ require.True(t, ok)
117+ require.Equal(t, []uint32{wm}, atomsOf(check))
118+ check, ok = p.prop(wm, "_NET_SUPPORTING_WM_CHECK")
119+ require.True(t, ok)
120+ require.Equal(t, []uint32{wm}, atomsOf(check))
121+ name, ok := p.prop(wm, "_NET_WM_NAME")
122+ require.True(t, ok)
123+ require.Equal(t, "neferwl", string(name.data))
124+ active, ok := p.prop(peerRoot, "_NET_ACTIVE_WINDOW")
125+ require.True(t, ok)
126+ require.Equal(t, []uint32{0}, atomsOf(active))
127+ for _, l := range []string{"_NET_CLIENT_LIST", "_NET_CLIENT_LIST_STACKING"} {
128+ v, ok := p.prop(peerRoot, l)
129+ require.True(t, ok, l)
130+ require.Empty(t, v.data, l)
131+ }
132+}
133+
134+func TestBootWMExists(t *testing.T) {
135+ fd, p := startXPeer(t, fullPeerExts())
136+ p.set(func(p *xpeer) { p.denyRoot = true })
137+ x, err := Boot(fd, testLog())
138+ require.ErrorIs(t, err, ErrWMExists)
139+ require.Nil(t, x)
140+}
141+
142+func TestBootNoComposite(t *testing.T) {
143+ exts := fullPeerExts()
144+ delete(exts, "Composite")
145+ fd, _ := startXPeer(t, exts)
146+ _, err := Boot(fd, testLog())
147+ require.ErrorIs(t, err, ErrNoComposite)
148+}
149+
150+func TestBootOptionalExtensions(t *testing.T) {
151+ exts := fullPeerExts()
152+ delete(exts, "XFIXES")
153+ delete(exts, "X-Resource")
154+ fd, _ := startXPeer(t, exts)
155+ x, err := Boot(fd, testLog())
156+ require.NoError(t, err)
157+ x.CloseConn()
158+}
159+
160+func TestBootConnectFailure(t *testing.T) {
161+ fd, p := startXPeer(t, fullPeerExts())
162+ p.hangup()
163+ _, err := Boot(fd, testLog())
164+ require.Error(t, err)
165+}
166+
167+func TestBootExistingWindows(t *testing.T) {
168+ x, p := bootPeer(t, func(p *xpeer) {
169+ p.children = []uint32{0x400001, 0x400002}
170+ p.attrs[0x400002] = [2]byte{1, 2} // override-redirect, viewable
171+ })
172+ _ = p
173+ require.NotZero(t, x.Slot(0x400001))
174+ require.NotZero(t, x.Slot(0x400002))
175+ require.Equal(t, kindManaged, x.tab.rec(x.Slot(0x400001)).kind)
176+ require.Equal(t, kindUnmanaged, x.tab.rec(x.Slot(0x400002)).kind)
177+}
178+
179+// readUntil dispatches the socket until cond holds.
180+func readUntil(t *testing.T, x *XWM, cond func() bool) {
181+ t.Helper()
182+ deadline := time.Now().Add(5 * time.Second)
183+ for !cond() {
184+ if time.Now().After(deadline) {
185+ t.Fatal("condition never held")
186+ }
187+ x.onReadable()
188+ time.Sleep(time.Millisecond)
189+ }
190+}
191+
192+func newPost() (post func(func()), runs *[]func()) {
193+ var q []func()
194+ return func(f func()) { q = append(q, f) }, &q
195+}
196+
197+func TestAttachAnnouncesWindowsFoundAtBoot(t *testing.T) {
198+ x, _ := bootPeer(t, func(p *xpeer) {
199+ p.children = []uint32{0x400001, 0x400002}
200+ p.attrs[0x400002] = [2]byte{1, 2}
201+ })
202+ h := newMockhost(t)
203+ src := newMockfdSource(t)
204+ h.EXPECT().Created(x.Slot(0x400001), uint32(0x400001)).Return(ports.WindowID(7)).Once()
205+ post, _ := newPost()
206+ x.attach(src, h, post)
207+ require.Equal(t, ports.WindowID(7), x.tab.rec(x.Slot(0x400001)).id)
208+}
209+
210+func attached(t *testing.T) (*XWM, *xpeer, *mockhost, *mockfdSource, *[]func()) {
211+ t.Helper()
212+ x, p := bootPeer(t, nil)
213+ h := newMockhost(t)
214+ src := newMockfdSource(t)
215+ post, runs := newPost()
216+ x.attach(src, h, post)
217+ return x, p, h, src, runs
218+}
219+
220+func TestCreateAndDestroyManaged(t *testing.T) {
221+ x, p, h, src, _ := attached(t)
222+ const win = 0x500001
223+ h.EXPECT().Created(mock.Anything, uint32(win)).Return(ports.WindowID(3)).Once()
224+ p.createNotify(win, false)
225+ readUntil(t, x, func() bool { return x.Slot(win) != 0 })
226+ s := x.Slot(win)
227+ require.Equal(t, kindManaged, x.tab.rec(s).kind)
228+ require.Equal(t, ports.WindowID(3), x.tab.rec(s).id)
229+
230+ // The window is watched for property and focus changes.
231+ req, _ := p.waitReq(0, func(r xpeerReq) bool { return r.major == opChangeWindowAttributes && r.u32(4) == win })
232+ require.EqualValues(t, xproto.EventMaskPropertyChange|xproto.EventMaskFocusChange, req.u32(12))
233+
234+ h.EXPECT().Dissociate(s, true).Once()
235+ p.destroyNotify(win)
236+ readUntil(t, x, func() bool { return x.Slot(win) == 0 })
237+ require.Zero(t, x.tab.n)
238+ _ = src
239+}
240+
241+func TestOverrideRedirectNeverReachesHost(t *testing.T) {
242+ x, p, _, _, _ := attached(t) // the mock fails the test on any host call
243+ p.createNotify(0x500002, true)
244+ readUntil(t, x, func() bool { return x.Slot(0x500002) != 0 })
245+ require.Equal(t, kindUnmanaged, x.tab.rec(x.Slot(0x500002)).kind)
246+ require.EqualValues(t, 1, x.Stats().UnmanagedSeen)
247+ p.destroyNotify(0x500002)
248+ readUntil(t, x, func() bool { return x.Slot(0x500002) == 0 })
249+}
250+
251+func TestCloseDissociatesEverything(t *testing.T) {
252+ x, p, h, src, _ := attached(t)
253+ for _, win := range []uint32{0x500001, 0x500002} {
254+ h.EXPECT().Created(mock.Anything, win).Return(ports.WindowID(win)).Once()
255+ p.createNotify(win, false)
256+ }
257+ p.createNotify(0x500003, true)
258+ readUntil(t, x, func() bool { return x.Slot(0x500003) != 0 })
259+ h.EXPECT().Dissociate(x.Slot(0x500001), true).Once()
260+ h.EXPECT().Dissociate(x.Slot(0x500002), true).Once()
261+ src.EXPECT().Remove().Once()
262+ x.Close()
263+ x.Close() // idempotent
264+ require.True(t, x.closed)
265+ require.Equal(t, -1, x.conn.Fd())
266+}
267+
268+func TestEOFClosesXWM(t *testing.T) {
269+ x, p, h, src, _ := attached(t)
270+ h.EXPECT().Created(mock.Anything, uint32(0x500001)).Return(ports.WindowID(1)).Once()
271+ p.createNotify(0x500001, false)
272+ readUntil(t, x, func() bool { return x.Slot(0x500001) != 0 })
273+ h.EXPECT().Dissociate(x.Slot(0x500001), true).Once()
274+ src.EXPECT().Remove().Once()
275+ p.hangup()
276+ readUntil(t, x, func() bool { return x.closed })
277+}
278+
279+func TestWakeCoalescesFlush(t *testing.T) {
280+ x, _, _, _, runs := attached(t)
281+ x.wake()
282+ x.wake()
283+ x.wake()
284+ require.Len(t, *runs, 1)
285+ (*runs)[0]()
286+ x.wake()
287+ require.Len(t, *runs, 2)
288+}
289+
290+func TestWriteBackpressure(t *testing.T) {
291+ x, p, _, src, _ := attached(t)
292+ p.pause()
293+ b := x.conn.Buf()
294+ big := make([]byte, 64<<10)
295+ for i := 0; i < 64; i++ { // 4 MiB: more than any socket buffer
296+ xproto.ChangeProperty(b, xproto.PropModeReplace, xproto.Window(x.root), xproto.AtomWmName, xproto.AtomString, 8, uint32(len(big)), big)
297+ }
298+ src.EXPECT().SetWritable(true).Return(nil).Once()
299+ x.flush()
300+ require.True(t, x.writable)
301+ x.flush() // still blocked: SetWritable is not called again
302+ x.onWritable()
303+ require.True(t, x.writable)
304+
305+ drained := false
306+ src.EXPECT().SetWritable(false).RunAndReturn(func(bool) error { drained = true; return nil }).Once()
307+ p.resume()
308+ deadline := time.Now().Add(10 * time.Second)
309+ for !drained {
310+ require.True(t, time.Now().Before(deadline), "never drained")
311+ x.onWritable()
312+ time.Sleep(time.Millisecond)
313+ }
314+ require.False(t, x.writable)
315+}
316+
317+func TestFlushWithNothingQueuedSendsNothing(t *testing.T) {
318+ x, p, _, _, _ := attached(t)
319+ n := len(p.requests())
320+ x.flush()
321+ time.Sleep(20 * time.Millisecond)
322+ require.Len(t, p.requests(), n, "nothing queued, nothing sent")
323+}
324+
325+// The XWM never blocks after Boot: NewXID, Sync and InternAtoms are startup
326+// only, so they may appear in production code only inside boot().
327+func TestNoBlockingCallsOutsideBoot(t *testing.T) {
328+ files, err := os.ReadDir(".")
329+ require.NoError(t, err)
330+ for _, f := range files {
331+ name := f.Name()
332+ if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
333+ continue
334+ }
335+ src, err := os.ReadFile(name)
336+ require.NoError(t, err)
337+ text := string(src)
338+ lo, hi := 0, 0
339+ if name == "xwm.go" {
340+ lo = strings.Index(text, "func (x *XWM) boot() error")
341+ hi = strings.Index(text, "// Attach registers")
342+ require.Greater(t, hi, lo)
343+ }
344+ for _, call := range []string{".NewXID(", ".Sync(", ".InternAtoms("} {
345+ for _, at := range allIndex(text, call) {
346+ require.True(t, name == "xwm.go" && at > lo && at < hi, "%s: %s outside boot", name, call)
347+ }
348+ }
349+ }
350+}
351+
352+func allIndex(s, sub string) []int {
353+ var out []int
354+ for off := 0; ; {
355+ i := strings.Index(s[off:], sub)
356+ if i < 0 {
357+ return out
358+ }
359+ out = append(out, off+i)
360+ off += i + len(sub)
361+ }
362+}

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
esc
close a dialog, leave a field

Branches 26

Tags 10

↑↓ moveenter openesc close