feat/data-dnd e05cb15

monitor_test.go RawHistoryFolder

1package core
2
3import (
4	"reflect"
5	"testing"
6
7	"github.com/bnema/neferwl/internal/ports"
8)
9
10func monitor() *Monitor {
11	m := NewMonitor()
12	m.SetOutput(100, 80)
13	m.SetMaxColumns(2)
14	return m
15}
16
17// windows lists each workspace's windows, in column order.
18func windows(m *Monitor) [][]WindowID {
19	var out [][]WindowID
20	for _, w := range m.Workspaces {
21		list := []WindowID{}
22		for _, c := range w.Columns {
23			list = append(list, c.Windows...)
24		}
25		out = append(out, list)
26	}
27	return out
28}
29
30func TestMonitorKeepsOneEmptyWorkspaceBelow(t *testing.T) {
31	m := monitor()
32	if len(m.Workspaces) != 1 || m.Active != 0 {
33		t.Fatal(windows(m))
34	}
35	m.AddWindow(1)
36	if !reflect.DeepEqual(windows(m), [][]WindowID{{1}, {}}) {
37		t.Fatal(windows(m))
38	}
39	// Cmd+9 past the end lands on the trailing empty workspace.
40	m.FocusNumber(9)
41	if m.Active != 1 {
42		t.Fatal(m.Active)
43	}
44	m.AddWindow(2)
45	if !reflect.DeepEqual(windows(m), [][]WindowID{{1}, {2}, {}}) {
46		t.Fatal(windows(m))
47	}
48}
49
50func TestMonitorRemovesEmptyWorkspaceOnceLeft(t *testing.T) {
51	m := monitor()
52	m.AddWindow(1)
53	m.FocusNumber(2)
54	m.AddWindow(2)
55	m.FocusNumber(3)
56	m.AddWindow(3)
57	// [1:{1}] [2:{2}] [3:{3}] [4:{}]
58	m.FocusNumber(2)
59	m.RemoveWindow(2)
60	// Still on the now-empty workspace 2: it stays while active.
61	if len(m.Workspaces) != 4 || m.Active != 1 {
62		t.Fatal(windows(m), m.Active)
63	}
64	m.FocusNumber(1)
65	// Once left it is removed: workspace 3 becomes 2.
66	if !reflect.DeepEqual(windows(m), [][]WindowID{{1}, {3}, {}}) || m.Active != 0 {
67		t.Fatal(windows(m), m.Active)
68	}
69	m.FocusNumber(2)
70	if id, _ := m.Focused(); id != 3 {
71		t.Fatal(id)
72	}
73}
74
75func TestMonitorEmptyFirstWorkspaceRemovedOnceLeft(t *testing.T) {
76	m := monitor()
77	m.AddWindow(1)
78	m.RemoveWindow(1)
79	m.FocusNumber(2)
80	// The only workspace stays.
81	if len(m.Workspaces) != 1 || m.Active != 0 {
82		t.Fatal(windows(m), m.Active)
83	}
84	m.AddWindow(1)
85	m.FocusNumber(2)
86	m.AddWindow(2)
87	m.FocusNumber(1)
88	m.RemoveWindow(1)
89	// Still on the now-empty workspace 1: it stays while active.
90	if !reflect.DeepEqual(windows(m), [][]WindowID{{}, {2}, {}}) || m.Active != 0 {
91		t.Fatal(windows(m), m.Active)
92	}
93	// Once left it is removed: workspace 2 becomes 1.
94	m.FocusNumber(2)
95	if !reflect.DeepEqual(windows(m), [][]WindowID{{2}, {}}) || m.Active != 0 {
96		t.Fatal(windows(m), m.Active)
97	}
98}
99
100// A window closing elsewhere empties workspace 1: the first non-empty
101// workspace is promoted at once and the view stays on its windows.
102func TestMonitorEmptyFirstWorkspaceRemovedFromElsewhere(t *testing.T) {
103	m := monitor()
104	m.AddWindow(1)
105	m.FocusNumber(2)
106	m.AddWindow(2)
107	m.RemoveWindow(1)
108	if !reflect.DeepEqual(windows(m), [][]WindowID{{2}, {}}) || m.Active != 0 {
109		t.Fatal(windows(m), m.Active)
110	}
111}
112
113func TestMonitorRemoveElsewhereKeepsActive(t *testing.T) {
114	m := monitor()
115	m.AddWindow(1)
116	m.FocusNumber(2)
117	m.AddWindow(2)
118	m.FocusNumber(3)
119	m.AddWindow(3)
120	m.FocusNumber(3)
121	// A window on another workspace closes: the user stays on workspace 3's windows.
122	m.RemoveWindow(2)
123	if !reflect.DeepEqual(windows(m), [][]WindowID{{1}, {3}, {}}) || m.Active != 1 {
124		t.Fatal(windows(m), m.Active)
125	}
126	if id, _ := m.Focused(); id != 3 {
127		t.Fatal(id)
128	}
129}
130
131func TestMonitorLayoutHidesOtherWorkspaces(t *testing.T) {
132	m := monitor()
133	m.AddWindow(1)
134	m.FocusNumber(2)
135	m.AddWindow(2)
136	for _, p := range m.Layout() {
137		if hidden := p.ID == 1; p.Hidden != hidden {
138			t.Fatal(m.Layout())
139		}
140	}
141}
142
143func TestMonitorActions(t *testing.T) {
144	m := monitor()
145	m.AddWindow(1)
146	m.AddWindow(2)
147	// Down past the only window of the column goes to the workspace below.
148	m.Apply(ActionFocusWindowDown)
149	if m.Active != 1 {
150		t.Fatal(m.Active)
151	}
152	m.Apply(ActionFocusWindowUp)
153	if m.Active != 0 {
154		t.Fatal(m.Active)
155	}
156	// Up at the top workspace stays.
157	m.Apply(ActionFocusWorkspacePrev)
158	if m.Active != 0 {
159		t.Fatal(m.Active)
160	}
161	// Move the focused window (2) down: focus stays on workspace 1.
162	m.Apply(ActionMoveWindowToWorkspaceNext)
163	if !reflect.DeepEqual(windows(m), [][]WindowID{{1}, {2}, {}}) || m.Active != 0 {
164		t.Fatal(windows(m), m.Active)
165	}
166	m.Apply("move-window-to-workspace 3")
167	if !reflect.DeepEqual(windows(m), [][]WindowID{{}, {2}, {1}, {}}) || m.Active != 0 {
168		t.Fatal(windows(m), m.Active)
169	}
170	m.Apply("focus-workspace 3")
171	if id, _ := m.Focused(); id != 1 {
172		t.Fatal(id)
173	}
174	// The empty workspace 1 is removed once left.
175	if !reflect.DeepEqual(windows(m), [][]WindowID{{2}, {1}, {}}) || m.Active != 1 {
176		t.Fatal(windows(m), m.Active)
177	}
178	if e := m.Apply("spawn foot"); !e.Spawn {
179		t.Fatal(e)
180	}
181}
182
183func TestEqualSharesAndBorderless(t *testing.T) {
184	m := monitor()
185	m.SetMaxColumns(3)
186	m.AddWindow(1)
187	p := m.Layout()
188	if p[0].Rect.W != 100 || p[0].Neighbors != 0 {
189		t.Fatal(p)
190	}
191	m.AddWindow(2)
192	m.AddWindow(3)
193	p = m.Layout()
194	if p[0].Rect.W != 33 || p[1].Rect.W != 33 || p[2].Rect.W != 33 || p[0].Neighbors != ports.SideRight || p[0].Inset != ports.SideRight {
195		t.Fatal(p)
196	}
197	// A fourth column scrolls: still 1/3 each.
198	m.AddWindow(4)
199	if w := m.Current(); w.columnWidth(3) != 33 || w.View == 0 {
200		t.Fatal(w.View)
201	}
202	// Growing one column gives it 2 of the 3 cells; auto columns keep
203	// their share.
204	m.Apply(ActionCycleColumnWidth)
205	if w := m.Current(); w.columnWidth(w.Focus) != 66 || w.columnWidth(0) != 33 {
206		t.Fatal(w.columnWidth(w.Focus), w.columnWidth(0))
207	}
208}
209
210// stack returns a monitor with columns [1] [2 3], focus on window 3.
211func stack() *Monitor {
212	m := monitor()
213	m.AddWindow(1)
214	m.AddWindow(2)
215	m.AddWindow(3)
216	w := m.Current()
217	w.Columns = []Column{{Windows: []WindowID{1}}, {Windows: []WindowID{2, 3}, Focus: 1}}
218	w.Focus = 1
219	return m
220}
221
222func TestMoveColumnToWorkspace(t *testing.T) {
223	t.Run("whole column, focus stays", func(t *testing.T) {
224		m := stack()
225		m.Apply("move-column-to-workspace 2")
226		if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1}, {2, 3}, {}}) || m.Active != 0 {
227			t.Fatal(got, m.Active)
228		}
229		if w := m.Workspaces[1]; len(w.Columns) != 1 || w.Columns[0].Focus != 1 {
230			t.Fatal(w.Columns)
231		}
232		if id, _ := m.Focused(); id != 1 {
233			t.Fatal(id)
234		}
235	})
236	t.Run("window only", func(t *testing.T) {
237		m := stack()
238		m.Apply(ActionMoveWindowToWorkspaceNext)
239		if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2}, {3}, {}}) {
240			t.Fatal(got)
241		}
242	})
243	t.Run("follow shows the target", func(t *testing.T) {
244		m := stack()
245		m.SetFollowMove(true)
246		m.Apply(ActionMoveColumnToWorkspaceNext)
247		if id, _ := m.Focused(); m.Active != 1 || id != 3 {
248			t.Fatal(m.Active, id)
249		}
250		if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1}, {2, 3}, {}}) {
251			t.Fatal(got)
252		}
253	})
254	t.Run("the slot window alone releases its slot", func(t *testing.T) {
255		m := stack()
256		c := &m.Current().Columns[1]
257		c.Slot, c.Focus = 2, 0
258		m.Apply(ActionMoveWindowToWorkspaceNext)
259		if c := m.Current().Columns[1]; c.Slot != 0 || c.Windows[0] != 3 {
260			t.Fatal(c)
261		}
262	})
263	t.Run("follow a floating window", func(t *testing.T) {
264		m := stack()
265		m.SetFollowMove(true)
266		m.AddFloating(9, 10, 10)
267		m.Current().FocusID(9)
268		m.Apply(ActionMoveWindowToWorkspaceNext)
269		if id, _ := m.Focused(); m.Active != 1 || id != 9 {
270			t.Fatal(m.Active, id)
271		}
272	})
273	t.Run("slot column loses its slot", func(t *testing.T) {
274		m := stack()
275		m.Current().Columns[1].Slot = 2
276		m.Apply(ActionMoveColumnToWorkspaceNext)
277		if c := m.Workspaces[1].Columns[0]; c.Slot != 0 {
278			t.Fatal(c)
279		}
280	})
281}
282
283func TestWorkspaceArg(t *testing.T) {
284	for _, tc := range []struct {
285		a  Action
286		n  int
287		op WorkspaceOp
288		ok bool
289	}{{"focus-workspace 3", 3, FocusWorkspace, true}, {"move-column-to-workspace 12", 12, MoveColumnToWorkspace, true}, {"move-window-to-workspace 2", 2, MoveWindowToWorkspace, true}, {"move-to-workspace 2", 0, 0, false}, {"focus-workspace 0", 0, 0, false}, {"focus-workspace x", 0, 0, false}, {"quit", 0, 0, false}} {
290		n, op, ok := WorkspaceArg(tc.a)
291		if n != tc.n || op != tc.op || ok != tc.ok {
292			t.Errorf("%s: %d %v %v", tc.a, n, op, ok)
293		}
294	}
295}
296
297func TestLoneCellColumnKeepsWidth(t *testing.T) {
298	m := monitor()
299	m.AddWindow(1)
300	// One cell of the two: half the width, still no neighbor.
301	m.Apply("set-column-width -1")
302	if p := m.Layout()[0]; p.Rect.W != 50 || p.Neighbors != 0 {
303		t.Fatal(p)
304	}
305}
306
307func named(m *Monitor, specs ...NamedWorkspace) *Monitor {
308	m.SetNamed(specs)
309	return m
310}
311
312// An empty workspace 1 stays while a named workspace is shown over it, so
313// the toggle can return to it; leaving it for another number drops it.
314func TestEmptyFirstWorkspaceUnderNamed(t *testing.T) {
315	m := named(monitor(), NamedWorkspace{Name: "dev"})
316	m.AddWindow(1)
317	m.FocusNumber(2)
318	m.AddWindow(2)
319	m.FocusNumber(1)
320	m.RemoveWindow(1)
321	m.Apply("workspace dev")
322	if !reflect.DeepEqual(windows(m), [][]WindowID{{}, {2}, {}}) || m.Current().Name != "dev" {
323		t.Fatal(windows(m), m.Current().Name)
324	}
325	m.Apply("workspace dev")
326	if m.Current() != m.Workspaces[0] || !reflect.DeepEqual(windows(m), [][]WindowID{{}, {2}, {}}) {
327		t.Fatal(windows(m), m.Active)
328	}
329	m.FocusNumber(2)
330	if !reflect.DeepEqual(windows(m), [][]WindowID{{2}, {}}) || m.Active != 0 {
331		t.Fatal(windows(m), m.Active)
332	}
333}
334
335func TestHiddenWorkspaceToggle(t *testing.T) {
336	m := named(monitor(), NamedWorkspace{Name: "dev"})
337	m.AddWindow(1)
338	if len(m.Workspaces) != 2 {
339		t.Fatal("hidden workspace is numbered", windows(m))
340	}
341	m.Apply("workspace dev")
342	if m.Current().Name != "dev" {
343		t.Fatal(m.Current().Name)
344	}
345	m.AddWindow(2)
346	// Up/down do not leave a hidden workspace.
347	m.Apply(ActionFocusWorkspaceNext)
348	m.Apply(ActionFocusWindowDown)
349	if m.Current().Name != "dev" {
350		t.Fatal("left the hidden workspace")
351	}
352	for _, p := range m.Layout() {
353		if hidden := p.ID == 1; p.Hidden != hidden {
354			t.Fatal(m.Layout())
355		}
356	}
357	// The same bind returns to the previous workspace.
358	m.Apply("workspace dev")
359	if id, _ := m.Focused(); id != 1 || m.Current().Name != "" {
360		t.Fatal(id, m.Current().Name)
361	}
362	// Cmd+N from the hidden workspace goes to that number.
363	m.Apply("workspace dev")
364	m.FocusNumber(1)
365	if id, _ := m.Focused(); id != 1 {
366		t.Fatal(id)
367	}
368	// Moving a window up/down from a hidden workspace does nothing.
369	m.Apply("workspace dev")
370	m.Apply(ActionMoveWindowToWorkspaceNext)
371	m.Apply(ActionMoveWindowToWorkspacePrev)
372	if id, _ := m.Focused(); id != 2 || m.Current().Name != "dev" {
373		t.Fatal(id, windows(m))
374	}
375}
376
377func TestNamedWorkspaceBindOnly(t *testing.T) {
378	m := named(monitor(), NamedWorkspace{Name: "web"})
379	if len(m.Workspaces) != 1 {
380		t.Fatal(windows(m))
381	}
382	m.Apply("workspace web")
383	if m.Current().Name != "web" {
384		t.Fatal(m.Current().Name)
385	}
386	m.Apply("workspace web")
387	if m.Current().Name != "" {
388		t.Fatal(m.Current().Name)
389	}
390}
391
392func TestNamedWorkspaceReload(t *testing.T) {
393	m := named(monitor(), NamedWorkspace{Name: "dev"}, NamedWorkspace{Name: "web"})
394	m.Apply("workspace dev")
395	m.AddWindow(7)
396	// Reload keeps windows and the workspace on screen; web becomes hidden.
397	named(m, NamedWorkspace{Name: "dev"}, NamedWorkspace{Name: "web"})
398	if m.Current().Name != "dev" || len(m.Workspaces) != 1 {
399		t.Fatal(m.Current().Name, windows(m))
400	}
401	// Dropping dev from config hands its windows to the numbered workspace.
402	named(m)
403	if m.Current().Name != "" || !reflect.DeepEqual(windows(m), [][]WindowID{{7}, {}}) {
404		t.Fatal(m.Current().Name, windows(m))
405	}
406}
407
408func TestNamedWorkspaceSettings(t *testing.T) {
409	m := named(monitor(), NamedWorkspace{Name: "dev", MaxColumns: 3, Overflow: OverflowFixed})
410	m.SetMaxColumns(1)
411	if m.Current().MaxColumns != 1 || m.Current().Overflow != "" {
412		t.Fatal(m.Current().MaxColumns, m.Current().Overflow)
413	}
414	m.Apply("workspace dev")
415	if m.Current().MaxColumns != 3 || m.Current().Overflow != OverflowFixed {
416		t.Fatal(m.Current().MaxColumns, m.Current().Overflow)
417	}
418	// New dynamic workspaces follow the defaults.
419	m.SetOverflow(OverflowScroll)
420	m.Apply("workspace dev")
421	m.FocusNumber(9)
422	if m.Current().MaxColumns != 1 || m.Current().Overflow != OverflowScroll {
423		t.Fatal(m.Current().MaxColumns, m.Current().Overflow)
424	}
425}
426
427// Cmd+F on a window that made itself fullscreen returns it to its column
428// (Wine opens monitor-sized apps fullscreen); the next press maximizes.
429func TestMaximizeLeavesClientFullscreen(t *testing.T) {
430	for _, o := range []Overflow{OverflowScroll, OverflowFixed} {
431		t.Run(string(o), func(t *testing.T) {
432			m := monitor()
433			m.SetOverflow(o)
434			m.AddWindow(1)
435			m.AddWindow(2)
436			m.SetFullscreen(2, true)
437			m.Apply(ActionMaximizeColumn)
438			if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2}, {}}) || m.Active != 0 {
439				t.Fatal(got, m.Active)
440			}
441			w := m.Current()
442			if w.fullscreen != 0 || w.Columns[1].FullWidth {
443				t.Fatal(w.fullscreen, w.Columns)
444			}
445			if id, _ := m.Focused(); id != 2 {
446				t.Fatal("focus", id)
447			}
448			m.Apply(ActionMaximizeColumn)
449			if !w.Columns[1].FullWidth {
450				t.Fatal(w.Columns)
451			}
452		})
453	}
454}
455
456// Ranging over every workspace allocates nothing: normalize and the
457// per-frame paths use it.
458func TestMonitorAllAllocations(t *testing.T) {
459	m := monitor()
460	m.AddWindow(1)
461	m.SetNamed([]NamedWorkspace{{Name: "s"}})
462	n := 0
463	if a := testing.AllocsPerRun(100, func() {
464		for range m.all() {
465			n++
466		}
467		m.byName("s")
468	}); a != 0 {
469		t.Fatalf("%v allocs", a)
470	}
471}
472
473// Fixed overflow keeps a fullscreen window in place: no workspace is
474// added, a window opening meanwhile waits hidden, and the bind or a client
475// exit restores the tiles with the user's focus where it was.
476func TestFixedFullscreenStaysInPlace(t *testing.T) {
477	m := monitor()
478	m.SetMaxColumns(4)
479	m.SetOverflow(OverflowFixed)
480	for id := WindowID(1); id <= 3; id++ {
481		m.AddWindow(id)
482	}
483	w := m.Current()
484	w.FocusID(2)
485	m.Apply(ActionCycleColumnWidth) // wider: kept through fullscreen
486	m.ToggleFullscreen()
487	m.AddWindow(4)
488	if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2, 3, 4}, {}}) || m.Current() != w || w.cover() != 2 {
489		t.Fatal(got, w.cover())
490	}
491	if id, _ := m.Focused(); id != 2 {
492		t.Fatal("focus", id)
493	}
494	m.ToggleFullscreen()
495	if id, _ := m.Focused(); id != 2 || w.fullscreen != 0 || w.Columns[1].Cells != 2 {
496		t.Fatal("after bind", id, w.Columns)
497	}
498}
499
500// A client fullscreen request never moves the view or the focus (ADR
501// 011). In fixed overflow, where it would cover the screen and take the
502// keyboard, only the focused window's request applies.
503func TestFullscreenRequestStaysPut(t *testing.T) {
504	for _, tc := range []struct {
505		name     string
506		overflow Overflow
507		id       WindowID // requests fullscreen; 1 is focused
508		applied  bool
509	}{
510		{"scroll focused", OverflowScroll, 1, true},
511		{"scroll unfocused", OverflowScroll, 2, true},
512		{"scroll other workspace", OverflowScroll, 3, true},
513		{"fixed focused", OverflowFixed, 1, true},
514		{"fixed unfocused", OverflowFixed, 2, false},
515		{"fixed other workspace", OverflowFixed, 3, true},
516	} {
517		t.Run(tc.name, func(t *testing.T) {
518			m := monitor()
519			m.SetOverflow(tc.overflow)
520			m.AddWindow(3) // alone, so focused on workspace 1
521			m.FocusNumber(2)
522			m.AddWindow(2)
523			m.AddWindow(1)
524			w, _ := m.find(tc.id)
525			m.SetFullscreen(tc.id, true)
526			if id, _ := m.Focused(); m.Active != 1 || id != 1 || (w.fullscreen == tc.id) != tc.applied {
527				t.Fatal("active", m.Active, "focused", id, "fullscreen", w.fullscreen)
528			}
529			m.SetFullscreen(tc.id, false)
530			if id, _ := m.Focused(); id != 1 || w.fullscreen != 0 || len(m.Workspaces) != 3 {
531				t.Fatal("round trip", id, windows(m))
532			}
533		})
534	}
535}

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