capture.go RawHistoryFolder

1// Package capture transfers each request to an output: the output closes its
2// destination fd exactly once and attempts one reply, never blocking after
3// its context is done.
4package capture
5
6import (
7	"context"
8	"errors"
9	"fmt"
10	"runtime/debug"
11	"time"
12
13	"github.com/bnema/neferwl/internal/ports"
14	"golang.org/x/sys/unix"
15)
16
17const maxMappedBytes = 1 << 30
18
19// Wayland SHM format identifiers; all capture pixels are opaque BGRA.
20const (
21	shmARGB8888 = 0
22	shmXRGB8888 = 1
23)
24
25func reply(ctx context.Context, req ports.CaptureRequest, err error, replies chan<- ports.CaptureDone) {
26	replyAt(ctx, req, err, monotonicNow(), replies)
27}
28
29func replyAt(ctx context.Context, req ports.CaptureRequest, err error, at time.Time, replies chan<- ports.CaptureDone) {
30	if req.Dst.File != nil {
31		err = errors.Join(err, req.Dst.File.Close())
32	}
33	done := ports.CaptureDone{ID: req.ID, Output: req.Output, Err: err, Time: at}
34	// Prefer a ready reply even after output cancellation: Wayland needs the
35	// completion to release admission credit while the session is still alive.
36	select {
37	case replies <- done:
38		return
39	default:
40	}
41	// A nil channel is allowed: cancellation still releases the sender.
42	select {
43	case <-ctx.Done():
44	case replies <- done:
45	}
46}
47
48// monotonicNow represents CLOCK_MONOTONIC in the Time seconds/nanoseconds fields.
49func monotonicNow() time.Time {
50	var ts unix.Timespec
51	if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts); err != nil {
52		return time.Time{}
53	}
54	return time.Unix(ts.Sec, ts.Nsec)
55}
56
57func Fail(ctx context.Context, req ports.CaptureRequest, err error, replies chan<- ports.CaptureDone) {
58	reply(ctx, req, err, replies)
59}
60
61func writeFrame(ctx context.Context, req ports.CaptureRequest, frame ports.CaptureFrame, at time.Time, replies chan<- ports.CaptureDone) {
62	var err error
63	var data []byte
64	// A client can truncate its file after Stat while it is still mapped.
65	// Turn SIGBUS into an error on this worker, not a compositor crash.
66	defer debug.SetPanicOnFault(debug.SetPanicOnFault(true))
67	defer func() {
68		if p := recover(); p != nil {
69			err = fmt.Errorf("capture read panic: %v\n%s", p, debug.Stack())
70		}
71		if data != nil {
72			err = errors.Join(err, unix.Munmap(data))
73		}
74		replyAt(ctx, req, err, at, replies)
75	}()
76	if req.Dst.File == nil || req.Width <= 0 || req.Width > maxMappedBytes/4 || req.Height <= 0 || req.Stride < req.Width*4 || req.Region.Dx() != req.Width || req.Region.Dy() != req.Height || req.Dst.Offset < 0 || (req.Format != shmARGB8888 && req.Format != shmXRGB8888) {
77		err = fmt.Errorf("invalid capture destination")
78		return
79	}
80	rowBytes := int64(req.Width) * 4
81	// Validate the bound before multiplication: padding must not wrap size.
82	if int64(req.Dst.Offset) > maxMappedBytes-rowBytes || (req.Height > 1 && int64(req.Stride) > (maxMappedBytes-int64(req.Dst.Offset)-rowBytes)/int64(req.Height-1)) {
83		err = fmt.Errorf("capture too large")
84		return
85	}
86	size := int64(req.Dst.Offset) + int64(req.Height-1)*int64(req.Stride) + rowBytes
87	if size <= 0 || size > int64(^uint(0)>>1) || size > maxMappedBytes {
88		err = fmt.Errorf("capture too large")
89		return
90	}
91	var st unix.Stat_t
92	if e := unix.Fstat(int(req.Dst.File.Fd()), &st); e != nil {
93		err = fmt.Errorf("fstat capture destination: %w", e)
94		return
95	}
96	if st.Size < size {
97		err = fmt.Errorf("capture buffer truncated")
98		return
99	}
100	// Map only for the duration of this capture; the renderer copies BGRA rows
101	// directly from its readback buffer into the selected shm region.
102	data, err = unix.Mmap(int(req.Dst.File.Fd()), 0, int(size), unix.PROT_WRITE, unix.MAP_SHARED)
103	if err != nil {
104		return
105	}
106	err = frame.Read(req.Region, data[req.Dst.Offset:], req.Stride)
107}

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