refactor/kvconf 3462c76

overview_test.go RawHistoryFolder

1package core
2
3import (
4	"math"
5	"testing"
6
7	"github.com/bnema/neferwl/internal/ports"
8)
9
10// overviewMonitor has workspace 1 with columns 1, 2, 3 (a third of the
11// width each) on a 300x200 output, and workspace 2 with column 4.
12func overviewMonitor() *Monitor {
13	m := newMonitor("", "")
14	m.SetOutput(300, 200)
15	m.SetMaxColumns(3)
16	for id := WindowID(1); id <= 3; id++ {
17		m.AddWindow(id)
18	}
19	m.Focus(1)
20	m.AddWindow(4)
21	m.Focus(0)
22	return m
23}
24
25func previewOf(t *testing.T, ps []Placement, id WindowID) Placement {
26	t.Helper()
27	for _, p := range ps {
28		if p.ID == id {
29			return p
30		}
31	}
32	t.Fatalf("no placement for %d", id)
33	return Placement{}
34}
35
36// Previews keep each tile's shape: width and height shrink by the same
37// factor, and the row fits the usable width.
38func TestOverviewPreviewsKeepAspect(t *testing.T) {
39	m := overviewMonitor()
40	normal := previewOf(t, m.Layout(), 2).Rect
41	m.ToggleOverview()
42	ps := m.Layout()
43	for id := WindowID(1); id <= 3; id++ {
44		p := previewOf(t, ps, id)
45		if p.Hidden || p.Preview <= 0 || p.Peek {
46			t.Fatalf("%d: %+v", id, p)
47		}
48		if p.Rect.X < 0 || p.Rect.X+p.Rect.W > 300 {
49			t.Fatalf("%d off screen: %+v", id, p.Rect)
50		}
51	}
52	p := previewOf(t, ps, 2)
53	got, want := float64(p.Rect.W)/float64(p.Rect.H), float64(normal.W)/float64(normal.H)
54	if math.Abs(got-want) > 0.02 {
55		t.Fatalf("aspect %v, want %v (%+v)", got, want, p.Rect)
56	}
57	if math.Abs(float64(p.Rect.W)-float64(normal.W)*p.Preview) > 1 {
58		t.Fatalf("width %d, zoom %v of %d", p.Rect.W, p.Preview, normal.W)
59	}
60	// Workspace 2 is below, dimmed and unfocused.
61	below := previewOf(t, ps, 4)
62	if below.Hidden || !below.Peek || below.Focused || below.Rect.Y <= p.Rect.Y+p.Rect.H {
63		t.Fatalf("below %+v, current %+v", below, p)
64	}
65}
66
67// Many columns shrink down to the floor, then the row scrolls to keep
68// the selected column on screen.
69func TestOverviewWideWorkspaceScrolls(t *testing.T) {
70	m := newMonitor("", "")
71	m.SetOutput(300, 200)
72	m.SetMaxColumns(1)
73	for id := WindowID(1); id <= 10; id++ {
74		m.AddWindow(id)
75	}
76	m.ToggleOverview()
77	ps := m.Layout()
78	p := previewOf(t, ps, 10)
79	if p.Preview != overviewMinZoom || !p.Focused || p.Rect.X < 0 || p.Rect.X+p.Rect.W > 300 {
80		t.Fatalf("%+v", p)
81	}
82}
83
84// h/l and j/k move the selection; Escape returns, Return keeps it.
85func TestOverviewNavigation(t *testing.T) {
86	m := overviewMonitor()
87	m.Current().FocusID(3)
88	m.ToggleOverview()
89	m.OverviewMove(-1, 0)
90	if id, _ := m.Focused(); id != 2 {
91		t.Fatalf("left: %d", id)
92	}
93	m.OverviewMove(0, 1)
94	if id, _ := m.Focused(); id != 4 || m.Active != 1 {
95		t.Fatalf("down: %d on %d", id, m.Active)
96	}
97	// The empty workspace below the last one is not entered.
98	m.OverviewMove(0, 1)
99	if m.Active != 1 {
100		t.Fatalf("entered empty workspace %d", m.Active)
101	}
102	m.CancelOverview()
103	if id, _ := m.Focused(); id != 3 || m.Active != 0 || m.ov.open {
104		t.Fatalf("cancel: %d on %d", id, m.Active)
105	}
106	m.ToggleOverview()
107	m.OverviewMove(0, 1)
108	m.ToggleOverview()
109	if id, _ := m.Focused(); id != 4 || m.Active != 1 || m.ov.open {
110		t.Fatalf("confirm: %d on %d", id, m.Active)
111	}
112}
113
114// A click picks the preview under it.
115func TestOverviewPick(t *testing.T) {
116	m := overviewMonitor()
117	m.ToggleOverview()
118	below := previewOf(t, m.Layout(), 4).Rect
119	id := overviewIn(m.Layout(), float64(below.X+1), float64(below.Y+1))
120	if id != 4 {
121		t.Fatalf("hit %d", id)
122	}
123	m.OverviewPick(id)
124	if f, _ := m.Focused(); f != 4 || m.ov.open {
125		t.Fatalf("pick: %d", f)
126	}
127}
128
129// gameMonitor has, on a 300x200 output, window 1 on workspace 1 and, on
130// workspace 2, a floating game 2 (fixed size) fullscreen; workspace 1 is
131// on screen.
132func gameMonitor(o Overflow) *Monitor {
133	m := newMonitor("", "")
134	m.SetOutput(300, 200)
135	m.SetOverflow(o)
136	m.AddWindow(1)
137	m.Focus(1)
138	m.AddFloating(2, 300, 200)
139	m.SetFullscreen(2, true)
140	m.Focus(0)
141	return m
142}
143
144// A floating game covering its workspace shows as that row's preview: the
145// row is not empty.
146func TestOverviewFullscreenFloat(t *testing.T) {
147	for _, o := range []Overflow{OverflowFixed, OverflowScroll} {
148		t.Run(string(o), func(t *testing.T) {
149			m := gameMonitor(o)
150			m.ToggleOverview()
151			ps := m.Layout()
152			cur, game := previewOf(t, ps, 1), previewOf(t, ps, 2)
153			if game.Hidden || !game.Peek || game.Focused || game.Preview <= 0 || game.Rect.Y <= cur.Rect.Y {
154				t.Fatalf("game %+v under row %+v", game, cur)
155			}
156			// Selected below, then picked: fullscreen and focused.
157			m.OverviewMove(0, 1)
158			if g := previewOf(t, m.Layout(), 2); !g.Focused || g.Peek {
159				t.Fatalf("selected game %+v", g)
160			}
161			m.OverviewMove(-1, 0) // nothing else on its row
162			m.ToggleOverview()
163			if f, _ := m.Focused(); f != 2 || m.ov.open || m.Active != 1 || !previewOf(t, m.Layout(), 2).Fullscreen {
164				t.Fatalf("confirm: %d on %d", f, m.Active)
165			}
166		})
167	}
168}
169
170// A click picks the game; Escape from its row returns where the overview
171// opened.
172func TestOverviewFullscreenFloatPickAndCancel(t *testing.T) {
173	m := gameMonitor(OverflowFixed)
174	m.ToggleOverview()
175	r := previewOf(t, m.Layout(), 2).Rect
176	if id := overviewIn(m.Layout(), float64(r.X+1), float64(r.Y+1)); id != 2 {
177		t.Fatalf("hit %d", id)
178	}
179	m.OverviewMove(0, 1)
180	m.CancelOverview()
181	if f, _ := m.Focused(); f != 1 || m.Active != 0 {
182		t.Fatalf("cancel: %d on %d", f, m.Active)
183	}
184	m.ToggleOverview()
185	m.OverviewPick(2)
186	if f, _ := m.Focused(); f != 2 || m.ov.open {
187		t.Fatalf("pick: %d", f)
188	}
189}
190
191// The overview opens over the game too: the game is the selected row,
192// the workspace above shows dimmed, and Escape returns to the game.
193func TestOverviewOpensOverFullscreenFloat(t *testing.T) {
194	m := gameMonitor(OverflowFixed)
195	m.Focus(1)
196	m.ToggleOverview()
197	if !m.ov.open {
198		t.Fatal("overview did not open")
199	}
200	ps := m.Layout()
201	game, above := previewOf(t, ps, 2), previewOf(t, ps, 1)
202	if !game.Focused || game.Peek || game.Preview <= 0 || !above.Peek || above.Rect.Y >= game.Rect.Y {
203		t.Fatalf("game %+v, above %+v", game, above)
204	}
205	m.CancelOverview()
206	if f, _ := m.Focused(); f != 2 || m.Active != 1 || m.ov.open || !previewOf(t, m.Layout(), 2).Fullscreen {
207		t.Fatalf("cancel: %d on %d", f, m.Active)
208	}
209}
210
211// A tiled fullscreen window (fixed overflow) is its row's front card:
212// Return keeps it fullscreen; the column it hides is behind, and picking
213// it leaves fullscreen.
214func TestOverviewOpensOverFullscreenTile(t *testing.T) {
215	open := func() *Monitor {
216		m := newMonitor("", "")
217		m.SetOutput(300, 200)
218		m.SetOverflow(OverflowFixed)
219		m.AddWindow(1)
220		m.AddWindow(2)
221		m.ToggleFullscreen()
222		m.ToggleOverview()
223		return m
224	}
225	m := open()
226	game := previewOf(t, m.Layout(), 2)
227	if !m.ov.open || !game.Focused || game.Peek || !game.Fullscreen {
228		t.Fatalf("game %+v", game)
229	}
230	m.ToggleOverview()
231	if f, _ := m.Focused(); f != 2 || m.Active != 0 || !previewOf(t, m.Layout(), 2).Fullscreen {
232		t.Fatalf("confirm: %d on %d", f, m.Active)
233	}
234	m = open()
235	m.OverviewMove(-1, 0)
236	m.ToggleOverview()
237	if f, _ := m.Focused(); f != 1 || m.Current().fullscreen != 0 {
238		t.Fatalf("behind: focused %d, fullscreen %d", f, m.Current().fullscreen)
239	}
240}
241
242// Under scroll overflow a fullscreen column does not pin the row: another
243// column can be selected and picked, as scrolling would on screen.
244func TestOverviewOverScrollFullscreenPicksColumn(t *testing.T) {
245	m := newMonitor("", "")
246	m.SetOutput(300, 200)
247	m.SetOverflow(OverflowScroll)
248	m.AddWindow(1)
249	m.AddWindow(2)
250	m.SetFullscreen(2, true)
251	m.ToggleOverview()
252	if p := previewOf(t, m.Layout(), 1); p.Hidden {
253		t.Fatalf("column 1 hidden: %+v", p)
254	}
255	m.OverviewMove(-1, 0)
256	m.ToggleOverview()
257	if f, _ := m.Focused(); f != 1 {
258		t.Fatalf("focused %d", f)
259	}
260}
261
262// A neighbor row with a fullscreen game shows it in front, the columns it
263// hides behind; a dialog under it stays hidden, as for any small float.
264func TestOverviewFullscreenFloatRow(t *testing.T) {
265	m := gameMonitor(OverflowScroll)
266	m.Focus(1)
267	m.AddWindow(5)
268	m.Current().FocusID(2) // back on the game, over column 5
269	m.AddWindow(3)
270	m.AddFloating(4, 50, 50)
271	m.Focus(0)
272	m.ToggleOverview()
273	ps := m.Layout()
274	for _, id := range []WindowID{2, 3, 5} {
275		if p := previewOf(t, ps, id); p.Hidden || !p.Peek {
276			t.Fatalf("%d: %+v", id, p)
277		}
278	}
279	if p := previewOf(t, ps, 4); !p.Hidden {
280		t.Fatalf("dialog shown: %+v", p)
281	}
282	if g := previewOf(t, ps, 2); !g.Fullscreen {
283		t.Fatalf("game %+v", g)
284	}
285}
286
287// pileMonitor is overviewMonitor with 5, 6 and 7 stashed on workspace 1
288// (7 selected) and the stash hidden.
289func pileMonitor() *Monitor {
290	m := overviewMonitor()
291	w := m.Current()
292	for id := WindowID(5); id <= 7; id++ {
293		w.AddWindow(id)
294		w.FocusID(id)
295		w.ToggleWindowStash()
296	}
297	w.ToggleStashVisible()
298	w.FocusID(1)
299	return m
300}
301
302// The stash shows as a pile on the left of its row, even hidden: its
303// selected window in front, the others behind, dimmed. The row starts
304// right of it.
305func TestOverviewPileLayout(t *testing.T) {
306	m := pileMonitor()
307	m.ToggleOverview()
308	ps := m.Layout()
309	front, back := previewOf(t, ps, 7), previewOf(t, ps, 5)
310	if front.Hidden || front.Peek || front.Focused || front.Preview != overviewCardZoom {
311		t.Fatalf("front %+v", front)
312	}
313	if back.Hidden || !back.Peek || back.Rect.X <= front.Rect.X || back.Rect.Y >= front.Rect.Y {
314		t.Fatalf("back %+v, front %+v", back, front)
315	}
316	first := previewOf(t, ps, 1)
317	if !first.Focused || first.Rect.X < back.Rect.X+back.Rect.W {
318		t.Fatalf("row %+v overlaps the pile %+v", first.Rect, back.Rect)
319	}
320	// Front last: drawn and hit on top.
321	idx := func(id WindowID) int {
322		for i, p := range ps {
323			if p.ID == id {
324				return i
325			}
326		}
327		return -1
328	}
329	if idx(7) < idx(5) || idx(7) < idx(6) {
330		t.Fatal("front card drawn under the others")
331	}
332}
333
334// A stash pile does not shift a row that fits beside it: the row stays
335// centred like the same workspace without a stash.
336func TestOverviewPileKeepsRowCentred(t *testing.T) {
337	m := newMonitor("", "")
338	m.SetOutput(3000, 1200)
339	m.SetOverflow(OverflowFixed)
340	m.SetMaxColumns(3)
341	for id := WindowID(1); id <= 3; id++ {
342		m.AddWindow(id)
343	}
344	m.ToggleOverview()
345	want := previewOf(t, m.Layout(), 1).Rect
346	m.CancelOverview()
347	w := m.Current()
348	w.AddWindow(4)
349	w.FocusID(4)
350	w.ToggleWindowStash()
351	w.FocusID(1)
352	m.ToggleOverview()
353	if got := previewOf(t, m.Layout(), 1).Rect; got != want {
354		t.Fatalf("row with a stash at %+v, without at %+v", got, want)
355	}
356}
357
358// Left enters the stash pile, up/down browse it without changing rows,
359// and right leaves it immediately. Confirm shows the chosen stash window.
360func TestOverviewPileNavigation(t *testing.T) {
361	m := pileMonitor()
362	w := m.Current()
363	m.ToggleOverview()
364	m.OverviewMove(-1, 0)
365	if at := m.cardAt(w); at != 2 {
366		t.Fatalf("entered at %d", at)
367	}
368	if f := previewOf(t, m.Layout(), 7); !f.Focused {
369		t.Fatalf("front card not selected: %+v", f)
370	}
371	if previewOf(t, m.Layout(), 1).Focused {
372		t.Fatal("column still selected")
373	}
374	m.OverviewMove(-1, 0) // left stays on the selected stash card
375	if at := m.cardAt(w); at != 2 {
376		t.Fatalf("left changed stash selection to %d", at)
377	}
378	m.OverviewMove(0, 1) // stops at the last entry, not the next workspace
379	if m.Current() != w || m.cardAt(w) != 2 {
380		t.Fatal("down escaped the stash pile")
381	}
382	m.OverviewMove(0, -1)
383	m.OverviewMove(0, -1)
384	m.OverviewMove(0, -1) // stops at the first entry
385	if at := m.cardAt(w); at != 0 || m.Current() != w {
386		t.Fatalf("up end %d, workspace changed %v", at, m.Current() != w)
387	}
388	m.OverviewMove(0, 1)
389	m.ToggleOverview()
390	if f, _ := m.Focused(); f != 6 || w.stashHidden || m.ov.open {
391		t.Fatalf("confirm on %d, hidden %v", f, w.stashHidden)
392	}
393	// Right returns to column 1 from any stash card.
394	w.FocusID(1)
395	m.ToggleOverview()
396	m.OverviewMove(-1, 0)
397	if at := m.cardAt(w); at != 1 {
398		t.Fatalf("entered at %d", at)
399	}
400	m.OverviewMove(1, 0)
401	if m.cardAt(w) >= 0 {
402		t.Fatal("still in the pile")
403	}
404	if f, _ := m.Focused(); f != 1 {
405		t.Fatalf("back on %d", f)
406	}
407	// Escape leaves the stash as it was.
408	m.OverviewMove(-1, 0)
409	m.CancelOverview()
410	if f, _ := m.Focused(); f != 1 {
411		t.Fatalf("cancel on %d", f)
412	}
413}
414
415// A click on a card shows the stash on it.
416func TestOverviewPilePick(t *testing.T) {
417	m := pileMonitor()
418	m.ToggleOverview()
419	r := previewOf(t, m.Layout(), 7).Rect
420	id := overviewIn(m.Layout(), float64(r.X+1), float64(r.Y+r.H-1))
421	if id != 7 {
422		t.Fatalf("hit %d", id)
423	}
424	m.OverviewPick(id)
425	if f, _ := m.Focused(); f != 7 || m.Current().stashHidden {
426		t.Fatalf("pick on %d", f)
427	}
428}
429
430// A workspace with only stashed windows opens with the pile selected, and
431// its neighbor's pile is dimmed and unselected.
432func TestOverviewPileOnly(t *testing.T) {
433	m := newMonitor("", "")
434	m.SetOutput(300, 200)
435	m.AddWindow(1)
436	w := m.Current()
437	w.ToggleWindowStash()
438	m.ToggleOverview()
439	if p := previewOf(t, m.Layout(), 1); !p.Focused || p.Hidden {
440		t.Fatalf("%+v", p)
441	}
442	m.CancelOverview()
443	m.Focus(1)
444	m.AddWindow(2)
445	m.Focus(0)
446	m.ToggleOverview()
447	m.OverviewMove(0, 1)
448	if m.Current() != w || m.cardAt(w) != 0 {
449		t.Fatal("vertical navigation escaped a stash-only workspace")
450	}
451	m.OverviewMove(1, 0)
452	if m.cardAt(w) >= 0 || m.ov.selected != 0 {
453		t.Fatal("right did not deselect the stash-only pile")
454	}
455	m.OverviewMove(0, 1)
456	if m.Current() == w {
457		t.Fatal("vertical navigation after leaving the pile did not change workspace")
458	}
459	m.CancelOverview()
460	m.Focus(1)
461	m.ToggleOverview()
462	if p := previewOf(t, m.Layout(), 1); p.Focused || !p.Peek || p.Hidden {
463		t.Fatalf("neighbor pile %+v", p)
464	}
465}
466
467// Fixed overflow previews use the actual buffer geometry, including the
468// spiral and window stacks, rather than laying full-height columns in a row.
469func TestOverviewFixedOverflowGeometry(t *testing.T) {
470	for _, expanded := range []bool{false, true} {
471		t.Run(map[bool]string{false: "spiral", true: "expanded"}[expanded], func(t *testing.T) {
472			m := newMonitor("", "")
473			m.SetOutput(600, 400)
474			m.SetOverflow(OverflowFixed)
475			m.SetMaxColumns(3)
476			for id := WindowID(1); id <= 5; id++ {
477				m.AddWindow(id)
478			}
479			w := m.Current()
480			if expanded {
481				w.Columns[2].Expanded = true
482			}
483			w.Columns[3].Windows = append(w.Columns[3].Windows, 6)
484			w.Columns[3].Focus = 1
485			w.FocusID(6)
486			screen := w.Layout()
487			m.ToggleOverview()
488			if _, span, _ := w.previewTiles(); span != w.Usable.W {
489				t.Fatalf("span %d, usable %d", span, w.Usable.W)
490			}
491			ps := m.Layout()
492			z := previewOf(t, ps, 6).Preview
493			scale := func(v int) int { return int(math.Round(float64(v) * z)) }
494			anchor, previewAnchor := previewOf(t, screen, 1).Rect, previewOf(t, ps, 1).Rect
495			for _, id := range []WindowID{1, 2, 3, 4, 5, 6} {
496				r, p := previewOf(t, screen, id).Rect, previewOf(t, ps, id)
497				if p.Hidden || p.Preview != z || math.Abs(float64(p.Rect.W-scale(r.W))) > 1 || math.Abs(float64(p.Rect.H-scale(r.H))) > 1 || math.Abs(float64(p.Rect.X-previewAnchor.X-scale(r.X-anchor.X))) > 1 || math.Abs(float64(p.Rect.Y-previewAnchor.Y-scale(r.Y-anchor.Y))) > 1 {
498					t.Fatalf("%d: screen %+v preview %+v anchor %+v %+v", id, r, p, anchor, previewAnchor)
499				}
500				if p.Focused != (id == 6) {
501					t.Fatalf("%d focus: %+v", id, p)
502				}
503				for _, other := range []WindowID{1, 2, 3, 4, 5, 6} {
504					if other > id && p.Rect.Overlaps(previewOf(t, ps, other).Rect) {
505						t.Fatalf("%d overlaps %d", id, other)
506					}
507				}
508				if p.Rect.W > 0 && p.Rect.H > 0 && overviewIn(m.Layout(), float64(p.Rect.X+p.Rect.W/2), float64(p.Rect.Y+p.Rect.H/2)) != id {
509					t.Fatalf("%d not hit at %+v", id, p.Rect)
510				}
511			}
512			if len(overviewOutline(ps, 2)) != 4 {
513				t.Fatal("missing selection outline")
514			}
515			m.OverviewMove(-1, 0)
516			if id, _ := m.Focused(); id != 3 {
517				t.Fatalf("left selected %d", id)
518			}
519			m.OverviewMove(1, 0)
520			if id, _ := m.Focused(); id != 6 {
521				t.Fatalf("right selected %d", id)
522			}
523		})
524	}
525}
526
527func TestOverviewFixedFullWidthShowsHiddenColumnsCard(t *testing.T) {
528	m := newMonitor("", "")
529	m.SetOutput(600, 400)
530	m.SetOverflow(OverflowFixed)
531	m.SetMaxColumns(3)
532	for id := WindowID(1); id <= 5; id++ {
533		m.AddWindow(id)
534	}
535	w := m.Current()
536	w.ToggleFullWidth()
537	m.ToggleOverview()
538	if p := previewOf(t, m.Layout(), 5); p.Hidden || p.Peek || !p.Focused {
539		t.Fatalf("front %+v", p)
540	}
541	for _, id := range []WindowID{1, 2} {
542		if p := previewOf(t, m.Layout(), id); p.Hidden || !p.Peek {
543			t.Fatalf("peek %d %+v", id, p)
544		}
545	}
546	m.OverviewMove(0, -1)
547	m.ToggleOverview()
548	if id, _ := w.Focused(); id != 1 || !w.Columns[0].FullWidth || w.Columns[4].FullWidth {
549		t.Fatalf("transfer %d %+v", id, w.Columns)
550	}
551}
552
553// The selected card follows its window: removing an earlier stash entry
554// keeps it, removing the card itself drops the selection to the column.
555// The card is what Focused reports, and close-window closes it.
556func TestOverviewCardFollowsWindow(t *testing.T) {
557	m := pileMonitor()
558	w := m.Current()
559	m.ToggleOverview()
560	m.OverviewMove(-1, 0) // on 7
561	if id, _ := m.Focused(); id != 7 {
562		t.Fatalf("focused %d, want the card", id)
563	}
564	if e := m.Apply(ActionCloseWindow); e.Close != 7 {
565		t.Fatalf("close %d, want the card", e.Close)
566	}
567	m.RemoveWindow(5)
568	if id, _ := m.Focused(); id != 7 {
569		t.Fatalf("after removing 5: %d", id)
570	}
571	m.RemoveWindow(7)
572	if id, _ := m.Focused(); id != 1 || m.cardAt(w) >= 0 {
573		t.Fatalf("after removing the card: %d", id)
574	}
575}
576
577// A workspace switched by a bind does not inherit the card selection.
578func TestOverviewCardAfterBindSwitch(t *testing.T) {
579	m := pileMonitor()
580	m.Workspaces[1].AddWindow(8)
581	m.Workspaces[1].FocusID(8)
582	m.Workspaces[1].ToggleWindowStash()
583	m.Workspaces[1].ToggleStashVisible()
584	m.ToggleOverview()
585	m.OverviewMove(-1, 0)
586	m.FocusNumber(2)
587	if id, _ := m.Focused(); id != 4 {
588		t.Fatalf("focused %d after the switch", id)
589	}
590	m.ToggleOverview()
591	if !m.Workspaces[1].stashHidden {
592		t.Fatal("the other workspace's stash was shown")
593	}
594}
595
596// Picking a card wins over another stashed window's pending fullscreen.
597func TestOverviewCardOverPendingFullscreen(t *testing.T) {
598	m := pileMonitor()
599	w := m.Current()
600	w.SetFullscreen(5, true) // hidden stash: pending
601	m.ToggleOverview()
602	m.OverviewMove(-1, 0)
603	m.ToggleOverview()
604	if id, _ := m.Focused(); id != 7 || w.fullscreen != 0 {
605		t.Fatalf("focused %d, fullscreen %d", id, w.fullscreen)
606	}
607}
608
609// On a tiny output the pile leaves room for the row.
610func TestOverviewPileTinyOutput(t *testing.T) {
611	m := pileMonitor()
612	m.SetOutput(30, 20)
613	m.ToggleOverview()
614	for _, p := range m.Layout() {
615		if !p.Hidden && (p.Rect.W < 0 || p.Rect.X < 0 || p.Rect.X > 30) {
616			t.Fatalf("%+v", p)
617		}
618	}
619	if w := m.Current(); w.pileWidth() > 10 {
620		t.Fatalf("pile %d of 30", w.pileWidth())
621	}
622}
623
624// The overview opens on what has the focus: a shown stash's card, else
625// the column under a focused native float (hidden in the overview), and
626// Return keeps it. Escape gives the float its focus back.
627func TestOverviewOpensOnFocus(t *testing.T) {
628	m := pileMonitor()
629	w := m.Current()
630	w.ToggleStashVisible()
631	m.ToggleOverview()
632	if at := m.cardAt(w); at != w.stashAt {
633		t.Fatalf("opened on card %d, want the stash's %d", at, w.stashAt)
634	}
635	m.ToggleOverview()
636
637	m = overviewMonitor()
638	w = m.Current()
639	col, _ := w.Focused()
640	w.AddFloating(9, 10, 10)
641	w.FocusID(9)
642	m.ToggleOverview()
643	if f, _ := m.Focused(); f != col || !previewOf(t, m.Layout(), col).Focused {
644		t.Fatalf("opened on %d, want column %d", f, col)
645	}
646	m.CancelOverview()
647	if f, _ := m.Focused(); f != 9 {
648		t.Fatalf("escape on %d, want the float", f)
649	}
650	w.FocusID(9)
651	m.ToggleOverview()
652	m.ToggleOverview()
653	if f, _ := m.Focused(); f != col {
654		t.Fatalf("return on %d, want column %d", f, col)
655	}
656}
657
658// Scrolling adds up to steps: what is left when the overview closes does
659// not count when it opens again, and a wheel frame of two notches moves
660// twice.
661func TestOverviewScrollSteps(t *testing.T) {
662	m := overviewMonitor()
663	w := m.Current()
664	finger := func(dx float64) ports.PointerAxis {
665		return ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: dx}}
666	}
667	m.ToggleOverview()
668	start, _ := m.Focused()
669	if m.overviewScroll(finger(-50)) {
670		t.Fatal("a partial scroll moved")
671	}
672	m.CancelOverview()
673	m.ToggleOverview()
674	if m.overviewScroll(finger(-50)) {
675		t.Fatal("the last session's scroll carried over")
676	}
677	m.CancelOverview()
678	w.FocusID(3)
679	m.ToggleOverview()
680	if !m.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Horizontal: ports.ScrollAxis{Set: true, V120: -240}}) {
681		t.Fatal("wheel did not move")
682	}
683	if f, _ := m.Focused(); f != 1 {
684		t.Fatalf("two notches from 3 landed on %d, want 1 (started on %d)", f, start)
685	}
686}
687
688// A two-finger scroll sideways steps to the next column after
689// overviewScrollStep, then once per overviewScrollRepeat, and stays on
690// columns; up or down it steps once until the fingers lift. Reopening the
691// overview, a wheel notch and continuous scrolling are not held by it.
692func TestOverviewFingerScroll(t *testing.T) {
693	finger := func(dx, dy float64) ports.PointerAxis {
694		return ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: dx}, Vertical: ports.ScrollAxis{Set: true, Value: dy}}
695	}
696	m := overviewMonitor()
697	focused := func() WindowID { f, _ := m.Focused(); return f }
698	m.Current().FocusID(3)
699	m.ToggleOverview()
700	// 70: one column; 100 more is under the longer repeat step.
701	if !m.overviewScroll(finger(-70, 0)) || focused() != 2 {
702		t.Fatalf("first step on %d", focused())
703	}
704	if m.overviewScroll(finger(-100, 0)) {
705		t.Fatal("stepped again before overviewScrollRepeat")
706	}
707	// The fingers drift down: still on columns, no row step.
708	if m.overviewScroll(finger(0, 200)) {
709		t.Fatal("a sideways scroll stepped down")
710	}
711	if !m.overviewScroll(finger(-50, 0)) || focused() != 1 {
712		t.Fatalf("long scroll on %d, want 1", focused())
713	}
714	// The fingers still rest: a wheel notch steps one column as always.
715	if !m.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Horizontal: ports.ScrollAxis{Set: true, V120: 120}}) || focused() != 2 {
716		t.Fatalf("wheel after a sideways scroll on %d, want 2", focused())
717	}
718	// Once lifted, the next scroll starts with the short step again, and
719	// one large frame takes the short step then a repeat.
720	c := &Core{screens: []*screen{{mon: m}}}
721	c.scrollStop(ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}})
722	if !m.overviewScroll(finger(220, 0)) || focused() != 3 {
723		t.Fatalf("large frame on %d, want 3", focused())
724	}
725	// Reopening forgets a sideways scroll left without its stop.
726	m.CancelOverview()
727	m.Current().FocusID(3)
728	m.ToggleOverview()
729	if !m.overviewScroll(finger(-70, 0)) || focused() != 2 {
730		t.Fatalf("reopened sideways scroll on %d, want 2", focused())
731	}
732
733	// Vertical: one step per scroll, through the stack's cards.
734	sm := stackMonitor()
735	sm.ToggleOverview()
736	before, _ := sm.Focused()
737	if !sm.overviewScroll(finger(0, 70)) {
738		t.Fatal("first vertical step")
739	}
740	after, _ := sm.Focused()
741	if after == before {
742		t.Fatal("vertical step selected nothing new")
743	}
744	if sm.overviewScroll(finger(0, 400)) {
745		t.Fatal("the same vertical scroll stepped twice")
746	}
747	if !sm.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, V120: -120}}) {
748		t.Fatal("wheel held by the finger scroll")
749	}
750	// A sideways finger scroll does not hold a vertical wheel notch.
751	sm.CancelOverview()
752	sm.ToggleOverview()
753	sm.overviewScroll(finger(70, 0))
754	if !sm.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, V120: 120}}) {
755		t.Fatal("vertical wheel held by a sideways finger scroll")
756	}
757	sm.CancelOverview()
758	sm.ToggleOverview()
759	if !sm.overviewScroll(finger(0, 70)) {
760		t.Fatal("reopened overview still held the old scroll")
761	}
762	cont := ports.PointerAxis{Source: ports.AxisContinuous, Vertical: ports.ScrollAxis{Set: true, Value: -70}}
763	if !sm.overviewScroll(cont) {
764		t.Fatal("continuous scroll held by the finger scroll")
765	}
766}

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 23

Tags 10

↑↓ moveenter openesc close