feat/hints a471fa4

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].Cells = 3
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].Cells != 3 || !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].Cells != 0 {
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// Neighbors show only in a margin that is left: with an odd width the gap
118// can match the narrower side, and the wider one must not show a sliver.
119func TestStashNeighborNeedsAMargin(t *testing.T) {
120	w := &Workspace{}
121	w.SetOutput(1001, 600)
122	w.SetMaxColumns(2)
123	w.stashGap = 10
124	for id := WindowID(1); id <= 3; id++ {
125		w.AddWindow(id)
126	}
127	for _, id := range []WindowID{3, 2} {
128		w.FocusID(id)
129		w.Apply(ActionToggleWindowStash)
130	}
131	w.FocusID(3)
132	if p := placement(w, 3); p.Rect.X != 100 || p.Rect.W != 800 {
133		t.Fatalf("setup: selected at %+v, want X 100 W 800", p.Rect)
134	}
135	for _, id := range []WindowID{2} {
136		if p := placement(w, id); !p.Hidden || p.Peek {
137			t.Fatalf("window %d shows as a sliver where no margin is left: %+v", id, p)
138		}
139	}
140}
141
142// Hiding the stash is the way out: tiles get the focus, a new stashed
143// window shows it again, and the native floats stay as they are.
144func TestToggleStashVisible(t *testing.T) {
145	w := workspace()
146	w.Apply(ActionToggleStashVisible)
147	w.AddWindow(1)
148	w.AddWindow(2)
149	w.AddWindow(3)
150	w.Apply(ActionToggleWindowStash)
151	w.FocusID(2)
152	w.Apply(ActionToggleWindowStash)
153	w.Apply(ActionFocusColumnLeft)
154	w.Apply(ActionToggleStashVisible)
155	if id, _ := w.Focused(); id != 1 || !w.stashHidden {
156		t.Fatalf("focused %d hidden %v", id, w.stashHidden)
157	}
158	for _, id := range []WindowID{2, 3} {
159		if p := placement(w, id); !p.Hidden || p.Focused {
160			t.Fatalf("hidden %+v", p)
161		}
162	}
163	if w.FocusID(2) {
164		t.Fatal("focused hidden stash window")
165	}
166	// Shown from a tile: the stash takes the focus on its selected window.
167	w.Apply(ActionToggleStashVisible)
168	if id, _ := w.Focused(); id != 3 || w.stashHidden {
169		t.Fatalf("restored focus %d", id)
170	}
171	w.Apply(ActionToggleStashVisible)
172	w.FocusID(1)
173	w.Apply(ActionToggleWindowStash)
174	if id, _ := w.Focused(); id != 1 || w.stashHidden || w.stashAt != 2 {
175		t.Fatalf("new stashed window: focused %d hidden %v", id, w.stashHidden)
176	}
177	// A native dialog stays when the stash hides.
178	w.AddFloating(9, 10, 10)
179	w.Apply(ActionToggleStashVisible)
180	if p := placement(w, 9); p.Hidden || !w.stashHidden {
181		t.Fatalf("dialog hidden with the stash: %+v", p)
182	}
183	if id, _ := w.Focused(); id != 9 {
184		t.Fatalf("focused %d, want the dialog", id)
185	}
186}
187
188// Closing the selected stashed window selects its right neighbor, else its
189// left one, else gives the focus back to the tiles.
190func TestStashClose(t *testing.T) {
191	w := workspace()
192	for id := WindowID(1); id <= 4; id++ {
193		w.AddWindow(id)
194	}
195	for _, id := range []WindowID{4, 3, 2} {
196		w.FocusID(id)
197		w.Apply(ActionToggleWindowStash)
198	}
199	w.FocusID(3) // the middle one
200	w.RemoveWindow(3)
201	if id, _ := w.Focused(); id != 2 {
202		t.Fatalf("after middle: %d", id)
203	}
204	w.RemoveWindow(2)
205	if id, _ := w.Focused(); id != 4 {
206		t.Fatalf("after last: %d", id)
207	}
208	w.RemoveWindow(4)
209	if id, _ := w.Focused(); id != 1 || len(w.Stash) != 0 || w.stashFocus {
210		t.Fatalf("after all: %d", id)
211	}
212}
213
214// A fullscreen window is exclusive: a new window waits hidden without
215// moving the view, in scroll and fixed overflow, and shows once it leaves.
216func TestExclusiveFullscreenNewWindow(t *testing.T) {
217	for _, overflow := range []Overflow{OverflowScroll, OverflowFixed} {
218		w := workspace()
219		w.Overflow = overflow
220		w.AddWindow(1)
221		w.SetFullscreen(1, true)
222		w.AddWindow(2)
223		for _, p := range w.Layout() {
224			if p.ID == 1 && (!p.Fullscreen || !p.Focused || p.Hidden) || p.ID == 2 && !p.Hidden {
225				t.Fatalf("overflow %v: %+v", overflow, p)
226			}
227		}
228		if w.ToggleStashVisible(); w.stashHidden {
229			t.Fatal("floats toggled under fullscreen")
230		}
231		w.SetFullscreen(1, false)
232		for _, p := range w.Layout() {
233			if p.ID == 2 && p.Hidden {
234				t.Fatalf("overflow %v: new window still hidden", overflow)
235			}
236		}
237	}
238}
239
240// A window mapped under a covering fullscreen float with no column waits
241// hidden as the first column.
242func TestFullscreenFloatNewWindowNoColumns(t *testing.T) {
243	w := workspace()
244	w.AddFloating(1, 300, 200)
245	w.SetFullscreen(1, true)
246	w.AddWindow(2)
247	if len(w.Columns) != 1 || w.Columns[0].Windows[0] != 2 {
248		t.Fatalf("columns %+v", w.Columns)
249	}
250	if id, _ := w.Focused(); id != 1 {
251		t.Fatalf("focus %d", id)
252	}
253}
254
255// Under a covering fullscreen float, or any fixed-overflow fullscreen,
256// binds act on the window on screen, never on a column it hides.
257func TestFullscreenPinsFocus(t *testing.T) {
258	w := workspace()
259	w.AddWindow(1)
260	w.AddWindow(2)
261	w.AddFloating(3, 20, 10)
262	w.SetFullscreen(3, true)
263	w.focusCover()
264	w.Apply(ActionMoveColumnLeft)
265	w.Apply(ActionCycleColumnWidth)
266	if id, _ := w.Focused(); id != 3 || w.fullscreen != 3 || w.Columns[0].Windows[0] != 1 || w.Columns[1].Cells != 0 {
267		t.Fatalf("focused %d fullscreen %d columns %+v", id, w.fullscreen, w.Columns)
268	}
269	if e := w.Apply(ActionCloseWindow); e.Close != 3 {
270		t.Fatalf("close %d, want the fullscreen window", e.Close)
271	}
272}
273
274// A focus move from a covering fullscreen window leaves fullscreen for the
275// window on that side; with none there, fullscreen stays.
276func TestFocusMoveLeavesFullscreen(t *testing.T) {
277	w := workspace()
278	w.AddWindow(1)
279	w.AddWindow(2)
280	w.AddFloating(3, 20, 10)
281	w.SetFullscreen(3, true)
282	w.FocusColumn(-1)
283	if id, _ := w.Focused(); id == 3 || w.fullscreen != 0 {
284		t.Fatalf("float: focused %d fullscreen %d", id, w.fullscreen)
285	}
286	f := workspace()
287	f.Overflow = OverflowFixed
288	f.AddWindow(1)
289	f.AddWindow(2)
290	f.SetFullscreen(2, true)
291	if f.FocusColumn(1); f.fullscreen != 2 || f.Focus != 1 {
292		t.Fatalf("fixed edge: focus %d fullscreen %d", f.Focus, f.fullscreen)
293	}
294	if f.FocusColumn(-1); f.fullscreen != 0 || f.Focus != 0 {
295		t.Fatalf("fixed: focus %d fullscreen %d", f.Focus, f.fullscreen)
296	}
297	// Side by side: nothing above, so up keeps fullscreen and reports
298	// the edge (the monitor goes to the workspace above).
299	f.Focus = 1
300	f.SetFullscreen(2, true)
301	if f.FocusWindow(-1) || f.fullscreen != 2 || f.Focus != 1 {
302		t.Fatalf("fixed up: focus %d fullscreen %d", f.Focus, f.fullscreen)
303	}
304	// Stacked: up reaches the window above in the column.
305	f.SetFullscreen(2, false)
306	f.stackWindow(2, 1)
307	f.FocusID(2)
308	f.SetFullscreen(2, true)
309	if !f.FocusWindow(-1) || f.fullscreen != 0 {
310		t.Fatalf("stacked up: fullscreen %d", f.fullscreen)
311	}
312	if id, _ := f.Focused(); id != 1 {
313		t.Fatalf("stacked up: focused %d", id)
314	}
315	// Scroll overflow: moving off a fullscreen column still works; it no
316	// longer covers.
317	s := workspace()
318	s.AddWindow(1)
319	s.AddWindow(2)
320	s.SetFullscreen(2, true)
321	if s.FocusColumn(-1); s.Focus != 0 || s.cover() != 0 {
322		t.Fatalf("scroll: focus %d cover %d", s.Focus, s.cover())
323	}
324}
325
326// A fullscreen window does not float until it leaves fullscreen.
327func TestToggleWindowStashIgnoresFullscreen(t *testing.T) {
328	w := workspace()
329	w.AddWindow(1)
330	w.SetFullscreen(1, true)
331	w.Apply(ActionToggleWindowStash)
332	if len(w.Stash) != 0 || w.fullscreen != 1 {
333		t.Fatalf("fullscreen window floated: %+v", w)
334	}
335	w.SetFullscreen(1, false)
336	w.Apply(ActionToggleWindowStash)
337	if len(w.Stash) != 1 {
338		t.Fatal("window did not float after leaving fullscreen")
339	}
340}
341
342// A lone maximized column comes back maximized.
343func TestToggleWindowStashKeepsFullWidth(t *testing.T) {
344	w := workspace()
345	w.Overflow = OverflowFixed
346	w.AddWindow(1)
347	w.AddWindow(2)
348	w.Apply(ActionMaximizeColumn)
349	w.Apply(ActionToggleWindowStash)
350	w.Apply(ActionToggleWindowStash)
351	if id, _ := w.Focused(); id != 2 || !w.Columns[w.Focus].FullWidth {
352		t.Fatalf("focused %d, column %+v", id, w.Columns[w.Focus])
353	}
354}
355
356// A lone wide column (fixed overflow) comes back at its width in cells.
357func TestToggleWindowStashKeepsCells(t *testing.T) {
358	w := workspace()
359	w.Overflow = OverflowFixed
360	w.SetMaxColumns(3)
361	w.AddWindow(1)
362	w.AddWindow(2)
363	w.Apply(ActionCycleColumnWidth)
364	if w.Columns[w.Focus].Cells != 2 {
365		t.Fatal("column not wider", w.Columns)
366	}
367	w.Apply(ActionToggleWindowStash)
368	w.Apply(ActionToggleWindowStash)
369	if id, _ := w.Focused(); id != 2 || w.Columns[w.Focus].Cells != 2 {
370		t.Fatalf("focused %d, column %+v", id, w.Columns[w.Focus])
371	}
372}
373
374// A client fullscreen request of a hidden stashed window leaves it hidden
375// on its workspace, even in fixed overflow (which moves fullscreen windows
376// out).
377func TestHiddenFloatFullscreenRequest(t *testing.T) {
378	m := newMonitor("", "")
379	m.SetOutput(100, 80)
380	m.SetOverflow(OverflowFixed)
381	m.AddWindow(1)
382	m.AddWindow(2)
383	w := m.Current()
384	w.ToggleWindowStash()
385	w.ToggleStashVisible()
386	m.SetFullscreen(2, true)
387	if m.Current() != w || w.stashIndex(2) < 0 || !w.stashHidden || w.fullscreen != 0 {
388		t.Fatalf("hidden stash left or showed: %+v", w)
389	}
390	w.ToggleStashVisible()
391	if w.fullscreen != 2 {
392		t.Fatalf("fullscreen lost on show: %d", w.fullscreen)
393	}
394}
395
396// A fullscreen stashed window (a game in scanout) keeps the stash shown:
397// hiding it would drop scanout and VRR.
398func TestToggleStashVisibleFullscreen(t *testing.T) {
399	w := workspace()
400	w.AddWindow(1)
401	w.AddWindow(2)
402	w.ToggleWindowStash()
403	w.SetFullscreen(2, true)
404	w.Apply(ActionToggleStashVisible)
405	if id, _ := w.Focused(); id != 2 || w.fullscreen != 2 || w.stashHidden {
406		t.Fatalf("fullscreen stash hidden: focused %d fullscreen %d", id, w.fullscreen)
407	}
408}
409
410func TestFloatingSceneDimAndConfigure(t *testing.T) {
411	cfg := ports.Config{}
412	cfg.Keyboard.CmdKey = "super"
413	cfg.Layout.MaxColumns = 2
414	cfg.Floating.Dim = 0.3
415	cfg.Border.Width = 2
416	commands := make(chan ports.ClientCommand, 128)
417	scenes := make(chan []ports.Scene, 1)
418	c, err := New(cfg, Channels{Commands: commands, Scenes: scenes}, Options{})
419	if err != nil {
420		t.Fatal(err)
421	}
422	w := c.cur().mon.Current()
423	c.cur().mon.SetOutput(100, 80)
424	w.AddWindow(1)
425	ctx := context.Background()
426	check := func(want float64) {
427		t.Helper()
428		if err := c.publish(ctx); err != nil {
429			t.Fatal(err)
430		}
431		if s := (<-scenes)[0]; s.Dim != want {
432			t.Fatalf("dim %v want %v", s.Dim, want)
433		}
434	}
435	check(0)
436	w.ToggleWindowStash()
437	check(0.3)
438	found := false
439	for len(commands) > 0 {
440		if v, ok := (<-commands).(ports.ConfigureWindow); ok && v.ID == 1 {
441			if v.Floating {
442				found = true
443				if v.Width != 76 || v.Height != 60 {
444					t.Fatalf("imposed configure %+v", v)
445				}
446			}
447		}
448	}
449	if !found {
450		t.Fatal("missing imposed configure")
451	}
452	w.AddFloating(2, 20, 10)
453	check(0.3)
454	found = false
455	for len(commands) > 0 {
456		if v, ok := (<-commands).(ports.ConfigureWindow); ok && v.ID == 2 {
457			found = true
458			if v.Width != 0 || v.Height != 0 {
459				t.Fatalf("native configure %+v", v)
460			}
461		}
462	}
463	if !found {
464		t.Fatal("missing native configure")
465	}
466	// Hiding the stash leaves the dialog, and its veil.
467	w.ToggleStashVisible()
468	check(0.3)
469	w.RemoveWindow(2)
470	check(0)
471	w.ToggleStashVisible()
472	check(0.3)
473	w.AddFloating(2, 20, 10)
474	w.SetFullscreen(1, true)
475	check(0)
476	w.SetFullscreen(1, false)
477	w.ToggleWindowStash()
478	w.SetFullscreen(1, true)
479	check(0)
480	// A dialog of a fullscreen tile waits hidden: no veil over the game.
481	w.SetFullscreen(1, false)
482	w.RemoveWindow(1)
483	w.RemoveWindow(2)
484	w.AddWindow(3)
485	w.SetFullscreen(3, true)
486	check(0)
487	w.AddFloating(4, 20, 10)
488	check(0)
489	w.SetFullscreen(3, false)
490	check(0.3)
491}
492
493// A stashed float's own fullscreen, asked while the stash was hidden,
494// comes back with it when it is activated: it replaces the cover.
495func TestActivateHiddenFloat(t *testing.T) {
496	w := workspace()
497	w.AddWindow(1)
498	w.AddWindow(2)
499	w.ToggleWindowStash()
500	w.ToggleStashVisible()
501	w.FocusID(1)
502	w.SetFullscreen(2, true)
503	w.SetFullscreen(1, true)
504	w.Activate(2)
505	if id, _ := w.Focused(); id != 2 || w.stashHidden || w.fullscreen != 2 {
506		t.Fatalf("own fullscreen: focused %d hidden %v fullscreen %d", id, w.stashHidden, w.fullscreen)
507	}
508}

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