v0.2.1 c196c81

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. The view (NeferGUI owner loop)
22// owns the fields below "owned"; other goroutines only use send.
23type fadeModel struct {
24	cmds      chan fadeCmd
25	wake      chan struct{}
26	animating atomic.Bool
27	// finish ends the run once a reveal completed; set by the runner before
28	// the loop starts.
29	finish context.CancelFunc
30
31	// owned by the view:
32	started  bool
33	from, to float64
34	start    time.Time
35	dur      time.Duration
36}
37
38func newFadeModel() *fadeModel {
39	return &fadeModel{cmds: make(chan fadeCmd, 4), wake: make(chan struct{}, 1)}
40}
41
42// send queues a retarget (dropping the oldest when full) and requests a redraw.
43func (m *fadeModel) send(c fadeCmd) {
44	for {
45		select {
46		case m.cmds <- c:
47			m.poke()
48			return
49		default:
50			select {
51			case <-m.cmds:
52			default:
53			}
54		}
55	}
56}
57
58func (m *fadeModel) poke() {
59	select {
60	case m.wake <- struct{}{}:
61	default:
62	}
63}
64
65// fadeAlpha is the overlay opacity at now and whether the fade has arrived.
66func fadeAlpha(from, to float64, start time.Time, dur time.Duration, now time.Time) (float64, bool) {
67	if dur <= 0 || !now.Before(start.Add(dur)) {
68		return to, true
69	}
70	t := float64(now.Sub(start)) / float64(dur)
71	return from + (to-from)*t, false
72}
73
74// fadeCSS is the full-screen black layer at opacity a.
75func fadeCSS(a float64) string {
76	return fmt.Sprintf("width:100%%;height:100%%;background-color:rgb(0 0 0 / %.1f%%)", a*100)
77}
78
79// fadeView applies queued retargets, then draws the overlay for this instant.
80func fadeView(f *nefergui.Frame, m *fadeModel) {
81	now := time.Now()
82	for {
83		select {
84		case c := <-m.cmds:
85			m.from, _ = fadeAlpha(m.from, m.to, m.start, m.dur, now)
86			m.to = 0
87			if c.black {
88				m.to = 1
89			}
90			m.start, m.dur, m.started = now, c.dur, true
91			continue
92		default:
93		}
94		break
95	}
96	a, done := fadeAlpha(m.from, m.to, m.start, m.dur, now)
97	m.animating.Store(!done)
98	f.Root(nefergui.Inline(fadeCSS(a)))
99	if done && m.started && m.to == 0 && m.finish != nil {
100		m.finish()
101	}
102}
103
104// fadeRunner runs the overlay until ctx ends or a reveal completes.
105type fadeRunner interface {
106	RunFade(ctx context.Context, m *fadeModel) error
107}
108
109// guiFader renders the overlay as a click-through layer-shell surface.
110type guiFader struct{}
111
112func (guiFader) RunFade(ctx context.Context, m *fadeModel) error {
113	ctx, cancel := context.WithCancel(ctx)
114	defer cancel()
115	m.finish = cancel
116	go func() {
117		t := time.NewTicker(fadeTick)
118		defer t.Stop()
119		for {
120			select {
121			case <-ctx.Done():
122				return
123			case <-t.C:
124				if m.animating.Load() {
125					m.poke()
126				}
127			}
128		}
129	}()
130	err := nefergui.Run(ctx, m, fadeView,
131		nefergui.Title("neferafk-fade"),
132		nefergui.Size(64, 64),
133		nefergui.Transparent(),
134		nefergui.Wake(m.wake),
135		nefergui.Layer(fadeLayer()),
136	)
137	if ctx.Err() != nil && err == context.Canceled {
138		return nil
139	}
140	return err
141}
142
143// fadeLayer: overlay on every output, all anchors, no keyboard, fully
144// click-through.
145func fadeLayer() nefergui.LayerConfig {
146	return nefergui.LayerConfig{
147		AllOutputs:    true,
148		Namespace:     "neferafk-fade",
149		Level:         nefergui.LayerOverlay,
150		Anchors:       nefergui.AnchorTop | nefergui.AnchorBottom | nefergui.AnchorLeft | nefergui.AnchorRight,
151		Keyboard:      nefergui.KeyboardNone,
152		ExclusiveZone: -1,
153		InputRects:    []nefergui.Rect{},
154	}
155}

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