feat/data-dnd e05cb15

slots_test.go RawHistoryFolder

1package core_test
2
3import (
4	"context"
5	"slices"
6	"strings"
7	"testing"
8	"time"
9
10	"github.com/bnema/neferwl/internal/core"
11	"github.com/bnema/neferwl/internal/ports"
12)
13
14type slotRig struct {
15	client     chan ports.ClientEvent
16	input      chan ports.InputEvent
17	reload     chan ports.ConfigChanged
18	spawn      chan ports.SpawnRequest
19	scenes     chan []ports.Scene
20	workspaces chan ports.Workspaces
21	errs       chan error
22	cfg        ports.Config
23	// With animations on and a clock the test moves (startSlotsMode),
24	// land flips the output past every spring.
25	clk    *stepClock
26	frames chan ports.OutputFrame
27	lands  int
28}
29
30// startSlots runs core with a hidden "dev" workspace of two slots, toggled
31// by Alt+d, on a 100x80 output. One flag makes dev numbered, two also set
32// fixed overflow.
33func startSlots(t *testing.T, numbered ...bool) *slotRig {
34	t.Helper()
35	return startSlotsMode(t, nil, numbered...)
36}
37
38// startSlotsMode is startSlots with animations forced off (*false) or on
39// with a clock the test moves (*true), whose springs land through land;
40// nil keeps the default config on the system clock.
41func startSlotsMode(t *testing.T, animated *bool, numbered ...bool) *slotRig {
42	t.Helper()
43	cfg := altCmdDefaults()
44	if animated != nil {
45		cfg.Animations.On = *animated
46	}
47	cfg.Border.Width = 0
48	// A cell is a tenth of the width: the slots take 7 and 3 cells.
49	cfg.Layout.MaxColumns = 10
50	cfg.Workspaces = []ports.WorkspaceConfig{{Name: "dev", Slots: []ports.SlotConfig{
51		{Index: 1, Cells: 7, Argv: []string{"code"}},
52		{Index: 2, Cells: 3, Argv: []string{"foot"}},
53	}}}
54	cfg.Binds["Alt+d"] = "workspace dev"
55	if len(numbered) > 1 {
56		cfg.Layout.Overflow = "fixed"
57	}
58	r := &slotRig{
59		client: make(chan ports.ClientEvent, 16), input: make(chan ports.InputEvent, 16),
60		reload: make(chan ports.ConfigChanged, 4), spawn: make(chan ports.SpawnRequest, 16),
61		scenes: make(chan []ports.Scene, 1), workspaces: make(chan ports.Workspaces, 1), errs: make(chan error, 4), cfg: cfg,
62	}
63	output := make(chan ports.OutputEvent, 1)
64	commands := make(chan ports.ClientCommand, 1024)
65	ch := core.Channels{Client: r.client, Input: r.input, Output: output, Config: r.reload, Commands: commands, Spawn: r.spawn, Scenes: r.scenes, Workspaces: r.workspaces, ConfigErrors: r.errs}
66	opts := core.Options{}
67	if animated != nil && *animated {
68		r.clk, r.frames = newStepClock(t), make(chan ports.OutputFrame)
69		opts.Clock, ch.Frames = r.clk.clock, r.frames
70		t.Cleanup(func() {
71			if r.lands == 0 {
72				t.Error("no spring ever ran: the animations-on variant checked nothing")
73			}
74		})
75	}
76	c, err := core.New(cfg, ch, opts)
77	if err != nil {
78		t.Fatal(err)
79	}
80	ctx, cancel := context.WithCancel(context.Background())
81	done := make(chan error, 1)
82	go func() { done <- c.Run(ctx) }()
83	t.Cleanup(func() { cancel(); <-done })
84	go func() { // commands are not under test
85		for range commands {
86		}
87	}()
88	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
89	scene(t, r.scenes)
90	return r
91}
92
93// land is s once every spring has landed: the scene the flips publish, or s
94// itself when nothing animates (always with animations off or on the
95// system clock).
96func (r *slotRig) land(t *testing.T, s ports.Scene) ports.Scene {
97	t.Helper()
98	if r.clk == nil {
99		return s
100	}
101	set, ok := r.clk.settle(t, r.frames, r.scenes, "OUT-1")
102	if !ok {
103		return s
104	}
105	r.lands++
106	return set[0]
107}
108
109// token returns the SlotEnv value of a spawn request.
110func token(t *testing.T, req ports.SpawnRequest) string {
111	t.Helper()
112	for _, e := range req.Env {
113		if v, ok := strings.CutPrefix(e, ports.SlotEnv+"="); ok {
114			return v
115		}
116	}
117	t.Fatalf("no %s in %v", ports.SlotEnv, req.Env)
118	return ""
119}
120
121// noSpawn checks that nothing was spawned. Core sends spawns before it
122// publishes the scene of the same event, so callers wait for that scene first.
123func noSpawn(t *testing.T, ch <-chan ports.SpawnRequest) {
124	t.Helper()
125	select {
126	case v := <-ch:
127		t.Fatalf("unexpected spawn %v", v.Argv)
128	default:
129	}
130}
131
132// press sends Alt+key and waits for the scene it publishes.
133func (r *slotRig) press(t *testing.T, key string) ports.Scene {
134	t.Helper()
135	r.input <- ports.KeyEvent{Keysym: key, Mods: ports.ModAlt, Pressed: true}
136	r.input <- ports.KeyEvent{Keysym: key, Mods: ports.ModAlt}
137	return scene(t, r.scenes)
138}
139
140// sync waits for core to handle everything sent so far.
141func (r *slotRig) sync(t *testing.T) ports.Scene {
142	t.Helper()
143	return r.press(t, "Left") // focus-column-left publishes a scene
144}
145
146// fill maps the slot windows 2 (code) and 3 (foot) and waits until core
147// has placed them: client and input events are separate channels.
148func (r *slotRig) fill(t *testing.T) (code, foot ports.SpawnRequest) {
149	t.Helper()
150	code, foot = receive(t, r.spawn), receive(t, r.spawn)
151	r.client <- ports.WindowMapped{ID: 2, Slot: token(t, code)}
152	r.client <- ports.WindowMapped{ID: 3, Slot: token(t, foot)}
153	sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(s.Windows) == 2 })
154	return code, foot
155}
156
157// sceneMatch waits for a first-output scene matching ok.
158func sceneMatch(t *testing.T, ch <-chan []ports.Scene, ok func(ports.Scene) bool) ports.Scene {
159	t.Helper()
160	deadline := time.After(time.Second)
161	for {
162		select {
163		case s := <-ch:
164			if ok(s[0]) {
165				return s[0]
166			}
167		case <-deadline:
168			t.Fatal("no matching scene")
169		}
170	}
171}
172
173func visible(s ports.Scene) map[ports.WindowID]ports.Rect {
174	m := map[ports.WindowID]ports.Rect{}
175	for _, w := range s.Windows {
176		if !w.Hidden {
177			m[w.ID] = w.Rect
178		}
179	}
180	return m
181}
182
183// Activating a workspace through the protocol must count as showing it for
184// pending-slot retry, just like switching to it with a key.
185func TestWorkspaceActivateRetriesPendingSlot(t *testing.T) {
186	r := startSlots(t)
187	first, second := receive(t, r.spawn), receive(t, r.spawn)
188	// The channel keeps the latest snapshot: one from before the output
189	// may still be there.
190	initial := receive(t, r.workspaces)
191	for len(initial.Outputs) == 0 {
192		initial = receive(t, r.workspaces)
193	}
194	var dev uint64
195	for _, w := range initial.Outputs[0].Workspaces {
196		if w.Configured == "dev" {
197			dev = w.ID
198		}
199	}
200	if dev == 0 {
201		t.Fatal(initial)
202	}
203	r.client <- ports.WorkspaceActivate{IDs: []uint64{dev}}
204	scene(t, r.scenes)
205	noSpawn(t, r.spawn) // first show marks pending slots stale
206	r.press(t, "d")     // leave dev
207	r.client <- ports.WorkspaceActivate{IDs: []uint64{dev}}
208	scene(t, r.scenes)
209	again := receive(t, r.spawn)
210	if token(t, again) == token(t, first) || token(t, again) == token(t, second) {
211		t.Fatal("activation did not refresh the pending slot token")
212	}
213}
214
215func TestSlotsSpawnAtStartAndFillInOrder(t *testing.T) {
216	r := startSlots(t)
217	code, foot := receive(t, r.spawn), receive(t, r.spawn)
218	if !slices.Equal(code.Argv, []string{"code"}) || !slices.Equal(foot.Argv, []string{"foot"}) || token(t, code) == token(t, foot) {
219		t.Fatal(code, foot)
220	}
221	// A normal window on workspace 1 keeps focus while slot windows map.
222	r.client <- ports.WindowMapped{ID: 1}
223	// foot maps first; code's window still goes left of it.
224	r.client <- ports.WindowMapped{ID: 3, Slot: token(t, foot)}
225	r.client <- ports.WindowMapped{ID: 2, Slot: token(t, code)}
226	s := sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(s.Windows) == 3 })
227	for _, w := range s.Windows {
228		if w.ID != 1 && !w.Hidden || w.ID == 1 && !w.Focused {
229			t.Fatalf("slot window on screen or focus moved: %+v", s.Windows)
230		}
231	}
232	s = r.press(t, "d")
233	got := visible(s)
234	if got[2].X != 0 || got[2].W != 70 || got[3].X != 70 || got[3].W != 30 {
235		t.Fatal(got)
236	}
237	noSpawn(t, r.spawn) // full slots are not respawned
238}
239
240func TestSlotRefilledOnlyWhenShown(t *testing.T) {
241	both(t, func(t *testing.T, animated bool) { slotRefilledOnlyWhenShown(t, startSlotsMode(t, &animated)) })
242}
243
244func slotRefilledOnlyWhenShown(t *testing.T, r *slotRig) {
245	// Wait for both windows: a one-window scene from before the unmap
246	// would let the key below race it.
247	_, foot := r.fill(t)
248	r.client <- ports.WindowUnmapped{ID: 3} // foot exits
249	sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(s.Windows) == 1 })
250	noSpawn(t, r.spawn) // nothing relaunches on its own
251	r.input <- ports.KeyEvent{Keysym: "d", Mods: ports.ModAlt, Pressed: true}
252	r.input <- ports.KeyEvent{Keysym: "d", Mods: ports.ModAlt}
253	again := receive(t, r.spawn)
254	if !slices.Equal(again.Argv, []string{"foot"}) || token(t, again) == token(t, foot) {
255		t.Fatal(again)
256	}
257	r.client <- ports.WindowMapped{ID: 4, Slot: token(t, again)}
258	s := sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(visible(s)) == 2 })
259	if got := visible(r.land(t, s)); got[4].X != 70 {
260		t.Fatal(got)
261	}
262	// Leaving and showing dev again with full slots spawns nothing.
263	r.press(t, "d")
264	r.press(t, "d")
265	noSpawn(t, r.spawn)
266}
267
268func TestSlotExtraWindowsAreNormal(t *testing.T) {
269	r := startSlots(t)
270	code := receive(t, r.spawn)
271	receive(t, r.spawn)
272	r.client <- ports.WindowMapped{ID: 2, Slot: token(t, code)}
273	// A second window of the same process: the slot is taken, normal rules.
274	r.client <- ports.WindowMapped{ID: 5, Slot: token(t, code)}
275	r.client <- ports.WindowMapped{ID: 6, Slot: "unknown"}
276	s := sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(s.Windows) == 3 })
277	if got := visible(s); len(got) != 2 || got[5] == (ports.Rect{}) || got[6] == (ports.Rect{}) {
278		t.Fatalf("extra windows not on the workspace on screen: %v", got)
279	}
280}
281
282func TestSlotReload(t *testing.T) {
283	r := startSlots(t)
284	code, foot := receive(t, r.spawn), receive(t, r.spawn)
285	r.client <- ports.WindowMapped{ID: 2, Slot: token(t, code)}
286	sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(s.Windows) == 1 })
287	cfg := r.cfg
288	ws := cfg.Workspaces[0]
289	ws.Slots = []ports.SlotConfig{
290		{Index: 1, Cells: 5, Argv: []string{"code"}},  // width change only
291		{Index: 2, Cells: 3, Argv: []string{"kitty"}}, // empty slot, new command
292		{Index: 3, Cells: 2, Argv: []string{"btop"}},  // new slot
293	}
294	cfg.Workspaces = []ports.WorkspaceConfig{ws}
295	r.reload <- ports.ConfigChanged{Config: cfg}
296	var argv []string
297	for range 2 {
298		argv = append(argv, receive(t, r.spawn).Argv[0])
299	}
300	slices.Sort(argv)
301	if !slices.Equal(argv, []string{"btop", "kitty"}) {
302		t.Fatal(argv)
303	}
304	scene(t, r.scenes)
305	noSpawn(t, r.spawn)
306	// The old foot spawn no longer fills slot 2 (its command changed): it
307	// opens as a normal window on workspace 1, on screen.
308	r.client <- ports.WindowMapped{ID: 3, Slot: token(t, foot)}
309	s := sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(s.Windows) == 2 })
310	if got := visible(s); len(got) != 1 || got[3] == (ports.Rect{}) {
311		t.Fatal(got)
312	}
313	r.input <- ports.KeyEvent{Keysym: "d", Mods: ports.ModAlt, Pressed: true}
314	r.input <- ports.KeyEvent{Keysym: "d", Mods: ports.ModAlt}
315	s = sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(visible(s)) == 1 })
316	if got := visible(s); got[2].W != 50 {
317		t.Fatal(got)
318	}
319	noSpawn(t, r.spawn) // kitty and btop are still starting
320	// Dropping the workspace's slots spawns nothing and keeps the window.
321	ws.Slots = nil
322	cfg.Workspaces = []ports.WorkspaceConfig{ws}
323	r.reload <- ports.ConfigChanged{Config: cfg}
324	scene(t, r.scenes)
325	noSpawn(t, r.spawn)
326}
327
328func TestInvalidSlotWidthRejected(t *testing.T) {
329	r := startSlots(t)
330	receive(t, r.spawn)
331	receive(t, r.spawn)
332	cfg := r.cfg
333	ws := cfg.Workspaces[0]
334	ws.Slots = []ports.SlotConfig{{Index: 1, Cells: 17, Argv: []string{"x"}}}
335	cfg.Workspaces = []ports.WorkspaceConfig{ws}
336	r.reload <- ports.ConfigChanged{Config: cfg}
337	if err := receive(t, r.errs); err == nil {
338		t.Fatal("no error")
339	}
340	r.sync(t)
341	noSpawn(t, r.spawn)
342}
343
344// Reloading while the slot workspace is on screen spawns a new slot once.
345func TestSlotReloadWhileShownSpawnsOnce(t *testing.T) {
346	r := startSlots(t)
347	r.fill(t)
348	r.press(t, "d")
349	cfg := r.cfg
350	ws := cfg.Workspaces[0]
351	ws.Slots = append(ws.Slots, ports.SlotConfig{Index: 3, Cells: 2, Argv: []string{"btop"}})
352	cfg.Workspaces = []ports.WorkspaceConfig{ws}
353	r.reload <- ports.ConfigChanged{Config: cfg}
354	if v := receive(t, r.spawn); v.Argv[0] != "btop" {
355		t.Fatal(v)
356	}
357	scene(t, r.scenes)
358	noSpawn(t, r.spawn)
359}
360
361// Binds on the workspace on screen do not refill its empty slots; only
362// showing it again does.
363func TestSlotNotRefilledByOtherBinds(t *testing.T) {
364	r := startSlots(t)
365	r.fill(t)
366	r.press(t, "d")
367	r.client <- ports.WindowUnmapped{ID: 3}
368	sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(s.Windows) == 1 })
369	r.sync(t)
370	noSpawn(t, r.spawn)
371	r.press(t, "d")
372	r.press(t, "d")
373	if v := receive(t, r.spawn); v.Argv[0] != "foot" {
374		t.Fatal(v)
375	}
376}
377
378// After a slot window closes, another window with the old token (a child
379// of the slot app) opens as a normal window, not in the empty slot.
380func TestSlotOldTokenIsNormal(t *testing.T) {
381	r := startSlots(t)
382	_, foot := r.fill(t)
383	r.client <- ports.WindowUnmapped{ID: 3}
384	r.client <- ports.WindowMapped{ID: 9, Slot: token(t, foot)}
385	s := sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(visible(s)) == 1 })
386	if got := visible(s); got[9] == (ports.Rect{}) {
387		t.Fatalf("old-token window hidden in the slot: %v", got)
388	}
389}
390
391// A slot whose command never maps a window gets one show to do it; on the
392// next show it is respawned and its old token no longer fills it.
393func TestPendingSlotRespawnedOnSecondShow(t *testing.T) {
394	both(t, func(t *testing.T, animated bool) { pendingSlotRespawnedOnSecondShow(t, startSlotsMode(t, &animated)) })
395}
396
397func pendingSlotRespawnedOnSecondShow(t *testing.T, r *slotRig) {
398	code, foot := receive(t, r.spawn), receive(t, r.spawn)
399	r.client <- ports.WindowMapped{ID: 2, Slot: token(t, code)}
400	sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(s.Windows) == 1 })
401	r.press(t, "d") // foot still starting: not respawned
402	noSpawn(t, r.spawn)
403	r.press(t, "d")
404	r.press(t, "d")
405	again := receive(t, r.spawn)
406	if again.Argv[0] != "foot" || token(t, again) == token(t, foot) {
407		t.Fatal(again)
408	}
409	r.client <- ports.WindowMapped{ID: 7, Slot: token(t, foot)}
410	s := sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(s.Windows) == 2 })
411	if got := visible(s); got[7] == (ports.Rect{}) {
412		t.Fatalf("stale-token window not placed as a normal one: %v", got)
413	}
414	r.client <- ports.WindowMapped{ID: 8, Slot: token(t, again)}
415	s = sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(s.Windows) == 3 })
416	if got := visible(r.land(t, s)); got[8].W != 30 {
417		t.Fatalf("respawned window not in its slot: %v", got)
418	}
419}
420
421// Moving a slot window to another workspace releases the slot; showing the
422// workspace again refills it.
423// Startup commands are spawned once, before the slots.
424func TestStartupCommands(t *testing.T) {
425	cfg := scrollDefaults()
426	cfg.Startup = [][]string{{"wl-paste", "--watch", "cliphist", "store"}, {"waybar"}}
427	spawn := make(chan ports.SpawnRequest, 4)
428	c, err := core.New(cfg, core.Channels{Client: make(chan ports.ClientEvent), Input: make(chan ports.InputEvent), Output: make(chan ports.OutputEvent), Config: make(chan ports.ConfigChanged), Commands: make(chan ports.ClientCommand, 16), Spawn: spawn, Scenes: make(chan []ports.Scene, 1), ConfigErrors: make(chan error, 1)}, core.Options{})
429	if err != nil {
430		t.Fatal(err)
431	}
432	ctx, cancel := context.WithCancel(context.Background())
433	done := make(chan error, 1)
434	go func() { done <- c.Run(ctx) }()
435	defer func() { cancel(); <-done }()
436	for _, want := range cfg.Startup {
437		if got := receive(t, spawn); !slices.Equal(got.Argv, want) {
438			t.Fatalf("spawned %q, want %q", got.Argv, want)
439		}
440	}
441}
442
443// A spawn bind with several commands sends each one, in order.
444func TestSpawnBindSeveralCommands(t *testing.T) {
445	cfg := altCmdDefaults()
446	cfg.Terminal.AutoOpen = "off"
447	cfg.Binds["Alt+p"] = "spawn grim -g x; notify-send done"
448	input := make(chan ports.InputEvent, 4)
449	output := make(chan ports.OutputEvent, 1)
450	scenes := make(chan []ports.Scene, 1)
451	spawn := make(chan ports.SpawnRequest, 4)
452	c, err := core.New(cfg, core.Channels{Client: make(chan ports.ClientEvent), Input: input, Output: output, Config: make(chan ports.ConfigChanged), Commands: make(chan ports.ClientCommand, 16), Spawn: spawn, Scenes: scenes, ConfigErrors: make(chan error, 1)}, core.Options{})
453	if err != nil {
454		t.Fatal(err)
455	}
456	ctx, cancel := context.WithCancel(context.Background())
457	done := make(chan error, 1)
458	go func() { done <- c.Run(ctx) }()
459	defer func() { cancel(); <-done }()
460	output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
461	scene(t, scenes)
462	input <- ports.KeyEvent{Keysym: "p", Mods: ports.ModAlt, Pressed: true}
463	for _, want := range [][]string{{"grim", "-g", "x"}, {"notify-send", "done"}} {
464		if got := receive(t, spawn); !slices.Equal(got.Argv, want) {
465			t.Fatalf("spawned %q, want %q", got.Argv, want)
466		}
467	}
468}
469
470// A fullscreen slot window (fixed overflow) keeps its slot: coming back to
471// dev spawns no duplicate.
472func TestSlotKeptWhileFullscreen(t *testing.T) {
473	r := startSlots(t, true, true)
474	r.fill(t)
475	r.press(t, "d") // on dev, focus on code (slot 1)
476	r.input <- ports.KeyEvent{Keysym: "f", Mods: ports.ModAlt | ports.ModShift, Pressed: true}
477	r.input <- ports.KeyEvent{Keysym: "f", Mods: ports.ModAlt | ports.ModShift}
478	scene(t, r.scenes)
479	r.press(t, "d") // leave and show dev while code is away
480	r.press(t, "d")
481	noSpawn(t, r.spawn)
482	// code opens a second window while fullscreen: the slot stays.
483	r.client <- ports.WindowMapped{ID: 9}
484	scene(t, r.scenes)
485	r.press(t, "d")
486	r.press(t, "d")
487	noSpawn(t, r.spawn)
488}
489
490// Expelling a slot window from a stacked column keeps one column per slot:
491// the slot and its width follow the window, and nothing is respawned.
492func TestSlotFollowsExpelledWindow(t *testing.T) {
493	r := startSlots(t, true) // dev is numbered: [1] [dev] [empty]
494	r.fill(t)
495	r.press(t, "d") // dev: [code] [foot], focus on code
496	r.client <- ports.WindowMapped{ID: 9}
497	sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(s.Windows) == 3 })
498	r.press(t, "bracketleft")  // 9 joins code: [code 9] [foot]
499	r.press(t, "Up")           // focus code, the slot window
500	r.press(t, "bracketright") // code leaves: [9] [code] [foot]
501	r.press(t, "d")
502	s := r.press(t, "d") // shown again: no slot is empty
503	noSpawn(t, r.spawn)
504	if got := visible(s); got[2].W != 70 {
505		t.Fatalf("slots: %v", got)
506	}
507}
508
509// A floating child keeps the pending claim for the first ordinary window.
510func TestFloatingSlotTokenDoesNotClaim(t *testing.T) {
511	r := startSlots(t)
512	code, _ := receive(t, r.spawn), receive(t, r.spawn)
513	r.client <- ports.WindowMapped{ID: 10, Slot: token(t, code), Floating: true, Width: 20, Height: 20}
514	sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(s.Windows) == 1 })
515	r.client <- ports.WindowMapped{ID: 11, Slot: token(t, code)}
516	sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(s.Windows) == 2 })
517	s := r.press(t, "d")
518	if got := visible(s); got[11].W != 70 {
519		t.Fatalf("floating child consumed slot claim: %v", got)
520	}
521	r.client <- ports.WindowMapped{ID: 12, Slot: token(t, code)}
522	s = sceneMatch(t, r.scenes, func(s ports.Scene) bool { return len(s.Windows) == 3 })
523	if got := visible(s); got[12].W == 70 {
524		t.Fatalf("duplicate token claimed slot: %v", got)
525	}
526}

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