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) or the position hint. Hint labels are shown to
14// the captures of Wayland clients only when hint.capture is on
15// (HintStyle.Capture), never to the compositor's own captures
16// (CaptureIDInternal) and never with hint.capture off. A scene that carries
17// something a capture must not show is never the source of its image:
18// requests of the displayed frame are served from a frame rendered without
19// it first (SubmitPlain, ExcludedScene).
20//
21// A frame needs at most two batches: one for the displayed-frame requests
22// (served from the displayed or from the plain frame, never both) and one
23// for the Exclude requests. While labels are kept for clients, an internal
24// capture would need a third, labelless one: Split fails it instead.
25
26// ErrInternalLabelled fails an internal capture (CaptureIDInternal) of a frame
27// that shows hint labels kept for client captures (hint.capture): serving it
28// without labels would need a third capture batch, and serving it with them
29// would make the hint scanner read its own labels. Core only starts a scan
30// from the idle hint mode (core/hint.go hintRequest), so labels and a scan in
31// flight coincide only on a stale scene; the scan fails with a transient
32// error and the user retries.
33var ErrInternalLabelled = fmt.Errorf("%w: internal capture on a frame that keeps hint labels for clients", ports.ErrCaptureTransient)
34
35// ErrInternalExclude fails an internal Exclude capture of a frame that keeps
36// hint labels: its excluded frame carries them too.
37var ErrInternalExclude = fmt.Errorf("%w: internal capture cannot read the excluded frame, which keeps hint labels", ports.ErrCaptureTransient)
38
39// ErrSessionInactive fails an Exclude request whose session is not the one
40// the scene carries. Serving it as a normal capture would leak its HUD.
41var ErrSessionInactive = fmt.Errorf("%w: capture exclusion is not active", ports.ErrCaptureTransient)
42
43// ErrSessionStale fails an Exclude request stamped with a revision newer than
44// the scene's: the scene predates a change of what an excluded frame must
45// hide (a new HUD layer, popup, target), so its exclusion list could leak.
46var ErrSessionStale = fmt.Errorf("%w: capture exclusion scene is older than the request", ports.ErrCaptureTransient)
47
48// ErrWorkspaceMoved fails a workspace request whose workspace is neither on
49// screen nor the one rendered off screen in the scene, or a window request
50// whose window is not the one rendered off screen: it moved, went away, or
51// the scene predates the session.
52var ErrWorkspaceMoved = fmt.Errorf("%w: captured workspace is not where the request expected", ports.ErrCaptureTransient)
53
54// excluder derives the scene of Exclude captures: the frame without the
55// excluded layers and popups. Its scratch slices are reused,
56// so a warmed output allocates nothing.
57type excluder struct {
58	windows []ports.SceneWindow
59	layers  []ports.SceneLayer
60}
61
62// Shows reports whether a content change of id needs a new frame: the
63// displayed scene draws it, or the hidden workspace of a capture does (its
64// child render and the reads it releases). Flips and Shown still count the
65// displayed scene only.
66func Shows(s ports.Scene, id ports.WindowID) bool {
67	return s.Shows(id) || s.CaptureScene != nil && s.CaptureScene.Shows(id)
68}
69
70// excludedShown reports whether the scene draws an excluded surface.
71func excludedShown(s ports.Scene) bool {
72	c := s.Capture
73	if c == nil || len(c.Excluded) == 0 {
74		return false
75	}
76	for _, w := range s.Windows {
77		if slices.Contains(c.Excluded, w.ID) {
78			return true
79		}
80	}
81	for _, l := range s.Layers {
82		if slices.Contains(c.Excluded, l.ID) {
83			return true
84		}
85	}
86	return false
87}
88
89// keepsLabels reports whether the scene shows hint labels that client
90// captures keep (hint.capture): the frames derived for captures keep them.
91func keepsLabels(s ports.Scene) bool { return s.HintStyle.Capture && len(s.HintLabels) > 0 }
92
93// internal reports a capture requested by the compositor itself (hint scan).
94func internal(q ports.CaptureRequest) bool { return q.ID&ports.CaptureIDInternal != 0 }
95
96// plain returns s as a capture shows it: without its capture state,
97// indicators and position hint, and without hint labels unless they are kept
98// for clients (keepsLabels). Seq is 0 so the renderer redraws the whole
99// target instead of reusing a displayed frame.
100func plain(s ports.Scene) ports.Scene {
101	s.Capture, s.CaptureIndicators, s.Seq = nil, nil, 0
102	if !s.HintStyle.Capture {
103		s.HintLabels = nil
104	}
105	s.PositionHint = ports.PositionHint{}
106	return s
107}
108
109// SubmitPlain serves the displayed-frame requests of a scene that carries
110// what captures must not show: the capture indicator, the position hint, or
111// hint labels while they are not kept for clients. The frame is rendered
112// without it, first (same renderer, queue order, like ExcludedScene), and
113// copied before the displayed one is drawn. It reports whether it took the
114// requests (and zeroed them); false when the displayed frame already shows
115// what they may (no indicator, no position hint, and no labels or labels
116// kept for clients) or there is nothing to serve, then the caller submits
117// them after the displayed frame as usual. On a render error the caller still
118// owns the requests. Internal requests never reach it on a frame that keeps
119// labels: Split fails them.
120//
121// Admission is the session gate's: the requests are handed over with
122// SubmitScoped under the scene's epoch, so a locked or changed epoch fails
123// them before BeginCapture. The render fence is returned, never closed here:
124// the plain render reads client buffers even when the requests are rejected,
125// so the caller owns it (hold or wait it) whatever happened, error included.
126func (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) {
127	if len(normal) == 0 || len(s.CaptureIndicators) == 0 && (len(s.HintLabels) == 0 || keepsLabels(s)) && !s.PositionHint.Shown() {
128		return false, nil, nil
129	}
130	done, err = r.Render(plain(s), surfaces)
131	if err != nil {
132		return false, done, err
133	}
134	p.SubmitScoped(s.Security, r, normal)
135	clear(normal)
136	return true, done, nil
137}
138
139// scene returns s without the excluded windows
140// (popups included) and layers. The result is valid until the next call.
141func (c *excluder) scene(s ports.Scene) ports.Scene {
142	var ex []ports.WindowID
143	if s.Capture != nil {
144		ex = s.Capture.Excluded
145	}
146	s = plain(s)
147	if len(ex) == 0 {
148		return s
149	}
150	c.windows = c.windows[:0]
151	for _, w := range s.Windows {
152		if !slices.Contains(ex, w.ID) {
153			c.windows = append(c.windows, w)
154		}
155	}
156	c.layers = c.layers[:0]
157	for _, l := range s.Layers {
158		if !slices.Contains(ex, l.ID) {
159			c.layers = append(c.layers, l)
160		}
161	}
162	s.Windows, s.Layers = c.windows, c.layers
163	return s
164}
165
166// ExcludedScene is the scene of Exclude captures: no excluded surface; see Split. Valid until the next call.
167func (p *Pipeline) ExcludedScene(s ports.Scene) ports.Scene { return p.excl.scene(s) }
168
169// Split answers or orders the requests of one output frame, in place. No
170// allocation. Requests that cannot be served are failed and removed:
171//   - internal requests of a frame that keeps hint labels for clients
172//     (ErrInternalLabelled, ErrInternalExclude);
173//   - Exclude requests of a session other than the scene's, or with no
174//     exclusion in it (ErrSessionInactive), and those newer than its Revision
175//     (ErrSessionStale);
176//   - workspace requests whose workspace is not where they expected
177//     (ErrWorkspaceMoved).
178//
179// The rest is reordered:
180//   - normal is the prefix, drawn from the displayed frame (or from the plain
181//     frame, see SubmitPlain);
182//   - excluded follows: Exclude requests, drawn from ExcludedScene rendered
183//     first, or from the displayed frame (they stay in normal) when it shows
184//     no excluded surface, no capture indicator or position hint, and no
185//     hint labels that must stay out of captures;
186//   - hidden: the requests of the workspace rendered off screen
187//     (OffScreen), served from CaptureScene (see SubmitHidden); they come last.
188//
189// The entries past the groups are zeroed.
190func (p *Pipeline) Split(s ports.Scene, reqs []ports.CaptureRequest) (normal, excluded, hidden []ports.CaptureRequest) {
191	n := 0
192	for _, q := range reqs {
193		var err error
194		switch {
195		case internal(q) && keepsLabels(s):
196			err = ErrInternalLabelled
197			if q.Exclude {
198				err = ErrInternalExclude
199			}
200		case q.Exclude && (s.Capture == nil || s.Capture.Session == 0 || q.Session != s.Capture.Session):
201			err = ErrSessionInactive
202		case q.Exclude && s.Capture.Revision < q.CaptureRevision:
203			err = ErrSessionStale
204		case q.Workspace != 0 && (s.Capture == nil || q.OffScreen && q.Workspace != s.Capture.Workspace || !q.OffScreen && q.Workspace != s.Capture.Shown):
205			err = ErrWorkspaceMoved
206		case q.Window != 0 && (s.Capture == nil || !q.OffScreen || q.Window != s.Capture.Window):
207			err = ErrWorkspaceMoved
208		}
209		if err != nil {
210			Fail(p.ctx, q, err, p.replies)
211			continue
212		}
213		reqs[n] = q
214		n++
215	}
216	clear(reqs[n:])
217	reqs = reqs[:n]
218	excl := excludedShown(s) || len(s.CaptureIndicators) > 0 || (len(s.HintLabels) > 0 && !s.HintStyle.Capture) || s.PositionHint.Shown()
219	group := func(q ports.CaptureRequest) int {
220		switch {
221		case q.OffScreen:
222			return 2
223		case q.Exclude && excl:
224			return 1
225		}
226		return 0
227	}
228	var counts [3]int
229	for _, q := range reqs {
230		counts[group(q)]++
231	}
232	j := 0
233	for g := range 3 {
234		for i := j; i < n; i++ {
235			if group(reqs[i]) == g {
236				reqs[i], reqs[j] = reqs[j], reqs[i]
237				j++
238			}
239		}
240	}
241	a, b := counts[0], counts[0]+counts[1]
242	return reqs[:a], reqs[a:b], reqs[b:n]
243}
244
245// Handed reports a zeroed request: one Split failed, or one already given to
246// the worker. The owner never answers it again.
247func Handed(q ports.CaptureRequest) bool { return q.ID == 0 && q.Dst.File == nil && q.Mapped == 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 23

Tags 10

↑↓ moveenter openesc close