layout_buffers_test.go RawHistoryFolder

1package core
2
3import (
4	"maps"
5	"reflect"
6	"slices"
7	"testing"
8	"time"
9
10	"github.com/bnema/neferwl/internal/ports"
11)
12
13// layoutVariants builds monitors covering the layout paths that use scratch:
14// scroll and fixed overflow, a column set in cells, row shares, floats, a stash,
15// a fullscreen window, a hidden neighbor workspace, the overview and a slide
16// toward a neighbor workspace.
17func layoutVariants(t *testing.T) map[string]*Monitor {
18	t.Helper()
19	base := func(o Overflow, n int) *Monitor {
20		m := newMonitor("", "")
21		m.SetOutput(300, 200)
22		m.SetOverflow(o)
23		m.SetGaps(5)
24		m.SetMaxColumns(3)
25		for id := WindowID(1); id <= WindowID(n); id++ {
26			m.AddWindow(id)
27		}
28		return m
29	}
30	v := map[string]*Monitor{}
31	v["scroll"] = base(OverflowScroll, 5)
32	v["fixed"] = base(OverflowFixed, 5)
33	v["fixed-spiral"] = base(OverflowFixed, 7)
34
35	m := base(OverflowFixed, 3)
36	m.Current().Columns[1].Cells = 2
37	v["fixed-cells"] = m
38
39	m = base(OverflowScroll, 4)
40	w := m.Current()
41	w.Columns[0].Windows = []WindowID{1, 5, 6}
42	w.Columns[0].Shares = []int{50, 30, 20}
43	m.AddWindow(5)
44	v["shares"] = m
45
46	m = base(OverflowScroll, 4)
47	m.AddFloating(10, 100, 80)
48	m.AddFloating(11, 60, 40)
49	v["floats"] = m
50
51	m = base(OverflowScroll, 4)
52	m.Current().Apply(ActionToggleWindowStash)
53	v["stash"] = m
54
55	m = base(OverflowScroll, 4)
56	m.Current().SetFullscreen(2, true)
57	v["fullscreen"] = m
58
59	m = base(OverflowScroll, 3)
60	m.Apply(ActionFocusWorkspaceNext)
61	m.AddWindow(20)
62	m.AddWindow(21)
63	m.Apply(ActionFocusWorkspacePrev)
64	v["two-workspaces"] = m
65
66	m = base(OverflowScroll, 3)
67	m.AddFloating(10, 100, 80)
68	m.ToggleOverview()
69	v["overview"] = m
70
71	m = base(OverflowScroll, 3)
72	m.Apply(ActionFocusWorkspaceNext)
73	m.AddWindow(20)
74	m.Apply(ActionFocusWorkspacePrev)
75	m.switchView.off = 0.4
76	v["slide"] = m
77
78	// The variants must hold what they are named for, on the workspace on
79	// screen (the one whose scratch the tests exercise).
80	for name, has := range map[string]func(*Workspace) bool{
81		"stash":       func(w *Workspace) bool { return len(w.Stash) > 0 && len(w.Columns) > 0 },
82		"floats":      func(w *Workspace) bool { return len(w.Floats) == 2 && len(w.Columns) > 0 },
83		"shares":      func(w *Workspace) bool { return validShares(w.Columns[0]) },
84		"fixed-cells": func(w *Workspace) bool { return w.Columns[1].Cells == 2 },
85		"fullscreen":  func(w *Workspace) bool { return w.fullscreen == 2 },
86		"scroll":      func(w *Workspace) bool { return len(w.Columns) == 5 },
87	} {
88		if w := v[name].Current(); len(w.Columns) == 0 || !has(w) {
89			t.Fatalf("variant %s: the workspace on screen lacks its feature: %+v", name, w.Columns)
90		}
91	}
92	return v
93}
94
95// TestLayoutIntoMatchesLayout: the buffer-reusing API gives the same
96// placements as the fresh one, whatever the buffer held before (a larger
97// stale layout, another monitor's), and repeatedly.
98func TestLayoutIntoMatchesLayout(t *testing.T) {
99	variants := layoutVariants(t)
100	var buf []Placement
101	for _, name := range slices.Sorted(maps.Keys(variants)) {
102		m := variants[name]
103		want := m.Layout()
104		if len(want) == 0 {
105			t.Fatalf("%s: empty layout", name)
106		}
107		for range 3 {
108			// Poison the reused buffer: stale entries must not leak.
109			for i := range buf[:cap(buf)] {
110				buf[:cap(buf)][i] = Placement{ID: 999, Rect: Rect{X: 1, Y: 1, W: 1, H: 1}, Focused: true, Preview: 3}
111			}
112			buf = m.layoutInto(buf)
113			if !slices.Equal(buf, want) {
114				t.Fatalf("%s: layoutInto differs\n got %+v\nwant %+v", name, buf, want)
115			}
116			if again := m.Layout(); !slices.Equal(again, want) {
117				t.Fatalf("%s: Layout changed after layoutInto\n got %+v\nwant %+v", name, again, want)
118			}
119		}
120		for w := range m.all() {
121			ws := w.Layout()
122			if got := w.layoutInto(buf); !slices.Equal(got, ws) {
123				t.Fatalf("%s: workspace layoutInto differs", name)
124			}
125		}
126	}
127}
128
129// TestLayoutResultsDoNotAlias: Layout() returns a slice the caller owns. A
130// retained result stays unchanged after further layouts of the same monitor
131// or workspace (nested or later, reused-buffer calls included), and two
132// results never share storage.
133func TestLayoutResultsDoNotAlias(t *testing.T) {
134	for name, m := range layoutVariants(t) {
135		first := m.Layout()
136		kept := slices.Clone(first)
137		wfirst := m.Current().Layout()
138		wkept := slices.Clone(wfirst)
139
140		second := m.Layout()
141		if &first[0] == &second[0] {
142			t.Fatalf("%s: two Layout() calls share storage", name)
143		}
144		var buf []Placement
145		for range 2 {
146			buf = m.layoutInto(buf)
147			buf = m.Current().layoutInto(buf)
148			m.Current().previewTiles()
149			m.Current().overviewZoom()
150		}
151		// layoutInto builds in the caller's buffer, never in a scratch of
152		// the workspace: with the capacity there it returns that storage.
153		w := m.Current()
154		big := make([]Placement, 0, 2*len(buf)+8)
155		res := w.layoutInto(big)
156		if len(res) == 0 || &res[0] != &big[:1][0] {
157			t.Fatalf("%s: Workspace.layoutInto did not build in the caller's buffer", name)
158		}
159		if res = m.layoutInto(big); len(res) == 0 || &res[0] != &big[:1][0] {
160			t.Fatalf("%s: Monitor.layoutInto did not build in the caller's buffer", name)
161		}
162		// A result kept across the calls that use the workspace scratch
163		// (zoom tiles, preview tiles, columns, rows, a second layout) is
164		// unchanged.
165		mine := w.layoutInto(make([]Placement, 0, len(res)))
166		mineKept := slices.Clone(mine)
167		w.overviewZoom()
168		w.peekStep()
169		w.previewTiles()
170		w.columnRects()
171		_ = w.Layout()
172		_ = w.layoutInto(nil)
173		_ = m.Layout()
174		if !slices.Equal(mine, mineKept) {
175			t.Fatalf("%s: a kept layoutInto result changed across scratch-using calls\n got %+v\nwant %+v", name, mine, mineKept)
176		}
177		// Change the layout itself, then lay out again.
178		if m.ov.open {
179			m.OverviewMove(1, 0)
180		} else {
181			m.Current().Apply(ActionCycleColumnWidth)
182		}
183		_ = m.Layout()
184		_ = m.Current().Layout()
185		m.switchView.off = 0.3
186		_ = m.Layout()
187		m.switchView.off = 0
188
189		if !slices.Equal(first, kept) {
190			t.Fatalf("%s: a retained Monitor.Layout() changed\n got %+v\nwant %+v", name, first, kept)
191		}
192		if !slices.Equal(wfirst, wkept) {
193			t.Fatalf("%s: a retained Workspace.Layout() changed", name)
194		}
195	}
196}
197
198// TestRectHelpersKeepAppendSemantics: the Into helpers append after dst's
199// length and give the same rows as the fresh ones.
200func TestRectHelpersKeepAppendSemantics(t *testing.T) {
201	r := Rect{X: 3, Y: 4, W: 50, H: 103}
202	pre := []Rect{{W: 1}, {W: 2}}
203	got := stackRectsInto(slices.Clone(pre), r, 3, 5)
204	if !slices.Equal(got[:2], pre) || !slices.Equal(got[2:], stackRects(r, 3, 5)) {
205		t.Fatalf("stackRectsInto: %+v", got)
206	}
207	c := Column{Windows: []WindowID{1, 2, 3}, Shares: []int{50, 30, 20}}
208	got = rowRectsInto(slices.Clone(pre), r, c, 5)
209	if !slices.Equal(got[:2], pre) || !slices.Equal(got[2:], rowRects(r, c, 5)) {
210		t.Fatalf("rowRectsInto: %+v", got)
211	}
212	// A reused destination overwrites in place.
213	buf := make([]Rect, 0, 8)
214	a := stackRectsInto(buf, r, 3, 5)
215	b := stackRectsInto(buf, r, 2, 5)
216	if &a[0] != &b[0] || len(b) != 2 {
217		t.Fatalf("not reused: %d %d", len(a), len(b))
218	}
219}
220
221// cloneScene copies every slice a scene carries.
222func cloneScene(s ports.Scene) ports.Scene {
223	s.Windows = slices.Clone(s.Windows)
224	s.Separators = slices.Clone(s.Separators)
225	s.DropHints = slices.Clone(s.DropHints)
226	s.Layers = slices.Clone(s.Layers)
227	s.CaptureIndicators = slices.Clone(s.CaptureIndicators)
228	return s
229}
230
231// TestPublishedScenesAreNeverMutated: scenes cross to another goroutine and
232// are immutable, so none may alias the layout scratch that the next publish
233// rebuilds. Frames run with camera and rect motions, then with the overview
234// opening; every earlier scene is compared with its copy at the end. Under
235// -race it also catches a later publish writing what a sent scene reads.
236func TestPublishedScenesAreNeverMutated(t *testing.T) {
237	type kept struct{ sent, copy ports.Scene }
238	var all []kept
239	// fresh is every screen's layout as a fresh Layout() gives it, taken
240	// before a step: the scene it publishes lists the same windows in the
241	// same order, and, where nothing moves (no rect motion, no camera), the
242	// same rects. A layout scratch shared with the real layouts the overview
243	// reads would show here.
244	type expect struct {
245		out    string
246		layout []Placement
247		still  bool
248	}
249	fresh := func(c *Core) []expect {
250		var e []expect
251		for _, sc := range c.screens {
252			if sc.name() != "" {
253				e = append(e, expect{sc.name(), sc.mon.Layout(), len(sc.rects) == 0 && !sc.mon.Current().view.motion.on})
254			}
255		}
256		return e
257	}
258	check := func(set []ports.Scene, want []expect, frame int) {
259		t.Helper()
260		if len(set) != len(want) {
261			t.Fatalf("frame %d: %d scenes, %d screens", frame, len(set), len(want))
262		}
263		for i, s := range set {
264			e := want[i]
265			if s.Output != e.out || len(s.Windows) != len(e.layout) {
266				t.Fatalf("frame %d: scene %s has %d windows, layout %d", frame, s.Output, len(s.Windows), len(e.layout))
267			}
268			for k, p := range e.layout {
269				w := s.Windows[k]
270				if w.ID != p.ID || w.Hidden != p.Hidden || (e.still && w.Rect != p.Rect) {
271					t.Fatalf("frame %d: scene %s window %d is %+v, the layout has %+v", frame, s.Output, k, w, p)
272				}
273			}
274		}
275	}
276	frame := 0
277	step := func(c *Core) {
278		t.Helper()
279		want := fresh(c)
280		if err := c.step(t.Context(), nil); err != nil {
281			t.Fatal(err)
282		}
283		select {
284		case set := <-c.ch.Scenes:
285			check(set, want, frame)
286			frame++
287			for _, s := range set {
288				all = append(all, kept{s, cloneScene(s)})
289			}
290		default:
291			t.Fatal("no scene published")
292		}
293	}
294
295	c := publishRig(t, 2)
296	t0 := time.Now()
297	for _, sc := range c.screens {
298		if sc.name() == "" {
299			continue
300		}
301		ws := sc.mon.Current()
302		ws.view.motion = c.spring(viewSpring(100, 0), t0)
303		ws.view.off = 100
304		startRectMotions(c, sc, t0)
305	}
306	for range 12 {
307		step(c)
308	}
309
310	// The overview opening: card motions, the real layouts, separators.
311	o := publishRig(t, 1)
312	now := time.Now()
313	shots := o.snapshot(now)
314	o.cur().mon.ToggleOverview()
315	o.transition(shots, now)
316	for range 12 {
317		step(o)
318	}
319	// The overview settled (no card motion): its scenes list the previews
320	// exactly as the monitor lays them out, frame after frame.
321	o.cur().stopRects()
322	still := 0
323	for range 3 {
324		if w := fresh(o); !w[0].still {
325			t.Fatal("setup: the overview still moves")
326		}
327		step(o)
328		still++
329	}
330	if !o.cur().mon.ov.open || still == 0 {
331		t.Fatal("setup: overview closed")
332	}
333
334	if len(all) < 39 {
335		t.Fatalf("only %d scenes", len(all))
336	}
337	changed := 0
338	for i := 1; i < len(all); i++ {
339		if !reflect.DeepEqual(all[i].sent, all[i-1].sent) {
340			changed++
341		}
342	}
343	if changed == 0 {
344		t.Fatal("frames never differ: the test would catch nothing")
345	}
346	for i, k := range all {
347		if !reflect.DeepEqual(k.sent, k.copy) {
348			t.Fatalf("scene %d (%s) was mutated after it was sent", i, k.sent.Output)
349		}
350	}
351}
352
353// TestSettledLayoutEmptyFallsBackToLive: an empty layout is nil (as a fresh
354// Layout() was), so a screen with nothing published yet measures the live
355// layout: a window added after a publish of an empty monitor is listed by
356// shownLayout without another publish.
357func TestSettledLayoutEmptyFallsBackToLive(t *testing.T) {
358	c := publishRig(t, 1)
359	sc := c.cur()
360	for _, p := range slices.Clone(sc.settledLayout) {
361		sc.mon.RemoveWindow(p.ID)
362	}
363	if err := c.publish(t.Context()); err != nil {
364		t.Fatal(err)
365	}
366	if sc.settledLayout != nil || sc.shown != nil {
367		t.Fatalf("empty layout kept: settled %v shown %v (cap %d)", sc.settledLayout, sc.shown, cap(sc.layoutBuf))
368	}
369	if got := sc.shownLayout(); len(got) != 0 {
370		t.Fatalf("empty monitor lists %+v", got)
371	}
372	sc.mon.AddWindow(77)
373	got := sc.shownLayout()
374	if len(got) != 1 || got[0].ID != 77 || got[0].Hidden {
375		t.Fatalf("the window added after the publish is not listed: %+v", got)
376	}
377}

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