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

@@ -8,7 +8,9 @@ import (
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/ui"
"github.com/steveyegge/gastown/internal/version"
"github.com/steveyegge/gastown/internal/workspace"
)
@@ -66,6 +68,9 @@ var branchCheckExemptCommands = map[string]bool{
// persistentPreRun runs before every command.
func persistentPreRun(cmd *cobra.Command, args []string) error {
// Initialize CLI theme (dark/light mode support)
initCLITheme()
// Get the root command name being run
cmdName := cmd.Name()
@@ -88,6 +93,22 @@ func persistentPreRun(cmd *cobra.Command, args []string) error {
return CheckBeadsVersion()
}
// initCLITheme initializes the CLI color theme based on settings and environment.
func initCLITheme() {
// Try to load town settings for CLITheme config
var configTheme string
if townRoot, err := workspace.FindFromCwd(); err == nil && townRoot != "" {
settingsPath := config.TownSettingsPath(townRoot)
if settings, err := config.LoadOrCreateTownSettings(settingsPath); err == nil {
configTheme = settings.CLITheme
}
}
// Initialize theme with config value (env var takes precedence inside InitTheme)
ui.InitTheme(configTheme)
ui.ApplyThemeMode()
}
// warnIfTownRootOffMain prints a warning if the town root is not on main branch.
// This is a non-blocking warning to help catch accidental branch switches.
func warnIfTownRootOffMain() {