diff --git a/src/fleen_app.rs b/src/fleen_app.rs index 13f5b3c..d9d6abb 100644 --- a/src/fleen_app.rs +++ b/src/fleen_app.rs @@ -1,4 +1,6 @@ use std::cell::RefCell; +use std::fs; +use std::io::Error; use std::path::PathBuf; use std::process::Command; use thiserror::Error; @@ -12,7 +14,11 @@ pub enum FleenError { #[error("Root dir is nonempty, you probably don't want to create an app here: {0}")] RootDirPopulatedError(PathBuf), #[error("Failed to open {0}: {1}")] - FileOpenError(String, String) + FileIoError(String, String), + #[error("Can't create {0} because it already exists")] + FileExistsError(PathBuf), + #[error("Can't create {0}: {1}")] + FileCreateError(PathBuf, String) } #[derive(Clone, Debug)] @@ -22,6 +28,11 @@ pub enum TreeEntry { CloseDir } +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum FileType { + File, Dir +} + pub struct FleenApp { root: PathBuf, files_cache: RefCell>> @@ -83,8 +94,41 @@ impl FleenApp { pub fn open_filename(&self, filename: &String) -> Result<(), FleenError> { Command::new("open").arg(filename.clone()).spawn().map_err(|err| { - FleenError::FileOpenError(filename.clone(), err.to_string()) + FleenError::FileIoError(filename.clone(), err.to_string()) })?; Ok(()) } + + pub fn create_page(&self, file_type: FileType, name: &String, parent: Option<&String>) -> Result<(), FleenError> { + let mut target = match parent { + Some(s) => PathBuf::from(s), + None => self.root.clone() + }; + target.push(name.clone()); + if target.exists() { + return Err(FleenError::FileExistsError(target)) + } + + match file_type { + FileType::File => std::fs::write(target.clone(), []), + FileType::Dir => std::fs::create_dir(target.clone()) + }.map_err(|err| FleenError::FileCreateError(target.clone(), err.to_string()))?; + + self.refresh_file_cache(true); + if file_type == FileType::File { + self.open_filename(&target.to_string_lossy().to_string())? + } + Ok(()) + } + + pub fn delete_page(&self, path: &String) -> Result<(), FleenError> { + let target = PathBuf::from(path); + if target.is_dir() { + fs::remove_dir_all(target) + } else { + fs::remove_file(target) + }.map_err(|err| FleenError::FileIoError(path.clone(), err.to_string()))?; + self.refresh_file_cache(true); + Ok(()) + } } diff --git a/src/main.rs b/src/main.rs index 120fb48..654a181 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use eframe::egui::{Color32, Context, Id, Stroke}; use eframe::{egui, Frame}; use egui_ltreeview::Action; -use crate::fleen_app::{FleenApp, FleenError, TreeEntry}; +use crate::fleen_app::{FileType, FleenApp, FleenError, TreeEntry}; fn main() { let native_options = eframe::NativeOptions::default(); @@ -13,10 +13,17 @@ fn main() { })).expect("Error running application"); } +enum DialogMode { + NewFile(String), + ConfirmDelete(String), + RenameFile(String) +} + struct FleenUi { app: Option, error: Option, - selected_file: Option + selected_file: Option, + dialog_mode: Option } impl Default for FleenUi { @@ -24,7 +31,8 @@ impl Default for FleenUi { Self { app: None, error: None, - selected_file: None + selected_file: None, + dialog_mode: None } } } @@ -55,6 +63,8 @@ impl FleenUi { } fn display(&mut self, ctx: &Context) { + let mut new_clicked = false; + egui::CentralPanel::default().show(ctx, |ui| { ui.horizontal(|ui| { ui.vertical(|ui| { @@ -64,10 +74,31 @@ impl FleenUi { self.handle_error(self.app.as_ref().unwrap().open_filename(fname)) } } - ui.add(egui::Button::new("New page")).clicked(); + + if ui.add(egui::Button::new("New page")).clicked() { + new_clicked = true; + self.dialog_mode = Some(DialogMode::NewFile(String::new())); + } + + + let delete_btn = egui::Button::new("Delete").fill(Color32::DARK_RED); + if let Some(selected) = &self.selected_file { + if ui.add(delete_btn).clicked() { + self.dialog_mode = Some(DialogMode::ConfirmDelete(selected.clone())); + } + } else { + ui.add_enabled(false, delete_btn); + } }); }) }); + + match self.dialog_mode { + Some(DialogMode::NewFile(_)) => self.new_file_dialog(ctx, new_clicked), + Some(DialogMode::ConfirmDelete(_)) => self.confirm_delete_dialog(ctx), + Some(DialogMode::RenameFile(_)) => todo!(), + None => {} + } } fn tree_view(&mut self, ui: &mut egui::Ui) { @@ -99,6 +130,72 @@ impl FleenUi { } } + fn new_file_dialog(&mut self, ctx: &Context, just_clicked: bool) { + egui::Window::new("New page").collapsible(false).resizable(false).show(ctx, |ui| { + ui.label("Name"); + // If we press enter on the text field, it emits lost_focus. We can't just create the file based + // on that, because it's not the only thing that causes it, though. So instead we'll just focus the + // create file button + //let fname = match &mut self.dialog_mode { Some(DialogMode::NewFile(s)) => s, _ => unreachable!() }; + let Some(DialogMode::NewFile(fname)) = &mut self.dialog_mode else { unreachable!() }; + let name_field = egui::TextEdit::singleline(fname); + let resp = ui.add(name_field); + let enter_key = resp.lost_focus(); + + // We want to focus the text field if we just opened the dialog, but egui doesn't currently + // offer a way to do that. However, we can pass in a bool for if the button that opens the + // dialog was just clicked, and use that to tell whether we should grab focus. It's only true + // the exact frame that the dialog was opened, which is what we're looking for. + if just_clicked { resp.request_focus() } + + ui.horizontal(|ui| { + let mut make_thing = |file_type: FileType| { + let Some(DialogMode::NewFile(fname)) = &self.dialog_mode else { unreachable!() }; + let app = self.app.as_ref().unwrap(); + let r = app.create_page(file_type, + fname, + self.selected_file.as_ref()); + if r.is_err() { + self.handle_error(r); + } else { + self.dialog_mode = None; // Close the dialog, we're done + } + }; + + let btn = ui.button("New file"); + if enter_key { btn.request_focus() } + if btn.clicked() { make_thing(FileType::File) } + if ui.button("New directory").clicked() { make_thing(FileType::Dir) } + if ui.button("Cancel").clicked() { + self.dialog_mode = None + } + }) + }); + } + + fn confirm_delete_dialog(&mut self, ctx: &Context) { + let (mut del, mut cancel) = (false, false); + let Some(DialogMode::ConfirmDelete(fname)) = &self.dialog_mode else { unreachable!() }; + egui::Window::new("Are you sure?").collapsible(false).resizable(false).show(ctx, |ui| { + let file = label_for_path(&PathBuf::from(fname)); + ui.heading(format!("Really delete {}?", file)); + ui.horizontal(|ui| { + let btn = egui::Button::new("Yep, I'm sure").fill(Color32::DARK_RED); + del = ui.add(btn).clicked(); + cancel = ui.button("Cancel").clicked(); + }) + }); + + if del { + let r = self.app.as_ref().unwrap().delete_page(&fname.clone()); + self.dialog_mode = None; + if r.is_err() { self.handle_error(r); } + self.selected_file = None; + } else if cancel { + self.dialog_mode = None + } + } + fn handle_error(&mut self, result: Result<(), FleenError>) { if let Err(e) = result { self.error = Some(e)