v0.3.0 cf370a0

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 TestMonitorFirstWorkspaceStays(t *testing.T) {
76	m := monitor()
77	m.AddWindow(1)
78	m.RemoveWindow(1)
79	m.FocusNumber(2)
80	if len(m.Workspaces) != 1 || m.Active != 0 {
81		t.Fatal(windows(m), m.Active)
82	}
83	// Leaving an empty first workspace does not remove it.
84	m.AddWindow(1)
85	m.FocusNumber(2)
86	m.AddWindow(2)
87	m.FocusNumber(1)
88	m.RemoveWindow(1)
89	m.FocusNumber(2)
90	if !reflect.DeepEqual(windows(m), [][]WindowID{{}, {2}, {}}) {
91		t.Fatal(windows(m))
92	}
93}
94
95func TestMonitorRemoveElsewhereKeepsActive(t *testing.T) {
96	m := monitor()
97	m.AddWindow(1)
98	m.FocusNumber(2)
99	m.AddWindow(2)
100	m.FocusNumber(3)
101	m.AddWindow(3)
102	m.FocusNumber(3)
103	// A window on another workspace closes: the user stays on workspace 3's windows.
104	m.RemoveWindow(2)
105	if !reflect.DeepEqual(windows(m), [][]WindowID{{1}, {3}, {}}) || m.Active != 1 {
106		t.Fatal(windows(m), m.Active)
107	}
108	if id, _ := m.Focused(); id != 3 {
109		t.Fatal(id)
110	}
111}
112
113func TestMonitorLayoutHidesOtherWorkspaces(t *testing.T) {
114	m := monitor()
115	m.AddWindow(1)
116	m.FocusNumber(2)
117	m.AddWindow(2)
118	for _, p := range m.Layout() {
119		if hidden := p.ID == 1; p.Hidden != hidden {
120			t.Fatal(m.Layout())
121		}
122	}
123}
124
125func TestMonitorActions(t *testing.T) {
126	m := monitor()
127	m.AddWindow(1)
128	m.AddWindow(2)
129	// Down past the only window of the column goes to the workspace below.
130	m.Apply(ActionFocusWindowDown)
131	if m.Active != 1 {
132		t.Fatal(m.Active)
133	}
134	m.Apply(ActionFocusWindowUp)
135	if m.Active != 0 {
136		t.Fatal(m.Active)
137	}
138	// Up at the top workspace stays.
139	m.Apply(ActionFocusWorkspaceUp)
140	if m.Active != 0 {
141		t.Fatal(m.Active)
142	}
143	// Move the focused window (2) down: focus stays on workspace 1.
144	m.Apply(ActionMoveWindowToWorkspaceDown)
145	if !reflect.DeepEqual(windows(m), [][]WindowID{{1}, {2}, {}}) || m.Active != 0 {
146		t.Fatal(windows(m), m.Active)
147	}
148	m.Apply("move-window-to-workspace 3")
149	if !reflect.DeepEqual(windows(m), [][]WindowID{{}, {2}, {1}, {}}) || m.Active != 0 {
150		t.Fatal(windows(m), m.Active)
151	}
152	m.Apply("focus-workspace 3")
153	if id, _ := m.Focused(); id != 1 {
154		t.Fatal(id)
155	}
156	// Workspace 1 is empty but first: it stays.
157	if len(m.Workspaces) != 4 {
158		t.Fatal(windows(m))
159	}
160	if e := m.Apply("spawn foot"); !e.Spawn {
161		t.Fatal(e)
162	}
163}
164
165func TestEqualSharesAndBorderless(t *testing.T) {
166	m := monitor()
167	m.SetMaxColumns(3)
168	m.AddWindow(1)
169	p := m.Layout()
170	if p[0].Rect.W != 100 || p[0].Neighbors != 0 {
171		t.Fatal(p)
172	}
173	m.AddWindow(2)
174	m.AddWindow(3)
175	p = m.Layout()
176	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 {
177		t.Fatal(p)
178	}
179	// A fourth column scrolls: still 1/3 each.
180	m.AddWindow(4)
181	if w := m.Current(); w.columnWidth(3) != 33 || w.ViewX == 0 {
182		t.Fatal(w.ViewX)
183	}
184	// A preset on one column is kept; auto columns keep their share.
185	m.SetPresets([]Width{{Num: 1, Den: 2}})
186	m.Apply(ActionCycleColumnWidth)
187	if w := m.Current(); w.columnWidth(w.Focus) != 50 || w.columnWidth(0) != 33 {
188		t.Fatal(w.columnWidth(w.Focus), w.columnWidth(0))
189	}
190}
191
192// stack returns a monitor with columns [1] [2 3], focus on window 3.
193func stack() *Monitor {
194	m := monitor()
195	m.AddWindow(1)
196	m.AddWindow(2)
197	m.AddWindow(3)
198	w := m.Current()
199	w.Columns = []Column{{Windows: []WindowID{1}}, {Windows: []WindowID{2, 3}, Focus: 1}}
200	w.Focus = 1
201	return m
202}
203
204func TestMoveColumnToWorkspace(t *testing.T) {
205	t.Run("whole column, focus stays", func(t *testing.T) {
206		m := stack()
207		m.Apply("move-column-to-workspace 2")
208		if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1}, {2, 3}, {}}) || m.Active != 0 {
209			t.Fatal(got, m.Active)
210		}
211		if w := m.Workspaces[1]; len(w.Columns) != 1 || w.Columns[0].Focus != 1 {
212			t.Fatal(w.Columns)
213		}
214		if id, _ := m.Focused(); id != 1 {
215			t.Fatal(id)
216		}
217	})
218	t.Run("window only", func(t *testing.T) {
219		m := stack()
220		m.Apply(ActionMoveWindowToWorkspaceDown)
221		if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2}, {3}, {}}) {
222			t.Fatal(got)
223		}
224	})
225	t.Run("follow shows the target", func(t *testing.T) {
226		m := stack()
227		m.SetFollowMove(true)
228		m.Apply(ActionMoveColumnToWorkspaceDown)
229		if id, _ := m.Focused(); m.Active != 1 || id != 3 {
230			t.Fatal(m.Active, id)
231		}
232		if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1}, {2, 3}, {}}) {
233			t.Fatal(got)
234		}
235	})
236	t.Run("the slot window alone releases its slot", func(t *testing.T) {
237		m := stack()
238		c := &m.Current().Columns[1]
239		c.Slot, c.Focus = 2, 0
240		m.Apply(ActionMoveWindowToWorkspaceDown)
241		if c := m.Current().Columns[1]; c.Slot != 0 || c.Windows[0] != 3 {
242			t.Fatal(c)
243		}
244	})
245	t.Run("follow a floating window", func(t *testing.T) {
246		m := stack()
247		m.SetFollowMove(true)
248		m.AddFloating(9, 10, 10)
249		m.Current().FocusID(9)
250		m.Apply(ActionMoveWindowToWorkspaceDown)
251		if id, _ := m.Focused(); m.Active != 1 || id != 9 {
252			t.Fatal(m.Active, id)
253		}
254	})
255	t.Run("slot column loses its slot", func(t *testing.T) {
256		m := stack()
257		m.Current().Columns[1].Slot = 2
258		m.Apply(ActionMoveColumnToWorkspaceDown)
259		if c := m.Workspaces[1].Columns[0]; c.Slot != 0 {
260			t.Fatal(c)
261		}
262	})
263}
264
265func TestWorkspaceArg(t *testing.T) {
266	for _, tc := range []struct {
267		a  Action
268		n  int
269		op WorkspaceOp
270		ok bool
271	}{{"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}} {
272		n, op, ok := WorkspaceArg(tc.a)
273		if n != tc.n || op != tc.op || ok != tc.ok {
274			t.Errorf("%s: %d %v %v", tc.a, n, op, ok)
275		}
276	}
277}
278
279func TestLonePresetColumnKeepsWidth(t *testing.T) {
280	m := monitor()
281	m.SetPresets([]Width{{Num: 1, Den: 2}})
282	m.AddWindow(1)
283	m.Apply(ActionCycleColumnWidth)
284	if p := m.Layout()[0]; p.Rect.W != 50 || p.Neighbors != 0 {
285		t.Fatal(p)
286	}
287}
288
289func named(m *Monitor, specs ...NamedWorkspace) *Monitor {
290	m.SetNamed(specs)
291	return m
292}
293
294func TestHiddenWorkspaceToggle(t *testing.T) {
295	m := named(monitor(), NamedWorkspace{Name: "dev"})
296	m.AddWindow(1)
297	if len(m.Workspaces) != 2 {
298		t.Fatal("hidden workspace is numbered", windows(m))
299	}
300	m.Apply("workspace dev")
301	if m.Current().Name != "dev" {
302		t.Fatal(m.Current().Name)
303	}
304	m.AddWindow(2)
305	// Up/down do not leave a hidden workspace.
306	m.Apply(ActionFocusWorkspaceDown)
307	m.Apply(ActionFocusWindowDown)
308	if m.Current().Name != "dev" {
309		t.Fatal("left the hidden workspace")
310	}
311	for _, p := range m.Layout() {
312		if hidden := p.ID == 1; p.Hidden != hidden {
313			t.Fatal(m.Layout())
314		}
315	}
316	// The same bind returns to the previous workspace.
317	m.Apply("workspace dev")
318	if id, _ := m.Focused(); id != 1 || m.Current().Name != "" {
319		t.Fatal(id, m.Current().Name)
320	}
321	// Cmd+N from the hidden workspace goes to that number.
322	m.Apply("workspace dev")
323	m.FocusNumber(1)
324	if id, _ := m.Focused(); id != 1 {
325		t.Fatal(id)
326	}
327	// Moving a window up/down from a hidden workspace does nothing.
328	m.Apply("workspace dev")
329	m.Apply(ActionMoveWindowToWorkspaceDown)
330	m.Apply(ActionMoveWindowToWorkspaceUp)
331	if id, _ := m.Focused(); id != 2 || m.Current().Name != "dev" {
332		t.Fatal(id, windows(m))
333	}
334}
335
336func TestNamedWorkspaceBindOnly(t *testing.T) {
337	m := named(monitor(), NamedWorkspace{Name: "web"})
338	if len(m.Workspaces) != 1 {
339		t.Fatal(windows(m))
340	}
341	m.Apply("workspace web")
342	if m.Current().Name != "web" {
343		t.Fatal(m.Current().Name)
344	}
345	m.Apply("workspace web")
346	if m.Current().Name != "" {
347		t.Fatal(m.Current().Name)
348	}
349}
350
351func TestNamedWorkspaceReload(t *testing.T) {
352	m := named(monitor(), NamedWorkspace{Name: "dev"}, NamedWorkspace{Name: "web"})
353	m.Apply("workspace dev")
354	m.AddWindow(7)
355	// Reload keeps windows and the workspace on screen; web becomes hidden.
356	named(m, NamedWorkspace{Name: "dev"}, NamedWorkspace{Name: "web"})
357	if m.Current().Name != "dev" || len(m.Workspaces) != 1 {
358		t.Fatal(m.Current().Name, windows(m))
359	}
360	// Dropping dev from config hands its windows to the numbered workspace.
361	named(m)
362	if m.Current().Name != "" || !reflect.DeepEqual(windows(m), [][]WindowID{{7}, {}}) {
363		t.Fatal(m.Current().Name, windows(m))
364	}
365}
366
367func TestNamedWorkspaceSettings(t *testing.T) {
368	m := named(monitor(), NamedWorkspace{Name: "dev", MaxColumns: 3, Overflow: OverflowFixed})
369	m.SetMaxColumns(1)
370	if m.Current().MaxColumns != 1 || m.Current().Overflow != "" {
371		t.Fatal(m.Current().MaxColumns, m.Current().Overflow)
372	}
373	m.Apply("workspace dev")
374	if m.Current().MaxColumns != 3 || m.Current().Overflow != OverflowFixed {
375		t.Fatal(m.Current().MaxColumns, m.Current().Overflow)
376	}
377	// New dynamic workspaces follow the defaults.
378	m.SetOverflow(OverflowScroll)
379	m.Apply("workspace dev")
380	m.FocusNumber(9)
381	if m.Current().MaxColumns != 1 || m.Current().Overflow != OverflowScroll {
382		t.Fatal(m.Current().MaxColumns, m.Current().Overflow)
383	}
384}
385
386// Cmd+F on a window that made itself fullscreen returns it to its column
387// (Wine opens monitor-sized apps fullscreen); the next press maximizes.
388func TestMaximizeLeavesClientFullscreen(t *testing.T) {
389	for _, o := range []Overflow{OverflowScroll, OverflowFixed} {
390		t.Run(string(o), func(t *testing.T) {
391			m := monitor()
392			m.SetOverflow(o)
393			m.AddWindow(1)
394			m.AddWindow(2)
395			m.SetFullscreen(2, true)
396			m.Apply(ActionMaximizeColumn)
397			if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2}, {}}) || m.Active != 0 {
398				t.Fatal(got, m.Active)
399			}
400			w := m.Current()
401			if w.fullscreen != 0 || w.Columns[1].FullWidth {
402				t.Fatal(w.fullscreen, w.Columns)
403			}
404			if id, _ := m.Focused(); id != 2 {
405				t.Fatal("focus", id)
406			}
407			m.Apply(ActionMaximizeColumn)
408			if !w.Columns[1].FullWidth {
409				t.Fatal(w.Columns)
410			}
411		})
412	}
413}
414
415// Ranging over every workspace allocates nothing: normalize and the
416// per-frame paths use it.
417func TestMonitorAllAllocations(t *testing.T) {
418	m := monitor()
419	m.AddWindow(1)
420	m.SetNamed([]NamedWorkspace{{Name: "s"}})
421	n := 0
422	if a := testing.AllocsPerRun(100, func() {
423		for range m.all() {
424			n++
425		}
426		m.byName("s")
427	}); a != 0 {
428		t.Fatalf("%v allocs", a)
429	}
430}
431
432// Fixed overflow keeps a fullscreen window in place: no workspace is
433// added, a window opening meanwhile waits hidden, and the bind or a client
434// exit restores the tiles with the user's focus where it was.
435func TestFixedFullscreenStaysInPlace(t *testing.T) {
436	m := monitor()
437	m.SetOverflow(OverflowFixed)
438	for id := WindowID(1); id <= 3; id++ {
439		m.AddWindow(id)
440	}
441	w := m.Current()
442	w.FocusID(2)
443	m.Apply(ActionCycleColumnWidth) // expanded: kept through fullscreen
444	m.ToggleFullscreen()
445	m.AddWindow(4)
446	if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2, 3, 4}, {}}) || m.Current() != w || w.cover() != 2 {
447		t.Fatal(got, w.cover())
448	}
449	if id, _ := m.Focused(); id != 2 {
450		t.Fatal("focus", id)
451	}
452	m.ToggleFullscreen()
453	if id, _ := m.Focused(); id != 2 || w.fullscreen != 0 || !w.Columns[1].Expanded {
454		t.Fatal("after bind", id, w.Columns)
455	}
456}
457
458// A client fullscreen request never moves the view or the focus (ADR
459// 011). In fixed overflow, where it would cover the screen and take the
460// keyboard, only the focused window's request applies.
461func TestFullscreenRequestStaysPut(t *testing.T) {
462	for _, tc := range []struct {
463		name     string
464		overflow Overflow
465		id       WindowID // requests fullscreen; 1 is focused
466		applied  bool
467	}{
468		{"scroll focused", OverflowScroll, 1, true},
469		{"scroll unfocused", OverflowScroll, 2, true},
470		{"scroll other workspace", OverflowScroll, 3, true},
471		{"fixed focused", OverflowFixed, 1, true},
472		{"fixed unfocused", OverflowFixed, 2, false},
473		{"fixed other workspace", OverflowFixed, 3, true},
474	} {
475		t.Run(tc.name, func(t *testing.T) {
476			m := monitor()
477			m.SetOverflow(tc.overflow)
478			m.AddWindow(3) // alone, so focused on workspace 1
479			m.FocusNumber(2)
480			m.AddWindow(2)
481			m.AddWindow(1)
482			w, _ := m.find(tc.id)
483			m.SetFullscreen(tc.id, true)
484			if id, _ := m.Focused(); m.Active != 1 || id != 1 || (w.fullscreen == tc.id) != tc.applied {
485				t.Fatal("active", m.Active, "focused", id, "fullscreen", w.fullscreen)
486			}
487			m.SetFullscreen(tc.id, false)
488			if id, _ := m.Focused(); id != 1 || w.fullscreen != 0 || len(m.Workspaces) != 3 {
489				t.Fatal("round trip", id, windows(m))
490			}
491		})
492	}
493}

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