diff --git a/internal/cmd/cycle.go b/internal/cmd/cycle.go new file mode 100644 index 00000000..e3e36e3a --- /dev/null +++ b/internal/cmd/cycle.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "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) + +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--crew-) + if strings.HasPrefix(session, "gt-") && strings.Contains(session, "-crew-") { + return cycleCrewSession(direction, session) + } + + // Unknown session type (polecat, witness, refinery) - do nothing + return nil +} diff --git a/internal/tmux/tmux.go b/internal/tmux/tmux.go index e5753da7..1b7870d8 100644 --- a/internal/tmux/tmux.go +++ b/internal/tmux/tmux.go @@ -653,39 +653,42 @@ func (t *Tmux) SwitchClient(targetSession string) error { return err } -// SetCrewCycleBindings sets up C-b n/p to cycle through crew sessions in the same rig. -// This allows quick switching between crew members without using the session picker. +// SetCrewCycleBindings sets up C-b n/p to cycle through sessions. +// This is now an alias for SetCycleBindings - the unified command detects +// session type automatically. // // IMPORTANT: We pass #{session_name} to the command because run-shell doesn't // reliably preserve the session context. tmux expands #{session_name} at binding // resolution time (when the key is pressed), giving us the correct session. func (t *Tmux) SetCrewCycleBindings(session string) error { - // C-b n → gt crew next (switch to next crew session) - // #{session_name} is expanded by tmux when the key is pressed - if _, err := t.run("bind-key", "-T", "prefix", "n", - "run-shell", "gt crew next --session '#{session_name}'"); err != nil { - return err - } - // C-b p → gt crew prev (switch to previous crew session) - if _, err := t.run("bind-key", "-T", "prefix", "p", - "run-shell", "gt crew prev --session '#{session_name}'"); err != nil { - return err - } - return nil + return t.SetCycleBindings(session) } -// SetTownCycleBindings sets up C-b n/p to cycle through town-level sessions. -// Town-level sessions are Mayor and Deacon - the global coordinators. -// This allows quick switching between them without using the session picker. +// SetTownCycleBindings sets up C-b n/p to cycle through sessions. +// This is now an alias for SetCycleBindings - the unified command detects +// session type automatically. func (t *Tmux) SetTownCycleBindings(session string) error { - // C-b n → gt town next (switch to next town session) + return t.SetCycleBindings(session) +} + +// SetCycleBindings sets up C-b n/p to cycle through related sessions. +// The gt cycle command automatically detects the session type and cycles +// within the appropriate group: +// - Town sessions: Mayor ↔ Deacon +// - Crew sessions: All crew members in the same rig +// +// IMPORTANT: We pass #{session_name} to the command because run-shell doesn't +// reliably preserve the session context. tmux expands #{session_name} at binding +// resolution time (when the key is pressed), giving us the correct session. +func (t *Tmux) SetCycleBindings(session string) error { + // C-b n → gt cycle next (auto-detects session type) if _, err := t.run("bind-key", "-T", "prefix", "n", - "run-shell", "gt town next --session '#{session_name}'"); err != nil { + "run-shell", "gt cycle next --session '#{session_name}'"); err != nil { return err } - // C-b p → gt town prev (switch to previous town session) + // C-b p → gt cycle prev (auto-detects session type) if _, err := t.run("bind-key", "-T", "prefix", "p", - "run-shell", "gt town prev --session '#{session_name}'"); err != nil { + "run-shell", "gt cycle prev --session '#{session_name}'"); err != nil { return err } return nil