diff --git a/pkg/cli/ansi.go b/pkg/cli/ansi.go index 200fc86..e4df66e 100644 --- a/pkg/cli/ansi.go +++ b/pkg/cli/ansi.go @@ -2,8 +2,10 @@ package cli import ( "fmt" + "os" "strconv" "strings" + "sync" ) // ANSI escape codes @@ -15,6 +17,40 @@ const ( ansiUnderline = "\033[4m" ) +var ( + colorEnabled = true + colorEnabledMu sync.RWMutex +) + +func init() { + // NO_COLOR standard: https://no-color.org/ + // If NO_COLOR is set (to any value, including empty), disable colors. + if _, exists := os.LookupEnv("NO_COLOR"); exists { + colorEnabled = false + return + } + + // TERM=dumb indicates a terminal without color support. + if os.Getenv("TERM") == "dumb" { + colorEnabled = false + } +} + +// ColorEnabled returns true if ANSI color output is enabled. +func ColorEnabled() bool { + colorEnabledMu.RLock() + defer colorEnabledMu.RUnlock() + return colorEnabled +} + +// SetColorEnabled enables or disables ANSI color output. +// This overrides the NO_COLOR environment variable check. +func SetColorEnabled(enabled bool) { + colorEnabledMu.Lock() + colorEnabled = enabled + colorEnabledMu.Unlock() +} + // AnsiStyle represents terminal text styling. // Use NewStyle() to create, chain methods, call Render(). type AnsiStyle struct { @@ -68,8 +104,9 @@ func (s *AnsiStyle) Background(hex string) *AnsiStyle { } // Render applies the style to text. +// Returns plain text if NO_COLOR is set or colors are disabled. func (s *AnsiStyle) Render(text string) string { - if s == nil { + if s == nil || !ColorEnabled() { return text } diff --git a/pkg/cli/ansi_test.go b/pkg/cli/ansi_test.go index 75ace2c..1ec7a3e 100644 --- a/pkg/cli/ansi_test.go +++ b/pkg/cli/ansi_test.go @@ -6,6 +6,10 @@ import ( ) func TestAnsiStyle_Render(t *testing.T) { + // Ensure colors are enabled for this test + SetColorEnabled(true) + defer SetColorEnabled(true) // Reset after test + s := NewStyle().Bold().Foreground("#ff0000") got := s.Render("test") if got == "test" { @@ -18,3 +22,76 @@ func TestAnsiStyle_Render(t *testing.T) { t.Error("Output should contain bold code") } } + +func TestColorEnabled_Good(t *testing.T) { + // Save original state + original := ColorEnabled() + defer SetColorEnabled(original) + + // Test enabling + SetColorEnabled(true) + if !ColorEnabled() { + t.Error("ColorEnabled should return true") + } + + // Test disabling + SetColorEnabled(false) + if ColorEnabled() { + t.Error("ColorEnabled should return false") + } +} + +func TestRender_ColorDisabled_Good(t *testing.T) { + // Save original state + original := ColorEnabled() + defer SetColorEnabled(original) + + // Disable colors + SetColorEnabled(false) + + s := NewStyle().Bold().Foreground("#ff0000") + got := s.Render("test") + + // Should return plain text without ANSI codes + if got != "test" { + t.Errorf("Expected plain 'test', got %q", got) + } +} + +func TestRender_ColorEnabled_Good(t *testing.T) { + // Save original state + original := ColorEnabled() + defer SetColorEnabled(original) + + // Enable colors + SetColorEnabled(true) + + s := NewStyle().Bold() + got := s.Render("test") + + // Should contain ANSI codes + if !strings.Contains(got, "\033[") { + t.Error("Expected ANSI codes when colors enabled") + } +} + +func TestUseASCII_Good(t *testing.T) { + // Save original state + original := ColorEnabled() + defer SetColorEnabled(original) + + // Enable first, then UseASCII should disable colors + SetColorEnabled(true) + UseASCII() + if ColorEnabled() { + t.Error("UseASCII should disable colors") + } +} + +func TestRender_NilStyle_Good(t *testing.T) { + var s *AnsiStyle + got := s.Render("test") + if got != "test" { + t.Errorf("Nil style should return plain text, got %q", got) + } +} diff --git a/pkg/cli/glyph.go b/pkg/cli/glyph.go index 97143ec..26023e5 100644 --- a/pkg/cli/glyph.go +++ b/pkg/cli/glyph.go @@ -25,8 +25,11 @@ func UseUnicode() { currentTheme = ThemeUnicode } // UseEmoji switches the glyph theme to Emoji. func UseEmoji() { currentTheme = ThemeEmoji } -// UseASCII switches the glyph theme to ASCII. -func UseASCII() { currentTheme = ThemeASCII } +// UseASCII switches the glyph theme to ASCII and disables colors. +func UseASCII() { + currentTheme = ThemeASCII + SetColorEnabled(false) +} func glyphMap() map[string]string { switch currentTheme {