Add the ability to force dark or light mode colors for CLI output, overriding the automatic terminal background detection. Changes: - Add CLITheme field to TownSettings for persisting preference - Add GetThemeMode() and HasDarkBackground() to ui package for theme detection with GT_THEME env var override - Add ApplyThemeMode() to explicitly set lipgloss dark background - Add 'gt theme cli' subcommand to view/set CLI theme preference - Initialize theme in CLI startup (persistentPreRun) - Add comprehensive tests for theme mode functionality Usage: - gt theme cli # show current theme - gt theme cli dark # force dark mode - gt theme cli light # force light mode - gt theme cli auto # auto-detect (default) - GT_THEME=dark gt status # per-command override Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
150 lines
3.9 KiB
Go
150 lines
3.9 KiB
Go
package ui
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/muesli/termenv"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// ThemeMode represents the CLI color scheme mode.
|
|
type ThemeMode string
|
|
|
|
const (
|
|
// ThemeModeAuto lets the terminal background guide color selection.
|
|
ThemeModeAuto ThemeMode = "auto"
|
|
// ThemeModeDark forces dark mode colors (light text on dark background).
|
|
ThemeModeDark ThemeMode = "dark"
|
|
// ThemeModeLight forces light mode colors (dark text on light background).
|
|
ThemeModeLight ThemeMode = "light"
|
|
)
|
|
|
|
// themeMode is the cached theme mode, set during init.
|
|
var themeMode ThemeMode
|
|
|
|
// hasDarkBackground caches whether we're in dark mode.
|
|
var hasDarkBackground bool
|
|
|
|
// InitTheme initializes the theme mode. Call this early in main.
|
|
// configTheme is the value from TownSettings.CLITheme (may be empty).
|
|
func InitTheme(configTheme string) {
|
|
themeMode = resolveThemeMode(configTheme)
|
|
hasDarkBackground = detectDarkBackground(themeMode)
|
|
}
|
|
|
|
// GetThemeMode returns the current CLI color scheme mode.
|
|
// Priority order:
|
|
// 1. GT_THEME environment variable ("dark", "light", "auto")
|
|
// 2. Configured value from settings (passed to InitTheme)
|
|
// 3. Default: "auto"
|
|
func GetThemeMode() ThemeMode {
|
|
return themeMode
|
|
}
|
|
|
|
// HasDarkBackground returns true if we're displaying on a dark background.
|
|
// This is used by lipgloss AdaptiveColor to select appropriate colors.
|
|
func HasDarkBackground() bool {
|
|
return hasDarkBackground
|
|
}
|
|
|
|
// resolveThemeMode determines the theme mode from env and config.
|
|
func resolveThemeMode(configTheme string) ThemeMode {
|
|
// Priority 1: GT_THEME environment variable
|
|
if envTheme := os.Getenv("GT_THEME"); envTheme != "" {
|
|
switch strings.ToLower(envTheme) {
|
|
case "dark":
|
|
return ThemeModeDark
|
|
case "light":
|
|
return ThemeModeLight
|
|
case "auto":
|
|
return ThemeModeAuto
|
|
}
|
|
// Invalid value - fall through to config
|
|
}
|
|
|
|
// Priority 2: Config value
|
|
if configTheme != "" {
|
|
switch strings.ToLower(configTheme) {
|
|
case "dark":
|
|
return ThemeModeDark
|
|
case "light":
|
|
return ThemeModeLight
|
|
case "auto":
|
|
return ThemeModeAuto
|
|
}
|
|
}
|
|
|
|
// Default: auto
|
|
return ThemeModeAuto
|
|
}
|
|
|
|
// detectDarkBackground determines if we're on a dark background.
|
|
func detectDarkBackground(mode ThemeMode) bool {
|
|
switch mode {
|
|
case ThemeModeDark:
|
|
return true
|
|
case ThemeModeLight:
|
|
return false
|
|
default:
|
|
// Auto mode - use termenv detection
|
|
return termenv.HasDarkBackground()
|
|
}
|
|
}
|
|
|
|
// IsTerminal returns true if stdout is connected to a terminal (TTY).
|
|
func IsTerminal() bool {
|
|
return term.IsTerminal(int(os.Stdout.Fd()))
|
|
}
|
|
|
|
// ShouldUseColor determines if ANSI color codes should be used.
|
|
// Respects NO_COLOR (https://no-color.org/), CLICOLOR, and CLICOLOR_FORCE conventions.
|
|
func ShouldUseColor() bool {
|
|
// NO_COLOR takes precedence - any value disables color
|
|
if _, exists := os.LookupEnv("NO_COLOR"); exists {
|
|
return false
|
|
}
|
|
|
|
// CLICOLOR=0 disables color
|
|
if os.Getenv("CLICOLOR") == "0" {
|
|
return false
|
|
}
|
|
|
|
// CLICOLOR_FORCE enables color even in non-TTY
|
|
if _, exists := os.LookupEnv("CLICOLOR_FORCE"); exists {
|
|
return true
|
|
}
|
|
|
|
// default: use color only if stdout is a TTY
|
|
return IsTerminal()
|
|
}
|
|
|
|
// ShouldUseEmoji determines if emoji decorations should be used.
|
|
// Disabled in non-TTY mode to keep output machine-readable.
|
|
func ShouldUseEmoji() bool {
|
|
// GT_NO_EMOJI disables emoji output
|
|
if _, exists := os.LookupEnv("GT_NO_EMOJI"); exists {
|
|
return false
|
|
}
|
|
|
|
// default: use emoji only if stdout is a TTY
|
|
return IsTerminal()
|
|
}
|
|
|
|
// IsAgentMode returns true if the CLI is running in agent-optimized mode.
|
|
// This is triggered by:
|
|
// - GT_AGENT_MODE=1 environment variable (explicit)
|
|
// - CLAUDE_CODE environment variable (auto-detect Claude Code)
|
|
//
|
|
// Agent mode provides ultra-compact output optimized for LLM context windows.
|
|
func IsAgentMode() bool {
|
|
if os.Getenv("GT_AGENT_MODE") == "1" {
|
|
return true
|
|
}
|
|
// auto-detect Claude Code environment
|
|
if os.Getenv("CLAUDE_CODE") != "" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|