detached dbe6e8a

refactor(wayland): use wlturbo v0.5.0 generated protocol proxies

Bump wlturbo to v0.5.0 and nefergui to v0.3.0, the versions NeferWL pins,
and replace the hand-written ext-image-copy-capture, ext-image-capture-source,
ext-workspace and xdg-output proxies, opcodes and event signatures with the
generated bindings. Only the NeferWL extension keeps hand-written proxies.
commit
dbe6e8ae3313c51d164b4675d57baf357c30b6c3
author
brice · · 2026-10-01 01:44 UTC
committer
brice · · 2026-10-01 02:26 UTC
parent
248b1e3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +212 -320
M go.mod +3 -3
@@ -3,9 +3,9 @@ module github.com/bnema/nefercap
33 go 1.27
44
55 require (
6- github.com/bnema/nefergui v0.2.0
6+ github.com/bnema/nefergui v0.3.0
77 github.com/bnema/purego-libwayland v0.8.0
8- github.com/bnema/wlturbo v0.3.0
8+ github.com/bnema/wlturbo v0.5.0
99 github.com/bnema/zerowrap v1.4.1
1010 github.com/stretchr/testify v1.12.1
1111 golang.org/x/sys v0.48.0
@@ -14,7 +14,7 @@ require (
1414 require (
1515 github.com/bnema/purego v0.13.0-bnema.1 // indirect
1616 github.com/bnema/purego-vulkan v0.6.0 // indirect
17- github.com/bnema/purego-xkbcommon v0.0.0-20260928071113-e79265dfcdc2 // indirect
17+ github.com/bnema/purego-xkbcommon v0.1.0 // indirect
1818 github.com/go-text/typesetting v0.3.5 // indirect
1919 github.com/mattn/go-colorable v0.1.15 // indirect
2020 github.com/mattn/go-isatty v0.0.22 // indirect
@@ -3,9 +3,9 @@ module github.com/bnema/nefercap
3 go 1.273 go 1.27
4 4
5 require (5 require (
6- github.com/bnema/nefergui v0.2.06+ github.com/bnema/nefergui v0.3.0
7 github.com/bnema/purego-libwayland v0.8.07 github.com/bnema/purego-libwayland v0.8.0
8- github.com/bnema/wlturbo v0.3.08+ github.com/bnema/wlturbo v0.5.0
9 github.com/bnema/zerowrap v1.4.19 github.com/bnema/zerowrap v1.4.1
10 github.com/stretchr/testify v1.12.110 github.com/stretchr/testify v1.12.1
11 golang.org/x/sys v0.48.011 golang.org/x/sys v0.48.0
@@ -14,7 +14,7 @@ require (
14 require (14 require (
15 github.com/bnema/purego v0.13.0-bnema.1 // indirect15 github.com/bnema/purego v0.13.0-bnema.1 // indirect
16 github.com/bnema/purego-vulkan v0.6.0 // indirect16 github.com/bnema/purego-vulkan v0.6.0 // indirect
17- github.com/bnema/purego-xkbcommon v0.0.0-20260928071113-e79265dfcdc2 // indirect17+ github.com/bnema/purego-xkbcommon v0.1.0 // indirect
18 github.com/go-text/typesetting v0.3.5 // indirect18 github.com/go-text/typesetting v0.3.5 // indirect
19 github.com/mattn/go-colorable v0.1.15 // indirect19 github.com/mattn/go-colorable v0.1.15 // indirect
20 github.com/mattn/go-isatty v0.0.22 // indirect20 github.com/mattn/go-isatty v0.0.22 // indirect
M go.sum +6 -6 generated file: collapsed
M internal/adapters/wayland/copycapture.go +59 -111
@@ -5,7 +5,8 @@ import (
55 "fmt"
66 "time"
77
8- "github.com/bnema/wlturbo"
8+ "github.com/bnema/wlturbo/protocol/imagecapturesource"
9+ "github.com/bnema/wlturbo/protocol/imagecopycapture"
910
1011 "github.com/bnema/nefercap/internal/ports"
1112 )
@@ -16,40 +17,6 @@ const (
1617 shmXRGB8888 = uint32(ports.XRGB8888)
1718 )
1819
19-// Hand-written proxies for ext-image-capture-source-v1 and
20-// ext-image-copy-capture-v1: the wlturbo release in use does not generate
21-// them. Only the requests and events nefercap uses are declared.
22-
23-// requestOnly is a bound global or created object that never sends events.
24-type requestOnly struct{ wlturbo.BaseProxy }
25-
26-func (*requestOnly) EventSignature(uint16) (string, bool) { return "", false }
27-
28-func newRequestOnly(ctx *wlturbo.Context) *requestOnly {
29- p := &requestOnly{}
30- p.SetContext(ctx)
31- return p
32-}
33-
34-const (
35- reqOutputSourceCreate = 0 // ext_output_image_capture_source_manager_v1.create_source
36- reqSourceDestroy = 0 // ext_image_capture_source_v1.destroy
37- reqCopyCreateSession = 0 // ext_image_copy_capture_manager_v1.create_session
38- reqSessionCreateFrame = 0
39- reqSessionDestroy = 1
40- reqFrameDestroy = 0
41- reqFrameAttachBuffer = 1
42- reqFrameDamageBuffer = 2
43- reqFrameCapture = 3
44-)
45-
46-// ext_image_copy_capture_frame_v1.failure_reason
47-const (
48- failUnknown = 0
49- failBufferConstraints = 1
50- failStopped = 2
51-)
52-
5320 // maxFrameAttempts bounds the retries of one frame after failures that the
5421 // protocol calls recoverable.
5522 const maxFrameAttempts = 4
@@ -73,8 +40,8 @@ type constraints struct {
7340 // session is one ext_image_copy_capture_session_v1. Its events are dispatched
7441 // on the goroutine that owns the operation.
7542 type session struct {
76- wlturbo.BaseProxy
77- key sessionKey
43+ proxy *imagecopycapture.ExtImageCopyCaptureSession
44+ key sessionKey
7845
7946 pending constraints
8047 current constraints
@@ -88,7 +55,8 @@ type session struct {
8855 // lives in buffer cur and stays untouched while the next one is written to
8956 // the other buffer, so a Capture that finds the compositor idle (it waits
9057 // for damage after the first frame) can repeat the last frame at once.
91- inflight *frameProxy
58+ frame captureFrame // outcome of the in-flight frame, reset per frame
59+ inflight *captureFrame // &frame while a frame is in flight, else nil
9260 inflightBuf int
9361 inflightGeom bufGeom // buffer layout the in-flight frame was made for
9462 haveLast bool
@@ -96,30 +64,20 @@ type session struct {
9664 lastGeom bufGeom
9765 served int // frames handed out by this session
9866 fails int // consecutive failed frames
99-}
10067
101-func (*session) EventSignature(op uint16) (string, bool) {
102- switch op {
103- case 0:
104- return "uint,uint,", true // buffer_size
105- case 1:
106- return "uint,", true // shm_format
107- case 2:
108- return "array,", true // dmabuf_device
109- case 3:
110- return "uint,array,", true // dmabuf_format
111- case 4, 5:
112- return "", true // done, stopped
113- }
114- return "", false
68+ // Frame event handlers, built once and shared by every frame proxy.
69+ onTransform func(uint32)
70+ onReady func()
71+ onFailed func(uint32)
11572 }
11673
117-func (c *session) Dispatch(e *wlturbo.Event) {
118- switch e.Opcode {
119- case 0:
120- c.pending.width, c.pending.height, c.pending.haveSize = e.Uint32(), e.Uint32(), true
121- case 1:
122- format := e.Uint32()
74+// newSession wraps a created session proxy and subscribes to its events.
75+func newSession(proxy *imagecopycapture.ExtImageCopyCaptureSession, key sessionKey) *session {
76+ c := &session{proxy: proxy, key: key}
77+ proxy.OnBufferSize(func(width, height uint32) {
78+ c.pending.width, c.pending.height, c.pending.haveSize = width, height, true
79+ })
80+ proxy.OnShmFormat(func(format uint32) {
12381 c.pending.sawShm = true
12482 switch {
12583 case format != shmARGB8888 && format != shmXRGB8888:
@@ -127,46 +85,40 @@ func (c *session) Dispatch(e *wlturbo.Event) {
12785 case !c.pending.haveFormat:
12886 c.pending.format, c.pending.haveFormat = format, true
12987 }
130- case 4:
88+ })
89+ proxy.OnDone(func() {
13190 c.current, c.pending = c.pending, constraints{}
13291 c.gen++
133- case 5:
134- c.stopped = true
135- }
92+ })
93+ proxy.OnStopped(func() { c.stopped = true })
94+ c.onTransform = func(transform uint32) { c.frame.transform = transform }
95+ c.onReady = func() { c.frame.ready = true }
96+ c.onFailed = func(reason uint32) { c.frame.failed, c.frame.reason = true, reason }
97+ return c
13698 }
13799
138-// frameProxy is one ext_image_copy_capture_frame_v1.
139-type frameProxy struct {
140- wlturbo.BaseProxy
100+// captureFrame is one ext_image_copy_capture_frame_v1 and its outcome. The
101+// session owns a single one, reset for each frame.
102+type captureFrame struct {
103+ proxy *imagecopycapture.ExtImageCopyCaptureFrame
141104 transform uint32
142105 ready bool
143106 failed bool
144107 reason uint32
145108 }
146109
147-func (*frameProxy) EventSignature(op uint16) (string, bool) {
148- switch op {
149- case 0, 4:
150- return "uint,", true // transform, failed
151- case 1:
152- return "int,int,int,int,", true // damage
153- case 2:
154- return "uint,uint,uint,", true // presentation_time
155- case 3:
156- return "", true // ready
157- }
158- return "", false
159-}
160-
161-func (f *frameProxy) Dispatch(e *wlturbo.Event) {
162- switch e.Opcode {
163- case 0:
164- f.transform = e.Uint32()
165- case 3:
166- f.ready = true
167- case 4:
168- f.failed, f.reason = true, e.Uint32()
110+// newCaptureFrame creates the next frame of c, resetting its outcome, and
111+// subscribes the session's shared handlers to its events.
112+func newCaptureFrame(c *session) (*captureFrame, error) {
113+ proxy, err := c.proxy.CreateFrame()
114+ if err != nil {
115+ return nil, err
169116 }
117+ c.frame = captureFrame{proxy: proxy}
118+ proxy.OnTransform(c.onTransform)
119+ proxy.OnReady(c.onReady)
120+ proxy.OnFailed(c.onFailed)
121+ return &c.frame, nil
170122 }
171123
172124 // sessionKey identifies what a session captures. Crop sessions have a zero
@@ -236,30 +188,27 @@ func (s *Source) sessionFor(key sessionKey, out *output) (*session, error) {
236188 }
237189 s.dropSession()
238190 }
239- src := newRequestOnly(s.wl)
191+ var src *imagecapturesource.ExtImageCaptureSource
240192 var err error
241193 switch {
242194 case key.workspace != 0:
243- err = s.createWorkspaceSource(src, key.workspace)
195+ src, err = s.createWorkspaceSource(key.workspace)
244196 case key.region != (ports.Region{}):
245- r := key.region
246- err = s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqNeferwlRegionSource, Name: "neferwl_image_capture_source_manager_v1.create_output_region_source", Child: src},
247- src, out.proxy, int32(r.X), int32(r.Y), int32(r.Width), int32(r.Height))
197+ src, err = s.createRegionSource(out, key.region)
248198 default:
249- err = s.wl.Request(wlturbo.Request{Proxy: s.outSource, Opcode: reqOutputSourceCreate, Name: "ext_output_image_capture_source_manager_v1.create_source", Child: src}, src, out.proxy)
199+ src, err = s.outSource.CreateSource(out.proxy)
250200 }
251201 if err != nil {
252202 s.terminate()
253203 return nil, fmt.Errorf("wayland: create capture source: %w", err)
254204 }
255- c := &session{key: key}
256- c.SetContext(s.wl)
257- err = s.wl.Request(wlturbo.Request{Proxy: s.copyManager, Opcode: reqCopyCreateSession, Name: "ext_image_copy_capture_manager_v1.create_session", Child: c}, c, src, uint32(0))
258- _ = s.wl.Request(wlturbo.Request{Proxy: src, Opcode: reqSourceDestroy, Name: "ext_image_capture_source_v1.destroy", Destructor: true})
205+ proxy, err := s.copyManager.CreateSession(src, 0)
206+ _ = src.Destroy()
259207 if err != nil {
260208 s.terminate()
261209 return nil, fmt.Errorf("wayland: create capture session: %w", err)
262210 }
211+ c := newSession(proxy, key)
263212 s.sess = c
264213 return c, nil
265214 }
@@ -276,7 +225,7 @@ func (s *Source) dropSession() {
276225 if c.exclusion != nil {
277226 c.exclusion.destroy(s)
278227 }
279- _ = s.wl.Request(wlturbo.Request{Proxy: c, Opcode: reqSessionDestroy, Name: "ext_image_copy_capture_session_v1.destroy", Destructor: true})
228+ _ = c.proxy.Destroy()
280229 }
281230
282231 // nextFrame returns the session's current frame.
@@ -341,7 +290,7 @@ func (s *Source) nextFrame(c *session, out *output) (ports.Frame, error) {
341290 if c.stopped {
342291 return ports.Frame{}, s.stoppedError(c, out)
343292 }
344- if c.haveLast && f.reason == failUnknown {
293+ if c.haveLast && f.reason == imagecopycapture.FAILURE_REASON_UNKNOWN {
345294 return s.lastFrame(c)
346295 }
347296 default: // still pending after the poll: repeat the last frame
@@ -372,9 +321,9 @@ func (s *Source) awaitFrame(c *session, poll bool) error {
372321
373322 // retryAfterFailure handles a failed frame: one roundtrip, so that a stop or a
374323 // new constraint batch that follows the failure is seen, then a retry.
375-func (s *Source) retryAfterFailure(c *session, f *frameProxy) error {
324+func (s *Source) retryAfterFailure(c *session, f *captureFrame) error {
376325 s.destroyFrame(c)
377- if f.reason == failStopped {
326+ if f.reason == imagecopycapture.FAILURE_REASON_STOPPED {
378327 c.stopped = true
379328 return nil
380329 }
@@ -382,14 +331,14 @@ func (s *Source) retryAfterFailure(c *session, f *frameProxy) error {
382331 return errCompositorFailedCapture
383332 }
384333 switch f.reason {
385- case failBufferConstraints, failUnknown:
334+ case imagecopycapture.FAILURE_REASON_BUFFER_CONSTRAINTS, imagecopycapture.FAILURE_REASON_UNKNOWN:
386335 return s.roundtrip()
387336 }
388337 return fmt.Errorf("%w: failure reason %d", errCompositorFailedCapture, f.reason)
389338 }
390339
391340 // complete hands out a frame the compositor finished.
392-func (s *Source) complete(c *session, g bufGeom, f *frameProxy) (ports.Frame, error) {
341+func (s *Source) complete(c *session, g bufGeom, f *captureFrame) (ports.Frame, error) {
393342 transform := f.transform
394343 idx := c.inflightBuf
395344 s.destroyFrame(c)
@@ -423,22 +372,21 @@ func (s *Source) startFrame(c *session, g bufGeom) error {
423372 if err := s.ensureBuffer(idx, g); err != nil {
424373 return err
425374 }
426- f := &frameProxy{}
427- f.SetContext(s.wl)
428- if err := s.wl.Request(wlturbo.Request{Proxy: c, Opcode: reqSessionCreateFrame, Name: "ext_image_copy_capture_session_v1.create_frame", Child: f}, f); err != nil {
375+ f, err := newCaptureFrame(c)
376+ if err != nil {
429377 s.terminate()
430378 return fmt.Errorf("wayland: create frame: %w", err)
431379 }
432380 c.inflight, c.inflightBuf, c.inflightGeom = f, idx, g
433- if err := s.wl.Request(wlturbo.Request{Proxy: f, Opcode: reqFrameAttachBuffer, Name: "ext_image_copy_capture_frame_v1.attach_buffer"}, s.bufs[idx].buffer); err != nil {
381+ if err := f.proxy.AttachBuffer(s.bufs[idx].buffer); err != nil {
434382 s.terminate()
435383 return fmt.Errorf("wayland: attach buffer: %w", err)
436384 }
437- if err := s.wl.Request(wlturbo.Request{Proxy: f, Opcode: reqFrameDamageBuffer, Name: "ext_image_copy_capture_frame_v1.damage_buffer"}, int32(0), int32(0), int32(g.width), int32(g.height)); err != nil {
385+ if err := f.proxy.DamageBuffer(0, 0, int32(g.width), int32(g.height)); err != nil {
438386 s.terminate()
439387 return fmt.Errorf("wayland: damage buffer: %w", err)
440388 }
441- if err := s.wl.Request(wlturbo.Request{Proxy: f, Opcode: reqFrameCapture, Name: "ext_image_copy_capture_frame_v1.capture"}); err != nil {
389+ if err := f.proxy.Capture(); err != nil {
442390 s.terminate()
443391 return fmt.Errorf("wayland: capture request: %w", err)
444392 }
@@ -449,7 +397,7 @@ func (s *Source) startFrame(c *session, g bufGeom) error {
449397 func (s *Source) destroyFrame(c *session) {
450398 if f := c.inflight; f != nil {
451399 c.inflight = nil
452- _ = s.wl.Request(wlturbo.Request{Proxy: f, Opcode: reqFrameDestroy, Name: "ext_image_copy_capture_frame_v1.destroy", Destructor: true})
400+ _ = f.proxy.Destroy()
453401 }
454402 }
455403
@@ -5,7 +5,8 @@ import (
5 "fmt"5 "fmt"
6 "time"6 "time"
7 7
8- "github.com/bnema/wlturbo"8+ "github.com/bnema/wlturbo/protocol/imagecapturesource"
9+ "github.com/bnema/wlturbo/protocol/imagecopycapture"
9 10
10 "github.com/bnema/nefercap/internal/ports"11 "github.com/bnema/nefercap/internal/ports"
11 )12 )
@@ -16,40 +17,6 @@ const (
16 shmXRGB8888 = uint32(ports.XRGB8888)17 shmXRGB8888 = uint32(ports.XRGB8888)
17 )18 )
18 19
19-// Hand-written proxies for ext-image-capture-source-v1 and
20-// ext-image-copy-capture-v1: the wlturbo release in use does not generate
21-// them. Only the requests and events nefercap uses are declared.
22-
23-// requestOnly is a bound global or created object that never sends events.
24-type requestOnly struct{ wlturbo.BaseProxy }
25-
26-func (*requestOnly) EventSignature(uint16) (string, bool) { return "", false }
27-
28-func newRequestOnly(ctx *wlturbo.Context) *requestOnly {
29- p := &requestOnly{}
30- p.SetContext(ctx)
31- return p
32-}
33-
34-const (
35- reqOutputSourceCreate = 0 // ext_output_image_capture_source_manager_v1.create_source
36- reqSourceDestroy = 0 // ext_image_capture_source_v1.destroy
37- reqCopyCreateSession = 0 // ext_image_copy_capture_manager_v1.create_session
38- reqSessionCreateFrame = 0
39- reqSessionDestroy = 1
40- reqFrameDestroy = 0
41- reqFrameAttachBuffer = 1
42- reqFrameDamageBuffer = 2
43- reqFrameCapture = 3
44-)
45-
46-// ext_image_copy_capture_frame_v1.failure_reason
47-const (
48- failUnknown = 0
49- failBufferConstraints = 1
50- failStopped = 2
51-)
52-
53 // maxFrameAttempts bounds the retries of one frame after failures that the20 // maxFrameAttempts bounds the retries of one frame after failures that the
54 // protocol calls recoverable.21 // protocol calls recoverable.
55 const maxFrameAttempts = 422 const maxFrameAttempts = 4
@@ -73,8 +40,8 @@ type constraints struct {
73 // session is one ext_image_copy_capture_session_v1. Its events are dispatched40 // session is one ext_image_copy_capture_session_v1. Its events are dispatched
74 // on the goroutine that owns the operation.41 // on the goroutine that owns the operation.
75 type session struct {42 type session struct {
76- wlturbo.BaseProxy43+ proxy *imagecopycapture.ExtImageCopyCaptureSession
77- key sessionKey44+ key sessionKey
78 45
79 pending constraints46 pending constraints
80 current constraints47 current constraints
@@ -88,7 +55,8 @@ type session struct {
88 // lives in buffer cur and stays untouched while the next one is written to55 // lives in buffer cur and stays untouched while the next one is written to
89 // the other buffer, so a Capture that finds the compositor idle (it waits56 // the other buffer, so a Capture that finds the compositor idle (it waits
90 // for damage after the first frame) can repeat the last frame at once.57 // for damage after the first frame) can repeat the last frame at once.
91- inflight *frameProxy58+ frame captureFrame // outcome of the in-flight frame, reset per frame
59+ inflight *captureFrame // &frame while a frame is in flight, else nil
92 inflightBuf int60 inflightBuf int
93 inflightGeom bufGeom // buffer layout the in-flight frame was made for61 inflightGeom bufGeom // buffer layout the in-flight frame was made for
94 haveLast bool62 haveLast bool
@@ -96,30 +64,20 @@ type session struct {
96 lastGeom bufGeom64 lastGeom bufGeom
97 served int // frames handed out by this session65 served int // frames handed out by this session
98 fails int // consecutive failed frames66 fails int // consecutive failed frames
99-}
100 67
101-func (*session) EventSignature(op uint16) (string, bool) {68+ // Frame event handlers, built once and shared by every frame proxy.
102- switch op {69+ onTransform func(uint32)
103- case 0:70+ onReady func()
104- return "uint,uint,", true // buffer_size71+ onFailed func(uint32)
105- case 1:
106- return "uint,", true // shm_format
107- case 2:
108- return "array,", true // dmabuf_device
109- case 3:
110- return "uint,array,", true // dmabuf_format
111- case 4, 5:
112- return "", true // done, stopped
113- }
114- return "", false
115 }72 }
116 73
117-func (c *session) Dispatch(e *wlturbo.Event) {74+// newSession wraps a created session proxy and subscribes to its events.
118- switch e.Opcode {75+func newSession(proxy *imagecopycapture.ExtImageCopyCaptureSession, key sessionKey) *session {
119- case 0:76+ c := &session{proxy: proxy, key: key}
120- c.pending.width, c.pending.height, c.pending.haveSize = e.Uint32(), e.Uint32(), true77+ proxy.OnBufferSize(func(width, height uint32) {
121- case 1:78+ c.pending.width, c.pending.height, c.pending.haveSize = width, height, true
122- format := e.Uint32()79+ })
80+ proxy.OnShmFormat(func(format uint32) {
123 c.pending.sawShm = true81 c.pending.sawShm = true
124 switch {82 switch {
125 case format != shmARGB8888 && format != shmXRGB8888:83 case format != shmARGB8888 && format != shmXRGB8888:
@@ -127,46 +85,40 @@ func (c *session) Dispatch(e *wlturbo.Event) {
127 case !c.pending.haveFormat:85 case !c.pending.haveFormat:
128 c.pending.format, c.pending.haveFormat = format, true86 c.pending.format, c.pending.haveFormat = format, true
129 }87 }
130- case 4:88+ })
89+ proxy.OnDone(func() {
131 c.current, c.pending = c.pending, constraints{}90 c.current, c.pending = c.pending, constraints{}
132 c.gen++91 c.gen++
133- case 5:92+ })
134- c.stopped = true93+ proxy.OnStopped(func() { c.stopped = true })
135- }94+ c.onTransform = func(transform uint32) { c.frame.transform = transform }
95+ c.onReady = func() { c.frame.ready = true }
96+ c.onFailed = func(reason uint32) { c.frame.failed, c.frame.reason = true, reason }
97+ return c
136 }98 }
137 99
138-// frameProxy is one ext_image_copy_capture_frame_v1.100+// captureFrame is one ext_image_copy_capture_frame_v1 and its outcome. The
139-type frameProxy struct {101+// session owns a single one, reset for each frame.
140- wlturbo.BaseProxy102+type captureFrame struct {
103+ proxy *imagecopycapture.ExtImageCopyCaptureFrame
141 transform uint32104 transform uint32
142 ready bool105 ready bool
143 failed bool106 failed bool
144 reason uint32107 reason uint32
145 }108 }
146 109
147-func (*frameProxy) EventSignature(op uint16) (string, bool) {110+// newCaptureFrame creates the next frame of c, resetting its outcome, and
148- switch op {111+// subscribes the session's shared handlers to its events.
149- case 0, 4:112+func newCaptureFrame(c *session) (*captureFrame, error) {
150- return "uint,", true // transform, failed113+ proxy, err := c.proxy.CreateFrame()
151- case 1:114+ if err != nil {
152- return "int,int,int,int,", true // damage115+ return nil, err
153- case 2:
154- return "uint,uint,uint,", true // presentation_time
155- case 3:
156- return "", true // ready
157- }
158- return "", false
159-}
160-
161-func (f *frameProxy) Dispatch(e *wlturbo.Event) {
162- switch e.Opcode {
163- case 0:
164- f.transform = e.Uint32()
165- case 3:
166- f.ready = true
167- case 4:
168- f.failed, f.reason = true, e.Uint32()
169 }116 }
117+ c.frame = captureFrame{proxy: proxy}
118+ proxy.OnTransform(c.onTransform)
119+ proxy.OnReady(c.onReady)
120+ proxy.OnFailed(c.onFailed)
121+ return &c.frame, nil
170 }122 }
171 123
172 // sessionKey identifies what a session captures. Crop sessions have a zero124 // sessionKey identifies what a session captures. Crop sessions have a zero
@@ -236,30 +188,27 @@ func (s *Source) sessionFor(key sessionKey, out *output) (*session, error) {
236 }188 }
237 s.dropSession()189 s.dropSession()
238 }190 }
239- src := newRequestOnly(s.wl)191+ var src *imagecapturesource.ExtImageCaptureSource
240 var err error192 var err error
241 switch {193 switch {
242 case key.workspace != 0:194 case key.workspace != 0:
243- err = s.createWorkspaceSource(src, key.workspace)195+ src, err = s.createWorkspaceSource(key.workspace)
244 case key.region != (ports.Region{}):196 case key.region != (ports.Region{}):
245- r := key.region197+ src, err = s.createRegionSource(out, key.region)
246- err = s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqNeferwlRegionSource, Name: "neferwl_image_capture_source_manager_v1.create_output_region_source", Child: src},
247- src, out.proxy, int32(r.X), int32(r.Y), int32(r.Width), int32(r.Height))
248 default:198 default:
249- err = s.wl.Request(wlturbo.Request{Proxy: s.outSource, Opcode: reqOutputSourceCreate, Name: "ext_output_image_capture_source_manager_v1.create_source", Child: src}, src, out.proxy)199+ src, err = s.outSource.CreateSource(out.proxy)
250 }200 }
251 if err != nil {201 if err != nil {
252 s.terminate()202 s.terminate()
253 return nil, fmt.Errorf("wayland: create capture source: %w", err)203 return nil, fmt.Errorf("wayland: create capture source: %w", err)
254 }204 }
255- c := &session{key: key}205+ proxy, err := s.copyManager.CreateSession(src, 0)
256- c.SetContext(s.wl)206+ _ = src.Destroy()
257- err = s.wl.Request(wlturbo.Request{Proxy: s.copyManager, Opcode: reqCopyCreateSession, Name: "ext_image_copy_capture_manager_v1.create_session", Child: c}, c, src, uint32(0))
258- _ = s.wl.Request(wlturbo.Request{Proxy: src, Opcode: reqSourceDestroy, Name: "ext_image_capture_source_v1.destroy", Destructor: true})
259 if err != nil {207 if err != nil {
260 s.terminate()208 s.terminate()
261 return nil, fmt.Errorf("wayland: create capture session: %w", err)209 return nil, fmt.Errorf("wayland: create capture session: %w", err)
262 }210 }
211+ c := newSession(proxy, key)
263 s.sess = c212 s.sess = c
264 return c, nil213 return c, nil
265 }214 }
@@ -276,7 +225,7 @@ func (s *Source) dropSession() {
276 if c.exclusion != nil {225 if c.exclusion != nil {
277 c.exclusion.destroy(s)226 c.exclusion.destroy(s)
278 }227 }
279- _ = s.wl.Request(wlturbo.Request{Proxy: c, Opcode: reqSessionDestroy, Name: "ext_image_copy_capture_session_v1.destroy", Destructor: true})228+ _ = c.proxy.Destroy()
280 }229 }
281 230
282 // nextFrame returns the session's current frame.231 // nextFrame returns the session's current frame.
@@ -341,7 +290,7 @@ func (s *Source) nextFrame(c *session, out *output) (ports.Frame, error) {
341 if c.stopped {290 if c.stopped {
342 return ports.Frame{}, s.stoppedError(c, out)291 return ports.Frame{}, s.stoppedError(c, out)
343 }292 }
344- if c.haveLast && f.reason == failUnknown {293+ if c.haveLast && f.reason == imagecopycapture.FAILURE_REASON_UNKNOWN {
345 return s.lastFrame(c)294 return s.lastFrame(c)
346 }295 }
347 default: // still pending after the poll: repeat the last frame296 default: // still pending after the poll: repeat the last frame
@@ -372,9 +321,9 @@ func (s *Source) awaitFrame(c *session, poll bool) error {
372 321
373 // retryAfterFailure handles a failed frame: one roundtrip, so that a stop or a322 // retryAfterFailure handles a failed frame: one roundtrip, so that a stop or a
374 // new constraint batch that follows the failure is seen, then a retry.323 // new constraint batch that follows the failure is seen, then a retry.
375-func (s *Source) retryAfterFailure(c *session, f *frameProxy) error {324+func (s *Source) retryAfterFailure(c *session, f *captureFrame) error {
376 s.destroyFrame(c)325 s.destroyFrame(c)
377- if f.reason == failStopped {326+ if f.reason == imagecopycapture.FAILURE_REASON_STOPPED {
378 c.stopped = true327 c.stopped = true
379 return nil328 return nil
380 }329 }
@@ -382,14 +331,14 @@ func (s *Source) retryAfterFailure(c *session, f *frameProxy) error {
382 return errCompositorFailedCapture331 return errCompositorFailedCapture
383 }332 }
384 switch f.reason {333 switch f.reason {
385- case failBufferConstraints, failUnknown:334+ case imagecopycapture.FAILURE_REASON_BUFFER_CONSTRAINTS, imagecopycapture.FAILURE_REASON_UNKNOWN:
386 return s.roundtrip()335 return s.roundtrip()
387 }336 }
388 return fmt.Errorf("%w: failure reason %d", errCompositorFailedCapture, f.reason)337 return fmt.Errorf("%w: failure reason %d", errCompositorFailedCapture, f.reason)
389 }338 }
390 339
391 // complete hands out a frame the compositor finished.340 // complete hands out a frame the compositor finished.
392-func (s *Source) complete(c *session, g bufGeom, f *frameProxy) (ports.Frame, error) {341+func (s *Source) complete(c *session, g bufGeom, f *captureFrame) (ports.Frame, error) {
393 transform := f.transform342 transform := f.transform
394 idx := c.inflightBuf343 idx := c.inflightBuf
395 s.destroyFrame(c)344 s.destroyFrame(c)
@@ -423,22 +372,21 @@ func (s *Source) startFrame(c *session, g bufGeom) error {
423 if err := s.ensureBuffer(idx, g); err != nil {372 if err := s.ensureBuffer(idx, g); err != nil {
424 return err373 return err
425 }374 }
426- f := &frameProxy{}375+ f, err := newCaptureFrame(c)
427- f.SetContext(s.wl)376+ if err != nil {
428- if err := s.wl.Request(wlturbo.Request{Proxy: c, Opcode: reqSessionCreateFrame, Name: "ext_image_copy_capture_session_v1.create_frame", Child: f}, f); err != nil {
429 s.terminate()377 s.terminate()
430 return fmt.Errorf("wayland: create frame: %w", err)378 return fmt.Errorf("wayland: create frame: %w", err)
431 }379 }
432 c.inflight, c.inflightBuf, c.inflightGeom = f, idx, g380 c.inflight, c.inflightBuf, c.inflightGeom = f, idx, g
433- if err := s.wl.Request(wlturbo.Request{Proxy: f, Opcode: reqFrameAttachBuffer, Name: "ext_image_copy_capture_frame_v1.attach_buffer"}, s.bufs[idx].buffer); err != nil {381+ if err := f.proxy.AttachBuffer(s.bufs[idx].buffer); err != nil {
434 s.terminate()382 s.terminate()
435 return fmt.Errorf("wayland: attach buffer: %w", err)383 return fmt.Errorf("wayland: attach buffer: %w", err)
436 }384 }
437- if err := s.wl.Request(wlturbo.Request{Proxy: f, Opcode: reqFrameDamageBuffer, Name: "ext_image_copy_capture_frame_v1.damage_buffer"}, int32(0), int32(0), int32(g.width), int32(g.height)); err != nil {385+ if err := f.proxy.DamageBuffer(0, 0, int32(g.width), int32(g.height)); err != nil {
438 s.terminate()386 s.terminate()
439 return fmt.Errorf("wayland: damage buffer: %w", err)387 return fmt.Errorf("wayland: damage buffer: %w", err)
440 }388 }
441- if err := s.wl.Request(wlturbo.Request{Proxy: f, Opcode: reqFrameCapture, Name: "ext_image_copy_capture_frame_v1.capture"}); err != nil {389+ if err := f.proxy.Capture(); err != nil {
442 s.terminate()390 s.terminate()
443 return fmt.Errorf("wayland: capture request: %w", err)391 return fmt.Errorf("wayland: capture request: %w", err)
444 }392 }
@@ -449,7 +397,7 @@ func (s *Source) startFrame(c *session, g bufGeom) error {
449 func (s *Source) destroyFrame(c *session) {397 func (s *Source) destroyFrame(c *session) {
450 if f := c.inflight; f != nil {398 if f := c.inflight; f != nil {
451 c.inflight = nil399 c.inflight = nil
452- _ = s.wl.Request(wlturbo.Request{Proxy: f, Opcode: reqFrameDestroy, Name: "ext_image_copy_capture_frame_v1.destroy", Destructor: true})400+ _ = f.proxy.Destroy()
453 }401 }
454 }402 }
455 403
M internal/adapters/wayland/extensions.go +49 -9
@@ -9,6 +9,8 @@ import (
99
1010 "github.com/bnema/wlturbo"
1111 "github.com/bnema/wlturbo/protocol/core"
12+ "github.com/bnema/wlturbo/protocol/imagecapturesource"
13+ "github.com/bnema/wlturbo/protocol/xdgoutput"
1214
1315 "github.com/bnema/nefercap/internal/ports"
1416 )
@@ -75,6 +77,18 @@ const (
7577 exclusionSessionStopped = 1
7678 )
7779
80+// requestOnly is a bound NeferWL global or created object that never sends
81+// events: wlturbo has no generated binding for the extension.
82+type requestOnly struct{ wlturbo.BaseProxy }
83+
84+func (*requestOnly) EventSignature(uint16) (string, bool) { return "", false }
85+
86+func newRequestOnly(ctx *wlturbo.Context) *requestOnly {
87+ p := &requestOnly{}
88+ p.SetContext(ctx)
89+ return p
90+}
91+
7892 // extensions holds the bound optional NeferWL globals.
7993 type extensions struct {
8094 source *requestOnly
@@ -82,28 +96,54 @@ type extensions struct {
8296 }
8397
8498 // bindExtras binds every optional global. None is an error when absent.
85-func (s *Source) bindExtras() (err error) {
86- if s.xdgOutputs, err = s.bindOptional(xdgOutputIface, xdgOutputVersion); err != nil {
99+func (s *Source) bindExtras() error {
100+ x := xdgoutput.NewZxdgOutputManager(s.wl)
101+ switch ok, err := s.bindOptional(xdgoutput.ZxdgOutputManagerInterface, xdgOutputVersion, x); {
102+ case err != nil:
87103 return err
104+ case ok:
105+ s.xdgOutputs = x
88106 }
89107 for _, o := range s.outputs {
90108 s.watchLogicalSize(o)
91109 }
92- if s.ext.source, err = s.bindOptional(sourceManagerIface, extensionVersion); err != nil {
110+ var err error
111+ if s.ext.source, err = s.bindOptionalExtension(sourceManagerIface); err != nil {
93112 return err
94113 }
95- if s.ext.exclusion, err = s.bindOptional(exclusionManagerIface, extensionVersion); err != nil {
114+ if s.ext.exclusion, err = s.bindOptionalExtension(exclusionManagerIface); err != nil {
96115 return err
97116 }
98117 return s.bindWorkspaces()
99118 }
100119
101-func (s *Source) createWorkspaceSource(src *requestOnly, id uint64) error {
120+// bindOptionalExtension binds a NeferWL global, nil when the compositor has
121+// none.
122+func (s *Source) bindOptionalExtension(iface string) (*requestOnly, error) {
123+ p := newRequestOnly(s.wl)
124+ if ok, err := s.bindOptional(iface, extensionVersion, p); err != nil || !ok {
125+ return nil, err
126+ }
127+ return p, nil
128+}
129+
130+// createWorkspaceSource makes a source that follows the workspace id.
131+func (s *Source) createWorkspaceSource(id uint64) (*imagecapturesource.ExtImageCaptureSource, error) {
102132 h := s.ws.byID[id]
103133 if h == nil || h.removed {
104- return ports.ErrWorkspaceUnavailable
134+ return nil, ports.ErrWorkspaceUnavailable
105135 }
106- return s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqNeferwlWorkspaceSource, Name: "neferwl_image_capture_source_manager_v1.create_workspace_source", Child: src}, src, h)
136+ src := imagecapturesource.NewExtImageCaptureSource(s.wl)
137+ err := s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqNeferwlWorkspaceSource, Name: "neferwl_image_capture_source_manager_v1.create_workspace_source", Child: src}, src, h.proxy)
138+ return src, err
139+}
140+
141+// createRegionSource makes a source of a region of out, in logical pixels.
142+func (s *Source) createRegionSource(out *output, r ports.Region) (*imagecapturesource.ExtImageCaptureSource, error) {
143+ src := imagecapturesource.NewExtImageCaptureSource(s.wl)
144+ err := s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqNeferwlRegionSource, Name: "neferwl_image_capture_source_manager_v1.create_output_region_source", Child: src},
145+ src, out.proxy, int32(r.X), int32(r.Y), int32(r.Width), int32(r.Height))
146+ return src, err
107147 }
108148
109149 // workspaceFrame is one neferwl_workspace_frame_v1: the frame of a workspace
@@ -151,7 +191,7 @@ func (s *Source) requestFrames() (bool, error) {
151191 }
152192 f := &workspaceFrame{}
153193 f.SetContext(s.wl)
154- if err := s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqGetWorkspaceFrame, Name: "neferwl_image_capture_source_manager_v1.get_workspace_frame", Child: f}, f, w); err != nil {
194+ if err := s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqGetWorkspaceFrame, Name: "neferwl_image_capture_source_manager_v1.get_workspace_frame", Child: f}, f, w.proxy); err != nil {
155195 s.terminate()
156196 return false, fmt.Errorf("wayland: get workspace frame: %w", err)
157197 }
@@ -253,7 +293,7 @@ func (s *Source) beginExclusion(t ports.Target) (string, error) {
253293 }
254294 e := &exclusion{}
255295 e.SetContext(s.wl)
256- if err := s.wl.Request(wlturbo.Request{Proxy: s.ext.exclusion, Opcode: reqGetExclusion, Name: "neferwl_capture_exclusion_manager_v1.get_exclusion", Child: e}, e, c); err != nil {
296+ if err := s.wl.Request(wlturbo.Request{Proxy: s.ext.exclusion, Opcode: reqGetExclusion, Name: "neferwl_capture_exclusion_manager_v1.get_exclusion", Child: e}, e, c.proxy); err != nil {
257297 s.terminate()
258298 return "", fmt.Errorf("wayland: get exclusion: %w", err)
259299 }
@@ -9,6 +9,8 @@ import (
9 9
10 "github.com/bnema/wlturbo"10 "github.com/bnema/wlturbo"
11 "github.com/bnema/wlturbo/protocol/core"11 "github.com/bnema/wlturbo/protocol/core"
12+ "github.com/bnema/wlturbo/protocol/imagecapturesource"
13+ "github.com/bnema/wlturbo/protocol/xdgoutput"
12 14
13 "github.com/bnema/nefercap/internal/ports"15 "github.com/bnema/nefercap/internal/ports"
14 )16 )
@@ -75,6 +77,18 @@ const (
75 exclusionSessionStopped = 177 exclusionSessionStopped = 1
76 )78 )
77 79
80+// requestOnly is a bound NeferWL global or created object that never sends
81+// events: wlturbo has no generated binding for the extension.
82+type requestOnly struct{ wlturbo.BaseProxy }
83+
84+func (*requestOnly) EventSignature(uint16) (string, bool) { return "", false }
85+
86+func newRequestOnly(ctx *wlturbo.Context) *requestOnly {
87+ p := &requestOnly{}
88+ p.SetContext(ctx)
89+ return p
90+}
91+
78 // extensions holds the bound optional NeferWL globals.92 // extensions holds the bound optional NeferWL globals.
79 type extensions struct {93 type extensions struct {
80 source *requestOnly94 source *requestOnly
@@ -82,28 +96,54 @@ type extensions struct {
82 }96 }
83 97
84 // bindExtras binds every optional global. None is an error when absent.98 // bindExtras binds every optional global. None is an error when absent.
85-func (s *Source) bindExtras() (err error) {99+func (s *Source) bindExtras() error {
86- if s.xdgOutputs, err = s.bindOptional(xdgOutputIface, xdgOutputVersion); err != nil {100+ x := xdgoutput.NewZxdgOutputManager(s.wl)
101+ switch ok, err := s.bindOptional(xdgoutput.ZxdgOutputManagerInterface, xdgOutputVersion, x); {
102+ case err != nil:
87 return err103 return err
104+ case ok:
105+ s.xdgOutputs = x
88 }106 }
89 for _, o := range s.outputs {107 for _, o := range s.outputs {
90 s.watchLogicalSize(o)108 s.watchLogicalSize(o)
91 }109 }
92- if s.ext.source, err = s.bindOptional(sourceManagerIface, extensionVersion); err != nil {110+ var err error
111+ if s.ext.source, err = s.bindOptionalExtension(sourceManagerIface); err != nil {
93 return err112 return err
94 }113 }
95- if s.ext.exclusion, err = s.bindOptional(exclusionManagerIface, extensionVersion); err != nil {114+ if s.ext.exclusion, err = s.bindOptionalExtension(exclusionManagerIface); err != nil {
96 return err115 return err
97 }116 }
98 return s.bindWorkspaces()117 return s.bindWorkspaces()
99 }118 }
100 119
101-func (s *Source) createWorkspaceSource(src *requestOnly, id uint64) error {120+// bindOptionalExtension binds a NeferWL global, nil when the compositor has
121+// none.
122+func (s *Source) bindOptionalExtension(iface string) (*requestOnly, error) {
123+ p := newRequestOnly(s.wl)
124+ if ok, err := s.bindOptional(iface, extensionVersion, p); err != nil || !ok {
125+ return nil, err
126+ }
127+ return p, nil
128+}
129+
130+// createWorkspaceSource makes a source that follows the workspace id.
131+func (s *Source) createWorkspaceSource(id uint64) (*imagecapturesource.ExtImageCaptureSource, error) {
102 h := s.ws.byID[id]132 h := s.ws.byID[id]
103 if h == nil || h.removed {133 if h == nil || h.removed {
104- return ports.ErrWorkspaceUnavailable134+ return nil, ports.ErrWorkspaceUnavailable
105 }135 }
106- return s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqNeferwlWorkspaceSource, Name: "neferwl_image_capture_source_manager_v1.create_workspace_source", Child: src}, src, h)136+ src := imagecapturesource.NewExtImageCaptureSource(s.wl)
137+ err := s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqNeferwlWorkspaceSource, Name: "neferwl_image_capture_source_manager_v1.create_workspace_source", Child: src}, src, h.proxy)
138+ return src, err
139+}
140+
141+// createRegionSource makes a source of a region of out, in logical pixels.
142+func (s *Source) createRegionSource(out *output, r ports.Region) (*imagecapturesource.ExtImageCaptureSource, error) {
143+ src := imagecapturesource.NewExtImageCaptureSource(s.wl)
144+ err := s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqNeferwlRegionSource, Name: "neferwl_image_capture_source_manager_v1.create_output_region_source", Child: src},
145+ src, out.proxy, int32(r.X), int32(r.Y), int32(r.Width), int32(r.Height))
146+ return src, err
107 }147 }
108 148
109 // workspaceFrame is one neferwl_workspace_frame_v1: the frame of a workspace149 // workspaceFrame is one neferwl_workspace_frame_v1: the frame of a workspace
@@ -151,7 +191,7 @@ func (s *Source) requestFrames() (bool, error) {
151 }191 }
152 f := &workspaceFrame{}192 f := &workspaceFrame{}
153 f.SetContext(s.wl)193 f.SetContext(s.wl)
154- if err := s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqGetWorkspaceFrame, Name: "neferwl_image_capture_source_manager_v1.get_workspace_frame", Child: f}, f, w); err != nil {194+ if err := s.wl.Request(wlturbo.Request{Proxy: s.ext.source, Opcode: reqGetWorkspaceFrame, Name: "neferwl_image_capture_source_manager_v1.get_workspace_frame", Child: f}, f, w.proxy); err != nil {
155 s.terminate()195 s.terminate()
156 return false, fmt.Errorf("wayland: get workspace frame: %w", err)196 return false, fmt.Errorf("wayland: get workspace frame: %w", err)
157 }197 }
@@ -253,7 +293,7 @@ func (s *Source) beginExclusion(t ports.Target) (string, error) {
253 }293 }
254 e := &exclusion{}294 e := &exclusion{}
255 e.SetContext(s.wl)295 e.SetContext(s.wl)
256- if err := s.wl.Request(wlturbo.Request{Proxy: s.ext.exclusion, Opcode: reqGetExclusion, Name: "neferwl_capture_exclusion_manager_v1.get_exclusion", Child: e}, e, c); err != nil {296+ if err := s.wl.Request(wlturbo.Request{Proxy: s.ext.exclusion, Opcode: reqGetExclusion, Name: "neferwl_capture_exclusion_manager_v1.get_exclusion", Child: e}, e, c.proxy); err != nil {
257 s.terminate()297 s.terminate()
258 return "", fmt.Errorf("wayland: get exclusion: %w", err)298 return "", fmt.Errorf("wayland: get exclusion: %w", err)
259 }299 }
M internal/adapters/wayland/source.go +22 -25
@@ -20,6 +20,9 @@ import (
2020
2121 "github.com/bnema/wlturbo"
2222 "github.com/bnema/wlturbo/protocol/core"
23+ "github.com/bnema/wlturbo/protocol/imagecapturesource"
24+ "github.com/bnema/wlturbo/protocol/imagecopycapture"
25+ "github.com/bnema/wlturbo/protocol/xdgoutput"
2326 "github.com/bnema/zerowrap"
2427
2528 "github.com/bnema/nefercap/internal/logging"
@@ -38,13 +41,6 @@ const (
3841 defaultPollWait = 2 * time.Millisecond
3942 )
4043
41-// Globals of the capture protocols.
42-const (
43- outputSourceIface = "ext_output_image_capture_source_manager_v1"
44- copyManagerIface = "ext_image_copy_capture_manager_v1"
45- xdgOutputIface = "zxdg_output_manager_v1"
46-)
47-
4844 // output is the client-side state of one bound wl_output global.
4945 type output struct {
5046 proxy *core.Output
@@ -54,7 +50,7 @@ type output struct {
5450 width, height int32
5551 scale int32
5652 lw, lh int32 // logical size from xdg-output, 0 when unknown
57- xdg *xdgOutput
53+ xdg *xdgoutput.ZxdgOutput
5854 removed bool
5955 }
6056
@@ -85,9 +81,9 @@ type Source struct {
8581 watchActive atomic.Bool
8682
8783 shm *core.Shm
88- copyManager *requestOnly // ext_image_copy_capture_manager_v1
89- outSource *requestOnly // ext_output_image_capture_source_manager_v1
90- xdgOutputs *requestOnly // zxdg_output_manager_v1, nil when absent
84+ copyManager *imagecopycapture.ExtImageCopyCaptureManager
85+ outSource *imagecapturesource.ExtOutputImageCaptureSourceManager
86+ xdgOutputs *xdgoutput.ZxdgOutputManager // nil when absent
9187 outputs map[uint32]*output
9288 byProxy map[uint32]*output // wl_output proxy ID to output
9389 bound int // wl_output globals bound so far
@@ -176,10 +172,12 @@ func (s *Source) discover() (err error) {
176172 if _, err = reg.BindNegotiated(core.ShmInterface, shmVersion, s.shm); err != nil {
177173 return fmt.Errorf("wayland: bind wl_shm: %w", err)
178174 }
179- if s.copyManager, err = s.bindRequired(copyManagerIface, copyVersion); err != nil {
175+ s.copyManager = imagecopycapture.NewExtImageCopyCaptureManager(s.wl)
176+ if err = s.bindRequired(imagecopycapture.ExtImageCopyCaptureManagerInterface, s.copyManager); err != nil {
180177 return err
181178 }
182- if s.outSource, err = s.bindRequired(outputSourceIface, copyVersion); err != nil {
179+ s.outSource = imagecapturesource.NewExtOutputImageCaptureSourceManager(s.wl)
180+ if err = s.bindRequired(imagecapturesource.ExtOutputImageCaptureSourceManagerInterface, s.outSource); err != nil {
183181 return err
184182 }
185183 if err = s.bindExtras(); err != nil {
@@ -189,28 +187,27 @@ func (s *Source) discover() (err error) {
189187 }
190188
191189 // bindRequired binds a global that capture cannot work without.
192-func (s *Source) bindRequired(iface string, version uint32) (*requestOnly, error) {
193- p := newRequestOnly(s.wl)
194- if _, err := s.display.Registry().BindNegotiated(iface, version, p); err != nil {
190+func (s *Source) bindRequired(iface string, p wlturbo.Proxy) error {
191+ if _, err := s.display.Registry().BindNegotiated(iface, copyVersion, p); err != nil {
195192 if errors.Is(err, wlturbo.ErrGlobalNotFound) {
196- return nil, fmt.Errorf("wayland: the compositor does not support ext-image-copy-capture-v1 (%s missing)", iface)
193+ return fmt.Errorf("wayland: the compositor does not support ext-image-copy-capture-v1 (%s missing)", iface)
197194 }
198- return nil, fmt.Errorf("wayland: bind %s: %w", iface, err)
195+ return fmt.Errorf("wayland: bind %s: %w", iface, err)
199196 }
200- return p, nil
197+ return nil
201198 }
202199
203-// bindOptional binds a global when the compositor offers it.
204-func (s *Source) bindOptional(iface string, version uint32) (*requestOnly, error) {
205- p := newRequestOnly(s.wl)
200+// bindOptional binds a global when the compositor offers it, and reports
201+// whether it did.
202+func (s *Source) bindOptional(iface string, version uint32, p wlturbo.Proxy) (bool, error) {
206203 _, err := s.display.Registry().BindNegotiated(iface, version, p)
207204 switch {
208205 case err == nil:
209- return p, nil
206+ return true, nil
210207 case errors.Is(err, wlturbo.ErrGlobalNotFound):
211- return nil, nil
208+ return false, nil
212209 }
213- return nil, fmt.Errorf("wayland: bind %s: %w", iface, err)
210+ return false, fmt.Errorf("wayland: bind %s: %w", iface, err)
214211 }
215212
216213 func (s *Source) onOutputGlobal(reg *wlturbo.Registry, name, version uint32) {
@@ -20,6 +20,9 @@ import (
20 20
21 "github.com/bnema/wlturbo"21 "github.com/bnema/wlturbo"
22 "github.com/bnema/wlturbo/protocol/core"22 "github.com/bnema/wlturbo/protocol/core"
23+ "github.com/bnema/wlturbo/protocol/imagecapturesource"
24+ "github.com/bnema/wlturbo/protocol/imagecopycapture"
25+ "github.com/bnema/wlturbo/protocol/xdgoutput"
23 "github.com/bnema/zerowrap"26 "github.com/bnema/zerowrap"
24 27
25 "github.com/bnema/nefercap/internal/logging"28 "github.com/bnema/nefercap/internal/logging"
@@ -38,13 +41,6 @@ const (
38 defaultPollWait = 2 * time.Millisecond41 defaultPollWait = 2 * time.Millisecond
39 )42 )
40 43
41-// Globals of the capture protocols.
42-const (
43- outputSourceIface = "ext_output_image_capture_source_manager_v1"
44- copyManagerIface = "ext_image_copy_capture_manager_v1"
45- xdgOutputIface = "zxdg_output_manager_v1"
46-)
47-
48 // output is the client-side state of one bound wl_output global.44 // output is the client-side state of one bound wl_output global.
49 type output struct {45 type output struct {
50 proxy *core.Output46 proxy *core.Output
@@ -54,7 +50,7 @@ type output struct {
54 width, height int3250 width, height int32
55 scale int3251 scale int32
56 lw, lh int32 // logical size from xdg-output, 0 when unknown52 lw, lh int32 // logical size from xdg-output, 0 when unknown
57- xdg *xdgOutput53+ xdg *xdgoutput.ZxdgOutput
58 removed bool54 removed bool
59 }55 }
60 56
@@ -85,9 +81,9 @@ type Source struct {
85 watchActive atomic.Bool81 watchActive atomic.Bool
86 82
87 shm *core.Shm83 shm *core.Shm
88- copyManager *requestOnly // ext_image_copy_capture_manager_v184+ copyManager *imagecopycapture.ExtImageCopyCaptureManager
89- outSource *requestOnly // ext_output_image_capture_source_manager_v185+ outSource *imagecapturesource.ExtOutputImageCaptureSourceManager
90- xdgOutputs *requestOnly // zxdg_output_manager_v1, nil when absent86+ xdgOutputs *xdgoutput.ZxdgOutputManager // nil when absent
91 outputs map[uint32]*output87 outputs map[uint32]*output
92 byProxy map[uint32]*output // wl_output proxy ID to output88 byProxy map[uint32]*output // wl_output proxy ID to output
93 bound int // wl_output globals bound so far89 bound int // wl_output globals bound so far
@@ -176,10 +172,12 @@ func (s *Source) discover() (err error) {
176 if _, err = reg.BindNegotiated(core.ShmInterface, shmVersion, s.shm); err != nil {172 if _, err = reg.BindNegotiated(core.ShmInterface, shmVersion, s.shm); err != nil {
177 return fmt.Errorf("wayland: bind wl_shm: %w", err)173 return fmt.Errorf("wayland: bind wl_shm: %w", err)
178 }174 }
179- if s.copyManager, err = s.bindRequired(copyManagerIface, copyVersion); err != nil {175+ s.copyManager = imagecopycapture.NewExtImageCopyCaptureManager(s.wl)
176+ if err = s.bindRequired(imagecopycapture.ExtImageCopyCaptureManagerInterface, s.copyManager); err != nil {
180 return err177 return err
181 }178 }
182- if s.outSource, err = s.bindRequired(outputSourceIface, copyVersion); err != nil {179+ s.outSource = imagecapturesource.NewExtOutputImageCaptureSourceManager(s.wl)
180+ if err = s.bindRequired(imagecapturesource.ExtOutputImageCaptureSourceManagerInterface, s.outSource); err != nil {
183 return err181 return err
184 }182 }
185 if err = s.bindExtras(); err != nil {183 if err = s.bindExtras(); err != nil {
@@ -189,28 +187,27 @@ func (s *Source) discover() (err error) {
189 }187 }
190 188
191 // bindRequired binds a global that capture cannot work without.189 // bindRequired binds a global that capture cannot work without.
192-func (s *Source) bindRequired(iface string, version uint32) (*requestOnly, error) {190+func (s *Source) bindRequired(iface string, p wlturbo.Proxy) error {
193- p := newRequestOnly(s.wl)191+ if _, err := s.display.Registry().BindNegotiated(iface, copyVersion, p); err != nil {
194- if _, err := s.display.Registry().BindNegotiated(iface, version, p); err != nil {
195 if errors.Is(err, wlturbo.ErrGlobalNotFound) {192 if errors.Is(err, wlturbo.ErrGlobalNotFound) {
196- return nil, fmt.Errorf("wayland: the compositor does not support ext-image-copy-capture-v1 (%s missing)", iface)193+ return fmt.Errorf("wayland: the compositor does not support ext-image-copy-capture-v1 (%s missing)", iface)
197 }194 }
198- return nil, fmt.Errorf("wayland: bind %s: %w", iface, err)195+ return fmt.Errorf("wayland: bind %s: %w", iface, err)
199 }196 }
200- return p, nil197+ return nil
201 }198 }
202 199
203-// bindOptional binds a global when the compositor offers it.200+// bindOptional binds a global when the compositor offers it, and reports
204-func (s *Source) bindOptional(iface string, version uint32) (*requestOnly, error) {201+// whether it did.
205- p := newRequestOnly(s.wl)202+func (s *Source) bindOptional(iface string, version uint32, p wlturbo.Proxy) (bool, error) {
206 _, err := s.display.Registry().BindNegotiated(iface, version, p)203 _, err := s.display.Registry().BindNegotiated(iface, version, p)
207 switch {204 switch {
208 case err == nil:205 case err == nil:
209- return p, nil206+ return true, nil
210 case errors.Is(err, wlturbo.ErrGlobalNotFound):207 case errors.Is(err, wlturbo.ErrGlobalNotFound):
211- return nil, nil208+ return false, nil
212 }209 }
213- return nil, fmt.Errorf("wayland: bind %s: %w", iface, err)210+ return false, fmt.Errorf("wayland: bind %s: %w", iface, err)
214 }211 }
215 212
216 func (s *Source) onOutputGlobal(reg *wlturbo.Registry, name, version uint32) {213 func (s *Source) onOutputGlobal(reg *wlturbo.Registry, name, version uint32) {
M internal/adapters/wayland/source_test.go +3 -2
@@ -409,8 +409,9 @@ func TestConcurrentCallsAreSerialized(t *testing.T) {
409409 }
410410 }
411411
412-// allocBudget is measured (see the log of TestCaptureAllocations), with
413-// headroom.
412+// allocBudget is measured (see the log of TestCaptureAllocations: 17 to 18),
413+// with a small margin. The session builds the frame handlers once, so a frame
414+// only allocates its proxy and the handler slices of the generated binding.
414415 const allocBudget = 20
415416
416417 // Steady state cost of one capture over the real transport. The measurement
@@ -409,8 +409,9 @@ func TestConcurrentCallsAreSerialized(t *testing.T) {
409 }409 }
410 }410 }
411 411
412-// allocBudget is measured (see the log of TestCaptureAllocations), with412+// allocBudget is measured (see the log of TestCaptureAllocations: 17 to 18),
413-// headroom.413+// with a small margin. The session builds the frame handlers once, so a frame
414+// only allocates its proxy and the handler slices of the generated binding.
414 const allocBudget = 20415 const allocBudget = 20
415 416
416 // Steady state cost of one capture over the real transport. The measurement417 // Steady state cost of one capture over the real transport. The measurement
M internal/adapters/wayland/workspaces.go +61 -123
@@ -3,13 +3,12 @@ package wayland
33 import (
44 "cmp"
55 "context"
6- "errors"
76 "slices"
87 "strconv"
98 "unicode"
109 "unicode/utf8"
1110
12- "github.com/bnema/wlturbo"
11+ extworkspace "github.com/bnema/wlturbo/protocol/workspace"
1312
1413 "github.com/bnema/nefercap/internal/ports"
1514 )
@@ -19,25 +18,19 @@ import (
1918 // compositor (no activate, no commit).
2019
2120 const (
22- workspaceIface = "ext_workspace_manager_v1"
2321 workspaceVersion = 1
2422 maxWorkspaces = 256 // bound of the inventory
2523 maxNameLen = 256 // bytes, for workspace names
2624 maxGroupOutputs = 64
2725
28- wsStateActive = 1
29-
30- reqGroupDestroy = 1
31- reqHandleDestroy = 0
32-
3326 maxInterned = 4 * maxWorkspaces // id strings remembered across removals
3427 )
3528
3629 // workspaceState is the ext-workspace inventory of a Source.
3730 type workspaceState struct {
38- mgr *workspaceManager // nil when the compositor has no ext-workspace
39- handles map[uint32]*workspace // by proxy ID
40- byID map[uint64]*workspace // by Source-local identity
31+ mgr *extworkspace.ExtWorkspaceManager // nil when the compositor has no ext-workspace
32+ handles map[uint32]*workspace // by proxy ID
33+ byID map[uint64]*workspace // by Source-local identity
4134 groups map[uint32]*workspaceGroup
4235 next uint64 // last identity handed out; identities start at 1
4336 interned map[string]uint64 // id string to identity, bounded by maxInterned
@@ -45,105 +38,67 @@ type workspaceState struct {
4538 shown uint64 // workspace last verified as displayed, without the extension
4639 }
4740
48-// workspaceManager receives the creation events. It is bound once.
49-type workspaceManager struct {
50- wlturbo.BaseProxy
51- s *Source
52-}
53-
54-func (*workspaceManager) EventSignature(op uint16) (string, bool) {
55- switch op {
56- case 0, 1:
57- return "new_id,", true // workspace_group, workspace
58- case 2, 3:
59- return "", true // done, finished
60- }
61- return "", false
62-}
63-
64-func (m *workspaceManager) Dispatch(e *wlturbo.Event) {
65- s := m.s
66- switch e.Opcode {
67- case 0:
68- id := e.Uint32()
69- g := &workspaceGroup{s: s, outputs: make(map[uint32]bool)}
70- g.SetContext(s.wl)
71- g.SetID(id)
72- g.SetVersion(m.Version())
73- s.wl.Register(g)
41+// watchWorkspaces subscribes to the creation events of the manager.
42+func (s *Source) watchWorkspaces(m *extworkspace.ExtWorkspaceManager) {
43+ m.OnWorkspaceGroup(func(proxy *extworkspace.ExtWorkspaceGroupHandle) {
44+ // wlturbo registered the proxy already, which absorbs its events.
7445 if len(s.ws.groups) >= maxWorkspaces {
7546 s.ws.overflow = true
7647 return
7748 }
78- s.ws.groups[id] = g
79- case 1:
80- id := e.Uint32()
81- w := &workspace{s: s}
82- w.SetContext(s.wl)
83- w.SetID(id)
84- w.SetVersion(m.Version())
85- s.wl.Register(w)
49+ g := &workspaceGroup{proxy: proxy, outputs: make(map[uint32]bool)}
50+ s.ws.groups[proxy.ID()] = g
51+ s.watchGroup(g)
52+ })
53+ m.OnWorkspace(func(proxy *extworkspace.ExtWorkspaceHandle) {
8654 if len(s.ws.handles) >= maxWorkspaces {
8755 s.ws.overflow = true
8856 return
8957 }
90- s.ws.handles[id] = w
91- case 2:
92- s.ws.assignIDs()
93- }
58+ w := &workspace{proxy: proxy}
59+ s.ws.handles[proxy.ID()] = w
60+ s.watchWorkspace(w)
61+ })
62+ m.OnDone(s.ws.assignIDs)
9463 }
9564
9665 type workspaceGroup struct {
97- wlturbo.BaseProxy
98- s *Source
66+ proxy *extworkspace.ExtWorkspaceGroupHandle
9967 outputs map[uint32]bool // wl_output proxy IDs
10068 }
10169
102-func (*workspaceGroup) EventSignature(op uint16) (string, bool) {
103- switch op {
104- case 0:
105- return "uint,", true // capabilities
106- case 1, 2, 3, 4:
107- return "object,", true // output_enter, output_leave, workspace_enter, workspace_leave
108- case 5:
109- return "", true // removed
110- }
111- return "", false
112-}
113-
114-func (g *workspaceGroup) Dispatch(e *wlturbo.Event) {
115- s := g.s
116- switch e.Opcode {
117- case 1:
118- if id := e.Uint32(); len(g.outputs) < maxGroupOutputs {
70+func (s *Source) watchGroup(g *workspaceGroup) {
71+ g.proxy.OnOutputEnter(func(id uint32) {
72+ if len(g.outputs) < maxGroupOutputs {
11973 g.outputs[id] = true
12074 }
121- case 2:
122- delete(g.outputs, e.Uint32())
123- case 3:
124- if w := s.ws.handles[e.Uint32()]; w != nil {
75+ })
76+ g.proxy.OnOutputLeave(func(id uint32) { delete(g.outputs, id) })
77+ g.proxy.OnWorkspaceEnter(func(id uint32) {
78+ if w := s.ws.handles[id]; w != nil {
12579 w.group = g
12680 }
127- case 4:
128- if w := s.ws.handles[e.Uint32()]; w != nil && w.group == g {
81+ })
82+ g.proxy.OnWorkspaceLeave(func(id uint32) {
83+ if w := s.ws.handles[id]; w != nil && w.group == g {
12984 w.group = nil
13085 }
131- case 5:
132- delete(s.ws.groups, g.ID())
86+ })
87+ g.proxy.OnRemoved(func() {
88+ delete(s.ws.groups, g.proxy.ID())
13389 for _, w := range s.ws.handles {
13490 if w.group == g {
13591 w.group = nil
13692 }
13793 }
138- _ = s.wl.Request(wlturbo.Request{Proxy: g, Opcode: reqGroupDestroy, Name: "ext_workspace_group_handle_v1.destroy", Destructor: true})
139- }
94+ _ = g.proxy.Destroy()
95+ })
14096 }
14197
142-// workspace is one ext_workspace_handle_v1. It is a wlturbo object so that it
143-// can be the argument of create_workspace_source.
98+// workspace is one ext_workspace_handle_v1. Its proxy is the argument of
99+// create_workspace_source.
144100 type workspace struct {
145- wlturbo.BaseProxy
146- s *Source
101+ proxy *extworkspace.ExtWorkspaceHandle
147102 idStr string // the id event: empty for a temporary workspace
148103 name string
149104 state uint32
@@ -153,43 +108,29 @@ type workspace struct {
153108 frame *workspaceFrame // NeferWL's extension only; nil until asked for
154109 }
155110
156-func (*workspace) EventSignature(op uint16) (string, bool) {
157- switch op {
158- case 0, 1:
159- return "string,", true // id, name
160- case 2:
161- return "array,", true // coordinates
162- case 3, 4:
163- return "uint,", true // state, capabilities
164- case 5:
165- return "", true // removed
166- }
167- return "", false
168-}
169-
170-func (w *workspace) Dispatch(e *wlturbo.Event) {
171- switch e.Opcode {
172- case 0:
111+// watchWorkspace subscribes to the events of w. Coordinates are not used:
112+// nothing here depends on the grid.
113+func (s *Source) watchWorkspace(w *workspace) {
114+ w.proxy.OnId(func(id string) {
173115 if w.idStr == "" {
174- w.idStr = e.String()
116+ w.idStr = id
175117 }
176- case 1:
177- if n := e.String(); validName(n) {
118+ })
119+ w.proxy.OnName(func(n string) {
120+ if validName(n) {
178121 w.name = n
179122 }
180- // opcode 2 (coordinates) is not used: nothing here depends on the grid.
181- case 3:
182- w.state = e.Uint32()
183- case 5:
123+ })
124+ w.proxy.OnState(func(state uint32) { w.state = state })
125+ w.proxy.OnRemoved(func() {
184126 w.removed = true
185- s := w.s
186127 s.releaseWorkspaceFrame(w)
187- delete(s.ws.handles, w.ID())
128+ delete(s.ws.handles, w.proxy.ID())
188129 if w.id != 0 && s.ws.byID[w.id] == w {
189130 delete(s.ws.byID, w.id)
190131 }
191- _ = s.wl.Request(wlturbo.Request{Proxy: w, Opcode: reqHandleDestroy, Name: "ext_workspace_handle_v1.destroy", Destructor: true})
192- }
132+ _ = w.proxy.Destroy()
133+ })
193134 }
194135
195136 // assignIDs gives every workspace announced since the last done its
@@ -207,7 +148,7 @@ func (ws *workspaceState) assignIDs() {
207148 }
208149 }
209150 // Map order is random: numbers follow the announcement order.
210- slices.SortFunc(fresh, func(a, b *workspace) int { return cmp.Compare(a.ID(), b.ID()) })
151+ slices.SortFunc(fresh, func(a, b *workspace) int { return cmp.Compare(a.proxy.ID(), b.proxy.ID()) })
211152 for _, w := range fresh {
212153 w.id = ws.intern(w.idStr)
213154 ws.byID[w.id] = w
@@ -266,17 +207,14 @@ func (s *Source) bindWorkspaces() error {
266207 s.ws.byID = make(map[uint64]*workspace)
267208 s.ws.groups = make(map[uint32]*workspaceGroup)
268209 s.ws.interned = make(map[string]uint64)
269- m := &workspaceManager{s: s}
270- m.SetContext(s.wl)
271- _, err := s.display.Registry().BindNegotiated(workspaceIface, workspaceVersion, m)
272- switch {
273- case err == nil:
274- s.ws.mgr = m
275- return nil
276- case errors.Is(err, wlturbo.ErrGlobalNotFound):
277- return nil
210+ m := extworkspace.NewExtWorkspaceManager(s.wl)
211+ ok, err := s.bindOptional(extworkspace.ExtWorkspaceManagerInterface, workspaceVersion, m)
212+ if err != nil || !ok {
213+ return err
278214 }
279- return err
215+ s.watchWorkspaces(m)
216+ s.ws.mgr = m
217+ return nil
280218 }
281219
282220 // Workspaces lists the workspaces ext-workspace-v1 advertises, one entry per
@@ -324,7 +262,7 @@ func (s *Source) workspaceList() []ports.Workspace {
324262 list = append(list, ports.Workspace{
325263 ID: w.id, OutputID: o.id, Name: w.name,
326264 Region: workspaceRegion(w, o),
327- Active: w.state&wsStateActive != 0,
265+ Active: w.state&extworkspace.STATE_ACTIVE != 0,
328266 })
329267 }
330268 }
@@ -372,7 +310,7 @@ func (s *Source) workspaceKey(t ports.Target, out *output) (sessionKey, ports.Re
372310 if err := s.roundtrip(); err != nil {
373311 return sessionKey{}, ports.Region{}, err
374312 }
375- if w.removed || w.state&wsStateActive == 0 || w.group == nil || !w.group.outputs[out.proxy.ID()] {
313+ if w.removed || w.state&extworkspace.STATE_ACTIVE == 0 || w.group == nil || !w.group.outputs[out.proxy.ID()] {
376314 return sessionKey{}, ports.Region{}, ports.ErrWorkspaceUnavailable
377315 }
378316 s.ws.shown = w.id
@@ -3,13 +3,12 @@ package wayland
3 import (3 import (
4 "cmp"4 "cmp"
5 "context"5 "context"
6- "errors"
7 "slices"6 "slices"
8 "strconv"7 "strconv"
9 "unicode"8 "unicode"
10 "unicode/utf8"9 "unicode/utf8"
11 10
12- "github.com/bnema/wlturbo"11+ extworkspace "github.com/bnema/wlturbo/protocol/workspace"
13 12
14 "github.com/bnema/nefercap/internal/ports"13 "github.com/bnema/nefercap/internal/ports"
15 )14 )
@@ -19,25 +18,19 @@ import (
19 // compositor (no activate, no commit).18 // compositor (no activate, no commit).
20 19
21 const (20 const (
22- workspaceIface = "ext_workspace_manager_v1"
23 workspaceVersion = 121 workspaceVersion = 1
24 maxWorkspaces = 256 // bound of the inventory22 maxWorkspaces = 256 // bound of the inventory
25 maxNameLen = 256 // bytes, for workspace names23 maxNameLen = 256 // bytes, for workspace names
26 maxGroupOutputs = 6424 maxGroupOutputs = 64
27 25
28- wsStateActive = 1
29-
30- reqGroupDestroy = 1
31- reqHandleDestroy = 0
32-
33 maxInterned = 4 * maxWorkspaces // id strings remembered across removals26 maxInterned = 4 * maxWorkspaces // id strings remembered across removals
34 )27 )
35 28
36 // workspaceState is the ext-workspace inventory of a Source.29 // workspaceState is the ext-workspace inventory of a Source.
37 type workspaceState struct {30 type workspaceState struct {
38- mgr *workspaceManager // nil when the compositor has no ext-workspace31+ mgr *extworkspace.ExtWorkspaceManager // nil when the compositor has no ext-workspace
39- handles map[uint32]*workspace // by proxy ID32+ handles map[uint32]*workspace // by proxy ID
40- byID map[uint64]*workspace // by Source-local identity33+ byID map[uint64]*workspace // by Source-local identity
41 groups map[uint32]*workspaceGroup34 groups map[uint32]*workspaceGroup
42 next uint64 // last identity handed out; identities start at 135 next uint64 // last identity handed out; identities start at 1
43 interned map[string]uint64 // id string to identity, bounded by maxInterned36 interned map[string]uint64 // id string to identity, bounded by maxInterned
@@ -45,105 +38,67 @@ type workspaceState struct {
45 shown uint64 // workspace last verified as displayed, without the extension38 shown uint64 // workspace last verified as displayed, without the extension
46 }39 }
47 40
48-// workspaceManager receives the creation events. It is bound once.41+// watchWorkspaces subscribes to the creation events of the manager.
49-type workspaceManager struct {42+func (s *Source) watchWorkspaces(m *extworkspace.ExtWorkspaceManager) {
50- wlturbo.BaseProxy43+ m.OnWorkspaceGroup(func(proxy *extworkspace.ExtWorkspaceGroupHandle) {
51- s *Source44+ // wlturbo registered the proxy already, which absorbs its events.
52-}
53-
54-func (*workspaceManager) EventSignature(op uint16) (string, bool) {
55- switch op {
56- case 0, 1:
57- return "new_id,", true // workspace_group, workspace
58- case 2, 3:
59- return "", true // done, finished
60- }
61- return "", false
62-}
63-
64-func (m *workspaceManager) Dispatch(e *wlturbo.Event) {
65- s := m.s
66- switch e.Opcode {
67- case 0:
68- id := e.Uint32()
69- g := &workspaceGroup{s: s, outputs: make(map[uint32]bool)}
70- g.SetContext(s.wl)
71- g.SetID(id)
72- g.SetVersion(m.Version())
73- s.wl.Register(g)
74 if len(s.ws.groups) >= maxWorkspaces {45 if len(s.ws.groups) >= maxWorkspaces {
75 s.ws.overflow = true46 s.ws.overflow = true
76 return47 return
77 }48 }
78- s.ws.groups[id] = g49+ g := &workspaceGroup{proxy: proxy, outputs: make(map[uint32]bool)}
79- case 1:50+ s.ws.groups[proxy.ID()] = g
80- id := e.Uint32()51+ s.watchGroup(g)
81- w := &workspace{s: s}52+ })
82- w.SetContext(s.wl)53+ m.OnWorkspace(func(proxy *extworkspace.ExtWorkspaceHandle) {
83- w.SetID(id)
84- w.SetVersion(m.Version())
85- s.wl.Register(w)
86 if len(s.ws.handles) >= maxWorkspaces {54 if len(s.ws.handles) >= maxWorkspaces {
87 s.ws.overflow = true55 s.ws.overflow = true
88 return56 return
89 }57 }
90- s.ws.handles[id] = w58+ w := &workspace{proxy: proxy}
91- case 2:59+ s.ws.handles[proxy.ID()] = w
92- s.ws.assignIDs()60+ s.watchWorkspace(w)
93- }61+ })
62+ m.OnDone(s.ws.assignIDs)
94 }63 }
95 64
96 type workspaceGroup struct {65 type workspaceGroup struct {
97- wlturbo.BaseProxy66+ proxy *extworkspace.ExtWorkspaceGroupHandle
98- s *Source
99 outputs map[uint32]bool // wl_output proxy IDs67 outputs map[uint32]bool // wl_output proxy IDs
100 }68 }
101 69
102-func (*workspaceGroup) EventSignature(op uint16) (string, bool) {70+func (s *Source) watchGroup(g *workspaceGroup) {
103- switch op {71+ g.proxy.OnOutputEnter(func(id uint32) {
104- case 0:72+ if len(g.outputs) < maxGroupOutputs {
105- return "uint,", true // capabilities
106- case 1, 2, 3, 4:
107- return "object,", true // output_enter, output_leave, workspace_enter, workspace_leave
108- case 5:
109- return "", true // removed
110- }
111- return "", false
112-}
113-
114-func (g *workspaceGroup) Dispatch(e *wlturbo.Event) {
115- s := g.s
116- switch e.Opcode {
117- case 1:
118- if id := e.Uint32(); len(g.outputs) < maxGroupOutputs {
119 g.outputs[id] = true73 g.outputs[id] = true
120 }74 }
121- case 2:75+ })
122- delete(g.outputs, e.Uint32())76+ g.proxy.OnOutputLeave(func(id uint32) { delete(g.outputs, id) })
123- case 3:77+ g.proxy.OnWorkspaceEnter(func(id uint32) {
124- if w := s.ws.handles[e.Uint32()]; w != nil {78+ if w := s.ws.handles[id]; w != nil {
125 w.group = g79 w.group = g
126 }80 }
127- case 4:81+ })
128- if w := s.ws.handles[e.Uint32()]; w != nil && w.group == g {82+ g.proxy.OnWorkspaceLeave(func(id uint32) {
83+ if w := s.ws.handles[id]; w != nil && w.group == g {
129 w.group = nil84 w.group = nil
130 }85 }
131- case 5:86+ })
132- delete(s.ws.groups, g.ID())87+ g.proxy.OnRemoved(func() {
88+ delete(s.ws.groups, g.proxy.ID())
133 for _, w := range s.ws.handles {89 for _, w := range s.ws.handles {
134 if w.group == g {90 if w.group == g {
135 w.group = nil91 w.group = nil
136 }92 }
137 }93 }
138- _ = s.wl.Request(wlturbo.Request{Proxy: g, Opcode: reqGroupDestroy, Name: "ext_workspace_group_handle_v1.destroy", Destructor: true})94+ _ = g.proxy.Destroy()
139- }95+ })
140 }96 }
141 97
142-// workspace is one ext_workspace_handle_v1. It is a wlturbo object so that it98+// workspace is one ext_workspace_handle_v1. Its proxy is the argument of
143-// can be the argument of create_workspace_source.99+// create_workspace_source.
144 type workspace struct {100 type workspace struct {
145- wlturbo.BaseProxy101+ proxy *extworkspace.ExtWorkspaceHandle
146- s *Source
147 idStr string // the id event: empty for a temporary workspace102 idStr string // the id event: empty for a temporary workspace
148 name string103 name string
149 state uint32104 state uint32
@@ -153,43 +108,29 @@ type workspace struct {
153 frame *workspaceFrame // NeferWL's extension only; nil until asked for108 frame *workspaceFrame // NeferWL's extension only; nil until asked for
154 }109 }
155 110
156-func (*workspace) EventSignature(op uint16) (string, bool) {111+// watchWorkspace subscribes to the events of w. Coordinates are not used:
157- switch op {112+// nothing here depends on the grid.
158- case 0, 1:113+func (s *Source) watchWorkspace(w *workspace) {
159- return "string,", true // id, name114+ w.proxy.OnId(func(id string) {
160- case 2:
161- return "array,", true // coordinates
162- case 3, 4:
163- return "uint,", true // state, capabilities
164- case 5:
165- return "", true // removed
166- }
167- return "", false
168-}
169-
170-func (w *workspace) Dispatch(e *wlturbo.Event) {
171- switch e.Opcode {
172- case 0:
173 if w.idStr == "" {115 if w.idStr == "" {
174- w.idStr = e.String()116+ w.idStr = id
175 }117 }
176- case 1:118+ })
177- if n := e.String(); validName(n) {119+ w.proxy.OnName(func(n string) {
120+ if validName(n) {
178 w.name = n121 w.name = n
179 }122 }
180- // opcode 2 (coordinates) is not used: nothing here depends on the grid.123+ })
181- case 3:124+ w.proxy.OnState(func(state uint32) { w.state = state })
182- w.state = e.Uint32()125+ w.proxy.OnRemoved(func() {
183- case 5:
184 w.removed = true126 w.removed = true
185- s := w.s
186 s.releaseWorkspaceFrame(w)127 s.releaseWorkspaceFrame(w)
187- delete(s.ws.handles, w.ID())128+ delete(s.ws.handles, w.proxy.ID())
188 if w.id != 0 && s.ws.byID[w.id] == w {129 if w.id != 0 && s.ws.byID[w.id] == w {
189 delete(s.ws.byID, w.id)130 delete(s.ws.byID, w.id)
190 }131 }
191- _ = s.wl.Request(wlturbo.Request{Proxy: w, Opcode: reqHandleDestroy, Name: "ext_workspace_handle_v1.destroy", Destructor: true})132+ _ = w.proxy.Destroy()
192- }133+ })
193 }134 }
194 135
195 // assignIDs gives every workspace announced since the last done its136 // assignIDs gives every workspace announced since the last done its
@@ -207,7 +148,7 @@ func (ws *workspaceState) assignIDs() {
207 }148 }
208 }149 }
209 // Map order is random: numbers follow the announcement order.150 // Map order is random: numbers follow the announcement order.
210- slices.SortFunc(fresh, func(a, b *workspace) int { return cmp.Compare(a.ID(), b.ID()) })151+ slices.SortFunc(fresh, func(a, b *workspace) int { return cmp.Compare(a.proxy.ID(), b.proxy.ID()) })
211 for _, w := range fresh {152 for _, w := range fresh {
212 w.id = ws.intern(w.idStr)153 w.id = ws.intern(w.idStr)
213 ws.byID[w.id] = w154 ws.byID[w.id] = w
@@ -266,17 +207,14 @@ func (s *Source) bindWorkspaces() error {
266 s.ws.byID = make(map[uint64]*workspace)207 s.ws.byID = make(map[uint64]*workspace)
267 s.ws.groups = make(map[uint32]*workspaceGroup)208 s.ws.groups = make(map[uint32]*workspaceGroup)
268 s.ws.interned = make(map[string]uint64)209 s.ws.interned = make(map[string]uint64)
269- m := &workspaceManager{s: s}210+ m := extworkspace.NewExtWorkspaceManager(s.wl)
270- m.SetContext(s.wl)211+ ok, err := s.bindOptional(extworkspace.ExtWorkspaceManagerInterface, workspaceVersion, m)
271- _, err := s.display.Registry().BindNegotiated(workspaceIface, workspaceVersion, m)212+ if err != nil || !ok {
272- switch {213+ return err
273- case err == nil:
274- s.ws.mgr = m
275- return nil
276- case errors.Is(err, wlturbo.ErrGlobalNotFound):
277- return nil
278 }214 }
279- return err215+ s.watchWorkspaces(m)
216+ s.ws.mgr = m
217+ return nil
280 }218 }
281 219
282 // Workspaces lists the workspaces ext-workspace-v1 advertises, one entry per220 // Workspaces lists the workspaces ext-workspace-v1 advertises, one entry per
@@ -324,7 +262,7 @@ func (s *Source) workspaceList() []ports.Workspace {
324 list = append(list, ports.Workspace{262 list = append(list, ports.Workspace{
325 ID: w.id, OutputID: o.id, Name: w.name,263 ID: w.id, OutputID: o.id, Name: w.name,
326 Region: workspaceRegion(w, o),264 Region: workspaceRegion(w, o),
327- Active: w.state&wsStateActive != 0,265+ Active: w.state&extworkspace.STATE_ACTIVE != 0,
328 })266 })
329 }267 }
330 }268 }
@@ -372,7 +310,7 @@ func (s *Source) workspaceKey(t ports.Target, out *output) (sessionKey, ports.Re
372 if err := s.roundtrip(); err != nil {310 if err := s.roundtrip(); err != nil {
373 return sessionKey{}, ports.Region{}, err311 return sessionKey{}, ports.Region{}, err
374 }312 }
375- if w.removed || w.state&wsStateActive == 0 || w.group == nil || !w.group.outputs[out.proxy.ID()] {313+ if w.removed || w.state&extworkspace.STATE_ACTIVE == 0 || w.group == nil || !w.group.outputs[out.proxy.ID()] {
376 return sessionKey{}, ports.Region{}, ports.ErrWorkspaceUnavailable314 return sessionKey{}, ports.Region{}, ports.ErrWorkspaceUnavailable
377 }315 }
378 s.ws.shown = w.id316 s.ws.shown = w.id
M internal/adapters/wayland/xdgoutput.go +9 -41
@@ -1,59 +1,27 @@
11 package wayland
22
3-import (
4- "github.com/bnema/wlturbo"
5-
6- "github.com/bnema/nefercap/internal/ports"
7-)
3+import "github.com/bnema/nefercap/internal/ports"
84
95 // zxdg_output_v1 gives the logical size of an output, which maps a logical
106 // region onto buffer pixels when a region has to be cropped from the output
117 // frame. It is optional: the current mode over the integer scale is the
128 // fallback.
139
14-const (
15- reqGetXdgOutput = 1 // zxdg_output_manager_v1.get_xdg_output
16- reqXdgOutputDestroy = 0 // zxdg_output_v1.destroy
17-)
18-
19-type xdgOutput struct {
20- wlturbo.BaseProxy
21- o *output
22-}
23-
24-func (*xdgOutput) EventSignature(op uint16) (string, bool) {
25- switch op {
26- case 0, 1:
27- return "int,int,", true // logical_position, logical_size
28- case 2:
29- return "", true // done
30- case 3, 4:
31- return "string,", true // name, description
32- }
33- return "", false
34-}
35-
36-func (x *xdgOutput) Dispatch(e *wlturbo.Event) {
37- if e.Opcode != 1 {
38- return
39- }
40- w, h := e.Int32(), e.Int32()
41- if w >= 0 && h >= 0 && w <= ports.MaxDimension && h <= ports.MaxDimension {
42- x.o.lw, x.o.lh = w, h
43- }
44-}
45-
4610 // watchLogicalSize subscribes to the logical size of o.
4711 func (s *Source) watchLogicalSize(o *output) {
4812 if s.xdgOutputs == nil {
4913 return
5014 }
51- x := &xdgOutput{o: o}
52- x.SetContext(s.wl)
53- if err := s.wl.Request(wlturbo.Request{Proxy: s.xdgOutputs, Opcode: reqGetXdgOutput, Name: "zxdg_output_manager_v1.get_xdg_output", Child: x}, x, o.proxy); err != nil {
15+ x, err := s.xdgOutputs.GetXdgOutput(o.proxy)
16+ if err != nil {
5417 s.log.Warn().Err(err).Msg("xdg-output request failed")
5518 return
5619 }
20+ x.OnLogicalSize(func(w, h int32) {
21+ if w >= 0 && h >= 0 && w <= ports.MaxDimension && h <= ports.MaxDimension {
22+ o.lw, o.lh = w, h
23+ }
24+ })
5725 o.xdg = x
5826 }
5927
@@ -61,6 +29,6 @@ func (s *Source) watchLogicalSize(o *output) {
6129 func (s *Source) releaseXdg(o *output) {
6230 if x := o.xdg; x != nil {
6331 o.xdg = nil
64- _ = s.wl.Request(wlturbo.Request{Proxy: x, Opcode: reqXdgOutputDestroy, Name: "zxdg_output_v1.destroy", Destructor: true})
32+ _ = x.Destroy()
6533 }
6634 }
@@ -1,59 +1,27 @@
1 package wayland1 package wayland
2 2
3-import (3+import "github.com/bnema/nefercap/internal/ports"
4- "github.com/bnema/wlturbo"
5-
6- "github.com/bnema/nefercap/internal/ports"
7-)
8 4
9 // zxdg_output_v1 gives the logical size of an output, which maps a logical5 // zxdg_output_v1 gives the logical size of an output, which maps a logical
10 // region onto buffer pixels when a region has to be cropped from the output6 // region onto buffer pixels when a region has to be cropped from the output
11 // frame. It is optional: the current mode over the integer scale is the7 // frame. It is optional: the current mode over the integer scale is the
12 // fallback.8 // fallback.
13 9
14-const (
15- reqGetXdgOutput = 1 // zxdg_output_manager_v1.get_xdg_output
16- reqXdgOutputDestroy = 0 // zxdg_output_v1.destroy
17-)
18-
19-type xdgOutput struct {
20- wlturbo.BaseProxy
21- o *output
22-}
23-
24-func (*xdgOutput) EventSignature(op uint16) (string, bool) {
25- switch op {
26- case 0, 1:
27- return "int,int,", true // logical_position, logical_size
28- case 2:
29- return "", true // done
30- case 3, 4:
31- return "string,", true // name, description
32- }
33- return "", false
34-}
35-
36-func (x *xdgOutput) Dispatch(e *wlturbo.Event) {
37- if e.Opcode != 1 {
38- return
39- }
40- w, h := e.Int32(), e.Int32()
41- if w >= 0 && h >= 0 && w <= ports.MaxDimension && h <= ports.MaxDimension {
42- x.o.lw, x.o.lh = w, h
43- }
44-}
45-
46 // watchLogicalSize subscribes to the logical size of o.10 // watchLogicalSize subscribes to the logical size of o.
47 func (s *Source) watchLogicalSize(o *output) {11 func (s *Source) watchLogicalSize(o *output) {
48 if s.xdgOutputs == nil {12 if s.xdgOutputs == nil {
49 return13 return
50 }14 }
51- x := &xdgOutput{o: o}15+ x, err := s.xdgOutputs.GetXdgOutput(o.proxy)
52- x.SetContext(s.wl)16+ if err != nil {
53- if err := s.wl.Request(wlturbo.Request{Proxy: s.xdgOutputs, Opcode: reqGetXdgOutput, Name: "zxdg_output_manager_v1.get_xdg_output", Child: x}, x, o.proxy); err != nil {
54 s.log.Warn().Err(err).Msg("xdg-output request failed")17 s.log.Warn().Err(err).Msg("xdg-output request failed")
55 return18 return
56 }19 }
20+ x.OnLogicalSize(func(w, h int32) {
21+ if w >= 0 && h >= 0 && w <= ports.MaxDimension && h <= ports.MaxDimension {
22+ o.lw, o.lh = w, h
23+ }
24+ })
57 o.xdg = x25 o.xdg = x
58 }26 }
59 27
@@ -61,6 +29,6 @@ func (s *Source) watchLogicalSize(o *output) {
61 func (s *Source) releaseXdg(o *output) {29 func (s *Source) releaseXdg(o *output) {
62 if x := o.xdg; x != nil {30 if x := o.xdg; x != nil {
63 o.xdg = nil31 o.xdg = nil
64- _ = s.wl.Request(wlturbo.Request{Proxy: x, Opcode: reqXdgOutputDestroy, Name: "zxdg_output_v1.destroy", Destructor: true})32+ _ = x.Destroy()
65 }33 }
66 }34 }

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