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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
16 files · +2549 -1
M .mockery.yml +12 -0
| @@ -195,3 +195,15 @@ packages: | |||
| 195 | 195 | dir: internal/adapters/xwayland | |
| 196 | 196 | pkgname: xwayland | |
| 197 | 197 | 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/xwayland | 195 | dir: internal/adapters/xwayland | ||
| 196 | pkgname: xwayland | 196 | pkgname: xwayland | ||
| 197 | filename: starter_mock_test.go | 197 | 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: | |||
| 69 | 69 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | |
| 70 | 70 | # Fast, mandatory guards for tiled content publications and SHM copies. | |
| 71 | 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=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 | |
| 73 | 74 | check: vet test arch fakes-check log-check perf-check | |
| 74 | 75 | ||
| 75 | 76 | # 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=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 | |||
| 73 | check: vet test arch fakes-check log-check perf-check | 74 | check: vet test arch fakes-check log-check perf-check | ||
| 74 | 75 | ||||
| 75 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | 76 | # 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 | + | } | |||