feat/hints a471fa4

repeat_test.go RawHistoryFolder

1package core
2
3import (
4	"testing"
5
6	"github.com/bnema/neferwl/internal/ports"
7)
8
9const (
10	keyH     = 43
11	keyLeft  = 113
12	keyF     = 41
13	keyShift = 50
14)
15
16// repeatCore is a Core on a test clock with four columns, the last one
17// focused, and a few move and toggle binds.
18func repeatCore(t *testing.T) (*Core, *indicatorClock) {
19	t.Helper()
20	c, ic := indicatorCore(t)
21	cfg := c.cfg
22	cfg.Layout.MaxColumns = 4
23	cfg.Binds = map[string]string{
24		"Cmd+h":         string(ActionFocusColumnLeft),
25		"Cmd+l":         string(ActionFocusColumnRight),
26		"Cmd+Left":      string(ActionFocusColumnLeft),
27		"Cmd+f":         string(ActionToggleFullscreen),
28		"Cmd+o":         string(ActionToggleOverview),
29		"Cmd+Shift+h":   string(ActionMoveColumnLeft),
30		"Cmd+Ctrl+Left": string(ActionFocusWorkspacePrev),
31	}
32	if err := c.apply(cfg); err != nil {
33		t.Fatal(err)
34	}
35	m := c.cur().mon
36	for id := WindowID(1); id <= 4; id++ {
37		m.AddWindow(id)
38	}
39	m.Current().FocusID(4)
40	indicatorScene(t, c)
41	return c, ic
42}
43
44func key(sym string, code uint32, mods ports.Mods, pressed bool) ports.KeyEvent {
45	return ports.KeyEvent{Keysym: sym, Keycode: code, Mods: mods, Pressed: pressed}
46}
47
48// tick fires the repeat timer once, as Run does on its channel.
49func tick(t *testing.T, c *Core) {
50	t.Helper()
51	if c.repeatC == nil {
52		t.Fatal("no repeat armed")
53	}
54	if err := c.repeatTick(t.Context()); err != nil {
55		t.Fatal(err)
56	}
57}
58
59// A held move bind repeats: the first repeat after repeatDelay, then one
60// per repeatInterval, until the key is released.
61func TestHeldBindRepeats(t *testing.T) {
62	c, ic := repeatCore(t)
63	before := len(ic.timers)
64	sendKeys(t, c, key("h", keyH, ports.ModSuper, true))
65	if focusedID(c.cur().mon) != 3 {
66		t.Fatalf("press: focus %d, want 3", focusedID(c.cur().mon))
67	}
68	if len(ic.timers) != before+1 || ic.timers[len(ic.timers)-1] != repeatDelay {
69		t.Fatalf("repeat timer %v, want %v", ic.timers[before:], repeatDelay)
70	}
71	tick(t, c)
72	tick(t, c)
73	if focusedID(c.cur().mon) != 1 {
74		t.Fatalf("two repeats: focus %d, want 1", focusedID(c.cur().mon))
75	}
76	if r := ic.resets[len(ic.resets)-1]; r != repeatInterval {
77		t.Fatalf("next repeat in %v, want %v", r, repeatInterval)
78	}
79	sendKeys(t, c, key("h", keyH, ports.ModSuper, false))
80	if c.repeatC != nil || c.repeat.timer != nil {
81		t.Fatal("release did not stop the repeat")
82	}
83}
84
85// Only moves repeat: a held toggle runs once.
86func TestHeldToggleDoesNotRepeat(t *testing.T) {
87	c, _ := repeatCore(t)
88	sendKeys(t, c, key("f", keyF, ports.ModSuper, true))
89	if c.repeatC != nil {
90		t.Fatal("toggle-fullscreen repeats")
91	}
92	for _, a := range []Action{ActionToggleFullscreen, ActionToggleOverview, ActionCloseWindow, ActionSpawnTerminal, ActionQuit, ActionToggleWindowStash, ActionMaximizeColumn} {
93		if repeats(a) {
94			t.Errorf("%s repeats", a)
95		}
96	}
97	for _, a := range []Action{ActionFocusColumnLeft, ActionMoveColumnRight, ActionFocusWorkspaceNext, ActionMoveWindowToWorkspacePrev, "set-column-width +1"} {
98		if !repeats(a) {
99			t.Errorf("%s does not repeat", a)
100		}
101	}
102}
103
104// Another key pressed or the modifiers changed end the repeat: releasing
105// Cmd first does not leave the move running.
106func TestRepeatStopsOnKeyOrModifierChange(t *testing.T) {
107	c, _ := repeatCore(t)
108	sendKeys(t, c, key("h", keyH, ports.ModSuper, true))
109	sendKeys(t, c, key("Super_L", keySuper, 0, false))
110	if c.repeatC != nil {
111		t.Fatal("releasing Cmd did not stop the repeat")
112	}
113	sendKeys(t, c, key("h", keyH, 0, false))
114
115	sendKeys(t, c, key("h", keyH, ports.ModSuper, true))
116	sendKeys(t, c, key("Shift_L", keyShift, ports.ModSuper|ports.ModShift, true))
117	if c.repeatC != nil {
118		t.Fatal("pressing Shift did not stop the repeat")
119	}
120}
121
122// In the overview, held h/j/k/l or arrows repeat without Cmd, and so do
123// the Cmd binds: holding Left walks the unfolded stash card by card.
124func TestOverviewHeldKeyRepeats(t *testing.T) {
125	c, _ := repeatCore(t)
126	m := c.cur().mon
127	w := m.Current()
128	w.Overflow = OverflowCascade
129	for id := WindowID(40); id < 44; id++ {
130		w.AddWindow(id)
131		w.FocusID(id)
132		w.ToggleWindowStash()
133	}
134	m.ToggleOverview()
135	m.selectCard(w, 3)
136	sendKeys(t, c, key("Left", keyLeft, 0, true))
137	if m.cardAt(w) != 2 {
138		t.Fatalf("press: card %d, want 2", m.cardAt(w))
139	}
140	tick(t, c)
141	tick(t, c)
142	if m.cardAt(w) != 0 {
143		t.Fatalf("two repeats: card %d, want 0", m.cardAt(w))
144	}
145	sendKeys(t, c, key("Left", keyLeft, 0, false))
146	if c.repeatC != nil {
147		t.Fatal("release did not stop the repeat")
148	}
149
150	// Cmd+Left, a bind, repeats in the overview too.
151	m.selectCard(w, 3)
152	sendKeys(t, c, key("Left", keyLeft, ports.ModSuper, true))
153	tick(t, c)
154	if m.cardAt(w) != 1 {
155		t.Fatalf("Cmd+Left held: card %d, want 1", m.cardAt(w))
156	}
157	sendKeys(t, c, key("Left", keyLeft, ports.ModSuper, false))
158
159	// The overview closing ends a repeat, of an overview key or a bind:
160	// Cmd+Left would go on in the layout.
161	for _, mods := range []ports.Mods{0, ports.ModSuper} {
162		if !m.ov.open {
163			m.ToggleOverview()
164		}
165		sendKeys(t, c, key("Left", keyLeft, mods, true))
166		m.CancelOverview()
167		focus := focusedID(m)
168		tick(t, c)
169		if c.repeatC != nil || focusedID(m) != focus {
170			t.Fatalf("mods %v: the repeat outlived the overview", mods)
171		}
172		sendKeys(t, c, key("Left", keyLeft, mods, false))
173	}
174}

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