stash_cover_test.go RawHistoryFolder

1package core
2
3import (
4	"context"
5	"slices"
6	"testing"
7
8	"github.com/bnema/neferwl/internal/ports"
9)
10
11// stashCoverCore has two screens; the first holds tile 1 and the hidden
12// stash [2 3], 3 selected, under a covering fullscreen window: tile 1, or
13// float 4 with float set.
14func stashCoverCore(t *testing.T, overflow Overflow, float bool) (*Core, *Workspace, WindowID) {
15	t.Helper()
16	cfg := ports.Config{}
17	cfg.Keyboard.CmdKey = "super"
18	cfg.Layout.Overflow = string(overflow)
19	cfg.Layout.MaxColumns = 2
20	c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}, Options{})
21	if err != nil {
22		t.Fatal(err)
23	}
24	c.addScreen(ports.OutputInfo{Name: "A", Width: 100, Height: 80})
25	c.addScreen(ports.OutputInfo{Name: "B", Width: 100, Height: 80})
26	m := c.screens[0].mon
27	for id := WindowID(1); id <= 3; id++ {
28		m.AddWindow(id)
29	}
30	w := m.Current()
31	for _, id := range []WindowID{2, 3} {
32		w.FocusID(id)
33		w.ToggleWindowStash()
34	}
35	c.applyAction(ActionToggleStashVisible) // hidden
36	cover := WindowID(1)
37	if float {
38		cover = 4
39		m.AddFloating(4, 100, 80)
40		m.SetFullscreen(4, true)
41	} else {
42		c.applyAction(ActionToggleFullscreen)
43	}
44	if w.cover() != cover || c.focusScreen != 0 {
45		t.Fatalf("setup: cover %d", w.cover())
46	}
47	return c, w, cover
48}
49
50// toggle-stash-visible is a user override: under any covering fullscreen
51// window it shows the stash on top, focused, the window fullscreen
52// behind. Moves stay in the stash, at its edges too. Toggling again
53// hides it, back on the fullscreen window.
54func TestStashShowsOverCover(t *testing.T) {
55	for _, tc := range []struct {
56		name     string
57		overflow Overflow
58		float    bool
59	}{
60		{"fixed tile", OverflowFixed, false},
61		{"scroll tile", OverflowScroll, false},
62		{"fixed float", OverflowFixed, true},
63		{"scroll float", OverflowScroll, true},
64	} {
65		t.Run(tc.name, func(t *testing.T) {
66			c, w, cover := stashCoverCore(t, tc.overflow, tc.float)
67			c.applyAction(ActionToggleStashVisible)
68			ps := w.Layout()
69			if id, _ := w.Focused(); id != 3 || w.cover() != cover {
70				t.Fatalf("focused %d cover %d", id, w.cover())
71			}
72			if p := previewOf(t, ps, cover); p.Hidden || !p.Fullscreen {
73				t.Fatalf("cover %+v", p)
74			}
75			if p := previewOf(t, ps, 3); p.Hidden || ps[len(ps)-1].ID != 3 && ps[len(ps)-2].ID != 3 {
76				t.Fatalf("stash not on top: %+v", ps)
77			}
78			c.applyAction(ActionFocusColumnLeft)
79			if id, _ := w.Focused(); id != 2 || w.cover() != cover {
80				t.Fatalf("left: focused %d cover %d", id, w.cover())
81			}
82			c.applyAction(ActionFocusColumnRight)
83			c.applyAction(ActionFocusColumnRight) // stash edge: stays, like a shown stash
84			if id, _ := w.Focused(); id != 3 || c.focusScreen != 0 {
85				t.Fatalf("edge: focused %d screen %d", id, c.focusScreen)
86			}
87			c.applyAction(ActionToggleStashVisible)
88			if id, _ := w.Focused(); id != cover || w.cover() != cover || !previewOf(t, w.Layout(), 3).Hidden {
89				t.Fatalf("hide: focused %d cover %d", id, w.cover())
90			}
91		})
92	}
93}
94
95// Only the user shows the stash over the cover: a window mapping
96// meanwhile is not captured, a dialog of the cover shows above the
97// stash, and the override ends with its cover.
98func TestStashOverCoverEdges(t *testing.T) {
99	c, w, cover := stashCoverCore(t, OverflowFixed, false)
100	m := c.screens[0].mon
101	m.SetStashCapture(true)
102	c.applyAction(ActionToggleStashVisible)
103	m.AddWindow(5)
104	if w.stashIndex(5) >= 0 || !previewOf(t, w.Layout(), 5).Hidden {
105		t.Fatalf("new window shown: %+v", w.Layout())
106	}
107	w.AddDialog(6, cover, 20, 20)
108	if ps := w.Layout(); ps[len(ps)-1].ID != 6 {
109		t.Fatalf("dialog under the stash: %+v", ps)
110	}
111	if id, _ := w.Focused(); id != 6 {
112		t.Fatalf("dialog focus %d", id)
113	}
114	c.applyAction(ActionFocusColumnLeft) // back to the stash
115	if id, _ := w.Focused(); id != 3 || !w.stashOverCover() {
116		t.Fatalf("from dialog: focused %d", id)
117	}
118	w.RemoveWindow(6)
119	// The cover closes: the stash it showed over is a plain shown stash;
120	// a later fullscreen window is never covered by it.
121	w.RemoveWindow(cover)
122	if w.stashOver != 0 {
123		t.Fatal("override outlived its cover")
124	}
125	w.FocusID(5)
126	w.ToggleFullscreen()
127	if p := previewOf(t, w.Layout(), 3); !p.Hidden {
128		t.Fatalf("stash over a new cover: %+v", p)
129	}
130}
131
132// Another window's fullscreen request ends the override: the old cover
133// asking again never brings the stash back over it.
134func TestStashOverCoverOtherRequest(t *testing.T) {
135	c, w, cover := stashCoverCore(t, OverflowScroll, false)
136	m := c.screens[0].mon
137	m.AddWindow(5)
138	c.applyAction(ActionToggleStashVisible)
139	m.SetFullscreen(5, true)
140	m.SetFullscreen(cover, true)
141	if w.cover() != cover || w.stashOverCover() {
142		t.Fatalf("cover %d, override revived", w.cover())
143	}
144}
145
146// Acting on a stashed window over the cover leaves the cover for it:
147// toggle-window-stash returns it to a visible column, toggle-fullscreen
148// makes it the fullscreen window.
149func TestStashOverCoverLeaves(t *testing.T) {
150	c, w, _ := stashCoverCore(t, OverflowFixed, false)
151	c.applyAction(ActionToggleStashVisible)
152	c.applyAction(ActionToggleWindowStash)
153	if id, _ := w.Focused(); id != 3 || w.fullscreen != 0 || previewOf(t, w.Layout(), 3).Hidden {
154		t.Fatalf("unstash: focused %d fullscreen %d", id, w.fullscreen)
155	}
156	c, w, _ = stashCoverCore(t, OverflowFixed, false)
157	c.applyAction(ActionToggleStashVisible)
158	c.applyAction(ActionToggleFullscreen)
159	if w.cover() != 3 || w.left != 1 {
160		t.Fatalf("fullscreen: cover %d left %d", w.cover(), w.left)
161	}
162}
163
164// Activating a window over a covering fullscreen window (xdg-activation,
165// taskbar) never ends that fullscreen when the window can show over it:
166// a stashed window shows the stash over the cover, and dialogs of the
167// cover or of stashed windows, and their dialogs, show above. A game
168// told it left fullscreen (configure 0x0) quits.
169func TestActivateOverCover(t *testing.T) {
170	covers := []struct {
171		name     string
172		overflow Overflow
173		float    bool
174	}{
175		{"fixed tile", OverflowFixed, false},
176		{"scroll tile", OverflowScroll, false},
177		{"fixed float", OverflowFixed, true},
178		{"scroll float", OverflowScroll, true},
179	}
180	cases := []struct {
181		name string
182		// setup prepares windows and returns the window to activate.
183		setup func(w *Workspace, cover WindowID) WindowID
184		// focused is the window focused after the activation.
185		focused WindowID
186		// over reports whether the stash shows over the cover then.
187		over bool
188	}{
189		{"stashed", func(*Workspace, WindowID) WindowID { return 2 }, 2, true},
190		{"modal of stashed", func(w *Workspace, _ WindowID) WindowID {
191			w.AddDialog(9, 2, 20, 10)
192			w.SetModal(9, true)
193			return 2
194		}, 9, true},
195		{"modal chain of stashed", func(w *Workspace, _ WindowID) WindowID {
196			w.AddDialog(9, 2, 20, 10)
197			w.SetModal(9, true)
198			w.AddDialog(8, 9, 10, 5)
199			w.SetModal(8, true)
200			return 2
201		}, 8, true},
202		{"dialog of stashed", func(w *Workspace, _ WindowID) WindowID {
203			w.AddDialog(9, 2, 20, 10)
204			return 9
205		}, 9, true},
206		{"dialog of cover", func(w *Workspace, cover WindowID) WindowID {
207			w.AddDialog(9, cover, 20, 10)
208			w.FocusID(cover)
209			return 9
210		}, 9, false},
211		{"modal chain of cover", func(w *Workspace, cover WindowID) WindowID {
212			w.AddDialog(9, cover, 20, 10)
213			w.SetModal(9, true)
214			w.AddDialog(8, 9, 10, 5)
215			w.SetModal(8, true)
216			w.FocusID(cover)
217			return cover
218		}, 8, false},
219		{"stashed from a focused cover dialog", func(w *Workspace, cover WindowID) WindowID {
220			w.ToggleStashVisible()
221			w.AddDialog(9, cover, 20, 10)
222			w.FocusID(9)
223			return 2
224		}, 2, true},
225	}
226	for _, cv := range covers {
227		for _, tc := range cases {
228			t.Run(cv.name+"/"+tc.name, func(t *testing.T) {
229				_, w, cover := stashCoverCore(t, cv.overflow, cv.float)
230				id := tc.setup(w, cover)
231				// A dialog of the hidden stash waits hidden under the cover.
232				if i := slices.IndexFunc(w.Floats, func(f Float) bool { return f.over == 2 }); i >= 0 && !w.stashOverCover() {
233					if p := previewOf(t, w.Layout(), w.Floats[i].ID); !p.Hidden {
234						t.Fatalf("dialog of the hidden stash shown before activation: %+v", p)
235					}
236				}
237				view := w.View
238				w.Activate(id)
239				if got, _ := w.Focused(); got != tc.focused || w.cover() != cover || w.left != 0 || w.stashOverCover() != tc.over {
240					t.Fatalf("focused %d (want %d) cover %d left %d over %v", got, tc.focused, w.cover(), w.left, w.stashOverCover())
241				}
242				ps := w.Layout()
243				if p := previewOf(t, ps, cover); p.Hidden || !p.Fullscreen {
244					t.Fatalf("cover %+v", p)
245				}
246				f := slices.IndexFunc(ps, isID(tc.focused))
247				if f < slices.IndexFunc(ps, isID(cover)) || ps[f].Hidden {
248					t.Fatalf("focused window not over the cover: %+v", ps)
249				}
250				if tc.over && f < slices.IndexFunc(ps, isID(2)) {
251					t.Fatalf("focused window under the stash: %+v", ps)
252				}
253				if !cv.float && w.View != view {
254					t.Fatalf("view %d, was %d", w.View, view)
255				}
256			})
257		}
258	}
259}
260
261// After an activation shows the stash over the cover, hiding the stash
262// or activating the cover returns to it; a dialog of a stashed window,
263// modal or mapping meanwhile, shows with the focus and closing it
264// returns to the stash over the cover.
265func TestActivateOverCoverReturns(t *testing.T) {
266	for _, cv := range []struct {
267		name     string
268		overflow Overflow
269		float    bool
270	}{
271		{"fixed tile", OverflowFixed, false},
272		{"scroll tile", OverflowScroll, false},
273		{"fixed float", OverflowFixed, true},
274		{"scroll float", OverflowScroll, true},
275	} {
276		t.Run(cv.name, func(t *testing.T) {
277			_, w, cover := stashCoverCore(t, cv.overflow, cv.float)
278			w.Activate(2)
279			w.ToggleStashVisible()
280			if id, _ := w.Focused(); id != cover || w.stashShown() || w.fullscreen != cover {
281				t.Fatalf("hide: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen)
282			}
283			w.Activate(2)
284			w.Activate(cover)
285			if id, _ := w.Focused(); id != cover || w.stashShown() || w.fullscreen != cover {
286				t.Fatalf("activate cover: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen)
287			}
288			w.AddDialog(7, 2, 20, 10)
289			w.SetModal(7, true)
290			w.FocusID(cover)
291			w.Activate(2)
292			w.RemoveWindow(7)
293			if id, _ := w.Focused(); id != 2 || !w.stashOverCover() {
294				t.Fatalf("modal closed: focused %d over %v", id, w.stashOverCover())
295			}
296			w.AddDialog(9, 2, 20, 10)
297			w.AddDialog(8, 9, 10, 5)
298			if id, _ := w.Focused(); id != 8 || previewOf(t, w.Layout(), 8).Hidden {
299				t.Fatalf("mapped chain: focused %d", id)
300			}
301			w.RemoveWindow(8)
302			w.RemoveWindow(9)
303			if id, _ := w.Focused(); id != 2 || !w.stashOverCover() {
304				t.Fatalf("closed: focused %d over %v", id, w.stashOverCover())
305			}
306			// The cover closes: the stash stays shown and focused, the
307			// override ends with it.
308			w.RemoveWindow(cover)
309			if id, _ := w.Focused(); id != 2 || w.stashOver != 0 || !w.stashShown() {
310				t.Fatalf("cover closed: focused %d over %d shown %v", id, w.stashOver, w.stashShown())
311			}
312		})
313	}
314}
315
316// Activating a dialog that cannot show over the cover leaves fullscreen
317// as any other window does; a dialog of a stashed window whose own
318// fullscreen waited while hidden brings that window fullscreen, as
319// activating the window itself does.
320func TestActivateDialogLeavesCover(t *testing.T) {
321	t.Run("dialog of a plain float", func(t *testing.T) {
322		_, w, cover := stashCoverCore(t, OverflowFixed, false)
323		w.AddFloating(5, 20, 10)
324		w.AddDialog(9, 5, 10, 5)
325		w.FocusID(cover)
326		w.Activate(9)
327		if id, _ := w.Focused(); id != 9 || w.fullscreen != 0 || w.left != cover {
328			t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left)
329		}
330	})
331	t.Run("dialog of another tile", func(t *testing.T) {
332		_, w, cover := stashCoverCore(t, OverflowFixed, true)
333		w.AddDialog(9, 1, 10, 5)
334		w.FocusID(cover)
335		w.Activate(9)
336		if id, _ := w.Focused(); id != 9 || w.fullscreen != 0 || w.left != cover {
337			t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left)
338		}
339	})
340	t.Run("dialog of a stashed window with a pending fullscreen", func(t *testing.T) {
341		_, w, cover := stashCoverCore(t, OverflowFixed, true)
342		w.SetFullscreen(2, true)
343		w.AddDialog(9, 2, 10, 5)
344		w.FocusID(cover)
345		w.Activate(9)
346		if id, _ := w.Focused(); id != 9 || w.fullscreen != 2 || w.stashHidden || w.left != cover {
347			t.Fatalf("focused %d fullscreen %d hidden %v left %d", id, w.fullscreen, w.stashHidden, w.left)
348		}
349		if p := previewOf(t, w.Layout(), 9); p.Hidden {
350			t.Fatalf("dialog hidden: %+v", p)
351		}
352	})
353}
354
355// A dialog parent loop under a cover ends: neither dialog shows over it.
356func TestOverCoverDialogLoop(t *testing.T) {
357	_, w, _ := stashCoverCore(t, OverflowFixed, true)
358	w.AddFloating(8, 10, 5)
359	w.AddFloating(9, 10, 5)
360	w.SetDialogParent(8, 9)
361	w.SetDialogParent(9, 8)
362	ps := w.Layout()
363	if !previewOf(t, ps, 8).Hidden || !previewOf(t, ps, 9).Hidden {
364		t.Fatalf("looped dialogs shown: %+v", ps)
365	}
366}
367
368// Through core: activating a stashed window keeps a fullscreen game's
369// configure fullscreen at the output size.
370func TestCoreActivateStashKeepsGameConfigure(t *testing.T) {
371	cfg := ports.Config{}
372	cfg.Keyboard.CmdKey = "super"
373	cfg.Layout.MaxColumns = 2
374	commands := make(chan ports.ClientCommand, 256)
375	c, err := New(cfg, Channels{Commands: commands, Scenes: make(chan []ports.Scene, 1)}, Options{})
376	if err != nil {
377		t.Fatal(err)
378	}
379	c.addScreen(ports.OutputInfo{Name: "A", Width: 100, Height: 80})
380	m := c.screens[0].mon
381	m.AddWindow(2)
382	w := m.Current()
383	w.ToggleWindowStash()
384	w.ToggleStashVisible()
385	m.AddFloating(4, 100, 80)
386	m.SetFullscreen(4, true)
387	ctx := context.Background()
388	if err := c.activate(ctx, 2); err != nil {
389		t.Fatal(err)
390	}
391	if err := c.publish(ctx); err != nil {
392		t.Fatal(err)
393	}
394	var last *ports.ConfigureWindow
395	for len(commands) > 0 {
396		if v, ok := (<-commands).(ports.ConfigureWindow); ok && v.ID == 4 {
397			last = &v
398		}
399	}
400	if last == nil || !last.Fullscreen || last.Width != 100 || last.Height != 80 {
401		t.Fatalf("game configure %+v", last)
402	}
403}
404
405func isID(id WindowID) func(Placement) bool {
406	return func(p Placement) bool { return p.ID == id }
407}

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