use std::{ fs::read_dir, mem, ops::Deref, path::{Path, PathBuf}, sync::mpsc, thread, }; use egui::{Response, Ui}; use eyre::{Context, OptionExt, eyre}; use serde::{Deserialize, Serialize}; pub enum Folder { NotLoaded { name: String, path: PathBuf, }, Loading { name: String, path: PathBuf, recv: mpsc::Receiver, }, Loaded(LoadedFolder), } pub struct LoadedFolder { pub name: String, pub path: PathBuf, pub child_folders: Vec, pub child_files: Vec, } pub struct File { pub name: String, pub path: PathBuf, } pub struct FolderResponse<'a> { inner: Response, pub open_file: Option<&'a Path>, } impl Deref for FolderResponse<'_> { type Target = Response; fn deref(&self) -> &Self::Target { &self.inner } } impl LoadedFolder { pub fn show<'a>(&'a mut self, ui: &mut Ui) -> FolderResponse<'a> { let mut open_file = None; let inner = ui .collapsing(&self.name, |ui| { for folder in &mut self.child_folders { open_file = open_file.or(folder.show(ui).open_file); } for file in &mut self.child_files { if ui.button(&file.name).clicked() { open_file = Some(file.path.as_path()) }; } }) .header_response; FolderResponse { inner, open_file } } fn load(path: PathBuf) -> eyre::Result { let name = path .file_name() .ok_or_eyre("Path is missing a file-name")? .to_string_lossy() .to_string(); let mut child_folders = vec![]; let mut child_files = vec![]; for entry in read_dir(&path).with_context(|| eyre!("Couldn't read dir {path:?}"))? { let entry = entry.with_context(|| eyre!("Couldn't read dir {path:?}"))?; let path = entry.path(); let name = path .file_name() .ok_or_eyre("Path is missing a file-name")? .to_string_lossy() .to_string(); let file_type = entry.file_type()?; if file_type.is_symlink() { log::error!("Symlinks not yet supported, skipping {path:?}"); continue; } else if file_type.is_file() { child_files.push(File { name, path }); } else if file_type.is_dir() { child_folders.push(Folder::NotLoaded { name, path }); } } let folder = LoadedFolder { name, path, child_folders, child_files, }; Ok(folder) } } impl Folder { fn load(&mut self, ui: &mut Ui) -> Option<&mut LoadedFolder> { if let Folder::NotLoaded { name, path } = self { let (tx, rx) = mpsc::channel(); { let path = path.clone(); let ctx = ui.ctx().clone(); thread::spawn(move || match LoadedFolder::load(path) { Err(e) => log::error!("Failed to load folder: {e}"), Ok(folder) => { let _ = tx.send(folder); ctx.request_repaint(); } }); } *self = Folder::Loading { name: mem::take(name), path: mem::take(path), recv: rx, }; } if let Folder::Loading { recv, .. } = self { match recv.try_recv() { Ok(folder) => *self = Folder::Loaded(folder), Err(_) => return None, } } let Folder::Loaded(folder) = self else { unreachable!() }; Some(folder) } pub fn show<'a>(&'a mut self, ui: &mut Ui) -> FolderResponse<'a> { self.load(ui); if let Folder::Loaded(folder) = self { return folder.show(ui); } FolderResponse { inner: ui.label(self.name()), open_file: None, } } pub fn path(&self) -> &Path { match self { Folder::NotLoaded { path, .. } => path, Folder::Loading { path, .. } => path, Folder::Loaded(folder) => &folder.path, } } pub fn name(&self) -> &str { match self { Folder::NotLoaded { name, .. } => name, Folder::Loading { name, .. } => name, Folder::Loaded(folder) => &folder.name, } } pub fn unload(&mut self) { let (name, path) = match self { Folder::NotLoaded { .. } => return, Folder::Loading { name, path, .. } => (name, path), Folder::Loaded(folder) => (&mut folder.name, &mut folder.path), }; *self = Folder::NotLoaded { name: mem::take(name), path: mem::take(path), } } } impl Serialize for Folder { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { self.path().serialize(serializer) } } impl<'de> Deserialize<'de> for Folder { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { use serde::de::Error; let path = PathBuf::deserialize(deserializer)?; let name = path .file_name() .ok_or(D::Error::custom("Path is missing a file-name"))? .to_string_lossy() .to_string(); Ok(Folder::NotLoaded { name, path }) } }