feat/hints a471fa4

workspace_axis_test.go RawHistoryFolder

1package core
2
3import (
4	"github.com/bnema/neferwl/internal/ports"
5	"testing"
6)
7
8// axisCore is a cascade core whose monitor "M" has three workspaces of five
9// windows (ids 11-15, 21-25, 31-35; bands of three) and a trailing empty
10// one. Workspace 2 is active. With sides, monitors L and R flank M.
11func axisCore(t *testing.T, sides bool, overflow Overflow, maxCols int) (*Core, *Monitor) {
12	t.Helper()
13	c, _ := cascadeMovesCore(t, 0, maxCols, sides)
14	m := c.cur().mon
15	m.SetOverflow(overflow)
16	m.SetMaxColumns(maxCols)
17	for ws := range 3 {
18		m.Focus(ws)
19		for i := 1; i <= 5; i++ {
20			m.AddWindow(WindowID((ws+1)*10 + i))
21		}
22	}
23	m.Focus(1)
24	return c, m
25}
26
27func wantActive(t *testing.T, m *Monitor, active int) {
28	t.Helper()
29	if m.Active != active {
30		t.Fatalf("active workspace %d, want %d", m.Active, active)
31	}
32}
33
34// AC1: past the band edge with no monitor, left/right step the workspace.
35func TestCascadeFocusColumnStepsWorkspaceAtBandEdge(t *testing.T) {
36	c, m := axisCore(t, false, OverflowCascade, 3)
37	m.Current().FocusID(23) // last column of band 1, not the last band
38	c.applyAction(ActionFocusColumnRight)
39	wantActive(t, m, 2)
40	m.Focus(1)
41	m.Current().FocusID(24) // first column of band 2
42	c.applyAction(ActionFocusColumnLeft)
43	wantActive(t, m, 0)
44	// The first workspace stays put at the start.
45	m.Current().FocusID(11)
46	c.applyAction(ActionFocusColumnLeft)
47	wantActive(t, m, 0)
48	// Inside the band, focus moves and the workspace stays.
49	c.applyAction(ActionFocusColumnRight)
50	wantActive(t, m, 0)
51	if id, _ := m.Current().Focused(); id != 12 {
52		t.Fatalf("focused %d, want 12", id)
53	}
54}
55
56// Occupied workspaces win over monitors; monitors still precede trailing empties.
57func TestCascadeFocusColumnGoesToWorkspaceBeforeMonitor(t *testing.T) {
58	c, m := axisCore(t, true, OverflowCascade, 3)
59	m.Current().FocusID(23)
60	c.applyAction(ActionFocusColumnRight)
61	wantActive(t, m, 2)
62	if c.cur().mon != m {
63		t.Fatal("left monitor before workspace")
64	}
65	m.Focus(1)
66	m.Current().FocusID(24)
67	c.applyAction(ActionFocusColumnLeft)
68	wantActive(t, m, 0)
69	if c.cur().mon != m {
70		t.Fatal("left monitor before workspace")
71	}
72	m.Focus(2)
73	m.Current().FocusID(35)
74	c.applyAction(ActionFocusColumnRight)
75	wantFocus(t, c, "R", 0)
76	wantActive(t, m, 2)
77}
78
79// With no monitor on that side, the trailing empty workspace is the last
80// stop, and left comes back from it.
81func TestCascadeFocusColumnReachesTrailingWorkspaceWithoutMonitor(t *testing.T) {
82	c, m := axisCore(t, false, OverflowCascade, 3)
83	m.Focus(2)
84	m.Current().FocusID(35)
85	c.applyAction(ActionFocusColumnRight)
86	wantActive(t, m, 3)
87	if !m.Current().empty() {
88		t.Fatal("want the trailing empty workspace")
89	}
90	c.applyAction(ActionFocusColumnLeft)
91	wantActive(t, m, 2)
92}
93
94// One monitor on the left only. Right steps to the next workspace; left
95// from its empty band returns to the occupied workspace before that monitor.
96func TestCascadeStepRightThenLeftReturnsToWorkspace(t *testing.T) {
97	c := layoutCore(1, tile{"L", 0, 0, 900, 600}, tile{"M", 900, 0, 900, 600})
98	m := c.cur().mon
99	m.SetOverflow(OverflowCascade)
100	m.SetMaxColumns(3)
101	m.AddWindow(1)
102	m.AddWindow(5)
103	c.applyAction(ActionFocusColumnRight)
104	wantActive(t, m, 1)
105	c.applyAction(ActionFocusColumnLeft)
106	wantFocus(t, c, "M", 5)
107	wantActive(t, m, 0)
108}
109
110// AC3: up/down cross bands and stop at the last, with no workspace change.
111func TestCascadeFocusWindowDownStaysOnWorkspace(t *testing.T) {
112	c, m := axisCore(t, false, OverflowCascade, 3)
113	m.Current().FocusID(22)
114	c.applyAction(ActionFocusWindowDown)
115	wantFocus(t, c, "M", 25)
116	wantActive(t, m, 1)
117	c.applyAction(ActionFocusWindowDown)
118	wantFocus(t, c, "M", 25)
119	wantActive(t, m, 1)
120}
121
122// AC4: scroll and fixed switch workspace with up/down at the column
123// bottom; left/right at the edge do nothing without a monitor.
124func TestScrollFixedWorkspaceAxisIsVertical(t *testing.T) {
125	for _, overflow := range []Overflow{OverflowScroll, OverflowFixed} {
126		t.Run(string(overflow), func(t *testing.T) {
127			c, m := axisCore(t, false, overflow, 3)
128			m.Current().FocusID(25)
129			c.applyAction(ActionFocusColumnRight)
130			wantActive(t, m, 1)
131			wantFocus(t, c, "M", 25)
132			c.applyAction(ActionFocusColumnLeft)
133			c.applyAction(ActionFocusColumnLeft)
134			c.applyAction(ActionFocusColumnLeft)
135			c.applyAction(ActionFocusColumnLeft)
136			c.applyAction(ActionFocusColumnLeft)
137			wantActive(t, m, 1)
138			c.applyAction(ActionFocusWindowDown)
139			wantActive(t, m, 2)
140			m.Focus(1)
141			m.Current().FocusID(21) // top of the first column
142			c.applyAction(ActionFocusWindowUp)
143			wantActive(t, m, 0)
144		})
145	}
146}
147
148// AC5: a shown named workspace is outside the workspace list.
149func TestCascadeNamedWorkspaceDoesNotStep(t *testing.T) {
150	c, m := axisCore(t, false, OverflowCascade, 3)
151	m.SetNamed([]NamedWorkspace{{Name: "n"}})
152	m.ToggleNamed("n")
153	shown := m.shown
154	if shown == nil {
155		t.Fatal("named workspace not shown")
156	}
157	shown.Overflow = OverflowCascade
158	shown.SetMaxColumns(3)
159	for id := WindowID(41); id <= 45; id++ {
160		m.AddWindow(id)
161	}
162	shown.FocusID(43)
163	c.applyAction(ActionFocusColumnRight)
164	if m.shown != shown || m.Active != 1 {
165		t.Fatalf("shown %p want %p, active %d", m.shown, shown, m.Active)
166	}
167	wantFocus(t, c, "M", 43)
168	shown.FocusID(41)
169	c.applyAction(ActionFocusColumnLeft)
170	if m.shown != shown || m.Active != 1 {
171		t.Fatalf("shown %p want %p, active %d", m.shown, shown, m.Active)
172	}
173	wantFocus(t, c, "M", 41)
174}
175
176// AC6: a covering fullscreen tile at the band edge steps the workspace and
177// stays fullscreen.
178func TestCascadeFullscreenBandEdgeStepsWorkspace(t *testing.T) {
179	c, m := axisCore(t, false, OverflowCascade, 3)
180	w := m.Current()
181	w.FocusID(23)
182	w.ToggleFullscreen()
183	c.applyAction(ActionFocusColumnRight)
184	wantActive(t, m, 2)
185	if w.fullscreen != 23 {
186		t.Fatalf("fullscreen %d, want 23", w.fullscreen)
187	}
188}
189
190// A covering fullscreen tile follows the same order: the monitor on that
191// side after the occupied workspace.
192func TestCascadeFullscreenGoesToWorkspaceBeforeMonitor(t *testing.T) {
193	c, m := axisCore(t, true, OverflowCascade, 3)
194	w := m.Current()
195	w.FocusID(23)
196	w.ToggleFullscreen()
197	c.applyAction(ActionFocusColumnRight)
198	wantActive(t, m, 2)
199	if c.cur().mon != m {
200		t.Fatal("left monitor before workspace")
201	}
202	if w.fullscreen != 23 {
203		t.Fatalf("fullscreen %d, want 23", w.fullscreen)
204	}
205}
206
207// A workspace holding only a float is occupied: with no monitor on that
208// side, it comes before the trailing empty one.
209func TestCascadeFloatOnlyWorkspaceIsOccupied(t *testing.T) {
210	c, m := axisCore(t, false, OverflowCascade, 3)
211	m.Focus(3)
212	m.AddFloating(9, 100, 100)
213	m.Focus(2)
214	m.Current().FocusID(35)
215	c.applyAction(ActionFocusColumnRight)
216	wantActive(t, m, 3)
217	if got := c.cur().name(); got != "M" {
218		t.Fatalf("focused screen %q, want M", got)
219	}
220}
221
222// AC7: a lone native float, or an empty cascade workspace, is at the edge on
223// both sides.
224func TestCascadeLoneFloatAndEmptyWorkspaceStepWorkspace(t *testing.T) {
225	c, m := axisCore(t, false, OverflowCascade, 3)
226	m.Focus(3) // the trailing empty workspace
227	if !m.Current().empty() {
228		t.Fatal("workspace 4 not empty")
229	}
230	c.applyAction(ActionFocusColumnLeft)
231	wantActive(t, m, 2)
232
233	// A lone native float on its own workspace, past the trailing one.
234	for _, tc := range []struct {
235		a    Action
236		want int
237	}{{ActionFocusColumnLeft, 2}, {ActionFocusColumnRight, 4}} {
238		c, m = axisCore(t, false, OverflowCascade, 3)
239		m.Focus(3)
240		m.AddFloating(9, 100, 100)
241		if w := m.Current(); len(w.Columns) != 0 || !w.floatFocus {
242			t.Fatalf("columns %d floatFocus %v, want a lone focused float", len(w.Columns), w.floatFocus)
243		}
244		c.applyAction(tc.a)
245		wantActive(t, m, tc.want)
246	}
247}
248
249// Column edges visit occupied workspaces before crossing outputs, in both directions.
250func TestColumnEdgeFocusWorkspaceBeforeScreen(t *testing.T) {
251	for _, tc := range []struct {
252		name                          string
253		screen, start, next, neighbor int
254		action                        Action
255	}{
256		{"left", 1, 1, 0, 0, ActionFocusColumnLeft},
257		{"right", 0, 0, 1, 1, ActionFocusColumnRight},
258	} {
259		t.Run(tc.name, func(t *testing.T) {
260			c := layoutCore(tc.screen, tile{"laptop", 0, 0, 1920, 1080}, tile{"4K", 1920, 0, 3840, 2160})
261			m := c.cur().mon
262			m.SetOverflow(OverflowCascade)
263			for ws := range 2 {
264				m.Focus(ws)
265				m.AddWindow(WindowID(ws + 1))
266			}
267			m.Focus(tc.start)
268			c.applyAction(tc.action)
269			if c.focusScreen != tc.screen {
270				t.Fatalf("screen %d, want %d before crossing workspace", c.focusScreen, tc.screen)
271			}
272			wantActive(t, m, tc.next)
273			c.applyAction(tc.action)
274			if c.focusScreen != tc.neighbor {
275				t.Fatalf("screen %d, want neighbor %d", c.focusScreen, tc.neighbor)
276			}
277			wantActive(t, m, tc.next)
278		})
279	}
280}
281
282func TestVerticalEdgeFocusWorkspaceBeforeScreen(t *testing.T) {
283	for _, overflow := range []Overflow{OverflowScroll, OverflowFixed} {
284		for _, tc := range []struct {
285			name                          string
286			screen, start, next, neighbor int
287			action                        Action
288		}{
289			{"up", 1, 1, 0, 0, ActionFocusWindowUp},
290			{"down", 0, 0, 1, 1, ActionFocusWindowDown},
291		} {
292			t.Run(string(overflow)+"/"+tc.name, func(t *testing.T) {
293				c := layoutCore(tc.screen, tile{"top", 0, 0, 900, 600}, tile{"bottom", 0, 600, 900, 600})
294				m := c.cur().mon
295				m.SetOverflow(overflow)
296				for ws := range 2 {
297					m.Focus(ws)
298					m.AddWindow(WindowID(ws + 1))
299				}
300				m.Focus(tc.start)
301				c.applyAction(tc.action)
302				if c.focusScreen != tc.screen {
303					t.Fatal("crossed screen before occupied workspace")
304				}
305				wantActive(t, m, tc.next)
306				c.applyAction(tc.action)
307				if c.focusScreen != tc.neighbor {
308					t.Fatalf("screen %d, want %d", c.focusScreen, tc.neighbor)
309				}
310				wantActive(t, m, tc.next)
311			})
312		}
313	}
314}
315
316func TestCascadeVerticalEdgeDoesNotCrossScreen(t *testing.T) {
317	c := layoutCore(1, tile{"top", 0, 0, 900, 600}, tile{"middle", 0, 600, 900, 600}, tile{"bottom", 0, 1200, 900, 600})
318	m := c.cur().mon
319	m.SetOverflow(OverflowCascade)
320	m.AddWindow(1)
321	for _, a := range []Action{ActionFocusWindowUp, ActionFocusWindowDown} {
322		c.applyAction(a)
323		wantFocus(t, c, "middle", 1)
324		wantActive(t, m, 0)
325	}
326}
327
328func TestVerticalEdgeFocusFlashesDestination(t *testing.T) {
329	for _, tc := range []struct {
330		a Action
331		y int
332	}{{ActionFocusWindowUp, -300}, {ActionFocusWindowDown, 300}} {
333		t.Run(string(tc.a), func(t *testing.T) {
334			c, _ := flashCore(t)
335			c.cur().mon.SetOverflow(OverflowScroll)
336			c.addScreen(ports.OutputInfo{Name: "B", Width: 400, Height: 300})
337			c.screens[1].x, c.screens[1].y = 0, tc.y
338			c.applyAction(tc.a)
339			if c.focusScreen != 1 || c.flash.pending == nil || *c.flash.pending != (flashTarget{level: flashMonitor, output: "B"}) {
340				t.Fatalf("screen %d, flash %+v", c.focusScreen, c.flash.pending)
341			}
342		})
343	}
344}

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