Skip to main content

panex_core/
lib.rs

1pub mod config;
2
3use serde::Serialize;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::time::UNIX_EPOCH;
7
8#[derive(Debug, Serialize, Clone)]
9pub struct FileEntry {
10    pub name: String,
11    pub path: String,
12    pub is_dir: bool,
13    pub size: u64,
14    pub modified: u64,
15}
16
17pub fn get_home_dir() -> Result<String, String> {
18    dirs::home_dir()
19        .map(|p| p.to_string_lossy().to_string())
20        .ok_or_else(|| "Could not determine home directory".to_string())
21}
22
23pub fn read_directory(path: &str) -> Result<Vec<FileEntry>, String> {
24    let dir_path = Path::new(path);
25    if !dir_path.is_dir() {
26        return Err(format!("Not a directory: {}", path));
27    }
28
29    let mut entries = Vec::new();
30
31    let read_dir = fs::read_dir(dir_path).map_err(|e| format!("Failed to read directory: {}", e))?;
32
33    for entry in read_dir {
34        let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
35        let metadata = entry
36            .metadata()
37            .map_err(|e| format!("Failed to read metadata: {}", e))?;
38
39        let modified = metadata
40            .modified()
41            .ok()
42            .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
43            .map(|d| d.as_secs())
44            .unwrap_or(0);
45
46        entries.push(FileEntry {
47            name: entry.file_name().to_string_lossy().to_string(),
48            path: entry.path().to_string_lossy().to_string(),
49            is_dir: metadata.is_dir(),
50            size: disk_size(&metadata),
51            modified,
52        });
53    }
54
55    entries.sort_by(|a, b| {
56        b.is_dir.cmp(&a.is_dir).then(a.name.to_lowercase().cmp(&b.name.to_lowercase()))
57    });
58
59    Ok(entries)
60}
61
62pub fn rename_entry(path: &str, new_name: &str) -> Result<(), String> {
63    let source = PathBuf::from(path);
64    if !source.exists() {
65        return Err(format!("Path does not exist: {}", path));
66    }
67
68    let parent = source
69        .parent()
70        .ok_or_else(|| "Cannot determine parent directory".to_string())?;
71    let dest = parent.join(new_name);
72
73    if dest.exists() {
74        return Err(format!("A file named '{}' already exists", new_name));
75    }
76
77    fs::rename(&source, &dest).map_err(|e| format!("Failed to rename: {}", e))
78}
79
80pub fn open_entry(path: &str) -> Result<(), String> {
81    open_entry_with_app(path, None)
82}
83
84/// Open a file with a specific application, or the system default if None.
85pub fn open_entry_with_app(path: &str, app: Option<&str>) -> Result<(), String> {
86    let target = std::path::PathBuf::from(path);
87    if !target.exists() {
88        return Err(format!("Path does not exist: {}", path));
89    }
90
91    #[cfg(target_os = "macos")]
92    {
93        let mut cmd = std::process::Command::new("open");
94        if let Some(app_name) = app {
95            cmd.args(["-a", app_name]);
96        }
97        cmd.arg(path)
98            .spawn()
99            .map_err(|e| format!("Failed to open: {}", e))?;
100    }
101
102    #[cfg(target_os = "windows")]
103    {
104        if let Some(app_name) = app {
105            std::process::Command::new(app_name)
106                .arg(path)
107                .spawn()
108                .map_err(|e| format!("Failed to open with {}: {}", app_name, e))?;
109        } else {
110            std::process::Command::new("cmd")
111                .args(["/C", "start", "", path])
112                .spawn()
113                .map_err(|e| format!("Failed to open: {}", e))?;
114        }
115    }
116
117    #[cfg(target_os = "linux")]
118    {
119        if let Some(app_name) = app {
120            std::process::Command::new(app_name)
121                .arg(path)
122                .spawn()
123                .map_err(|e| format!("Failed to open with {}: {}", app_name, e))?;
124        } else {
125            std::process::Command::new("xdg-open")
126                .arg(path)
127                .spawn()
128                .map_err(|e| format!("Failed to open: {}", e))?;
129        }
130    }
131
132    Ok(())
133}
134
135/// Get the file extension (without dot) from a path.
136pub fn get_extension(path: &str) -> Option<String> {
137    Path::new(path)
138        .extension()
139        .map(|e| e.to_string_lossy().to_string())
140}
141
142pub fn delete_entry(path: &str, permanent: bool) -> Result<(), String> {
143    let target = PathBuf::from(path);
144    if !target.exists() {
145        return Err(format!("Path does not exist: {}", path));
146    }
147
148    if permanent {
149        if target.is_dir() {
150            fs::remove_dir_all(&target).map_err(|e| format!("Failed to delete: {}", e))
151        } else {
152            fs::remove_file(&target).map_err(|e| format!("Failed to delete: {}", e))
153        }
154    } else {
155        {
156            #[cfg(target_os = "macos")]
157            {
158                use trash::macos::{DeleteMethod, TrashContextExtMacos};
159                use trash::TrashContext;
160                let mut ctx = TrashContext::default();
161                ctx.set_delete_method(DeleteMethod::NsFileManager);
162                ctx.delete(&target).map_err(|e| format!("Failed to move to trash: {}", e))
163            }
164            #[cfg(not(target_os = "macos"))]
165            {
166                trash::delete(&target).map_err(|e| format!("Failed to move to trash: {}", e))
167            }
168        }
169    }
170}
171
172pub fn copy_entry(source: &str, dest_dir: &str) -> Result<String, String> {
173    let src = PathBuf::from(source);
174    if !src.exists() {
175        return Err(format!("Source does not exist: {}", source));
176    }
177    let dest = PathBuf::from(dest_dir);
178    if !dest.is_dir() {
179        return Err(format!("Destination is not a directory: {}", dest_dir));
180    }
181
182    let file_name = src
183        .file_name()
184        .ok_or_else(|| "Cannot determine file name".to_string())?;
185    let dest_path = dest.join(file_name);
186
187    if src.is_dir() {
188        copy_dir_recursive(&src, &dest_path)?;
189    } else {
190        fs::copy(&src, &dest_path).map_err(|e| format!("Failed to copy file: {}", e))?;
191    }
192
193    Ok(dest_path.to_string_lossy().to_string())
194}
195
196fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<(), String> {
197    fs::create_dir_all(dest).map_err(|e| format!("Failed to create directory: {}", e))?;
198
199    let entries =
200        fs::read_dir(src).map_err(|e| format!("Failed to read source directory: {}", e))?;
201
202    for entry in entries {
203        let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
204        let entry_dest = dest.join(entry.file_name());
205
206        if entry.path().is_dir() {
207            copy_dir_recursive(&entry.path(), &entry_dest)?;
208        } else {
209            fs::copy(entry.path(), &entry_dest)
210                .map_err(|e| format!("Failed to copy file: {}", e))?;
211        }
212    }
213
214    Ok(())
215}
216
217pub fn calculate_directory_size(path: &str) -> Result<u64, String> {
218    let dir_path = Path::new(path);
219    if !dir_path.is_dir() {
220        return Err(format!("Not a directory: {}", path));
221    }
222
223    fn walk(dir: &Path) -> u64 {
224        let mut total: u64 = 0;
225        if let Ok(entries) = fs::read_dir(dir) {
226            for entry in entries {
227                if let Ok(entry) = entry {
228                    if let Ok(meta) = entry.metadata() {
229                        if meta.is_dir() {
230                            total += walk(&entry.path());
231                        } else {
232                            total += disk_size(&meta);
233                        }
234                    }
235                }
236            }
237        }
238        total
239    }
240
241    Ok(walk(dir_path))
242}
243
244/// Returns actual disk usage (blocks * 512) on Unix, logical size on Windows.
245#[cfg(unix)]
246fn disk_size(meta: &fs::Metadata) -> u64 {
247    use std::os::unix::fs::MetadataExt;
248    meta.blocks() * 512
249}
250
251#[cfg(not(unix))]
252fn disk_size(meta: &fs::Metadata) -> u64 {
253    meta.len()
254}
255
256pub fn create_file(dir: &str, name: &str) -> Result<(), String> {
257    let path = Path::new(dir).join(name);
258    if path.exists() {
259        return Err(format!("A file named '{}' already exists", name));
260    }
261    fs::File::create(&path).map_err(|e| format!("Failed to create file: {}", e))?;
262    Ok(())
263}
264
265pub fn create_folder(dir: &str, name: &str) -> Result<(), String> {
266    let path = Path::new(dir).join(name);
267    if path.exists() {
268        return Err(format!("A folder named '{}' already exists", name));
269    }
270    fs::create_dir(&path).map_err(|e| format!("Failed to create folder: {}", e))?;
271    Ok(())
272}
273
274pub fn open_in_terminal(path: &str) -> Result<(), String> {
275    let dir = Path::new(path);
276    if !dir.is_dir() {
277        return Err(format!("Not a directory: {}", path));
278    }
279
280    #[cfg(target_os = "macos")]
281    {
282        // Prefer iTerm2 if installed, fall back to Terminal.app
283        let app = if Path::new("/Applications/iTerm.app").exists() {
284            "iTerm"
285        } else {
286            "Terminal"
287        };
288        std::process::Command::new("open")
289            .args(["-a", app, path])
290            .spawn()
291            .map_err(|e| format!("Failed to open terminal: {}", e))?;
292    }
293
294    #[cfg(target_os = "windows")]
295    {
296        std::process::Command::new("cmd")
297            .args(["/C", "start", "cmd", "/K", &format!("cd /d {}", path)])
298            .spawn()
299            .map_err(|e| format!("Failed to open terminal: {}", e))?;
300    }
301
302    #[cfg(target_os = "linux")]
303    {
304        // Try common terminal emulators in order
305        let terminals = ["x-terminal-emulator", "gnome-terminal", "konsole", "xterm"];
306        let mut launched = false;
307        for term in &terminals {
308            let result = if *term == "gnome-terminal" {
309                std::process::Command::new(term)
310                    .arg("--working-directory")
311                    .arg(path)
312                    .spawn()
313            } else {
314                std::process::Command::new(term)
315                    .current_dir(path)
316                    .spawn()
317            };
318            if result.is_ok() {
319                launched = true;
320                break;
321            }
322        }
323        if !launched {
324            return Err("No supported terminal emulator found".to_string());
325        }
326    }
327
328    Ok(())
329}
330
331/// Open a terminal running a specific command (e.g., "hx /path/to/file").
332pub fn open_in_terminal_with_command(command: &str, args: &[&str]) -> Result<(), String> {
333    #[cfg(target_os = "macos")]
334    {
335        // Build the full command string for the shell
336        let mut full_cmd = shell_escape(command);
337        for arg in args {
338            full_cmd.push(' ');
339            full_cmd.push_str(&shell_escape(arg));
340        }
341
342        let app = if Path::new("/Applications/iTerm.app").exists() {
343            "iTerm"
344        } else {
345            "Terminal"
346        };
347
348        if app == "iTerm" {
349            // Use AppleScript to open a new iTerm tab with the command
350            let script = format!(
351                r#"tell application "iTerm"
352                    activate
353                    tell current window
354                        create tab with default profile
355                        tell current session
356                            write text "{}"
357                        end tell
358                    end tell
359                end tell"#,
360                full_cmd
361            );
362            std::process::Command::new("osascript")
363                .args(["-e", &script])
364                .spawn()
365                .map_err(|e| format!("Failed to open iTerm: {}", e))?;
366        } else {
367            // Terminal.app via AppleScript
368            let script = format!(
369                r#"tell application "Terminal"
370                    activate
371                    do script "{}"
372                end tell"#,
373                full_cmd
374            );
375            std::process::Command::new("osascript")
376                .args(["-e", &script])
377                .spawn()
378                .map_err(|e| format!("Failed to open Terminal: {}", e))?;
379        }
380    }
381
382    #[cfg(target_os = "windows")]
383    {
384        let mut full_cmd = command.to_string();
385        for arg in args {
386            full_cmd.push(' ');
387            full_cmd.push_str(arg);
388        }
389        std::process::Command::new("cmd")
390            .args(["/C", "start", "cmd", "/K", &full_cmd])
391            .spawn()
392            .map_err(|e| format!("Failed to open terminal: {}", e))?;
393    }
394
395    #[cfg(target_os = "linux")]
396    {
397        let mut full_cmd = command.to_string();
398        for arg in args {
399            full_cmd.push(' ');
400            full_cmd.push_str(arg);
401        }
402        let terminals = ["x-terminal-emulator", "gnome-terminal", "konsole", "xterm"];
403        let mut launched = false;
404        for term in &terminals {
405            let result = if *term == "gnome-terminal" {
406                std::process::Command::new(term)
407                    .args(["--", "sh", "-c", &full_cmd])
408                    .spawn()
409            } else {
410                std::process::Command::new(term)
411                    .args(["-e", &format!("sh -c '{}'", full_cmd)])
412                    .spawn()
413            };
414            if result.is_ok() {
415                launched = true;
416                break;
417            }
418        }
419        if !launched {
420            return Err("No supported terminal emulator found".to_string());
421        }
422    }
423
424    Ok(())
425}
426
427fn shell_escape(s: &str) -> String {
428    s.replace('\\', "\\\\").replace('"', "\\\"")
429}
430
431pub fn move_entry(source: &str, dest_dir: &str) -> Result<String, String> {
432    let src = PathBuf::from(source);
433    if !src.exists() {
434        return Err(format!("Source does not exist: {}", source));
435    }
436    let dest = PathBuf::from(dest_dir);
437    if !dest.is_dir() {
438        return Err(format!("Destination is not a directory: {}", dest_dir));
439    }
440
441    let file_name = src
442        .file_name()
443        .ok_or_else(|| "Cannot determine file name".to_string())?;
444    let dest_path = dest.join(file_name);
445
446    // Try fast rename first (works on same volume)
447    match fs::rename(&src, &dest_path) {
448        Ok(()) => return Ok(dest_path.to_string_lossy().to_string()),
449        Err(_) => {
450            // Cross-volume: copy then delete
451            copy_entry(source, dest_dir)?;
452            if src.is_dir() {
453                fs::remove_dir_all(&src)
454                    .map_err(|e| format!("Copied but failed to remove source: {}", e))?;
455            } else {
456                fs::remove_file(&src)
457                    .map_err(|e| format!("Copied but failed to remove source: {}", e))?;
458            }
459            Ok(dest_path.to_string_lossy().to_string())
460        }
461    }
462}