v0.4.2 baf7aea

query_test.go RawHistoryFolder

1package vt
2
3import (
4	"bytes"
5	"testing"
6
7	renderer "github.com/bnema/vev-vt/core"
8)
9
10func TestScreenQueryResponses(t *testing.T) {
11	cases := []struct {
12		name  string
13		input string
14		want  string
15	}{
16		{name: "primary DA bare", input: "\x1b[c", want: "\x1b[?62;22c"},
17		{name: "primary DA with 0", input: "\x1b[0c", want: "\x1b[?62;22c"},
18		{name: "secondary DA", input: "\x1b[>c", want: "\x1b[>0;0;0c"},
19		{name: "secondary DA with 0", input: "\x1b[>0c", want: "\x1b[>0;0;0c"},
20		{name: "DSR status report", input: "\x1b[5n", want: "\x1b[0n"},
21		{name: "CPR at home", input: "\x1b[6n", want: "\x1b[1;1R"},
22		{name: "CPR after CUP", input: "\x1b[3;7H\x1b[6n", want: "\x1b[3;7R"},
23		{name: "private CPR", input: "\x1b[3;7H\x1b[?6n", want: "\x1b[?3;7R"},
24		{name: "CPR in DECOM reports origin-relative row", input: "\x1b[2;4r\x1b[?6h\x1b[6n", want: "\x1b[1;1R"},
25		{name: "DECRQM DECOM reset", input: "\x1b[?6$p", want: "\x1b[?6;2$y"},
26		{name: "DECRQM DECOM set", input: "\x1b[?6h\x1b[?6$p", want: "\x1b[?6;1$y"},
27		{name: "DECRQM IRM reset", input: "\x1b[4$p", want: "\x1b[4;2$y"},
28		{name: "DECRQM IRM set", input: "\x1b[4h\x1b[4$p", want: "\x1b[4;1$y"},
29		{name: "DECRQM 2026 reset", input: "\x1b[?2026$p", want: "\x1b[?2026;2$y"},
30		{name: "DECRQM 2026 set", input: "\x1b[?2026h\x1b[?2026$p", want: "\x1b[?2026;1$y"},
31		{name: "DECRQM 2031 reset", input: "\x1b[?2031$p", want: "\x1b[?2031;2$y"},
32		{name: "DECRQM 2031 set", input: "\x1b[?2031h\x1b[?2031$p", want: "\x1b[?2031;1$y"},
33		{name: "color scheme DSR dark", input: "\x1b[?996n", want: ColorSchemeReportDark},
34		{name: "color scheme DSR light", input: "\x1b[?996n", want: ColorSchemeReportLight},
35		{name: "DECRQM unknown mode", input: "\x1b[?1337$p", want: "\x1b[?1337;0$y"},
36		{name: "kitty keyboard query unanswered", input: "\x1b[?u", want: ""},
37		{name: "XTVERSION unanswered", input: "\x1b[>0q", want: ""},
38		{name: "DA split across writes", input: "", want: "\x1b[?62;22c"},
39	}
40	for _, tc := range cases {
41		t.Run(tc.name, func(t *testing.T) {
42			s := NewScreen(80, 24)
43			var got bytes.Buffer
44			s.OnResponse = func(b []byte) { got.Write(b) }
45			switch tc.name {
46			case "DA split across writes":
47				s.Write([]byte("\x1b["))
48				s.Write([]byte("0c"))
49			case "color scheme DSR light":
50				s.SetColorScheme(true)
51				s.Write([]byte(tc.input))
52			case "color scheme DSR dark":
53				s.SetColorScheme(false)
54				s.Write([]byte(tc.input))
55			default:
56				s.Write([]byte(tc.input))
57			}
58			if got.String() != tc.want {
59				t.Fatalf("response = %q, want %q", got.String(), tc.want)
60			}
61		})
62	}
63}
64
65func TestScreenOSCDefaultColorQueries(t *testing.T) {
66	cases := []struct {
67		name  string
68		input string
69		want  string
70	}{
71		{name: "foreground BEL", input: "\x1b]10;?\a", want: "\x1b]10;rgb:1212/3434/5656\a"},
72		{name: "background BEL", input: "\x1b]11;?\a", want: "\x1b]11;rgb:7878/9a9a/bcbc\a"},
73		{name: "foreground ST", input: "\x1b]10;?\x1b\\", want: "\x1b]10;rgb:1212/3434/5656\x1b\\"},
74		{name: "background ST", input: "\x1b]11;?\x1b\\", want: "\x1b]11;rgb:7878/9a9a/bcbc\x1b\\"},
75	}
76	for _, tc := range cases {
77		t.Run(tc.name, func(t *testing.T) {
78			s := NewScreen(80, 24)
79			s.SetDefaultColors(
80				renderer.RGB{R: 0x12, G: 0x34, B: 0x56},
81				renderer.RGB{R: 0x78, G: 0x9a, B: 0xbc},
82				true,
83			)
84			var got bytes.Buffer
85			s.OnResponse = func(b []byte) { got.Write(b) }
86			s.Write([]byte(tc.input))
87			if got.String() != tc.want {
88				t.Fatalf("response = %q, want %q", got.String(), tc.want)
89			}
90		})
91	}
92}
93
94func TestScreenOSCDefaultColorQueriesUnknownAreSilent(t *testing.T) {
95	s := NewScreen(80, 24)
96	s.SetDefaultColors(renderer.RGB{R: 1, G: 2, B: 3}, renderer.RGB{R: 4, G: 5, B: 6}, false)
97	var got bytes.Buffer
98	s.OnResponse = func(b []byte) { got.Write(b) }
99	s.Write([]byte("\x1b]10;?\a\x1b]11;?\a"))
100	if got.Len() != 0 {
101		t.Fatalf("response = %q, want silence", got.String())
102	}
103}
104
105func TestScreenOSCDefaultColorQuerySplitAcrossWrites(t *testing.T) {
106	s := NewScreen(80, 24)
107	s.SetDefaultColors(renderer.RGB{R: 0x01, G: 0x02, B: 0x03}, renderer.RGB{R: 0x04, G: 0x05, B: 0x06}, true)
108	var got bytes.Buffer
109	s.OnResponse = func(b []byte) { got.Write(b) }
110	s.Write([]byte("\x1b]10"))
111	s.Write([]byte(";?\a"))
112	if got.String() != "\x1b]10;rgb:0101/0202/0303\a" {
113		t.Fatalf("response = %q", got.String())
114	}
115}
116
117func TestScreenQueriesWithNilResponderDoNotPanic(t *testing.T) {
118	s := NewScreen(80, 24)
119	s.Write([]byte("\x1b[c\x1b[6n\x1b[?2026$p"))
120	s.SetDefaultColors(renderer.RGB{R: 1, G: 2, B: 3}, renderer.RGB{R: 4, G: 5, B: 6}, true)
121	s.Write([]byte("\x1b]10;?\a\x1b]11;?\x1b\\"))
122}
123
124func TestCSIuDispatch(t *testing.T) {
125	cases := []struct {
126		name    string
127		input   string
128		wantRow int
129		wantCol int
130	}{
131		{name: "bare u restores cursor", input: "\x1b[5;10H\x1b[s\x1b[2;2H\x1b[u", wantRow: 4, wantCol: 9},
132		{name: "kitty query leaves cursor", input: "\x1b[5;10H\x1b[s\x1b[2;2H\x1b[?u", wantRow: 1, wantCol: 1},
133		{name: "kitty push leaves cursor", input: "\x1b[5;10H\x1b[s\x1b[2;2H\x1b[>1u", wantRow: 1, wantCol: 1},
134		{name: "kitty pop leaves cursor", input: "\x1b[5;10H\x1b[s\x1b[2;2H\x1b[<u", wantRow: 1, wantCol: 1},
135	}
136	for _, tc := range cases {
137		t.Run(tc.name, func(t *testing.T) {
138			s := NewScreen(80, 24)
139			s.Write([]byte(tc.input))
140			if s.Row != tc.wantRow || s.Col != tc.wantCol {
141				t.Fatalf("cursor = %d;%d, want %d;%d", s.Row, s.Col, tc.wantRow, tc.wantCol)
142			}
143		})
144	}
145}
146
147func TestScreenColorSchemeQueriesUnknownAreSilent(t *testing.T) {
148	s := NewScreen(80, 24)
149	s.SetColorScheme(true)
150	s.ClearColorScheme()
151	var got bytes.Buffer
152	s.OnResponse = func(b []byte) { got.Write(b) }
153	s.Write([]byte("\x1b[?996n"))
154	if got.Len() != 0 {
155		t.Fatalf("response = %q, want silence", got.String())
156	}
157}
158
159func TestScreenResetClearsColorSchemeModeSubscription(t *testing.T) {
160	s := NewScreen(80, 24)
161	var got bytes.Buffer
162	s.OnResponse = func(b []byte) { got.Write(b) }
163
164	s.Write([]byte("\x1b[?2031h"))
165	s.Write([]byte("\x1bc"))
166
167	s.SetColorScheme(true)
168	if got.Len() != 0 {
169		t.Fatalf("unsolicited response after reset = %q, want silence", got.String())
170	}
171
172	got.Reset()
173	s.Write([]byte("\x1b[?2031$p"))
174	if got.String() != "\x1b[?2031;2$y" {
175		t.Fatalf("DECRQM 2031 after reset = %q, want reset state", got.String())
176	}
177}
178
179func TestScreenColorSchemeUnsolicitedOnlySubscribedAndChanged(t *testing.T) {
180	s := NewScreen(80, 24)
181	var got bytes.Buffer
182	s.OnResponse = func(b []byte) { got.Write(b) }
183
184	s.SetColorScheme(true)
185	if got.Len() != 0 {
186		t.Fatalf("unsubscribed response = %q, want silence", got.String())
187	}
188
189	s.Write([]byte("\x1b[?2031h"))
190	if !s.ColorSchemeMode() {
191		t.Fatalf("ColorSchemeMode() = false, want true")
192	}
193	s.SetColorScheme(true)
194	if got.Len() != 0 {
195		t.Fatalf("unchanged response = %q, want silence", got.String())
196	}
197	s.SetColorScheme(false)
198	if got.String() != ColorSchemeReportDark {
199		t.Fatalf("changed response = %q, want dark notification", got.String())
200	}
201	got.Reset()
202	s.Write([]byte("\x1b[?2031l"))
203	s.SetColorScheme(true)
204	if got.Len() != 0 {
205		t.Fatalf("disabled response = %q, want silence", got.String())
206	}
207}

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 2

Tags 13

↑↓ moveenter openesc close