Skip to content

Commit 9f55120

Browse files
authored
fix: Console Color Profiles (#610)
* fix: Color profile for console * fix: Console color profile for dark mode * fix: Simplify color profile identification using charm * docs: Add trade-off comments
1 parent f88f76a commit 9f55120

4 files changed

Lines changed: 470 additions & 35 deletions

File tree

pkg/reporter/colors.go

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
package reporter
2+
3+
import (
4+
"os"
5+
6+
"github.com/charmbracelet/colorprofile"
7+
"github.com/charmbracelet/lipgloss"
8+
"github.com/jedib0t/go-pretty/v6/text"
9+
)
10+
11+
// Package reporter provides terminal color support with automatic capability detection
12+
// and dark/light mode awareness.
13+
//
14+
// Color Choice Philosophy:
15+
// For ANSI256/TrueColor terminals, this package prioritizes readability over strict
16+
// semantic color naming. Specifically:
17+
// - Cyan is used instead of blue for better visibility on dark backgrounds
18+
// - Cyan is used instead of magenta for better contrast on dark terminals
19+
//
20+
// This is an intentional UX decision to address issue #537 where blue and magenta
21+
// colors had poor visibility on dark terminal themes. The ANSI fallback still uses
22+
// traditional blue/magenta colors with bold styling for compatibility with older terminals.
23+
24+
// ColorConfig holds the terminal color configuration
25+
type ColorConfig struct {
26+
profile colorprofile.Profile
27+
hasDarkBackground bool
28+
}
29+
30+
var globalColorConfig *ColorConfig
31+
32+
func init() {
33+
globalColorConfig = &ColorConfig{
34+
profile: colorprofile.Detect(os.Stdout, os.Environ()),
35+
hasDarkBackground: lipgloss.HasDarkBackground(),
36+
}
37+
}
38+
39+
// GetColorConfig returns the global color configuration
40+
func GetColorConfig() *ColorConfig {
41+
return globalColorConfig
42+
}
43+
44+
// Semantic color functions for consistent theming
45+
// These functions adapt colors based on terminal capability
46+
47+
// CriticalBgText returns text with critical severity background
48+
func (c *ColorConfig) CriticalBgText(s string) string {
49+
switch c.profile {
50+
case colorprofile.NoTTY, colorprofile.Ascii:
51+
return s
52+
case colorprofile.ANSI:
53+
return text.Colors{text.BgRed, text.FgWhite, text.Bold}.Sprint(s)
54+
case colorprofile.ANSI256, colorprofile.TrueColor:
55+
return text.Colors{text.BgHiRed, text.FgBlack}.Sprint(s)
56+
default:
57+
return s
58+
}
59+
}
60+
61+
// CriticalText returns text with critical severity foreground
62+
func (c *ColorConfig) CriticalText(s string) string {
63+
switch c.profile {
64+
case colorprofile.NoTTY, colorprofile.Ascii:
65+
return s
66+
case colorprofile.ANSI:
67+
return text.Colors{text.FgRed, text.Bold}.Sprint(s)
68+
case colorprofile.ANSI256, colorprofile.TrueColor:
69+
return text.FgHiRed.Sprint(s)
70+
default:
71+
return s
72+
}
73+
}
74+
75+
// HighBgText returns text with high severity background
76+
func (c *ColorConfig) HighBgText(s string) string {
77+
switch c.profile {
78+
case colorprofile.NoTTY, colorprofile.Ascii:
79+
return s
80+
case colorprofile.ANSI:
81+
return text.Colors{text.BgRed, text.FgWhite, text.Bold}.Sprint(s)
82+
case colorprofile.ANSI256, colorprofile.TrueColor:
83+
return text.Colors{text.BgHiRed, text.FgBlack}.Sprint(s)
84+
default:
85+
return s
86+
}
87+
}
88+
89+
// MediumBgText returns text with medium severity background
90+
func (c *ColorConfig) MediumBgText(s string) string {
91+
switch c.profile {
92+
case colorprofile.NoTTY, colorprofile.Ascii:
93+
return s
94+
case colorprofile.ANSI:
95+
return text.Colors{text.BgYellow, text.FgBlack, text.Bold}.Sprint(s)
96+
case colorprofile.ANSI256, colorprofile.TrueColor:
97+
return text.Colors{text.BgHiYellow, text.FgBlack}.Sprint(s)
98+
default:
99+
return s
100+
}
101+
}
102+
103+
// LowBgText returns text with low severity background
104+
// Note: Uses cyan instead of blue for ANSI256/TrueColor to improve visibility on dark terminals.
105+
// Blue backgrounds have poor contrast on dark terminal themes (see issue #537).
106+
func (c *ColorConfig) LowBgText(s string) string {
107+
switch c.profile {
108+
case colorprofile.NoTTY, colorprofile.Ascii:
109+
return s
110+
case colorprofile.ANSI:
111+
return text.Colors{text.BgBlue, text.FgWhite, text.Bold}.Sprint(s)
112+
case colorprofile.ANSI256, colorprofile.TrueColor:
113+
return text.Colors{text.BgHiCyan, text.FgBlack}.Sprint(s)
114+
default:
115+
return s
116+
}
117+
}
118+
119+
// WarningText returns text with warning color
120+
func (c *ColorConfig) WarningText(s string) string {
121+
switch c.profile {
122+
case colorprofile.NoTTY, colorprofile.Ascii:
123+
return s
124+
case colorprofile.ANSI:
125+
return text.Colors{text.FgYellow, text.Bold}.Sprint(s)
126+
case colorprofile.ANSI256, colorprofile.TrueColor:
127+
return text.FgHiYellow.Sprint(s)
128+
default:
129+
return s
130+
}
131+
}
132+
133+
// WarningBgText returns text with warning background
134+
func (c *ColorConfig) WarningBgText(s string) string {
135+
switch c.profile {
136+
case colorprofile.NoTTY, colorprofile.Ascii:
137+
return s
138+
case colorprofile.ANSI:
139+
return text.Colors{text.BgYellow, text.FgBlack, text.Bold}.Sprint(s)
140+
case colorprofile.ANSI256, colorprofile.TrueColor:
141+
return text.Colors{text.BgHiYellow, text.FgBlack}.Sprint(s)
142+
default:
143+
return s
144+
}
145+
}
146+
147+
// SuccessBgText returns text with success background
148+
func (c *ColorConfig) SuccessBgText(s string) string {
149+
switch c.profile {
150+
case colorprofile.NoTTY, colorprofile.Ascii:
151+
return s
152+
case colorprofile.ANSI:
153+
return text.Colors{text.BgGreen, text.FgBlack, text.Bold}.Sprint(s)
154+
case colorprofile.ANSI256, colorprofile.TrueColor:
155+
return text.Colors{text.BgHiGreen, text.FgBlack}.Sprint(s)
156+
default:
157+
return s
158+
}
159+
}
160+
161+
// InfoBgText returns text with info background
162+
// Note: Uses cyan instead of blue for ANSI256/TrueColor to improve visibility on dark terminals.
163+
// Blue backgrounds have poor contrast on dark terminal themes (see issue #537).
164+
func (c *ColorConfig) InfoBgText(s string) string {
165+
switch c.profile {
166+
case colorprofile.NoTTY, colorprofile.Ascii:
167+
return s
168+
case colorprofile.ANSI:
169+
return text.Colors{text.BgBlue, text.FgWhite, text.Bold}.Sprint(s)
170+
case colorprofile.ANSI256, colorprofile.TrueColor:
171+
return text.Colors{text.BgHiCyan, text.FgBlack}.Sprint(s)
172+
default:
173+
return s
174+
}
175+
}
176+
177+
// InfoText returns text with info foreground
178+
// Note: Uses cyan instead of blue for ANSI256/TrueColor to improve visibility on dark terminals.
179+
// Blue text has poor contrast on dark terminal themes (see issue #537).
180+
func (c *ColorConfig) InfoText(s string) string {
181+
switch c.profile {
182+
case colorprofile.NoTTY, colorprofile.Ascii:
183+
return s
184+
case colorprofile.ANSI:
185+
return text.Colors{text.FgBlue, text.Bold}.Sprint(s)
186+
case colorprofile.ANSI256, colorprofile.TrueColor:
187+
return text.FgHiCyan.Sprint(s)
188+
default:
189+
return s
190+
}
191+
}
192+
193+
// MagentaBgText returns text with tag background
194+
// Note: Uses cyan instead of magenta for ANSI256/TrueColor to improve visibility on dark terminals.
195+
// Magenta/pink backgrounds have poor contrast on dark terminal themes (see issue #537).
196+
func (c *ColorConfig) MagentaBgText(s string) string {
197+
switch c.profile {
198+
case colorprofile.NoTTY, colorprofile.Ascii:
199+
return s
200+
case colorprofile.ANSI:
201+
return text.Colors{text.BgMagenta, text.FgWhite, text.Bold}.Sprint(s)
202+
case colorprofile.ANSI256, colorprofile.TrueColor:
203+
return text.Colors{text.BgCyan, text.FgBlack}.Sprint(s)
204+
default:
205+
return s
206+
}
207+
}
208+
209+
// WhiteBgText returns text with white background
210+
func (c *ColorConfig) WhiteBgText(s string) string {
211+
switch c.profile {
212+
case colorprofile.NoTTY, colorprofile.Ascii:
213+
return s
214+
case colorprofile.ANSI:
215+
return text.Colors{text.BgWhite, text.FgBlack, text.Bold}.Sprint(s)
216+
case colorprofile.ANSI256, colorprofile.TrueColor:
217+
return text.Colors{text.BgHiWhite, text.FgBlack}.Sprint(s)
218+
default:
219+
return s
220+
}
221+
}
222+
223+
// FaintText returns text with faint/dim styling
224+
func (c *ColorConfig) FaintText(s string) string {
225+
if c.profile == colorprofile.NoTTY || c.profile == colorprofile.Ascii {
226+
return s
227+
}
228+
return text.Faint.Sprint(s)
229+
}
230+
231+
// BoldText returns text with bold styling
232+
func (c *ColorConfig) BoldText(s string) string {
233+
if c.profile == colorprofile.NoTTY || c.profile == colorprofile.Ascii {
234+
return s
235+
}
236+
return text.Bold.Sprint(s)
237+
}
238+
239+
// Global convenience functions that use the global color config
240+
241+
// CriticalBgText returns text with critical severity background
242+
func CriticalBgText(s string) string {
243+
return globalColorConfig.CriticalBgText(s)
244+
}
245+
246+
// CriticalText returns text with critical severity foreground
247+
func CriticalText(s string) string {
248+
return globalColorConfig.CriticalText(s)
249+
}
250+
251+
// HighBgText returns text with high severity background
252+
func HighBgText(s string) string {
253+
return globalColorConfig.HighBgText(s)
254+
}
255+
256+
// MediumBgText returns text with medium severity background
257+
func MediumBgText(s string) string {
258+
return globalColorConfig.MediumBgText(s)
259+
}
260+
261+
// LowBgText returns text with low severity background
262+
func LowBgText(s string) string {
263+
return globalColorConfig.LowBgText(s)
264+
}
265+
266+
// WarningText returns text with warning color
267+
func WarningText(s string) string {
268+
return globalColorConfig.WarningText(s)
269+
}
270+
271+
// WarningBgText returns text with warning background
272+
func WarningBgText(s string) string {
273+
return globalColorConfig.WarningBgText(s)
274+
}
275+
276+
// SuccessBgText returns text with success background
277+
func SuccessBgText(s string) string {
278+
return globalColorConfig.SuccessBgText(s)
279+
}
280+
281+
// InfoBgText returns text with info background
282+
func InfoBgText(s string) string {
283+
return globalColorConfig.InfoBgText(s)
284+
}
285+
286+
// InfoText returns text with info foreground
287+
func InfoText(s string) string {
288+
return globalColorConfig.InfoText(s)
289+
}
290+
291+
// MagentaBgText returns text with magenta background
292+
func MagentaBgText(s string) string {
293+
return globalColorConfig.MagentaBgText(s)
294+
}
295+
296+
// WhiteBgText returns text with white background
297+
func WhiteBgText(s string) string {
298+
return globalColorConfig.WhiteBgText(s)
299+
}
300+
301+
// FaintText returns text with faint/dim styling
302+
func FaintText(s string) string {
303+
return globalColorConfig.FaintText(s)
304+
}
305+
306+
// BoldText returns text with bold styling
307+
func BoldText(s string) string {
308+
return globalColorConfig.BoldText(s)
309+
}

0 commit comments

Comments
 (0)