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>
163 lines
3.4 KiB
Go
163 lines
3.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// ANSI escape codes
|
|
const (
|
|
ansiReset = "\033[0m"
|
|
ansiBold = "\033[1m"
|
|
ansiDim = "\033[2m"
|
|
ansiItalic = "\033[3m"
|
|
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 {
|
|
bold bool
|
|
dim bool
|
|
italic bool
|
|
underline bool
|
|
fg string
|
|
bg string
|
|
}
|
|
|
|
// NewStyle creates a new empty style.
|
|
func NewStyle() *AnsiStyle {
|
|
return &AnsiStyle{}
|
|
}
|
|
|
|
// Bold enables bold text.
|
|
func (s *AnsiStyle) Bold() *AnsiStyle {
|
|
s.bold = true
|
|
return s
|
|
}
|
|
|
|
// Dim enables dim text.
|
|
func (s *AnsiStyle) Dim() *AnsiStyle {
|
|
s.dim = true
|
|
return s
|
|
}
|
|
|
|
// Italic enables italic text.
|
|
func (s *AnsiStyle) Italic() *AnsiStyle {
|
|
s.italic = true
|
|
return s
|
|
}
|
|
|
|
// Underline enables underlined text.
|
|
func (s *AnsiStyle) Underline() *AnsiStyle {
|
|
s.underline = true
|
|
return s
|
|
}
|
|
|
|
// Foreground sets foreground color from hex string.
|
|
func (s *AnsiStyle) Foreground(hex string) *AnsiStyle {
|
|
s.fg = fgColorHex(hex)
|
|
return s
|
|
}
|
|
|
|
// Background sets background color from hex string.
|
|
func (s *AnsiStyle) Background(hex string) *AnsiStyle {
|
|
s.bg = bgColorHex(hex)
|
|
return s
|
|
}
|
|
|
|
// 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 || !ColorEnabled() {
|
|
return text
|
|
}
|
|
|
|
var codes []string
|
|
if s.bold {
|
|
codes = append(codes, ansiBold)
|
|
}
|
|
if s.dim {
|
|
codes = append(codes, ansiDim)
|
|
}
|
|
if s.italic {
|
|
codes = append(codes, ansiItalic)
|
|
}
|
|
if s.underline {
|
|
codes = append(codes, ansiUnderline)
|
|
}
|
|
if s.fg != "" {
|
|
codes = append(codes, s.fg)
|
|
}
|
|
if s.bg != "" {
|
|
codes = append(codes, s.bg)
|
|
}
|
|
|
|
if len(codes) == 0 {
|
|
return text
|
|
}
|
|
|
|
return strings.Join(codes, "") + text + ansiReset
|
|
}
|
|
|
|
// fgColorHex converts a hex string to an ANSI foreground color code.
|
|
func fgColorHex(hex string) string {
|
|
r, g, b := hexToRGB(hex)
|
|
return fmt.Sprintf("\033[38;2;%d;%d;%dm", r, g, b)
|
|
}
|
|
|
|
// bgColorHex converts a hex string to an ANSI background color code.
|
|
func bgColorHex(hex string) string {
|
|
r, g, b := hexToRGB(hex)
|
|
return fmt.Sprintf("\033[48;2;%d;%d;%dm", r, g, b)
|
|
}
|
|
|
|
// hexToRGB converts a hex string to RGB values.
|
|
func hexToRGB(hex string) (int, int, int) {
|
|
hex = strings.TrimPrefix(hex, "#")
|
|
if len(hex) != 6 {
|
|
return 255, 255, 255
|
|
}
|
|
// Use 8-bit parsing since RGB values are 0-255, avoiding integer overflow on 32-bit systems.
|
|
r, _ := strconv.ParseUint(hex[0:2], 16, 8)
|
|
g, _ := strconv.ParseUint(hex[2:4], 16, 8)
|
|
b, _ := strconv.ParseUint(hex[4:6], 16, 8)
|
|
return int(r), int(g), int(b)
|
|
}
|