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
84pub 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
135pub 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#[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 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 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
331pub fn open_in_terminal_with_command(command: &str, args: &[&str]) -> Result<(), String> {
333 #[cfg(target_os = "macos")]
334 {
335 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 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 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 match fs::rename(&src, &dest_path) {
448 Ok(()) => return Ok(dest_path.to_string_lossy().to_string()),
449 Err(_) => {
450 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}