v0.4.0 b6be3a8

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 swipe, its landing slide or a transition still shows the neighbours: the
189// pulse waits for the layout to settle, then decides on it.
190func TestFocusPulseWaitsForSlide(t *testing.T) {
191	for _, tc := range []struct {
192		name  string
193		slide func(*testing.T, *Core, time.Time)
194		alone bool
195	}{
196		{"workspace landing, alone", func(_ *testing.T, c *Core, now time.Time) {
197			c.cur().mon.switchMotion = newMotion(workspaceSpring(0.5, 0), now, 1)
198		}, true},
199		{"workspace drag, alone", func(_ *testing.T, c *Core, _ time.Time) { c.cur().mon.switchOff = 0.3 }, true},
200		{"column drag, beside another", func(_ *testing.T, c *Core, _ time.Time) { c.cur().mon.Current().shift = 40 }, false},
201		{"rect motion, beside another", func(_ *testing.T, c *Core, now time.Time) {
202			sc := c.cur()
203			sc.rects = map[WindowID]rectMotion{1: {x: c.spring(viewSpring(40, 0), now), dx: 40}}
204			sc.rectsWS = sc.mon.Current()
205		}, false},
206		{"camera transition of a key, beside another", func(t *testing.T, c *Core, now time.Time) {
207			m := c.cur().mon
208			for id := WindowID(3); id <= 4; id++ {
209				m.AddWindow(id)
210			}
211			indicatorScene(t, c)
212			before := c.snapshot(now)
213			for range 3 {
214				c.applyAction(ActionFocusColumnLeft)
215			}
216			c.transition(before, now)
217			if !m.Current().motion.on || m.settled() {
218				t.Fatal("no camera transition after the focus moved the view")
219			}
220		}, false},
221	} {
222		t.Run(tc.name, func(t *testing.T) {
223			c, ic := pulseCore(t)
224			m := c.cur().mon
225			m.Current().FocusID(1)
226			indicatorScene(t, c)
227			tc.slide(t, c, ic.now)
228			indicatorScene(t, c)
229			ic.now = ic.now.Add(pulseSettle)
230			if c.pulseTick() || c.pulse.timerC == nil {
231				t.Fatal("pulse decided during a slide")
232			}
233			m.stopSwitch()
234			m.Current().stopSlide()
235			c.cur().stopRects()
236			if tc.alone {
237				m.Current().ToggleFullWidth()
238			}
239			indicatorScene(t, c)
240			ic.now = ic.now.Add(pulseRecheck)
241			if got := c.pulseTick(); got == tc.alone {
242				t.Fatalf("pulse started %v after landing, window alone %v", got, tc.alone)
243			}
244		})
245	}
246}
247
248// A popup grab or layer taking the keyboard stops the pulse; the window
249// getting it back is not a new focus and does not pulse again.
250func TestFocusPulseIgnoresKeyboardDetours(t *testing.T) {
251	c, ic := pulseCore(t)
252	c.cur().mon.Current().FocusID(1)
253	indicatorScene(t, c)
254	ic.now = ic.now.Add(pulseSettle)
255	c.pulseTick()
256	// The keyboard goes elsewhere (held false), then comes back.
257	c.pulseFocus(1, false)
258	if c.animating() {
259		t.Fatal("pulse kept while the keyboard is elsewhere")
260	}
261	ic.now = ic.now.Add(2 * pulseCooldown)
262	c.pulseFocus(1, true)
263	if c.pulse.timerC != nil {
264		t.Fatal("returning keyboard counts as a new focus")
265	}
266}
267
268func TestFocusPulseOff(t *testing.T) {
269	c, ic := pulseCore(t)
270	c.cfg.Focus.Animation = ports.FocusAnimationOff
271	c.cur().mon.Current().FocusID(1)
272	indicatorScene(t, c)
273	if c.pulse.timerC != nil {
274		t.Fatal("timer armed with the pulse off")
275	}
276	ic.now = ic.now.Add(pulseSettle)
277	if c.pulseTick() || c.animating() {
278		t.Fatal("pulse with focus.animation off")
279	}
280	// Turning it back on does not pulse the window already focused.
281	c.cfg.Focus.Animation = ports.FocusAnimationPulse
282	indicatorScene(t, c)
283	if c.pulse.timerC != nil {
284		t.Fatal("enabling the pulse pulses the focused window")
285	}
286}
287
288func TestFocusPulseOffWithAnimationsOff(t *testing.T) {
289	c, ic := pulseCore(t)
290	c.cfg.Animations.On = false
291	if c.pulseOn() {
292		t.Fatal("pulse on with animations off")
293	}
294	c.cur().mon.Current().FocusID(1)
295	indicatorScene(t, c)
296	if c.pulse.timerC != nil || c.pulse.id != 0 {
297		t.Fatal("pulse armed with animations off")
298	}
299	ic.now = ic.now.Add(pulseSettle)
300	if c.pulseTick() || c.animating() {
301		t.Fatal("pulse with animations off")
302	}
303}

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