v0.12.2 1adbe2f

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 TestReaderParsesCSIUCtrlNumber(t *testing.T) {
31	k := readKey(t, "\x1b[49;5u")
32	if k.Kind != KeyRune || k.Rune != '1' || !k.Ctrl {
33		t.Fatalf("Read kind=%v rune=%q ctrl=%v, want ctrl+1", k.Kind, k.Rune, k.Ctrl)
34	}
35}
36
37func TestReaderParsesCSIUSuperNumber(t *testing.T) {
38	k := readKey(t, "\x1b[50;9u")
39	if k.Kind != KeyRune || k.Rune != '2' || !k.Super {
40		t.Fatalf("Read kind=%v rune=%q super=%v, want super+2", k.Kind, k.Rune, k.Super)
41	}
42}
43
44func TestReaderParsesCSIUSuperNumberWithEventType(t *testing.T) {
45	k := readKey(t, "\x1b[51;9:3u")
46	if k.Kind != KeyRune || k.Rune != '3' || !k.Super {
47		t.Fatalf("Read kind=%v rune=%q super=%v, want super+3", k.Kind, k.Rune, k.Super)
48	}
49}
50
51func TestReaderParsesCSIUHyperNumberAsSuper(t *testing.T) {
52	k := readKey(t, "\x1b[52;33u")
53	if k.Kind != KeyRune || k.Rune != '4' || !k.Super {
54		t.Fatalf("Read kind=%v rune=%q super=%v, want hyper+4 as super", k.Kind, k.Rune, k.Super)
55	}
56}
57
58func TestReaderParsesRawCtrlVAsClipboardPaste(t *testing.T) {
59	k := readKey(t, "\x16")
60	if k.Kind != KeyPasteClipboard || !k.Ctrl {
61		t.Fatalf("Read kind=%v ctrl=%v, want ctrl+v clipboard paste", k.Kind, k.Ctrl)
62	}
63}
64
65func TestReaderParsesCtrlB(t *testing.T) {
66	for _, input := range []string{"\x02", "\x1b[98;5u", "\x1b[27;5;98~"} {
67		k := readKey(t, input)
68		if k.Kind != KeyCtrlB {
69			t.Fatalf("Read(%q) kind=%v ctrl=%v, want ctrl+b", input, k.Kind, k.Ctrl)
70		}
71		if input != "\x02" && !k.Ctrl {
72			t.Fatalf("Read(%q) ctrl=%v, want enhanced ctrl+b metadata", input, k.Ctrl)
73		}
74	}
75}
76
77func TestReaderParsesCSIUCtrlVAsClipboardPaste(t *testing.T) {
78	k := readKey(t, "\x1b[118;5u")
79	if k.Kind != KeyPasteClipboard || !k.Ctrl {
80		t.Fatalf("Read kind=%v ctrl=%v, want enhanced ctrl+v clipboard paste", k.Kind, k.Ctrl)
81	}
82}
83
84func TestReaderParsesModifyOtherKeysCtrlC(t *testing.T) {
85	k := readKey(t, "\x1b[27;5;99~")
86	if k.Kind != KeyCtrlC || !k.Ctrl {
87		t.Fatalf("Read kind=%v ctrl=%v, want ctrl+c", k.Kind, k.Ctrl)
88	}
89}
90
91func TestReaderParsesCSIUEsc(t *testing.T) {
92	k := readKey(t, "\x1b[27u")
93	if k.Kind != KeyEsc {
94		t.Fatalf("Read kind=%v, want esc", k.Kind)
95	}
96}
97
98func TestReaderParsesCSIUTabAndBackspace(t *testing.T) {
99	if k := readKey(t, "\x1b[9u"); k.Kind != KeyTab {
100		t.Fatalf("Read kind=%v, want tab", k.Kind)
101	}
102	if k := readKey(t, "\x1b[9;2u"); k.Kind != KeyShiftTab {
103		t.Fatalf("Read kind=%v, want shift-tab", k.Kind)
104	}
105	if k := readKey(t, "\x1b[127u"); k.Kind != KeyBackspace {
106		t.Fatalf("Read kind=%v, want backspace", k.Kind)
107	}
108}
109
110func TestReaderParsesRawEscapeEscapeAsTwoEscapes(t *testing.T) {
111	r := newPeekReader("\x1b\x1b")
112	if k := readReaderKey(t, r); k.Kind != KeyEsc {
113		t.Fatalf("first Read kind=%v, want esc", k.Kind)
114	}
115	if k := readReaderKey(t, r); k.Kind != KeyEsc {
116		t.Fatalf("second Read kind=%v, want esc", k.Kind)
117	}
118}
119
120func TestReaderParsesRawEscapeEscapeCSIAsEscapeThenUp(t *testing.T) {
121	r := newPeekReader("\x1b\x1b[A")
122	if k := readReaderKey(t, r); k.Kind != KeyEsc {
123		t.Fatalf("first Read kind=%v, want esc", k.Kind)
124	}
125	if k := readReaderKey(t, r); k.Kind != KeyUp {
126		t.Fatalf("second Read kind=%v, want up", k.Kind)
127	}
128}
129
130func TestReaderPreservesDoubleEscapePrintableAmbiguity(t *testing.T) {
131	r := newPeekReader("\x1b\x1bx")
132	if got := readReaderKey(t, r); got.Kind != KeyEsc {
133		t.Fatalf("first Read = %+v, want bare escape", got)
134	}
135	if got := readReaderKey(t, r); got.Kind != KeyRune || got.Rune != 'x' || !got.Alt {
136		t.Fatalf("second Read = %+v, want Alt+x after the documented ambiguity", got)
137	}
138}
139
140func TestReaderPreservesAmbiguousAltEscapeMappings(t *testing.T) {
141	cases := []struct {
142		name string
143		seq  string
144		want Key
145	}{
146		{name: "printable", seq: "\x1bx", want: Key{Kind: KeyRune, Rune: 'x', Alt: true}},
147		{name: "delete", seq: "\x1b\x7f", want: Key{Kind: KeyBackspace, Alt: true}},
148		{name: "word-left", seq: "\x1bb", want: Key{Kind: KeyLeft, Alt: true}},
149		{name: "word-right", seq: "\x1bf", want: Key{Kind: KeyRight, Alt: true}},
150	}
151	for _, tc := range cases {
152		t.Run(tc.name, func(t *testing.T) {
153			got := readReaderKey(t, newPeekReader(tc.seq))
154			if got != tc.want {
155				t.Fatalf("Read(%q) = %+v, want %+v", tc.seq, got, tc.want)
156			}
157		})
158	}
159}
160
161func TestReaderWithoutPeekDoesNotBlockBareEscapeOrStartAnotherPump(t *testing.T) {
162	release := make(chan struct{})
163	lookaheadStarted := make(chan struct{})
164	calls := 0
165	r := NewReader(func() (byte, error) {
166		calls++
167		if calls == 1 {
168			return 0x1b, nil
169		}
170		if calls == 2 {
171			close(lookaheadStarted)
172			<-release
173			return 'x', nil
174		}
175		return 0, io.EOF
176	})
177
178	readDone := make(chan struct{})
179	var got Key
180	var err error
181	go func() {
182		got, err = r.Read()
183		close(readDone)
184	}()
185	select {
186	case <-lookaheadStarted:
187	case <-time.After(500 * time.Millisecond):
188		t.Fatal("lookahead source was not started")
189	}
190	select {
191	case <-readDone:
192	case <-time.After(500 * time.Millisecond):
193		t.Fatal("Read blocked while disambiguating bare escape")
194	}
195	if err != nil {
196		t.Fatalf("Read: %v", err)
197	}
198	if got.Kind != KeyEsc {
199		t.Fatalf("Read kind=%v, want esc", got.Kind)
200	}
201
202	// The timed-out source call is still the sole owner of the next byte.
203	// Releasing it must make that byte available to the next Read without
204	// starting a second blocked callback.
205	close(release)
206	got, err = r.Read()
207	if err != nil {
208		t.Fatalf("Read after lookahead: %v", err)
209	}
210	if got.Kind != KeyRune || got.Rune != 'x' || calls != 2 {
211		t.Fatalf("Read after lookahead = %+v with %d source calls, want x with 2 calls", got, calls)
212	}
213}
214
215func TestReaderParsesSGRMouseWheel(t *testing.T) {
216	cases := []struct {
217		seq  string
218		want KeyKind
219	}{
220		{"\x1b[<64;10;20M", KeyMouseWheelUp},
221		{"\x1b[<65;10;20M", KeyMouseWheelDown},
222	}
223	for _, tc := range cases {
224		k := readKey(t, tc.seq)
225		if k.Kind != tc.want {
226			t.Fatalf("Read(%q) kind=%v, want %v", tc.seq, k.Kind, tc.want)
227		}
228	}
229}
230
231func readKey(t *testing.T, seq string) Key {
232	t.Helper()
233	return readReaderKey(t, NewReader(func() (byte, error) {
234		if len(seq) == 0 {
235			return 0, io.EOF
236		}
237		b := seq[0]
238		seq = seq[1:]
239		return b, nil
240	}))
241}
242
243func newPeekReader(seq string) *Reader {
244	idx := 0
245	read := func() (byte, error) {
246		if idx >= len(seq) {
247			return 0, io.EOF
248		}
249		b := seq[idx]
250		idx++
251		return b, nil
252	}
253	peek := func(time.Duration) (byte, bool, error) {
254		if idx >= len(seq) {
255			return 0, false, nil
256		}
257		b := seq[idx]
258		idx++
259		return b, true, nil
260	}
261	return NewReaderWithPeek(read, peek)
262}
263
264func readReaderKey(t *testing.T, r *Reader) Key {
265	t.Helper()
266	k, err := r.Read()
267	if err != nil {
268		t.Fatalf("Read: %v", err)
269	}
270	return k
271}

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