use std::collections::HashSet;
use std::{
fs,
path::{Path, PathBuf},
};
#[derive(Debug, Clone)]
pub enum FileEntry {
File {
path: PathBuf,
name: String,
},
Directory {
path: PathBuf,
name: String,
children: Vec<FileEntry>, },
}
#[derive(Debug, Clone)]
pub struct FileTree {
pub root: PathBuf,
pub entries: Vec<FileEntry>,
pub expanded: HashSet<PathBuf>,
pub selected: Option<PathBuf>,
}
impl FileTree {
pub fn new(root: PathBuf) -> Self {
let entries = scan_directory(&root);
Self {
root,
entries,
expanded: HashSet::new(),
selected: None,
}
}
pub fn toggle_folder(&mut self, path: &Path) {
if self.expanded.contains(path) {
self.expanded.remove(path);
} else {
self.expanded.insert(path.to_path_buf());
populate_children(&mut self.entries, path); }
}
pub fn is_expanded(&self, path: &Path) -> bool {
self.expanded.contains(path)
}
pub fn select(&mut self, path: PathBuf) {
self.selected = Some(path);
}
pub fn refresh(&mut self) {
self.entries = scan_directory(&self.root);
let mut expanded: Vec<PathBuf> = self.expanded.iter().cloned().collect();
expanded.sort_by_key(|p| p.components().count());
for path in expanded {
populate_children(&mut self.entries, &path);
}
}
}
const IGNORED_DIRS: &[&str] = &[".git", "node_modules", "target", ".DS_Store", "__pycache__"];
#[derive(Debug, Clone)]
pub struct ContextMenuTarget {
pub path: PathBuf,
pub is_dir: bool,
pub position: iced::Point,
}
fn scan_directory(path: &Path) -> Vec<FileEntry> {
let mut entries = Vec::new();
let Ok(read_dir) = fs::read_dir(path) else {
return entries; };
for entry in read_dir.flatten() {
let entry_path = entry.path();
let name = entry.file_name().to_string_lossy().to_string();
if IGNORED_DIRS.contains(&name.as_str()) {
continue;
}
if entry_path.is_dir() {
entries.push(FileEntry::Directory {
path: entry_path,
name,
children: Vec::new(),
});
} else {
entries.push(FileEntry::File {
path: entry_path,
name,
});
}
}
entries.sort_by(|a, b| match (a, b) {
(FileEntry::Directory { name: name_a, .. }, FileEntry::Directory { name: name_b, .. }) => {
name_a.to_lowercase().cmp(&name_b.to_lowercase())
},
(FileEntry::File { name: name_a, .. }, FileEntry::File { name: name_b, .. }) => {
name_a.to_lowercase().cmp(&name_b.to_lowercase())
},
(FileEntry::Directory { .. }, FileEntry::File { .. }) => std::cmp::Ordering::Less,
(FileEntry::File { .. }, FileEntry::Directory { .. }) => std::cmp::Ordering::Greater,
});
entries
}
fn populate_children(entries: &mut [FileEntry], target: &Path) {
for entry in entries.iter_mut() {
if let FileEntry::Directory { path, children, .. } = entry {
if path == target {
if children.is_empty() {
*children = scan_directory(path);
}
return;
}
populate_children(children, target);
}
}
}