use std::path::{Path, PathBuf}; use std::fs; #[derive(Clone, Debug, PartialEq)] pub enum FileType { Directory, File, Symlink, } #[derive(Clone, Debug)] pub struct FileEntry { pub name: String, pub path: PathBuf, pub file_type: FileType, pub size: u64, pub is_hidden: bool, } impl FileEntry { pub fn icon(&self) -> &'static str { match &self.file_type { FileType::Directory => "📁", FileType::Symlink => "🔗", FileType::File => { let ext = self .name .rsplit('.') .next() .unwrap_or("") .to_lowercase(); match ext.as_str() { "rs" => "🦀", "png" | "jpg" | "jpeg" | "gif" | "bmp" | "svg" | "webp" | "ico" => "🖼", "mp4" | "mkv" | "avi" | "mov" | "webm" => "🎬", "mp3" | "flac" | "ogg" | "wav" | "aac" => "🎵", "zip" | "tar" | "gz" | "bz2" | "xz" | "7z" | "rar" => "📦", "pdf" => "📕", "txt" | "md" | "log" => "📄", "json" | "yaml" | "yml" | "toml" | "xml" => "⚙", "sh" | "bash" | "zsh" | "fish" => "🐚", "py" => "🐍", "js" | "ts" => "📜", "c" | "cpp" | "h" | "hpp" => "🔧", "exe" | "bin" | "out" | "run" => "⚙", _ => "📎", } } } } /// ASCII fallback icons for terminals that don't support emoji (e.g. SSH with limited fonts) pub fn icon_ascii(&self) -> &'static str { match &self.file_type { FileType::Directory => "[/]", FileType::Symlink => "[~]", FileType::File => { let ext = self .name .rsplit('.') .next() .unwrap_or("") .to_lowercase(); match ext.as_str() { "rs" | "py" | "js" | "ts" | "c" | "cpp" | "h" | "hpp" | "sh" | "bash" | "zsh" | "fish" => "[s]", "png" | "jpg" | "jpeg" | "gif" | "bmp" | "svg" | "webp" | "ico" => "[i]", "mp4" | "mkv" | "avi" | "mov" | "webm" => "[v]", "mp3" | "flac" | "ogg" | "wav" | "aac" => "[a]", "zip" | "tar" | "gz" | "bz2" | "xz" | "7z" | "rar" => "[z]", "pdf" => "[p]", "txt" | "md" | "log" => "[t]", "json" | "yaml" | "yml" | "toml" | "xml" => "[c]", "exe" | "bin" | "out" | "run" => "[x]", _ => "[ ]", } } } } pub fn is_dir(&self) -> bool { self.file_type == FileType::Directory } pub fn formatted_size(&self) -> String { if self.file_type == FileType::Directory { return String::from(""); } format_size(self.size) } } pub fn format_size(bytes: u64) -> String { const KB: u64 = 1024; const MB: u64 = 1024 * KB; const GB: u64 = 1024 * MB; if bytes >= GB { format!("{:.1} GB", bytes as f64 / GB as f64) } else if bytes >= MB { format!("{:.1} MB", bytes as f64 / MB as f64) } else if bytes >= KB { format!("{} KB", bytes / KB) } else { format!("{} B", bytes) } } pub fn read_dir(path: &Path) -> Vec { let mut entries = Vec::new(); let Ok(dir) = fs::read_dir(path) else { return entries; }; for entry in dir.flatten() { let name = entry.file_name().to_string_lossy().to_string(); let path = entry.path(); let is_hidden = name.starts_with('.'); let (file_type, size) = if let Ok(meta) = entry.metadata() { let ft = if meta.is_symlink() { FileType::Symlink } else if meta.is_dir() { FileType::Directory } else { FileType::File }; (ft, meta.len()) } else { (FileType::File, 0) }; entries.push(FileEntry { name, path, file_type, size, is_hidden }); } // Sort: dirs first, then files, both alphabetically entries.sort_by(|a, b| { match (&a.file_type, &b.file_type) { (FileType::Directory, FileType::Directory) => a.name.to_lowercase().cmp(&b.name.to_lowercase()), (FileType::Directory, _) => std::cmp::Ordering::Less, (_, FileType::Directory) => std::cmp::Ordering::Greater, _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()), } }); entries } /// Recursively compute total size and file count of a path pub fn recursive_info(path: &Path) -> (u64, u64) { let mut total_size: u64 = 0; let mut file_count: u64 = 0; if path.is_file() { let size = path.metadata().map(|m| m.len()).unwrap_or(0); return (size, 1); } if let Ok(dir) = fs::read_dir(path) { for entry in dir.flatten() { let p = entry.path(); if p.is_dir() { let (s, c) = recursive_info(&p); total_size += s; file_count += c; } else { total_size += p.metadata().map(|m| m.len()).unwrap_or(0); file_count += 1; } } } (total_size, file_count) } #[cfg(unix)] pub fn file_permissions(path: &Path) -> String { use std::os::unix::fs::PermissionsExt; let mode = path.metadata().map(|m| m.permissions().mode()).unwrap_or(0); format!("{:o}", mode & 0o777) } #[cfg(windows)] pub fn file_permissions(_path: &Path) -> String { String::from("N/A") } pub fn file_owner(path: &Path) -> (String, String) { #[cfg(unix)] { use std::os::unix::fs::MetadataExt; if let Ok(meta) = path.metadata() { let uid = meta.uid(); let gid = meta.gid(); let user_name = lookup_name("/etc/passwd", uid) .map(|n| format!("{} ({})", n, uid)) .unwrap_or_else(|| uid.to_string()); let group_name = lookup_name("/etc/group", gid) .map(|n| format!("{} ({})", n, gid)) .unwrap_or_else(|| gid.to_string()); return (user_name, group_name); } } #[cfg(windows)] { let _ = path; } (String::from("N/A"), String::from("N/A")) } /// Parse /etc/passwd or /etc/group and return the name for a given numeric id. /// Both files share the same format: `name:x:id:...` #[cfg(unix)] fn lookup_name(db_file: &str, id: u32) -> Option { let content = std::fs::read_to_string(db_file).ok()?; for line in content.lines() { let mut parts = line.splitn(4, ':'); let name = parts.next()?; parts.next(); // password/placeholder let entry_id: u32 = parts.next()?.parse().ok()?; if entry_id == id { return Some(name.to_string()); } } None } /// Copy a file or directory recursively pub fn copy_entry(src: &Path, dst_dir: &Path) -> Result<(), String> { let name = src.file_name().ok_or("No filename")?; let dst = dst_dir.join(name); if src.is_dir() { fs_extra::dir::copy( src, dst_dir, &fs_extra::dir::CopyOptions::new(), ) .map(|_| ()) .map_err(|e| e.to_string()) } else { fs::copy(src, &dst).map(|_| ()).map_err(|e| e.to_string()) } } /// Move (cut/paste) a file or directory pub fn move_entry(src: &Path, dst_dir: &Path) -> Result<(), String> { let name = src.file_name().ok_or("No filename")?; let dst = dst_dir.join(name); if src.is_dir() { fs_extra::dir::move_dir( src, dst_dir, &fs_extra::dir::CopyOptions::new(), ) .map(|_| ()) .map_err(|e| e.to_string()) } else { fs::rename(src, &dst).map_err(|e| e.to_string()) } } pub fn delete_entry(path: &Path) -> Result<(), String> { if path.is_dir() { fs::remove_dir_all(path).map_err(|e| e.to_string()) } else { fs::remove_file(path).map_err(|e| e.to_string()) } } pub fn get_root_dirs() -> Vec { #[cfg(unix)] { let roots = ["/", "/home", "/etc", "/usr", "/var", "/opt", "/tmp"]; roots .iter() .filter_map(|r| { let p = PathBuf::from(r); if p.exists() { Some(p) } else { None } }) .collect() } #[cfg(windows)] { let mut drives = Vec::new(); for letter in b'A'..=b'Z' { let d = format!("{}:\\", letter as char); let p = PathBuf::from(&d); if p.exists() { drives.push(p); } } drives } }