diff --git a/env.go b/env.go index 3f2396f9..d019c537 100644 --- a/env.go +++ b/env.go @@ -8,29 +8,21 @@ import ( "github.com/charmbracelet/x/exp/term" ) -// envNoColor returns true if the environment variables explicitly disable color output -// by setting NO_COLOR (https://no-color.org/) -// or CLICOLOR/CLICOLOR_FORCE (https://bixense.com/clicolors/) -// If NO_COLOR is set, this will return true, ignoring CLICOLOR/CLICOLOR_FORCE -// If CLICOLOR=="0", it will be true only if CLICOLOR_FORCE is also "0" or is unset. -func envNoColor(env map[string]string) bool { - return isTrue(env["NO_COLOR"]) && !isTrue(env["CLICOLOR"]) && !cliColorForced(env) -} - -// EnvColorProfile returns the color profile based on environment variables set -// Supports NO_COLOR (https://no-color.org/) -// and CLICOLOR/CLICOLOR_FORCE (https://bixense.com/clicolors/) -// If none of these environment variables are set, this behaves the same as ColorProfile() -// It will return the Ascii color profile if EnvNoColor() returns true -// If the terminal does not support any colors, but CLICOLOR_FORCE is set and not "0" -// then the ANSI color profile will be returned. -func EnvColorProfile(stdout *os.File, environ []string) Profile { +// DetectColorProfile returns the color profile based on the terminal output, +// and environment variables. +// +// If the output is not a terminal, the color profile will be NoTTY unless +// CLICOLOR_FORCE=1 is set. This respects the NO_COLOR and +// CLICOLOR/CLICOLOR_FORCE environment variables. +// +// See https://no-color.org/ and https://bixense.com/clicolors/ for more information. +func DetectColorProfile(stdout *os.File, environ []string) Profile { if environ == nil { environ = os.Environ() } env := environMap(environ) - p := detectColorProfile(env) + p := envColorProfile(env) if stdout == nil || !term.IsTerminal(stdout.Fd()) { p = NoTTY } @@ -41,7 +33,34 @@ func EnvColorProfile(stdout *os.File, environ []string) Profile { if cliColorForced(env) && p <= Ascii { p = ANSI - if cp := detectColorProfile(env); cp > p { + if cp := envColorProfile(env); cp > p { + p = cp + } + } + + return p +} + +// EnvColorProfile returns the color profile based on environment variables. +// +// This respects the NO_COLOR and CLICOLOR/CLICOLOR_FORCE environment +// variables. +// +// See https://no-color.org/ and https://bixense.com/clicolors/ for more information. +func EnvColorProfile(environ []string) Profile { + if environ == nil { + environ = os.Environ() + } + + env := environMap(environ) + p := envColorProfile(env) + if envNoColor(env) && p > Ascii { + return Ascii + } + + if cliColorForced(env) && p <= Ascii { + p = ANSI + if cp := envColorProfile(env); cp > p { p = cp } } @@ -49,6 +68,15 @@ func EnvColorProfile(stdout *os.File, environ []string) Profile { return p } +// envNoColor returns true if the environment variables explicitly disable color output +// by setting NO_COLOR (https://no-color.org/) +// or CLICOLOR/CLICOLOR_FORCE (https://bixense.com/clicolors/) +// If NO_COLOR is set, this will return true, ignoring CLICOLOR/CLICOLOR_FORCE +// If CLICOLOR is false, it will be true only if CLICOLOR_FORCE is also false or is unset. +func envNoColor(env map[string]string) bool { + return isTrue(env["NO_COLOR"]) && !isTrue(env["CLICOLOR"]) && !cliColorForced(env) +} + func cliColorForced(env map[string]string) bool { if forced := env["CLICOLOR_FORCE"]; forced != "" { return isTrue(forced) @@ -56,9 +84,8 @@ func cliColorForced(env map[string]string) bool { return false } -// detectColorProfile returns the supported color profile: -// Ascii, ANSI, ANSI256, or TrueColor. -func detectColorProfile(env map[string]string) (p Profile) { +// envColorProfile returns infers the color profile from the environment. +func envColorProfile(env map[string]string) (p Profile) { setProfile := func(profile Profile) { if profile > p { p = profile diff --git a/renderer.go b/renderer.go index 2c5513b6..b431ce6e 100644 --- a/renderer.go +++ b/renderer.go @@ -8,14 +8,13 @@ import ( "github.com/lucasb-eyer/go-colorful" ) -// We're manually creating the struct here to avoid initializing the output and -// query the terminal multiple times. var ( renderer *Renderer rendererOnce sync.Once ) -// Renderer is a lipgloss terminal renderer. +// Renderer is a terminal style renderer that keep track of the color profile +// and background color detection. type Renderer struct { colorProfile Profile mtx sync.RWMutex @@ -41,7 +40,7 @@ func DefaultRenderer() *Renderer { EnableLegacyWindowsANSI(os.Stdout) } - cp := EnvColorProfile(os.Stdout, os.Environ()) + cp := DetectColorProfile(os.Stdout, os.Environ()) renderer = NewRenderer(cp, hasDarkBackground) }) return renderer @@ -52,14 +51,11 @@ func SetDefaultRenderer(r *Renderer) { rendererOnce.Do(func() { renderer = r }) } -// NewRenderer creates a new Renderer. +// NewRenderer creates a new Lip Gloss Renderer. // -// The stdout argument is used to detect if the renderer is writing to a -// terminal. If it is nil, the renderer will assume it's not writing to a -// terminal. -// The environ argument is used to detect the color profile based on the -// environment variables. If it's nil, os.Environ() will be used. -// Set hasDarkBackground to true if the terminal has a dark background. +// It takes a color profile and a boolean indicating whether the terminal has a +// dark background. +// These values are then used to determine how to render colors and styles. func NewRenderer(cp Profile, hasDarkBackground bool) *Renderer { return &Renderer{ colorProfile: cp, @@ -67,7 +63,7 @@ func NewRenderer(cp Profile, hasDarkBackground bool) *Renderer { } } -// ColorProfile returns the detected color profile. +// ColorProfile returns the current color profile. func (r *Renderer) ColorProfile() Profile { r.mtx.RLock() defer r.mtx.RUnlock() @@ -75,23 +71,18 @@ func (r *Renderer) ColorProfile() Profile { return r.colorProfile } -// ColorProfile returns the detected color profile. +// ColorProfile returns the ren color profile. func ColorProfile() Profile { return DefaultRenderer().ColorProfile() } -// SetColorProfile sets the color profile on the renderer. This function exists -// mostly for testing purposes so that you can assure you're testing against -// a specific profile. -// -// Outside of testing you likely won't want to use this function as the color -// profile will detect and cache the terminal's color capabilities and choose -// the best available profile. +// SetColorProfile sets the color profile on the renderer. // // Available color profiles are: // -// Ascii // no color, 1-bit -// ANSI //16 colors, 4-bit +// NoTTY // no colors or styles +// Ascii // no colors, other styles are allowed (bold, italic, underline) +// ANSI // 16 colors, 4-bit // ANSI256 // 256 colors, 8-bit // TrueColor // 16,777,216 colors, 24-bit // @@ -103,18 +94,13 @@ func (r *Renderer) SetColorProfile(p Profile) { r.colorProfile = p } -// SetColorProfile sets the color profile on the default renderer. This -// function exists mostly for testing purposes so that you can assure you're -// testing against a specific profile. -// -// Outside of testing you likely won't want to use this function as the color -// profile will detect and cache the terminal's color capabilities and choose -// the best available profile. +// SetColorProfile sets the color profile on the renderer. // // Available color profiles are: // -// Ascii // no color, 1-bit -// ANSI //16 colors, 4-bit +// NoTTY // no colors or styles +// Ascii // no colors, other styles are allowed (bold, italic, underline) +// ANSI // 16 colors, 4-bit // ANSI256 // 256 colors, 8-bit // TrueColor // 16,777,216 colors, 24-bit // @@ -123,14 +109,12 @@ func SetColorProfile(p Profile) { DefaultRenderer().SetColorProfile(p) } -// HasDarkBackground returns whether or not the terminal has a dark background. +// HasDarkBackground returns whether or not the renderer has a dark background. func HasDarkBackground() bool { return DefaultRenderer().HasDarkBackground() } -// HasDarkBackground returns whether or not the renderer will render to a dark -// background. A dark background can either be auto-detected, or set explicitly -// on the renderer. +// HasDarkBackground returns whether or not the renderer has a dark background. func (r *Renderer) HasDarkBackground() bool { r.mtx.RLock() defer r.mtx.RUnlock() @@ -139,25 +123,15 @@ func (r *Renderer) HasDarkBackground() bool { } // SetHasDarkBackground sets the background color detection value for the -// default renderer. This function exists mostly for testing purposes so that -// you can assure you're testing against a specific background color setting. -// -// Outside of testing you likely won't want to use this function as the -// backgrounds value will be automatically detected and cached against the -// terminal's current background color setting. +// default renderer. // // This function is thread-safe. func SetHasDarkBackground(b bool) { DefaultRenderer().SetHasDarkBackground(b) } -// SetHasDarkBackground sets the background color detection value on the -// renderer. This function exists mostly for testing purposes so that you can -// assure you're testing against a specific background color setting. -// -// Outside of testing you likely won't want to use this function as the -// backgrounds value will be automatically detected and cached against the -// terminal's current background color setting. +// SetHasDarkBackground sets the background color detection value for the +// default renderer. // // This function is thread-safe. func (r *Renderer) SetHasDarkBackground(b bool) { diff --git a/renderer_test.go b/renderer_test.go index e9494c82..35131e17 100644 --- a/renderer_test.go +++ b/renderer_test.go @@ -6,12 +6,12 @@ import ( ) func TestRendererHasDarkBackground(t *testing.T) { - r1 := NewRenderer(EnvColorProfile(os.Stdout, nil), true) + r1 := NewRenderer(DetectColorProfile(os.Stdout, nil), true) r1.SetHasDarkBackground(false) if r1.HasDarkBackground() { t.Error("Expected renderer to have light background") } - r2 := NewRenderer(EnvColorProfile(os.Stdout, nil), false) + r2 := NewRenderer(DetectColorProfile(os.Stdout, nil), false) r2.SetHasDarkBackground(true) if !r2.HasDarkBackground() { t.Error("Expected renderer to have dark background") @@ -25,7 +25,7 @@ func TestRendererWithOutput(t *testing.T) { } defer f.Close() defer os.Remove(f.Name()) - r := NewRenderer(EnvColorProfile(f, nil), true) + r := NewRenderer(DetectColorProfile(f, nil), true) r.SetColorProfile(TrueColor) if r.ColorProfile() != TrueColor { t.Error("Expected renderer to use true color") @@ -33,7 +33,7 @@ func TestRendererWithOutput(t *testing.T) { } func TestRace(t *testing.T) { - r := NewRenderer(EnvColorProfile(os.Stdout, nil), true) + r := NewRenderer(DetectColorProfile(os.Stdout, nil), true) for i := 0; i < 100; i++ { t.Run("SetColorProfile", func(t *testing.T) { diff --git a/style_test.go b/style_test.go index 97f03b85..8e1895fb 100644 --- a/style_test.go +++ b/style_test.go @@ -109,7 +109,7 @@ func TestStyleCustomRender(t *testing.T) { } func TestStyleRenderer(t *testing.T) { - r := NewRenderer(EnvColorProfile(os.Stdout, nil), true) + r := NewRenderer(DetectColorProfile(os.Stdout, nil), true) s1 := NewStyle().Bold(true) s2 := s1.Renderer(r) if s1.r == s2.r {