feat(theme): add dark mode CLI theme support (#911)

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>
This commit is contained in:
Kody Wildfeuer
2026-01-25 01:15:48 -05:00
committed by GitHub
parent 377b4877cd
commit baf9311bfe
6 changed files with 338 additions and 3 deletions

View File

@@ -245,3 +245,90 @@ func TestIsAgentMode_CLAUDE_CODE_AnyValue(t *testing.T) {
t.Error("IsAgentMode() should return true when CLAUDE_CODE is set to any value")
}
}
func TestInitTheme_EnvOverridesConfig(t *testing.T) {
oldGTTheme := os.Getenv("GT_THEME")
defer func() {
if oldGTTheme != "" {
os.Setenv("GT_THEME", oldGTTheme)
} else {
os.Unsetenv("GT_THEME")
}
}()
// Test: env var overrides config
os.Setenv("GT_THEME", "dark")
InitTheme("light") // config says light
if GetThemeMode() != ThemeModeDark {
t.Errorf("Expected dark mode from env var, got %s", GetThemeMode())
}
os.Setenv("GT_THEME", "light")
InitTheme("dark") // config says dark
if GetThemeMode() != ThemeModeLight {
t.Errorf("Expected light mode from env var, got %s", GetThemeMode())
}
}
func TestInitTheme_ConfigUsedWhenNoEnv(t *testing.T) {
oldGTTheme := os.Getenv("GT_THEME")
defer func() {
if oldGTTheme != "" {
os.Setenv("GT_THEME", oldGTTheme)
} else {
os.Unsetenv("GT_THEME")
}
}()
os.Unsetenv("GT_THEME")
InitTheme("dark")
if GetThemeMode() != ThemeModeDark {
t.Errorf("Expected dark mode from config, got %s", GetThemeMode())
}
InitTheme("light")
if GetThemeMode() != ThemeModeLight {
t.Errorf("Expected light mode from config, got %s", GetThemeMode())
}
}
func TestInitTheme_DefaultsToAuto(t *testing.T) {
oldGTTheme := os.Getenv("GT_THEME")
defer func() {
if oldGTTheme != "" {
os.Setenv("GT_THEME", oldGTTheme)
} else {
os.Unsetenv("GT_THEME")
}
}()
os.Unsetenv("GT_THEME")
InitTheme("") // no config
if GetThemeMode() != ThemeModeAuto {
t.Errorf("Expected auto mode as default, got %s", GetThemeMode())
}
}
func TestHasDarkBackground_ForcedModes(t *testing.T) {
oldGTTheme := os.Getenv("GT_THEME")
defer func() {
if oldGTTheme != "" {
os.Setenv("GT_THEME", oldGTTheme)
} else {
os.Unsetenv("GT_THEME")
}
}()
os.Setenv("GT_THEME", "dark")
InitTheme("")
if !HasDarkBackground() {
t.Error("Expected HasDarkBackground() to return true when mode is dark")
}
os.Setenv("GT_THEME", "light")
InitTheme("")
if HasDarkBackground() {
t.Error("Expected HasDarkBackground() to return false when mode is light")
}
}