Files
gastown/internal/cmd/theme.go
joe 4bcf50bf1c Revert to simple gt-mayor/gt-deacon session names
Reverts the session naming changes from PR #70. Multi-town support
on a single machine is not a real use case - rigs provide project
isolation, and true isolation should use containers/VMs.

Changes:
- MayorSessionName() and DeaconSessionName() no longer take townName parameter
- ParseSessionName() handles simple gt-mayor and gt-deacon formats
- Removed Town field from AgentIdentity and AgentSession structs
- Updated all callers and tests

Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 14:33:24 -08:00

365 lines
9.6 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/session"
"github.com/steveyegge/gastown/internal/tmux"
"github.com/steveyegge/gastown/internal/workspace"
)
var (
themeListFlag bool
themeApplyFlag bool
themeApplyAllFlag bool
)
var themeCmd = &cobra.Command{
Use: "theme [name]",
GroupID: GroupConfig,
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 running sessions",
Long: `Apply theme to running Gas Town sessions.
By default, only applies to sessions in the current rig.
Use --all to apply to sessions across all rigs.`,
RunE: runThemeApply,
}
func init() {
rootCmd.AddCommand(themeCmd)
themeCmd.AddCommand(themeApplyCmd)
themeCmd.Flags().BoolVarP(&themeListFlag, "list", "l", false, "List available themes")
themeApplyCmd.Flags().BoolVarP(&themeApplyAllFlag, "all", "a", false, "Apply to all rigs, not just current")
}
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 settings/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()
// Get session names for comparison
mayorSession := session.MayorSessionName()
deaconSession := session.DeaconSessionName()
// Apply to matching sessions
applied := 0
for _, sess := range sessions {
if !strings.HasPrefix(sess, "gt-") {
continue
}
// Determine theme and identity for this session
var theme tmux.Theme
var rig, worker, role string
if sess == mayorSession {
theme = tmux.MayorTheme()
worker = "Mayor"
role = "coordinator"
} else if sess == deaconSession {
theme = tmux.DeaconTheme()
worker = "Deacon"
role = "health-check"
} else if strings.HasSuffix(sess, "-witness") && strings.HasPrefix(sess, "gt-") {
// Witness sessions: gt-<rig>-witness
rig = strings.TrimPrefix(strings.TrimSuffix(sess, "-witness"), "gt-")
theme = getThemeForRole(rig, "witness")
worker = "witness"
role = "witness"
} else {
// Parse session name: gt-<rig>-<worker> or gt-<rig>-crew-<name>
parts := strings.SplitN(sess, "-", 3)
if len(parts) < 3 {
continue
}
rig = parts[1]
// Skip if not matching current rig (unless --all flag)
if !themeApplyAllFlag && rigName != "" && rig != rigName {
continue
}
workerPart := parts[2]
if strings.HasPrefix(workerPart, "crew-") {
worker = strings.TrimPrefix(workerPart, "crew-")
role = "crew"
} else if workerPart == "refinery" {
worker = "refinery"
role = "refinery"
} else {
worker = workerPart
role = "polecat"
}
// Use role-based theme resolution
theme = getThemeForRole(rig, role)
}
// Apply theme and status format
if err := t.ApplyTheme(sess, theme); err != nil {
fmt.Printf(" %s: failed (%v)\n", sess, err)
continue
}
if err := t.SetStatusFormat(sess, rig, worker, role); err != nil {
fmt.Printf(" %s: failed to set format (%v)\n", sess, err)
continue
}
if err := t.SetDynamicStatus(sess); err != nil {
fmt.Printf(" %s: failed to set dynamic status (%v)\n", sess, err)
continue
}
fmt.Printf(" %s: applied %s theme\n", sess, 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)
}
// getThemeForRole returns the theme for a specific role in a rig.
// Resolution order:
// 1. Per-rig role override (rig/settings/config.json)
// 2. Global role default (mayor/config.json)
// 3. Built-in role defaults (witness=rust, refinery=plum)
// 4. Rig theme (config or hash-based)
func getThemeForRole(rigName, role string) tmux.Theme {
townRoot, _ := workspace.FindFromCwd()
// 1. Check per-rig role override
if townRoot != "" {
settingsPath := filepath.Join(townRoot, rigName, "settings", "config.json")
if settings, err := config.LoadRigSettings(settingsPath); err == nil {
if settings.Theme != nil && settings.Theme.RoleThemes != nil {
if themeName, ok := settings.Theme.RoleThemes[role]; ok {
if theme := tmux.GetThemeByName(themeName); theme != nil {
return *theme
}
}
}
}
}
// 2. Check global role default (mayor config)
if townRoot != "" {
mayorConfigPath := filepath.Join(townRoot, "mayor", "config.json")
if mayorCfg, err := config.LoadMayorConfig(mayorConfigPath); err == nil {
if mayorCfg.Theme != nil && mayorCfg.Theme.RoleDefaults != nil {
if themeName, ok := mayorCfg.Theme.RoleDefaults[role]; ok {
if theme := tmux.GetThemeByName(themeName); theme != nil {
return *theme
}
}
}
}
}
// 3. Check built-in role defaults
builtins := config.BuiltinRoleThemes()
if themeName, ok := builtins[role]; ok {
if theme := tmux.GetThemeByName(themeName); theme != nil {
return *theme
}
}
// 4. Fall back to rig theme
return getThemeForRig(rigName)
}
// loadRigTheme loads the theme name from rig settings.
func loadRigTheme(rigName string) string {
townRoot, err := workspace.FindFromCwd()
if err != nil || townRoot == "" {
return ""
}
settingsPath := filepath.Join(townRoot, rigName, "settings", "config.json")
settings, err := config.LoadRigSettings(settingsPath)
if err != nil {
return ""
}
if settings.Theme != nil && settings.Theme.Name != "" {
return settings.Theme.Name
}
return ""
}
// saveRigTheme saves the theme name to rig settings.
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")
}
settingsPath := filepath.Join(townRoot, rigName, "settings", "config.json")
// Load existing settings or create new
var settings *config.RigSettings
settings, err = config.LoadRigSettings(settingsPath)
if err != nil {
// Create new settings if not found
if os.IsNotExist(err) || strings.Contains(err.Error(), "not found") {
settings = config.NewRigSettings()
} else {
return fmt.Errorf("loading settings: %w", err)
}
}
// Set theme
settings.Theme = &config.ThemeConfig{
Name: themeName,
}
// Save
if err := config.SaveRigSettings(settingsPath, settings); err != nil {
return fmt.Errorf("saving settings: %w", err)
}
return nil
}