feat: Unify C-b n/p cycling across session types

The previous implementation set separate bindings for crew vs town
sessions, but tmux key bindings are global. This meant whichever
session type was started last would overwrite the other's bindings.

New approach:
- Add unified `gt cycle next/prev` command that auto-detects session type
- Town sessions (gt-mayor, gt-deacon) cycle within town group
- Crew sessions (gt-*-crew-*) cycle within their rig's crew
- Other sessions (polecats, witness, refinery) do nothing on cycle

The old SetCrewCycleBindings and SetTownCycleBindings are now aliases
for the unified SetCycleBindings function.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-28 16:07:58 -08:00
parent faee888a9f
commit 40977b1ccc
2 changed files with 112 additions and 21 deletions

88
internal/cmd/cycle.go Normal file
View File

@@ -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-<rig>-crew-<name>)
if strings.HasPrefix(session, "gt-") && strings.Contains(session, "-crew-") {
return cycleCrewSession(direction, session)
}
// Unknown session type (polecat, witness, refinery) - do nothing
return nil
}

View File

@@ -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