#21 WIP: feat(xwayland): add built-in Xwayland launcher
- state
- draft
- branch
- feat/xwayland-builtin → main
- opened
- bnema ·
- updated
M .mockery.yml +30 -0
| @@ -87,6 +87,11 @@ packages: | |||
| 87 | 87 | dir: internal/adapters/wayland | |
| 88 | 88 | pkgname: wayland | |
| 89 | 89 | filename: acquire_poller_mock_test.go | |
| 90 | + | xwmSelection: | |
| 91 | + | config: | |
| 92 | + | dir: internal/adapters/wayland | |
| 93 | + | pkgname: wayland | |
| 94 | + | filename: xwm_selection_mock_test.go | |
| 90 | 95 | syncobjDevice: | |
| 91 | 96 | config: | |
| 92 | 97 | dir: internal/adapters/wayland | |
| @@ -187,3 +192,28 @@ packages: | |||
| 187 | 192 | dir: internal/adapters/screencast | |
| 188 | 193 | pkgname: screencast | |
| 189 | 194 | filename: pipewire_opener_mock_test.go | |
| 195 | + | ||
| 196 | + | github.com/bnema/neferwl/internal/adapters/xwayland: | |
| 197 | + | interfaces: | |
| 198 | + | starter: | |
| 199 | + | config: | |
| 200 | + | dir: internal/adapters/xwayland | |
| 201 | + | pkgname: xwayland | |
| 202 | + | filename: starter_mock_test.go | |
| 203 | + | github.com/bnema/neferwl/internal/adapters/wayland/xwm: | |
| 204 | + | interfaces: | |
| 205 | + | host: | |
| 206 | + | config: | |
| 207 | + | dir: internal/adapters/wayland/xwm | |
| 208 | + | pkgname: xwm | |
| 209 | + | filename: host_mock_test.go | |
| 210 | + | fdSource: | |
| 211 | + | config: | |
| 212 | + | dir: internal/adapters/wayland/xwm | |
| 213 | + | pkgname: xwm | |
| 214 | + | filename: fdsource_mock_test.go | |
| 215 | + | loop: | |
| 216 | + | config: | |
| 217 | + | dir: internal/adapters/wayland/xwm | |
| 218 | + | pkgname: xwm | |
| 219 | + | filename: loop_mock_test.go | |
| @@ -87,6 +87,11 @@ packages: | |||||
| 87 | dir: internal/adapters/wayland | 87 | dir: internal/adapters/wayland | ||
| 88 | pkgname: wayland | 88 | pkgname: wayland | ||
| 89 | filename: acquire_poller_mock_test.go | 89 | filename: acquire_poller_mock_test.go | ||
| 90 | + | xwmSelection: | |||
| 91 | + | config: | |||
| 92 | + | dir: internal/adapters/wayland | |||
| 93 | + | pkgname: wayland | |||
| 94 | + | filename: xwm_selection_mock_test.go | |||
| 90 | syncobjDevice: | 95 | syncobjDevice: | ||
| 91 | config: | 96 | config: | ||
| 92 | dir: internal/adapters/wayland | 97 | dir: internal/adapters/wayland | ||
| @@ -187,3 +192,28 @@ packages: | |||||
| 187 | dir: internal/adapters/screencast | 192 | dir: internal/adapters/screencast | ||
| 188 | pkgname: screencast | 193 | pkgname: screencast | ||
| 189 | filename: pipewire_opener_mock_test.go | 194 | filename: pipewire_opener_mock_test.go | ||
| 195 | + | ||||
| 196 | + | github.com/bnema/neferwl/internal/adapters/xwayland: | |||
| 197 | + | interfaces: | |||
| 198 | + | starter: | |||
| 199 | + | config: | |||
| 200 | + | dir: internal/adapters/xwayland | |||
| 201 | + | pkgname: xwayland | |||
| 202 | + | filename: starter_mock_test.go | |||
| 203 | + | github.com/bnema/neferwl/internal/adapters/wayland/xwm: | |||
| 204 | + | interfaces: | |||
| 205 | + | host: | |||
| 206 | + | config: | |||
| 207 | + | dir: internal/adapters/wayland/xwm | |||
| 208 | + | pkgname: xwm | |||
| 209 | + | filename: host_mock_test.go | |||
| 210 | + | fdSource: | |||
| 211 | + | config: | |||
| 212 | + | dir: internal/adapters/wayland/xwm | |||
| 213 | + | pkgname: xwm | |||
| 214 | + | filename: fdsource_mock_test.go | |||
| 215 | + | loop: | |||
| 216 | + | config: | |||
| 217 | + | dir: internal/adapters/wayland/xwm | |||
| 218 | + | pkgname: xwm | |||
| 219 | + | filename: loop_mock_test.go | |||
M Makefile +9 -2
| @@ -1,4 +1,4 @@ | |||
| 1 | - | .PHONY: build test vet race mocks mocks-check spv-check fakes-check log-check arch adapter-imports check perf-check color-check bin tty logs pkg install | |
| 1 | + | .PHONY: build test vet race e2e-xwayland mocks mocks-check spv-check fakes-check log-check arch adapter-imports check perf-check color-check bin tty logs pkg install | |
| 2 | 2 | ||
| 3 | 3 | # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net. | |
| 4 | 4 | TTY_TIMEOUT ?= 0 | |
| @@ -26,6 +26,7 @@ test: | |||
| 26 | 26 | CGO_ENABLED=0 go test ./... | |
| 27 | 27 | vet: | |
| 28 | 28 | CGO_ENABLED=0 go vet ./... | |
| 29 | + | CGO_ENABLED=0 go vet -tags xwayland ./internal/app | |
| 29 | 30 | race: | |
| 30 | 31 | # cgo is enabled only for the race test binary. | |
| 31 | 32 | CGO_ENABLED=1 go test -race ./... | |
| @@ -69,7 +70,8 @@ adapter-imports: | |||
| 69 | 70 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | |
| 70 | 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | |
| 71 | 72 | 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 | |
| 73 | + | 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|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|TestUnmanagedGeometryAllocations|TestFocusInUnmanagedAllocations|TestUnmanagedPublishAllocations|TestUnmanagedHitAllocations|TestOverrideMovedUnchangedAllocations|TestFullscreenConfigureAllocations|TestKeyboardGrabSyncAllocations|TestSelectionCopyAllocations|TestSelectionCopyAllocationsWaylandToX|TestSelectionINCRChunkAllocations|TestXSettingsNoChangeAllocations|TestSettleLeftAllocs)$$' -count=1 | |
| 74 | + | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | |
| 73 | 75 | check: vet test arch fakes-check log-check perf-check | |
| 74 | 76 | ||
| 75 | 77 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | |
| @@ -77,6 +79,11 @@ check: vet test arch fakes-check log-check perf-check | |||
| 77 | 79 | color-check: | |
| 78 | 80 | CGO_ENABLED=0 go test -tags colorcheck ./internal/app -run '^TestColorCheck' -count=1 -v | |
| 79 | 81 | ||
| 82 | + | # End-to-end test of the built-in XWM: a real Xwayland and an X11 probe on a | |
| 83 | + | # headless NeferWL. Needs Xwayland and a GPU; not part of check. | |
| 84 | + | e2e-xwayland: | |
| 85 | + | CGO_ENABLED=1 go test -tags xwayland -race ./internal/app -run TestXwayland -count=1 -v | |
| 86 | + | ||
| 80 | 87 | # Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules | |
| 81 | 88 | # come from the module proxy in prepare(); the build itself runs offline. | |
| 82 | 89 | # pacman-ordered version: 0.0.0.r<commits>.g<hash>; a tag replaces 0.0.0. | |
| @@ -1,4 +1,4 @@ | |||||
| 1 | - | .PHONY: build test vet race mocks mocks-check spv-check fakes-check log-check arch adapter-imports check perf-check color-check bin tty logs pkg install | 1 | + | .PHONY: build test vet race e2e-xwayland mocks mocks-check spv-check fakes-check log-check arch adapter-imports check perf-check color-check bin tty logs pkg install |
| 2 | 2 | ||||
| 3 | # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net. | 3 | # 0 runs until quit; set e.g. TTY_TIMEOUT=60s for a safety net. | ||
| 4 | TTY_TIMEOUT ?= 0 | 4 | TTY_TIMEOUT ?= 0 | ||
| @@ -26,6 +26,7 @@ test: | |||||
| 26 | CGO_ENABLED=0 go test ./... | 26 | CGO_ENABLED=0 go test ./... | ||
| 27 | vet: | 27 | vet: | ||
| 28 | CGO_ENABLED=0 go vet ./... | 28 | CGO_ENABLED=0 go vet ./... | ||
| 29 | + | CGO_ENABLED=0 go vet -tags xwayland ./internal/app | |||
| 29 | race: | 30 | race: | ||
| 30 | # cgo is enabled only for the race test binary. | 31 | # cgo is enabled only for the race test binary. | ||
| 31 | CGO_ENABLED=1 go test -race ./... | 32 | CGO_ENABLED=1 go test -race ./... | ||
| @@ -69,7 +70,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; } | 70 | [ -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. | 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | ||
| 71 | perf-check: | 72 | 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 | 73 | + | 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|TestPropertyUnchangedAllocations|TestDispatchAllocations|TestConfigureFlushAllocations|TestFocusAllocations|TestSpaceAllocations|TestUnmanagedGeometryAllocations|TestFocusInUnmanagedAllocations|TestUnmanagedPublishAllocations|TestUnmanagedHitAllocations|TestOverrideMovedUnchangedAllocations|TestFullscreenConfigureAllocations|TestKeyboardGrabSyncAllocations|TestSelectionCopyAllocations|TestSelectionCopyAllocationsWaylandToX|TestSelectionINCRChunkAllocations|TestXSettingsNoChangeAllocations|TestSettleLeftAllocs)$$' -count=1 |
| 74 | + | 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 | 75 | check: vet test arch fakes-check log-check perf-check | ||
| 74 | 76 | ||||
| 75 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | 77 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | ||
| @@ -77,6 +79,11 @@ check: vet test arch fakes-check log-check perf-check | |||||
| 77 | color-check: | 79 | color-check: | ||
| 78 | CGO_ENABLED=0 go test -tags colorcheck ./internal/app -run '^TestColorCheck' -count=1 -v | 80 | CGO_ENABLED=0 go test -tags colorcheck ./internal/app -run '^TestColorCheck' -count=1 -v | ||
| 79 | 81 | ||||
| 82 | + | # End-to-end test of the built-in XWM: a real Xwayland and an X11 probe on a | |||
| 83 | + | # headless NeferWL. Needs Xwayland and a GPU; not part of check. | |||
| 84 | + | e2e-xwayland: | |||
| 85 | + | CGO_ENABLED=1 go test -tags xwayland -race ./internal/app -run TestXwayland -count=1 -v | |||
| 86 | + | ||||
| 80 | # Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules | 87 | # Arch package of the committed HEAD (packaging/arch/PKGBUILD). Go modules | ||
| 81 | # come from the module proxy in prepare(); the build itself runs offline. | 88 | # come from the module proxy in prepare(); the build itself runs offline. | ||
| 82 | # pacman-ordered version: 0.0.0.r<commits>.g<hash>; a tag replaces 0.0.0. | 89 | # pacman-ordered version: 0.0.0.r<commits>.g<hash>; a tag replaces 0.0.0. | ||
M README.md +1 -1
| @@ -149,7 +149,7 @@ See [Configuration](docs/config.md) for all keys, actions, bindings and automati | |||
| 149 | 149 | - Explicit sync carries client fences to the GPU and KMS, enabled by default with no flag. HDR10 on capable displays includes configurable SDR brightness. | |
| 150 | 150 | - Separate workspaces and fractional scale per output; workspaces move when a monitor is unplugged and return when it reconnects. | |
| 151 | 151 | - Hidden apps get frame callbacks once per second and the suspended state, whether on another workspace, in a hidden stash, off screen or under fullscreen. Captured windows keep their output's refresh rate. | |
| 152 | - | - Steam and other X11 clients run through xwayland-satellite; Wine can run natively on Wayland. | |
| 152 | + | - Steam and other X11 clients run through xwayland-satellite, or through the experimental built-in window manager (`xwayland = builtin`; menus, popups and tooltips, fullscreen games with keyboard grab; text clipboard and primary selection in both directions, XSETTINGS and a themed root cursor). Wine can run natively on Wayland. | |
| 153 | 153 | ||
| 154 | 154 | ### Windows and workspaces | |
| 155 | 155 | ||
| @@ -149,7 +149,7 @@ See [Configuration](docs/config.md) for all keys, actions, bindings and automati | |||||
| 149 | - Explicit sync carries client fences to the GPU and KMS, enabled by default with no flag. HDR10 on capable displays includes configurable SDR brightness. | 149 | - Explicit sync carries client fences to the GPU and KMS, enabled by default with no flag. HDR10 on capable displays includes configurable SDR brightness. | ||
| 150 | - Separate workspaces and fractional scale per output; workspaces move when a monitor is unplugged and return when it reconnects. | 150 | - Separate workspaces and fractional scale per output; workspaces move when a monitor is unplugged and return when it reconnects. | ||
| 151 | - Hidden apps get frame callbacks once per second and the suspended state, whether on another workspace, in a hidden stash, off screen or under fullscreen. Captured windows keep their output's refresh rate. | 151 | - Hidden apps get frame callbacks once per second and the suspended state, whether on another workspace, in a hidden stash, off screen or under fullscreen. Captured windows keep their output's refresh rate. | ||
| 152 | - | - Steam and other X11 clients run through xwayland-satellite; Wine can run natively on Wayland. | 152 | + | - Steam and other X11 clients run through xwayland-satellite, or through the experimental built-in window manager (`xwayland = builtin`; menus, popups and tooltips, fullscreen games with keyboard grab; text clipboard and primary selection in both directions, XSETTINGS and a themed root cursor). Wine can run natively on Wayland. |
| 153 | 153 | ||||
| 154 | ### Windows and workspaces | 154 | ### Windows and workspaces | ||
| 155 | 155 | ||||
M docs/config.md +9 -3
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | # Configuration | |
| 2 | 2 | ||
| 3 | - | NeferWL reads `~/.config/neferwl/config` (or `$XDG_CONFIG_HOME/neferwl/config`). Changes apply when you save, except `startup` and `xwayland`, which apply at the next start. | |
| 3 | + | NeferWL reads `~/.config/neferwl/config` (or `$XDG_CONFIG_HOME/neferwl/config`). Changes apply when you save, except `startup`, `xwayland` and `xwayland.xsettings`, which apply at the next start. | |
| 4 | 4 | ||
| 5 | 5 | ```sh | |
| 6 | 6 | neferwl --config <path> # use another file | |
| @@ -27,7 +27,8 @@ See [examples/config](../examples/config) for every key with its default. | |||
| 27 | 27 | | `terminal` | `$TERMINAL` or `foot` | Terminal command | | |
| 28 | 28 | | `terminal.auto-open` | `first` | Open a terminal on the first workspace (`first`), on every empty one (`all`), or never (`off`) | | |
| 29 | 29 | | `startup` | none | Commands run at session start, separated by `;` | | |
| 30 | - | | `xwayland` | `xwayland-satellite` | X11 support; `off` disables it | | |
| 30 | + | | `xwayland` | `satellite` | `off`, `satellite`, `builtin` or a satellite binary path | | |
| 31 | + | | `xwayland.xsettings` | `on` | With `xwayland = builtin`, publish `Gtk/CursorThemeName`, `Gtk/CursorThemeSize`, `Xft/DPI`, `Gdk/WindowScalingFactor` and `Gdk/UnscaledDPI` to X11 apps through XSETTINGS (restart required); the scale is always announced as 1 (96 dpi) because X11 apps see logical sizes and neferwl upscales their windows; `off` leaves them to the apps | | |
| 31 | 32 | | `background` | `#111111` | Background color | | |
| 32 | 33 | | `border.radius` | `0` | Corner radius in logical pixels, `0` to `64`; floats always, tiles only with gaps | | |
| 33 | 34 | | `border.width` | `2` | Border width in pixels; `0` hides borders | | |
| @@ -103,6 +104,8 @@ destination output, even if that output has no windows. Same-screen workspace | |||
| 103 | 104 | switches and mouse focus do not trigger a flash. | |
| 104 | 105 | It runs when `animations = on`. | |
| 105 | 106 | ||
| 107 | + | `xwayland = satellite` uses `xwayland-satellite` from `PATH`; a custom path also selects satellite. `off` disables X11. `builtin` (experimental): neferwl starts `Xwayland` itself and manages X11 windows as its window manager: windows tile, take focus, close with `close-window` (`WM_DELETE_WINDOW`) and show their title and class. Menus, popups and tooltips (override-redirect windows) are drawn above their window at the right place and never appear as windows in `neferwl state`. X11 apps get the cursor theme and size from `XCURSOR_THEME` and `XCURSOR_SIZE`, a scale-1 DPI (96) through XSETTINGS (X11 apps see logical sizes; neferwl upscales their windows) (`xwayland.xsettings = off` turns it off), and the themed arrow on the X background. The clipboard and the primary selection are shared with X11 apps in both directions (text only; nothing crosses while the session is locked or otherwise protected). | |
| 108 | + | ||
| 106 | 109 | ## Automatic terminal | |
| 107 | 110 | ||
| 108 | 111 | The command comes from `terminal`, then `$TERMINAL`, then `foot`. | |
| @@ -144,7 +147,7 @@ Rules place an app's windows when they first open. | |||
| 144 | 147 | | `rule.<name>.width` | Column width in [cells](#column-widths), 1 to 16 | | |
| 145 | 148 | ||
| 146 | 149 | ```ini | |
| 147 | - | rule.games.app-id = steam_app_.* | |
| 150 | + | rule.games.app-id = steam_app_\d+ | |
| 148 | 151 | rule.games.workspace = 3 | |
| 149 | 152 | rule.chat.app-id = discord|org\.telegram\..* | |
| 150 | 153 | rule.chat.workspace = chat | |
| @@ -155,6 +158,9 @@ rule.calc.floating = on | |||
| 155 | 158 | ||
| 156 | 159 | - Every matching rule applies, in file order; later rules win. | |
| 157 | 160 | - A rule never moves your focus or view. | |
| 161 | + | - Proton games (X11) use `steam_app_<id>` as app ID, taken from `WM_CLASS`. | |
| 162 | + | - An X11 window that opens exactly on one output's area opens on that output, unless a rule sets `monitor`. | |
| 163 | + | - A rule can float a Proton game: `rule.games.app-id = steam_app_\d+` with `rule.games.floating = on`. Without a rule, X11 games tile until they ask for fullscreen. | |
| 158 | 164 | - To find an app ID: `neferwl state | jq '.windows[].app_id'`. | |
| 159 | 165 | ||
| 160 | 166 | ## Output placement | |
| @@ -1,6 +1,6 @@ | |||||
| 1 | # Configuration | 1 | # Configuration | ||
| 2 | 2 | ||||
| 3 | - | NeferWL reads `~/.config/neferwl/config` (or `$XDG_CONFIG_HOME/neferwl/config`). Changes apply when you save, except `startup` and `xwayland`, which apply at the next start. | 3 | + | NeferWL reads `~/.config/neferwl/config` (or `$XDG_CONFIG_HOME/neferwl/config`). Changes apply when you save, except `startup`, `xwayland` and `xwayland.xsettings`, which apply at the next start. |
| 4 | 4 | ||||
| 5 | ```sh | 5 | ```sh | ||
| 6 | neferwl --config <path> # use another file | 6 | neferwl --config <path> # use another file | ||
| @@ -27,7 +27,8 @@ See [examples/config](../examples/config) for every key with its default. | |||||
| 27 | | `terminal` | `$TERMINAL` or `foot` | Terminal command | | 27 | | `terminal` | `$TERMINAL` or `foot` | Terminal command | | ||
| 28 | | `terminal.auto-open` | `first` | Open a terminal on the first workspace (`first`), on every empty one (`all`), or never (`off`) | | 28 | | `terminal.auto-open` | `first` | Open a terminal on the first workspace (`first`), on every empty one (`all`), or never (`off`) | | ||
| 29 | | `startup` | none | Commands run at session start, separated by `;` | | 29 | | `startup` | none | Commands run at session start, separated by `;` | | ||
| 30 | - | | `xwayland` | `xwayland-satellite` | X11 support; `off` disables it | | 30 | + | | `xwayland` | `satellite` | `off`, `satellite`, `builtin` or a satellite binary path | |
| 31 | + | | `xwayland.xsettings` | `on` | With `xwayland = builtin`, publish `Gtk/CursorThemeName`, `Gtk/CursorThemeSize`, `Xft/DPI`, `Gdk/WindowScalingFactor` and `Gdk/UnscaledDPI` to X11 apps through XSETTINGS (restart required); the scale is always announced as 1 (96 dpi) because X11 apps see logical sizes and neferwl upscales their windows; `off` leaves them to the apps | | |||
| 31 | | `background` | `#111111` | Background color | | 32 | | `background` | `#111111` | Background color | | ||
| 32 | | `border.radius` | `0` | Corner radius in logical pixels, `0` to `64`; floats always, tiles only with gaps | | 33 | | `border.radius` | `0` | Corner radius in logical pixels, `0` to `64`; floats always, tiles only with gaps | | ||
| 33 | | `border.width` | `2` | Border width in pixels; `0` hides borders | | 34 | | `border.width` | `2` | Border width in pixels; `0` hides borders | | ||
| @@ -103,6 +104,8 @@ destination output, even if that output has no windows. Same-screen workspace | |||||
| 103 | switches and mouse focus do not trigger a flash. | 104 | switches and mouse focus do not trigger a flash. | ||
| 104 | It runs when `animations = on`. | 105 | It runs when `animations = on`. | ||
| 105 | 106 | ||||
| 107 | + | `xwayland = satellite` uses `xwayland-satellite` from `PATH`; a custom path also selects satellite. `off` disables X11. `builtin` (experimental): neferwl starts `Xwayland` itself and manages X11 windows as its window manager: windows tile, take focus, close with `close-window` (`WM_DELETE_WINDOW`) and show their title and class. Menus, popups and tooltips (override-redirect windows) are drawn above their window at the right place and never appear as windows in `neferwl state`. X11 apps get the cursor theme and size from `XCURSOR_THEME` and `XCURSOR_SIZE`, a scale-1 DPI (96) through XSETTINGS (X11 apps see logical sizes; neferwl upscales their windows) (`xwayland.xsettings = off` turns it off), and the themed arrow on the X background. The clipboard and the primary selection are shared with X11 apps in both directions (text only; nothing crosses while the session is locked or otherwise protected). | |||
| 108 | + | ||||
| 106 | ## Automatic terminal | 109 | ## Automatic terminal | ||
| 107 | 110 | ||||
| 108 | The command comes from `terminal`, then `$TERMINAL`, then `foot`. | 111 | The command comes from `terminal`, then `$TERMINAL`, then `foot`. | ||
| @@ -144,7 +147,7 @@ Rules place an app's windows when they first open. | |||||
| 144 | | `rule.<name>.width` | Column width in [cells](#column-widths), 1 to 16 | | 147 | | `rule.<name>.width` | Column width in [cells](#column-widths), 1 to 16 | | ||
| 145 | 148 | ||||
| 146 | ```ini | 149 | ```ini | ||
| 147 | - | rule.games.app-id = steam_app_.* | 150 | + | rule.games.app-id = steam_app_\d+ |
| 148 | rule.games.workspace = 3 | 151 | rule.games.workspace = 3 | ||
| 149 | rule.chat.app-id = discord|org\.telegram\..* | 152 | rule.chat.app-id = discord|org\.telegram\..* | ||
| 150 | rule.chat.workspace = chat | 153 | rule.chat.workspace = chat | ||
| @@ -155,6 +158,9 @@ rule.calc.floating = on | |||||
| 155 | 158 | ||||
| 156 | - Every matching rule applies, in file order; later rules win. | 159 | - Every matching rule applies, in file order; later rules win. | ||
| 157 | - A rule never moves your focus or view. | 160 | - A rule never moves your focus or view. | ||
| 161 | + | - Proton games (X11) use `steam_app_<id>` as app ID, taken from `WM_CLASS`. | |||
| 162 | + | - An X11 window that opens exactly on one output's area opens on that output, unless a rule sets `monitor`. | |||
| 163 | + | - A rule can float a Proton game: `rule.games.app-id = steam_app_\d+` with `rule.games.floating = on`. Without a rule, X11 games tile until they ask for fullscreen. | |||
| 158 | - To find an app ID: `neferwl state | jq '.windows[].app_id'`. | 164 | - To find an app ID: `neferwl state | jq '.windows[].app_id'`. | ||
| 159 | 165 | ||||
| 160 | ## Output placement | 166 | ## Output placement | ||
M docs/desktop.md +12 -1
| @@ -52,12 +52,23 @@ Candidate popups are not drawn yet. | |||
| 52 | 52 | ||
| 53 | 53 | ## X11 apps | |
| 54 | 54 | ||
| 55 | - | X11 apps such as Steam run through [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) 0.7+, found in `PATH`. NeferWL starts it when the first X11 app connects; do not start it yourself. | |
| 55 | + | By default, X11 apps such as Steam run through [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) 0.7+, found in `PATH`. NeferWL starts it when the first X11 app connects; do not start it yourself. | |
| 56 | 56 | ||
| 57 | 57 | ```text | |
| 58 | 58 | xwayland = off # no X11 support | |
| 59 | 59 | ``` | |
| 60 | 60 | ||
| 61 | + | With `xwayland = builtin` (experimental), neferwl starts `Xwayland` itself and manages X11 windows: they tile, take focus and close like Wayland windows. Menus, popups and tooltips of X11 apps are shown above their window and do not take the keyboard focus. X11 apps get the cursor theme (`XCURSOR_THEME`), cursor size (`XCURSOR_SIZE`) and DPI through XSETTINGS, always announced as scale 1 (96 dpi) because X11 apps see logical sizes and neferwl upscales their windows (`Gtk/CursorThemeName`, `Gtk/CursorThemeSize`, `Xft/DPI`, `Gdk/WindowScalingFactor`, `Gdk/UnscaledDPI`; `xwayland.xsettings = off` to disable), and the X background shows the themed arrow. The clipboard and the primary selection work between X11 and Wayland apps in both directions, for text; large copies are transferred in chunks (INCR). While the session is locked, no selection data crosses. | |
| 62 | + | ||
| 63 | + | Games with `builtin`: | |
| 64 | + | ||
| 65 | + | - **Fullscreen:** an X11 window can ask for fullscreen (`_NET_WM_STATE_FULLSCREEN`) and leave it again; `toggle-fullscreen` works too. A window that maps with the fullscreen state already set (games do) opens fullscreen in a column of its own, focused, even if its size is fixed. A request made within 1 second *after* the map is ignored, so a launcher that remembers a fullscreen size does not cover the windows it opens next. The app can ask again later. | |
| 66 | + | - **Output:** a window that opens exactly at one output's area opens on that output, and its fullscreen fills it. | |
| 67 | + | - **Keyboard grab:** an X11 app that grabs the keyboard inhibits the compositor binds only while it is focused and fullscreen; a windowed app cannot lock the binds. There is no escape bind, as with Wayland shortcut inhibitors; leave by unfullscreening from the app or switching away with the mouse. | |
| 68 | + | - **Pointer:** mouse-look works through relative pointer and pointer constraints. | |
| 69 | + | - **App ID:** Proton windows are named `steam_app_<id>`; match them with a [window rule](config.md#window-rules). | |
| 70 | + | - **Bypass hint:** `_NET_WM_BYPASS_COMPOSITOR` is read but changes nothing; direct scanout is chosen from the window state. | |
| 71 | + | ||
| 61 | 72 | ## Session locking | |
| 62 | 73 | ||
| 63 | 74 | NeferWL supports `ext-session-lock-v1` with an external locker such as swaylock or hyprlock. The locker checks the password; NeferWL hides the desktop and blocks input. | |
| @@ -52,12 +52,23 @@ Candidate popups are not drawn yet. | |||||
| 52 | 52 | ||||
| 53 | ## X11 apps | 53 | ## X11 apps | ||
| 54 | 54 | ||||
| 55 | - | X11 apps such as Steam run through [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) 0.7+, found in `PATH`. NeferWL starts it when the first X11 app connects; do not start it yourself. | 55 | + | By default, X11 apps such as Steam run through [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) 0.7+, found in `PATH`. NeferWL starts it when the first X11 app connects; do not start it yourself. |
| 56 | 56 | ||||
| 57 | ```text | 57 | ```text | ||
| 58 | xwayland = off # no X11 support | 58 | xwayland = off # no X11 support | ||
| 59 | ``` | 59 | ``` | ||
| 60 | 60 | ||||
| 61 | + | With `xwayland = builtin` (experimental), neferwl starts `Xwayland` itself and manages X11 windows: they tile, take focus and close like Wayland windows. Menus, popups and tooltips of X11 apps are shown above their window and do not take the keyboard focus. X11 apps get the cursor theme (`XCURSOR_THEME`), cursor size (`XCURSOR_SIZE`) and DPI through XSETTINGS, always announced as scale 1 (96 dpi) because X11 apps see logical sizes and neferwl upscales their windows (`Gtk/CursorThemeName`, `Gtk/CursorThemeSize`, `Xft/DPI`, `Gdk/WindowScalingFactor`, `Gdk/UnscaledDPI`; `xwayland.xsettings = off` to disable), and the X background shows the themed arrow. The clipboard and the primary selection work between X11 and Wayland apps in both directions, for text; large copies are transferred in chunks (INCR). While the session is locked, no selection data crosses. | |||
| 62 | + | ||||
| 63 | + | Games with `builtin`: | |||
| 64 | + | ||||
| 65 | + | - **Fullscreen:** an X11 window can ask for fullscreen (`_NET_WM_STATE_FULLSCREEN`) and leave it again; `toggle-fullscreen` works too. A window that maps with the fullscreen state already set (games do) opens fullscreen in a column of its own, focused, even if its size is fixed. A request made within 1 second *after* the map is ignored, so a launcher that remembers a fullscreen size does not cover the windows it opens next. The app can ask again later. | |||
| 66 | + | - **Output:** a window that opens exactly at one output's area opens on that output, and its fullscreen fills it. | |||
| 67 | + | - **Keyboard grab:** an X11 app that grabs the keyboard inhibits the compositor binds only while it is focused and fullscreen; a windowed app cannot lock the binds. There is no escape bind, as with Wayland shortcut inhibitors; leave by unfullscreening from the app or switching away with the mouse. | |||
| 68 | + | - **Pointer:** mouse-look works through relative pointer and pointer constraints. | |||
| 69 | + | - **App ID:** Proton windows are named `steam_app_<id>`; match them with a [window rule](config.md#window-rules). | |||
| 70 | + | - **Bypass hint:** `_NET_WM_BYPASS_COMPOSITOR` is read but changes nothing; direct scanout is chosen from the window state. | |||
| 71 | + | ||||
| 61 | ## Session locking | 72 | ## Session locking | ||
| 62 | 73 | ||||
| 63 | NeferWL supports `ext-session-lock-v1` with an external locker such as swaylock or hyprlock. The locker checks the password; NeferWL hides the desktop and blocks input. | 74 | NeferWL supports `ext-session-lock-v1` with an external locker such as swaylock or hyprlock. The locker checks the password; NeferWL hides the desktop and blocks input. | ||
M docs/dev/headless.md +25 -0
| @@ -62,6 +62,31 @@ WAYLAND_DISPLAY=<name> go run ./examples/testime -preedit nihon -commit 日本 | |||
| 62 | 62 | ||
| 63 | 63 | `-count <n>` exits after n commits; `-delay` sets the time between preedit and commit. | |
| 64 | 64 | ||
| 65 | + | ## X11 check | |
| 66 | + | ||
| 67 | + | `make e2e-xwayland` runs the end-to-end test of the built-in window manager (`xwayland = builtin`). It is behind the `xwayland` build tag, so `go test ./...` and `make check` only compile it (`go vet -tags xwayland`). It needs `Xwayland` on `PATH` and a GPU, and skips without Xwayland. The clipboard test also needs `wl-copy` and `wl-paste` (wl-clipboard) and skips without them; they run with the headless `WAYLAND_DISPLAY` and need no extra focus step, since the focused probe window is enough for `wl-copy --foreground` to set the selection. | |
| 68 | + | ||
| 69 | + | The test starts a headless NeferWL with a real `Xwayland` on the first free X display (a live session's `:0` stays untouched) and runs `internal/app/testdata/x11probe`, a small X11 client (`-class`, `-title`, `-size`, `-sleep`, `-at X,Y`) that paints its window green, prints `mapped WxH` and `focus`, and exits 0 on `WM_DELETE_WINDOW`. With `-override` it also maps a magenta override-redirect window (`-override-at`, `-override-size`, `-override-type menu|tooltip`, `-override-for`, `-override-shape`, `-override-dismiss-on-focusout`) and prints `override mapped`, `override unmapped` or `dismissed`. `-fullscreen` (with `-fullscreen-delay`, `-fullscreen-off`) requests `_NET_WM_STATE_FULLSCREEN` by client message and prints `fullscreen WxH` / `fullscreen off WxH` when the window manager publishes the state; `-keyboard-grab` calls `XGrabKeyboard` and prints `grab`. Windowless modes: `-copy TEXT|@file` (with `-copy-sel CLIPBOARD|PRIMARY`, `-copy-for`) owns a selection and serves `TARGETS`, `UTF8_STRING`, `STRING` and `TEXT`, with INCR above 64 KiB, and prints `copy owned`; `-paste CLIPBOARD|PRIMARY` prints `paste "<text>"`, `paste len=N sha256=H` above 4 KiB, or `paste none`; `-xsettings` prints `xsettings serial=N Xft/DPI=… Gdk/WindowScalingFactor=… Gdk/UnscaledDPI=… Gtk/CursorThemeName=… Gtk/CursorThemeSize=…` or `xsettings none`; `-root-cursor` prints `cursor WxH` from XFixes `GetCursorImage`. `-at` also sends a ConfigureRequest with the window rect before the map, like games do. It checks: | |
| 70 | + | ||
| 71 | + | - the state lists one visible `X11Probe` window with the probe's pid, and the screenshot is mostly green inside its tile; | |
| 72 | + | - the probe receives focus; | |
| 73 | + | - a second probe tiles beside the first; | |
| 74 | + | - `key Super+q` closes the focused probe within 2 seconds and the state drops to one window; | |
| 75 | + | - an override-redirect menu is drawn magenta above the green toplevel at its X position, is not listed in the state, leaves focus on the toplevel and disappears from the screenshot once the probe unmaps it; | |
| 76 | + | - a menu is dismissed when its owner gets `FocusOut` because another X11 window took the focus; | |
| 77 | + | - a menu with an empty input shape is still drawn and still not a window; | |
| 78 | + | - a fullscreen request 1.5 s after the map fills the output (all green, no gap) and `Super+Shift+f` leaves it; the client can also leave it itself; a request at the map stays tiled (the 1 s grace); | |
| 79 | + | - with the keyboard grab, `Super+q` does not close a fullscreen focused client and works again once it leaves fullscreen; on a windowed client it closes it; | |
| 80 | + | - on two outputs, a window created at the second output's origin opens on `HEADLESS-2` and fullscreens there; | |
| 81 | + | - clipboard: `wl-copy` text of 4 KiB and 1 MiB (INCR) is read by `x11probe -paste`, and an `x11probe -copy` of the same sizes is read byte-exact by `wl-paste`, for CLIPBOARD and PRIMARY; `wl-copy --clear` gives `paste none`; | |
| 82 | + | - XSETTINGS: serial, `Xft/DPI=98304` and `Gdk/WindowScalingFactor=1` at any output scale (also with `output.HEADLESS-1.scale = 2`), and the cursor theme and size from `XCURSOR_THEME`/`XCURSOR_SIZE`; `xwayland.xsettings = off` leaves no owner; | |
| 83 | + | - root cursor: `GetCursorImage` over the root is the theme arrow at the theme size (a throwaway theme in `XCURSOR_PATH`); | |
| 84 | + | - a window rule `rule.games.app-id = steam_app_\d+` floats an X11 window of class `steam_app_413150`, and not another class. | |
| 85 | + | ||
| 86 | + | Not covered headless: relative pointer lock (needs a real pointer device) and Xwayland output hotplug (`--size` is fixed); these are unit-tested in the `wayland` adapter. | |
| 87 | + | ||
| 88 | + | Run it by hand with `go test -tags xwayland -race ./internal/app -run TestXwayland -count=1 -v`. | |
| 89 | + | ||
| 65 | 90 | ## Colour check | |
| 66 | 91 | ||
| 67 | 92 | `make color-check` renders `examples/testpattern` on a headless output and checks the colours, in SDR and in HDR (`--headless-hdr`). It needs a GPU and `/dev/udmabuf`, and is not part of `make check`. | |
| @@ -62,6 +62,31 @@ WAYLAND_DISPLAY=<name> go run ./examples/testime -preedit nihon -commit 日本 | |||||
| 62 | 62 | ||||
| 63 | `-count <n>` exits after n commits; `-delay` sets the time between preedit and commit. | 63 | `-count <n>` exits after n commits; `-delay` sets the time between preedit and commit. | ||
| 64 | 64 | ||||
| 65 | + | ## X11 check | |||
| 66 | + | ||||
| 67 | + | `make e2e-xwayland` runs the end-to-end test of the built-in window manager (`xwayland = builtin`). It is behind the `xwayland` build tag, so `go test ./...` and `make check` only compile it (`go vet -tags xwayland`). It needs `Xwayland` on `PATH` and a GPU, and skips without Xwayland. The clipboard test also needs `wl-copy` and `wl-paste` (wl-clipboard) and skips without them; they run with the headless `WAYLAND_DISPLAY` and need no extra focus step, since the focused probe window is enough for `wl-copy --foreground` to set the selection. | |||
| 68 | + | ||||
| 69 | + | The test starts a headless NeferWL with a real `Xwayland` on the first free X display (a live session's `:0` stays untouched) and runs `internal/app/testdata/x11probe`, a small X11 client (`-class`, `-title`, `-size`, `-sleep`, `-at X,Y`) that paints its window green, prints `mapped WxH` and `focus`, and exits 0 on `WM_DELETE_WINDOW`. With `-override` it also maps a magenta override-redirect window (`-override-at`, `-override-size`, `-override-type menu|tooltip`, `-override-for`, `-override-shape`, `-override-dismiss-on-focusout`) and prints `override mapped`, `override unmapped` or `dismissed`. `-fullscreen` (with `-fullscreen-delay`, `-fullscreen-off`) requests `_NET_WM_STATE_FULLSCREEN` by client message and prints `fullscreen WxH` / `fullscreen off WxH` when the window manager publishes the state; `-keyboard-grab` calls `XGrabKeyboard` and prints `grab`. Windowless modes: `-copy TEXT|@file` (with `-copy-sel CLIPBOARD|PRIMARY`, `-copy-for`) owns a selection and serves `TARGETS`, `UTF8_STRING`, `STRING` and `TEXT`, with INCR above 64 KiB, and prints `copy owned`; `-paste CLIPBOARD|PRIMARY` prints `paste "<text>"`, `paste len=N sha256=H` above 4 KiB, or `paste none`; `-xsettings` prints `xsettings serial=N Xft/DPI=… Gdk/WindowScalingFactor=… Gdk/UnscaledDPI=… Gtk/CursorThemeName=… Gtk/CursorThemeSize=…` or `xsettings none`; `-root-cursor` prints `cursor WxH` from XFixes `GetCursorImage`. `-at` also sends a ConfigureRequest with the window rect before the map, like games do. It checks: | |||
| 70 | + | ||||
| 71 | + | - the state lists one visible `X11Probe` window with the probe's pid, and the screenshot is mostly green inside its tile; | |||
| 72 | + | - the probe receives focus; | |||
| 73 | + | - a second probe tiles beside the first; | |||
| 74 | + | - `key Super+q` closes the focused probe within 2 seconds and the state drops to one window; | |||
| 75 | + | - an override-redirect menu is drawn magenta above the green toplevel at its X position, is not listed in the state, leaves focus on the toplevel and disappears from the screenshot once the probe unmaps it; | |||
| 76 | + | - a menu is dismissed when its owner gets `FocusOut` because another X11 window took the focus; | |||
| 77 | + | - a menu with an empty input shape is still drawn and still not a window; | |||
| 78 | + | - a fullscreen request 1.5 s after the map fills the output (all green, no gap) and `Super+Shift+f` leaves it; the client can also leave it itself; a request at the map stays tiled (the 1 s grace); | |||
| 79 | + | - with the keyboard grab, `Super+q` does not close a fullscreen focused client and works again once it leaves fullscreen; on a windowed client it closes it; | |||
| 80 | + | - on two outputs, a window created at the second output's origin opens on `HEADLESS-2` and fullscreens there; | |||
| 81 | + | - clipboard: `wl-copy` text of 4 KiB and 1 MiB (INCR) is read by `x11probe -paste`, and an `x11probe -copy` of the same sizes is read byte-exact by `wl-paste`, for CLIPBOARD and PRIMARY; `wl-copy --clear` gives `paste none`; | |||
| 82 | + | - XSETTINGS: serial, `Xft/DPI=98304` and `Gdk/WindowScalingFactor=1` at any output scale (also with `output.HEADLESS-1.scale = 2`), and the cursor theme and size from `XCURSOR_THEME`/`XCURSOR_SIZE`; `xwayland.xsettings = off` leaves no owner; | |||
| 83 | + | - root cursor: `GetCursorImage` over the root is the theme arrow at the theme size (a throwaway theme in `XCURSOR_PATH`); | |||
| 84 | + | - a window rule `rule.games.app-id = steam_app_\d+` floats an X11 window of class `steam_app_413150`, and not another class. | |||
| 85 | + | ||||
| 86 | + | Not covered headless: relative pointer lock (needs a real pointer device) and Xwayland output hotplug (`--size` is fixed); these are unit-tested in the `wayland` adapter. | |||
| 87 | + | ||||
| 88 | + | Run it by hand with `go test -tags xwayland -race ./internal/app -run TestXwayland -count=1 -v`. | |||
| 89 | + | ||||
| 65 | ## Colour check | 90 | ## Colour check | ||
| 66 | 91 | ||||
| 67 | `make color-check` renders `examples/testpattern` on a headless output and checks the colours, in SDR and in HDR (`--headless-hdr`). It needs a GPU and `/dev/udmabuf`, and is not part of `make check`. | 92 | `make color-check` renders `examples/testpattern` on a headless output and checks the colours, in SDR and in HDR (`--headless-hdr`). It needs a GPU and `/dev/udmabuf`, and is not part of `make check`. | ||
M go.mod +4 -3
| @@ -3,10 +3,11 @@ module github.com/bnema/neferwl | |||
| 3 | 3 | go 1.27 | |
| 4 | 4 | ||
| 5 | 5 | require ( | |
| 6 | + | bnema.dev/xturbo v0.1.0 | |
| 6 | 7 | github.com/bnema/go-wayland-bindings v0.1.0 | |
| 7 | 8 | github.com/bnema/kvconf v0.1.2 | |
| 8 | - | github.com/bnema/purego v0.13.0-bnema.1 | |
| 9 | - | github.com/bnema/purego-libwayland v0.11.0 | |
| 9 | + | github.com/bnema/purego v0.13.0-bnema.2 | |
| 10 | + | github.com/bnema/purego-libwayland v0.12.0 | |
| 10 | 11 | github.com/bnema/purego-pipewire v0.2.0 | |
| 11 | 12 | github.com/bnema/purego-vulkan v0.6.0 | |
| 12 | 13 | github.com/bnema/wlturbo v0.5.0 | |
| @@ -16,7 +17,7 @@ require ( | |||
| 16 | 17 | github.com/rs/zerolog v1.35.1 | |
| 17 | 18 | github.com/stretchr/testify v1.12.1 | |
| 18 | 19 | golang.org/x/image v0.46.0 | |
| 19 | - | golang.org/x/sys v0.48.0 | |
| 20 | + | golang.org/x/sys v0.49.0 | |
| 20 | 21 | golang.org/x/term v0.46.0 | |
| 21 | 22 | ) | |
| 22 | 23 | ||
| @@ -3,10 +3,11 @@ module github.com/bnema/neferwl | |||||
| 3 | go 1.27 | 3 | go 1.27 | ||
| 4 | 4 | ||||
| 5 | require ( | 5 | require ( | ||
| 6 | + | bnema.dev/xturbo v0.1.0 | |||
| 6 | github.com/bnema/go-wayland-bindings v0.1.0 | 7 | github.com/bnema/go-wayland-bindings v0.1.0 | ||
| 7 | github.com/bnema/kvconf v0.1.2 | 8 | github.com/bnema/kvconf v0.1.2 | ||
| 8 | - | github.com/bnema/purego v0.13.0-bnema.1 | 9 | + | github.com/bnema/purego v0.13.0-bnema.2 |
| 9 | - | github.com/bnema/purego-libwayland v0.11.0 | 10 | + | github.com/bnema/purego-libwayland v0.12.0 |
| 10 | github.com/bnema/purego-pipewire v0.2.0 | 11 | github.com/bnema/purego-pipewire v0.2.0 | ||
| 11 | github.com/bnema/purego-vulkan v0.6.0 | 12 | github.com/bnema/purego-vulkan v0.6.0 | ||
| 12 | github.com/bnema/wlturbo v0.5.0 | 13 | github.com/bnema/wlturbo v0.5.0 | ||
| @@ -16,7 +17,7 @@ require ( | |||||
| 16 | github.com/rs/zerolog v1.35.1 | 17 | github.com/rs/zerolog v1.35.1 | ||
| 17 | github.com/stretchr/testify v1.12.1 | 18 | github.com/stretchr/testify v1.12.1 | ||
| 18 | golang.org/x/image v0.46.0 | 19 | golang.org/x/image v0.46.0 | ||
| 19 | - | golang.org/x/sys v0.48.0 | 20 | + | golang.org/x/sys v0.49.0 |
| 20 | golang.org/x/term v0.46.0 | 21 | golang.org/x/term v0.46.0 | ||
| 21 | ) | 22 | ) | ||
| 22 | 23 | ||||
M go.sum +8 -6
| @@ -1,11 +1,13 @@ | |||
| 1 | + | bnema.dev/xturbo v0.1.0 h1:UkdSoJLRvJeGTiT+KmgDBIuKQ85i+TcC/PYSeK05Trg= | |
| 2 | + | bnema.dev/xturbo v0.1.0/go.mod h1:iPphe/cFZVK1d4kLgCLaen4QRH7cGwHYQ+9HBtzaLeY= | |
| 1 | 3 | github.com/bnema/go-wayland-bindings v0.1.0 h1:mi2vNa8BB5ZCo/hq/9d6Af01yhqY6QXAZ6pkljFI1xk= | |
| 2 | 4 | github.com/bnema/go-wayland-bindings v0.1.0/go.mod h1:eWOgjp9vzZEqxm059q1G9F5NXplmv9UAmrDJiivxVjA= | |
| 3 | 5 | github.com/bnema/kvconf v0.1.2 h1:1hDKL9jkQSBdR9dSKbwTONhUUBfJgAdh5vu+yFHOb70= | |
| 4 | 6 | github.com/bnema/kvconf v0.1.2/go.mod h1:+P2X9I/sLx+Lfir45s9a7WA0GnC22WgKbZWE9LK6lcU= | |
| 5 | - | github.com/bnema/purego v0.13.0-bnema.1 h1:fKRdztnvTmRqAjANMoN+utEIH73dvPqVPJh2iZCCAO8= | |
| 6 | - | github.com/bnema/purego v0.13.0-bnema.1/go.mod h1:xS1iX3noypxnbOUEUAqOvkfnUzgHuu19VtL4+dfUMJM= | |
| 7 | - | github.com/bnema/purego-libwayland v0.11.0 h1:9DHAgg1M9+blG5rAC9EWlJt2GFRnA/pxuXE0S0S2Cl8= | |
| 8 | - | github.com/bnema/purego-libwayland v0.11.0/go.mod h1:ZGc8Hz68KwJEHztCBU68o0pRFmDZgGFe3dJ+U1MHgwA= | |
| 7 | + | github.com/bnema/purego v0.13.0-bnema.2 h1:FgGAFNswiShd+A4lFuP5broqjOzW4Lf8f6OZKjgXzAM= | |
| 8 | + | github.com/bnema/purego v0.13.0-bnema.2/go.mod h1:xS1iX3noypxnbOUEUAqOvkfnUzgHuu19VtL4+dfUMJM= | |
| 9 | + | github.com/bnema/purego-libwayland v0.12.0 h1:TTISNNN3zfEwD0G46ZDzI7MMzXRmduLq+NMqBPhB+9g= | |
| 10 | + | github.com/bnema/purego-libwayland v0.12.0/go.mod h1:HHDHathJMypu+/pDKWuyD8xcDneU5GoUt3nRen6SKU4= | |
| 9 | 11 | github.com/bnema/purego-pipewire v0.2.0 h1:drIE0NjxynoCn9gCHTQFKkQVYEh/qsBNt2L2qZFYwro= | |
| 10 | 12 | github.com/bnema/purego-pipewire v0.2.0/go.mod h1:FxEYkYIWIP2g8Zo25jlnQSoP9yjsmb5vIuZwnqPg9uQ= | |
| 11 | 13 | github.com/bnema/purego-vulkan v0.6.0 h1:SJgu0smuDC2vtBv0dJ6dmRKewJW5EUXQwTXyb/p9JSE= | |
| @@ -32,8 +34,8 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= | |||
| 32 | 34 | go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= | |
| 33 | 35 | golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ= | |
| 34 | 36 | golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew= | |
| 35 | - | golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= | |
| 36 | - | golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= | |
| 37 | + | golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= | |
| 38 | + | golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= | |
| 37 | 39 | golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= | |
| 38 | 40 | golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= | |
| 39 | 41 | golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= | |
| @@ -1,11 +1,13 @@ | |||||
| 1 | + | bnema.dev/xturbo v0.1.0 h1:UkdSoJLRvJeGTiT+KmgDBIuKQ85i+TcC/PYSeK05Trg= | |||
| 2 | + | bnema.dev/xturbo v0.1.0/go.mod h1:iPphe/cFZVK1d4kLgCLaen4QRH7cGwHYQ+9HBtzaLeY= | |||
| 1 | github.com/bnema/go-wayland-bindings v0.1.0 h1:mi2vNa8BB5ZCo/hq/9d6Af01yhqY6QXAZ6pkljFI1xk= | 3 | github.com/bnema/go-wayland-bindings v0.1.0 h1:mi2vNa8BB5ZCo/hq/9d6Af01yhqY6QXAZ6pkljFI1xk= | ||
| 2 | github.com/bnema/go-wayland-bindings v0.1.0/go.mod h1:eWOgjp9vzZEqxm059q1G9F5NXplmv9UAmrDJiivxVjA= | 4 | github.com/bnema/go-wayland-bindings v0.1.0/go.mod h1:eWOgjp9vzZEqxm059q1G9F5NXplmv9UAmrDJiivxVjA= | ||
| 3 | github.com/bnema/kvconf v0.1.2 h1:1hDKL9jkQSBdR9dSKbwTONhUUBfJgAdh5vu+yFHOb70= | 5 | github.com/bnema/kvconf v0.1.2 h1:1hDKL9jkQSBdR9dSKbwTONhUUBfJgAdh5vu+yFHOb70= | ||
| 4 | github.com/bnema/kvconf v0.1.2/go.mod h1:+P2X9I/sLx+Lfir45s9a7WA0GnC22WgKbZWE9LK6lcU= | 6 | github.com/bnema/kvconf v0.1.2/go.mod h1:+P2X9I/sLx+Lfir45s9a7WA0GnC22WgKbZWE9LK6lcU= | ||
| 5 | - | github.com/bnema/purego v0.13.0-bnema.1 h1:fKRdztnvTmRqAjANMoN+utEIH73dvPqVPJh2iZCCAO8= | 7 | + | github.com/bnema/purego v0.13.0-bnema.2 h1:FgGAFNswiShd+A4lFuP5broqjOzW4Lf8f6OZKjgXzAM= |
| 6 | - | github.com/bnema/purego v0.13.0-bnema.1/go.mod h1:xS1iX3noypxnbOUEUAqOvkfnUzgHuu19VtL4+dfUMJM= | 8 | + | github.com/bnema/purego v0.13.0-bnema.2/go.mod h1:xS1iX3noypxnbOUEUAqOvkfnUzgHuu19VtL4+dfUMJM= |
| 7 | - | github.com/bnema/purego-libwayland v0.11.0 h1:9DHAgg1M9+blG5rAC9EWlJt2GFRnA/pxuXE0S0S2Cl8= | 9 | + | github.com/bnema/purego-libwayland v0.12.0 h1:TTISNNN3zfEwD0G46ZDzI7MMzXRmduLq+NMqBPhB+9g= |
| 8 | - | github.com/bnema/purego-libwayland v0.11.0/go.mod h1:ZGc8Hz68KwJEHztCBU68o0pRFmDZgGFe3dJ+U1MHgwA= | 10 | + | github.com/bnema/purego-libwayland v0.12.0/go.mod h1:HHDHathJMypu+/pDKWuyD8xcDneU5GoUt3nRen6SKU4= |
| 9 | github.com/bnema/purego-pipewire v0.2.0 h1:drIE0NjxynoCn9gCHTQFKkQVYEh/qsBNt2L2qZFYwro= | 11 | github.com/bnema/purego-pipewire v0.2.0 h1:drIE0NjxynoCn9gCHTQFKkQVYEh/qsBNt2L2qZFYwro= | ||
| 10 | github.com/bnema/purego-pipewire v0.2.0/go.mod h1:FxEYkYIWIP2g8Zo25jlnQSoP9yjsmb5vIuZwnqPg9uQ= | 12 | github.com/bnema/purego-pipewire v0.2.0/go.mod h1:FxEYkYIWIP2g8Zo25jlnQSoP9yjsmb5vIuZwnqPg9uQ= | ||
| 11 | github.com/bnema/purego-vulkan v0.6.0 h1:SJgu0smuDC2vtBv0dJ6dmRKewJW5EUXQwTXyb/p9JSE= | 13 | github.com/bnema/purego-vulkan v0.6.0 h1:SJgu0smuDC2vtBv0dJ6dmRKewJW5EUXQwTXyb/p9JSE= | ||
| @@ -32,8 +34,8 @@ go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= | |||||
| 32 | go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= | 34 | go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= | ||
| 33 | golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ= | 35 | golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ= | ||
| 34 | golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew= | 36 | golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew= | ||
| 35 | - | golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= | 37 | + | golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= |
| 36 | - | golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= | 38 | + | golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= |
| 37 | golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= | 39 | golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= | ||
| 38 | golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= | 40 | golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= | ||
| 39 | golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= | 41 | golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= | ||
M internal/adapters/config/config.go +18 -4
| @@ -167,7 +167,7 @@ func Defaults() ports.Config { | |||
| 167 | 167 | // Hint mode is experimental: off unless the config turns it on. | |
| 168 | 168 | c.Hint.Enable = false | |
| 169 | 169 | c.Hint.TextClick = ports.ModCtrl | ports.ModShift | |
| 170 | - | c.Xwayland = "xwayland-satellite" | |
| 170 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite", XSettings: true} | |
| 171 | 171 | c.Background.Color = "#111111" | |
| 172 | 172 | c.Floating.Dim = 0.3 | |
| 173 | 173 | c.Stash.Width = 80 | |
| @@ -724,13 +724,27 @@ func set(c *ports.Config, key, v string) error { | |||
| 724 | 724 | } | |
| 725 | 725 | c.Terminal.AutoOpen = v | |
| 726 | 726 | case "xwayland": | |
| 727 | + | xs := c.Xwayland.XSettings // independent of the backend | |
| 727 | 728 | switch { | |
| 728 | 729 | case v == "off": | |
| 729 | - | c.Xwayland = "" | |
| 730 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandOff, XSettings: xs} | |
| 731 | + | case v == "satellite": | |
| 732 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite", XSettings: xs} | |
| 733 | + | case v == "builtin": | |
| 734 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandBuiltin, Binary: "Xwayland", XSettings: xs} | |
| 730 | 735 | case v == "" || strings.ContainsAny(v, " \t"): | |
| 731 | - | return fmt.Errorf("must be off or the xwayland-satellite program") | |
| 736 | + | return fmt.Errorf("must be off, satellite, builtin or a satellite program") | |
| 732 | 737 | default: | |
| 733 | - | c.Xwayland = v | |
| 738 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: v, XSettings: xs} | |
| 739 | + | } | |
| 740 | + | case "xwayland.xsettings": | |
| 741 | + | switch v { | |
| 742 | + | case "on": | |
| 743 | + | c.Xwayland.XSettings = true | |
| 744 | + | case "off": | |
| 745 | + | c.Xwayland.XSettings = false | |
| 746 | + | default: | |
| 747 | + | return fmt.Errorf("must be on or off") | |
| 734 | 748 | } | |
| 735 | 749 | case "background": | |
| 736 | 750 | if !color.MatchString(v) { | |
| @@ -167,7 +167,7 @@ func Defaults() ports.Config { | |||||
| 167 | // Hint mode is experimental: off unless the config turns it on. | 167 | // Hint mode is experimental: off unless the config turns it on. | ||
| 168 | c.Hint.Enable = false | 168 | c.Hint.Enable = false | ||
| 169 | c.Hint.TextClick = ports.ModCtrl | ports.ModShift | 169 | c.Hint.TextClick = ports.ModCtrl | ports.ModShift | ||
| 170 | - | c.Xwayland = "xwayland-satellite" | 170 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite", XSettings: true} |
| 171 | c.Background.Color = "#111111" | 171 | c.Background.Color = "#111111" | ||
| 172 | c.Floating.Dim = 0.3 | 172 | c.Floating.Dim = 0.3 | ||
| 173 | c.Stash.Width = 80 | 173 | c.Stash.Width = 80 | ||
| @@ -724,13 +724,27 @@ func set(c *ports.Config, key, v string) error { | |||||
| 724 | } | 724 | } | ||
| 725 | c.Terminal.AutoOpen = v | 725 | c.Terminal.AutoOpen = v | ||
| 726 | case "xwayland": | 726 | case "xwayland": | ||
| 727 | + | xs := c.Xwayland.XSettings // independent of the backend | |||
| 727 | switch { | 728 | switch { | ||
| 728 | case v == "off": | 729 | case v == "off": | ||
| 729 | - | c.Xwayland = "" | 730 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandOff, XSettings: xs} |
| 731 | + | case v == "satellite": | |||
| 732 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite", XSettings: xs} | |||
| 733 | + | case v == "builtin": | |||
| 734 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandBuiltin, Binary: "Xwayland", XSettings: xs} | |||
| 730 | case v == "" || strings.ContainsAny(v, " \t"): | 735 | case v == "" || strings.ContainsAny(v, " \t"): | ||
| 731 | - | return fmt.Errorf("must be off or the xwayland-satellite program") | 736 | + | return fmt.Errorf("must be off, satellite, builtin or a satellite program") |
| 732 | default: | 737 | default: | ||
| 733 | - | c.Xwayland = v | 738 | + | c.Xwayland = ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: v, XSettings: xs} |
| 739 | + | } | |||
| 740 | + | case "xwayland.xsettings": | |||
| 741 | + | switch v { | |||
| 742 | + | case "on": | |||
| 743 | + | c.Xwayland.XSettings = true | |||
| 744 | + | case "off": | |||
| 745 | + | c.Xwayland.XSettings = false | |||
| 746 | + | default: | |||
| 747 | + | return fmt.Errorf("must be on or off") | |||
| 734 | } | 748 | } | ||
| 735 | case "background": | 749 | case "background": | ||
| 736 | if !color.MatchString(v) { | 750 | if !color.MatchString(v) { | ||
M internal/adapters/config/config_test.go +36 -4
| @@ -340,13 +340,45 @@ func TestWorkspacePrevNextAction(t *testing.T) { | |||
| 340 | 340 | } | |
| 341 | 341 | ||
| 342 | 342 | func TestXwayland(t *testing.T) { | |
| 343 | - | if Defaults().Xwayland != "xwayland-satellite" { | |
| 343 | + | if Defaults().Xwayland != (ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite", XSettings: true}) { | |
| 344 | 344 | t.Fatal(Defaults().Xwayland) | |
| 345 | 345 | } | |
| 346 | - | if c, w := parseString(t, "xwayland = off"); c.Xwayland != "" || len(w) != 0 { | |
| 347 | - | t.Fatal(c.Xwayland, w) | |
| 346 | + | for _, tt := range []struct { | |
| 347 | + | value string | |
| 348 | + | want ports.XwaylandConfig | |
| 349 | + | }{ | |
| 350 | + | {"off", ports.XwaylandConfig{Backend: ports.XwaylandOff, XSettings: true}}, | |
| 351 | + | {"satellite", ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite", XSettings: true}}, | |
| 352 | + | {"builtin", ports.XwaylandConfig{Backend: ports.XwaylandBuiltin, Binary: "Xwayland", XSettings: true}}, | |
| 353 | + | {"/opt/xwls", ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "/opt/xwls", XSettings: true}}, | |
| 354 | + | } { | |
| 355 | + | t.Run(tt.value, func(t *testing.T) { | |
| 356 | + | c, w := parseString(t, "xwayland = "+tt.value) | |
| 357 | + | if c.Xwayland != tt.want || len(w) != 0 { | |
| 358 | + | t.Fatal(c.Xwayland, w) | |
| 359 | + | } | |
| 360 | + | }) | |
| 361 | + | } | |
| 362 | + | } | |
| 363 | + | ||
| 364 | + | func TestXwaylandXSettings(t *testing.T) { | |
| 365 | + | for _, tt := range []struct { | |
| 366 | + | src string | |
| 367 | + | want bool | |
| 368 | + | }{ | |
| 369 | + | {"", true}, | |
| 370 | + | {"xwayland.xsettings = on", true}, | |
| 371 | + | {"xwayland.xsettings = off", false}, | |
| 372 | + | {"xwayland.xsettings = off\nxwayland = builtin", false}, | |
| 373 | + | {"xwayland = builtin\nxwayland.xsettings = off", false}, | |
| 374 | + | } { | |
| 375 | + | c, w := parseString(t, tt.src) | |
| 376 | + | if c.Xwayland.XSettings != tt.want || len(w) != 0 { | |
| 377 | + | t.Fatal(tt.src, c.Xwayland, w) | |
| 378 | + | } | |
| 348 | 379 | } | |
| 349 | - | if c, w := parseString(t, "xwayland = /opt/xwls"); c.Xwayland != "/opt/xwls" || len(w) != 0 { | |
| 380 | + | c, w := parseString(t, "xwayland.xsettings = maybe") | |
| 381 | + | if !c.Xwayland.XSettings || len(w) != 1 || !strings.Contains(w[0].Msg, "must be on or off") { | |
| 350 | 382 | t.Fatal(c.Xwayland, w) | |
| 351 | 383 | } | |
| 352 | 384 | } | |
| @@ -340,13 +340,45 @@ func TestWorkspacePrevNextAction(t *testing.T) { | |||||
| 340 | } | 340 | } | ||
| 341 | 341 | ||||
| 342 | func TestXwayland(t *testing.T) { | 342 | func TestXwayland(t *testing.T) { | ||
| 343 | - | if Defaults().Xwayland != "xwayland-satellite" { | 343 | + | if Defaults().Xwayland != (ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite", XSettings: true}) { |
| 344 | t.Fatal(Defaults().Xwayland) | 344 | t.Fatal(Defaults().Xwayland) | ||
| 345 | } | 345 | } | ||
| 346 | - | if c, w := parseString(t, "xwayland = off"); c.Xwayland != "" || len(w) != 0 { | 346 | + | for _, tt := range []struct { |
| 347 | - | t.Fatal(c.Xwayland, w) | 347 | + | value string |
| 348 | + | want ports.XwaylandConfig | |||
| 349 | + | }{ | |||
| 350 | + | {"off", ports.XwaylandConfig{Backend: ports.XwaylandOff, XSettings: true}}, | |||
| 351 | + | {"satellite", ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "xwayland-satellite", XSettings: true}}, | |||
| 352 | + | {"builtin", ports.XwaylandConfig{Backend: ports.XwaylandBuiltin, Binary: "Xwayland", XSettings: true}}, | |||
| 353 | + | {"/opt/xwls", ports.XwaylandConfig{Backend: ports.XwaylandSatellite, Binary: "/opt/xwls", XSettings: true}}, | |||
| 354 | + | } { | |||
| 355 | + | t.Run(tt.value, func(t *testing.T) { | |||
| 356 | + | c, w := parseString(t, "xwayland = "+tt.value) | |||
| 357 | + | if c.Xwayland != tt.want || len(w) != 0 { | |||
| 358 | + | t.Fatal(c.Xwayland, w) | |||
| 359 | + | } | |||
| 360 | + | }) | |||
| 361 | + | } | |||
| 362 | + | } | |||
| 363 | + | ||||
| 364 | + | func TestXwaylandXSettings(t *testing.T) { | |||
| 365 | + | for _, tt := range []struct { | |||
| 366 | + | src string | |||
| 367 | + | want bool | |||
| 368 | + | }{ | |||
| 369 | + | {"", true}, | |||
| 370 | + | {"xwayland.xsettings = on", true}, | |||
| 371 | + | {"xwayland.xsettings = off", false}, | |||
| 372 | + | {"xwayland.xsettings = off\nxwayland = builtin", false}, | |||
| 373 | + | {"xwayland = builtin\nxwayland.xsettings = off", false}, | |||
| 374 | + | } { | |||
| 375 | + | c, w := parseString(t, tt.src) | |||
| 376 | + | if c.Xwayland.XSettings != tt.want || len(w) != 0 { | |||
| 377 | + | t.Fatal(tt.src, c.Xwayland, w) | |||
| 378 | + | } | |||
| 348 | } | 379 | } | ||
| 349 | - | if c, w := parseString(t, "xwayland = /opt/xwls"); c.Xwayland != "/opt/xwls" || len(w) != 0 { | 380 | + | c, w := parseString(t, "xwayland.xsettings = maybe") |
| 381 | + | if !c.Xwayland.XSettings || len(w) != 1 || !strings.Contains(w[0].Msg, "must be on or off") { | |||
| 350 | t.Fatal(c.Xwayland, w) | 382 | t.Fatal(c.Xwayland, w) | ||
| 351 | } | 383 | } | ||
| 352 | } | 384 | } | ||
M internal/adapters/drm/colorop_match.go +11 -0
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "encoding/binary" | |
| 5 | 5 | "fmt" | |
| 6 | 6 | "math" | |
| 7 | + | "os" | |
| 7 | 8 | "slices" | |
| 8 | 9 | "strings" | |
| 9 | 10 | ||
| @@ -244,6 +245,13 @@ func ctmBytes(m [3][3]float64) []byte { | |||
| 244 | 245 | return b | |
| 245 | 246 | } | |
| 246 | 247 | ||
| 248 | + | // sdrPlanePipeline lets sRGB buffers on an HDR output scan out through the | |
| 249 | + | // plane colour pipeline. Off by default: on amdgpu (DP-2, 5120x2160) that path | |
| 250 | + | // looked oversaturated against the composed one and its cause is not yet | |
| 251 | + | // established; the buffers are composed ("hdr_sdr_content"). Set | |
| 252 | + | // NEFERWL_DRM_SDR_PLANE=1 to test the pipeline path. | |
| 253 | + | var sdrPlanePipeline = os.Getenv("NEFERWL_DRM_SDR_PLANE") == "1" | |
| 254 | + | ||
| 247 | 255 | // planeColor decides how plane (with pipeline, nil: none) shows a client | |
| 248 | 256 | // buffer of format whose content is c, on an output in HDR10 mode when hdrOn. | |
| 249 | 257 | // opaque is the surface's Opaque. A buffer already in the output's encoding | |
| @@ -267,6 +275,9 @@ func planeColor(c ports.SurfaceColor, format uint32, opaque, hdrOn bool, pipelin | |||
| 267 | 275 | return colorBypass, "hdr_format" | |
| 268 | 276 | } | |
| 269 | 277 | return colorBypass, "" | |
| 278 | + | case !sdrPlanePipeline: | |
| 279 | + | // Unverified on hardware: the owner saw oversaturated colours. | |
| 280 | + | return colorBypass, "hdr_sdr_content" | |
| 270 | 281 | case pipeline == nil || !opaque || !isSDRColor(c): | |
| 271 | 282 | // The kernel applies the pipeline to premultiplied pixels: exact | |
| 272 | 283 | // only without alpha. | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "encoding/binary" | 4 | "encoding/binary" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "math" | 6 | "math" | ||
| 7 | + | "os" | |||
| 7 | "slices" | 8 | "slices" | ||
| 8 | "strings" | 9 | "strings" | ||
| 9 | 10 | ||||
| @@ -244,6 +245,13 @@ func ctmBytes(m [3][3]float64) []byte { | |||||
| 244 | return b | 245 | return b | ||
| 245 | } | 246 | } | ||
| 246 | 247 | ||||
| 248 | + | // sdrPlanePipeline lets sRGB buffers on an HDR output scan out through the | |||
| 249 | + | // plane colour pipeline. Off by default: on amdgpu (DP-2, 5120x2160) that path | |||
| 250 | + | // looked oversaturated against the composed one and its cause is not yet | |||
| 251 | + | // established; the buffers are composed ("hdr_sdr_content"). Set | |||
| 252 | + | // NEFERWL_DRM_SDR_PLANE=1 to test the pipeline path. | |||
| 253 | + | var sdrPlanePipeline = os.Getenv("NEFERWL_DRM_SDR_PLANE") == "1" | |||
| 254 | + | ||||
| 247 | // planeColor decides how plane (with pipeline, nil: none) shows a client | 255 | // planeColor decides how plane (with pipeline, nil: none) shows a client | ||
| 248 | // buffer of format whose content is c, on an output in HDR10 mode when hdrOn. | 256 | // buffer of format whose content is c, on an output in HDR10 mode when hdrOn. | ||
| 249 | // opaque is the surface's Opaque. A buffer already in the output's encoding | 257 | // opaque is the surface's Opaque. A buffer already in the output's encoding | ||
| @@ -267,6 +275,9 @@ func planeColor(c ports.SurfaceColor, format uint32, opaque, hdrOn bool, pipelin | |||||
| 267 | return colorBypass, "hdr_format" | 275 | return colorBypass, "hdr_format" | ||
| 268 | } | 276 | } | ||
| 269 | return colorBypass, "" | 277 | return colorBypass, "" | ||
| 278 | + | case !sdrPlanePipeline: | |||
| 279 | + | // Unverified on hardware: the owner saw oversaturated colours. | |||
| 280 | + | return colorBypass, "hdr_sdr_content" | |||
| 270 | case pipeline == nil || !opaque || !isSDRColor(c): | 281 | case pipeline == nil || !opaque || !isSDRColor(c): | ||
| 271 | // The kernel applies the pipeline to premultiplied pixels: exact | 282 | // The kernel applies the pipeline to premultiplied pixels: exact | ||
| 272 | // only without alpha. | 283 | // only without alpha. | ||
A internal/adapters/drm/sdrplane_test.go +30 -0
| @@ -0,0 +1,30 @@ | |||
| 1 | + | package drm | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | func init() { sdrPlanePipeline = true } // the pipeline tests exercise the path itself | |
| 11 | + | ||
| 12 | + | // Golden bytes: row-major 3x4, little-endian S31.32 sign-magnitude, zero offset column. | |
| 13 | + | func TestCTMBlobGolden(t *testing.T) { | |
| 14 | + | b := ctmBytes([3][3]float64{{1, -0.5, 0}, {0, 1, 0}, {0, 0, 1}}) | |
| 15 | + | want := []uint64{1 << 32, 1<<63 | 1<<31, 0, 0, 0, 1 << 32, 0, 0, 0, 0, 1 << 32, 0} | |
| 16 | + | for i, w := range want { | |
| 17 | + | if g := binary.LittleEndian.Uint64(b[i*8:]); g != w { | |
| 18 | + | t.Errorf("word %d = %#x, want %#x", i, g, w) | |
| 19 | + | } | |
| 20 | + | } | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | func TestPlaneColorSwitchOff(t *testing.T) { | |
| 24 | + | sdrPlanePipeline = false | |
| 25 | + | defer func() { sdrPlanePipeline = true }() | |
| 26 | + | m, why := planeColor(ports.SurfaceColor{}, fourccXRGB, true, true, &colorPipeline{}) | |
| 27 | + | if m != colorBypass || why != "hdr_sdr_content" { | |
| 28 | + | t.Fatalf("got %d %q", m, why) | |
| 29 | + | } | |
| 30 | + | } | |
| @@ -0,0 +1,30 @@ | |||||
| 1 | + | package drm | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | func init() { sdrPlanePipeline = true } // the pipeline tests exercise the path itself | |||
| 11 | + | ||||
| 12 | + | // Golden bytes: row-major 3x4, little-endian S31.32 sign-magnitude, zero offset column. | |||
| 13 | + | func TestCTMBlobGolden(t *testing.T) { | |||
| 14 | + | b := ctmBytes([3][3]float64{{1, -0.5, 0}, {0, 1, 0}, {0, 0, 1}}) | |||
| 15 | + | want := []uint64{1 << 32, 1<<63 | 1<<31, 0, 0, 0, 1 << 32, 0, 0, 0, 0, 1 << 32, 0} | |||
| 16 | + | for i, w := range want { | |||
| 17 | + | if g := binary.LittleEndian.Uint64(b[i*8:]); g != w { | |||
| 18 | + | t.Errorf("word %d = %#x, want %#x", i, g, w) | |||
| 19 | + | } | |||
| 20 | + | } | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | func TestPlaneColorSwitchOff(t *testing.T) { | |||
| 24 | + | sdrPlanePipeline = false | |||
| 25 | + | defer func() { sdrPlanePipeline = true }() | |||
| 26 | + | m, why := planeColor(ports.SurfaceColor{}, fourccXRGB, true, true, &colorPipeline{}) | |||
| 27 | + | if m != colorBypass || why != "hdr_sdr_content" { | |||
| 28 | + | t.Fatalf("got %d %q", m, why) | |||
| 29 | + | } | |||
| 30 | + | } | |||
M internal/adapters/vulkan/capture_test.go +3 -0
| @@ -358,6 +358,9 @@ func openFDs(t *testing.T) int { | |||
| 358 | 358 | // slot keeps its sync file for every lease. No mock stands in the measured | |
| 359 | 359 | // closure. | |
| 360 | 360 | func TestCaptureCycleAllocations(t *testing.T) { | |
| 361 | + | if raceEnabled { | |
| 362 | + | t.Skip("allocation budgets are not stable under the race runtime") | |
| 363 | + | } | |
| 361 | 364 | for _, tc := range []struct { | |
| 362 | 365 | name string | |
| 363 | 366 | new func(*testing.T) *Renderer | |
| @@ -358,6 +358,9 @@ func openFDs(t *testing.T) int { | |||||
| 358 | // slot keeps its sync file for every lease. No mock stands in the measured | 358 | // slot keeps its sync file for every lease. No mock stands in the measured | ||
| 359 | // closure. | 359 | // closure. | ||
| 360 | func TestCaptureCycleAllocations(t *testing.T) { | 360 | func TestCaptureCycleAllocations(t *testing.T) { | ||
| 361 | + | if raceEnabled { | |||
| 362 | + | t.Skip("allocation budgets are not stable under the race runtime") | |||
| 363 | + | } | |||
| 361 | for _, tc := range []struct { | 364 | for _, tc := range []struct { | ||
| 362 | name string | 365 | name string | ||
| 363 | new func(*testing.T) *Renderer | 366 | new func(*testing.T) *Renderer | ||
A internal/adapters/vulkan/norace_test.go +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | //go:build !race | |
| 2 | + | ||
| 3 | + | package vulkan | |
| 4 | + | ||
| 5 | + | const raceEnabled = false | |
| @@ -0,0 +1,5 @@ | |||||
| 1 | + | //go:build !race | |||
| 2 | + | ||||
| 3 | + | package vulkan | |||
| 4 | + | ||||
| 5 | + | const raceEnabled = false | |||
A internal/adapters/vulkan/race_test.go +8 -0
| @@ -0,0 +1,8 @@ | |||
| 1 | + | //go:build race | |
| 2 | + | ||
| 3 | + | package vulkan | |
| 4 | + | ||
| 5 | + | // raceEnabled: sync.Pool drops items at random under the race runtime, so the | |
| 6 | + | // allocation budgets of calls that go through purego cannot hold. perf-check | |
| 7 | + | // runs them without it. | |
| 8 | + | const raceEnabled = true | |
| @@ -0,0 +1,8 @@ | |||||
| 1 | + | //go:build race | |||
| 2 | + | ||||
| 3 | + | package vulkan | |||
| 4 | + | ||||
| 5 | + | // raceEnabled: sync.Pool drops items at random under the race runtime, so the | |||
| 6 | + | // allocation budgets of calls that go through purego cannot hold. perf-check | |||
| 7 | + | // runs them without it. | |||
| 8 | + | const raceEnabled = true | |||
M internal/adapters/vulkan/scene_perf_test.go +6 -0
| @@ -16,6 +16,9 @@ import ( | |||
| 16 | 16 | // A new scene Seq with one of two windows moved is a partial redraw seeded by | |
| 17 | 17 | // sceneDelta and allocates nothing either. | |
| 18 | 18 | func TestRenderSteadyStateAllocations(t *testing.T) { | |
| 19 | + | if raceEnabled { | |
| 20 | + | t.Skip("allocation budgets are not stable under the race runtime") | |
| 21 | + | } | |
| 19 | 22 | for _, tc := range []struct { | |
| 20 | 23 | name string | |
| 21 | 24 | transform ports.BufferTransform | |
| @@ -145,6 +148,9 @@ func TestRenderSteadyStateAllocations(t *testing.T) { | |||
| 145 | 148 | // keeps one alloc of headroom for drivers whose import path differs. | |
| 146 | 149 | // Skipped without a GPU or /dev/udmabuf. | |
| 147 | 150 | func TestRenderSteadyStateDMABufAllocations(t *testing.T) { | |
| 151 | + | if raceEnabled { | |
| 152 | + | t.Skip("allocation budgets are not stable under the race runtime") | |
| 153 | + | } | |
| 148 | 154 | r, err := New(80, 32) | |
| 149 | 155 | if err != nil { | |
| 150 | 156 | t.Skipf("Vulkan unavailable: %v", err) | |
| @@ -16,6 +16,9 @@ import ( | |||||
| 16 | // A new scene Seq with one of two windows moved is a partial redraw seeded by | 16 | // A new scene Seq with one of two windows moved is a partial redraw seeded by | ||
| 17 | // sceneDelta and allocates nothing either. | 17 | // sceneDelta and allocates nothing either. | ||
| 18 | func TestRenderSteadyStateAllocations(t *testing.T) { | 18 | func TestRenderSteadyStateAllocations(t *testing.T) { | ||
| 19 | + | if raceEnabled { | |||
| 20 | + | t.Skip("allocation budgets are not stable under the race runtime") | |||
| 21 | + | } | |||
| 19 | for _, tc := range []struct { | 22 | for _, tc := range []struct { | ||
| 20 | name string | 23 | name string | ||
| 21 | transform ports.BufferTransform | 24 | transform ports.BufferTransform | ||
| @@ -145,6 +148,9 @@ func TestRenderSteadyStateAllocations(t *testing.T) { | |||||
| 145 | // keeps one alloc of headroom for drivers whose import path differs. | 148 | // keeps one alloc of headroom for drivers whose import path differs. | ||
| 146 | // Skipped without a GPU or /dev/udmabuf. | 149 | // Skipped without a GPU or /dev/udmabuf. | ||
| 147 | func TestRenderSteadyStateDMABufAllocations(t *testing.T) { | 150 | func TestRenderSteadyStateDMABufAllocations(t *testing.T) { | ||
| 151 | + | if raceEnabled { | |||
| 152 | + | t.Skip("allocation budgets are not stable under the race runtime") | |||
| 153 | + | } | |||
| 148 | r, err := New(80, 32) | 154 | r, err := New(80, 32) | ||
| 149 | if err != nil { | 155 | if err != nil { | ||
| 150 | t.Skipf("Vulkan unavailable: %v", err) | 156 | t.Skipf("Vulkan unavailable: %v", err) | ||
M internal/adapters/wayland/activation.go +3 -3
| @@ -66,7 +66,7 @@ func (a activation) Activate(_ *xdgactivation.ActivationV1, token string, surf * | |||
| 66 | 66 | } | |
| 67 | 67 | var w *window | |
| 68 | 68 | if reason == "" { | |
| 69 | - | if w = s.windows[state.root().windowID()]; w == nil || w.toplevel == nil || !w.mapped { | |
| 69 | + | if w = s.windows[state.root().windowID()]; w == nil || !w.isToplevel() || !w.mapped { | |
| 70 | 70 | reason = "not_toplevel" | |
| 71 | 71 | } | |
| 72 | 72 | } | |
| @@ -116,8 +116,8 @@ func (t *tokenRequest) Commit(r *xdgactivation.ActivationTokenV1) { | |||
| 116 | 116 | ||
| 117 | 117 | // focusClient is the client with the keyboard focus. | |
| 118 | 118 | func (s *Server) focusClient() server.Client { | |
| 119 | - | if w := s.windows[s.seat.focused]; w != nil && w.xdg.resource.Resource.Alive() { | |
| 120 | - | return w.xdg.resource.Client() | |
| 119 | + | if w := s.windows[s.seat.focused]; w != nil && w.alive() { | |
| 120 | + | return w.client() | |
| 121 | 121 | } | |
| 122 | 122 | if l := s.layers[s.seat.focused]; l != nil && l.resource.Resource.Alive() { | |
| 123 | 123 | return l.resource.Client() | |
| @@ -66,7 +66,7 @@ func (a activation) Activate(_ *xdgactivation.ActivationV1, token string, surf * | |||||
| 66 | } | 66 | } | ||
| 67 | var w *window | 67 | var w *window | ||
| 68 | if reason == "" { | 68 | if reason == "" { | ||
| 69 | - | if w = s.windows[state.root().windowID()]; w == nil || w.toplevel == nil || !w.mapped { | 69 | + | if w = s.windows[state.root().windowID()]; w == nil || !w.isToplevel() || !w.mapped { |
| 70 | reason = "not_toplevel" | 70 | reason = "not_toplevel" | ||
| 71 | } | 71 | } | ||
| 72 | } | 72 | } | ||
| @@ -116,8 +116,8 @@ func (t *tokenRequest) Commit(r *xdgactivation.ActivationTokenV1) { | |||||
| 116 | 116 | ||||
| 117 | // focusClient is the client with the keyboard focus. | 117 | // focusClient is the client with the keyboard focus. | ||
| 118 | func (s *Server) focusClient() server.Client { | 118 | func (s *Server) focusClient() server.Client { | ||
| 119 | - | if w := s.windows[s.seat.focused]; w != nil && w.xdg.resource.Resource.Alive() { | 119 | + | if w := s.windows[s.seat.focused]; w != nil && w.alive() { |
| 120 | - | return w.xdg.resource.Client() | 120 | + | return w.client() |
| 121 | } | 121 | } | ||
| 122 | if l := s.layers[s.seat.focused]; l != nil && l.resource.Resource.Alive() { | 122 | if l := s.layers[s.seat.focused]; l != nil && l.resource.Resource.Alive() { | ||
| 123 | return l.resource.Client() | 123 | return l.resource.Client() | ||
M internal/adapters/wayland/alloc_hotpath_test.go +9 -0
| @@ -246,3 +246,12 @@ func TestCommandForwardAllocations(t *testing.T) { | |||
| 246 | 246 | t.Errorf("command push + merge + drain + stats: %.1f allocs, want 0", allocs) | |
| 247 | 247 | } | |
| 248 | 248 | } | |
| 249 | + | ||
| 250 | + | // A settled grab (none, or unchanged) is re-checked at every focus and | |
| 251 | + | // configure without allocating. | |
| 252 | + | func TestKeyboardGrabSyncAllocations(t *testing.T) { | |
| 253 | + | s := &Server{windows: map[ports.WindowID]*window{}} | |
| 254 | + | if n := testing.AllocsPerRun(100, s.syncKeyboardGrab); n != 0 { | |
| 255 | + | t.Fatalf("%v allocs", n) | |
| 256 | + | } | |
| 257 | + | } | |
| @@ -246,3 +246,12 @@ func TestCommandForwardAllocations(t *testing.T) { | |||||
| 246 | t.Errorf("command push + merge + drain + stats: %.1f allocs, want 0", allocs) | 246 | t.Errorf("command push + merge + drain + stats: %.1f allocs, want 0", allocs) | ||
| 247 | } | 247 | } | ||
| 248 | } | 248 | } | ||
| 249 | + | ||||
| 250 | + | // A settled grab (none, or unchanged) is re-checked at every focus and | |||
| 251 | + | // configure without allocating. | |||
| 252 | + | func TestKeyboardGrabSyncAllocations(t *testing.T) { | |||
| 253 | + | s := &Server{windows: map[ports.WindowID]*window{}} | |||
| 254 | + | if n := testing.AllocsPerRun(100, s.syncKeyboardGrab); n != 0 { | |||
| 255 | + | t.Fatalf("%v allocs", n) | |||
| 256 | + | } | |||
| 257 | + | } | |||
M internal/adapters/wayland/clipboard.go +5 -0
| @@ -37,6 +37,8 @@ type clipSource struct { | |||
| 37 | 37 | // used: the source was set, rejected or dragged; it cannot be set | |
| 38 | 38 | // again and takes no more MIME types. | |
| 39 | 39 | used bool | |
| 40 | + | // x11: the offer is an X client's selection (xSource), not a Wayland one. | |
| 41 | + | x11 bool | |
| 40 | 42 | // res is the wl_data_source (nil for the other protocols); actions are | |
| 41 | 43 | // its set_actions, for drag and drop only (dnd.go). | |
| 42 | 44 | res *wayland.DataSource | |
| @@ -105,6 +107,7 @@ func (s *Server) setSelection(kind int, src *clipSource) { | |||
| 105 | 107 | if src != nil && src != s.selections[kind] && src.alive() { | |
| 106 | 108 | src.cancel() | |
| 107 | 109 | } | |
| 110 | + | s.exposeToX(kind, nil) // X clients must not read a protected selection | |
| 108 | 111 | return | |
| 109 | 112 | } | |
| 110 | 113 | old := s.selections[kind] | |
| @@ -116,6 +119,7 @@ func (s *Server) setSelection(kind int, src *clipSource) { | |||
| 116 | 119 | old.cancel() | |
| 117 | 120 | } | |
| 118 | 121 | s.announceAll(kind) | |
| 122 | + | s.exposeToX(kind, src) | |
| 119 | 123 | } | |
| 120 | 124 | ||
| 121 | 125 | // announceAll sends the selection of kind to the focused client and to | |
| @@ -187,6 +191,7 @@ func (s *Server) sourceGone(src *clipSource) { | |||
| 187 | 191 | if s.selections[kind] == src { | |
| 188 | 192 | s.selections[kind] = nil | |
| 189 | 193 | s.announceAll(kind) | |
| 194 | + | s.exposeToX(kind, nil) | |
| 190 | 195 | } | |
| 191 | 196 | } | |
| 192 | 197 | } | |
| @@ -37,6 +37,8 @@ type clipSource struct { | |||||
| 37 | // used: the source was set, rejected or dragged; it cannot be set | 37 | // used: the source was set, rejected or dragged; it cannot be set | ||
| 38 | // again and takes no more MIME types. | 38 | // again and takes no more MIME types. | ||
| 39 | used bool | 39 | used bool | ||
| 40 | + | // x11: the offer is an X client's selection (xSource), not a Wayland one. | |||
| 41 | + | x11 bool | |||
| 40 | // res is the wl_data_source (nil for the other protocols); actions are | 42 | // res is the wl_data_source (nil for the other protocols); actions are | ||
| 41 | // its set_actions, for drag and drop only (dnd.go). | 43 | // its set_actions, for drag and drop only (dnd.go). | ||
| 42 | res *wayland.DataSource | 44 | res *wayland.DataSource | ||
| @@ -105,6 +107,7 @@ func (s *Server) setSelection(kind int, src *clipSource) { | |||||
| 105 | if src != nil && src != s.selections[kind] && src.alive() { | 107 | if src != nil && src != s.selections[kind] && src.alive() { | ||
| 106 | src.cancel() | 108 | src.cancel() | ||
| 107 | } | 109 | } | ||
| 110 | + | s.exposeToX(kind, nil) // X clients must not read a protected selection | |||
| 108 | return | 111 | return | ||
| 109 | } | 112 | } | ||
| 110 | old := s.selections[kind] | 113 | old := s.selections[kind] | ||
| @@ -116,6 +119,7 @@ func (s *Server) setSelection(kind int, src *clipSource) { | |||||
| 116 | old.cancel() | 119 | old.cancel() | ||
| 117 | } | 120 | } | ||
| 118 | s.announceAll(kind) | 121 | s.announceAll(kind) | ||
| 122 | + | s.exposeToX(kind, src) | |||
| 119 | } | 123 | } | ||
| 120 | 124 | ||||
| 121 | // announceAll sends the selection of kind to the focused client and to | 125 | // announceAll sends the selection of kind to the focused client and to | ||
| @@ -187,6 +191,7 @@ func (s *Server) sourceGone(src *clipSource) { | |||||
| 187 | if s.selections[kind] == src { | 191 | if s.selections[kind] == src { | ||
| 188 | s.selections[kind] = nil | 192 | s.selections[kind] = nil | ||
| 189 | s.announceAll(kind) | 193 | s.announceAll(kind) | ||
| 194 | + | s.exposeToX(kind, nil) | |||
| 190 | } | 195 | } | ||
| 191 | } | 196 | } | ||
| 192 | } | 197 | } | ||
A internal/adapters/wayland/clipboard_xwm.go +93 -0
| @@ -0,0 +1,93 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "slices" | |
| 5 | + | ||
| 6 | + | "golang.org/x/sys/unix" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | // The text clipboard bridge between Wayland and the built-in XWM. An X | |
| 10 | + | // client's selection becomes a clipSource like any other (xSource); a Wayland | |
| 11 | + | // selection is exposed to X through exposeToX. The X protocol side lives in | |
| 12 | + | // the xwm package; nothing of it reaches core or ports. | |
| 13 | + | ||
| 14 | + | // xwmSelection is the XWM as the clipboard sees it. | |
| 15 | + | type xwmSelection interface { | |
| 16 | + | // WaylandSelection: a Wayland client owns kind with these MIME types; | |
| 17 | + | // nil mimes: none does (or it is protected). | |
| 18 | + | WaylandSelection(kind int, mimes []string) | |
| 19 | + | // SelectionReceive hands the XWM the write end of a reader's pipe (it | |
| 20 | + | // closes it) to fill with the X selection as mime. | |
| 21 | + | SelectionReceive(kind int, mime string, fd int) | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // xSource is the clipSource of an X-owned selection: no Wayland client, the | |
| 25 | + | // data comes from the XWM. It is valid while it is the current X source. | |
| 26 | + | func (s *Server) xSource(kind int, mimes []string) *clipSource { | |
| 27 | + | src := &clipSource{mimes: slices.Clone(mimes), used: true, x11: true} | |
| 28 | + | src.alive = func() bool { return s.xsel[kind] == src } | |
| 29 | + | src.cancel = func() {} | |
| 30 | + | src.send = func(mime string, fd int) { | |
| 31 | + | x := s.xwmSel | |
| 32 | + | if x == nil { | |
| 33 | + | return | |
| 34 | + | } | |
| 35 | + | // receive closes its fd after send: the XWM gets its own. | |
| 36 | + | dup, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0) | |
| 37 | + | if err != nil { | |
| 38 | + | return | |
| 39 | + | } | |
| 40 | + | x.SelectionReceive(kind, mime, dup) | |
| 41 | + | } | |
| 42 | + | return src | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // setXSelection is the XWM's report: an X client owns kind (mimes), or none | |
| 46 | + | // does. A Wayland owner that already replaced the X one is never clobbered. | |
| 47 | + | func (s *Server) setXSelection(kind int, mimes []string) { | |
| 48 | + | if len(mimes) == 0 { | |
| 49 | + | cur := s.xsel[kind] | |
| 50 | + | s.xsel[kind] = nil | |
| 51 | + | if cur != nil && s.selections[kind] == cur { | |
| 52 | + | s.setSelection(kind, nil) | |
| 53 | + | } | |
| 54 | + | return | |
| 55 | + | } | |
| 56 | + | src := s.xSource(kind, mimes) | |
| 57 | + | s.xsel[kind] = src | |
| 58 | + | s.setSelection(kind, src) | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | // exposeToX tells the XWM what Wayland offers. An X source is not exposed | |
| 62 | + | // back (it is X's own); nil withdraws. | |
| 63 | + | func (s *Server) exposeToX(kind int, src *clipSource) { | |
| 64 | + | if s.xwmSel == nil { | |
| 65 | + | return | |
| 66 | + | } | |
| 67 | + | if src == nil || src.x11 { | |
| 68 | + | s.xwmSel.WaylandSelection(kind, nil) | |
| 69 | + | return | |
| 70 | + | } | |
| 71 | + | s.xwmSel.WaylandSelection(kind, src.mimes) | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // xSelectionRead returns a non-blocking read end of the Wayland selection | |
| 75 | + | // kind as mime, or -1. | |
| 76 | + | func (s *Server) xSelectionRead(kind int, mime string) int { | |
| 77 | + | src := s.selections[kind] | |
| 78 | + | if src == nil || src.x11 || s.protected() || !src.alive() || !slices.Contains(src.mimes, mime) { | |
| 79 | + | return -1 | |
| 80 | + | } | |
| 81 | + | var p [2]int | |
| 82 | + | if unix.Pipe2(p[:], unix.O_CLOEXEC) != nil { | |
| 83 | + | return -1 | |
| 84 | + | } | |
| 85 | + | if unix.SetNonblock(p[0], true) != nil { | |
| 86 | + | unix.Close(p[0]) | |
| 87 | + | unix.Close(p[1]) | |
| 88 | + | return -1 | |
| 89 | + | } | |
| 90 | + | src.send(mime, p[1]) | |
| 91 | + | unix.Close(p[1]) | |
| 92 | + | return p[0] | |
| 93 | + | } | |
| @@ -0,0 +1,93 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "slices" | |||
| 5 | + | ||||
| 6 | + | "golang.org/x/sys/unix" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | // The text clipboard bridge between Wayland and the built-in XWM. An X | |||
| 10 | + | // client's selection becomes a clipSource like any other (xSource); a Wayland | |||
| 11 | + | // selection is exposed to X through exposeToX. The X protocol side lives in | |||
| 12 | + | // the xwm package; nothing of it reaches core or ports. | |||
| 13 | + | ||||
| 14 | + | // xwmSelection is the XWM as the clipboard sees it. | |||
| 15 | + | type xwmSelection interface { | |||
| 16 | + | // WaylandSelection: a Wayland client owns kind with these MIME types; | |||
| 17 | + | // nil mimes: none does (or it is protected). | |||
| 18 | + | WaylandSelection(kind int, mimes []string) | |||
| 19 | + | // SelectionReceive hands the XWM the write end of a reader's pipe (it | |||
| 20 | + | // closes it) to fill with the X selection as mime. | |||
| 21 | + | SelectionReceive(kind int, mime string, fd int) | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // xSource is the clipSource of an X-owned selection: no Wayland client, the | |||
| 25 | + | // data comes from the XWM. It is valid while it is the current X source. | |||
| 26 | + | func (s *Server) xSource(kind int, mimes []string) *clipSource { | |||
| 27 | + | src := &clipSource{mimes: slices.Clone(mimes), used: true, x11: true} | |||
| 28 | + | src.alive = func() bool { return s.xsel[kind] == src } | |||
| 29 | + | src.cancel = func() {} | |||
| 30 | + | src.send = func(mime string, fd int) { | |||
| 31 | + | x := s.xwmSel | |||
| 32 | + | if x == nil { | |||
| 33 | + | return | |||
| 34 | + | } | |||
| 35 | + | // receive closes its fd after send: the XWM gets its own. | |||
| 36 | + | dup, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0) | |||
| 37 | + | if err != nil { | |||
| 38 | + | return | |||
| 39 | + | } | |||
| 40 | + | x.SelectionReceive(kind, mime, dup) | |||
| 41 | + | } | |||
| 42 | + | return src | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | // setXSelection is the XWM's report: an X client owns kind (mimes), or none | |||
| 46 | + | // does. A Wayland owner that already replaced the X one is never clobbered. | |||
| 47 | + | func (s *Server) setXSelection(kind int, mimes []string) { | |||
| 48 | + | if len(mimes) == 0 { | |||
| 49 | + | cur := s.xsel[kind] | |||
| 50 | + | s.xsel[kind] = nil | |||
| 51 | + | if cur != nil && s.selections[kind] == cur { | |||
| 52 | + | s.setSelection(kind, nil) | |||
| 53 | + | } | |||
| 54 | + | return | |||
| 55 | + | } | |||
| 56 | + | src := s.xSource(kind, mimes) | |||
| 57 | + | s.xsel[kind] = src | |||
| 58 | + | s.setSelection(kind, src) | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | // exposeToX tells the XWM what Wayland offers. An X source is not exposed | |||
| 62 | + | // back (it is X's own); nil withdraws. | |||
| 63 | + | func (s *Server) exposeToX(kind int, src *clipSource) { | |||
| 64 | + | if s.xwmSel == nil { | |||
| 65 | + | return | |||
| 66 | + | } | |||
| 67 | + | if src == nil || src.x11 { | |||
| 68 | + | s.xwmSel.WaylandSelection(kind, nil) | |||
| 69 | + | return | |||
| 70 | + | } | |||
| 71 | + | s.xwmSel.WaylandSelection(kind, src.mimes) | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | // xSelectionRead returns a non-blocking read end of the Wayland selection | |||
| 75 | + | // kind as mime, or -1. | |||
| 76 | + | func (s *Server) xSelectionRead(kind int, mime string) int { | |||
| 77 | + | src := s.selections[kind] | |||
| 78 | + | if src == nil || src.x11 || s.protected() || !src.alive() || !slices.Contains(src.mimes, mime) { | |||
| 79 | + | return -1 | |||
| 80 | + | } | |||
| 81 | + | var p [2]int | |||
| 82 | + | if unix.Pipe2(p[:], unix.O_CLOEXEC) != nil { | |||
| 83 | + | return -1 | |||
| 84 | + | } | |||
| 85 | + | if unix.SetNonblock(p[0], true) != nil { | |||
| 86 | + | unix.Close(p[0]) | |||
| 87 | + | unix.Close(p[1]) | |||
| 88 | + | return -1 | |||
| 89 | + | } | |||
| 90 | + | src.send(mime, p[1]) | |||
| 91 | + | unix.Close(p[1]) | |||
| 92 | + | return p[0] | |||
| 93 | + | } | |||
A internal/adapters/wayland/clipboard_xwm_test.go +147 -0
| @@ -0,0 +1,147 @@ | |||
| 1 | + | package wayland | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |
| 8 | + | "github.com/stretchr/testify/mock" | |
| 9 | + | "golang.org/x/sys/unix" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | func onDisplay(t *testing.T, s *Server, f func()) { | |
| 13 | + | t.Helper() | |
| 14 | + | if !s.display.Do(f) { | |
| 15 | + | t.Fatal("display stopped") | |
| 16 | + | } | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // receiveVia asks the offer for mime into a fresh pipe and returns its read end. | |
| 20 | + | func receiveVia(t *testing.T, cc *clipClient, offer uint32, mime string) int { | |
| 21 | + | t.Helper() | |
| 22 | + | var fds [2]int | |
| 23 | + | if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil { | |
| 24 | + | t.Fatal(err) | |
| 25 | + | } | |
| 26 | + | t.Cleanup(func() { unix.Close(fds[0]) }) | |
| 27 | + | if err := wireRequest(cc.c, offer, uint16(wayland.DataOfferRequestReceive), []int{fds[1]}, mime); err != nil { | |
| 28 | + | t.Fatal(err) | |
| 29 | + | } | |
| 30 | + | _ = unix.Close(fds[1]) | |
| 31 | + | if err := cc.c.Roundtrip(); err != nil { | |
| 32 | + | t.Fatal(err) | |
| 33 | + | } | |
| 34 | + | return fds[0] | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | // An X selection reaches the focused client and not the others, is replaced | |
| 38 | + | // when a Wayland client copies (and is then not read back from X), and is | |
| 39 | + | // never read while the session is protected. | |
| 40 | + | func TestXSelectionReachesWaylandClients(t *testing.T) { | |
| 41 | + | s, events, commands, dir := keyboardServer(t) | |
| 42 | + | state := installSecurity(t, s) | |
| 43 | + | xm := newMockxwmSelection(t) | |
| 44 | + | onDisplay(t, s, func() { s.xwmSel = xm }) | |
| 45 | + | a := newClipClient(t, s, dir, events) | |
| 46 | + | b := newClipClient(t, s, dir, events) | |
| 47 | + | a.focus(t, commands) | |
| 48 | + | ||
| 49 | + | // X owns CLIPBOARD: the focused client gets an offer, the other none. | |
| 50 | + | xm.EXPECT().WaylandSelection(selClipboard, []string(nil)).Maybe() | |
| 51 | + | onDisplay(t, s, func() { s.setXSelection(selClipboard, []string{"text/plain;charset=utf-8", "UTF8_STRING"}) }) | |
| 52 | + | offer, ok := a.selection(t) | |
| 53 | + | if !ok || offer == 0 { | |
| 54 | + | t.Fatalf("focused client got no X offer: %d %v", offer, ok) | |
| 55 | + | } | |
| 56 | + | if id, ok := b.selection(t); ok { | |
| 57 | + | t.Fatalf("unfocused client got %d", id) | |
| 58 | + | } | |
| 59 | + | ||
| 60 | + | // Reading reaches the XWM with a pipe it owns. | |
| 61 | + | got := make(chan string, 1) | |
| 62 | + | xm.EXPECT().SelectionReceive(selClipboard, "UTF8_STRING", mock.Anything).Run(func(_ int, mime string, fd int) { | |
| 63 | + | _, _ = unix.Write(fd, []byte("from x")) | |
| 64 | + | _ = unix.Close(fd) | |
| 65 | + | got <- mime | |
| 66 | + | }).Once() | |
| 67 | + | r := receiveVia(t, a, offer, "UTF8_STRING") | |
| 68 | + | select { | |
| 69 | + | case <-got: | |
| 70 | + | case <-time.After(2 * time.Second): | |
| 71 | + | t.Fatal("XWM never asked to receive") | |
| 72 | + | } | |
| 73 | + | buf := make([]byte, 16) | |
| 74 | + | n, err := unix.Read(r, buf) | |
| 75 | + | if err != nil || string(buf[:n]) != "from x" { | |
| 76 | + | t.Fatalf("pipe: %q %v", buf[:n], err) | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | // Focus follows: b gets the X offer too (focus replay). | |
| 80 | + | if id := b.focus(t, commands); id == 0 { | |
| 81 | + | t.Fatal("no X offer on focus") | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | // A Wayland copy replaces it and is exposed to X. | |
| 85 | + | xm.EXPECT().WaylandSelection(selClipboard, []string{"text/plain"}).Once() | |
| 86 | + | b.copy(t) | |
| 87 | + | if id, ok := b.selection(t); !ok || id == 0 { | |
| 88 | + | t.Fatal("copy gave no offer") | |
| 89 | + | } | |
| 90 | + | // X clearing afterwards never clobbers the Wayland owner. | |
| 91 | + | onDisplay(t, s, func() { s.setXSelection(selClipboard, nil) }) | |
| 92 | + | if id, ok := b.selection(t); ok && id == 0 { | |
| 93 | + | t.Fatal("the Wayland selection was cleared by X") | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | // Protection: the X offer is not readable, and X cannot read Wayland. | |
| 97 | + | onDisplay(t, s, func() { s.setXSelection(selClipboard, []string{"UTF8_STRING"}) }) | |
| 98 | + | protect(t, s, state, 1, true) | |
| 99 | + | var fd int | |
| 100 | + | onDisplay(t, s, func() { fd = s.xSelectionRead(selClipboard, "text/plain") }) | |
| 101 | + | if fd >= 0 { | |
| 102 | + | unix.Close(fd) | |
| 103 | + | t.Fatal("protected selection readable by X") | |
| 104 | + | } | |
| 105 | + | protect(t, s, state, 2, false) | |
| 106 | + | } | |
| 107 | + | ||
| 108 | + | // A Wayland client's text selection is read for X through a pipe it fills. | |
| 109 | + | func TestXSelectionReadFromWayland(t *testing.T) { | |
| 110 | + | s, events, commands, dir := keyboardServer(t) | |
| 111 | + | xm := newMockxwmSelection(t) | |
| 112 | + | onDisplay(t, s, func() { s.xwmSel = xm }) | |
| 113 | + | a := newClipClient(t, s, dir, events) | |
| 114 | + | a.focus(t, commands) | |
| 115 | + | xm.EXPECT().WaylandSelection(selClipboard, []string(nil)).Maybe() // teardown destroys the source | |
| 116 | + | xm.EXPECT().WaylandSelection(selClipboard, []string{"text/plain"}).Once() | |
| 117 | + | src := a.copy(t) | |
| 118 | + | var fd int | |
| 119 | + | onDisplay(t, s, func() { fd = s.xSelectionRead(selClipboard, "text/plain") }) | |
| 120 | + | if fd < 0 { | |
| 121 | + | t.Fatal("no read end") | |
| 122 | + | } | |
| 123 | + | defer unix.Close(fd) | |
| 124 | + | deadline := time.After(2 * time.Second) | |
| 125 | + | for len(src.sends) == 0 { | |
| 126 | + | if err := a.c.Roundtrip(); err != nil { | |
| 127 | + | t.Fatal(err) | |
| 128 | + | } | |
| 129 | + | select { | |
| 130 | + | case <-deadline: | |
| 131 | + | t.Fatal("source never asked to send") | |
| 132 | + | case <-time.After(5 * time.Millisecond): | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | buf := make([]byte, 16) | |
| 136 | + | n, err := unix.Read(fd, buf) | |
| 137 | + | if err != nil || string(buf[:n]) != "pasted" { | |
| 138 | + | t.Fatalf("read %q %v", buf[:n], err) | |
| 139 | + | } | |
| 140 | + | onDisplay(t, s, func() { | |
| 141 | + | fd2 := s.xSelectionRead(selClipboard, "image/png") | |
| 142 | + | if fd2 >= 0 { | |
| 143 | + | unix.Close(fd2) | |
| 144 | + | t.Error("unoffered mime readable") | |
| 145 | + | } | |
| 146 | + | }) | |
| 147 | + | } | |
| @@ -0,0 +1,147 @@ | |||||
| 1 | + | package wayland | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/go-wayland-bindings/server/wayland" | |||
| 8 | + | "github.com/stretchr/testify/mock" | |||
| 9 | + | "golang.org/x/sys/unix" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | func onDisplay(t *testing.T, s *Server, f func()) { | |||
| 13 | + | t.Helper() | |||
| 14 | + | if !s.display.Do(f) { | |||
| 15 | + | t.Fatal("display stopped") | |||
| 16 | + | } | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | // receiveVia asks the offer for mime into a fresh pipe and returns its read end. | |||
| 20 | + | func receiveVia(t *testing.T, cc *clipClient, offer uint32, mime string) int { | |||
| 21 | + | t.Helper() | |||
| 22 | + | var fds [2]int | |||
| 23 | + | if err := unix.Pipe2(fds[:], unix.O_CLOEXEC); err != nil { | |||
| 24 | + | t.Fatal(err) | |||
| 25 | + | } | |||
| 26 | + | t.Cleanup(func() { unix.Close(fds[0]) }) | |||
| 27 | + | if err := wireRequest(cc.c, offer, uint16(wayland.DataOfferRequestReceive), []int{fds[1]}, mime); err != nil { | |||
| 28 | + | t.Fatal(err) | |||
| 29 | + | } | |||
| 30 | + | _ = unix.Close(fds[1]) | |||
| 31 | + | if err := cc.c.Roundtrip(); err != nil { | |||
| 32 | + | t.Fatal(err) | |||
| 33 | + | } | |||
| 34 | + | return fds[0] | |||
| 35 | + | } | |||
| 36 | + | ||||
| 37 | + | // An X selection reaches the focused client and not the others, is replaced | |||
| 38 | + | // when a Wayland client copies (and is then not read back from X), and is | |||
| 39 | + | // never read while the session is protected. | |||
| 40 | + | func TestXSelectionReachesWaylandClients(t *testing.T) { | |||
| 41 | + | s, events, commands, dir := keyboardServer(t) | |||
| 42 | + | state := installSecurity(t, s) | |||
| 43 | + | xm := newMockxwmSelection(t) | |||
| 44 | + | onDisplay(t, s, func() { s.xwmSel = xm }) | |||
| 45 | + | a := newClipClient(t, s, dir, events) | |||
| 46 | + | b := newClipClient(t, s, dir, events) | |||
| 47 | + | a.focus(t, commands) | |||
| 48 | + | ||||
| 49 | + | // X owns CLIPBOARD: the focused client gets an offer, the other none. | |||
| 50 | + | xm.EXPECT().WaylandSelection(selClipboard, []string(nil)).Maybe() | |||
| 51 | + | onDisplay(t, s, func() { s.setXSelection(selClipboard, []string{"text/plain;charset=utf-8", "UTF8_STRING"}) }) | |||
| 52 | + | offer, ok := a.selection(t) | |||
| 53 | + | if !ok || offer == 0 { | |||
| 54 | + | t.Fatalf("focused client got no X offer: %d %v", offer, ok) | |||
| 55 | + | } | |||
| 56 | + | if id, ok := b.selection(t); ok { | |||
| 57 | + | t.Fatalf("unfocused client got %d", id) | |||
| 58 | + | } | |||
| 59 | + | ||||
| 60 | + | // Reading reaches the XWM with a pipe it owns. | |||
| 61 | + | got := make(chan string, 1) | |||
| 62 | + | xm.EXPECT().SelectionReceive(selClipboard, "UTF8_STRING", mock.Anything).Run(func(_ int, mime string, fd int) { | |||
| 63 | + | _, _ = unix.Write(fd, []byte("from x")) | |||
| 64 | + | _ = unix.Close(fd) | |||
| 65 | + | got <- mime | |||
| 66 | + | }).Once() | |||
| 67 | + | r := receiveVia(t, a, offer, "UTF8_STRING") | |||
| 68 | + | select { | |||
| 69 | + | case <-got: | |||
| 70 | + | case <-time.After(2 * time.Second): | |||
| 71 | + | t.Fatal("XWM never asked to receive") | |||
| 72 | + | } | |||
| 73 | + | buf := make([]byte, 16) | |||
| 74 | + | n, err := unix.Read(r, buf) | |||
| 75 | + | if err != nil || string(buf[:n]) != "from x" { | |||
| 76 | + | t.Fatalf("pipe: %q %v", buf[:n], err) | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | // Focus follows: b gets the X offer too (focus replay). | |||
| 80 | + | if id := b.focus(t, commands); id == 0 { | |||
| 81 | + | t.Fatal("no X offer on focus") | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | // A Wayland copy replaces it and is exposed to X. | |||
| 85 | + | xm.EXPECT().WaylandSelection(selClipboard, []string{"text/plain"}).Once() | |||
| 86 | + | b.copy(t) | |||
| 87 | + | if id, ok := b.selection(t); !ok || id == 0 { | |||
| 88 | + | t.Fatal("copy gave no offer") | |||
| 89 | + | } | |||
| 90 | + | // X clearing afterwards never clobbers the Wayland owner. | |||
| 91 | + | onDisplay(t, s, func() { s.setXSelection(selClipboard, nil) }) | |||
| 92 | + | if id, ok := b.selection(t); ok && id == 0 { | |||
| 93 | + | t.Fatal("the Wayland selection was cleared by X") | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | // Protection: the X offer is not readable, and X cannot read Wayland. | |||
| 97 | + | onDisplay(t, s, func() { s.setXSelection(selClipboard, []string{"UTF8_STRING"}) }) | |||
| 98 | + | protect(t, s, state, 1, true) | |||
| 99 | + | var fd int | |||
| 100 | + | onDisplay(t, s, func() { fd = s.xSelectionRead(selClipboard, "text/plain") }) | |||
| 101 | + | if fd >= 0 { | |||
| 102 | + | unix.Close(fd) | |||
| 103 | + | t.Fatal("protected selection readable by X") | |||
| 104 | + | } | |||
| 105 | + | protect(t, s, state, 2, false) | |||
| 106 | + | } | |||
| 107 | + | ||||
| 108 | + | // A Wayland client's text selection is read for X through a pipe it fills. | |||
| 109 | + | func TestXSelectionReadFromWayland(t *testing.T) { | |||
| 110 | + | s, events, commands, dir := keyboardServer(t) | |||
| 111 | + | xm := newMockxwmSelection(t) | |||
| 112 | + | onDisplay(t, s, func() { s.xwmSel = xm }) | |||
| 113 | + | a := newClipClient(t, s, dir, events) | |||
| 114 | + | a.focus(t, commands) | |||
| 115 | + | xm.EXPECT().WaylandSelection(selClipboard, []string(nil)).Maybe() // teardown destroys the source | |||
| 116 | + | xm.EXPECT().WaylandSelection(selClipboard, []string{"text/plain"}).Once() | |||
| 117 | + | src := a.copy(t) | |||
| 118 | + | var fd int | |||
| 119 | + | onDisplay(t, s, func() { fd = s.xSelectionRead(selClipboard, "text/plain") }) | |||
| 120 | + | if fd < 0 { | |||
| 121 | + | t.Fatal("no read end") | |||
| 122 | + | } | |||
| 123 | + | defer unix.Close(fd) | |||
| 124 | + | deadline := time.After(2 * time.Second) | |||
| 125 | + | for len(src.sends) == 0 { | |||
| 126 | + | if err := a.c.Roundtrip(); err != nil { | |||
| 127 | + | t.Fatal(err) | |||
| 128 | + | } | |||
| 129 | + | select { | |||
| 130 | + | case <-deadline: | |||
| 131 | + | t.Fatal("source never asked to send") | |||
| 132 | + | case <-time.After(5 * time.Millisecond): | |||
| 133 | + | } | |||
| 134 | + | } | |||
| 135 | + | buf := make([]byte, 16) | |||
| 136 | + | n, err := unix.Read(fd, buf) | |||
| 137 | + | if err != nil || string(buf[:n]) != "pasted" { | |||
| 138 | + | t.Fatalf("read %q %v", buf[:n], err) | |||
| 139 | + | } | |||
| 140 | + | onDisplay(t, s, func() { | |||
| 141 | + | fd2 := s.xSelectionRead(selClipboard, "image/png") | |||
| 142 | + | if fd2 >= 0 { | |||
| 143 | + | unix.Close(fd2) | |||
| 144 | + | t.Error("unoffered mime readable") | |||
| 145 | + | } | |||
| 146 | + | }) | |||
| 147 | + | } | |||
M internal/adapters/wayland/constraints.go +14 -13
| @@ -61,11 +61,11 @@ func (h pointerWarp) WarpPointer(r *pointerwarp.WpPointerWarpV1, surf *wayland.S | |||
| 61 | 61 | return | |
| 62 | 62 | } | |
| 63 | 63 | w := s.windows[s.seat.pointerFocus] | |
| 64 | - | if w == nil || !w.mapped || w.xdg.resource.Client() != r.Client() || s.surfaces[surf.Resource] != w.xdg.surface { | |
| 64 | + | if w == nil || !w.mapped || w.client() != r.Client() || s.surfaces[surf.Resource] != w.surf() { | |
| 65 | 65 | return | |
| 66 | 66 | } | |
| 67 | 67 | // Surface-local to window-local (core's, from the geometry origin). | |
| 68 | - | g := w.xdg.geometry | |
| 68 | + | g := w.geometry() | |
| 69 | 69 | s.emit(ports.PointerWarp{ID: w.id, X: x.Float() - float64(g.X), Y: y.Float() - float64(g.Y)}) | |
| 70 | 70 | } | |
| 71 | 71 | ||
| @@ -80,12 +80,12 @@ func (s *Server) relativeMotion(w *window, c ports.PointerMotionTo) bool { | |||
| 80 | 80 | if s.protected() || c.DX == 0 && c.DY == 0 && c.UnaccelDX == 0 && c.UnaccelDY == 0 { | |
| 81 | 81 | return false | |
| 82 | 82 | } | |
| 83 | - | if !w.mapped || !w.xdg.resource.Resource.Alive() { | |
| 83 | + | if !w.mapped || !w.alive() { | |
| 84 | 84 | return false | |
| 85 | 85 | } | |
| 86 | 86 | sent := false | |
| 87 | 87 | usec := uint64(c.Time / time.Microsecond) | |
| 88 | - | for _, rel := range s.relatives[w.xdg.resource.Client()] { | |
| 88 | + | for _, rel := range s.relatives[w.client()] { | |
| 89 | 89 | if rel.Resource.Alive() { | |
| 90 | 90 | rel.SendRelativeMotion(uint32(usec>>32), uint32(usec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY)) | |
| 91 | 91 | sent = true | |
| @@ -272,7 +272,7 @@ func (s *Server) updateConstraint() { | |||
| 272 | 272 | } | |
| 273 | 273 | var want *constraint | |
| 274 | 274 | if w := s.windows[s.seat.pointerFocus]; w != nil && w.mapped && s.seat.pointerFocus == s.seat.focused { | |
| 275 | - | if c := s.constraints[w.xdg.surface]; c != nil && !c.defunct && (c.active || c.contains(w)) { | |
| 275 | + | if c := s.constraints[w.surf()]; c != nil && !c.defunct && (c.active || c.contains(w)) { | |
| 276 | 276 | want = c | |
| 277 | 277 | } | |
| 278 | 278 | } | |
| @@ -298,8 +298,8 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 298 | 298 | if s.protected() { | |
| 299 | 299 | return | |
| 300 | 300 | } | |
| 301 | - | x := c.surface.xdg | |
| 302 | - | if x == nil || x.window == nil { | |
| 301 | + | w := c.surface.toplevelWindow() | |
| 302 | + | if w == nil { | |
| 303 | 303 | return | |
| 304 | 304 | } | |
| 305 | 305 | mode := ports.ConstraintConfine | |
| @@ -308,8 +308,9 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 308 | 308 | } | |
| 309 | 309 | r := c.region | |
| 310 | 310 | if r.W > 0 && r.H > 0 { | |
| 311 | - | r.X -= x.geometry.X | |
| 312 | - | r.Y -= x.geometry.Y | |
| 311 | + | g := w.geometry() | |
| 312 | + | r.X -= g.X | |
| 313 | + | r.Y -= g.Y | |
| 313 | 314 | } | |
| 314 | 315 | // Core supports one confinement rect, so this bounds the intersection; | |
| 315 | 316 | // contains uses the exact input region to decide activation. | |
| @@ -333,17 +334,17 @@ func (s *Server) emitConstraint(c *constraint) { | |||
| 333 | 334 | r = box | |
| 334 | 335 | } | |
| 335 | 336 | } | |
| 336 | - | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | |
| 337 | + | s.emit(ports.PointerConstrained{ID: w.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | |
| 337 | 338 | } | |
| 338 | 339 | ||
| 339 | 340 | // contains reports whether the pointer is in the region: a constraint | |
| 340 | 341 | // activates only there, so it never warps the pointer. | |
| 341 | 342 | func (c *constraint) contains(w *window) bool { | |
| 342 | 343 | r := c.region | |
| 343 | - | x, y := w.surfacePoint(w.xdg.server.seat.pointerX, w.xdg.server.seat.pointerY) | |
| 344 | + | x, y := w.surfacePoint(c.surface.server.seat.pointerX, c.surface.server.seat.pointerY) | |
| 344 | 345 | all, rects := c.surface.effectiveInput() | |
| 345 | 346 | if !all { | |
| 346 | - | localX, localY := x-float64(w.xdg.geometry.X), y-float64(w.xdg.geometry.Y) | |
| 347 | + | localX, localY := x-float64(w.geometry().X), y-float64(w.geometry().Y) | |
| 347 | 348 | inside := false | |
| 348 | 349 | for _, input := range rects { | |
| 349 | 350 | if localX >= float64(input.X) && localX < float64(input.X+input.W) && localY >= float64(input.Y) && localY < float64(input.Y+input.H) { | |
| @@ -363,5 +364,5 @@ func (c *constraint) contains(w *window) bool { | |||
| 363 | 364 | ||
| 364 | 365 | // locked reports whether the window's pointer is locked. | |
| 365 | 366 | func (s *Server) locked(w *window) bool { | |
| 366 | - | return !s.protected() && s.constraint != nil && s.constraint.lock != nil && s.constraint.surface == w.xdg.surface | |
| 367 | + | return !s.protected() && s.constraint != nil && s.constraint.lock != nil && s.constraint.surface == w.surf() | |
| 367 | 368 | } | |
| @@ -61,11 +61,11 @@ func (h pointerWarp) WarpPointer(r *pointerwarp.WpPointerWarpV1, surf *wayland.S | |||||
| 61 | return | 61 | return | ||
| 62 | } | 62 | } | ||
| 63 | w := s.windows[s.seat.pointerFocus] | 63 | w := s.windows[s.seat.pointerFocus] | ||
| 64 | - | if w == nil || !w.mapped || w.xdg.resource.Client() != r.Client() || s.surfaces[surf.Resource] != w.xdg.surface { | 64 | + | if w == nil || !w.mapped || w.client() != r.Client() || s.surfaces[surf.Resource] != w.surf() { |
| 65 | return | 65 | return | ||
| 66 | } | 66 | } | ||
| 67 | // Surface-local to window-local (core's, from the geometry origin). | 67 | // Surface-local to window-local (core's, from the geometry origin). | ||
| 68 | - | g := w.xdg.geometry | 68 | + | g := w.geometry() |
| 69 | s.emit(ports.PointerWarp{ID: w.id, X: x.Float() - float64(g.X), Y: y.Float() - float64(g.Y)}) | 69 | s.emit(ports.PointerWarp{ID: w.id, X: x.Float() - float64(g.X), Y: y.Float() - float64(g.Y)}) | ||
| 70 | } | 70 | } | ||
| 71 | 71 | ||||
| @@ -80,12 +80,12 @@ func (s *Server) relativeMotion(w *window, c ports.PointerMotionTo) bool { | |||||
| 80 | if s.protected() || c.DX == 0 && c.DY == 0 && c.UnaccelDX == 0 && c.UnaccelDY == 0 { | 80 | if s.protected() || c.DX == 0 && c.DY == 0 && c.UnaccelDX == 0 && c.UnaccelDY == 0 { | ||
| 81 | return false | 81 | return false | ||
| 82 | } | 82 | } | ||
| 83 | - | if !w.mapped || !w.xdg.resource.Resource.Alive() { | 83 | + | if !w.mapped || !w.alive() { |
| 84 | return false | 84 | return false | ||
| 85 | } | 85 | } | ||
| 86 | sent := false | 86 | sent := false | ||
| 87 | usec := uint64(c.Time / time.Microsecond) | 87 | usec := uint64(c.Time / time.Microsecond) | ||
| 88 | - | for _, rel := range s.relatives[w.xdg.resource.Client()] { | 88 | + | for _, rel := range s.relatives[w.client()] { |
| 89 | if rel.Resource.Alive() { | 89 | if rel.Resource.Alive() { | ||
| 90 | rel.SendRelativeMotion(uint32(usec>>32), uint32(usec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY)) | 90 | rel.SendRelativeMotion(uint32(usec>>32), uint32(usec), server.FixedFromFloat(c.DX), server.FixedFromFloat(c.DY), server.FixedFromFloat(c.UnaccelDX), server.FixedFromFloat(c.UnaccelDY)) | ||
| 91 | sent = true | 91 | sent = true | ||
| @@ -272,7 +272,7 @@ func (s *Server) updateConstraint() { | |||||
| 272 | } | 272 | } | ||
| 273 | var want *constraint | 273 | var want *constraint | ||
| 274 | if w := s.windows[s.seat.pointerFocus]; w != nil && w.mapped && s.seat.pointerFocus == s.seat.focused { | 274 | if w := s.windows[s.seat.pointerFocus]; w != nil && w.mapped && s.seat.pointerFocus == s.seat.focused { | ||
| 275 | - | if c := s.constraints[w.xdg.surface]; c != nil && !c.defunct && (c.active || c.contains(w)) { | 275 | + | if c := s.constraints[w.surf()]; c != nil && !c.defunct && (c.active || c.contains(w)) { |
| 276 | want = c | 276 | want = c | ||
| 277 | } | 277 | } | ||
| 278 | } | 278 | } | ||
| @@ -298,8 +298,8 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 298 | if s.protected() { | 298 | if s.protected() { | ||
| 299 | return | 299 | return | ||
| 300 | } | 300 | } | ||
| 301 | - | x := c.surface.xdg | 301 | + | w := c.surface.toplevelWindow() |
| 302 | - | if x == nil || x.window == nil { | 302 | + | if w == nil { |
| 303 | return | 303 | return | ||
| 304 | } | 304 | } | ||
| 305 | mode := ports.ConstraintConfine | 305 | mode := ports.ConstraintConfine | ||
| @@ -308,8 +308,9 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 308 | } | 308 | } | ||
| 309 | r := c.region | 309 | r := c.region | ||
| 310 | if r.W > 0 && r.H > 0 { | 310 | if r.W > 0 && r.H > 0 { | ||
| 311 | - | r.X -= x.geometry.X | 311 | + | g := w.geometry() |
| 312 | - | r.Y -= x.geometry.Y | 312 | + | r.X -= g.X |
| 313 | + | r.Y -= g.Y | |||
| 313 | } | 314 | } | ||
| 314 | // Core supports one confinement rect, so this bounds the intersection; | 315 | // Core supports one confinement rect, so this bounds the intersection; | ||
| 315 | // contains uses the exact input region to decide activation. | 316 | // contains uses the exact input region to decide activation. | ||
| @@ -333,17 +334,17 @@ func (s *Server) emitConstraint(c *constraint) { | |||||
| 333 | r = box | 334 | r = box | ||
| 334 | } | 335 | } | ||
| 335 | } | 336 | } | ||
| 336 | - | s.emit(ports.PointerConstrained{ID: x.window.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) | 337 | + | s.emit(ports.PointerConstrained{ID: w.id, PointerConstraint: ports.PointerConstraint{Mode: mode, Rect: r}}) |
| 337 | } | 338 | } | ||
| 338 | 339 | ||||
| 339 | // contains reports whether the pointer is in the region: a constraint | 340 | // contains reports whether the pointer is in the region: a constraint | ||
| 340 | // activates only there, so it never warps the pointer. | 341 | // activates only there, so it never warps the pointer. | ||
| 341 | func (c *constraint) contains(w *window) bool { | 342 | func (c *constraint) contains(w *window) bool { | ||
| 342 | r := c.region | 343 | r := c.region | ||
| 343 | - | x, y := w.surfacePoint(w.xdg.server.seat.pointerX, w.xdg.server.seat.pointerY) | 344 | + | x, y := w.surfacePoint(c.surface.server.seat.pointerX, c.surface.server.seat.pointerY) |
| 344 | all, rects := c.surface.effectiveInput() | 345 | all, rects := c.surface.effectiveInput() | ||
| 345 | if !all { | 346 | if !all { | ||
| 346 | - | localX, localY := x-float64(w.xdg.geometry.X), y-float64(w.xdg.geometry.Y) | 347 | + | localX, localY := x-float64(w.geometry().X), y-float64(w.geometry().Y) |
| 347 | inside := false | 348 | inside := false | ||
| 348 | for _, input := range rects { | 349 | for _, input := range rects { | ||
| 349 | if localX >= float64(input.X) && localX < float64(input.X+input.W) && localY >= float64(input.Y) && localY < float64(input.Y+input.H) { | 350 | if localX >= float64(input.X) && localX < float64(input.X+input.W) && localY >= float64(input.Y) && localY < float64(input.Y+input.H) { | ||
| @@ -363,5 +364,5 @@ func (c *constraint) contains(w *window) bool { | |||||
| 363 | 364 | ||||
| 364 | // locked reports whether the window's pointer is locked. | 365 | // locked reports whether the window's pointer is locked. | ||
| 365 | func (s *Server) locked(w *window) bool { | 366 | func (s *Server) locked(w *window) bool { | ||
| 366 | - | return !s.protected() && s.constraint != nil && s.constraint.lock != nil && s.constraint.surface == w.xdg.surface | 367 | + | return !s.protected() && s.constraint != nil && s.constraint.lock != nil && s.constraint.surface == w.surf() |
| 367 | } | 368 | } | ||
M internal/adapters/wayland/decoration.go +1 -1
| @@ -80,7 +80,7 @@ func (d *decoration) UnsetMode(r *xdgdecoration.ZxdgToplevelDecorationV1) | |||
| 80 | 80 | func (d *decoration) send(r *xdgdecoration.ZxdgToplevelDecorationV1) { | |
| 81 | 81 | r.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide)) | |
| 82 | 82 | for _, w := range d.s.windows { | |
| 83 | - | if w.toplevel == d.toplevel && w.hasLast && w.toplevel.Resource.Alive() && w.xdg.resource.Resource.Alive() { | |
| 83 | + | if w.toplevel == d.toplevel && w.hasLast && w.toplevelAlive() && w.alive() { | |
| 84 | 84 | w.sendConfigure() | |
| 85 | 85 | return | |
| 86 | 86 | } | |
| @@ -80,7 +80,7 @@ func (d *decoration) UnsetMode(r *xdgdecoration.ZxdgToplevelDecorationV1) | |||||
| 80 | func (d *decoration) send(r *xdgdecoration.ZxdgToplevelDecorationV1) { | 80 | func (d *decoration) send(r *xdgdecoration.ZxdgToplevelDecorationV1) { | ||
| 81 | r.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide)) | 81 | r.SendConfigure(uint32(xdgdecoration.ZxdgToplevelDecorationV1ModeServerSide)) | ||
| 82 | for _, w := range d.s.windows { | 82 | for _, w := range d.s.windows { | ||
| 83 | - | if w.toplevel == d.toplevel && w.hasLast && w.toplevel.Resource.Alive() && w.xdg.resource.Resource.Alive() { | 83 | + | if w.toplevel == d.toplevel && w.hasLast && w.toplevelAlive() && w.alive() { |
| 84 | w.sendConfigure() | 84 | w.sendConfigure() | ||
| 85 | return | 85 | return | ||
| 86 | } | 86 | } | ||
M internal/adapters/wayland/dialog.go +1 -1
| @@ -77,6 +77,6 @@ func (w *window) setModal(modal bool) { | |||
| 77 | 77 | } | |
| 78 | 78 | w.modal = modal | |
| 79 | 79 | if w.mapped { | |
| 80 | - | w.xdg.server.emit(ports.WindowModal{ID: w.id, Modal: modal}) | |
| 80 | + | w.srv().emit(ports.WindowModal{ID: w.id, Modal: modal}) | |
| 81 | 81 | } | |
| 82 | 82 | } | |
| @@ -77,6 +77,6 @@ func (w *window) setModal(modal bool) { | |||||
| 77 | } | 77 | } | ||
| 78 | w.modal = modal | 78 | w.modal = modal | ||
| 79 | if w.mapped { | 79 | if w.mapped { | ||
| 80 | - | w.xdg.server.emit(ports.WindowModal{ID: w.id, Modal: modal}) | 80 | + | w.srv().emit(ports.WindowModal{ID: w.id, Modal: modal}) |
| 81 | } | 81 | } | ||
| 82 | } | 82 | } | ||
M internal/adapters/wayland/dmabuf.go +3 -2
| @@ -223,10 +223,11 @@ func (g *dmabufGlobal) fullscreenOutput(surf *surface) (string, bool) { | |||
| 223 | 223 | return "", false | |
| 224 | 224 | } | |
| 225 | 225 | root := toplevelRoot(surf) | |
| 226 | - | if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast || !root.xdg.window.last.Fullscreen || g.server.invisible(surf) { | |
| 226 | + | w := root.toplevelWindow() | |
| 227 | + | if w == nil || !w.hasLast || !w.last.Fullscreen || g.server.invisible(surf) { | |
| 227 | 228 | return "", false | |
| 228 | 229 | } | |
| 229 | - | return root.xdg.window.last.Output, true | |
| 230 | + | return w.last.Output, true | |
| 230 | 231 | } | |
| 231 | 232 | ||
| 232 | 233 | // scanoutFor is the scanout offer for a surface: its window is fullscreen | |
| @@ -223,10 +223,11 @@ func (g *dmabufGlobal) fullscreenOutput(surf *surface) (string, bool) { | |||||
| 223 | return "", false | 223 | return "", false | ||
| 224 | } | 224 | } | ||
| 225 | root := toplevelRoot(surf) | 225 | root := toplevelRoot(surf) | ||
| 226 | - | if root.xdg == nil || root.xdg.window == nil || !root.xdg.window.hasLast || !root.xdg.window.last.Fullscreen || g.server.invisible(surf) { | 226 | + | w := root.toplevelWindow() |
| 227 | + | if w == nil || !w.hasLast || !w.last.Fullscreen || g.server.invisible(surf) { | |||
| 227 | return "", false | 228 | return "", false | ||
| 228 | } | 229 | } | ||
| 229 | - | return root.xdg.window.last.Output, true | 230 | + | return w.last.Output, true |
| 230 | } | 231 | } | ||
| 231 | 232 | ||||
| 232 | // scanoutFor is the scanout offer for a surface: its window is fullscreen | 233 | // scanoutFor is the scanout offer for a surface: its window is fullscreen | ||
M internal/adapters/wayland/extforeigntoplevel.go +1 -1
M internal/adapters/wayland/foreigntoplevel.go +6 -6
M internal/adapters/wayland/frames.go +10 -6
M internal/adapters/wayland/globals.go +11 -1
M internal/adapters/wayland/imagecopy.go +1 -1
M internal/adapters/wayland/inhibit.go +5 -0
M internal/adapters/wayland/outputs.go +72 -1
M internal/adapters/wayland/popup.go +2 -2
M internal/adapters/wayland/scale.go +5 -3
M internal/adapters/wayland/screencast.go +1 -1
M internal/adapters/wayland/seat.go +11 -9
M internal/adapters/wayland/server.go +51 -16
M internal/adapters/wayland/surface.go +15 -2
A internal/adapters/wayland/window.go +112 -0
M internal/adapters/wayland/wire_client_test.go +6 -0
A internal/adapters/wayland/x11override_test.go +107 -0
A internal/adapters/wayland/x11peer_test.go +332 -0
A internal/adapters/wayland/x11window.go +458 -0
A internal/adapters/wayland/x11window_test.go +627 -0
M internal/adapters/wayland/xdg.go +32 -21
A internal/adapters/wayland/xdgoutputx_test.go +143 -0
A internal/adapters/wayland/xwayland.go +75 -0
A internal/adapters/wayland/xwayland_test.go +84 -0
A internal/adapters/wayland/xwaylandgrab.go +90 -0
A internal/adapters/wayland/xwm/atoms.go +118 -0
A internal/adapters/wayland/xwm/configure.go +317 -0
A internal/adapters/wayland/xwm/configure_test.go +591 -0
A internal/adapters/wayland/xwm/cursor.go +109 -0
A internal/adapters/wayland/xwm/cursor_test.go +86 -0
A internal/adapters/wayland/xwm/events.go +303 -0
A internal/adapters/wayland/xwm/fdsource_mock_test.go +129 -0
A internal/adapters/wayland/xwm/fixes_test.go +230 -0
A internal/adapters/wayland/xwm/focus.go +218 -0
A internal/adapters/wayland/xwm/host.go +82 -0
A internal/adapters/wayland/xwm/host_mock_test.go +775 -0
A internal/adapters/wayland/xwm/loop_mock_test.go +180 -0
A internal/adapters/wayland/xwm/mapflow.go +359 -0
A internal/adapters/wayland/xwm/mapflow_test.go +806 -0
A internal/adapters/wayland/xwm/props.go +376 -0
A internal/adapters/wayland/xwm/selection.go +1050 -0
A internal/adapters/wayland/xwm/selection_test.go +801 -0
A internal/adapters/wayland/xwm/selection_xsettings.go +198 -0
A internal/adapters/wayland/xwm/space.go +158 -0
A internal/adapters/wayland/xwm/space_test.go +110 -0
A internal/adapters/wayland/xwm/streams_test.go +197 -0
A internal/adapters/wayland/xwm/table.go +242 -0
A internal/adapters/wayland/xwm/table_test.go +138 -0
A internal/adapters/wayland/xwm/testdata/streams/property_unchanged.hex +950 -0
A internal/adapters/wayland/xwm/testdata/streams/steady.hex +2150 -0
A internal/adapters/wayland/xwm/unmanaged.go +109 -0
A internal/adapters/wayland/xwm/unmanaged_test.go +247 -0
A internal/adapters/wayland/xwm/xpeer_test.go +655 -0
A internal/adapters/wayland/xwm/xsettings_test.go +244 -0
A internal/adapters/wayland/xwm/xwm.go +509 -0
A internal/adapters/wayland/xwm/xwm_test.go +387 -0
A internal/adapters/wayland/xwm_selection_mock_test.go +143 -0
A internal/adapters/xwayland/builtin.go +216 -0
A internal/adapters/xwayland/starter_mock_test.go +110 -0
M internal/adapters/xwayland/xwayland.go +46 -18
M internal/adapters/xwayland/xwayland_test.go +207 -0
M internal/app/app.go +13 -6
M internal/app/cursor.go +18 -0
M internal/app/session.go +2 -1
A internal/app/testdata/x11probe/main.go +692 -0
A internal/app/xwayland_test.go +808 -0
M internal/core/client.go +8 -0
M internal/core/core.go +16 -5
M internal/core/core_test.go +52 -3
M internal/core/datadrag.go +1 -1
M internal/core/fullscreen_left_core_test.go +1 -6
M internal/core/fullscreen_left_test.go +29 -0
M internal/core/layout.go +7 -1
A internal/core/place_test.go +80 -0
M internal/core/rules_test.go +44 -0
M internal/core/scene.go +12 -1
M internal/core/spawn_placement.go +9 -1
M internal/core/transition.go +20 -0
A internal/core/unmanaged.go +117 -0
A internal/core/unmanaged_test.go +145 -0
M internal/ports/client_commands.go +10 -0
M internal/ports/client_events.go +37 -0
M internal/ports/config.go +35 -3
Comments0 open / 0
no comments yet.