main ab6686c

color_profile_test.go RawHistoryFolder

1package ansi
2
3import "testing"
4
5func TestRendererANSI256ProjectsRGBColors(t *testing.T) {
6	frame := NewFrame(1, 1)
7	frame.Set(0, 0, Cell{Rune: 'X', Style: Style{
8		Foreground:           -1,
9		Background:           -1,
10		HasForegroundRGB:     true,
11		ForegroundRGB:        RGB{R: 255, G: 0, B: 0},
12		HasBackgroundRGB:     true,
13		BackgroundRGB:        RGB{R: 8, G: 8, B: 8},
14		HasUnderlineColorRGB: true,
15		UnderlineColorRGB:    RGB{R: 128, G: 128, B: 128},
16	}})
17
18	out, err := NewWithColorProfile(Capabilities{}, ColorProfileANSI256).Draw(frame, []Damage{FullRedraw()})
19	if err != nil {
20		t.Fatal(err)
21	}
22	want := "\x1b[1;1H\x1b[0;38;5;196;48;5;232;58;5;244mX\x1b[0m"
23	if got := string(out); got != want {
24		t.Fatalf("output = %q, want %q", got, want)
25	}
26}
27
28func TestRendererANSI16UsesBasicColorsAndOmitsUnderlineColor(t *testing.T) {
29	frame := NewFrame(1, 1)
30	frame.Set(0, 0, Cell{Rune: 'X', Style: Style{
31		Bold:                 true,
32		Attrs:                AttrUnderline,
33		Foreground:           -1,
34		Background:           4,
35		HasForegroundRGB:     true,
36		ForegroundRGB:        RGB{R: 255, G: 0, B: 0},
37		UnderlineStyle:       UnderlineCurly,
38		HasUnderlineColor:    true,
39		UnderlineColor:       9,
40		HasUnderlineColorRGB: true,
41		UnderlineColorRGB:    RGB{R: 0, G: 255, B: 0},
42	}})
43
44	out, err := NewWithColorProfile(Capabilities{}, ColorProfileANSI16).Draw(frame, []Damage{FullRedraw()})
45	if err != nil {
46		t.Fatal(err)
47	}
48	want := "\x1b[1;1H\x1b[0;1;4:3;91;44mX\x1b[0m"
49	if got := string(out); got != want {
50		t.Fatalf("output = %q, want %q", got, want)
51	}
52}
53
54func TestRendererANSI16DownsamplesExtendedIndexedColors(t *testing.T) {
55	frame := NewFrame(1, 1)
56	frame.Set(0, 0, Cell{Rune: 'X', Style: Style{Foreground: 196, Background: 21}})
57
58	out, err := NewWithColorProfile(Capabilities{}, ColorProfileANSI16).Draw(frame, []Damage{FullRedraw()})
59	if err != nil {
60		t.Fatal(err)
61	}
62	want := "\x1b[1;1H\x1b[0;91;104mX\x1b[0m"
63	if got := string(out); got != want {
64		t.Fatalf("output = %q, want %q", got, want)
65	}
66}
67
68func TestRendererIndexedColorsFollowProfile(t *testing.T) {
69	style := Style{
70		Foreground:        9,
71		Background:        12,
72		HasUnderlineColor: true,
73		UnderlineColor:    10,
74	}
75	tests := []struct {
76		name      string
77		profile   ColorProfile
78		wantStyle string
79	}{
80		{name: "truecolor preserves indexed colors", profile: ColorProfileTrueColor, wantStyle: "\x1b[0;38;5;9;48;5;12;58;5;10m"},
81		{name: "ANSI-256 preserves indexed colors", profile: ColorProfileANSI256, wantStyle: "\x1b[0;38;5;9;48;5;12;58;5;10m"},
82		{name: "ANSI-16 preserves basic indexes", profile: ColorProfileANSI16, wantStyle: "\x1b[0;91;104m"},
83	}
84
85	for _, tt := range tests {
86		t.Run(tt.name, func(t *testing.T) {
87			frame := NewFrame(1, 1)
88			frame.Set(0, 0, Cell{Rune: 'X', Style: style})
89			out, err := NewWithColorProfile(Capabilities{}, tt.profile).Draw(frame, []Damage{FullRedraw()})
90			if err != nil {
91				t.Fatal(err)
92			}
93			want := "\x1b[1;1H" + tt.wantStyle + "X\x1b[0m"
94			if got := string(out); got != want {
95				t.Fatalf("output = %q, want %q", got, want)
96			}
97		})
98	}
99}
100
101func TestRendererMonochromeOmitsColorsAndPreservesAttributes(t *testing.T) {
102	frame := NewFrame(2, 1)
103	style := Style{
104		Bold:                 true,
105		Italic:               true,
106		Inverse:              true,
107		Attrs:                AttrUnderline | AttrStrikethrough,
108		Foreground:           -1,
109		Background:           200,
110		HasForegroundRGB:     true,
111		ForegroundRGB:        RGB{R: 12, G: 34, B: 56},
112		UnderlineStyle:       UnderlineDouble,
113		HasUnderlineColor:    true,
114		UnderlineColor:       9,
115		HasUnderlineColorRGB: true,
116		UnderlineColorRGB:    RGB{R: 200, G: 100, B: 50},
117	}
118	frame.Set(0, 0, Cell{Rune: 'A', Style: style})
119	style.ForegroundRGB = RGB{R: 255, G: 0, B: 0}
120	frame.Set(1, 0, Cell{Rune: 'B', Style: style})
121
122	out, err := NewWithColorProfile(Capabilities{}, ColorProfileMonochrome).Draw(frame, []Damage{FullRedraw()})
123	if err != nil {
124		t.Fatal(err)
125	}
126	want := "\x1b[1;1H\x1b[0;1;3;21;7;9mAB\x1b[0m"
127	if got := string(out); got != want {
128		t.Fatalf("output = %q, want %q", got, want)
129	}
130}
131
132func TestRendererColorProfilesApplyToFullIncrementalAndResetDraws(t *testing.T) {
133	style := Style{
134		Attrs:                AttrUnderline,
135		Foreground:           -1,
136		Background:           -1,
137		HasForegroundRGB:     true,
138		ForegroundRGB:        RGB{R: 255, G: 0, B: 0},
139		HasBackgroundRGB:     true,
140		BackgroundRGB:        RGB{R: 0, G: 0, B: 255},
141		HasUnderlineColorRGB: true,
142		UnderlineColorRGB:    RGB{R: 0, G: 255, B: 0},
143	}
144	tests := []struct {
145		name    string
146		profile ColorProfile
147		sgr     string
148	}{
149		{name: "truecolor", profile: ColorProfileTrueColor, sgr: "\x1b[0;4;38;2;255;0;0;48;2;0;0;255;58;2;0;255;0m"},
150		{name: "ANSI-256", profile: ColorProfileANSI256, sgr: "\x1b[0;4;38;5;196;48;5;21;58;5;46m"},
151		{name: "ANSI-16", profile: ColorProfileANSI16, sgr: "\x1b[0;4;91;104m"},
152		{name: "monochrome", profile: ColorProfileMonochrome, sgr: "\x1b[0;4m"},
153	}
154
155	for _, tt := range tests {
156		t.Run(tt.name, func(t *testing.T) {
157			renderer := NewWithColorProfile(Capabilities{}, tt.profile)
158			frame := NewFrame(1, 1)
159			frame.Set(0, 0, Cell{Rune: 'A', Style: style})
160			wantFull := "\x1b[1;1H" + tt.sgr + "A\x1b[0m"
161			out, err := renderer.Draw(frame, []Damage{FullRedraw()})
162			if err != nil {
163				t.Fatal(err)
164			}
165			if got := string(out); got != wantFull {
166				t.Fatalf("full output = %q, want %q", got, wantFull)
167			}
168
169			frame.Set(0, 0, Cell{Rune: 'B', Style: style})
170			wantIncremental := "\x1b[1;1H" + tt.sgr + "B\x1b[0m"
171			out, err = renderer.Draw(frame, []Damage{{Kind: DamageText, X: 0, Y: 0, Width: 1, Height: 1}})
172			if err != nil {
173				t.Fatal(err)
174			}
175			if got := string(out); got != wantIncremental {
176				t.Fatalf("incremental output = %q, want %q", got, wantIncremental)
177			}
178
179			unchanged, err := renderer.Draw(frame, nil)
180			if err != nil {
181				t.Fatal(err)
182			}
183			if len(unchanged) != 0 {
184				t.Fatalf("unchanged draw = %q, want no output", unchanged)
185			}
186
187			renderer.Reset()
188			out, err = renderer.Draw(frame, nil)
189			if err != nil {
190				t.Fatal(err)
191			}
192			if got := string(out); got != wantIncremental {
193				t.Fatalf("post-reset output = %q, want %q", got, wantIncremental)
194			}
195		})
196	}
197}
198
199func TestNewPreservesUnkeyedCapabilitiesAndTrueColorDefault(t *testing.T) {
200	renderer := New(Capabilities{true})
201	frame := NewFrame(1, 1)
202	frame.Set(0, 0, Cell{Rune: 'X', Style: Style{
203		Foreground:       -1,
204		Background:       -1,
205		HasForegroundRGB: true,
206		ForegroundRGB:    RGB{R: 1, G: 2, B: 3},
207	}})
208	out, err := renderer.Draw(frame, []Damage{FullRedraw()})
209	if err != nil {
210		t.Fatal(err)
211	}
212	want := SyncStartCSI + "\x1b[1;1H\x1b[0;38;2;1;2;3mX\x1b[0m" + SyncEndCSI
213	if got := string(out); got != want {
214		t.Fatalf("output = %q, want %q", got, want)
215	}
216}
217
218func TestConstrainedProfilesOmitInvalidIndexedColors(t *testing.T) {
219	style := Style{
220		Foreground:        256,
221		Background:        257,
222		HasUnderlineColor: true,
223		UnderlineColor:    -1,
224	}
225	profiles := []struct {
226		name    string
227		profile ColorProfile
228	}{
229		{name: "ANSI-256", profile: ColorProfileANSI256},
230		{name: "ANSI-16", profile: ColorProfileANSI16},
231		{name: "monochrome", profile: ColorProfileMonochrome},
232	}
233	for _, tt := range profiles {
234		t.Run(tt.name, func(t *testing.T) {
235			frame := NewFrame(1, 1)
236			frame.Set(0, 0, Cell{Rune: 'X', Style: style})
237			out, err := NewWithColorProfile(Capabilities{}, tt.profile).Draw(frame, []Damage{FullRedraw()})
238			if err != nil {
239				t.Fatal(err)
240			}
241			want := "\x1b[1;1HX\x1b[0m"
242			if got := string(out); got != want {
243				t.Fatalf("output = %q, want %q", got, want)
244			}
245		})
246	}
247}
248
249func TestColorQuantizationContract(t *testing.T) {
250	tests := []struct {
251		name    string
252		rgb     RGB
253		ansi16  int
254		ansi256 int
255	}{
256		{name: "black", rgb: RGB{}, ansi16: 0, ansi256: 16},
257		{name: "bright red", rgb: RGB{R: 255}, ansi16: 9, ansi256: 196},
258		{name: "exact cube color", rgb: RGB{R: 95, G: 135, B: 175}, ansi16: 8, ansi256: 67},
259		{name: "first gray", rgb: RGB{R: 8, G: 8, B: 8}, ansi16: 0, ansi256: 232},
260		{name: "middle gray", rgb: RGB{R: 128, G: 128, B: 128}, ansi16: 8, ansi256: 244},
261		{name: "last gray", rgb: RGB{R: 238, G: 238, B: 238}, ansi16: 15, ansi256: 255},
262		{name: "white", rgb: RGB{R: 255, G: 255, B: 255}, ansi16: 15, ansi256: 231},
263		{name: "arbitrary RGB", rgb: RGB{R: 12, G: 34, B: 56}, ansi16: 0, ansi256: 235},
264		{name: "cube midpoint chooses lower index", rgb: RGB{R: 115}, ansi16: 1, ansi256: 52},
265		{name: "ANSI-16 tie chooses lower index", rgb: RGB{R: 64}, ansi16: 0, ansi256: 52},
266	}
267
268	for _, tt := range tests {
269		t.Run(tt.name, func(t *testing.T) {
270			if got := rgbToANSI16(tt.rgb); got != tt.ansi16 {
271				t.Errorf("rgbToANSI16(%+v) = %d, want %d", tt.rgb, got, tt.ansi16)
272			}
273			if got := rgbToANSI256(tt.rgb); got != tt.ansi256 {
274				t.Errorf("rgbToANSI256(%+v) = %d, want %d", tt.rgb, got, tt.ansi256)
275			}
276		})
277	}
278}

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