main ece7682

normalize_test.go RawHistoryFolder

1package kittykey
2
3import (
4	"strings"
5	"testing"
6
7	"github.com/stretchr/testify/require"
8)
9
10func TestNormalizerRewritesRequestFlagsToOuterFlags(t *testing.T) {
11	tests := []struct {
12		name string
13		in   string
14		want string
15	}{
16		{name: "legacy passthrough", in: "hello\x1b[A\x1bOP\r", want: "hello\x1b[A\x1bOP\r"},
17		{name: "outer csi u passthrough", in: "\x1b[97;5u\x1b[38::49;5u", want: "\x1b[97;5u\x1b[38::49;5u"},
18		{name: "text key", in: "\x1b[97;;97u", want: "a"},
19		{name: "text key without text field", in: "\x1b[97u", want: "a"},
20		{name: "shift text", in: "\x1b[97:65;2;65u", want: "A"},
21		{name: "shift without text uses shifted", in: "\x1b[97:65;2u", want: "A"},
22		{name: "composed text event", in: "\x1b[0;;229u", want: "å"},
23		{name: "multi codepoint text", in: "\x1b[101;;101:769u", want: "e\u0301"},
24		{name: "control codes dropped from text", in: "\x1b[0;;7:97u", want: "a"},
25		{name: "repeat is press", in: "\x1b[97;1:2;97u", want: "a"},
26		{name: "release dropped", in: "\x1b[97;1:3u", want: ""},
27		{name: "space", in: "\x1b[32;;32u", want: " "},
28		{name: "enter", in: "\x1b[13u", want: "\r"},
29		{name: "tab", in: "\x1b[9u", want: "\t"},
30		{name: "backspace", in: "\x1b[127u", want: "\x7f"},
31		{name: "shift enter stays csi u", in: "\x1b[13;2u", want: "\x1b[13;2u"},
32		{name: "escape", in: "\x1b[27u", want: "\x1b[27u"},
33		{name: "ctrl letter", in: "\x1b[99;5u", want: "\x1b[99;5u"},
34		{name: "ctrl digit with base", in: "\x1b[38::49;5u", want: "\x1b[38::49;5u"},
35		{name: "alt letter without text", in: "\x1b[97;3u", want: "\x1b[97;3u"},
36		{name: "altgr text wins over modifiers", in: "\x1b[113;7;64u", want: "@"},
37		{name: "long ime commit", in: "\x1b[0;;" + strings.Repeat("19968:", 11) + "19968u", want: strings.Repeat("一", 12)},
38		{name: "caps lock dropped from text key", in: "\x1b[97;65;65u", want: "A"},
39		{name: "caps lock kept on ctrl letter", in: "\x1b[97;69u", want: "\x1b[97;69u"},
40		{name: "both locks kept on alt letter", in: "\x1b[97;195u", want: "\x1b[97;195u"},
41		{name: "num lock kept on escape", in: "\x1b[27;129u", want: "\x1b[27;129u"},
42		{name: "caps lock kept on arrow", in: "\x1b[1;65A", want: "\x1b[1;65A"},
43		{name: "caps lock arrow repeat", in: "\x1b[1;65:2A", want: "\x1b[1;65A"},
44		{name: "caps lock delete", in: "\x1b[3;65~", want: "\x1b[3;65~"},
45		{name: "ctrl press dropped", in: "\x1b[57442;5u", want: ""},
46		{name: "ctrl release dropped", in: "\x1b[57442;1:3u", want: ""},
47		{name: "shift key dropped", in: "\x1b[57441;2u", want: ""},
48		{name: "caps lock dropped", in: "\x1b[57358;65u", want: ""},
49		{name: "arrow unmodified passthrough", in: "\x1b[A", want: "\x1b[A"},
50		{name: "arrow repeat", in: "\x1b[1;1:2A", want: "\x1b[A"},
51		{name: "arrow release dropped", in: "\x1b[1;1:3A", want: ""},
52		{name: "ctrl arrow", in: "\x1b[1;5C", want: "\x1b[1;5C"},
53		{name: "ctrl arrow repeat", in: "\x1b[1;5:2C", want: "\x1b[1;5C"},
54		{name: "delete release dropped", in: "\x1b[3;1:3~", want: ""},
55		{name: "delete repeat", in: "\x1b[3;1:2~", want: "\x1b[3~"},
56		{name: "ctrl delete", in: "\x1b[3;5~", want: "\x1b[3;5~"},
57		{name: "mouse untouched", in: "\x1b[<0;1;1M", want: "\x1b[<0;1;1M"},
58		{name: "keyboard reply untouched", in: "\x1b[?5u", want: "\x1b[?5u"},
59		{name: "cursor report untouched", in: "\x1b[12;40R", want: "\x1b[12;40R"},
60		{name: "paste body untouched", in: "\x1b[200~\x1b[97;1:3u\x1b[201~\x1b[97;1:3u", want: "\x1b[200~\x1b[97;1:3u\x1b[201~"},
61		{name: "lone esc passes before any kitty key", in: "x\x1b", want: "x\x1b"},
62		{name: "lone esc held after a kitty key", in: "\x1b[97;;97u\x1b", want: "a"},
63		{name: "trailing garbage", in: "\x1b[97;;97u\x1b[zz", want: "a\x1b[zz"},
64		{name: "bad event type kept", in: "\x1b[97;1:4u", want: "\x1b[97;1:4u"},
65	}
66	for _, tt := range tests {
67		t.Run(tt.name, func(t *testing.T) {
68			var n Normalizer
69			require.Equal(t, tt.want, string(n.Normalize([]byte(tt.in))))
70		})
71	}
72}
73
74func TestNormalizerHoldsSplitSequences(t *testing.T) {
75	tests := []struct {
76		name  string
77		reads []string
78		want  []string
79	}{
80		{name: "split csi u", reads: []string{"a\x1b[97;", ";98u"}, want: []string{"a", "b"}},
81		{name: "split csi prefix", reads: []string{"\x1b[", "57442;1:3u"}, want: []string{"", ""}},
82		{name: "split paste close", reads: []string{"\x1b[200~x\x1b[20", "1~\x1b[97;;97u"}, want: []string{"\x1b[200~x", "\x1b[201~a"}},
83		{name: "split after esc", reads: []string{"\x1b[27u\x1b", "[57442;5u", "\x1b", "[97;;97u"}, want: []string{"\x1b[27u", "", "", "a"}},
84		{name: "legacy escape before the switch", reads: []string{"\x1b", "\x1b[97;;97u"}, want: []string{"\x1b", "a"}},
85		{name: "split paste open", reads: []string{"\x1b[20", "0~\x1b[97;1:3u\x1b[201~"}, want: []string{"", "\x1b[200~\x1b[97;1:3u\x1b[201~"}},
86	}
87	for _, tt := range tests {
88		t.Run(tt.name, func(t *testing.T) {
89			var n Normalizer
90			for i, read := range tt.reads {
91				require.Equal(t, tt.want[i], string(n.Normalize([]byte(read))), "read %d", i)
92			}
93		})
94	}
95}
96
97// TestNormalizerEverySplitMatchesOneRead feeds each input split at every byte
98// and requires the same output and Ctrl state as one read.
99func TestNormalizerEverySplitMatchesOneRead(t *testing.T) {
100	// Each input starts with a kitty key, as a terminal in RequestFlags mode
101	// sends before any split can matter.
102	inputs := []string{
103		"\x1b[57442;5u\x1b[49;5u\x1b[49;5:3u",
104		"a\x1b[97;;97u\x1b[57442;1:3u",
105		"\x1b[200~\x1b[97;1:3u\x1b[201~\x1b[1;65:2A",
106		"\x1b[0;;19968:19968u\x1b[27u",
107	}
108	for _, in := range inputs {
109		var whole Normalizer
110		whole.Normalize([]byte("\x1b[27u"))
111		want := string(whole.Normalize([]byte(in)))
112		for cut := 1; cut < len(in); cut++ {
113			var n Normalizer
114			n.Normalize([]byte("\x1b[27u"))
115			got := string(n.Normalize([]byte(in[:cut]))) + string(n.Normalize([]byte(in[cut:])))
116			require.Equal(t, want, got, "input %q cut %d", in, cut)
117			require.Equal(t, whole.CtrlHeld(), n.CtrlHeld(), "input %q cut %d", in, cut)
118		}
119	}
120}
121
122func TestNormalizerTracksCtrl(t *testing.T) {
123	tests := []struct {
124		name  string
125		reads []string
126		held  []bool
127	}{
128		{name: "left ctrl press and release", reads: []string{"\x1b[57442;5u", "\x1b[57442;1:3u"}, held: []bool{true, false}},
129		{name: "right ctrl", reads: []string{"\x1b[57448;5u", "\x1b[57448;1:3u"}, held: []bool{true, false}},
130		{name: "ctrl repeat keeps held", reads: []string{"\x1b[57442;5u", "\x1b[57442;5:2u"}, held: []bool{true, true}},
131		{name: "one of two ctrl released", reads: []string{"\x1b[57442;5u", "\x1b[57448;5u", "\x1b[57448;5:3u"}, held: []bool{true, true, true}},
132		{name: "ctrl digit keeps held", reads: []string{"\x1b[57442;5u", "\x1b[49;5u", "\x1b[49;5:3u"}, held: []bool{true, true, true}},
133		{name: "key without ctrl clears", reads: []string{"\x1b[57442;5u", "\x1b[97;;97u"}, held: []bool{true, false}},
134		{name: "arrow without ctrl clears", reads: []string{"\x1b[57442;5u", "\x1b[1;1:2A"}, held: []bool{true, false}},
135		{name: "ctrl shortcut clears", reads: []string{"\x1b[57442;5u", "\x1b[114;5u", "\x1b[114;5:3u"}, held: []bool{true, false, false}},
136		{name: "ctrl arrow clears", reads: []string{"\x1b[57442;5u", "\x1b[1;5C"}, held: []bool{true, false}},
137		{name: "azerty ctrl digit keeps held", reads: []string{"\x1b[57442;5u", "\x1b[38::49;5u"}, held: []bool{true, true}},
138		{name: "ctrl shift keeps held", reads: []string{"\x1b[57442;5u", "\x1b[57441;6u", "\x1b[57441;5:3u"}, held: []bool{true, true, true}},
139		{name: "composed text keeps held", reads: []string{"\x1b[57442;5u", "\x1b[0;;229u"}, held: []bool{true, true}},
140		{name: "legacy text never sets", reads: []string{"\x1b[97;5u"}, held: []bool{false}},
141		{name: "shift does not set", reads: []string{"\x1b[57441;2u"}, held: []bool{false}},
142		{name: "ctrl in paste ignored", reads: []string{"\x1b[200~\x1b[57442;5u\x1b[201~"}, held: []bool{false}},
143	}
144	for _, tt := range tests {
145		t.Run(tt.name, func(t *testing.T) {
146			var n Normalizer
147			for i, read := range tt.reads {
148				n.Normalize([]byte(read))
149				require.Equal(t, tt.held[i], n.CtrlHeld(), "read %d", i)
150			}
151		})
152	}
153	var n Normalizer
154	n.Normalize([]byte("\x1b[57442;5u"))
155	n.ReleaseCtrl()
156	require.False(t, n.CtrlHeld())
157}
158
159func FuzzNormalize(f *testing.F) {
160	for _, seed := range []string{"\x1b[97;;97u", "\x1b[57442;5u", "\x1b[200~x\x1b[201~", "\x1b[1;5:3A", "a\x1b[", "\x1b[0;;229:7u"} {
161		f.Add([]byte(seed))
162	}
163	f.Fuzz(func(t *testing.T, data []byte) {
164		var n Normalizer
165		out := n.Normalize(data)
166		out = append(out, n.Normalize(nil)...)
167		require.LessOrEqual(t, len(out), 4*len(data)+16)
168	})
169}

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