v0.3.0 0a8834d

fade.go RawHistoryFolder

1package visual
2
3import (
4	"context"
5	"fmt"
6	"sync/atomic"
7	"time"
8
9	"github.com/bnema/nefergui"
10)
11
12// fadeTick is the redraw cadence while the fade animates.
13const fadeTick = 16 * time.Millisecond
14
15// fadeCmd retargets the fade: black fades to opaque, otherwise back to clear.
16type fadeCmd struct {
17	black bool
18	dur   time.Duration
19}
20
21// fadeModel is the overlay's animation state, shared by the surfaces of every
22// output. The view (NeferGUI owner loop) owns the fields below "owned"; other
23// goroutines only use send and animatingAt.
24type fadeModel struct {
25	cmds chan fadeCmd
26	wake chan struct{}
27	// base is the monotonic origin for tickUntil; fixed at construction.
28	base time.Time
29	// tickUntil keeps redraws coming past the fade end, as nanoseconds since
30	// base (monotonic, like fadeAlpha). It must not depend on which surface
31	// drew last: a wake redraws all surfaces, but each draws at its own instant.
32	tickUntil atomic.Int64
33	// finish ends the run once a reveal completed; set by the runner before
34	// the loop starts.
35	finish context.CancelFunc
36
37	// owned by the view:
38	started  bool
39	from, to float64
40	start    time.Time
41	dur      time.Duration
42}
43
44func newFadeModel() *fadeModel {
45	return &fadeModel{cmds: make(chan fadeCmd, 4), wake: make(chan struct{}, 1), base: time.Now()}
46}
47
48// send queues a retarget (dropping the oldest when full) and requests a redraw.
49func (m *fadeModel) send(c fadeCmd) {
50	for {
51		select {
52		case m.cmds <- c:
53			m.poke()
54			return
55		default:
56			select {
57			case <-m.cmds:
58			default:
59			}
60		}
61	}
62}
63
64func (m *fadeModel) poke() {
65	select {
66	case m.wake <- struct{}{}:
67	default:
68	}
69}
70
71// fadeAlpha is the overlay opacity at now and whether the fade has arrived.
72func fadeAlpha(from, to float64, start time.Time, dur time.Duration, now time.Time) (float64, bool) {
73	if dur <= 0 || !now.Before(start.Add(dur)) {
74		return to, true
75	}
76	t := float64(now.Sub(start)) / float64(dur)
77	return from + (to-from)*t, false
78}
79
80// fadeCSS is the full-screen black layer at opacity a.
81func fadeCSS(a float64) string {
82	return fmt.Sprintf("width:100%%;height:100%%;background-color:rgb(0 0 0 / %.1f%%)", a*100)
83}
84
85// animatingAt reports whether the ticker must still request redraws.
86func (m *fadeModel) animatingAt(now time.Time) bool {
87	return int64(now.Sub(m.base)) < m.tickUntil.Load()
88}
89
90// fadeView applies queued retargets, then draws the overlay for this instant.
91func fadeView(f *nefergui.Frame, m *fadeModel) {
92	a, done := m.step(time.Now())
93	f.Root(nefergui.Inline(fadeCSS(a)))
94	if done && m.started && m.to == 0 && m.finish != nil {
95		m.finish()
96	}
97}
98
99// step applies queued retargets and returns the opacity at now.
100func (m *fadeModel) step(now time.Time) (float64, bool) {
101	for {
102		select {
103		case c := <-m.cmds:
104			m.from, _ = fadeAlpha(m.from, m.to, m.start, m.dur, now)
105			m.to = 0
106			if c.black {
107				m.to = 1
108			}
109			m.start, m.dur, m.started = now, c.dur, true
110			// Any wake at or after the end marks every surface dirty until it
111			// draws, so that draw shows the final opacity. The extra ticks are
112			// margin for ticker jitter.
113			m.tickUntil.Store(int64(now.Add(c.dur + 2*fadeTick).Sub(m.base)))
114			continue
115		default:
116		}
117		break
118	}
119	return fadeAlpha(m.from, m.to, m.start, m.dur, now)
120}
121
122// fadeRunner runs the overlay until ctx ends or a reveal completes.
123type fadeRunner interface {
124	RunFade(ctx context.Context, m *fadeModel) error
125}
126
127// guiFader renders the overlay as a click-through layer-shell surface.
128type guiFader struct{}
129
130func (guiFader) RunFade(ctx context.Context, m *fadeModel) error {
131	ctx, cancel := context.WithCancel(ctx)
132	defer cancel()
133	m.finish = cancel
134	go func() {
135		t := time.NewTicker(fadeTick)
136		defer t.Stop()
137		was := false
138		for {
139			select {
140			case <-ctx.Done():
141				return
142			case now := <-t.C:
143				// One more poke when animation stops: even a starved ticker
144				// then wakes every surface at least once past the end.
145				animating := m.animatingAt(now)
146				if animating || was {
147					m.poke()
148				}
149				was = animating
150			}
151		}
152	}()
153	err := nefergui.Run(ctx, m, fadeView,
154		nefergui.Title("neferafk-fade"),
155		nefergui.Size(64, 64),
156		nefergui.Transparent(),
157		nefergui.Wake(m.wake),
158		nefergui.Layer(fadeLayer()),
159	)
160	if ctx.Err() != nil && err == context.Canceled {
161		return nil
162	}
163	return err
164}
165
166// fadeLayer: overlay on every output, all anchors, no keyboard, fully
167// click-through.
168func fadeLayer() nefergui.LayerConfig {
169	return nefergui.LayerConfig{
170		AllOutputs:    true,
171		Namespace:     "neferafk-fade",
172		Level:         nefergui.LayerOverlay,
173		Anchors:       nefergui.AnchorTop | nefergui.AnchorBottom | nefergui.AnchorLeft | nefergui.AnchorRight,
174		Keyboard:      nefergui.KeyboardNone,
175		ExclusiveZone: -1,
176		InputRects:    []nefergui.Rect{},
177	}
178}

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