Skip to main content

panex_core/
lib.rs

1use serde::Serialize;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::time::UNIX_EPOCH;
5
6#[derive(Debug, Serialize, Clone)]
7pub struct FileEntry {
8    pub name: String,
9    pub path: String,
10    pub is_dir: bool,
11    pub size: u64,
12    pub modified: u64,
13}
14
15pub fn get_home_dir() -> Result<String, String> {
16    dirs::home_dir()
17        .map(|p| p.to_string_lossy().to_string())
18        .ok_or_else(|| "Could not determine home directory".to_string())
19}
20
21pub fn read_directory(path: &str) -> Result<Vec<FileEntry>, String> {
22    let dir_path = Path::new(path);
23    if !dir_path.is_dir() {
24        return Err(format!("Not a directory: {}", path));
25    }
26
27    let mut entries = Vec::new();
28
29    let read_dir = fs::read_dir(dir_path).map_err(|e| format!("Failed to read directory: {}", e))?;
30
31    for entry in read_dir {
32        let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
33        let metadata = entry
34            .metadata()
35            .map_err(|e| format!("Failed to read metadata: {}", e))?;
36
37        let modified = metadata
38            .modified()
39            .ok()
40            .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
41            .map(|d| d.as_secs())
42            .unwrap_or(0);
43
44        entries.push(FileEntry {
45            name: entry.file_name().to_string_lossy().to_string(),
46            path: entry.path().to_string_lossy().to_string(),
47            is_dir: metadata.is_dir(),
48            size: disk_size(&metadata),
49            modified,
50        });
51    }
52
53    entries.sort_by(|a, b| {
54        b.is_dir.cmp(&a.is_dir).then(a.name.to_lowercase().cmp(&b.name.to_lowercase()))
55    });
56
57    Ok(entries)
58}
59
60pub fn rename_entry(path: &str, new_name: &str) -> Result<(), String> {
61    let source = PathBuf::from(path);
62    if !source.exists() {
63        return Err(format!("Path does not exist: {}", path));
64    }
65
66    let parent = source
67        .parent()
68        .ok_or_else(|| "Cannot determine parent directory".to_string())?;
69    let dest = parent.join(new_name);
70
71    if dest.exists() {
72        return Err(format!("A file named '{}' already exists", new_name));
73    }
74
75    fs::rename(&source, &dest).map_err(|e| format!("Failed to rename: {}", e))
76}
77
78pub fn open_entry(path: &str) -> Result<(), String> {
79    let target = std::path::PathBuf::from(path);
80    if !target.exists() {
81        return Err(format!("Path does not exist: {}", path));
82    }
83
84    #[cfg(target_os = "macos")]
85    {
86        std::process::Command::new("open")
87            .arg(path)
88            .spawn()
89            .map_err(|e| format!("Failed to open: {}", e))?;
90    }
91
92    #[cfg(target_os = "windows")]
93    {
94        std::process::Command::new("cmd")
95            .args(["/C", "start", "", path])
96            .spawn()
97            .map_err(|e| format!("Failed to open: {}", e))?;
98    }
99
100    #[cfg(target_os = "linux")]
101    {
102        std::process::Command::new("xdg-open")
103            .arg(path)
104            .spawn()
105            .map_err(|e| format!("Failed to open: {}", e))?;
106    }
107
108    Ok(())
109}
110
111pub fn delete_entry(path: &str, permanent: bool) -> Result<(), String> {
112    let target = PathBuf::from(path);
113    if !target.exists() {
114        return Err(format!("Path does not exist: {}", path));
115    }
116
117    if permanent {
118        if target.is_dir() {
119            fs::remove_dir_all(&target).map_err(|e| format!("Failed to delete: {}", e))
120        } else {
121            fs::remove_file(&target).map_err(|e| format!("Failed to delete: {}", e))
122        }
123    } else {
124        {
125            #[cfg(target_os = "macos")]
126            {
127                use trash::macos::{DeleteMethod, TrashContextExtMacos};
128                use trash::TrashContext;
129                let mut ctx = TrashContext::default();
130                ctx.set_delete_method(DeleteMethod::NsFileManager);
131                ctx.delete(&target).map_err(|e| format!("Failed to move to trash: {}", e))
132            }
133            #[cfg(not(target_os = "macos"))]
134            {
135                trash::delete(&target).map_err(|e| format!("Failed to move to trash: {}", e))
136            }
137        }
138    }
139}
140
141pub fn copy_entry(source: &str, dest_dir: &str) -> Result<String, String> {
142    let src = PathBuf::from(source);
143    if !src.exists() {
144        return Err(format!("Source does not exist: {}", source));
145    }
146    let dest = PathBuf::from(dest_dir);
147    if !dest.is_dir() {
148        return Err(format!("Destination is not a directory: {}", dest_dir));
149    }
150
151    let file_name = src
152        .file_name()
153        .ok_or_else(|| "Cannot determine file name".to_string())?;
154    let dest_path = dest.join(file_name);
155
156    if src.is_dir() {
157        copy_dir_recursive(&src, &dest_path)?;
158    } else {
159        fs::copy(&src, &dest_path).map_err(|e| format!("Failed to copy file: {}", e))?;
160    }
161
162    Ok(dest_path.to_string_lossy().to_string())
163}
164
165fn copy_dir_recursive(src: &Path, dest: &Path) -> Result<(), String> {
166    fs::create_dir_all(dest).map_err(|e| format!("Failed to create directory: {}", e))?;
167
168    let entries =
169        fs::read_dir(src).map_err(|e| format!("Failed to read source directory: {}", e))?;
170
171    for entry in entries {
172        let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
173        let entry_dest = dest.join(entry.file_name());
174
175        if entry.path().is_dir() {
176            copy_dir_recursive(&entry.path(), &entry_dest)?;
177        } else {
178            fs::copy(entry.path(), &entry_dest)
179                .map_err(|e| format!("Failed to copy file: {}", e))?;
180        }
181    }
182
183    Ok(())
184}
185
186pub fn calculate_directory_size(path: &str) -> Result<u64, String> {
187    let dir_path = Path::new(path);
188    if !dir_path.is_dir() {
189        return Err(format!("Not a directory: {}", path));
190    }
191
192    fn walk(dir: &Path) -> u64 {
193        let mut total: u64 = 0;
194        if let Ok(entries) = fs::read_dir(dir) {
195            for entry in entries {
196                if let Ok(entry) = entry {
197                    if let Ok(meta) = entry.metadata() {
198                        if meta.is_dir() {
199                            total += walk(&entry.path());
200                        } else {
201                            total += disk_size(&meta);
202                        }
203                    }
204                }
205            }
206        }
207        total
208    }
209
210    Ok(walk(dir_path))
211}
212
213/// Returns actual disk usage (blocks * 512) on Unix, logical size on Windows.
214#[cfg(unix)]
215fn disk_size(meta: &fs::Metadata) -> u64 {
216    use std::os::unix::fs::MetadataExt;
217    meta.blocks() * 512
218}
219
220#[cfg(not(unix))]
221fn disk_size(meta: &fs::Metadata) -> u64 {
222    meta.len()
223}
224
225pub fn create_file(dir: &str, name: &str) -> Result<(), String> {
226    let path = Path::new(dir).join(name);
227    if path.exists() {
228        return Err(format!("A file named '{}' already exists", name));
229    }
230    fs::File::create(&path).map_err(|e| format!("Failed to create file: {}", e))?;
231    Ok(())
232}
233
234pub fn create_folder(dir: &str, name: &str) -> Result<(), String> {
235    let path = Path::new(dir).join(name);
236    if path.exists() {
237        return Err(format!("A folder named '{}' already exists", name));
238    }
239    fs::create_dir(&path).map_err(|e| format!("Failed to create folder: {}", e))?;
240    Ok(())
241}
242
243pub fn open_in_terminal(path: &str) -> Result<(), String> {
244    let dir = Path::new(path);
245    if !dir.is_dir() {
246        return Err(format!("Not a directory: {}", path));
247    }
248
249    #[cfg(target_os = "macos")]
250    {
251        // Prefer iTerm2 if installed, fall back to Terminal.app
252        let app = if Path::new("/Applications/iTerm.app").exists() {
253            "iTerm"
254        } else {
255            "Terminal"
256        };
257        std::process::Command::new("open")
258            .args(["-a", app, path])
259            .spawn()
260            .map_err(|e| format!("Failed to open terminal: {}", e))?;
261    }
262
263    #[cfg(target_os = "windows")]
264    {
265        std::process::Command::new("cmd")
266            .args(["/C", "start", "cmd", "/K", &format!("cd /d {}", path)])
267            .spawn()
268            .map_err(|e| format!("Failed to open terminal: {}", e))?;
269    }
270
271    #[cfg(target_os = "linux")]
272    {
273        // Try common terminal emulators in order
274        let terminals = ["x-terminal-emulator", "gnome-terminal", "konsole", "xterm"];
275        let mut launched = false;
276        for term in &terminals {
277            let result = if *term == "gnome-terminal" {
278                std::process::Command::new(term)
279                    .arg("--working-directory")
280                    .arg(path)
281                    .spawn()
282            } else {
283                std::process::Command::new(term)
284                    .current_dir(path)
285                    .spawn()
286            };
287            if result.is_ok() {
288                launched = true;
289                break;
290            }
291        }
292        if !launched {
293            return Err("No supported terminal emulator found".to_string());
294        }
295    }
296
297    Ok(())
298}
299
300pub fn move_entry(source: &str, dest_dir: &str) -> Result<String, String> {
301    let src = PathBuf::from(source);
302    if !src.exists() {
303        return Err(format!("Source does not exist: {}", source));
304    }
305    let dest = PathBuf::from(dest_dir);
306    if !dest.is_dir() {
307        return Err(format!("Destination is not a directory: {}", dest_dir));
308    }
309
310    let file_name = src
311        .file_name()
312        .ok_or_else(|| "Cannot determine file name".to_string())?;
313    let dest_path = dest.join(file_name);
314
315    // Try fast rename first (works on same volume)
316    match fs::rename(&src, &dest_path) {
317        Ok(()) => return Ok(dest_path.to_string_lossy().to_string()),
318        Err(_) => {
319            // Cross-volume: copy then delete
320            copy_entry(source, dest_dir)?;
321            if src.is_dir() {
322                fs::remove_dir_all(&src)
323                    .map_err(|e| format!("Copied but failed to remove source: {}", e))?;
324            } else {
325                fs::remove_file(&src)
326                    .map_err(|e| format!("Copied but failed to remove source: {}", e))?;
327            }
328            Ok(dest_path.to_string_lossy().to_string())
329        }
330    }
331}