feat/data-dnd e05cb15

stash_spawn_test.go RawHistoryFolder

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

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