main f2d166b

flash.go RawHistoryFolder

1package core
2
3import (
4	"slices"
5	"time"
6
7	"github.com/bnema/neferwl/internal/ports"
8)
9
10type flashLevel uint8
11
12const (
13	flashWindow flashLevel = iota
14	flashColumn
15	flashMonitor
16	flashSettle   = 150 * time.Millisecond
17	flashRecheck  = 50 * time.Millisecond
18	flashCooldown = time.Second
19)
20
21type flashTarget struct {
22	level  flashLevel
23	window WindowID
24	output string
25}
26
27type focusFlash struct {
28	pending    *flashTarget
29	since      time.Time
30	timerC     <-chan time.Time
31	timerStop  func() bool
32	running    *flashTarget
33	rect       Rect
34	radius     int // Corner radius of the target, as SceneWindow.Radius.
35	start      time.Time
36	value      float64 // Retained between this output's own flips.
37	lastStarts map[flashTarget]time.Time
38	layout     []Placement       // Owner scratch, never shared with a published scene.
39	published  *ports.FocusFlash // Immutable last sample, reused when unchanged.
40}
41
42func (c *Core) flashOn() bool { return c.animOn() && c.cfg.Focus.Flash && !c.protectionRequested() }
43
44func (c *Core) requestFlash(level flashLevel, window WindowID) {
45	if !c.flashOn() {
46		return
47	}
48	c.stopFlash()
49	f := &c.flash
50	f.pending = &flashTarget{level: level, window: window, output: c.cur().name()}
51	f.since = c.now()
52	f.timerC, f.timerStop = newTimer(c.opts.Clock, flashSettle)
53}
54
55func (c *Core) flashTick() bool {
56	f := &c.flash
57	f.timerC, f.timerStop = nil, nil
58	if f.pending == nil {
59		return false
60	}
61	target := *f.pending
62	f.pending = nil
63	if !c.flashOn() {
64		return false
65	}
66	i := c.screenIndex(target.output)
67	if i < 0 {
68		return false
69	}
70	sc := c.screens[i]
71	if sc.mon.ov.open {
72		return false
73	}
74	now := c.now()
75	if target.level != flashMonitor {
76		owner, _ := c.screenOf(target.window)
77		record := c.windows.lookup(target.window)
78		if owner != sc || record.client.ID == 0 {
79			return false
80		}
81	}
82	wait := flashSettle - now.Sub(f.since)
83	if wait > 0 || !sc.settled() {
84		if wait <= 0 {
85			wait = flashRecheck
86		}
87		f.pending = &target
88		f.timerC, f.timerStop = newTimer(c.opts.Clock, wait)
89		return false
90	}
91	rect, radius, ok := c.flashRect(target, sc)
92	if !ok {
93		return false
94	}
95	if at, ok := f.lastStarts[target]; ok && now.Sub(at) < flashCooldown {
96		return false
97	}
98	if f.lastStarts == nil {
99		f.lastStarts = make(map[flashTarget]time.Time)
100	}
101	for key, at := range f.lastStarts {
102		if now.Sub(at) >= flashCooldown {
103			delete(f.lastStarts, key)
104		}
105	}
106	f.lastStarts[target] = now
107	f.running, f.rect, f.radius, f.start = &target, rect, radius, now
108	f.value = c.cfg.Focus.FlashOpacity
109	return true
110}
111
112func (c *Core) advanceFlash(only *screen) {
113	f := &c.flash
114	if f.running == nil {
115		return
116	}
117	if !c.flashOn() || c.screenIndex(f.running.output) < 0 {
118		c.stopFlash()
119		return
120	}
121	sc := c.screens[c.screenIndex(f.running.output)]
122	if rect, radius, ok := c.flashRect(*f.running, sc); !ok || rect != f.rect || radius != f.radius {
123		c.stopFlash()
124		return
125	}
126	if only != nil && only.name() != f.running.output {
127		return
128	}
129	d := c.cfg.Focus.FlashDuration
130	elapsed := c.now().Sub(f.start)
131	if d <= 0 || elapsed >= d {
132		f.running = nil
133		f.value = 0
134		return
135	}
136	f.value = c.cfg.Focus.FlashOpacity * (1 - float64(max(elapsed, 0))/float64(d))
137}
138
139// flashRect resolves the current target, never retaining a border after its
140// visibility or geometry changes. stopFlash leaves the published sample intact
141// so the scene diff also damages the old border.
142func (c *Core) flashRect(target flashTarget, sc *screen) (Rect, int, bool) {
143	if sc.mon.ov.open {
144		return Rect{}, 0, false
145	}
146	if target.level == flashMonitor {
147		o := sc.mon.Output()
148		return Rect{W: o.W, H: o.H}, 0, true
149	}
150	owner, _ := c.screenOf(target.window)
151	if owner != sc || c.windows.lookup(target.window).client.ID == 0 {
152		return Rect{}, 0, false
153	}
154	view := sc.mon.viewport()
155	c.flash.layout = sc.mon.layoutInto(c.flash.layout)
156	var rect Rect
157	radius := 0
158	found := false
159	for _, p := range c.flash.layout {
160		if p.ID == target.window {
161			if p.Fullscreen || p.Preview != 0 || !view.shows(p) {
162				return Rect{}, 0, false
163			}
164			// The flash sits inside the drawn border, hugging the body.
165			rect, found = c.clientRect(p), true
166			radius = max(windowRadius(p, sc.mon.Current().gap(), c.cfg.Border.Radius)-c.cfg.Border.Width, 0)
167			break
168		}
169	}
170	if !found {
171		return Rect{}, 0, false
172	}
173	if target.level == flashColumn {
174		w := sc.mon.Current()
175		col := w.columnOf(target.window)
176		if col < 0 {
177			return Rect{}, 0, false
178		}
179		for _, p := range c.flash.layout {
180			if !p.Hidden && slices.Contains(w.Columns[col].Windows, p.ID) {
181				rect = union(rect, c.clientRect(p))
182			}
183		}
184	}
185	return rect, radius, true
186}
187
188func (c *Core) flashFor(sc *screen) *ports.FocusFlash {
189	f := &c.flash
190	if !c.flashing(sc) {
191		return nil
192	}
193	color := c.cfg.Focus.FlashColor
194	if color == "" {
195		color = c.cfg.Border.Active
196	}
197	sample := ports.FocusFlash{Rect: f.rect, Width: c.cfg.Focus.FlashWidth, Radius: f.radius, Color: color, Alpha: f.value}
198	if f.published == nil || *f.published != sample {
199		// Allocate only for a changed sample; sample itself stays on the stack.
200		published := new(ports.FocusFlash)
201		*published = sample
202		f.published = published
203	}
204	return f.published
205}
206
207func (c *Core) flashing(sc *screen) bool {
208	return c.flash.running != nil && c.flash.running.output == sc.name()
209}
210
211func (c *Core) stopFlashTimer() {
212	if c.flash.timerStop != nil {
213		c.flash.timerStop()
214	}
215	c.flash.timerC, c.flash.timerStop = nil, nil
216}
217
218func (c *Core) stopFlash() {
219	c.stopFlashTimer()
220	c.flash.pending, c.flash.running, c.flash.value = nil, nil, 0
221}
222
223func (m *Monitor) settled() bool {
224	if m.switchView.busy() {
225		return false
226	}
227	for w := range m.all() {
228		if w.view.busy() || w.stashView.busy() {
229			return false
230		}
231	}
232	return true
233}
234
235func (s *screen) settled() bool { return s.mon.settled() && len(s.rects) == 0 }

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