v0.8.0 cad1d13

kittykey_test.go RawHistoryFolder

1package kittykey
2
3import (
4	"testing"
5
6	"github.com/stretchr/testify/require"
7)
8
9func TestParse(t *testing.T) {
10	tests := []struct {
11		name    string
12		in      string
13		ev      Event
14		ok      bool
15		partial bool
16	}{
17		{name: "plain", in: "\x1b[97u", ev: Event{Code: 'a'}, ok: true},
18		{name: "ctrl", in: "\x1b[49;5u", ev: Event{Code: '1', Mods: ModCtrl}, ok: true},
19		{name: "azerty ctrl 1 with base", in: "\x1b[38::49;5u", ev: Event{Code: '&', Base: '1', Mods: ModCtrl}, ok: true},
20		{name: "shifted and base", in: "\x1b[97:65:97;2u", ev: Event{Code: 'a', Shifted: 'A', Base: 'a', Mods: ModShift}, ok: true},
21		{name: "locks stripped", in: "\x1b[97;197u", ev: Event{Code: 'a', Mods: ModCtrl}, ok: true},
22		{name: "release", in: "\x1b[97;1:3u", ev: Event{Code: 'a', Release: true}, ok: true},
23		{name: "text field ignored", in: "\x1b[97;1;97u", ev: Event{Code: 'a'}, ok: true},
24		{name: "trailing garbage", in: "\x1b[97uxyz", ev: Event{Code: 'a'}, ok: true},
25		{name: "truncated", in: "\x1b[49;5", partial: true},
26		{name: "truncated csi", in: "\x1b[", partial: true},
27		{name: "truncated esc", in: "\x1b", partial: true},
28		{name: "empty params", in: "\x1b[u"},
29		{name: "zero code", in: "\x1b[0u"},
30		{name: "mods zero", in: "\x1b[97;0u"},
31		{name: "bad event", in: "\x1b[97;1:4u"},
32		{name: "too many fields", in: "\x1b[97;1;2;3u"},
33		{name: "other final", in: "\x1b[1;5A"},
34		{name: "private reply", in: "\x1b[?5u"},
35		{name: "huge number", in: "\x1b[99999999u"},
36		{name: "not csi", in: "a"},
37	}
38	for _, tt := range tests {
39		t.Run(tt.name, func(t *testing.T) {
40			ev, n, ok, partial := Parse([]byte(tt.in))
41			require.Equal(t, tt.ok, ok)
42			require.Equal(t, tt.partial, partial)
43			if tt.ok {
44				require.Equal(t, tt.ev, ev)
45				want := len(tt.in)
46				if tt.name == "trailing garbage" {
47					want -= len("xyz")
48				}
49				require.Equal(t, want, n)
50			}
51		})
52	}
53}
54
55func TestTranslateLegacy(t *testing.T) {
56	tests := []struct {
57		name string
58		in   string
59		want string
60	}{
61		{name: "legacy passthrough", in: "hello\x1b[A\x1bj\r", want: "hello\x1b[A\x1bj\r"},
62		{name: "escape", in: "\x1b[27u", want: "\x1b"},
63		{name: "alt escape", in: "\x1b[27;3u", want: "\x1b\x1b"},
64		{name: "enter", in: "\x1b[13u", want: "\r"},
65		{name: "tab", in: "\x1b[9u", want: "\t"},
66		{name: "shift tab", in: "\x1b[9;2u", want: "\x1b[Z"},
67		{name: "backspace", in: "\x1b[127u", want: "\x7f"},
68		{name: "ctrl backspace", in: "\x1b[127;5u", want: "\x08"},
69		{name: "ctrl i", in: "\x1b[105;5u", want: "\t"},
70		{name: "ctrl a", in: "\x1b[97;5u", want: "\x01"},
71		{name: "ctrl alt a", in: "\x1b[97;7u", want: "\x1b\x01"},
72		{name: "ctrl shift a", in: "\x1b[97:65;6u", want: "\x01"},
73		{name: "ctrl space", in: "\x1b[32;5u", want: "\x00"},
74		{name: "ctrl 1 has no control byte", in: "\x1b[49;5u", want: "1"},
75		{name: "ctrl 3", in: "\x1b[51;5u", want: "\x1b"},
76		{name: "azerty ctrl ampersand", in: "\x1b[38::49;5u", want: "&"},
77		{name: "cyrillic ctrl uses base", in: "\x1b[1092::97;5u", want: "\x01"},
78		{name: "shift letter with shifted", in: "\x1b[97:65;2u", want: "A"},
79		{name: "shift letter without shifted", in: "\x1b[97;2u", want: "A"},
80		{name: "alt utf8", in: "\x1b[233;3u", want: "\x1bé"},
81		{name: "super dropped", in: "\x1b[97;9u", want: ""},
82		{name: "release dropped", in: "\x1b[97;1:3u", want: ""},
83		{name: "private use dropped", in: "\x1b[57441u", want: ""},
84		{name: "keypad digit", in: "\x1b[57400u", want: "1"},
85		{name: "keypad enter", in: "\x1b[57414u", want: "\r"},
86		{name: "ctrl keypad left", in: "\x1b[57417;5u", want: "\x1b[1;5D"},
87		{name: "ctrl keypad delete", in: "\x1b[57426;5u", want: "\x1b[3;5~"},
88		{name: "keypad begin", in: "\x1b[57427u", want: "\x1b[E"},
89		{name: "keypad separator", in: "\x1b[57416u", want: ","},
90		{name: "f1 unmodified", in: "\x1b[P", want: "\x1bOP"},
91		{name: "f2 modified kept", in: "\x1b[1;5Q", want: "\x1b[1;5Q"},
92		{name: "mixed", in: "a\x1b[97;5ub\x1b[27uc", want: "a\x01b\x1bc"},
93		{name: "truncated prefix untouched", in: "x\x1b[97;5", want: "x\x1b[97;5"},
94		{name: "trailing garbage", in: "\x1b[97;5u\x1b[zz", want: "\x01\x1b[zz"},
95		{name: "paste body untouched", in: "\x1b[200~\x1b[97;5u\x1b[201~\x1b[97;5u", want: "\x1b[200~\x1b[97;5u\x1b[201~\x01"},
96		{name: "unterminated paste untouched", in: "\x1b[200~\x1b[97;5u", want: "\x1b[200~\x1b[97;5u"},
97		{name: "mouse untouched", in: "\x1b[<0;1;1M", want: "\x1b[<0;1;1M"},
98		{name: "keyboard reply untouched", in: "\x1b[?5u", want: "\x1b[?5u"},
99	}
100	for _, tt := range tests {
101		t.Run(tt.name, func(t *testing.T) {
102			require.Equal(t, []byte(tt.want), append([]byte{}, Translate([]byte(tt.in), 0)...))
103		})
104	}
105}
106
107func TestTranslateKittyReceiver(t *testing.T) {
108	tests := []struct {
109		name  string
110		in    string
111		flags int
112		want  string
113	}{
114		{name: "same flags identity", in: "\x1b[38::49;5u", flags: OuterFlags, want: "\x1b[38::49;5u"},
115		{name: "strip alternates", in: "\x1b[38::49;5u", flags: FlagDisambiguate, want: "\x1b[38;5u"},
116		{name: "keep shifted", in: "\x1b[97:65;2u", flags: FlagDisambiguate | FlagAlternateKeys, want: "\x1b[97:65;2u"},
117		{name: "unmodified", in: "\x1b[27u", flags: FlagDisambiguate, want: "\x1b[27u"},
118		{name: "release dropped without events", in: "\x1b[97;1:3u", flags: FlagDisambiguate, want: ""},
119		{name: "release kept with events", in: "\x1b[97;1:3u", flags: FlagDisambiguate | FlagReportEvents, want: "\x1b[97;1:3u"},
120		{name: "f1 stays csi", in: "\x1b[P", flags: FlagDisambiguate, want: "\x1b[P"},
121	}
122	for _, tt := range tests {
123		t.Run(tt.name, func(t *testing.T) {
124			require.Equal(t, []byte(tt.want), append([]byte{}, Translate([]byte(tt.in), tt.flags)...))
125		})
126	}
127}
128
129func TestTranslateReturnsInputWhenUnchanged(t *testing.T) {
130	in := []byte("plain \x1b[A text")
131	out := Translate(in, 0)
132	require.Same(t, &in[0], &out[0])
133}
134
135func TestEventDigit(t *testing.T) {
136	tests := []struct {
137		ev   Event
138		want int
139		ok   bool
140	}{
141		{ev: Event{Code: '1'}, want: 1, ok: true},
142		{ev: Event{Code: '&', Base: '1'}, want: 1, ok: true},
143		{ev: Event{Code: 'é', Base: '2'}, want: 2, ok: true},
144		{ev: Event{Code: '0'}},
145		{ev: Event{Code: 'a'}},
146	}
147	for _, tt := range tests {
148		got, ok := tt.ev.Digit()
149		require.Equal(t, tt.ok, ok, "%+v", tt.ev)
150		require.Equal(t, tt.want, got)
151	}
152}
153
154func FuzzTranslate(f *testing.F) {
155	for _, seed := range []string{"\x1b[97;5u", "\x1b[38::49;5u", "\x1b[200~x\x1b[201~", "\x1b[1;5P", "a\x1b["} {
156		f.Add([]byte(seed), 0)
157		f.Add([]byte(seed), OuterFlags)
158	}
159	f.Fuzz(func(t *testing.T, data []byte, flags int) {
160		out := Translate(data, flags&0x1f)
161		// Translation never grows input by more than a bounded factor.
162		require.LessOrEqual(t, len(out), 2*len(data)+16)
163	})
164}
165
166func BenchmarkTranslateLegacyText(b *testing.B) {
167	data := []byte("the quick brown fox jumps over the lazy dog\r")
168	b.ReportAllocs()
169	for b.Loop() {
170		_ = Translate(data, 0)
171	}
172}

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 12

Tags 11

↑↓ moveenter openesc close