1use std::path::Path;
7
8#[cfg(any(target_os = "windows", target_os = "linux"))]
9use std::path::PathBuf;
10
11pub fn open_with_application(file_path: &Path, app_name: &str) -> Result<(), String> {
13 let app = app_name.trim();
14 if app.is_empty() {
15 return Err("application name is empty".into());
16 }
17 if !file_path.exists() {
18 return Err(format!("File not found: {}", file_path.display()));
19 }
20
21 #[cfg(target_os = "macos")]
22 {
23 return open_macos(file_path, app);
24 }
25 #[cfg(target_os = "windows")]
26 {
27 return open_windows(file_path, app);
28 }
29 #[cfg(target_os = "linux")]
30 {
31 return open_linux(file_path, app);
32 }
33 #[cfg(not(any(
34 target_os = "macos",
35 target_os = "linux",
36 target_os = "windows"
37 )))]
38 {
39 let _ = (file_path, app);
40 Err("open_with_app is not supported on this platform".into())
41 }
42}
43
44#[cfg(target_os = "macos")]
45fn open_macos(file_path: &Path, app_name: &str) -> Result<(), String> {
46 let output = std::process::Command::new("open")
47 .args(["-a", app_name, &file_path.to_string_lossy()])
48 .output()
49 .map_err(|e| e.to_string())?;
50 if !output.status.success() {
51 let stderr = String::from_utf8_lossy(&output.stderr);
52 return Err(format!(
53 "Could not open with {}: {}",
54 app_name,
55 stderr.trim()
56 ));
57 }
58 Ok(())
59}
60
61#[cfg(target_os = "windows")]
62fn open_windows(file_path: &Path, app_name: &str) -> Result<(), String> {
63 if matches!(app_name, "GarageBand" | "Logic Pro") {
64 return Err(format!("{app_name} is only available on macOS"));
65 }
66 if app_name == "Preview" {
67 return opener::open(file_path).map_err(|e| e.to_string());
68 }
69 let exe = resolve_windows_executable(app_name)?;
70 std::process::Command::new(&exe)
71 .arg(file_path)
72 .spawn()
73 .map_err(|e| format!("failed to start {}: {e}", exe.display()))?;
74 Ok(())
75}
76
77#[cfg(target_os = "windows")]
78fn resolve_windows_executable(app_name: &str) -> Result<PathBuf, String> {
79 let p = Path::new(app_name);
80 if p.is_file() {
81 return Ok(p.to_path_buf());
82 }
83 if app_name.contains('/') || app_name.contains('\\') {
84 if p.exists() {
85 return Ok(p.to_path_buf());
86 }
87 return Err(format!("application path not found: {app_name}"));
88 }
89
90 match app_name {
91 "TextEdit" => {
92 return where_win("notepad.exe").ok_or_else(|| {
93 "notepad.exe not found (expected on Windows)".into()
94 });
95 }
96 "Music" => {
97 return first_existing_file(&[
98 r"C:\Program Files\Windows Media Player\wmplayer.exe",
99 ])
100 .or_else(|_| vlc_windows());
101 }
102 "QuickTime Player" => return vlc_windows(),
103 "Audacity" => {
104 return where_win("audacity.exe").ok_or_else(|| {
105 "Audacity not found in PATH (install Audacity or pass the full path to Audacity.exe)"
106 .into()
107 });
108 }
109 "Ableton Live 12 Standard" | "Ableton Live 11 Suite" | "Ableton Live 12 Suite" => {
110 if let Some(exe) = find_ableton_live_exe() {
111 return Ok(exe);
112 }
113 return Err(
114 "Ableton Live executable not found under Program Files or ProgramData".into(),
115 );
116 }
117 "Adobe Acrobat" => return find_adobe_acrobat_windows(),
118 _ => {}
119 }
120
121 if let Some(pb) = where_win(app_name) {
122 return Ok(pb);
123 }
124 if let Some(pb) = where_win(&format!("{app_name}.exe")) {
125 return Ok(pb);
126 }
127
128 Err(format!(
129 "Could not resolve application {app_name:?}. Install the app, add it to PATH, or pass the full path to the .exe."
130 ))
131}
132
133#[cfg(target_os = "windows")]
134fn where_win(name: &str) -> Option<PathBuf> {
135 let output = std::process::Command::new("where").arg(name).output().ok()?;
136 if !output.status.success() {
137 return None;
138 }
139 let stdout = String::from_utf8_lossy(&output.stdout);
140 let line = stdout.lines().next()?.trim();
141 if line.is_empty() {
142 return None;
143 }
144 let pb = PathBuf::from(line);
145 pb.is_file().then_some(pb)
146}
147
148#[cfg(target_os = "windows")]
149fn first_existing_file(paths: &[&str]) -> Result<PathBuf, String> {
150 for s in paths {
151 let p = PathBuf::from(s);
152 if p.is_file() {
153 return Ok(p);
154 }
155 }
156 Err("no candidate executable found".into())
157}
158
159#[cfg(target_os = "windows")]
160fn vlc_windows() -> Result<PathBuf, String> {
161 where_win("vlc.exe").or_else(|| {
162 first_existing_file(&[
163 r"C:\Program Files\VideoLAN\VLC\vlc.exe",
164 r"C:\Program Files (x86)\VideoLAN\VLC\vlc.exe",
165 ])
166 .ok()
167 })
168 .ok_or_else(|| "VLC not found (install VLC or map QuickTime to another player)".into())
169}
170
171#[cfg(target_os = "windows")]
172fn find_ableton_live_exe() -> Option<PathBuf> {
173 let roots = [
174 std::env::var_os("ProgramData").map(PathBuf::from),
175 std::env::var_os("ProgramFiles").map(PathBuf::from),
176 ];
177 for opt in roots.into_iter().flatten() {
178 let ableton = opt.join("Ableton");
179 let Ok(entries) = std::fs::read_dir(&ableton) else {
180 continue;
181 };
182 for e in entries.flatten() {
183 let dir = e.path();
184 if !dir.is_dir() {
185 continue;
186 }
187 let name = dir.file_name()?.to_string_lossy();
188 if !name.contains("Live") {
189 continue;
190 }
191 let program = dir.join("Program");
192 let Ok(files) = std::fs::read_dir(&program) else {
193 continue;
194 };
195 for f in files.flatten() {
196 let path = f.path();
197 if path.is_file() && path.extension().is_some_and(|x| x == "exe") {
198 let stem = path.file_name()?.to_string_lossy();
199 if stem.contains("Live") {
200 return Some(path);
201 }
202 }
203 }
204 }
205 }
206 None
207}
208
209#[cfg(target_os = "windows")]
210fn find_adobe_acrobat_windows() -> Result<PathBuf, String> {
211 let roots = [
212 std::env::var_os("ProgramFiles").map(PathBuf::from),
213 std::env::var_os("ProgramFiles(x86)").map(PathBuf::from),
214 ];
215 for opt in roots.into_iter().flatten() {
216 let adobe = opt.join("Adobe");
217 let Ok(entries) = std::fs::read_dir(&adobe) else {
218 continue;
219 };
220 for e in entries.flatten() {
221 let dir = e.path();
222 if !dir.is_dir() {
223 continue;
224 }
225 let name = dir.file_name().and_then(|n| n.to_str()).unwrap_or("");
226 if !name.starts_with("Acrobat") && !name.starts_with("Adobe Acrobat") {
227 continue;
228 }
229 let exe = dir.join("Acrobat").join("AcroRd32.exe");
230 if exe.is_file() {
231 return Ok(exe);
232 }
233 }
234 }
235 where_win("AcroRd32.exe").ok_or_else(|| {
236 "Adobe Acrobat Reader not found under Program Files or PATH".into()
237 })
238}
239
240#[cfg(target_os = "linux")]
241fn open_linux(file_path: &Path, app_name: &str) -> Result<(), String> {
242 if matches!(app_name, "GarageBand" | "Logic Pro") {
243 return Err(format!("{app_name} is only available on macOS"));
244 }
245 if app_name == "Preview" {
246 return opener::open(file_path).map_err(|e| e.to_string());
247 }
248 let exe = resolve_linux_executable(app_name)?;
249 std::process::Command::new(&exe)
250 .arg(file_path)
251 .spawn()
252 .map_err(|e| format!("failed to start {}: {e}", exe.display()))?;
253 Ok(())
254}
255
256#[cfg(target_os = "linux")]
257fn which_linux(candidates: &[&str]) -> Option<PathBuf> {
258 for c in candidates {
259 let output = std::process::Command::new("which").arg(c).output().ok()?;
260 if !output.status.success() {
261 continue;
262 }
263 let stdout = String::from_utf8_lossy(&output.stdout);
264 let line = stdout.lines().next()?.trim();
265 if line.is_empty() {
266 continue;
267 }
268 let pb = PathBuf::from(line);
269 if pb.is_file() {
270 return Some(pb);
271 }
272 }
273 None
274}
275
276#[cfg(target_os = "linux")]
277fn resolve_linux_executable(app_name: &str) -> Result<PathBuf, String> {
278 let p = Path::new(app_name);
279 if p.is_file() {
280 return Ok(p.to_path_buf());
281 }
282 if app_name.contains('/') {
283 if p.exists() {
284 return Ok(p.to_path_buf());
285 }
286 return Err(format!("application path not found: {app_name}"));
287 }
288
289 match app_name {
290 "TextEdit" => which_linux(&[
291 "gnome-text-editor",
292 "gedit",
293 "kwrite",
294 "mousepad",
295 "xed",
296 "pluma",
297 "leafpad",
298 ])
299 .ok_or_else(|| {
300 "No text editor found in PATH (install gedit, gnome-text-editor, or similar)".into()
301 }),
302 "Music" => which_linux(&["vlc", "mpv", "audacious", "rhythmbox"]).ok_or_else(|| {
303 "No music player found in PATH (install vlc or mpv)".into()
304 }),
305 "QuickTime Player" => which_linux(&["vlc", "mpv", "totem", "celluloid"]).ok_or_else(|| {
306 "No video player found in PATH (install vlc or mpv)".into()
307 }),
308 "Audacity" => which_linux(&["audacity"]).ok_or_else(|| {
309 "Audacity not found in PATH (install Audacity or pass the full path)".into()
310 }),
311 "Ableton Live 12 Standard" | "Ableton Live 11 Suite" | "Ableton Live 12 Suite" => {
312 which_linux(&["Live", "live", "ableton"]).ok_or_else(|| {
313 "Ableton Live not found in PATH (install Ableton or pass the full path to the Live binary)".into()
314 })
315 }
316 "Adobe Acrobat" => which_linux(&["acroread", "evince", "okular", "atril", "xreader"])
317 .ok_or_else(|| {
318 "PDF viewer not found in PATH (install evince, okular, or Adobe Reader)".into()
319 }),
320 _ => {
321 if let Some(pb) = which_linux(&[app_name]) {
322 return Ok(pb);
323 }
324 Err(format!(
325 "Could not resolve application {app_name:?} in PATH. Pass a full path to the binary."
326 ))
327 }
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn open_with_errors_on_empty_app_name() {
337 let tmp = std::env::temp_dir().join("audio_haxor_open_with_test_file");
338 let _ = std::fs::write(&tmp, b"x");
339 let r = open_with_application(&tmp, " ");
340 let _ = std::fs::remove_file(&tmp);
341 assert!(r.is_err());
342 }
343}