v0.3.0 da0887c

popup.go RawHistoryFolder

1package bar
2
3import (
4	"slices"
5	"time"
6
7	"github.com/bnema/neferbar/internal/layout"
8	"github.com/bnema/neferbar/internal/module"
9	"github.com/bnema/neferbar/internal/popup"
10)
11
12// popupPoll is how long the loop waits before it asks again for a popup frame
13// that is waiting for the GPU.
14const popupPoll = 2 * time.Millisecond
15
16// updatePopupStyle writes the stylesheet of the popups when the bar's colors,
17// font or size changed. Without a stylesheet no popup opens.
18func (b *Bar) updatePopupStyle() {
19	if b.pop == nil {
20		return
21	}
22	css := popup.CSS(popup.Style{
23		Font: b.cfg.Bar.Font, Size: b.cfg.Bar.Size * b.cfg.Bar.Scale,
24		FG: b.fg, BG: b.bg, Accent: b.pal[b.cfg.Bar.Accent&15],
25	})
26	if css == b.popCSS {
27		return
28	}
29	path, err := popup.WriteCSS(css)
30	if err != nil {
31		b.log.Warn("popups are off: cannot write their stylesheet", "err", err)
32		b.pop.SetStyles("")
33		b.popCSS = ""
34		return
35	}
36	b.popCSS = css
37	b.pop.SetStyles(path)
38}
39
40// closePopup dismisses the tooltip or menu being shown.
41func (b *Bar) closePopup() { b.pop.Close() }
42
43// closeTooltip dismisses the popup when it is a tooltip; a menu stays.
44func (b *Bar) closeTooltip() {
45	if b.pop.Kind() == popup.KindTooltip {
46		b.pop.Close()
47	}
48}
49
50// stepPopup runs after every wake-up of the loop: it starts the GPU warm-up,
51// carries out what the modules asked for, and draws the popup.
52func (b *Bar) stepPopup() {
53	if b.pop == nil {
54		return
55	}
56	b.popC = nil
57	if !b.warmed && b.fb != nil && b.popCSS != "" && b.anyInteractive() {
58		b.warmed = true
59		b.pop.Warm(b.fb)
60	}
61	for _, m := range b.mods {
62		b.drainControl(m)
63	}
64	if b.pop.OwnerRestarted() {
65		b.closePopup()
66	}
67	if !b.pop.Active() {
68		return
69	}
70	b.pop.Draw()
71	if b.pop.Pending() {
72		b.popC = time.After(popupPoll)
73	}
74}
75
76func (b *Bar) anyInteractive() bool {
77	for _, m := range b.mods {
78		if m.Interactive {
79			return true
80		}
81	}
82	return false
83}
84
85// drainControl takes the control lines module m printed since the last
86// wake-up and carries them out. A script that prints a tooltip on every
87// mouse move would otherwise open and close a popup (a renderer, a surface)
88// for each one: of the tooltip and close lines of one wake-up only the last
89// counts.
90func (b *Bar) drainControl(m *module.Module) {
91	raw, ok := m.TakeControl()
92	if !ok {
93		return
94	}
95	b.ctlBuf = b.ctlBuf[:0]
96	for ; ok; raw, ok = m.TakeControl() {
97		ctrl, err := module.ParseControl(raw)
98		if err != nil {
99			b.badControl(m, err)
100			continue
101		}
102		b.ctlBuf = append(b.ctlBuf, ctrl)
103	}
104	b.ctlBuf = coalesceControl(b.ctlBuf)
105	for i := range b.ctlBuf {
106		b.control(m, b.ctlBuf[i])
107	}
108	clear(b.ctlBuf) // keeps no parsed menu or table alive until the next wake-up
109}
110
111// badControl logs an invalid control line, once per module.
112func (b *Bar) badControl(m *module.Module, err error) {
113	if b.badCtl[m] {
114		return
115	}
116	if b.badCtl == nil {
117		b.badCtl = map[*module.Module]bool{}
118	}
119	b.badCtl[m] = true
120	b.log.Warn("module sent an invalid control line; ignored (shown once per module)", "module", m.Name, "err", err)
121}
122
123// coalesceControl keeps, in order, every menu line and only the last of the
124// tooltip and close lines. It works in place.
125func coalesceControl(cs []module.Control) []module.Control {
126	last := -1
127	for i, c := range cs {
128		if c.Type != module.ControlMenu {
129			last = i
130		}
131	}
132	i := 0
133	return slices.DeleteFunc(cs, func(c module.Control) bool {
134		drop := c.Type != module.ControlMenu && i != last
135		i++
136		return drop
137	})
138}
139
140// tooltipInterval is the shortest time between two tooltips opened for one
141// module.
142const tooltipInterval = 250 * time.Millisecond
143
144// tooltipLimiter rate-limits the tooltips of each module.
145type tooltipLimiter struct {
146	last map[*module.Module]time.Time
147}
148
149// allow reports whether m may open a tooltip at now, and counts it if so.
150func (l *tooltipLimiter) allow(m *module.Module, now time.Time) bool {
151	if t, ok := l.last[m]; ok && now.Sub(t) < tooltipInterval {
152		return false
153	}
154	if l.last == nil {
155		l.last = map[*module.Module]time.Time{}
156	}
157	l.last[m] = now
158	return true
159}
160
161// forget drops what is kept for modules that are not in keep.
162func (l *tooltipLimiter) forget(keep []*module.Module) {
163	for m := range l.last {
164		if !slices.Contains(keep, m) {
165			delete(l.last, m)
166		}
167	}
168}
169
170// forgetModules drops the per-module state of modules that were stopped.
171func (b *Bar) forgetModules(keep []*module.Module) {
172	for m := range b.badCtl {
173		if !slices.Contains(keep, m) {
174			delete(b.badCtl, m)
175		}
176	}
177	b.tips.forget(keep)
178}
179
180// control handles one control line of module m.
181func (b *Bar) control(m *module.Module, ctrl module.Control) {
182	if !m.Interactive {
183		return // only modules that asked for the pointer may open popups
184	}
185	switch ctrl.Type {
186	case module.ControlClose:
187		if b.pop.Kind() == popup.KindTooltip && b.pop.Owner() == m {
188			b.pop.Close()
189		}
190	case module.ControlTooltip:
191		// Only for the module under the pointer, and never over a menu.
192		if m != b.ptr.hover || b.pop.Kind() == popup.KindMenu {
193			return
194		}
195		if b.pop.ShowsTooltip(m, ctrl) || !b.tips.allow(m, time.Now()) {
196			return // already shown, or the script asks too often
197		}
198		b.openPopup(m, popup.KindTooltip, ctrl, 0)
199	case module.ControlMenu:
200		serial, ok := b.ptr.takeMenuPress(m, ctrl.Click, time.Now())
201		if !ok {
202			b.log.Debug("menu dropped: it does not answer the latest press", "module", m.Name, "click", ctrl.Click)
203			return
204		}
205		b.openPopup(m, popup.KindMenu, ctrl, serial)
206	}
207}
208
209// openPopup shows ctrl as a popup of module m, anchored to the cells it names.
210func (b *Bar) openPopup(m *module.Module, kind popup.Kind, ctrl module.Control, serial uint32) {
211	if !b.configured || b.fb == nil || b.face == nil || b.lay == nil {
212		return
213	}
214	b.spanBuf = b.lay.Spans(b.spanBuf[:0])
215	var span layout.Span
216	for _, s := range b.spanBuf {
217		if s.M == m {
218			span = s
219		}
220	}
221	if span.M == nil {
222		return // the module shows nothing now
223	}
224	_, h, scale := b.surf.Size()
225	anchor, ok := popup.Anchor(span.Start, span.Width, ctrl.Col, ctrl.Width, b.face.CellW, scale, h)
226	if !ok {
227		b.log.Debug("popup dropped: its column is outside the module", "module", m.Name, "col", ctrl.Col)
228		return
229	}
230	err := b.pop.Open(popup.Request{
231		Kind: kind, Owner: m, Ctrl: ctrl, Anchor: anchor, Parent: b.surf,
232		BottomBar: b.cfg.Bar.Position == "bottom", Feedback: b.fb, Serial: serial,
233	})
234	if err != nil {
235		b.log.Warn("cannot open a popup", "module", m.Name, "err", err)
236	}
237}

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 5

Tags 4

↑↓ moveenter openesc close