v0.26.0 7e66a81

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

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