feat(cli): add NO_COLOR environment variable support (#98)
Implement the NO_COLOR standard (https://no-color.org/) for CLI output. When NO_COLOR is set (to any value), ANSI color codes are disabled. Changes: - Add init() to check NO_COLOR and TERM=dumb environment variables - Add ColorEnabled() to query current color state - Add SetColorEnabled() to programmatically enable/disable colors - Modify AnsiStyle.Render() to return plain text when colors disabled - Update UseASCII() to also disable colors (consistent with ASCII mode) - Add comprehensive tests for color enable/disable functionality Usage: NO_COLOR=1 core dev status # Runs without color output TERM=dumb core dev status # Also disables colors Closes #87 Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9b678f21a0
commit
c58bc3e344
3 changed files with 120 additions and 3 deletions
|
|
@ -2,8 +2,10 @@ package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ANSI escape codes
|
// ANSI escape codes
|
||||||
|
|
@ -15,6 +17,40 @@ const (
|
||||||
ansiUnderline = "\033[4m"
|
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.
|
// AnsiStyle represents terminal text styling.
|
||||||
// Use NewStyle() to create, chain methods, call Render().
|
// Use NewStyle() to create, chain methods, call Render().
|
||||||
type AnsiStyle struct {
|
type AnsiStyle struct {
|
||||||
|
|
@ -68,8 +104,9 @@ func (s *AnsiStyle) Background(hex string) *AnsiStyle {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render applies the style to text.
|
// 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 {
|
func (s *AnsiStyle) Render(text string) string {
|
||||||
if s == nil {
|
if s == nil || !ColorEnabled() {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAnsiStyle_Render(t *testing.T) {
|
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")
|
s := NewStyle().Bold().Foreground("#ff0000")
|
||||||
got := s.Render("test")
|
got := s.Render("test")
|
||||||
if got == "test" {
|
if got == "test" {
|
||||||
|
|
@ -18,3 +22,76 @@ func TestAnsiStyle_Render(t *testing.T) {
|
||||||
t.Error("Output should contain bold code")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,11 @@ func UseUnicode() { currentTheme = ThemeUnicode }
|
||||||
// UseEmoji switches the glyph theme to Emoji.
|
// UseEmoji switches the glyph theme to Emoji.
|
||||||
func UseEmoji() { currentTheme = ThemeEmoji }
|
func UseEmoji() { currentTheme = ThemeEmoji }
|
||||||
|
|
||||||
// UseASCII switches the glyph theme to ASCII.
|
// UseASCII switches the glyph theme to ASCII and disables colors.
|
||||||
func UseASCII() { currentTheme = ThemeASCII }
|
func UseASCII() {
|
||||||
|
currentTheme = ThemeASCII
|
||||||
|
SetColorEnabled(false)
|
||||||
|
}
|
||||||
|
|
||||||
func glyphMap() map[string]string {
|
func glyphMap() map[string]string {
|
||||||
switch currentTheme {
|
switch currentTheme {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue