diff --git a/.claude-plugin/skills/worktrunk/reference/switch.md b/.claude-plugin/skills/worktrunk/reference/switch.md index c9305e262..ac07891ee 100644 --- a/.claude-plugin/skills/worktrunk/reference/switch.md +++ b/.claude-plugin/skills/worktrunk/reference/switch.md @@ -109,6 +109,20 @@ Usage: wt switch [OPTIONS] --no-verify Skip hooks + -t, --tmux + Run in new tmux window/session + + Creates the worktree in a new tmux window (if already in tmux) or + session (if not). All hooks run there instead of the current terminal. + By default, the session/window is attached. Use --detach for + background. + + -d, --detach + Run tmux session in background + + Only valid with --tmux. Creates a detached session instead of + attaching to it. + -h, --help Print help (see a summary with '-h') diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1794fb34b..e5358089f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -71,8 +71,9 @@ repos: entry: '\.output\(\)' # shell_exec.rs: defines run() which legitimately calls .output() # select.rs: skim API's selected.output() is not Command::output() + # tmux.rs: quick tmux status checks that don't need logging # tests/benches: test utilities run commands directly - exclude: '^(src/shell_exec\.rs|src/commands/select\.rs|tests/|benches/)' + exclude: '^(src/shell_exec\.rs|src/commands/select\.rs|src/commands/tmux\.rs|tests/|benches/)' ci: # pre-commit.ci doesn't have Rust toolchain, so skip Rust-specific hooks. diff --git a/docs/content/switch.md b/docs/content/switch.md index ca707a9b3..f94dd9796 100644 --- a/docs/content/switch.md +++ b/docs/content/switch.md @@ -132,6 +132,20 @@ Usage: wt switch [OPTIONS] --no-verify Skip hooks + -t, --tmux + Run in new tmux window/session + + Creates the worktree in a new tmux window (if already in tmux) or + session (if not). All hooks run there instead of the current terminal. + By default, the session/window is attached. Use --detach for + background. + + -d, --detach + Run tmux session in background + + Only valid with --tmux. Creates a detached session instead of + attaching to it. + -h, --help Print help (see a summary with '-h') diff --git a/src/cli/mod.rs b/src/cli/mod.rs index fe7d73e65..7b37a1322 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -346,6 +346,21 @@ To change which branch a worktree is on, use `git switch` inside that worktree. /// Skip hooks #[arg(long = "no-verify", action = clap::ArgAction::SetFalse, default_value_t = true)] verify: bool, + + /// Run in new tmux window/session + /// + /// Creates the worktree in a new tmux window (if already in tmux) or + /// session (if not). All hooks run there instead of the current terminal. + /// By default, the session/window is attached. Use --detach for background. + #[arg(short = 't', long)] + tmux: bool, + + /// Run tmux session in background + /// + /// Only valid with --tmux. Creates a detached session instead of + /// attaching to it. + #[arg(short = 'd', long, requires = "tmux")] + detach: bool, }, /// List worktrees and their status diff --git a/src/commands/mod.rs b/src/commands/mod.rs index bdb3489ee..4a355d70f 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -19,6 +19,7 @@ pub mod repository_ext; pub mod select; pub mod statusline; pub mod step_commands; +pub mod tmux; pub mod worktree; pub use command_approval::approve_hooks; diff --git a/src/commands/tmux.rs b/src/commands/tmux.rs new file mode 100644 index 000000000..ecaffda03 --- /dev/null +++ b/src/commands/tmux.rs @@ -0,0 +1,283 @@ +//! Tmux integration for launching worktree operations in new windows/sessions. + +use anyhow::{Result, bail}; +use std::env; +use std::process::Command; + +use worktrunk::shell_exec; + +/// Check if tmux is available in PATH. +pub fn is_available() -> bool { + which::which("tmux").is_ok() +} + +/// Check if we're inside a tmux session. +pub fn is_inside_tmux() -> bool { + env::var("TMUX").is_ok() +} + +/// Sanitize branch name for tmux session/window name. +/// +/// Tmux doesn't allow certain characters in session/window names. +fn sanitize_name(branch: &str) -> String { + branch + .chars() + .map(|c| if matches!(c, '/' | '.' | ':') { '-' } else { c }) + .collect() +} + +/// Options for spawning a worktree switch in tmux. +pub struct TmuxSwitchOptions<'a> { + pub branch: &'a str, + pub create: bool, + pub base: Option<&'a str>, + pub execute: Option<&'a str>, + pub execute_args: &'a [String], + pub yes: bool, + pub clobber: bool, + pub verify: bool, + pub detach: bool, +} + +/// Check if a tmux session with the given name exists. +fn session_exists(name: &str) -> bool { + Command::new("tmux") + .args(["has-session", "-t", name]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} + +/// Check if a window with the given name exists in the current tmux session. +fn window_exists(name: &str) -> bool { + Command::new("tmux") + .args(["list-windows", "-F", "#{window_name}"]) + .output() + .map(|o| { + String::from_utf8_lossy(&o.stdout) + .lines() + .any(|line| line == name) + }) + .unwrap_or(false) +} + +/// Environment variables to skip when passing to tmux session. +const SKIP_ENV_VARS: &[&str] = &[ + "PWD", + "OLDPWD", + "_", + "SHLVL", + "TMUX", + "TMUX_PANE", + "TERM_SESSION_ID", + "SHELL_SESSION_ID", + "WORKTRUNK_DIRECTIVE_FILE", + "COMP_LINE", + "COMP_POINT", +]; + +/// Write environment variables to a temp file and return the path. +/// The file contains export statements that can be sourced. +fn write_env_file() -> Option { + use std::io::Write; + + let path = std::env::temp_dir().join(format!("wt-env-{}", std::process::id())); + let mut file = std::fs::File::create(&path).ok()?; + + for (key, value) in env::vars() { + if SKIP_ENV_VARS.contains(&key.as_str()) || key.starts_with("__") { + continue; + } + if let Ok(escaped) = shlex::try_quote(&value) { + writeln!(file, "export {}={}", key, escaped).ok()?; + } + } + + Some(path) +} + +/// Capture the visible output from a tmux pane, cleaning up whitespace. +fn capture_pane(session: &str) -> String { + Command::new("tmux") + .args(["capture-pane", "-t", session, "-p"]) + .output() + .map(|o| { + let raw = String::from_utf8_lossy(&o.stdout); + // Filter out blank lines and tmux's "Pane is dead" message + raw.lines() + .filter(|line| { + let trimmed = line.trim(); + !trimmed.is_empty() && !trimmed.starts_with("Pane is dead") + }) + .collect::>() + .join("\n") + }) + .unwrap_or_default() +} + +/// Result of spawning a tmux session/window. +pub enum TmuxSpawnResult { + /// Created detached session with this name + Detached(String), + /// Detached session failed quickly - includes captured output + DetachedFailed { name: String, output: String }, + /// Created new window (already in tmux) + Window(String), + /// Switched to existing window (was inside tmux) + SwitchedWindow(String), +} + +/// Spawn worktree switch in a new tmux window/session. +/// +/// # Returns +/// * `TmuxSpawnResult::Window` - If already in tmux, created new window +/// * `TmuxSpawnResult::Detached` - If not in tmux and detach=true +/// * Never returns if not in tmux and detach=false (exec replaces process) +#[cfg(unix)] +pub fn spawn_switch_in_tmux(opts: &TmuxSwitchOptions<'_>) -> Result { + use std::os::unix::process::CommandExt; + + if !is_available() { + bail!("tmux not found. Install tmux or run without --tmux"); + } + + let name = sanitize_name(opts.branch); + + // Build the wt command to run inside tmux + let mut wt_args = vec!["switch".to_string()]; + if opts.create { + wt_args.push("--create".to_string()); + } + wt_args.push(opts.branch.to_string()); + if let Some(b) = opts.base { + wt_args.push("--base".to_string()); + wt_args.push(b.to_string()); + } + if let Some(cmd) = opts.execute { + wt_args.push("--execute".to_string()); + wt_args.push(cmd.to_string()); + } + if opts.yes { + wt_args.push("--yes".to_string()); + } + if opts.clobber { + wt_args.push("--clobber".to_string()); + } + if !opts.verify { + wt_args.push("--no-verify".to_string()); + } + // Add execute_args after -- separator + if !opts.execute_args.is_empty() { + wt_args.push("--".to_string()); + wt_args.extend(opts.execute_args.iter().cloned()); + } + + // Shell-escape each argument + let escaped_args: Vec = wt_args + .iter() + .map(|arg| shlex::try_quote(arg).unwrap_or(arg.into()).into_owned()) + .collect(); + + // Write env vars to temp file for sourcing in tmux + let env_file = write_env_file(); + + // Command to run: source env file, then wt switch, then keep shell open + let wt_command = match &env_file { + Some(path) => format!( + "source {} && rm -f {} && wt {} && exec $SHELL", + path.display(), + path.display(), + escaped_args.join(" ") + ), + None => format!("wt {} && exec $SHELL", escaped_args.join(" ")), + }; + + if is_inside_tmux() { + // Already in tmux: check if window exists + if window_exists(&name) { + // Switch to existing window + let mut cmd = Command::new("tmux"); + cmd.args(["select-window", "-t", &name]); + shell_exec::run(&mut cmd, None)?; + Ok(TmuxSpawnResult::SwitchedWindow(name)) + } else { + // Create new window and switch to it + let mut cmd = Command::new("tmux"); + cmd.args(["new-window", "-n", &name, &wt_command]); + shell_exec::run(&mut cmd, None)?; + Ok(TmuxSpawnResult::Window(name)) + } + } else if session_exists(&name) { + // Session exists: attach to it (exec replaces process) + let mut cmd = Command::new("tmux"); + cmd.args(["attach-session", "-t", &name]); + let err = cmd.exec(); + Err(err.into()) + } else if opts.detach { + // Not in tmux, detach mode: create detached session + // Use remain-on-exit so we can capture output if command fails quickly + let mut cmd = Command::new("tmux"); + cmd.args([ + "new-session", + "-d", + "-s", + &name, + "-x", + "200", // Wide enough to not wrap output + "-y", + "50", + &wt_command, + ]); + shell_exec::run(&mut cmd, None)?; + + // Set remain-on-exit so pane stays if command fails + let mut cmd = Command::new("tmux"); + cmd.args(["set-option", "-t", &name, "remain-on-exit", "on"]); + let _ = cmd.output(); // Ignore errors + + // Wait briefly and check if the command is still running + std::thread::sleep(std::time::Duration::from_secs(2)); + + // Check if the pane is dead (command exited) + let pane_dead = Command::new("tmux") + .args(["list-panes", "-t", &name, "-F", "#{pane_dead}"]) + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).trim() == "1") + .unwrap_or(false); + + if pane_dead { + // Command exited quickly - likely an error + let output = capture_pane(&name); + + // Kill the dead session + let mut cmd = Command::new("tmux"); + cmd.args(["kill-session", "-t", &name]); + let _ = cmd.output(); + + Ok(TmuxSpawnResult::DetachedFailed { + name: name.clone(), + output, + }) + } else { + // Command still running - turn off remain-on-exit for normal behavior + let mut cmd = Command::new("tmux"); + cmd.args(["set-option", "-t", &name, "remain-on-exit", "off"]); + let _ = cmd.output(); + + Ok(TmuxSpawnResult::Detached(name)) + } + } else { + // Not in tmux, attach mode: exec into tmux (replaces process) + let mut cmd = Command::new("tmux"); + cmd.args(["new-session", "-s", &name, &wt_command]); + // exec() replaces the current process with tmux, so this only returns on error + let err = cmd.exec(); + Err(err.into()) + } +} + +/// Windows stub - tmux is not available on Windows. +#[cfg(not(unix))] +pub fn spawn_switch_in_tmux(_opts: &TmuxSwitchOptions<'_>) -> Result { + bail!("tmux is not available on Windows") +} diff --git a/src/main.rs b/src/main.rs index 54b1bd653..16b317966 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1265,144 +1265,213 @@ fn main() { yes, clobber, verify, - } => WorktrunkConfig::load() - .context("Failed to load config") - .and_then(|mut config| { - // "Approve at the Gate": collect and approve hooks upfront - // This ensures approval happens once at the command entry point - // If user declines, skip hooks but continue with worktree operation - let approved = if verify { - let repo = Repository::current(); - let repo_root = repo.worktree_base().context("Failed to switch worktree")?; - // Compute worktree path for template expansion in approval prompt - let worktree_path = compute_worktree_path(&repo, &branch, &config)?; - let ctx = CommandContext::new( - &repo, - &config, - Some(&branch), - &worktree_path, - &repo_root, - yes, - ); - // Approve different hooks based on whether we're creating or switching - if create { - approve_hooks( - &ctx, - &[ - HookType::PostCreate, - HookType::PostStart, - HookType::PostSwitch, - ], - )? - } else { - // When switching to existing, only post-switch needs approval - approve_hooks(&ctx, &[HookType::PostSwitch])? - } - } else { - true // --no-verify: skip all hooks - }; - - // Skip hooks if --no-verify or user declined approval - let skip_hooks = !verify || !approved; - - // Show message if user declined approval - if !approved { - crate::output::print(info_message(if create { - "Commands declined, continuing worktree creation" - } else { - "Commands declined" - }))?; - } - - // Execute switch operation (creates worktree, runs post-create hooks if approved) - let (result, branch_info) = handle_switch( - &branch, + tmux, + detach, + } => { + // Handle --tmux early: spawn in tmux and skip normal processing + if tmux { + use crate::commands::tmux::{self, TmuxSpawnResult, TmuxSwitchOptions}; + use worktrunk::styling::success_message; + + let opts = TmuxSwitchOptions { + branch: &branch, create, - base.as_deref(), + base: base.as_deref(), + execute: execute.as_deref(), + execute_args: &execute_args, yes, clobber, - skip_hooks, - &config, - )?; - - // Show success message (temporal locality: immediately after worktree operation) - // Returns path to display in hooks when user's shell won't be in the worktree - // Also shows worktree-path hint on first --create (before shell integration warning) - let hooks_display_path = - handle_switch_output(&result, &branch_info, execute.as_deref())?; - - // Offer shell integration if not already installed/active - // (only shows prompt/hint when shell integration isn't working) - // With --execute: show hints only (don't interrupt with prompt) - // Best-effort: don't fail switch if offer fails - if !output::is_shell_integration_active() { - let skip_prompt = execute.is_some(); - let _ = - output::prompt_shell_integration(&mut config, &binary_name(), skip_prompt); - } - - // Spawn background hooks after success message - // - post-switch: runs on ALL switches (shows "@ path" when shell won't be there) - // - post-start: runs only when creating a NEW worktree - if !skip_hooks { - let repo = Repository::current(); - let repo_root = repo.worktree_base().context("Failed to switch worktree")?; - let ctx = CommandContext::new( - &repo, - &config, - Some(&branch_info.branch), - result.path(), - &repo_root, - yes, - ); + verify, + detach, + }; + tmux::spawn_switch_in_tmux(&opts).and_then(|result| { + use worktrunk::styling::{error_message, format_with_gutter}; - // Build extra vars for base branch context - // "base" is the branch we branched from when creating a new worktree. - // For existing worktrees, there's no base concept. - let (base_branch, base_worktree_path): (Option<&str>, Option<&str>) = - match &result { - SwitchResult::Created { - base_branch, - base_worktree_path, - .. - } => (base_branch.as_deref(), base_worktree_path.as_deref()), - SwitchResult::Existing(_) | SwitchResult::AlreadyAt(_) => (None, None), - }; - let extra_vars: Vec<(&str, &str)> = [ - base_branch.map(|b| ("base", b)), - base_worktree_path.map(|p| ("base_worktree_path", p)), - ] - .into_iter() - .flatten() - .collect(); - - // Post-switch runs first (immediate "I'm here" signal) - ctx.spawn_post_switch_commands(&extra_vars, hooks_display_path.as_deref())?; - - // Post-start runs only on creation (setup tasks) - if matches!(&result, SwitchResult::Created { .. }) { - ctx.spawn_post_start_commands(&extra_vars, hooks_display_path.as_deref())?; + match result { + TmuxSpawnResult::Window(_name) => { + // Already in tmux, created new window - nothing to print, + // tmux new-window switches to the new window automatically + } + TmuxSpawnResult::SwitchedWindow(_name) => { + // Switched to existing window - nothing to print, + // tmux select-window switches automatically + } + TmuxSpawnResult::Detached(name) => { + output::print(success_message(cformat!( + "Worktree creation started in tmux session {name}" + )))?; + output::print(hint_message(cformat!( + "Run tmux attach -t {name} to view progress" + )))?; + } + TmuxSpawnResult::DetachedFailed { name, output } => { + output::print(error_message(cformat!( + "Worktree creation failed in tmux session {name}" + )))?; + if !output.is_empty() { + output::print(format_with_gutter(&output, None))?; + } + return Err(anyhow::anyhow!("tmux session exited with error")); + } } - } + Ok(()) + }) + } else { + WorktrunkConfig::load() + .context("Failed to load config") + .and_then(|mut config| { + // "Approve at the Gate": collect and approve hooks upfront + // This ensures approval happens once at the command entry point + // If user declines, skip hooks but continue with worktree operation + let approved = if verify { + let repo = Repository::current(); + let repo_root = + repo.worktree_base().context("Failed to switch worktree")?; + // Compute worktree path for template expansion in approval prompt + let worktree_path = compute_worktree_path(&repo, &branch, &config)?; + let ctx = CommandContext::new( + &repo, + &config, + Some(&branch), + &worktree_path, + &repo_root, + yes, + ); + // Approve different hooks based on whether we're creating or switching + if create { + approve_hooks( + &ctx, + &[ + HookType::PostCreate, + HookType::PostStart, + HookType::PostSwitch, + ], + )? + } else { + // When switching to existing, only post-switch needs approval + approve_hooks(&ctx, &[HookType::PostSwitch])? + } + } else { + true // --no-verify: skip all hooks + }; - // Execute user command after post-start hooks have been spawned - // Note: execute_args requires execute via clap's `requires` attribute - if let Some(cmd) = execute { - // Append any trailing args (after --) to the execute command - let full_cmd = if execute_args.is_empty() { - cmd - } else { - let escaped_args: Vec<_> = execute_args - .iter() - .map(|arg| shlex::try_quote(arg).unwrap_or(arg.into()).into_owned()) + // Skip hooks if --no-verify or user declined approval + let skip_hooks = !verify || !approved; + + // Show message if user declined approval + if !approved { + crate::output::print(info_message(if create { + "Commands declined, continuing worktree creation" + } else { + "Commands declined" + }))?; + } + + // Execute switch operation (creates worktree, runs post-create hooks if approved) + let (result, branch_info) = handle_switch( + &branch, + create, + base.as_deref(), + yes, + clobber, + skip_hooks, + &config, + )?; + + // Show success message (temporal locality: immediately after worktree operation) + // Returns path to display in hooks when user's shell won't be in the worktree + // Also shows worktree-path hint on first --create (before shell integration warning) + let hooks_display_path = + handle_switch_output(&result, &branch_info, execute.as_deref())?; + + // Offer shell integration if not already installed/active + // (only shows prompt/hint when shell integration isn't working) + // With --execute: show hints only (don't interrupt with prompt) + // Best-effort: don't fail switch if offer fails + if !output::is_shell_integration_active() { + let skip_prompt = execute.is_some(); + let _ = output::prompt_shell_integration( + &mut config, + &binary_name(), + skip_prompt, + ); + } + + // Spawn background hooks after success message + // - post-switch: runs on ALL switches (shows "@ path" when shell won't be there) + // - post-start: runs only when creating a NEW worktree + if !skip_hooks { + let repo = Repository::current(); + let repo_root = + repo.worktree_base().context("Failed to switch worktree")?; + let ctx = CommandContext::new( + &repo, + &config, + Some(&branch_info.branch), + result.path(), + &repo_root, + yes, + ); + + // Build extra vars for base branch context + // "base" is the branch we branched from when creating a new worktree. + // For existing worktrees, there's no base concept. + let (base_branch, base_worktree_path): (Option<&str>, Option<&str>) = + match &result { + SwitchResult::Created { + base_branch, + base_worktree_path, + .. + } => (base_branch.as_deref(), base_worktree_path.as_deref()), + SwitchResult::Existing(_) | SwitchResult::AlreadyAt(_) => { + (None, None) + } + }; + let extra_vars: Vec<(&str, &str)> = [ + base_branch.map(|b| ("base", b)), + base_worktree_path.map(|p| ("base_worktree_path", p)), + ] + .into_iter() + .flatten() .collect(); - format!("{} {}", cmd, escaped_args.join(" ")) - }; - execute_user_command(&full_cmd)?; - } - Ok(()) - }), + // Post-switch runs first (immediate "I'm here" signal) + ctx.spawn_post_switch_commands( + &extra_vars, + hooks_display_path.as_deref(), + )?; + + // Post-start runs only on creation (setup tasks) + if matches!(&result, SwitchResult::Created { .. }) { + ctx.spawn_post_start_commands( + &extra_vars, + hooks_display_path.as_deref(), + )?; + } + } + + // Execute user command after post-start hooks have been spawned + // Note: execute_args requires execute via clap's `requires` attribute + if let Some(cmd) = execute { + // Append any trailing args (after --) to the execute command + let full_cmd = if execute_args.is_empty() { + cmd + } else { + let escaped_args: Vec<_> = execute_args + .iter() + .map(|arg| { + shlex::try_quote(arg).unwrap_or(arg.into()).into_owned() + }) + .collect(); + format!("{} {}", cmd, escaped_args.join(" ")) + }; + execute_user_command(&full_cmd)?; + } + + Ok(()) + }) + } + } Commands::Remove { branches, delete_branch, diff --git a/tests/integration_tests/switch.rs b/tests/integration_tests/switch.rs index 7d7b65361..298795f72 100644 --- a/tests/integration_tests/switch.rs +++ b/tests/integration_tests/switch.rs @@ -1046,3 +1046,23 @@ fn test_switch_create_no_hint_with_custom_worktree_path(repo: TestRepo) { "Hint should be suppressed when user has custom worktree-path config" ); } + +// --tmux flag tests + +#[rstest] +fn test_switch_detach_requires_tmux(repo: TestRepo) { + // --detach requires --tmux + let output = repo + .wt_command() + .args(["switch", "--create", "feature", "--detach"]) + .output() + .unwrap(); + + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("--tmux") || stderr.contains("required"), + "Should indicate --tmux is required, got: {}", + stderr + ); +} diff --git a/tests/snapshots/integration__integration_tests__help__help_page_switch.snap b/tests/snapshots/integration__integration_tests__help__help_page_switch.snap index 594e3a17d..b79f50cf0 100644 --- a/tests/snapshots/integration__integration_tests__help__help_page_switch.snap +++ b/tests/snapshots/integration__integration_tests__help__help_page_switch.snap @@ -144,6 +144,20 @@ Usage: wt switch [OPTIONS]  [-- --no-verify Skip hooks + -t, --tmux + Run in new tmux window/session +  + Creates the worktree in a new tmux window (if already in tmux) or + session (if not). All hooks run there instead of the current terminal. + By default, the session/window is attached. Use --detach for + background. + + -d, --detach + Run tmux session in background +  + Only valid with --tmux. Creates a detached session instead of + attaching to it. + -h, --help Print help (see a summary with '-h') diff --git a/tests/snapshots/integration__integration_tests__help__help_switch_long.snap b/tests/snapshots/integration__integration_tests__help__help_switch_long.snap index 5d82241c4..e70fe0333 100644 --- a/tests/snapshots/integration__integration_tests__help__help_switch_long.snap +++ b/tests/snapshots/integration__integration_tests__help__help_switch_long.snap @@ -67,6 +67,17 @@ Usage: wt switch [OPTIONS]  [-- --no-verify Skip hooks + -t, --tmux + Run in new tmux window/session + + Creates the worktree in a new tmux window (if already in tmux) or session (if not). All hooks run there instead of the current terminal. By + default, the session/window is attached. Use --detach for background. + + -d, --detach + Run tmux session in background + + Only valid with --tmux. Creates a detached session instead of attaching to it. + -h, --help Print help (see a summary with '-h') diff --git a/tests/snapshots/integration__integration_tests__help__help_switch_short.snap b/tests/snapshots/integration__integration_tests__help__help_switch_short.snap index 56611ed94..156801c7b 100644 --- a/tests/snapshots/integration__integration_tests__help__help_switch_short.snap +++ b/tests/snapshots/integration__integration_tests__help__help_switch_short.snap @@ -35,6 +35,8 @@ Usage: wt switch [OPTIONS]  [-- -y, --yes Skip approval prompts --clobber Remove stale paths at target --no-verify Skip hooks + -t, --tmux Run in new tmux window/session + -d, --detach Run tmux session in background -h, --help Print help (see more with '--help') Global Options: