main f2d166b

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
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}

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