Files
gastown/internal/cmd/theme.go
Steve Yegge c7e83b1619 Add persistent theme config and fix crew session theming
- Fix crew sessions missing theme application (ConfigureGasTownSession)
- Add theme persistence to .gastown/config.json
- gt theme <name> now saves to config
- gt theme apply reads from config, falls back to hash-based default
- Improve rig detection using GT_RIG env var and path parsing
- gt theme shows whether theme is configured or default

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 20:43:34 -08:00

294 lines
7.2 KiB
Go

package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/tmux"
"github.com/steveyegge/gastown/internal/workspace"
)
var (
themeListFlag bool
themeApplyFlag bool
)
var themeCmd = &cobra.Command{
Use: "theme [name]",
Short: "View or set tmux theme for the current rig",
Long: `Manage tmux status bar themes for Gas Town sessions.
Without arguments, shows the current theme assignment.
With a name argument, sets the theme for this rig.
Examples:
gt theme # Show current theme
gt theme --list # List available themes
gt theme forest # Set theme to 'forest'
gt theme apply # Apply theme to all running sessions in this rig`,
RunE: runTheme,
}
var themeApplyCmd = &cobra.Command{
Use: "apply",
Short: "Apply theme to all running sessions in this rig",
RunE: runThemeApply,
}
func init() {
rootCmd.AddCommand(themeCmd)
themeCmd.AddCommand(themeApplyCmd)
themeCmd.Flags().BoolVarP(&themeListFlag, "list", "l", false, "List available themes")
}
func runTheme(cmd *cobra.Command, args []string) error {
// List mode
if themeListFlag {
fmt.Println("Available themes:")
for _, name := range tmux.ListThemeNames() {
theme := tmux.GetThemeByName(name)
fmt.Printf(" %-10s %s\n", name, theme.Style())
}
// Also show Mayor theme
mayor := tmux.MayorTheme()
fmt.Printf(" %-10s %s (Mayor only)\n", mayor.Name, mayor.Style())
return nil
}
// Determine current rig
rigName := detectCurrentRig()
if rigName == "" {
rigName = "unknown"
}
// Show current theme assignment
if len(args) == 0 {
theme := getThemeForRig(rigName)
fmt.Printf("Rig: %s\n", rigName)
fmt.Printf("Theme: %s (%s)\n", theme.Name, theme.Style())
// Show if it's configured vs default
if configured := loadRigTheme(rigName); configured != "" {
fmt.Printf("(configured in .gastown/config.json)\n")
} else {
fmt.Printf("(default, based on rig name hash)\n")
}
return nil
}
// Set theme
themeName := args[0]
theme := tmux.GetThemeByName(themeName)
if theme == nil {
return fmt.Errorf("unknown theme: %s (use --list to see available themes)", themeName)
}
// Save to rig config
if err := saveRigTheme(rigName, themeName); err != nil {
return fmt.Errorf("saving theme config: %w", err)
}
fmt.Printf("Theme '%s' saved for rig '%s'\n", themeName, rigName)
fmt.Println("Run 'gt theme apply' to apply to running sessions")
return nil
}
func runThemeApply(cmd *cobra.Command, args []string) error {
t := tmux.NewTmux()
// Get all sessions
sessions, err := t.ListSessions()
if err != nil {
return fmt.Errorf("listing sessions: %w", err)
}
// Determine current rig
rigName := detectCurrentRig()
// Apply to matching sessions
applied := 0
for _, session := range sessions {
if !strings.HasPrefix(session, "gt-") {
continue
}
// Determine theme and identity for this session
var theme tmux.Theme
var rig, worker, role string
if session == "gt-mayor" {
theme = tmux.MayorTheme()
worker = "Mayor"
role = "coordinator"
} else {
// Parse session name: gt-<rig>-<worker> or gt-<rig>-crew-<name>
parts := strings.SplitN(session, "-", 3)
if len(parts) < 3 {
continue
}
rig = parts[1]
// Skip if not matching current rig (if we know it)
if rigName != "" && rig != rigName {
continue
}
workerPart := parts[2]
if strings.HasPrefix(workerPart, "crew-") {
worker = strings.TrimPrefix(workerPart, "crew-")
role = "crew"
} else {
worker = workerPart
role = "polecat"
}
// Use configured theme, fall back to hash-based assignment
theme = getThemeForRig(rig)
}
// Apply theme and status format
if err := t.ApplyTheme(session, theme); err != nil {
fmt.Printf(" %s: failed (%v)\n", session, err)
continue
}
if err := t.SetStatusFormat(session, rig, worker, role); err != nil {
fmt.Printf(" %s: failed to set format (%v)\n", session, err)
continue
}
if err := t.SetDynamicStatus(session); err != nil {
fmt.Printf(" %s: failed to set dynamic status (%v)\n", session, err)
continue
}
fmt.Printf(" %s: applied %s theme\n", session, theme.Name)
applied++
}
if applied == 0 {
fmt.Println("No matching sessions found")
} else {
fmt.Printf("\nApplied theme to %d session(s)\n", applied)
}
return nil
}
// detectCurrentRig determines the rig from environment or cwd.
func detectCurrentRig() string {
// Try environment first (GT_RIG is set in tmux sessions)
if rig := os.Getenv("GT_RIG"); rig != "" {
return rig
}
// Try to extract from tmux session name
if session := detectCurrentSession(); session != "" {
// Extract rig from session name: gt-<rig>-...
parts := strings.SplitN(session, "-", 3)
if len(parts) >= 2 && parts[0] == "gt" && parts[1] != "mayor" && parts[1] != "deacon" {
return parts[1]
}
}
// Try to detect from actual cwd path
cwd, err := os.Getwd()
if err != nil {
return ""
}
// Find town root to extract rig name
townRoot, err := workspace.FindFromCwd()
if err != nil || townRoot == "" {
return ""
}
// Get path relative to town root
rel, err := filepath.Rel(townRoot, cwd)
if err != nil {
return ""
}
// Extract first path component (rig name)
// Patterns: <rig>/..., mayor/..., deacon/...
parts := strings.Split(rel, string(filepath.Separator))
if len(parts) > 0 && parts[0] != "." && parts[0] != "mayor" && parts[0] != "deacon" {
return parts[0]
}
return ""
}
// getThemeForRig returns the theme for a rig, checking config first.
func getThemeForRig(rigName string) tmux.Theme {
// Try to load configured theme
if themeName := loadRigTheme(rigName); themeName != "" {
if theme := tmux.GetThemeByName(themeName); theme != nil {
return *theme
}
}
// Fall back to hash-based assignment
return tmux.AssignTheme(rigName)
}
// loadRigTheme loads the theme name from rig config.
func loadRigTheme(rigName string) string {
townRoot, err := workspace.FindFromCwd()
if err != nil || townRoot == "" {
return ""
}
configPath := filepath.Join(townRoot, rigName, ".gastown", "config.json")
cfg, err := config.LoadRigConfig(configPath)
if err != nil {
return ""
}
if cfg.Theme != nil && cfg.Theme.Name != "" {
return cfg.Theme.Name
}
return ""
}
// saveRigTheme saves the theme name to rig config.
func saveRigTheme(rigName, themeName 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")
}
configPath := filepath.Join(townRoot, rigName, ".gastown", "config.json")
// Load existing config or create new
var cfg *config.RigConfig
cfg, err = config.LoadRigConfig(configPath)
if err != nil {
// Create new config if not found
if os.IsNotExist(err) || strings.Contains(err.Error(), "not found") {
cfg = &config.RigConfig{
Type: "rig",
Version: config.CurrentRigConfigVersion,
}
} else {
return fmt.Errorf("loading config: %w", err)
}
}
// Set theme
cfg.Theme = &config.ThemeConfig{
Name: themeName,
}
// Save
if err := config.SaveRigConfig(configPath, cfg); err != nil {
return fmt.Errorf("saving config: %w", err)
}
return nil
}