feat/data-dnd e05cb15

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		// A dialog filling the usable area is a covering float: it stays
207		// over the stash all the same.
208		{"full-size dialog of stashed", func(w *Workspace, _ WindowID) WindowID {
209			w.AddDialog(9, 2, 100, 80)
210			return 9
211		}, 9, true},
212		{"dialog of cover", func(w *Workspace, cover WindowID) WindowID {
213			w.AddDialog(9, cover, 20, 10)
214			w.FocusID(cover)
215			return 9
216		}, 9, false},
217		{"modal chain of cover", func(w *Workspace, cover WindowID) WindowID {
218			w.AddDialog(9, cover, 20, 10)
219			w.SetModal(9, true)
220			w.AddDialog(8, 9, 10, 5)
221			w.SetModal(8, true)
222			w.FocusID(cover)
223			return cover
224		}, 8, false},
225		{"stashed from a focused cover dialog", func(w *Workspace, cover WindowID) WindowID {
226			w.ToggleStashVisible()
227			w.AddDialog(9, cover, 20, 10)
228			w.FocusID(9)
229			return 2
230		}, 2, true},
231	}
232	for _, cv := range covers {
233		for _, tc := range cases {
234			t.Run(cv.name+"/"+tc.name, func(t *testing.T) {
235				_, w, cover := stashCoverCore(t, cv.overflow, cv.float)
236				id := tc.setup(w, cover)
237				// A dialog of the hidden stash waits hidden under the cover.
238				if i := slices.IndexFunc(w.Floats, func(f Float) bool { return f.over == 2 }); i >= 0 && !w.stashOverCover() {
239					if p := previewOf(t, w.Layout(), w.Floats[i].ID); !p.Hidden {
240						t.Fatalf("dialog of the hidden stash shown before activation: %+v", p)
241					}
242				}
243				view := w.View
244				w.Activate(id)
245				if got, _ := w.Focused(); got != tc.focused || w.cover() != cover || w.left != 0 || w.stashOverCover() != tc.over {
246					t.Fatalf("focused %d (want %d) cover %d left %d over %v", got, tc.focused, w.cover(), w.left, w.stashOverCover())
247				}
248				ps := w.Layout()
249				if p := previewOf(t, ps, cover); p.Hidden || !p.Fullscreen {
250					t.Fatalf("cover %+v", p)
251				}
252				f := slices.IndexFunc(ps, isID(tc.focused))
253				if f < slices.IndexFunc(ps, isID(cover)) || ps[f].Hidden {
254					t.Fatalf("focused window not over the cover: %+v", ps)
255				}
256				if tc.over && f < slices.IndexFunc(ps, isID(2)) {
257					t.Fatalf("focused window under the stash: %+v", ps)
258				}
259				if !cv.float && w.View != view {
260					t.Fatalf("view %d, was %d", w.View, view)
261				}
262			})
263		}
264	}
265}
266
267// After an activation shows the stash over the cover, hiding the stash
268// or activating the cover returns to it; a dialog of a stashed window,
269// modal or mapping meanwhile, shows with the focus and closing it
270// returns to the stash over the cover.
271func TestActivateOverCoverReturns(t *testing.T) {
272	for _, cv := range []struct {
273		name     string
274		overflow Overflow
275		float    bool
276	}{
277		{"fixed tile", OverflowFixed, false},
278		{"scroll tile", OverflowScroll, false},
279		{"fixed float", OverflowFixed, true},
280		{"scroll float", OverflowScroll, true},
281	} {
282		t.Run(cv.name, func(t *testing.T) {
283			_, w, cover := stashCoverCore(t, cv.overflow, cv.float)
284			w.Activate(2)
285			w.ToggleStashVisible()
286			if id, _ := w.Focused(); id != cover || w.stashShown() || w.fullscreen != cover {
287				t.Fatalf("hide: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen)
288			}
289			w.Activate(2)
290			w.Activate(cover)
291			if id, _ := w.Focused(); id != cover || w.stashShown() || w.fullscreen != cover {
292				t.Fatalf("activate cover: focused %d shown %v fullscreen %d", id, w.stashShown(), w.fullscreen)
293			}
294			w.AddDialog(7, 2, 20, 10)
295			w.SetModal(7, true)
296			w.FocusID(cover)
297			w.Activate(2)
298			w.RemoveWindow(7)
299			if id, _ := w.Focused(); id != 2 || !w.stashOverCover() {
300				t.Fatalf("modal closed: focused %d over %v", id, w.stashOverCover())
301			}
302			w.AddDialog(9, 2, 20, 10)
303			w.AddDialog(8, 9, 10, 5)
304			if id, _ := w.Focused(); id != 8 || previewOf(t, w.Layout(), 8).Hidden {
305				t.Fatalf("mapped chain: focused %d", id)
306			}
307			w.RemoveWindow(8)
308			w.RemoveWindow(9)
309			if id, _ := w.Focused(); id != 2 || !w.stashOverCover() {
310				t.Fatalf("closed: focused %d over %v", id, w.stashOverCover())
311			}
312			// The cover closes: the stash stays shown and focused, the
313			// override ends with it.
314			w.RemoveWindow(cover)
315			if id, _ := w.Focused(); id != 2 || w.stashOver != 0 || !w.stashShown() {
316				t.Fatalf("cover closed: focused %d over %d shown %v", id, w.stashOver, w.stashShown())
317			}
318		})
319	}
320}
321
322// Activating a dialog that cannot show over the cover leaves fullscreen
323// as any other window does; a dialog of a stashed window whose own
324// fullscreen waited while hidden brings that window fullscreen, as
325// activating the window itself does.
326func TestActivateDialogLeavesCover(t *testing.T) {
327	t.Run("dialog of a plain float", func(t *testing.T) {
328		_, w, cover := stashCoverCore(t, OverflowFixed, false)
329		w.AddFloating(5, 20, 10)
330		w.AddDialog(9, 5, 10, 5)
331		w.FocusID(cover)
332		w.Activate(9)
333		if id, _ := w.Focused(); id != 9 || w.fullscreen != 0 || w.left != cover {
334			t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left)
335		}
336	})
337	t.Run("dialog of another tile", func(t *testing.T) {
338		_, w, cover := stashCoverCore(t, OverflowFixed, true)
339		w.AddDialog(9, 1, 10, 5)
340		w.FocusID(cover)
341		w.Activate(9)
342		if id, _ := w.Focused(); id != 9 || w.fullscreen != 0 || w.left != cover {
343			t.Fatalf("focused %d fullscreen %d left %d", id, w.fullscreen, w.left)
344		}
345	})
346	t.Run("dialog of a stashed window with a pending fullscreen", func(t *testing.T) {
347		_, w, cover := stashCoverCore(t, OverflowFixed, true)
348		w.SetFullscreen(2, true)
349		w.AddDialog(9, 2, 10, 5)
350		w.FocusID(cover)
351		w.Activate(9)
352		if id, _ := w.Focused(); id != 9 || w.fullscreen != 2 || w.stashHidden || w.left != cover {
353			t.Fatalf("focused %d fullscreen %d hidden %v left %d", id, w.fullscreen, w.stashHidden, w.left)
354		}
355		if p := previewOf(t, w.Layout(), 9); p.Hidden {
356			t.Fatalf("dialog hidden: %+v", p)
357		}
358	})
359}
360
361// A dialog parent loop under a cover ends: neither dialog shows over it.
362func TestOverCoverDialogLoop(t *testing.T) {
363	_, w, _ := stashCoverCore(t, OverflowFixed, true)
364	w.AddFloating(8, 10, 5)
365	w.AddFloating(9, 10, 5)
366	w.SetDialogParent(8, 9)
367	w.SetDialogParent(9, 8)
368	ps := w.Layout()
369	if !previewOf(t, ps, 8).Hidden || !previewOf(t, ps, 9).Hidden {
370		t.Fatalf("looped dialogs shown: %+v", ps)
371	}
372}
373
374// Through core: activating a stashed window keeps a fullscreen game's
375// configure fullscreen at the output size.
376func TestCoreActivateStashKeepsGameConfigure(t *testing.T) {
377	cfg := ports.Config{}
378	cfg.Keyboard.CmdKey = "super"
379	cfg.Layout.MaxColumns = 2
380	commands := make(chan ports.ClientCommand, 256)
381	c, err := New(cfg, Channels{Commands: commands, Scenes: make(chan []ports.Scene, 1)}, Options{})
382	if err != nil {
383		t.Fatal(err)
384	}
385	c.addScreen(ports.OutputInfo{Name: "A", Width: 100, Height: 80})
386	m := c.screens[0].mon
387	m.AddWindow(2)
388	w := m.Current()
389	w.ToggleWindowStash()
390	w.ToggleStashVisible()
391	m.AddFloating(4, 100, 80)
392	m.SetFullscreen(4, true)
393	ctx := context.Background()
394	if err := c.activate(ctx, 2); err != nil {
395		t.Fatal(err)
396	}
397	if err := c.publish(ctx); err != nil {
398		t.Fatal(err)
399	}
400	var last *ports.ConfigureWindow
401	for len(commands) > 0 {
402		if v, ok := (<-commands).(ports.ConfigureWindow); ok && v.ID == 4 {
403			last = &v
404		}
405	}
406	if last == nil || !last.Fullscreen || last.Width != 100 || last.Height != 80 {
407		t.Fatalf("game configure %+v", last)
408	}
409}
410
411func isID(id WindowID) func(Placement) bool {
412	return func(p Placement) bool { return p.ID == id }
413}

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