Files
gastown/internal/cmd/cycle.go
Steve Yegge b172fd4787 feat: Add rig infra cycling (witness ↔ refinery)
Extended the unified cycle system to include rig infrastructure sessions:
- Witness ↔ Refinery (per rig) now cycle with C-b n/p

Also moved SetCycleBindings into ConfigureGasTownSession so ALL Gas Town
sessions automatically get the unified cycle bindings. Removed redundant
individual calls from crew, mayor, and deacon startup code.

Cycle groups are now:
- Town: Mayor ↔ Deacon
- Crew (per rig): All crew members in same rig
- Infra (per rig): Witness ↔ Refinery

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 16:36:38 -08:00

176 lines
5.0 KiB
Go

package cmd
import (
"fmt"
"os/exec"
"sort"
"strings"
"github.com/spf13/cobra"
)
// cycleSession is the --session flag for cycle next/prev commands.
// When run via tmux key binding (run-shell), the session context may not be
// correct, so we pass the session name explicitly via #{session_name} expansion.
var cycleSession string
func init() {
rootCmd.AddCommand(cycleCmd)
cycleCmd.AddCommand(cycleNextCmd)
cycleCmd.AddCommand(cyclePrevCmd)
cycleNextCmd.Flags().StringVar(&cycleSession, "session", "", "Override current session (used by tmux binding)")
cyclePrevCmd.Flags().StringVar(&cycleSession, "session", "", "Override current session (used by tmux binding)")
}
var cycleCmd = &cobra.Command{
Use: "cycle",
Short: "Cycle between sessions in the same group",
Long: `Cycle between related tmux sessions based on the current session type.
Session groups:
- Town sessions: Mayor ↔ Deacon
- Crew sessions: All crew members in the same rig (e.g., gastown/crew/max ↔ gastown/crew/joe)
- Rig infra sessions: Witness ↔ Refinery (per rig)
The appropriate cycling is detected automatically from the session name.`,
}
var cycleNextCmd = &cobra.Command{
Use: "next",
Short: "Switch to next session in group",
Long: `Switch to the next session in the current group.
This command is typically invoked via the C-b n keybinding. It automatically
detects whether you're in a town-level session (Mayor/Deacon) or a crew session
and cycles within the appropriate group.`,
RunE: func(cmd *cobra.Command, args []string) error {
return cycleToSession(1, cycleSession)
},
}
var cyclePrevCmd = &cobra.Command{
Use: "prev",
Short: "Switch to previous session in group",
Long: `Switch to the previous session in the current group.
This command is typically invoked via the C-b p keybinding. It automatically
detects whether you're in a town-level session (Mayor/Deacon) or a crew session
and cycles within the appropriate group.`,
RunE: func(cmd *cobra.Command, args []string) error {
return cycleToSession(-1, cycleSession)
},
}
// cycleToSession dispatches to the appropriate cycling function based on session type.
// direction: 1 for next, -1 for previous
// sessionOverride: if non-empty, use this instead of detecting current session
func cycleToSession(direction int, sessionOverride string) error {
session := sessionOverride
if session == "" {
var err error
session, err = getCurrentTmuxSession()
if err != nil {
return nil // Not in tmux, nothing to do
}
}
// Check if it's a town-level session
for _, townSession := range townLevelSessions {
if session == townSession {
return cycleTownSession(direction, session)
}
}
// Check if it's a crew session (format: gt-<rig>-crew-<name>)
if strings.HasPrefix(session, "gt-") && strings.Contains(session, "-crew-") {
return cycleCrewSession(direction, session)
}
// Check if it's a rig infra session (witness or refinery)
if rig := parseRigInfraSession(session); rig != "" {
return cycleRigInfraSession(direction, session, rig)
}
// Unknown session type (polecat) - do nothing
return nil
}
// parseRigInfraSession extracts rig name if this is a witness or refinery session.
// Returns empty string if not a rig infra session.
// Format: gt-<rig>-witness or gt-<rig>-refinery
func parseRigInfraSession(session string) string {
if !strings.HasPrefix(session, "gt-") {
return ""
}
rest := session[3:] // Remove "gt-" prefix
// Check for -witness or -refinery suffix
if strings.HasSuffix(rest, "-witness") {
return strings.TrimSuffix(rest, "-witness")
}
if strings.HasSuffix(rest, "-refinery") {
return strings.TrimSuffix(rest, "-refinery")
}
return ""
}
// cycleRigInfraSession cycles between witness and refinery sessions for a rig.
func cycleRigInfraSession(direction int, currentSession, rig string) error {
// Find running infra sessions for this rig
witnessSession := fmt.Sprintf("gt-%s-witness", rig)
refinerySession := fmt.Sprintf("gt-%s-refinery", rig)
var sessions []string
allSessions, err := listTmuxSessions()
if err != nil {
return err
}
for _, s := range allSessions {
if s == witnessSession || s == refinerySession {
sessions = append(sessions, s)
}
}
if len(sessions) == 0 {
return nil // No infra sessions running
}
// Sort for consistent ordering
sort.Strings(sessions)
// Find current position
currentIdx := -1
for i, s := range sessions {
if s == currentSession {
currentIdx = i
break
}
}
if currentIdx == -1 {
return nil // Current session not in list
}
// Calculate target index (with wrapping)
targetIdx := (currentIdx + direction + len(sessions)) % len(sessions)
if targetIdx == currentIdx {
return nil // Only one session
}
// Switch to target session
cmd := exec.Command("tmux", "switch-client", "-t", sessions[targetIdx])
return cmd.Run()
}
// listTmuxSessions returns all tmux session names.
func listTmuxSessions() ([]string, error) {
out, err := exec.Command("tmux", "list-sessions", "-F", "#{session_name}").Output()
if err != nil {
return nil, err
}
return splitLines(string(out)), nil
}