package bonfire import ( "io" "os" "strings" "charm.land/lipgloss/v2" "github.com/charmbracelet/colorprofile" ) // ColorMode selects how console output decides on color: from the terminal // and the environment, always, or never. type ColorMode int const ( // ColorAuto colors output when the writer is a terminal and the // environment allows it (NO_COLOR and TERM=dumb disable color, // FORCE_COLOR enables it on any writer). ColorAuto ColorMode = iota // ColorAlways colors output whatever the writer is (--ansi). ColorAlways // ColorNever writes no escape codes at all (--no-ansi). ColorNever ) // Palette is the shared console look: the status styles of list, help, // widgets and log lines. Render through a writer that applies the color // profile (DetectProfile), because lipgloss always emits ANSI codes. type Palette struct { // Success marks completed work (green). Success lipgloss.Style // Current marks the item in progress and informational lines (cyan). Current lipgloss.Style // Warning marks warnings (yellow). Warning lipgloss.Style // Error marks failures (red). Error lipgloss.Style // ErrorBold marks fatal failures and error levels (bold red). ErrorBold lipgloss.Style // Muted marks secondary detail such as times and keys (gray). Muted lipgloss.Style // Header marks headings and names (bold). Header lipgloss.Style } // DefaultPalette returns the console palette, ported from SSU's status // styles: green success, cyan current, yellow warning, red and bold red // errors, gray (240) muted text and bold headers. func DefaultPalette() Palette { return Palette{ Success: lipgloss.NewStyle().Foreground(lipgloss.Color("2")), Current: lipgloss.NewStyle().Foreground(lipgloss.Color("6")), Warning: lipgloss.NewStyle().Foreground(lipgloss.Color("3")), Error: lipgloss.NewStyle().Foreground(lipgloss.Color("1")), ErrorBold: lipgloss.NewStyle().Foreground(lipgloss.Color("1")).Bold(true), Muted: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), Header: lipgloss.NewStyle().Bold(true), } } // DetectProfile returns the color profile for console output written to w. // ColorNever gives colorprofile.NoTTY (no escape codes at all). ColorAlways // gives the detected profile raised to at least 256 colors. ColorAuto treats // a non-empty FORCE_COLOR as ColorAlways unless NO_COLOR is set or TERM is // dumb, and otherwise detects the profile from w and environ, which honours // NO_COLOR and TERM=dumb. A nil environ means os.Environ(). func DetectProfile(w io.Writer, mode ColorMode, environ []string) colorprofile.Profile { if mode == ColorNever { return colorprofile.NoTTY } if environ == nil { environ = os.Environ() } if mode == ColorAuto && forceColor(environ) { mode = ColorAlways } p := colorprofile.Detect(w, environ) if mode == ColorAlways && p < colorprofile.ANSI256 { p = colorprofile.ANSI256 } return p } func forceColor(environ []string) bool { if envValue(environ, "NO_COLOR") != "" || envValue(environ, "TERM") == "dumb" { return false } return envValue(environ, "FORCE_COLOR") != "" } func envValue(environ []string, key string) string { for i := len(environ) - 1; i >= 0; i-- { k, v, ok := strings.Cut(environ[i], "=") if ok && k == key { return v } } return "" }