feat/hints a471fa4

overview_maximized_test.go RawHistoryFolder

1package core
2
3import (
4	"math"
5	"slices"
6	"testing"
7
8	"github.com/bnema/neferwl/internal/ports"
9)
10
11func maximizedOverview() *Monitor {
12	m := newMonitor("", "")
13	m.SetOutput(600, 400)
14	m.SetOverflow(OverflowFixed)
15	m.SetMaxColumns(3)
16	for id := WindowID(1); id <= 4; id++ {
17		m.AddWindow(id)
18	}
19	m.Current().FocusID(2)
20	m.Current().ToggleFullWidth()
21	return m
22}
23
24func TestOverviewMaximizedGeometryAndNavigation(t *testing.T) {
25	m := maximizedOverview()
26	w := m.Current()
27	w.Columns[2].Windows = append(w.Columns[2].Windows, 5)
28	rects := w.hiddenColumnRects()
29	m.ToggleOverview()
30	if got := w.stack(); !slices.Equal(got, []stackItem{{stackColumn, 2}, {kind: stackColumns}}) {
31		t.Fatalf("items %v", got)
32	}
33	if p := previewOf(t, m.Layout(), 2); p.Hidden || p.Peek || !p.Focused {
34		t.Fatalf("maximized front %+v", p)
35	}
36	ps := m.Layout()
37	for _, id := range []WindowID{1, 3, 4, 5} {
38		p := previewOf(t, ps, id)
39		i := w.columnOf(id)
40		r := rects[i]
41		if id == 3 || id == 5 {
42			r = stackRects(r, 2, w.gap())[map[WindowID]int{3: 0, 5: 1}[id]]
43		}
44		if p.Hidden || !p.Peek || p.Focused || math.Abs(float64(p.Rect.W)-float64(r.W)*p.Preview) > 1 || math.Abs(float64(p.Rect.H)-float64(r.H)*p.Preview) > 1 {
45			t.Fatalf("hidden tile %d: %+v cell %+v", id, p, r)
46		}
47		for _, other := range []WindowID{1, 3, 4, 5} {
48			if id < other && p.Rect.Overlaps(previewOf(t, ps, other).Rect) {
49				t.Fatalf("hidden tiles %d/%d overlap", id, other)
50			}
51		}
52	}
53	// The hidden columns fill the card as if the maximized column were
54	// gone: no hole where it was.
55	area := 0
56	for _, id := range []WindowID{1, 3, 4, 5} {
57		r := previewOf(t, ps, id).Rect
58		area += r.W * r.H
59	}
60	z := previewOf(t, ps, 1).Preview
61	full := float64(w.Usable.W) * float64(w.Usable.H) * z * z
62	if float64(area) < 0.8*full {
63		t.Fatalf("hidden tiles cover %d of %.0f: hole left by the maximized column", area, full)
64	}
65	m.OverviewMove(0, -1)
66	if m.stackFront(w).kind != stackColumns || m.hiddenColumn(w) != 0 || !previewOf(t, m.Layout(), 1).Focused || w.Focus != 1 || !w.Columns[1].FullWidth {
67		t.Fatal("up changed real maximization or failed to select hidden card")
68	}
69	m.OverviewMove(1, 0)
70	if m.hiddenColumn(w) != 2 || !previewOf(t, m.Layout(), 3).Focused || w.Focus != 1 {
71		t.Fatal("right did not select column 3 provisionally")
72	}
73	m.OverviewMove(-1, 0)
74	if m.hiddenColumn(w) != 0 {
75		t.Fatal("left did not skip maximized slot")
76	}
77	m.OverviewMove(1, 0)
78	m.ToggleOverview()
79	if id, _ := w.Focused(); id != 3 || !w.Columns[2].FullWidth || w.Columns[1].FullWidth || !previewOf(t, w.Layout(), 1).Hidden {
80		t.Fatalf("confirm focus %d columns %+v", id, w.Columns)
81	}
82	m.ToggleOverview()
83	if m.hiddenColumn(w) != 1 || m.stackFront(w).kind != stackColumn {
84		t.Fatal("previously maximized column not selected behind front")
85	}
86	m.OverviewMove(0, -1)
87	if p := previewOf(t, m.Layout(), 2); !p.Focused || p.Peek {
88		t.Fatalf("MRU selection %+v", p)
89	}
90	m.CancelOverview()
91}
92
93func TestOverviewMaximizedCancelAndClick(t *testing.T) {
94	m := maximizedOverview()
95	w := m.Current()
96	w.AddFloating(9, 600, 400)
97	w.FocusID(2)
98	before := slices.Clone(order(w))
99	m.ToggleOverview()
100	m.OverviewMove(0, -1)
101	m.OverviewMove(0, -1)
102	m.CancelOverview()
103	if id, _ := w.Focused(); id != 2 || !w.Columns[1].FullWidth || !slices.Equal(order(w), before) {
104		t.Fatalf("cancel focus %d order %v", id, order(w))
105	}
106	m.ToggleOverview()
107	p := previewOf(t, m.Layout(), 3)
108	if !p.Peek {
109		t.Fatalf("peek %+v", p)
110	}
111	m.OverviewPick(3)
112	if id, _ := w.Focused(); id != 3 || !w.Columns[2].FullWidth {
113		t.Fatalf("click focus %d max %+v", id, w.Columns)
114	}
115}
116
117func TestOverviewMaximizedFloatWrapAndNeighbor(t *testing.T) {
118	m := maximizedOverview()
119	w := m.Current()
120	w.AddFloating(9, 600, 400)
121	m.Focus(1)
122	m.AddWindow(10)
123	m.AddWindow(11)
124	m.Current().FocusID(10)
125	m.Current().ToggleFullWidth()
126	m.Focus(0)
127	m.ToggleOverview()
128	if got := w.stack(); !slices.Equal(got, []stackItem{{stackFloat, 9}, {stackColumn, 2}, {kind: stackColumns}}) {
129		t.Fatalf("stack %v", got)
130	}
131	if p, q := previewOf(t, m.Layout(), 10), previewOf(t, m.Layout(), 11); !p.Peek || !q.Peek || p.Focused || q.Focused {
132		t.Fatalf("neighbor %+v %+v", p, q)
133	}
134	m.OverviewMove(0, -1)
135	if m.stackFront(w) != (stackItem{stackColumn, 2}) {
136		t.Fatalf("front %v", m.stackFront(w))
137	}
138	m.OverviewMove(0, 1)
139	if m.stackFront(w) != (stackItem{stackFloat, 9}) {
140		t.Fatalf("back %v", m.stackFront(w))
141	}
142	m.CancelOverview()
143}
144
145func TestOverviewMaximizedLeftAndStash(t *testing.T) {
146	m := maximizedOverview()
147	w := m.Current()
148	w.FocusID(4)
149	w.ToggleWindowStash()
150	w.FocusID(2)
151	w.ToggleFullWidth()
152	m.ToggleOverview()
153	m.OverviewMove(-1, 0)
154	if m.cardAt(w) >= 0 || m.stackFront(w).kind != stackColumns {
155		t.Fatalf("left should send card behind: %v", m.stackFront(w))
156	}
157	m.OverviewMove(0, 1)
158	if m.stackFront(w) != (stackItem{stackColumn, 2}) {
159		t.Fatalf("front %v", m.stackFront(w))
160	}
161	m.CancelOverview()
162}
163
164// Escape restores the opening column state after a real workspace mutation.
165func TestOverviewCancelRestoresFullWidthAfterMutation(t *testing.T) {
166	m := maximizedOverview()
167	w := m.Current()
168	m.ToggleOverview()
169	w.ToggleFullWidth()
170	if w.Columns[1].FullWidth {
171		t.Fatal("workspace mutation did not toggle maximization")
172	}
173	m.CancelOverview()
174	if w.Focus != 1 || !w.Columns[1].FullWidth {
175		t.Fatalf("cancel lost maximization: focus %d columns %+v", w.Focus, w.Columns)
176	}
177	// The snapshot follows a surviving column if another column disappears.
178	m.ToggleOverview()
179	w.RemoveWindow(1)
180	w.ToggleFullWidth()
181	m.CancelOverview()
182	if id, _ := w.Focused(); id != 2 || !w.Columns[0].FullWidth {
183		t.Fatalf("cancel after removal: focus %d columns %+v", id, w.Columns)
184	}
185}
186
187// Escape maximizes only the column holding the window focused at opening,
188// where the focus returns, even when binds split its column meanwhile.
189func TestOverviewCancelFullWidthAfterSplit(t *testing.T) {
190	m := maximizedOverview()
191	w := m.Current()
192	w.FocusID(3)
193	w.ConsumeOrExpel(-1) // column 2 holds [2 3]
194	w.FocusID(3)
195	w.ToggleFullWidth()
196	m.ToggleOverview()
197	w.FocusID(3)
198	w.ConsumeOrExpel(1) // 3 leaves for another column
199	w.FocusID(2)
200	w.ToggleFullWidth() // 2's column maximized instead
201	m.CancelOverview()
202	maxed := 0
203	for _, c := range w.Columns {
204		if c.FullWidth {
205			maxed++
206			if !slices.Contains(c.Windows, 3) {
207				t.Fatalf("wrong column maximized: %+v", w.Columns)
208			}
209		}
210	}
211	if id, _ := w.Focused(); maxed != 1 || id != 3 {
212		t.Fatalf("%d maximized, focus %d: %+v", maxed, id, w.Columns)
213	}
214}
215
216// In scroll overflow, Escape leaves a maximized column the focus left.
217func TestOverviewCancelKeepsScrollFullWidth(t *testing.T) {
218	m := maximizedOverview()
219	w := m.Current()
220	w.Overflow = OverflowScroll
221	w.FocusID(3) // column 2 stays maximized in scroll overflow
222	if !w.Columns[1].FullWidth {
223		t.Fatal("setup: column 2 not maximized")
224	}
225	m.ToggleOverview()
226	m.CancelOverview()
227	if !w.Columns[1].FullWidth {
228		t.Fatalf("escape cleared column 2: %+v", w.Columns)
229	}
230}
231
232// A selected single-column card follows its anchor when earlier columns go.
233func TestOverviewHiddenSelectionFollowsWindowAfterRemoval(t *testing.T) {
234	m := maximizedOverview()
235	w := m.Current()
236	m.ToggleOverview()
237	m.OverviewMove(0, -1)
238	m.OverviewMove(1, 0) // select column 3 inside the group
239	w.RemoveWindow(1)
240	if p := previewOf(t, m.Layout(), 3); !p.Focused || p.Peek {
241		t.Fatalf("selection moved: %+v", p)
242	}
243	m.ToggleOverview()
244	if id, _ := w.Focused(); id != 3 || !w.Columns[1].FullWidth {
245		t.Fatalf("confirm %d %+v", id, w.Columns)
246	}
247}
248
249func TestOverviewHiddenSelectionRemovedFallsBack(t *testing.T) {
250	m := maximizedOverview()
251	w := m.Current()
252	m.ToggleOverview()
253	m.OverviewMove(0, -1)
254	m.OverviewMove(1, 0) // select column 3 inside the group
255	w.RemoveWindow(3)
256	if p := previewOf(t, m.Layout(), 4); !p.Focused {
257		t.Fatalf("fallback %+v", p)
258	}
259	m.ToggleOverview()
260	if id, _ := w.Focused(); id != 4 {
261		t.Fatalf("fallback %d", id)
262	}
263}
264
265func TestOverviewMaximizedPreviewsKeepConfigures(t *testing.T) {
266	m := maximizedOverview()
267	w := m.Current()
268	s := newConfigures()
269	before := map[WindowID]ports.ConfigureWindow{}
270	for _, p := range w.Layout() {
271		// Hidden clients retain their last unmaximized configure.
272		if p.Hidden {
273			r := w.columnRectsFor(true)[int(p.ID)-1]
274			p.Rect, p.Hidden = r, false
275		}
276		v, _ := s.next(p, configureTarget{output: "test", client: p.Rect, view: whole(w.Usable)})
277		s.mark(v)
278		before[p.ID] = v
279	}
280	m.ToggleOverview()
281	m.OverviewMove(0, -1)
282	for _, p := range m.Layout() {
283		if p.ID < 1 || p.ID > 4 {
284			continue
285		}
286		v, _ := s.next(p, configureTarget{output: "test", view: whole(w.Usable)})
287		if v.Width != before[p.ID].Width || v.Height != before[p.ID].Height {
288			t.Fatalf("%d resized from %+v to %+v", p.ID, before[p.ID], v)
289		}
290	}
291}
292
293func TestOverviewScrollFullWidthUnchanged(t *testing.T) {
294	m := maximizedOverview()
295	w := m.Current()
296	w.Overflow = OverflowScroll
297	m.ToggleOverview()
298	if got := w.stack(); !slices.Equal(got, []stackItem{{kind: stackColumns}}) {
299		t.Fatalf("scroll items %v", got)
300	}
301	for _, id := range []WindowID{1, 3, 4} {
302		if p := previewOf(t, m.Layout(), id); p.Hidden || p.Peek {
303			t.Fatalf("scroll column %d %+v", id, p)
304		}
305	}
306}

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