mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-01 12:22:09 +08:00
feat: session manger (#867)
* feat: init session manger * feat: persist selected app to localStorage - Save app selection when switching providers - Restore last selected app on page load * feat: persist current view to localStorage - Save view selection when switching tabs - Restore last selected view on page load * styles: update ui * feat: Improve macOS Terminal activation and refactor Kitty launch to use command with user's default shell. * fix: session view * feat: toc * feat: Implement FlexSearch for improved session search functionality. * feat: Redesign session manager search and filter UI for a more compact and dynamic experience. * refactor: modularize session manager by extracting components and utility functions into dedicated files. * feat: Enhance session terminal launching with support for iTerm2, Ghostty, WezTerm, and Alacritty, including UI and custom configuration options. * feat: Conditionally render terminal selection and resume session buttons only on macOS.
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
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),
|
||||
"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 "{}"
|
||||
end tell"#,
|
||||
escaped
|
||||
);
|
||||
|
||||
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 "{}"
|
||||
end tell
|
||||
end tell"#,
|
||||
escaped
|
||||
);
|
||||
|
||||
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> {
|
||||
// Ghostty usage: open -na Ghostty --args +work-dir=... -e shell -c command
|
||||
|
||||
// Using `open` to launch.
|
||||
let mut args = vec!["-na", "Ghostty", "--args"];
|
||||
|
||||
// Ghostty uses --working-directory for working directory (or +work-dir, but --working-directory is standard in newer versions/compat)
|
||||
// Note: The user's error output didn't show the working dir arg failure, so we assume flag is okay or we stick to compatible ones.
|
||||
// Documentation says --working-directory is supported in CLI.
|
||||
let work_dir_arg = if let Some(dir) = cwd {
|
||||
format!("--working-directory={}", dir)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
if !work_dir_arg.is_empty() {
|
||||
args.push(&work_dir_arg);
|
||||
}
|
||||
|
||||
// Command execution
|
||||
args.push("-e");
|
||||
|
||||
// We pass the command and its arguments separately.
|
||||
// The previous issue was passing the entire "cmd args" string as a single argument to -e,
|
||||
// which led Ghostty to look for a binary named "cmd args".
|
||||
// Splitting by whitespace allows Ghostty to see ["cmd", "args"].
|
||||
// Note: This assumes simple commands without quoted arguments containing spaces.
|
||||
let full_command = build_shell_command(command, None);
|
||||
for part in full_command.split_whitespace() {
|
||||
args.push(part);
|
||||
}
|
||||
|
||||
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 full_command = build_shell_command(command, None);
|
||||
|
||||
let mut args = vec!["-na", "WezTerm", "--args", "start"];
|
||||
|
||||
if let Some(dir) = cwd {
|
||||
args.push("--cwd");
|
||||
args.push(dir);
|
||||
}
|
||||
|
||||
// Invoke shell to run the command string (to handle pipes, etc)
|
||||
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
|
||||
args.push("--");
|
||||
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 WezTerm: {e}"))?;
|
||||
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Failed to launch WezTerm.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
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('"', "\\\"")
|
||||
}
|
||||
Reference in New Issue
Block a user