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}