main 4509ec7

input_test.go RawHistoryFolder

1package tui
2
3import (
4	"io"
5	"testing"
6	"time"
7)
8
9func TestReaderParsesCSIUShiftEnter(t *testing.T) {
10	k := readKey(t, "\x1b[13;2u")
11	if k.Kind != KeyEnter || !k.Shift || k.Alt {
12		t.Fatalf("Read kind=%v shift=%v alt=%v, want shift+enter", k.Kind, k.Shift, k.Alt)
13	}
14}
15
16func TestReaderParsesModifyOtherKeysShiftEnter(t *testing.T) {
17	k := readKey(t, "\x1b[27;2;13~")
18	if k.Kind != KeyEnter || !k.Shift || k.Alt {
19		t.Fatalf("Read kind=%v shift=%v alt=%v, want shift+enter", k.Kind, k.Shift, k.Alt)
20	}
21}
22
23func TestReaderParsesCSIUCtrlC(t *testing.T) {
24	k := readKey(t, "\x1b[99;5u")
25	if k.Kind != KeyCtrlC || !k.Ctrl {
26		t.Fatalf("Read kind=%v ctrl=%v, want ctrl+c", k.Kind, k.Ctrl)
27	}
28}
29
30func TestReaderParsesCtrlR(t *testing.T) {
31	for name, sequence := range map[string]string{
32		"raw":               "\x12",
33		"kitty protocol":    "\x1b[114;5u",
34		"modify other keys": "\x1b[27;5;114~",
35	} {
36		t.Run(name, func(t *testing.T) {
37			k := readKey(t, sequence)
38			if k.Kind != KeyCtrlR {
39				t.Fatalf("Read kind=%v, want ctrl+r", k.Kind)
40			}
41		})
42	}
43}
44
45func TestReaderParsesCSIUCtrlNumber(t *testing.T) {
46	k := readKey(t, "\x1b[49;5u")
47	if k.Kind != KeyRune || k.Rune != '1' || !k.Ctrl {
48		t.Fatalf("Read kind=%v rune=%q ctrl=%v, want ctrl+1", k.Kind, k.Rune, k.Ctrl)
49	}
50}
51
52func TestReaderParsesCSIUSuperNumber(t *testing.T) {
53	k := readKey(t, "\x1b[50;9u")
54	if k.Kind != KeyRune || k.Rune != '2' || !k.Super {
55		t.Fatalf("Read kind=%v rune=%q super=%v, want super+2", k.Kind, k.Rune, k.Super)
56	}
57}
58
59func TestReaderParsesCSIUSuperNumberWithEventType(t *testing.T) {
60	k := readKey(t, "\x1b[51;9:3u")
61	if k.Kind != KeyRune || k.Rune != '3' || !k.Super {
62		t.Fatalf("Read kind=%v rune=%q super=%v, want super+3", k.Kind, k.Rune, k.Super)
63	}
64}
65
66func TestReaderParsesCSIUHyperNumberAsSuper(t *testing.T) {
67	k := readKey(t, "\x1b[52;33u")
68	if k.Kind != KeyRune || k.Rune != '4' || !k.Super {
69		t.Fatalf("Read kind=%v rune=%q super=%v, want hyper+4 as super", k.Kind, k.Rune, k.Super)
70	}
71}
72
73func TestReaderParsesRawCtrlVAsClipboardPaste(t *testing.T) {
74	k := readKey(t, "\x16")
75	if k.Kind != KeyPasteClipboard || !k.Ctrl {
76		t.Fatalf("Read kind=%v ctrl=%v, want ctrl+v clipboard paste", k.Kind, k.Ctrl)
77	}
78}
79
80func TestReaderParsesRawAltC(t *testing.T) {
81	k := readKey(t, "\x1bc")
82	if k.Kind != KeyRune || k.Rune != 'c' || !k.Alt {
83		t.Fatalf("Read = %+v, want alt+c", k)
84	}
85}
86
87func TestReaderParsesEnhancedAltC(t *testing.T) {
88	for name, sequence := range map[string]string{
89		"kitty protocol":    "\x1b[99;3u",
90		"modify other keys": "\x1b[27;3;99~",
91	} {
92		t.Run(name, func(t *testing.T) {
93			k := readKey(t, sequence)
94			if k.Kind != KeyRune || k.Rune != 'c' || !k.Alt {
95				t.Fatalf("Read = %+v, want alt+c", k)
96			}
97		})
98	}
99}
100
101func TestReaderKeepsUnknownEnhancedCtrlLetterUnknown(t *testing.T) {
102	k := readKey(t, "\x1b[122;5u")
103	if k.Kind != KeyUnknown {
104		t.Fatalf("Read = %+v, want unknown ctrl+z", k)
105	}
106}
107
108func TestReaderParsesCSIUCtrlVAsClipboardPaste(t *testing.T) {
109	k := readKey(t, "\x1b[118;5u")
110	if k.Kind != KeyPasteClipboard || !k.Ctrl {
111		t.Fatalf("Read kind=%v ctrl=%v, want enhanced ctrl+v clipboard paste", k.Kind, k.Ctrl)
112	}
113}
114
115func TestReaderParsesModifyOtherKeysCtrlC(t *testing.T) {
116	k := readKey(t, "\x1b[27;5;99~")
117	if k.Kind != KeyCtrlC || !k.Ctrl {
118		t.Fatalf("Read kind=%v ctrl=%v, want ctrl+c", k.Kind, k.Ctrl)
119	}
120}
121
122func TestReaderParsesCSIUEsc(t *testing.T) {
123	k := readKey(t, "\x1b[27u")
124	if k.Kind != KeyEsc {
125		t.Fatalf("Read kind=%v, want esc", k.Kind)
126	}
127}
128
129func TestReaderParsesCSIUTabAndBackspace(t *testing.T) {
130	if k := readKey(t, "\x1b[9u"); k.Kind != KeyTab {
131		t.Fatalf("Read kind=%v, want tab", k.Kind)
132	}
133	if k := readKey(t, "\x1b[9;2u"); k.Kind != KeyShiftTab {
134		t.Fatalf("Read kind=%v, want shift-tab", k.Kind)
135	}
136	if k := readKey(t, "\x1b[127u"); k.Kind != KeyBackspace {
137		t.Fatalf("Read kind=%v, want backspace", k.Kind)
138	}
139}
140
141func TestReaderParsesRawEscapeEscapeAsTwoEscapes(t *testing.T) {
142	r := newPeekReader("\x1b\x1b")
143	if k := readReaderKey(t, r); k.Kind != KeyEsc {
144		t.Fatalf("first Read kind=%v, want esc", k.Kind)
145	}
146	if k := readReaderKey(t, r); k.Kind != KeyEsc {
147		t.Fatalf("second Read kind=%v, want esc", k.Kind)
148	}
149}
150
151func TestReaderParsesRawEscapeEscapeCSIAsEscapeThenUp(t *testing.T) {
152	r := newPeekReader("\x1b\x1b[A")
153	if k := readReaderKey(t, r); k.Kind != KeyEsc {
154		t.Fatalf("first Read kind=%v, want esc", k.Kind)
155	}
156	if k := readReaderKey(t, r); k.Kind != KeyUp {
157		t.Fatalf("second Read kind=%v, want up", k.Kind)
158	}
159}
160
161func TestReaderPreservesDoubleEscapePrintableAmbiguity(t *testing.T) {
162	r := newPeekReader("\x1b\x1bx")
163	if got := readReaderKey(t, r); got.Kind != KeyEsc {
164		t.Fatalf("first Read = %+v, want bare escape", got)
165	}
166	if got := readReaderKey(t, r); got.Kind != KeyRune || got.Rune != 'x' || !got.Alt {
167		t.Fatalf("second Read = %+v, want Alt+x after the documented ambiguity", got)
168	}
169}
170
171func TestReaderPreservesAmbiguousAltEscapeMappings(t *testing.T) {
172	cases := []struct {
173		name string
174		seq  string
175		want Key
176	}{
177		{name: "printable", seq: "\x1bx", want: Key{Kind: KeyRune, Rune: 'x', Alt: true}},
178		{name: "delete", seq: "\x1b\x7f", want: Key{Kind: KeyBackspace, Alt: true}},
179		{name: "word-left", seq: "\x1bb", want: Key{Kind: KeyLeft, Alt: true}},
180		{name: "word-right", seq: "\x1bf", want: Key{Kind: KeyRight, Alt: true}},
181	}
182	for _, tc := range cases {
183		t.Run(tc.name, func(t *testing.T) {
184			got := readReaderKey(t, newPeekReader(tc.seq))
185			if got != tc.want {
186				t.Fatalf("Read(%q) = %+v, want %+v", tc.seq, got, tc.want)
187			}
188		})
189	}
190}
191
192func TestReaderWithoutPeekDoesNotBlockBareEscapeOrStartAnotherPump(t *testing.T) {
193	release := make(chan struct{})
194	lookaheadStarted := make(chan struct{})
195	calls := 0
196	r := NewReader(func() (byte, error) {
197		calls++
198		if calls == 1 {
199			return 0x1b, nil
200		}
201		if calls == 2 {
202			close(lookaheadStarted)
203			<-release
204			return 'x', nil
205		}
206		return 0, io.EOF
207	})
208
209	readDone := make(chan struct{})
210	var got Key
211	var err error
212	go func() {
213		got, err = r.Read()
214		close(readDone)
215	}()
216	select {
217	case <-lookaheadStarted:
218	case <-time.After(500 * time.Millisecond):
219		t.Fatal("lookahead source was not started")
220	}
221	select {
222	case <-readDone:
223	case <-time.After(500 * time.Millisecond):
224		t.Fatal("Read blocked while disambiguating bare escape")
225	}
226	if err != nil {
227		t.Fatalf("Read: %v", err)
228	}
229	if got.Kind != KeyEsc {
230		t.Fatalf("Read kind=%v, want esc", got.Kind)
231	}
232
233	// The timed-out source call is still the sole owner of the next byte.
234	// Releasing it must make that byte available to the next Read without
235	// starting a second blocked callback.
236	close(release)
237	got, err = r.Read()
238	if err != nil {
239		t.Fatalf("Read after lookahead: %v", err)
240	}
241	if got.Kind != KeyRune || got.Rune != 'x' || calls != 2 {
242		t.Fatalf("Read after lookahead = %+v with %d source calls, want x with 2 calls", got, calls)
243	}
244}
245
246func TestReaderParsesSS3CursorKeys(t *testing.T) {
247	cases := []struct {
248		seq  string
249		want KeyKind
250	}{
251		{"\x1bOA", KeyUp},
252		{"\x1bOB", KeyDown},
253		{"\x1bOC", KeyRight},
254		{"\x1bOD", KeyLeft},
255		{"\x1bOH", KeyHome},
256		{"\x1bOF", KeyEnd},
257	}
258	for _, tc := range cases {
259		if k := readKey(t, tc.seq); k.Kind != tc.want {
260			t.Fatalf("Read(%q) kind=%v, want %v", tc.seq, k.Kind, tc.want)
261		}
262	}
263}
264
265func TestReaderParsesSGRMouseWheel(t *testing.T) {
266	cases := []struct {
267		seq  string
268		want KeyKind
269	}{
270		{"\x1b[<64;10;20M", KeyMouseWheelUp},
271		{"\x1b[<65;10;20M", KeyMouseWheelDown},
272	}
273	for _, tc := range cases {
274		k := readKey(t, tc.seq)
275		if k.Kind != tc.want {
276			t.Fatalf("Read(%q) kind=%v, want %v", tc.seq, k.Kind, tc.want)
277		}
278	}
279}
280
281func readKey(t *testing.T, seq string) Key {
282	t.Helper()
283	return readReaderKey(t, NewReader(func() (byte, error) {
284		if len(seq) == 0 {
285			return 0, io.EOF
286		}
287		b := seq[0]
288		seq = seq[1:]
289		return b, nil
290	}))
291}
292
293func newPeekReader(seq string) *Reader {
294	idx := 0
295	read := func() (byte, error) {
296		if idx >= len(seq) {
297			return 0, io.EOF
298		}
299		b := seq[idx]
300		idx++
301		return b, nil
302	}
303	peek := func(time.Duration) (byte, bool, error) {
304		if idx >= len(seq) {
305			return 0, false, nil
306		}
307		b := seq[idx]
308		idx++
309		return b, true, nil
310	}
311	return NewReaderWithPeek(read, peek)
312}
313
314func readReaderKey(t *testing.T, r *Reader) Key {
315	t.Helper()
316	k, err := r.Read()
317	if err != nil {
318		t.Fatalf("Read: %v", err)
319	}
320	return k
321}

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 1

Tags 476

↑↓ moveenter openesc close