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.Animations.On = true
16	c.cfg.Focus.Animation = ports.FocusAnimationPulse
17	c.cfg.Focus.Strength = 0.05
18	c.cur().mon.AddWindow(1)
19	c.cur().mon.AddWindow(2)
20	indicatorScene(t, c)
21	ic.now = ic.now.Add(pulseSettle)
22	c.pulseTick()
23	ic.now = ic.now.Add(pulseCooldown)
24	indicatorScene(t, c)
25	return c, ic
26}
27
28func pulseOf(s ports.Scene, id WindowID) float64 {
29	for _, w := range s.Windows {
30		if w.ID == id {
31			return w.FocusEffect
32		}
33	}
34	return -1
35}
36
37func TestFocusPulseAfterSettle(t *testing.T) {
38	c, ic := pulseCore(t)
39	c.cur().mon.Current().FocusID(1)
40	if s := indicatorScene(t, c); pulseOf(s, 1) != 0 {
41		t.Fatalf("pulse before settle: %v", pulseOf(s, 1))
42	}
43	ic.now = ic.now.Add(pulseSettle)
44	if !c.pulseTick() {
45		t.Fatal("no pulse after settle")
46	}
47	if !c.animating() {
48		t.Fatal("a running pulse asks for no frames")
49	}
50	ic.now = ic.now.Add(pulseRise)
51	s := indicatorScene(t, c)
52	if p := pulseOf(s, 1); p < 0.05*0.99 || p > 0.05 {
53		t.Fatalf("peak %v, want the configured 0.05", p)
54	}
55	if pulseOf(s, 2) != 0 {
56		t.Fatal("unfocused window pulses")
57	}
58	ic.now = ic.now.Add(pulseFall)
59	if s := indicatorScene(t, c); pulseOf(s, 1) != 0 || c.animating() {
60		t.Fatalf("pulse left after its end: %v", pulseOf(s, 1))
61	}
62}
63
64// Switching faster than pulseSettle never pulses: only the last focus counts.
65func TestFocusPulseDebounced(t *testing.T) {
66	c, ic := pulseCore(t)
67	for range 4 {
68		c.cur().mon.Current().FocusID(1)
69		indicatorScene(t, c)
70		ic.now = ic.now.Add(pulseSettle / 3)
71		c.cur().mon.Current().FocusID(2)
72		indicatorScene(t, c)
73		ic.now = ic.now.Add(pulseSettle / 3)
74	}
75	// A stale timer of an earlier focus fires: the focus is too recent.
76	if c.pulseTick() {
77		t.Fatal("pulse while switching")
78	}
79	if c.pulse.timerC == nil {
80		t.Fatal("no timer for the last focus")
81	}
82	ic.now = ic.now.Add(pulseSettle)
83	// Window 2 pulsed in pulseCore over a second ago: it may pulse again.
84	if !c.pulseTick() || c.pulse.id != 2 {
85		t.Fatalf("last focus did not pulse: %d", c.pulse.id)
86	}
87}
88
89// A focus change stops the running pulse; coming back within the cooldown
90// does not pulse the same window again.
91func TestFocusPulseStopsAndCoolsDown(t *testing.T) {
92	c, ic := pulseCore(t)
93	c.cur().mon.Current().FocusID(1)
94	indicatorScene(t, c)
95	ic.now = ic.now.Add(pulseSettle)
96	c.pulseTick()
97	ic.now = ic.now.Add(pulseRise / 2)
98	c.cur().mon.Current().FocusID(2)
99	if s := indicatorScene(t, c); pulseOf(s, 1) != 0 || pulseOf(s, 2) != 0 {
100		t.Fatal("pulse survived a focus change")
101	}
102	c.cur().mon.Current().FocusID(1)
103	indicatorScene(t, c)
104	ic.now = ic.now.Add(pulseSettle)
105	if c.pulseTick() {
106		t.Fatal("same window pulsed within the cooldown")
107	}
108}
109
110// A window focused as it maps does not pulse; focusing it again later does.
111func TestFocusPulseSkipsNewWindow(t *testing.T) {
112	c, ic := pulseCore(t)
113	c.windows.mapped(ports.WindowMapped{ID: 3}, ic.now)
114	c.cur().mon.AddWindow(3)
115	indicatorScene(t, c)
116	if c.pulse.timerC != nil {
117		t.Fatal("opening a window arms the pulse")
118	}
119	ic.now = ic.now.Add(pulseSettle)
120	if c.pulseTick() || c.animating() {
121		t.Fatal("a new window pulses")
122	}
123	c.cur().mon.Current().FocusID(1)
124	indicatorScene(t, c)
125	c.cur().mon.Current().FocusID(3)
126	indicatorScene(t, c)
127	ic.now = ic.now.Add(pulseSettle)
128	if !c.pulseTick() || c.pulse.id != 3 {
129		t.Fatal("an existing window did not pulse")
130	}
131}
132
133// A window that mapped earlier without the focus pulses when focused.
134func TestFocusPulseWindowMappedEarlier(t *testing.T) {
135	c, ic := pulseCore(t)
136	c.windows.mapped(ports.WindowMapped{ID: 1}, ic.now.Add(-time.Minute))
137	c.cur().mon.Current().FocusID(1)
138	indicatorScene(t, c)
139	ic.now = ic.now.Add(pulseSettle)
140	if !c.pulseTick() || c.pulse.id != 1 {
141		t.Fatal("a window mapped earlier did not pulse")
142	}
143}
144
145func TestFocusPulseSkipsFullscreen(t *testing.T) {
146	c, ic := pulseCore(t)
147	c.cur().mon.Current().FocusID(1)
148	c.cur().mon.ToggleFullscreen()
149	indicatorScene(t, c)
150	ic.now = ic.now.Add(pulseSettle)
151	if c.pulseTick() || c.animating() {
152		t.Fatal("fullscreen window asks for pulse frames")
153	}
154	// Fullscreen entered mid-pulse ends it.
155	c.cur().mon.ToggleFullscreen()
156	c.cur().mon.Current().FocusID(2)
157	indicatorScene(t, c)
158	ic.now = ic.now.Add(pulseSettle)
159	if !c.pulseTick() {
160		t.Fatal("no pulse on the tiled window")
161	}
162	c.cur().mon.ToggleFullscreen()
163	if s := indicatorScene(t, c); pulseOf(s, 2) != 0 || c.animating() {
164		t.Fatal("pulse survived fullscreen")
165	}
166}
167
168// A window alone on screen (a maximized column here) needs no pulse; once
169// another window shows beside it, focusing it pulses again.
170func TestFocusPulseSkipsAloneOnScreen(t *testing.T) {
171	c, ic := pulseCore(t)
172	c.cur().mon.Current().FocusID(1)
173	c.cur().mon.Current().ToggleFullWidth()
174	indicatorScene(t, c)
175	ic.now = ic.now.Add(pulseSettle)
176	if c.pulseTick() || c.animating() {
177		t.Fatal("a window alone on screen pulses")
178	}
179	c.cur().mon.Current().ToggleFullWidth()
180	c.cur().mon.Current().FocusID(2)
181	indicatorScene(t, c)
182	ic.now = ic.now.Add(pulseSettle)
183	if !c.pulseTick() || c.pulse.id != 2 {
184		t.Fatal("a window beside another did not pulse")
185	}
186}
187
188// A cascade window alone in its band above a bottom panel is alone on
189// screen: the next band, under the panel, does not count.
190func TestFocusPulseSkipsAloneAbovePanel(t *testing.T) {
191	c, ic := pulseCore(t)
192	m := c.cur().mon
193	m.SetOverflow(OverflowCascade)
194	m.SetMaxColumns(1)
195	m.SetUsable(Rect{W: 300, H: 180})
196	m.Current().FocusID(1)
197	indicatorScene(t, c)
198	ic.now = ic.now.Add(pulseSettle)
199	if c.pulseTick() || c.animating() {
200		t.Fatal("a window alone in its band above a panel pulses")
201	}
202}
203
204// A swipe, its landing slide or a transition still shows the neighbours: the
205// pulse waits for the layout to settle, then decides on it.
206func TestFocusPulseWaitsForSlide(t *testing.T) {
207	for _, tc := range []struct {
208		name  string
209		slide func(*testing.T, *Core, time.Time)
210		alone bool
211	}{
212		{"workspace landing, alone", func(_ *testing.T, c *Core, now time.Time) {
213			c.cur().mon.switchView.motion = newMotion(workspaceSpring(0.5, 0), now, 1)
214		}, true},
215		{"workspace drag, alone", func(_ *testing.T, c *Core, _ time.Time) { c.cur().mon.switchView.off = 0.3 }, true},
216		{"column drag, beside another", func(_ *testing.T, c *Core, _ time.Time) { c.cur().mon.Current().view.off = 40 }, false},
217		{"rect motion, beside another", func(_ *testing.T, c *Core, now time.Time) {
218			sc := c.cur()
219			sc.rects = map[WindowID]rectMotion{1: {x: c.spring(viewSpring(40, 0), now), dx: 40}}
220			sc.rectsWS = sc.mon.Current()
221		}, false},
222		{"camera transition of a key, beside another", func(t *testing.T, c *Core, now time.Time) {
223			m := c.cur().mon
224			for id := WindowID(3); id <= 4; id++ {
225				m.AddWindow(id)
226			}
227			indicatorScene(t, c)
228			before := c.snapshot(now)
229			for range 3 {
230				c.applyAction(ActionFocusColumnLeft)
231			}
232			c.transition(before, now)
233			if !m.Current().view.motion.on || m.settled() {
234				t.Fatal("no camera transition after the focus moved the view")
235			}
236		}, false},
237	} {
238		t.Run(tc.name, func(t *testing.T) {
239			c, ic := pulseCore(t)
240			m := c.cur().mon
241			m.Current().FocusID(1)
242			indicatorScene(t, c)
243			tc.slide(t, c, ic.now)
244			indicatorScene(t, c)
245			ic.now = ic.now.Add(pulseSettle)
246			if c.pulseTick() || c.pulse.timerC == nil {
247				t.Fatal("pulse decided during a slide")
248			}
249			m.stopSwitch()
250			m.Current().view.stop()
251			c.cur().stopRects()
252			if tc.alone {
253				m.Current().ToggleFullWidth()
254			}
255			indicatorScene(t, c)
256			ic.now = ic.now.Add(pulseRecheck)
257			if got := c.pulseTick(); got == tc.alone {
258				t.Fatalf("pulse started %v after landing, window alone %v", got, tc.alone)
259			}
260		})
261	}
262}
263
264// A popup grab or layer taking the keyboard stops the pulse; the window
265// getting it back is not a new focus and does not pulse again.
266func TestFocusPulseIgnoresKeyboardDetours(t *testing.T) {
267	c, ic := pulseCore(t)
268	c.cur().mon.Current().FocusID(1)
269	indicatorScene(t, c)
270	ic.now = ic.now.Add(pulseSettle)
271	c.pulseTick()
272	// The keyboard goes elsewhere (held false), then comes back.
273	c.pulseFocus(1, false)
274	if c.animating() {
275		t.Fatal("pulse kept while the keyboard is elsewhere")
276	}
277	ic.now = ic.now.Add(2 * pulseCooldown)
278	c.pulseFocus(1, true)
279	if c.pulse.timerC != nil {
280		t.Fatal("returning keyboard counts as a new focus")
281	}
282}
283
284func TestFocusPulseOff(t *testing.T) {
285	c, ic := pulseCore(t)
286	c.cfg.Focus.Animation = ports.FocusAnimationOff
287	c.cur().mon.Current().FocusID(1)
288	indicatorScene(t, c)
289	if c.pulse.timerC != nil {
290		t.Fatal("timer armed with the pulse off")
291	}
292	ic.now = ic.now.Add(pulseSettle)
293	if c.pulseTick() || c.animating() {
294		t.Fatal("pulse with focus.animation off")
295	}
296	// Turning it back on does not pulse the window already focused.
297	c.cfg.Focus.Animation = ports.FocusAnimationPulse
298	indicatorScene(t, c)
299	if c.pulse.timerC != nil {
300		t.Fatal("enabling the pulse pulses the focused window")
301	}
302}
303
304func TestFocusPulseOffWithAnimationsOff(t *testing.T) {
305	c, ic := pulseCore(t)
306	c.cfg.Animations.On = false
307	if c.pulseOn() {
308		t.Fatal("pulse on with animations off")
309	}
310	c.cur().mon.Current().FocusID(1)
311	indicatorScene(t, c)
312	if c.pulse.timerC != nil || c.pulse.id != 0 {
313		t.Fatal("pulse armed with animations off")
314	}
315	ic.now = ic.now.Add(pulseSettle)
316	if c.pulseTick() || c.animating() {
317		t.Fatal("pulse with animations off")
318	}
319}

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