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() {

View File

@@ -6,6 +6,7 @@ import (
"path/filepath"
"strings"
"github.com/muesli/termenv"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/session"
@@ -14,11 +15,14 @@ import (
)
var (
themeListFlag bool
themeApplyFlag bool
themeListFlag bool
themeApplyFlag bool
themeApplyAllFlag bool
)
// Valid CLI theme modes
var validCLIThemes = []string{"auto", "dark", "light"}
var themeCmd = &cobra.Command{
Use: "theme [name]",
GroupID: GroupConfig,
@@ -43,12 +47,37 @@ var themeApplyCmd = &cobra.Command{
By default, only applies to sessions in the current rig.
Use --all to apply to sessions across all rigs.`,
RunE: runThemeApply,
RunE: runThemeApply,
}
var themeCLICmd = &cobra.Command{
Use: "cli [mode]",
Short: "View or set CLI color scheme (dark/light/auto)",
Long: `Manage CLI output color scheme for Gas Town commands.
Without arguments, shows the current CLI theme mode and detection.
With a mode argument, sets the CLI theme preference.
Modes:
auto - Automatically detect terminal background (default)
dark - Force dark mode colors (light text for dark backgrounds)
light - Force light mode colors (dark text for light backgrounds)
The setting is stored in town settings (settings/config.json) and can
be overridden per-session via the GT_THEME environment variable.
Examples:
gt theme cli # Show current CLI theme
gt theme cli dark # Set CLI theme to dark mode
gt theme cli auto # Reset to auto-detection
GT_THEME=light gt status # Override for a single command`,
RunE: runThemeCLI,
}
func init() {
rootCmd.AddCommand(themeCmd)
themeCmd.AddCommand(themeApplyCmd)
themeCmd.AddCommand(themeCLICmd)
themeCmd.Flags().BoolVarP(&themeListFlag, "list", "l", false, "List available themes")
themeApplyCmd.Flags().BoolVarP(&themeApplyAllFlag, "all", "a", false, "Apply to all rigs, not just current")
}
@@ -362,3 +391,99 @@ func saveRigTheme(rigName, themeName string) error {
return nil
}
func runThemeCLI(cmd *cobra.Command, args []string) error {
townRoot, err := workspace.FindFromCwd()
if err != nil {
return fmt.Errorf("finding workspace: %w", err)
}
if townRoot == "" {
return fmt.Errorf("not in a Gas Town workspace")
}
settingsPath := config.TownSettingsPath(townRoot)
// Show current theme
if len(args) == 0 {
settings, err := config.LoadOrCreateTownSettings(settingsPath)
if err != nil {
return fmt.Errorf("loading settings: %w", err)
}
// Determine effective mode
configValue := settings.CLITheme
if configValue == "" {
configValue = "auto"
}
// Check for env override
envValue := os.Getenv("GT_THEME")
effectiveMode := configValue
if envValue != "" {
effectiveMode = strings.ToLower(envValue)
}
fmt.Printf("CLI Theme:\n")
fmt.Printf(" Configured: %s\n", configValue)
if envValue != "" {
fmt.Printf(" Override: %s (via GT_THEME)\n", envValue)
}
fmt.Printf(" Effective: %s\n", effectiveMode)
// Show detection result for auto mode
if effectiveMode == "auto" {
detected := "light"
if detectTerminalBackground() {
detected = "dark"
}
fmt.Printf(" Detected: %s background\n", detected)
}
return nil
}
// Set CLI theme
mode := strings.ToLower(args[0])
if !isValidCLITheme(mode) {
return fmt.Errorf("invalid CLI theme '%s' (valid: auto, dark, light)", mode)
}
// Load existing settings
settings, err := config.LoadOrCreateTownSettings(settingsPath)
if err != nil {
return fmt.Errorf("loading settings: %w", err)
}
// Update CLITheme
settings.CLITheme = mode
// Save
if err := config.SaveTownSettings(settingsPath, settings); err != nil {
return fmt.Errorf("saving settings: %w", err)
}
fmt.Printf("CLI theme set to '%s'\n", mode)
if mode == "auto" {
fmt.Println("Colors will adapt to your terminal's background.")
} else {
fmt.Printf("Colors optimized for %s backgrounds.\n", mode)
}
return nil
}
// isValidCLITheme checks if a CLI theme mode is valid.
func isValidCLITheme(mode string) bool {
for _, valid := range validCLIThemes {
if mode == valid {
return true
}
}
return false
}
// detectTerminalBackground returns true if terminal has dark background.
func detectTerminalBackground() bool {
// Use termenv for detection
return termenv.HasDarkBackground()
}