v0.4.0 76bcd84

record_lifecycle.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"errors"
6	"sync/atomic"
7
8	"github.com/bnema/nefercap/internal/adapters/uierrors"
9)
10
11var (
12	// errUserStop is the cancellation cause of a stop requested by the user
13	// (control command or HUD button). It is not a failure.
14	errUserStop = errors.New("recording stopped by user")
15	// errHUDClosed and errHUDClosedEarly report a HUD that ended although
16	// nobody asked it to.
17	errHUDClosed      = errors.New("recording controls closed unexpectedly")
18	errHUDClosedEarly = errors.New("recording controls closed before authorization")
19)
20
21// recordingOutcome says what a recording left behind, independently of the
22// error: only a Saved recording has a path worth printing.
23type recordingOutcome struct{ Saved bool }
24
25// lifecycle is the state shared by the recording owner, the stop watchers and
26// the compositor's detach callback. Everything here is safe for concurrent use
27// and non-blocking. It never touches the Source: the owner goroutine alone
28// runs capture, so borrowed pixels and protocol state stay single-owner.
29//
30// The recording context carries the first cause that ended it: errUserStop for
31// a requested stop, or the first real failure. A failure is also kept in a
32// one-slot channel so it survives a parent cancellation that won the context.
33type lifecycle struct {
34	parent       context.Context
35	cancelRecord context.CancelCauseFunc
36	cancelHUD    context.CancelFunc
37	shuttingDown atomic.Bool
38	failure      chan error
39}
40
41func newLifecycle(parent context.Context, cancelRecord context.CancelCauseFunc, cancelHUD context.CancelFunc) *lifecycle {
42	return &lifecycle{parent: parent, cancelRecord: cancelRecord, cancelHUD: cancelHUD, failure: make(chan error, 1)}
43}
44
45// userStop ends the recording and its HUD without a failure. The flag is set
46// first so the detach the HUD teardown provokes is recognised as an echo.
47func (l *lifecycle) userStop() {
48	l.shuttingDown.Store(true)
49	l.cancelRecord(errUserStop)
50	l.cancelHUD()
51}
52
53// beginShutdown marks the owner's own teardown; call it before stopping the HUD.
54func (l *lifecycle) beginShutdown() { l.shuttingDown.Store(true) }
55
56// fail records the first real failure, and ends recording and HUD with it as
57// the cause. It reports whether err was the first.
58func (l *lifecycle) fail(err error) bool {
59	select {
60	case l.failure <- err:
61	default:
62		return false
63	}
64	l.cancelRecord(err)
65	l.cancelHUD()
66	return true
67}
68
69// detached is the compositor's layer-detach callback. A detach that arrives
70// before shutdown begins is real: the exclusion is gone and recording must
71// stop. One that follows our own shutdown or the parent's cancellation is the
72// echo of closing the HUD and is ignored.
73func (l *lifecycle) detached(err error) {
74	if l.shuttingDown.Load() || l.parent.Err() != nil {
75		return
76	}
77	if err == nil {
78		err = errHUDClosed
79	}
80	l.fail(err)
81}
82
83// hudEnded handles a HUD that returned. If nobody asked it to stop, that is a
84// failure: its own real error, or a closed error when it ended quietly. It
85// returns the part of err that classify must still consider.
86func (l *lifecycle) hudEnded(err error, early bool) error {
87	if l.shuttingDown.Load() || l.parent.Err() != nil {
88		return err
89	}
90	own := err != nil && !uierrors.IsCancellation(err)
91	reason := err
92	if !own {
93		reason = errHUDClosed
94		if early {
95			reason = errHUDClosedEarly
96		}
97	}
98	if !l.fail(reason) || !own {
99		return err
100	}
101	return nil
102}
103
104// failed returns the first recorded failure, if any. Call it once the
105// recording context is done, so the slot has been written.
106func (l *lifecycle) failed() error {
107	select {
108	case err := <-l.failure:
109		return err
110	default:
111		return nil
112	}
113}
114
115// stopOnSignal turns one HUD button press into a user stop. Its lifetime is
116// bounded by ctx and the completion channel is joined by the caller.
117func stopOnSignal(ctx context.Context, signal <-chan struct{}, stop context.CancelFunc) <-chan struct{} {
118	done := make(chan struct{})
119	go func() {
120		defer close(done)
121		select {
122		case <-ctx.Done():
123		case <-signal:
124			stop()
125		}
126	}()
127	return done
128}
129
130// ending gathers everything that explains how one recording ended.
131type ending struct {
132	runErr    error // core.Run, nil when it did not run
133	hudErr    error // HUD result after it was stopped
134	failure   error // first real failure recorded by the lifecycle
135	parentErr error // parent context error at the end
136	userStop  bool
137}
138
139// classify maps an ending to the error of the invocation. Real failures win
140// over any cancellation, including ones joined with a cancellation; a bare
141// user stop is success; a bare parent cancellation returns the parent's error.
142func classify(e ending) error {
143	var real []error
144	if e.failure != nil {
145		real = append(real, e.failure)
146	}
147	for _, err := range []error{e.runErr, e.hudErr} {
148		if err != nil && !uierrors.IsCancellation(err) {
149			real = append(real, err)
150		}
151	}
152	switch len(real) {
153	case 0:
154	case 1:
155		return real[0]
156	default:
157		return errors.Join(real...)
158	}
159	switch {
160	case e.userStop:
161		return nil
162	case e.parentErr != nil:
163		return e.parentErr
164	}
165	return e.runErr
166}

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