detached 7d47882

fx.go RawHistoryFolder

1package selection
2
3import (
4	"context"
5	"math"
6	"sync/atomic"
7	"time"
8
9	"github.com/bnema/nefercap/internal/core"
10	"github.com/bnema/nefercap/internal/ports"
11)
12
13// Short effects confirm a key press: a toast names the new mode or target and
14// the selection border pulses. They only exist while the selector is open, so
15// they never reach a capture.
16const (
17	fxStep     = 30 * time.Millisecond
18	toastFade  = 9 * fxStep // o1..o9 after the hold
19	modeHold   = 600 * time.Millisecond
20	targetHold = 400 * time.Millisecond
21	warnHold   = 1500 * time.Millisecond
22	pulseSpan  = 210 * time.Millisecond
23	pulseExtra = 3.0 // logical pixels added to the border at the pulse start
24	hudSteps   = 4   // HUD fade length in fxSteps: 120 ms each way
25)
26
27// hudOpacity indexes opacityClasses by HUD fade level; level hudSteps is hidden.
28var hudOpacity = [hudSteps]int{0, 3, 5, 7}
29
30// opacityClasses are the toast steps of style.css, from opaque to faint.
31var opacityClasses = [...]string{"o0", "o1", "o2", "o3", "o4", "o5", "o6", "o7", "o8", "o9"}
32
33// Toast texts are constants so triggering one never allocates.
34const (
35	toastShot      = "Screenshot mode"
36	toastRec       = "Record mode"
37	toastRegion    = "Region · drag to select"
38	toastMonitor   = "Monitor"
39	toastWorkspace = "Workspace"
40	toastNoWS      = "No workspace found · using monitor"
41	toastFrozen    = "Frozen screen"
42	toastLive      = "Live screen"
43)
44
45// toast is one centered message. start is zero until the first frame draws
46// it, so a slow frame does not eat the hold.
47type toast struct {
48	text  string
49	warn  bool
50	hold  time.Duration
51	start time.Time
52	step  int // index in opacityClasses; valid while on
53	on    bool
54}
55
56// pulse thickens the selection border and lets it shrink back.
57type pulse struct {
58	start time.Time
59	on    bool
60}
61
62// hudFade fades the chips, header, footer and toast out while a region is
63// dragged, and back in after. level counts steps away from fully shown.
64type hudFade struct {
65	hiding bool
66	from   int       // level when the current fade started
67	start  time.Time // zero until the first frame of the fade
68	level  int
69}
70
71// animator wakes its overlay every fxStep while an effect runs and sleeps
72// otherwise, so an idle selector does not redraw. until is written by the
73// overlay's owner loop and read by the animator goroutine.
74type animator struct {
75	kick  chan struct{}
76	until atomic.Int64 // unix nanoseconds
77}
78
79func newAnimator() *animator { return &animator{kick: make(chan struct{}, 1)} }
80
81// extend keeps the overlay redrawing until t. A nil animator does nothing.
82func (a *animator) extend(t time.Time) {
83	if a == nil {
84		return
85	}
86	if n := t.UnixNano(); n > a.until.Load() {
87		a.until.Store(n)
88	}
89	select {
90	case a.kick <- struct{}{}:
91	default:
92	}
93}
94
95// run sends a non-blocking wake every fxStep until the effects end, then waits
96// for the next kick. It returns when ctx ends.
97func (a *animator) run(ctx context.Context, wake chan<- struct{}) {
98	t := time.NewTicker(fxStep)
99	t.Stop()
100	defer t.Stop()
101	for {
102		select {
103		case <-ctx.Done():
104			return
105		case <-a.kick:
106		}
107		t.Reset(fxStep)
108		// One step past the end draws the frame that removes the effect.
109		for time.Now().UnixNano() < a.until.Load()+int64(fxStep) {
110			select {
111			case <-ctx.Done():
112				return
113			case <-t.C:
114				select {
115				case wake <- struct{}{}:
116				default:
117				}
118			}
119		}
120		t.Stop()
121	}
122}
123
124// showToast replaces the current toast. It never allocates.
125func (m *model) showToast(text string, hold time.Duration, warn bool) {
126	m.toast = toast{text: text, warn: warn, hold: hold, on: true}
127	m.fx++
128}
129
130// pulseBorder restarts the border pulse.
131func (m *model) pulseBorder() {
132	m.pulse = pulse{on: true}
133	m.fx++
134}
135
136// targetToast is the toast that confirms a target key.
137func targetToast(kind core.PickKind) string {
138	switch kind {
139	case core.PickMonitor:
140		return toastMonitor
141	case core.PickWorkspace:
142		return toastWorkspace
143	}
144	return toastRegion
145}
146
147// freezeToast confirms an F press.
148func freezeToast(live bool) string {
149	if live {
150		return toastLive
151	}
152	return toastFrozen
153}
154
155func modeToast(mode ports.Mode) string {
156	if mode == ports.Record {
157		return toastRec
158	}
159	return toastShot
160}
161
162// animate advances the effects to now. The view calls it before every scene;
163// it never allocates.
164func (m *model) animate(now time.Time) {
165	m.edgeW = edge
166	m.fadeHUD(now)
167	if t := &m.toast; t.on {
168		if t.start.IsZero() {
169			t.start = now
170			m.anim.extend(now.Add(t.hold + toastFade))
171		}
172		el := now.Sub(t.start)
173		switch {
174		case el >= t.hold+toastFade:
175			t.on = false
176		case el <= t.hold:
177			t.step = 0
178		default:
179			t.step = min(1+int((el-t.hold)/fxStep), len(opacityClasses)-1)
180		}
181	}
182	if p := &m.pulse; p.on {
183		if p.start.IsZero() {
184			p.start = now
185			m.anim.extend(now.Add(pulseSpan))
186		}
187		el := now.Sub(p.start)
188		if el >= pulseSpan {
189			p.on = false
190		} else {
191			m.edgeW = edge + math.Round(pulseExtra*float64(pulseSpan-el)/float64(pulseSpan))
192		}
193	}
194}
195
196// fadeHUD moves the HUD fade one step per fxStep toward hidden while a region
197// is dragged and toward shown otherwise. A reversal starts from the current
198// level, so a short drag never jumps.
199func (m *model) fadeHUD(now time.Time) {
200	h := &m.hud
201	if hide := m.picker.Dragging(); hide != h.hiding {
202		h.hiding, h.from, h.start = hide, h.level, time.Time{}
203	}
204	target := 0
205	if h.hiding {
206		target = hudSteps
207	}
208	if h.level == target {
209		return
210	}
211	if h.start.IsZero() {
212		h.start = now
213		m.anim.extend(now.Add(hudSteps * fxStep))
214	}
215	n := 1 + int(now.Sub(h.start)/fxStep)
216	if target > h.from {
217		h.level = min(h.from+n, target)
218	} else {
219		h.level = max(h.from-n, target)
220	}
221}
222
223// hudStep is the opacityClasses index of the HUD this frame.
224func (m *model) hudStep() int { return hudOpacity[min(m.hud.level, hudSteps-1)] }
225
226// hudClass is the opacity class of the HUD this frame.
227func (m *model) hudClass() string { return opacityClasses[m.hudStep()] }
228
229// toastClass is the fainter of the toast's own fade and the HUD fade.
230func (m *model) toastClass() string { return opacityClasses[max(m.toast.step, m.hudStep())] }
231
232// hudHidden reports a fully faded HUD, which is not laid out or drawn.
233func (m *model) hudHidden() bool { return m.hud.level >= hudSteps }

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