feat(wayland): Wayland to X text selection with INCR and X-owned clipboard sources
- commit
- 1f67695ef79716a00323ce58c354ab2c36424994
- author
- brice · · 2026-10-22 16:25 UTC
- committer
- brice · · 2026-10-22 17:55 UTC
- parent
- 8c66911
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
14 files · +1171 -4
M .mockery.yml +5 -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 | |
| @@ -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 | ||
M Makefile +1 -1
| @@ -70,7 +70,7 @@ adapter-imports: | |||
| 70 | 70 | [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; } | |
| 71 | 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | |
| 72 | 72 | perf-check: | |
| 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|TestSelectionINCRChunkAllocations)$$' -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)$$' -count=1 | |
| 74 | 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | |
| 75 | 75 | check: vet test arch fakes-check log-check perf-check | |
| 76 | 76 | ||
| @@ -70,7 +70,7 @@ adapter-imports: | |||||
| 70 | [ -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; } | ||
| 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | 71 | # Fast, mandatory guards for tiled content publications and SHM copies. | ||
| 72 | perf-check: | 72 | perf-check: | ||
| 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|TestSelectionINCRChunkAllocations)$$' -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)$$' -count=1 |
| 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | 74 | CGO_ENABLED=0 go test -tags xturbo_poison ./internal/adapters/wayland/xwm -count=1 | ||
| 75 | check: vet test arch fakes-check log-check perf-check | 75 | check: vet test arch fakes-check log-check perf-check | ||
| 76 | 76 | ||||
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/server.go +3 -0
| @@ -313,6 +313,8 @@ type Server struct { | |||
| 313 | 313 | cursorReady chan struct{} | |
| 314 | 314 | // Clipboard and primary selection (clipboard.go). | |
| 315 | 315 | selections [2]*clipSource | |
| 316 | + | xsel [2]*clipSource // the current X-owned source per kind, if any | |
| 317 | + | xwmSel xwmSelection // the XWM's clipboard side; nil without one | |
| 316 | 318 | // Drag and drop (dnd.go): the drag in progress, drops waiting for | |
| 317 | 319 | // their finish, and each client's data devices. | |
| 318 | 320 | dnd *dndState | |
| @@ -607,6 +609,7 @@ func (s *Server) Run(ctx context.Context) error { | |||
| 607 | 609 | if s.xwm != nil { | |
| 608 | 610 | s.xwm.CloseConn() | |
| 609 | 611 | s.xwm = nil | |
| 612 | + | s.xwmSel = nil | |
| 610 | 613 | } | |
| 611 | 614 | s.forgetXwayland() | |
| 612 | 615 | s.stopSecurityContexts() | |
| @@ -313,6 +313,8 @@ type Server struct { | |||||
| 313 | cursorReady chan struct{} | 313 | cursorReady chan struct{} | ||
| 314 | // Clipboard and primary selection (clipboard.go). | 314 | // Clipboard and primary selection (clipboard.go). | ||
| 315 | selections [2]*clipSource | 315 | selections [2]*clipSource | ||
| 316 | + | xsel [2]*clipSource // the current X-owned source per kind, if any | |||
| 317 | + | xwmSel xwmSelection // the XWM's clipboard side; nil without one | |||
| 316 | // Drag and drop (dnd.go): the drag in progress, drops waiting for | 318 | // Drag and drop (dnd.go): the drag in progress, drops waiting for | ||
| 317 | // their finish, and each client's data devices. | 319 | // their finish, and each client's data devices. | ||
| 318 | dnd *dndState | 320 | dnd *dndState | ||
| @@ -607,6 +609,7 @@ func (s *Server) Run(ctx context.Context) error { | |||||
| 607 | if s.xwm != nil { | 609 | if s.xwm != nil { | ||
| 608 | s.xwm.CloseConn() | 610 | s.xwm.CloseConn() | ||
| 609 | s.xwm = nil | 611 | s.xwm = nil | ||
| 612 | + | s.xwmSel = nil | |||
| 610 | } | 613 | } | ||
| 611 | s.forgetXwayland() | 614 | s.forgetXwayland() | ||
| 612 | s.stopSecurityContexts() | 615 | s.stopSecurityContexts() | ||
M internal/adapters/wayland/x11window.go +13 -2
| @@ -99,10 +99,11 @@ func (xwaylandSurface) Destroy(*xwaylandshell.XwaylandSurfaceV1) {} | |||
| 99 | 99 | func (s *Server) installXWM(x *xwm.XWM) { | |
| 100 | 100 | s.teardownXWM() | |
| 101 | 101 | s.xwm = x | |
| 102 | + | s.xwmSel = x | |
| 102 | 103 | x.UseSpace(&s.xspace) | |
| 103 | 104 | if err := x.Attach(s.display, hostImpl{s}); err != nil { | |
| 104 | 105 | s.log.Warn().Err(err).Msg("X11 window management disabled") | |
| 105 | - | s.xwm = nil | |
| 106 | + | s.xwm, s.xwmSel = nil, nil | |
| 106 | 107 | x.CloseConn() | |
| 107 | 108 | return | |
| 108 | 109 | } | |
| @@ -112,13 +113,18 @@ func (s *Server) installXWM(x *xwm.XWM) { | |||
| 112 | 113 | x.SetSerial(surf.identity, surf.x11Serial) | |
| 113 | 114 | } | |
| 114 | 115 | } | |
| 116 | + | for kind, src := range s.selections { // what Wayland offered before the XWM attached | |
| 117 | + | if src != nil && src.alive() { | |
| 118 | + | s.exposeToX(kind, src) | |
| 119 | + | } | |
| 120 | + | } | |
| 115 | 121 | x.LayoutChanged(true) | |
| 116 | 122 | } | |
| 117 | 123 | ||
| 118 | 124 | // teardownXWM closes the window manager, dropping its windows. Idempotent. | |
| 119 | 125 | func (s *Server) teardownXWM() { | |
| 120 | 126 | if x := s.xwm; x != nil { | |
| 121 | - | s.xwm = nil | |
| 127 | + | s.xwm, s.xwmSel = nil, nil | |
| 122 | 128 | x.Close() | |
| 123 | 129 | } | |
| 124 | 130 | } | |
| @@ -377,6 +383,11 @@ func (h hostImpl) OverrideMoved(slot xwm.Slot, x, y, w, h2 int) { | |||
| 377 | 383 | // OverrideGone ends an override window: unmapped, or destroyed. | |
| 378 | 384 | func (h hostImpl) SelectionOwned(kind int, mimes []string) { | |
| 379 | 385 | h.s.log.Debug().Int("kind", kind).Int("mimes", len(mimes)).Msg("X selection owner changed") | |
| 386 | + | h.s.setXSelection(kind, mimes) | |
| 387 | + | } | |
| 388 | + | ||
| 389 | + | func (h hostImpl) SelectionRead(kind int, mime string) int { | |
| 390 | + | return h.s.xSelectionRead(kind, mime) | |
| 380 | 391 | } | |
| 381 | 392 | ||
| 382 | 393 | func (h hostImpl) After(d time.Duration, fn func()) func() { | |
| @@ -99,10 +99,11 @@ func (xwaylandSurface) Destroy(*xwaylandshell.XwaylandSurfaceV1) {} | |||||
| 99 | func (s *Server) installXWM(x *xwm.XWM) { | 99 | func (s *Server) installXWM(x *xwm.XWM) { | ||
| 100 | s.teardownXWM() | 100 | s.teardownXWM() | ||
| 101 | s.xwm = x | 101 | s.xwm = x | ||
| 102 | + | s.xwmSel = x | |||
| 102 | x.UseSpace(&s.xspace) | 103 | x.UseSpace(&s.xspace) | ||
| 103 | if err := x.Attach(s.display, hostImpl{s}); err != nil { | 104 | if err := x.Attach(s.display, hostImpl{s}); err != nil { | ||
| 104 | s.log.Warn().Err(err).Msg("X11 window management disabled") | 105 | s.log.Warn().Err(err).Msg("X11 window management disabled") | ||
| 105 | - | s.xwm = nil | 106 | + | s.xwm, s.xwmSel = nil, nil |
| 106 | x.CloseConn() | 107 | x.CloseConn() | ||
| 107 | return | 108 | return | ||
| 108 | } | 109 | } | ||
| @@ -112,13 +113,18 @@ func (s *Server) installXWM(x *xwm.XWM) { | |||||
| 112 | x.SetSerial(surf.identity, surf.x11Serial) | 113 | x.SetSerial(surf.identity, surf.x11Serial) | ||
| 113 | } | 114 | } | ||
| 114 | } | 115 | } | ||
| 116 | + | for kind, src := range s.selections { // what Wayland offered before the XWM attached | |||
| 117 | + | if src != nil && src.alive() { | |||
| 118 | + | s.exposeToX(kind, src) | |||
| 119 | + | } | |||
| 120 | + | } | |||
| 115 | x.LayoutChanged(true) | 121 | x.LayoutChanged(true) | ||
| 116 | } | 122 | } | ||
| 117 | 123 | ||||
| 118 | // teardownXWM closes the window manager, dropping its windows. Idempotent. | 124 | // teardownXWM closes the window manager, dropping its windows. Idempotent. | ||
| 119 | func (s *Server) teardownXWM() { | 125 | func (s *Server) teardownXWM() { | ||
| 120 | if x := s.xwm; x != nil { | 126 | if x := s.xwm; x != nil { | ||
| 121 | - | s.xwm = nil | 127 | + | s.xwm, s.xwmSel = nil, nil |
| 122 | x.Close() | 128 | x.Close() | ||
| 123 | } | 129 | } | ||
| 124 | } | 130 | } | ||
| @@ -377,6 +383,11 @@ func (h hostImpl) OverrideMoved(slot xwm.Slot, x, y, w, h2 int) { | |||||
| 377 | // OverrideGone ends an override window: unmapped, or destroyed. | 383 | // OverrideGone ends an override window: unmapped, or destroyed. | ||
| 378 | func (h hostImpl) SelectionOwned(kind int, mimes []string) { | 384 | func (h hostImpl) SelectionOwned(kind int, mimes []string) { | ||
| 379 | h.s.log.Debug().Int("kind", kind).Int("mimes", len(mimes)).Msg("X selection owner changed") | 385 | h.s.log.Debug().Int("kind", kind).Int("mimes", len(mimes)).Msg("X selection owner changed") | ||
| 386 | + | h.s.setXSelection(kind, mimes) | |||
| 387 | + | } | |||
| 388 | + | ||||
| 389 | + | func (h hostImpl) SelectionRead(kind int, mime string) int { | |||
| 390 | + | return h.s.xSelectionRead(kind, mime) | |||
| 380 | } | 391 | } | ||
| 381 | 392 | ||||
| 382 | func (h hostImpl) After(d time.Duration, fn func()) func() { | 393 | func (h hostImpl) After(d time.Duration, fn func()) func() { | ||
M internal/adapters/wayland/xwm/events.go +2 -1
| @@ -23,6 +23,7 @@ const ( | |||
| 23 | 23 | kindOverrideType | |
| 24 | 24 | kindSelTargets | |
| 25 | 25 | kindSelData | |
| 26 | + | kindSelOut | |
| 26 | 27 | ) | |
| 27 | 28 | ||
| 28 | 29 | // onEvent dispatches an X event. Before Attach there is no host: only the | |
| @@ -136,7 +137,7 @@ func (x *XWM) onError(e *xturbo.XError) { | |||
| 136 | 137 | return | |
| 137 | 138 | } | |
| 138 | 139 | switch e.Kind { | |
| 139 | - | case kindSelTargets, kindSelData: | |
| 140 | + | case kindSelTargets, kindSelData, kindSelOut: | |
| 140 | 141 | x.onSelError(e) | |
| 141 | 142 | case kindRootMask: | |
| 142 | 143 | if e.Code == xproto.BadAccess { | |
| @@ -23,6 +23,7 @@ const ( | |||||
| 23 | kindOverrideType | 23 | kindOverrideType | ||
| 24 | kindSelTargets | 24 | kindSelTargets | ||
| 25 | kindSelData | 25 | kindSelData | ||
| 26 | + | kindSelOut | |||
| 26 | ) | 27 | ) | ||
| 27 | 28 | ||||
| 28 | // onEvent dispatches an X event. Before Attach there is no host: only the | 29 | // onEvent dispatches an X event. Before Attach there is no host: only the | ||
| @@ -136,7 +137,7 @@ func (x *XWM) onError(e *xturbo.XError) { | |||||
| 136 | return | 137 | return | ||
| 137 | } | 138 | } | ||
| 138 | switch e.Kind { | 139 | switch e.Kind { | ||
| 139 | - | case kindSelTargets, kindSelData: | 140 | + | case kindSelTargets, kindSelData, kindSelOut: |
| 140 | x.onSelError(e) | 141 | x.onSelError(e) | ||
| 141 | case kindRootMask: | 142 | case kindRootMask: | ||
| 142 | if e.Code == xproto.BadAccess { | 143 | if e.Code == xproto.BadAccess { | ||
M internal/adapters/wayland/xwm/host.go +5 -0
| @@ -63,6 +63,11 @@ type host interface { | |||
| 63 | 63 | // scratch: the host copies it. | |
| 64 | 64 | SelectionOwned(kind int, mimes []string) | |
| 65 | 65 | ||
| 66 | + | // SelectionRead asks for the data of the current Wayland selection kind | |
| 67 | + | // as mime: it returns a non-blocking read end the XWM closes, or -1 when | |
| 68 | + | // there is none, it is protected, or mime is not offered. | |
| 69 | + | SelectionRead(kind int, mime string) (fd int) | |
| 70 | + | ||
| 66 | 71 | // After runs fn on the display goroutine once d has elapsed, unless the | |
| 67 | 72 | // returned cancel ran first. cancel is idempotent and display goroutine | |
| 68 | 73 | // only; fn may still run once after cancel when it was already queued, | |
| @@ -63,6 +63,11 @@ type host interface { | |||||
| 63 | // scratch: the host copies it. | 63 | // scratch: the host copies it. | ||
| 64 | SelectionOwned(kind int, mimes []string) | 64 | SelectionOwned(kind int, mimes []string) | ||
| 65 | 65 | ||||
| 66 | + | // SelectionRead asks for the data of the current Wayland selection kind | |||
| 67 | + | // as mime: it returns a non-blocking read end the XWM closes, or -1 when | |||
| 68 | + | // there is none, it is protected, or mime is not offered. | |||
| 69 | + | SelectionRead(kind int, mime string) (fd int) | |||
| 70 | + | ||||
| 66 | // After runs fn on the display goroutine once d has elapsed, unless the | 71 | // After runs fn on the display goroutine once d has elapsed, unless the | ||
| 67 | // returned cancel ran first. cancel is idempotent and display goroutine | 72 | // returned cancel ran first. cancel is idempotent and display goroutine | ||
| 68 | // only; fn may still run once after cancel when it was already queued, | 73 | // only; fn may still run once after cancel when it was already queued, | ||
M internal/adapters/wayland/xwm/host_mock_test.go +57 -0
| @@ -671,6 +671,63 @@ func (_c *mockhost_SelectionOwned_Call) RunAndReturn(run func(kind int, mimes [] | |||
| 671 | 671 | return _c | |
| 672 | 672 | } | |
| 673 | 673 | ||
| 674 | + | // SelectionRead provides a mock function for the type mockhost | |
| 675 | + | func (_mock *mockhost) SelectionRead(kind int, mime string) int { | |
| 676 | + | ret := _mock.Called(kind, mime) | |
| 677 | + | ||
| 678 | + | if len(ret) == 0 { | |
| 679 | + | panic("no return value specified for SelectionRead") | |
| 680 | + | } | |
| 681 | + | ||
| 682 | + | var r0 int | |
| 683 | + | if returnFunc, ok := ret.Get(0).(func(int, string) int); ok { | |
| 684 | + | r0 = returnFunc(kind, mime) | |
| 685 | + | } else { | |
| 686 | + | r0 = ret.Get(0).(int) | |
| 687 | + | } | |
| 688 | + | return r0 | |
| 689 | + | } | |
| 690 | + | ||
| 691 | + | // mockhost_SelectionRead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectionRead' | |
| 692 | + | type mockhost_SelectionRead_Call struct { | |
| 693 | + | *mock.Call | |
| 694 | + | } | |
| 695 | + | ||
| 696 | + | // SelectionRead is a helper method to define mock.On call | |
| 697 | + | // - kind int | |
| 698 | + | // - mime string | |
| 699 | + | func (_e *mockhost_Expecter) SelectionRead(kind any, mime any) *mockhost_SelectionRead_Call { | |
| 700 | + | return &mockhost_SelectionRead_Call{Call: _e.mock.On("SelectionRead", kind, mime)} | |
| 701 | + | } | |
| 702 | + | ||
| 703 | + | func (_c *mockhost_SelectionRead_Call) Run(run func(kind int, mime string)) *mockhost_SelectionRead_Call { | |
| 704 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 705 | + | var arg0 int | |
| 706 | + | if args[0] != nil { | |
| 707 | + | arg0 = args[0].(int) | |
| 708 | + | } | |
| 709 | + | var arg1 string | |
| 710 | + | if args[1] != nil { | |
| 711 | + | arg1 = args[1].(string) | |
| 712 | + | } | |
| 713 | + | run( | |
| 714 | + | arg0, | |
| 715 | + | arg1, | |
| 716 | + | ) | |
| 717 | + | }) | |
| 718 | + | return _c | |
| 719 | + | } | |
| 720 | + | ||
| 721 | + | func (_c *mockhost_SelectionRead_Call) Return(fd int) *mockhost_SelectionRead_Call { | |
| 722 | + | _c.Call.Return(fd) | |
| 723 | + | return _c | |
| 724 | + | } | |
| 725 | + | ||
| 726 | + | func (_c *mockhost_SelectionRead_Call) RunAndReturn(run func(kind int, mime string) int) *mockhost_SelectionRead_Call { | |
| 727 | + | _c.Call.Return(run) | |
| 728 | + | return _c | |
| 729 | + | } | |
| 730 | + | ||
| 674 | 731 | // Title provides a mock function for the type mockhost | |
| 675 | 732 | func (_mock *mockhost) Title(slot Slot, title string) { | |
| 676 | 733 | _mock.Called(slot, title) | |
| @@ -671,6 +671,63 @@ func (_c *mockhost_SelectionOwned_Call) RunAndReturn(run func(kind int, mimes [] | |||||
| 671 | return _c | 671 | return _c | ||
| 672 | } | 672 | } | ||
| 673 | 673 | ||||
| 674 | + | // SelectionRead provides a mock function for the type mockhost | |||
| 675 | + | func (_mock *mockhost) SelectionRead(kind int, mime string) int { | |||
| 676 | + | ret := _mock.Called(kind, mime) | |||
| 677 | + | ||||
| 678 | + | if len(ret) == 0 { | |||
| 679 | + | panic("no return value specified for SelectionRead") | |||
| 680 | + | } | |||
| 681 | + | ||||
| 682 | + | var r0 int | |||
| 683 | + | if returnFunc, ok := ret.Get(0).(func(int, string) int); ok { | |||
| 684 | + | r0 = returnFunc(kind, mime) | |||
| 685 | + | } else { | |||
| 686 | + | r0 = ret.Get(0).(int) | |||
| 687 | + | } | |||
| 688 | + | return r0 | |||
| 689 | + | } | |||
| 690 | + | ||||
| 691 | + | // mockhost_SelectionRead_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectionRead' | |||
| 692 | + | type mockhost_SelectionRead_Call struct { | |||
| 693 | + | *mock.Call | |||
| 694 | + | } | |||
| 695 | + | ||||
| 696 | + | // SelectionRead is a helper method to define mock.On call | |||
| 697 | + | // - kind int | |||
| 698 | + | // - mime string | |||
| 699 | + | func (_e *mockhost_Expecter) SelectionRead(kind any, mime any) *mockhost_SelectionRead_Call { | |||
| 700 | + | return &mockhost_SelectionRead_Call{Call: _e.mock.On("SelectionRead", kind, mime)} | |||
| 701 | + | } | |||
| 702 | + | ||||
| 703 | + | func (_c *mockhost_SelectionRead_Call) Run(run func(kind int, mime string)) *mockhost_SelectionRead_Call { | |||
| 704 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 705 | + | var arg0 int | |||
| 706 | + | if args[0] != nil { | |||
| 707 | + | arg0 = args[0].(int) | |||
| 708 | + | } | |||
| 709 | + | var arg1 string | |||
| 710 | + | if args[1] != nil { | |||
| 711 | + | arg1 = args[1].(string) | |||
| 712 | + | } | |||
| 713 | + | run( | |||
| 714 | + | arg0, | |||
| 715 | + | arg1, | |||
| 716 | + | ) | |||
| 717 | + | }) | |||
| 718 | + | return _c | |||
| 719 | + | } | |||
| 720 | + | ||||
| 721 | + | func (_c *mockhost_SelectionRead_Call) Return(fd int) *mockhost_SelectionRead_Call { | |||
| 722 | + | _c.Call.Return(fd) | |||
| 723 | + | return _c | |||
| 724 | + | } | |||
| 725 | + | ||||
| 726 | + | func (_c *mockhost_SelectionRead_Call) RunAndReturn(run func(kind int, mime string) int) *mockhost_SelectionRead_Call { | |||
| 727 | + | _c.Call.Return(run) | |||
| 728 | + | return _c | |||
| 729 | + | } | |||
| 730 | + | ||||
| 674 | // Title provides a mock function for the type mockhost | 731 | // Title provides a mock function for the type mockhost | ||
| 675 | func (_mock *mockhost) Title(slot Slot, title string) { | 732 | func (_mock *mockhost) Title(slot Slot, title string) { | ||
| 676 | _mock.Called(slot, title) | 733 | _mock.Called(slot, title) | ||
M internal/adapters/wayland/xwm/props.go +3 -0
| @@ -117,6 +117,9 @@ func (x *XWM) onPropertyNotify(ev *xturbo.Event) { | |||
| 117 | 117 | x.onSelProperty(ev) | |
| 118 | 118 | return | |
| 119 | 119 | } | |
| 120 | + | if x.onRequestorProperty(e) { | |
| 121 | + | return | |
| 122 | + | } | |
| 120 | 123 | s := x.tab.lookup(uint32(e.Window())) | |
| 121 | 124 | if s == 0 { | |
| 122 | 125 | return | |
| @@ -117,6 +117,9 @@ func (x *XWM) onPropertyNotify(ev *xturbo.Event) { | |||||
| 117 | x.onSelProperty(ev) | 117 | x.onSelProperty(ev) | ||
| 118 | return | 118 | return | ||
| 119 | } | 119 | } | ||
| 120 | + | if x.onRequestorProperty(e) { | |||
| 121 | + | return | |||
| 122 | + | } | |||
| 120 | s := x.tab.lookup(uint32(e.Window())) | 123 | s := x.tab.lookup(uint32(e.Window())) | ||
| 121 | if s == 0 { | 124 | if s == 0 { | ||
| 122 | return | 125 | return | ||
M internal/adapters/wayland/xwm/selection.go +433 -0
| @@ -33,6 +33,10 @@ type selState struct { | |||
| 33 | 33 | atom uint32 | |
| 34 | 34 | gen uint32 // bumped on every ownership change; stales TARGETS probes | |
| 35 | 35 | owner uint32 // X owner window other than selWin, 0 none | |
| 36 | + | ours bool // selWin owns the selection (Wayland -> X) | |
| 37 | + | // wl is what the Wayland side offers while ours: raw text availability. | |
| 38 | + | wlUTF8, wlPlain bool | |
| 39 | + | wlMime [len(textMimes)]string // offered text MIMEs, in textMimes order, "" absent | |
| 36 | 40 | } | |
| 37 | 41 | ||
| 38 | 42 | const ( | |
| @@ -109,6 +113,7 @@ type selBridge struct { | |||
| 109 | 113 | mimes [len(textMimes)]string // scratch handed to SelectionOwned | |
| 110 | 114 | pool bufPool | |
| 111 | 115 | in [nXfer]xin | |
| 116 | + | out [nXfer]xout | |
| 112 | 117 | order uint64 | |
| 113 | 118 | // active is the transfer that owns the conversion property, -1 none. | |
| 114 | 119 | active int | |
| @@ -128,6 +133,11 @@ func (x *XWM) initSelection() { | |||
| 128 | 133 | t.onR = func() { t.x.inAbort(t.idx, true) } | |
| 129 | 134 | t.onW = func() { t.x.inWritable(t.idx) } | |
| 130 | 135 | } | |
| 136 | + | for i := range b.out { | |
| 137 | + | o := &b.out[i] | |
| 138 | + | o.x, o.idx, o.buf, o.fd = x, i, -1, -1 | |
| 139 | + | o.onR = func() { o.x.outReadable(o.idx) } | |
| 140 | + | } | |
| 131 | 141 | } | |
| 132 | 142 | ||
| 133 | 143 | // bootSelection creates the selection window and asks for ownership events. | |
| @@ -170,6 +180,10 @@ func (x *XWM) onSelectionEvent(ev *xturbo.Event) { | |||
| 170 | 180 | x.onXFixesSelection(ev) | |
| 171 | 181 | case xproto.KindSelectionNotify: | |
| 172 | 182 | x.onSelectionNotify(ev) | |
| 183 | + | case xproto.KindSelectionRequest: | |
| 184 | + | x.onSelectionRequest(ev) | |
| 185 | + | case xproto.KindSelectionClear: | |
| 186 | + | x.onSelectionClear(ev) | |
| 173 | 187 | } | |
| 174 | 188 | } | |
| 175 | 189 | ||
| @@ -520,6 +534,7 @@ func (x *XWM) onSelProperty(ev *xturbo.Event) { | |||
| 520 | 534 | if e.State() != xproto.PropertyNewValue || uint32(e.Atom()) != x.atoms[atomNeferwlData] { | |
| 521 | 535 | return | |
| 522 | 536 | } | |
| 537 | + | // (a Deleted state on a requestor is handled by onRequestorProperty) | |
| 523 | 538 | i := x.sel.active | |
| 524 | 539 | if i < 0 || !x.sel.in[i].incr { | |
| 525 | 540 | return | |
| @@ -537,6 +552,12 @@ func (x *XWM) onSelProperty(ev *xturbo.Event) { | |||
| 537 | 552 | ||
| 538 | 553 | // onSelError: a selection request failed; the awaited reply never comes. | |
| 539 | 554 | func (x *XWM) onSelError(e *xturbo.XError) { | |
| 555 | + | if e.Kind == kindSelOut { | |
| 556 | + | if i := int(e.Args[0]); i >= 0 && i < nXfer && x.sel.out[i].gen == e.Args[1] && x.sel.out[i].state != outFree { | |
| 557 | + | x.outAbort(i) | |
| 558 | + | } | |
| 559 | + | return | |
| 560 | + | } | |
| 540 | 561 | if e.Kind == kindSelData { | |
| 541 | 562 | if i := int(e.Args[0]); i >= 0 && i < nXfer && x.sel.in[i].gen == e.Args[1] && x.sel.in[i].state == inReading { | |
| 542 | 563 | x.inAbort(i, false) | |
| @@ -580,6 +601,23 @@ func (x *XWM) selTick() { | |||
| 580 | 601 | } | |
| 581 | 602 | live = true | |
| 582 | 603 | } | |
| 604 | + | for i := range b.out { | |
| 605 | + | o := &b.out[i] | |
| 606 | + | if o.state == outFree { | |
| 607 | + | continue | |
| 608 | + | } | |
| 609 | + | o.idle++ | |
| 610 | + | limit := rIdleMax | |
| 611 | + | if o.incr && o.n > 0 && !o.credit { | |
| 612 | + | limit = xIdleMax // the requestor does not delete the property | |
| 613 | + | } | |
| 614 | + | if o.idle >= limit { | |
| 615 | + | x.log.Warn().Int("kind", o.kind).Msg("selection: transfer timed out") | |
| 616 | + | x.outAbort(i) | |
| 617 | + | continue | |
| 618 | + | } | |
| 619 | + | live = true | |
| 620 | + | } | |
| 583 | 621 | if live { | |
| 584 | 622 | x.selEnsureTimer() | |
| 585 | 623 | } | |
| @@ -587,6 +625,11 @@ func (x *XWM) selTick() { | |||
| 587 | 625 | ||
| 588 | 626 | // selClose aborts every transfer. | |
| 589 | 627 | func (x *XWM) selClose() { | |
| 628 | + | for i := range x.sel.out { | |
| 629 | + | if x.sel.out[i].state != outFree { | |
| 630 | + | x.outAbort(i) | |
| 631 | + | } | |
| 632 | + | } | |
| 590 | 633 | for i := range x.sel.in { | |
| 591 | 634 | if x.sel.in[i].state != inFree { | |
| 592 | 635 | x.inAbort(i, false) | |
| @@ -597,3 +640,393 @@ func (x *XWM) selClose() { | |||
| 597 | 640 | x.sel.cancel = nil | |
| 598 | 641 | } | |
| 599 | 642 | } | |
| 643 | + | ||
| 644 | + | // ---- Wayland -> X ---- | |
| 645 | + | ||
| 646 | + | // Wayland -> X transfer states. | |
| 647 | + | const ( | |
| 648 | + | outFree uint8 = iota | |
| 649 | + | outActive | |
| 650 | + | ) | |
| 651 | + | ||
| 652 | + | // xout is one Wayland -> X transfer. | |
| 653 | + | type xout struct { | |
| 654 | + | x *XWM | |
| 655 | + | idx int | |
| 656 | + | state uint8 | |
| 657 | + | kind int | |
| 658 | + | requestor uint32 | |
| 659 | + | property uint32 | |
| 660 | + | target uint32 | |
| 661 | + | time uint32 | |
| 662 | + | fd int | |
| 663 | + | buf int | |
| 664 | + | n int | |
| 665 | + | eof bool | |
| 666 | + | incr bool // INCR announced | |
| 667 | + | credit bool // the requestor deleted the property: the next chunk may go | |
| 668 | + | notified bool // the requestor got its SelectionNotify | |
| 669 | + | idle int | |
| 670 | + | gen uint32 | |
| 671 | + | src fdSource | |
| 672 | + | onR func() | |
| 673 | + | } | |
| 674 | + | ||
| 675 | + | // WaylandSelection: a Wayland client owns kind with these MIME types, or none | |
| 676 | + | // does (mimes nil). The XWM takes X ownership when text is offered. | |
| 677 | + | func (x *XWM) WaylandSelection(kind int, mimes []string) { | |
| 678 | + | if x.selWin == 0 || x.closed || kind < 0 || kind >= selKinds || x.host == nil { | |
| 679 | + | return | |
| 680 | + | } | |
| 681 | + | st := &x.sel.st[kind] | |
| 682 | + | st.wlUTF8, st.wlPlain = false, false | |
| 683 | + | st.wlMime = [len(textMimes)]string{} | |
| 684 | + | for _, m := range mimes { | |
| 685 | + | for i, tm := range textMimes { | |
| 686 | + | if m != tm { | |
| 687 | + | continue | |
| 688 | + | } | |
| 689 | + | st.wlMime[i] = m | |
| 690 | + | if mimeTarget[i] == atomUTF8String { | |
| 691 | + | st.wlUTF8 = true | |
| 692 | + | } else { | |
| 693 | + | st.wlPlain = true | |
| 694 | + | } | |
| 695 | + | } | |
| 696 | + | } | |
| 697 | + | for i := range x.sel.out { // the data being served is stale | |
| 698 | + | if x.sel.out[i].state != outFree && x.sel.out[i].kind == kind { | |
| 699 | + | x.outAbort(i) | |
| 700 | + | } | |
| 701 | + | } | |
| 702 | + | b := x.conn.Buf() | |
| 703 | + | if !st.wlUTF8 && !st.wlPlain { | |
| 704 | + | if st.ours { | |
| 705 | + | st.ours = false | |
| 706 | + | xproto.SetSelectionOwner(b, 0, xproto.Atom(st.atom), 0) | |
| 707 | + | x.wake() | |
| 708 | + | } | |
| 709 | + | return | |
| 710 | + | } | |
| 711 | + | st.ours = true | |
| 712 | + | st.gen++ | |
| 713 | + | st.owner = 0 | |
| 714 | + | xproto.SetSelectionOwner(b, xproto.Window(x.selWin), xproto.Atom(st.atom), 0) | |
| 715 | + | x.log.Debug().Int("kind", kind).Msg("selection: Wayland owner") | |
| 716 | + | x.wake() | |
| 717 | + | } | |
| 718 | + | ||
| 719 | + | // onSelectionClear: someone else took a selection from selWin. | |
| 720 | + | func (x *XWM) onSelectionClear(ev *xturbo.Event) { | |
| 721 | + | e := xproto.SelectionClearOf(ev) | |
| 722 | + | k := x.selKind(uint32(e.Selection())) | |
| 723 | + | if k < 0 || uint32(e.Owner()) != x.selWin { | |
| 724 | + | return | |
| 725 | + | } | |
| 726 | + | x.sel.st[k].ours = false | |
| 727 | + | for i := range x.sel.out { | |
| 728 | + | if x.sel.out[i].state != outFree && x.sel.out[i].kind == k { | |
| 729 | + | x.outAbort(i) | |
| 730 | + | } | |
| 731 | + | } | |
| 732 | + | } | |
| 733 | + | ||
| 734 | + | // notify sends a SelectionNotify to the requestor; property 0 refuses. | |
| 735 | + | func (x *XWM) notify(requestor, selection, target, property, time uint32) { | |
| 736 | + | d := xproto.SelectionNotifyEventData{ | |
| 737 | + | Time: xproto.Timestamp(time), Requestor: xproto.Window(requestor), | |
| 738 | + | Selection: xproto.Atom(selection), Target: xproto.Atom(target), Property: xproto.Atom(property), | |
| 739 | + | } | |
| 740 | + | var ev [32]byte | |
| 741 | + | d.Put(&ev) | |
| 742 | + | xproto.SendEvent(x.conn.Buf(), false, xproto.Window(requestor), 0, &ev) | |
| 743 | + | x.wake() | |
| 744 | + | } | |
| 745 | + | ||
| 746 | + | // onSelectionRequest: an X client asks selWin for the Wayland selection. | |
| 747 | + | func (x *XWM) onSelectionRequest(ev *xturbo.Event) { | |
| 748 | + | e := xproto.SelectionRequestOf(ev) | |
| 749 | + | if uint32(e.Owner()) != x.selWin { | |
| 750 | + | return | |
| 751 | + | } | |
| 752 | + | sel, req, target, prop, time := uint32(e.Selection()), uint32(e.Requestor()), uint32(e.Target()), uint32(e.Property()), uint32(e.Time()) | |
| 753 | + | k := x.selKind(sel) | |
| 754 | + | if k < 0 || !x.sel.st[k].ours { | |
| 755 | + | x.notify(req, sel, target, 0, time) | |
| 756 | + | return | |
| 757 | + | } | |
| 758 | + | if prop == 0 { // obsolete client | |
| 759 | + | prop = target | |
| 760 | + | } | |
| 761 | + | st := &x.sel.st[k] | |
| 762 | + | switch target { | |
| 763 | + | case x.atoms[atomTargets]: | |
| 764 | + | var list [5]uint32 | |
| 765 | + | n := 0 | |
| 766 | + | list[n], n = x.atoms[atomTargets], n+1 | |
| 767 | + | if st.wlUTF8 { | |
| 768 | + | list[n], n = x.atoms[atomUTF8String], n+1 | |
| 769 | + | list[n], n = x.atoms[atomText], n+1 | |
| 770 | + | } | |
| 771 | + | list[n], n = x.atoms[atomString], n+1 | |
| 772 | + | xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(req), xproto.Atom(prop), | |
| 773 | + | xproto.AtomAtom, 32, uint32(n), x.encode32(list[:n])) | |
| 774 | + | x.notify(req, sel, target, prop, time) | |
| 775 | + | return | |
| 776 | + | case x.atoms[atomUTF8String], x.atoms[atomText], x.atoms[atomString]: | |
| 777 | + | default: | |
| 778 | + | x.notify(req, sel, target, 0, time) | |
| 779 | + | return | |
| 780 | + | } | |
| 781 | + | // Choose the Wayland MIME: UTF-8 targets prefer the UTF-8 types; STRING | |
| 782 | + | // prefers plain text and falls back to the UTF-8 bytes unchanged. | |
| 783 | + | mime := "" | |
| 784 | + | order := [...]int{0, 2, 4, 1, 3} // utf8 types, then plain | |
| 785 | + | if target == x.atoms[atomString] { | |
| 786 | + | order = [...]int{1, 3, 0, 2, 4} | |
| 787 | + | } | |
| 788 | + | for _, i := range order { | |
| 789 | + | if st.wlMime[i] != "" && (target == x.atoms[atomString] || mimeTarget[i] == atomUTF8String) { | |
| 790 | + | mime = st.wlMime[i] | |
| 791 | + | break | |
| 792 | + | } | |
| 793 | + | } | |
| 794 | + | if mime == "" { | |
| 795 | + | x.notify(req, sel, target, 0, time) | |
| 796 | + | return | |
| 797 | + | } | |
| 798 | + | for i := range x.sel.out { // one transfer per (requestor, property) | |
| 799 | + | o := &x.sel.out[i] | |
| 800 | + | if o.state != outFree && o.requestor == req && o.property == prop { | |
| 801 | + | x.notify(req, sel, target, 0, time) | |
| 802 | + | return | |
| 803 | + | } | |
| 804 | + | } | |
| 805 | + | slot := -1 | |
| 806 | + | for i := range x.sel.out { | |
| 807 | + | if x.sel.out[i].state == outFree { | |
| 808 | + | slot = i | |
| 809 | + | break | |
| 810 | + | } | |
| 811 | + | } | |
| 812 | + | if slot < 0 || x.sel.pool.n == 0 { | |
| 813 | + | x.log.Warn().Msg("selection: too many transfers, request refused") | |
| 814 | + | x.notify(req, sel, target, 0, time) | |
| 815 | + | return | |
| 816 | + | } | |
| 817 | + | fd := x.host.SelectionRead(k, mime) | |
| 818 | + | if fd < 0 { | |
| 819 | + | x.notify(req, sel, target, 0, time) | |
| 820 | + | return | |
| 821 | + | } | |
| 822 | + | x.startOut(slot, k, req, prop, target, time, fd) | |
| 823 | + | } | |
| 824 | + | ||
| 825 | + | // startOut begins serving the data on fd (a non-blocking read end it now | |
| 826 | + | // owns) to requestor. | |
| 827 | + | func (x *XWM) startOut(slot, k int, req, prop, target, time uint32, fd int) { | |
| 828 | + | o := &x.sel.out[slot] | |
| 829 | + | o.state, o.kind, o.requestor, o.property, o.target, o.time = outActive, k, req, prop, target, time | |
| 830 | + | o.fd, o.buf, o.n, o.eof, o.incr, o.credit, o.notified, o.idle, o.src = fd, x.sel.pool.get(), 0, false, false, false, false, 0, nil | |
| 831 | + | x.selEnsureTimer() | |
| 832 | + | x.outRead(slot) // data may already be waiting | |
| 833 | + | } | |
| 834 | + | ||
| 835 | + | // encode32 packs vals into the shared scratch. | |
| 836 | + | func (x *XWM) encode32(vals []uint32) []byte { | |
| 837 | + | n := 4 * len(vals) | |
| 838 | + | if cap(x.scratch) < n { | |
| 839 | + | x.scratch = make([]byte, 0, n) | |
| 840 | + | } | |
| 841 | + | p := x.scratch[:n] | |
| 842 | + | for i, v := range vals { | |
| 843 | + | binary.LittleEndian.PutUint32(p[4*i:], v) | |
| 844 | + | } | |
| 845 | + | return p | |
| 846 | + | } | |
| 847 | + | ||
| 848 | + | // outWatch registers the pipe's read end with the display. | |
| 849 | + | func (x *XWM) outWatch(i int) { | |
| 850 | + | o := &x.sel.out[i] | |
| 851 | + | if o.src != nil { | |
| 852 | + | return | |
| 853 | + | } | |
| 854 | + | if x.addFD == nil { | |
| 855 | + | x.outAbort(i) | |
| 856 | + | return | |
| 857 | + | } | |
| 858 | + | src, err := x.addFD(o.fd, o.onR, nil) | |
| 859 | + | if err != nil { | |
| 860 | + | x.outAbort(i) | |
| 861 | + | return | |
| 862 | + | } | |
| 863 | + | o.src = src | |
| 864 | + | } | |
| 865 | + | ||
| 866 | + | // outUnwatch drops read interest: the buffer is full and nothing may be read | |
| 867 | + | // until the requestor consumed the previous chunk. | |
| 868 | + | func (x *XWM) outUnwatch(i int) { | |
| 869 | + | o := &x.sel.out[i] | |
| 870 | + | if o.src != nil { | |
| 871 | + | o.src.Remove() | |
| 872 | + | o.src = nil | |
| 873 | + | } | |
| 874 | + | } | |
| 875 | + | ||
| 876 | + | // outReadable is the pipe's read callback. | |
| 877 | + | func (x *XWM) outReadable(i int) { | |
| 878 | + | if x.sel.out[i].state == outFree { | |
| 879 | + | return | |
| 880 | + | } | |
| 881 | + | x.outRead(i) | |
| 882 | + | } | |
| 883 | + | ||
| 884 | + | // outRead fills the buffer from the pipe and advances the transfer. | |
| 885 | + | func (x *XWM) outRead(i int) { | |
| 886 | + | o := &x.sel.out[i] | |
| 887 | + | buf := &x.sel.pool.bufs[o.buf] | |
| 888 | + | for o.n < chunk && !o.eof { | |
| 889 | + | n, err := unix.Read(o.fd, buf[o.n:]) | |
| 890 | + | switch { | |
| 891 | + | case err == nil && n == 0: | |
| 892 | + | o.eof = true | |
| 893 | + | case err == nil: | |
| 894 | + | o.n += n | |
| 895 | + | o.idle = 0 | |
| 896 | + | case errors.Is(err, unix.EINTR): | |
| 897 | + | continue | |
| 898 | + | case errors.Is(err, unix.EAGAIN): | |
| 899 | + | x.outPump(i) | |
| 900 | + | return | |
| 901 | + | default: | |
| 902 | + | x.outAbort(i) | |
| 903 | + | return | |
| 904 | + | } | |
| 905 | + | } | |
| 906 | + | x.outPump(i) | |
| 907 | + | } | |
| 908 | + | ||
| 909 | + | // outPump sends what the state allows. | |
| 910 | + | func (x *XWM) outPump(i int) { | |
| 911 | + | o := &x.sel.out[i] | |
| 912 | + | if o.state == outFree { | |
| 913 | + | return | |
| 914 | + | } | |
| 915 | + | full := o.n == chunk | |
| 916 | + | if !o.incr { | |
| 917 | + | switch { | |
| 918 | + | case o.eof: // whole value | |
| 919 | + | x.outSetProp(i, o.n) | |
| 920 | + | x.notify(o.requestor, x.sel.st[o.kind].atom, o.target, o.property, o.time) | |
| 921 | + | x.outFinish(i) | |
| 922 | + | case full: // does not fit one request: INCR | |
| 923 | + | o.incr, o.notified = true, true | |
| 924 | + | var v xproto.ChangeWindowAttributesValues | |
| 925 | + | v.SetEventMask(xproto.EventMaskPropertyChange) | |
| 926 | + | xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(o.requestor), &v) | |
| 927 | + | xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(o.requestor), xproto.Atom(o.property), | |
| 928 | + | xproto.Atom(x.atoms[atomIncr]), 32, 1, x.encode32([]uint32{0})) | |
| 929 | + | x.notify(o.requestor, x.sel.st[o.kind].atom, o.target, o.property, o.time) | |
| 930 | + | x.outUnwatch(i) | |
| 931 | + | default: | |
| 932 | + | x.outWatch(i) | |
| 933 | + | } | |
| 934 | + | return | |
| 935 | + | } | |
| 936 | + | if o.credit && (full || o.eof) { | |
| 937 | + | o.credit = false | |
| 938 | + | o.idle = 0 | |
| 939 | + | x.outSetProp(i, o.n) | |
| 940 | + | if o.n == 0 { // the empty chunk ends the transfer | |
| 941 | + | x.outFinish(i) | |
| 942 | + | return | |
| 943 | + | } | |
| 944 | + | o.n = 0 | |
| 945 | + | } | |
| 946 | + | if o.n < chunk && !o.eof { | |
| 947 | + | x.outWatch(i) | |
| 948 | + | } else { | |
| 949 | + | x.outUnwatch(i) | |
| 950 | + | } | |
| 951 | + | } | |
| 952 | + | ||
| 953 | + | // outSetProp writes the first n buffered bytes as the property value. | |
| 954 | + | func (x *XWM) outSetProp(i, n int) { | |
| 955 | + | o := &x.sel.out[i] | |
| 956 | + | seq := xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(o.requestor), xproto.Atom(o.property), | |
| 957 | + | xproto.Atom(o.target), 8, uint32(n), x.sel.pool.bufs[o.buf][:n]) | |
| 958 | + | if seq != 0 { | |
| 959 | + | if err := x.conn.Expect(seq, kindSelOut, uint32(i), o.gen, 0, 0); err != nil { | |
| 960 | + | x.stats.RingFull++ | |
| 961 | + | } | |
| 962 | + | } | |
| 963 | + | x.wake() | |
| 964 | + | } | |
| 965 | + | ||
| 966 | + | // outFinish ends a completed transfer. | |
| 967 | + | func (x *XWM) outFinish(i int) { | |
| 968 | + | o := &x.sel.out[i] | |
| 969 | + | if o.incr && !x.closed { | |
| 970 | + | var v xproto.ChangeWindowAttributesValues | |
| 971 | + | v.SetEventMask(0) | |
| 972 | + | xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(o.requestor), &v) | |
| 973 | + | x.wake() | |
| 974 | + | } | |
| 975 | + | x.outRelease(i) | |
| 976 | + | } | |
| 977 | + | ||
| 978 | + | // outRelease frees the transfer's resources. | |
| 979 | + | func (x *XWM) outRelease(i int) { | |
| 980 | + | o := &x.sel.out[i] | |
| 981 | + | x.outUnwatch(i) | |
| 982 | + | if o.fd >= 0 { | |
| 983 | + | _ = unix.Close(o.fd) | |
| 984 | + | } | |
| 985 | + | if o.buf >= 0 { | |
| 986 | + | x.sel.pool.put(o.buf) | |
| 987 | + | } | |
| 988 | + | o.fd, o.buf = -1, -1 | |
| 989 | + | o.gen++ | |
| 990 | + | o.state = outFree | |
| 991 | + | } | |
| 992 | + | ||
| 993 | + | // outAbort ends a transfer early: the requestor is refused (before its | |
| 994 | + | // notification) or released (INCR: mask dropped, property deleted). | |
| 995 | + | func (x *XWM) outAbort(i int) { | |
| 996 | + | o := &x.sel.out[i] | |
| 997 | + | if o.state == outFree { | |
| 998 | + | return | |
| 999 | + | } | |
| 1000 | + | if !x.closed { | |
| 1001 | + | switch { | |
| 1002 | + | case !o.notified: | |
| 1003 | + | x.notify(o.requestor, x.sel.st[o.kind].atom, o.target, 0, o.time) | |
| 1004 | + | case o.incr: | |
| 1005 | + | var v xproto.ChangeWindowAttributesValues | |
| 1006 | + | v.SetEventMask(0) | |
| 1007 | + | xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(o.requestor), &v) | |
| 1008 | + | xproto.DeleteProperty(x.conn.Buf(), xproto.Window(o.requestor), xproto.Atom(o.property)) | |
| 1009 | + | x.wake() | |
| 1010 | + | } | |
| 1011 | + | } | |
| 1012 | + | x.outRelease(i) | |
| 1013 | + | } | |
| 1014 | + | ||
| 1015 | + | // onRequestorProperty: PropertyNotify on a window we serve by INCR. | |
| 1016 | + | func (x *XWM) onRequestorProperty(e xproto.PropertyNotifyEvent) bool { | |
| 1017 | + | if e.State() != xproto.PropertyDelete { | |
| 1018 | + | return false | |
| 1019 | + | } | |
| 1020 | + | hit := false | |
| 1021 | + | for i := range x.sel.out { | |
| 1022 | + | o := &x.sel.out[i] | |
| 1023 | + | if o.state == outFree || !o.incr || o.requestor != uint32(e.Window()) || o.property != uint32(e.Atom()) { | |
| 1024 | + | continue | |
| 1025 | + | } | |
| 1026 | + | hit = true | |
| 1027 | + | o.credit = true | |
| 1028 | + | o.idle = 0 | |
| 1029 | + | x.outPump(i) | |
| 1030 | + | } | |
| 1031 | + | return hit | |
| 1032 | + | } | |
| @@ -33,6 +33,10 @@ type selState struct { | |||||
| 33 | atom uint32 | 33 | atom uint32 | ||
| 34 | gen uint32 // bumped on every ownership change; stales TARGETS probes | 34 | gen uint32 // bumped on every ownership change; stales TARGETS probes | ||
| 35 | owner uint32 // X owner window other than selWin, 0 none | 35 | owner uint32 // X owner window other than selWin, 0 none | ||
| 36 | + | ours bool // selWin owns the selection (Wayland -> X) | |||
| 37 | + | // wl is what the Wayland side offers while ours: raw text availability. | |||
| 38 | + | wlUTF8, wlPlain bool | |||
| 39 | + | wlMime [len(textMimes)]string // offered text MIMEs, in textMimes order, "" absent | |||
| 36 | } | 40 | } | ||
| 37 | 41 | ||||
| 38 | const ( | 42 | const ( | ||
| @@ -109,6 +113,7 @@ type selBridge struct { | |||||
| 109 | mimes [len(textMimes)]string // scratch handed to SelectionOwned | 113 | mimes [len(textMimes)]string // scratch handed to SelectionOwned | ||
| 110 | pool bufPool | 114 | pool bufPool | ||
| 111 | in [nXfer]xin | 115 | in [nXfer]xin | ||
| 116 | + | out [nXfer]xout | |||
| 112 | order uint64 | 117 | order uint64 | ||
| 113 | // active is the transfer that owns the conversion property, -1 none. | 118 | // active is the transfer that owns the conversion property, -1 none. | ||
| 114 | active int | 119 | active int | ||
| @@ -128,6 +133,11 @@ func (x *XWM) initSelection() { | |||||
| 128 | t.onR = func() { t.x.inAbort(t.idx, true) } | 133 | t.onR = func() { t.x.inAbort(t.idx, true) } | ||
| 129 | t.onW = func() { t.x.inWritable(t.idx) } | 134 | t.onW = func() { t.x.inWritable(t.idx) } | ||
| 130 | } | 135 | } | ||
| 136 | + | for i := range b.out { | |||
| 137 | + | o := &b.out[i] | |||
| 138 | + | o.x, o.idx, o.buf, o.fd = x, i, -1, -1 | |||
| 139 | + | o.onR = func() { o.x.outReadable(o.idx) } | |||
| 140 | + | } | |||
| 131 | } | 141 | } | ||
| 132 | 142 | ||||
| 133 | // bootSelection creates the selection window and asks for ownership events. | 143 | // bootSelection creates the selection window and asks for ownership events. | ||
| @@ -170,6 +180,10 @@ func (x *XWM) onSelectionEvent(ev *xturbo.Event) { | |||||
| 170 | x.onXFixesSelection(ev) | 180 | x.onXFixesSelection(ev) | ||
| 171 | case xproto.KindSelectionNotify: | 181 | case xproto.KindSelectionNotify: | ||
| 172 | x.onSelectionNotify(ev) | 182 | x.onSelectionNotify(ev) | ||
| 183 | + | case xproto.KindSelectionRequest: | |||
| 184 | + | x.onSelectionRequest(ev) | |||
| 185 | + | case xproto.KindSelectionClear: | |||
| 186 | + | x.onSelectionClear(ev) | |||
| 173 | } | 187 | } | ||
| 174 | } | 188 | } | ||
| 175 | 189 | ||||
| @@ -520,6 +534,7 @@ func (x *XWM) onSelProperty(ev *xturbo.Event) { | |||||
| 520 | if e.State() != xproto.PropertyNewValue || uint32(e.Atom()) != x.atoms[atomNeferwlData] { | 534 | if e.State() != xproto.PropertyNewValue || uint32(e.Atom()) != x.atoms[atomNeferwlData] { | ||
| 521 | return | 535 | return | ||
| 522 | } | 536 | } | ||
| 537 | + | // (a Deleted state on a requestor is handled by onRequestorProperty) | |||
| 523 | i := x.sel.active | 538 | i := x.sel.active | ||
| 524 | if i < 0 || !x.sel.in[i].incr { | 539 | if i < 0 || !x.sel.in[i].incr { | ||
| 525 | return | 540 | return | ||
| @@ -537,6 +552,12 @@ func (x *XWM) onSelProperty(ev *xturbo.Event) { | |||||
| 537 | 552 | ||||
| 538 | // onSelError: a selection request failed; the awaited reply never comes. | 553 | // onSelError: a selection request failed; the awaited reply never comes. | ||
| 539 | func (x *XWM) onSelError(e *xturbo.XError) { | 554 | func (x *XWM) onSelError(e *xturbo.XError) { | ||
| 555 | + | if e.Kind == kindSelOut { | |||
| 556 | + | if i := int(e.Args[0]); i >= 0 && i < nXfer && x.sel.out[i].gen == e.Args[1] && x.sel.out[i].state != outFree { | |||
| 557 | + | x.outAbort(i) | |||
| 558 | + | } | |||
| 559 | + | return | |||
| 560 | + | } | |||
| 540 | if e.Kind == kindSelData { | 561 | if e.Kind == kindSelData { | ||
| 541 | if i := int(e.Args[0]); i >= 0 && i < nXfer && x.sel.in[i].gen == e.Args[1] && x.sel.in[i].state == inReading { | 562 | if i := int(e.Args[0]); i >= 0 && i < nXfer && x.sel.in[i].gen == e.Args[1] && x.sel.in[i].state == inReading { | ||
| 542 | x.inAbort(i, false) | 563 | x.inAbort(i, false) | ||
| @@ -580,6 +601,23 @@ func (x *XWM) selTick() { | |||||
| 580 | } | 601 | } | ||
| 581 | live = true | 602 | live = true | ||
| 582 | } | 603 | } | ||
| 604 | + | for i := range b.out { | |||
| 605 | + | o := &b.out[i] | |||
| 606 | + | if o.state == outFree { | |||
| 607 | + | continue | |||
| 608 | + | } | |||
| 609 | + | o.idle++ | |||
| 610 | + | limit := rIdleMax | |||
| 611 | + | if o.incr && o.n > 0 && !o.credit { | |||
| 612 | + | limit = xIdleMax // the requestor does not delete the property | |||
| 613 | + | } | |||
| 614 | + | if o.idle >= limit { | |||
| 615 | + | x.log.Warn().Int("kind", o.kind).Msg("selection: transfer timed out") | |||
| 616 | + | x.outAbort(i) | |||
| 617 | + | continue | |||
| 618 | + | } | |||
| 619 | + | live = true | |||
| 620 | + | } | |||
| 583 | if live { | 621 | if live { | ||
| 584 | x.selEnsureTimer() | 622 | x.selEnsureTimer() | ||
| 585 | } | 623 | } | ||
| @@ -587,6 +625,11 @@ func (x *XWM) selTick() { | |||||
| 587 | 625 | ||||
| 588 | // selClose aborts every transfer. | 626 | // selClose aborts every transfer. | ||
| 589 | func (x *XWM) selClose() { | 627 | func (x *XWM) selClose() { | ||
| 628 | + | for i := range x.sel.out { | |||
| 629 | + | if x.sel.out[i].state != outFree { | |||
| 630 | + | x.outAbort(i) | |||
| 631 | + | } | |||
| 632 | + | } | |||
| 590 | for i := range x.sel.in { | 633 | for i := range x.sel.in { | ||
| 591 | if x.sel.in[i].state != inFree { | 634 | if x.sel.in[i].state != inFree { | ||
| 592 | x.inAbort(i, false) | 635 | x.inAbort(i, false) | ||
| @@ -597,3 +640,393 @@ func (x *XWM) selClose() { | |||||
| 597 | x.sel.cancel = nil | 640 | x.sel.cancel = nil | ||
| 598 | } | 641 | } | ||
| 599 | } | 642 | } | ||
| 643 | + | ||||
| 644 | + | // ---- Wayland -> X ---- | |||
| 645 | + | ||||
| 646 | + | // Wayland -> X transfer states. | |||
| 647 | + | const ( | |||
| 648 | + | outFree uint8 = iota | |||
| 649 | + | outActive | |||
| 650 | + | ) | |||
| 651 | + | ||||
| 652 | + | // xout is one Wayland -> X transfer. | |||
| 653 | + | type xout struct { | |||
| 654 | + | x *XWM | |||
| 655 | + | idx int | |||
| 656 | + | state uint8 | |||
| 657 | + | kind int | |||
| 658 | + | requestor uint32 | |||
| 659 | + | property uint32 | |||
| 660 | + | target uint32 | |||
| 661 | + | time uint32 | |||
| 662 | + | fd int | |||
| 663 | + | buf int | |||
| 664 | + | n int | |||
| 665 | + | eof bool | |||
| 666 | + | incr bool // INCR announced | |||
| 667 | + | credit bool // the requestor deleted the property: the next chunk may go | |||
| 668 | + | notified bool // the requestor got its SelectionNotify | |||
| 669 | + | idle int | |||
| 670 | + | gen uint32 | |||
| 671 | + | src fdSource | |||
| 672 | + | onR func() | |||
| 673 | + | } | |||
| 674 | + | ||||
| 675 | + | // WaylandSelection: a Wayland client owns kind with these MIME types, or none | |||
| 676 | + | // does (mimes nil). The XWM takes X ownership when text is offered. | |||
| 677 | + | func (x *XWM) WaylandSelection(kind int, mimes []string) { | |||
| 678 | + | if x.selWin == 0 || x.closed || kind < 0 || kind >= selKinds || x.host == nil { | |||
| 679 | + | return | |||
| 680 | + | } | |||
| 681 | + | st := &x.sel.st[kind] | |||
| 682 | + | st.wlUTF8, st.wlPlain = false, false | |||
| 683 | + | st.wlMime = [len(textMimes)]string{} | |||
| 684 | + | for _, m := range mimes { | |||
| 685 | + | for i, tm := range textMimes { | |||
| 686 | + | if m != tm { | |||
| 687 | + | continue | |||
| 688 | + | } | |||
| 689 | + | st.wlMime[i] = m | |||
| 690 | + | if mimeTarget[i] == atomUTF8String { | |||
| 691 | + | st.wlUTF8 = true | |||
| 692 | + | } else { | |||
| 693 | + | st.wlPlain = true | |||
| 694 | + | } | |||
| 695 | + | } | |||
| 696 | + | } | |||
| 697 | + | for i := range x.sel.out { // the data being served is stale | |||
| 698 | + | if x.sel.out[i].state != outFree && x.sel.out[i].kind == kind { | |||
| 699 | + | x.outAbort(i) | |||
| 700 | + | } | |||
| 701 | + | } | |||
| 702 | + | b := x.conn.Buf() | |||
| 703 | + | if !st.wlUTF8 && !st.wlPlain { | |||
| 704 | + | if st.ours { | |||
| 705 | + | st.ours = false | |||
| 706 | + | xproto.SetSelectionOwner(b, 0, xproto.Atom(st.atom), 0) | |||
| 707 | + | x.wake() | |||
| 708 | + | } | |||
| 709 | + | return | |||
| 710 | + | } | |||
| 711 | + | st.ours = true | |||
| 712 | + | st.gen++ | |||
| 713 | + | st.owner = 0 | |||
| 714 | + | xproto.SetSelectionOwner(b, xproto.Window(x.selWin), xproto.Atom(st.atom), 0) | |||
| 715 | + | x.log.Debug().Int("kind", kind).Msg("selection: Wayland owner") | |||
| 716 | + | x.wake() | |||
| 717 | + | } | |||
| 718 | + | ||||
| 719 | + | // onSelectionClear: someone else took a selection from selWin. | |||
| 720 | + | func (x *XWM) onSelectionClear(ev *xturbo.Event) { | |||
| 721 | + | e := xproto.SelectionClearOf(ev) | |||
| 722 | + | k := x.selKind(uint32(e.Selection())) | |||
| 723 | + | if k < 0 || uint32(e.Owner()) != x.selWin { | |||
| 724 | + | return | |||
| 725 | + | } | |||
| 726 | + | x.sel.st[k].ours = false | |||
| 727 | + | for i := range x.sel.out { | |||
| 728 | + | if x.sel.out[i].state != outFree && x.sel.out[i].kind == k { | |||
| 729 | + | x.outAbort(i) | |||
| 730 | + | } | |||
| 731 | + | } | |||
| 732 | + | } | |||
| 733 | + | ||||
| 734 | + | // notify sends a SelectionNotify to the requestor; property 0 refuses. | |||
| 735 | + | func (x *XWM) notify(requestor, selection, target, property, time uint32) { | |||
| 736 | + | d := xproto.SelectionNotifyEventData{ | |||
| 737 | + | Time: xproto.Timestamp(time), Requestor: xproto.Window(requestor), | |||
| 738 | + | Selection: xproto.Atom(selection), Target: xproto.Atom(target), Property: xproto.Atom(property), | |||
| 739 | + | } | |||
| 740 | + | var ev [32]byte | |||
| 741 | + | d.Put(&ev) | |||
| 742 | + | xproto.SendEvent(x.conn.Buf(), false, xproto.Window(requestor), 0, &ev) | |||
| 743 | + | x.wake() | |||
| 744 | + | } | |||
| 745 | + | ||||
| 746 | + | // onSelectionRequest: an X client asks selWin for the Wayland selection. | |||
| 747 | + | func (x *XWM) onSelectionRequest(ev *xturbo.Event) { | |||
| 748 | + | e := xproto.SelectionRequestOf(ev) | |||
| 749 | + | if uint32(e.Owner()) != x.selWin { | |||
| 750 | + | return | |||
| 751 | + | } | |||
| 752 | + | sel, req, target, prop, time := uint32(e.Selection()), uint32(e.Requestor()), uint32(e.Target()), uint32(e.Property()), uint32(e.Time()) | |||
| 753 | + | k := x.selKind(sel) | |||
| 754 | + | if k < 0 || !x.sel.st[k].ours { | |||
| 755 | + | x.notify(req, sel, target, 0, time) | |||
| 756 | + | return | |||
| 757 | + | } | |||
| 758 | + | if prop == 0 { // obsolete client | |||
| 759 | + | prop = target | |||
| 760 | + | } | |||
| 761 | + | st := &x.sel.st[k] | |||
| 762 | + | switch target { | |||
| 763 | + | case x.atoms[atomTargets]: | |||
| 764 | + | var list [5]uint32 | |||
| 765 | + | n := 0 | |||
| 766 | + | list[n], n = x.atoms[atomTargets], n+1 | |||
| 767 | + | if st.wlUTF8 { | |||
| 768 | + | list[n], n = x.atoms[atomUTF8String], n+1 | |||
| 769 | + | list[n], n = x.atoms[atomText], n+1 | |||
| 770 | + | } | |||
| 771 | + | list[n], n = x.atoms[atomString], n+1 | |||
| 772 | + | xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(req), xproto.Atom(prop), | |||
| 773 | + | xproto.AtomAtom, 32, uint32(n), x.encode32(list[:n])) | |||
| 774 | + | x.notify(req, sel, target, prop, time) | |||
| 775 | + | return | |||
| 776 | + | case x.atoms[atomUTF8String], x.atoms[atomText], x.atoms[atomString]: | |||
| 777 | + | default: | |||
| 778 | + | x.notify(req, sel, target, 0, time) | |||
| 779 | + | return | |||
| 780 | + | } | |||
| 781 | + | // Choose the Wayland MIME: UTF-8 targets prefer the UTF-8 types; STRING | |||
| 782 | + | // prefers plain text and falls back to the UTF-8 bytes unchanged. | |||
| 783 | + | mime := "" | |||
| 784 | + | order := [...]int{0, 2, 4, 1, 3} // utf8 types, then plain | |||
| 785 | + | if target == x.atoms[atomString] { | |||
| 786 | + | order = [...]int{1, 3, 0, 2, 4} | |||
| 787 | + | } | |||
| 788 | + | for _, i := range order { | |||
| 789 | + | if st.wlMime[i] != "" && (target == x.atoms[atomString] || mimeTarget[i] == atomUTF8String) { | |||
| 790 | + | mime = st.wlMime[i] | |||
| 791 | + | break | |||
| 792 | + | } | |||
| 793 | + | } | |||
| 794 | + | if mime == "" { | |||
| 795 | + | x.notify(req, sel, target, 0, time) | |||
| 796 | + | return | |||
| 797 | + | } | |||
| 798 | + | for i := range x.sel.out { // one transfer per (requestor, property) | |||
| 799 | + | o := &x.sel.out[i] | |||
| 800 | + | if o.state != outFree && o.requestor == req && o.property == prop { | |||
| 801 | + | x.notify(req, sel, target, 0, time) | |||
| 802 | + | return | |||
| 803 | + | } | |||
| 804 | + | } | |||
| 805 | + | slot := -1 | |||
| 806 | + | for i := range x.sel.out { | |||
| 807 | + | if x.sel.out[i].state == outFree { | |||
| 808 | + | slot = i | |||
| 809 | + | break | |||
| 810 | + | } | |||
| 811 | + | } | |||
| 812 | + | if slot < 0 || x.sel.pool.n == 0 { | |||
| 813 | + | x.log.Warn().Msg("selection: too many transfers, request refused") | |||
| 814 | + | x.notify(req, sel, target, 0, time) | |||
| 815 | + | return | |||
| 816 | + | } | |||
| 817 | + | fd := x.host.SelectionRead(k, mime) | |||
| 818 | + | if fd < 0 { | |||
| 819 | + | x.notify(req, sel, target, 0, time) | |||
| 820 | + | return | |||
| 821 | + | } | |||
| 822 | + | x.startOut(slot, k, req, prop, target, time, fd) | |||
| 823 | + | } | |||
| 824 | + | ||||
| 825 | + | // startOut begins serving the data on fd (a non-blocking read end it now | |||
| 826 | + | // owns) to requestor. | |||
| 827 | + | func (x *XWM) startOut(slot, k int, req, prop, target, time uint32, fd int) { | |||
| 828 | + | o := &x.sel.out[slot] | |||
| 829 | + | o.state, o.kind, o.requestor, o.property, o.target, o.time = outActive, k, req, prop, target, time | |||
| 830 | + | o.fd, o.buf, o.n, o.eof, o.incr, o.credit, o.notified, o.idle, o.src = fd, x.sel.pool.get(), 0, false, false, false, false, 0, nil | |||
| 831 | + | x.selEnsureTimer() | |||
| 832 | + | x.outRead(slot) // data may already be waiting | |||
| 833 | + | } | |||
| 834 | + | ||||
| 835 | + | // encode32 packs vals into the shared scratch. | |||
| 836 | + | func (x *XWM) encode32(vals []uint32) []byte { | |||
| 837 | + | n := 4 * len(vals) | |||
| 838 | + | if cap(x.scratch) < n { | |||
| 839 | + | x.scratch = make([]byte, 0, n) | |||
| 840 | + | } | |||
| 841 | + | p := x.scratch[:n] | |||
| 842 | + | for i, v := range vals { | |||
| 843 | + | binary.LittleEndian.PutUint32(p[4*i:], v) | |||
| 844 | + | } | |||
| 845 | + | return p | |||
| 846 | + | } | |||
| 847 | + | ||||
| 848 | + | // outWatch registers the pipe's read end with the display. | |||
| 849 | + | func (x *XWM) outWatch(i int) { | |||
| 850 | + | o := &x.sel.out[i] | |||
| 851 | + | if o.src != nil { | |||
| 852 | + | return | |||
| 853 | + | } | |||
| 854 | + | if x.addFD == nil { | |||
| 855 | + | x.outAbort(i) | |||
| 856 | + | return | |||
| 857 | + | } | |||
| 858 | + | src, err := x.addFD(o.fd, o.onR, nil) | |||
| 859 | + | if err != nil { | |||
| 860 | + | x.outAbort(i) | |||
| 861 | + | return | |||
| 862 | + | } | |||
| 863 | + | o.src = src | |||
| 864 | + | } | |||
| 865 | + | ||||
| 866 | + | // outUnwatch drops read interest: the buffer is full and nothing may be read | |||
| 867 | + | // until the requestor consumed the previous chunk. | |||
| 868 | + | func (x *XWM) outUnwatch(i int) { | |||
| 869 | + | o := &x.sel.out[i] | |||
| 870 | + | if o.src != nil { | |||
| 871 | + | o.src.Remove() | |||
| 872 | + | o.src = nil | |||
| 873 | + | } | |||
| 874 | + | } | |||
| 875 | + | ||||
| 876 | + | // outReadable is the pipe's read callback. | |||
| 877 | + | func (x *XWM) outReadable(i int) { | |||
| 878 | + | if x.sel.out[i].state == outFree { | |||
| 879 | + | return | |||
| 880 | + | } | |||
| 881 | + | x.outRead(i) | |||
| 882 | + | } | |||
| 883 | + | ||||
| 884 | + | // outRead fills the buffer from the pipe and advances the transfer. | |||
| 885 | + | func (x *XWM) outRead(i int) { | |||
| 886 | + | o := &x.sel.out[i] | |||
| 887 | + | buf := &x.sel.pool.bufs[o.buf] | |||
| 888 | + | for o.n < chunk && !o.eof { | |||
| 889 | + | n, err := unix.Read(o.fd, buf[o.n:]) | |||
| 890 | + | switch { | |||
| 891 | + | case err == nil && n == 0: | |||
| 892 | + | o.eof = true | |||
| 893 | + | case err == nil: | |||
| 894 | + | o.n += n | |||
| 895 | + | o.idle = 0 | |||
| 896 | + | case errors.Is(err, unix.EINTR): | |||
| 897 | + | continue | |||
| 898 | + | case errors.Is(err, unix.EAGAIN): | |||
| 899 | + | x.outPump(i) | |||
| 900 | + | return | |||
| 901 | + | default: | |||
| 902 | + | x.outAbort(i) | |||
| 903 | + | return | |||
| 904 | + | } | |||
| 905 | + | } | |||
| 906 | + | x.outPump(i) | |||
| 907 | + | } | |||
| 908 | + | ||||
| 909 | + | // outPump sends what the state allows. | |||
| 910 | + | func (x *XWM) outPump(i int) { | |||
| 911 | + | o := &x.sel.out[i] | |||
| 912 | + | if o.state == outFree { | |||
| 913 | + | return | |||
| 914 | + | } | |||
| 915 | + | full := o.n == chunk | |||
| 916 | + | if !o.incr { | |||
| 917 | + | switch { | |||
| 918 | + | case o.eof: // whole value | |||
| 919 | + | x.outSetProp(i, o.n) | |||
| 920 | + | x.notify(o.requestor, x.sel.st[o.kind].atom, o.target, o.property, o.time) | |||
| 921 | + | x.outFinish(i) | |||
| 922 | + | case full: // does not fit one request: INCR | |||
| 923 | + | o.incr, o.notified = true, true | |||
| 924 | + | var v xproto.ChangeWindowAttributesValues | |||
| 925 | + | v.SetEventMask(xproto.EventMaskPropertyChange) | |||
| 926 | + | xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(o.requestor), &v) | |||
| 927 | + | xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(o.requestor), xproto.Atom(o.property), | |||
| 928 | + | xproto.Atom(x.atoms[atomIncr]), 32, 1, x.encode32([]uint32{0})) | |||
| 929 | + | x.notify(o.requestor, x.sel.st[o.kind].atom, o.target, o.property, o.time) | |||
| 930 | + | x.outUnwatch(i) | |||
| 931 | + | default: | |||
| 932 | + | x.outWatch(i) | |||
| 933 | + | } | |||
| 934 | + | return | |||
| 935 | + | } | |||
| 936 | + | if o.credit && (full || o.eof) { | |||
| 937 | + | o.credit = false | |||
| 938 | + | o.idle = 0 | |||
| 939 | + | x.outSetProp(i, o.n) | |||
| 940 | + | if o.n == 0 { // the empty chunk ends the transfer | |||
| 941 | + | x.outFinish(i) | |||
| 942 | + | return | |||
| 943 | + | } | |||
| 944 | + | o.n = 0 | |||
| 945 | + | } | |||
| 946 | + | if o.n < chunk && !o.eof { | |||
| 947 | + | x.outWatch(i) | |||
| 948 | + | } else { | |||
| 949 | + | x.outUnwatch(i) | |||
| 950 | + | } | |||
| 951 | + | } | |||
| 952 | + | ||||
| 953 | + | // outSetProp writes the first n buffered bytes as the property value. | |||
| 954 | + | func (x *XWM) outSetProp(i, n int) { | |||
| 955 | + | o := &x.sel.out[i] | |||
| 956 | + | seq := xproto.ChangeProperty(x.conn.Buf(), xproto.PropModeReplace, xproto.Window(o.requestor), xproto.Atom(o.property), | |||
| 957 | + | xproto.Atom(o.target), 8, uint32(n), x.sel.pool.bufs[o.buf][:n]) | |||
| 958 | + | if seq != 0 { | |||
| 959 | + | if err := x.conn.Expect(seq, kindSelOut, uint32(i), o.gen, 0, 0); err != nil { | |||
| 960 | + | x.stats.RingFull++ | |||
| 961 | + | } | |||
| 962 | + | } | |||
| 963 | + | x.wake() | |||
| 964 | + | } | |||
| 965 | + | ||||
| 966 | + | // outFinish ends a completed transfer. | |||
| 967 | + | func (x *XWM) outFinish(i int) { | |||
| 968 | + | o := &x.sel.out[i] | |||
| 969 | + | if o.incr && !x.closed { | |||
| 970 | + | var v xproto.ChangeWindowAttributesValues | |||
| 971 | + | v.SetEventMask(0) | |||
| 972 | + | xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(o.requestor), &v) | |||
| 973 | + | x.wake() | |||
| 974 | + | } | |||
| 975 | + | x.outRelease(i) | |||
| 976 | + | } | |||
| 977 | + | ||||
| 978 | + | // outRelease frees the transfer's resources. | |||
| 979 | + | func (x *XWM) outRelease(i int) { | |||
| 980 | + | o := &x.sel.out[i] | |||
| 981 | + | x.outUnwatch(i) | |||
| 982 | + | if o.fd >= 0 { | |||
| 983 | + | _ = unix.Close(o.fd) | |||
| 984 | + | } | |||
| 985 | + | if o.buf >= 0 { | |||
| 986 | + | x.sel.pool.put(o.buf) | |||
| 987 | + | } | |||
| 988 | + | o.fd, o.buf = -1, -1 | |||
| 989 | + | o.gen++ | |||
| 990 | + | o.state = outFree | |||
| 991 | + | } | |||
| 992 | + | ||||
| 993 | + | // outAbort ends a transfer early: the requestor is refused (before its | |||
| 994 | + | // notification) or released (INCR: mask dropped, property deleted). | |||
| 995 | + | func (x *XWM) outAbort(i int) { | |||
| 996 | + | o := &x.sel.out[i] | |||
| 997 | + | if o.state == outFree { | |||
| 998 | + | return | |||
| 999 | + | } | |||
| 1000 | + | if !x.closed { | |||
| 1001 | + | switch { | |||
| 1002 | + | case !o.notified: | |||
| 1003 | + | x.notify(o.requestor, x.sel.st[o.kind].atom, o.target, 0, o.time) | |||
| 1004 | + | case o.incr: | |||
| 1005 | + | var v xproto.ChangeWindowAttributesValues | |||
| 1006 | + | v.SetEventMask(0) | |||
| 1007 | + | xproto.ChangeWindowAttributes(x.conn.Buf(), xproto.Window(o.requestor), &v) | |||
| 1008 | + | xproto.DeleteProperty(x.conn.Buf(), xproto.Window(o.requestor), xproto.Atom(o.property)) | |||
| 1009 | + | x.wake() | |||
| 1010 | + | } | |||
| 1011 | + | } | |||
| 1012 | + | x.outRelease(i) | |||
| 1013 | + | } | |||
| 1014 | + | ||||
| 1015 | + | // onRequestorProperty: PropertyNotify on a window we serve by INCR. | |||
| 1016 | + | func (x *XWM) onRequestorProperty(e xproto.PropertyNotifyEvent) bool { | |||
| 1017 | + | if e.State() != xproto.PropertyDelete { | |||
| 1018 | + | return false | |||
| 1019 | + | } | |||
| 1020 | + | hit := false | |||
| 1021 | + | for i := range x.sel.out { | |||
| 1022 | + | o := &x.sel.out[i] | |||
| 1023 | + | if o.state == outFree || !o.incr || o.requestor != uint32(e.Window()) || o.property != uint32(e.Atom()) { | |||
| 1024 | + | continue | |||
| 1025 | + | } | |||
| 1026 | + | hit = true | |||
| 1027 | + | o.credit = true | |||
| 1028 | + | o.idle = 0 | |||
| 1029 | + | x.outPump(i) | |||
| 1030 | + | } | |||
| 1031 | + | return hit | |||
| 1032 | + | } | |||
M internal/adapters/wayland/xwm/selection_test.go +261 -0
| @@ -422,3 +422,264 @@ func TestSelectionINCRChunkAllocations(t *testing.T) { | |||
| 422 | 422 | require.Zero(t, testing.AllocsPerRun(100, step)) | |
| 423 | 423 | require.Equal(t, inWaitIncr, x.sel.in[0].state) | |
| 424 | 424 | } | |
| 425 | + | ||
| 426 | + | // ---- Wayland -> X ---- | |
| 427 | + | ||
| 428 | + | const reqWin = 0x500020 | |
| 429 | + | ||
| 430 | + | func (p *xpeer) selectionRequest(owner, requestor, selection, target, property uint32) { | |
| 431 | + | p.event(30, func(b *[32]byte) { | |
| 432 | + | binary.LittleEndian.PutUint32(b[8:], owner) | |
| 433 | + | binary.LittleEndian.PutUint32(b[12:], requestor) | |
| 434 | + | binary.LittleEndian.PutUint32(b[16:], selection) | |
| 435 | + | binary.LittleEndian.PutUint32(b[20:], target) | |
| 436 | + | binary.LittleEndian.PutUint32(b[24:], property) | |
| 437 | + | }) | |
| 438 | + | } | |
| 439 | + | ||
| 440 | + | func (p *xpeer) selectionClear(owner, selection uint32) { | |
| 441 | + | p.event(29, func(b *[32]byte) { | |
| 442 | + | binary.LittleEndian.PutUint32(b[8:], owner) | |
| 443 | + | binary.LittleEndian.PutUint32(b[12:], selection) | |
| 444 | + | }) | |
| 445 | + | } | |
| 446 | + | ||
| 447 | + | // sentNotify returns the property of the SelectionNotify SendEvent after skip. | |
| 448 | + | func sentNotify(t *testing.T, r *selRig) (property uint32) { | |
| 449 | + | t.Helper() | |
| 450 | + | req, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == opSendEvent && q.raw[12] == 31 }) | |
| 451 | + | r.skip = n | |
| 452 | + | require.EqualValues(t, reqWin, req.u32(4)) | |
| 453 | + | require.Equal(t, r.clip, binary.LittleEndian.Uint32(req.raw[12+12:])) | |
| 454 | + | return binary.LittleEndian.Uint32(req.raw[12+20:]) | |
| 455 | + | } | |
| 456 | + | ||
| 457 | + | // ownedByWayland attaches an XWM and gives CLIPBOARD to a Wayland client offering mimes. | |
| 458 | + | func ownedByWayland(t *testing.T, mimes ...string) *selRig { | |
| 459 | + | t.Helper() | |
| 460 | + | x, p, h, _, _ := attached(t) | |
| 461 | + | r := &selRig{x: x, p: p, h: h, clip: p.atom("CLIPBOARD"), utf8: p.atom("UTF8_STRING"), data: p.atom("_NEFERWL_DATA")} | |
| 462 | + | h.EXPECT().After(mock.Anything, mock.Anything).Run(func(_ time.Duration, fn func()) { r.timer = fn }).Return(func() {}).Maybe() | |
| 463 | + | x.WaylandSelection(SelClipboard, mimes) | |
| 464 | + | x.flush() | |
| 465 | + | req, n := pumpReq(t, x, p, 0, func(q xpeerReq) bool { return q.major == opSetSelectionOwner && q.u32(8) == r.clip }) | |
| 466 | + | require.Equal(t, x.selWin, req.u32(4)) | |
| 467 | + | r.skip = n | |
| 468 | + | return r | |
| 469 | + | } | |
| 470 | + | ||
| 471 | + | // serve queues a SelectionRequest; SelectionRead hands out the read end of a | |
| 472 | + | // pipe whose write end is returned. | |
| 473 | + | func (r *selRig) serve(t *testing.T, target uint32) (w *os.File) { | |
| 474 | + | t.Helper() | |
| 475 | + | fds := make([]int, 2) | |
| 476 | + | require.NoError(t, unix.Pipe2(fds, unix.O_CLOEXEC|unix.O_NONBLOCK)) | |
| 477 | + | r.h.EXPECT().SelectionRead(SelClipboard, mock.Anything).Return(fds[0]).Once() | |
| 478 | + | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, target, r.data) | |
| 479 | + | w = os.NewFile(uintptr(fds[1]), "pipe-w") | |
| 480 | + | t.Cleanup(func() { w.Close() }) | |
| 481 | + | return w | |
| 482 | + | } | |
| 483 | + | ||
| 484 | + | func TestSelectionWaylandToXTargets(t *testing.T) { | |
| 485 | + | r := ownedByWayland(t, "text/plain") | |
| 486 | + | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.p.atom("TARGETS"), r.data) | |
| 487 | + | require.Equal(t, r.data, sentNotify(t, r)) | |
| 488 | + | v, ok := r.p.prop(reqWin, "_NEFERWL_DATA") | |
| 489 | + | require.True(t, ok) | |
| 490 | + | require.Equal(t, []uint32{r.p.atom("TARGETS"), r.p.atom("STRING")}, atomsOf(v), "no UTF8 target for text/plain only") | |
| 491 | + | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.p.atom("image/png"), r.data) | |
| 492 | + | require.Zero(t, sentNotify(t, r), "unknown target refused") | |
| 493 | + | // UTF8_STRING is not offered by a text/plain-only source, but STRING reads it. | |
| 494 | + | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.utf8, r.data) | |
| 495 | + | require.Zero(t, sentNotify(t, r), "no UTF-8 text offered") | |
| 496 | + | } | |
| 497 | + | ||
| 498 | + | func TestSelectionWaylandToXSmall(t *testing.T) { | |
| 499 | + | r := ownedByWayland(t, "text/plain;charset=utf-8") | |
| 500 | + | w := r.serve(t, r.utf8) | |
| 501 | + | data := pattern(4096) | |
| 502 | + | _, err := w.Write(data) | |
| 503 | + | require.NoError(t, err) | |
| 504 | + | require.NoError(t, w.Close()) | |
| 505 | + | require.Equal(t, r.data, sentNotify(t, r)) | |
| 506 | + | v, ok := r.p.prop(reqWin, "_NEFERWL_DATA") | |
| 507 | + | require.True(t, ok) | |
| 508 | + | require.Equal(t, r.utf8, v.typ) | |
| 509 | + | require.True(t, bytes.Equal(data, v.data)) | |
| 510 | + | require.Equal(t, nXfer, r.x.sel.pool.n) | |
| 511 | + | } | |
| 512 | + | ||
| 513 | + | func TestSelectionWaylandToXINCR(t *testing.T) { | |
| 514 | + | r := ownedByWayland(t, "text/plain;charset=utf-8") | |
| 515 | + | src := newMockfdSource(t) | |
| 516 | + | var onR func() | |
| 517 | + | removes := 0 | |
| 518 | + | r.x.addFD = func(_ int, rd, _ func()) (fdSource, error) { onR = rd; return src, nil } | |
| 519 | + | src.EXPECT().Remove().Run(func() { removes++ }).Return().Maybe() | |
| 520 | + | const total = 1<<20 + 123 | |
| 521 | + | data := pattern(total) | |
| 522 | + | w := r.serve(t, r.utf8) | |
| 523 | + | require.NoError(t, unix.SetNonblock(int(w.Fd()), false)) | |
| 524 | + | go func() { _, _ = w.Write(data); w.Close() }() | |
| 525 | + | ||
| 526 | + | // The first fill is a whole chunk: INCR is announced. | |
| 527 | + | var got []byte | |
| 528 | + | deadline := time.Now().Add(5 * time.Second) | |
| 529 | + | for !r.x.sel.out[0].incr { | |
| 530 | + | require.True(t, time.Now().Before(deadline), "INCR never announced") | |
| 531 | + | r.x.onReadable() | |
| 532 | + | if onR != nil && r.x.sel.out[0].state == outActive { | |
| 533 | + | onR() | |
| 534 | + | } | |
| 535 | + | time.Sleep(time.Millisecond) | |
| 536 | + | } | |
| 537 | + | require.Equal(t, r.data, sentNotify(t, r)) | |
| 538 | + | v, _ := r.p.prop(reqWin, "_NEFERWL_DATA") | |
| 539 | + | require.Equal(t, r.p.atom("INCR"), v.typ) | |
| 540 | + | mask, _ := pumpReq(t, r.x, r.p, 0, func(q xpeerReq) bool { | |
| 541 | + | return q.major == opChangeWindowAttributes && q.u32(4) == reqWin && q.u32(12) == uint32(xproto.EventMaskPropertyChange) | |
| 542 | + | }) | |
| 543 | + | require.NotZero(t, mask.raw) | |
| 544 | + | ||
| 545 | + | chunks := 0 | |
| 546 | + | for { | |
| 547 | + | // The requestor deletes the property: the next chunk goes out. | |
| 548 | + | r.p.propertyNotify(reqWin, r.data, true) | |
| 549 | + | before := len(r.p.requests()) | |
| 550 | + | if onR != nil { | |
| 551 | + | time.Sleep(time.Millisecond) | |
| 552 | + | } | |
| 553 | + | var cp xpeerProp | |
| 554 | + | readUntil(t, r.x, func() bool { | |
| 555 | + | if onR != nil && r.x.sel.out[0].state != outFree && r.x.sel.out[0].n < chunk && !r.x.sel.out[0].eof { | |
| 556 | + | onR() | |
| 557 | + | } | |
| 558 | + | r.x.flush() | |
| 559 | + | for _, q := range r.p.requests()[before:] { | |
| 560 | + | if q.major == opChangeProperty && q.u32(4) == reqWin { | |
| 561 | + | return true | |
| 562 | + | } | |
| 563 | + | } | |
| 564 | + | return false | |
| 565 | + | }) | |
| 566 | + | cp, _ = r.p.prop(reqWin, "_NEFERWL_DATA") | |
| 567 | + | chunks++ | |
| 568 | + | if len(cp.data) == 0 { | |
| 569 | + | break | |
| 570 | + | } | |
| 571 | + | require.Equal(t, r.utf8, cp.typ) | |
| 572 | + | got = append(got, cp.data...) | |
| 573 | + | } | |
| 574 | + | require.True(t, bytes.Equal(data, got), "byte-exact: %d of %d", len(got), total) | |
| 575 | + | require.Equal(t, total/chunk+2, chunks, "16 full chunks, the tail, then the empty terminator") | |
| 576 | + | readUntil(t, r.x, func() bool { return r.x.sel.out[0].state == outFree }) | |
| 577 | + | // The mask is removed at the end. | |
| 578 | + | reqs := r.p.requests() | |
| 579 | + | last := reqs[len(reqs)-1] | |
| 580 | + | for i := len(reqs) - 1; i >= 0; i-- { | |
| 581 | + | if reqs[i].major == opChangeWindowAttributes && reqs[i].u32(4) == reqWin { | |
| 582 | + | last = reqs[i] | |
| 583 | + | break | |
| 584 | + | } | |
| 585 | + | } | |
| 586 | + | require.Zero(t, last.u32(12), "event mask dropped") | |
| 587 | + | require.Equal(t, nXfer, r.x.sel.pool.n) | |
| 588 | + | } | |
| 589 | + | ||
| 590 | + | func TestSelectionWaylandToXTimeoutAndClear(t *testing.T) { | |
| 591 | + | r := ownedByWayland(t, "text/plain;charset=utf-8") | |
| 592 | + | src := newMockfdSource(t) | |
| 593 | + | src.EXPECT().Remove().Return().Maybe() | |
| 594 | + | r.x.addFD = func(_ int, _, _ func()) (fdSource, error) { return src, nil } | |
| 595 | + | w := r.serve(t, r.utf8) | |
| 596 | + | _, err := w.Write(pattern(chunk)) // a full chunk: INCR | |
| 597 | + | require.NoError(t, err) | |
| 598 | + | readUntil(t, r.x, func() bool { return r.x.sel.out[0].incr }) | |
| 599 | + | require.Equal(t, r.data, sentNotify(t, r)) | |
| 600 | + | require.NotNil(t, r.timer) | |
| 601 | + | for i := 0; i < xIdleMax; i++ { | |
| 602 | + | require.Equal(t, outActive, r.x.sel.out[0].state, i) | |
| 603 | + | r.timer() | |
| 604 | + | } | |
| 605 | + | require.Equal(t, outFree, r.x.sel.out[0].state, "no Deleted for 10 s aborts") | |
| 606 | + | pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == opDeleteProperty && q.u32(4) == reqWin }) | |
| 607 | + | ||
| 608 | + | // SelectionClear aborts a running transfer and refuses further requests. | |
| 609 | + | w2 := r.serve(t, r.utf8) | |
| 610 | + | _, err = w2.Write(pattern(chunk)) | |
| 611 | + | require.NoError(t, err) | |
| 612 | + | readUntil(t, r.x, func() bool { return r.x.sel.out[0].state == outActive }) | |
| 613 | + | r.p.selectionClear(r.x.selWin, r.clip) | |
| 614 | + | readUntil(t, r.x, func() bool { return r.x.sel.out[0].state == outFree }) | |
| 615 | + | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.utf8, r.data) | |
| 616 | + | require.Equal(t, r.data, sentNotify(t, r), "INCR announcement of the aborted transfer") | |
| 617 | + | require.Zero(t, sentNotify(t, r), "request after the clear is refused") | |
| 618 | + | } | |
| 619 | + | ||
| 620 | + | // Replacing the Wayland selection mid-transfer aborts it; nil releases X ownership. | |
| 621 | + | func TestSelectionClearAndRace(t *testing.T) { | |
| 622 | + | r := ownedByWayland(t, "text/plain;charset=utf-8") | |
| 623 | + | src := newMockfdSource(t) | |
| 624 | + | src.EXPECT().Remove().Return().Maybe() | |
| 625 | + | r.x.addFD = func(_ int, _, _ func()) (fdSource, error) { return src, nil } | |
| 626 | + | w := r.serve(t, r.utf8) | |
| 627 | + | _, err := w.Write(pattern(chunk)) | |
| 628 | + | require.NoError(t, err) | |
| 629 | + | readUntil(t, r.x, func() bool { return r.x.sel.out[0].incr }) | |
| 630 | + | r.x.WaylandSelection(SelClipboard, []string{"text/plain"}) | |
| 631 | + | require.Equal(t, outFree, r.x.sel.out[0].state) | |
| 632 | + | r.x.WaylandSelection(SelClipboard, nil) // protected or cleared | |
| 633 | + | r.x.flush() | |
| 634 | + | req, _ := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == opSetSelectionOwner && q.u32(4) == 0 }) | |
| 635 | + | require.Equal(t, r.clip, req.u32(8), "ownership released") | |
| 636 | + | require.False(t, r.x.sel.st[SelClipboard].ours) | |
| 637 | + | } | |
| 638 | + | ||
| 639 | + | func TestSelectionCopyAllocationsWaylandToX(t *testing.T) { | |
| 640 | + | r := ownedByWayland(t, "text/plain;charset=utf-8") | |
| 641 | + | x := r.x | |
| 642 | + | sinkPeer(t, x, r.p) | |
| 643 | + | x.after = func(time.Duration, func()) func() { return func() {} } | |
| 644 | + | x.sel.timerOn = true | |
| 645 | + | data := pattern(4096) | |
| 646 | + | var ev xturbo.Event | |
| 647 | + | ev.Kind = xproto.KindSelectionRequest | |
| 648 | + | binary.LittleEndian.PutUint32(ev.Raw[8:], x.selWin) | |
| 649 | + | binary.LittleEndian.PutUint32(ev.Raw[12:], reqWin) | |
| 650 | + | binary.LittleEndian.PutUint32(ev.Raw[16:], r.clip) | |
| 651 | + | binary.LittleEndian.PutUint32(ev.Raw[20:], r.utf8) | |
| 652 | + | binary.LittleEndian.PutUint32(ev.Raw[24:], r.data) | |
| 653 | + | fds := make([]int, 2) | |
| 654 | + | round := func() { | |
| 655 | + | if unix.Pipe2(fds, unix.O_CLOEXEC|unix.O_NONBLOCK) != nil { | |
| 656 | + | t.Fatal("pipe") | |
| 657 | + | } | |
| 658 | + | if _, err := unix.Write(fds[1], data); err != nil { | |
| 659 | + | t.Fatal("write") | |
| 660 | + | } | |
| 661 | + | unix.Close(fds[1]) | |
| 662 | + | x.host.(*mockhost).ExpectedCalls = nil | |
| 663 | + | x.onSelectionRequest(&ev) | |
| 664 | + | x.posted = false | |
| 665 | + | x.flush() | |
| 666 | + | } | |
| 667 | + | _ = round | |
| 668 | + | // The host mock's matching is not allocation free, so the host call is replaced. | |
| 669 | + | x.sel.st[SelClipboard].wlMime = [len(textMimes)]string{"text/plain;charset=utf-8"} | |
| 670 | + | allocs := testing.AllocsPerRun(100, func() { | |
| 671 | + | if unix.Pipe2(fds, unix.O_CLOEXEC|unix.O_NONBLOCK) != nil { | |
| 672 | + | t.Fatal("pipe") | |
| 673 | + | } | |
| 674 | + | if _, err := unix.Write(fds[1], data); err != nil { | |
| 675 | + | t.Fatal("write") | |
| 676 | + | } | |
| 677 | + | unix.Close(fds[1]) | |
| 678 | + | x.startOut(0, SelClipboard, reqWin, r.data, r.utf8, 0, fds[0]) | |
| 679 | + | x.posted = false | |
| 680 | + | x.flush() | |
| 681 | + | }) | |
| 682 | + | // Pipe2 itself does not allocate; the measured path is read + ChangeProperty + SendEvent. | |
| 683 | + | require.Zero(t, allocs) | |
| 684 | + | require.Equal(t, outFree, x.sel.out[0].state) | |
| 685 | + | } | |
| @@ -422,3 +422,264 @@ func TestSelectionINCRChunkAllocations(t *testing.T) { | |||||
| 422 | require.Zero(t, testing.AllocsPerRun(100, step)) | 422 | require.Zero(t, testing.AllocsPerRun(100, step)) | ||
| 423 | require.Equal(t, inWaitIncr, x.sel.in[0].state) | 423 | require.Equal(t, inWaitIncr, x.sel.in[0].state) | ||
| 424 | } | 424 | } | ||
| 425 | + | ||||
| 426 | + | // ---- Wayland -> X ---- | |||
| 427 | + | ||||
| 428 | + | const reqWin = 0x500020 | |||
| 429 | + | ||||
| 430 | + | func (p *xpeer) selectionRequest(owner, requestor, selection, target, property uint32) { | |||
| 431 | + | p.event(30, func(b *[32]byte) { | |||
| 432 | + | binary.LittleEndian.PutUint32(b[8:], owner) | |||
| 433 | + | binary.LittleEndian.PutUint32(b[12:], requestor) | |||
| 434 | + | binary.LittleEndian.PutUint32(b[16:], selection) | |||
| 435 | + | binary.LittleEndian.PutUint32(b[20:], target) | |||
| 436 | + | binary.LittleEndian.PutUint32(b[24:], property) | |||
| 437 | + | }) | |||
| 438 | + | } | |||
| 439 | + | ||||
| 440 | + | func (p *xpeer) selectionClear(owner, selection uint32) { | |||
| 441 | + | p.event(29, func(b *[32]byte) { | |||
| 442 | + | binary.LittleEndian.PutUint32(b[8:], owner) | |||
| 443 | + | binary.LittleEndian.PutUint32(b[12:], selection) | |||
| 444 | + | }) | |||
| 445 | + | } | |||
| 446 | + | ||||
| 447 | + | // sentNotify returns the property of the SelectionNotify SendEvent after skip. | |||
| 448 | + | func sentNotify(t *testing.T, r *selRig) (property uint32) { | |||
| 449 | + | t.Helper() | |||
| 450 | + | req, n := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == opSendEvent && q.raw[12] == 31 }) | |||
| 451 | + | r.skip = n | |||
| 452 | + | require.EqualValues(t, reqWin, req.u32(4)) | |||
| 453 | + | require.Equal(t, r.clip, binary.LittleEndian.Uint32(req.raw[12+12:])) | |||
| 454 | + | return binary.LittleEndian.Uint32(req.raw[12+20:]) | |||
| 455 | + | } | |||
| 456 | + | ||||
| 457 | + | // ownedByWayland attaches an XWM and gives CLIPBOARD to a Wayland client offering mimes. | |||
| 458 | + | func ownedByWayland(t *testing.T, mimes ...string) *selRig { | |||
| 459 | + | t.Helper() | |||
| 460 | + | x, p, h, _, _ := attached(t) | |||
| 461 | + | r := &selRig{x: x, p: p, h: h, clip: p.atom("CLIPBOARD"), utf8: p.atom("UTF8_STRING"), data: p.atom("_NEFERWL_DATA")} | |||
| 462 | + | h.EXPECT().After(mock.Anything, mock.Anything).Run(func(_ time.Duration, fn func()) { r.timer = fn }).Return(func() {}).Maybe() | |||
| 463 | + | x.WaylandSelection(SelClipboard, mimes) | |||
| 464 | + | x.flush() | |||
| 465 | + | req, n := pumpReq(t, x, p, 0, func(q xpeerReq) bool { return q.major == opSetSelectionOwner && q.u32(8) == r.clip }) | |||
| 466 | + | require.Equal(t, x.selWin, req.u32(4)) | |||
| 467 | + | r.skip = n | |||
| 468 | + | return r | |||
| 469 | + | } | |||
| 470 | + | ||||
| 471 | + | // serve queues a SelectionRequest; SelectionRead hands out the read end of a | |||
| 472 | + | // pipe whose write end is returned. | |||
| 473 | + | func (r *selRig) serve(t *testing.T, target uint32) (w *os.File) { | |||
| 474 | + | t.Helper() | |||
| 475 | + | fds := make([]int, 2) | |||
| 476 | + | require.NoError(t, unix.Pipe2(fds, unix.O_CLOEXEC|unix.O_NONBLOCK)) | |||
| 477 | + | r.h.EXPECT().SelectionRead(SelClipboard, mock.Anything).Return(fds[0]).Once() | |||
| 478 | + | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, target, r.data) | |||
| 479 | + | w = os.NewFile(uintptr(fds[1]), "pipe-w") | |||
| 480 | + | t.Cleanup(func() { w.Close() }) | |||
| 481 | + | return w | |||
| 482 | + | } | |||
| 483 | + | ||||
| 484 | + | func TestSelectionWaylandToXTargets(t *testing.T) { | |||
| 485 | + | r := ownedByWayland(t, "text/plain") | |||
| 486 | + | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.p.atom("TARGETS"), r.data) | |||
| 487 | + | require.Equal(t, r.data, sentNotify(t, r)) | |||
| 488 | + | v, ok := r.p.prop(reqWin, "_NEFERWL_DATA") | |||
| 489 | + | require.True(t, ok) | |||
| 490 | + | require.Equal(t, []uint32{r.p.atom("TARGETS"), r.p.atom("STRING")}, atomsOf(v), "no UTF8 target for text/plain only") | |||
| 491 | + | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.p.atom("image/png"), r.data) | |||
| 492 | + | require.Zero(t, sentNotify(t, r), "unknown target refused") | |||
| 493 | + | // UTF8_STRING is not offered by a text/plain-only source, but STRING reads it. | |||
| 494 | + | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.utf8, r.data) | |||
| 495 | + | require.Zero(t, sentNotify(t, r), "no UTF-8 text offered") | |||
| 496 | + | } | |||
| 497 | + | ||||
| 498 | + | func TestSelectionWaylandToXSmall(t *testing.T) { | |||
| 499 | + | r := ownedByWayland(t, "text/plain;charset=utf-8") | |||
| 500 | + | w := r.serve(t, r.utf8) | |||
| 501 | + | data := pattern(4096) | |||
| 502 | + | _, err := w.Write(data) | |||
| 503 | + | require.NoError(t, err) | |||
| 504 | + | require.NoError(t, w.Close()) | |||
| 505 | + | require.Equal(t, r.data, sentNotify(t, r)) | |||
| 506 | + | v, ok := r.p.prop(reqWin, "_NEFERWL_DATA") | |||
| 507 | + | require.True(t, ok) | |||
| 508 | + | require.Equal(t, r.utf8, v.typ) | |||
| 509 | + | require.True(t, bytes.Equal(data, v.data)) | |||
| 510 | + | require.Equal(t, nXfer, r.x.sel.pool.n) | |||
| 511 | + | } | |||
| 512 | + | ||||
| 513 | + | func TestSelectionWaylandToXINCR(t *testing.T) { | |||
| 514 | + | r := ownedByWayland(t, "text/plain;charset=utf-8") | |||
| 515 | + | src := newMockfdSource(t) | |||
| 516 | + | var onR func() | |||
| 517 | + | removes := 0 | |||
| 518 | + | r.x.addFD = func(_ int, rd, _ func()) (fdSource, error) { onR = rd; return src, nil } | |||
| 519 | + | src.EXPECT().Remove().Run(func() { removes++ }).Return().Maybe() | |||
| 520 | + | const total = 1<<20 + 123 | |||
| 521 | + | data := pattern(total) | |||
| 522 | + | w := r.serve(t, r.utf8) | |||
| 523 | + | require.NoError(t, unix.SetNonblock(int(w.Fd()), false)) | |||
| 524 | + | go func() { _, _ = w.Write(data); w.Close() }() | |||
| 525 | + | ||||
| 526 | + | // The first fill is a whole chunk: INCR is announced. | |||
| 527 | + | var got []byte | |||
| 528 | + | deadline := time.Now().Add(5 * time.Second) | |||
| 529 | + | for !r.x.sel.out[0].incr { | |||
| 530 | + | require.True(t, time.Now().Before(deadline), "INCR never announced") | |||
| 531 | + | r.x.onReadable() | |||
| 532 | + | if onR != nil && r.x.sel.out[0].state == outActive { | |||
| 533 | + | onR() | |||
| 534 | + | } | |||
| 535 | + | time.Sleep(time.Millisecond) | |||
| 536 | + | } | |||
| 537 | + | require.Equal(t, r.data, sentNotify(t, r)) | |||
| 538 | + | v, _ := r.p.prop(reqWin, "_NEFERWL_DATA") | |||
| 539 | + | require.Equal(t, r.p.atom("INCR"), v.typ) | |||
| 540 | + | mask, _ := pumpReq(t, r.x, r.p, 0, func(q xpeerReq) bool { | |||
| 541 | + | return q.major == opChangeWindowAttributes && q.u32(4) == reqWin && q.u32(12) == uint32(xproto.EventMaskPropertyChange) | |||
| 542 | + | }) | |||
| 543 | + | require.NotZero(t, mask.raw) | |||
| 544 | + | ||||
| 545 | + | chunks := 0 | |||
| 546 | + | for { | |||
| 547 | + | // The requestor deletes the property: the next chunk goes out. | |||
| 548 | + | r.p.propertyNotify(reqWin, r.data, true) | |||
| 549 | + | before := len(r.p.requests()) | |||
| 550 | + | if onR != nil { | |||
| 551 | + | time.Sleep(time.Millisecond) | |||
| 552 | + | } | |||
| 553 | + | var cp xpeerProp | |||
| 554 | + | readUntil(t, r.x, func() bool { | |||
| 555 | + | if onR != nil && r.x.sel.out[0].state != outFree && r.x.sel.out[0].n < chunk && !r.x.sel.out[0].eof { | |||
| 556 | + | onR() | |||
| 557 | + | } | |||
| 558 | + | r.x.flush() | |||
| 559 | + | for _, q := range r.p.requests()[before:] { | |||
| 560 | + | if q.major == opChangeProperty && q.u32(4) == reqWin { | |||
| 561 | + | return true | |||
| 562 | + | } | |||
| 563 | + | } | |||
| 564 | + | return false | |||
| 565 | + | }) | |||
| 566 | + | cp, _ = r.p.prop(reqWin, "_NEFERWL_DATA") | |||
| 567 | + | chunks++ | |||
| 568 | + | if len(cp.data) == 0 { | |||
| 569 | + | break | |||
| 570 | + | } | |||
| 571 | + | require.Equal(t, r.utf8, cp.typ) | |||
| 572 | + | got = append(got, cp.data...) | |||
| 573 | + | } | |||
| 574 | + | require.True(t, bytes.Equal(data, got), "byte-exact: %d of %d", len(got), total) | |||
| 575 | + | require.Equal(t, total/chunk+2, chunks, "16 full chunks, the tail, then the empty terminator") | |||
| 576 | + | readUntil(t, r.x, func() bool { return r.x.sel.out[0].state == outFree }) | |||
| 577 | + | // The mask is removed at the end. | |||
| 578 | + | reqs := r.p.requests() | |||
| 579 | + | last := reqs[len(reqs)-1] | |||
| 580 | + | for i := len(reqs) - 1; i >= 0; i-- { | |||
| 581 | + | if reqs[i].major == opChangeWindowAttributes && reqs[i].u32(4) == reqWin { | |||
| 582 | + | last = reqs[i] | |||
| 583 | + | break | |||
| 584 | + | } | |||
| 585 | + | } | |||
| 586 | + | require.Zero(t, last.u32(12), "event mask dropped") | |||
| 587 | + | require.Equal(t, nXfer, r.x.sel.pool.n) | |||
| 588 | + | } | |||
| 589 | + | ||||
| 590 | + | func TestSelectionWaylandToXTimeoutAndClear(t *testing.T) { | |||
| 591 | + | r := ownedByWayland(t, "text/plain;charset=utf-8") | |||
| 592 | + | src := newMockfdSource(t) | |||
| 593 | + | src.EXPECT().Remove().Return().Maybe() | |||
| 594 | + | r.x.addFD = func(_ int, _, _ func()) (fdSource, error) { return src, nil } | |||
| 595 | + | w := r.serve(t, r.utf8) | |||
| 596 | + | _, err := w.Write(pattern(chunk)) // a full chunk: INCR | |||
| 597 | + | require.NoError(t, err) | |||
| 598 | + | readUntil(t, r.x, func() bool { return r.x.sel.out[0].incr }) | |||
| 599 | + | require.Equal(t, r.data, sentNotify(t, r)) | |||
| 600 | + | require.NotNil(t, r.timer) | |||
| 601 | + | for i := 0; i < xIdleMax; i++ { | |||
| 602 | + | require.Equal(t, outActive, r.x.sel.out[0].state, i) | |||
| 603 | + | r.timer() | |||
| 604 | + | } | |||
| 605 | + | require.Equal(t, outFree, r.x.sel.out[0].state, "no Deleted for 10 s aborts") | |||
| 606 | + | pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == opDeleteProperty && q.u32(4) == reqWin }) | |||
| 607 | + | ||||
| 608 | + | // SelectionClear aborts a running transfer and refuses further requests. | |||
| 609 | + | w2 := r.serve(t, r.utf8) | |||
| 610 | + | _, err = w2.Write(pattern(chunk)) | |||
| 611 | + | require.NoError(t, err) | |||
| 612 | + | readUntil(t, r.x, func() bool { return r.x.sel.out[0].state == outActive }) | |||
| 613 | + | r.p.selectionClear(r.x.selWin, r.clip) | |||
| 614 | + | readUntil(t, r.x, func() bool { return r.x.sel.out[0].state == outFree }) | |||
| 615 | + | r.p.selectionRequest(r.x.selWin, reqWin, r.clip, r.utf8, r.data) | |||
| 616 | + | require.Equal(t, r.data, sentNotify(t, r), "INCR announcement of the aborted transfer") | |||
| 617 | + | require.Zero(t, sentNotify(t, r), "request after the clear is refused") | |||
| 618 | + | } | |||
| 619 | + | ||||
| 620 | + | // Replacing the Wayland selection mid-transfer aborts it; nil releases X ownership. | |||
| 621 | + | func TestSelectionClearAndRace(t *testing.T) { | |||
| 622 | + | r := ownedByWayland(t, "text/plain;charset=utf-8") | |||
| 623 | + | src := newMockfdSource(t) | |||
| 624 | + | src.EXPECT().Remove().Return().Maybe() | |||
| 625 | + | r.x.addFD = func(_ int, _, _ func()) (fdSource, error) { return src, nil } | |||
| 626 | + | w := r.serve(t, r.utf8) | |||
| 627 | + | _, err := w.Write(pattern(chunk)) | |||
| 628 | + | require.NoError(t, err) | |||
| 629 | + | readUntil(t, r.x, func() bool { return r.x.sel.out[0].incr }) | |||
| 630 | + | r.x.WaylandSelection(SelClipboard, []string{"text/plain"}) | |||
| 631 | + | require.Equal(t, outFree, r.x.sel.out[0].state) | |||
| 632 | + | r.x.WaylandSelection(SelClipboard, nil) // protected or cleared | |||
| 633 | + | r.x.flush() | |||
| 634 | + | req, _ := pumpReq(t, r.x, r.p, r.skip, func(q xpeerReq) bool { return q.major == opSetSelectionOwner && q.u32(4) == 0 }) | |||
| 635 | + | require.Equal(t, r.clip, req.u32(8), "ownership released") | |||
| 636 | + | require.False(t, r.x.sel.st[SelClipboard].ours) | |||
| 637 | + | } | |||
| 638 | + | ||||
| 639 | + | func TestSelectionCopyAllocationsWaylandToX(t *testing.T) { | |||
| 640 | + | r := ownedByWayland(t, "text/plain;charset=utf-8") | |||
| 641 | + | x := r.x | |||
| 642 | + | sinkPeer(t, x, r.p) | |||
| 643 | + | x.after = func(time.Duration, func()) func() { return func() {} } | |||
| 644 | + | x.sel.timerOn = true | |||
| 645 | + | data := pattern(4096) | |||
| 646 | + | var ev xturbo.Event | |||
| 647 | + | ev.Kind = xproto.KindSelectionRequest | |||
| 648 | + | binary.LittleEndian.PutUint32(ev.Raw[8:], x.selWin) | |||
| 649 | + | binary.LittleEndian.PutUint32(ev.Raw[12:], reqWin) | |||
| 650 | + | binary.LittleEndian.PutUint32(ev.Raw[16:], r.clip) | |||
| 651 | + | binary.LittleEndian.PutUint32(ev.Raw[20:], r.utf8) | |||
| 652 | + | binary.LittleEndian.PutUint32(ev.Raw[24:], r.data) | |||
| 653 | + | fds := make([]int, 2) | |||
| 654 | + | round := func() { | |||
| 655 | + | if unix.Pipe2(fds, unix.O_CLOEXEC|unix.O_NONBLOCK) != nil { | |||
| 656 | + | t.Fatal("pipe") | |||
| 657 | + | } | |||
| 658 | + | if _, err := unix.Write(fds[1], data); err != nil { | |||
| 659 | + | t.Fatal("write") | |||
| 660 | + | } | |||
| 661 | + | unix.Close(fds[1]) | |||
| 662 | + | x.host.(*mockhost).ExpectedCalls = nil | |||
| 663 | + | x.onSelectionRequest(&ev) | |||
| 664 | + | x.posted = false | |||
| 665 | + | x.flush() | |||
| 666 | + | } | |||
| 667 | + | _ = round | |||
| 668 | + | // The host mock's matching is not allocation free, so the host call is replaced. | |||
| 669 | + | x.sel.st[SelClipboard].wlMime = [len(textMimes)]string{"text/plain;charset=utf-8"} | |||
| 670 | + | allocs := testing.AllocsPerRun(100, func() { | |||
| 671 | + | if unix.Pipe2(fds, unix.O_CLOEXEC|unix.O_NONBLOCK) != nil { | |||
| 672 | + | t.Fatal("pipe") | |||
| 673 | + | } | |||
| 674 | + | if _, err := unix.Write(fds[1], data); err != nil { | |||
| 675 | + | t.Fatal("write") | |||
| 676 | + | } | |||
| 677 | + | unix.Close(fds[1]) | |||
| 678 | + | x.startOut(0, SelClipboard, reqWin, r.data, r.utf8, 0, fds[0]) | |||
| 679 | + | x.posted = false | |||
| 680 | + | x.flush() | |||
| 681 | + | }) | |||
| 682 | + | // Pipe2 itself does not allocate; the measured path is read + ChangeProperty + SendEvent. | |||
| 683 | + | require.Zero(t, allocs) | |||
| 684 | + | require.Equal(t, outFree, x.sel.out[0].state) | |||
| 685 | + | } | |||
A internal/adapters/wayland/xwm_selection_mock_test.go +143 -0
| @@ -0,0 +1,143 @@ | |||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |
| 2 | + | // github.com/vektra/mockery | |
| 3 | + | // template: testify | |
| 4 | + | ||
| 5 | + | package wayland | |
| 6 | + | ||
| 7 | + | import ( | |
| 8 | + | mock "github.com/stretchr/testify/mock" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // newMockxwmSelection creates a new instance of mockxwmSelection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |
| 12 | + | // The first argument is typically a *testing.T value. | |
| 13 | + | func newMockxwmSelection(t interface { | |
| 14 | + | mock.TestingT | |
| 15 | + | Cleanup(func()) | |
| 16 | + | }) *mockxwmSelection { | |
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 18 | + | helper.Helper() | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | mock := &mockxwmSelection{} | |
| 22 | + | mock.Mock.Test(t) | |
| 23 | + | ||
| 24 | + | t.Cleanup(func() { | |
| 25 | + | if helper, ok := t.(interface{ Helper() }); ok { | |
| 26 | + | helper.Helper() | |
| 27 | + | } | |
| 28 | + | mock.AssertExpectations(t) | |
| 29 | + | }) | |
| 30 | + | ||
| 31 | + | return mock | |
| 32 | + | } | |
| 33 | + | ||
| 34 | + | // mockxwmSelection is an autogenerated mock type for the xwmSelection type | |
| 35 | + | type mockxwmSelection struct { | |
| 36 | + | mock.Mock | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | type mockxwmSelection_Expecter struct { | |
| 40 | + | mock *mock.Mock | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func (_m *mockxwmSelection) EXPECT() *mockxwmSelection_Expecter { | |
| 44 | + | return &mockxwmSelection_Expecter{mock: &_m.Mock} | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // SelectionReceive provides a mock function for the type mockxwmSelection | |
| 48 | + | func (_mock *mockxwmSelection) SelectionReceive(kind int, mime string, fd int) { | |
| 49 | + | _mock.Called(kind, mime, fd) | |
| 50 | + | return | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // mockxwmSelection_SelectionReceive_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectionReceive' | |
| 54 | + | type mockxwmSelection_SelectionReceive_Call struct { | |
| 55 | + | *mock.Call | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | // SelectionReceive is a helper method to define mock.On call | |
| 59 | + | // - kind int | |
| 60 | + | // - mime string | |
| 61 | + | // - fd int | |
| 62 | + | func (_e *mockxwmSelection_Expecter) SelectionReceive(kind any, mime any, fd any) *mockxwmSelection_SelectionReceive_Call { | |
| 63 | + | return &mockxwmSelection_SelectionReceive_Call{Call: _e.mock.On("SelectionReceive", kind, mime, fd)} | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | func (_c *mockxwmSelection_SelectionReceive_Call) Run(run func(kind int, mime string, fd int)) *mockxwmSelection_SelectionReceive_Call { | |
| 67 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 68 | + | var arg0 int | |
| 69 | + | if args[0] != nil { | |
| 70 | + | arg0 = args[0].(int) | |
| 71 | + | } | |
| 72 | + | var arg1 string | |
| 73 | + | if args[1] != nil { | |
| 74 | + | arg1 = args[1].(string) | |
| 75 | + | } | |
| 76 | + | var arg2 int | |
| 77 | + | if args[2] != nil { | |
| 78 | + | arg2 = args[2].(int) | |
| 79 | + | } | |
| 80 | + | run( | |
| 81 | + | arg0, | |
| 82 | + | arg1, | |
| 83 | + | arg2, | |
| 84 | + | ) | |
| 85 | + | }) | |
| 86 | + | return _c | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | func (_c *mockxwmSelection_SelectionReceive_Call) Return() *mockxwmSelection_SelectionReceive_Call { | |
| 90 | + | _c.Call.Return() | |
| 91 | + | return _c | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | func (_c *mockxwmSelection_SelectionReceive_Call) RunAndReturn(run func(kind int, mime string, fd int)) *mockxwmSelection_SelectionReceive_Call { | |
| 95 | + | _c.Run(run) | |
| 96 | + | return _c | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // WaylandSelection provides a mock function for the type mockxwmSelection | |
| 100 | + | func (_mock *mockxwmSelection) WaylandSelection(kind int, mimes []string) { | |
| 101 | + | _mock.Called(kind, mimes) | |
| 102 | + | return | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | // mockxwmSelection_WaylandSelection_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WaylandSelection' | |
| 106 | + | type mockxwmSelection_WaylandSelection_Call struct { | |
| 107 | + | *mock.Call | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | // WaylandSelection is a helper method to define mock.On call | |
| 111 | + | // - kind int | |
| 112 | + | // - mimes []string | |
| 113 | + | func (_e *mockxwmSelection_Expecter) WaylandSelection(kind any, mimes any) *mockxwmSelection_WaylandSelection_Call { | |
| 114 | + | return &mockxwmSelection_WaylandSelection_Call{Call: _e.mock.On("WaylandSelection", kind, mimes)} | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | func (_c *mockxwmSelection_WaylandSelection_Call) Run(run func(kind int, mimes []string)) *mockxwmSelection_WaylandSelection_Call { | |
| 118 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 119 | + | var arg0 int | |
| 120 | + | if args[0] != nil { | |
| 121 | + | arg0 = args[0].(int) | |
| 122 | + | } | |
| 123 | + | var arg1 []string | |
| 124 | + | if args[1] != nil { | |
| 125 | + | arg1 = args[1].([]string) | |
| 126 | + | } | |
| 127 | + | run( | |
| 128 | + | arg0, | |
| 129 | + | arg1, | |
| 130 | + | ) | |
| 131 | + | }) | |
| 132 | + | return _c | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | func (_c *mockxwmSelection_WaylandSelection_Call) Return() *mockxwmSelection_WaylandSelection_Call { | |
| 136 | + | _c.Call.Return() | |
| 137 | + | return _c | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | func (_c *mockxwmSelection_WaylandSelection_Call) RunAndReturn(run func(kind int, mimes []string)) *mockxwmSelection_WaylandSelection_Call { | |
| 141 | + | _c.Run(run) | |
| 142 | + | return _c | |
| 143 | + | } | |
| @@ -0,0 +1,143 @@ | |||||
| 1 | + | // Code generated by mockery; DO NOT EDIT. | |||
| 2 | + | // github.com/vektra/mockery | |||
| 3 | + | // template: testify | |||
| 4 | + | ||||
| 5 | + | package wayland | |||
| 6 | + | ||||
| 7 | + | import ( | |||
| 8 | + | mock "github.com/stretchr/testify/mock" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // newMockxwmSelection creates a new instance of mockxwmSelection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. | |||
| 12 | + | // The first argument is typically a *testing.T value. | |||
| 13 | + | func newMockxwmSelection(t interface { | |||
| 14 | + | mock.TestingT | |||
| 15 | + | Cleanup(func()) | |||
| 16 | + | }) *mockxwmSelection { | |||
| 17 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 18 | + | helper.Helper() | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | mock := &mockxwmSelection{} | |||
| 22 | + | mock.Mock.Test(t) | |||
| 23 | + | ||||
| 24 | + | t.Cleanup(func() { | |||
| 25 | + | if helper, ok := t.(interface{ Helper() }); ok { | |||
| 26 | + | helper.Helper() | |||
| 27 | + | } | |||
| 28 | + | mock.AssertExpectations(t) | |||
| 29 | + | }) | |||
| 30 | + | ||||
| 31 | + | return mock | |||
| 32 | + | } | |||
| 33 | + | ||||
| 34 | + | // mockxwmSelection is an autogenerated mock type for the xwmSelection type | |||
| 35 | + | type mockxwmSelection struct { | |||
| 36 | + | mock.Mock | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | type mockxwmSelection_Expecter struct { | |||
| 40 | + | mock *mock.Mock | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func (_m *mockxwmSelection) EXPECT() *mockxwmSelection_Expecter { | |||
| 44 | + | return &mockxwmSelection_Expecter{mock: &_m.Mock} | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // SelectionReceive provides a mock function for the type mockxwmSelection | |||
| 48 | + | func (_mock *mockxwmSelection) SelectionReceive(kind int, mime string, fd int) { | |||
| 49 | + | _mock.Called(kind, mime, fd) | |||
| 50 | + | return | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // mockxwmSelection_SelectionReceive_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectionReceive' | |||
| 54 | + | type mockxwmSelection_SelectionReceive_Call struct { | |||
| 55 | + | *mock.Call | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | // SelectionReceive is a helper method to define mock.On call | |||
| 59 | + | // - kind int | |||
| 60 | + | // - mime string | |||
| 61 | + | // - fd int | |||
| 62 | + | func (_e *mockxwmSelection_Expecter) SelectionReceive(kind any, mime any, fd any) *mockxwmSelection_SelectionReceive_Call { | |||
| 63 | + | return &mockxwmSelection_SelectionReceive_Call{Call: _e.mock.On("SelectionReceive", kind, mime, fd)} | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | func (_c *mockxwmSelection_SelectionReceive_Call) Run(run func(kind int, mime string, fd int)) *mockxwmSelection_SelectionReceive_Call { | |||
| 67 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 68 | + | var arg0 int | |||
| 69 | + | if args[0] != nil { | |||
| 70 | + | arg0 = args[0].(int) | |||
| 71 | + | } | |||
| 72 | + | var arg1 string | |||
| 73 | + | if args[1] != nil { | |||
| 74 | + | arg1 = args[1].(string) | |||
| 75 | + | } | |||
| 76 | + | var arg2 int | |||
| 77 | + | if args[2] != nil { | |||
| 78 | + | arg2 = args[2].(int) | |||
| 79 | + | } | |||
| 80 | + | run( | |||
| 81 | + | arg0, | |||
| 82 | + | arg1, | |||
| 83 | + | arg2, | |||
| 84 | + | ) | |||
| 85 | + | }) | |||
| 86 | + | return _c | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | func (_c *mockxwmSelection_SelectionReceive_Call) Return() *mockxwmSelection_SelectionReceive_Call { | |||
| 90 | + | _c.Call.Return() | |||
| 91 | + | return _c | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | func (_c *mockxwmSelection_SelectionReceive_Call) RunAndReturn(run func(kind int, mime string, fd int)) *mockxwmSelection_SelectionReceive_Call { | |||
| 95 | + | _c.Run(run) | |||
| 96 | + | return _c | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // WaylandSelection provides a mock function for the type mockxwmSelection | |||
| 100 | + | func (_mock *mockxwmSelection) WaylandSelection(kind int, mimes []string) { | |||
| 101 | + | _mock.Called(kind, mimes) | |||
| 102 | + | return | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | // mockxwmSelection_WaylandSelection_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WaylandSelection' | |||
| 106 | + | type mockxwmSelection_WaylandSelection_Call struct { | |||
| 107 | + | *mock.Call | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | // WaylandSelection is a helper method to define mock.On call | |||
| 111 | + | // - kind int | |||
| 112 | + | // - mimes []string | |||
| 113 | + | func (_e *mockxwmSelection_Expecter) WaylandSelection(kind any, mimes any) *mockxwmSelection_WaylandSelection_Call { | |||
| 114 | + | return &mockxwmSelection_WaylandSelection_Call{Call: _e.mock.On("WaylandSelection", kind, mimes)} | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | func (_c *mockxwmSelection_WaylandSelection_Call) Run(run func(kind int, mimes []string)) *mockxwmSelection_WaylandSelection_Call { | |||
| 118 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 119 | + | var arg0 int | |||
| 120 | + | if args[0] != nil { | |||
| 121 | + | arg0 = args[0].(int) | |||
| 122 | + | } | |||
| 123 | + | var arg1 []string | |||
| 124 | + | if args[1] != nil { | |||
| 125 | + | arg1 = args[1].([]string) | |||
| 126 | + | } | |||
| 127 | + | run( | |||
| 128 | + | arg0, | |||
| 129 | + | arg1, | |||
| 130 | + | ) | |||
| 131 | + | }) | |||
| 132 | + | return _c | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | func (_c *mockxwmSelection_WaylandSelection_Call) Return() *mockxwmSelection_WaylandSelection_Call { | |||
| 136 | + | _c.Call.Return() | |||
| 137 | + | return _c | |||
| 138 | + | } | |||
| 139 | + | ||||
| 140 | + | func (_c *mockxwmSelection_WaylandSelection_Call) RunAndReturn(run func(kind int, mimes []string)) *mockxwmSelection_WaylandSelection_Call { | |||
| 141 | + | _c.Run(run) | |||
| 142 | + | return _c | |||
| 143 | + | } | |||