Skip to main content

nex_core/
action_executor.rs

1use std::fmt::{Display, Formatter};
2use std::path::{Path, PathBuf};
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum LaunchError {
6    EmptyPath,
7    MissingPath(PathBuf),
8    LaunchFailed { message: String, code: Option<i32> },
9}
10
11impl Display for LaunchError {
12    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
13        match self {
14            Self::EmptyPath => write!(f, "empty path"),
15            Self::MissingPath(path) => write!(f, "path does not exist: {}", path.display()),
16            Self::LaunchFailed { message, code } => {
17                if let Some(code) = code {
18                    write!(f, "launch failed: {message} (code {code})")
19                } else {
20                    write!(f, "launch failed: {message}")
21                }
22            }
23        }
24    }
25}
26
27impl std::error::Error for LaunchError {}
28
29pub fn launch_path(path: &str) -> Result<(), LaunchError> {
30    let trimmed = path.trim();
31    if trimmed.is_empty() {
32        return Err(LaunchError::EmptyPath);
33    }
34
35    if is_non_filesystem_open_target(trimmed) {
36        return launch_open(trimmed);
37    }
38
39    let candidate = Path::new(trimmed);
40    if !candidate.exists() {
41        return Err(LaunchError::MissingPath(candidate.to_path_buf()));
42    }
43
44    launch_existing_path(candidate)?;
45
46    Ok(())
47}
48
49pub fn launch_open_target(target: &str) -> Result<(), LaunchError> {
50    let trimmed = target.trim();
51    if trimmed.is_empty() {
52        return Err(LaunchError::EmptyPath);
53    }
54    launch_open(trimmed)
55}
56
57#[cfg(target_os = "windows")]
58fn launch_existing_path(candidate: &Path) -> Result<(), LaunchError> {
59    let target = candidate.to_string_lossy().into_owned();
60    launch_open(&target)
61}
62
63#[cfg(target_os = "windows")]
64fn launch_open(target: &str) -> Result<(), LaunchError> {
65    if target.trim().to_ascii_lowercase().starts_with("shell:") {
66        return launch_shell_target(target);
67    }
68
69    use windows_sys::Win32::UI::Shell::ShellExecuteW;
70    use windows_sys::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL;
71
72    let wide_target = to_wide(&target);
73    let result = unsafe {
74        ShellExecuteW(
75            std::ptr::null_mut(),
76            std::ptr::null(),
77            wide_target.as_ptr(),
78            std::ptr::null(),
79            std::ptr::null(),
80            SW_SHOWNORMAL,
81        )
82    } as isize;
83
84    if result <= 32 {
85        return Err(LaunchError::LaunchFailed {
86            message: format!("ShellExecuteW failed for '{target}'"),
87            code: Some(result as i32),
88        });
89    }
90
91    Ok(())
92}
93
94#[cfg(target_os = "windows")]
95fn launch_shell_target(target: &str) -> Result<(), LaunchError> {
96    std::process::Command::new("explorer.exe")
97        .arg(target)
98        .spawn()
99        .map_err(|error| LaunchError::LaunchFailed {
100            message: format!("failed to launch shell target '{target}': {error}"),
101            code: None,
102        })?;
103    Ok(())
104}
105
106#[cfg(not(target_os = "windows"))]
107fn launch_existing_path(_candidate: &Path) -> Result<(), LaunchError> {
108    Ok(())
109}
110
111#[cfg(not(target_os = "windows"))]
112fn launch_open(_target: &str) -> Result<(), LaunchError> {
113    Ok(())
114}
115
116#[cfg(target_os = "windows")]
117fn to_wide(value: &str) -> Vec<u16> {
118    value.encode_utf16().chain(std::iter::once(0)).collect()
119}
120
121fn is_non_filesystem_open_target(value: &str) -> bool {
122    let trimmed = value.trim();
123    if trimmed.is_empty() {
124        return false;
125    }
126
127    let lowered = trimmed.to_ascii_lowercase();
128    if lowered.starts_with("shell:") || lowered.starts_with("ms-") {
129        return true;
130    }
131
132    if trimmed.contains("://") {
133        return true;
134    }
135
136    !looks_like_filesystem_path(trimmed)
137}
138
139fn looks_like_filesystem_path(path: &str) -> bool {
140    if path.starts_with('/') || path.starts_with('\\') {
141        return true;
142    }
143
144    let bytes = path.as_bytes();
145    bytes.len() >= 3 && bytes[1] == b':' && (bytes[2] == b'\\' || bytes[2] == b'/')
146}