feat/data-dnd e05cb15

transition_stash_internal_test.go RawHistoryFolder

1package core
2
3import (
4	"math"
5	"slices"
6	"testing"
7	"time"
8
9	"github.com/bnema/neferwl/internal/ports"
10)
11
12// stashRig is indicatorCore with animations on and a stash of windows 4, 3
13// and 2 (2 selected, 3 peeking on its left) over the tile 1, settled. The
14// peek veil is 0.5.
15func stashRig(t *testing.T, on bool) (*Core, *indicatorClock, *screen, chan ports.ClientCommand) {
16	t.Helper()
17	c, ic, cmds := fadeCore(t)
18	c.cfg.Animations.On = on
19	c.cfg.Stash.Dim = 0.5
20	sc := c.cur()
21	for id := WindowID(1); id <= 4; id++ {
22		sc.mon.AddWindow(id)
23	}
24	for _, id := range []WindowID{4, 3, 2} {
25		sc.mon.Current().FocusID(id)
26		c.applyAction(ActionToggleWindowStash)
27	}
28	settleShown(t, c)
29	drainConfigures(cmds)
30	return c, ic, sc, cmds
31}
32
33// act is a bind: snapshot, action, transition.
34func act(c *Core, ic *indicatorClock, a Action) {
35	shots := c.snapshot(ic.now)
36	c.applyAction(a)
37	c.transition(shots, ic.now)
38}
39
40// Showing the stash fades in and grows every window it brings on screen;
41// the windows it keeps off screen stay out; the settled layout is the
42// final one at once, with no extra configure.
43func TestStashShowAppears(t *testing.T) {
44	c, ic, sc, cmds := stashRig(t, true)
45	act(c, ic, ActionToggleStashVisible)
46	settleShown(t, c)
47	if !sc.mon.Current().stashHidden {
48		t.Fatal("setup: the stash is shown")
49	}
50	ic.now = ic.now.Add(5 * time.Second)
51	c.animate(ic.now, nil)
52	settleShown(t, c)
53	drainConfigures(cmds)
54
55	act(c, ic, ActionToggleStashVisible)
56	s := indicatorScene(t, c)
57	for _, id := range []WindowID{2, 3} {
58		p := settledOf(t, sc, id)
59		w := sceneWindow(t, s, id)
60		if w.Hidden || w.Fade != 1 || w.Rect != scaledRect(p.Rect, appearScale) || w.Zoom < 0.85 || w.Zoom > 0.95 {
61			t.Fatalf("window %d first frame %+v, want invisible at 90 %% of %+v", id, w, p.Rect)
62		}
63	}
64	if w := sceneWindow(t, s, 4); !w.Hidden || w.Fade != 0 {
65		t.Fatalf("window 4 stays off screen: %+v", w)
66	}
67	// The action configures what it changes once, at the settled size.
68	cfgs := configuresOf(cmds)
69	for _, id := range []WindowID{2, 3} {
70		if len(cfgs[id]) != 1 {
71			t.Fatalf("window %d configured %d times, want once: %+v", id, len(cfgs[id]), cfgs[id])
72		}
73		want := c.clientRect(settledOf(t, sc, id))
74		if v := cfgs[id][0]; !v.Visible || v.Width != want.W || v.Height != want.H {
75			t.Fatalf("window %d configure %+v, want %dx%d (the settled client rect)", id, v, want.W, want.H)
76		}
77	}
78
79	s = frame(t, c, ic, 40*time.Millisecond)
80	for _, id := range []WindowID{2, 3} {
81		w := sceneWindow(t, s, id)
82		if !(w.Fade > 0 && w.Fade < 1) || !(w.Zoom > 0.9 && w.Zoom < 1) {
83			t.Fatalf("window %d mid-way fade %v zoom %v, want strictly between", id, w.Fade, w.Zoom)
84		}
85	}
86	s = frame(t, c, ic, 5*time.Second)
87	for _, id := range []WindowID{2, 3} {
88		p := settledOf(t, sc, id)
89		if w := sceneWindow(t, s, id); w.Fade != 0 || w.Zoom != 0 || w.Rect != p.Rect {
90			t.Fatalf("window %d settled %+v, want %+v", id, w, p.Rect)
91		}
92	}
93	if c.animating() || len(sc.rects) != 0 {
94		t.Fatalf("still animating: %d rects", len(sc.rects))
95	}
96	if ids := drainConfigures(cmds); len(ids) != 0 {
97		t.Fatalf("the animation reconfigured %v", ids)
98	}
99}
100
101// Hiding the stash keeps its windows drawn while they fade out and shrink,
102// out of reach of input and focus, until they settle.
103func TestStashHideLeaves(t *testing.T) {
104	c, ic, sc, cmds := stashRig(t, true)
105	left := map[WindowID]Placement{}
106	for _, id := range []WindowID{2, 3} {
107		left[id] = settledOf(t, sc, id)
108	}
109	act(c, ic, ActionToggleStashVisible)
110	s := indicatorScene(t, c)
111	if len(sc.settledLayout) != len(sc.shown) {
112		t.Fatalf("settled %d shown %d", len(sc.settledLayout), len(sc.shown))
113	}
114	for _, id := range []WindowID{2, 3} {
115		w := sceneWindow(t, s, id)
116		if w.Hidden || w.Fade != 0 || w.Rect != left[id].Rect {
117			t.Fatalf("window %d first frame %+v, want drawn opaque at %+v", id, w, left[id].Rect)
118		}
119		if _, _, ok := c.windowRect(id); ok {
120			t.Fatalf("window %d is still on screen for input", id)
121		}
122	}
123	// The selected window is under the tile: its centre hits the tile.
124	r := left[2].Rect
125	if id, _, _ := c.hit(float64(r.X+r.W/2), float64(r.Y+r.H/2)); id == 2 || id == 3 {
126		t.Fatalf("hit %d on a leaving window", id)
127	}
128	if id, _ := sc.mon.Focused(); id == 2 || id == 3 {
129		t.Fatalf("focused %d", id)
130	}
131	if ids := drainConfigures(cmds); slices.Contains(ids, 2) || slices.Contains(ids, 3) {
132		t.Fatalf("a leaving window was configured: %v", ids)
133	}
134
135	s = frame(t, c, ic, 40*time.Millisecond)
136	for _, id := range []WindowID{2, 3} {
137		if w := sceneWindow(t, s, id); !(w.Fade > 0 && w.Fade < 1) || !(w.Rect.W < left[id].Rect.W) {
138			t.Fatalf("window %d mid-way %+v", id, w)
139		}
140	}
141	s = frame(t, c, ic, 5*time.Second)
142	for _, id := range []WindowID{2, 3} {
143		if w := sceneWindow(t, s, id); !w.Hidden {
144			t.Fatalf("window %d still drawn after the settle: %+v", id, w)
145		}
146	}
147	if c.animating() || len(sc.rects) != 0 {
148		t.Fatalf("still animating: %d rects", len(sc.rects))
149	}
150	// The hide's own configure comes once the fade is over, from the size
151	// the window had (not from nothing).
152	var hidden []ports.ConfigureWindow
153	for {
154		select {
155		case cmd := <-cmds:
156			if v, ok := cmd.(ports.ConfigureWindow); ok && v.ID == 2 {
157				hidden = append(hidden, v)
158			}
159			continue
160		default:
161		}
162		break
163	}
164	if len(hidden) != 1 || hidden[0].Visible || hidden[0].Activated {
165		t.Fatalf("configures of window 2 after the hide: %+v", hidden)
166	}
167}
168
169// With animations off both are instant.
170func TestStashToggleOffIsInstant(t *testing.T) {
171	c, ic, sc, _ := stashRig(t, false)
172	act(c, ic, ActionToggleStashVisible)
173	s := indicatorScene(t, c)
174	if w := sceneWindow(t, s, 2); !w.Hidden || len(sc.rects) != 0 || c.animating() {
175		t.Fatalf("hide with animations off: %+v, %d rects", w, len(sc.rects))
176	}
177	act(c, ic, ActionToggleStashVisible)
178	s = indicatorScene(t, c)
179	if w := sceneWindow(t, s, 2); w.Hidden || w.Fade != 0 || w.Zoom != 0 || len(sc.rects) != 0 || c.animating() {
180		t.Fatalf("show with animations off: %+v, %d rects", w, len(sc.rects))
181	}
182}
183
184// Navigating in the stash animates the peek veil: the window that leaves the
185// centre darkens from 0, the one that reaches it clears from the veil.
186func TestStashNavigationAnimatesPeekDim(t *testing.T) {
187	c, ic, sc, _ := stashRig(t, true)
188	if w := sceneWindow(t, indicatorScene(t, c), 3); w.Dim != 0.5 {
189		t.Fatalf("setup: peek dim %v", w.Dim)
190	}
191	act(c, ic, ActionFocusColumnLeft) // 3 selected, 2 peeks on its right
192	s := indicatorScene(t, c)
193	if w := sceneWindow(t, s, 3); w.Dim != 0.5 || w.Focused != true {
194		t.Fatalf("window 3 right after: %+v, want the veil it had", w)
195	}
196	if w := sceneWindow(t, s, 2); w.Dim != 0 {
197		t.Fatalf("window 2 right after: dim %v, want 0 (it was selected)", w.Dim)
198	}
199	s = frame(t, c, ic, 40*time.Millisecond)
200	if w := sceneWindow(t, s, 3); !(w.Dim > 0 && w.Dim < 0.5) {
201		t.Fatalf("window 3 mid-way dim %v, want strictly between 0 and 0.5", w.Dim)
202	}
203	if w := sceneWindow(t, s, 2); !(w.Dim > 0 && w.Dim < 0.5) {
204		t.Fatalf("window 2 mid-way dim %v, want strictly between 0 and 0.5", w.Dim)
205	}
206	s = frame(t, c, ic, 5*time.Second)
207	if w := sceneWindow(t, s, 3); w.Dim != 0 {
208		t.Fatalf("window 3 settled dim %v, want 0", w.Dim)
209	}
210	if w := sceneWindow(t, s, 2); w.Dim != 0.5 {
211		t.Fatalf("window 2 settled dim %v, want 0.5", w.Dim)
212	}
213	if c.animating() || len(sc.rects) != 0 {
214		t.Fatalf("still animating: %d rects", len(sc.rects))
215	}
216}
217
218// A navigation that chains on a running one starts the veil where it was.
219func TestStashNavigationChainsPeekDim(t *testing.T) {
220	c, ic, _, _ := stashRig(t, true)
221	act(c, ic, ActionFocusColumnLeft)
222	indicatorScene(t, c)
223	ic.now = ic.now.Add(30 * time.Millisecond)
224	c.animate(ic.now, nil)
225	mid := sceneWindow(t, indicatorScene(t, c), 3).Dim
226	act(c, ic, ActionFocusColumnRight) // 3 goes back to a peek
227	if got := sceneWindow(t, indicatorScene(t, c), 3).Dim; got < mid-0.02 || got > mid+0.02 {
228		t.Fatalf("window 3 veil %v after the chained action, want the %v it had", got, mid)
229	}
230}
231
232// The overview pile: the stash cards shrink from the stash windows' rects
233// and settle on the pile layout (C1's rules).
234func TestOverviewPileScalesFromStash(t *testing.T) {
235	c, ic, sc, _ := stashRig(t, true)
236	from := map[WindowID]ports.Rect{}
237	s := indicatorScene(t, c)
238	for _, id := range []WindowID{2, 3} {
239		from[id] = sceneWindow(t, s, id).Rect
240	}
241	toggleOverview(c, sc, ic.now)
242	s = indicatorScene(t, c)
243	for _, id := range []WindowID{2, 3} {
244		w := sceneWindow(t, s, id)
245		p := settledOf(t, sc, id)
246		if w.Hidden || w.Preview != overviewCardZoom || w.Rect != from[id] || w.Zoom != 1 {
247			t.Fatalf("window %d first frame %+v, want its stash rect %+v at content scale 1 (card %+v)", id, w, from[id], p.Rect)
248		}
249		checkContent(t, w, from[id], "first frame")
250	}
251	s = frame(t, c, ic, 40*time.Millisecond)
252	for _, id := range []WindowID{2, 3} {
253		w := sceneWindow(t, s, id)
254		p := settledOf(t, sc, id)
255		if !(w.Rect.W < from[id].W && w.Rect.W > p.Rect.W) || !(w.Zoom > overviewCardZoom && w.Zoom < 1) {
256			t.Fatalf("window %d mid-way %+v, want between %+v and %+v", id, w, from[id], p.Rect)
257		}
258		checkContent(t, w, from[id], "mid-way")
259	}
260	s = frame(t, c, ic, 5*time.Second)
261	for _, id := range []WindowID{2, 3} {
262		w := sceneWindow(t, s, id)
263		p := settledOf(t, sc, id)
264		if w.Rect != p.Rect || w.Zoom != 0 || w.Fade != 0 || w.Preview != overviewCardZoom {
265			t.Fatalf("window %d settled %+v, want the pile card %+v", id, w, p)
266		}
267	}
268}
269
270// A navigation slides the stash view: the windows keep the places they
271// were drawn at and move with one spring, with no rect motion of their own.
272// What the view brings in is off screen until it reaches the margin; what
273// it sends off goes once the slide ends.
274func TestStashNavigationSlides(t *testing.T) {
275	c, ic, sc, cmds := stashRig(t, true)
276	w := sc.mon.Current()
277	w.AddWindow(5)
278	w.stashFocus = false                   // the new tile has the focus, not the stash
279	c.applyAction(ActionToggleWindowStash) // stash 4 3 2 5: 5 selected, 2 peeks
280	if len(w.Stash) != 4 || w.stashAt != 3 {
281		t.Fatalf("setup: stash %+v at %d", w.Stash, w.stashAt)
282	}
283	settleShown(t, c)
284	centre := sceneWindow(t, indicatorScene(t, c), 5).Rect
285	act(c, ic, ActionFocusColumnLeft) // 2 selected, 3 comes in, 5 becomes a peek
286	s := indicatorScene(t, c)
287	if got := sceneWindow(t, s, 5); got.Hidden || got.Rect != centre || got.Dim != 0 {
288		t.Fatalf("window 5 jumped: %+v, want %+v unveiled", got, centre)
289	}
290	if got := sceneWindow(t, s, 3); !got.Hidden {
291		t.Fatalf("window 3 is on screen before the slide: %+v", got)
292	}
293	if len(sc.rects) != 0 || !w.stashView.motion.on {
294		t.Fatalf("%d rect motions, slide %v: the view slides alone", len(sc.rects), w.stashView.motion.on)
295	}
296	if got := frame(t, c, ic, 40*time.Millisecond); !(sceneWindow(t, got, 5).Dim > 0) {
297		t.Fatal("window 5 has no veil mid-slide")
298	}
299	settledAll := frame(t, c, ic, 5*time.Second)
300	if got := sceneWindow(t, settledAll, 3); got.Hidden || got.Dim != 0.5 {
301		t.Fatalf("window 3 settled %+v", got)
302	}
303	drainConfigures(cmds)
304	act(c, ic, ActionFocusColumnLeft) // 3 selected, 4 comes in, 5 goes
305	frame(t, c, ic, 5*time.Second)
306	s = indicatorScene(t, c)
307	if got := sceneWindow(t, s, 5); !got.Hidden {
308		t.Fatalf("window 5 still drawn after the settle: %+v", got)
309	}
310	if got := sceneWindow(t, s, 4); got.Hidden {
311		t.Fatalf("window 4 not at the margin: %+v", got)
312	}
313	if c.animating() || w.stashView.off != 0 {
314		t.Fatal("still animating")
315	}
316}
317
318// near fails the test unless a and b differ by less than eps.
319func near(t *testing.T, what string, a, b, eps float64) {
320	t.Helper()
321	if math.Abs(a-b) > eps {
322		t.Fatalf("%s jumped: %v then %v", what, a, b)
323	}
324}
325
326// A hide during the show's fade-in continues from what was drawn: no jump
327// in fade or rect, then it settles gone.
328func TestStashHideDuringShowDoesNotJump(t *testing.T) {
329	c, ic, sc, _ := stashRig(t, true)
330	act(c, ic, ActionToggleStashVisible)
331	indicatorScene(t, c)
332	ic.now = ic.now.Add(5 * time.Second)
333	c.animate(ic.now, nil)
334	indicatorScene(t, c)
335	act(c, ic, ActionToggleStashVisible) // show
336	indicatorScene(t, c)
337	s := frame(t, c, ic, 30*time.Millisecond)
338	last := sceneWindow(t, s, 2)
339	if !(last.Fade > 0 && last.Fade < 1) {
340		t.Fatalf("setup: fade %v", last.Fade)
341	}
342	act(c, ic, ActionToggleStashVisible) // hide, same instant
343	w := sceneWindow(t, indicatorScene(t, c), 2)
344	if w.Hidden {
345		t.Fatalf("window 2 vanished: %+v", w)
346	}
347	near(t, "fade", last.Fade, w.Fade, 0.02)
348	near(t, "width", float64(last.Rect.W), float64(w.Rect.W), 2)
349	near(t, "x", float64(last.Rect.X), float64(w.Rect.X), 2)
350	near(t, "zoom", contentScale(last), contentScale(w), 0.03)
351	s = frame(t, c, ic, 5*time.Second)
352	if w := sceneWindow(t, s, 2); !w.Hidden || c.animating() || len(sc.rects) != 0 {
353		t.Fatalf("after the settle: %+v, %d rects", w, len(sc.rects))
354	}
355}
356
357// A show during the hide's fade-out continues from what was drawn.
358func TestStashShowDuringHideDoesNotJump(t *testing.T) {
359	c, ic, sc, _ := stashRig(t, true)
360	act(c, ic, ActionToggleStashVisible) // hide
361	indicatorScene(t, c)
362	s := frame(t, c, ic, 30*time.Millisecond)
363	last := sceneWindow(t, s, 2)
364	if !(last.Fade > 0 && last.Fade < 1) {
365		t.Fatalf("setup: fade %v", last.Fade)
366	}
367	act(c, ic, ActionToggleStashVisible) // show
368	w := sceneWindow(t, indicatorScene(t, c), 2)
369	if w.Hidden {
370		t.Fatalf("window 2 hidden: %+v", w)
371	}
372	near(t, "fade", last.Fade, w.Fade, 0.02)
373	near(t, "width", float64(last.Rect.W), float64(w.Rect.W), 2)
374	near(t, "x", float64(last.Rect.X), float64(w.Rect.X), 2)
375	near(t, "zoom", contentScale(last), contentScale(w), 0.03)
376	// The entrance now runs back to opaque (the fade's speed carries over:
377	// it may still rise a few frames before it turns).
378	s = frame(t, c, ic, 150*time.Millisecond)
379	if n := sceneWindow(t, s, 2); !(n.Fade < w.Fade) {
380		t.Fatalf("fade %v then %v, want it to clear", w.Fade, n.Fade)
381	}
382	s = frame(t, c, ic, 5*time.Second)
383	p := settledOf(t, sc, 2)
384	if w := sceneWindow(t, s, 2); w.Fade != 0 || w.Zoom != 0 || w.Rect != p.Rect || c.animating() {
385		t.Fatalf("settled %+v, want %+v", w, p.Rect)
386	}
387}
388
389// A second navigation during the slide goes on from the view as drawn: no
390// window jumps, and the one sent off goes once the slide ends.
391func TestStashNavigationChainsFromSlide(t *testing.T) {
392	c, ic, _, _ := stashRig(t, true)
393	// 2 selected, 3 peeks left. Left: 3 centred, 2 peeks right, 4 enters.
394	act(c, ic, ActionFocusColumnLeft)
395	indicatorScene(t, c)
396	s := frame(t, c, ic, 30*time.Millisecond)
397	last := sceneWindow(t, s, 2)
398	if !c.cur().mon.Current().stashView.motion.on {
399		t.Fatal("setup: the slide already ended")
400	}
401	// Left again: 4 centred, 3 peeks right, 2 goes off.
402	act(c, ic, ActionFocusColumnLeft)
403	w := sceneWindow(t, indicatorScene(t, c), 2)
404	if w.Hidden {
405		t.Fatalf("window 2 vanished: %+v", w)
406	}
407	near(t, "x", float64(last.Rect.X), float64(w.Rect.X), 2)
408	near(t, "width", float64(last.Rect.W), float64(w.Rect.W), 2)
409	near(t, "dim", last.Dim, w.Dim, 0.03)
410	if s = frame(t, c, ic, 5*time.Second); !sceneWindow(t, s, 2).Hidden {
411		t.Fatal("window 2 still drawn after the settle")
412	}
413}
414
415// Leaving entries land in the same order whatever the map's: a hidden stash
416// float (replaced in place) and absent tiles and floats (ID order, tiles
417// before the first float, floats last).
418func TestWithLeavingOrderIsDeterministic(t *testing.T) {
419	c, ic := indicatorCore(t)
420	c.cfg.Animations.On = true
421	sc := c.cur()
422	w := sc.mon.Current()
423	for id := WindowID(1); id <= 4; id++ {
424		sc.mon.AddWindow(id)
425	}
426	w.FocusID(4)
427	c.applyAction(ActionToggleWindowStash) // 4 stashed, shown
428	w.AddFloating(5, 20, 10)
429	settleShown(t, c)
430	var left []Placement
431	for _, p := range sc.settledLayout {
432		if p.ID == 2 || p.ID == 3 || p.ID == 4 || p.ID == 5 {
433			left = append(left, p)
434		}
435	}
436	if len(left) != 4 {
437		t.Fatalf("setup: %d placements", len(left))
438	}
439	// 2 and 3 close (absent tiles), 5 closes (absent float), 4 hides.
440	sc.mon.RemoveWindow(2)
441	sc.mon.RemoveWindow(3)
442	sc.mon.RemoveWindow(5)
443	c.applyAction(ActionToggleStashVisible)
444	for _, p := range left {
445		c.leaveFrom(sc, p, nil, ic.now)
446	}
447	var want []WindowID
448	for i := range 40 {
449		c.refreshShown()
450		var ids []WindowID
451		for _, p := range sc.settledLayout {
452			ids = append(ids, p.ID)
453		}
454		if i == 0 {
455			want = ids
456			if n := len(ids); n < 4 {
457				t.Fatalf("layout %v", ids)
458			}
459			i2, i3 := slices.Index(ids, 2), slices.Index(ids, 3)
460			i4, i5 := slices.Index(ids, 4), slices.Index(ids, 5)
461			// Absent tiles by ID, then the leaving stash float (in its
462			// slot), the absent float last.
463			if !(i2 >= 0 && i2 < i3 && i3 < i4 && i4 < i5 && i5 == len(ids)-1) {
464				t.Fatalf("order %v: want tiles 2, 3, then the stash float 4, then 5", ids)
465			}
466			if k := slices.Index(ids, 4); !sc.settledLayout[k].Leaving {
467				t.Fatal("the hidden stash window is not replaced in place")
468			}
469			continue
470		}
471		if !slices.Equal(ids, want) {
472			t.Fatalf("run %d: order %v, first run %v", i, ids, want)
473		}
474	}
475}
476
477// Walking the stash carousel slides its cards; a card that goes round to
478// the other end fades in there instead of crossing the row, whatever the
479// direction, the count or the size of the step (a fast wheel). A carousel
480// that does not wrap round slides every card.
481func TestCarouselWrapFadesIn(t *testing.T) {
482	for _, tc := range []struct {
483		name        string
484		n, from, to int
485		wrapped     []int
486	}{
487		{"right", 3, 0, 1, []int{2}},
488		{"left", 3, 0, 2, []int{1}},
489		{"even", 4, 3, 0, []int{1}},
490		{"jump", 5, 0, 2, []int{3, 4}},
491		{"two cards", 2, 0, 1, nil},
492	} {
493		t.Run(tc.name, func(t *testing.T) {
494			c, ic, _ := fadeCore(t)
495			c.cfg.Animations.On = true
496			sc := c.cur()
497			m := sc.mon
498			w := m.Current()
499			w.Overflow = OverflowCascade
500			m.AddWindow(1)
501			for id := WindowID(2); id < WindowID(2+tc.n); id++ {
502				m.AddWindow(id)
503				w.FocusID(id)
504				c.applyAction(ActionToggleWindowStash)
505			}
506			w.FocusID(1)
507			c.applyAction(ActionToggleOverview)
508			m.selectCard(w, tc.from)
509			settleShown(t, c)
510			shots := c.snapshot(ic.now)
511			m.selectCard(w, tc.to)
512			c.transition(shots, ic.now)
513			for i, e := range w.Stash {
514				if previewOf(t, m.Layout(), e.ID).Hidden {
515					continue
516				}
517				rm := sc.rects[e.ID]
518				if slices.Contains(tc.wrapped, i) {
519					if rm.dx != 0 || !rm.fade.on {
520						t.Fatalf("wrapped card %d: dx %v, fade %v", i, rm.dx, rm.fade.on)
521					}
522					continue
523				}
524				if rm.dx == 0 || rm.fade.on {
525					t.Fatalf("card %d: dx %v, fade %v", i, rm.dx, rm.fade.on)
526				}
527			}
528		})
529	}
530}

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