detached 2c2814d

refactor(wayland): extract the shared capture target from capture sessions

commit
2c2814d4accacf5acd336b468b8511c548b8d8f4
author
brice · · 2026-10-14 19:05 UTC
committer
brice · · 2026-10-14 21:39 UTC
parent
8e85801
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +76 -31
M internal/adapters/wayland/imagecopy.go +76 -31
@@ -180,7 +180,7 @@ func (m copyCaptureManager) CreateSession(r *ext.ExtImageCopyCaptureManagerV1, i
180180 if src != nil {
181181 cs = s.captureSources[src.Resource]
182182 }
183- state := &captureSession{s: s, src: cs, cursor: options&1 != 0}
183+ state := &captureSession{captureTarget: captureTarget{s: s, src: cs}, cursor: options&1 != 0}
184184 res, err := ext.NewExtImageCopyCaptureSessionV1(r.Client(), 1, id, state)
185185 if err != nil {
186186 return
@@ -243,19 +243,26 @@ func (c *cursorCaptureSession) GetCaptureSession(_ *ext.ExtImageCopyCaptureCurso
243243 }
244244 }
245245
246+// captureTarget is what every registered capture follows, whatever consumes
247+// its frames (an ext_image_copy_capture_session_v1 or a screencast cast): the
248+// source, the id core knows it by and core's last decision about it.
249+type captureTarget struct {
250+ s *Server
251+ src *captureSource
252+ id uint64 // registered with core; 0 when never
253+ // st is the last decision of core; haveState tells there is one.
254+ st ports.CaptureSessionState
255+ haveState bool
256+}
257+
246258 // captureSession is one ext_image_copy_capture_session_v1.
247259 type captureSession struct {
248- s *Server
260+ captureTarget
249261 res *ext.ExtImageCopyCaptureSessionV1
250- src *captureSource
251- id uint64 // registered with core; 0 when never
252262 cursor, stopped bool
253263 frame *captureExtFrame
254264 sent image.Point // buffer size of the last constraints
255- // st is the last decision of core; haveState tells there is one.
256- st ports.CaptureSessionState
257- haveState bool
258- excl *exclusion // the live exclusion object of this session
265+ excl *exclusion // the live exclusion object of this session
259266 // exclSeen: the last state of core knew an exclusion. Frames of an
260267 // excluded session leave the HUD out while it is (see tag).
261268 exclSeen bool
@@ -290,7 +297,7 @@ func (c *captureSession) end(send, notify bool) {
290297 // image of its off-screen render, from the origin) and whether the workspace
291298 // is off screen. Core's last state decides where a workspace is; before it,
292299 // the last workspace snapshot does.
293-func (c *captureSession) geometry() (o *output, rect image.Rectangle, hidden, ok bool) {
300+func (c *captureTarget) geometry() (o *output, rect image.Rectangle, hidden, ok bool) {
294301 src := c.src
295302 if src == nil {
296303 return nil, image.Rectangle{}, false, false
@@ -377,24 +384,57 @@ func (c *captureSession) sendConstraints() {
377384 c.res.SendDone()
378385 }
379386
380-// refresh follows the target: new constraints when the buffer size changed,
381-// the end when nothing is left of it.
382-func (c *captureSession) refresh() {
383- if c.stopped || c.id == 0 {
384- return
385- }
387+// targetFate says what became of a followed target.
388+type targetFate uint8
389+
390+const (
391+ targetLive targetFate = iota
392+ targetWorkspaceGone // the workspace left the snapshot
393+ targetEmpty // an output or region with nothing left
394+)
395+
396+// follow looks at the target now: its fate and, while it lives, its geometry
397+// (rect, ok as geometry gives them). A window or workspace that is not
398+// locatable for the moment stays live: core's next state decides.
399+func (c *captureTarget) follow() (fate targetFate, rect image.Rectangle, ok bool) {
386400 if c.src.kind == srcWorkspace {
387401 // The workspace left the snapshot: its handle is removed, stop at once
388402 // without waiting for core's state.
389403 if _, _, found := c.s.workspaceInfo(c.src.workspace); !found {
390- c.s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonWorkspaceGone)).Msg("capture session end")
391- c.end(true, true)
392- return
404+ return targetWorkspaceGone, image.Rectangle{}, false
393405 }
394406 }
395- _, rect, _, ok := c.geometry()
407+ _, rect, _, ok = c.geometry()
408+ if !ok && c.src.kind != srcWorkspace && c.src.kind != srcWindow {
409+ return targetEmpty, rect, false
410+ }
411+ return targetLive, rect, ok
412+}
413+
414+// applyState takes what core decided; it reports a decision that ends the
415+// target (a terminal reason).
416+func (c *captureTarget) applyState(st ports.CaptureSessionState) (ended bool) {
417+ if st.Reason.Terminal() {
418+ c.s.log.Info().Uint64("session", c.id).Str("reason", string(st.Reason)).Msg("capture session end")
419+ return true
420+ }
421+ c.st, c.haveState = st, true
422+ c.s.log.Debug().Uint64("session", c.id).Bool("active", st.Active).Bool("hidden", st.Hidden).Uint64("revision", st.Revision).Msg("capture session state")
423+ return false
424+}
425+
426+// refresh follows the target: new constraints when the buffer size changed,
427+// the end when nothing is left of it.
428+func (c *captureSession) refresh() {
429+ if c.stopped || c.id == 0 {
430+ return
431+ }
432+ fate, rect, ok := c.follow()
396433 switch {
397- case !ok && c.src.kind != srcWorkspace && c.src.kind != srcWindow:
434+ case fate == targetWorkspaceGone:
435+ c.s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonWorkspaceGone)).Msg("capture session end")
436+ c.end(true, true)
437+ case fate == targetEmpty:
398438 c.s.log.Info().Uint64("session", c.id).Msg("capture session end: target empty")
399439 c.end(true, true)
400440 case ok && image.Pt(rect.Dx(), rect.Dy()) != c.sent:
@@ -438,13 +478,11 @@ func (s *Server) captureState(st ports.CaptureSessionState) {
438478 if c == nil {
439479 return
440480 }
441- if st.Reason.Terminal() {
442- s.log.Info().Uint64("session", c.id).Str("reason", string(st.Reason)).Msg("capture session end")
481+ if c.applyState(st) {
443482 c.end(true, false)
444483 return
445484 }
446- c.st, c.haveState, c.exclSeen = st, true, st.Exclusion
447- s.log.Debug().Uint64("session", c.id).Bool("active", st.Active).Bool("hidden", st.Hidden).Uint64("revision", st.Revision).Msg("capture session state")
485+ c.exclSeen = st.Exclusion
448486 if ex := c.excl; ex != nil {
449487 ex.confirm(st.Layers)
450488 }
@@ -457,6 +495,19 @@ func (s *Server) captureState(st ports.CaptureSessionState) {
457495 // still in the last state, a frame is not served rather than shown with the
458496 // HUD in it (ok false).
459497 func (c *captureSession) tag(hidden bool) (t captureTag, ok bool) {
498+ t = c.targetTag(hidden)
499+ if c.excl != nil && !c.exclSeen {
500+ return t, false
501+ }
502+ if c.exclSeen {
503+ t.exclude, t.revision = true, c.st.Revision
504+ }
505+ return t, true
506+}
507+
508+// targetTag is the part of a frame's tag the target decides: the session,
509+// what is captured and what core is told of it.
510+func (c *captureTarget) targetTag(hidden bool) (t captureTag) {
460511 t = captureTag{session: c.id}
461512 switch c.src.kind {
462513 case srcWorkspace:
@@ -475,13 +526,7 @@ func (c *captureSession) tag(hidden bool) (t captureTag, ok bool) {
475526 case srcWindow:
476527 t.taken = ports.CaptureFrameTaken{Window: c.src.window}
477528 }
478- if c.excl != nil && !c.exclSeen {
479- return t, false
480- }
481- if c.exclSeen {
482- t.exclude, t.revision = true, c.st.Revision
483- }
484- return t, true
529+ return t
485530 }
486531
487532 func (c *captureSession) CreateFrame(r *ext.ExtImageCopyCaptureSessionV1, id uint32) {
@@ -180,7 +180,7 @@ func (m copyCaptureManager) CreateSession(r *ext.ExtImageCopyCaptureManagerV1, i
180 if src != nil {180 if src != nil {
181 cs = s.captureSources[src.Resource]181 cs = s.captureSources[src.Resource]
182 }182 }
183- state := &captureSession{s: s, src: cs, cursor: options&1 != 0}183+ state := &captureSession{captureTarget: captureTarget{s: s, src: cs}, cursor: options&1 != 0}
184 res, err := ext.NewExtImageCopyCaptureSessionV1(r.Client(), 1, id, state)184 res, err := ext.NewExtImageCopyCaptureSessionV1(r.Client(), 1, id, state)
185 if err != nil {185 if err != nil {
186 return186 return
@@ -243,19 +243,26 @@ func (c *cursorCaptureSession) GetCaptureSession(_ *ext.ExtImageCopyCaptureCurso
243 }243 }
244 }244 }
245 245
246+// captureTarget is what every registered capture follows, whatever consumes
247+// its frames (an ext_image_copy_capture_session_v1 or a screencast cast): the
248+// source, the id core knows it by and core's last decision about it.
249+type captureTarget struct {
250+ s *Server
251+ src *captureSource
252+ id uint64 // registered with core; 0 when never
253+ // st is the last decision of core; haveState tells there is one.
254+ st ports.CaptureSessionState
255+ haveState bool
256+}
257+
246 // captureSession is one ext_image_copy_capture_session_v1.258 // captureSession is one ext_image_copy_capture_session_v1.
247 type captureSession struct {259 type captureSession struct {
248- s *Server260+ captureTarget
249 res *ext.ExtImageCopyCaptureSessionV1261 res *ext.ExtImageCopyCaptureSessionV1
250- src *captureSource
251- id uint64 // registered with core; 0 when never
252 cursor, stopped bool262 cursor, stopped bool
253 frame *captureExtFrame263 frame *captureExtFrame
254 sent image.Point // buffer size of the last constraints264 sent image.Point // buffer size of the last constraints
255- // st is the last decision of core; haveState tells there is one.265+ excl *exclusion // the live exclusion object of this session
256- st ports.CaptureSessionState
257- haveState bool
258- excl *exclusion // the live exclusion object of this session
259 // exclSeen: the last state of core knew an exclusion. Frames of an266 // exclSeen: the last state of core knew an exclusion. Frames of an
260 // excluded session leave the HUD out while it is (see tag).267 // excluded session leave the HUD out while it is (see tag).
261 exclSeen bool268 exclSeen bool
@@ -290,7 +297,7 @@ func (c *captureSession) end(send, notify bool) {
290 // image of its off-screen render, from the origin) and whether the workspace297 // image of its off-screen render, from the origin) and whether the workspace
291 // is off screen. Core's last state decides where a workspace is; before it,298 // is off screen. Core's last state decides where a workspace is; before it,
292 // the last workspace snapshot does.299 // the last workspace snapshot does.
293-func (c *captureSession) geometry() (o *output, rect image.Rectangle, hidden, ok bool) {300+func (c *captureTarget) geometry() (o *output, rect image.Rectangle, hidden, ok bool) {
294 src := c.src301 src := c.src
295 if src == nil {302 if src == nil {
296 return nil, image.Rectangle{}, false, false303 return nil, image.Rectangle{}, false, false
@@ -377,24 +384,57 @@ func (c *captureSession) sendConstraints() {
377 c.res.SendDone()384 c.res.SendDone()
378 }385 }
379 386
380-// refresh follows the target: new constraints when the buffer size changed,387+// targetFate says what became of a followed target.
381-// the end when nothing is left of it.388+type targetFate uint8
382-func (c *captureSession) refresh() {389+
383- if c.stopped || c.id == 0 {390+const (
384- return391+ targetLive targetFate = iota
385- }392+ targetWorkspaceGone // the workspace left the snapshot
393+ targetEmpty // an output or region with nothing left
394+)
395+
396+// follow looks at the target now: its fate and, while it lives, its geometry
397+// (rect, ok as geometry gives them). A window or workspace that is not
398+// locatable for the moment stays live: core's next state decides.
399+func (c *captureTarget) follow() (fate targetFate, rect image.Rectangle, ok bool) {
386 if c.src.kind == srcWorkspace {400 if c.src.kind == srcWorkspace {
387 // The workspace left the snapshot: its handle is removed, stop at once401 // The workspace left the snapshot: its handle is removed, stop at once
388 // without waiting for core's state.402 // without waiting for core's state.
389 if _, _, found := c.s.workspaceInfo(c.src.workspace); !found {403 if _, _, found := c.s.workspaceInfo(c.src.workspace); !found {
390- c.s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonWorkspaceGone)).Msg("capture session end")404+ return targetWorkspaceGone, image.Rectangle{}, false
391- c.end(true, true)
392- return
393 }405 }
394 }406 }
395- _, rect, _, ok := c.geometry()407+ _, rect, _, ok = c.geometry()
408+ if !ok && c.src.kind != srcWorkspace && c.src.kind != srcWindow {
409+ return targetEmpty, rect, false
410+ }
411+ return targetLive, rect, ok
412+}
413+
414+// applyState takes what core decided; it reports a decision that ends the
415+// target (a terminal reason).
416+func (c *captureTarget) applyState(st ports.CaptureSessionState) (ended bool) {
417+ if st.Reason.Terminal() {
418+ c.s.log.Info().Uint64("session", c.id).Str("reason", string(st.Reason)).Msg("capture session end")
419+ return true
420+ }
421+ c.st, c.haveState = st, true
422+ c.s.log.Debug().Uint64("session", c.id).Bool("active", st.Active).Bool("hidden", st.Hidden).Uint64("revision", st.Revision).Msg("capture session state")
423+ return false
424+}
425+
426+// refresh follows the target: new constraints when the buffer size changed,
427+// the end when nothing is left of it.
428+func (c *captureSession) refresh() {
429+ if c.stopped || c.id == 0 {
430+ return
431+ }
432+ fate, rect, ok := c.follow()
396 switch {433 switch {
397- case !ok && c.src.kind != srcWorkspace && c.src.kind != srcWindow:434+ case fate == targetWorkspaceGone:
435+ c.s.log.Info().Uint64("session", c.id).Str("reason", string(ports.CaptureReasonWorkspaceGone)).Msg("capture session end")
436+ c.end(true, true)
437+ case fate == targetEmpty:
398 c.s.log.Info().Uint64("session", c.id).Msg("capture session end: target empty")438 c.s.log.Info().Uint64("session", c.id).Msg("capture session end: target empty")
399 c.end(true, true)439 c.end(true, true)
400 case ok && image.Pt(rect.Dx(), rect.Dy()) != c.sent:440 case ok && image.Pt(rect.Dx(), rect.Dy()) != c.sent:
@@ -438,13 +478,11 @@ func (s *Server) captureState(st ports.CaptureSessionState) {
438 if c == nil {478 if c == nil {
439 return479 return
440 }480 }
441- if st.Reason.Terminal() {481+ if c.applyState(st) {
442- s.log.Info().Uint64("session", c.id).Str("reason", string(st.Reason)).Msg("capture session end")
443 c.end(true, false)482 c.end(true, false)
444 return483 return
445 }484 }
446- c.st, c.haveState, c.exclSeen = st, true, st.Exclusion485+ c.exclSeen = st.Exclusion
447- s.log.Debug().Uint64("session", c.id).Bool("active", st.Active).Bool("hidden", st.Hidden).Uint64("revision", st.Revision).Msg("capture session state")
448 if ex := c.excl; ex != nil {486 if ex := c.excl; ex != nil {
449 ex.confirm(st.Layers)487 ex.confirm(st.Layers)
450 }488 }
@@ -457,6 +495,19 @@ func (s *Server) captureState(st ports.CaptureSessionState) {
457 // still in the last state, a frame is not served rather than shown with the495 // still in the last state, a frame is not served rather than shown with the
458 // HUD in it (ok false).496 // HUD in it (ok false).
459 func (c *captureSession) tag(hidden bool) (t captureTag, ok bool) {497 func (c *captureSession) tag(hidden bool) (t captureTag, ok bool) {
498+ t = c.targetTag(hidden)
499+ if c.excl != nil && !c.exclSeen {
500+ return t, false
501+ }
502+ if c.exclSeen {
503+ t.exclude, t.revision = true, c.st.Revision
504+ }
505+ return t, true
506+}
507+
508+// targetTag is the part of a frame's tag the target decides: the session,
509+// what is captured and what core is told of it.
510+func (c *captureTarget) targetTag(hidden bool) (t captureTag) {
460 t = captureTag{session: c.id}511 t = captureTag{session: c.id}
461 switch c.src.kind {512 switch c.src.kind {
462 case srcWorkspace:513 case srcWorkspace:
@@ -475,13 +526,7 @@ func (c *captureSession) tag(hidden bool) (t captureTag, ok bool) {
475 case srcWindow:526 case srcWindow:
476 t.taken = ports.CaptureFrameTaken{Window: c.src.window}527 t.taken = ports.CaptureFrameTaken{Window: c.src.window}
477 }528 }
478- if c.excl != nil && !c.exclSeen {529+ return t
479- return t, false
480- }
481- if c.exclSeen {
482- t.exclude, t.revision = true, c.st.Revision
483- }
484- return t, true
485 }530 }
486 531
487 func (c *captureSession) CreateFrame(r *ext.ExtImageCopyCaptureSessionV1, id uint32) {532 func (c *captureSession) CreateFrame(r *ext.ExtImageCopyCaptureSessionV1, id uint32) {

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 26

Tags 10

↑↓ moveenter openesc close