1package core_test
2
3import (
4 "slices"
5 "testing"
6 "time"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11// stashSpawnRig has a stash bind on Alt+f and, as in the defaults, foot as
12// the terminal. Tiles map without any automatic terminal.
13func stashSpawnRig(t *testing.T, edit func(*ports.Config)) *multiRig {
14 t.Helper()
15 return startMulti(t, func(c *ports.Config) {
16 c.Binds["Alt+Ctrl+Shift+f"] = "stash firefox --new-window"
17 c.Stash.Capture = false
18 if edit != nil {
19 edit(c)
20 }
21 }, left)
22}
23
24func slotToken(t *testing.T, req ports.SpawnRequest) string {
25 t.Helper()
26 if len(req.Env) != 1 || len(req.Env[0]) <= len(ports.SlotEnv)+1 || req.Env[0][:len(ports.SlotEnv)+1] != ports.SlotEnv+"=" {
27 t.Fatalf("no slot token: %v", req.Env)
28 }
29 return req.Env[0][len(ports.SlotEnv)+1:]
30}
31
32func sceneWindow(set []ports.Scene, id ports.WindowID) (ports.SceneWindow, bool) {
33 for _, s := range set {
34 for _, w := range s.Windows {
35 if w.ID == id {
36 return w, true
37 }
38 }
39 }
40 return ports.SceneWindow{}, false
41}
42
43// The window of a stash bind opens in the stash of the workspace the key
44// was pressed on, shown and focused, even when the user moved on before it
45// mapped; toggle-window-stash then tiles it as a new column.
46func TestStashBindOpensWindowInStash(t *testing.T) {
47 r := stashSpawnRig(t, nil)
48 r.mapWindow(t, 1)
49 r.key(t, "f", ports.ModAlt|ports.ModCtrl|ports.ModShift)
50 req := receive(t, r.spawn)
51 if !slices.Equal(req.Argv, []string{"firefox", "--new-window"}) {
52 t.Fatalf("argv %v", req.Argv)
53 }
54 token := slotToken(t, req)
55 r.key(t, "Next", ports.ModAlt) // focus moves to workspace 2
56 r.client <- ports.WindowMapped{ID: 2, Slot: token}
57 set := receive(t, r.scenes)
58 if w, ok := sceneWindow(set, 2); ok && !w.Hidden {
59 t.Fatalf("window shown on the workspace the user moved to: %+v", w)
60 }
61 set = r.key(t, "Prior", ports.ModAlt)
62 w, ok := sceneWindow(set, 2)
63 if !ok || w.Hidden || !w.Floating || !w.Focused {
64 t.Fatalf("stashed window %+v", w)
65 }
66 if w1, _ := sceneWindow(set, 1); w1.Focused {
67 t.Fatal("tile kept the focus")
68 }
69 // Unstashing tiles it as a column of its own.
70 set = r.key(t, "s", ports.ModAlt|ports.ModShift)
71 if w, ok := sceneWindow(set, 2); !ok || w.Floating || w.Hidden || !w.Focused {
72 t.Fatalf("unstashed window %+v", w)
73 }
74 if len(r.spawn) != 0 {
75 t.Fatal("unexpected spawn")
76 }
77}
78
79func TestStashBindRunsEveryCommand(t *testing.T) {
80 r := stashSpawnRig(t, func(c *ports.Config) { c.Binds["Alt+Ctrl+Shift+f"] = "stash one; two" })
81 r.key(t, "f", ports.ModAlt|ports.ModCtrl|ports.ModShift)
82 a, b := receive(t, r.spawn), receive(t, r.spawn)
83 if a.Argv[0] != "one" || b.Argv[0] != "two" || slotToken(t, a) == slotToken(t, b) {
84 t.Fatal(a, b)
85 }
86}
87
88// With an empty stash, toggle-stash-visible opens the terminal in it, once
89// while it is awaited.
90func TestToggleEmptyStashOpensTerminal(t *testing.T) {
91 r := stashSpawnRig(t, nil)
92 r.mapWindow(t, 1)
93 r.key(t, "s", ports.ModAlt)
94 req := receive(t, r.spawn)
95 if len(req.Argv) != 1 || req.Argv[0] != "foot" {
96 t.Fatalf("argv %v", req.Argv)
97 }
98 token := slotToken(t, req)
99 r.key(t, "s", ports.ModAlt)
100 if len(r.spawn) != 0 {
101 t.Fatal("second terminal while the first is awaited")
102 }
103 r.client <- ports.WindowMapped{ID: 2, Slot: token}
104 set := receive(t, r.scenes)
105 if w, ok := sceneWindow(set, 2); !ok || w.Hidden || !w.Floating || !w.Focused {
106 t.Fatalf("terminal %+v", w)
107 }
108 // The stash has a window now: the toggle hides it, no spawn.
109 set = r.key(t, "s", ports.ModAlt)
110 if w, _ := sceneWindow(set, 2); !w.Hidden {
111 t.Fatalf("stash not hidden: %+v", w)
112 }
113 if len(r.spawn) != 0 {
114 t.Fatal("spawned for a stash holding a window")
115 }
116}
117
118func TestToggleNonEmptyStashDoesNotSpawn(t *testing.T) {
119 r := stashSpawnRig(t, nil)
120 r.mapWindow(t, 1)
121 r.mapWindow(t, 2)
122 r.key(t, "s", ports.ModAlt|ports.ModShift) // 2 stashed
123 r.key(t, "s", ports.ModAlt)
124 r.key(t, "s", ports.ModAlt)
125 if len(r.spawn) != 0 {
126 t.Fatal("spawned for a non-empty stash")
127 }
128}
129
130func TestToggleEmptyStashRespectsOption(t *testing.T) {
131 r := stashSpawnRig(t, func(c *ports.Config) { c.Stash.SpawnTerminal = false })
132 r.mapWindow(t, 1)
133 r.key(t, "s", ports.ModAlt)
134 if len(r.spawn) != 0 {
135 t.Fatal("spawned with stash.spawn-terminal off")
136 }
137 r = stashSpawnRig(t, func(c *ports.Config) { c.Terminal.Command = nil })
138 r.mapWindow(t, 1)
139 r.key(t, "s", ports.ModAlt)
140 if len(r.spawn) != 0 {
141 t.Fatal("spawned without a terminal command")
142 }
143}
144
145func TestToggleEmptyStashInOverviewDoesNotSpawn(t *testing.T) {
146 r := stashSpawnRig(t, nil)
147 r.mapWindow(t, 1)
148 r.key(t, "o", ports.ModAlt)
149 r.key(t, "s", ports.ModAlt)
150 if len(r.spawn) != 0 {
151 t.Fatal("spawned from the overview")
152 }
153}
154
155// A four-finger swipe down on an empty stash is toggle-stash-visible: with
156// stash.spawn-terminal it opens the terminal in the stash, once while that
157// one is awaited, and the window that maps lands in the stash.
158func TestFourFingerSwipeEmptyStashOpensTerminal(t *testing.T) {
159 r := startSwipe(t, func(c *ports.Config) { c.Stash.SpawnTerminal = true })
160 r.four(t, 0, 40, 10)
161 req := receive(t, r.spawn)
162 if len(req.Argv) != 1 || req.Argv[0] != "foot" {
163 t.Fatalf("argv %v", req.Argv)
164 }
165 token := slotToken(t, req)
166 r.four(t, 0, 40, 10)
167 if len(r.spawn) != 0 {
168 t.Fatal("second terminal while the first is awaited")
169 }
170 r.client <- ports.WindowMapped{ID: 2, Slot: token}
171 if w, ok := sceneWindow(receive(t, r.scenes), 2); !ok || w.Hidden || !w.Floating || !w.Focused {
172 t.Fatalf("terminal %+v", w)
173 }
174}
175
176func TestFourFingerSwipeEmptyStashRespectsOption(t *testing.T) {
177 r := startSwipe(t, func(c *ports.Config) { c.Stash.SpawnTerminal = false })
178 r.four(t, 0, 40, 10)
179 if len(r.spawn) != 0 {
180 t.Fatal("spawned with stash.spawn-terminal off")
181 }
182}
183
184// The terminal of a four-finger swipe shows no workspace: the empty slots of
185// the workspace on screen are not refilled, as with the bind.
186func TestFourFingerSwipeEmptyStashDoesNotRefillSlots(t *testing.T) {
187 r := startSlots(t)
188 r.fill(t)
189 r.press(t, "d") // dev on screen, its slots filled
190 r.client <- ports.WindowUnmapped{ID: 2}
191 sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(s.Windows) == 1 })
192 noSpawn(t, r.spawn) // nothing relaunches on its own
193 var at time.Duration
194 r.input <- ports.SwipeBegin{Fingers: 4, Time: at}
195 for range 10 {
196 at += 8 * time.Millisecond
197 r.input <- ports.SwipeUpdate{DY: 40, Time: at}
198 }
199 r.input <- ports.SwipeEnd{Time: at}
200 req := receive(t, r.spawn)
201 if len(req.Argv) != 1 || req.Argv[0] != "foot" {
202 t.Fatalf("argv %v, want the stash terminal", req.Argv)
203 }
204 r.sync(t)
205 noSpawn(t, r.spawn) // the empty slot of dev stays empty
206}