mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
2c9252dec5
* fix: launch Ghostty via shell command Use Ghostty's shell execution path instead of injecting raw terminal input so Claude resume commands run reliably when opening a session terminal. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ghostty): pass cwd via --working-directory instead of shell string Use Ghostty's native --working-directory flag to set the working directory, matching the pattern used by Alacritty. This avoids shell expansion of special characters (e.g. $VAR, spaces) in project paths. The command is now passed directly to -c without a cd prefix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
339 lines
9.6 KiB
Rust
339 lines
9.6 KiB
Rust
use std::process::Command;
|
|
|
|
pub fn launch_terminal(
|
|
target: &str,
|
|
command: &str,
|
|
cwd: Option<&str>,
|
|
custom_config: Option<&str>,
|
|
) -> Result<(), String> {
|
|
if command.trim().is_empty() {
|
|
return Err("Resume command is empty".to_string());
|
|
}
|
|
|
|
if !cfg!(target_os = "macos") {
|
|
return Err("Terminal resume is only supported on macOS".to_string());
|
|
}
|
|
|
|
match target {
|
|
"terminal" => launch_macos_terminal(command, cwd),
|
|
"iTerm" | "iterm" => launch_iterm(command, cwd),
|
|
"ghostty" => launch_ghostty(command, cwd),
|
|
"kitty" => launch_kitty(command, cwd),
|
|
"wezterm" => launch_wezterm(command, cwd),
|
|
"kaku" => launch_kaku(command, cwd),
|
|
"alacritty" => launch_alacritty(command, cwd),
|
|
"custom" => launch_custom(command, cwd, custom_config),
|
|
_ => Err(format!("Unsupported terminal target: {target}")),
|
|
}
|
|
}
|
|
|
|
fn launch_macos_terminal(command: &str, cwd: Option<&str>) -> Result<(), String> {
|
|
let full_command = build_shell_command(command, cwd);
|
|
let escaped = escape_osascript(&full_command);
|
|
let script = format!(
|
|
r#"tell application "Terminal"
|
|
activate
|
|
do script "{escaped}"
|
|
end tell"#
|
|
);
|
|
|
|
let status = Command::new("osascript")
|
|
.arg("-e")
|
|
.arg(script)
|
|
.status()
|
|
.map_err(|e| format!("Failed to launch Terminal: {e}"))?;
|
|
|
|
if status.success() {
|
|
Ok(())
|
|
} else {
|
|
Err("Terminal command execution failed".to_string())
|
|
}
|
|
}
|
|
|
|
fn launch_iterm(command: &str, cwd: Option<&str>) -> Result<(), String> {
|
|
let full_command = build_shell_command(command, cwd);
|
|
let escaped = escape_osascript(&full_command);
|
|
// iTerm2 AppleScript to create a new window and execute command
|
|
let script = format!(
|
|
r#"tell application "iTerm"
|
|
activate
|
|
create window with default profile
|
|
tell current session of current window
|
|
write text "{escaped}"
|
|
end tell
|
|
end tell"#
|
|
);
|
|
|
|
let status = Command::new("osascript")
|
|
.arg("-e")
|
|
.arg(script)
|
|
.status()
|
|
.map_err(|e| format!("Failed to launch iTerm: {e}"))?;
|
|
|
|
if status.success() {
|
|
Ok(())
|
|
} else {
|
|
Err("iTerm command execution failed".to_string())
|
|
}
|
|
}
|
|
|
|
fn launch_ghostty(command: &str, cwd: Option<&str>) -> Result<(), String> {
|
|
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
|
|
|
|
let mut args = vec![
|
|
"-na".to_string(),
|
|
"Ghostty".to_string(),
|
|
"--args".to_string(),
|
|
"--quit-after-last-window-closed=true".to_string(),
|
|
];
|
|
|
|
if let Some(dir) = cwd {
|
|
if !dir.trim().is_empty() {
|
|
args.push(format!("--working-directory={dir}"));
|
|
}
|
|
}
|
|
|
|
args.push("-e".to_string());
|
|
args.push(shell);
|
|
args.push("-l".to_string());
|
|
args.push("-c".to_string());
|
|
args.push(command.to_string());
|
|
|
|
let status = Command::new("open")
|
|
.args(&args)
|
|
.status()
|
|
.map_err(|e| format!("Failed to launch Ghostty: {e}"))?;
|
|
|
|
if status.success() {
|
|
Ok(())
|
|
} else {
|
|
Err("Failed to launch Ghostty. Make sure it is installed.".to_string())
|
|
}
|
|
}
|
|
|
|
fn launch_kitty(command: &str, cwd: Option<&str>) -> Result<(), String> {
|
|
let full_command = build_shell_command(command, cwd);
|
|
|
|
// 获取用户默认 shell
|
|
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
|
|
|
|
let status = Command::new("open")
|
|
.arg("-na")
|
|
.arg("kitty")
|
|
.arg("--args")
|
|
.arg("-e")
|
|
.arg(&shell)
|
|
.arg("-l")
|
|
.arg("-c")
|
|
.arg(&full_command)
|
|
.status()
|
|
.map_err(|e| format!("Failed to launch Kitty: {e}"))?;
|
|
|
|
if status.success() {
|
|
Ok(())
|
|
} else {
|
|
Err("Failed to launch Kitty. Make sure it is installed.".to_string())
|
|
}
|
|
}
|
|
|
|
fn launch_wezterm(command: &str, cwd: Option<&str>) -> Result<(), String> {
|
|
// wezterm start --cwd ... -- command
|
|
// To invoke via `open`, we use `open -na "WezTerm" --args start ...`
|
|
let args = build_wezterm_compatible_args("WezTerm", command, cwd);
|
|
|
|
let status = Command::new("open")
|
|
.args(args.iter().map(String::as_str))
|
|
.status()
|
|
.map_err(|e| format!("Failed to launch WezTerm: {e}"))?;
|
|
|
|
if status.success() {
|
|
Ok(())
|
|
} else {
|
|
Err("Failed to launch WezTerm.".to_string())
|
|
}
|
|
}
|
|
|
|
fn launch_kaku(command: &str, cwd: Option<&str>) -> Result<(), String> {
|
|
// Kaku is a WezTerm-derived terminal and keeps a compatible `start` entrypoint.
|
|
let args = build_wezterm_compatible_args("Kaku", command, cwd);
|
|
|
|
let status = Command::new("open")
|
|
.args(args.iter().map(String::as_str))
|
|
.status()
|
|
.map_err(|e| format!("Failed to launch Kaku: {e}"))?;
|
|
|
|
if status.success() {
|
|
Ok(())
|
|
} else {
|
|
Err("Failed to launch Kaku.".to_string())
|
|
}
|
|
}
|
|
|
|
fn build_wezterm_compatible_args(app_name: &str, command: &str, cwd: Option<&str>) -> Vec<String> {
|
|
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
|
|
build_wezterm_compatible_args_with_shell(app_name, command, cwd, &shell)
|
|
}
|
|
|
|
fn build_wezterm_compatible_args_with_shell(
|
|
app_name: &str,
|
|
command: &str,
|
|
cwd: Option<&str>,
|
|
shell: &str,
|
|
) -> Vec<String> {
|
|
let full_command = build_shell_command(command, None);
|
|
let mut args = vec![
|
|
"-na".to_string(),
|
|
app_name.to_string(),
|
|
"--args".to_string(),
|
|
"start".to_string(),
|
|
];
|
|
|
|
if let Some(dir) = cwd {
|
|
args.push("--cwd".to_string());
|
|
args.push(dir.to_string());
|
|
}
|
|
|
|
// Invoke shell to run the command string (to handle pipes, etc)
|
|
args.push("--".to_string());
|
|
args.push(shell.to_string());
|
|
args.push("-c".to_string());
|
|
args.push(full_command);
|
|
args
|
|
}
|
|
|
|
fn launch_alacritty(command: &str, cwd: Option<&str>) -> Result<(), String> {
|
|
// Alacritty: open -na Alacritty --args --working-directory ... -e shell -c command
|
|
let full_command = build_shell_command(command, None);
|
|
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
|
|
|
|
let mut args = vec!["-na", "Alacritty", "--args"];
|
|
|
|
if let Some(dir) = cwd {
|
|
args.push("--working-directory");
|
|
args.push(dir);
|
|
}
|
|
|
|
args.push("-e");
|
|
args.push(&shell);
|
|
args.push("-c");
|
|
args.push(&full_command);
|
|
|
|
let status = Command::new("open")
|
|
.args(&args)
|
|
.status()
|
|
.map_err(|e| format!("Failed to launch Alacritty: {e}"))?;
|
|
|
|
if status.success() {
|
|
Ok(())
|
|
} else {
|
|
Err("Failed to launch Alacritty.".to_string())
|
|
}
|
|
}
|
|
|
|
fn launch_custom(
|
|
command: &str,
|
|
cwd: Option<&str>,
|
|
custom_config: Option<&str>,
|
|
) -> Result<(), String> {
|
|
let template = custom_config.ok_or("No custom terminal config provided")?;
|
|
|
|
if template.trim().is_empty() {
|
|
return Err("Custom terminal command template is empty".to_string());
|
|
}
|
|
|
|
let cmd_str = command;
|
|
let dir_str = cwd.unwrap_or(".");
|
|
|
|
let final_cmd_line = template
|
|
.replace("{command}", cmd_str)
|
|
.replace("{cwd}", dir_str);
|
|
|
|
// Execute via sh -c
|
|
let status = Command::new("sh")
|
|
.arg("-c")
|
|
.arg(&final_cmd_line)
|
|
.status()
|
|
.map_err(|e| format!("Failed to execute custom terminal launcher: {e}"))?;
|
|
|
|
if status.success() {
|
|
Ok(())
|
|
} else {
|
|
Err("Custom terminal execution returned error code".to_string())
|
|
}
|
|
}
|
|
|
|
fn build_shell_command(command: &str, cwd: Option<&str>) -> String {
|
|
match cwd {
|
|
Some(dir) if !dir.trim().is_empty() => {
|
|
format!("cd {} && {}", shell_escape(dir), command)
|
|
}
|
|
_ => command.to_string(),
|
|
}
|
|
}
|
|
|
|
fn shell_escape(value: &str) -> String {
|
|
let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
|
|
format!("\"{escaped}\"")
|
|
}
|
|
|
|
fn escape_osascript(value: &str) -> String {
|
|
value.replace('\\', "\\\\").replace('"', "\\\"")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn build_shell_command_keeps_command_without_cwd_prefix_when_not_provided() {
|
|
assert_eq!(
|
|
build_shell_command("claude --resume abc-123", None),
|
|
"claude --resume abc-123"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn wezterm_compatible_terminals_use_start_and_cwd_arguments() {
|
|
let args = build_wezterm_compatible_args_with_shell(
|
|
"Kaku",
|
|
"claude --resume abc-123",
|
|
Some("/tmp/project dir"),
|
|
"/bin/zsh",
|
|
);
|
|
|
|
assert_eq!(
|
|
args,
|
|
vec![
|
|
"-na".to_string(),
|
|
"Kaku".to_string(),
|
|
"--args".to_string(),
|
|
"start".to_string(),
|
|
"--cwd".to_string(),
|
|
"/tmp/project dir".to_string(),
|
|
"--".to_string(),
|
|
"/bin/zsh".to_string(),
|
|
"-c".to_string(),
|
|
"claude --resume abc-123".to_string(),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ghostty_uses_working_directory_arg_for_cwd() {
|
|
// cwd should be passed as --working-directory, not embedded in the shell command string
|
|
// This avoids shell expansion of special characters in directory paths
|
|
let cwd = "/tmp/project dir";
|
|
let command = "claude --resume abc-123";
|
|
|
|
// Verify build_shell_command does NOT include cwd when used in ghostty context
|
|
// (ghostty passes cwd via --working-directory flag instead)
|
|
assert_eq!(
|
|
build_shell_command(command, None),
|
|
"claude --resume abc-123"
|
|
);
|
|
|
|
// Verify shell_escape works correctly for paths with spaces
|
|
assert_eq!(shell_escape(cwd), "\"/tmp/project dir\"");
|
|
}
|
|
}
|