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)})
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 ends
323// the fullscreen like any exit: the view leaves the fullscreen alignment.
324func TestActivateHiddenStashEndsScrollFullscreen(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 != 0 {
335 t.Fatalf("focused %d fullscreen %d", id, w.fullscreen)
336 }
337 view := w.ViewX
338 w.scroll()
339 if w.ViewX != view {
340 t.Fatalf("view %d, settled %d", view, w.ViewX)
341 }
342}