feat/data-dnd e05cb15

stash_test.go RawHistoryFolder

1package core
2
3import (
4	"slices"
5	"testing"
6
7	"github.com/bnema/neferwl/internal/ports"
8)
9
10// stashIDs lists the stash of w, left to right.
11func stashIDs(w *Workspace) []WindowID {
12	var ids []WindowID
13	for _, f := range w.Stash {
14		ids = append(ids, f.ID)
15	}
16	return ids
17}
18
19// stashMonitor has tiles 1, 2, 3 with 2 and 3 stashed, 3 selected.
20func stashMonitor(overflow Overflow) *Monitor {
21	m := newMonitor("", "")
22	m.SetOutput(100, 80)
23	m.SetOverflow(overflow)
24	for id := WindowID(1); id <= 3; id++ {
25		m.AddWindow(id)
26	}
27	w := m.Current()
28	for _, id := range []WindowID{2, 3} {
29		w.FocusID(id)
30		w.ToggleWindowStash()
31	}
32	return m
33}
34
35// A stashed window moved to another workspace joins the end of its stash,
36// selected, and returns there as a last column.
37func TestStashMoveToWorkspace(t *testing.T) {
38	for _, overflow := range []Overflow{OverflowScroll, OverflowFixed} {
39		m := stashMonitor(overflow)
40		w := m.Current()
41		m.MoveToWorkspace(1, false)
42		to := m.Workspaces[1]
43		if !slices.Equal(stashIDs(w), []WindowID{2}) || !slices.Equal(stashIDs(to), []WindowID{3}) || to.stashAt != 0 || to.stashHidden {
44			t.Fatalf("%v: from %v to %v", overflow, stashIDs(w), stashIDs(to))
45		}
46		if id, _ := w.Focused(); id != 2 {
47			t.Fatalf("%v: source focus %d", overflow, id)
48		}
49		to.AddWindow(9)
50		to.FocusID(3)
51		to.ToggleWindowStash()
52		if len(to.Columns) != 2 || to.Columns[1].Windows[0] != 3 {
53			t.Fatalf("%v: unstash elsewhere %+v", overflow, to.Columns)
54		}
55	}
56}
57
58// A workspace moved to another monitor keeps its stash, selection and
59// hidden state.
60func TestStashFollowsWorkspace(t *testing.T) {
61	m := stashMonitor(OverflowScroll)
62	w := m.Current()
63	w.FocusID(2)
64	w.ToggleStashVisible()
65	m.take(w)
66	dst := newMonitor("", "")
67	dst.SetOutput(200, 100)
68	dst.adopt(w, false, 0)
69	if !slices.Equal(stashIDs(w), []WindowID{2, 3}) || w.stashAt != 0 || !w.stashHidden {
70		t.Fatalf("stash %v at %d hidden %v", stashIDs(w), w.stashAt, w.stashHidden)
71	}
72	w.ToggleStashVisible()
73	if p := placement(w, 2); p.Rect != (Rect{X: 20, Y: 10, W: 160, H: 80}) {
74		t.Fatalf("placement on the new output %+v", p)
75	}
76}
77
78// A stashed window goes fullscreen in place and stays in its stash slot,
79// selected when the user was on it; a client exit while the user is on
80// another workspace leaves the view there.
81func TestStashFullscreenRoundTrip(t *testing.T) {
82	m := stashMonitor(OverflowFixed)
83	w := m.Current()
84	w.FocusID(2)
85	m.ToggleFullscreen()
86	if m.Current() != w || w.cover() != 2 || !slices.Equal(stashIDs(w), []WindowID{2, 3}) {
87		t.Fatalf("fullscreen: cover %d stash %v", w.cover(), stashIDs(w))
88	}
89	m.ToggleFullscreen()
90	if id, _ := w.Focused(); id != 2 || w.stashAt != 0 || w.fullscreen != 0 {
91		t.Fatalf("focused %d at %d", id, w.stashAt)
92	}
93	m.ToggleFullscreen()
94	m.Focus(1)
95	m.SetFullscreen(2, false)
96	if !slices.Equal(stashIDs(w), []WindowID{2, 3}) || w.fullscreen != 0 || m.Active != 1 {
97		t.Fatalf("request: %v on %d", stashIDs(w), m.Active)
98	}
99}
100
101// A shown stash without tiles has the focus, however it got there: a
102// named workspace handing its windows over, or a fullscreen window
103// returning while the user is elsewhere.
104func TestStashAloneHasFocus(t *testing.T) {
105	m := stashMonitor(OverflowFixed)
106	w := m.Current()
107	w.FocusID(1)
108	w.RemoveWindow(1) // only stashed windows left, the tile had the focus
109	if id, _ := w.Focused(); id != 3 {
110		t.Fatalf("focused %d, want the selected stashed window", id)
111	}
112	// Fixed overflow: the stashed window goes fullscreen, its only tile
113	// closes, then the client leaves fullscreen.
114	m = newMonitor("", "")
115	m.SetOutput(100, 80)
116	m.SetOverflow(OverflowFixed)
117	m.AddWindow(1)
118	m.AddWindow(2)
119	w = m.Current()
120	w.ToggleWindowStash()
121	m.ToggleFullscreen()
122	if w.cover() != 2 {
123		t.Fatal("not fullscreen")
124	}
125	m.RemoveWindow(1)
126	m.SetFullscreen(2, false)
127	if len(w.Columns) != 0 || !slices.Equal(stashIDs(w), []WindowID{2}) {
128		t.Fatalf("workspace %+v", w)
129	}
130	if id, ok := w.Focused(); !ok || id != 2 {
131		t.Fatalf("focused %d %v", id, ok)
132	}
133}
134
135// A window opening under a fullscreen stashed window, with no tiles left,
136// waits as the only column.
137func TestStashFullscreenWithoutTilesAddWindow(t *testing.T) {
138	m := newMonitor("", "")
139	m.SetOutput(100, 80)
140	m.AddWindow(1)
141	w := m.Current()
142	w.ToggleWindowStash()
143	w.ToggleFullscreen()
144	w.AddWindow(2)
145	if len(w.Columns) != 1 || w.Columns[0].Windows[0] != 2 || w.cover() != 1 {
146		t.Fatalf("columns %+v cover %d", w.Columns, w.cover())
147	}
148}
149
150// A fullscreen request made while the stash was hidden applies on show,
151// even over an off-screen fullscreen tile.
152func TestStashPendingFullscreenOverTile(t *testing.T) {
153	w := workspace()
154	w.SetMaxColumns(1)
155	w.AddWindow(1)
156	w.AddWindow(2)
157	w.AddWindow(3)
158	w.ToggleWindowStash() // 3 stashed
159	w.ToggleStashVisible()
160	w.SetFullscreen(3, true) // waits
161	w.FocusID(1)
162	w.SetFullscreen(1, true)
163	w.FocusID(2) // 1 scrolled off: it no longer covers
164	if w.cover() != 0 {
165		t.Fatal("tile still covers")
166	}
167	w.ToggleStashVisible()
168	if w.fullscreen != 3 || w.hiddenFullscreen != 0 {
169		t.Fatalf("fullscreen %d pending %d", w.fullscreen, w.hiddenFullscreen)
170	}
171	if id, _ := w.Focused(); id != 3 {
172		t.Fatalf("focused %d", id)
173	}
174}
175
176// A removed named workspace hands its stash to the active one; with no
177// tiles there, the stash takes the focus.
178func TestStashFromRemovedNamedWorkspace(t *testing.T) {
179	m := newMonitor("", "")
180	m.SetOutput(100, 80)
181	m.SetNamed([]NamedWorkspace{{Name: "notes"}})
182	m.ToggleNamed("notes")
183	m.AddWindow(1)
184	m.AddWindow(2)
185	w := m.Current()
186	w.ToggleWindowStash()
187	w.FocusID(1)
188	w.ToggleWindowStash()
189	m.SetNamed(nil)
190	to := m.Workspaces[m.Active]
191	if !slices.Equal(stashIDs(to), []WindowID{2, 1}) {
192		t.Fatalf("stash %v", stashIDs(to))
193	}
194	if id, ok := to.Focused(); !ok || id != 2 {
195		t.Fatalf("focused %d %v", id, ok)
196	}
197}
198
199// With stash.capture, a window opened while the stash is shown joins its
200// end, selected, and takes the focus unless a native dialog has it. A
201// hidden stash, a covering fullscreen window or capture off let it tile.
202func TestStashCapture(t *testing.T) {
203	for _, tc := range []struct {
204		name     string
205		overflow Overflow
206		setup    func(*Monitor)
207		captured bool
208		focused  WindowID
209	}{
210		{"shown", OverflowScroll, func(*Monitor) {}, true, 4},
211		{"shown fixed", OverflowFixed, func(*Monitor) {}, true, 4},
212		{"dialog keeps focus", OverflowScroll, func(m *Monitor) { m.AddFloating(9, 10, 10) }, true, 9},
213		{"hidden", OverflowScroll, func(m *Monitor) { m.Current().ToggleStashVisible() }, false, 4},
214		{"under fullscreen", OverflowScroll, func(m *Monitor) { m.Current().SetFullscreen(3, true) }, false, 3},
215		{"off", OverflowScroll, func(m *Monitor) { m.SetStashCapture(false) }, false, 3},
216	} {
217		t.Run(tc.name, func(t *testing.T) {
218			m := stashMonitor(tc.overflow)
219			m.SetStashCapture(true)
220			tc.setup(m)
221			w := m.Current()
222			m.AddWindow(4)
223			if captured := w.stashIndex(4) == 2 && w.stashAt == 2; captured != tc.captured {
224				t.Fatalf("captured %v, stash %+v", captured, w.Stash)
225			}
226			if id, _ := w.Focused(); id != tc.focused {
227				t.Fatalf("focused %d", id)
228			}
229		})
230	}
231}
232
233// A captured window has no former column: toggle-window-stash tiles it as
234// a new column after the focused one.
235func TestStashCaptureUnstash(t *testing.T) {
236	m := stashMonitor(OverflowScroll)
237	m.SetStashCapture(true)
238	w := m.Current()
239	m.AddWindow(4)
240	w.ToggleWindowStash()
241	if len(w.Columns) != 2 || w.Columns[1].Windows[0] != 4 || w.Focus != 1 || w.stashIndex(4) >= 0 {
242		t.Fatalf("columns %+v focus %d stash %+v", w.Columns, w.Focus, w.Stash)
243	}
244}
245
246// stash.width sets the selected window's width; the neighbors sit
247// stash.gap beside it, in what the margins show.
248func TestStashWidth(t *testing.T) {
249	m := stashMonitor(OverflowScroll)
250	m.SetStash(50, 10)
251	w := m.Current()
252	if p := placement(w, 3); p.Rect != (Rect{X: 25, Y: 8, W: 50, H: 64}) {
253		t.Fatalf("selected %+v", p)
254	}
255	// 25 margin - 10 gap: 15 of it shows, then the gap, then the window.
256	if p := placement(w, 2); !p.Peek || p.Rect != (Rect{X: 15 - 50, Y: 8, W: 50, H: 64}) {
257		t.Fatalf("left peek %+v", p)
258	}
259	// At 90%, a 2% gap leaves 3% of each neighbor.
260	m.SetStash(90, 2)
261	if p := placement(w, 2); !p.Peek || p.Rect.X+p.Rect.W != 3 {
262		t.Fatalf("narrow peek %+v", p)
263	}
264	// A gap wider than the margin hides the neighbors.
265	m.SetStash(90, 10)
266	if p := placement(w, 2); !p.Hidden {
267		t.Fatalf("peek past the gap %+v", p)
268	}
269	// 0 is the default, out-of-range values are clamped.
270	m.SetStash(0, 3)
271	if p := placement(w, 3); p.Rect.W != 80 {
272		t.Fatalf("default width %d", p.Rect.W)
273	}
274	m.SetStash(5, 3)
275	if p := placement(w, 3); p.Rect.W != 10 {
276		t.Fatalf("clamped low %d", p.Rect.W)
277	}
278	m.SetStash(100, 3)
279	if p := placement(w, 3); p.Rect.W != 90 {
280		t.Fatalf("clamped high %d", p.Rect.W)
281	}
282}
283
284// focus-column from a fullscreen stashed window at the end of the stash
285// goes to the neighbor monitor and keeps fullscreen; inside the stash it
286// leaves fullscreen for the next stashed window.
287func TestFullscreenStashEdgeGoesToMonitor(t *testing.T) {
288	cfg := ports.Config{}
289	cfg.Keyboard.CmdKey = "super"
290	cfg.Layout.MaxColumns = 2
291	cfg.Layout.Overflow = string(OverflowFixed)
292	c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1)}, Options{})
293	if err != nil {
294		t.Fatal(err)
295	}
296	c.addScreen(ports.OutputInfo{Name: "A", Width: 100, Height: 80})
297	c.addScreen(ports.OutputInfo{Name: "B", Width: 100, Height: 80})
298	m := c.screens[0].mon
299	for id := WindowID(1); id <= 3; id++ {
300		m.AddWindow(id)
301	}
302	w := m.Current()
303	w.FocusID(2)
304	w.ToggleWindowStash()
305	w.FocusID(3)
306	w.ToggleWindowStash() // stash [2 3], 3 selected
307	m.ToggleFullscreen()
308	if w.cover() != 3 {
309		t.Fatal("setup", w.cover())
310	}
311	c.applyAction(ActionFocusColumnRight)
312	if c.focusScreen != 1 || w.cover() != 3 {
313		t.Fatalf("edge: screen %d cover %d", c.focusScreen, w.cover())
314	}
315	c.focusScreen = 0
316	c.applyAction(ActionFocusColumnLeft)
317	if id, _ := w.Focused(); id != 2 || w.fullscreen != 0 || c.focusScreen != 0 {
318		t.Fatalf("inside: focused %d fullscreen %d", id, w.fullscreen)
319	}
320}
321
322// Activating a hidden stashed window over a scroll fullscreen tile shows
323// the stash over it: the tile stays fullscreen and the view stays aligned.
324func TestActivateHiddenStashOverScrollFullscreen(t *testing.T) {
325	w := workspace()
326	w.AddWindow(1)
327	w.AddWindow(2)
328	w.AddWindow(3)
329	w.ToggleWindowStash() // 3 stashed
330	w.ToggleStashVisible()
331	w.FocusID(2)
332	w.SetFullscreen(2, true)
333	w.Activate(3)
334	if id, _ := w.Focused(); id != 3 || w.fullscreen != 2 || !w.stashOverCover() {
335		t.Fatalf("focused %d fullscreen %d over %v", id, w.fullscreen, w.stashOverCover())
336	}
337	view := w.View
338	w.scroll()
339	if w.View != view {
340		t.Fatalf("view %d, settled %d", view, w.View)
341	}
342}

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