v0.2.0 13c6a90

layout_test.go RawHistoryFolder

1package core
2
3import (
4	"math"
5	"reflect"
6	"testing"
7
8	"github.com/bnema/neferwl/internal/ports"
9)
10
11func workspace() *Workspace {
12	w := &Workspace{}
13	w.SetOutput(100, 81)
14	w.SetGaps(5)
15	w.SetMaxColumns(2)
16	return w
17}
18func TestWidths(t *testing.T) {
19	for _, tc := range []struct {
20		s      string
21		ok     bool
22		pixels int
23	}{{"1", true, 0}, {"1/2", true, 0}, {"25px", true, 25}, {"2/1", false, 0}, {"0px", false, 0}, {"1/0", false, 0}, {"abc", false, 0}} {
24		v, e := ParseWidth(tc.s)
25		if (e == nil) != tc.ok || (e == nil && v.Pixels != tc.pixels) {
26			t.Errorf("%q: %v %v", tc.s, v, e)
27		}
28	}
29	if n := (Width{Num: 1, Den: 2}).Resolve(100, 5); n != 43 {
30		t.Fatal(n)
31	}
32	if n := (Width{Pixels: 200}).Resolve(100, 5); n != 90 {
33		t.Fatal(n)
34	}
35}
36func TestOperations(t *testing.T) {
37	w := workspace()
38	w.FocusColumn(1)
39	w.FocusWindow(-1)
40	w.MoveColumn(1)
41	w.CycleWidth()
42	w.ToggleFullscreen()
43	w.RemoveWindow(1)
44	if _, ok := w.Focused(); ok {
45		t.Fatal("empty focus")
46	}
47	w.AddWindow(1)
48	w.AddWindow(2)
49	w.AddWindow(3)
50	// Three auto columns, two visible: each (100-3*5)/2 = 42 wide.
51	if w.Focus != 2 || w.ViewX != 46 || w.columnWidth(0) != 42 {
52		t.Fatalf("scroll: %+v", w)
53	}
54	w.FocusColumn(-1)
55	if w.ViewX != 46 {
56		t.Fatal(w.ViewX)
57	}
58	w.FocusColumn(-1)
59	if w.ViewX != 0 {
60		t.Fatal(w.ViewX)
61	}
62	w.MoveColumn(1)
63	if w.Focus != 1 || w.Columns[1].Windows[0] != 1 {
64		t.Fatal(w.Columns)
65	}
66	w.RemoveWindow(1)
67	if id, _ := w.Focused(); id != 2 {
68		t.Fatal(id)
69	}
70	w.RemoveWindow(3)
71	w.RemoveWindow(2)
72	if len(w.Columns) != 0 {
73		t.Fatal(w.Columns)
74	}
75}
76func TestStackAndLayout(t *testing.T) {
77	w := workspace()
78	w.Columns = []Column{{Windows: []WindowID{1, 2, 3}, Focus: 1}}
79	w.RemoveWindow(2)
80	if id, _ := w.Focused(); id != 1 {
81		t.Fatal(id)
82	}
83	w.FocusWindow(1)
84	if id, _ := w.Focused(); id != 3 {
85		t.Fatal(id)
86	}
87	w.FocusWindow(-1)
88	p := w.Layout()
89	if p[0].Rect != (Rect{X: 5, Y: 5, W: 90, H: 33}) || p[1].Rect != (Rect{X: 5, Y: 43, W: 90, H: 33}) || p[0].Neighbors != ports.SideBottom || p[1].Inset != ports.SideTop {
90		t.Fatal(p)
91	}
92	w.ToggleFullscreen()
93	if p = w.Layout(); !p[0].Fullscreen || p[0].Rect != (Rect{X: 0, Y: 0, W: 100, H: 81}) || !p[1].Hidden || p[1].Rect != (Rect{}) {
94		t.Fatal(p)
95	}
96	w.ToggleFullscreen()
97	if w.Layout()[0].Fullscreen {
98		t.Fatal("toggle")
99	}
100	w.SetUsable(Rect{X: 10, Y: 10, W: 60, H: 50})
101	w.SetGaps(2)
102	if w.Layout()[0].Rect.X != 12 {
103		t.Fatal(w.Layout())
104	}
105	w.SetOutput(120, 90)
106	if w.Usable.W != 60 {
107		t.Fatal(w.Usable)
108	}
109}
110func TestMaximizeColumnScroll(t *testing.T) {
111	w := workspace()
112	for id := WindowID(1); id <= 3; id++ {
113		w.AddWindow(id)
114	}
115	w.Columns[1].Width = Width{Pixels: 30}
116	w.FocusID(2)
117	w.Apply(ActionMaximizeColumn)
118	if !w.Columns[1].FullWidth || w.Columns[1].Width.Pixels != 30 || w.Layout()[1].Rect != (Rect{X: 5, Y: 5, W: 90, H: 71}) {
119		t.Fatal(w.Columns, w.Layout())
120	}
121	if w.Layout()[0].Rect.X >= 0 || w.Layout()[2].Rect.X < 100 || w.Layout()[1].Fullscreen {
122		t.Fatal(w.Layout())
123	}
124	w.Apply(ActionMaximizeColumn)
125	if w.Columns[1].FullWidth || w.Columns[1].Width.Pixels != 30 || w.Layout()[1].Rect.W != 30 {
126		t.Fatal(w.Columns, w.Layout())
127	}
128}
129
130func TestMaximizeColumnFixed(t *testing.T) {
131	w := workspace()
132	w.Overflow = OverflowFixed
133	for id := WindowID(1); id <= 3; id++ {
134		w.AddWindow(id)
135	}
136	w.FocusID(2)
137	w.Apply(ActionMaximizeColumn)
138	p := w.Layout()
139	if p[1].Rect != (Rect{X: 5, Y: 5, W: 90, H: 71}) || p[1].Fullscreen || !p[0].Hidden || !p[2].Hidden {
140		t.Fatal(p)
141	}
142	w.FocusColumn(1)
143	p = w.Layout()
144	if w.Columns[1].FullWidth || p[2].Hidden || p[0].Hidden || p[1].Hidden {
145		t.Fatal(w.Columns, p)
146	}
147}
148
149func TestMaximizeColumnFixedAddWindow(t *testing.T) {
150	w := workspace()
151	w.Overflow = OverflowFixed
152	w.AddWindow(1)
153	w.AddWindow(2)
154	w.ToggleFullWidth()
155	w.AddWindow(3)
156	p := w.Layout()
157	if w.Columns[1].FullWidth || p[0].Hidden || p[1].Hidden || p[2].Hidden || p[2].Rect.W == 0 {
158		t.Fatal(w.Columns, p)
159	}
160}
161
162func TestPresetsAndActions(t *testing.T) {
163	w := workspace()
164	w.AddWindow(1)
165	p := []Width{{Pixels: 30}, {Pixels: 40}}
166	w.SetPresets(p)
167	p[0].Pixels = 99
168	w.AddWindow(2)
169	w.Apply(ActionCycleColumnWidth)
170	if w.Columns[1].Width.Pixels != 30 {
171		t.Fatal(w.Columns)
172	}
173	w.CycleWidth()
174	if w.Columns[1].Width.Pixels != 40 {
175		t.Fatal(w.Columns)
176	}
177	// After the last preset, back to the automatic share.
178	w.CycleWidth()
179	if w.Columns[1].Width != (Width{}) {
180		t.Fatal(w.Columns)
181	}
182	w.RemoveWindow(2)
183	for _, tc := range []struct {
184		a    Action
185		want Effect
186	}{{ActionSpawnTerminal, Effect{Spawn: true}}, {ActionCloseWindow, Effect{Close: 1}}, {ActionQuit, Effect{Quit: true}}, {ActionFocusColumnLeft, Effect{}}, {ActionFocusColumnRight, Effect{}}, {ActionFocusWindowUp, Effect{}}, {ActionFocusWindowDown, Effect{}}, {ActionMoveColumnLeft, Effect{}}, {ActionMoveColumnRight, Effect{}}, {ActionToggleFullscreen, Effect{}}, {ActionCycleColumnWidth, Effect{}}, {"spawn fuzzel --prompt x", Effect{Spawn: true, Argv: []string{"fuzzel", "--prompt", "x"}}}, {"spawn   ", Effect{}}} {
187		if got := w.Apply(tc.a); !reflect.DeepEqual(got, tc.want) {
188			t.Errorf("%s: %+v", tc.a, got)
189		}
190	}
191	w.SetPresets(nil)
192	old := w.Columns[0].Width
193	w.CycleWidth()
194	if w.Columns[0].Width != old {
195		t.Fatal("empty presets")
196	}
197}
198func FuzzWorkspaceOps(f *testing.F) {
199	f.Add([]byte{0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
200	f.Fuzz(func(t *testing.T, ops []byte) {
201		w := workspace()
202		w.SetPresets([]Width{{Pixels: 25}, {Pixels: 50}})
203		next := WindowID(1)
204		for _, op := range ops {
205			switch op % 18 {
206			case 0:
207				w.AddWindow(next)
208				next++
209			case 1:
210				if id, ok := w.Focused(); ok {
211					w.RemoveWindow(id)
212				}
213			case 2:
214				w.FocusColumn(-1)
215			case 3:
216				w.FocusColumn(1)
217			case 4:
218				w.FocusWindow(-1)
219			case 5:
220				w.FocusWindow(1)
221			case 6:
222				w.MoveColumn(-1)
223			case 7:
224				w.MoveColumn(1)
225			case 8:
226				w.CycleWidth()
227			case 9:
228				w.ToggleFullscreen()
229			case 10:
230				w.SetGaps(int(op) % 10)
231			case 11:
232				w.SetOutput(int(op)-50, int(op)-80)
233			case 12:
234				w.SetUsable(Rect{X: int(op) % 20, Y: int(op) % 20, W: int(op) - 50, H: int(op) - 80})
235			case 13:
236				w.SetGaps(int(op) * 2)
237			case 14:
238				if len(w.Columns) > 1 {
239					stackFocused(w)
240				}
241			case 15:
242				if len(w.Columns) > 0 {
243					w.Columns[w.Focus].Width = Width{Num: math.MaxInt, Den: math.MaxInt}
244					w.scroll()
245				}
246			case 16:
247				w.SetOutput(0, 0)
248			case 17:
249				w.SetUsable(Rect{W: math.MaxInt, H: math.MaxInt})
250			}
251			seen := map[WindowID]bool{}
252			for _, c := range w.Columns {
253				if len(c.Windows) == 0 || c.Focus < 0 || c.Focus >= len(c.Windows) {
254					t.Fatal("invalid column")
255				}
256				for _, id := range c.Windows {
257					if seen[id] {
258						t.Fatal("duplicate")
259					}
260					seen[id] = true
261				}
262			}
263			if len(w.Columns) > 0 {
264				if w.Focus < 0 || w.Focus >= len(w.Columns) {
265					t.Fatal("focus")
266				}
267				left := w.columnX(w.Focus) - w.ViewX
268				width := w.columnWidth(w.Focus)
269				minX, maxX := w.Usable.X+w.gap(), w.Usable.X+w.Usable.W-w.gap()
270				if w.fullscreenColumn(w.Focus) {
271					minX, maxX = 0, w.Output.W
272				}
273				if width <= maxX-minX && (left < minX || left+width > maxX) {
274					t.Fatal("scroll")
275				}
276			}
277			placements := w.Layout()
278			if len(placements) != len(seen) {
279				t.Fatal("count")
280			}
281			fullCount := 0
282			for _, p := range placements {
283				if p.Fullscreen {
284					fullCount++
285				}
286				r := p.Rect
287				if r.W < 0 || r.H < 0 || r.Y < 0 || r.Y > w.Output.H || r.H > w.Output.H-r.Y {
288					t.Fatalf("bounds: %+v output %+v", p, w.Output)
289				}
290				if p.Hidden && r != (Rect{}) {
291					t.Fatal("hidden rect")
292				}
293				if !p.Hidden && !p.Fullscreen && (r.Y < w.Usable.Y || r.Y > w.Usable.Y+w.Usable.H || r.H > w.Usable.Y+w.Usable.H-r.Y) {
294					t.Fatalf("usable vertical bounds: %+v usable %+v", p, w.Usable)
295				}
296				if !seen[p.ID] {
297					t.Fatal("missing")
298				}
299				delete(seen, p.ID)
300			}
301			if fullCount > 1 {
302				t.Fatal("multiple fullscreen")
303			}
304		}
305	})
306}
307
308// stackFocused moves the next column's window into the focused column.
309func stackFocused(w *Workspace) {
310	i := w.Focus
311	j := (i + 1) % len(w.Columns)
312	id := w.Columns[j].Windows[0]
313	w.Columns[i].Windows = append(w.Columns[i].Windows, id)
314	w.Columns[j].Windows = w.Columns[j].Windows[1:]
315	if len(w.Columns[j].Windows) == 0 {
316		w.Columns = append(w.Columns[:j], w.Columns[j+1:]...)
317		if j < i {
318			w.Focus--
319		}
320	} else if w.Columns[j].Focus >= len(w.Columns[j].Windows) {
321		w.Columns[j].Focus = len(w.Columns[j].Windows) - 1
322	}
323	w.scroll()
324}
325
326func TestLargeWidthAndBounds(t *testing.T) {
327	for _, tc := range []struct {
328		width              Width
329		usable, gaps, want int
330	}{
331		{Width{Num: 1, Den: 1}, math.MaxInt, 0, math.MaxInt},
332		{Width{Num: math.MaxInt, Den: math.MaxInt}, math.MaxInt, 0, math.MaxInt},
333		{Width{Num: math.MaxInt, Den: math.MaxInt}, math.MaxInt, 5, math.MaxInt - 10},
334		{Width{Num: 1, Den: 2}, math.MaxInt, 0, (math.MaxInt + 1) / 2},
335		{Width{Num: math.MaxInt, Den: math.MaxInt}, 100, 5, 90},
336	} {
337		if got := tc.width.Resolve(tc.usable, tc.gaps); got != tc.want {
338			t.Errorf("Resolve(%+v, %d, %d) = %d; want %d", tc.width, tc.usable, tc.gaps, got, tc.want)
339		}
340	}
341	w := workspace()
342	w.AddWindow(1)
343	w.SetUsable(Rect{X: -1, Y: -2, W: -10, H: math.MaxInt})
344	w.SetGaps(math.MaxInt)
345	for _, p := range w.Layout() {
346		if p.Rect.W != 0 || p.Rect.H < 0 {
347			t.Fatal(p)
348		}
349	}
350	w.SetOutput(-1, -1)
351	if w.Output.W != 0 || w.Output.H != 0 {
352		t.Fatal(w.Output)
353	}
354}
355func TestUnchangedUsablePreservesViewX(t *testing.T) {
356	w := workspace()
357	w.AddWindow(1)
358	w.AddWindow(2)
359	w.AddWindow(3)
360	before := w.ViewX
361	if before == 0 {
362		t.Fatal("expected a scrolled workspace")
363	}
364	w.SetUsable(w.Usable)
365	if w.ViewX != before {
366		t.Fatalf("ViewX changed from %d to %d", before, w.ViewX)
367	}
368}
369
370func TestFullscreenPersistsAndScrolls(t *testing.T) {
371	w := workspace()
372	w.AddWindow(1)
373	w.AddWindow(2)
374	w.FocusColumn(-1)
375	w.ToggleFullscreen()
376	if p := w.Layout()[0]; !p.Fullscreen || p.Rect.X != 0 || p.Rect.W != 100 {
377		t.Fatal(p)
378	}
379	w.FocusColumn(1)
380	if p := w.Layout()[0]; !p.Fullscreen || p.Rect.X >= 0 {
381		t.Fatal(p)
382	}
383	w.ToggleFullscreen()
384	if w.Layout()[0].Fullscreen || !w.Layout()[1].Fullscreen {
385		t.Fatal(w.Layout())
386	}
387	w.RemoveWindow(2)
388	if w.Layout()[0].Fullscreen {
389		t.Fatal(w.Layout())
390	}
391}
392
393func TestSetFullscreenStackDeactivation(t *testing.T) {
394	w := workspace()
395	w.Columns = []Column{{Windows: []WindowID{1, 2}, Focus: 0}}
396	// The covering fullscreen window is the only one drawn, so it has the
397	// focus; window 1 gets it back when 2 leaves fullscreen.
398	w.SetFullscreen(2, true)
399	p := w.Layout()
400	if !p[0].Hidden || !p[1].Fullscreen || !p[1].Focused || p[0].Focused {
401		t.Fatal(p)
402	}
403	w.SetFullscreen(2, false)
404	if p := w.Layout(); p[0].Hidden || !p[0].Focused {
405		t.Fatal(p)
406	}
407}
408
409// Fixed overflow keeps every window on screen: past max-columns, new columns
410// split the newest one, alternating top/bottom and left/right.
411func TestFixedOverflowSpiral(t *testing.T) {
412	w := &Workspace{MaxColumns: 2, Overflow: OverflowFixed}
413	w.SetOutput(100, 80)
414	for id := WindowID(1); id <= 5; id++ {
415		w.AddWindow(id)
416	}
417	got := map[WindowID]Rect{}
418	for _, p := range w.Layout() {
419		got[p.ID] = p.Rect
420	}
421	want := map[WindowID]Rect{
422		1: {X: 0, Y: 0, W: 50, H: 80},
423		2: {X: 50, Y: 0, W: 50, H: 40},  // top half of slot 2
424		3: {X: 50, Y: 40, W: 25, H: 40}, // left of the bottom half
425		4: {X: 75, Y: 40, W: 25, H: 20},
426		5: {X: 75, Y: 60, W: 25, H: 20},
427	}
428	if !reflect.DeepEqual(got, want) {
429		t.Fatalf("got %v\nwant %v", got, want)
430	}
431	if w.ViewX != 0 {
432		t.Fatal("fixed overflow scrolled", w.ViewX)
433	}
434	// Up to max-columns it is the plain equal split.
435	w2 := &Workspace{MaxColumns: 2, Overflow: OverflowFixed}
436	w2.SetOutput(100, 80)
437	w2.AddWindow(1)
438	w2.AddWindow(2)
439	if p := w2.Layout(); p[0].Rect.W != 50 || p[1].Rect != (Rect{X: 50, Y: 0, W: 50, H: 80}) {
440		t.Fatal(p)
441	}
442}
443
444// In fixed overflow, cycle-column-width expands the focused column over
445// max-columns - 1 cells in its place; the others stack beside it.
446func TestFixedExpandColumn(t *testing.T) {
447	rects := func(w *Workspace) map[WindowID]Rect {
448		got := map[WindowID]Rect{}
449		for id, p := range placements(w) {
450			got[id] = p.Rect
451		}
452		return got
453	}
454	check := func(t *testing.T, w *Workspace, want map[WindowID]Rect) {
455		t.Helper()
456		if got := rects(w); !reflect.DeepEqual(got, want) {
457			t.Fatalf("got %v\nwant %v", got, want)
458		}
459	}
460	t.Run("free cell", func(t *testing.T) {
461		w := fixedWorkspace(4, 0, 2)
462		w.FocusID(1)
463		w.CycleWidth()
464		check(t, w, map[WindowID]Rect{1: {W: 75, H: 80}, 2: {X: 75, W: 25, H: 80}})
465		w.CycleWidth()
466		check(t, w, map[WindowID]Rect{1: {W: 50, H: 80}, 2: {X: 50, W: 50, H: 80}})
467	})
468	t.Run("middle column keeps its place", func(t *testing.T) {
469		w := fixedWorkspace(4, 0, 4)
470		w.FocusID(2)
471		w.CycleWidth()
472		check(t, w, map[WindowID]Rect{
473			1: {W: 12, H: 80},
474			2: {X: 12, W: 75, H: 80},
475			3: {X: 87, W: 13, H: 40},
476			4: {X: 87, Y: 40, W: 13, H: 40},
477		})
478	})
479	t.Run("with gaps", func(t *testing.T) {
480		w := fixedWorkspace(2, 4, 3)
481		w.FocusID(1)
482		w.CycleWidth()
483		// Two cells of 44: the wide one keeps one, the others stack in the other.
484		check(t, w, map[WindowID]Rect{
485			1: {X: 4, Y: 4, W: 44, H: 72},
486			2: {X: 52, Y: 4, W: 44, H: 34},
487			3: {X: 52, Y: 42, W: 44, H: 34},
488		})
489	})
490	t.Run("stays on focus change and new windows", func(t *testing.T) {
491		w := fixedWorkspace(4, 0, 2)
492		w.FocusID(1)
493		w.CycleWidth()
494		w.FocusColumn(1)
495		w.AddWindow(3)
496		check(t, w, map[WindowID]Rect{1: {W: 75, H: 80}, 2: {X: 75, W: 25, H: 40}, 3: {X: 75, Y: 40, W: 25, H: 40}})
497	})
498	t.Run("moves to the focused column", func(t *testing.T) {
499		w := fixedWorkspace(4, 0, 2)
500		w.FocusID(1)
501		w.CycleWidth()
502		w.FocusID(2)
503		w.CycleWidth()
504		check(t, w, map[WindowID]Rect{1: {W: 25, H: 80}, 2: {X: 25, W: 75, H: 80}})
505	})
506	t.Run("closing it shares again", func(t *testing.T) {
507		w := fixedWorkspace(3, 0, 3)
508		w.FocusID(1)
509		w.CycleWidth()
510		w.RemoveWindow(1)
511		check(t, w, map[WindowID]Rect{2: {W: 50, H: 80}, 3: {X: 50, W: 50, H: 80}})
512	})
513	t.Run("no-op when it cannot grow", func(t *testing.T) {
514		for _, w := range []*Workspace{fixedWorkspace(3, 0, 1), fixedWorkspace(1, 0, 2)} {
515			before := rects(w)
516			w.CycleWidth()
517			check(t, w, before)
518		}
519	})
520	t.Run("stays on screen when gaps do not fit", func(t *testing.T) {
521		for _, w := range []*Workspace{fixedWorkspace(2, 4, 30), fixedWorkspace(4, 4, 4)} {
522			if w.MaxColumns == 4 {
523				w.SetOutput(12, 80)
524			}
525			w.FocusID(2)
526			w.CycleWidth()
527			for id, p := range placements(w) {
528				r := p.Rect
529				if r.X < 0 || r.Y < 0 || r.W < 0 || r.H < 0 || r.X+r.W > w.Output.W || r.Y+r.H > w.Output.H {
530					t.Fatal(id, r)
531				}
532			}
533		}
534	})
535	t.Run("maximize wins and gives it back", func(t *testing.T) {
536		w := fixedWorkspace(4, 0, 2)
537		w.FocusID(1)
538		w.CycleWidth()
539		w.ToggleFullWidth()
540		if p := placements(w); p[1].Rect != (Rect{W: 100, H: 80}) || !p[2].Hidden {
541			t.Fatal(p)
542		}
543		w.ToggleFullWidth()
544		check(t, w, map[WindowID]Rect{1: {W: 75, H: 80}, 2: {X: 75, W: 25, H: 80}})
545	})
546}
547
548// Up and down cross the columns the spiral or the expanded strips stack.
549func TestFixedFocusStackedColumns(t *testing.T) {
550	focused := func(w *Workspace) WindowID { id, _ := w.Focused(); return id }
551	t.Run("spiral", func(t *testing.T) {
552		// 1 left, 2 top right, 3 bottom left, 4 over 5 bottom right.
553		w := fixedWorkspace(2, 4, 5)
554		for _, want := range []WindowID{4, 2} {
555			if !w.FocusWindow(-1) || focused(w) != want {
556				t.Fatal(focused(w), want)
557			}
558		}
559		if w.FocusWindow(-1) {
560			t.Fatal("moved past the top", focused(w))
561		}
562		w.FocusID(1)
563		if w.FocusWindow(1) {
564			t.Fatal("full height column moved", focused(w))
565		}
566	})
567	t.Run("left and right follow the screen", func(t *testing.T) {
568		// 1 left, 2 top right, 3 bottom left, 4 over 5 bottom right.
569		w := fixedWorkspace(2, 4, 5)
570		for _, step := range []struct {
571			dir  int
572			want WindowID
573		}{{-1, 3}, {-1, 1}, {1, 2}} {
574			w.FocusColumn(step.dir)
575			if focused(w) != step.want {
576				t.Fatal(focused(w), step.want)
577			}
578		}
579		w.FocusID(3)
580		w.FocusColumn(1)
581		if focused(w) != 4 {
582			t.Fatal(focused(w))
583		}
584		if w.FocusID(2); w.columnToward(1) >= 0 {
585			t.Fatal("right edge has a neighbor")
586		}
587	})
588	t.Run("expanded strip", func(t *testing.T) {
589		// 1 wide; 2, 3 and 4 stacked on its right.
590		w := fixedWorkspace(3, 0, 4)
591		w.FocusID(1)
592		w.CycleWidth()
593		w.FocusID(2)
594		for _, want := range []WindowID{3, 4} {
595			if !w.FocusWindow(1) || focused(w) != want {
596				t.Fatal(focused(w), want)
597			}
598		}
599		if !w.FocusWindow(-1) || focused(w) != 3 {
600			t.Fatal(focused(w))
601		}
602	})
603	t.Run("lands on the nearest window of a stacked column", func(t *testing.T) {
604		// 1 left; 2 top right holds two windows; 3 bottom right.
605		w := &Workspace{MaxColumns: 2, Overflow: OverflowFixed}
606		w.SetOutput(100, 80)
607		w.Columns = []Column{{Windows: []WindowID{1}}, {Windows: []WindowID{2, 5}}, {Windows: []WindowID{3}}}
608		w.Focus = 2
609		if !w.FocusWindow(-1) || focused(w) != 5 {
610			t.Fatal(focused(w))
611		}
612	})
613}
614
615func fixedWorkspace(maxCols, gaps, n int) *Workspace {
616	w := &Workspace{MaxColumns: maxCols, Overflow: OverflowFixed, Gaps: gaps}
617	w.SetOutput(100, 80)
618	for id := 1; id <= n; id++ {
619		w.AddWindow(WindowID(id))
620	}
621	return w
622}
623
624func placements(w *Workspace) map[WindowID]Placement {
625	got := map[WindowID]Placement{}
626	for _, p := range w.Layout() {
627		got[p.ID] = p
628	}
629	return got
630}
631
632func TestFixedOverflowEdgeCases(t *testing.T) {
633	t.Run("new windows go last whatever the focus", func(t *testing.T) {
634		w := fixedWorkspace(2, 0, 3)
635		w.FocusID(1)
636		w.AddWindow(4)
637		if got := w.Columns[len(w.Columns)-1].Windows[0]; got != 4 {
638			t.Fatal(got)
639		}
640	})
641	for _, col := range []WindowID{1, 4} { // a plain column and a spiral slot
642		t.Run("fullscreen covers the output", func(t *testing.T) {
643			w := fixedWorkspace(2, 4, 4)
644			w.FocusID(col)
645			w.ToggleFullscreen()
646			for id, p := range placements(w) {
647				if id == col && (p.Rect != Rect{W: 100, H: 80} || !p.Fullscreen) {
648					t.Fatal(p)
649				}
650				if id != col && !p.Hidden {
651					t.Fatalf("window %d visible over fullscreen: %+v", id, p)
652				}
653			}
654		})
655	}
656	t.Run("presets ignored", func(t *testing.T) {
657		w := fixedWorkspace(2, 0, 2)
658		w.SetPresets([]Width{{Num: 2, Den: 3}})
659		w.CycleWidth()
660		w.FocusColumn(-1)
661		w.CycleWidth()
662		for id, p := range placements(w) {
663			if p.Rect.X+p.Rect.W > 100 || p.Rect.W != 50 {
664				t.Fatal(id, p)
665			}
666		}
667	})
668	t.Run("gaps between spiral halves", func(t *testing.T) {
669		got := placements(fixedWorkspace(1, 4, 2))
670		if got[1].Rect != (Rect{X: 4, Y: 4, W: 92, H: 34}) || got[2].Rect != (Rect{X: 4, Y: 42, W: 92, H: 34}) {
671			t.Fatal(got[1].Rect, got[2].Rect)
672		}
673		if got[1].Neighbors == 0 || got[2].Neighbors == 0 {
674			t.Fatal("spiral windows must keep borders to show focus")
675		}
676	})
677}
678
679// Activating a window hidden by another's fullscreen shows it: the
680// fullscreen is exclusive, so it is left, even for a float.
681func TestActivateLeavesFullscreen(t *testing.T) {
682	for _, tc := range []struct {
683		name  string
684		setup func(w *Workspace)
685		keep  bool
686	}{
687		{"fixed", func(w *Workspace) { w.Overflow = OverflowFixed; w.MaxColumns = 2; w.AddWindow(3) }, false},
688		{"same column", func(w *Workspace) { w.Columns[0].Windows = append(w.Columns[0].Windows, 3) }, false},
689		{"floating", func(w *Workspace) { w.AddFloating(3, 10, 10) }, false},
690	} {
691		t.Run(tc.name, func(t *testing.T) {
692			w := &Workspace{}
693			w.SetOutput(100, 80)
694			w.AddWindow(2)
695			tc.setup(w)
696			w.FocusID(2)
697			w.SetFullscreen(2, true)
698			w.Activate(3)
699			for _, p := range w.Layout() {
700				if p.ID == 3 && (p.Hidden || !p.Focused) {
701					t.Fatalf("3 not shown and focused: %+v", p)
702				}
703				if p.ID == 2 && p.Fullscreen != tc.keep {
704					t.Fatalf("fullscreen of 2 = %v", p.Fullscreen)
705				}
706			}
707		})
708	}
709}

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 26

Tags 10

↑↓ moveenter openesc close