mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-29 02:01:50 +08:00
feat(terminal): add Otty support with tab-aware launching (#6620)
This commit is contained in:
@@ -106,7 +106,7 @@ Supported terminals (by platform):
|
||||
|
||||
| Platform | Terminal Options |
|
||||
|----------|-----------------|
|
||||
| macOS | Terminal, iTerm2, Alacritty, Kitty, Ghostty, WezTerm |
|
||||
| macOS | Terminal, iTerm2, Alacritty, Kitty, Ghostty, Otty, WezTerm, Kaku, Warp |
|
||||
| Windows | CMD, PowerShell, Windows Terminal |
|
||||
| Linux | GNOME Terminal, Konsole, Xfce4 Terminal, Alacritty, Kitty, Ghostty |
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ Click the **Resume** button (play icon) on a selected session to continue the co
|
||||
- The terminal opens in the session's project directory
|
||||
- If terminal launch fails, the command is copied to your clipboard instead
|
||||
|
||||
**Supported terminals (macOS):** Terminal.app, iTerm2, Ghostty, Kitty, WezTerm, Alacritty, Warp
|
||||
**Supported terminals (macOS):** Terminal.app, iTerm2, Ghostty, Otty, Kitty, WezTerm, Kaku, Alacritty, Warp
|
||||
|
||||
**On other platforms:**
|
||||
- The resume command is copied to your clipboard
|
||||
|
||||
@@ -106,7 +106,7 @@ CC Switch がターミナルを開く際に使用するターミナルアプリ
|
||||
|
||||
| プラットフォーム | ターミナル選択肢 |
|
||||
| ------- | ------------------------------------------------------------------ |
|
||||
| macOS | Terminal、iTerm2、Alacritty、Kitty、Ghostty、WezTerm |
|
||||
| macOS | Terminal、iTerm2、Alacritty、Kitty、Ghostty、Otty、WezTerm、Kaku、Warp |
|
||||
| Windows | CMD、PowerShell、Windows Terminal |
|
||||
| Linux | GNOME Terminal、Konsole、Xfce4 Terminal、Alacritty、Kitty、Ghostty |
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
- ターミナルはセッションのプロジェクトディレクトリで開きます
|
||||
- ターミナルの起動に失敗した場合、コマンドがクリップボードにコピーされます
|
||||
|
||||
**対応ターミナル(macOS):** Terminal.app、iTerm2、Ghostty、Kitty、WezTerm、Alacritty、Warp
|
||||
**対応ターミナル(macOS):** Terminal.app、iTerm2、Ghostty、Otty、Kitty、WezTerm、Kaku、Alacritty、Warp
|
||||
|
||||
**その他のプラットフォーム:**
|
||||
- 再開コマンドがクリップボードにコピーされます
|
||||
|
||||
@@ -106,7 +106,7 @@ v3.13.0 起新增「轻量模式」——一种**仅托盘运行**的状态,
|
||||
|
||||
| 平台 | 终端选项 |
|
||||
| ------- | ------------------------------------------------------------------ |
|
||||
| macOS | Terminal、iTerm2、Alacritty、Kitty、Ghostty、WezTerm |
|
||||
| macOS | Terminal、iTerm2、Alacritty、Kitty、Ghostty、Otty、WezTerm、Kaku、Warp |
|
||||
| Windows | CMD、PowerShell、Windows Terminal |
|
||||
| Linux | GNOME Terminal、Konsole、Xfce4 Terminal、Alacritty、Kitty、Ghostty |
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
- 终端会在会话的项目目录中打开
|
||||
- 如果终端启动失败,命令会被复制到剪贴板
|
||||
|
||||
**支持的终端(macOS)**:Terminal.app、iTerm2、Ghostty、Kitty、WezTerm、Alacritty、Warp
|
||||
**支持的终端(macOS)**:Terminal.app、iTerm2、Ghostty、Otty、Kitty、WezTerm、Kaku、Alacritty、Warp
|
||||
|
||||
**其他平台**:
|
||||
- 恢复命令会被复制到剪贴板
|
||||
|
||||
@@ -3899,6 +3899,7 @@ echo "{config_path}"
|
||||
"alacritty" => launch_macos_open_app("Alacritty", &script_file, true),
|
||||
"kitty" => launch_macos_open_app("kitty", &script_file, false),
|
||||
"ghostty" => launch_macos_ghostty(&script_file),
|
||||
"otty" => launch_macos_otty(&script_file),
|
||||
"wezterm" => launch_macos_open_app("WezTerm", &script_file, true),
|
||||
"kaku" => launch_macos_open_app("Kaku", &script_file, true),
|
||||
_ => launch_macos_terminal_app(&script_file),
|
||||
@@ -3995,6 +3996,77 @@ fn launch_macos_terminal_app(script_file: &std::path::Path) -> Result<(), String
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn launch_macos_otty(script_file: &std::path::Path) -> Result<(), String> {
|
||||
use std::process::Command;
|
||||
|
||||
let otty_cli = find_macos_otty_cli().ok_or_else(|| {
|
||||
"未找到 Otty CLI。请将 Otty 安装到 /Applications 或 ~/Applications。".to_string()
|
||||
})?;
|
||||
|
||||
let command = build_macos_dash_c_command(script_file);
|
||||
let tab_result = Command::new(&otty_cli)
|
||||
.args(["tab", "new", "--window", "0", "--command", &command])
|
||||
.output()
|
||||
.map_err(|e| format!("启动 Otty CLI 失败: {e}"))?;
|
||||
|
||||
if tab_result.status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"Otty 新建 Tab 失败,改为新建窗口: {}",
|
||||
decode_command_output(&tab_result.stderr)
|
||||
);
|
||||
|
||||
let window_result = Command::new(&otty_cli)
|
||||
.args(["open", "--command", &command])
|
||||
.output()
|
||||
.map_err(|e| format!("启动 Otty CLI 失败: {e}"))?;
|
||||
|
||||
if window_result.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"Otty 新建窗口失败 (exit code: {:?}): {}",
|
||||
window_result.status.code(),
|
||||
decode_command_output(&window_result.stderr)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn find_macos_otty_cli() -> Option<std::path::PathBuf> {
|
||||
macos_otty_cli_candidates()
|
||||
.into_iter()
|
||||
.find(|path| path.is_file() && is_executable_file(path))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn macos_otty_cli_candidates() -> Vec<std::path::PathBuf> {
|
||||
let mut candidates = vec![std::path::PathBuf::from(
|
||||
"/Applications/Otty.app/Contents/MacOS/otty-cli",
|
||||
)];
|
||||
|
||||
if let Some(home) = std::env::var_os("HOME") {
|
||||
candidates.push(
|
||||
std::path::PathBuf::from(home).join("Applications/Otty.app/Contents/MacOS/otty-cli"),
|
||||
);
|
||||
}
|
||||
|
||||
candidates.push(std::path::PathBuf::from("/usr/local/bin/otty"));
|
||||
candidates.push(std::path::PathBuf::from("/opt/homebrew/bin/otty"));
|
||||
|
||||
if let Some(path) = std::env::var_os("PATH") {
|
||||
for directory in std::env::split_paths(&path) {
|
||||
candidates.push(directory.join("otty"));
|
||||
candidates.push(directory.join("otty-cli"));
|
||||
}
|
||||
}
|
||||
|
||||
candidates
|
||||
}
|
||||
|
||||
/// macOS: iTerm2
|
||||
#[cfg(target_os = "macos")]
|
||||
fn build_macos_iterm2_applescript(script_file: &std::path::Path) -> String {
|
||||
@@ -4459,6 +4531,7 @@ read -r _
|
||||
"alacritty" => launch_macos_open_app("Alacritty", &script_file, true),
|
||||
"kitty" => launch_macos_open_app("kitty", &script_file, false),
|
||||
"ghostty" => launch_macos_ghostty(&script_file),
|
||||
"otty" => launch_macos_otty(&script_file),
|
||||
"wezterm" => launch_macos_open_app("WezTerm", &script_file, true),
|
||||
"kaku" => launch_macos_open_app("Kaku", &script_file, true),
|
||||
_ => launch_macos_terminal_app(&script_file),
|
||||
@@ -6830,6 +6903,27 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn otty_launcher_command_executes_the_temporary_script() {
|
||||
assert_eq!(
|
||||
build_macos_dash_c_command(Path::new("/tmp/cc_switch_launcher.sh")),
|
||||
"exec sh '/tmp/cc_switch_launcher.sh'"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn otty_cli_candidates_include_bundle_and_installed_cli_locations() {
|
||||
let candidates = macos_otty_cli_candidates();
|
||||
|
||||
assert!(candidates.contains(&PathBuf::from(
|
||||
"/Applications/Otty.app/Contents/MacOS/otty-cli"
|
||||
)));
|
||||
assert!(candidates.contains(&PathBuf::from("/usr/local/bin/otty")));
|
||||
assert!(candidates.contains(&PathBuf::from("/opt/homebrew/bin/otty")));
|
||||
}
|
||||
|
||||
/// Restored windows should not receive the launcher command.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
|
||||
@@ -19,6 +19,8 @@ pub fn launch_terminal(
|
||||
"iTerm" | "iterm" => launch_iterm(command, cwd),
|
||||
"ghostty" => launch_ghostty(command, cwd),
|
||||
"kitty" => launch_kitty(command, cwd),
|
||||
#[cfg(target_os = "macos")]
|
||||
"otty" => launch_otty(command, cwd),
|
||||
"wezterm" => launch_wezterm(command, cwd),
|
||||
"kaku" => launch_kaku(command, cwd),
|
||||
"alacritty" => launch_alacritty(command, cwd),
|
||||
@@ -52,6 +54,105 @@ end tell"#
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn build_otty_tab_args(command: &str, cwd: Option<&str>) -> Vec<String> {
|
||||
let mut args = vec![
|
||||
"tab".to_string(),
|
||||
"new".to_string(),
|
||||
"--window".to_string(),
|
||||
"0".to_string(),
|
||||
];
|
||||
if let Some(cwd) = cwd.filter(|value| !value.trim().is_empty()) {
|
||||
args.push("--cwd".to_string());
|
||||
args.push(cwd.to_string());
|
||||
}
|
||||
args.push("--command".to_string());
|
||||
args.push(command.to_string());
|
||||
args
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn build_otty_window_args(command: &str, cwd: Option<&str>) -> Vec<String> {
|
||||
let mut args = vec![
|
||||
"open".to_string(),
|
||||
"--command".to_string(),
|
||||
command.to_string(),
|
||||
];
|
||||
if let Some(cwd) = cwd.filter(|value| !value.trim().is_empty()) {
|
||||
args.push(cwd.to_string());
|
||||
}
|
||||
args
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn launch_otty(command: &str, cwd: Option<&str>) -> Result<(), String> {
|
||||
let otty_cli = find_otty_cli().ok_or_else(|| {
|
||||
"Otty CLI not found. Install Otty to /Applications or ~/Applications.".to_string()
|
||||
})?;
|
||||
|
||||
let tab_result = Command::new(&otty_cli)
|
||||
.args(build_otty_tab_args(command, cwd))
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to launch Otty CLI: {e}"))?;
|
||||
if tab_result.status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let window_result = Command::new(&otty_cli)
|
||||
.args(build_otty_window_args(command, cwd))
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to launch Otty CLI: {e}"))?;
|
||||
if window_result.status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"Failed to launch Otty: {}",
|
||||
String::from_utf8_lossy(&window_result.stderr).trim()
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn find_otty_cli() -> Option<std::path::PathBuf> {
|
||||
otty_cli_candidates()
|
||||
.into_iter()
|
||||
.find(|path| path.is_file() && is_executable_file(path))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn otty_cli_candidates() -> Vec<std::path::PathBuf> {
|
||||
let mut candidates = vec![std::path::PathBuf::from(
|
||||
"/Applications/Otty.app/Contents/MacOS/otty-cli",
|
||||
)];
|
||||
|
||||
if let Some(home) = std::env::var_os("HOME") {
|
||||
candidates.push(
|
||||
std::path::PathBuf::from(home).join("Applications/Otty.app/Contents/MacOS/otty-cli"),
|
||||
);
|
||||
}
|
||||
|
||||
candidates.push(std::path::PathBuf::from("/usr/local/bin/otty"));
|
||||
candidates.push(std::path::PathBuf::from("/opt/homebrew/bin/otty"));
|
||||
|
||||
if let Some(path) = std::env::var_os("PATH") {
|
||||
for directory in std::env::split_paths(&path) {
|
||||
candidates.push(directory.join("otty"));
|
||||
candidates.push(directory.join("otty-cli"));
|
||||
}
|
||||
}
|
||||
|
||||
candidates
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn is_executable_file(path: &std::path::Path) -> bool {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
path.metadata()
|
||||
.map(|metadata| metadata.permissions().mode() & 0o111 != 0)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn launch_iterm(command: &str, cwd: Option<&str>) -> Result<(), String> {
|
||||
let full_command = build_shell_command(command, cwd);
|
||||
let escaped = escape_osascript(&full_command);
|
||||
@@ -353,6 +454,50 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn otty_launches_a_new_tab_without_injecting_into_the_current_session() {
|
||||
assert_eq!(
|
||||
build_otty_tab_args("claude --resume abc-123", Some("/tmp/project-$(id -un)")),
|
||||
vec![
|
||||
"tab",
|
||||
"new",
|
||||
"--window",
|
||||
"0",
|
||||
"--cwd",
|
||||
"/tmp/project-$(id -un)",
|
||||
"--command",
|
||||
"claude --resume abc-123",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn otty_falls_back_to_a_new_window_with_the_same_command_and_cwd() {
|
||||
assert_eq!(
|
||||
build_otty_window_args("claude --resume abc-123", Some("/tmp/project dir")),
|
||||
vec![
|
||||
"open",
|
||||
"--command",
|
||||
"claude --resume abc-123",
|
||||
"/tmp/project dir",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn otty_cli_candidates_include_bundle_and_installed_cli_locations() {
|
||||
let candidates = otty_cli_candidates();
|
||||
|
||||
assert!(candidates.contains(&std::path::PathBuf::from(
|
||||
"/Applications/Otty.app/Contents/MacOS/otty-cli"
|
||||
)));
|
||||
assert!(candidates.contains(&std::path::PathBuf::from("/usr/local/bin/otty")));
|
||||
assert!(candidates.contains(&std::path::PathBuf::from("/opt/homebrew/bin/otty")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wezterm_compatible_terminals_use_start_and_cwd_arguments() {
|
||||
let args = build_wezterm_compatible_args_with_shell(
|
||||
|
||||
@@ -485,7 +485,7 @@ pub struct AppSettings {
|
||||
|
||||
// ===== 终端设置 =====
|
||||
/// 首选终端应用(可选,默认使用系统默认终端)
|
||||
/// - macOS: "terminal" | "iterm2" | "warp" | "alacritty" | "kitty" | "ghostty" | "wezterm" | "kaku"
|
||||
/// - macOS: "terminal" | "iterm2" | "warp" | "alacritty" | "kitty" | "ghostty" | "otty" | "wezterm" | "kaku"
|
||||
/// - Windows: "cmd" | "powershell" | "wt" (Windows Terminal)
|
||||
/// - Linux: "gnome-terminal" | "konsole" | "xfce4-terminal" | "alacritty" | "kitty" | "ghostty"
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
||||
@@ -15,6 +15,7 @@ const MACOS_TERMINALS = [
|
||||
{ value: "alacritty", labelKey: "settings.terminal.options.macos.alacritty" },
|
||||
{ value: "kitty", labelKey: "settings.terminal.options.macos.kitty" },
|
||||
{ value: "ghostty", labelKey: "settings.terminal.options.macos.ghostty" },
|
||||
{ value: "otty", labelKey: "settings.terminal.options.macos.otty" },
|
||||
{ value: "wezterm", labelKey: "settings.terminal.options.macos.wezterm" },
|
||||
{ value: "kaku", labelKey: "settings.terminal.options.macos.kaku" },
|
||||
{ value: "warp", labelKey: "settings.terminal.options.macos.warp" },
|
||||
|
||||
@@ -751,6 +751,7 @@
|
||||
"alacritty": "Alacritty",
|
||||
"kitty": "Kitty",
|
||||
"ghostty": "Ghostty",
|
||||
"otty": "Otty",
|
||||
"wezterm": "WezTerm",
|
||||
"kaku": "Kaku",
|
||||
"warp": "Warp"
|
||||
|
||||
@@ -751,6 +751,7 @@
|
||||
"alacritty": "Alacritty",
|
||||
"kitty": "Kitty",
|
||||
"ghostty": "Ghostty",
|
||||
"otty": "Otty",
|
||||
"wezterm": "WezTerm",
|
||||
"kaku": "Kaku",
|
||||
"warp": "Warp"
|
||||
|
||||
@@ -751,6 +751,7 @@
|
||||
"alacritty": "Alacritty",
|
||||
"kitty": "Kitty",
|
||||
"ghostty": "Ghostty",
|
||||
"otty": "Otty",
|
||||
"wezterm": "WezTerm",
|
||||
"kaku": "Kaku",
|
||||
"warp": "Warp"
|
||||
|
||||
@@ -751,6 +751,7 @@
|
||||
"alacritty": "Alacritty",
|
||||
"kitty": "Kitty",
|
||||
"ghostty": "Ghostty",
|
||||
"otty": "Otty",
|
||||
"wezterm": "WezTerm",
|
||||
"kaku": "Kaku",
|
||||
"warp": "Warp"
|
||||
|
||||
+1
-1
@@ -450,7 +450,7 @@ export interface Settings {
|
||||
|
||||
// ===== 终端设置 =====
|
||||
// 首选终端应用(可选,默认使用系统默认终端)
|
||||
// macOS: "terminal" | "iterm2" | "warp" | "alacritty" | "kitty" | "ghostty" | "wezterm" | "kaku"
|
||||
// macOS: "terminal" | "iterm2" | "warp" | "alacritty" | "kitty" | "ghostty" | "otty" | "wezterm" | "kaku"
|
||||
// Windows: "cmd" | "powershell" | "wt"
|
||||
// Linux: "gnome-terminal" | "konsole" | "xfce4-terminal" | "alacritty" | "kitty" | "ghostty"
|
||||
preferredTerminal?: string;
|
||||
|
||||
Reference in New Issue
Block a user