diff --git a/internal/cli/kill_cmd.go b/internal/cli/kill_cmd.go index 0225e98..a3e1d3b 100644 --- a/internal/cli/kill_cmd.go +++ b/internal/cli/kill_cmd.go @@ -7,10 +7,9 @@ import ( ) var killCmd = &cobra.Command{ - Use: "kill [session]", + Use: "kill [session...]", Aliases: []string{"k"}, - Short: "Kill a running tmux session (current session if no arg)", - Args: cobra.MaximumNArgs(1), + Short: "Kill running tmux sessions (current session if no arg)", RunE: runKill, ValidArgsFunction: completeRunningSessions, } @@ -19,24 +18,26 @@ func runKill(cmd *cobra.Command, args []string) error { opts := GetOpts() if len(args) > 0 { - sessionName := args[0] - // Check if session exists - if !tmux.SessionExists(opts, sessionName) { - return NewUserError("tmux session '" + sessionName + "' does not exist") + var errs []error + for _, sessionName := range args { + if !tmux.SessionExists(opts, sessionName) { + errs = append(errs, NewUserError("tmux session '"+sessionName+"' does not exist")) + continue + } + if err := tmux.KillSession(opts, sessionName); err != nil { + errs = append(errs, err) + } } - return tmux.KillSession(opts, sessionName) + return joinErrors(errs) } // No arg - kill current session return exec.RunCommand(opts, "tmux kill-session") } -// completeRunningSessions returns running session names for shell completion +// completeRunningSessions returns running session names for shell completion, +// excluding sessions already provided as arguments. func completeRunningSessions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // Don't complete if we already have an argument - if len(args) > 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - - return tmux.GetSessionNames(), cobra.ShellCompDirectiveNoFileComp + all := tmux.GetSessionNames() + return filterUsed(all, args), cobra.ShellCompDirectiveNoFileComp } diff --git a/internal/cli/remove_cmd.go b/internal/cli/remove_cmd.go index 95e5a1e..b4a22ed 100644 --- a/internal/cli/remove_cmd.go +++ b/internal/cli/remove_cmd.go @@ -11,12 +11,12 @@ import ( var removeConfigFile string var removeCmd = &cobra.Command{ - Use: "remove ", + Use: "remove ", Aliases: []string{"rm"}, - Short: "Remove a tmux workspace from the config file", - Args: cobra.ExactArgs(1), + Short: "Remove tmux workspaces from the config file", + Args: cobra.MinimumNArgs(1), RunE: runRemove, - ValidArgsFunction: completeSessionNames, + ValidArgsFunction: completeSessionNamesMulti, } func init() { @@ -25,30 +25,32 @@ func init() { func runRemove(cmd *cobra.Command, args []string) error { opts := GetOpts() - key := args[0] - // Verify the key exists allConfig, err := config.GetTmuxConfig() if err != nil { return err } - _, actualKey, exists := allConfig.Get(key) - if !exists { - return NewUserError("tmux config item '" + key + "' not found") + var errs []error + for _, key := range args { + _, actualKey, exists := allConfig.Get(key) + if !exists { + errs = append(errs, NewUserError("tmux config item '"+key+"' not found")) + continue + } + + err = config.RemoveConfigFromFile(actualKey, removeConfigFile, opts.Dry) + if err != nil { + errs = append(errs, err) + continue + } + + if !opts.Dry { + fmt.Printf("Removed tmux config item '%s'\n", key) + } + + exec.Log(opts, "Removed config item:", key) } - err = config.RemoveConfigFromFile(actualKey, removeConfigFile, opts.Dry) - if err != nil { - return err - } - - if !opts.Dry { - fmt.Printf("Removed tmux config item '%s'\n", key) - } - - // Log action in verbose/dry mode - exec.Log(opts, "Removed config item:", key) - - return nil + return joinErrors(errs) } diff --git a/internal/cli/remove_cmd_test.go b/internal/cli/remove_cmd_test.go index d2e59e9..af18cee 100644 --- a/internal/cli/remove_cmd_test.go +++ b/internal/cli/remove_cmd_test.go @@ -9,7 +9,7 @@ func TestRemoveCmd_Exists(t *testing.T) { t.Error("expected removeCmd to not be nil") } - if removeCmd.Use != "remove " { + if removeCmd.Use != "remove " { t.Errorf("unexpected Use: %q", removeCmd.Use) } } diff --git a/internal/cli/root.go b/internal/cli/root.go index a511a5e..946751b 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -1,6 +1,7 @@ package cli import ( + "errors" "fmt" "os" @@ -41,6 +42,11 @@ func NewUserError(message string) *UserError { return &UserError{Message: message} } +// joinErrors combines multiple errors into one, returning nil if there are none. +func joinErrors(errs []error) error { + return errors.Join(errs...) +} + // rootCmd represents the base command var rootCmd = &cobra.Command{ Use: "tx [session]", @@ -79,6 +85,39 @@ func completeSessionNames(cmd *cobra.Command, args []string, toComplete string) return names, cobra.ShellCompDirectiveNoFileComp } +// completeSessionNamesMulti returns session names excluding already-provided args. +func completeSessionNamesMulti(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + cfg, err := config.GetTmuxConfig() + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + var names []string + for name, item := range cfg { + if name != config.ConfigKey { + names = append(names, name) + names = append(names, item.Aliases...) + } + } + + return filterUsed(names, args), cobra.ShellCompDirectiveNoFileComp +} + +// filterUsed returns items from candidates that are not already in used. +func filterUsed(candidates []string, used []string) []string { + seen := make(map[string]bool, len(used)) + for _, u := range used { + seen[u] = true + } + var result []string + for _, c := range candidates { + if !seen[c] { + result = append(result, c) + } + } + return result +} + // buildFzfItems creates fzf items from a config file func buildFzfItems(cfg config.ConfigFile) []fzf.Item { items := make([]fzf.Item, 0, len(cfg))