v0.2.0 13c6a90

pulse.go RawHistoryFolder

1package core
2
3import (
4	"math"
5	"time"
6
7	"github.com/bnema/neferwl/internal/ports"
8)
9
10// The focus pulse is the pulse animation of the focus indicator
11// (focus.animation): it briefly shows the indicator's effect (focus.effect, a
12// light screen blend visible on black backgrounds too) on a window that just
13// got the keyboard focus. Only the last focus counts: it must hold for
14// pulseSettle before the pulse starts, a focus change stops a running one, and
15// the window that pulsed last does not pulse again within pulseCooldown
16// (switching back after another window held the focus is a new change).
17// Fast switching therefore shows nothing. Fullscreen windows, overview
18// previews, a window focused as it maps and a protected session never pulse.
19
20const (
21	pulseSettle   = 150 * time.Millisecond
22	pulseRise     = 90 * time.Millisecond
23	pulseFall     = 230 * time.Millisecond
24	pulseCooldown = time.Second
25)
26
27// focusPulse is the pulse state. target is the window focus last seen and
28// since when; id is the window pulsing from start (0: none). last and lastAt
29// are the previous pulse, for the cooldown. drawable is whether the last
30// scene could show a pulse on target.
31type focusPulse struct {
32	target    WindowID
33	since     time.Time
34	id        WindowID
35	start     time.Time
36	last      WindowID
37	lastAt    time.Time
38	drawable  bool
39	timerC    <-chan time.Time
40	timerStop func() bool
41}
42
43func (c *Core) pulseOn() bool { return c.cfg.Focus.Animation == ports.FocusAnimationPulse }
44
45// pulseFocus follows the window focus. held is false while something else
46// (a popup grab, a layer, a lock surface) has the keyboard: the running pulse
47// stops, and the window getting it back is not a new focus. A new window
48// stops the running pulse and waits pulseSettle before one starts. With the
49// pulse off the focus is still followed, so turning it on pulses nothing.
50func (c *Core) pulseFocus(window WindowID, held bool) {
51	p := &c.pulse
52	if !held || !c.pulseOn() {
53		p.id = 0
54		c.stopPulseTimer()
55		if held {
56			p.target = window
57		}
58		return
59	}
60	if window == p.target {
61		return
62	}
63	p.target, p.id = window, 0
64	c.stopPulseTimer()
65	if window == 0 {
66		return
67	}
68	p.since = c.now()
69	if c.justMapped(window) {
70		return
71	}
72	p.timerC, p.timerStop = newTimer(c.ch.Clock, pulseSettle)
73}
74
75// justMapped reports whether the window got the focus as it mapped: opening
76// an app is not a focus change, and its first frames may still be on their
77// way. Its focus counts from the next change.
78func (c *Core) justMapped(id WindowID) bool {
79	p := &c.pulse
80	return p.target == id && p.since.Sub(c.windows.lookup(id).mappedAt) < pulseSettle
81}
82
83// pulseTick runs when the settle timer fired. It reports whether a pulse
84// started.
85func (c *Core) pulseTick() bool {
86	p := &c.pulse
87	p.timerC, p.timerStop = nil, nil
88	if p.target == 0 || !c.pulseOn() || !p.drawable || c.justMapped(p.target) {
89		return false
90	}
91	now := c.now()
92	if wait := pulseSettle - now.Sub(p.since); wait > 0 {
93		p.timerC, p.timerStop = newTimer(c.ch.Clock, wait)
94		return false
95	}
96	if p.target == p.last && now.Sub(p.lastAt) < pulseCooldown {
97		return false
98	}
99	p.id, p.start, p.last, p.lastAt = p.target, now, p.target, now
100	return true
101}
102
103// advancePulse is the effect of the running pulse at now: a quick sine rise
104// to focus.strength, then a smooth fall. It ends the pulse once over.
105func (c *Core) advancePulse(now time.Time) float64 {
106	p := &c.pulse
107	if p.id == 0 {
108		return 0
109	}
110	peak := c.cfg.Focus.Strength
111	t := now.Sub(p.start)
112	switch {
113	case t < 0:
114		return 0
115	case t < pulseRise:
116		return peak * math.Sin(float64(t)/float64(pulseRise)*math.Pi/2)
117	case t < pulseRise+pulseFall:
118		x := 1 - float64(t-pulseRise)/float64(pulseFall)
119		return peak * x * x * (3 - 2*x)
120	}
121	p.id = 0
122	return 0
123}
124
125// pulsing reports whether a pulse runs on the screen.
126func (c *Core) pulsing(sc *screen) bool {
127	return c.pulse.id != 0 && sc == c.cur()
128}
129
130func (c *Core) stopPulseTimer() {
131	p := &c.pulse
132	if p.timerStop != nil {
133		p.timerStop()
134	}
135	p.timerC, p.timerStop = nil, nil
136}
137
138// stopPulse ends the running pulse and its timer (security change). The
139// focus is kept: the window focused again after an unlock does not pulse.
140func (c *Core) stopPulse() {
141	c.stopPulseTimer()
142	c.pulse.id, c.pulse.drawable = 0, false
143}

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