v0.4.0 b6be3a8

floating_test.go RawHistoryFolder

1package core
2
3import (
4	"context"
5	"slices"
6	"testing"
7
8	"github.com/bnema/neferwl/internal/ports"
9)
10
11func TestToggleWindowStash(t *testing.T) {
12	for _, stacked := range []bool{false, true} {
13		t.Run(map[bool]string{false: "column", true: "stack"}[stacked], func(t *testing.T) {
14			w := workspace()
15			w.border = 2
16			w.AddWindow(1)
17			w.AddWindow(2)
18			w.Columns[1].Width = Width{Pixels: 35}
19			if stacked {
20				w.Columns[1].Windows = []WindowID{3, 2}
21				w.Columns[1].Focus = 1
22			}
23			w.Apply(ActionToggleWindowStash)
24			if len(w.Stash) != 1 || len(w.Floats) != 0 || !w.Stash[0].back.holdsStack(Column{Windows: []WindowID{3}}) && stacked {
25				t.Fatalf("stash %+v", w.Stash)
26			}
27			p := placement(w, 2)
28			if !p.Focused || !p.Floating || p.Rect != (Rect{X: 10, Y: 8, W: 80, H: 64}) {
29				t.Fatalf("placement %+v", p)
30			}
31			w.ResizeFloating(2, 12, 14)
32			if placement(w, 2).Rect != p.Rect {
33				t.Fatal("client resize moved stashed window")
34			}
35			w.Apply(ActionToggleWindowStash)
36			id, _ := w.Focused()
37			if id != 2 || len(w.Stash) != 0 || w.Focus != 1 || w.Columns[1].Width != (Width{Pixels: 35}) || !slices.Contains(w.Columns[1].Windows, 2) {
38				t.Fatalf("round trip: %+v", w)
39			}
40		})
41	}
42	w := workspace()
43	w.AddFloating(4, 20, 10)
44	w.Apply(ActionToggleWindowStash)
45	if len(w.Floats) != 0 || len(w.Columns) != 1 || w.Columns[0].Windows[0] != 4 {
46		t.Fatalf("native float: %+v", w)
47	}
48}
49
50// placement returns the placement of id in the workspace layout.
51func placement(w *Workspace, id WindowID) Placement {
52	for _, p := range w.Layout() {
53		if p.ID == id {
54			return p
55		}
56	}
57	return Placement{}
58}
59
60// The stash is a strip: new windows go last, selected and centred; the
61// neighbors peek in beside it, stash.gap away; focus moves inside it and stops at its
62// ends; up and down do nothing there.
63func TestStashStrip(t *testing.T) {
64	w := workspace()
65	w.stashGap = 5
66	for id := WindowID(1); id <= 4; id++ {
67		w.AddWindow(id)
68	}
69	for _, id := range []WindowID{4, 3, 2} {
70		w.FocusID(id)
71		w.Apply(ActionToggleWindowStash)
72	}
73	if got := []WindowID{w.Stash[0].ID, w.Stash[1].ID, w.Stash[2].ID}; !slices.Equal(got, []WindowID{4, 3, 2}) {
74		t.Fatalf("order %v", got)
75	}
76	if id, _ := w.Focused(); id != 2 || w.stashAt != 2 {
77		t.Fatalf("focused %d at %d", id, w.stashAt)
78	}
79	center := Rect{X: 10, Y: 8, W: 80, H: 64}
80	if p := placement(w, 2); p.Rect != center || p.Peek || p.Hidden {
81		t.Fatalf("selected %+v", p)
82	}
83	if p := placement(w, 3); !p.Peek || p.Rect != (Rect{X: 5 - 80, Y: 8, W: 80, H: 64}) {
84		t.Fatalf("left peek %+v", p)
85	}
86	if p := placement(w, 4); !p.Hidden {
87		t.Fatalf("far window %+v", p)
88	}
89	w.Apply(ActionFocusColumnRight)
90	if id, _ := w.Focused(); id != 2 {
91		t.Fatalf("moved past the end: %d", id)
92	}
93	w.Apply(ActionFocusColumnLeft)
94	if p := placement(w, 2); !p.Peek || p.Rect != (Rect{X: 95, Y: 8, W: 80, H: 64}) {
95		t.Fatalf("right peek %+v", p)
96	}
97	w.Apply(ActionFocusColumnLeft)
98	w.Apply(ActionFocusColumnLeft)
99	w.Apply(ActionFocusWindowUp)
100	if id, _ := w.Focused(); id != 4 {
101		t.Fatalf("focused %d, want the first", id)
102	}
103	// Column actions do nothing while the stash has the focus.
104	w.Apply(ActionMoveColumnLeft)
105	w.Apply(ActionCycleColumnWidth)
106	if w.Columns[0].Windows[0] != 1 || w.Columns[0].Width != (Width{}) {
107		t.Fatalf("column changed: %+v", w.Columns)
108	}
109	// A gap as wide as the margin leaves nothing to show: neighbors wait
110	// off screen.
111	w.stashGap = 10
112	if p := placement(w, 3); !p.Hidden || p.Peek {
113		t.Fatalf("no peek: %+v", p)
114	}
115}
116
117// Hiding the stash is the way out: tiles get the focus, a new stashed
118// window shows it again, and the native floats stay as they are.
119func TestToggleStashVisible(t *testing.T) {
120	w := workspace()
121	w.Apply(ActionToggleStashVisible)
122	w.AddWindow(1)
123	w.AddWindow(2)
124	w.AddWindow(3)
125	w.Apply(ActionToggleWindowStash)
126	w.FocusID(2)
127	w.Apply(ActionToggleWindowStash)
128	w.Apply(ActionFocusColumnLeft)
129	w.Apply(ActionToggleStashVisible)
130	if id, _ := w.Focused(); id != 1 || !w.stashHidden {
131		t.Fatalf("focused %d hidden %v", id, w.stashHidden)
132	}
133	for _, id := range []WindowID{2, 3} {
134		if p := placement(w, id); !p.Hidden || p.Focused {
135			t.Fatalf("hidden %+v", p)
136		}
137	}
138	if w.FocusID(2) {
139		t.Fatal("focused hidden stash window")
140	}
141	// Shown from a tile: the stash takes the focus on its selected window.
142	w.Apply(ActionToggleStashVisible)
143	if id, _ := w.Focused(); id != 3 || w.stashHidden {
144		t.Fatalf("restored focus %d", id)
145	}
146	w.Apply(ActionToggleStashVisible)
147	w.FocusID(1)
148	w.Apply(ActionToggleWindowStash)
149	if id, _ := w.Focused(); id != 1 || w.stashHidden || w.stashAt != 2 {
150		t.Fatalf("new stashed window: focused %d hidden %v", id, w.stashHidden)
151	}
152	// A native dialog stays when the stash hides.
153	w.AddFloating(9, 10, 10)
154	w.Apply(ActionToggleStashVisible)
155	if p := placement(w, 9); p.Hidden || !w.stashHidden {
156		t.Fatalf("dialog hidden with the stash: %+v", p)
157	}
158	if id, _ := w.Focused(); id != 9 {
159		t.Fatalf("focused %d, want the dialog", id)
160	}
161}
162
163// Closing the selected stashed window selects its right neighbor, else its
164// left one, else gives the focus back to the tiles.
165func TestStashClose(t *testing.T) {
166	w := workspace()
167	for id := WindowID(1); id <= 4; id++ {
168		w.AddWindow(id)
169	}
170	for _, id := range []WindowID{4, 3, 2} {
171		w.FocusID(id)
172		w.Apply(ActionToggleWindowStash)
173	}
174	w.FocusID(3) // the middle one
175	w.RemoveWindow(3)
176	if id, _ := w.Focused(); id != 2 {
177		t.Fatalf("after middle: %d", id)
178	}
179	w.RemoveWindow(2)
180	if id, _ := w.Focused(); id != 4 {
181		t.Fatalf("after last: %d", id)
182	}
183	w.RemoveWindow(4)
184	if id, _ := w.Focused(); id != 1 || len(w.Stash) != 0 || w.stashFocus {
185		t.Fatalf("after all: %d", id)
186	}
187}
188
189// A fullscreen window is exclusive: a new window waits hidden without
190// moving the view, in scroll and fixed overflow, and shows once it leaves.
191func TestExclusiveFullscreenNewWindow(t *testing.T) {
192	for _, overflow := range []Overflow{OverflowScroll, OverflowFixed} {
193		w := workspace()
194		w.Overflow = overflow
195		w.AddWindow(1)
196		w.SetFullscreen(1, true)
197		w.AddWindow(2)
198		for _, p := range w.Layout() {
199			if p.ID == 1 && (!p.Fullscreen || !p.Focused || p.Hidden) || p.ID == 2 && !p.Hidden {
200				t.Fatalf("overflow %v: %+v", overflow, p)
201			}
202		}
203		if w.ToggleStashVisible(); w.stashHidden {
204			t.Fatal("floats toggled under fullscreen")
205		}
206		w.SetFullscreen(1, false)
207		for _, p := range w.Layout() {
208			if p.ID == 2 && p.Hidden {
209				t.Fatalf("overflow %v: new window still hidden", overflow)
210			}
211		}
212	}
213}
214
215// A window mapped under a covering fullscreen float with no column waits
216// hidden as the first column.
217func TestFullscreenFloatNewWindowNoColumns(t *testing.T) {
218	w := workspace()
219	w.AddFloating(1, 300, 200)
220	w.SetFullscreen(1, true)
221	w.AddWindow(2)
222	if len(w.Columns) != 1 || w.Columns[0].Windows[0] != 2 {
223		t.Fatalf("columns %+v", w.Columns)
224	}
225	if id, _ := w.Focused(); id != 1 {
226		t.Fatalf("focus %d", id)
227	}
228}
229
230// Under a covering fullscreen float, or any fixed-overflow fullscreen,
231// binds act on the window on screen, never on a column it hides.
232func TestFullscreenPinsFocus(t *testing.T) {
233	w := workspace()
234	w.AddWindow(1)
235	w.AddWindow(2)
236	w.AddFloating(3, 20, 10)
237	w.SetFullscreen(3, true)
238	w.focusCover()
239	w.Apply(ActionMoveColumnLeft)
240	w.Apply(ActionCycleColumnWidth)
241	if id, _ := w.Focused(); id != 3 || w.fullscreen != 3 || w.Columns[0].Windows[0] != 1 || w.Columns[1].Width != (Width{}) {
242		t.Fatalf("focused %d fullscreen %d columns %+v", id, w.fullscreen, w.Columns)
243	}
244	if e := w.Apply(ActionCloseWindow); e.Close != 3 {
245		t.Fatalf("close %d, want the fullscreen window", e.Close)
246	}
247}
248
249// A focus move from a covering fullscreen window leaves fullscreen for the
250// window on that side; with none there, fullscreen stays.
251func TestFocusMoveLeavesFullscreen(t *testing.T) {
252	w := workspace()
253	w.AddWindow(1)
254	w.AddWindow(2)
255	w.AddFloating(3, 20, 10)
256	w.SetFullscreen(3, true)
257	w.FocusColumn(-1)
258	if id, _ := w.Focused(); id == 3 || w.fullscreen != 0 {
259		t.Fatalf("float: focused %d fullscreen %d", id, w.fullscreen)
260	}
261	f := workspace()
262	f.Overflow = OverflowFixed
263	f.AddWindow(1)
264	f.AddWindow(2)
265	f.SetFullscreen(2, true)
266	if f.FocusColumn(1); f.fullscreen != 2 || f.Focus != 1 {
267		t.Fatalf("fixed edge: focus %d fullscreen %d", f.Focus, f.fullscreen)
268	}
269	if f.FocusColumn(-1); f.fullscreen != 0 || f.Focus != 0 {
270		t.Fatalf("fixed: focus %d fullscreen %d", f.Focus, f.fullscreen)
271	}
272	// Side by side: nothing above, so up keeps fullscreen and reports
273	// the edge (the monitor goes to the workspace above).
274	f.Focus = 1
275	f.SetFullscreen(2, true)
276	if f.FocusWindow(-1) || f.fullscreen != 2 || f.Focus != 1 {
277		t.Fatalf("fixed up: focus %d fullscreen %d", f.Focus, f.fullscreen)
278	}
279	// Stacked: up reaches the window above in the column.
280	f.SetFullscreen(2, false)
281	f.stackWindow(2, 1)
282	f.FocusID(2)
283	f.SetFullscreen(2, true)
284	if !f.FocusWindow(-1) || f.fullscreen != 0 {
285		t.Fatalf("stacked up: fullscreen %d", f.fullscreen)
286	}
287	if id, _ := f.Focused(); id != 1 {
288		t.Fatalf("stacked up: focused %d", id)
289	}
290	// Scroll overflow: moving off a fullscreen column still works; it no
291	// longer covers.
292	s := workspace()
293	s.AddWindow(1)
294	s.AddWindow(2)
295	s.SetFullscreen(2, true)
296	if s.FocusColumn(-1); s.Focus != 0 || s.cover() != 0 {
297		t.Fatalf("scroll: focus %d cover %d", s.Focus, s.cover())
298	}
299}
300
301// A fullscreen window does not float until it leaves fullscreen.
302func TestToggleWindowStashIgnoresFullscreen(t *testing.T) {
303	w := workspace()
304	w.AddWindow(1)
305	w.SetFullscreen(1, true)
306	w.Apply(ActionToggleWindowStash)
307	if len(w.Stash) != 0 || w.fullscreen != 1 {
308		t.Fatalf("fullscreen window floated: %+v", w)
309	}
310	w.SetFullscreen(1, false)
311	w.Apply(ActionToggleWindowStash)
312	if len(w.Stash) != 1 {
313		t.Fatal("window did not float after leaving fullscreen")
314	}
315}
316
317// A lone maximized column comes back maximized.
318func TestToggleWindowStashKeepsFullWidth(t *testing.T) {
319	w := workspace()
320	w.Overflow = OverflowFixed
321	w.AddWindow(1)
322	w.AddWindow(2)
323	w.Apply(ActionMaximizeColumn)
324	w.Apply(ActionToggleWindowStash)
325	w.Apply(ActionToggleWindowStash)
326	if id, _ := w.Focused(); id != 2 || !w.Columns[w.Focus].FullWidth {
327		t.Fatalf("focused %d, column %+v", id, w.Columns[w.Focus])
328	}
329}
330
331// A lone expanded column (fixed overflow) comes back expanded.
332func TestToggleWindowStashKeepsExpanded(t *testing.T) {
333	w := workspace()
334	w.Overflow = OverflowFixed
335	w.SetMaxColumns(3)
336	w.AddWindow(1)
337	w.AddWindow(2)
338	w.CycleWidth()
339	if !w.Columns[w.Focus].Expanded {
340		t.Fatal("column not expanded")
341	}
342	w.Apply(ActionToggleWindowStash)
343	w.Apply(ActionToggleWindowStash)
344	if id, _ := w.Focused(); id != 2 || !w.Columns[w.Focus].Expanded {
345		t.Fatalf("focused %d, column %+v", id, w.Columns[w.Focus])
346	}
347}
348
349// A client fullscreen request of a hidden stashed window leaves it hidden
350// on its workspace, even in fixed overflow (which moves fullscreen windows
351// out).
352func TestHiddenFloatFullscreenRequest(t *testing.T) {
353	m := newMonitor("", "")
354	m.SetOutput(100, 80)
355	m.SetOverflow(OverflowFixed)
356	m.AddWindow(1)
357	m.AddWindow(2)
358	w := m.Current()
359	w.ToggleWindowStash()
360	w.ToggleStashVisible()
361	m.SetFullscreen(2, true)
362	if m.Current() != w || w.stashIndex(2) < 0 || !w.stashHidden || w.fullscreen != 0 {
363		t.Fatalf("hidden stash left or showed: %+v", w)
364	}
365	w.ToggleStashVisible()
366	if w.fullscreen != 2 {
367		t.Fatalf("fullscreen lost on show: %d", w.fullscreen)
368	}
369}
370
371// A fullscreen stashed window (a game in scanout) keeps the stash shown:
372// hiding it would drop scanout and VRR.
373func TestToggleStashVisibleFullscreen(t *testing.T) {
374	w := workspace()
375	w.AddWindow(1)
376	w.AddWindow(2)
377	w.ToggleWindowStash()
378	w.SetFullscreen(2, true)
379	w.Apply(ActionToggleStashVisible)
380	if id, _ := w.Focused(); id != 2 || w.fullscreen != 2 || w.stashHidden {
381		t.Fatalf("fullscreen stash hidden: focused %d fullscreen %d", id, w.fullscreen)
382	}
383}
384
385func TestFloatingSceneDimAndConfigure(t *testing.T) {
386	cfg := ports.Config{}
387	cfg.Keyboard.CmdKey = "super"
388	cfg.Layout.MaxColumns = 2
389	cfg.Floating.Dim = 0.3
390	cfg.Border.Width = 2
391	commands := make(chan ports.ClientCommand, 128)
392	scenes := make(chan []ports.Scene, 1)
393	c, err := New(cfg, Channels{Commands: commands, Scenes: scenes})
394	if err != nil {
395		t.Fatal(err)
396	}
397	w := c.cur().mon.Current()
398	c.cur().mon.SetOutput(100, 80)
399	w.AddWindow(1)
400	ctx := context.Background()
401	check := func(want float64) {
402		t.Helper()
403		if err := c.publish(ctx); err != nil {
404			t.Fatal(err)
405		}
406		if s := (<-scenes)[0]; s.Dim != want {
407			t.Fatalf("dim %v want %v", s.Dim, want)
408		}
409	}
410	check(0)
411	w.ToggleWindowStash()
412	check(0.3)
413	found := false
414	for len(commands) > 0 {
415		if v, ok := (<-commands).(ports.ConfigureWindow); ok && v.ID == 1 {
416			if v.Floating {
417				found = true
418				if v.Width != 76 || v.Height != 60 {
419					t.Fatalf("imposed configure %+v", v)
420				}
421			}
422		}
423	}
424	if !found {
425		t.Fatal("missing imposed configure")
426	}
427	w.AddFloating(2, 20, 10)
428	check(0.3)
429	found = false
430	for len(commands) > 0 {
431		if v, ok := (<-commands).(ports.ConfigureWindow); ok && v.ID == 2 {
432			found = true
433			if v.Width != 0 || v.Height != 0 {
434				t.Fatalf("native configure %+v", v)
435			}
436		}
437	}
438	if !found {
439		t.Fatal("missing native configure")
440	}
441	// Hiding the stash leaves the dialog, and its veil.
442	w.ToggleStashVisible()
443	check(0.3)
444	w.RemoveWindow(2)
445	check(0)
446	w.ToggleStashVisible()
447	check(0.3)
448	w.AddFloating(2, 20, 10)
449	w.SetFullscreen(1, true)
450	check(0)
451	w.SetFullscreen(1, false)
452	w.ToggleWindowStash()
453	w.SetFullscreen(1, true)
454	check(0)
455	// A dialog of a fullscreen tile waits hidden: no veil over the game.
456	w.SetFullscreen(1, false)
457	w.RemoveWindow(1)
458	w.RemoveWindow(2)
459	w.AddWindow(3)
460	w.SetFullscreen(3, true)
461	check(0)
462	w.AddFloating(4, 20, 10)
463	check(0)
464	w.SetFullscreen(3, false)
465	check(0.3)
466}
467
468// Activating a hidden stashed window shows the stash and focuses it; the
469// fullscreen window it hid behind leaves fullscreen.
470func TestActivateHiddenFloat(t *testing.T) {
471	w := workspace()
472	w.AddWindow(1)
473	w.AddWindow(2)
474	w.ToggleWindowStash()
475	w.ToggleStashVisible()
476	w.FocusID(1)
477	w.SetFullscreen(1, true)
478	w.Activate(2)
479	if id, _ := w.Focused(); id != 2 || w.stashHidden || w.fullscreen != 0 {
480		t.Fatalf("focused %d hidden %v fullscreen %d", id, w.stashHidden, w.fullscreen)
481	}
482	// The float's own fullscreen, asked while hidden, comes back with it.
483	w.FocusID(2)
484	w.ToggleStashVisible()
485	w.SetFullscreen(2, true)
486	w.SetFullscreen(1, true)
487	w.Activate(2)
488	if id, _ := w.Focused(); id != 2 || w.stashHidden || w.fullscreen != 2 {
489		t.Fatalf("own fullscreen: focused %d hidden %v fullscreen %d", id, w.stashHidden, w.fullscreen)
490	}
491}

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