v0.3.0 cf370a0

excluded.go RawHistoryFolder

1package capture
2
3import (
4	"fmt"
5	"os"
6	"slices"
7
8	"github.com/bnema/neferwl/internal/ports"
9)
10
11// A request with Exclude leaves out the exclusion's HUD layers and popups;
12// every other capture shows them. No capture shows the capture indicator
13// (Scene.CaptureIndicators): a scene that carries it is never the source of a
14// capture image. Requests of the displayed frame are then served from the
15// frame rendered without it first (SubmitPlain, ExcludedScene).
16
17// ErrSessionInactive fails an Exclude request whose session is not the one
18// the scene carries. Serving it as a normal capture would leak its HUD.
19var ErrSessionInactive = fmt.Errorf("%w: capture exclusion is not active", ports.ErrCaptureTransient)
20
21// ErrSessionStale fails an Exclude request stamped with a revision newer than
22// the scene's: the scene predates a change of what an excluded frame must
23// hide (a new HUD layer, popup, target), so its exclusion list could leak.
24var ErrSessionStale = fmt.Errorf("%w: capture exclusion scene is older than the request", ports.ErrCaptureTransient)
25
26// ErrWorkspaceMoved fails a workspace request whose workspace is neither on
27// screen nor the one rendered off screen in the scene, or a window request
28// whose window is not the one rendered off screen: it moved, went away, or
29// the scene predates the session.
30var ErrWorkspaceMoved = fmt.Errorf("%w: captured workspace is not where the request expected", ports.ErrCaptureTransient)
31
32// excluder derives the scene of Exclude captures: the frame without the
33// excluded layers and popups. Its scratch slices are reused,
34// so a warmed output allocates nothing.
35type excluder struct {
36	windows []ports.SceneWindow
37	layers  []ports.SceneLayer
38}
39
40// Shows reports whether a content change of id needs a new frame: the
41// displayed scene draws it, or the hidden workspace of a capture does (its
42// child render and the reads it releases). Flips and Shown still count the
43// displayed scene only.
44func Shows(s ports.Scene, id ports.WindowID) bool {
45	return s.Shows(id) || s.CaptureScene != nil && s.CaptureScene.Shows(id)
46}
47
48// excludedShown reports whether the scene draws an excluded surface.
49func excludedShown(s ports.Scene) bool {
50	c := s.Capture
51	if c == nil || len(c.Excluded) == 0 {
52		return false
53	}
54	for _, w := range s.Windows {
55		if slices.Contains(c.Excluded, w.ID) {
56			return true
57		}
58	}
59	for _, l := range s.Layers {
60		if slices.Contains(c.Excluded, l.ID) {
61			return true
62		}
63	}
64	return false
65}
66
67// plain returns s without its capture state and indicators, Seq 0 so the
68// renderer redraws the whole target instead of reusing a displayed frame.
69func plain(s ports.Scene) ports.Scene {
70	s.Capture, s.CaptureIndicators, s.Seq = nil, nil, 0
71	return s
72}
73
74// SubmitPlain serves the displayed-frame requests of a scene that carries the
75// capture indicator: the frame is rendered without it, first (same renderer,
76// queue order, like ExcludedScene), and copied before the displayed one is
77// drawn. It reports whether it took the requests (and zeroed them); false
78// when the scene has no indicator or there is nothing to serve, the caller
79// submits them after the displayed frame as usual. On a render error the
80// caller still owns the requests.
81//
82// Admission is the session gate's: the requests are handed over with
83// SubmitScoped under the scene's epoch, so a locked or changed epoch fails
84// them before BeginCapture. The render fence is returned, never closed here:
85// the plain render reads client buffers even when the requests are rejected,
86// so the caller owns it (hold or wait it) whatever happened, error included.
87func (p *Pipeline) SubmitPlain(r ports.Renderer, s ports.Scene, surfaces map[ports.WindowID]ports.SurfaceContent, normal []ports.CaptureRequest) (took bool, done *os.File, err error) {
88	if len(normal) == 0 || len(s.CaptureIndicators) == 0 {
89		return false, nil, nil
90	}
91	done, err = r.Render(plain(s), surfaces)
92	if err != nil {
93		return false, done, err
94	}
95	p.SubmitScoped(s.Security, r, normal)
96	clear(normal)
97	return true, done, nil
98}
99
100// scene returns s without the excluded windows
101// (popups included) and layers. The result is valid until the next call.
102func (c *excluder) scene(s ports.Scene) ports.Scene {
103	var ex []ports.WindowID
104	if s.Capture != nil {
105		ex = s.Capture.Excluded
106	}
107	s = plain(s)
108	if len(ex) == 0 {
109		return s
110	}
111	c.windows = c.windows[:0]
112	for _, w := range s.Windows {
113		if !slices.Contains(ex, w.ID) {
114			c.windows = append(c.windows, w)
115		}
116	}
117	c.layers = c.layers[:0]
118	for _, l := range s.Layers {
119		if !slices.Contains(ex, l.ID) {
120			c.layers = append(c.layers, l)
121		}
122	}
123	s.Windows, s.Layers = c.windows, c.layers
124	return s
125}
126
127// ExcludedScene is the scene of Exclude captures: no excluded surface; see Split. Valid until the next call.
128func (p *Pipeline) ExcludedScene(s ports.Scene) ports.Scene { return p.excl.scene(s) }
129
130// Split answers or orders the requests of one output frame, in place. No
131// allocation. Requests that cannot be served are failed and removed:
132// Exclude requests of a session other than the scene's, or with no exclusion
133// in it (ErrSessionInactive), and those newer than its Revision
134// (ErrSessionStale); workspace requests whose workspace is not where they
135// expected (ErrWorkspaceMoved). The rest is reordered:
136//   - normal is the prefix, drawn from the displayed frame;
137//   - excluded follows: Exclude requests, drawn from ExcludedScene rendered first,
138//     or from the displayed frame (they stay in normal) when it shows no
139//     excluded surface and no capture indicator;
140//   - hidden: the requests of the workspace rendered off screen
141//     (OffScreen), served from CaptureScene (see SubmitHidden); they come last.
142//
143// The entries past the groups are zeroed.
144func (p *Pipeline) Split(s ports.Scene, reqs []ports.CaptureRequest) (normal, excluded, hidden []ports.CaptureRequest) {
145	n := 0
146	for _, q := range reqs {
147		var err error
148		switch {
149		case q.Exclude && (s.Capture == nil || s.Capture.Session == 0 || q.Session != s.Capture.Session):
150			err = ErrSessionInactive
151		case q.Exclude && s.Capture.Revision < q.CaptureRevision:
152			err = ErrSessionStale
153		case q.Workspace != 0 && (s.Capture == nil || q.OffScreen && q.Workspace != s.Capture.Workspace || !q.OffScreen && q.Workspace != s.Capture.Shown):
154			err = ErrWorkspaceMoved
155		case q.Window != 0 && (s.Capture == nil || !q.OffScreen || q.Window != s.Capture.Window):
156			err = ErrWorkspaceMoved
157		}
158		if err != nil {
159			Fail(p.ctx, q, err, p.replies)
160			continue
161		}
162		reqs[n] = q
163		n++
164	}
165	clear(reqs[n:])
166	reqs = reqs[:n]
167	excl := excludedShown(s) || len(s.CaptureIndicators) > 0
168	group := func(q ports.CaptureRequest) int {
169		switch {
170		case q.OffScreen:
171			return 2
172		case q.Exclude && excl:
173			return 1
174		}
175		return 0
176	}
177	var counts [3]int
178	for _, q := range reqs {
179		counts[group(q)]++
180	}
181	j := 0
182	for g := range 3 {
183		for i := j; i < n; i++ {
184			if group(reqs[i]) == g {
185				reqs[i], reqs[j] = reqs[j], reqs[i]
186				j++
187			}
188		}
189	}
190	a, b := counts[0], counts[0]+counts[1]
191	return reqs[:a], reqs[a:b], reqs[b:n]
192}
193
194// Handed reports a zeroed request: one Split failed, or one already given to
195// the worker. The owner never answers it again.
196func Handed(q ports.CaptureRequest) bool { return q.ID == 0 && q.Dst.File == nil }

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 25

Tags 10

↑↓ moveenter openesc close