detached 3115564

test(wayland): add an in-process libwayland test compositor

Speaks wl_output, xdg-output, wl_shm, ext-image-capture-source,
ext-image-copy-capture, ext-workspace and NeferWL's capture extension over a
real socket, with hold, failure, resize, transform and hotplug controls. The
extension's server bindings are generated from its XML (gen.sh).
NEFERCAP_REQUIRE_WAYLAND=1 turns a missing libwayland-server into a failure.
commit
311556438c14176b42c09987e4e71549d2ce6852
author
brice · · 2026-09-30 19:56 UTC
committer
brice · · 2026-10-01 02:17 UTC
parent
709e2cd
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
11 files · +1745 -0
M go.mod +1 -0
@@ -4,6 +4,7 @@ go 1.27
44
55 require (
66 github.com/bnema/nefergui v0.2.0
7+ github.com/bnema/purego-libwayland v0.7.0
78 github.com/bnema/wlturbo v0.3.0
89 github.com/bnema/zerowrap v1.4.1
910 github.com/stretchr/testify v1.12.1
@@ -4,6 +4,7 @@ go 1.27
4 4
5 require (5 require (
6 github.com/bnema/nefergui v0.2.06 github.com/bnema/nefergui v0.2.0
7+ github.com/bnema/purego-libwayland v0.7.0
7 github.com/bnema/wlturbo v0.3.08 github.com/bnema/wlturbo v0.3.0
8 github.com/bnema/zerowrap v1.4.19 github.com/bnema/zerowrap v1.4.1
9 github.com/stretchr/testify v1.12.110 github.com/stretchr/testify v1.12.1
M go.sum +2 -0
@@ -2,6 +2,8 @@ github.com/bnema/nefergui v0.2.0 h1:+kuePc+HyZFmlm0C0JkFq7TfL/Et4ZQjNlGw7lgjeXc=
22 github.com/bnema/nefergui v0.2.0/go.mod h1:JNjwv/YlesI0FflJvmHB5OqLd/bHfqFakpvJ2zJr+F4=
33 github.com/bnema/purego v0.13.0-bnema.1 h1:fKRdztnvTmRqAjANMoN+utEIH73dvPqVPJh2iZCCAO8=
44 github.com/bnema/purego v0.13.0-bnema.1/go.mod h1:xS1iX3noypxnbOUEUAqOvkfnUzgHuu19VtL4+dfUMJM=
5+github.com/bnema/purego-libwayland v0.7.0 h1:Q25nd/KmcivEucUceEmmNt1EgiryugAXnI4c7OV9tnU=
6+github.com/bnema/purego-libwayland v0.7.0/go.mod h1:+2zIEBX+iNlRB27xrm5Z8Dnd+cmqX+1AYvuwguxK2dg=
57 github.com/bnema/purego-vulkan v0.6.0 h1:SJgu0smuDC2vtBv0dJ6dmRKewJW5EUXQwTXyb/p9JSE=
68 github.com/bnema/purego-vulkan v0.6.0/go.mod h1:7kFoR1p01JbknuZlfA20oeImryBe8Fx+Zd9hm2uSUM4=
79 github.com/bnema/purego-xkbcommon v0.0.0-20260928071113-e79265dfcdc2 h1:XePqfzZzlT1WEQ8G9atwlXjW5jFZAkEoUGnlYvsHAHg=
@@ -2,6 +2,8 @@ github.com/bnema/nefergui v0.2.0 h1:+kuePc+HyZFmlm0C0JkFq7TfL/Et4ZQjNlGw7lgjeXc=
2 github.com/bnema/nefergui v0.2.0/go.mod h1:JNjwv/YlesI0FflJvmHB5OqLd/bHfqFakpvJ2zJr+F4=2 github.com/bnema/nefergui v0.2.0/go.mod h1:JNjwv/YlesI0FflJvmHB5OqLd/bHfqFakpvJ2zJr+F4=
3 github.com/bnema/purego v0.13.0-bnema.1 h1:fKRdztnvTmRqAjANMoN+utEIH73dvPqVPJh2iZCCAO8=3 github.com/bnema/purego v0.13.0-bnema.1 h1:fKRdztnvTmRqAjANMoN+utEIH73dvPqVPJh2iZCCAO8=
4 github.com/bnema/purego v0.13.0-bnema.1/go.mod h1:xS1iX3noypxnbOUEUAqOvkfnUzgHuu19VtL4+dfUMJM=4 github.com/bnema/purego v0.13.0-bnema.1/go.mod h1:xS1iX3noypxnbOUEUAqOvkfnUzgHuu19VtL4+dfUMJM=
5+github.com/bnema/purego-libwayland v0.7.0 h1:Q25nd/KmcivEucUceEmmNt1EgiryugAXnI4c7OV9tnU=
6+github.com/bnema/purego-libwayland v0.7.0/go.mod h1:+2zIEBX+iNlRB27xrm5Z8Dnd+cmqX+1AYvuwguxK2dg=
5 github.com/bnema/purego-vulkan v0.6.0 h1:SJgu0smuDC2vtBv0dJ6dmRKewJW5EUXQwTXyb/p9JSE=7 github.com/bnema/purego-vulkan v0.6.0 h1:SJgu0smuDC2vtBv0dJ6dmRKewJW5EUXQwTXyb/p9JSE=
6 github.com/bnema/purego-vulkan v0.6.0/go.mod h1:7kFoR1p01JbknuZlfA20oeImryBe8Fx+Zd9hm2uSUM4=8 github.com/bnema/purego-vulkan v0.6.0/go.mod h1:7kFoR1p01JbknuZlfA20oeImryBe8Fx+Zd9hm2uSUM4=
7 github.com/bnema/purego-xkbcommon v0.0.0-20260928071113-e79265dfcdc2 h1:XePqfzZzlT1WEQ8G9atwlXjW5jFZAkEoUGnlYvsHAHg=9 github.com/bnema/purego-xkbcommon v0.0.0-20260928071113-e79265dfcdc2 h1:XePqfzZzlT1WEQ8G9atwlXjW5jFZAkEoUGnlYvsHAHg=
A internal/adapters/wayland/testserver/capture.go +254 -0
@@ -0,0 +1,254 @@
1+package testserver
2+
3+import (
4+ "github.com/bnema/purego-libwayland/protocol/extimagecapturesource"
5+ "github.com/bnema/purego-libwayland/protocol/extimagecopycapture"
6+ "github.com/bnema/purego-libwayland/protocol/wayland"
7+)
8+
9+// Pixel is the content the server paints: a function of the output pixel and
10+// the frame sequence, so tests can tell which part of which frame they read.
11+func Pixel(x, y int, seq uint32) [4]byte { return [4]byte{byte(x), byte(y), byte(seq), 0xff} }
12+
13+type copyManager struct{ s *Server }
14+
15+func (m copyManager) CreateSession(self *extimagecopycapture.ExtImageCopyCaptureManagerV1, id uint32, src *extimagecapturesource.ExtImageCaptureSourceV1, options uint32) {
16+ s := m.s
17+ c := &session{s: s, src: s.sources[src.Resource]}
18+ if c.src == nil {
19+ self.PostError(1, "unknown source")
20+ return
21+ }
22+ res, err := extimagecopycapture.NewExtImageCopyCaptureSessionV1(self.Client(), 1, id, sessionHandler{c})
23+ if err != nil {
24+ return
25+ }
26+ c.res = res
27+ c.out = c.src.out
28+ if c.src.ws != nil {
29+ c.out = s.outputNamed(c.src.ws.spec.Output)
30+ }
31+ s.sessions = append(s.sessions, c)
32+ s.stats.Sessions++
33+ s.stats.LastSource = c.src.kind
34+ s.stats.LastRegion = c.src.region
35+ res.OnDestroy = func() {
36+ c.dead = true
37+ s.stats.DestroyedSessions++
38+ }
39+ if s.cfg.StopAtCreate || c.src.gone || c.out == nil || c.out.removed {
40+ c.stop()
41+ return
42+ }
43+ c.sendConstraints()
44+}
45+func (copyManager) CreatePointerCursorSession(self *extimagecopycapture.ExtImageCopyCaptureManagerV1, _ uint32, _ *extimagecapturesource.ExtImageCaptureSourceV1, _ *wayland.Pointer) {
46+ self.PostError(1, "cursor sessions are not supported")
47+}
48+func (copyManager) Destroy(*extimagecopycapture.ExtImageCopyCaptureManagerV1) {}
49+
50+func (s *Server) outputNamed(name string) *output {
51+ for _, o := range s.outputs {
52+ if o.spec.Name == name {
53+ return o
54+ }
55+ }
56+ return nil
57+}
58+
59+type session struct {
60+ s *Server
61+ res *extimagecopycapture.ExtImageCopyCaptureSessionV1
62+ src *source
63+ out *output
64+ frame *frame // the live frame, nil when none
65+ w, h uint32 // the latest announced buffer size
66+ dead bool
67+ stopped bool
68+ answered int // frames completed with ready
69+}
70+
71+// size is the buffer size of the session's source.
72+func (c *session) size() (w, h uint32) {
73+ scale := uint32(max(c.out.spec.Scale, 1))
74+ if c.src.kind == "region" {
75+ return uint32(c.src.region[2]) * scale, uint32(c.src.region[3]) * scale
76+ }
77+ return c.out.spec.Width, c.out.spec.Height
78+}
79+
80+func (c *session) sendConstraints() {
81+ c.w, c.h = c.size()
82+ c.res.SendBufferSize(c.w, c.h)
83+ if !c.s.cfg.NoShmFormat {
84+ for _, f := range c.s.cfg.Formats {
85+ c.res.SendShmFormat(f)
86+ }
87+ }
88+ c.res.SendDone()
89+}
90+
91+func (c *session) stop() {
92+ if c.stopped || c.dead {
93+ return
94+ }
95+ c.stopped = true
96+ c.res.SendStopped()
97+}
98+
99+type sessionHandler struct{ c *session }
100+
101+func (h sessionHandler) CreateFrame(self *extimagecopycapture.ExtImageCopyCaptureSessionV1, id uint32) {
102+ c := h.c
103+ if c.frame != nil {
104+ self.PostError(uint32(extimagecopycapture.ExtImageCopyCaptureSessionV1ErrorDuplicateFrame), "create_frame before destroying the previous frame")
105+ return
106+ }
107+ f := &frame{c: c}
108+ res, err := extimagecopycapture.NewExtImageCopyCaptureFrameV1(self.Client(), 1, id, f)
109+ if err != nil {
110+ return
111+ }
112+ f.res = res
113+ c.frame = f
114+ c.s.stats.Frames++
115+ res.OnDestroy = func() {
116+ if c.frame == f {
117+ c.frame = nil
118+ }
119+ f.gone = true
120+ }
121+}
122+func (sessionHandler) Destroy(*extimagecopycapture.ExtImageCopyCaptureSessionV1) {}
123+
124+type frame struct {
125+ c *session
126+ res *extimagecopycapture.ExtImageCopyCaptureFrameV1
127+ buf *buffer
128+ captured bool
129+ gone bool
130+}
131+
132+func (f *frame) Destroy(*extimagecopycapture.ExtImageCopyCaptureFrameV1) {}
133+func (f *frame) AttachBuffer(self *extimagecopycapture.ExtImageCopyCaptureFrameV1, b *wayland.Buffer) {
134+ f.buf = f.c.s.bufs[b.Resource]
135+}
136+func (f *frame) DamageBuffer(*extimagecopycapture.ExtImageCopyCaptureFrameV1, int32, int32, int32, int32) {
137+}
138+
139+func (f *frame) Capture(self *extimagecopycapture.ExtImageCopyCaptureFrameV1) {
140+ s := f.c.s
141+ if f.buf == nil {
142+ self.PostError(uint32(extimagecopycapture.ExtImageCopyCaptureFrameV1ErrorNoBuffer), "capture without buffer")
143+ return
144+ }
145+ if f.captured {
146+ self.PostError(uint32(extimagecopycapture.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured), "captured twice")
147+ return
148+ }
149+ f.captured = true
150+ s.stats.Captures++
151+ if s.cfg.Hold || (s.cfg.HoldAfterFirst && f.c.answered > 0) {
152+ s.held = append(s.held, f)
153+ return
154+ }
155+ f.complete()
156+}
157+
158+// complete answers a captured frame: failed, or pixels and ready.
159+func (f *frame) complete() {
160+ c, s := f.c, f.c.s
161+ switch {
162+ case f.gone || c.dead:
163+ return
164+ case c.stopped:
165+ f.res.SendFailed(uint32(extimagecopycapture.ExtImageCopyCaptureFrameV1FailureReasonStopped))
166+ return
167+ case s.cfg.FailFrames > 0:
168+ s.cfg.FailFrames--
169+ f.res.SendFailed(s.cfg.FailReason)
170+ return
171+ case f.buf.width != c.w || f.buf.height != c.h || !f.formatOK():
172+ f.res.SendFailed(uint32(extimagecopycapture.ExtImageCopyCaptureFrameV1FailureReasonBufferConstraints))
173+ return
174+ }
175+ s.seq++
176+ data := f.buf.bytes()
177+ if data == nil {
178+ f.res.SendFailed(uint32(extimagecopycapture.ExtImageCopyCaptureFrameV1FailureReasonUnknown))
179+ return
180+ }
181+ ox, oy, scale := 0, 0, int(max(c.out.spec.Scale, 1))
182+ if c.src.kind == "region" {
183+ ox, oy = int(c.src.region[0])*scale, int(c.src.region[1])*scale
184+ }
185+ for y := 0; y < int(f.buf.height); y++ {
186+ row := data[y*int(f.buf.stride):]
187+ for x := 0; x < int(f.buf.width); x++ {
188+ p := Pixel(ox+x, oy+y, s.seq)
189+ copy(row[x*4:], p[:])
190+ }
191+ }
192+ s.stats.Ready++
193+ c.answered++
194+ f.res.SendTransform(s.cfg.Transform)
195+ f.res.SendDamage(0, 0, int32(f.buf.width), int32(f.buf.height))
196+ f.res.SendReady()
197+}
198+
199+func (f *frame) formatOK() bool {
200+ for _, x := range f.c.s.cfg.Formats {
201+ if x == f.buf.format {
202+ return true
203+ }
204+ }
205+ return false
206+}
207+
208+// Release completes every held frame.
209+func (s *Server) Release() {
210+ s.Do(func() {
211+ held := s.held
212+ s.held = nil
213+ for _, f := range held {
214+ f.complete()
215+ }
216+ })
217+}
218+
219+// Held reports how many captured frames wait for Release.
220+func (s *Server) Held() int {
221+ n := 0
222+ s.Do(func() { n = len(s.held) })
223+ return n
224+}
225+
226+// Resize changes an output's mode and sends new constraints to its sessions.
227+func (s *Server) Resize(name string, w, h uint32) {
228+ s.Do(func() {
229+ o := s.outputNamed(name)
230+ if o == nil {
231+ return
232+ }
233+ o.spec.Width, o.spec.Height = w, h
234+ for _, c := range s.sessions {
235+ if c.out == o && !c.dead && !c.stopped {
236+ c.sendConstraints()
237+ }
238+ }
239+ })
240+}
241+
242+// StopSessions ends every live session with the stopped event.
243+func (s *Server) StopSessions() {
244+ s.Do(func() {
245+ for _, c := range s.sessions {
246+ c.stop()
247+ }
248+ })
249+}
250+
251+// Fail makes the next n captures answer failed(reason).
252+func (s *Server) Fail(n int, reason uint32) {
253+ s.Do(func() { s.cfg.FailFrames, s.cfg.FailReason = n, reason })
254+}
@@ -0,0 +1,254 @@
1+package testserver
2+
3+import (
4+ "github.com/bnema/purego-libwayland/protocol/extimagecapturesource"
5+ "github.com/bnema/purego-libwayland/protocol/extimagecopycapture"
6+ "github.com/bnema/purego-libwayland/protocol/wayland"
7+)
8+
9+// Pixel is the content the server paints: a function of the output pixel and
10+// the frame sequence, so tests can tell which part of which frame they read.
11+func Pixel(x, y int, seq uint32) [4]byte { return [4]byte{byte(x), byte(y), byte(seq), 0xff} }
12+
13+type copyManager struct{ s *Server }
14+
15+func (m copyManager) CreateSession(self *extimagecopycapture.ExtImageCopyCaptureManagerV1, id uint32, src *extimagecapturesource.ExtImageCaptureSourceV1, options uint32) {
16+ s := m.s
17+ c := &session{s: s, src: s.sources[src.Resource]}
18+ if c.src == nil {
19+ self.PostError(1, "unknown source")
20+ return
21+ }
22+ res, err := extimagecopycapture.NewExtImageCopyCaptureSessionV1(self.Client(), 1, id, sessionHandler{c})
23+ if err != nil {
24+ return
25+ }
26+ c.res = res
27+ c.out = c.src.out
28+ if c.src.ws != nil {
29+ c.out = s.outputNamed(c.src.ws.spec.Output)
30+ }
31+ s.sessions = append(s.sessions, c)
32+ s.stats.Sessions++
33+ s.stats.LastSource = c.src.kind
34+ s.stats.LastRegion = c.src.region
35+ res.OnDestroy = func() {
36+ c.dead = true
37+ s.stats.DestroyedSessions++
38+ }
39+ if s.cfg.StopAtCreate || c.src.gone || c.out == nil || c.out.removed {
40+ c.stop()
41+ return
42+ }
43+ c.sendConstraints()
44+}
45+func (copyManager) CreatePointerCursorSession(self *extimagecopycapture.ExtImageCopyCaptureManagerV1, _ uint32, _ *extimagecapturesource.ExtImageCaptureSourceV1, _ *wayland.Pointer) {
46+ self.PostError(1, "cursor sessions are not supported")
47+}
48+func (copyManager) Destroy(*extimagecopycapture.ExtImageCopyCaptureManagerV1) {}
49+
50+func (s *Server) outputNamed(name string) *output {
51+ for _, o := range s.outputs {
52+ if o.spec.Name == name {
53+ return o
54+ }
55+ }
56+ return nil
57+}
58+
59+type session struct {
60+ s *Server
61+ res *extimagecopycapture.ExtImageCopyCaptureSessionV1
62+ src *source
63+ out *output
64+ frame *frame // the live frame, nil when none
65+ w, h uint32 // the latest announced buffer size
66+ dead bool
67+ stopped bool
68+ answered int // frames completed with ready
69+}
70+
71+// size is the buffer size of the session's source.
72+func (c *session) size() (w, h uint32) {
73+ scale := uint32(max(c.out.spec.Scale, 1))
74+ if c.src.kind == "region" {
75+ return uint32(c.src.region[2]) * scale, uint32(c.src.region[3]) * scale
76+ }
77+ return c.out.spec.Width, c.out.spec.Height
78+}
79+
80+func (c *session) sendConstraints() {
81+ c.w, c.h = c.size()
82+ c.res.SendBufferSize(c.w, c.h)
83+ if !c.s.cfg.NoShmFormat {
84+ for _, f := range c.s.cfg.Formats {
85+ c.res.SendShmFormat(f)
86+ }
87+ }
88+ c.res.SendDone()
89+}
90+
91+func (c *session) stop() {
92+ if c.stopped || c.dead {
93+ return
94+ }
95+ c.stopped = true
96+ c.res.SendStopped()
97+}
98+
99+type sessionHandler struct{ c *session }
100+
101+func (h sessionHandler) CreateFrame(self *extimagecopycapture.ExtImageCopyCaptureSessionV1, id uint32) {
102+ c := h.c
103+ if c.frame != nil {
104+ self.PostError(uint32(extimagecopycapture.ExtImageCopyCaptureSessionV1ErrorDuplicateFrame), "create_frame before destroying the previous frame")
105+ return
106+ }
107+ f := &frame{c: c}
108+ res, err := extimagecopycapture.NewExtImageCopyCaptureFrameV1(self.Client(), 1, id, f)
109+ if err != nil {
110+ return
111+ }
112+ f.res = res
113+ c.frame = f
114+ c.s.stats.Frames++
115+ res.OnDestroy = func() {
116+ if c.frame == f {
117+ c.frame = nil
118+ }
119+ f.gone = true
120+ }
121+}
122+func (sessionHandler) Destroy(*extimagecopycapture.ExtImageCopyCaptureSessionV1) {}
123+
124+type frame struct {
125+ c *session
126+ res *extimagecopycapture.ExtImageCopyCaptureFrameV1
127+ buf *buffer
128+ captured bool
129+ gone bool
130+}
131+
132+func (f *frame) Destroy(*extimagecopycapture.ExtImageCopyCaptureFrameV1) {}
133+func (f *frame) AttachBuffer(self *extimagecopycapture.ExtImageCopyCaptureFrameV1, b *wayland.Buffer) {
134+ f.buf = f.c.s.bufs[b.Resource]
135+}
136+func (f *frame) DamageBuffer(*extimagecopycapture.ExtImageCopyCaptureFrameV1, int32, int32, int32, int32) {
137+}
138+
139+func (f *frame) Capture(self *extimagecopycapture.ExtImageCopyCaptureFrameV1) {
140+ s := f.c.s
141+ if f.buf == nil {
142+ self.PostError(uint32(extimagecopycapture.ExtImageCopyCaptureFrameV1ErrorNoBuffer), "capture without buffer")
143+ return
144+ }
145+ if f.captured {
146+ self.PostError(uint32(extimagecopycapture.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured), "captured twice")
147+ return
148+ }
149+ f.captured = true
150+ s.stats.Captures++
151+ if s.cfg.Hold || (s.cfg.HoldAfterFirst && f.c.answered > 0) {
152+ s.held = append(s.held, f)
153+ return
154+ }
155+ f.complete()
156+}
157+
158+// complete answers a captured frame: failed, or pixels and ready.
159+func (f *frame) complete() {
160+ c, s := f.c, f.c.s
161+ switch {
162+ case f.gone || c.dead:
163+ return
164+ case c.stopped:
165+ f.res.SendFailed(uint32(extimagecopycapture.ExtImageCopyCaptureFrameV1FailureReasonStopped))
166+ return
167+ case s.cfg.FailFrames > 0:
168+ s.cfg.FailFrames--
169+ f.res.SendFailed(s.cfg.FailReason)
170+ return
171+ case f.buf.width != c.w || f.buf.height != c.h || !f.formatOK():
172+ f.res.SendFailed(uint32(extimagecopycapture.ExtImageCopyCaptureFrameV1FailureReasonBufferConstraints))
173+ return
174+ }
175+ s.seq++
176+ data := f.buf.bytes()
177+ if data == nil {
178+ f.res.SendFailed(uint32(extimagecopycapture.ExtImageCopyCaptureFrameV1FailureReasonUnknown))
179+ return
180+ }
181+ ox, oy, scale := 0, 0, int(max(c.out.spec.Scale, 1))
182+ if c.src.kind == "region" {
183+ ox, oy = int(c.src.region[0])*scale, int(c.src.region[1])*scale
184+ }
185+ for y := 0; y < int(f.buf.height); y++ {
186+ row := data[y*int(f.buf.stride):]
187+ for x := 0; x < int(f.buf.width); x++ {
188+ p := Pixel(ox+x, oy+y, s.seq)
189+ copy(row[x*4:], p[:])
190+ }
191+ }
192+ s.stats.Ready++
193+ c.answered++
194+ f.res.SendTransform(s.cfg.Transform)
195+ f.res.SendDamage(0, 0, int32(f.buf.width), int32(f.buf.height))
196+ f.res.SendReady()
197+}
198+
199+func (f *frame) formatOK() bool {
200+ for _, x := range f.c.s.cfg.Formats {
201+ if x == f.buf.format {
202+ return true
203+ }
204+ }
205+ return false
206+}
207+
208+// Release completes every held frame.
209+func (s *Server) Release() {
210+ s.Do(func() {
211+ held := s.held
212+ s.held = nil
213+ for _, f := range held {
214+ f.complete()
215+ }
216+ })
217+}
218+
219+// Held reports how many captured frames wait for Release.
220+func (s *Server) Held() int {
221+ n := 0
222+ s.Do(func() { n = len(s.held) })
223+ return n
224+}
225+
226+// Resize changes an output's mode and sends new constraints to its sessions.
227+func (s *Server) Resize(name string, w, h uint32) {
228+ s.Do(func() {
229+ o := s.outputNamed(name)
230+ if o == nil {
231+ return
232+ }
233+ o.spec.Width, o.spec.Height = w, h
234+ for _, c := range s.sessions {
235+ if c.out == o && !c.dead && !c.stopped {
236+ c.sendConstraints()
237+ }
238+ }
239+ })
240+}
241+
242+// StopSessions ends every live session with the stopped event.
243+func (s *Server) StopSessions() {
244+ s.Do(func() {
245+ for _, c := range s.sessions {
246+ c.stop()
247+ }
248+ })
249+}
250+
251+// Fail makes the next n captures answer failed(reason).
252+func (s *Server) Fail(n int, reason uint32) {
253+ s.Do(func() { s.cfg.FailFrames, s.cfg.FailReason = n, reason })
254+}
A internal/adapters/wayland/testserver/exclusion.go +81 -0
@@ -0,0 +1,81 @@
1+package testserver
2+
3+import (
4+ "github.com/bnema/purego-libwayland/protocol/extimagecopycapture"
5+ "github.com/bnema/purego-libwayland/protocol/wayland"
6+
7+ "github.com/bnema/nefercap/internal/adapters/wayland/testserver/neferwl"
8+)
9+
10+type exclusionManager struct{ s *Server }
11+
12+func (exclusionManager) Destroy(*neferwl.NeferwlCaptureExclusionManagerV1) {}
13+
14+func (m exclusionManager) GetExclusion(self *neferwl.NeferwlCaptureExclusionManagerV1, id uint32, sess *extimagecopycapture.ExtImageCopyCaptureSessionV1) {
15+ s := m.s
16+ res, err := neferwl.NewNeferwlCaptureExclusionV1(self.Client(), 1, id, exclusionHandler{s})
17+ if err != nil {
18+ return
19+ }
20+ s.stats.Exclusions++
21+ res.OnDestroy = func() { s.stats.DestroyedExclusions++ }
22+ if s.cfg.ExclusionFail != 0 {
23+ res.SendFailed(s.cfg.ExclusionFail - 1)
24+ return
25+ }
26+ res.SendToken(Token)
27+}
28+
29+func (m exclusionManager) AttachSurface(self *neferwl.NeferwlCaptureExclusionManagerV1, id uint32, token string, _ *wayland.Surface) {
30+ s := m.s
31+ res, err := neferwl.NewNeferwlCaptureLayerV1(self.Client(), 1, id, layerHandler{})
32+ if err != nil {
33+ return
34+ }
35+ s.stats.Attached = append(s.stats.Attached, token)
36+ s.layers = append(s.layers, res.Resource)
37+ layer := res
38+ switch {
39+ case s.cfg.AttachSilent:
40+ case s.cfg.AttachFail != 0:
41+ layer.SendFailed(s.cfg.AttachFail - 1)
42+ default:
43+ layer.SendAttached()
44+ }
45+}
46+
47+type exclusionHandler struct{ s *Server }
48+
49+func (exclusionHandler) Destroy(*neferwl.NeferwlCaptureExclusionV1) {}
50+
51+type layerHandler struct{}
52+
53+func (layerHandler) Destroy(*neferwl.NeferwlCaptureLayerV1) {}
54+
55+// Detach sends detached(reason) on the n-th attached layer.
56+func (s *Server) Detach(n int, reason uint32) {
57+ s.Do(func() {
58+ if n < len(s.layers) {
59+ neferwl.WrapNeferwlCaptureLayerV1(s.layers[n]).SendDetached(reason)
60+ }
61+ })
62+}
63+
64+// SendRaw posts a layer event out of order, to test violations: opcode 0
65+// attached, 1 failed, 2 detached.
66+func (s *Server) SendRaw(n int, opcode uint32, arg uint32) {
67+ s.Do(func() {
68+ if n >= len(s.layers) {
69+ return
70+ }
71+ l := neferwl.WrapNeferwlCaptureLayerV1(s.layers[n])
72+ switch opcode {
73+ case 0:
74+ l.SendAttached()
75+ case 1:
76+ l.SendFailed(arg)
77+ default:
78+ l.SendDetached(arg)
79+ }
80+ })
81+}
@@ -0,0 +1,81 @@
1+package testserver
2+
3+import (
4+ "github.com/bnema/purego-libwayland/protocol/extimagecopycapture"
5+ "github.com/bnema/purego-libwayland/protocol/wayland"
6+
7+ "github.com/bnema/nefercap/internal/adapters/wayland/testserver/neferwl"
8+)
9+
10+type exclusionManager struct{ s *Server }
11+
12+func (exclusionManager) Destroy(*neferwl.NeferwlCaptureExclusionManagerV1) {}
13+
14+func (m exclusionManager) GetExclusion(self *neferwl.NeferwlCaptureExclusionManagerV1, id uint32, sess *extimagecopycapture.ExtImageCopyCaptureSessionV1) {
15+ s := m.s
16+ res, err := neferwl.NewNeferwlCaptureExclusionV1(self.Client(), 1, id, exclusionHandler{s})
17+ if err != nil {
18+ return
19+ }
20+ s.stats.Exclusions++
21+ res.OnDestroy = func() { s.stats.DestroyedExclusions++ }
22+ if s.cfg.ExclusionFail != 0 {
23+ res.SendFailed(s.cfg.ExclusionFail - 1)
24+ return
25+ }
26+ res.SendToken(Token)
27+}
28+
29+func (m exclusionManager) AttachSurface(self *neferwl.NeferwlCaptureExclusionManagerV1, id uint32, token string, _ *wayland.Surface) {
30+ s := m.s
31+ res, err := neferwl.NewNeferwlCaptureLayerV1(self.Client(), 1, id, layerHandler{})
32+ if err != nil {
33+ return
34+ }
35+ s.stats.Attached = append(s.stats.Attached, token)
36+ s.layers = append(s.layers, res.Resource)
37+ layer := res
38+ switch {
39+ case s.cfg.AttachSilent:
40+ case s.cfg.AttachFail != 0:
41+ layer.SendFailed(s.cfg.AttachFail - 1)
42+ default:
43+ layer.SendAttached()
44+ }
45+}
46+
47+type exclusionHandler struct{ s *Server }
48+
49+func (exclusionHandler) Destroy(*neferwl.NeferwlCaptureExclusionV1) {}
50+
51+type layerHandler struct{}
52+
53+func (layerHandler) Destroy(*neferwl.NeferwlCaptureLayerV1) {}
54+
55+// Detach sends detached(reason) on the n-th attached layer.
56+func (s *Server) Detach(n int, reason uint32) {
57+ s.Do(func() {
58+ if n < len(s.layers) {
59+ neferwl.WrapNeferwlCaptureLayerV1(s.layers[n]).SendDetached(reason)
60+ }
61+ })
62+}
63+
64+// SendRaw posts a layer event out of order, to test violations: opcode 0
65+// attached, 1 failed, 2 detached.
66+func (s *Server) SendRaw(n int, opcode uint32, arg uint32) {
67+ s.Do(func() {
68+ if n >= len(s.layers) {
69+ return
70+ }
71+ l := neferwl.WrapNeferwlCaptureLayerV1(s.layers[n])
72+ switch opcode {
73+ case 0:
74+ l.SendAttached()
75+ case 1:
76+ l.SendFailed(arg)
77+ default:
78+ l.SendDetached(arg)
79+ }
80+ })
81+}
A internal/adapters/wayland/testserver/neferwl/gen.sh +14 -0
@@ -0,0 +1,14 @@
1+#!/bin/sh
2+# Regenerates neferwl.go (test-only server bindings of NeferWL's capture
3+# extension) from the vendored XML with the wlgen of the purego-libwayland
4+# module in use. Offline: go.work or the module cache.
5+set -eu
6+cd "$(dirname "$0")"
7+mod=$(go list -m -f '{{.Dir}}' github.com/bnema/purego-libwayland)
8+p=github.com/bnema/purego-libwayland/protocol
9+go run github.com/bnema/purego-libwayland/cmd/wlgen -package neferwl -out neferwl.go \
10+ -import wayland=$p/wayland -import-xml wayland="$mod/protocols/wayland.xml" \
11+ -import extimagecapturesource=$p/extimagecapturesource -import-xml extimagecapturesource="$mod/protocols/ext-image-capture-source-v1.xml" \
12+ -import extimagecopycapture=$p/extimagecopycapture -import-xml extimagecopycapture="$mod/protocols/ext-image-copy-capture-v1.xml" \
13+ -import extworkspace=$p/extworkspace -import-xml extworkspace="$mod/protocols/ext-workspace-v1.xml" \
14+ neferwl-image-capture-v1.xml
@@ -0,0 +1,14 @@
1+#!/bin/sh
2+# Regenerates neferwl.go (test-only server bindings of NeferWL's capture
3+# extension) from the vendored XML with the wlgen of the purego-libwayland
4+# module in use. Offline: go.work or the module cache.
5+set -eu
6+cd "$(dirname "$0")"
7+mod=$(go list -m -f '{{.Dir}}' github.com/bnema/purego-libwayland)
8+p=github.com/bnema/purego-libwayland/protocol
9+go run github.com/bnema/purego-libwayland/cmd/wlgen -package neferwl -out neferwl.go \
10+ -import wayland=$p/wayland -import-xml wayland="$mod/protocols/wayland.xml" \
11+ -import extimagecapturesource=$p/extimagecapturesource -import-xml extimagecapturesource="$mod/protocols/ext-image-capture-source-v1.xml" \
12+ -import extimagecopycapture=$p/extimagecopycapture -import-xml extimagecopycapture="$mod/protocols/ext-image-copy-capture-v1.xml" \
13+ -import extworkspace=$p/extworkspace -import-xml extworkspace="$mod/protocols/ext-workspace-v1.xml" \
14+ neferwl-image-capture-v1.xml
A internal/adapters/wayland/testserver/neferwl/neferwl-image-capture-v1.xml +231 -0
@@ -0,0 +1,231 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<protocol name="neferwl_image_capture_v1">
3+ <copyright>
4+ neferwl private protocol. No stability promise beyond the version of the
5+ interface.
6+ </copyright>
7+
8+ <description summary="neferwl additions to ext-image-copy-capture">
9+ Optional additions to ext-image-capture-source-v1, ext-workspace-v1 and
10+ ext-image-copy-capture-v1 for what those protocols do not cover:
11+ workspace and output-region capture sources, the frame of a workspace,
12+ and leaving a client's own surfaces (a recording HUD) out of its
13+ captures.
14+
15+ Capture always goes through ext-image-copy-capture. The compositor applies
16+ the same rules to every capture, whatever its source: it may refuse a
17+ client (ext_image_copy_capture_session_v1.stopped), and it draws its own
18+ capture indicator on screen, never in a captured image, which no client
19+ can turn off.
20+ </description>
21+
22+ <interface name="neferwl_image_capture_source_manager_v1" version="1">
23+ <description summary="workspace and output-region capture sources">
24+ Global. Sources created here are ext_image_capture_source_v1 objects,
25+ used with ext_image_copy_capture_manager_v1.create_session like any
26+ other source.
27+ </description>
28+
29+ <enum name="error">
30+ <entry name="invalid_region" value="0" summary="width or height is not positive"/>
31+ </enum>
32+
33+ <request name="destroy" type="destructor">
34+ <description summary="destroy the manager">
35+ Sources created through it stay valid.
36+ </description>
37+ </request>
38+
39+ <request name="create_workspace_source">
40+ <description summary="capture a workspace">
41+ The source is the workspace frame, without bars, whether or not the
42+ workspace is on screen: a workspace that is not on screen is rendered
43+ for capture only. Its buffer size follows the workspace frame. A
44+ session on it is stopped when the workspace is removed. A handle
45+ already removed gives a source whose sessions stop at once.
46+ </description>
47+ <arg name="source" type="new_id" interface="ext_image_capture_source_v1"/>
48+ <arg name="workspace" type="object" interface="ext_workspace_handle_v1"/>
49+ </request>
50+
51+ <request name="create_output_region_source">
52+ <description summary="capture a rectangle of an output">
53+ The rectangle is in output-local logical pixels and is clipped to the
54+ output. The buffer covers only that rectangle, in physical pixels. A
55+ session on it is stopped when the output goes away or when nothing of
56+ the rectangle is left on the output.
57+ </description>
58+ <arg name="source" type="new_id" interface="ext_image_capture_source_v1"/>
59+ <arg name="output" type="object" interface="wl_output"/>
60+ <arg name="x" type="int"/>
61+ <arg name="y" type="int"/>
62+ <arg name="width" type="int"/>
63+ <arg name="height" type="int"/>
64+ </request>
65+
66+ <request name="get_workspace_frame">
67+ <description summary="learn the frame of a workspace">
68+ ext-workspace-v1 carries no geometry. The frame object reports the
69+ rectangle that a workspace source of the same handle captures. For a
70+ handle already removed, the object never sends frame.
71+ </description>
72+ <arg name="id" type="new_id" interface="neferwl_workspace_frame_v1"/>
73+ <arg name="workspace" type="object" interface="ext_workspace_handle_v1"/>
74+ </request>
75+ </interface>
76+
77+ <interface name="neferwl_workspace_frame_v1" version="1">
78+ <description summary="the frame of one workspace">
79+ The frame is the area of its output that the workspace draws in,
80+ without bars: the whole output, or the smaller size set for the
81+ workspace.
82+ </description>
83+
84+ <request name="destroy" type="destructor">
85+ <description summary="destroy the frame object"/>
86+ </request>
87+
88+ <event name="frame">
89+ <description summary="the workspace frame">
90+ The frame in output-local logical pixels. Sent when the object is
91+ created and again whenever the frame changes. Not sent once the
92+ workspace is removed.
93+ </description>
94+ <arg name="x" type="int"/>
95+ <arg name="y" type="int"/>
96+ <arg name="width" type="int"/>
97+ <arg name="height" type="int"/>
98+ </event>
99+ </interface>
100+
101+ <interface name="neferwl_capture_exclusion_manager_v1" version="1">
102+ <description summary="leave a client's own surfaces out of its captures">
103+ Global. A recorder asks for an exclusion on its capture session and
104+ gets a token; its HUD, usually on another connection of the same user,
105+ attaches layer surfaces with that token. Frames of that session leave
106+ the attached surfaces out. Every other capture shows them.
107+ </description>
108+
109+ <enum name="error">
110+ <entry name="surface_role" value="0" summary="the surface is not a wlr-layer-shell surface"/>
111+ <entry name="invalid_layer" value="1" summary="the layer is neither top nor overlay"/>
112+ <entry name="exclusive_keyboard" value="2" summary="the layer takes the keyboard exclusively"/>
113+ <entry name="invalid_surface" value="3" summary="the surface is already mapped"/>
114+ <entry name="already_excluded" value="4" summary="the capture session already has an exclusion"/>
115+ </enum>
116+
117+ <request name="destroy" type="destructor">
118+ <description summary="destroy the manager">
119+ Exclusions and attachments created through it stay alive.
120+ </description>
121+ </request>
122+
123+ <request name="get_exclusion">
124+ <description summary="create the exclusion of a capture session">
125+ The exclusion answers with exactly one of token or failed. It ends
126+ with the capture session.
127+ </description>
128+ <arg name="id" type="new_id" interface="neferwl_capture_exclusion_v1"/>
129+ <arg name="session" type="object" interface="ext_image_copy_capture_session_v1"/>
130+ </request>
131+
132+ <request name="attach_surface">
133+ <description summary="attach a layer surface to an exclusion by token">
134+ Marks a layer surface of this client as excluded from the frames of
135+ the session that owns the token. The client must have the same user
136+ ID as the compositor and must also be allowed to capture: its
137+ executable is on the capture allowlist (or the allowlist is off), it
138+ is not sandboxed, and the session is not locked. The surface must already have the
139+ wlr-layer-shell role, on the top or overlay layer, must not have
140+ mapped yet and must not ask for exclusive keyboard interactivity. At
141+ most 4 layers per exclusion. An attached layer never gets keyboard
142+ interactivity; if it moves to another layer the compositor closes it
143+ and sends detached.
144+
145+ The attachment answers with exactly one of attached or failed. The
146+ surface is excluded from this request on, before it can map; the
147+ client must not show it before attached.
148+ </description>
149+ <arg name="id" type="new_id" interface="neferwl_capture_layer_v1"/>
150+ <arg name="token" type="string"/>
151+ <arg name="surface" type="object" interface="wl_surface"/>
152+ </request>
153+ </interface>
154+
155+ <interface name="neferwl_capture_exclusion_v1" version="1">
156+ <description summary="the exclusion of one capture session">
157+ Destroying it detaches every attached layer.
158+ </description>
159+
160+ <enum name="failure">
161+ <entry name="busy" value="0" summary="another exclusion is live in the compositor"/>
162+ <entry name="session_stopped" value="1" summary="the capture session is stopped"/>
163+ </enum>
164+
165+ <request name="destroy" type="destructor">
166+ <description summary="destroy the exclusion"/>
167+ </request>
168+
169+ <event name="token">
170+ <description summary="the attachment token">
171+ 64 lowercase hexadecimal characters (32 random bytes). A secret for
172+ the owner and its HUD, never logged, valid until the exclusion ends.
173+ </description>
174+ <arg name="token" type="string"/>
175+ </event>
176+
177+ <event name="failed">
178+ <description summary="no exclusion">
179+ Final. Destroy the object.
180+ </description>
181+ <arg name="reason" type="uint" enum="failure"/>
182+ </event>
183+ </interface>
184+
185+ <interface name="neferwl_capture_layer_v1" version="1">
186+ <description summary="a layer surface attached to an exclusion">
187+ Created by attach_surface. Exactly one of attached or failed comes
188+ first; detached comes at most once, after attached.
189+ </description>
190+
191+ <enum name="failure">
192+ <entry name="unknown_token" value="0" summary="no live exclusion has this token"/>
193+ <entry name="unauthorized" value="1" summary="peer credentials differ from the compositor user's or are unreadable, or the client may not capture (not on the allowlist, sandboxed, or session locked)"/>
194+ <entry name="too_many_layers" value="2" summary="the exclusion already has 4 attached layers"/>
195+ <entry name="already_attached" value="3" summary="the surface is already attached"/>
196+ <entry name="exclusion_ended" value="4" summary="the exclusion ended before the compositor confirmed the layer"/>
197+ <entry name="layer_destroyed" value="5" summary="the layer role was destroyed before the compositor confirmed it"/>
198+ </enum>
199+
200+ <enum name="detach_reason">
201+ <entry name="exclusion_ended" value="0" summary="the exclusion ended; the compositor closed the layer surface"/>
202+ <entry name="layer_destroyed" value="1" summary="the layer role was destroyed"/>
203+ </enum>
204+
205+ <request name="destroy" type="destructor">
206+ <description summary="destroy the attachment object">
207+ The surface stays excluded until its layer role is destroyed or the
208+ exclusion ends.
209+ </description>
210+ </request>
211+
212+ <event name="attached">
213+ <description summary="the layer is excluded">
214+ The compositor confirmed the attachment. Safe to show.
215+ </description>
216+ </event>
217+
218+ <event name="failed">
219+ <description summary="the attachment was refused"/>
220+ <arg name="reason" type="uint" enum="failure"/>
221+ </event>
222+
223+ <event name="detached">
224+ <description summary="the layer left the exclusion">
225+ Sent once after attached. When the exclusion ended, the compositor
226+ also closed the layer surface (zwlr_layer_surface_v1.closed).
227+ </description>
228+ <arg name="reason" type="uint" enum="detach_reason"/>
229+ </event>
230+ </interface>
231+</protocol>
@@ -0,0 +1,231 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<protocol name="neferwl_image_capture_v1">
3+ <copyright>
4+ neferwl private protocol. No stability promise beyond the version of the
5+ interface.
6+ </copyright>
7+
8+ <description summary="neferwl additions to ext-image-copy-capture">
9+ Optional additions to ext-image-capture-source-v1, ext-workspace-v1 and
10+ ext-image-copy-capture-v1 for what those protocols do not cover:
11+ workspace and output-region capture sources, the frame of a workspace,
12+ and leaving a client's own surfaces (a recording HUD) out of its
13+ captures.
14+
15+ Capture always goes through ext-image-copy-capture. The compositor applies
16+ the same rules to every capture, whatever its source: it may refuse a
17+ client (ext_image_copy_capture_session_v1.stopped), and it draws its own
18+ capture indicator on screen, never in a captured image, which no client
19+ can turn off.
20+ </description>
21+
22+ <interface name="neferwl_image_capture_source_manager_v1" version="1">
23+ <description summary="workspace and output-region capture sources">
24+ Global. Sources created here are ext_image_capture_source_v1 objects,
25+ used with ext_image_copy_capture_manager_v1.create_session like any
26+ other source.
27+ </description>
28+
29+ <enum name="error">
30+ <entry name="invalid_region" value="0" summary="width or height is not positive"/>
31+ </enum>
32+
33+ <request name="destroy" type="destructor">
34+ <description summary="destroy the manager">
35+ Sources created through it stay valid.
36+ </description>
37+ </request>
38+
39+ <request name="create_workspace_source">
40+ <description summary="capture a workspace">
41+ The source is the workspace frame, without bars, whether or not the
42+ workspace is on screen: a workspace that is not on screen is rendered
43+ for capture only. Its buffer size follows the workspace frame. A
44+ session on it is stopped when the workspace is removed. A handle
45+ already removed gives a source whose sessions stop at once.
46+ </description>
47+ <arg name="source" type="new_id" interface="ext_image_capture_source_v1"/>
48+ <arg name="workspace" type="object" interface="ext_workspace_handle_v1"/>
49+ </request>
50+
51+ <request name="create_output_region_source">
52+ <description summary="capture a rectangle of an output">
53+ The rectangle is in output-local logical pixels and is clipped to the
54+ output. The buffer covers only that rectangle, in physical pixels. A
55+ session on it is stopped when the output goes away or when nothing of
56+ the rectangle is left on the output.
57+ </description>
58+ <arg name="source" type="new_id" interface="ext_image_capture_source_v1"/>
59+ <arg name="output" type="object" interface="wl_output"/>
60+ <arg name="x" type="int"/>
61+ <arg name="y" type="int"/>
62+ <arg name="width" type="int"/>
63+ <arg name="height" type="int"/>
64+ </request>
65+
66+ <request name="get_workspace_frame">
67+ <description summary="learn the frame of a workspace">
68+ ext-workspace-v1 carries no geometry. The frame object reports the
69+ rectangle that a workspace source of the same handle captures. For a
70+ handle already removed, the object never sends frame.
71+ </description>
72+ <arg name="id" type="new_id" interface="neferwl_workspace_frame_v1"/>
73+ <arg name="workspace" type="object" interface="ext_workspace_handle_v1"/>
74+ </request>
75+ </interface>
76+
77+ <interface name="neferwl_workspace_frame_v1" version="1">
78+ <description summary="the frame of one workspace">
79+ The frame is the area of its output that the workspace draws in,
80+ without bars: the whole output, or the smaller size set for the
81+ workspace.
82+ </description>
83+
84+ <request name="destroy" type="destructor">
85+ <description summary="destroy the frame object"/>
86+ </request>
87+
88+ <event name="frame">
89+ <description summary="the workspace frame">
90+ The frame in output-local logical pixels. Sent when the object is
91+ created and again whenever the frame changes. Not sent once the
92+ workspace is removed.
93+ </description>
94+ <arg name="x" type="int"/>
95+ <arg name="y" type="int"/>
96+ <arg name="width" type="int"/>
97+ <arg name="height" type="int"/>
98+ </event>
99+ </interface>
100+
101+ <interface name="neferwl_capture_exclusion_manager_v1" version="1">
102+ <description summary="leave a client's own surfaces out of its captures">
103+ Global. A recorder asks for an exclusion on its capture session and
104+ gets a token; its HUD, usually on another connection of the same user,
105+ attaches layer surfaces with that token. Frames of that session leave
106+ the attached surfaces out. Every other capture shows them.
107+ </description>
108+
109+ <enum name="error">
110+ <entry name="surface_role" value="0" summary="the surface is not a wlr-layer-shell surface"/>
111+ <entry name="invalid_layer" value="1" summary="the layer is neither top nor overlay"/>
112+ <entry name="exclusive_keyboard" value="2" summary="the layer takes the keyboard exclusively"/>
113+ <entry name="invalid_surface" value="3" summary="the surface is already mapped"/>
114+ <entry name="already_excluded" value="4" summary="the capture session already has an exclusion"/>
115+ </enum>
116+
117+ <request name="destroy" type="destructor">
118+ <description summary="destroy the manager">
119+ Exclusions and attachments created through it stay alive.
120+ </description>
121+ </request>
122+
123+ <request name="get_exclusion">
124+ <description summary="create the exclusion of a capture session">
125+ The exclusion answers with exactly one of token or failed. It ends
126+ with the capture session.
127+ </description>
128+ <arg name="id" type="new_id" interface="neferwl_capture_exclusion_v1"/>
129+ <arg name="session" type="object" interface="ext_image_copy_capture_session_v1"/>
130+ </request>
131+
132+ <request name="attach_surface">
133+ <description summary="attach a layer surface to an exclusion by token">
134+ Marks a layer surface of this client as excluded from the frames of
135+ the session that owns the token. The client must have the same user
136+ ID as the compositor and must also be allowed to capture: its
137+ executable is on the capture allowlist (or the allowlist is off), it
138+ is not sandboxed, and the session is not locked. The surface must already have the
139+ wlr-layer-shell role, on the top or overlay layer, must not have
140+ mapped yet and must not ask for exclusive keyboard interactivity. At
141+ most 4 layers per exclusion. An attached layer never gets keyboard
142+ interactivity; if it moves to another layer the compositor closes it
143+ and sends detached.
144+
145+ The attachment answers with exactly one of attached or failed. The
146+ surface is excluded from this request on, before it can map; the
147+ client must not show it before attached.
148+ </description>
149+ <arg name="id" type="new_id" interface="neferwl_capture_layer_v1"/>
150+ <arg name="token" type="string"/>
151+ <arg name="surface" type="object" interface="wl_surface"/>
152+ </request>
153+ </interface>
154+
155+ <interface name="neferwl_capture_exclusion_v1" version="1">
156+ <description summary="the exclusion of one capture session">
157+ Destroying it detaches every attached layer.
158+ </description>
159+
160+ <enum name="failure">
161+ <entry name="busy" value="0" summary="another exclusion is live in the compositor"/>
162+ <entry name="session_stopped" value="1" summary="the capture session is stopped"/>
163+ </enum>
164+
165+ <request name="destroy" type="destructor">
166+ <description summary="destroy the exclusion"/>
167+ </request>
168+
169+ <event name="token">
170+ <description summary="the attachment token">
171+ 64 lowercase hexadecimal characters (32 random bytes). A secret for
172+ the owner and its HUD, never logged, valid until the exclusion ends.
173+ </description>
174+ <arg name="token" type="string"/>
175+ </event>
176+
177+ <event name="failed">
178+ <description summary="no exclusion">
179+ Final. Destroy the object.
180+ </description>
181+ <arg name="reason" type="uint" enum="failure"/>
182+ </event>
183+ </interface>
184+
185+ <interface name="neferwl_capture_layer_v1" version="1">
186+ <description summary="a layer surface attached to an exclusion">
187+ Created by attach_surface. Exactly one of attached or failed comes
188+ first; detached comes at most once, after attached.
189+ </description>
190+
191+ <enum name="failure">
192+ <entry name="unknown_token" value="0" summary="no live exclusion has this token"/>
193+ <entry name="unauthorized" value="1" summary="peer credentials differ from the compositor user's or are unreadable, or the client may not capture (not on the allowlist, sandboxed, or session locked)"/>
194+ <entry name="too_many_layers" value="2" summary="the exclusion already has 4 attached layers"/>
195+ <entry name="already_attached" value="3" summary="the surface is already attached"/>
196+ <entry name="exclusion_ended" value="4" summary="the exclusion ended before the compositor confirmed the layer"/>
197+ <entry name="layer_destroyed" value="5" summary="the layer role was destroyed before the compositor confirmed it"/>
198+ </enum>
199+
200+ <enum name="detach_reason">
201+ <entry name="exclusion_ended" value="0" summary="the exclusion ended; the compositor closed the layer surface"/>
202+ <entry name="layer_destroyed" value="1" summary="the layer role was destroyed"/>
203+ </enum>
204+
205+ <request name="destroy" type="destructor">
206+ <description summary="destroy the attachment object">
207+ The surface stays excluded until its layer role is destroyed or the
208+ exclusion ends.
209+ </description>
210+ </request>
211+
212+ <event name="attached">
213+ <description summary="the layer is excluded">
214+ The compositor confirmed the attachment. Safe to show.
215+ </description>
216+ </event>
217+
218+ <event name="failed">
219+ <description summary="the attachment was refused"/>
220+ <arg name="reason" type="uint" enum="failure"/>
221+ </event>
222+
223+ <event name="detached">
224+ <description summary="the layer left the exclusion">
225+ Sent once after attached. When the exclusion ended, the compositor
226+ also closed the layer surface (zwlr_layer_surface_v1.closed).
227+ </description>
228+ <arg name="reason" type="uint" enum="detach_reason"/>
229+ </event>
230+ </interface>
231+</protocol>
A internal/adapters/wayland/testserver/neferwl/neferwl.go +403 -0
@@ -0,0 +1,403 @@
1+// Code generated by wlgen; DO NOT EDIT.
2+package neferwl
3+
4+import (
5+ "fmt"
6+ extimagecapturesource "github.com/bnema/purego-libwayland/protocol/extimagecapturesource"
7+ extimagecopycapture "github.com/bnema/purego-libwayland/protocol/extimagecopycapture"
8+ extworkspace "github.com/bnema/purego-libwayland/protocol/extworkspace"
9+ wayland "github.com/bnema/purego-libwayland/protocol/wayland"
10+ "github.com/bnema/purego-libwayland/server"
11+ "runtime"
12+)
13+
14+var NeferwlImageCaptureSourceManagerV1Interface *server.Interface
15+var NeferwlWorkspaceFrameV1Interface *server.Interface
16+var NeferwlCaptureExclusionManagerV1Interface *server.Interface
17+var NeferwlCaptureExclusionV1Interface *server.Interface
18+var NeferwlCaptureLayerV1Interface *server.Interface
19+
20+func init() {
21+ NeferwlImageCaptureSourceManagerV1Interface = &server.Interface{Name: "neferwl_image_capture_source_manager_v1", Version: 1}
22+ NeferwlWorkspaceFrameV1Interface = &server.Interface{Name: "neferwl_workspace_frame_v1", Version: 1}
23+ NeferwlCaptureExclusionManagerV1Interface = &server.Interface{Name: "neferwl_capture_exclusion_manager_v1", Version: 1}
24+ NeferwlCaptureExclusionV1Interface = &server.Interface{Name: "neferwl_capture_exclusion_v1", Version: 1}
25+ NeferwlCaptureLayerV1Interface = &server.Interface{Name: "neferwl_capture_layer_v1", Version: 1}
26+ NeferwlImageCaptureSourceManagerV1Interface.Requests = []server.Message{
27+ {Name: "destroy", Signature: "", Types: []*server.Interface{}},
28+ {Name: "create_workspace_source", Signature: "no", Types: []*server.Interface{extimagecapturesource.ExtImageCaptureSourceV1Interface, extworkspace.ExtWorkspaceHandleV1Interface}},
29+ {Name: "create_output_region_source", Signature: "noiiii", Types: []*server.Interface{extimagecapturesource.ExtImageCaptureSourceV1Interface, wayland.OutputInterface, nil, nil, nil, nil}},
30+ {Name: "get_workspace_frame", Signature: "no", Types: []*server.Interface{NeferwlWorkspaceFrameV1Interface, extworkspace.ExtWorkspaceHandleV1Interface}},
31+ }
32+ NeferwlImageCaptureSourceManagerV1Interface.Events = []server.Message{}
33+ NeferwlWorkspaceFrameV1Interface.Requests = []server.Message{
34+ {Name: "destroy", Signature: "", Types: []*server.Interface{}},
35+ }
36+ NeferwlWorkspaceFrameV1Interface.Events = []server.Message{
37+ {Name: "frame", Signature: "iiii", Types: []*server.Interface{nil, nil, nil, nil}},
38+ }
39+ NeferwlCaptureExclusionManagerV1Interface.Requests = []server.Message{
40+ {Name: "destroy", Signature: "", Types: []*server.Interface{}},
41+ {Name: "get_exclusion", Signature: "no", Types: []*server.Interface{NeferwlCaptureExclusionV1Interface, extimagecopycapture.ExtImageCopyCaptureSessionV1Interface}},
42+ {Name: "attach_surface", Signature: "nso", Types: []*server.Interface{NeferwlCaptureLayerV1Interface, nil, wayland.SurfaceInterface}},
43+ }
44+ NeferwlCaptureExclusionManagerV1Interface.Events = []server.Message{}
45+ NeferwlCaptureExclusionV1Interface.Requests = []server.Message{
46+ {Name: "destroy", Signature: "", Types: []*server.Interface{}},
47+ }
48+ NeferwlCaptureExclusionV1Interface.Events = []server.Message{
49+ {Name: "token", Signature: "s", Types: []*server.Interface{nil}},
50+ {Name: "failed", Signature: "u", Types: []*server.Interface{nil}},
51+ }
52+ NeferwlCaptureLayerV1Interface.Requests = []server.Message{
53+ {Name: "destroy", Signature: "", Types: []*server.Interface{}},
54+ }
55+ NeferwlCaptureLayerV1Interface.Events = []server.Message{
56+ {Name: "attached", Signature: "", Types: []*server.Interface{}},
57+ {Name: "failed", Signature: "u", Types: []*server.Interface{nil}},
58+ {Name: "detached", Signature: "u", Types: []*server.Interface{nil}},
59+ }
60+ if err := server.NewInterfaces(NeferwlImageCaptureSourceManagerV1Interface, NeferwlWorkspaceFrameV1Interface, NeferwlCaptureExclusionManagerV1Interface, NeferwlCaptureExclusionV1Interface, NeferwlCaptureLayerV1Interface); err != nil {
61+ panic(err)
62+ }
63+}
64+
65+// NeferwlImageCaptureSourceManagerV1 workspace and output-region capture sources.
66+type NeferwlImageCaptureSourceManagerV1 struct{ *server.Resource }
67+
68+func WrapNeferwlImageCaptureSourceManagerV1(r *server.Resource) *NeferwlImageCaptureSourceManagerV1 {
69+ if r == nil {
70+ return nil
71+ }
72+ return &NeferwlImageCaptureSourceManagerV1{r}
73+}
74+
75+const NeferwlImageCaptureSourceManagerV1RequestDestroy uint32 = 0
76+const NeferwlImageCaptureSourceManagerV1RequestCreateWorkspaceSource uint32 = 1
77+const NeferwlImageCaptureSourceManagerV1RequestCreateOutputRegionSource uint32 = 2
78+const NeferwlImageCaptureSourceManagerV1RequestGetWorkspaceFrame uint32 = 3
79+
80+// NeferwlImageCaptureSourceManagerV1Error .
81+type NeferwlImageCaptureSourceManagerV1Error uint32
82+
83+const (
84+ NeferwlImageCaptureSourceManagerV1ErrorInvalidRegion NeferwlImageCaptureSourceManagerV1Error = 0
85+)
86+
87+// NeferwlImageCaptureSourceManagerV1Handler handles neferwl_image_capture_source_manager_v1 requests.
88+type NeferwlImageCaptureSourceManagerV1Handler interface {
89+ Destroy(self *NeferwlImageCaptureSourceManagerV1)
90+ CreateWorkspaceSource(self *NeferwlImageCaptureSourceManagerV1, source uint32, workspace *extworkspace.ExtWorkspaceHandleV1)
91+ CreateOutputRegionSource(self *NeferwlImageCaptureSourceManagerV1, source uint32, output *wayland.Output, x int32, y int32, width int32, height int32)
92+ GetWorkspaceFrame(self *NeferwlImageCaptureSourceManagerV1, id uint32, workspace *extworkspace.ExtWorkspaceHandleV1)
93+}
94+
95+func NewNeferwlImageCaptureSourceManagerV1(c server.Client, version int32, id uint32, h NeferwlImageCaptureSourceManagerV1Handler) (*NeferwlImageCaptureSourceManagerV1, error) {
96+ if h == nil {
97+ return nil, fmt.Errorf("purego-libwayland: nil handler for neferwl_image_capture_source_manager_v1")
98+ }
99+ var self *NeferwlImageCaptureSourceManagerV1
100+ r, err := c.CreateResource(NeferwlImageCaptureSourceManagerV1Interface, version, id, func(_ *server.Resource, opcode uint32, args []server.Arg) {
101+ switch opcode {
102+ case 0:
103+ h.Destroy(self)
104+ self.Destroy()
105+ case 1:
106+ h.CreateWorkspaceSource(self, args[0].NewID(), extworkspace.WrapExtWorkspaceHandleV1(args[1].Resource()))
107+ case 2:
108+ h.CreateOutputRegionSource(self, args[0].NewID(), wayland.WrapOutput(args[1].Resource()), args[2].Int(), args[3].Int(), args[4].Int(), args[5].Int())
109+ case 3:
110+ h.GetWorkspaceFrame(self, args[0].NewID(), extworkspace.WrapExtWorkspaceHandleV1(args[1].Resource()))
111+ }
112+ })
113+ if err != nil {
114+ return nil, err
115+ }
116+ self = WrapNeferwlImageCaptureSourceManagerV1(r)
117+ r.Data = self
118+ return self, nil
119+}
120+func NewNeferwlImageCaptureSourceManagerV1Global(d *server.Display, version int32, bind server.BindFunc) error {
121+ return d.CreateGlobal(NeferwlImageCaptureSourceManagerV1Interface, version, bind)
122+}
123+
124+// NeferwlWorkspaceFrameV1 the frame of one workspace.
125+type NeferwlWorkspaceFrameV1 struct{ *server.Resource }
126+
127+func WrapNeferwlWorkspaceFrameV1(r *server.Resource) *NeferwlWorkspaceFrameV1 {
128+ if r == nil {
129+ return nil
130+ }
131+ return &NeferwlWorkspaceFrameV1{r}
132+}
133+
134+const NeferwlWorkspaceFrameV1RequestDestroy uint32 = 0
135+const NeferwlWorkspaceFrameV1EventFrame uint32 = 0
136+
137+// NeferwlWorkspaceFrameV1Handler handles neferwl_workspace_frame_v1 requests.
138+type NeferwlWorkspaceFrameV1Handler interface {
139+ Destroy(self *NeferwlWorkspaceFrameV1)
140+}
141+
142+func NewNeferwlWorkspaceFrameV1(c server.Client, version int32, id uint32, h NeferwlWorkspaceFrameV1Handler) (*NeferwlWorkspaceFrameV1, error) {
143+ if h == nil {
144+ return nil, fmt.Errorf("purego-libwayland: nil handler for neferwl_workspace_frame_v1")
145+ }
146+ var self *NeferwlWorkspaceFrameV1
147+ r, err := c.CreateResource(NeferwlWorkspaceFrameV1Interface, version, id, func(_ *server.Resource, opcode uint32, args []server.Arg) {
148+ switch opcode {
149+ case 0:
150+ h.Destroy(self)
151+ self.Destroy()
152+ }
153+ })
154+ if err != nil {
155+ return nil, err
156+ }
157+ self = WrapNeferwlWorkspaceFrameV1(r)
158+ r.Data = self
159+ return self, nil
160+}
161+
162+// SendFrame the workspace frame.
163+func (self *NeferwlWorkspaceFrameV1) SendFrame(x int32, y int32, width int32, height int32) {
164+ self.PostEvent(NeferwlWorkspaceFrameV1EventFrame, server.Int(x), server.Int(y), server.Int(width), server.Int(height))
165+}
166+func NewNeferwlWorkspaceFrameV1Global(d *server.Display, version int32, bind server.BindFunc) error {
167+ return d.CreateGlobal(NeferwlWorkspaceFrameV1Interface, version, bind)
168+}
169+
170+// NeferwlCaptureExclusionManagerV1 leave a client's own surfaces out of its captures.
171+type NeferwlCaptureExclusionManagerV1 struct{ *server.Resource }
172+
173+func WrapNeferwlCaptureExclusionManagerV1(r *server.Resource) *NeferwlCaptureExclusionManagerV1 {
174+ if r == nil {
175+ return nil
176+ }
177+ return &NeferwlCaptureExclusionManagerV1{r}
178+}
179+
180+const NeferwlCaptureExclusionManagerV1RequestDestroy uint32 = 0
181+const NeferwlCaptureExclusionManagerV1RequestGetExclusion uint32 = 1
182+const NeferwlCaptureExclusionManagerV1RequestAttachSurface uint32 = 2
183+
184+// NeferwlCaptureExclusionManagerV1Error .
185+type NeferwlCaptureExclusionManagerV1Error uint32
186+
187+const (
188+ NeferwlCaptureExclusionManagerV1ErrorSurfaceRole NeferwlCaptureExclusionManagerV1Error = 0
189+ NeferwlCaptureExclusionManagerV1ErrorInvalidLayer NeferwlCaptureExclusionManagerV1Error = 1
190+ NeferwlCaptureExclusionManagerV1ErrorExclusiveKeyboard NeferwlCaptureExclusionManagerV1Error = 2
191+ NeferwlCaptureExclusionManagerV1ErrorInvalidSurface NeferwlCaptureExclusionManagerV1Error = 3
192+ NeferwlCaptureExclusionManagerV1ErrorAlreadyExcluded NeferwlCaptureExclusionManagerV1Error = 4
193+)
194+
195+// NeferwlCaptureExclusionManagerV1Handler handles neferwl_capture_exclusion_manager_v1 requests.
196+type NeferwlCaptureExclusionManagerV1Handler interface {
197+ Destroy(self *NeferwlCaptureExclusionManagerV1)
198+ GetExclusion(self *NeferwlCaptureExclusionManagerV1, id uint32, session *extimagecopycapture.ExtImageCopyCaptureSessionV1)
199+ AttachSurface(self *NeferwlCaptureExclusionManagerV1, id uint32, token string, surface *wayland.Surface)
200+}
201+
202+func NewNeferwlCaptureExclusionManagerV1(c server.Client, version int32, id uint32, h NeferwlCaptureExclusionManagerV1Handler) (*NeferwlCaptureExclusionManagerV1, error) {
203+ if h == nil {
204+ return nil, fmt.Errorf("purego-libwayland: nil handler for neferwl_capture_exclusion_manager_v1")
205+ }
206+ var self *NeferwlCaptureExclusionManagerV1
207+ r, err := c.CreateResource(NeferwlCaptureExclusionManagerV1Interface, version, id, func(_ *server.Resource, opcode uint32, args []server.Arg) {
208+ switch opcode {
209+ case 0:
210+ h.Destroy(self)
211+ self.Destroy()
212+ case 1:
213+ h.GetExclusion(self, args[0].NewID(), extimagecopycapture.WrapExtImageCopyCaptureSessionV1(args[1].Resource()))
214+ case 2:
215+ h.AttachSurface(self, args[0].NewID(), args[1].String(), wayland.WrapSurface(args[2].Resource()))
216+ }
217+ })
218+ if err != nil {
219+ return nil, err
220+ }
221+ self = WrapNeferwlCaptureExclusionManagerV1(r)
222+ r.Data = self
223+ return self, nil
224+}
225+func NewNeferwlCaptureExclusionManagerV1Global(d *server.Display, version int32, bind server.BindFunc) error {
226+ return d.CreateGlobal(NeferwlCaptureExclusionManagerV1Interface, version, bind)
227+}
228+
229+// NeferwlCaptureExclusionV1 the exclusion of one capture session.
230+type NeferwlCaptureExclusionV1 struct{ *server.Resource }
231+
232+func WrapNeferwlCaptureExclusionV1(r *server.Resource) *NeferwlCaptureExclusionV1 {
233+ if r == nil {
234+ return nil
235+ }
236+ return &NeferwlCaptureExclusionV1{r}
237+}
238+
239+const NeferwlCaptureExclusionV1RequestDestroy uint32 = 0
240+const NeferwlCaptureExclusionV1EventToken uint32 = 0
241+const NeferwlCaptureExclusionV1EventFailed uint32 = 1
242+
243+// NeferwlCaptureExclusionV1Failure .
244+type NeferwlCaptureExclusionV1Failure uint32
245+
246+const (
247+ NeferwlCaptureExclusionV1FailureBusy NeferwlCaptureExclusionV1Failure = 0
248+ NeferwlCaptureExclusionV1FailureSessionStopped NeferwlCaptureExclusionV1Failure = 1
249+)
250+
251+// NeferwlCaptureExclusionV1Handler handles neferwl_capture_exclusion_v1 requests.
252+type NeferwlCaptureExclusionV1Handler interface {
253+ Destroy(self *NeferwlCaptureExclusionV1)
254+}
255+
256+func NewNeferwlCaptureExclusionV1(c server.Client, version int32, id uint32, h NeferwlCaptureExclusionV1Handler) (*NeferwlCaptureExclusionV1, error) {
257+ if h == nil {
258+ return nil, fmt.Errorf("purego-libwayland: nil handler for neferwl_capture_exclusion_v1")
259+ }
260+ var self *NeferwlCaptureExclusionV1
261+ r, err := c.CreateResource(NeferwlCaptureExclusionV1Interface, version, id, func(_ *server.Resource, opcode uint32, args []server.Arg) {
262+ switch opcode {
263+ case 0:
264+ h.Destroy(self)
265+ self.Destroy()
266+ }
267+ })
268+ if err != nil {
269+ return nil, err
270+ }
271+ self = WrapNeferwlCaptureExclusionV1(r)
272+ r.Data = self
273+ return self, nil
274+}
275+
276+// SendToken the attachment token.
277+func (self *NeferwlCaptureExclusionV1) SendToken(token string) {
278+ var p runtime.Pinner
279+ defer p.Unpin()
280+ self.PostEvent(NeferwlCaptureExclusionV1EventToken, server.String(&p, token, false))
281+}
282+
283+// SendFailed no exclusion.
284+func (self *NeferwlCaptureExclusionV1) SendFailed(reason uint32) {
285+ self.PostEvent(NeferwlCaptureExclusionV1EventFailed, server.Uint(reason))
286+}
287+func NewNeferwlCaptureExclusionV1Global(d *server.Display, version int32, bind server.BindFunc) error {
288+ return d.CreateGlobal(NeferwlCaptureExclusionV1Interface, version, bind)
289+}
290+
291+// NeferwlCaptureLayerV1 a layer surface attached to an exclusion.
292+type NeferwlCaptureLayerV1 struct{ *server.Resource }
293+
294+func WrapNeferwlCaptureLayerV1(r *server.Resource) *NeferwlCaptureLayerV1 {
295+ if r == nil {
296+ return nil
297+ }
298+ return &NeferwlCaptureLayerV1{r}
299+}
300+
301+const NeferwlCaptureLayerV1RequestDestroy uint32 = 0
302+const NeferwlCaptureLayerV1EventAttached uint32 = 0
303+const NeferwlCaptureLayerV1EventFailed uint32 = 1
304+const NeferwlCaptureLayerV1EventDetached uint32 = 2
305+
306+// NeferwlCaptureLayerV1Failure .
307+type NeferwlCaptureLayerV1Failure uint32
308+
309+const (
310+ NeferwlCaptureLayerV1FailureUnknownToken NeferwlCaptureLayerV1Failure = 0
311+ NeferwlCaptureLayerV1FailureUnauthorized NeferwlCaptureLayerV1Failure = 1
312+ NeferwlCaptureLayerV1FailureTooManyLayers NeferwlCaptureLayerV1Failure = 2
313+ NeferwlCaptureLayerV1FailureAlreadyAttached NeferwlCaptureLayerV1Failure = 3
314+ NeferwlCaptureLayerV1FailureExclusionEnded NeferwlCaptureLayerV1Failure = 4
315+ NeferwlCaptureLayerV1FailureLayerDestroyed NeferwlCaptureLayerV1Failure = 5
316+)
317+
318+// NeferwlCaptureLayerV1DetachReason .
319+type NeferwlCaptureLayerV1DetachReason uint32
320+
321+const (
322+ NeferwlCaptureLayerV1DetachReasonExclusionEnded NeferwlCaptureLayerV1DetachReason = 0
323+ NeferwlCaptureLayerV1DetachReasonLayerDestroyed NeferwlCaptureLayerV1DetachReason = 1
324+)
325+
326+// NeferwlCaptureLayerV1Handler handles neferwl_capture_layer_v1 requests.
327+type NeferwlCaptureLayerV1Handler interface {
328+ Destroy(self *NeferwlCaptureLayerV1)
329+}
330+
331+func NewNeferwlCaptureLayerV1(c server.Client, version int32, id uint32, h NeferwlCaptureLayerV1Handler) (*NeferwlCaptureLayerV1, error) {
332+ if h == nil {
333+ return nil, fmt.Errorf("purego-libwayland: nil handler for neferwl_capture_layer_v1")
334+ }
335+ var self *NeferwlCaptureLayerV1
336+ r, err := c.CreateResource(NeferwlCaptureLayerV1Interface, version, id, func(_ *server.Resource, opcode uint32, args []server.Arg) {
337+ switch opcode {
338+ case 0:
339+ h.Destroy(self)
340+ self.Destroy()
341+ }
342+ })
343+ if err != nil {
344+ return nil, err
345+ }
346+ self = WrapNeferwlCaptureLayerV1(r)
347+ r.Data = self
348+ return self, nil
349+}
350+
351+// SendAttached the layer is excluded.
352+func (self *NeferwlCaptureLayerV1) SendAttached() {
353+ self.PostEvent(NeferwlCaptureLayerV1EventAttached)
354+}
355+
356+// SendFailed the attachment was refused.
357+func (self *NeferwlCaptureLayerV1) SendFailed(reason uint32) {
358+ self.PostEvent(NeferwlCaptureLayerV1EventFailed, server.Uint(reason))
359+}
360+
361+// SendDetached the layer left the exclusion.
362+func (self *NeferwlCaptureLayerV1) SendDetached(reason uint32) {
363+ self.PostEvent(NeferwlCaptureLayerV1EventDetached, server.Uint(reason))
364+}
365+func NewNeferwlCaptureLayerV1Global(d *server.Display, version int32, bind server.BindFunc) error {
366+ return d.CreateGlobal(NeferwlCaptureLayerV1Interface, version, bind)
367+}
368+func resourceOf(v interface{ GetResource() *server.Resource }) *server.Resource {
369+ if v == nil {
370+ return nil
371+ }
372+ return v.GetResource()
373+}
374+func (self *NeferwlImageCaptureSourceManagerV1) GetResource() *server.Resource {
375+ if self == nil {
376+ return nil
377+ }
378+ return self.Resource
379+}
380+func (self *NeferwlWorkspaceFrameV1) GetResource() *server.Resource {
381+ if self == nil {
382+ return nil
383+ }
384+ return self.Resource
385+}
386+func (self *NeferwlCaptureExclusionManagerV1) GetResource() *server.Resource {
387+ if self == nil {
388+ return nil
389+ }
390+ return self.Resource
391+}
392+func (self *NeferwlCaptureExclusionV1) GetResource() *server.Resource {
393+ if self == nil {
394+ return nil
395+ }
396+ return self.Resource
397+}
398+func (self *NeferwlCaptureLayerV1) GetResource() *server.Resource {
399+ if self == nil {
400+ return nil
401+ }
402+ return self.Resource
403+}
@@ -0,0 +1,403 @@
1+// Code generated by wlgen; DO NOT EDIT.
2+package neferwl
3+
4+import (
5+ "fmt"
6+ extimagecapturesource "github.com/bnema/purego-libwayland/protocol/extimagecapturesource"
7+ extimagecopycapture "github.com/bnema/purego-libwayland/protocol/extimagecopycapture"
8+ extworkspace "github.com/bnema/purego-libwayland/protocol/extworkspace"
9+ wayland "github.com/bnema/purego-libwayland/protocol/wayland"
10+ "github.com/bnema/purego-libwayland/server"
11+ "runtime"
12+)
13+
14+var NeferwlImageCaptureSourceManagerV1Interface *server.Interface
15+var NeferwlWorkspaceFrameV1Interface *server.Interface
16+var NeferwlCaptureExclusionManagerV1Interface *server.Interface
17+var NeferwlCaptureExclusionV1Interface *server.Interface
18+var NeferwlCaptureLayerV1Interface *server.Interface
19+
20+func init() {
21+ NeferwlImageCaptureSourceManagerV1Interface = &server.Interface{Name: "neferwl_image_capture_source_manager_v1", Version: 1}
22+ NeferwlWorkspaceFrameV1Interface = &server.Interface{Name: "neferwl_workspace_frame_v1", Version: 1}
23+ NeferwlCaptureExclusionManagerV1Interface = &server.Interface{Name: "neferwl_capture_exclusion_manager_v1", Version: 1}
24+ NeferwlCaptureExclusionV1Interface = &server.Interface{Name: "neferwl_capture_exclusion_v1", Version: 1}
25+ NeferwlCaptureLayerV1Interface = &server.Interface{Name: "neferwl_capture_layer_v1", Version: 1}
26+ NeferwlImageCaptureSourceManagerV1Interface.Requests = []server.Message{
27+ {Name: "destroy", Signature: "", Types: []*server.Interface{}},
28+ {Name: "create_workspace_source", Signature: "no", Types: []*server.Interface{extimagecapturesource.ExtImageCaptureSourceV1Interface, extworkspace.ExtWorkspaceHandleV1Interface}},
29+ {Name: "create_output_region_source", Signature: "noiiii", Types: []*server.Interface{extimagecapturesource.ExtImageCaptureSourceV1Interface, wayland.OutputInterface, nil, nil, nil, nil}},
30+ {Name: "get_workspace_frame", Signature: "no", Types: []*server.Interface{NeferwlWorkspaceFrameV1Interface, extworkspace.ExtWorkspaceHandleV1Interface}},
31+ }
32+ NeferwlImageCaptureSourceManagerV1Interface.Events = []server.Message{}
33+ NeferwlWorkspaceFrameV1Interface.Requests = []server.Message{
34+ {Name: "destroy", Signature: "", Types: []*server.Interface{}},
35+ }
36+ NeferwlWorkspaceFrameV1Interface.Events = []server.Message{
37+ {Name: "frame", Signature: "iiii", Types: []*server.Interface{nil, nil, nil, nil}},
38+ }
39+ NeferwlCaptureExclusionManagerV1Interface.Requests = []server.Message{
40+ {Name: "destroy", Signature: "", Types: []*server.Interface{}},
41+ {Name: "get_exclusion", Signature: "no", Types: []*server.Interface{NeferwlCaptureExclusionV1Interface, extimagecopycapture.ExtImageCopyCaptureSessionV1Interface}},
42+ {Name: "attach_surface", Signature: "nso", Types: []*server.Interface{NeferwlCaptureLayerV1Interface, nil, wayland.SurfaceInterface}},
43+ }
44+ NeferwlCaptureExclusionManagerV1Interface.Events = []server.Message{}
45+ NeferwlCaptureExclusionV1Interface.Requests = []server.Message{
46+ {Name: "destroy", Signature: "", Types: []*server.Interface{}},
47+ }
48+ NeferwlCaptureExclusionV1Interface.Events = []server.Message{
49+ {Name: "token", Signature: "s", Types: []*server.Interface{nil}},
50+ {Name: "failed", Signature: "u", Types: []*server.Interface{nil}},
51+ }
52+ NeferwlCaptureLayerV1Interface.Requests = []server.Message{
53+ {Name: "destroy", Signature: "", Types: []*server.Interface{}},
54+ }
55+ NeferwlCaptureLayerV1Interface.Events = []server.Message{
56+ {Name: "attached", Signature: "", Types: []*server.Interface{}},
57+ {Name: "failed", Signature: "u", Types: []*server.Interface{nil}},
58+ {Name: "detached", Signature: "u", Types: []*server.Interface{nil}},
59+ }
60+ if err := server.NewInterfaces(NeferwlImageCaptureSourceManagerV1Interface, NeferwlWorkspaceFrameV1Interface, NeferwlCaptureExclusionManagerV1Interface, NeferwlCaptureExclusionV1Interface, NeferwlCaptureLayerV1Interface); err != nil {
61+ panic(err)
62+ }
63+}
64+
65+// NeferwlImageCaptureSourceManagerV1 workspace and output-region capture sources.
66+type NeferwlImageCaptureSourceManagerV1 struct{ *server.Resource }
67+
68+func WrapNeferwlImageCaptureSourceManagerV1(r *server.Resource) *NeferwlImageCaptureSourceManagerV1 {
69+ if r == nil {
70+ return nil
71+ }
72+ return &NeferwlImageCaptureSourceManagerV1{r}
73+}
74+
75+const NeferwlImageCaptureSourceManagerV1RequestDestroy uint32 = 0
76+const NeferwlImageCaptureSourceManagerV1RequestCreateWorkspaceSource uint32 = 1
77+const NeferwlImageCaptureSourceManagerV1RequestCreateOutputRegionSource uint32 = 2
78+const NeferwlImageCaptureSourceManagerV1RequestGetWorkspaceFrame uint32 = 3
79+
80+// NeferwlImageCaptureSourceManagerV1Error .
81+type NeferwlImageCaptureSourceManagerV1Error uint32
82+
83+const (
84+ NeferwlImageCaptureSourceManagerV1ErrorInvalidRegion NeferwlImageCaptureSourceManagerV1Error = 0
85+)
86+
87+// NeferwlImageCaptureSourceManagerV1Handler handles neferwl_image_capture_source_manager_v1 requests.
88+type NeferwlImageCaptureSourceManagerV1Handler interface {
89+ Destroy(self *NeferwlImageCaptureSourceManagerV1)
90+ CreateWorkspaceSource(self *NeferwlImageCaptureSourceManagerV1, source uint32, workspace *extworkspace.ExtWorkspaceHandleV1)
91+ CreateOutputRegionSource(self *NeferwlImageCaptureSourceManagerV1, source uint32, output *wayland.Output, x int32, y int32, width int32, height int32)
92+ GetWorkspaceFrame(self *NeferwlImageCaptureSourceManagerV1, id uint32, workspace *extworkspace.ExtWorkspaceHandleV1)
93+}
94+
95+func NewNeferwlImageCaptureSourceManagerV1(c server.Client, version int32, id uint32, h NeferwlImageCaptureSourceManagerV1Handler) (*NeferwlImageCaptureSourceManagerV1, error) {
96+ if h == nil {
97+ return nil, fmt.Errorf("purego-libwayland: nil handler for neferwl_image_capture_source_manager_v1")
98+ }
99+ var self *NeferwlImageCaptureSourceManagerV1
100+ r, err := c.CreateResource(NeferwlImageCaptureSourceManagerV1Interface, version, id, func(_ *server.Resource, opcode uint32, args []server.Arg) {
101+ switch opcode {
102+ case 0:
103+ h.Destroy(self)
104+ self.Destroy()
105+ case 1:
106+ h.CreateWorkspaceSource(self, args[0].NewID(), extworkspace.WrapExtWorkspaceHandleV1(args[1].Resource()))
107+ case 2:
108+ h.CreateOutputRegionSource(self, args[0].NewID(), wayland.WrapOutput(args[1].Resource()), args[2].Int(), args[3].Int(), args[4].Int(), args[5].Int())
109+ case 3:
110+ h.GetWorkspaceFrame(self, args[0].NewID(), extworkspace.WrapExtWorkspaceHandleV1(args[1].Resource()))
111+ }
112+ })
113+ if err != nil {
114+ return nil, err
115+ }
116+ self = WrapNeferwlImageCaptureSourceManagerV1(r)
117+ r.Data = self
118+ return self, nil
119+}
120+func NewNeferwlImageCaptureSourceManagerV1Global(d *server.Display, version int32, bind server.BindFunc) error {
121+ return d.CreateGlobal(NeferwlImageCaptureSourceManagerV1Interface, version, bind)
122+}
123+
124+// NeferwlWorkspaceFrameV1 the frame of one workspace.
125+type NeferwlWorkspaceFrameV1 struct{ *server.Resource }
126+
127+func WrapNeferwlWorkspaceFrameV1(r *server.Resource) *NeferwlWorkspaceFrameV1 {
128+ if r == nil {
129+ return nil
130+ }
131+ return &NeferwlWorkspaceFrameV1{r}
132+}
133+
134+const NeferwlWorkspaceFrameV1RequestDestroy uint32 = 0
135+const NeferwlWorkspaceFrameV1EventFrame uint32 = 0
136+
137+// NeferwlWorkspaceFrameV1Handler handles neferwl_workspace_frame_v1 requests.
138+type NeferwlWorkspaceFrameV1Handler interface {
139+ Destroy(self *NeferwlWorkspaceFrameV1)
140+}
141+
142+func NewNeferwlWorkspaceFrameV1(c server.Client, version int32, id uint32, h NeferwlWorkspaceFrameV1Handler) (*NeferwlWorkspaceFrameV1, error) {
143+ if h == nil {
144+ return nil, fmt.Errorf("purego-libwayland: nil handler for neferwl_workspace_frame_v1")
145+ }
146+ var self *NeferwlWorkspaceFrameV1
147+ r, err := c.CreateResource(NeferwlWorkspaceFrameV1Interface, version, id, func(_ *server.Resource, opcode uint32, args []server.Arg) {
148+ switch opcode {
149+ case 0:
150+ h.Destroy(self)
151+ self.Destroy()
152+ }
153+ })
154+ if err != nil {
155+ return nil, err
156+ }
157+ self = WrapNeferwlWorkspaceFrameV1(r)
158+ r.Data = self
159+ return self, nil
160+}
161+
162+// SendFrame the workspace frame.
163+func (self *NeferwlWorkspaceFrameV1) SendFrame(x int32, y int32, width int32, height int32) {
164+ self.PostEvent(NeferwlWorkspaceFrameV1EventFrame, server.Int(x), server.Int(y), server.Int(width), server.Int(height))
165+}
166+func NewNeferwlWorkspaceFrameV1Global(d *server.Display, version int32, bind server.BindFunc) error {
167+ return d.CreateGlobal(NeferwlWorkspaceFrameV1Interface, version, bind)
168+}
169+
170+// NeferwlCaptureExclusionManagerV1 leave a client's own surfaces out of its captures.
171+type NeferwlCaptureExclusionManagerV1 struct{ *server.Resource }
172+
173+func WrapNeferwlCaptureExclusionManagerV1(r *server.Resource) *NeferwlCaptureExclusionManagerV1 {
174+ if r == nil {
175+ return nil
176+ }
177+ return &NeferwlCaptureExclusionManagerV1{r}
178+}
179+
180+const NeferwlCaptureExclusionManagerV1RequestDestroy uint32 = 0
181+const NeferwlCaptureExclusionManagerV1RequestGetExclusion uint32 = 1
182+const NeferwlCaptureExclusionManagerV1RequestAttachSurface uint32 = 2
183+
184+// NeferwlCaptureExclusionManagerV1Error .
185+type NeferwlCaptureExclusionManagerV1Error uint32
186+
187+const (
188+ NeferwlCaptureExclusionManagerV1ErrorSurfaceRole NeferwlCaptureExclusionManagerV1Error = 0
189+ NeferwlCaptureExclusionManagerV1ErrorInvalidLayer NeferwlCaptureExclusionManagerV1Error = 1
190+ NeferwlCaptureExclusionManagerV1ErrorExclusiveKeyboard NeferwlCaptureExclusionManagerV1Error = 2
191+ NeferwlCaptureExclusionManagerV1ErrorInvalidSurface NeferwlCaptureExclusionManagerV1Error = 3
192+ NeferwlCaptureExclusionManagerV1ErrorAlreadyExcluded NeferwlCaptureExclusionManagerV1Error = 4
193+)
194+
195+// NeferwlCaptureExclusionManagerV1Handler handles neferwl_capture_exclusion_manager_v1 requests.
196+type NeferwlCaptureExclusionManagerV1Handler interface {
197+ Destroy(self *NeferwlCaptureExclusionManagerV1)
198+ GetExclusion(self *NeferwlCaptureExclusionManagerV1, id uint32, session *extimagecopycapture.ExtImageCopyCaptureSessionV1)
199+ AttachSurface(self *NeferwlCaptureExclusionManagerV1, id uint32, token string, surface *wayland.Surface)
200+}
201+
202+func NewNeferwlCaptureExclusionManagerV1(c server.Client, version int32, id uint32, h NeferwlCaptureExclusionManagerV1Handler) (*NeferwlCaptureExclusionManagerV1, error) {
203+ if h == nil {
204+ return nil, fmt.Errorf("purego-libwayland: nil handler for neferwl_capture_exclusion_manager_v1")
205+ }
206+ var self *NeferwlCaptureExclusionManagerV1
207+ r, err := c.CreateResource(NeferwlCaptureExclusionManagerV1Interface, version, id, func(_ *server.Resource, opcode uint32, args []server.Arg) {
208+ switch opcode {
209+ case 0:
210+ h.Destroy(self)
211+ self.Destroy()
212+ case 1:
213+ h.GetExclusion(self, args[0].NewID(), extimagecopycapture.WrapExtImageCopyCaptureSessionV1(args[1].Resource()))
214+ case 2:
215+ h.AttachSurface(self, args[0].NewID(), args[1].String(), wayland.WrapSurface(args[2].Resource()))
216+ }
217+ })
218+ if err != nil {
219+ return nil, err
220+ }
221+ self = WrapNeferwlCaptureExclusionManagerV1(r)
222+ r.Data = self
223+ return self, nil
224+}
225+func NewNeferwlCaptureExclusionManagerV1Global(d *server.Display, version int32, bind server.BindFunc) error {
226+ return d.CreateGlobal(NeferwlCaptureExclusionManagerV1Interface, version, bind)
227+}
228+
229+// NeferwlCaptureExclusionV1 the exclusion of one capture session.
230+type NeferwlCaptureExclusionV1 struct{ *server.Resource }
231+
232+func WrapNeferwlCaptureExclusionV1(r *server.Resource) *NeferwlCaptureExclusionV1 {
233+ if r == nil {
234+ return nil
235+ }
236+ return &NeferwlCaptureExclusionV1{r}
237+}
238+
239+const NeferwlCaptureExclusionV1RequestDestroy uint32 = 0
240+const NeferwlCaptureExclusionV1EventToken uint32 = 0
241+const NeferwlCaptureExclusionV1EventFailed uint32 = 1
242+
243+// NeferwlCaptureExclusionV1Failure .
244+type NeferwlCaptureExclusionV1Failure uint32
245+
246+const (
247+ NeferwlCaptureExclusionV1FailureBusy NeferwlCaptureExclusionV1Failure = 0
248+ NeferwlCaptureExclusionV1FailureSessionStopped NeferwlCaptureExclusionV1Failure = 1
249+)
250+
251+// NeferwlCaptureExclusionV1Handler handles neferwl_capture_exclusion_v1 requests.
252+type NeferwlCaptureExclusionV1Handler interface {
253+ Destroy(self *NeferwlCaptureExclusionV1)
254+}
255+
256+func NewNeferwlCaptureExclusionV1(c server.Client, version int32, id uint32, h NeferwlCaptureExclusionV1Handler) (*NeferwlCaptureExclusionV1, error) {
257+ if h == nil {
258+ return nil, fmt.Errorf("purego-libwayland: nil handler for neferwl_capture_exclusion_v1")
259+ }
260+ var self *NeferwlCaptureExclusionV1
261+ r, err := c.CreateResource(NeferwlCaptureExclusionV1Interface, version, id, func(_ *server.Resource, opcode uint32, args []server.Arg) {
262+ switch opcode {
263+ case 0:
264+ h.Destroy(self)
265+ self.Destroy()
266+ }
267+ })
268+ if err != nil {
269+ return nil, err
270+ }
271+ self = WrapNeferwlCaptureExclusionV1(r)
272+ r.Data = self
273+ return self, nil
274+}
275+
276+// SendToken the attachment token.
277+func (self *NeferwlCaptureExclusionV1) SendToken(token string) {
278+ var p runtime.Pinner
279+ defer p.Unpin()
280+ self.PostEvent(NeferwlCaptureExclusionV1EventToken, server.String(&p, token, false))
281+}
282+
283+// SendFailed no exclusion.
284+func (self *NeferwlCaptureExclusionV1) SendFailed(reason uint32) {
285+ self.PostEvent(NeferwlCaptureExclusionV1EventFailed, server.Uint(reason))
286+}
287+func NewNeferwlCaptureExclusionV1Global(d *server.Display, version int32, bind server.BindFunc) error {
288+ return d.CreateGlobal(NeferwlCaptureExclusionV1Interface, version, bind)
289+}
290+
291+// NeferwlCaptureLayerV1 a layer surface attached to an exclusion.
292+type NeferwlCaptureLayerV1 struct{ *server.Resource }
293+
294+func WrapNeferwlCaptureLayerV1(r *server.Resource) *NeferwlCaptureLayerV1 {
295+ if r == nil {
296+ return nil
297+ }
298+ return &NeferwlCaptureLayerV1{r}
299+}
300+
301+const NeferwlCaptureLayerV1RequestDestroy uint32 = 0
302+const NeferwlCaptureLayerV1EventAttached uint32 = 0
303+const NeferwlCaptureLayerV1EventFailed uint32 = 1
304+const NeferwlCaptureLayerV1EventDetached uint32 = 2
305+
306+// NeferwlCaptureLayerV1Failure .
307+type NeferwlCaptureLayerV1Failure uint32
308+
309+const (
310+ NeferwlCaptureLayerV1FailureUnknownToken NeferwlCaptureLayerV1Failure = 0
311+ NeferwlCaptureLayerV1FailureUnauthorized NeferwlCaptureLayerV1Failure = 1
312+ NeferwlCaptureLayerV1FailureTooManyLayers NeferwlCaptureLayerV1Failure = 2
313+ NeferwlCaptureLayerV1FailureAlreadyAttached NeferwlCaptureLayerV1Failure = 3
314+ NeferwlCaptureLayerV1FailureExclusionEnded NeferwlCaptureLayerV1Failure = 4
315+ NeferwlCaptureLayerV1FailureLayerDestroyed NeferwlCaptureLayerV1Failure = 5
316+)
317+
318+// NeferwlCaptureLayerV1DetachReason .
319+type NeferwlCaptureLayerV1DetachReason uint32
320+
321+const (
322+ NeferwlCaptureLayerV1DetachReasonExclusionEnded NeferwlCaptureLayerV1DetachReason = 0
323+ NeferwlCaptureLayerV1DetachReasonLayerDestroyed NeferwlCaptureLayerV1DetachReason = 1
324+)
325+
326+// NeferwlCaptureLayerV1Handler handles neferwl_capture_layer_v1 requests.
327+type NeferwlCaptureLayerV1Handler interface {
328+ Destroy(self *NeferwlCaptureLayerV1)
329+}
330+
331+func NewNeferwlCaptureLayerV1(c server.Client, version int32, id uint32, h NeferwlCaptureLayerV1Handler) (*NeferwlCaptureLayerV1, error) {
332+ if h == nil {
333+ return nil, fmt.Errorf("purego-libwayland: nil handler for neferwl_capture_layer_v1")
334+ }
335+ var self *NeferwlCaptureLayerV1
336+ r, err := c.CreateResource(NeferwlCaptureLayerV1Interface, version, id, func(_ *server.Resource, opcode uint32, args []server.Arg) {
337+ switch opcode {
338+ case 0:
339+ h.Destroy(self)
340+ self.Destroy()
341+ }
342+ })
343+ if err != nil {
344+ return nil, err
345+ }
346+ self = WrapNeferwlCaptureLayerV1(r)
347+ r.Data = self
348+ return self, nil
349+}
350+
351+// SendAttached the layer is excluded.
352+func (self *NeferwlCaptureLayerV1) SendAttached() {
353+ self.PostEvent(NeferwlCaptureLayerV1EventAttached)
354+}
355+
356+// SendFailed the attachment was refused.
357+func (self *NeferwlCaptureLayerV1) SendFailed(reason uint32) {
358+ self.PostEvent(NeferwlCaptureLayerV1EventFailed, server.Uint(reason))
359+}
360+
361+// SendDetached the layer left the exclusion.
362+func (self *NeferwlCaptureLayerV1) SendDetached(reason uint32) {
363+ self.PostEvent(NeferwlCaptureLayerV1EventDetached, server.Uint(reason))
364+}
365+func NewNeferwlCaptureLayerV1Global(d *server.Display, version int32, bind server.BindFunc) error {
366+ return d.CreateGlobal(NeferwlCaptureLayerV1Interface, version, bind)
367+}
368+func resourceOf(v interface{ GetResource() *server.Resource }) *server.Resource {
369+ if v == nil {
370+ return nil
371+ }
372+ return v.GetResource()
373+}
374+func (self *NeferwlImageCaptureSourceManagerV1) GetResource() *server.Resource {
375+ if self == nil {
376+ return nil
377+ }
378+ return self.Resource
379+}
380+func (self *NeferwlWorkspaceFrameV1) GetResource() *server.Resource {
381+ if self == nil {
382+ return nil
383+ }
384+ return self.Resource
385+}
386+func (self *NeferwlCaptureExclusionManagerV1) GetResource() *server.Resource {
387+ if self == nil {
388+ return nil
389+ }
390+ return self.Resource
391+}
392+func (self *NeferwlCaptureExclusionV1) GetResource() *server.Resource {
393+ if self == nil {
394+ return nil
395+ }
396+ return self.Resource
397+}
398+func (self *NeferwlCaptureLayerV1) GetResource() *server.Resource {
399+ if self == nil {
400+ return nil
401+ }
402+ return self.Resource
403+}
A internal/adapters/wayland/testserver/outputs.go +174 -0
@@ -0,0 +1,174 @@
1+package testserver
2+
3+import (
4+ "github.com/bnema/purego-libwayland/protocol/extimagecapturesource"
5+ "github.com/bnema/purego-libwayland/protocol/wayland"
6+ "github.com/bnema/purego-libwayland/server"
7+ "golang.org/x/sys/unix"
8+)
9+
10+func (s *Server) addOutput(spec OutputSpec, must func(error)) {
11+ o := &output{spec: spec}
12+ v := spec.Version
13+ if v == 0 {
14+ v = 4
15+ }
16+ g, err := s.d.AddGlobal(wayland.OutputInterface, int32(v), func(c server.Client, ver, id uint32) {
17+ res, err := wayland.NewOutput(c, int32(ver), id, outputHandler{})
18+ if err != nil {
19+ must(err)
20+ return
21+ }
22+ o.res = append(o.res, res)
23+ res.SendGeometry(0, 0, 0, 0, 0, "test", "test", 0)
24+ res.SendMode(1, int32(spec.Width), int32(spec.Height), 60000)
25+ if ver >= 2 {
26+ scale := spec.Scale
27+ if scale == 0 {
28+ scale = 1
29+ }
30+ res.SendScale(scale)
31+ }
32+ if ver >= 4 {
33+ res.SendName(spec.Name)
34+ }
35+ if ver >= 2 {
36+ res.SendDone()
37+ }
38+ })
39+ must(err)
40+ o.global = g
41+ s.outputs = append(s.outputs, o)
42+}
43+
44+type outputHandler struct{}
45+
46+func (outputHandler) Release(*wayland.Output) {}
47+
48+func (s *Server) outputOf(r *server.Resource) *output {
49+ for _, o := range s.outputs {
50+ for _, res := range o.res {
51+ if res.Resource == r {
52+ return o
53+ }
54+ }
55+ }
56+ return nil
57+}
58+
59+// RemoveOutput withdraws the named output's global and stops its sessions.
60+func (s *Server) RemoveOutput(name string) {
61+ s.Do(func() {
62+ for _, o := range s.outputs {
63+ if o.spec.Name == name && !o.removed {
64+ o.removed = true
65+ o.global.Remove()
66+ for _, c := range s.sessions {
67+ if c.out == o {
68+ c.stop()
69+ }
70+ }
71+ }
72+ }
73+ })
74+}
75+
76+// pool is one wl_shm_pool mapping, shared by the buffers made from it.
77+type pool struct {
78+ data []byte
79+ refs int
80+ destroyed bool
81+}
82+
83+func (p *pool) release() {
84+ if p.refs == 0 && p.destroyed && p.data != nil {
85+ _ = unix.Munmap(p.data)
86+ p.data = nil
87+ }
88+}
89+
90+type buffer struct {
91+ p *pool
92+ offset int
93+ width, height, stride, format uint32
94+}
95+
96+func (b *buffer) bytes() []byte {
97+ n := int(b.stride) * int(b.height)
98+ if b.p.data == nil || b.offset+n > len(b.p.data) {
99+ return nil
100+ }
101+ return b.p.data[b.offset : b.offset+n]
102+}
103+
104+type shmHandler struct{ s *Server }
105+
106+func (h shmHandler) CreatePool(self *wayland.Shm, id uint32, fd int, size int32) {
107+ defer unix.Close(fd)
108+ data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
109+ if err != nil {
110+ self.PostError(0, "mmap failed")
111+ return
112+ }
113+ _, _ = wayland.NewShmPool(self.Client(), 1, id, &poolHandler{s: h.s, p: &pool{data: data}})
114+}
115+func (shmHandler) Release(*wayland.Shm) {}
116+
117+type poolHandler struct {
118+ s *Server
119+ p *pool
120+}
121+
122+func (h *poolHandler) CreateBuffer(self *wayland.ShmPool, id uint32, offset, width, height, stride int32, format uint32) {
123+ b := &buffer{p: h.p, offset: int(offset), width: uint32(width), height: uint32(height), stride: uint32(stride), format: format}
124+ res, err := wayland.NewBuffer(self.Client(), 1, id, bufferHandler{h.s, b})
125+ if err != nil {
126+ return
127+ }
128+ h.p.refs++
129+ h.s.bufs[res.Resource] = b
130+ h.s.stats.Buffers++
131+ res.OnDestroy = func() {
132+ delete(h.s.bufs, res.Resource)
133+ h.s.stats.DestroyedBuffers++
134+ h.p.refs--
135+ h.p.release()
136+ }
137+}
138+func (h *poolHandler) Destroy(*wayland.ShmPool) { h.p.destroyed = true; h.p.release() }
139+func (h *poolHandler) Resize(*wayland.ShmPool, int32) {}
140+
141+type bufferHandler struct {
142+ s *Server
143+ b *buffer
144+}
145+
146+func (bufferHandler) Destroy(*wayland.Buffer) {}
147+
148+type outputSourceManager struct{ s *Server }
149+
150+func (m outputSourceManager) CreateSource(self *extimagecapturesource.ExtOutputImageCaptureSourceManagerV1, id uint32, out *wayland.Output) {
151+ m.s.newSource(self.Client(), id, &source{kind: "output", out: m.s.outputOf(out.Resource)})
152+}
153+func (outputSourceManager) Destroy(*extimagecapturesource.ExtOutputImageCaptureSourceManagerV1) {}
154+
155+type source struct {
156+ kind string // output, region, workspace
157+ out *output
158+ region [4]int32
159+ ws *workspace
160+ gone bool // the workspace handle was already removed
161+}
162+
163+func (s *Server) newSource(c server.Client, id uint32, src *source) {
164+ res, err := extimagecapturesource.NewExtImageCaptureSourceV1(c, 1, id, sourceHandler{})
165+ if err != nil {
166+ return
167+ }
168+ s.sources[res.Resource] = src
169+ res.OnDestroy = func() { delete(s.sources, res.Resource) } // sessions keep their own pointer
170+}
171+
172+type sourceHandler struct{}
173+
174+func (sourceHandler) Destroy(*extimagecapturesource.ExtImageCaptureSourceV1) {}
@@ -0,0 +1,174 @@
1+package testserver
2+
3+import (
4+ "github.com/bnema/purego-libwayland/protocol/extimagecapturesource"
5+ "github.com/bnema/purego-libwayland/protocol/wayland"
6+ "github.com/bnema/purego-libwayland/server"
7+ "golang.org/x/sys/unix"
8+)
9+
10+func (s *Server) addOutput(spec OutputSpec, must func(error)) {
11+ o := &output{spec: spec}
12+ v := spec.Version
13+ if v == 0 {
14+ v = 4
15+ }
16+ g, err := s.d.AddGlobal(wayland.OutputInterface, int32(v), func(c server.Client, ver, id uint32) {
17+ res, err := wayland.NewOutput(c, int32(ver), id, outputHandler{})
18+ if err != nil {
19+ must(err)
20+ return
21+ }
22+ o.res = append(o.res, res)
23+ res.SendGeometry(0, 0, 0, 0, 0, "test", "test", 0)
24+ res.SendMode(1, int32(spec.Width), int32(spec.Height), 60000)
25+ if ver >= 2 {
26+ scale := spec.Scale
27+ if scale == 0 {
28+ scale = 1
29+ }
30+ res.SendScale(scale)
31+ }
32+ if ver >= 4 {
33+ res.SendName(spec.Name)
34+ }
35+ if ver >= 2 {
36+ res.SendDone()
37+ }
38+ })
39+ must(err)
40+ o.global = g
41+ s.outputs = append(s.outputs, o)
42+}
43+
44+type outputHandler struct{}
45+
46+func (outputHandler) Release(*wayland.Output) {}
47+
48+func (s *Server) outputOf(r *server.Resource) *output {
49+ for _, o := range s.outputs {
50+ for _, res := range o.res {
51+ if res.Resource == r {
52+ return o
53+ }
54+ }
55+ }
56+ return nil
57+}
58+
59+// RemoveOutput withdraws the named output's global and stops its sessions.
60+func (s *Server) RemoveOutput(name string) {
61+ s.Do(func() {
62+ for _, o := range s.outputs {
63+ if o.spec.Name == name && !o.removed {
64+ o.removed = true
65+ o.global.Remove()
66+ for _, c := range s.sessions {
67+ if c.out == o {
68+ c.stop()
69+ }
70+ }
71+ }
72+ }
73+ })
74+}
75+
76+// pool is one wl_shm_pool mapping, shared by the buffers made from it.
77+type pool struct {
78+ data []byte
79+ refs int
80+ destroyed bool
81+}
82+
83+func (p *pool) release() {
84+ if p.refs == 0 && p.destroyed && p.data != nil {
85+ _ = unix.Munmap(p.data)
86+ p.data = nil
87+ }
88+}
89+
90+type buffer struct {
91+ p *pool
92+ offset int
93+ width, height, stride, format uint32
94+}
95+
96+func (b *buffer) bytes() []byte {
97+ n := int(b.stride) * int(b.height)
98+ if b.p.data == nil || b.offset+n > len(b.p.data) {
99+ return nil
100+ }
101+ return b.p.data[b.offset : b.offset+n]
102+}
103+
104+type shmHandler struct{ s *Server }
105+
106+func (h shmHandler) CreatePool(self *wayland.Shm, id uint32, fd int, size int32) {
107+ defer unix.Close(fd)
108+ data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
109+ if err != nil {
110+ self.PostError(0, "mmap failed")
111+ return
112+ }
113+ _, _ = wayland.NewShmPool(self.Client(), 1, id, &poolHandler{s: h.s, p: &pool{data: data}})
114+}
115+func (shmHandler) Release(*wayland.Shm) {}
116+
117+type poolHandler struct {
118+ s *Server
119+ p *pool
120+}
121+
122+func (h *poolHandler) CreateBuffer(self *wayland.ShmPool, id uint32, offset, width, height, stride int32, format uint32) {
123+ b := &buffer{p: h.p, offset: int(offset), width: uint32(width), height: uint32(height), stride: uint32(stride), format: format}
124+ res, err := wayland.NewBuffer(self.Client(), 1, id, bufferHandler{h.s, b})
125+ if err != nil {
126+ return
127+ }
128+ h.p.refs++
129+ h.s.bufs[res.Resource] = b
130+ h.s.stats.Buffers++
131+ res.OnDestroy = func() {
132+ delete(h.s.bufs, res.Resource)
133+ h.s.stats.DestroyedBuffers++
134+ h.p.refs--
135+ h.p.release()
136+ }
137+}
138+func (h *poolHandler) Destroy(*wayland.ShmPool) { h.p.destroyed = true; h.p.release() }
139+func (h *poolHandler) Resize(*wayland.ShmPool, int32) {}
140+
141+type bufferHandler struct {
142+ s *Server
143+ b *buffer
144+}
145+
146+func (bufferHandler) Destroy(*wayland.Buffer) {}
147+
148+type outputSourceManager struct{ s *Server }
149+
150+func (m outputSourceManager) CreateSource(self *extimagecapturesource.ExtOutputImageCaptureSourceManagerV1, id uint32, out *wayland.Output) {
151+ m.s.newSource(self.Client(), id, &source{kind: "output", out: m.s.outputOf(out.Resource)})
152+}
153+func (outputSourceManager) Destroy(*extimagecapturesource.ExtOutputImageCaptureSourceManagerV1) {}
154+
155+type source struct {
156+ kind string // output, region, workspace
157+ out *output
158+ region [4]int32
159+ ws *workspace
160+ gone bool // the workspace handle was already removed
161+}
162+
163+func (s *Server) newSource(c server.Client, id uint32, src *source) {
164+ res, err := extimagecapturesource.NewExtImageCaptureSourceV1(c, 1, id, sourceHandler{})
165+ if err != nil {
166+ return
167+ }
168+ s.sources[res.Resource] = src
169+ res.OnDestroy = func() { delete(s.sources, res.Resource) } // sessions keep their own pointer
170+}
171+
172+type sourceHandler struct{}
173+
174+func (sourceHandler) Destroy(*extimagecapturesource.ExtImageCaptureSourceV1) {}
A internal/adapters/wayland/testserver/server.go +301 -0
@@ -0,0 +1,301 @@
1+// Package testserver is a minimal in-process Wayland compositor for tests: a
2+// real libwayland server (purego-libwayland) that speaks wl_output, wl_shm,
3+// ext-image-capture-source-v1, ext-image-copy-capture-v1, ext-workspace-v1 and
4+// NeferWL's capture extension. It is a protocol peer, not a substitute for a
5+// project type: clients connect to its socket like to any compositor.
6+//
7+// All server state is touched only on the display goroutine: handlers run
8+// there, and tests reach it through Do.
9+package testserver
10+
11+import (
12+ "context"
13+ "os"
14+ "path/filepath"
15+ "testing"
16+
17+ "github.com/bnema/purego-libwayland/protocol/extimagecapturesource"
18+ "github.com/bnema/purego-libwayland/protocol/extimagecopycapture"
19+ "github.com/bnema/purego-libwayland/protocol/extworkspace"
20+ "github.com/bnema/purego-libwayland/protocol/wayland"
21+ "github.com/bnema/purego-libwayland/server"
22+ "golang.org/x/sys/unix"
23+
24+ "github.com/bnema/nefercap/internal/adapters/wayland/testserver/neferwl"
25+)
26+
27+// Token is the exclusion token the server hands out.
28+const Token = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
29+
30+// wl_shm formats.
31+const (
32+ ARGB8888 = 0
33+ XRGB8888 = 1
34+)
35+
36+// OutputSpec describes one wl_output. The capture buffer of an output source
37+// is its mode size.
38+type OutputSpec struct {
39+ Name string
40+ Width, Height uint32
41+ Scale int32
42+ Version uint32 // 0 selects 4
43+ // LogicalWidth/Height, when set, are announced through xdg-output.
44+ LogicalWidth, LogicalHeight int32
45+}
46+
47+// WorkspaceSpec describes one ext-workspace handle; all of them live in one
48+// group that holds every output.
49+type WorkspaceSpec struct {
50+ ID string // empty: a temporary workspace, no id event
51+ Name string
52+ Output string // the output whose buffer size a workspace source has
53+ Active bool
54+ Hidden bool
55+ // Frame is the output-local logical rectangle (x, y, width, height) that
56+ // neferwl_workspace_frame_v1 reports; zero reports nothing.
57+ Frame [4]int32
58+}
59+
60+// Config selects what the server offers.
61+type Config struct {
62+ Outputs []OutputSpec // default: one 8x4 output "TEST-1"
63+ // NoCopyCapture leaves out ext-image-copy-capture and the output source.
64+ NoCopyCapture bool
65+ Formats []uint32 // shm formats announced, default XRGB8888
66+ // NoShmFormat announces size and done but no shm_format.
67+ NoShmFormat bool
68+ // Hold keeps every captured frame pending until Release.
69+ Hold bool
70+ // HoldAfterFirst answers the first frame of each session at once and holds
71+ // the following ones until Release, like a compositor that waits for
72+ // damage on a static screen.
73+ HoldAfterFirst bool
74+ // Workspaces enables ext-workspace-v1 when non-nil (possibly empty).
75+ Workspaces []WorkspaceSpec
76+ HasWorkspaces bool
77+ // NeferwlSource and NeferwlExclusion enable the extension's two globals.
78+ NeferwlSource bool
79+ NeferwlExclusion bool
80+ // ExclusionFail answers get_exclusion with failed(ExclusionFailReason-1)
81+ // when non-zero.
82+ ExclusionFail uint32
83+ // AttachFail answers attach_surface with failed(AttachFail-1) when non-zero.
84+ AttachFail uint32
85+ // AttachSilent never answers attach_surface.
86+ AttachSilent bool
87+ // StopAtCreate answers every create_session with stopped and no
88+ // constraints, like a compositor that refuses the client.
89+ StopAtCreate bool
90+ // Transform is reported by every ready frame (wl_output.transform).
91+ Transform uint32
92+ // FailFrames answers that many capture requests with failed(FailReason).
93+ FailFrames int
94+ FailReason uint32
95+}
96+
97+// Stats are counters observed by tests.
98+type Stats struct {
99+ Sessions, Frames, Captures, Ready int
100+ DestroyedSessions int
101+ Buffers, DestroyedBuffers int
102+ LastSource string // "output", "region", "workspace"
103+ LastRegion [4]int32
104+ Exclusions int
105+ Attached []string
106+ DestroyedExclusions int
107+ WorkspaceFrames int // neferwl_workspace_frame_v1 objects created
108+ DestroyedWorkspaceFrames int
109+ DestroyedXdgOutputs int
110+}
111+
112+// Server is a running test compositor.
113+type Server struct {
114+ // Path is the socket path for wayland.New.
115+ Path string
116+
117+ d *server.Display
118+ cfg Config
119+
120+ outputs []*output
121+ bufs map[*server.Resource]*buffer
122+ sources map[*server.Resource]*source
123+ sessions []*session
124+ workspaces []*workspace
125+ wsMgrs []*wsManager
126+ layers []*server.Resource
127+ frames []*frameObject
128+ stats Stats
129+ held []*frame
130+ seq uint32
131+}
132+
133+type output struct {
134+ spec OutputSpec
135+ global *server.Global
136+ res []*wayland.Output
137+ removed bool
138+}
139+
140+// Start runs a compositor on a fresh socket until the test ends.
141+func Start(t testing.TB, cfg Config) *Server {
142+ t.Helper()
143+ if len(cfg.Outputs) == 0 {
144+ cfg.Outputs = []OutputSpec{{Name: "TEST-1", Width: 8, Height: 4, Scale: 1}}
145+ }
146+ if len(cfg.Formats) == 0 {
147+ cfg.Formats = []uint32{XRGB8888}
148+ }
149+ d, err := server.NewDisplay()
150+ if err != nil {
151+ // NEFERCAP_REQUIRE_WAYLAND=1 (set by make test and make race) turns a
152+ // missing libwayland-server into a failure, so the suite cannot pass
153+ // without its protocol tests.
154+ if os.Getenv("NEFERCAP_REQUIRE_WAYLAND") == "1" {
155+ t.Fatalf("libwayland-server unavailable: %v", err)
156+ }
157+ t.Skipf("libwayland-server unavailable: %v", err)
158+ }
159+ dir, err := filepath.EvalSymlinks(t.TempDir())
160+ if err != nil {
161+ t.Fatal(err)
162+ }
163+ path := filepath.Join(dir, "wl-test")
164+ fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
165+ if err != nil {
166+ t.Fatal(err)
167+ }
168+ if err := unix.Bind(fd, &unix.SockaddrUnix{Name: path}); err != nil {
169+ t.Fatal(err)
170+ }
171+ if err := unix.Listen(fd, 16); err != nil {
172+ t.Fatal(err)
173+ }
174+ if err := d.AddSocketFD(fd); err != nil { // libwayland owns fd now
175+ t.Fatal(err)
176+ }
177+ s := &Server{Path: path, d: d, cfg: cfg, bufs: map[*server.Resource]*buffer{}, sources: map[*server.Resource]*source{}}
178+ ctx, cancel := context.WithCancel(context.Background())
179+ done := make(chan error, 1)
180+ go func() { done <- d.Run(ctx) }()
181+ if !d.Do(func() { s.setup(t) }) {
182+ t.Fatal("test compositor stopped")
183+ }
184+ t.Cleanup(func() {
185+ cancel()
186+ <-done
187+ })
188+ return s
189+}
190+
191+// Do runs f on the display goroutine, where server state may be touched.
192+func (s *Server) Do(f func()) {
193+ if !s.d.Do(f) {
194+ // stopped: nothing to observe
195+ return
196+ }
197+}
198+
199+// Stats returns a snapshot of the counters.
200+func (s *Server) Stats() Stats {
201+ var st Stats
202+ s.Do(func() {
203+ st = s.stats
204+ st.Attached = append([]string(nil), s.stats.Attached...)
205+ })
206+ return st
207+}
208+
209+func (s *Server) setup(t testing.TB) {
210+ must := func(err error) {
211+ if err != nil {
212+ t.Error(err)
213+ }
214+ }
215+ for _, spec := range s.cfg.Outputs {
216+ s.addOutput(spec, must)
217+ }
218+ must(wayland.NewShmGlobal(s.d, 1, func(c server.Client, v, id uint32) {
219+ shm, err := wayland.NewShm(c, int32(v), id, shmHandler{s})
220+ if err != nil {
221+ t.Error(err)
222+ return
223+ }
224+ for _, f := range []uint32{ARGB8888, XRGB8888} {
225+ shm.SendFormat(f)
226+ }
227+ }))
228+ if s.xdgOutputWanted() {
229+ s.addXdgOutput(must)
230+ }
231+ must(wayland.NewCompositorGlobal(s.d, 4, func(c server.Client, v, id uint32) {
232+ if _, err := wayland.NewCompositor(c, int32(v), id, compositorHandler{}); err != nil {
233+ t.Error(err)
234+ }
235+ }))
236+ if !s.cfg.NoCopyCapture {
237+ must(extimagecapturesource.NewExtOutputImageCaptureSourceManagerV1Global(s.d, 1, func(c server.Client, v, id uint32) {
238+ if _, err := extimagecapturesource.NewExtOutputImageCaptureSourceManagerV1(c, int32(v), id, outputSourceManager{s}); err != nil {
239+ t.Error(err)
240+ }
241+ }))
242+ must(extimagecopycapture.NewExtImageCopyCaptureManagerV1Global(s.d, 1, func(c server.Client, v, id uint32) {
243+ if _, err := extimagecopycapture.NewExtImageCopyCaptureManagerV1(c, int32(v), id, copyManager{s}); err != nil {
244+ t.Error(err)
245+ }
246+ }))
247+ }
248+ if s.cfg.HasWorkspaces {
249+ for _, w := range s.cfg.Workspaces {
250+ s.workspaces = append(s.workspaces, &workspace{spec: w})
251+ }
252+ must(extworkspace.NewExtWorkspaceManagerV1Global(s.d, 1, func(c server.Client, v, id uint32) {
253+ m := &wsManager{s: s}
254+ res, err := extworkspace.NewExtWorkspaceManagerV1(c, int32(v), id, m)
255+ if err != nil {
256+ t.Error(err)
257+ return
258+ }
259+ m.res = res
260+ s.wsMgrs = append(s.wsMgrs, m)
261+ m.announce(c)
262+ }))
263+ }
264+ if s.cfg.NeferwlSource {
265+ must(neferwl.NewNeferwlImageCaptureSourceManagerV1Global(s.d, 1, func(c server.Client, v, id uint32) {
266+ if _, err := neferwl.NewNeferwlImageCaptureSourceManagerV1(c, int32(v), id, neferwlSources{s}); err != nil {
267+ t.Error(err)
268+ }
269+ }))
270+ }
271+ if s.cfg.NeferwlExclusion {
272+ must(neferwl.NewNeferwlCaptureExclusionManagerV1Global(s.d, 1, func(c server.Client, v, id uint32) {
273+ if _, err := neferwl.NewNeferwlCaptureExclusionManagerV1(c, int32(v), id, exclusionManager{s}); err != nil {
274+ t.Error(err)
275+ }
276+ }))
277+ }
278+}
279+
280+type compositorHandler struct{}
281+
282+func (compositorHandler) CreateSurface(self *wayland.Compositor, id uint32) {
283+ _, _ = wayland.NewSurface(self.Client(), 4, id, surfaceHandler{})
284+}
285+func (compositorHandler) CreateRegion(*wayland.Compositor, uint32) {}
286+func (compositorHandler) Release(*wayland.Compositor) {}
287+
288+type surfaceHandler struct{}
289+
290+func (surfaceHandler) Destroy(*wayland.Surface) {}
291+func (surfaceHandler) Attach(*wayland.Surface, *wayland.Buffer, int32, int32) {}
292+func (surfaceHandler) Damage(*wayland.Surface, int32, int32, int32, int32) {}
293+func (surfaceHandler) Frame(*wayland.Surface, uint32) {}
294+func (surfaceHandler) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {}
295+func (surfaceHandler) SetInputRegion(*wayland.Surface, *wayland.Region) {}
296+func (surfaceHandler) Commit(*wayland.Surface) {}
297+func (surfaceHandler) SetBufferTransform(*wayland.Surface, int32) {}
298+func (surfaceHandler) SetBufferScale(*wayland.Surface, int32) {}
299+func (surfaceHandler) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {}
300+func (surfaceHandler) Offset(*wayland.Surface, int32, int32) {}
301+func (surfaceHandler) GetRelease(*wayland.Surface, uint32) {}
@@ -0,0 +1,301 @@
1+// Package testserver is a minimal in-process Wayland compositor for tests: a
2+// real libwayland server (purego-libwayland) that speaks wl_output, wl_shm,
3+// ext-image-capture-source-v1, ext-image-copy-capture-v1, ext-workspace-v1 and
4+// NeferWL's capture extension. It is a protocol peer, not a substitute for a
5+// project type: clients connect to its socket like to any compositor.
6+//
7+// All server state is touched only on the display goroutine: handlers run
8+// there, and tests reach it through Do.
9+package testserver
10+
11+import (
12+ "context"
13+ "os"
14+ "path/filepath"
15+ "testing"
16+
17+ "github.com/bnema/purego-libwayland/protocol/extimagecapturesource"
18+ "github.com/bnema/purego-libwayland/protocol/extimagecopycapture"
19+ "github.com/bnema/purego-libwayland/protocol/extworkspace"
20+ "github.com/bnema/purego-libwayland/protocol/wayland"
21+ "github.com/bnema/purego-libwayland/server"
22+ "golang.org/x/sys/unix"
23+
24+ "github.com/bnema/nefercap/internal/adapters/wayland/testserver/neferwl"
25+)
26+
27+// Token is the exclusion token the server hands out.
28+const Token = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
29+
30+// wl_shm formats.
31+const (
32+ ARGB8888 = 0
33+ XRGB8888 = 1
34+)
35+
36+// OutputSpec describes one wl_output. The capture buffer of an output source
37+// is its mode size.
38+type OutputSpec struct {
39+ Name string
40+ Width, Height uint32
41+ Scale int32
42+ Version uint32 // 0 selects 4
43+ // LogicalWidth/Height, when set, are announced through xdg-output.
44+ LogicalWidth, LogicalHeight int32
45+}
46+
47+// WorkspaceSpec describes one ext-workspace handle; all of them live in one
48+// group that holds every output.
49+type WorkspaceSpec struct {
50+ ID string // empty: a temporary workspace, no id event
51+ Name string
52+ Output string // the output whose buffer size a workspace source has
53+ Active bool
54+ Hidden bool
55+ // Frame is the output-local logical rectangle (x, y, width, height) that
56+ // neferwl_workspace_frame_v1 reports; zero reports nothing.
57+ Frame [4]int32
58+}
59+
60+// Config selects what the server offers.
61+type Config struct {
62+ Outputs []OutputSpec // default: one 8x4 output "TEST-1"
63+ // NoCopyCapture leaves out ext-image-copy-capture and the output source.
64+ NoCopyCapture bool
65+ Formats []uint32 // shm formats announced, default XRGB8888
66+ // NoShmFormat announces size and done but no shm_format.
67+ NoShmFormat bool
68+ // Hold keeps every captured frame pending until Release.
69+ Hold bool
70+ // HoldAfterFirst answers the first frame of each session at once and holds
71+ // the following ones until Release, like a compositor that waits for
72+ // damage on a static screen.
73+ HoldAfterFirst bool
74+ // Workspaces enables ext-workspace-v1 when non-nil (possibly empty).
75+ Workspaces []WorkspaceSpec
76+ HasWorkspaces bool
77+ // NeferwlSource and NeferwlExclusion enable the extension's two globals.
78+ NeferwlSource bool
79+ NeferwlExclusion bool
80+ // ExclusionFail answers get_exclusion with failed(ExclusionFailReason-1)
81+ // when non-zero.
82+ ExclusionFail uint32
83+ // AttachFail answers attach_surface with failed(AttachFail-1) when non-zero.
84+ AttachFail uint32
85+ // AttachSilent never answers attach_surface.
86+ AttachSilent bool
87+ // StopAtCreate answers every create_session with stopped and no
88+ // constraints, like a compositor that refuses the client.
89+ StopAtCreate bool
90+ // Transform is reported by every ready frame (wl_output.transform).
91+ Transform uint32
92+ // FailFrames answers that many capture requests with failed(FailReason).
93+ FailFrames int
94+ FailReason uint32
95+}
96+
97+// Stats are counters observed by tests.
98+type Stats struct {
99+ Sessions, Frames, Captures, Ready int
100+ DestroyedSessions int
101+ Buffers, DestroyedBuffers int
102+ LastSource string // "output", "region", "workspace"
103+ LastRegion [4]int32
104+ Exclusions int
105+ Attached []string
106+ DestroyedExclusions int
107+ WorkspaceFrames int // neferwl_workspace_frame_v1 objects created
108+ DestroyedWorkspaceFrames int
109+ DestroyedXdgOutputs int
110+}
111+
112+// Server is a running test compositor.
113+type Server struct {
114+ // Path is the socket path for wayland.New.
115+ Path string
116+
117+ d *server.Display
118+ cfg Config
119+
120+ outputs []*output
121+ bufs map[*server.Resource]*buffer
122+ sources map[*server.Resource]*source
123+ sessions []*session
124+ workspaces []*workspace
125+ wsMgrs []*wsManager
126+ layers []*server.Resource
127+ frames []*frameObject
128+ stats Stats
129+ held []*frame
130+ seq uint32
131+}
132+
133+type output struct {
134+ spec OutputSpec
135+ global *server.Global
136+ res []*wayland.Output
137+ removed bool
138+}
139+
140+// Start runs a compositor on a fresh socket until the test ends.
141+func Start(t testing.TB, cfg Config) *Server {
142+ t.Helper()
143+ if len(cfg.Outputs) == 0 {
144+ cfg.Outputs = []OutputSpec{{Name: "TEST-1", Width: 8, Height: 4, Scale: 1}}
145+ }
146+ if len(cfg.Formats) == 0 {
147+ cfg.Formats = []uint32{XRGB8888}
148+ }
149+ d, err := server.NewDisplay()
150+ if err != nil {
151+ // NEFERCAP_REQUIRE_WAYLAND=1 (set by make test and make race) turns a
152+ // missing libwayland-server into a failure, so the suite cannot pass
153+ // without its protocol tests.
154+ if os.Getenv("NEFERCAP_REQUIRE_WAYLAND") == "1" {
155+ t.Fatalf("libwayland-server unavailable: %v", err)
156+ }
157+ t.Skipf("libwayland-server unavailable: %v", err)
158+ }
159+ dir, err := filepath.EvalSymlinks(t.TempDir())
160+ if err != nil {
161+ t.Fatal(err)
162+ }
163+ path := filepath.Join(dir, "wl-test")
164+ fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
165+ if err != nil {
166+ t.Fatal(err)
167+ }
168+ if err := unix.Bind(fd, &unix.SockaddrUnix{Name: path}); err != nil {
169+ t.Fatal(err)
170+ }
171+ if err := unix.Listen(fd, 16); err != nil {
172+ t.Fatal(err)
173+ }
174+ if err := d.AddSocketFD(fd); err != nil { // libwayland owns fd now
175+ t.Fatal(err)
176+ }
177+ s := &Server{Path: path, d: d, cfg: cfg, bufs: map[*server.Resource]*buffer{}, sources: map[*server.Resource]*source{}}
178+ ctx, cancel := context.WithCancel(context.Background())
179+ done := make(chan error, 1)
180+ go func() { done <- d.Run(ctx) }()
181+ if !d.Do(func() { s.setup(t) }) {
182+ t.Fatal("test compositor stopped")
183+ }
184+ t.Cleanup(func() {
185+ cancel()
186+ <-done
187+ })
188+ return s
189+}
190+
191+// Do runs f on the display goroutine, where server state may be touched.
192+func (s *Server) Do(f func()) {
193+ if !s.d.Do(f) {
194+ // stopped: nothing to observe
195+ return
196+ }
197+}
198+
199+// Stats returns a snapshot of the counters.
200+func (s *Server) Stats() Stats {
201+ var st Stats
202+ s.Do(func() {
203+ st = s.stats
204+ st.Attached = append([]string(nil), s.stats.Attached...)
205+ })
206+ return st
207+}
208+
209+func (s *Server) setup(t testing.TB) {
210+ must := func(err error) {
211+ if err != nil {
212+ t.Error(err)
213+ }
214+ }
215+ for _, spec := range s.cfg.Outputs {
216+ s.addOutput(spec, must)
217+ }
218+ must(wayland.NewShmGlobal(s.d, 1, func(c server.Client, v, id uint32) {
219+ shm, err := wayland.NewShm(c, int32(v), id, shmHandler{s})
220+ if err != nil {
221+ t.Error(err)
222+ return
223+ }
224+ for _, f := range []uint32{ARGB8888, XRGB8888} {
225+ shm.SendFormat(f)
226+ }
227+ }))
228+ if s.xdgOutputWanted() {
229+ s.addXdgOutput(must)
230+ }
231+ must(wayland.NewCompositorGlobal(s.d, 4, func(c server.Client, v, id uint32) {
232+ if _, err := wayland.NewCompositor(c, int32(v), id, compositorHandler{}); err != nil {
233+ t.Error(err)
234+ }
235+ }))
236+ if !s.cfg.NoCopyCapture {
237+ must(extimagecapturesource.NewExtOutputImageCaptureSourceManagerV1Global(s.d, 1, func(c server.Client, v, id uint32) {
238+ if _, err := extimagecapturesource.NewExtOutputImageCaptureSourceManagerV1(c, int32(v), id, outputSourceManager{s}); err != nil {
239+ t.Error(err)
240+ }
241+ }))
242+ must(extimagecopycapture.NewExtImageCopyCaptureManagerV1Global(s.d, 1, func(c server.Client, v, id uint32) {
243+ if _, err := extimagecopycapture.NewExtImageCopyCaptureManagerV1(c, int32(v), id, copyManager{s}); err != nil {
244+ t.Error(err)
245+ }
246+ }))
247+ }
248+ if s.cfg.HasWorkspaces {
249+ for _, w := range s.cfg.Workspaces {
250+ s.workspaces = append(s.workspaces, &workspace{spec: w})
251+ }
252+ must(extworkspace.NewExtWorkspaceManagerV1Global(s.d, 1, func(c server.Client, v, id uint32) {
253+ m := &wsManager{s: s}
254+ res, err := extworkspace.NewExtWorkspaceManagerV1(c, int32(v), id, m)
255+ if err != nil {
256+ t.Error(err)
257+ return
258+ }
259+ m.res = res
260+ s.wsMgrs = append(s.wsMgrs, m)
261+ m.announce(c)
262+ }))
263+ }
264+ if s.cfg.NeferwlSource {
265+ must(neferwl.NewNeferwlImageCaptureSourceManagerV1Global(s.d, 1, func(c server.Client, v, id uint32) {
266+ if _, err := neferwl.NewNeferwlImageCaptureSourceManagerV1(c, int32(v), id, neferwlSources{s}); err != nil {
267+ t.Error(err)
268+ }
269+ }))
270+ }
271+ if s.cfg.NeferwlExclusion {
272+ must(neferwl.NewNeferwlCaptureExclusionManagerV1Global(s.d, 1, func(c server.Client, v, id uint32) {
273+ if _, err := neferwl.NewNeferwlCaptureExclusionManagerV1(c, int32(v), id, exclusionManager{s}); err != nil {
274+ t.Error(err)
275+ }
276+ }))
277+ }
278+}
279+
280+type compositorHandler struct{}
281+
282+func (compositorHandler) CreateSurface(self *wayland.Compositor, id uint32) {
283+ _, _ = wayland.NewSurface(self.Client(), 4, id, surfaceHandler{})
284+}
285+func (compositorHandler) CreateRegion(*wayland.Compositor, uint32) {}
286+func (compositorHandler) Release(*wayland.Compositor) {}
287+
288+type surfaceHandler struct{}
289+
290+func (surfaceHandler) Destroy(*wayland.Surface) {}
291+func (surfaceHandler) Attach(*wayland.Surface, *wayland.Buffer, int32, int32) {}
292+func (surfaceHandler) Damage(*wayland.Surface, int32, int32, int32, int32) {}
293+func (surfaceHandler) Frame(*wayland.Surface, uint32) {}
294+func (surfaceHandler) SetOpaqueRegion(*wayland.Surface, *wayland.Region) {}
295+func (surfaceHandler) SetInputRegion(*wayland.Surface, *wayland.Region) {}
296+func (surfaceHandler) Commit(*wayland.Surface) {}
297+func (surfaceHandler) SetBufferTransform(*wayland.Surface, int32) {}
298+func (surfaceHandler) SetBufferScale(*wayland.Surface, int32) {}
299+func (surfaceHandler) DamageBuffer(*wayland.Surface, int32, int32, int32, int32) {}
300+func (surfaceHandler) Offset(*wayland.Surface, int32, int32) {}
301+func (surfaceHandler) GetRelease(*wayland.Surface, uint32) {}
A internal/adapters/wayland/testserver/workspaces.go +239 -0
@@ -0,0 +1,239 @@
1+package testserver
2+
3+import (
4+ "encoding/binary"
5+
6+ "github.com/bnema/purego-libwayland/protocol/extworkspace"
7+ "github.com/bnema/purego-libwayland/protocol/wayland"
8+ "github.com/bnema/purego-libwayland/server"
9+
10+ "github.com/bnema/nefercap/internal/adapters/wayland/testserver/neferwl"
11+)
12+
13+type workspace struct {
14+ spec WorkspaceSpec
15+ removed bool
16+}
17+
18+type wsHandle struct {
19+ w *workspace
20+ res *extworkspace.ExtWorkspaceHandleV1
21+}
22+
23+type wsManager struct {
24+ s *Server
25+ res *extworkspace.ExtWorkspaceManagerV1
26+ group *extworkspace.ExtWorkspaceGroupHandleV1
27+ handles []*wsHandle
28+}
29+
30+func (m *wsManager) Commit(*extworkspace.ExtWorkspaceManagerV1) {}
31+func (m *wsManager) Stop(*extworkspace.ExtWorkspaceManagerV1) {}
32+
33+type groupHandler struct{}
34+
35+func (groupHandler) CreateWorkspace(*extworkspace.ExtWorkspaceGroupHandleV1, string) {}
36+func (groupHandler) Destroy(*extworkspace.ExtWorkspaceGroupHandleV1) {}
37+
38+type handleHandler struct{}
39+
40+func (handleHandler) Destroy(*extworkspace.ExtWorkspaceHandleV1) {}
41+func (handleHandler) Activate(*extworkspace.ExtWorkspaceHandleV1) {}
42+func (handleHandler) Deactivate(*extworkspace.ExtWorkspaceHandleV1) {}
43+func (handleHandler) Assign(*extworkspace.ExtWorkspaceHandleV1, *extworkspace.ExtWorkspaceGroupHandleV1) {
44+}
45+func (handleHandler) Remove(*extworkspace.ExtWorkspaceHandleV1) {}
46+
47+func stateBits(w WorkspaceSpec) uint32 {
48+ var st uint32
49+ if w.Active {
50+ st |= uint32(extworkspace.ExtWorkspaceHandleV1StateActive)
51+ }
52+ if w.Hidden {
53+ st |= uint32(extworkspace.ExtWorkspaceHandleV1StateHidden)
54+ }
55+ return st
56+}
57+
58+// announce sends the group, its outputs and every workspace, then done.
59+func (m *wsManager) announce(c server.Client) {
60+ g, err := extworkspace.NewExtWorkspaceGroupHandleV1(c, 1, 0, groupHandler{})
61+ if err != nil {
62+ return
63+ }
64+ m.group = g
65+ m.res.SendWorkspaceGroup(g)
66+ g.SendCapabilities(0)
67+ for _, o := range m.s.outputs {
68+ for _, r := range o.res {
69+ if r.Client() == c {
70+ g.SendOutputEnter(r)
71+ }
72+ }
73+ }
74+ for i, w := range m.s.workspaces {
75+ if !w.removed {
76+ m.add(c, g, w, i)
77+ }
78+ }
79+ m.res.SendDone()
80+}
81+
82+func (m *wsManager) add(c server.Client, g *extworkspace.ExtWorkspaceGroupHandleV1, w *workspace, index int) {
83+ r, err := extworkspace.NewExtWorkspaceHandleV1(c, 1, 0, handleHandler{})
84+ if err != nil {
85+ return
86+ }
87+ m.handles = append(m.handles, &wsHandle{w: w, res: r})
88+ m.res.SendWorkspace(r)
89+ if w.spec.ID != "" {
90+ r.SendId(w.spec.ID)
91+ }
92+ r.SendName(w.spec.Name)
93+ var b [4]byte
94+ binary.NativeEndian.PutUint32(b[:], uint32(index))
95+ r.SendCoordinates(b[:])
96+ r.SendState(stateBits(w.spec))
97+ r.SendCapabilities(0)
98+ g.SendWorkspaceEnter(r)
99+}
100+
101+func (s *Server) workspaceNamed(name string) *workspace {
102+ for _, w := range s.workspaces {
103+ if w.spec.Name == name {
104+ return w
105+ }
106+ }
107+ return nil
108+}
109+
110+// SetActive changes the active state of the named workspace.
111+func (s *Server) SetActive(name string, active bool) {
112+ s.Do(func() {
113+ w := s.workspaceNamed(name)
114+ if w == nil {
115+ return
116+ }
117+ w.spec.Active = active
118+ for _, m := range s.wsMgrs {
119+ for _, h := range m.handles {
120+ if h.w == w && !w.removed {
121+ h.res.SendState(stateBits(w.spec))
122+ }
123+ }
124+ m.res.SendDone()
125+ }
126+ })
127+}
128+
129+// RemoveWorkspace removes the named workspace from every manager.
130+func (s *Server) RemoveWorkspace(name string) {
131+ s.Do(func() {
132+ w := s.workspaceNamed(name)
133+ if w == nil || w.removed {
134+ return
135+ }
136+ w.removed = true
137+ for _, m := range s.wsMgrs {
138+ for _, h := range m.handles {
139+ if h.w == w {
140+ m.group.SendWorkspaceLeave(h.res)
141+ h.res.SendRemoved()
142+ }
143+ }
144+ m.res.SendDone()
145+ }
146+ for _, c := range s.sessions {
147+ if c.src.ws == w {
148+ c.stop()
149+ }
150+ }
151+ })
152+}
153+
154+// workspaceOf finds the workspace behind a handle resource.
155+func (s *Server) workspaceOf(r *server.Resource) *workspace {
156+ for _, m := range s.wsMgrs {
157+ for _, h := range m.handles {
158+ if h.res.Resource == r {
159+ return h.w
160+ }
161+ }
162+ }
163+ return nil
164+}
165+
166+type neferwlSources struct{ s *Server }
167+
168+// frameObject is one neferwl_workspace_frame_v1.
169+type frameObject struct {
170+ w *workspace
171+ res *neferwl.NeferwlWorkspaceFrameV1
172+}
173+
174+func (frameObject) Destroy(*neferwl.NeferwlWorkspaceFrameV1) {}
175+
176+func (f *frameObject) send() {
177+ if r := f.w.spec.Frame; r != ([4]int32{}) {
178+ f.res.SendFrame(r[0], r[1], r[2], r[3])
179+ }
180+}
181+
182+func (m neferwlSources) GetWorkspaceFrame(self *neferwl.NeferwlImageCaptureSourceManagerV1, id uint32, h *extworkspace.ExtWorkspaceHandleV1) {
183+ s := m.s
184+ f := &frameObject{w: s.workspaceOf(h.Resource)}
185+ res, err := neferwl.NewNeferwlWorkspaceFrameV1(self.Client(), 1, id, f)
186+ if err != nil {
187+ return
188+ }
189+ f.res = res
190+ s.stats.WorkspaceFrames++
191+ res.OnDestroy = func() {
192+ s.stats.DestroyedWorkspaceFrames++
193+ s.frames = removeFrame(s.frames, f)
194+ }
195+ if f.w == nil || f.w.removed {
196+ return
197+ }
198+ s.frames = append(s.frames, f)
199+ f.send()
200+}
201+
202+func removeFrame(list []*frameObject, f *frameObject) []*frameObject {
203+ for i, g := range list {
204+ if g == f {
205+ return append(list[:i], list[i+1:]...)
206+ }
207+ }
208+ return list
209+}
210+
211+// SetFrame changes the frame of the named workspace and reports it to every
212+// frame object that watches it.
213+func (s *Server) SetFrame(name string, frame [4]int32) {
214+ s.Do(func() {
215+ w := s.workspaceNamed(name)
216+ if w == nil {
217+ return
218+ }
219+ w.spec.Frame = frame
220+ for _, f := range s.frames {
221+ if f.w == w && !w.removed {
222+ f.send()
223+ }
224+ }
225+ })
226+}
227+
228+func (neferwlSources) Destroy(*neferwl.NeferwlImageCaptureSourceManagerV1) {}
229+func (m neferwlSources) CreateWorkspaceSource(self *neferwl.NeferwlImageCaptureSourceManagerV1, id uint32, h *extworkspace.ExtWorkspaceHandleV1) {
230+ w := m.s.workspaceOf(h.Resource)
231+ m.s.newSource(self.Client(), id, &source{kind: "workspace", ws: w, gone: w == nil || w.removed})
232+}
233+func (m neferwlSources) CreateOutputRegionSource(self *neferwl.NeferwlImageCaptureSourceManagerV1, id uint32, out *wayland.Output, x, y, w, h int32) {
234+ if w < 1 || h < 1 {
235+ self.PostError(uint32(neferwl.NeferwlImageCaptureSourceManagerV1ErrorInvalidRegion), "empty region")
236+ return
237+ }
238+ m.s.newSource(self.Client(), id, &source{kind: "region", out: m.s.outputOf(out.Resource), region: [4]int32{x, y, w, h}})
239+}
@@ -0,0 +1,239 @@
1+package testserver
2+
3+import (
4+ "encoding/binary"
5+
6+ "github.com/bnema/purego-libwayland/protocol/extworkspace"
7+ "github.com/bnema/purego-libwayland/protocol/wayland"
8+ "github.com/bnema/purego-libwayland/server"
9+
10+ "github.com/bnema/nefercap/internal/adapters/wayland/testserver/neferwl"
11+)
12+
13+type workspace struct {
14+ spec WorkspaceSpec
15+ removed bool
16+}
17+
18+type wsHandle struct {
19+ w *workspace
20+ res *extworkspace.ExtWorkspaceHandleV1
21+}
22+
23+type wsManager struct {
24+ s *Server
25+ res *extworkspace.ExtWorkspaceManagerV1
26+ group *extworkspace.ExtWorkspaceGroupHandleV1
27+ handles []*wsHandle
28+}
29+
30+func (m *wsManager) Commit(*extworkspace.ExtWorkspaceManagerV1) {}
31+func (m *wsManager) Stop(*extworkspace.ExtWorkspaceManagerV1) {}
32+
33+type groupHandler struct{}
34+
35+func (groupHandler) CreateWorkspace(*extworkspace.ExtWorkspaceGroupHandleV1, string) {}
36+func (groupHandler) Destroy(*extworkspace.ExtWorkspaceGroupHandleV1) {}
37+
38+type handleHandler struct{}
39+
40+func (handleHandler) Destroy(*extworkspace.ExtWorkspaceHandleV1) {}
41+func (handleHandler) Activate(*extworkspace.ExtWorkspaceHandleV1) {}
42+func (handleHandler) Deactivate(*extworkspace.ExtWorkspaceHandleV1) {}
43+func (handleHandler) Assign(*extworkspace.ExtWorkspaceHandleV1, *extworkspace.ExtWorkspaceGroupHandleV1) {
44+}
45+func (handleHandler) Remove(*extworkspace.ExtWorkspaceHandleV1) {}
46+
47+func stateBits(w WorkspaceSpec) uint32 {
48+ var st uint32
49+ if w.Active {
50+ st |= uint32(extworkspace.ExtWorkspaceHandleV1StateActive)
51+ }
52+ if w.Hidden {
53+ st |= uint32(extworkspace.ExtWorkspaceHandleV1StateHidden)
54+ }
55+ return st
56+}
57+
58+// announce sends the group, its outputs and every workspace, then done.
59+func (m *wsManager) announce(c server.Client) {
60+ g, err := extworkspace.NewExtWorkspaceGroupHandleV1(c, 1, 0, groupHandler{})
61+ if err != nil {
62+ return
63+ }
64+ m.group = g
65+ m.res.SendWorkspaceGroup(g)
66+ g.SendCapabilities(0)
67+ for _, o := range m.s.outputs {
68+ for _, r := range o.res {
69+ if r.Client() == c {
70+ g.SendOutputEnter(r)
71+ }
72+ }
73+ }
74+ for i, w := range m.s.workspaces {
75+ if !w.removed {
76+ m.add(c, g, w, i)
77+ }
78+ }
79+ m.res.SendDone()
80+}
81+
82+func (m *wsManager) add(c server.Client, g *extworkspace.ExtWorkspaceGroupHandleV1, w *workspace, index int) {
83+ r, err := extworkspace.NewExtWorkspaceHandleV1(c, 1, 0, handleHandler{})
84+ if err != nil {
85+ return
86+ }
87+ m.handles = append(m.handles, &wsHandle{w: w, res: r})
88+ m.res.SendWorkspace(r)
89+ if w.spec.ID != "" {
90+ r.SendId(w.spec.ID)
91+ }
92+ r.SendName(w.spec.Name)
93+ var b [4]byte
94+ binary.NativeEndian.PutUint32(b[:], uint32(index))
95+ r.SendCoordinates(b[:])
96+ r.SendState(stateBits(w.spec))
97+ r.SendCapabilities(0)
98+ g.SendWorkspaceEnter(r)
99+}
100+
101+func (s *Server) workspaceNamed(name string) *workspace {
102+ for _, w := range s.workspaces {
103+ if w.spec.Name == name {
104+ return w
105+ }
106+ }
107+ return nil
108+}
109+
110+// SetActive changes the active state of the named workspace.
111+func (s *Server) SetActive(name string, active bool) {
112+ s.Do(func() {
113+ w := s.workspaceNamed(name)
114+ if w == nil {
115+ return
116+ }
117+ w.spec.Active = active
118+ for _, m := range s.wsMgrs {
119+ for _, h := range m.handles {
120+ if h.w == w && !w.removed {
121+ h.res.SendState(stateBits(w.spec))
122+ }
123+ }
124+ m.res.SendDone()
125+ }
126+ })
127+}
128+
129+// RemoveWorkspace removes the named workspace from every manager.
130+func (s *Server) RemoveWorkspace(name string) {
131+ s.Do(func() {
132+ w := s.workspaceNamed(name)
133+ if w == nil || w.removed {
134+ return
135+ }
136+ w.removed = true
137+ for _, m := range s.wsMgrs {
138+ for _, h := range m.handles {
139+ if h.w == w {
140+ m.group.SendWorkspaceLeave(h.res)
141+ h.res.SendRemoved()
142+ }
143+ }
144+ m.res.SendDone()
145+ }
146+ for _, c := range s.sessions {
147+ if c.src.ws == w {
148+ c.stop()
149+ }
150+ }
151+ })
152+}
153+
154+// workspaceOf finds the workspace behind a handle resource.
155+func (s *Server) workspaceOf(r *server.Resource) *workspace {
156+ for _, m := range s.wsMgrs {
157+ for _, h := range m.handles {
158+ if h.res.Resource == r {
159+ return h.w
160+ }
161+ }
162+ }
163+ return nil
164+}
165+
166+type neferwlSources struct{ s *Server }
167+
168+// frameObject is one neferwl_workspace_frame_v1.
169+type frameObject struct {
170+ w *workspace
171+ res *neferwl.NeferwlWorkspaceFrameV1
172+}
173+
174+func (frameObject) Destroy(*neferwl.NeferwlWorkspaceFrameV1) {}
175+
176+func (f *frameObject) send() {
177+ if r := f.w.spec.Frame; r != ([4]int32{}) {
178+ f.res.SendFrame(r[0], r[1], r[2], r[3])
179+ }
180+}
181+
182+func (m neferwlSources) GetWorkspaceFrame(self *neferwl.NeferwlImageCaptureSourceManagerV1, id uint32, h *extworkspace.ExtWorkspaceHandleV1) {
183+ s := m.s
184+ f := &frameObject{w: s.workspaceOf(h.Resource)}
185+ res, err := neferwl.NewNeferwlWorkspaceFrameV1(self.Client(), 1, id, f)
186+ if err != nil {
187+ return
188+ }
189+ f.res = res
190+ s.stats.WorkspaceFrames++
191+ res.OnDestroy = func() {
192+ s.stats.DestroyedWorkspaceFrames++
193+ s.frames = removeFrame(s.frames, f)
194+ }
195+ if f.w == nil || f.w.removed {
196+ return
197+ }
198+ s.frames = append(s.frames, f)
199+ f.send()
200+}
201+
202+func removeFrame(list []*frameObject, f *frameObject) []*frameObject {
203+ for i, g := range list {
204+ if g == f {
205+ return append(list[:i], list[i+1:]...)
206+ }
207+ }
208+ return list
209+}
210+
211+// SetFrame changes the frame of the named workspace and reports it to every
212+// frame object that watches it.
213+func (s *Server) SetFrame(name string, frame [4]int32) {
214+ s.Do(func() {
215+ w := s.workspaceNamed(name)
216+ if w == nil {
217+ return
218+ }
219+ w.spec.Frame = frame
220+ for _, f := range s.frames {
221+ if f.w == w && !w.removed {
222+ f.send()
223+ }
224+ }
225+ })
226+}
227+
228+func (neferwlSources) Destroy(*neferwl.NeferwlImageCaptureSourceManagerV1) {}
229+func (m neferwlSources) CreateWorkspaceSource(self *neferwl.NeferwlImageCaptureSourceManagerV1, id uint32, h *extworkspace.ExtWorkspaceHandleV1) {
230+ w := m.s.workspaceOf(h.Resource)
231+ m.s.newSource(self.Client(), id, &source{kind: "workspace", ws: w, gone: w == nil || w.removed})
232+}
233+func (m neferwlSources) CreateOutputRegionSource(self *neferwl.NeferwlImageCaptureSourceManagerV1, id uint32, out *wayland.Output, x, y, w, h int32) {
234+ if w < 1 || h < 1 {
235+ self.PostError(uint32(neferwl.NeferwlImageCaptureSourceManagerV1ErrorInvalidRegion), "empty region")
236+ return
237+ }
238+ m.s.newSource(self.Client(), id, &source{kind: "region", out: m.s.outputOf(out.Resource), region: [4]int32{x, y, w, h}})
239+}
A internal/adapters/wayland/testserver/xdgoutput.go +45 -0
@@ -0,0 +1,45 @@
1+package testserver
2+
3+import (
4+ "github.com/bnema/purego-libwayland/protocol/wayland"
5+ "github.com/bnema/purego-libwayland/protocol/xdgoutput"
6+ "github.com/bnema/purego-libwayland/server"
7+)
8+
9+// xdg-output is offered only when an output announces a logical size.
10+func (s *Server) xdgOutputWanted() bool {
11+ for _, o := range s.outputs {
12+ if o.spec.LogicalWidth > 0 {
13+ return true
14+ }
15+ }
16+ return false
17+}
18+
19+func (s *Server) addXdgOutput(must func(error)) {
20+ must(xdgoutput.NewZxdgOutputManagerV1Global(s.d, 3, func(c server.Client, v, id uint32) {
21+ if _, err := xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgManager{s}); err != nil {
22+ must(err)
23+ }
24+ }))
25+}
26+
27+type xdgManager struct{ s *Server }
28+
29+func (xdgManager) Destroy(*xdgoutput.ZxdgOutputManagerV1) {}
30+func (m xdgManager) GetXdgOutput(self *xdgoutput.ZxdgOutputManagerV1, id uint32, out *wayland.Output) {
31+ res, err := xdgoutput.NewZxdgOutputV1(self.Client(), int32(self.Version()), id, xdgHandler{})
32+ if err != nil {
33+ return
34+ }
35+ res.OnDestroy = func() { m.s.stats.DestroyedXdgOutputs++ }
36+ if o := m.s.outputOf(out.Resource); o != nil && o.spec.LogicalWidth > 0 {
37+ res.SendLogicalPosition(0, 0)
38+ res.SendLogicalSize(o.spec.LogicalWidth, o.spec.LogicalHeight)
39+ res.SendDone()
40+ }
41+}
42+
43+type xdgHandler struct{}
44+
45+func (xdgHandler) Destroy(*xdgoutput.ZxdgOutputV1) {}
@@ -0,0 +1,45 @@
1+package testserver
2+
3+import (
4+ "github.com/bnema/purego-libwayland/protocol/wayland"
5+ "github.com/bnema/purego-libwayland/protocol/xdgoutput"
6+ "github.com/bnema/purego-libwayland/server"
7+)
8+
9+// xdg-output is offered only when an output announces a logical size.
10+func (s *Server) xdgOutputWanted() bool {
11+ for _, o := range s.outputs {
12+ if o.spec.LogicalWidth > 0 {
13+ return true
14+ }
15+ }
16+ return false
17+}
18+
19+func (s *Server) addXdgOutput(must func(error)) {
20+ must(xdgoutput.NewZxdgOutputManagerV1Global(s.d, 3, func(c server.Client, v, id uint32) {
21+ if _, err := xdgoutput.NewZxdgOutputManagerV1(c, int32(v), id, xdgManager{s}); err != nil {
22+ must(err)
23+ }
24+ }))
25+}
26+
27+type xdgManager struct{ s *Server }
28+
29+func (xdgManager) Destroy(*xdgoutput.ZxdgOutputManagerV1) {}
30+func (m xdgManager) GetXdgOutput(self *xdgoutput.ZxdgOutputManagerV1, id uint32, out *wayland.Output) {
31+ res, err := xdgoutput.NewZxdgOutputV1(self.Client(), int32(self.Version()), id, xdgHandler{})
32+ if err != nil {
33+ return
34+ }
35+ res.OnDestroy = func() { m.s.stats.DestroyedXdgOutputs++ }
36+ if o := m.s.outputOf(out.Resource); o != nil && o.spec.LogicalWidth > 0 {
37+ res.SendLogicalPosition(0, 0)
38+ res.SendLogicalSize(o.spec.LogicalWidth, o.spec.LogicalHeight)
39+ res.SendDone()
40+ }
41+}
42+
43+type xdgHandler struct{}
44+
45+func (xdgHandler) Destroy(*xdgoutput.ZxdgOutputV1) {}

Keyboard shortcuts

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

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

Branches 2

Tags 9

↑↓ moveenter openesc close