mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-04-11 05:46:01 +08:00
feat(terminal): add Tabby terminal support on macOS, Windows, and Linux
Add Tabby (https://github.com/Eugeny/tabby) as a preferred terminal option across all three platforms: - macOS: launch via `open -na Tabby --args run` with login shell - Windows: search multiple executable candidates (PATH, LOCALAPPDATA, ProgramFiles) and launch via `tabby run cmd /K` - Linux: add `tabby` and `tabby-terminal` to the auto-detection list Also includes: - Session manager integration with login-shell command wrapping - Terminal selector UI entries for all platforms - i18n labels (zh/en/ja) - Unit tests for Tabby arg building (with/without cwd) Closes https://github.com/farion1231/cc-switch/issues/1708
This commit is contained in:
@@ -888,6 +888,7 @@ exec bash --norc --noprofile
|
||||
"kitty" => launch_macos_open_app("kitty", &script_file, false),
|
||||
"ghostty" => launch_macos_open_app("Ghostty", &script_file, true),
|
||||
"wezterm" => launch_macos_open_app("WezTerm", &script_file, true),
|
||||
"tabby" => launch_macos_tabby(&script_file),
|
||||
_ => launch_macos_terminal_app(&script_file), // "terminal" or default
|
||||
};
|
||||
|
||||
@@ -1005,6 +1006,39 @@ fn launch_macos_open_app(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// macOS: Tabby
|
||||
#[cfg(target_os = "macos")]
|
||||
fn launch_macos_tabby(script_file: &std::path::Path) -> Result<(), String> {
|
||||
use std::process::Command;
|
||||
|
||||
let output = Command::new("open")
|
||||
.args(build_macos_tabby_args(script_file))
|
||||
.output()
|
||||
.map_err(|e| format!("启动 Tabby 失败: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!(
|
||||
"Tabby 启动失败 (exit code: {:?}): {}",
|
||||
output.status.code(),
|
||||
stderr
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_macos_tabby_args(script_file: &std::path::Path) -> Vec<String> {
|
||||
vec![
|
||||
"-na".to_string(),
|
||||
"Tabby".to_string(),
|
||||
"--args".to_string(),
|
||||
"run".to_string(),
|
||||
"bash".to_string(),
|
||||
script_file.to_string_lossy().into_owned(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Linux: 根据用户首选终端启动
|
||||
#[cfg(target_os = "linux")]
|
||||
fn launch_linux_terminal(config_file: &std::path::Path) -> Result<(), String> {
|
||||
@@ -1023,6 +1057,8 @@ fn launch_linux_terminal(config_file: &std::path::Path) -> Result<(), String> {
|
||||
("alacritty", vec!["-e"]),
|
||||
("kitty", vec!["-e"]),
|
||||
("ghostty", vec!["-e"]),
|
||||
("tabby", vec!["run"]),
|
||||
("tabby-terminal", vec!["run"]),
|
||||
];
|
||||
|
||||
// Create temp script file
|
||||
@@ -1148,6 +1184,7 @@ del \"%~f0\" >nul 2>&1
|
||||
"PowerShell",
|
||||
),
|
||||
"wt" => run_windows_start_command(&["wt", "cmd", "/K", &bat_path], "Windows Terminal"),
|
||||
"tabby" => run_windows_tabby_command(&bat_path),
|
||||
_ => run_windows_start_command(&["cmd", "/K", &bat_path], "cmd"), // "cmd" or default
|
||||
};
|
||||
|
||||
@@ -1164,6 +1201,56 @@ del \"%~f0\" >nul 2>&1
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn run_windows_tabby_command(bat_path: &str) -> Result<(), String> {
|
||||
use std::process::Command;
|
||||
|
||||
let mut last_error = String::from("未找到可用的 Tabby 可执行文件");
|
||||
|
||||
for candidate in windows_tabby_executable_candidates() {
|
||||
let result = Command::new(&candidate)
|
||||
.args(["run", "cmd", "/K", bat_path])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.spawn();
|
||||
|
||||
match result {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(e) => {
|
||||
last_error = format!("执行 {} 失败: {}", candidate.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_error)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn windows_tabby_executable_candidates() -> Vec<std::path::PathBuf> {
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
for candidate in ["tabby", "tabby.exe", "Tabby", "Tabby.exe"] {
|
||||
push_unique_path(&mut candidates, std::path::PathBuf::from(candidate));
|
||||
}
|
||||
|
||||
if let Some(local_app_data) = std::env::var_os("LOCALAPPDATA") {
|
||||
let tabby_dir = std::path::PathBuf::from(local_app_data)
|
||||
.join("Programs")
|
||||
.join("Tabby");
|
||||
push_unique_path(&mut candidates, tabby_dir.join("Tabby.exe"));
|
||||
push_unique_path(&mut candidates, tabby_dir.join("tabby.exe"));
|
||||
}
|
||||
|
||||
for env_key in ["ProgramFiles", "ProgramFiles(x86)"] {
|
||||
if let Some(base_dir) = std::env::var_os(env_key) {
|
||||
let tabby_dir = std::path::PathBuf::from(base_dir).join("Tabby");
|
||||
push_unique_path(&mut candidates, tabby_dir.join("Tabby.exe"));
|
||||
push_unique_path(&mut candidates, tabby_dir.join("tabby.exe"));
|
||||
}
|
||||
}
|
||||
|
||||
candidates
|
||||
}
|
||||
|
||||
/// Windows: Run a start command with common error handling
|
||||
#[cfg(target_os = "windows")]
|
||||
fn run_windows_start_command(args: &[&str], terminal_name: &str) -> Result<(), String> {
|
||||
|
||||
@@ -22,10 +22,10 @@ pub mod skill;
|
||||
mod stream_check;
|
||||
mod sync_support;
|
||||
|
||||
mod lightweight;
|
||||
mod usage;
|
||||
mod webdav_sync;
|
||||
mod workspace;
|
||||
mod lightweight;
|
||||
|
||||
pub use auth::*;
|
||||
pub use config::*;
|
||||
@@ -48,7 +48,7 @@ pub use settings::*;
|
||||
pub use skill::*;
|
||||
pub use stream_check::*;
|
||||
|
||||
pub use lightweight::*;
|
||||
pub use usage::*;
|
||||
pub use webdav_sync::*;
|
||||
pub use workspace::*;
|
||||
pub use lightweight::*;
|
||||
|
||||
@@ -87,4 +87,4 @@ pub fn exit_lightweight_mode(app: &tauri::AppHandle) -> Result<(), String> {
|
||||
|
||||
pub fn is_lightweight_mode() -> bool {
|
||||
LIGHTWEIGHT_MODE.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ pub fn launch_terminal(
|
||||
"kitty" => launch_kitty(command, cwd),
|
||||
"wezterm" => launch_wezterm(command, cwd),
|
||||
"alacritty" => launch_alacritty(command, cwd),
|
||||
"tabby" => launch_tabby(command, cwd),
|
||||
"custom" => launch_custom(command, cwd, custom_config),
|
||||
_ => Err(format!("Unsupported terminal target: {target}")),
|
||||
}
|
||||
@@ -211,6 +212,35 @@ fn launch_alacritty(command: &str, cwd: Option<&str>) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn launch_tabby(command: &str, cwd: Option<&str>) -> Result<(), String> {
|
||||
let status = Command::new("open")
|
||||
.args(build_tabby_args(command, cwd))
|
||||
.status()
|
||||
.map_err(|e| format!("Failed to launch Tabby: {e}"))?;
|
||||
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Failed to launch Tabby.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn build_tabby_args(command: &str, cwd: Option<&str>) -> Vec<String> {
|
||||
let full_command = build_shell_command(command, cwd);
|
||||
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
|
||||
|
||||
vec![
|
||||
"-na".to_string(),
|
||||
"Tabby".to_string(),
|
||||
"--args".to_string(),
|
||||
"run".to_string(),
|
||||
shell,
|
||||
"-l".to_string(),
|
||||
"-c".to_string(),
|
||||
full_command,
|
||||
]
|
||||
}
|
||||
|
||||
fn launch_custom(
|
||||
command: &str,
|
||||
cwd: Option<&str>,
|
||||
@@ -305,4 +335,44 @@ mod tests {
|
||||
"raw:echo foo\\\\\\\\bar\\npwd\\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tabby_uses_login_shell_for_resume_commands() {
|
||||
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
|
||||
let args = build_tabby_args("claude --resume abc-123", Some("/tmp/project dir"));
|
||||
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
"-na",
|
||||
"Tabby",
|
||||
"--args",
|
||||
"run",
|
||||
&shell,
|
||||
"-l",
|
||||
"-c",
|
||||
"cd \"/tmp/project dir\" && claude --resume abc-123",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tabby_keeps_command_when_no_cwd_is_provided() {
|
||||
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
|
||||
let args = build_tabby_args("claude --resume abc-123", None);
|
||||
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
"-na",
|
||||
"Tabby",
|
||||
"--args",
|
||||
"run",
|
||||
&shell,
|
||||
"-l",
|
||||
"-c",
|
||||
"claude --resume abc-123",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,9 +264,9 @@ pub struct AppSettings {
|
||||
|
||||
// ===== 终端设置 =====
|
||||
/// 首选终端应用(可选,默认使用系统默认终端)
|
||||
/// - macOS: "terminal" | "iterm2" | "warp" | "alacritty" | "kitty" | "ghostty"
|
||||
/// - Windows: "cmd" | "powershell" | "wt" (Windows Terminal)
|
||||
/// - Linux: "gnome-terminal" | "konsole" | "xfce4-terminal" | "alacritty" | "kitty" | "ghostty"
|
||||
/// - macOS: "terminal" | "iterm2" | "alacritty" | "kitty" | "ghostty" | "wezterm" | "tabby"
|
||||
/// - Windows: "cmd" | "powershell" | "wt" (Windows Terminal) | "tabby"
|
||||
/// - Linux: "gnome-terminal" | "konsole" | "xfce4-terminal" | "alacritty" | "kitty" | "ghostty" | "tabby"
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub preferred_terminal: Option<String>,
|
||||
}
|
||||
|
||||
@@ -393,11 +393,10 @@ pub fn create_tray_menu(
|
||||
true,
|
||||
crate::lightweight::is_lightweight_mode(),
|
||||
None::<&str>,
|
||||
).map_err(|e| AppError::Message(format!("创建轻量模式菜单失败: {e}")))?;
|
||||
)
|
||||
.map_err(|e| AppError::Message(format!("创建轻量模式菜单失败: {e}")))?;
|
||||
|
||||
menu_builder = menu_builder
|
||||
.item(&lightweight_item)
|
||||
.separator();
|
||||
menu_builder = menu_builder.item(&lightweight_item).separator();
|
||||
|
||||
// 退出菜单(分隔符已在上面的 section 循环中添加)
|
||||
let quit_item = MenuItem::with_id(app, "quit", tray_texts.quit, true, None::<&str>)
|
||||
|
||||
@@ -16,6 +16,7 @@ const MACOS_TERMINALS = [
|
||||
{ value: "kitty", labelKey: "settings.terminal.options.macos.kitty" },
|
||||
{ value: "ghostty", labelKey: "settings.terminal.options.macos.ghostty" },
|
||||
{ value: "wezterm", labelKey: "settings.terminal.options.macos.wezterm" },
|
||||
{ value: "tabby", labelKey: "settings.terminal.options.macos.tabby" },
|
||||
] as const;
|
||||
|
||||
const WINDOWS_TERMINALS = [
|
||||
@@ -25,6 +26,7 @@ const WINDOWS_TERMINALS = [
|
||||
labelKey: "settings.terminal.options.windows.powershell",
|
||||
},
|
||||
{ value: "wt", labelKey: "settings.terminal.options.windows.wt" },
|
||||
{ value: "tabby", labelKey: "settings.terminal.options.windows.tabby" },
|
||||
] as const;
|
||||
|
||||
const LINUX_TERMINALS = [
|
||||
@@ -40,6 +42,7 @@ const LINUX_TERMINALS = [
|
||||
{ value: "alacritty", labelKey: "settings.terminal.options.linux.alacritty" },
|
||||
{ value: "kitty", labelKey: "settings.terminal.options.linux.kitty" },
|
||||
{ value: "ghostty", labelKey: "settings.terminal.options.linux.ghostty" },
|
||||
{ value: "tabby", labelKey: "settings.terminal.options.linux.tabby" },
|
||||
] as const;
|
||||
|
||||
// Get terminals for the current platform
|
||||
|
||||
@@ -496,12 +496,14 @@
|
||||
"alacritty": "Alacritty",
|
||||
"kitty": "Kitty",
|
||||
"ghostty": "Ghostty",
|
||||
"wezterm": "WezTerm"
|
||||
"wezterm": "WezTerm",
|
||||
"tabby": "Tabby"
|
||||
},
|
||||
"windows": {
|
||||
"cmd": "Command Prompt",
|
||||
"powershell": "PowerShell",
|
||||
"wt": "Windows Terminal"
|
||||
"wt": "Windows Terminal",
|
||||
"tabby": "Tabby"
|
||||
},
|
||||
"linux": {
|
||||
"gnomeTerminal": "GNOME Terminal",
|
||||
@@ -509,7 +511,8 @@
|
||||
"xfce4Terminal": "Xfce4 Terminal",
|
||||
"alacritty": "Alacritty",
|
||||
"kitty": "Kitty",
|
||||
"ghostty": "Ghostty"
|
||||
"ghostty": "Ghostty",
|
||||
"tabby": "Tabby"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -496,12 +496,14 @@
|
||||
"alacritty": "Alacritty",
|
||||
"kitty": "Kitty",
|
||||
"ghostty": "Ghostty",
|
||||
"wezterm": "WezTerm"
|
||||
"wezterm": "WezTerm",
|
||||
"tabby": "Tabby"
|
||||
},
|
||||
"windows": {
|
||||
"cmd": "コマンドプロンプト",
|
||||
"powershell": "PowerShell",
|
||||
"wt": "Windows Terminal"
|
||||
"wt": "Windows Terminal",
|
||||
"tabby": "Tabby"
|
||||
},
|
||||
"linux": {
|
||||
"gnomeTerminal": "GNOME Terminal",
|
||||
@@ -509,7 +511,8 @@
|
||||
"xfce4Terminal": "Xfce4 Terminal",
|
||||
"alacritty": "Alacritty",
|
||||
"kitty": "Kitty",
|
||||
"ghostty": "Ghostty"
|
||||
"ghostty": "Ghostty",
|
||||
"tabby": "Tabby"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -496,12 +496,14 @@
|
||||
"alacritty": "Alacritty",
|
||||
"kitty": "Kitty",
|
||||
"ghostty": "Ghostty",
|
||||
"wezterm": "WezTerm"
|
||||
"wezterm": "WezTerm",
|
||||
"tabby": "Tabby"
|
||||
},
|
||||
"windows": {
|
||||
"cmd": "命令提示符",
|
||||
"powershell": "PowerShell",
|
||||
"wt": "Windows Terminal"
|
||||
"wt": "Windows Terminal",
|
||||
"tabby": "Tabby"
|
||||
},
|
||||
"linux": {
|
||||
"gnomeTerminal": "GNOME Terminal",
|
||||
@@ -509,7 +511,8 @@
|
||||
"xfce4Terminal": "Xfce4 Terminal",
|
||||
"alacritty": "Alacritty",
|
||||
"kitty": "Kitty",
|
||||
"ghostty": "Ghostty"
|
||||
"ghostty": "Ghostty",
|
||||
"tabby": "Tabby"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -303,9 +303,9 @@ export interface Settings {
|
||||
|
||||
// ===== 终端设置 =====
|
||||
// 首选终端应用(可选,默认使用系统默认终端)
|
||||
// macOS: "terminal" | "iterm2" | "warp" | "alacritty" | "kitty" | "ghostty"
|
||||
// Windows: "cmd" | "powershell" | "wt"
|
||||
// Linux: "gnome-terminal" | "konsole" | "xfce4-terminal" | "alacritty" | "kitty" | "ghostty"
|
||||
// macOS: "terminal" | "iterm2" | "alacritty" | "kitty" | "ghostty" | "wezterm" | "tabby"
|
||||
// Windows: "cmd" | "powershell" | "wt" | "tabby"
|
||||
// Linux: "gnome-terminal" | "konsole" | "xfce4-terminal" | "alacritty" | "kitty" | "ghostty" | "tabby"
|
||||
preferredTerminal?: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user