1package tui
2
3import (
4 "bytes"
5 "strings"
6 "testing"
7)
8
9func TestDetectTrueColor(t *testing.T) {
10 tests := []struct {
11 name string
12 term string
13 colorTerm string
14 want bool
15 }{
16 {name: "COLORTERM truecolor", term: "screen", colorTerm: "truecolor", want: true},
17 {name: "COLORTERM 24bit", term: "screen", colorTerm: "24bit", want: true},
18 {name: "direct TERM", term: "xterm-direct", want: true},
19 {name: "direct TERM suffix", term: "screen-direct", want: true},
20 {name: "256 color TERM", term: "xterm-256color", colorTerm: "", want: false},
21 {name: "unknown TERM", term: "dumb", colorTerm: "", want: false},
22 }
23 for _, tt := range tests {
24 t.Run(tt.name, func(t *testing.T) {
25 if got := DetectTrueColor(tt.term, tt.colorTerm); got != tt.want {
26 t.Fatalf("DetectTrueColor(%q, %q) = %v, want %v", tt.term, tt.colorTerm, got, tt.want)
27 }
28 })
29 }
30}
31
32func TestParseOSCColorResponse(t *testing.T) {
33 tests := []struct {
34 name string
35 raw string
36 kind int
37 slot int
38 want TerminalColor
39 }{
40 {
41 name: "foreground hash",
42 raw: "\x1b]10;#123456\x07",
43 kind: 10,
44 want: ColorRGB(0x12, 0x34, 0x56),
45 },
46 {
47 name: "background rgb",
48 raw: "\x1b]11;rgb:ffff/8000/0000\x1b\\",
49 kind: 11,
50 want: ColorRGB(255, 127, 0),
51 },
52 {
53 name: "palette slot",
54 raw: "\x1b]4;12;rgb:12/34/56\x07",
55 kind: 4,
56 slot: 12,
57 want: ColorRGB(0x12, 0x34, 0x56),
58 },
59 }
60 for _, tt := range tests {
61 t.Run(tt.name, func(t *testing.T) {
62 got, ok := parseOSCColorResponse([]byte(tt.raw))
63 if !ok {
64 t.Fatal("parseOSCColorResponse returned !ok")
65 }
66 if got.kind != tt.kind || got.slot != tt.slot || got.color != tt.want {
67 t.Fatalf("response = %#v, want kind=%d slot=%d color=%#v", got, tt.kind, tt.slot, tt.want)
68 }
69 })
70 }
71}
72
73func TestANSIThemeColorsAdaptToTargetSGR(t *testing.T) {
74 if got := Dark.FGColor(ColorANSI(100), "x"); got != "\x1b[90mx\x1b[0m" {
75 t.Fatalf("ANSI foreground = %q, want bright-black foreground", got)
76 }
77 if got := Dark.BG(ColorANSI(31), "x"); got != "\x1b[41mx\x1b[0m" {
78 t.Fatalf("ANSI background = %q, want red background", got)
79 }
80}
81
82func TestNearestXtermColorCacheIsStable(t *testing.T) {
83 colors := [][3]int{
84 {0, 0, 0},
85 {66, 69, 75},
86 {200, 100, 50},
87 {-20, 300, 128},
88 }
89 for _, color := range colors {
90 first := nearestXtermColor(color[0], color[1], color[2])
91 second := nearestXtermColor(color[0], color[1], color[2])
92 if first != second {
93 t.Fatalf("nearest color changed for %#v: first=%d second=%d", color, first, second)
94 }
95 if first < 0 || first > 255 {
96 t.Fatalf("nearest color out of range for %#v: %d", color, first)
97 }
98 }
99}
100
101func inheritedTestTheme(trueColor bool) Theme {
102 detected := Dark
103 detected.Terminal = TerminalProfile{
104 Foreground: ColorRGB(220, 210, 200),
105 Background: ColorRGB(12, 18, 24),
106 HasForeground: true,
107 HasBackground: true,
108 TrueColor: trueColor,
109 }
110 detected.Terminal.Palette[10] = ColorRGB(20, 220, 80)
111 detected.Terminal.Palette[12] = ColorRGB(40, 100, 240)
112 detected.Terminal.Palette[13] = ColorRGB(220, 70, 210)
113 detected.Terminal.PaletteKnown = 1<<10 | 1<<12 | 1<<13
114 return InheritedTheme(detected)
115}
116
117func TestInheritedThemeUsesTrueColorAndTerminalPalette(t *testing.T) {
118 th := inheritedTestTheme(true)
119 if !th.Inherited || !th.Terminal.TrueColor {
120 t.Fatalf("inherited theme mode = (inherited=%v, trueColor=%v), want truecolor", th.Inherited, th.Terminal.TrueColor)
121 }
122 if got := th.FGColor(th.Accent, "x"); !strings.Contains(got, "\x1b[38;2;40;100;240m") {
123 t.Fatalf("accent did not use the reported terminal palette: %q", got)
124 }
125 if got := th.FGColor(th.FG, "x"); !strings.Contains(got, "\x1b[38;2;220;210;200m") {
126 t.Fatalf("foreground did not inherit the terminal default: %q", got)
127 }
128 if got := th.UserBubble("x", 3); !strings.Contains(got, "\x1b[48;2;") {
129 t.Fatalf("user bubble did not use truecolor background: %q", got)
130 }
131 if got := strings.Join(th.HighlightCode("func main() {}", "go"), "\n"); !strings.Contains(got, "\x1b[38;2;") {
132 t.Fatalf("syntax highlighting did not use truecolor: %q", got)
133 }
134 if got := th.DimColor(th.FG, 45); got.Mode != terminalColorRGB {
135 t.Fatalf("DimColor mode = %v, want RGB", got.Mode)
136 }
137}
138
139func TestInheritedThemeFallsBackTo256Colors(t *testing.T) {
140 th := inheritedTestTheme(false)
141 if !th.Inherited || th.Terminal.TrueColor {
142 t.Fatalf("inherited theme mode = (inherited=%v, trueColor=%v), want 256", th.Inherited, th.Terminal.TrueColor)
143 }
144 if got := th.FGColor(th.Accent, "x"); !strings.Contains(got, "\x1b[38;5;12m") || strings.Contains(got, "\x1b[38;2;") {
145 t.Fatalf("accent did not use 256-color fallback: %q", got)
146 }
147 if got := th.UserBubble("x", 3); strings.Contains(got, "\x1b[48;2;") || !strings.Contains(got, "\x1b[48;5;") {
148 t.Fatalf("user bubble did not quantize its background: %q", got)
149 }
150 if got := th.DimColor(th.FG, 45); got.Mode != terminalColor256 {
151 t.Fatalf("DimColor mode = %v, want 256", got.Mode)
152 }
153 if got := strings.Join(th.HighlightCode("func main() {}", "go"), "\n"); strings.Contains(got, "\x1b[38;2;") {
154 t.Fatalf("syntax highlighting unexpectedly used truecolor: %q", got)
155 }
156}
157
158func TestExplicitThemesFollowTerminalCapability(t *testing.T) {
159 if got, want := Dark.FGColor(Dark.Accent, "x"), "\x1b[38;5;111mx\x1b[0m"; got != want {
160 t.Fatalf("explicit theme output = %q, want %q", got, want)
161 }
162 if got := Dark.UserBubble("x", 1); !strings.Contains(got, "\x1b[48;5;") || strings.Contains(got, "\x1b[48;2;") {
163 t.Fatalf("non-truecolor RGB bubble did not quantize: %q", got)
164 }
165
166 trueColor := withTerminalProfile(Dark, Theme{Terminal: TerminalProfile{TrueColor: true}})
167 if got := trueColor.FGColor(trueColor.Accent, "x"); !strings.Contains(got, "\x1b[38;2;") {
168 t.Fatalf("truecolor explicit theme did not emit RGB: %q", got)
169 }
170 if got := trueColor.UserBubble("x", 1); !strings.Contains(got, "\x1b[48;2;66;69;75m") {
171 t.Fatalf("truecolor explicit RGB bubble did not emit RGB: %q", got)
172 }
173}
174
175func TestAvailableThemesIncludesInherited(t *testing.T) {
176 options := AvailableThemes(t.TempDir())
177 for _, option := range options {
178 if option.Value == "inherited" {
179 if option.Label != "inherited (from terminal)" || !option.Builtin {
180 t.Fatalf("unexpected inherited option: %#v", option)
181 }
182 return
183 }
184 }
185 t.Fatal("available themes did not include inherited")
186}
187
188func TestIsLightThemePreservesExplicitDarkTheme(t *testing.T) {
189 dark := Dark
190 dark.Terminal.SchemeKnown = true
191 dark.Terminal.Light = true
192 if IsLightTheme(dark) {
193 t.Fatal("explicit dark theme was reclassified from the terminal scheme")
194 }
195
196 inherited := InheritedTheme(dark)
197 if !IsLightTheme(inherited) {
198 t.Fatal("inherited light terminal theme was not recognized as light")
199 }
200}
201
202func TestRendererDetectsInheritedSelectionBackground(t *testing.T) {
203 var buf bytes.Buffer
204 r := NewRenderer(&buf)
205 r.Resize(20, 2)
206 r.SetTheme(inheritedTestTheme(true))
207 selected := r.theme.SelectionStyle() + "selected" + reset
208 r.Draw([]string{"", selected}, -1, -1)
209 buf.Reset()
210
211 r.Draw([]string{"", "plain"}, -1, -1)
212 if !strings.Contains(buf.String(), MoveTo(1, 1)) {
213 t.Fatalf("selection repaint did not include unchanged rows: %q", buf.String())
214 }
215}