feat/hints a471fa4

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	start      time.Time
35	value      float64 // Retained between this output's own flips.
36	lastStarts map[flashTarget]time.Time
37	layout     []Placement       // Owner scratch, never shared with a published scene.
38	published  *ports.FocusFlash // Immutable last sample, reused when unchanged.
39}
40
41func (c *Core) flashOn() bool { return c.animOn() && c.cfg.Focus.Flash && !c.protectionRequested() }
42
43func (c *Core) requestFlash(level flashLevel, window WindowID) {
44	if !c.flashOn() {
45		return
46	}
47	c.stopFlash()
48	f := &c.flash
49	f.pending = &flashTarget{level: level, window: window, output: c.cur().name()}
50	f.since = c.now()
51	f.timerC, f.timerStop = newTimer(c.opts.Clock, flashSettle)
52}
53
54func (c *Core) flashTick() bool {
55	f := &c.flash
56	f.timerC, f.timerStop = nil, nil
57	if f.pending == nil {
58		return false
59	}
60	target := *f.pending
61	f.pending = nil
62	if !c.flashOn() {
63		return false
64	}
65	i := c.screenIndex(target.output)
66	if i < 0 {
67		return false
68	}
69	sc := c.screens[i]
70	if sc.mon.ov.open {
71		return false
72	}
73	now := c.now()
74	if target.level != flashMonitor {
75		owner, _ := c.screenOf(target.window)
76		record := c.windows.lookup(target.window)
77		if owner != sc || record.client.ID == 0 {
78			return false
79		}
80	}
81	wait := flashSettle - now.Sub(f.since)
82	if wait > 0 || !sc.settled() {
83		if wait <= 0 {
84			wait = flashRecheck
85		}
86		f.pending = &target
87		f.timerC, f.timerStop = newTimer(c.opts.Clock, wait)
88		return false
89	}
90	layout := sc.mon.Layout()
91	var rect Rect
92	if target.level == flashMonitor {
93		o := sc.mon.Output()
94		rect = Rect{W: o.W, H: o.H}
95	} else {
96		at := slices.IndexFunc(layout, func(p Placement) bool { return p.ID == target.window && !p.Hidden && p.Preview == 0 })
97		if at < 0 || layout[at].Fullscreen {
98			return false
99		}
100		rect = layout[at].Rect
101		if target.level == flashColumn {
102			w := sc.mon.Current()
103			col := w.columnOf(target.window)
104			if col < 0 {
105				return false
106			}
107			for _, p := range layout {
108				if p.Hidden || !slices.Contains(w.Columns[col].Windows, p.ID) {
109					continue
110				}
111				rect = union(rect, p.Rect)
112			}
113		}
114	}
115	if at, ok := f.lastStarts[target]; ok && now.Sub(at) < flashCooldown {
116		return false
117	}
118	if f.lastStarts == nil {
119		f.lastStarts = make(map[flashTarget]time.Time)
120	}
121	for key, at := range f.lastStarts {
122		if now.Sub(at) >= flashCooldown {
123			delete(f.lastStarts, key)
124		}
125	}
126	f.lastStarts[target] = now
127	f.running, f.rect, f.start = &target, rect, now
128	f.value = c.cfg.Focus.FlashOpacity
129	return true
130}
131
132func (c *Core) advanceFlash(only *screen) {
133	f := &c.flash
134	if f.running == nil {
135		return
136	}
137	if !c.flashOn() || c.screenIndex(f.running.output) < 0 {
138		c.stopFlash()
139		return
140	}
141	sc := c.screens[c.screenIndex(f.running.output)]
142	if !c.flashVisible(*f.running, sc) {
143		c.stopFlash()
144		return
145	}
146	if only != nil && only.name() != f.running.output {
147		return
148	}
149	d := c.cfg.Focus.FlashDuration
150	elapsed := c.now().Sub(f.start)
151	if d <= 0 || elapsed >= d {
152		f.running = nil
153		f.value = 0
154		return
155	}
156	f.value = c.cfg.Focus.FlashOpacity * (1 - float64(max(elapsed, 0))/float64(d))
157}
158
159// flashVisible checks the current layout, not leaving placements retained for
160// transitions: an old border must not outlive its target's visibility.
161func (c *Core) flashVisible(target flashTarget, sc *screen) bool {
162	if sc.mon.ov.open {
163		return false
164	}
165	if target.level == flashMonitor {
166		return true
167	}
168	owner, _ := c.screenOf(target.window)
169	if owner != sc || c.windows.lookup(target.window).client.ID == 0 {
170		return false
171	}
172	view := sc.mon.viewport()
173	c.flash.layout = sc.mon.layoutInto(c.flash.layout)
174	for _, p := range c.flash.layout {
175		if p.ID == target.window {
176			return !p.Fullscreen && p.Preview == 0 && view.shows(p)
177		}
178	}
179	return false
180}
181
182func (c *Core) flashFor(sc *screen) *ports.FocusFlash {
183	f := &c.flash
184	if !c.flashing(sc) {
185		return nil
186	}
187	color := c.cfg.Focus.FlashColor
188	if color == "" {
189		color = c.cfg.Border.Active
190	}
191	sample := ports.FocusFlash{Rect: f.rect, Width: c.cfg.Focus.FlashWidth, Color: color, Alpha: f.value}
192	if f.published == nil || *f.published != sample {
193		f.published = &ports.FocusFlash{Rect: sample.Rect, Width: sample.Width, Color: sample.Color, Alpha: sample.Alpha}
194	}
195	return f.published
196}
197
198func (c *Core) flashing(sc *screen) bool {
199	return c.flash.running != nil && c.flash.running.output == sc.name()
200}
201
202func (c *Core) stopFlashTimer() {
203	if c.flash.timerStop != nil {
204		c.flash.timerStop()
205	}
206	c.flash.timerC, c.flash.timerStop = nil, nil
207}
208
209func (c *Core) stopFlash() {
210	c.stopFlashTimer()
211	c.flash.pending, c.flash.running, c.flash.value = nil, nil, 0
212}
213
214func (m *Monitor) settled() bool {
215	if m.switchView.busy() {
216		return false
217	}
218	for w := range m.all() {
219		if w.view.busy() || w.stashView.busy() {
220			return false
221		}
222	}
223	return true
224}
225
226func (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