Files
TUI-FM/src/file_ops.rs
2026-03-29 03:19:33 +02:00

297 lines
8.8 KiB
Rust

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("<dir>");
}
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<FileEntry> {
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<String> {
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<PathBuf> {
#[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
}
}