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 _, cells := range []bool{false, true} {
471		t.Run(map[bool]string{false: "spiral", true: "cells ignored"}[cells], 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 cells {
481				// The spiral ignores cells: the preview must still match.
482				w.Columns[0].Cells, w.Columns[2].Cells = 1, 3
483			}
484			w.Columns[3].Windows = append(w.Columns[3].Windows, 6)
485			w.Columns[3].Focus = 1
486			w.FocusID(6)
487			screen := w.Layout()
488			m.ToggleOverview()
489			if _, span, _ := w.previewTiles(); span != w.Usable.W {
490				t.Fatalf("span %d, usable %d", span, w.Usable.W)
491			}
492			ps := m.Layout()
493			z := previewOf(t, ps, 6).Preview
494			scale := func(v int) int { return int(math.Round(float64(v) * z)) }
495			anchor, previewAnchor := previewOf(t, screen, 1).Rect, previewOf(t, ps, 1).Rect
496			for _, id := range []WindowID{1, 2, 3, 4, 5, 6} {
497				r, p := previewOf(t, screen, id).Rect, previewOf(t, ps, id)
498				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 {
499					t.Fatalf("%d: screen %+v preview %+v anchor %+v %+v", id, r, p, anchor, previewAnchor)
500				}
501				if p.Focused != (id == 6) {
502					t.Fatalf("%d focus: %+v", id, p)
503				}
504				for _, other := range []WindowID{1, 2, 3, 4, 5, 6} {
505					if other > id && p.Rect.Overlaps(previewOf(t, ps, other).Rect) {
506						t.Fatalf("%d overlaps %d", id, other)
507					}
508				}
509				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 {
510					t.Fatalf("%d not hit at %+v", id, p.Rect)
511				}
512			}
513			if len(overviewOutline(ps, 2)) != 4 {
514				t.Fatal("missing selection outline")
515			}
516			m.OverviewMove(-1, 0)
517			if id, _ := m.Focused(); id != 3 {
518				t.Fatalf("left selected %d", id)
519			}
520			m.OverviewMove(1, 0)
521			if id, _ := m.Focused(); id != 6 {
522				t.Fatalf("right selected %d", id)
523			}
524		})
525	}
526}
527
528func TestOverviewFixedFullWidthShowsHiddenColumnsCard(t *testing.T) {
529	m := newMonitor("", "")
530	m.SetOutput(600, 400)
531	m.SetOverflow(OverflowFixed)
532	m.SetMaxColumns(3)
533	for id := WindowID(1); id <= 5; id++ {
534		m.AddWindow(id)
535	}
536	w := m.Current()
537	w.ToggleFullWidth()
538	m.ToggleOverview()
539	if p := previewOf(t, m.Layout(), 5); p.Hidden || p.Peek || !p.Focused {
540		t.Fatalf("front %+v", p)
541	}
542	for _, id := range []WindowID{1, 2} {
543		if p := previewOf(t, m.Layout(), id); p.Hidden || !p.Peek {
544			t.Fatalf("peek %d %+v", id, p)
545		}
546	}
547	m.OverviewMove(0, -1)
548	m.ToggleOverview()
549	if id, _ := w.Focused(); id != 1 || !w.Columns[0].FullWidth || w.Columns[4].FullWidth {
550		t.Fatalf("transfer %d %+v", id, w.Columns)
551	}
552}
553
554// The selected card follows its window: removing an earlier stash entry
555// keeps it, removing the card itself drops the selection to the column.
556// The card is what Focused reports, and close-window closes it.
557func TestOverviewCardFollowsWindow(t *testing.T) {
558	m := pileMonitor()
559	w := m.Current()
560	m.ToggleOverview()
561	m.OverviewMove(-1, 0) // on 7
562	if id, _ := m.Focused(); id != 7 {
563		t.Fatalf("focused %d, want the card", id)
564	}
565	if e := m.Apply(ActionCloseWindow); e.Close != 7 {
566		t.Fatalf("close %d, want the card", e.Close)
567	}
568	m.RemoveWindow(5)
569	if id, _ := m.Focused(); id != 7 {
570		t.Fatalf("after removing 5: %d", id)
571	}
572	m.RemoveWindow(7)
573	if id, _ := m.Focused(); id != 1 || m.cardAt(w) >= 0 {
574		t.Fatalf("after removing the card: %d", id)
575	}
576}
577
578// A workspace switched by a bind does not inherit the card selection.
579func TestOverviewCardAfterBindSwitch(t *testing.T) {
580	m := pileMonitor()
581	m.Workspaces[1].AddWindow(8)
582	m.Workspaces[1].FocusID(8)
583	m.Workspaces[1].ToggleWindowStash()
584	m.Workspaces[1].ToggleStashVisible()
585	m.ToggleOverview()
586	m.OverviewMove(-1, 0)
587	m.FocusNumber(2)
588	if id, _ := m.Focused(); id != 4 {
589		t.Fatalf("focused %d after the switch", id)
590	}
591	m.ToggleOverview()
592	if !m.Workspaces[1].stashHidden {
593		t.Fatal("the other workspace's stash was shown")
594	}
595}
596
597// Picking a card wins over another stashed window's pending fullscreen.
598func TestOverviewCardOverPendingFullscreen(t *testing.T) {
599	m := pileMonitor()
600	w := m.Current()
601	w.SetFullscreen(5, true) // hidden stash: pending
602	m.ToggleOverview()
603	m.OverviewMove(-1, 0)
604	m.ToggleOverview()
605	if id, _ := m.Focused(); id != 7 || w.fullscreen != 0 {
606		t.Fatalf("focused %d, fullscreen %d", id, w.fullscreen)
607	}
608}
609
610// On a tiny output the pile leaves room for the row.
611func TestOverviewPileTinyOutput(t *testing.T) {
612	m := pileMonitor()
613	m.SetOutput(30, 20)
614	m.ToggleOverview()
615	for _, p := range m.Layout() {
616		if !p.Hidden && (p.Rect.W < 0 || p.Rect.X < 0 || p.Rect.X > 30) {
617			t.Fatalf("%+v", p)
618		}
619	}
620	if w := m.Current(); w.pileWidth() > 10 {
621		t.Fatalf("pile %d of 30", w.pileWidth())
622	}
623}
624
625// The overview opens on what has the focus: a shown stash's card, else
626// the column under a focused native float (hidden in the overview), and
627// Return keeps it. Escape gives the float its focus back.
628func TestOverviewOpensOnFocus(t *testing.T) {
629	m := pileMonitor()
630	w := m.Current()
631	w.ToggleStashVisible()
632	m.ToggleOverview()
633	if at := m.cardAt(w); at != w.stashAt {
634		t.Fatalf("opened on card %d, want the stash's %d", at, w.stashAt)
635	}
636	m.ToggleOverview()
637
638	m = overviewMonitor()
639	w = m.Current()
640	col, _ := w.Focused()
641	w.AddFloating(9, 10, 10)
642	w.FocusID(9)
643	m.ToggleOverview()
644	if f, _ := m.Focused(); f != col || !previewOf(t, m.Layout(), col).Focused {
645		t.Fatalf("opened on %d, want column %d", f, col)
646	}
647	m.CancelOverview()
648	if f, _ := m.Focused(); f != 9 {
649		t.Fatalf("escape on %d, want the float", f)
650	}
651	w.FocusID(9)
652	m.ToggleOverview()
653	m.ToggleOverview()
654	if f, _ := m.Focused(); f != col {
655		t.Fatalf("return on %d, want column %d", f, col)
656	}
657}
658
659// Scrolling adds up to steps: what is left when the overview closes does
660// not count when it opens again, and a wheel frame of two notches moves
661// twice.
662func TestOverviewScrollSteps(t *testing.T) {
663	m := overviewMonitor()
664	w := m.Current()
665	finger := func(dx float64) ports.PointerAxis {
666		return ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: dx}}
667	}
668	m.ToggleOverview()
669	start, _ := m.Focused()
670	if m.overviewScroll(finger(-50)) {
671		t.Fatal("a partial scroll moved")
672	}
673	m.CancelOverview()
674	m.ToggleOverview()
675	if m.overviewScroll(finger(-50)) {
676		t.Fatal("the last session's scroll carried over")
677	}
678	m.CancelOverview()
679	w.FocusID(3)
680	m.ToggleOverview()
681	if !m.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Horizontal: ports.ScrollAxis{Set: true, V120: -240}}) {
682		t.Fatal("wheel did not move")
683	}
684	if f, _ := m.Focused(); f != 1 {
685		t.Fatalf("two notches from 3 landed on %d, want 1 (started on %d)", f, start)
686	}
687}
688
689// A two-finger scroll sideways steps to the next column after
690// overviewScrollStep, then once per overviewScrollRepeat, and stays on
691// columns; up or down it steps once until the fingers lift. Reopening the
692// overview, a wheel notch and continuous scrolling are not held by it.
693func TestOverviewFingerScroll(t *testing.T) {
694	finger := func(dx, dy float64) ports.PointerAxis {
695		return ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Value: dx}, Vertical: ports.ScrollAxis{Set: true, Value: dy}}
696	}
697	m := overviewMonitor()
698	focused := func() WindowID { f, _ := m.Focused(); return f }
699	m.Current().FocusID(3)
700	m.ToggleOverview()
701	// 70: one column; 100 more is under the longer repeat step.
702	if !m.overviewScroll(finger(-70, 0)) || focused() != 2 {
703		t.Fatalf("first step on %d", focused())
704	}
705	if m.overviewScroll(finger(-100, 0)) {
706		t.Fatal("stepped again before overviewScrollRepeat")
707	}
708	// The fingers drift down: still on columns, no row step.
709	if m.overviewScroll(finger(0, 200)) {
710		t.Fatal("a sideways scroll stepped down")
711	}
712	if !m.overviewScroll(finger(-50, 0)) || focused() != 1 {
713		t.Fatalf("long scroll on %d, want 1", focused())
714	}
715	// The fingers still rest: a wheel notch steps one column as always.
716	if !m.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Horizontal: ports.ScrollAxis{Set: true, V120: 120}}) || focused() != 2 {
717		t.Fatalf("wheel after a sideways scroll on %d, want 2", focused())
718	}
719	// Once lifted, the next scroll starts with the short step again, and
720	// one large frame takes the short step then a repeat.
721	c := &Core{screens: []*screen{{mon: m}}}
722	c.scrollStop(ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}})
723	if !m.overviewScroll(finger(220, 0)) || focused() != 3 {
724		t.Fatalf("large frame on %d, want 3", focused())
725	}
726	// Reopening forgets a sideways scroll left without its stop.
727	m.CancelOverview()
728	m.Current().FocusID(3)
729	m.ToggleOverview()
730	if !m.overviewScroll(finger(-70, 0)) || focused() != 2 {
731		t.Fatalf("reopened sideways scroll on %d, want 2", focused())
732	}
733
734	// Vertical: one step per scroll, through the stack's cards.
735	sm := stackMonitor()
736	sm.ToggleOverview()
737	before, _ := sm.Focused()
738	if !sm.overviewScroll(finger(0, 70)) {
739		t.Fatal("first vertical step")
740	}
741	after, _ := sm.Focused()
742	if after == before {
743		t.Fatal("vertical step selected nothing new")
744	}
745	if sm.overviewScroll(finger(0, 400)) {
746		t.Fatal("the same vertical scroll stepped twice")
747	}
748	if !sm.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, V120: -120}}) {
749		t.Fatal("wheel held by the finger scroll")
750	}
751	// A sideways finger scroll does not hold a vertical wheel notch.
752	sm.CancelOverview()
753	sm.ToggleOverview()
754	sm.overviewScroll(finger(70, 0))
755	if !sm.overviewScroll(ports.PointerAxis{Source: ports.AxisWheel, Vertical: ports.ScrollAxis{Set: true, V120: 120}}) {
756		t.Fatal("vertical wheel held by a sideways finger scroll")
757	}
758	sm.CancelOverview()
759	sm.ToggleOverview()
760	if !sm.overviewScroll(finger(0, 70)) {
761		t.Fatal("reopened overview still held the old scroll")
762	}
763	cont := ports.PointerAxis{Source: ports.AxisContinuous, Vertical: ports.ScrollAxis{Set: true, Value: -70}}
764	if !sm.overviewScroll(cont) {
765		t.Fatal("continuous scroll held by the finger scroll")
766	}
767}

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