position_hint.go RawHistoryFolder

1package core
2
3import (
4	"time"
5
6	"github.com/bnema/neferwl/internal/ports"
7)
8
9const positionHintHold = 600 * time.Millisecond
10const positionHintFade = 150 * time.Millisecond
11
12// positionHintState belongs exclusively to the core owner. Published samples
13// are fixed-size values, so no buffer recycling or renderer acknowledgement is
14// needed. The timer is retained (but stopped when idle) and Reset on reuse.
15type positionHintState struct {
16	workspace   *Workspace
17	band        int
18	focused     bool
19	initialized bool
20	start       time.Time
21	live        bool
22	fading      bool
23	sample      ports.PositionHint
24}
25
26type positionHintTimer struct {
27	timer  ports.Timer
28	timerC <-chan time.Time
29}
30
31// updatePositionHints compares actual positions at the publication boundary,
32// independent of which input/action changed them. Initial output discovery is
33// not a move. Other outputs retain their own start time and sample.
34func (c *Core) updatePositionHints(only *screen) {
35	for i, sc := range c.screens {
36		h := &sc.positionHint
37		// Overview selection is provisional. Preserve the last committed
38		// baseline until closing, so accept compares against entry and cancel
39		// cannot announce intermediate browsing positions.
40		if sc.mon.ov.open {
41			h.live, h.sample = false, ports.PositionHint{}
42			continue
43		}
44		// Disabled hints skip the band walk; dropping the baseline keeps a
45		// later re-enable from announcing the position it finds.
46		if !c.cfg.Focus.PositionHint {
47			*h = positionHintState{}
48			continue
49		}
50		w := sc.mon.Current()
51		band := 0
52		if w.Overflow == OverflowCascade {
53			band = w.band(w.Focus)
54		}
55		focused := i == c.focusScreen
56		changed := h.initialized && (h.workspace != w || h.band != band || focused && !h.focused)
57		// Transferring the current workspace changes the source's fallback
58		// position too, but only the receiving output should announce it.
59		if h.initialized && h.workspace != w {
60			for _, other := range c.screens {
61				if other != sc && other.mon.has(h.workspace) {
62					changed = false
63					break
64				}
65			}
66		}
67		h.workspace, h.band, h.focused, h.initialized = w, band, focused, true
68		if c.protectionRequested() || w.cover() != 0 {
69			h.live, h.sample = false, ports.PositionHint{}
70			continue
71		}
72		if changed {
73			h.start, h.live, h.fading = c.now(), true, false
74		}
75		if !h.live {
76			continue
77		}
78		if !changed && only != nil && only != sc {
79			continue
80		}
81		elapsed := c.now().Sub(h.start)
82		opacity := float32(1)
83		h.fading = elapsed >= positionHintHold
84		if h.fading {
85			if !c.animOn() || elapsed >= positionHintHold+positionHintFade {
86				h.live, h.sample = false, ports.PositionHint{}
87				continue
88			}
89			opacity = 1 - float32(elapsed-positionHintHold)/float32(positionHintFade)
90		}
91		h.sample = positionHintGrid(sc.mon)
92		h.sample.Opacity = opacity
93	}
94	c.armPositionHintTimer()
95}
96
97// Use ext-workspace's complete ordered list: numbered (including empty
98// spares), then configured hidden named workspaces. Unlike overview cards,
99// these remain addressable even when empty. Window the list at its active
100// entry when it exceeds the cap. Bands stay top-aligned; excess rows clamp.
101func positionHintGrid(m *Monitor) ports.PositionHint {
102	h := ports.PositionHint{Vertical: m.overviewAxis() == verticalAxis}
103	active, total := 0, 0
104	for w := range m.all() {
105		if w == m.Current() {
106			active = total
107		}
108		total++
109	}
110	cap := ports.PositionHintMaxColumns
111	if h.Vertical {
112		cap = ports.PositionHintMaxRows
113	}
114	first := max(0, active-cap+1)
115	if h.Vertical {
116		h.Columns, h.Counts[0], h.ActiveRow = 1, uint8(min(total, cap)), uint8(active-first)
117		return h
118	}
119	h.Columns, h.ActiveCol = uint8(min(total, cap)), uint8(active-first)
120	i := 0
121	for w := range m.all() {
122		if i >= first && i < first+cap {
123			n := 1
124			if w.Overflow == OverflowCascade {
125				n, _ = w.cascadeBands()
126			}
127			h.Counts[i-first] = uint8(min(n, ports.PositionHintMaxRows))
128		}
129		i++
130	}
131	w := m.Current()
132	if w.Overflow == OverflowCascade {
133		h.ActiveRow = uint8(min(w.band(w.Focus), ports.PositionHintMaxRows-1))
134	}
135	return h
136}
137
138// positionHintFading drives fade frames through the shared scene publication,
139// like springs and the focus flash: each fade frame republishes the whole
140// scene, which allocates its slices. The hint state itself allocates nothing.
141func (c *Core) positionHintFading(sc *screen) bool {
142	return sc.positionHint.live && sc.positionHint.fading
143}
144
145// One timer wakes at the earliest hold boundary. Fade frames use the existing
146// flip/fallback scheduler; with no live hints no timer is armed or clock read.
147func (c *Core) armPositionHintTimer() {
148	var earliest time.Time
149	for _, sc := range c.screens {
150		if sc.positionHint.live && !sc.positionHint.fading {
151			end := sc.positionHint.start.Add(positionHintHold)
152			if earliest.IsZero() || end.Before(earliest) {
153				earliest = end
154			}
155		}
156	}
157	t := &c.positionHintTimer
158	if earliest.IsZero() {
159		c.stopPositionHintTimer()
160		return
161	}
162	d := max(earliest.Sub(c.now()), time.Nanosecond)
163	if t.timer == nil {
164		t.timer = newPortTimer(c.opts.Clock, d)
165	} else {
166		t.timer.Reset(d)
167	}
168	t.timerC = t.timer.C()
169}
170
171func (c *Core) stopPositionHintTimer() {
172	t := &c.positionHintTimer
173	if t.timerC != nil {
174		t.timer.Stop()
175		t.timerC = nil
176	}
177}

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 23

Tags 10

↑↓ moveenter openesc close