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#[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 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 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 match fs::rename(&src, &dest_path) {
317 Ok(()) => return Ok(dest_path.to_string_lossy().to_string()),
318 Err(_) => {
319 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}