1use anyhow::{bail, Context, Result};
2use std::path::Path;
3use std::process::Command;
4
5pub fn open_in_editor(path: &Path, command: &str) -> Result<()> {
9 let path_str = path
10 .to_str()
11 .context("Workspace path contains non-UTF-8 characters")?;
12
13 let cmd_str = if command.contains(" . ") || command.ends_with(" .") || command == "." {
15 command.replacen(" .", &format!(" {path_str}"), 1)
16 } else {
17 format!("{command} {path_str}")
18 };
19
20 run_shell_command(&cmd_str)
21 .with_context(|| format!("Failed to open editor with command: {cmd_str}"))
22}
23
24pub fn open_in_explorer(path: &Path) -> Result<()> {
26 platform_open_in_explorer(path)
27}
28
29#[cfg(target_os = "macos")]
30fn platform_open_in_explorer(path: &Path) -> Result<()> {
31 Command::new("open")
32 .arg(path)
33 .spawn()
34 .context("Failed to open Finder")?;
35 Ok(())
36}
37
38#[cfg(target_os = "linux")]
39fn platform_open_in_explorer(path: &Path) -> Result<()> {
40 Command::new("xdg-open")
41 .arg(path)
42 .spawn()
43 .context("Failed to open file manager")?;
44 Ok(())
45}
46
47#[cfg(target_os = "windows")]
48fn platform_open_in_explorer(path: &Path) -> Result<()> {
49 Command::new("explorer")
50 .arg(path)
51 .spawn()
52 .context("Failed to open Explorer")?;
53 Ok(())
54}
55
56#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
57fn platform_open_in_explorer(_path: &Path) -> Result<()> {
58 bail!("open_in_explorer is not implemented for this platform")
59}
60
61pub fn open_in_terminal(path: &Path, command: Option<&str>) -> Result<()> {
65 if let Some(cmd) = command {
66 let path_str = path
67 .to_str()
68 .context("Workspace path contains non-UTF-8 characters")?;
69 let cmd_str = if cmd.contains(" . ") || cmd.ends_with(" .") || cmd == "." {
70 cmd.replacen(" .", &format!(" {path_str}"), 1)
71 } else {
72 format!("{cmd} {path_str}")
73 };
74 return run_shell_command(&cmd_str)
75 .with_context(|| format!("Failed to open terminal with command: {cmd_str}"));
76 }
77
78 open_default_terminal(path)
79}
80
81#[cfg(target_os = "macos")]
82fn open_default_terminal(path: &Path) -> Result<()> {
83 let path_str = path
84 .to_str()
85 .context("Workspace path contains non-UTF-8 characters")?;
86 let escaped = path_str.replace('\'', "'\\''");
88 let script = format!(
89 r#"tell application "Terminal"
90 activate
91 do script "cd '{escaped}'"
92end tell"#
93 );
94 Command::new("osascript")
95 .arg("-e")
96 .arg(&script)
97 .spawn()
98 .context("Failed to open Terminal.app via osascript")?;
99 Ok(())
100}
101
102#[cfg(target_os = "linux")]
103fn open_default_terminal(path: &Path) -> Result<()> {
104 let terminals: &[&[&str]] = &[
106 &["gnome-terminal", "--working-directory"],
107 &["xterm", "-e", "bash -c 'cd \"$1\" && exec bash' -- "],
108 &["konsole", "--workdir"],
109 &["xfce4-terminal", "--working-directory"],
110 ];
111
112 for args in terminals {
113 let (prog, rest) = args.split_first().unwrap();
114 let mut cmd = Command::new(prog);
115 for arg in rest {
116 cmd.arg(arg);
117 }
118 cmd.arg(path);
119 if cmd.spawn().is_ok() {
120 return Ok(());
121 }
122 }
123
124 bail!("No suitable terminal emulator found on this Linux system")
125}
126
127#[cfg(target_os = "windows")]
128fn open_default_terminal(path: &Path) -> Result<()> {
129 let result = Command::new("wt")
131 .args(["--startingDirectory"])
132 .arg(path)
133 .spawn();
134
135 if result.is_ok() {
136 return Ok(());
137 }
138
139 Command::new("cmd")
140 .args(["/c", "start", "cmd.exe", "/k"])
141 .arg(format!("cd /d \"{}\"", path.display()))
142 .spawn()
143 .context("Failed to open cmd.exe")?;
144
145 Ok(())
146}
147
148#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
149fn open_default_terminal(_path: &Path) -> Result<()> {
150 bail!("open_in_terminal is not implemented for this platform")
151}
152
153fn run_shell_command(cmd: &str) -> Result<()> {
155 let mut parts = shlex_split(cmd);
156 if parts.is_empty() {
157 bail!("Empty command");
158 }
159 let program = parts.remove(0);
160 Command::new(&program)
161 .args(&parts)
162 .spawn()
163 .with_context(|| format!("Failed to spawn {program}"))?;
164 Ok(())
165}
166
167fn shlex_split(s: &str) -> Vec<String> {
169 let mut parts = Vec::new();
170 let mut current = String::new();
171 let mut in_quotes = false;
172 let mut chars = s.chars().peekable();
173
174 while let Some(c) = chars.next() {
175 match c {
176 '"' => in_quotes = !in_quotes,
177 ' ' | '\t' if !in_quotes => {
178 if !current.is_empty() {
179 parts.push(current.clone());
180 current.clear();
181 }
182 }
183 _ => current.push(c),
184 }
185 }
186 if !current.is_empty() {
187 parts.push(current);
188 }
189 parts
190}