pulse_test.go RawHistoryFolder

1package core
2
3import (
4	"testing"
5	"time"
6
7	"github.com/bnema/neferwl/internal/ports"
8)
9
10// pulseCore is indicatorCore with the focus pulse on and two tiled windows,
11// 2 focused, its pulse settled and over.
12func pulseCore(t *testing.T) (*Core, *indicatorClock) {
13	t.Helper()
14	c, ic := indicatorCore(t)
15	c.cfg.Focus.Animation = ports.FocusAnimationPulse
16	c.cfg.Focus.Strength = 0.05
17	c.cur().mon.AddWindow(1)
18	c.cur().mon.AddWindow(2)
19	indicatorScene(t, c)
20	ic.now = ic.now.Add(pulseSettle)
21	c.pulseTick()
22	ic.now = ic.now.Add(pulseCooldown)
23	indicatorScene(t, c)
24	return c, ic
25}
26
27func pulseOf(s ports.Scene, id WindowID) float64 {
28	for _, w := range s.Windows {
29		if w.ID == id {
30			return w.FocusEffect
31		}
32	}
33	return -1
34}
35
36func TestFocusPulseAfterSettle(t *testing.T) {
37	c, ic := pulseCore(t)
38	c.cur().mon.Current().FocusID(1)
39	if s := indicatorScene(t, c); pulseOf(s, 1) != 0 {
40		t.Fatalf("pulse before settle: %v", pulseOf(s, 1))
41	}
42	ic.now = ic.now.Add(pulseSettle)
43	if !c.pulseTick() {
44		t.Fatal("no pulse after settle")
45	}
46	if !c.animating() {
47		t.Fatal("a running pulse asks for no frames")
48	}
49	ic.now = ic.now.Add(pulseRise)
50	s := indicatorScene(t, c)
51	if p := pulseOf(s, 1); p < 0.05*0.99 || p > 0.05 {
52		t.Fatalf("peak %v, want the configured 0.05", p)
53	}
54	if pulseOf(s, 2) != 0 {
55		t.Fatal("unfocused window pulses")
56	}
57	ic.now = ic.now.Add(pulseFall)
58	if s := indicatorScene(t, c); pulseOf(s, 1) != 0 || c.animating() {
59		t.Fatalf("pulse left after its end: %v", pulseOf(s, 1))
60	}
61}
62
63// Switching faster than pulseSettle never pulses: only the last focus counts.
64func TestFocusPulseDebounced(t *testing.T) {
65	c, ic := pulseCore(t)
66	for range 4 {
67		c.cur().mon.Current().FocusID(1)
68		indicatorScene(t, c)
69		ic.now = ic.now.Add(pulseSettle / 3)
70		c.cur().mon.Current().FocusID(2)
71		indicatorScene(t, c)
72		ic.now = ic.now.Add(pulseSettle / 3)
73	}
74	// A stale timer of an earlier focus fires: the focus is too recent.
75	if c.pulseTick() {
76		t.Fatal("pulse while switching")
77	}
78	if c.pulse.timerC == nil {
79		t.Fatal("no timer for the last focus")
80	}
81	ic.now = ic.now.Add(pulseSettle)
82	// Window 2 pulsed in pulseCore over a second ago: it may pulse again.
83	if !c.pulseTick() || c.pulse.id != 2 {
84		t.Fatalf("last focus did not pulse: %d", c.pulse.id)
85	}
86}
87
88// A focus change stops the running pulse; coming back within the cooldown
89// does not pulse the same window again.
90func TestFocusPulseStopsAndCoolsDown(t *testing.T) {
91	c, ic := pulseCore(t)
92	c.cur().mon.Current().FocusID(1)
93	indicatorScene(t, c)
94	ic.now = ic.now.Add(pulseSettle)
95	c.pulseTick()
96	ic.now = ic.now.Add(pulseRise / 2)
97	c.cur().mon.Current().FocusID(2)
98	if s := indicatorScene(t, c); pulseOf(s, 1) != 0 || pulseOf(s, 2) != 0 {
99		t.Fatal("pulse survived a focus change")
100	}
101	c.cur().mon.Current().FocusID(1)
102	indicatorScene(t, c)
103	ic.now = ic.now.Add(pulseSettle)
104	if c.pulseTick() {
105		t.Fatal("same window pulsed within the cooldown")
106	}
107}
108
109// A window focused as it maps does not pulse; focusing it again later does.
110func TestFocusPulseSkipsNewWindow(t *testing.T) {
111	c, ic := pulseCore(t)
112	c.windows.mapped(ports.WindowMapped{ID: 3}, ic.now)
113	c.cur().mon.AddWindow(3)
114	indicatorScene(t, c)
115	if c.pulse.timerC != nil {
116		t.Fatal("opening a window arms the pulse")
117	}
118	ic.now = ic.now.Add(pulseSettle)
119	if c.pulseTick() || c.animating() {
120		t.Fatal("a new window pulses")
121	}
122	c.cur().mon.Current().FocusID(1)
123	indicatorScene(t, c)
124	c.cur().mon.Current().FocusID(3)
125	indicatorScene(t, c)
126	ic.now = ic.now.Add(pulseSettle)
127	if !c.pulseTick() || c.pulse.id != 3 {
128		t.Fatal("an existing window did not pulse")
129	}
130}
131
132// A window that mapped earlier without the focus pulses when focused.
133func TestFocusPulseWindowMappedEarlier(t *testing.T) {
134	c, ic := pulseCore(t)
135	c.windows.mapped(ports.WindowMapped{ID: 1}, ic.now.Add(-time.Minute))
136	c.cur().mon.Current().FocusID(1)
137	indicatorScene(t, c)
138	ic.now = ic.now.Add(pulseSettle)
139	if !c.pulseTick() || c.pulse.id != 1 {
140		t.Fatal("a window mapped earlier did not pulse")
141	}
142}
143
144func TestFocusPulseSkipsFullscreen(t *testing.T) {
145	c, ic := pulseCore(t)
146	c.cur().mon.Current().FocusID(1)
147	c.cur().mon.ToggleFullscreen()
148	indicatorScene(t, c)
149	ic.now = ic.now.Add(pulseSettle)
150	if c.pulseTick() || c.animating() {
151		t.Fatal("fullscreen window asks for pulse frames")
152	}
153	// Fullscreen entered mid-pulse ends it.
154	c.cur().mon.ToggleFullscreen()
155	c.cur().mon.Current().FocusID(2)
156	indicatorScene(t, c)
157	ic.now = ic.now.Add(pulseSettle)
158	if !c.pulseTick() {
159		t.Fatal("no pulse on the tiled window")
160	}
161	c.cur().mon.ToggleFullscreen()
162	if s := indicatorScene(t, c); pulseOf(s, 2) != 0 || c.animating() {
163		t.Fatal("pulse survived fullscreen")
164	}
165}
166
167// A popup grab or layer taking the keyboard stops the pulse; the window
168// getting it back is not a new focus and does not pulse again.
169func TestFocusPulseIgnoresKeyboardDetours(t *testing.T) {
170	c, ic := pulseCore(t)
171	c.cur().mon.Current().FocusID(1)
172	indicatorScene(t, c)
173	ic.now = ic.now.Add(pulseSettle)
174	c.pulseTick()
175	// The keyboard goes elsewhere (held false), then comes back.
176	c.pulseFocus(1, false)
177	if c.animating() {
178		t.Fatal("pulse kept while the keyboard is elsewhere")
179	}
180	ic.now = ic.now.Add(2 * pulseCooldown)
181	c.pulseFocus(1, true)
182	if c.pulse.timerC != nil {
183		t.Fatal("returning keyboard counts as a new focus")
184	}
185}
186
187func TestFocusPulseOff(t *testing.T) {
188	c, ic := pulseCore(t)
189	c.cfg.Focus.Animation = ports.FocusAnimationOff
190	c.cur().mon.Current().FocusID(1)
191	indicatorScene(t, c)
192	if c.pulse.timerC != nil {
193		t.Fatal("timer armed with the pulse off")
194	}
195	ic.now = ic.now.Add(pulseSettle)
196	if c.pulseTick() || c.animating() {
197		t.Fatal("pulse with focus.animation off")
198	}
199	// Turning it back on does not pulse the window already focused.
200	c.cfg.Focus.Animation = ports.FocusAnimationPulse
201	indicatorScene(t, c)
202	if c.pulse.timerC != nil {
203		t.Fatal("enabling the pulse pulses the focused window")
204	}
205}

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 26

Tags 10

↑↓ moveenter openesc close