From 7648748d4d8f4a7a2d30aa3f9d939751240b8e0b Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Mon, 16 Sep 2024 23:31:12 -0300 Subject: [PATCH 01/23] wip: massive refactor of collection storage and layout --- .rustfmt.toml | 11 + Cargo.lock | 22 + Cargo.toml | 12 +- hac-client/Cargo.toml | 2 + hac-client/src/app.rs | 20 +- hac-client/src/main.rs | 40 +- .../collection_dashboard.rs | 1706 ++++++++--------- .../collection_dashboard/collection_list.rs | 392 ++-- .../collection_viewer/collection_viewer.rs | 30 +- hac-client/src/screen_manager.rs | 320 ++-- hac-client/tests/collection_dashboard.rs | 457 ----- hac-config/src/config.rs | 10 +- hac-config/src/lib.rs | 13 +- hac-core/src/collection.rs | 5 - hac-core/src/collection/collection.rs | 71 - hac-core/src/collection/mod.rs | 75 + hac-core/src/fs/fs.rs | 90 +- hac-loader/Cargo.toml | 14 + .../src/collection_loader/json_collection.rs | 79 + .../src/collection_loader/json_loader.rs | 172 ++ hac-loader/src/collection_loader/mod.rs | 53 + .../src/data.rs => hac-loader/src/lib.rs | 19 +- hac-loader/test_collections/basic.json | 37 + hac-loader/test_collections/complex.json | 84 + hac-store/Cargo.toml | 7 + hac-store/src/collection.rs | 66 + hac-store/src/lib.rs | 2 + hac-store/src/slab.rs | 127 ++ 28 files changed, 2071 insertions(+), 1865 deletions(-) create mode 100644 .rustfmt.toml delete mode 100755 hac-client/tests/collection_dashboard.rs delete mode 100755 hac-core/src/collection.rs delete mode 100755 hac-core/src/collection/collection.rs create mode 100644 hac-core/src/collection/mod.rs create mode 100644 hac-loader/Cargo.toml create mode 100644 hac-loader/src/collection_loader/json_collection.rs create mode 100644 hac-loader/src/collection_loader/json_loader.rs create mode 100644 hac-loader/src/collection_loader/mod.rs rename hac-config/src/data.rs => hac-loader/src/lib.rs (76%) mode change 100755 => 100644 create mode 100644 hac-loader/test_collections/basic.json create mode 100644 hac-loader/test_collections/complex.json create mode 100644 hac-store/Cargo.toml create mode 100644 hac-store/src/collection.rs create mode 100644 hac-store/src/lib.rs create mode 100644 hac-store/src/slab.rs diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..ba69af2 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,11 @@ +edition = "2021" +unstable_features = true +use_field_init_shorthand = true +imports_granularity = "Module" +group_imports = "StdExternalCrate" +format_code_in_doc_comments = true +reorder_impl_items = true +single_line_let_else_max_width = 100 +single_line_if_else_max_width = 100 +max_width = 120 + diff --git a/Cargo.lock b/Cargo.lock index f1b2a8c..d093fcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -604,6 +604,8 @@ dependencies = [ "hac-colors", "hac-config", "hac-core", + "hac-loader", + "hac-store", "lazy_static", "mutants", "rand", @@ -662,6 +664,26 @@ dependencies = [ "tree-sitter-json", ] +[[package]] +name = "hac-loader" +version = "0.2.0" +dependencies = [ + "anyhow", + "dirs", + "hac-config", + "hac-store", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "hac-store" +version = "0.2.0" +dependencies = [ + "serde", +] + [[package]] name = "hashbrown" version = "0.14.5" diff --git a/Cargo.toml b/Cargo.toml index d8e388c..45c3ab3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,13 @@ [workspace] -members = ["hac-config", "hac-core", "hac-client", "hac-colors", "hac-cli"] +members = [ + "hac-config", + "hac-core", + "hac-client", + "hac-colors", + "hac-cli", + "hac-store", + "hac-loader", +] default-members = ["hac-client"] resolver = "2" @@ -10,10 +18,12 @@ name = "hac" version = "0.2.0" [workspace.dependencies] +hac-store = { path = "hac-store" } hac-config = { path = "hac-config" } hac-core = { path = "hac-core" } hac-colors = { path = "hac-colors" } hac-cli = { path = "hac-cli" } +hac-loader = { path = "hac-loader" } anyhow = "1.0.81" crossterm = { version = "0.27.0", features = ["event-stream"] } diff --git a/hac-client/Cargo.toml b/hac-client/Cargo.toml index 4a831c2..e1e8f25 100644 --- a/hac-client/Cargo.toml +++ b/hac-client/Cargo.toml @@ -18,6 +18,8 @@ hac-core.workspace = true hac-config.workspace = true hac-colors.workspace = true hac-cli.workspace = true +hac-loader.workspace = true +hac-store.workspace = true anyhow.workspace = true crossterm.workspace = true diff --git a/hac-client/src/app.rs b/hac-client/src/app.rs index a9dad9a..6986359 100755 --- a/hac-client/src/app.rs +++ b/hac-client/src/app.rs @@ -1,4 +1,5 @@ -use hac_core::{collection::Collection, command::Command}; +use hac_core::command::Command; +use hac_loader::collection_loader::CollectionMeta; use crate::event_pool::{Event, EventPool}; use crate::pages::{Eventful, Renderable}; @@ -19,19 +20,13 @@ pub struct App<'app> { impl<'app> App<'app> { pub fn new( colors: &'app hac_colors::Colors, - collections: Vec, + collections: Vec, config: &'app hac_config::Config, dry_run: bool, ) -> anyhow::Result { let terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?; Ok(Self { - screen_manager: ScreenManager::new( - terminal.size()?, - colors, - collections, - config, - dry_run, - )?, + screen_manager: ScreenManager::new(terminal.size()?, colors, collections, config, dry_run)?, event_pool: EventPool::new(60f64, 30f64), should_quit: false, terminal, @@ -46,8 +41,7 @@ impl<'app> App<'app> { startup()?; - self.screen_manager - .register_command_handler(command_tx.clone())?; + self.screen_manager.register_command_handler(command_tx.clone())?; loop { { @@ -74,9 +68,7 @@ impl<'app> App<'app> { })?; } event => { - if let Some(command) = - self.screen_manager.handle_event(Some(event.clone()))? - { + if let Some(command) = self.screen_manager.handle_event(Some(event.clone()))? { command_tx .send(command) .expect("failed to send command through channel") diff --git a/hac-client/src/main.rs b/hac-client/src/main.rs index 47dddb2..0be96e2 100755 --- a/hac-client/src/main.rs +++ b/hac-client/src/main.rs @@ -1,9 +1,10 @@ use hac_cli::RuntimeBehavior; use hac_client::app; -use hac_core::collection::collection; +use hac_core::collection; fn setup_tracing() -> anyhow::Result { - let (data_dir, logfile) = hac_config::log_file(); + let logfile = hac_config::LOGFILE; + let data_dir = hac_loader::data_dir(); let appender = tracing_appender::rolling::never(data_dir, logfile); let (writer, guard) = tracing_appender::non_blocking(appender); let subscriber = tracing_subscriber::FmtSubscriber::builder() @@ -21,29 +22,30 @@ fn setup_tracing() -> anyhow::Result anyhow::Result<()> { let runtime_behavior = hac_cli::Cli::parse_args(); - match runtime_behavior { - RuntimeBehavior::PrintConfigPath => hac_cli::Cli::print_config_path( - hac_config::get_config_dir_path(), - hac_config::get_usual_path(), - ), - RuntimeBehavior::PrintDataPath => { - hac_cli::Cli::print_data_path(hac_config::get_collections_dir()) - } - RuntimeBehavior::DumpDefaultConfig => { - hac_cli::Cli::print_default_config(hac_config::default_as_str()) - } - _ => {} - } + //match runtime_behavior { + // RuntimeBehavior::PrintConfigPath => hac_cli::Cli::print_config_path( + // hac_config::get_config_dir_path(), + // hac_config::get_usual_path(), + // ), + // RuntimeBehavior::PrintDataPath => { + // hac_cli::Cli::print_data_path(hac_config::get_collections_dir()) + // } + // RuntimeBehavior::DumpDefaultConfig => { + // hac_cli::Cli::print_default_config(hac_config::default_as_str()) + // } + // _ => {} + //} let dry_run = runtime_behavior.eq(&RuntimeBehavior::DryRun); let _guard = setup_tracing()?; - hac_config::get_or_create_data_dir(); - let config = hac_config::load_config(); + hac_loader::get_or_create_data_dir(); + hac_loader::get_or_create_collections_dir(); + let collections = hac_loader::collection_loader::collections_metadata()?; let colors = hac_colors::Colors::default(); - let mut collections = collection::get_collections_from_config()?; - collections.sort_by_key(|key| key.info.name.clone()); + let config = hac_config::load_config(); + let mut app = app::App::new(&colors, collections, &config, dry_run)?; app.run().await?; diff --git a/hac-client/src/pages/collection_dashboard/collection_dashboard.rs b/hac-client/src/pages/collection_dashboard/collection_dashboard.rs index d8bb7e1..cdc477f 100755 --- a/hac-client/src/pages/collection_dashboard/collection_dashboard.rs +++ b/hac-client/src/pages/collection_dashboard/collection_dashboard.rs @@ -1,9 +1,9 @@ -use hac_core::{collection::types::Collection, command::Command}; +use hac_core::command::Command; +use hac_loader::collection_loader::CollectionMeta; +use hac_store::collection::Collection; use crate::pages::collection_dashboard::collection_list::{CollectionList, CollectionListState}; -use crate::pages::collection_dashboard::new_collection_form::{ - FormFocus, FormState, NewCollectionForm, -}; +use crate::pages::collection_dashboard::new_collection_form::{FormFocus, FormState, NewCollectionForm}; use crate::pages::confirm_popup::ConfirmPopup; use crate::pages::error_popup::ErrorPopup; use crate::pages::overlay::{draw_overlay, make_overlay}; @@ -33,13 +33,13 @@ struct DashboardLayout { #[derive(Debug)] pub struct CollectionDashboard<'a> { - layout: DashboardLayout, - collections: Vec, + colors: &'a hac_colors::Colors, + collections: Vec, + selected: usize, + layout: DashboardLayout, list: CollectionList<'a>, - list_state: CollectionListState, form_state: FormState, - colors: &'a hac_colors::Colors, filter: String, pane_focus: PaneFocus, pub command_sender: Option>, @@ -61,21 +61,16 @@ impl<'a> CollectionDashboard<'a> { pub fn new( size: Rect, colors: &'a hac_colors::Colors, - collections: Vec, + collections: Vec, dry_run: bool, ) -> anyhow::Result { - let mut list_state = CollectionListState::new(collections.clone()); - collections - .is_empty() - .not() - .then(|| list_state.select(Some(0))); - Ok(CollectionDashboard { - list_state, - form_state: FormState::default(), + collections, colors, + selected: 0, + + form_state: FormState::default(), layout: build_layout(size), - collections, list: CollectionList::new(colors), filter: String::new(), command_sender: None, @@ -85,457 +80,451 @@ impl<'a> CollectionDashboard<'a> { }) } - pub fn display_error(&mut self, message: String) { - self.pane_focus = PaneFocus::Error; - self.error_message = message; - } - - fn filter_list(&mut self) { - self.list_state.set_items( - self.collections - .clone() - .into_iter() - .filter(|s| s.info.name.contains(&self.filter)) - .collect(), - ); - self.list_state.select(None); - } - - fn handle_filter_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - match (key_event.code, key_event.modifiers) { - (KeyCode::Char('c'), KeyModifiers::CONTROL) | (KeyCode::Esc, _) => { - self.pane_focus = PaneFocus::List; - self.filter = String::new(); - self.filter_list(); - } - (KeyCode::Backspace, _) => { - if self.filter.is_empty() { - self.pane_focus = PaneFocus::List; - } - self.filter.pop(); - self.filter_list(); - } - (KeyCode::Enter, _) => { - self.pane_focus = PaneFocus::List; - self.filter_list(); - } - (KeyCode::Char(c), _) => { - self.filter.push(c); - self.filter_list(); - } - _ => {} - }; - - Ok(None) - } - - fn handle_list_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - match key_event.code { - KeyCode::Enter => { - return Ok(self - .list_state - .items - .is_empty() - .not() - .then(|| { - self.list_state - .selected() - .and_then(|i| self.collections.get(i)) - .expect( - "user should never be allowed to select a non existing collection", - ) - }) - .map(|collection| { - tracing::debug!("selected collection: {}", collection.info.name); - Command::SelectCollection(collection.clone()) - })); - } - KeyCode::Char('d') => { - if self.list_state.selected().is_some() { - self.pane_focus = PaneFocus::Prompt; - } - } - KeyCode::Char('n') | KeyCode::Char('c') => { - self.pane_focus = PaneFocus::Form; - } - KeyCode::Char('h') | KeyCode::Left => { - if !self.list_state.items.is_empty() { - self.list_state.select( - self.list_state - .selected() - .map(|i| i.saturating_sub(1)) - .or(Some(0)), - ); - } - } - KeyCode::Char('j') | KeyCode::Down => { - if !self.list_state.items.is_empty() { - self.list_state.select( - self.list_state - .selected() - .map(|i| { - usize::min( - self.list_state.items.len() - 1, - i + self.list.items_per_row(&self.layout.collections_pane), - ) - }) - .or(Some(0)), - ); - } - } - KeyCode::Char('k') | KeyCode::Up => { - if !self.list_state.items.is_empty() { - self.list_state.select( - self.list_state - .selected() - .map(|i| { - i.saturating_sub( - self.list.items_per_row(&self.layout.collections_pane), - ) - }) - .or(Some(0)), - ); - } - } - KeyCode::Char('l') | KeyCode::Right => { - if !self.list_state.items.is_empty() { - self.list_state.select( - self.list_state - .selected() - .map(|i| usize::min(self.list_state.items.len().sub(1), i.add(1))) - .or(Some(0)), - ); - } - } - KeyCode::Char('?') => self.pane_focus = PaneFocus::Help, - KeyCode::Char('/') => self.pane_focus = PaneFocus::Filter, - _ => {} - }; - Ok(None) - } - - fn handle_form_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - match (key_event.code, key_event.modifiers) { - (KeyCode::Tab, _) => match self.form_state.focused_field { - FormFocus::Name => self.form_state.focused_field = FormFocus::Description, - FormFocus::Description => self.form_state.focused_field = FormFocus::Confirm, - FormFocus::Confirm => self.form_state.focused_field = FormFocus::Cancel, - FormFocus::Cancel => self.form_state.focused_field = FormFocus::Name, - }, - (KeyCode::Char(c), _) => match self.form_state.focused_field { - FormFocus::Name => self.form_state.name.push(c), - FormFocus::Description => self.form_state.description.push(c), - _ => {} - }, - (KeyCode::Enter, _) => match self.form_state.focused_field { - FormFocus::Confirm => { - let name = self.form_state.name.clone(); - let description = self.form_state.description.clone(); - - let sender_copy = self - .command_sender - .clone() - .expect("should always have a sender at this point"); - - let dry_run = self.dry_run; - - tokio::spawn(async move { - match hac_core::fs::create_collection(name, description, dry_run).await { - Ok(collection) => { - if sender_copy - .send(Command::CreateCollection(collection)) - .is_err() - { - tracing::error!("failed to send command through channel"); - std::process::abort(); - } - } - Err(e) => { - if sender_copy.send(Command::Error(e.to_string())).is_err() { - tracing::error!("failed to send error command through channel"); - std::process::abort(); - } - } - } - }); - } - FormFocus::Cancel => { - self.pane_focus = PaneFocus::List; - self.form_state.reset(); - } - _ => {} - }, - (KeyCode::Backspace, _) => match self.form_state.focused_field { - FormFocus::Name => { - self.form_state.name.pop(); - } - FormFocus::Description => { - self.form_state.description.pop(); - } - _ => {} - }, - _ => {} - } - Ok(None) - } - - #[tracing::instrument(skip_all)] - fn handle_confirm_popup_key_event( - &mut self, - key_event: KeyEvent, - ) -> anyhow::Result> { - match key_event.code { - KeyCode::Char('y') => { - let selected = self - .list_state - .selected() - .expect("deleting when nothing is selected should never happen"); - let collection = self - .collections - .get(selected) - .expect("should never attempt to delete a non existing item"); - let path = collection.path.clone(); - - if !self.dry_run { - tokio::spawn(async move { - tracing::debug!("attempting to delete collection: {:?}", path); - hac_core::fs::delete_collection(&path) - .await - .expect("failed to delete collection from filesystem"); - }); - } - - self.collections.remove(selected); - self.list_state.set_items(self.collections.clone()); - self.list_state.select(None); - self.pane_focus = PaneFocus::List; - } - KeyCode::Char('n') => { - self.pane_focus = PaneFocus::List; - } - _ => {} - }; - - Ok(None) - } - - fn handle_error_popup_key_event( - &mut self, - key_event: KeyEvent, - ) -> anyhow::Result> { - match key_event.code { - KeyCode::Char('o') | KeyCode::Esc | KeyCode::Enter => { - self.pane_focus = PaneFocus::List; - } - _ => {} - }; - - Ok(None) - } - + //pub fn display_error(&mut self, message: String) { + // self.pane_focus = PaneFocus::Error; + // self.error_message = message; + //} + // + //fn filter_list(&mut self) { + // self.list_state.set_items( + // self.collections + // .clone() + // .into_iter() + // .filter(|s| s.info.name.contains(&self.filter)) + // .collect(), + // ); + // self.list_state.select(None); + //} + // + //fn handle_filter_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + // match (key_event.code, key_event.modifiers) { + // (KeyCode::Char('c'), KeyModifiers::CONTROL) | (KeyCode::Esc, _) => { + // self.pane_focus = PaneFocus::List; + // self.filter = String::new(); + // self.filter_list(); + // } + // (KeyCode::Backspace, _) => { + // if self.filter.is_empty() { + // self.pane_focus = PaneFocus::List; + // } + // self.filter.pop(); + // self.filter_list(); + // } + // (KeyCode::Enter, _) => { + // self.pane_focus = PaneFocus::List; + // self.filter_list(); + // } + // (KeyCode::Char(c), _) => { + // self.filter.push(c); + // self.filter_list(); + // } + // _ => {} + // }; + // + // Ok(None) + //} + // + //fn handle_list_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + // match key_event.code { + // KeyCode::Enter => { + // return Ok(self + // .list_state + // .items + // .is_empty() + // .not() + // .then(|| { + // self.list_state + // .selected() + // .and_then(|i| self.collections.get(i)) + // .expect( + // "user should never be allowed to select a non existing collection", + // ) + // }) + // .map(|collection| { + // tracing::debug!("selected collection: {}", collection.info.name); + // Command::SelectCollection(collection.clone()) + // })); + // } + // KeyCode::Char('d') => { + // if self.list_state.selected().is_some() { + // self.pane_focus = PaneFocus::Prompt; + // } + // } + // KeyCode::Char('n') | KeyCode::Char('c') => { + // self.pane_focus = PaneFocus::Form; + // } + // KeyCode::Char('h') | KeyCode::Left => { + // if !self.list_state.items.is_empty() { + // self.list_state.select( + // self.list_state + // .selected() + // .map(|i| i.saturating_sub(1)) + // .or(Some(0)), + // ); + // } + // } + // KeyCode::Char('j') | KeyCode::Down => { + // if !self.list_state.items.is_empty() { + // self.list_state.select( + // self.list_state + // .selected() + // .map(|i| { + // usize::min( + // self.list_state.items.len() - 1, + // i + self.list.items_per_row(&self.layout.collections_pane), + // ) + // }) + // .or(Some(0)), + // ); + // } + // } + // KeyCode::Char('k') | KeyCode::Up => { + // if !self.list_state.items.is_empty() { + // self.list_state.select( + // self.list_state + // .selected() + // .map(|i| { + // i.saturating_sub( + // self.list.items_per_row(&self.layout.collections_pane), + // ) + // }) + // .or(Some(0)), + // ); + // } + // } + // KeyCode::Char('l') | KeyCode::Right => { + // if !self.list_state.items.is_empty() { + // self.list_state.select( + // self.list_state + // .selected() + // .map(|i| usize::min(self.list_state.items.len().sub(1), i.add(1))) + // .or(Some(0)), + // ); + // } + // } + // KeyCode::Char('?') => self.pane_focus = PaneFocus::Help, + // KeyCode::Char('/') => self.pane_focus = PaneFocus::Filter, + // _ => {} + // }; + // Ok(None) + //} + // + //fn handle_form_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + // match (key_event.code, key_event.modifiers) { + // (KeyCode::Tab, _) => match self.form_state.focused_field { + // FormFocus::Name => self.form_state.focused_field = FormFocus::Description, + // FormFocus::Description => self.form_state.focused_field = FormFocus::Confirm, + // FormFocus::Confirm => self.form_state.focused_field = FormFocus::Cancel, + // FormFocus::Cancel => self.form_state.focused_field = FormFocus::Name, + // }, + // (KeyCode::Char(c), _) => match self.form_state.focused_field { + // FormFocus::Name => self.form_state.name.push(c), + // FormFocus::Description => self.form_state.description.push(c), + // _ => {} + // }, + // (KeyCode::Enter, _) => match self.form_state.focused_field { + // FormFocus::Confirm => { + // let name = self.form_state.name.clone(); + // let description = self.form_state.description.clone(); + // + // let sender_copy = self + // .command_sender + // .clone() + // .expect("should always have a sender at this point"); + // + // let dry_run = self.dry_run; + // + // tokio::spawn(async move { + // match hac_core::fs::create_collection(name, description, dry_run).await { + // Ok(collection) => { + // if sender_copy + // .send(Command::CreateCollection(collection)) + // .is_err() + // { + // tracing::error!("failed to send command through channel"); + // std::process::abort(); + // } + // } + // Err(e) => { + // if sender_copy.send(Command::Error(e.to_string())).is_err() { + // tracing::error!("failed to send error command through channel"); + // std::process::abort(); + // } + // } + // } + // }); + // } + // FormFocus::Cancel => { + // self.pane_focus = PaneFocus::List; + // self.form_state.reset(); + // } + // _ => {} + // }, + // (KeyCode::Backspace, _) => match self.form_state.focused_field { + // FormFocus::Name => { + // self.form_state.name.pop(); + // } + // FormFocus::Description => { + // self.form_state.description.pop(); + // } + // _ => {} + // }, + // _ => {} + // } + // Ok(None) + //} + // + //#[tracing::instrument(skip_all)] + //fn handle_confirm_popup_key_event( + // &mut self, + // key_event: KeyEvent, + //) -> anyhow::Result> { + // match key_event.code { + // KeyCode::Char('y') => { + // let selected = self + // .list_state + // .selected() + // .expect("deleting when nothing is selected should never happen"); + // let collection = self + // .collections + // .get(selected) + // .expect("should never attempt to delete a non existing item"); + // let path = collection.path.clone(); + // + // if !self.dry_run { + // tokio::spawn(async move { + // tracing::debug!("attempting to delete collection: {:?}", path); + // hac_core::fs::delete_collection(&path) + // .await + // .expect("failed to delete collection from filesystem"); + // }); + // } + // + // self.collections.remove(selected); + // self.list_state.set_items(self.collections.clone()); + // self.list_state.select(None); + // self.pane_focus = PaneFocus::List; + // } + // KeyCode::Char('n') => { + // self.pane_focus = PaneFocus::List; + // } + // _ => {} + // }; + // + // Ok(None) + //} + // + //fn handle_error_popup_key_event( + // &mut self, + // key_event: KeyEvent, + //) -> anyhow::Result> { + // match key_event.code { + // KeyCode::Char('o') | KeyCode::Esc | KeyCode::Enter => { + // self.pane_focus = PaneFocus::List; + // } + // _ => {} + // }; + // + // Ok(None) + //} + // fn draw_hint_text(&self, frame: &mut Frame) { - let hint = - "[h/j/k/l to move] [n -> new] [enter -> select item] [? -> help] [ -> quit]" - .fg(self.colors.normal.magenta) - .into_centered_line(); + let hint = "[j/k ↑/↓ choose] [n -> new] [enter -> select] [? -> show more] [ctrl c -> quit]" + .fg(self.colors.normal.magenta) + .into_centered_line(); frame.render_widget(hint, self.layout.hint_pane); } - - fn draw_help_popup(&self, frame: &mut Frame) { - make_overlay(self.colors, self.colors.primary.background, 0.2, frame); - - let lines = vec![ - Line::from(vec![ - "h/".fg(self.colors.bright.magenta), - " - select left item".into(), - ]), - Line::from(vec![ - "j/".fg(self.colors.bright.magenta), - " - select item below".into(), - ]), - Line::from(vec![ - "k/".fg(self.colors.bright.magenta), - " - select item above".into(), - ]), - Line::from(vec![ - "l/".fg(self.colors.bright.magenta), - " - select right item".into(), - ]), - Line::from(vec![ - "n/c".fg(self.colors.bright.magenta), - " - creates a new collection".into(), - ]), - Line::from(vec![ - "d".fg(self.colors.bright.magenta), - " - deletes the selected collection".into(), - ]), - Line::from(vec![ - "?".fg(self.colors.bright.magenta), - " - toggle this help window".into(), - ]), - Line::from(vec![ - "enter".fg(self.colors.bright.magenta), - " - select item under cursor".into(), - ]), - Line::from(vec![ - "/".fg(self.colors.bright.magenta), - " - enter filter mode".into(), - ]), - Line::from(vec![ - "".fg(self.colors.bright.magenta), - " - quits the application".into(), - ]), - Line::from(""), - Line::from("press any key to go back".fg(self.colors.normal.magenta)).centered(), - ]; - - frame.render_widget( - Paragraph::new(lines) - .wrap(Wrap { trim: true }) - .block(Block::default().padding(Padding::new(2, 2, 1, 1))), - self.layout.help_popup, - ); - } - - fn draw_filter_prompt(&self, frame: &mut Frame) { - let filter = Line::from(format!("/{}", self.filter)); - frame.render_widget(filter, self.layout.hint_pane); - } - - fn draw_collection_list(&mut self, frame: &mut Frame) { - frame.render_stateful_widget( - self.list.clone(), - self.layout.collections_pane, - &mut self.list_state, - ); - } - - fn draw_no_matches_text(&self, frame: &mut Frame) -> anyhow::Result<()> { - let layout = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Fill(1), - Constraint::Length(8), - Constraint::Fill(1), - ]) - .split(self.layout.collections_pane)[1]; - - let no_matches = BigText::builder() - .pixel_size(PixelSize::Quadrant) - .style(Style::default().fg(self.colors.normal.magenta)) - .lines(vec!["No matches".into()]) - .alignment(Alignment::Center) - .build()?; - - frame.render_widget(no_matches, layout); - - Ok(()) - } - - fn draw_empty_message(&self, frame: &mut Frame) -> anyhow::Result<()> { - let size = Layout::default() - .direction(Direction::Vertical) - .flex(Flex::Center) - .constraints([ - Constraint::Fill(1), - Constraint::Length(8), - Constraint::Fill(1), - ]) - .split(self.layout.collections_pane)[1]; - - let empty_message = BigText::builder() - .pixel_size(PixelSize::Quadrant) - .style(Style::default().fg(self.colors.normal.magenta)) - .lines(vec!["No collections".into()]) - .alignment(Alignment::Center) - .build()?; - - frame.render_widget(empty_message, size); - - Ok(()) - } - + // + //fn draw_help_popup(&self, frame: &mut Frame) { + // make_overlay(self.colors, self.colors.primary.background, 0.2, frame); + // + // let lines = vec![ + // Line::from(vec![ + // "h/".fg(self.colors.bright.magenta), + // " - select left item".into(), + // ]), + // Line::from(vec![ + // "j/".fg(self.colors.bright.magenta), + // " - select item below".into(), + // ]), + // Line::from(vec![ + // "k/".fg(self.colors.bright.magenta), + // " - select item above".into(), + // ]), + // Line::from(vec![ + // "l/".fg(self.colors.bright.magenta), + // " - select right item".into(), + // ]), + // Line::from(vec![ + // "n/c".fg(self.colors.bright.magenta), + // " - creates a new collection".into(), + // ]), + // Line::from(vec![ + // "d".fg(self.colors.bright.magenta), + // " - deletes the selected collection".into(), + // ]), + // Line::from(vec![ + // "?".fg(self.colors.bright.magenta), + // " - toggle this help window".into(), + // ]), + // Line::from(vec![ + // "enter".fg(self.colors.bright.magenta), + // " - select item under cursor".into(), + // ]), + // Line::from(vec![ + // "/".fg(self.colors.bright.magenta), + // " - enter filter mode".into(), + // ]), + // Line::from(vec![ + // "".fg(self.colors.bright.magenta), + // " - quits the application".into(), + // ]), + // Line::from(""), + // Line::from("press any key to go back".fg(self.colors.normal.magenta)).centered(), + // ]; + // + // frame.render_widget( + // Paragraph::new(lines) + // .wrap(Wrap { trim: true }) + // .block(Block::default().padding(Padding::new(2, 2, 1, 1))), + // self.layout.help_popup, + // ); + //} + // + //fn draw_filter_prompt(&self, frame: &mut Frame) { + // let filter = Line::from(format!("/{}", self.filter)); + // frame.render_widget(filter, self.layout.hint_pane); + //} + // + fn draw_collection_list(&mut self, frame: &mut Frame) {} + // + //fn draw_no_matches_text(&self, frame: &mut Frame) -> anyhow::Result<()> { + // let layout = Layout::default() + // .direction(Direction::Vertical) + // .constraints([ + // Constraint::Fill(1), + // Constraint::Length(8), + // Constraint::Fill(1), + // ]) + // .split(self.layout.collections_pane)[1]; + // + // let no_matches = BigText::builder() + // .pixel_size(PixelSize::Quadrant) + // .style(Style::default().fg(self.colors.normal.magenta)) + // .lines(vec!["No matches".into()]) + // .alignment(Alignment::Center) + // .build()?; + // + // frame.render_widget(no_matches, layout); + // + // Ok(()) + //} + // + //fn draw_empty_message(&self, frame: &mut Frame) -> anyhow::Result<()> { + // let size = Layout::default() + // .direction(Direction::Vertical) + // .flex(Flex::Center) + // .constraints([ + // Constraint::Fill(1), + // Constraint::Length(8), + // Constraint::Fill(1), + // ]) + // .split(self.layout.collections_pane)[1]; + // + // let empty_message = BigText::builder() + // .pixel_size(PixelSize::Quadrant) + // .style(Style::default().fg(self.colors.normal.magenta)) + // .lines(vec!["No collections".into()]) + // .alignment(Alignment::Center) + // .build()?; + // + // frame.render_widget(empty_message, size); + // + // Ok(()) + //} + // fn draw_background(&self, size: Rect, frame: &mut Frame) { frame.render_widget(Clear, size); frame.render_widget(Block::default().bg(self.colors.primary.background), size); } - - fn draw_error_popup(&self, frame: &mut Frame) { - let popup = ErrorPopup::new(self.error_message.clone(), self.colors); - popup.render(self.layout.error_popup, frame.buffer_mut()); - } - - fn draw_form_popup(&mut self, size: Rect, frame: &mut Frame) { - self.draw_background(size, frame); - draw_overlay(self.colors, size, "新", frame); - - let form = NewCollectionForm::new(self.colors); - form.render( - self.layout.form_popup, - frame.buffer_mut(), - &mut self.form_state, - ); - } - - fn draw_delete_prompt(&self, frame: &mut Frame) { - let selected_index = self - .list_state - .selected() - .expect("attempted to open confirm popup without an item selected"); - let selected_item_name = &self - .collections - .get(selected_index) - .expect("should never be able to have an out of bounds selection") - .info - .name; - - let confirm_popup = ConfirmPopup::new( - format!( - "You really want to delete collection {}?", - selected_item_name - ), - self.colors, - ); - confirm_popup.render(self.layout.confirm_popup, frame.buffer_mut()); - } - - fn draw_title(&self, frame: &mut Frame) -> anyhow::Result<()> { - let title = BigText::builder() - .pixel_size(PixelSize::Quadrant) - .style(Style::default().fg(self.colors.normal.red)) - .lines(vec!["Select a collection".into()]) - .alignment(Alignment::Center) - .build()?; - - frame.render_widget(title, self.layout.title_pane); - - Ok(()) - } + // + //fn draw_error_popup(&self, frame: &mut Frame) { + // let popup = ErrorPopup::new(self.error_message.clone(), self.colors); + // popup.render(self.layout.error_popup, frame.buffer_mut()); + //} + // + //fn draw_form_popup(&mut self, size: Rect, frame: &mut Frame) { + // self.draw_background(size, frame); + // draw_overlay(self.colors, size, "新", frame); + // + // let form = NewCollectionForm::new(self.colors); + // form.render( + // self.layout.form_popup, + // frame.buffer_mut(), + // &mut self.form_state, + // ); + //} + // + //fn draw_delete_prompt(&self, frame: &mut Frame) { + // let selected_index = self + // .list_state + // .selected() + // .expect("attempted to open confirm popup without an item selected"); + // let selected_item_name = &self + // .collections + // .get(selected_index) + // .expect("should never be able to have an out of bounds selection") + // .info + // .name; + // + // let confirm_popup = ConfirmPopup::new( + // format!( + // "You really want to delete collection {}?", + // selected_item_name + // ), + // self.colors, + // ); + // confirm_popup.render(self.layout.confirm_popup, frame.buffer_mut()); + //} + // + //fn draw_title(&self, frame: &mut Frame) -> anyhow::Result<()> { + // let title = BigText::builder() + // .pixel_size(PixelSize::Quadrant) + // .style(Style::default().fg(self.colors.normal.red)) + // .lines(vec!["Select a collection".into()]) + // .alignment(Alignment::Center) + // .build()?; + // + // frame.render_widget(title, self.layout.title_pane); + // + // Ok(()) + //} } impl Renderable for CollectionDashboard<'_> { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { self.draw_background(size, frame); - self.draw_title(frame)?; - - match ( - self.collections.is_empty(), - self.list_state.items.is_empty(), - ) { - (false, false) => self.draw_collection_list(frame), - (false, true) => self.draw_no_matches_text(frame)?, - (true, true) => self.draw_empty_message(frame)?, - (true, false) => unreachable!(), - } - + //self.draw_title(frame)?; + + //match ( + // self.collections.is_empty(), + // self.list_state.items.is_empty(), + //) { + // (false, false) => self.draw_collection_list(frame), + // (false, true) => self.draw_no_matches_text(frame)?, + // (true, true) => self.draw_empty_message(frame)?, + // (true, false) => unreachable!(), + //} + // match self.pane_focus { - PaneFocus::Error => self.draw_error_popup(frame), - PaneFocus::Form => self.draw_form_popup(size, frame), - PaneFocus::Filter => self.draw_filter_prompt(frame), - PaneFocus::Help => self.draw_help_popup(frame), - PaneFocus::Prompt => self.draw_delete_prompt(frame), + //PaneFocus::Error => self.draw_error_popup(frame), + //PaneFocus::Form => self.draw_form_popup(size, frame), + //PaneFocus::Filter => self.draw_filter_prompt(frame), + //PaneFocus::Help => self.draw_help_popup(frame), + //PaneFocus::Prompt => self.draw_delete_prompt(frame), PaneFocus::List => self.draw_hint_text(frame), + _ => todo!(), } Ok(()) @@ -558,18 +547,19 @@ impl Eventful for CollectionDashboard<'_> { if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { return Ok(Some(Command::Quit)); }; - - match self.pane_focus { - PaneFocus::List => self.handle_list_key_event(key_event), - PaneFocus::Form => self.handle_form_key_event(key_event), - PaneFocus::Error => self.handle_error_popup_key_event(key_event), - PaneFocus::Prompt => self.handle_confirm_popup_key_event(key_event), - PaneFocus::Filter => self.handle_filter_key_event(key_event), - PaneFocus::Help => { - self.pane_focus = PaneFocus::List; - Ok(None) - } - } + // + //match self.pane_focus { + // PaneFocus::List => self.handle_list_key_event(key_event), + // PaneFocus::Form => self.handle_form_key_event(key_event), + // PaneFocus::Error => self.handle_error_popup_key_event(key_event), + // PaneFocus::Prompt => self.handle_confirm_popup_key_event(key_event), + // PaneFocus::Filter => self.handle_filter_key_event(key_event), + // PaneFocus::Help => { + // self.pane_focus = PaneFocus::List; + // Ok(None) + // } + //} + Ok(None) } } @@ -582,11 +572,7 @@ fn build_layout(size: Rect) -> DashboardLayout { let [_, title_pane, collections_pane] = Layout::default() .direction(Direction::Vertical) - .constraints([ - Constraint::Length(1), - Constraint::Length(5), - Constraint::Fill(1), - ]) + .constraints([Constraint::Length(1), Constraint::Length(5), Constraint::Fill(1)]) .areas(top); let help_popup = Rect::new( @@ -625,392 +611,392 @@ fn build_layout(size: Rect) -> DashboardLayout { } } -#[cfg(test)] -mod tests { - use hac_core::collection; - use ratatui::{backend::TestBackend, buffer::Cell, Terminal}; - use std::{ - fs::{create_dir, File}, - io::Write, - }; - use tempfile::{tempdir, TempDir}; - - use super::*; - - fn setup_temp_collections(amount: usize) -> (TempDir, String) { - let tmp_data_dir = tempdir().expect("Failed to create temp data dir"); - - let tmp_dir = tmp_data_dir.path().join("collections"); - create_dir(&tmp_dir).expect("Failed to create collections directory"); - - for i in 0..amount { - let file_path = tmp_dir.join(format!("test_collection_{}.json", i)); - let mut tmp_file = File::create(&file_path).expect("Failed to create file"); - - write!( - tmp_file, - r#"{{"info": {{ "name": "test_collection_{}", "description": "test_description_{}" }}}}"#, - i, i - ).expect("Failed to write to file"); - - tmp_file.flush().expect("Failed to flush file"); - } - - (tmp_data_dir, tmp_dir.to_string_lossy().to_string()) - } - - fn feed_keys(dashboard: &mut CollectionDashboard, events: &[KeyEvent]) { - for event in events { - _ = dashboard.handle_key_event(*event); - } - } - - #[test] - fn test_build_layout() { - let size = Rect::new(0, 0, 80, 24); - let expected = DashboardLayout { - collections_pane: Rect::new(1, 6, 79, 17), - hint_pane: Rect::new(1, 23, 79, 1), - title_pane: Rect::new(1, 1, 79, 5), - help_popup: Rect::new(14, 5, 50, 14), - confirm_popup: Rect::new(19, 8, 39, 8), - form_popup: Rect::new(19, 5, 39, 14), - error_popup: Rect::new(19, 2, 39, 20), - }; - - let layout = build_layout(size); - - assert_eq!(layout, expected); - } - - #[test] - fn test_open_close_help() { - let size = Rect::new(0, 0, 80, 24); - let colors = hac_colors::Colors::default(); - let (_guard, path) = setup_temp_collections(1); - let collection = collection::collection::get_collections(path).unwrap(); - - let mut dashboard = CollectionDashboard::new(size, &colors, collection, false).unwrap(); - - assert_eq!(dashboard.collections.len(), 1); - assert_eq!(dashboard.list_state.selected(), Some(0)); - - assert_eq!(dashboard.pane_focus, PaneFocus::List); - - feed_keys( - &mut dashboard, - &[KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)], - ); - - assert_eq!(dashboard.pane_focus, PaneFocus::Help); - - feed_keys( - &mut dashboard, - &[KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE)], - ); - - assert_eq!(dashboard.pane_focus, PaneFocus::List); - } - - #[test] - fn test_actions_without_any_collections() { - let size = Rect::new(0, 0, 80, 24); - let colors = hac_colors::Colors::default(); - let mut dashboard = CollectionDashboard::new(size, &colors, vec![], false).unwrap(); - - assert!(dashboard.collections.is_empty()); - assert_eq!(dashboard.list_state.selected(), None); - - feed_keys( - &mut dashboard, - &[ - KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE), - ], - ); - - assert!(dashboard.collections.is_empty()); - assert_eq!(dashboard.list_state.selected(), None); - } - - #[test] - fn test_filtering_list() { - let size = Rect::new(0, 0, 80, 24); - let colors = hac_colors::Colors::default(); - let (_guard, path) = setup_temp_collections(10); - let collections = collection::collection::get_collections(path).unwrap(); - - let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); - - assert_eq!(dashboard.collections.len(), 10); - assert_eq!(dashboard.list_state.selected(), Some(0)); - - feed_keys( - &mut dashboard, - &[ - // enter filtering - search for non-existing item - KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE), - ], - ); - - assert_eq!(dashboard.pane_focus, PaneFocus::Filter); - assert_eq!(dashboard.list_state.items.len(), 0); - - feed_keys( - &mut dashboard, - &[ - // erase until filtering is cancelled - KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), - KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), - KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), - KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), - ], - ); - - assert_eq!(dashboard.pane_focus, PaneFocus::List); - assert_eq!(dashboard.list_state.items.len(), 10); - - feed_keys( - &mut dashboard, - &[ - // enter filtering again and cancel with hotkey - KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), - KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE), - ], - ); - - assert_eq!(dashboard.pane_focus, PaneFocus::List); - assert_eq!(dashboard.list_state.items.len(), 10); - - feed_keys( - &mut dashboard, - &[ - // enter filtering again and actually filter the list - KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), - ], - ); - - assert_eq!(dashboard.pane_focus, PaneFocus::List); - assert_eq!(dashboard.list_state.items.len(), 1); - } - - #[test] - fn test_moving_out_of_bounds() { - let size = Rect::new(0, 0, 80, 24); - let colors = hac_colors::Colors::default(); - let (_guard, path) = setup_temp_collections(3); - let collections = collection::collection::get_collections(path).unwrap(); - - let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); - - feed_keys( - &mut dashboard, - &[ - // moving down until end is reached, twice more - KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), - ], - ); - - assert_eq!(dashboard.list_state.selected(), Some(2)); - - feed_keys( - &mut dashboard, - &[ - // moving right until beginning is reached, twice more - KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), - ], - ); - - assert_eq!(dashboard.list_state.selected(), Some(0)); - } - - #[test] - fn test_creating_new_collections() { - let size = Rect::new(0, 0, 80, 24); - let colors = hac_colors::Colors::default(); - let (_guard, path) = setup_temp_collections(3); - let collections = collection::collection::get_collections(path).unwrap(); - - let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); - - feed_keys( - &mut dashboard, - &[KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE)], - ); - - assert_eq!(dashboard.pane_focus, PaneFocus::Form); - - feed_keys( - &mut dashboard, - &[ - // going to the cancel button, and closing the form, typing something to ensure - // state reset - KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), - KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), - KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), - KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), - KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), - KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), - KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), - KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), - ], - ); - - assert_eq!(dashboard.pane_focus, PaneFocus::List); - - feed_keys( - &mut dashboard, - &[KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE)], - ); - - assert_eq!(dashboard.pane_focus, PaneFocus::Form); - - feed_keys( - &mut dashboard, - &[ - // filling in the name field - KeyEvent::new(KeyCode::Char('H'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE), - ], - ); - - assert_eq!(dashboard.form_state.name, "Hello"); - - feed_keys( - &mut dashboard, - &[ - // filling in the description - KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('W'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE), - ], - ); - - assert_eq!(dashboard.form_state.name, "Hello"); - assert_eq!(dashboard.form_state.description, "World"); - } - - #[test] - fn test_prompt_delete_collections() { - let size = Rect::new(0, 0, 80, 24); - let colors = hac_colors::Colors::default(); - let (_guard, path) = setup_temp_collections(3); - let collections = collection::collection::get_collections(path).unwrap(); - let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); - - feed_keys( - &mut dashboard, - &[KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE)], - ); - - assert_eq!(dashboard.pane_focus, PaneFocus::Prompt); - - feed_keys( - &mut dashboard, - &[KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE)], - ); - - assert_eq!(dashboard.pane_focus, PaneFocus::List); - } - - #[test] - fn test_display_error() { - let size = Rect::new(0, 0, 80, 24); - let colors = hac_colors::Colors::default(); - let mut dashboard = CollectionDashboard::new(size, &colors, vec![], false).unwrap(); - - dashboard.display_error("any error message".into()); - - assert_eq!(dashboard.pane_focus, PaneFocus::Error); - assert_eq!(dashboard.error_message, "any error message"); - } - - #[test] - fn test_draw_background() { - let colors = hac_colors::Colors::default(); - let size = Rect::new(0, 0, 80, 22); - let dashboard = CollectionDashboard::new(size, &colors, vec![], false).unwrap(); - - let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); - let mut frame = terminal.get_frame(); - - for cell in frame.buffer_mut().content.iter() { - assert_eq!(cell, &Cell::default()); - } - - dashboard.draw_background(size, &mut frame); - - for cell in frame.buffer_mut().content.iter() { - assert_eq!(cell.bg, colors.primary.background); - } - } - - #[test] - fn test_close_error_popup() { - let colors = hac_colors::Colors::default(); - let size = Rect::new(0, 0, 80, 22); - let (_guard, path) = setup_temp_collections(3); - let collections = collection::collection::get_collections(path).unwrap(); - let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); - - dashboard.display_error("any_error_message".into()); - assert_eq!(dashboard.pane_focus, PaneFocus::Error); - feed_keys( - &mut dashboard, - &[KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE)], - ); - - assert_eq!(dashboard.pane_focus, PaneFocus::List); - } - - #[test] - fn test_resizing() { - let colors = hac_colors::Colors::default(); - let size = Rect::new(0, 0, 80, 22); - let new_size = Rect::new(0, 0, 80, 24); - let (_guard, path) = setup_temp_collections(3); - let collections = collection::collection::get_collections(path).unwrap(); - let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); - let expected = DashboardLayout { - collections_pane: Rect::new(1, 6, 79, 17), - hint_pane: Rect::new(1, 23, 79, 1), - title_pane: Rect::new(1, 1, 79, 5), - help_popup: Rect::new(14, 5, 50, 14), - confirm_popup: Rect::new(19, 8, 39, 8), - form_popup: Rect::new(19, 5, 39, 14), - error_popup: Rect::new(19, 2, 39, 20), - }; - - dashboard.resize(new_size); - assert_eq!(dashboard.layout, expected); - } -} +//#[cfg(test)] +//mod tests { +// use hac_core::collection; +// use ratatui::{backend::TestBackend, buffer::Cell, Terminal}; +// use std::{ +// fs::{create_dir, File}, +// io::Write, +// }; +// use tempfile::{tempdir, TempDir}; +// +// use super::*; +// +// fn setup_temp_collections(amount: usize) -> (TempDir, String) { +// let tmp_data_dir = tempdir().expect("Failed to create temp data dir"); +// +// let tmp_dir = tmp_data_dir.path().join("collections"); +// create_dir(&tmp_dir).expect("Failed to create collections directory"); +// +// for i in 0..amount { +// let file_path = tmp_dir.join(format!("test_collection_{}.json", i)); +// let mut tmp_file = File::create(&file_path).expect("Failed to create file"); +// +// write!( +// tmp_file, +// r#"{{"info": {{ "name": "test_collection_{}", "description": "test_description_{}" }}}}"#, +// i, i +// ).expect("Failed to write to file"); +// +// tmp_file.flush().expect("Failed to flush file"); +// } +// +// (tmp_data_dir, tmp_dir.to_string_lossy().to_string()) +// } +// +// fn feed_keys(dashboard: &mut CollectionDashboard, events: &[KeyEvent]) { +// for event in events { +// _ = dashboard.handle_key_event(*event); +// } +// } +// +// #[test] +// fn test_build_layout() { +// let size = Rect::new(0, 0, 80, 24); +// let expected = DashboardLayout { +// collections_pane: Rect::new(1, 6, 79, 17), +// hint_pane: Rect::new(1, 23, 79, 1), +// title_pane: Rect::new(1, 1, 79, 5), +// help_popup: Rect::new(14, 5, 50, 14), +// confirm_popup: Rect::new(19, 8, 39, 8), +// form_popup: Rect::new(19, 5, 39, 14), +// error_popup: Rect::new(19, 2, 39, 20), +// }; +// +// let layout = build_layout(size); +// +// assert_eq!(layout, expected); +// } +// +// #[test] +// fn test_open_close_help() { +// let size = Rect::new(0, 0, 80, 24); +// let colors = hac_colors::Colors::default(); +// let (_guard, path) = setup_temp_collections(1); +// let collection = collection::get_collections(path).unwrap(); +// +// let mut dashboard = CollectionDashboard::new(size, &colors, collection, false).unwrap(); +// +// assert_eq!(dashboard.collections.len(), 1); +// assert_eq!(dashboard.list_state.selected(), Some(0)); +// +// assert_eq!(dashboard.pane_focus, PaneFocus::List); +// +// feed_keys( +// &mut dashboard, +// &[KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)], +// ); +// +// assert_eq!(dashboard.pane_focus, PaneFocus::Help); +// +// feed_keys( +// &mut dashboard, +// &[KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE)], +// ); +// +// assert_eq!(dashboard.pane_focus, PaneFocus::List); +// } +// +// #[test] +// fn test_actions_without_any_collections() { +// let size = Rect::new(0, 0, 80, 24); +// let colors = hac_colors::Colors::default(); +// let mut dashboard = CollectionDashboard::new(size, &colors, vec![], false).unwrap(); +// +// assert!(dashboard.collections.is_empty()); +// assert_eq!(dashboard.list_state.selected(), None); +// +// feed_keys( +// &mut dashboard, +// &[ +// KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE), +// ], +// ); +// +// assert!(dashboard.collections.is_empty()); +// assert_eq!(dashboard.list_state.selected(), None); +// } +// +// #[test] +// fn test_filtering_list() { +// let size = Rect::new(0, 0, 80, 24); +// let colors = hac_colors::Colors::default(); +// let (_guard, path) = setup_temp_collections(10); +// let collections = collection::get_collections(path).unwrap(); +// +// let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); +// +// assert_eq!(dashboard.collections.len(), 10); +// assert_eq!(dashboard.list_state.selected(), Some(0)); +// +// feed_keys( +// &mut dashboard, +// &[ +// // enter filtering - search for non-existing item +// KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE), +// ], +// ); +// +// assert_eq!(dashboard.pane_focus, PaneFocus::Filter); +// assert_eq!(dashboard.list_state.items.len(), 0); +// +// feed_keys( +// &mut dashboard, +// &[ +// // erase until filtering is cancelled +// KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), +// ], +// ); +// +// assert_eq!(dashboard.pane_focus, PaneFocus::List); +// assert_eq!(dashboard.list_state.items.len(), 10); +// +// feed_keys( +// &mut dashboard, +// &[ +// // enter filtering again and cancel with hotkey +// KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), +// KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE), +// ], +// ); +// +// assert_eq!(dashboard.pane_focus, PaneFocus::List); +// assert_eq!(dashboard.list_state.items.len(), 10); +// +// feed_keys( +// &mut dashboard, +// &[ +// // enter filtering again and actually filter the list +// KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), +// ], +// ); +// +// assert_eq!(dashboard.pane_focus, PaneFocus::List); +// assert_eq!(dashboard.list_state.items.len(), 1); +// } +// +// #[test] +// fn test_moving_out_of_bounds() { +// let size = Rect::new(0, 0, 80, 24); +// let colors = hac_colors::Colors::default(); +// let (_guard, path) = setup_temp_collections(3); +// let collections = collection::get_collections(path).unwrap(); +// +// let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); +// +// feed_keys( +// &mut dashboard, +// &[ +// // moving down until end is reached, twice more +// KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), +// ], +// ); +// +// assert_eq!(dashboard.list_state.selected(), Some(2)); +// +// feed_keys( +// &mut dashboard, +// &[ +// // moving right until beginning is reached, twice more +// KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), +// ], +// ); +// +// assert_eq!(dashboard.list_state.selected(), Some(0)); +// } +// +// #[test] +// fn test_creating_new_collections() { +// let size = Rect::new(0, 0, 80, 24); +// let colors = hac_colors::Colors::default(); +// let (_guard, path) = setup_temp_collections(3); +// let collections = collection::get_collections(path).unwrap(); +// +// let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); +// +// feed_keys( +// &mut dashboard, +// &[KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE)], +// ); +// +// assert_eq!(dashboard.pane_focus, PaneFocus::Form); +// +// feed_keys( +// &mut dashboard, +// &[ +// // going to the cancel button, and closing the form, typing something to ensure +// // state reset +// KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), +// ], +// ); +// +// assert_eq!(dashboard.pane_focus, PaneFocus::List); +// +// feed_keys( +// &mut dashboard, +// &[KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE)], +// ); +// +// assert_eq!(dashboard.pane_focus, PaneFocus::Form); +// +// feed_keys( +// &mut dashboard, +// &[ +// // filling in the name field +// KeyEvent::new(KeyCode::Char('H'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE), +// ], +// ); +// +// assert_eq!(dashboard.form_state.name, "Hello"); +// +// feed_keys( +// &mut dashboard, +// &[ +// // filling in the description +// KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('W'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE), +// KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE), +// ], +// ); +// +// assert_eq!(dashboard.form_state.name, "Hello"); +// assert_eq!(dashboard.form_state.description, "World"); +// } +// +// #[test] +// fn test_prompt_delete_collections() { +// let size = Rect::new(0, 0, 80, 24); +// let colors = hac_colors::Colors::default(); +// let (_guard, path) = setup_temp_collections(3); +// let collections = collection::get_collections(path).unwrap(); +// let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); +// +// feed_keys( +// &mut dashboard, +// &[KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE)], +// ); +// +// assert_eq!(dashboard.pane_focus, PaneFocus::Prompt); +// +// feed_keys( +// &mut dashboard, +// &[KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE)], +// ); +// +// assert_eq!(dashboard.pane_focus, PaneFocus::List); +// } +// +// #[test] +// fn test_display_error() { +// let size = Rect::new(0, 0, 80, 24); +// let colors = hac_colors::Colors::default(); +// let mut dashboard = CollectionDashboard::new(size, &colors, vec![], false).unwrap(); +// +// dashboard.display_error("any error message".into()); +// +// assert_eq!(dashboard.pane_focus, PaneFocus::Error); +// assert_eq!(dashboard.error_message, "any error message"); +// } +// +// #[test] +// fn test_draw_background() { +// let colors = hac_colors::Colors::default(); +// let size = Rect::new(0, 0, 80, 22); +// let dashboard = CollectionDashboard::new(size, &colors, vec![], false).unwrap(); +// +// let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); +// let mut frame = terminal.get_frame(); +// +// for cell in frame.buffer_mut().content.iter() { +// assert_eq!(cell, &Cell::default()); +// } +// +// dashboard.draw_background(size, &mut frame); +// +// for cell in frame.buffer_mut().content.iter() { +// assert_eq!(cell.bg, colors.primary.background); +// } +// } +// +// #[test] +// fn test_close_error_popup() { +// let colors = hac_colors::Colors::default(); +// let size = Rect::new(0, 0, 80, 22); +// let (_guard, path) = setup_temp_collections(3); +// let collections = collection::get_collections(path).unwrap(); +// let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); +// +// dashboard.display_error("any_error_message".into()); +// assert_eq!(dashboard.pane_focus, PaneFocus::Error); +// feed_keys( +// &mut dashboard, +// &[KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE)], +// ); +// +// assert_eq!(dashboard.pane_focus, PaneFocus::List); +// } +// +// #[test] +// fn test_resizing() { +// let colors = hac_colors::Colors::default(); +// let size = Rect::new(0, 0, 80, 22); +// let new_size = Rect::new(0, 0, 80, 24); +// let (_guard, path) = setup_temp_collections(3); +// let collections = collection::get_collections(path).unwrap(); +// let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); +// let expected = DashboardLayout { +// collections_pane: Rect::new(1, 6, 79, 17), +// hint_pane: Rect::new(1, 23, 79, 1), +// title_pane: Rect::new(1, 1, 79, 5), +// help_popup: Rect::new(14, 5, 50, 14), +// confirm_popup: Rect::new(19, 8, 39, 8), +// form_popup: Rect::new(19, 5, 39, 14), +// error_popup: Rect::new(19, 2, 39, 20), +// }; +// +// dashboard.resize(new_size); +// assert_eq!(dashboard.layout, expected); +// } +//} diff --git a/hac-client/src/pages/collection_dashboard/collection_list.rs b/hac-client/src/pages/collection_dashboard/collection_list.rs index ea34e4a..15b1af5 100755 --- a/hac-client/src/pages/collection_dashboard/collection_list.rs +++ b/hac-client/src/pages/collection_dashboard/collection_list.rs @@ -1,4 +1,4 @@ -use hac_core::collection::Collection; +use hac_store::collection::Collection; use std::collections::VecDeque; use std::ops::{Add, Div, Mul}; @@ -12,14 +12,14 @@ use ratatui::widgets::{ }; #[derive(Debug)] -pub struct CollectionListState { +pub struct CollectionListState<'a> { selected: Option, - pub(super) items: Vec, + pub(super) items: &'a [Collection], scroll: usize, } -impl CollectionListState { - pub fn new(items: Vec) -> Self { +impl<'a> CollectionListState<'a> { + pub fn new(items: &'a [Collection]) -> Self { CollectionListState { selected: None, items, @@ -35,7 +35,7 @@ impl CollectionListState { self.selected } - pub fn set_items(&mut self, items: Vec) { + pub fn set_items(&mut self, items: &'a [Collection]) { self.items = items; } } @@ -85,195 +85,195 @@ impl<'a> CollectionList<'a> { .collect::>() } - fn build_card( - &self, - state: &CollectionListState, - collection: &Collection, - index: usize, - ) -> Paragraph<'_> { - let lines = vec![ - collection - .info - .name - .clone() - .fg(self.colors.normal.white) - .into(), - collection - .info - .description - .clone() - .unwrap_or_default() - .fg(self.colors.bright.yellow) - .into(), - ]; - - let border_color = if state - .selected - .is_some_and(|selected| selected.eq(&(index.add(state.scroll)))) - { - self.colors.bright.magenta - } else { - self.colors.primary.hover - }; - - Paragraph::new(lines).block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(border_color)), - ) - } -} - -impl StatefulWidget for CollectionList<'_> { - type State = CollectionListState; - - fn render(self, size: Rect, buf: &mut Buffer, state: &mut Self::State) { - let list_size = Rect::new(size.x, size.y, size.width.saturating_sub(3), size.height); - let scrollbar_size = Rect::new(size.width.saturating_sub(1), size.y, 1, size.height); - let mut rects = self.build_layout(&list_size); - - let mut scrollbar_state = - ScrollbarState::new(state.items.len().div(self.items_per_row(&size))) - .position(state.scroll); - - let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) - .style(Style::default().fg(self.colors.normal.magenta)) - .begin_symbol(Some("↑")) - .end_symbol(Some("↓")); - - let items_on_display = self - .items_per_row(&list_size) - .mul(self.total_rows(&list_size)); - if let Some(index) = state.selected { - index - .gt(&items_on_display.saturating_sub(1).add(state.scroll)) - .then(|| state.scroll = state.scroll.add(self.items_per_row(&list_size))); - - state.scroll.gt(&0).then(|| { - index - .add(1) - .saturating_sub(state.scroll) - .eq(&0) - .then(|| state.scroll = state.scroll.saturating_sub(self.items_per_row(&size))); - }); - }; - - state - .items - .iter() - .skip(state.scroll) - .take(rects.len()) - .enumerate() - .map(|(i, collection)| self.build_card(state, collection, i)) - .for_each(|card| card.render(rects.pop_front().unwrap(), buf)); - - scrollbar.render(scrollbar_size, buf, &mut scrollbar_state); - } + //fn build_card( + // &self, + // state: &CollectionListState, + // collection: &Collection, + // index: usize, + //) -> Paragraph<'_> { + //let lines = vec![ + // collection + // .info + // .name + // .clone() + // .fg(self.colors.normal.white) + // .into(), + // collection + // .info + // .description + // .clone() + // .unwrap_or_default() + // .fg(self.colors.bright.yellow) + // .into(), + //]; + // + //let border_color = if state + // .selected + // .is_some_and(|selected| selected.eq(&(index.add(state.scroll)))) + //{ + // self.colors.bright.magenta + //} else { + // self.colors.primary.hover + //}; + // + //Paragraph::new(lines).block( + // Block::default() + // .borders(Borders::ALL) + // .border_type(BorderType::Rounded) + // .border_style(Style::default().fg(border_color)), + //) + //} } -#[cfg(test)] -mod tests { - use std::ops::Sub; - - use super::*; - use hac_core::collection::types::*; - use ratatui::{backend::TestBackend, buffer::Cell, Terminal}; - - fn sample_collection() -> Collection { - Collection { - info: Info { - name: String::from("any_name"), - description: None, - }, - path: "any_path".into(), - requests: None, - } - } - - #[test] - fn test_build_layout() { - let colors = hac_colors::Colors::default(); - let collection_list = CollectionList::new(&colors); - let size = Rect::new(0, 0, 31, 10); - - let layout = collection_list.build_layout(&size); - - assert!(!layout.is_empty()); - assert_eq!(layout.len(), 2); - } - - #[test] - fn test_items_per_row() { - let colors = hac_colors::Colors::default(); - let collection_list = CollectionList::new(&colors); - let zero_items = Rect::new(0, 0, 30, 10); - let one_item = Rect::new(0, 0, 31, 10); - - let amount = collection_list.items_per_row(&zero_items); - assert_eq!(amount, 0); - - let amount = collection_list.items_per_row(&one_item); - assert_eq!(amount, 1); - } - - #[test] - fn test_build_card() { - let colors = hac_colors::Colors::default(); - let collection_list = CollectionList::new(&colors); - let collections = vec![Collection { - info: Info { - name: String::from("any_name"), - description: None, - }, - path: "any_path".into(), - requests: None, - }]; - let state = CollectionListState::new(collections.clone()); - - let lines = vec![ - "any_name".fg(colors.normal.white).into(), - "".fg(colors.bright.yellow).into(), - ]; - let expected = Paragraph::new(lines).block( - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(colors.primary.hover)), - ); - - let card = collection_list.build_card(&state, &collections[0], 0); - - assert_eq!(card, expected); - } - - #[test] - fn test_rendering() { - let colors = hac_colors::Colors::default(); - let collections = (0..100).map(|_| sample_collection()).collect::>(); - - let backend = TestBackend::new(80, 22); - let mut terminal = Terminal::new(backend).unwrap(); - let size = terminal.size().unwrap(); - let mut frame = terminal.get_frame(); - - let mut state = CollectionListState::new(collections.clone()); - let collection_list = CollectionList::new(&colors); - - for cell in &frame.buffer_mut().content { - assert_eq!(cell, &Cell::default()); - } - - collection_list.render(size, frame.buffer_mut(), &mut state); - - for cell in frame - .buffer_mut() - .content - .iter() - .skip(size.width.sub(1).into()) - .step_by(size.width.into()) - { - assert!(cell.symbol().ne(" ")); - } - } -} +//impl StatefulWidget for CollectionList<'_> { +// type State = CollectionListState; +// +// fn render(self, size: Rect, buf: &mut Buffer, state: &mut Self::State) { +// let list_size = Rect::new(size.x, size.y, size.width.saturating_sub(3), size.height); +// let scrollbar_size = Rect::new(size.width.saturating_sub(1), size.y, 1, size.height); +// let mut rects = self.build_layout(&list_size); +// +// let mut scrollbar_state = +// ScrollbarState::new(state.items.len().div(self.items_per_row(&size))) +// .position(state.scroll); +// +// let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) +// .style(Style::default().fg(self.colors.normal.magenta)) +// .begin_symbol(Some("↑")) +// .end_symbol(Some("↓")); +// +// let items_on_display = self +// .items_per_row(&list_size) +// .mul(self.total_rows(&list_size)); +// if let Some(index) = state.selected { +// index +// .gt(&items_on_display.saturating_sub(1).add(state.scroll)) +// .then(|| state.scroll = state.scroll.add(self.items_per_row(&list_size))); +// +// state.scroll.gt(&0).then(|| { +// index +// .add(1) +// .saturating_sub(state.scroll) +// .eq(&0) +// .then(|| state.scroll = state.scroll.saturating_sub(self.items_per_row(&size))); +// }); +// }; +// +// state +// .items +// .iter() +// .skip(state.scroll) +// .take(rects.len()) +// .enumerate() +// .map(|(i, collection)| self.build_card(state, collection, i)) +// .for_each(|card| card.render(rects.pop_front().unwrap(), buf)); +// +// scrollbar.render(scrollbar_size, buf, &mut scrollbar_state); +// } +//} + +//#[cfg(test)] +//mod tests { +// use std::ops::Sub; +// +// use super::*; +// use hac_core::collection::types::*; +// use ratatui::{backend::TestBackend, buffer::Cell, Terminal}; +// +// fn sample_collection() -> Collection { +// Collection { +// info: Info { +// name: String::from("any_name"), +// description: None, +// }, +// path: "any_path".into(), +// requests: None, +// } +// } +// +// #[test] +// fn test_build_layout() { +// let colors = hac_colors::Colors::default(); +// let collection_list = CollectionList::new(&colors); +// let size = Rect::new(0, 0, 31, 10); +// +// let layout = collection_list.build_layout(&size); +// +// assert!(!layout.is_empty()); +// assert_eq!(layout.len(), 2); +// } +// +// #[test] +// fn test_items_per_row() { +// let colors = hac_colors::Colors::default(); +// let collection_list = CollectionList::new(&colors); +// let zero_items = Rect::new(0, 0, 30, 10); +// let one_item = Rect::new(0, 0, 31, 10); +// +// let amount = collection_list.items_per_row(&zero_items); +// assert_eq!(amount, 0); +// +// let amount = collection_list.items_per_row(&one_item); +// assert_eq!(amount, 1); +// } +// +// #[test] +// fn test_build_card() { +// let colors = hac_colors::Colors::default(); +// let collection_list = CollectionList::new(&colors); +// let collections = vec![Collection { +// info: Info { +// name: String::from("any_name"), +// description: None, +// }, +// path: "any_path".into(), +// requests: None, +// }]; +// let state = CollectionListState::new(collections.clone()); +// +// let lines = vec![ +// "any_name".fg(colors.normal.white).into(), +// "".fg(colors.bright.yellow).into(), +// ]; +// let expected = Paragraph::new(lines).block( +// Block::default() +// .borders(Borders::ALL) +// .border_type(BorderType::Rounded) +// .border_style(Style::default().fg(colors.primary.hover)), +// ); +// +// let card = collection_list.build_card(&state, &collections[0], 0); +// +// assert_eq!(card, expected); +// } +// +// #[test] +// fn test_rendering() { +// let colors = hac_colors::Colors::default(); +// let collections = (0..100).map(|_| sample_collection()).collect::>(); +// +// let backend = TestBackend::new(80, 22); +// let mut terminal = Terminal::new(backend).unwrap(); +// let size = terminal.size().unwrap(); +// let mut frame = terminal.get_frame(); +// +// let mut state = CollectionListState::new(collections.clone()); +// let collection_list = CollectionList::new(&colors); +// +// for cell in &frame.buffer_mut().content { +// assert_eq!(cell, &Cell::default()); +// } +// +// collection_list.render(size, frame.buffer_mut(), &mut state); +// +// for cell in frame +// .buffer_mut() +// .content +// .iter() +// .skip(size.width.sub(1).into()) +// .step_by(size.width.into()) +// { +// assert!(cell.symbol().ne(" ")); +// } +// } +//} diff --git a/hac-client/src/pages/collection_viewer/collection_viewer.rs b/hac-client/src/pages/collection_viewer/collection_viewer.rs index 0a3f0a4..592b338 100755 --- a/hac-client/src/pages/collection_viewer/collection_viewer.rs +++ b/hac-client/src/pages/collection_viewer/collection_viewer.rs @@ -247,21 +247,21 @@ impl<'cv> CollectionViewer<'cv> { self.collection_sync_timer = std::time::Instant::now(); - if self.dry_run { - return; - } - - tokio::spawn(async move { - match hac_core::fs::sync_collection(collection).await { - Ok(_) => {} - Err(e) => { - if sender.send(Command::Error(e.to_string())).is_err() { - tracing::error!("failed to send error command through channel"); - std::process::abort(); - } - } - } - }); + //if self.dry_run { + // return; + //} + + //tokio::spawn(async move { + // match hac_core::fs::sync_collection(collection).await { + // Ok(_) => {} + // Err(e) => { + // if sender.send(Command::Error(e.to_string())).is_err() { + // tracing::error!("failed to send error command through channel"); + // std::process::abort(); + // } + // } + // } + //}); } fn update_selection(&mut self, pane_to_select: Option) { diff --git a/hac-client/src/screen_manager.rs b/hac-client/src/screen_manager.rs index 6a593fc..1574d63 100755 --- a/hac-client/src/screen_manager.rs +++ b/hac-client/src/screen_manager.rs @@ -1,4 +1,5 @@ -use hac_core::{collection::Collection, command::Command}; +use hac_core::command::Command; +use hac_loader::collection_loader::CollectionMeta; use crate::event_pool::Event; use crate::pages::collection_dashboard::CollectionDashboard; @@ -49,7 +50,7 @@ impl<'sm> ScreenManager<'sm> { pub fn new( size: Rect, colors: &'sm hac_colors::Colors, - collections: Vec, + collections: Vec, config: &'sm hac_config::Config, dry_run: bool, ) -> anyhow::Result { @@ -96,7 +97,9 @@ impl<'sm> ScreenManager<'sm> { self.config, self.dry_run, )); - self.collection_viewer.as_mut().unwrap() + self.collection_viewer + .as_mut() + .unwrap() .register_command_handler( self.sender .as_ref() @@ -106,7 +109,7 @@ impl<'sm> ScreenManager<'sm> { .ok(); } Command::Error(msg) => { - self.collection_list.display_error(msg); + //self.collection_list.display_error(msg); } _ => {} } @@ -118,9 +121,7 @@ impl Renderable for ScreenManager<'_> { match (size.width < 80, size.height < 22) { (true, _) => self.switch_screen(Screens::TerminalTooSmall), (_, true) => self.switch_screen(Screens::TerminalTooSmall), - (false, false) if self.curr_screen.eq(&Screens::TerminalTooSmall) => { - self.restore_screen() - } + (false, false) if self.curr_screen.eq(&Screens::TerminalTooSmall) => self.restore_screen(), _ => {} } @@ -128,9 +129,7 @@ impl Renderable for ScreenManager<'_> { Screens::CollectionViewer => self .collection_viewer .as_mut() - .expect( - "should never be able to switch to editor screen without having a collection", - ) + .expect("should never be able to switch to editor screen without having a collection") .draw(frame, frame.size())?, Screens::CollectionDashboard => self.collection_list.draw(frame, frame.size())?, Screens::TerminalTooSmall => self.terminal_too_small.draw(frame, frame.size())?, @@ -141,8 +140,7 @@ impl Renderable for ScreenManager<'_> { fn register_command_handler(&mut self, sender: UnboundedSender) -> anyhow::Result<()> { self.sender = Some(sender.clone()); - self.collection_list - .register_command_handler(sender.clone())?; + self.collection_list.register_command_handler(sender.clone())?; Ok(()) } @@ -177,9 +175,7 @@ impl Eventful for ScreenManager<'_> { Screens::CollectionViewer => self .collection_viewer .as_mut() - .expect( - "should never be able to switch to editor screen without having a collection", - ) + .expect("should never be able to switch to editor screen without having a collection") .handle_event(event), Screens::CollectionDashboard => self.collection_list.handle_event(event), Screens::TerminalTooSmall => Ok(None), @@ -187,150 +183,150 @@ impl Eventful for ScreenManager<'_> { } } -#[cfg(test)] -mod tests { - use super::*; - use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; - use hac_core::collection::{self, types::*}; - use ratatui::{backend::TestBackend, Terminal}; - use std::{ - fs::{create_dir, File}, - io::Write, - }; - use tempfile::{tempdir, TempDir}; - - fn setup_temp_collections(amount: usize) -> (TempDir, String) { - let tmp_data_dir = tempdir().expect("Failed to create temp data dir"); - - let tmp_dir = tmp_data_dir.path().join("collections"); - create_dir(&tmp_dir).expect("Failed to create collections directory"); - - for i in 0..amount { - let file_path = tmp_dir.join(format!("test_collection_{}.json", i)); - let mut tmp_file = File::create(&file_path).expect("Failed to create file"); - - write!( - tmp_file, - r#"{{"info": {{ "name": "test_collection_{}", "description": "test_description_{}" }}}}"#, - i, i - ).expect("Failed to write to file"); - - tmp_file.flush().expect("Failed to flush file"); - } - - (tmp_data_dir, tmp_dir.to_string_lossy().to_string()) - } - - #[test] - fn test_show_terminal_too_small_screen() { - let small_in_width = Rect::new(0, 0, 79, 22); - let small_in_height = Rect::new(0, 0, 100, 19); - let colors = hac_colors::Colors::default(); - let (_guard, path) = setup_temp_collections(10); - let collections = collection::collection::get_collections(path).unwrap(); - let config = hac_config::load_config(); - let mut sm = - ScreenManager::new(small_in_width, &colors, collections, &config, false).unwrap(); - let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); - - sm.draw(&mut terminal.get_frame(), small_in_width).unwrap(); - assert_eq!(sm.curr_screen, Screens::TerminalTooSmall); - - sm.draw(&mut terminal.get_frame(), small_in_height).unwrap(); - assert_eq!(sm.curr_screen, Screens::TerminalTooSmall); - } - - #[test] - fn test_restore_screeen() { - let small = Rect::new(0, 0, 79, 22); - let enough = Rect::new(0, 0, 80, 22); - let colors = hac_colors::Colors::default(); - let (_guard, path) = setup_temp_collections(10); - let collections = collection::collection::get_collections(path).unwrap(); - let config = hac_config::load_config(); - let mut sm = ScreenManager::new(small, &colors, collections, &config, false).unwrap(); - let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); - - terminal.resize(small).unwrap(); - sm.draw(&mut terminal.get_frame(), small).unwrap(); - assert_eq!(sm.curr_screen, Screens::TerminalTooSmall); - assert_eq!(sm.prev_screen, Screens::CollectionDashboard); - - terminal.resize(enough).unwrap(); - sm.draw(&mut terminal.get_frame(), enough).unwrap(); - assert_eq!(sm.curr_screen, Screens::CollectionDashboard); - assert_eq!(sm.prev_screen, Screens::TerminalTooSmall); - } - - #[test] - fn test_resizing() { - let initial = Rect::new(0, 0, 80, 22); - let expected = Rect::new(0, 0, 100, 22); - let colors = hac_colors::Colors::default(); - let (_guard, path) = setup_temp_collections(10); - let collection = collection::collection::get_collections(path).unwrap(); - let config = hac_config::load_config(); - let mut sm = ScreenManager::new(initial, &colors, collection, &config, false).unwrap(); - - sm.resize(expected); - - assert_eq!(sm.size, expected); - } - - #[test] - fn test_switch_to_explorer_on_select() { - let initial = Rect::new(0, 0, 80, 22); - let colors = hac_colors::Colors::default(); - let collection = Collection { - info: Info { - name: String::from("any_name"), - description: None, - }, - path: "any_path".into(), - requests: None, - }; - let command = Command::SelectCollection(collection.clone()); - let (_guard, path) = setup_temp_collections(10); - let collection = collection::collection::get_collections(path).unwrap(); - let config = hac_config::load_config(); - let (tx, _) = tokio::sync::mpsc::unbounded_channel::(); - let mut sm = ScreenManager::new(initial, &colors, collection, &config, false).unwrap(); - _ = sm.register_command_handler(tx.clone()); - assert_eq!(sm.curr_screen, Screens::CollectionDashboard); - - sm.handle_command(command); - assert_eq!(sm.curr_screen, Screens::CollectionViewer); - } - - #[test] - fn test_register_command_sender_for_dashboard() { - let initial = Rect::new(0, 0, 80, 22); - let colors = hac_colors::Colors::default(); - let (_guard, path) = setup_temp_collections(10); - let collections = collection::collection::get_collections(path).unwrap(); - let config = hac_config::load_config(); - let mut sm = ScreenManager::new(initial, &colors, collections, &config, false).unwrap(); - - let (tx, _) = tokio::sync::mpsc::unbounded_channel::(); - - sm.register_command_handler(tx.clone()).unwrap(); - - assert!(sm.collection_list.command_sender.is_some()); - } - - #[test] - fn test_quit_event() { - let initial = Rect::new(0, 0, 80, 22); - let colors = hac_colors::Colors::default(); - let (_guard, path) = setup_temp_collections(10); - let collections = collection::collection::get_collections(path).unwrap(); - let config = hac_config::load_config(); - let mut sm = ScreenManager::new(initial, &colors, collections, &config, false).unwrap(); - - let event = Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)); - - let command = sm.handle_event(Some(event)).unwrap(); - - assert!(command.is_some()); - } -} +//#[cfg(test)] +//mod tests { +// use super::*; +// use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +// use hac_core::collection::{self, types::*}; +// use ratatui::{backend::TestBackend, Terminal}; +// use std::{ +// fs::{create_dir, File}, +// io::Write, +// }; +// use tempfile::{tempdir, TempDir}; +// +// fn setup_temp_collections(amount: usize) -> (TempDir, String) { +// let tmp_data_dir = tempdir().expect("Failed to create temp data dir"); +// +// let tmp_dir = tmp_data_dir.path().join("collections"); +// create_dir(&tmp_dir).expect("Failed to create collections directory"); +// +// for i in 0..amount { +// let file_path = tmp_dir.join(format!("test_collection_{}.json", i)); +// let mut tmp_file = File::create(&file_path).expect("Failed to create file"); +// +// write!( +// tmp_file, +// r#"{{"info": {{ "name": "test_collection_{}", "description": "test_description_{}" }}}}"#, +// i, i +// ).expect("Failed to write to file"); +// +// tmp_file.flush().expect("Failed to flush file"); +// } +// +// (tmp_data_dir, tmp_dir.to_string_lossy().to_string()) +// } +// +// #[test] +// fn test_show_terminal_too_small_screen() { +// let small_in_width = Rect::new(0, 0, 79, 22); +// let small_in_height = Rect::new(0, 0, 100, 19); +// let colors = hac_colors::Colors::default(); +// let (_guard, path) = setup_temp_collections(10); +// let collections = collection::get_collections(path).unwrap(); +// let config = hac_config::load_config(); +// let mut sm = +// ScreenManager::new(small_in_width, &colors, collections, &config, false).unwrap(); +// let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); +// +// sm.draw(&mut terminal.get_frame(), small_in_width).unwrap(); +// assert_eq!(sm.curr_screen, Screens::TerminalTooSmall); +// +// sm.draw(&mut terminal.get_frame(), small_in_height).unwrap(); +// assert_eq!(sm.curr_screen, Screens::TerminalTooSmall); +// } +// +// #[test] +// fn test_restore_screeen() { +// let small = Rect::new(0, 0, 79, 22); +// let enough = Rect::new(0, 0, 80, 22); +// let colors = hac_colors::Colors::default(); +// let (_guard, path) = setup_temp_collections(10); +// let collections = collection::get_collections(path).unwrap(); +// let config = hac_config::load_config(); +// let mut sm = ScreenManager::new(small, &colors, collections, &config, false).unwrap(); +// let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); +// +// terminal.resize(small).unwrap(); +// sm.draw(&mut terminal.get_frame(), small).unwrap(); +// assert_eq!(sm.curr_screen, Screens::TerminalTooSmall); +// assert_eq!(sm.prev_screen, Screens::CollectionDashboard); +// +// terminal.resize(enough).unwrap(); +// sm.draw(&mut terminal.get_frame(), enough).unwrap(); +// assert_eq!(sm.curr_screen, Screens::CollectionDashboard); +// assert_eq!(sm.prev_screen, Screens::TerminalTooSmall); +// } +// +// #[test] +// fn test_resizing() { +// let initial = Rect::new(0, 0, 80, 22); +// let expected = Rect::new(0, 0, 100, 22); +// let colors = hac_colors::Colors::default(); +// let (_guard, path) = setup_temp_collections(10); +// let collection = collection::get_collections(path).unwrap(); +// let config = hac_config::load_config(); +// let mut sm = ScreenManager::new(initial, &colors, collection, &config, false).unwrap(); +// +// sm.resize(expected); +// +// assert_eq!(sm.size, expected); +// } +// +// #[test] +// fn test_switch_to_explorer_on_select() { +// let initial = Rect::new(0, 0, 80, 22); +// let colors = hac_colors::Colors::default(); +// let collection = Collection { +// info: Info { +// name: String::from("any_name"), +// description: None, +// }, +// path: "any_path".into(), +// requests: None, +// }; +// let command = Command::SelectCollection(collection.clone()); +// let (_guard, path) = setup_temp_collections(10); +// let collection = collection::get_collections(path).unwrap(); +// let config = hac_config::load_config(); +// let (tx, _) = tokio::sync::mpsc::unbounded_channel::(); +// let mut sm = ScreenManager::new(initial, &colors, collection, &config, false).unwrap(); +// _ = sm.register_command_handler(tx.clone()); +// assert_eq!(sm.curr_screen, Screens::CollectionDashboard); +// +// sm.handle_command(command); +// assert_eq!(sm.curr_screen, Screens::CollectionViewer); +// } +// +// #[test] +// fn test_register_command_sender_for_dashboard() { +// let initial = Rect::new(0, 0, 80, 22); +// let colors = hac_colors::Colors::default(); +// let (_guard, path) = setup_temp_collections(10); +// let collections = collection::get_collections(path).unwrap(); +// let config = hac_config::load_config(); +// let mut sm = ScreenManager::new(initial, &colors, collections, &config, false).unwrap(); +// +// let (tx, _) = tokio::sync::mpsc::unbounded_channel::(); +// +// sm.register_command_handler(tx.clone()).unwrap(); +// +// assert!(sm.collection_list.command_sender.is_some()); +// } +// +// #[test] +// fn test_quit_event() { +// let initial = Rect::new(0, 0, 80, 22); +// let colors = hac_colors::Colors::default(); +// let (_guard, path) = setup_temp_collections(10); +// let collections = collection::get_collections(path).unwrap(); +// let config = hac_config::load_config(); +// let mut sm = ScreenManager::new(initial, &colors, collections, &config, false).unwrap(); +// +// let event = Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)); +// +// let command = sm.handle_event(Some(event)).unwrap(); +// +// assert!(command.is_some()); +// } +//} diff --git a/hac-client/tests/collection_dashboard.rs b/hac-client/tests/collection_dashboard.rs deleted file mode 100755 index 464eebf..0000000 --- a/hac-client/tests/collection_dashboard.rs +++ /dev/null @@ -1,457 +0,0 @@ -use hac_core::collection; - -use hac_client::pages::{collection_dashboard::CollectionDashboard, Eventful, Renderable}; - -use std::fs::{create_dir, File}; -use std::io::Write; - -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use ratatui::{backend::TestBackend, layout::Rect, Frame, Terminal}; -use tempfile::{tempdir, TempDir}; - -fn setup_temp_collections(amount: usize) -> (TempDir, String) { - let tmp_data_dir = tempdir().expect("Failed to create temp data dir"); - - let tmp_dir = tmp_data_dir.path().join("collections"); - create_dir(&tmp_dir).expect("Failed to create collections directory"); - - for i in 0..amount { - let file_path = tmp_dir.join(format!("test_collection_{}.json", i)); - let mut tmp_file = File::create(&file_path).expect("Failed to create file"); - - write!( - tmp_file, - r#"{{"info": {{ "name": "test_collection_{}", "description": "test_description_{}" }}}}"#, - i, i - ).expect("Failed to write to file"); - - tmp_file.flush().expect("Failed to flush file"); - } - - (tmp_data_dir, tmp_dir.to_string_lossy().to_string()) -} - -fn feed_keys(dashboard: &mut CollectionDashboard, events: &[KeyEvent]) { - for event in events { - _ = dashboard.handle_key_event(*event); - } -} - -fn get_rendered_from_buffer(frame: &mut Frame, size: Rect) -> Vec { - frame - .buffer_mut() - .content - .chunks(size.width.into()) - .map(|row| row.iter().map(|cell| cell.symbol()).collect::()) - .collect::>() -} - -#[test] -fn test_draw_empty_message() { - let colors = hac_colors::Colors::default(); - let size = Rect::new(0, 0, 80, 22); - let mut dashboard = CollectionDashboard::new(size, &colors, vec![], false).unwrap(); - let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); - let mut frame = terminal.get_frame(); - - let expected = [ - " █▖▐▌ ▝█ ▝█ ▟ ▀ ", - " █▜▟▌▟▀▙ ▟▀▙ ▟▀▙ █ █ ▟▀▙ ▟▀▙ ▝█▀ ▝█ ▟▀▙ █▀▙ ▟▀▀ ", - " █ ▜▌█ █ █ ▄ █ █ █ █ █▀▀ █ ▄ █▗ █ █ █ █ █ ▝▀▙ ", - " ▀ ▝▘▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▘ ▝▀▘ ▝▀▘ ▀ ▀ ▀▀▘ ", - ]; - - dashboard.draw(&mut frame, size).unwrap(); - - let rendered = frame - .buffer_mut() - .content - .chunks(size.width.into()) - .map(|row| row.iter().map(|cell| cell.symbol()).collect::()) - .collect::>(); - - let result = [&rendered[10], &rendered[11], &rendered[12], &rendered[13]]; - - assert_eq!(result, expected); -} - -#[test] -fn test_draw_no_matches_message() { - let colors = hac_colors::Colors::default(); - let size = Rect::new(0, 0, 80, 22); - let (_guard, path) = setup_temp_collections(3); - let collections = collection::collection::get_collections(path).unwrap(); - let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); - let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); - let mut frame = terminal.get_frame(); - - let expected = [ - " █▖▐▌ ▟ ▜▌ ", - " █▜▟▌▟▀▙ █▄█▖▝▀▙ ▝█▀ ▟▀▙ ▐▙▜▖▟▀▙ ▟▀▀ ", - " █ ▜▌█ █ █▜▜▌▟▀█ █▗ █ ▄ ▐▌▐▌█▀▀ ▝▀▙ ", - " ▀ ▝▘▝▀▘ ▀ ▝▘▝▀▝▘ ▝▘ ▝▀▘ ▀▘▝▘▝▀▘ ▀▀▘ ", - ]; - - feed_keys( - &mut dashboard, - &[ - KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('s'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('t'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), - ], - ); - dashboard.draw(&mut frame, size).unwrap(); - - let rendered = frame - .buffer_mut() - .content - .chunks(size.width.into()) - .map(|row| row.iter().map(|cell| cell.symbol()).collect::()) - .collect::>(); - - let result = [&rendered[10], &rendered[11], &rendered[12], &rendered[13]]; - - assert_eq!(result, expected); -} - -#[test] -fn draw_hint_text() { - let colors = hac_colors::Colors::default(); - let size = Rect::new(0, 0, 80, 22); - let (_guard, path) = setup_temp_collections(3); - let collections = collection::collection::get_collections(path).unwrap(); - let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); - let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); - let mut frame = terminal.get_frame(); - - let expected = - [" [h/j/k/l to move] [n -> new] [enter -> select item] [? -> help] [ -> quit]"]; - - dashboard.draw(&mut frame, size).unwrap(); - let rendered = get_rendered_from_buffer(&mut frame, size); - let result = [rendered.last().unwrap().as_str()]; - - assert_eq!(result, expected); -} - -#[test] -fn draw_filter_prompt() { - let colors = hac_colors::Colors::default(); - let size = Rect::new(0, 0, 80, 22); - let (_guard, path) = setup_temp_collections(3); - let collections = collection::collection::get_collections(path).unwrap(); - let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); - let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); - let mut frame = terminal.get_frame(); - let expected = - [" /any_filter "]; - - feed_keys( - &mut dashboard, - &[ - KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('_'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('t'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE), - ], - ); - - dashboard.draw(&mut frame, size).unwrap(); - let rendered = get_rendered_from_buffer(&mut frame, size); - let result = [rendered.last().unwrap().as_str()]; - - assert_eq!(result, expected); -} - -#[test] -fn test_draw_title() { - let colors = hac_colors::Colors::default(); - let size = Rect::new(0, 0, 80, 22); - let mut dashboard = CollectionDashboard::new(size, &colors, vec![], false).unwrap(); - let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); - let mut frame = terminal.get_frame(); - - let expected = [ - " ▟▀▙ ▝█ ▟ ▝█ ▝█ ▟ ▀ ", - " ▜▙ ▟▀▙ █ ▟▀▙ ▟▀▙ ▝█▀ ▝▀▙ ▟▀▙ ▟▀▙ █ █ ▟▀▙ ▟▀▙ ▝█▀ ▝█ ▟▀▙ █▀▙ ", - " ▄▝█ █▀▀ █ █▀▀ █ ▄ █▗ ▟▀█ █ ▄ █ █ █ █ █▀▀ █ ▄ █▗ █ █ █ █ █ ", - " ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▘ ▝▀▝▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▘ ▝▀▘ ▝▀▘ ▀ ▀ ", - ]; - - dashboard.draw(&mut frame, size).unwrap(); - - let rendered = frame - .buffer_mut() - .content - .chunks(size.width.into()) - .map(|row| row.iter().map(|cell| cell.symbol()).collect::()) - .collect::>(); - - let result = [&rendered[1], &rendered[2], &rendered[3], &rendered[4]]; - - assert_eq!(result, expected); -} - -#[test] -fn test_draw_error() { - let colors = hac_colors::Colors::default(); - let size = Rect::new(0, 0, 80, 22); - let mut dashboard = CollectionDashboard::new(size, &colors, vec![], false).unwrap(); - let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); - let mut frame = terminal.get_frame(); - - let expected = [ - " ", - " ▟▀▙ ▝█ ┌─────────────────────────────────────┐ ▟ ▀ ", - " ▜▙ ▟▀▙ █ ▟▀▙ ▟│ │▟▀▙ ▝█▀ ▝█ ▟▀▙ █▀▙ ", - " ▄▝█ █▀▀ █ █▀▀ █│ any_error_message │█ ▄ █▗ █ █ █ █ █ ", - " ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝│ │▝▀▘ ▝▘ ▝▀▘ ▝▀▘ ▀ ▀ ", - " │ │ ", - " │ │ ", - " │ │ ", - " │ │ ", - " │ │ ", - " █▖▐▌ │ │ ", - " █▜▟▌▟▀▙│ │▙ █▀▙ ▟▀▀ ", - " █ ▜▌█ █│ │█ █ █ ▝▀▙ ", - " ▀ ▝▘▝▀▘│ │▘ ▀ ▀ ▀▀▘ ", - " │ │ ", - " │ │ ", - " │ │ ", - " │ │ ", - " │ (O)k │ ", - " │ │ ", - " └─────────────────────────────────────┘ ", - " ", - ]; - - dashboard.display_error("any_error_message".into()); - dashboard.draw(&mut frame, size).unwrap(); - - let rendered = frame - .buffer_mut() - .content - .chunks(size.width.into()) - .map(|row| row.iter().map(|cell| cell.symbol()).collect::()) - .collect::>(); - - assert_eq!(rendered, expected); -} - -#[test] -fn test_draw_help() { - let colors = hac_colors::Colors::default(); - let size = Rect::new(0, 0, 80, 22); - let mut dashboard = CollectionDashboard::new(size, &colors, vec![], false).unwrap(); - let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); - let mut frame = terminal.get_frame(); - - let expected = [ - " ", - " ▟▀▙ ▝█ ▟ ▝█ ▝█ ▟ ▀ ", - " ▜▙ ▟▀▙ █ ▟▀▙ ▟▀▙ ▝█▀ ▝▀▙ ▟▀▙ ▟▀▙ █ █ ▟▀▙ ▟▀▙ ▝█▀ ▝█ ▟▀▙ █▀▙ ", - " ▄▝█ █▀▀ █ █▀▀ █ ▄ █▗ ▟▀█ █ ▄ █ █ █ █ █▀▀ █ ▄ █▗ █ █ █ █ █ ", - " ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▘ ▝▀▝▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▘ ▝▀▘ ▝▀▘ ▀ ▀ ", - " h/ - select left item ", - " j/ - select item below ", - " k/ - select item above ", - " l/ - select right item ", - " n/c - creates a new collection ", - " █▖▐▌d - deletes the selected collection ", - " █▜▟▌? - toggle this help window█ ▟▀▙ █▀▙ ▟▀▀ ", - " █ ▜▌enter - select item under cursor █ █ █ █ ▝▀▙ ", - " ▀ ▝▘/ - enter filter mode ▝▘ ▝▀▘ ▝▀▘ ▀ ▀ ▀▀▘ ", - " - quits the application ", - " ", - " press any key to go back ", - " ", - " ", - " ", - " ", - " ", - ]; - - feed_keys( - &mut dashboard, - &[KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)], - ); - dashboard.draw(&mut frame, size).unwrap(); - - let rendered = frame - .buffer_mut() - .content - .chunks(size.width.into()) - .map(|row| row.iter().map(|cell| cell.symbol()).collect::()) - .collect::>(); - - assert_eq!(rendered, expected); -} - -#[test] -fn test_draw_form_popup() { - let colors = hac_colors::Colors::default(); - let size = Rect::new(0, 0, 80, 22); - let mut dashboard = CollectionDashboard::new(size, &colors, vec![], false).unwrap(); - let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); - let mut frame = terminal.get_frame(); - - let expected = [ - "新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 ┌Name─────────────────────────────┐ 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 │My awesome API │ 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 └─────────────────────────────────┘ 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 ┌Description──────────────────────┐ 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 │Request testing │ 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 └─────────────────────────────────┘ 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 ╭────────╮ ╭────────╮ 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 │ Create │ │ Cancel │ 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 ╰────────╯ ╰────────╯ 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 [Tab] to switch focus [Enter] to se 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 ", - "新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 新 ", - ]; - - feed_keys( - &mut dashboard, - &[KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE)], - ); - dashboard.draw(&mut frame, size).unwrap(); - - let rendered = frame - .buffer_mut() - .content - .chunks(size.width.into()) - .map(|row| row.iter().map(|cell| cell.symbol()).collect::()) - .collect::>(); - - println!("{:#?}", frame.buffer_mut().content()); - - assert_eq!(rendered, expected); -} - -#[test] -fn test_draw_delete_prompt() { - let colors = hac_colors::Colors::default(); - let size = Rect::new(0, 0, 80, 22); - let (_guard, path) = setup_temp_collections(3); - let collections = collection::collection::get_collections(path).unwrap(); - let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); - let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); - let mut frame = terminal.get_frame(); - - let expected = [ - " ", - " ▟▀▙ ▝█ ▟ ▝█ ▝█ ▟ ▀ ", - " ▜▙ ▟▀▙ █ ▟▀▙ ▟▀▙ ▝█▀ ▝▀▙ ▟▀▙ ▟▀▙ █ █ ▟▀▙ ▟▀▙ ▝█▀ ▝█ ▟▀▙ █▀▙ ", - " ▄▝█ █▀▀ █ █▀▀ █ ▄ █▗ ▟▀█ █ ▄ █ █ █ █ █▀▀ █ ▄ █▗ █ █ █ █ █ ", - " ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▘ ▝▀▝▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▘ ▝▀▘ ▝▀▘ ▀ ▀ ", - " ", - " ╭────────────────────────────────────╮╭────────────────────────────────────╮ ↑ ", - " │test_collection_0┌─────────────────────────────────────┐ │ █ ", - " │test_description_│ │ │ █ ", - " ╰─────────────────│ You really want to delete │──────────────────╯ █ ", - " ╭─────────────────│ collection test_collection_0? │ █ ", - " │test_collection_2│ │ █ ", - " │test_description_│ (y)es (n)o │ █ ", - " ╰─────────────────│ │ █ ", - " └─────────────────────────────────────┘ █ ", - " █ ", - " █ ", - " █ ", - " █ ", - " █ ", - " ↓ ", - " ", - ]; - - feed_keys( - &mut dashboard, - &[KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE)], - ); - dashboard.draw(&mut frame, size).unwrap(); - - let rendered = frame - .buffer_mut() - .content - .chunks(size.width.into()) - .map(|row| row.iter().map(|cell| cell.symbol()).collect::()) - .collect::>(); - - println!("{:?}", frame.buffer_mut()); - - assert_eq!(rendered, expected); -} - -#[test] -fn test_draw_collections_list() { - let colors = hac_colors::Colors::default(); - let size = Rect::new(0, 0, 80, 22); - let (_guard, path) = setup_temp_collections(3); - let collections = collection::collection::get_collections(path).unwrap(); - let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); - let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); - let mut frame = terminal.get_frame(); - - let expected = [ - " ", - " ▟▀▙ ▝█ ▟ ▝█ ▝█ ▟ ▀ ", - " ▜▙ ▟▀▙ █ ▟▀▙ ▟▀▙ ▝█▀ ▝▀▙ ▟▀▙ ▟▀▙ █ █ ▟▀▙ ▟▀▙ ▝█▀ ▝█ ▟▀▙ █▀▙ ", - " ▄▝█ █▀▀ █ █▀▀ █ ▄ █▗ ▟▀█ █ ▄ █ █ █ █ █▀▀ █ ▄ █▗ █ █ █ █ █ ", - " ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▘ ▝▀▝▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▀▘ ▝▘ ▝▀▘ ▝▀▘ ▀ ▀ ", - " ", - " ╭────────────────────────────────────╮╭────────────────────────────────────╮ ↑ ", - " │test_collection_0 ││test_collection_1 │ █ ", - " │test_description_0 ││test_description_1 │ █ ", - " ╰────────────────────────────────────╯╰────────────────────────────────────╯ █ ", - " ╭────────────────────────────────────╮ █ ", - " │test_collection_2 │ █ ", - " │test_description_2 │ █ ", - " ╰────────────────────────────────────╯ █ ", - " █ ", - " █ ", - " █ ", - " █ ", - " █ ", - " █ ", - " ↓ ", - " [h/j/k/l to move] [n -> new] [enter -> select item] [? -> help] [ -> quit]", - ]; - - dashboard.draw(&mut frame, size).unwrap(); - - let rendered = frame - .buffer_mut() - .content - .chunks(size.width.into()) - .map(|row| row.iter().map(|cell| cell.symbol()).collect::()) - .collect::>(); - - assert_eq!(rendered, expected); -} diff --git a/hac-config/src/config.rs b/hac-config/src/config.rs index 1afd18d..387ddfa 100755 --- a/hac-config/src/config.rs +++ b/hac-config/src/config.rs @@ -49,9 +49,17 @@ pub enum Action { JumpToEmptyLineAbove, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, Serialize, Deserialize, Copy, Clone)] +pub enum CollectionExtensions { + #[default] + Json, +} + +#[derive(Debug, Serialize, Deserialize)] pub struct Config { pub editor_keys: Keys, + #[serde(default)] + pub collection_ext: CollectionExtensions, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] diff --git a/hac-config/src/lib.rs b/hac-config/src/lib.rs index ab305cc..4693c0d 100755 --- a/hac-config/src/lib.rs +++ b/hac-config/src/lib.rs @@ -1,13 +1,9 @@ pub mod config; -pub mod data; mod default_config; pub use config::{ default_as_str, get_config_dir_path, get_usual_path, load_config, Action, Config, KeyAction, }; -pub use data::{ - get_collections_dir, get_or_create_collections_dir, get_or_create_data_dir, log_file, -}; use serde::{Deserialize, Serialize}; #[derive(PartialEq, Deserialize, Serialize, Debug, Clone)] @@ -17,19 +13,20 @@ pub enum EditorMode { } pub static APP_NAME: &str = "hac"; +pub static LOGFILE: &str = "hac.log"; pub static COLLECTIONS_DIR: &str = "collections"; pub static CONFIG_FILE: &str = "hac.toml"; pub static THEMES_DIR: &str = "themes"; pub static CONFIG_ENV_VAR: &str = "HAC_CONFIG"; #[cfg(unix)] -static XDG_ENV_VARS: [&str; 2] = ["XDG_CONFIG_HOME", "XDG_DATA_HOME"]; +pub static XDG_ENV_VARS: [&str; 2] = ["XDG_CONFIG_HOME", "XDG_DATA_HOME"]; #[cfg(windows)] -static XDG_ENV_VARS: [&str; 2] = ["LOCALAPPDATA", "LOCALAPPDATA"]; +pub static XDG_ENV_VARS: [&str; 2] = ["LOCALAPPDATA", "LOCALAPPDATA"]; #[cfg(unix)] -static XDG_DEFAULTS: [&str; 2] = [".config", ".local/share"]; +pub static XDG_DEFAULTS: [&str; 2] = [".config", ".local/share"]; #[cfg(windows)] -static XDG_DEFAULTS: [&str; 2] = ["AppData\\Local", "AppData\\Local"]; +pub static XDG_DEFAULTS: [&str; 2] = ["AppData\\Local", "AppData\\Local"]; diff --git a/hac-core/src/collection.rs b/hac-core/src/collection.rs deleted file mode 100755 index e06ccc4..0000000 --- a/hac-core/src/collection.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[allow(clippy::module_inception)] -pub mod collection; -pub mod types; -pub use types::Collection; -mod errors; diff --git a/hac-core/src/collection/collection.rs b/hac-core/src/collection/collection.rs deleted file mode 100755 index ea3cb7e..0000000 --- a/hac-core/src/collection/collection.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::collection::types::{Collection, Info}; - -use std::path::Path; -use std::time::{self, UNIX_EPOCH}; - -#[tracing::instrument(err)] -pub fn get_collections_from_config() -> anyhow::Result> { - let collections_dir = hac_config::get_or_create_collections_dir(); - get_collections(collections_dir) -} - -#[tracing::instrument(skip(collections_dir), err)] -pub fn get_collections

(collections_dir: P) -> anyhow::Result> -where - P: AsRef, -{ - let items = std::fs::read_dir(&collections_dir)?; - - let mut collections = vec![]; - - for item in items.into_iter().flatten() { - let file_name = item.file_name(); - let collection_name = collections_dir.as_ref().join(file_name); - let file = std::fs::read_to_string(&collection_name)?; - let mut collection: Collection = serde_json::from_str(&file)?; - collection.path = collection_name; - collections.push(collection); - } - - collections.sort_by(|a, b| a.info.name.cmp(&b.info.name)); - - Ok(collections) -} - -pub fn create_from_form(name: String, description: String) -> Collection { - let name = if name.is_empty() { - let now = time::SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(); - format!("Unnamed Collection {}", now) - } else { - name - }; - - let collections_dir = hac_config::get_collections_dir(); - let name_as_file_name = name.to_lowercase().replace(' ', "_"); - let collection_name = collections_dir.join(name_as_file_name); - - Collection { - info: Info { - name, - description: Some(description), - }, - requests: None, - path: format!("{}.json", collection_name.to_string_lossy()).into(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_creating_from_form() { - let collection = create_from_form("any valid name".into(), "any desctiption".into()); - - assert!(collection.path.to_string_lossy().ends_with(".json")); - assert!(collection.info.name.eq("any valid name")); - assert!(collection.info.description.is_some()) - } -} diff --git a/hac-core/src/collection/mod.rs b/hac-core/src/collection/mod.rs new file mode 100644 index 0000000..0dbb258 --- /dev/null +++ b/hac-core/src/collection/mod.rs @@ -0,0 +1,75 @@ +pub mod types; +pub use types::Collection; +mod errors; + +use crate::collection::types::Info; + +use std::path::Path; +use std::time::{self, UNIX_EPOCH}; + +//#[tracing::instrument(err)] +//pub fn get_collections_from_config() -> anyhow::Result> { +// let collections_dir = hac_config::get_or_create_collections_dir(); +// get_collections(collections_dir) +//} +// +//#[tracing::instrument(skip(collections_dir), err)] +//pub fn get_collections

(collections_dir: P) -> anyhow::Result> +//where +// P: AsRef, +//{ +// let items = std::fs::read_dir(&collections_dir)?; +// +// let mut collections = vec![]; +// +// for item in items.into_iter().flatten() { +// let file_name = item.file_name(); +// let collection_name = collections_dir.as_ref().join(file_name); +// let file = std::fs::read_to_string(&collection_name)?; +// let mut collection: Collection = serde_json::from_str(&file)?; +// collection.path = collection_name; +// collections.push(collection); +// } +// +// collections.sort_by(|a, b| a.info.name.cmp(&b.info.name)); +// +// Ok(collections) +//} +// +//pub fn create_from_form(name: String, description: String) -> Collection { +// let name = if name.is_empty() { +// let now = time::SystemTime::now() +// .duration_since(UNIX_EPOCH) +// .unwrap() +// .as_millis(); +// format!("Unnamed Collection {}", now) +// } else { +// name +// }; +// +// let collections_dir = hac_config::get_collections_dir(); +// let name_as_file_name = name.to_lowercase().replace(' ', "_"); +// let collection_name = collections_dir.join(name_as_file_name); +// +// Collection { +// info: Info { +// name, +// description: Some(description), +// }, +// requests: None, +// path: format!("{}.json", collection_name.to_string_lossy()).into(), +// } +//} +// +//#[cfg(test)] +//mod tests { +// use super::*; +// #[test] +// fn test_creating_from_form() { +// let collection = create_from_form("any valid name".into(), "any desctiption".into()); +// +// assert!(collection.path.to_string_lossy().ends_with(".json")); +// assert!(collection.info.name.eq("any valid name")); +// assert!(collection.info.description.is_some()) +// } +//} diff --git a/hac-core/src/fs/fs.rs b/hac-core/src/fs/fs.rs index 4f1f9db..f6b963c 100755 --- a/hac-core/src/fs/fs.rs +++ b/hac-core/src/fs/fs.rs @@ -1,4 +1,4 @@ -use crate::collection::{collection::create_from_form, Collection}; +//use crate::collection::{create_from_form, Collection}; use crate::fs::error::FsError; use std::path::Path; @@ -17,48 +17,48 @@ where Ok(()) } -#[tracing::instrument(err)] -pub async fn create_collection( - name: String, - description: String, - dry_run: bool, -) -> anyhow::Result { - let collection = create_from_form(name, description); +//#[tracing::instrument(err)] +//pub async fn create_collection( +// name: String, +// description: String, +// dry_run: bool, +//) -> anyhow::Result { +// let collection = create_from_form(name, description); +// +// if collection.path.exists() { +// return Err(FsError::CollectionAlreadyExists( +// collection.path.to_string_lossy().to_string(), +// )); +// } +// +// let serialized_collection = serde_json::to_string(&collection) +// .map_err(|e| FsError::SerializationError(e.to_string()))?; +// +// // if we are on a dry_run, we skip syncing +// if !dry_run { +// tokio::fs::write(&collection.path, serialized_collection) +// .await +// .map_err(|e| FsError::IOError(format!("failed to write collection: {:?}", e)))?; +// } +// +// tracing::debug!("successfully created new collection: {:?}", collection.path); +// Ok(collection) +//} - if collection.path.exists() { - return Err(FsError::CollectionAlreadyExists( - collection.path.to_string_lossy().to_string(), - )); - } - - let serialized_collection = serde_json::to_string(&collection) - .map_err(|e| FsError::SerializationError(e.to_string()))?; - - // if we are on a dry_run, we skip syncing - if !dry_run { - tokio::fs::write(&collection.path, serialized_collection) - .await - .map_err(|e| FsError::IOError(format!("failed to write collection: {:?}", e)))?; - } - - tracing::debug!("successfully created new collection: {:?}", collection.path); - Ok(collection) -} - -pub async fn sync_collection(collection: Collection) -> anyhow::Result<(), FsError> { - let collection_str = serde_json::to_string(&collection) - .map_err(|e| FsError::SerializationError(e.to_string()))?; - - tokio::fs::write(&collection.path, collection_str) - .await - .map_err(|_| { - FsError::IOError(format!( - "failed to synchronize collection {:?}", - collection.path - )) - })?; - - tracing::debug!("synchronization of collection: {:?}", collection.path); - - Ok(()) -} +//pub async fn sync_collection(collection: Collection) -> anyhow::Result<(), FsError> { +// let collection_str = serde_json::to_string(&collection) +// .map_err(|e| FsError::SerializationError(e.to_string()))?; +// +// tokio::fs::write(&collection.path, collection_str) +// .await +// .map_err(|_| { +// FsError::IOError(format!( +// "failed to synchronize collection {:?}", +// collection.path +// )) +// })?; +// +// tracing::debug!("synchronization of collection: {:?}", collection.path); +// +// Ok(()) +//} diff --git a/hac-loader/Cargo.toml b/hac-loader/Cargo.toml new file mode 100644 index 0000000..5a29bd3 --- /dev/null +++ b/hac-loader/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "hac-loader" +version = "0.2.0" +edition = "2021" + +[dependencies] +hac-store.workspace = true +hac-config.workspace = true + +serde.workspace = true +serde_json.workspace = true +anyhow.workspace = true +tracing.workspace = true +dirs = "5.0.1" diff --git a/hac-loader/src/collection_loader/json_collection.rs b/hac-loader/src/collection_loader/json_collection.rs new file mode 100644 index 0000000..694aa2c --- /dev/null +++ b/hac-loader/src/collection_loader/json_collection.rs @@ -0,0 +1,79 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +struct JsonInfo { + name: String, + path: std::path::PathBuf, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JsonCollection { + pub info: JsonCollectionInfo, + pub requests: Vec, + #[serde(skip)] + pub file_info: JsonInfo, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JsonCollectionInfo { + pub name: String, + pub description: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ReqKind { + Req(JsonRequest), + Folder(JsonFolder), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JsonRequest { + pub name: String, + pub method: JsonReqMethod, + pub uri: String, + pub headers: Vec, + #[serde(rename = "authKind")] + pub auth_kind: JsonAuthKind, + #[serde(rename = "bodyKind")] + pub body_kind: JsonBodyKind, + pub body: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JsonHeaderEntry { + pub key: String, + pub val: String, + pub enabled: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum JsonBodyKind { + Json, + #[serde(rename = "NO_BODY")] + NoBody, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum JsonReqMethod { + Get, + Post, + Put, + Patch, + Delete, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum JsonAuthKind { + Bearer, + #[serde(rename = "NO_AUTH")] + NoAuth, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JsonFolder { + pub name: String, + pub requests: Vec, +} diff --git a/hac-loader/src/collection_loader/json_loader.rs b/hac-loader/src/collection_loader/json_loader.rs new file mode 100644 index 0000000..2acf336 --- /dev/null +++ b/hac-loader/src/collection_loader/json_loader.rs @@ -0,0 +1,172 @@ +use super::json_collection::{ + JsonBodyKind, JsonCollection, JsonCollectionInfo, JsonFolder, JsonHeaderEntry, JsonReqMethod, JsonRequest, ReqKind, +}; +use super::IntoCollection; +use hac_store::{ + collection::{BodyKind, Collection, CollectionInfo, Folder, HeaderEntry, KeyKind, ReqMethod, Request}, + slab::{Key, Slab}, +}; + +pub struct JsonLoader(JsonCollection); + +impl IntoCollection for JsonLoader { + fn into_collection(self) -> hac_store::collection::Collection { + from_file_to_collection(self.0) + } +} + +impl JsonLoader { + pub fn parse(content: &str) -> anyhow::Result { + match serde_json::from_str(content) { + Ok(collection) => Ok(Self(collection)), + Err(e) => todo!("{e:?}"), + } + } +} + +fn from_file_to_collection(file_collection: JsonCollection) -> Collection { + let mut folders = Slab::::new(); + let mut requests = Slab::::new(); + let mut root_requests = Slab::::new(); + + for item in file_collection.requests { + match item { + ReqKind::Req(inner) => _ = root_requests.push(inner.into()), + ReqKind::Folder(inner) => _ = collect_folder(inner, &mut requests, &mut folders), + }; + } + + Collection { + info: file_collection.info.into(), + folders, + requests, + root_requests, + } +} + +fn collect_folder(file_folder: JsonFolder, requests: &mut Slab, folders: &mut Slab) -> Key { + // inserting a folder to be modified later to ensure we reserve an idx + let idx = folders.push(Folder::default()); + let mut childrens = vec![]; + + for item in file_folder.requests { + match item { + ReqKind::Req(inner) => { + let mut req: Request = inner.into(); + req.parent = Some(idx); + let req_idx = requests.push(req); + childrens.push(KeyKind::Req(req_idx)); + } + ReqKind::Folder(inner) => { + let folder_idx = collect_folder(inner, requests, folders); + childrens.push(KeyKind::Folder(folder_idx)); + } + } + } + + let folder = folders.get_mut(idx); + folder.name = file_folder.name; + folder.requests = childrens; + idx +} + +impl From for BodyKind { + fn from(body_kind: JsonBodyKind) -> Self { + match body_kind { + JsonBodyKind::Json => Self::Json, + JsonBodyKind::NoBody => Self::NoBody, + } + } +} + +impl From for HeaderEntry { + fn from(entry: JsonHeaderEntry) -> Self { + Self { + key: entry.key, + val: entry.val, + enabled: entry.enabled, + } + } +} + +impl From for ReqMethod { + fn from(file_req: JsonReqMethod) -> Self { + match file_req { + JsonReqMethod::Get => Self::Get, + JsonReqMethod::Post => Self::Post, + JsonReqMethod::Put => Self::Put, + JsonReqMethod::Patch => Self::Patch, + JsonReqMethod::Delete => Self::Delete, + } + } +} + +impl From for Request { + fn from(file_req: JsonRequest) -> Self { + Self { + parent: None, + body: file_req.body, + name: file_req.name, + uri: file_req.uri, + body_kind: file_req.body_kind.into(), + method: file_req.method.into(), + headers: file_req.headers.into_iter().map(Into::into).collect::>(), + } + } +} + +impl From for CollectionInfo { + fn from(collection_info: JsonCollectionInfo) -> Self { + Self { + name: collection_info.name, + description: collection_info.description, + } + } +} + +impl From for Folder { + fn from(file_folder: JsonFolder) -> Self { + Self { + name: file_folder.name, + requests: vec![], + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // simple collection with just a few entries + const BASE_COLLECTION: &str = include_str!("../../test_collections/basic.json"); + // collection with complex structure of directories and requests + const FULL_COLLECTION: &str = include_str!("../../test_collections/complex.json"); + + #[test] + fn load_from_json_to_inner_collection() { + let result = JsonLoader::parse(BASE_COLLECTION).unwrap(); + let collection = result.into_collection(); + + assert!(collection.requests.len() == 1); + assert!(collection.folders.len() == 1); + assert!(collection.root_requests.len() == 1); + assert_eq!(collection.root_requests.get(0).name, "Sample Root Request"); + assert_eq!(collection.requests.get(0).name, "Sample Parented Request"); + assert_eq!(collection.folders.get(0).name, "Sample Folder"); + + let result = JsonLoader::parse(FULL_COLLECTION).unwrap(); + let collection = result.into_collection(); + + assert!(collection.requests.len() == 4); + assert!(collection.folders.len() == 3); + assert!(collection.root_requests.len() == 1); + assert_eq!(collection.root_requests.get(0).name, "Root Request 1"); + assert_eq!(collection.requests.get(0).name, "Parented Request 1 Folder 1 - 1"); + assert_eq!(collection.requests.get(1).name, "Parented Request 1 Folder 1 - 2"); + assert_eq!(collection.requests.get(2).name, "Parented Request 2 Folder 1 - 1"); + assert_eq!(collection.requests.get(3).name, "Parented Request 1 Folder 1"); + assert_eq!(collection.folders.get(0).name, "Folder 1"); + assert_eq!(collection.folders.get(1).name, "Folder 1 - 1"); + assert_eq!(collection.folders.get(2).name, "Folder 1 - 2"); + } +} diff --git a/hac-loader/src/collection_loader/mod.rs b/hac-loader/src/collection_loader/mod.rs new file mode 100644 index 0000000..13f9a8a --- /dev/null +++ b/hac-loader/src/collection_loader/mod.rs @@ -0,0 +1,53 @@ +mod json_collection; +mod json_loader; + +use hac_config::config::CollectionExtensions; +use hac_store::collection::Collection; +use json_loader::JsonLoader; + +pub trait IntoCollection { + fn into_collection(self) -> hac_store::collection::Collection; +} + +pub fn read_collection_file(file_path: F, parser: P) -> anyhow::Result +where + F: AsRef, + P: FnOnce(&str) -> anyhow::Result, + T: IntoCollection, +{ + match std::fs::read_to_string(file_path.as_ref()) { + Ok(content) => Ok(parser(&content)?.into_collection()), + Err(e) => todo!("{e:?}"), + } +} + +pub fn load_collection>( + file_path: F, + config: &hac_config::Config, +) -> anyhow::Result { + match config.collection_ext { + CollectionExtensions::Json => Ok(read_collection_file(file_path, JsonLoader::parse)?), + } +} + +// TODO: i probably want to introduce some metadata storage, or caching to know +// total requests, total saved responses, and other infos without having to read +// the entire collection. +#[derive(Debug)] +pub struct CollectionMeta { + name: String, + path: std::path::PathBuf, +} + +pub fn collections_metadata() -> anyhow::Result> { + let entries = std::fs::read_dir(super::collections_dir())?.flatten(); + + let collections = entries.fold(vec![], |mut acc, entry| { + let name = entry.file_name().to_string_lossy().to_string(); + let path = entry.path(); + acc.push(CollectionMeta { name, path }); + acc + }); + + Ok(collections) +} diff --git a/hac-config/src/data.rs b/hac-loader/src/lib.rs old mode 100755 new mode 100644 similarity index 76% rename from hac-config/src/data.rs rename to hac-loader/src/lib.rs index 9b928c0..ee6002a --- a/hac-config/src/data.rs +++ b/hac-loader/src/lib.rs @@ -1,8 +1,9 @@ -use crate::{APP_NAME, COLLECTIONS_DIR, XDG_DEFAULTS, XDG_ENV_VARS}; +pub mod collection_loader; +use hac_config::{APP_NAME, COLLECTIONS_DIR, XDG_DEFAULTS, XDG_ENV_VARS}; use std::path::PathBuf; -pub fn get_data_dir() -> PathBuf { +pub fn data_dir() -> PathBuf { let data_dir = std::env::var(XDG_ENV_VARS[1]) .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from(XDG_DEFAULTS[1])); @@ -14,7 +15,7 @@ pub fn get_data_dir() -> PathBuf { } pub fn get_or_create_data_dir() -> PathBuf { - let data_dir = get_data_dir(); + let data_dir = data_dir(); if !data_dir.exists() && !data_dir.is_dir() { match std::fs::create_dir(&data_dir) { @@ -32,13 +33,14 @@ pub fn get_or_create_data_dir() -> PathBuf { data_dir } -pub fn get_collections_dir() -> PathBuf { - let data_dir = get_data_dir(); +pub fn collections_dir() -> PathBuf { + let data_dir = data_dir(); data_dir.join(COLLECTIONS_DIR) } +#[tracing::instrument] pub fn get_or_create_collections_dir() -> PathBuf { - let collections_dir = get_collections_dir(); + let collections_dir = collections_dir(); if !collections_dir.exists() && !collections_dir.is_dir() { match std::fs::create_dir(&collections_dir) { @@ -47,7 +49,6 @@ pub fn get_or_create_collections_dir() -> PathBuf { // if we fail to do so, panicking is adequate as we won't be able to properly run the // application Err(_) => { - tracing::error!("failed to create collections_dir at: {collections_dir:?}"); panic!("failed to create collections_dir at: {collections_dir:?}"); } } @@ -55,7 +56,3 @@ pub fn get_or_create_collections_dir() -> PathBuf { collections_dir } - -pub fn log_file() -> (PathBuf, String) { - (get_data_dir(), format!("{}.log", APP_NAME)) -} diff --git a/hac-loader/test_collections/basic.json b/hac-loader/test_collections/basic.json new file mode 100644 index 0000000..6290ab6 --- /dev/null +++ b/hac-loader/test_collections/basic.json @@ -0,0 +1,37 @@ +{ + "info": { + "name": "Basic Collection", + "description": "Basic Collection Description" + }, + "requests": [ + { + "name": "Sample Folder", + "requests": [ + { + "name": "Sample Parented Request", + "method": "POST", + "uri": "https://example.com", + "headers": [ + { "key": "Sample Header 1", "val": "Header Value 1", "enabled": true }, + { "key": "Sample Header 2", "val": "Header Value 2", "enabled": false } + ], + "authKind": "NO_AUTH", + "bodyKind": "JSON", + "body": "{\"sampleJsonBody\": { \"sampleKey\": \"sampleValue\" } }" + } + ] + }, + { + "name": "Sample Root Request", + "method": "POST", + "uri": "https://example.com", + "headers": [ + { "key": "Sample Header 1", "val": "Header Value 1", "enabled": true }, + { "key": "Sample Header 2", "val": "Header Value 2", "enabled": false } + ], + "authKind": "NO_AUTH", + "bodyKind": "NO_BODY", + "body": "{\"sampleJsonBody\": { \"sampleKey\": \"sampleValue\" } }" + } + ] +} diff --git a/hac-loader/test_collections/complex.json b/hac-loader/test_collections/complex.json new file mode 100644 index 0000000..4ef9949 --- /dev/null +++ b/hac-loader/test_collections/complex.json @@ -0,0 +1,84 @@ +{ + "info": { + "name": "Basic Collection", + "description": "Basic Collection Description" + }, + "requests": [ + { + "name": "Root Request 1", + "method": "POST", + "uri": "https://example.com", + "headers": [ + { "key": "Sample Header 1", "val": "Header Value 1", "enabled": true }, + { "key": "Sample Header 2", "val": "Header Value 2", "enabled": false } + ], + "authKind": "NO_AUTH", + "bodyKind": "NO_BODY", + "body": "{\"sampleJsonBody\": { \"sampleKey\": \"sampleValue\" } }" + }, + { + "name": "Folder 1", + "requests": [ + { + "name": "Folder 1 - 1", + "requests": [ + { + "name": "Parented Request 1 Folder 1 - 1", + "method": "POST", + "uri": "https://example.com", + "headers": [ + { "key": "Sample Header 1", "val": "Header Value 1", "enabled": true }, + { "key": "Sample Header 2", "val": "Header Value 2", "enabled": false } + ], + "authKind": "NO_AUTH", + "bodyKind": "JSON", + "body": "{\"sampleJsonBody\": { \"sampleKey\": \"sampleValue\" } }" + }, + { + "name": "Folder 1 - 2", + "requests": [ + { + "name": "Parented Request 1 Folder 1 - 2", + "method": "POST", + "uri": "https://example.com", + "headers": [ + { "key": "Sample Header 1", "val": "Header Value 1", "enabled": true }, + { "key": "Sample Header 2", "val": "Header Value 2", "enabled": false } + ], + "authKind": "NO_AUTH", + "bodyKind": "JSON", + "body": "{\"sampleJsonBody\": { \"sampleKey\": \"sampleValue\" } }" + } + ] + }, + { + "name": "Parented Request 2 Folder 1 - 1", + "method": "POST", + "uri": "https://example.com", + "headers": [ + { "key": "Sample Header 1", "val": "Header Value 1", "enabled": true }, + { "key": "Sample Header 2", "val": "Header Value 2", "enabled": false } + ], + "authKind": "NO_AUTH", + "bodyKind": "JSON", + "body": "{\"sampleJsonBody\": { \"sampleKey\": \"sampleValue\" } }" + } + ] + }, + { + "name": "Parented Request 1 Folder 1", + "method": "POST", + "uri": "https://example.com", + "headers": [ + { "key": "Sample Header 1", "val": "Header Value 1", "enabled": true }, + { "key": "Sample Header 2", "val": "Header Value 2", "enabled": false } + ], + "authKind": "NO_AUTH", + "bodyKind": "JSON", + "body": "{\"sampleJsonBody\": { \"sampleKey\": \"sampleValue\" } }" + } + ] + } + ] +} + diff --git a/hac-store/Cargo.toml b/hac-store/Cargo.toml new file mode 100644 index 0000000..bfaa145 --- /dev/null +++ b/hac-store/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "hac-store" +version = "0.2.0" +edition = "2021" + +[dependencies] +serde.workspace = true diff --git a/hac-store/src/collection.rs b/hac-store/src/collection.rs new file mode 100644 index 0000000..cec0920 --- /dev/null +++ b/hac-store/src/collection.rs @@ -0,0 +1,66 @@ +use crate::slab::{Key, Slab}; + +#[derive(Debug)] +pub enum ReqTree { + Req(Key), + Folder(Vec), +} + +#[derive(Debug)] +pub struct Collection { + pub info: CollectionInfo, + pub requests: Slab, + pub root_requests: Slab, + pub folders: Slab, +} + +#[derive(Debug)] +pub struct CollectionInfo { + pub name: String, + pub description: String, +} + +#[derive(Debug)] +pub enum ReqMethod { + Get, + Post, + Put, + Patch, + Delete, +} + +#[derive(Debug)] +pub struct HeaderEntry { + pub key: String, + pub val: String, + pub enabled: bool, +} + +#[derive(Debug)] +pub enum BodyKind { + Json, + NoBody, +} + +#[derive(Debug)] +pub struct Request { + pub parent: Option, + pub method: ReqMethod, + pub name: String, + pub uri: String, + pub headers: Vec, + pub body: String, + pub body_kind: BodyKind, +} + +#[derive(Debug)] +pub enum KeyKind { + Req(Key), + Folder(Key), +} + +#[derive(Debug, Default)] +pub struct Folder { + pub name: String, + pub requests: Vec, +} diff --git a/hac-store/src/lib.rs b/hac-store/src/lib.rs new file mode 100644 index 0000000..8a33a95 --- /dev/null +++ b/hac-store/src/lib.rs @@ -0,0 +1,2 @@ +pub mod collection; +pub mod slab; diff --git a/hac-store/src/slab.rs b/hac-store/src/slab.rs new file mode 100644 index 0000000..ba868f8 --- /dev/null +++ b/hac-store/src/slab.rs @@ -0,0 +1,127 @@ +pub type Key = usize; + +#[derive(Debug)] +pub enum Entry { + Full(T), + Free(Option), +} + +impl Entry { + pub fn swap(&mut self, val: T) { + debug_assert!(matches!(self, Entry::Free(_))); + std::mem::swap(self, &mut Self::full(val)) + } + + pub fn free(idx: Option) -> Self { + Self::Free(idx) + } + + pub fn full(val: T) -> Self { + Self::Full(val) + } +} + +#[derive(Debug, Default)] +pub struct Slab { + inner: Vec>, + next_idx: Option, +} + +impl Slab { + pub fn new() -> Self { + Self { + inner: vec![], + next_idx: None, + } + } + + pub fn push(&mut self, val: T) -> Key { + match self.next_idx.take() { + Some(idx) => { + let entry = &mut self.inner[idx]; + + let Entry::Free(next_idx) = entry else { + panic!("attempt to insert into a full slot."); + }; + + self.next_idx = next_idx.take(); + entry.swap(val); + idx + } + None => { + self.inner.push(Entry::full(val)); + self.inner.len() - 1 + } + } + } + + pub fn remove(&mut self, idx: Key) -> T { + let mut entry = Entry::free(self.next_idx.take()); + self.next_idx = Some(idx); + std::mem::swap(&mut self.inner[idx], &mut entry); + + match entry { + Entry::Full(val) => val, + Entry::Free(_) => panic!("cannot remove a free entry"), + } + } + + pub fn get(&self, idx: Key) -> &T { + let Entry::Full(val) = &self.inner[idx] else { + panic!("attempted to get an empty entry"); + }; + val + } + + pub fn get_mut(&mut self, idx: Key) -> &mut T { + let Entry::Full(val) = &mut self.inner[idx] else { + panic!("attempted to get an empty entry"); + }; + val + } + + pub fn len(&self) -> usize { + self.inner.len() + } + + pub fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + pub fn next_idx(&self) -> Key { + match self.next_idx { + Some(idx) => idx, + None => self.len(), + } + } + + pub fn iter(&mut self) -> impl Iterator { + self.inner.iter().filter_map(|e| match e { + Entry::Full(val) => Some(val), + Entry::Free(_) => None, + }) + } + + pub fn iter_mut(&mut self) -> impl Iterator { + self.inner.iter_mut().filter_map(|e| match e { + Entry::Full(val) => Some(val), + Entry::Free(_) => None, + }) + } +} + +impl IntoIterator for Slab { + type Item = T; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.inner + .into_iter() + .filter_map(|e| match e { + Entry::Full(val) => Some(val), + Entry::Free(_) => None, + }) + .collect::>() + .into_iter() + } +} From 51624640e4e579ea9797b30e5785eebf61c9d243 Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Thu, 19 Sep 2024 18:23:52 -0300 Subject: [PATCH 02/23] feat: revamping dashboard layout and functionality --- Cargo.lock | 164 ++++++------- hac-client/Cargo.toml | 1 - hac-client/src/ascii.rs | 21 ++ .../src/{components.rs => components/mod.rs} | 0 hac-client/src/pages.rs | 5 +- .../collection_dashboard.rs | 221 +++++++++++++++--- hac-client/src/pages/terminal_too_small.rs | 6 +- hac-client/src/pages/under_construction.rs | 21 +- hac-client/src/screen_manager.rs | 2 +- hac-loader/Cargo.toml | 1 + hac-loader/src/collection_loader/mod.rs | 115 ++++++++- 11 files changed, 398 insertions(+), 159 deletions(-) rename hac-client/src/{components.rs => components/mod.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index d093fcc..06e46f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.14" @@ -177,6 +192,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + [[package]] name = "clap" version = "4.5.15" @@ -196,7 +225,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", "terminal_size", ] @@ -300,41 +329,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "darling" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" -dependencies = [ - "darling_core", - "quote", - "syn", -] - [[package]] name = "deranged" version = "0.3.11" @@ -344,37 +338,6 @@ dependencies = [ "powerfmt", ] -[[package]] -name = "derive_builder" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" -dependencies = [ - "derive_builder_core", - "syn", -] - [[package]] name = "dirs" version = "5.0.1" @@ -455,12 +418,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "font8x8" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -618,7 +575,6 @@ dependencies = [ "tracing-appender", "tracing-subscriber", "tree-sitter", - "tui-big-text", "uuid", ] @@ -669,6 +625,7 @@ name = "hac-loader" version = "0.2.0" dependencies = [ "anyhow", + "chrono", "dirs", "hac-config", "hac-store", @@ -809,10 +766,27 @@ dependencies = [ ] [[package]] -name = "ident_case" -version = "1.0.1" +name = "iana-time-zone" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] [[package]] name = "idna" @@ -1009,6 +983,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -1588,12 +1571,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9557cb6521e8d009c51a8666f09356f4b817ba9ba0981a305bd86aee47bd35c" -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "strsim" version = "0.11.1" @@ -1942,18 +1919,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tui-big-text" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74e81324f07ec6df239679d17120e479b09a29a564df88f7ae2e1979ecc717a8" -dependencies = [ - "derive_builder", - "font8x8", - "itertools 0.12.1", - "ratatui", -] - [[package]] name = "unicode-bidi" version = "0.3.15" @@ -2168,6 +2133,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/hac-client/Cargo.toml b/hac-client/Cargo.toml index e1e8f25..49107c4 100644 --- a/hac-client/Cargo.toml +++ b/hac-client/Cargo.toml @@ -33,7 +33,6 @@ divan.workspace = true lazy_static.workspace = true futures = "0.3.30" -tui-big-text = { version = "0.4.3" } tracing-subscriber = { version = "0.3.18" } tracing-appender = "0.2.3" uuid = { version = "1.10.0", features = ["v4", "fast-rng", "macro-diagnostics"] } diff --git a/hac-client/src/ascii.rs b/hac-client/src/ascii.rs index b65a547..20aaaf2 100755 --- a/hac-client/src/ascii.rs +++ b/hac-client/src/ascii.rs @@ -130,6 +130,27 @@ pub static LOGO_ASCII: &[&[&str]] = &[ r#" @% "#, r#" :" "#, ], + &[ + r#" _ "#, + r#"| |__ __ _ ___ "#, + r#"| '_ \ / _` |/ __|"#, + r#"| | | | (_| | (__ "#, + r#"|_| |_|\__,_|\___|"#, + ], + &[ + r#" __ "#, + r#" / /_ ____ ______"#, + r#" / __ \/ __ `/ ___/"#, + r#" / / / / /_/ / /__ "#, + r#"/_/ /_/\__,_/\___/ "#, + ], + &[ + r#" __ __ ______ ______ "#, + r#"/\ \_\ \ /\ __ \ /\ ___\ "#, + r#"\ \ __ \ \ \ __ \ \ \ \____ "#, + r#" \ \_\ \_\ \ \_\ \_\ \ \_____\ "#, + r#" \/_/\/_/ \/_/\/_/ \/_____/ "#, + ], ]; pub static UNDER_CONSTRUCTION: &[&str] = &[ diff --git a/hac-client/src/components.rs b/hac-client/src/components/mod.rs similarity index 100% rename from hac-client/src/components.rs rename to hac-client/src/components/mod.rs diff --git a/hac-client/src/pages.rs b/hac-client/src/pages.rs index 708c2da..9cb05a1 100755 --- a/hac-client/src/pages.rs +++ b/hac-client/src/pages.rs @@ -16,7 +16,10 @@ use tokio::sync::mpsc::UnboundedSender; /// A `Page` is anything that is a top level page and can be drawn to the screen pub trait Renderable { - fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()>; + #[allow(unused_variables)] + fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { + Ok(()) + } /// pages need to adapt to change of sizes on the application, this function is called /// by the top level event loop whenever a resize event is produced diff --git a/hac-client/src/pages/collection_dashboard/collection_dashboard.rs b/hac-client/src/pages/collection_dashboard/collection_dashboard.rs index cdc477f..3780125 100755 --- a/hac-client/src/pages/collection_dashboard/collection_dashboard.rs +++ b/hac-client/src/pages/collection_dashboard/collection_dashboard.rs @@ -1,5 +1,5 @@ use hac_core::command::Command; -use hac_loader::collection_loader::CollectionMeta; +use hac_loader::collection_loader::{CollectionMeta, ReadableByteSize}; use hac_store::collection::Collection; use crate::pages::collection_dashboard::collection_list::{CollectionList, CollectionListState}; @@ -15,10 +15,9 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::layout::{Alignment, Constraint, Direction, Flex, Layout, Rect}; use ratatui::style::{Style, Stylize}; use ratatui::text::Line; -use ratatui::widgets::{Block, Clear, Padding, Paragraph, StatefulWidget, Widget, Wrap}; +use ratatui::widgets::{Block, BorderType, Borders, Clear, Padding, Paragraph, StatefulWidget, Widget, Wrap}; use ratatui::Frame; use tokio::sync::mpsc::UnboundedSender; -use tui_big_text::{BigText, PixelSize}; #[derive(Debug, PartialEq)] struct DashboardLayout { @@ -31,14 +30,40 @@ struct DashboardLayout { error_popup: Rect, } -#[derive(Debug)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +enum SortingKind { + #[default] + Recent, + Name, + Size, +} + +impl SortingKind { + pub fn next(&self) -> Self { + match self { + SortingKind::Recent => SortingKind::Name, + SortingKind::Name => SortingKind::Size, + SortingKind::Size => SortingKind::Recent, + } + } + + pub fn prev(&self) -> Self { + match self { + SortingKind::Recent => SortingKind::Size, + SortingKind::Name => SortingKind::Recent, + SortingKind::Size => SortingKind::Name, + } + } +} + pub struct CollectionDashboard<'a> { colors: &'a hac_colors::Colors, collections: Vec, + sorting_kind: SortingKind, selected: usize, + scroll: usize, layout: DashboardLayout, - list: CollectionList<'a>, form_state: FormState, filter: String, pane_focus: PaneFocus, @@ -58,6 +83,8 @@ enum PaneFocus { } impl<'a> CollectionDashboard<'a> { + const LIST_ITEM_HEIGHT: u16 = 3; + pub fn new( size: Rect, colors: &'a hac_colors::Colors, @@ -68,10 +95,11 @@ impl<'a> CollectionDashboard<'a> { collections, colors, selected: 0, + scroll: 0, + sorting_kind: SortingKind::default(), form_state: FormState::default(), layout: build_layout(size), - list: CollectionList::new(colors), filter: String::new(), command_sender: None, error_message: String::default(), @@ -80,6 +108,127 @@ impl<'a> CollectionDashboard<'a> { }) } + fn sort_list(&mut self) { + match self.sorting_kind { + SortingKind::Recent => self.collections.sort_by(|a, b| a.modified().cmp(b.modified())), + SortingKind::Name => self.collections.sort_by(|a, b| a.name().cmp(b.name())), + SortingKind::Size => self.collections.sort_by_key(|a| std::cmp::Reverse(a.size())), + } + } + + fn draw_title(&self, frame: &mut Frame) -> anyhow::Result<()> { + let selected_style = |kind: SortingKind| { + if kind == self.sorting_kind { + self.colors.bright.blue + } else { + self.colors.bright.black + } + }; + + let title = " HAC ".bg(self.colors.normal.red).fg(self.colors.normal.white); + let sorting = vec![ + "Most recent".fg(selected_style(SortingKind::Recent)), + " ❘ ".fg(self.colors.bright.black), + "By name".fg(selected_style(SortingKind::Name)), + " ❘ ".fg(self.colors.bright.black), + "By size".fg(selected_style(SortingKind::Size)), + ]; + + let lines = vec![Line::from(title), "".into(), Line::from(sorting)]; + frame.render_widget(Paragraph::new(lines), self.layout.title_pane); + + Ok(()) + } + + fn max_items_onscreen(&self) -> usize { + (self.layout.collections_pane.height / Self::LIST_ITEM_HEIGHT).into() + } + + fn draw_collection_list(&mut self, frame: &mut Frame) -> anyhow::Result<()> { + let layout = self.layout.collections_pane; + let max_items = self.max_items_onscreen(); + + for (ref mut idx, (i, item)) in self + .collections + .iter() + .enumerate() + .skip(self.scroll) + .take(max_items) + .enumerate() + { + let selected = i == self.selected; + + let item = match selected { + true => Paragraph::new(vec![ + Line::from(item.name().to_string().fg(self.colors.bright.red)), + Line::from(vec![ + item.modified().to_string().fg(self.colors.bright.black), + " - ".fg(self.colors.bright.black), + item.size().readable_byte_size().fg(self.colors.bright.black), + ]), + ]) + .block( + Block::new() + .borders(Borders::LEFT) + .fg(self.colors.bright.red) + .padding(Padding::left(1)), + ), + false => Paragraph::new(vec![ + Line::from(item.name().to_string().fg(self.colors.normal.white)), + Line::from(vec![ + item.modified().to_string().fg(self.colors.bright.black), + " - ".fg(self.colors.bright.black), + item.size().readable_byte_size().fg(self.colors.bright.black), + ]), + ]) + .block(Block::new().padding(Padding::left(2))), + }; + + let size = Rect::new(layout.x, layout.y + (*idx as u16 * 3), layout.width, 2); + frame.render_widget(item, size); + *idx += 1; + } + + Ok(()) + } + + fn draw_hint_text(&self, frame: &mut Frame) { + let hint = vec![ + "j/k} ↑/↓".fg(self.colors.normal.green), + " - choose • ".fg(self.colors.bright.black), + "n".fg(self.colors.normal.green), + " - new • ".fg(self.colors.bright.black), + "enter".fg(self.colors.normal.green), + " - select • ".fg(self.colors.bright.black), + "?".fg(self.colors.normal.green), + " - show more • ".fg(self.colors.bright.black), + "ctrl c".fg(self.colors.normal.green), + " - quit".fg(self.colors.bright.black), + ]; + + frame.render_widget(Line::from(hint), self.layout.hint_pane); + } + + fn draw_background(&self, size: Rect, frame: &mut Frame) { + frame.render_widget(Clear, size); + frame.render_widget(Block::default().bg(self.colors.primary.background), size); + } + + fn maybe_scroll_list(&mut self) { + if self.scroll > self.selected { + let offset = self.scroll - self.selected; + self.scroll -= offset; + return; + } + + let max_items = self.max_items_onscreen() - 1; + let normalized = self.selected - self.scroll; + if normalized >= max_items { + let offset = self.selected - max_items; + self.scroll = offset; + } + } + //pub fn display_error(&mut self, message: String) { // self.pane_focus = PaneFocus::Error; // self.error_message = message; @@ -328,13 +477,6 @@ impl<'a> CollectionDashboard<'a> { // Ok(None) //} // - fn draw_hint_text(&self, frame: &mut Frame) { - let hint = "[j/k ↑/↓ choose] [n -> new] [enter -> select] [? -> show more] [ctrl c -> quit]" - .fg(self.colors.normal.magenta) - .into_centered_line(); - - frame.render_widget(hint, self.layout.hint_pane); - } // //fn draw_help_popup(&self, frame: &mut Frame) { // make_overlay(self.colors, self.colors.primary.background, 0.2, frame); @@ -397,7 +539,6 @@ impl<'a> CollectionDashboard<'a> { // frame.render_widget(filter, self.layout.hint_pane); //} // - fn draw_collection_list(&mut self, frame: &mut Frame) {} // //fn draw_no_matches_text(&self, frame: &mut Frame) -> anyhow::Result<()> { // let layout = Layout::default() @@ -444,10 +585,6 @@ impl<'a> CollectionDashboard<'a> { // Ok(()) //} // - fn draw_background(&self, size: Rect, frame: &mut Frame) { - frame.render_widget(Clear, size); - frame.render_widget(Block::default().bg(self.colors.primary.background), size); - } // //fn draw_error_popup(&self, frame: &mut Frame) { // let popup = ErrorPopup::new(self.error_message.clone(), self.colors); @@ -488,24 +625,14 @@ impl<'a> CollectionDashboard<'a> { // confirm_popup.render(self.layout.confirm_popup, frame.buffer_mut()); //} // - //fn draw_title(&self, frame: &mut Frame) -> anyhow::Result<()> { - // let title = BigText::builder() - // .pixel_size(PixelSize::Quadrant) - // .style(Style::default().fg(self.colors.normal.red)) - // .lines(vec!["Select a collection".into()]) - // .alignment(Alignment::Center) - // .build()?; - // - // frame.render_widget(title, self.layout.title_pane); - // - // Ok(()) - //} } impl Renderable for CollectionDashboard<'_> { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { self.draw_background(size, frame); - //self.draw_title(frame)?; + self.draw_title(frame)?; + + self.draw_collection_list(frame)?; //match ( // self.collections.is_empty(), @@ -547,7 +674,27 @@ impl Eventful for CollectionDashboard<'_> { if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { return Ok(Some(Command::Quit)); }; - // + + match key_event.code { + KeyCode::Char('j') | KeyCode::Down => { + self.selected = usize::min(self.selected + 1, self.collections.len() - 1); + self.maybe_scroll_list(); + } + KeyCode::Char('k') | KeyCode::Up => { + self.selected = self.selected.saturating_sub(1); + self.maybe_scroll_list(); + } + KeyCode::Tab => { + self.sorting_kind = self.sorting_kind.next(); + self.sort_list(); + } + KeyCode::BackTab => { + self.sorting_kind = self.sorting_kind.prev(); + self.sort_list(); + } + _ => {} + } + //match self.pane_focus { // PaneFocus::List => self.handle_list_key_event(key_event), // PaneFocus::Form => self.handle_form_key_event(key_event), @@ -559,6 +706,7 @@ impl Eventful for CollectionDashboard<'_> { // Ok(None) // } //} + Ok(None) } } @@ -570,9 +718,14 @@ fn build_layout(size: Rect) -> DashboardLayout { .constraints([Constraint::Fill(1), Constraint::Length(1)]) .areas(size); - let [_, title_pane, collections_pane] = Layout::default() + let [_, title_pane, _, collections_pane] = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Length(1), Constraint::Length(5), Constraint::Fill(1)]) + .constraints([ + Constraint::Length(1), + Constraint::Length(3), + Constraint::Length(1), + Constraint::Fill(1), + ]) .areas(top); let help_popup = Rect::new( diff --git a/hac-client/src/pages/terminal_too_small.rs b/hac-client/src/pages/terminal_too_small.rs index 8edcd1f..464b889 100755 --- a/hac-client/src/pages/terminal_too_small.rs +++ b/hac-client/src/pages/terminal_too_small.rs @@ -52,11 +52,7 @@ impl Renderable for TerminalTooSmall<'_> { fn build_layout(size: Rect) -> Rect { Layout::default() - .constraints([ - Constraint::Fill(1), - Constraint::Length(5), - Constraint::Fill(1), - ]) + .constraints([Constraint::Fill(1), Constraint::Length(5), Constraint::Fill(1)]) .direction(Direction::Vertical) .flex(Flex::Center) .split(size)[1] diff --git a/hac-client/src/pages/under_construction.rs b/hac-client/src/pages/under_construction.rs index 7722656..7603f2a 100755 --- a/hac-client/src/pages/under_construction.rs +++ b/hac-client/src/pages/under_construction.rs @@ -25,19 +25,11 @@ impl Renderable for UnderConstruction<'_> { let icon_height = icon_lines.len(); let icon_half_height = icon_height.div_ceil(2); let half_height = size.height.div_ceil(2); - let starting_y = half_height - .saturating_sub(icon_half_height as u16) - .saturating_sub(1); + let starting_y = half_height.saturating_sub(icon_half_height as u16).saturating_sub(1); - let icon_size = Rect::new( - size.x, - size.y.add(starting_y), - size.width, - icon_height as u16, - ); + let icon_size = Rect::new(size.x, size.y.add(starting_y), size.width, icon_height as u16); - let message = - Line::from("Hold on, we're cooking up something new!").fg(self.colors.normal.red); + let message = Line::from("Hold on, we're cooking up something new!").fg(self.colors.normal.red); let message_size = Rect::new( size.x, @@ -47,12 +39,7 @@ impl Renderable for UnderConstruction<'_> { ); if icon_height >= (size.height - 3).into() { - let rect = Rect::new( - size.x, - size.y.add(size.height.div_ceil(2).sub(1)), - size.width, - 1, - ); + let rect = Rect::new(size.x, size.y.add(size.height.div_ceil(2).sub(1)), size.width, 1); frame.render_widget(Paragraph::new(message).centered(), rect); return Ok(()); } diff --git a/hac-client/src/screen_manager.rs b/hac-client/src/screen_manager.rs index 1574d63..e60da9d 100755 --- a/hac-client/src/screen_manager.rs +++ b/hac-client/src/screen_manager.rs @@ -118,7 +118,7 @@ impl<'sm> ScreenManager<'sm> { impl Renderable for ScreenManager<'_> { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { - match (size.width < 80, size.height < 22) { + match (size.width < 75, size.height < 22) { (true, _) => self.switch_screen(Screens::TerminalTooSmall), (_, true) => self.switch_screen(Screens::TerminalTooSmall), (false, false) if self.curr_screen.eq(&Screens::TerminalTooSmall) => self.restore_screen(), diff --git a/hac-loader/Cargo.toml b/hac-loader/Cargo.toml index 5a29bd3..dc4fa1c 100644 --- a/hac-loader/Cargo.toml +++ b/hac-loader/Cargo.toml @@ -12,3 +12,4 @@ serde_json.workspace = true anyhow.workspace = true tracing.workspace = true dirs = "5.0.1" +chrono = "0.4.38" diff --git a/hac-loader/src/collection_loader/mod.rs b/hac-loader/src/collection_loader/mod.rs index 13f9a8a..1c4e1f2 100644 --- a/hac-loader/src/collection_loader/mod.rs +++ b/hac-loader/src/collection_loader/mod.rs @@ -1,9 +1,11 @@ mod json_collection; mod json_loader; +use chrono::{Datelike, Timelike}; use hac_config::config::CollectionExtensions; use hac_store::collection::Collection; use json_loader::JsonLoader; +use std::time::SystemTime; pub trait IntoCollection { fn into_collection(self) -> hac_store::collection::Collection; @@ -33,21 +35,124 @@ pub fn load_collection>( // TODO: i probably want to introduce some metadata storage, or caching to know // total requests, total saved responses, and other infos without having to read // the entire collection. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct CollectionMeta { name: String, path: std::path::PathBuf, + size: u64, + modified: CollectionModifiedMeta, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CollectionModifiedMeta { + year: i32, + month: u32, + day: u32, + hour: u32, + minutes: u32, + system_time: SystemTime, +} + +impl PartialOrd for CollectionModifiedMeta { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.system_time.cmp(&other.system_time)) + } +} + +impl Ord for CollectionModifiedMeta { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.system_time.cmp(&other.system_time) + } +} + +impl std::fmt::Display for CollectionModifiedMeta { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}-{}-{} {}:{}", + self.year, self.month, self.day, self.hour, self.minutes, + ) + } +} + +impl CollectionMeta { + pub fn name(&self) -> &str { + &self.name + } + + pub fn path(&self) -> &std::path::PathBuf { + &self.path + } + + pub fn modified(&self) -> &CollectionModifiedMeta { + &self.modified + } + + pub fn size(&self) -> u64 { + self.size + } +} + +pub trait ReadableByteSize { + fn readable_byte_size(&self) -> String; +} + +fn readable_byte_size(val: N) -> String +where + N: Copy + Into, +{ + let units = ["B", "KB", "MB", "GB", "TB", "PB"]; + let mut size = val.into() as f64; + let mut unit_index = 0; + + while size >= 1024.0 && unit_index < units.len() - 1 { + size /= 1024.0; + unit_index += 1; + } + + format!("{:.2}{}", size, units[unit_index]) +} + +impl ReadableByteSize for u64 { + fn readable_byte_size(&self) -> String { + readable_byte_size(*self) + } +} + +fn format_modified_date(modified_time: SystemTime) -> anyhow::Result { + let datetime = chrono::DateTime::::from(modified_time); + let year = datetime.year(); + let month = datetime.day(); + let day = datetime.month(); + let hour = datetime.hour(); + let minutes = datetime.minute(); + Ok(CollectionModifiedMeta { + year, + month, + day, + hour, + minutes, + system_time: modified_time, + }) } pub fn collections_metadata() -> anyhow::Result> { let entries = std::fs::read_dir(super::collections_dir())?.flatten(); - let collections = entries.fold(vec![], |mut acc, entry| { + let mut collections = vec![]; + for entry in entries { let name = entry.file_name().to_string_lossy().to_string(); let path = entry.path(); - acc.push(CollectionMeta { name, path }); - acc - }); + let metadata = entry.metadata()?; + let modified = format_modified_date(metadata.modified()?)?; + let size = metadata.len(); + collections.push(CollectionMeta { + name, + path, + modified, + size, + }); + } Ok(collections) } From 8950f1822e1f18867ee1c7ecbf3e6cf590d3b6dc Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Fri, 20 Sep 2024 21:49:30 -0300 Subject: [PATCH 03/23] wip: new routing mechanism and config --- .vscode/launch.json | 197 ++++++++++ TODO.md | 13 + hac-client/src/app.rs | 110 ++++-- hac-client/src/lib.rs | 10 + hac-client/src/main.rs | 9 +- hac-client/src/pages.rs | 19 +- .../collection_dashboard.rs | 28 +- .../pages/collection_list/collection_list.rs | 324 +++++++++++++++++ .../collection_list/create_collection.rs | 64 ++++ hac-client/src/pages/collection_list/mod.rs | 45 +++ .../collection_viewer/collection_viewer.rs | 60 +--- hac-client/src/pages/terminal_too_small.rs | 10 +- hac-client/src/router.rs | 109 ++++++ hac-client/src/screen_manager.rs | 339 +++++------------- .rustfmt.toml => rustfmt.toml | 3 +- 15 files changed, 980 insertions(+), 360 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 hac-client/src/pages/collection_list/collection_list.rs create mode 100644 hac-client/src/pages/collection_list/create_collection.rs create mode 100644 hac-client/src/pages/collection_list/mod.rs create mode 100644 hac-client/src/router.rs rename .rustfmt.toml => rustfmt.toml (89%) diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5714ef2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,197 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'hac_config'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=hac-config" + ], + "filter": { + "name": "hac_config", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'hac_core'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=hac-core" + ], + "filter": { + "name": "hac_core", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'hac_client'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=hac-client" + ], + "filter": { + "name": "hac_client", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'hac'", + "cargo": { + "args": [ + "build", + "--bin=hac", + "--package=hac-client" + ], + "filter": { + "name": "hac", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'hac'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=hac", + "--package=hac-client" + ], + "filter": { + "name": "hac", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug benchmark 'collection_viewer_bench'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bench=collection_viewer_bench", + "--package=hac-client" + ], + "filter": { + "name": "collection_viewer_bench", + "kind": "bench" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'hac_cli'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=hac-cli" + ], + "filter": { + "name": "hac_cli", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'hac_colors'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=hac-colors" + ], + "filter": { + "name": "hac_colors", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'hac_loader'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=hac-loader" + ], + "filter": { + "name": "hac_loader", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'hac_store'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=hac-store" + ], + "filter": { + "name": "hac_store", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/TODO.md b/TODO.md index 770b9a7..0919e57 100755 --- a/TODO.md +++ b/TODO.md @@ -15,3 +15,16 @@ - [ ] edit bodies in $EDITOR - [ ] add scripting to requests - [ ] export requests to other formats + + +```rust + +struct AppRouter { + routers: Vec, +} + +struct Router { + routes: HashMap>, +} + +``` diff --git a/hac-client/src/app.rs b/hac-client/src/app.rs index 6986359..fcad060 100755 --- a/hac-client/src/app.rs +++ b/hac-client/src/app.rs @@ -1,80 +1,122 @@ +use std::io::Stdout; +use std::rc::Rc; +use std::sync::mpsc::Receiver; +use std::sync::mpsc::Sender; +use std::sync::RwLock; + +use hac_colors::Colors; +use hac_config::Config; use hac_core::command::Command; use hac_loader::collection_loader::CollectionMeta; +use ratatui::backend::CrosstermBackend; +use ratatui::Terminal; -use crate::event_pool::{Event, EventPool}; -use crate::pages::{Eventful, Renderable}; -use crate::screen_manager::ScreenManager; +use crate::event_pool::Event; +use crate::event_pool::EventPool; +use crate::pages::collection_list::make_collection_list_router; +use crate::pages::Eventful; +use crate::pages::Renderable; +use crate::router::Router; +use crate::HacColors; +use crate::HacConfig; -use std::io::Stdout; +#[derive(Default, Hash, Debug, PartialEq, Eq, Copy, Clone)] +pub enum AppRoutes { + #[default] + CollectionListRouter, + CollectionViewerRouter, +} -use ratatui::{backend::CrosstermBackend, Terminal}; -use tokio::sync::mpsc; +impl Into for AppRoutes { + fn into(self) -> u8 { + match self { + Self::CollectionListRouter => 0, + Self::CollectionViewerRouter => 1, + } + } +} -pub struct App<'app> { +pub struct App { event_pool: EventPool, terminal: Terminal>, should_quit: bool, - screen_manager: ScreenManager<'app>, + command_receiver: Receiver, + command_sender: Sender, + router: Router, + + config: Rc>, + colors: Rc, } -impl<'app> App<'app> { - pub fn new( - colors: &'app hac_colors::Colors, - collections: Vec, - config: &'app hac_config::Config, - dry_run: bool, - ) -> anyhow::Result { +impl App { + pub fn new(collections: Vec, config: HacConfig, colors: HacColors) -> anyhow::Result { let terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?; + let (command_sender, command_receiver) = std::sync::mpsc::channel(); + let size = terminal.size()?; + + let mut router = Router::new(command_sender.clone()); + + let mut collection_list_router = make_collection_list_router( + command_sender.clone(), + collections.clone(), + size, + config.clone(), + colors.clone(), + ); + collection_list_router.attach_parent_navigator(router.navigate_sender()); + router.add_route(AppRoutes::CollectionListRouter.into(), Box::new(collection_list_router)); + Ok(Self { - screen_manager: ScreenManager::new(terminal.size()?, colors, collections, config, dry_run)?, event_pool: EventPool::new(60f64, 30f64), should_quit: false, + router, + command_receiver, + command_sender, terminal, + + config, + colors, }) } /// this is the main method which starts the event loop task, listen for events and commands /// to pass them down the chain, and render the terminal screen pub async fn run(&mut self) -> anyhow::Result<()> { - let (command_tx, mut command_rx) = mpsc::unbounded_channel(); self.event_pool.start(); startup()?; - self.screen_manager.register_command_handler(command_tx.clone())?; - loop { { - while let Ok(command) = command_rx.try_recv() { + while let Ok(command) = self.command_receiver.try_recv() { match command { Command::Quit => self.should_quit = true, - _ => self.screen_manager.handle_command(command), + _ => self.router.handle_command(command)?, } } } if let Some(event) = self.event_pool.next().await { match event { - Event::Tick => self.screen_manager.handle_tick()?, - Event::Resize(new_size) => self.screen_manager.resize(new_size), + Event::Tick => self.router.tick()?, + Event::Resize(new_size) => self.router.resize(new_size), + Event::Key(_) => { + if let Some(command) = self.router.handle_event(Some(event.clone()))? { + self.command_sender + .send(command) + .expect("failed to send command through channel") + } + } Event::Render => { self.terminal.draw(|f| { - let result = self.screen_manager.draw(f, f.size()); - if let Err(e) = result { - command_tx + if let Err(e) = self.router.draw(f, f.size()) { + self.command_sender .send(Command::Error(format!("Failed to draw: {:?}", e))) .expect("failed to send command through channel"); } })?; } - event => { - if let Some(command) = self.screen_manager.handle_event(Some(event.clone()))? { - command_tx - .send(command) - .expect("failed to send command through channel") - } - } - }; + } } if self.should_quit { diff --git a/hac-client/src/lib.rs b/hac-client/src/lib.rs index 03d2045..e69cdf6 100755 --- a/hac-client/src/lib.rs +++ b/hac-client/src/lib.rs @@ -1,7 +1,17 @@ +use std::rc::Rc; +use std::sync::RwLock; + +use hac_colors::Colors; +use hac_config::Config; + pub mod app; mod ascii; mod components; pub mod event_pool; pub mod pages; +mod router; pub mod screen_manager; pub mod utils; + +pub type HacConfig = Rc>; +pub type HacColors = Rc; diff --git a/hac-client/src/main.rs b/hac-client/src/main.rs index 0be96e2..e4521fd 100755 --- a/hac-client/src/main.rs +++ b/hac-client/src/main.rs @@ -1,6 +1,10 @@ +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::RwLock; + use hac_cli::RuntimeBehavior; use hac_client::app; -use hac_core::collection; +use hac_config::Config; fn setup_tracing() -> anyhow::Result { let logfile = hac_config::LOGFILE; @@ -43,10 +47,11 @@ async fn main() -> anyhow::Result<()> { hac_loader::get_or_create_collections_dir(); let collections = hac_loader::collection_loader::collections_metadata()?; + let colors = hac_colors::Colors::default(); let config = hac_config::load_config(); - let mut app = app::App::new(&colors, collections, &config, dry_run)?; + let mut app = app::App::new(collections, Rc::new(RwLock::new(config)), Rc::new(colors))?; app.run().await?; Ok(()) diff --git a/hac-client/src/pages.rs b/hac-client/src/pages.rs index 9cb05a1..246ee03 100755 --- a/hac-client/src/pages.rs +++ b/hac-client/src/pages.rs @@ -1,4 +1,5 @@ pub mod collection_dashboard; +pub mod collection_list; pub mod collection_viewer; pub mod confirm_popup; pub mod error_popup; @@ -8,11 +9,12 @@ mod spinner; pub mod terminal_too_small; mod under_construction; +use std::sync::mpsc::Sender; + use crate::event_pool::Event; use crossterm::event::KeyEvent; use hac_core::command::Command; use ratatui::{layout::Rect, Frame}; -use tokio::sync::mpsc::UnboundedSender; /// A `Page` is anything that is a top level page and can be drawn to the screen pub trait Renderable { @@ -21,6 +23,12 @@ pub trait Renderable { Ok(()) } + #[allow(unused_variables)] + fn update(&mut self, data: Option>) {} + + #[allow(unused_variables)] + fn attach_navigator(&mut self, navigator: std::sync::mpsc::Sender) {} + /// pages need to adapt to change of sizes on the application, this function is called /// by the top level event loop whenever a resize event is produced #[allow(unused_variables)] @@ -29,13 +37,16 @@ pub trait Renderable { /// register a page to be a command handler, which means this page will now receive /// commands from the channel to handle whatever the commands it is interested into #[allow(unused_variables)] - fn register_command_handler(&mut self, sender: UnboundedSender) -> anyhow::Result<()> { + fn register_command_handler(&mut self, sender: Sender) {} + + #[allow(unused_variables)] + fn handle_command(&mut self, command: Command) -> anyhow::Result<()> { Ok(()) } /// tick is a bigger interval than the one used by the render cycle, it is mainly used /// for actions that rely on time, such as syncing changes to disk - fn handle_tick(&mut self) -> anyhow::Result<()> { + fn tick(&mut self) -> anyhow::Result<()> { Ok(()) } } @@ -43,7 +54,7 @@ pub trait Renderable { /// An `Eventful` page is a page that can handle key events, and mouse events /// when support for them gets added. pub trait Eventful { - type Result; + type Result: std::any::Any; /// the top level event loop doesnt differentiate between kinds of events, so this is what /// delegate each kind of events to the responsible function diff --git a/hac-client/src/pages/collection_dashboard/collection_dashboard.rs b/hac-client/src/pages/collection_dashboard/collection_dashboard.rs index 3780125..ddc5e8d 100755 --- a/hac-client/src/pages/collection_dashboard/collection_dashboard.rs +++ b/hac-client/src/pages/collection_dashboard/collection_dashboard.rs @@ -8,8 +8,12 @@ use crate::pages::confirm_popup::ConfirmPopup; use crate::pages::error_popup::ErrorPopup; use crate::pages::overlay::{draw_overlay, make_overlay}; use crate::pages::{Eventful, Renderable}; +use crate::router::{EventfulRenderable, Navigate}; +use std::fmt::Debug; +use std::hash::Hash; use std::ops::{Add, Div, Not, Sub}; +use std::sync::mpsc::Sender; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::layout::{Alignment, Constraint, Direction, Flex, Layout, Rect}; @@ -17,7 +21,6 @@ use ratatui::style::{Style, Stylize}; use ratatui::text::Line; use ratatui::widgets::{Block, BorderType, Borders, Clear, Padding, Paragraph, StatefulWidget, Widget, Wrap}; use ratatui::Frame; -use tokio::sync::mpsc::UnboundedSender; #[derive(Debug, PartialEq)] struct DashboardLayout { @@ -56,20 +59,23 @@ impl SortingKind { } } +#[derive(Debug)] pub struct CollectionDashboard<'a> { - colors: &'a hac_colors::Colors, + colors: hac_colors::Colors, collections: Vec, sorting_kind: SortingKind, selected: usize, scroll: usize, + navigator: Option>, layout: DashboardLayout, form_state: FormState, filter: String, pane_focus: PaneFocus, - pub command_sender: Option>, + pub command_sender: Option>, error_message: String, dry_run: bool, + _a: std::marker::PhantomData<&'a ()>, } #[derive(Debug, PartialEq, Eq)] @@ -85,18 +91,14 @@ enum PaneFocus { impl<'a> CollectionDashboard<'a> { const LIST_ITEM_HEIGHT: u16 = 3; - pub fn new( - size: Rect, - colors: &'a hac_colors::Colors, - collections: Vec, - dry_run: bool, - ) -> anyhow::Result { - Ok(CollectionDashboard { + pub fn new(size: Rect, colors: hac_colors::Colors, collections: Vec, dry_run: bool) -> Self { + CollectionDashboard { collections, colors, selected: 0, scroll: 0, sorting_kind: SortingKind::default(), + navigator: None, form_state: FormState::default(), layout: build_layout(size), @@ -105,7 +107,8 @@ impl<'a> CollectionDashboard<'a> { error_message: String::default(), pane_focus: PaneFocus::List, dry_run, - }) + _a: std::marker::PhantomData, + } } fn sort_list(&mut self) { @@ -657,9 +660,8 @@ impl Renderable for CollectionDashboard<'_> { Ok(()) } - fn register_command_handler(&mut self, sender: UnboundedSender) -> anyhow::Result<()> { + fn register_command_handler(&mut self, sender: Sender) { self.command_sender = Some(sender.clone()); - Ok(()) } fn resize(&mut self, new_size: Rect) { diff --git a/hac-client/src/pages/collection_list/collection_list.rs b/hac-client/src/pages/collection_list/collection_list.rs new file mode 100644 index 0000000..85fefff --- /dev/null +++ b/hac-client/src/pages/collection_list/collection_list.rs @@ -0,0 +1,324 @@ +use std::fmt::Debug; +use std::sync::mpsc::channel; +use std::sync::mpsc::Sender; + +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use crossterm::event::KeyModifiers; +use hac_core::command::Command; +use hac_loader::collection_loader::CollectionMeta; +use hac_loader::collection_loader::ReadableByteSize; +use ratatui::layout::Alignment; +use ratatui::layout::Constraint; +use ratatui::layout::Direction; +use ratatui::layout::Flex; +use ratatui::layout::Layout; +use ratatui::layout::Rect; +use ratatui::style::Style; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::widgets::Block; +use ratatui::widgets::BorderType; +use ratatui::widgets::Borders; +use ratatui::widgets::Clear; +use ratatui::widgets::Padding; +use ratatui::widgets::Paragraph; +use ratatui::widgets::StatefulWidget; +use ratatui::widgets::Widget; +use ratatui::widgets::Wrap; +use ratatui::Frame; + +use crate::app::AppRoutes; +use crate::pages::collection_list::Routes; +use crate::pages::overlay::draw_overlay; +use crate::pages::overlay::make_overlay; +use crate::pages::Eventful; +use crate::pages::Renderable; +use crate::router::Navigate; +use crate::HacColors; +use crate::HacConfig; + +#[derive(Debug, PartialEq)] +struct DashboardLayout { + collections_pane: Rect, + hint_pane: Rect, + title_pane: Rect, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +enum SortingKind { + #[default] + Recent, + Name, + Size, +} + +impl SortingKind { + pub fn next(&self) -> Self { + match self { + SortingKind::Recent => SortingKind::Name, + SortingKind::Name => SortingKind::Size, + SortingKind::Size => SortingKind::Recent, + } + } + + pub fn prev(&self) -> Self { + match self { + SortingKind::Recent => SortingKind::Size, + SortingKind::Name => SortingKind::Recent, + SortingKind::Size => SortingKind::Name, + } + } +} + +#[derive(Debug)] +pub struct CollectionList { + config: HacConfig, + colors: HacColors, + sorting_kind: SortingKind, + collections: Vec, + selected: usize, + scroll: usize, + navigator: Sender, + layout: DashboardLayout, + pub command_sender: Option>, +} + +impl CollectionList { + const LIST_ITEM_HEIGHT: u16 = 3; + + pub fn new(collections: Vec, size: Rect, config: HacConfig, colors: HacColors) -> Self { + let (dummy, _) = channel(); + Self { + colors, + config, + + selected: 0, + scroll: 0, + sorting_kind: SortingKind::default(), + navigator: dummy, + collections, + layout: build_layout(size), + command_sender: None, + } + } + + fn sort_list(&mut self) { + match self.sorting_kind { + SortingKind::Name => self.collections.sort_by(|a, b| a.name().cmp(b.name())), + SortingKind::Recent => self.collections.sort_by(|a, b| a.modified().cmp(b.modified())), + SortingKind::Size => self.collections.sort_by_key(|a| std::cmp::Reverse(a.size())), + } + } + + fn draw_title(&self, frame: &mut Frame) -> anyhow::Result<()> { + let selected_style = |kind: SortingKind| { + if kind == self.sorting_kind { + self.colors.bright.blue + } else { + self.colors.bright.black + } + }; + + let title = " HAC ".bg(self.colors.normal.red).fg(self.colors.normal.white); + let sorting = vec![ + "Most recent".fg(selected_style(SortingKind::Recent)), + " ❘ ".fg(self.colors.bright.black), + "By name".fg(selected_style(SortingKind::Name)), + " ❘ ".fg(self.colors.bright.black), + "By size".fg(selected_style(SortingKind::Size)), + ]; + + let lines = vec![Line::from(title), "".into(), Line::from(sorting)]; + frame.render_widget(Paragraph::new(lines), self.layout.title_pane); + + Ok(()) + } + + fn max_items_onscreen(&self) -> usize { + (self.layout.collections_pane.height / Self::LIST_ITEM_HEIGHT).into() + } + + fn draw_collection_list(&mut self, frame: &mut Frame) -> anyhow::Result<()> { + let layout = self.layout.collections_pane; + let max_items = self.max_items_onscreen(); + + for (ref mut idx, (i, item)) in self + .collections + .iter() + .enumerate() + .skip(self.scroll) + .take(max_items) + .enumerate() + { + let selected = i == self.selected; + + let item = match selected { + true => Paragraph::new(vec![ + Line::from(item.name().to_string().fg(self.colors.bright.red)), + Line::from(vec![ + item.modified().to_string().fg(self.colors.bright.black), + " - ".fg(self.colors.bright.black), + item.size().readable_byte_size().fg(self.colors.bright.black), + ]), + ]) + .block( + Block::new() + .borders(Borders::LEFT) + .fg(self.colors.bright.red) + .padding(Padding::left(1)), + ), + false => Paragraph::new(vec![ + Line::from(item.name().to_string().fg(self.colors.normal.white)), + Line::from(vec![ + item.modified().to_string().fg(self.colors.bright.black), + " - ".fg(self.colors.bright.black), + item.size().readable_byte_size().fg(self.colors.bright.black), + ]), + ]) + .block(Block::new().padding(Padding::left(2))), + }; + + let size = Rect::new(layout.x, layout.y + (*idx as u16 * 3), layout.width, 2); + frame.render_widget(item, size); + *idx += 1; + } + + Ok(()) + } + + fn draw_hint_text(&self, frame: &mut Frame) { + let hint = vec![ + "j/k} ↑/↓".fg(self.colors.normal.green), + " - choose • ".fg(self.colors.bright.black), + "n".fg(self.colors.normal.green), + " - new • ".fg(self.colors.bright.black), + "enter".fg(self.colors.normal.green), + " - select • ".fg(self.colors.bright.black), + "?".fg(self.colors.normal.green), + " - show more • ".fg(self.colors.bright.black), + "ctrl c".fg(self.colors.normal.green), + " - quit".fg(self.colors.bright.black), + ]; + + frame.render_widget(Line::from(hint), self.layout.hint_pane); + } + + fn draw_background(&self, size: Rect, frame: &mut Frame) { + frame.render_widget(Clear, size); + frame.render_widget(Block::default().bg(self.colors.primary.background), size); + } + + fn maybe_scroll_list(&mut self) { + if self.scroll > self.selected { + let offset = self.scroll - self.selected; + self.scroll -= offset; + return; + } + + let max_items = self.max_items_onscreen() - 1; + let normalized = self.selected - self.scroll; + if normalized >= max_items { + let offset = self.selected - max_items; + self.scroll = offset; + } + } +} + +impl Renderable for CollectionList { + fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { + self.draw_background(size, frame); + self.draw_title(frame)?; + + self.draw_collection_list(frame)?; + self.draw_hint_text(frame); + + Ok(()) + } + + fn attach_navigator(&mut self, navigator: std::sync::mpsc::Sender) { + self.navigator = navigator; + } + + fn register_command_handler(&mut self, sender: Sender) { + self.command_sender = Some(sender.clone()); + } + + fn resize(&mut self, new_size: Rect) { + self.layout = build_layout(new_size); + } +} + +impl Eventful for CollectionList { + type Result = Command; + + fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { + return Ok(Some(Command::Quit)); + }; + + match key_event.code { + KeyCode::Char('j') | KeyCode::Down => { + self.selected = usize::min(self.selected + 1, self.collections.len() - 1); + self.maybe_scroll_list(); + } + KeyCode::Char('k') | KeyCode::Up => { + self.selected = self.selected.saturating_sub(1); + self.maybe_scroll_list(); + } + KeyCode::Tab => { + self.sorting_kind = self.sorting_kind.next(); + self.sort_list(); + } + KeyCode::BackTab => { + self.sorting_kind = self.sorting_kind.prev(); + self.sort_list(); + } + KeyCode::Char('n') => { + self.navigator + .send(Navigate::To(Routes::CreateCollection.into(), None)) + .expect("failed to send navigation message"); + } + KeyCode::Enter => { + if self.collections.is_empty() { + return Ok(None); + } + assert!(self.collections.len() > self.selected); + let path = self.collections[self.selected].path().clone(); + self.navigator + .send(Navigate::Up( + AppRoutes::CollectionViewerRouter.into(), + Some(Box::new(path)), + )) + .expect("failed to send navigation message"); + } + _ => {} + } + + Ok(None) + } +} + +fn build_layout(size: Rect) -> DashboardLayout { + let size = Rect::new(size.x + 1, size.y, size.width - 1, size.height); + let [top, help_pane] = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Fill(1), Constraint::Length(1)]) + .areas(size); + + let [_, title_pane, _, collections_pane] = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), + Constraint::Length(3), + Constraint::Length(1), + Constraint::Fill(1), + ]) + .areas(top); + + DashboardLayout { + collections_pane, + hint_pane: help_pane, + title_pane, + } +} diff --git a/hac-client/src/pages/collection_list/create_collection.rs b/hac-client/src/pages/collection_list/create_collection.rs new file mode 100644 index 0000000..ded6911 --- /dev/null +++ b/hac-client/src/pages/collection_list/create_collection.rs @@ -0,0 +1,64 @@ +use std::sync::mpsc::channel; +use std::sync::mpsc::Sender; + +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use crossterm::event::KeyModifiers; +use hac_core::command::Command; +use ratatui::layout::Rect; +use ratatui::widgets::Paragraph; +use ratatui::Frame; + +use crate::pages::Eventful; +use crate::pages::Renderable; +use crate::router::Navigate; +use crate::HacColors; +use crate::HacConfig; + +#[derive(Debug)] +pub struct CreateCollection { + size: Rect, + colors: HacColors, + config: HacConfig, + navigator: Sender, +} + +impl CreateCollection { + pub fn new(size: Rect, config: HacConfig, colors: HacColors) -> Self { + let (dummy, _) = channel(); + Self { + config, + colors, + size, + navigator: dummy, + } + } +} + +impl Renderable for CreateCollection { + fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { + frame.render_widget(Paragraph::new("lol"), self.size); + + Ok(()) + } + + fn resize(&mut self, new_size: Rect) { + self.size = new_size; + } + + fn attach_navigator(&mut self, navigator: Sender) { + self.navigator = navigator; + } +} + +impl Eventful for CreateCollection { + type Result = Command; + + fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { + return Ok(Some(Command::Quit)); + } + + Ok(None) + } +} diff --git a/hac-client/src/pages/collection_list/mod.rs b/hac-client/src/pages/collection_list/mod.rs new file mode 100644 index 0000000..b8b9f8a --- /dev/null +++ b/hac-client/src/pages/collection_list/mod.rs @@ -0,0 +1,45 @@ +mod collection_list; +mod create_collection; +use std::sync::mpsc::Sender; + +use collection_list::CollectionList; +use create_collection::CreateCollection; +use hac_core::command::Command; +use hac_loader::collection_loader::CollectionMeta; +use ratatui::layout::Rect; + +use crate::router::Router; +use crate::HacColors; +use crate::HacConfig; + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Default)] +pub enum Routes { + #[default] + ListCollections, + CreateCollection, +} + +impl Into for Routes { + fn into(self) -> u8 { + match self { + Self::ListCollections => 0, + Self::CreateCollection => 1, + } + } +} + +pub fn make_collection_list_router( + command_sender: Sender, + collections: Vec, + size: Rect, + config: HacConfig, + colors: HacColors, +) -> Router { + let mut router = Router::new(command_sender); + let collection_list = CollectionList::new(collections, size, config.clone(), colors.clone()); + let create_collection = CreateCollection::new(size, config.clone(), colors.clone()); + + router.add_route(Routes::ListCollections.into(), Box::new(collection_list)); + router.add_route(Routes::CreateCollection.into(), Box::new(create_collection)); + router +} diff --git a/hac-client/src/pages/collection_viewer/collection_viewer.rs b/hac-client/src/pages/collection_viewer/collection_viewer.rs index 592b338..313407e 100755 --- a/hac-client/src/pages/collection_viewer/collection_viewer.rs +++ b/hac-client/src/pages/collection_viewer/collection_viewer.rs @@ -13,6 +13,7 @@ use std::cell::RefCell; use std::collections::HashMap; use std::ops::{Add, Div}; use std::rc::Rc; +use std::sync::mpsc::Sender; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::layout::{Constraint, Direction, Layout, Rect}; @@ -84,7 +85,7 @@ pub struct CollectionViewer<'cv> { colors: &'cv hac_colors::Colors, config: &'cv hac_config::Config, layout: ExplorerLayout, - global_command_sender: Option>, + global_command_sender: Option>, collection_sync_timer: std::time::Instant, collection_store: Rc>, @@ -108,15 +109,9 @@ impl<'cv> CollectionViewer<'cv> { let sidebar = sidebar::Sidebar::new(colors, collection_store.clone()); - let request_editor = - RequestEditor::new(colors, config, collection_store.clone(), layout.req_editor); + let request_editor = RequestEditor::new(colors, config, collection_store.clone(), layout.req_editor); - let response_viewer = ResponseViewer::new( - colors, - collection_store.clone(), - None, - layout.response_preview, - ); + let response_viewer = ResponseViewer::new(colors, collection_store.clone(), None, layout.response_preview); let request_uri = RequestUri::new(colors, collection_store.clone(), layout.req_uri); @@ -152,11 +147,7 @@ impl<'cv> CollectionViewer<'cv> { None, self.layout.response_preview, ); - self.request_uri = RequestUri::new( - self.colors, - self.collection_store.clone(), - self.layout.req_uri, - ); + self.request_uri = RequestUri::new(self.colors, self.collection_store.clone(), self.layout.req_uri); } fn focus_next(&mut self) { @@ -233,15 +224,13 @@ impl<'cv> CollectionViewer<'cv> { *inner = request.clone(); } } - RequestKind::Nested(dir) => { - dir.requests.write().unwrap().iter_mut().for_each(|other| { - if let RequestKind::Single(inner) = other { - if request.read().unwrap().id.eq(&inner.read().unwrap().id) { - *inner = request.clone(); - } + RequestKind::Nested(dir) => dir.requests.write().unwrap().iter_mut().for_each(|other| { + if let RequestKind::Single(inner) = other { + if request.read().unwrap().id.eq(&inner.read().unwrap().id) { + *inner = request.clone(); } - }) - } + } + }), }); } @@ -288,8 +277,7 @@ impl Renderable for CollectionViewer<'_> { self.drain_responses_channel(); self.sidebar.draw(frame, self.layout.sidebar)?; - self.response_viewer - .draw(frame, self.layout.response_preview)?; + self.response_viewer.draw(frame, self.layout.response_preview)?; self.request_editor.draw(frame, self.layout.req_editor)?; self.request_uri.draw(frame, self.layout.req_uri)?; @@ -345,12 +333,7 @@ impl Renderable for CollectionViewer<'_> { .as_ref() .is_some_and(|pane| pane.eq(&PaneFocus::ReqUri)) { - if let Some(request) = self - .collection_store - .borrow() - .get_selected_request() - .as_ref() - { + if let Some(request) = self.collection_store.borrow().get_selected_request().as_ref() { frame.set_cursor( self.layout .req_uri @@ -365,16 +348,15 @@ impl Renderable for CollectionViewer<'_> { Ok(()) } - fn handle_tick(&mut self) -> anyhow::Result<()> { + fn tick(&mut self) -> anyhow::Result<()> { if self.collection_sync_timer.elapsed().as_secs().ge(&5) { self.sync_collection_changes(); } Ok(()) } - fn register_command_handler(&mut self, sender: UnboundedSender) -> anyhow::Result<()> { + fn register_command_handler(&mut self, sender: Sender) { self.global_command_sender = Some(sender); - Ok(()) } fn resize(&mut self, new_size: Rect) { @@ -396,10 +378,8 @@ impl Eventful for CollectionViewer<'_> { modifiers: KeyModifiers::CONTROL, .. }, - ) = ( - self.collection_store.borrow().get_selected_pane(), - key_event, - ) { + ) = (self.collection_store.borrow().get_selected_pane(), key_event) + { return Ok(Some(Command::Quit)); } @@ -474,11 +454,7 @@ impl Eventful for CollectionViewer<'_> { PaneFocus::ReqUri => match self.request_uri.handle_key_event(key_event)? { Some(RequestUriEvent::Quit) => return Ok(Some(Command::Quit)), Some(RequestUriEvent::SendRequest) => hac_core::net::handle_request( - self.collection_store - .borrow() - .get_selected_request() - .as_ref() - .unwrap(), + self.collection_store.borrow().get_selected_request().as_ref().unwrap(), self.request_tx.clone(), ), Some(RequestUriEvent::RemoveSelection) => self.update_selection(None), diff --git a/hac-client/src/pages/terminal_too_small.rs b/hac-client/src/pages/terminal_too_small.rs index 464b889..75c3275 100755 --- a/hac-client/src/pages/terminal_too_small.rs +++ b/hac-client/src/pages/terminal_too_small.rs @@ -10,17 +10,17 @@ use ratatui::Frame; /// `screen_manager` when the terminal gets smaller than a certain threshold, /// this page will display over everything and will automatically be hidden /// when the terminal gets bigger than said threshold -pub struct TerminalTooSmall<'tts> { - colors: &'tts hac_colors::Colors, +pub struct TerminalTooSmall { + colors: hac_colors::Colors, } -impl<'a> TerminalTooSmall<'a> { - pub fn new(colors: &'a hac_colors::Colors) -> Self { +impl TerminalTooSmall { + pub fn new(colors: hac_colors::Colors) -> Self { TerminalTooSmall { colors } } } -impl Renderable for TerminalTooSmall<'_> { +impl Renderable for TerminalTooSmall { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { let layout = build_layout(size); diff --git a/hac-client/src/router.rs b/hac-client/src/router.rs new file mode 100644 index 0000000..4c56799 --- /dev/null +++ b/hac-client/src/router.rs @@ -0,0 +1,109 @@ +use std::fmt::Debug; +use std::sync::mpsc::Sender; +use std::{collections::HashMap, sync::mpsc::Receiver}; + +use hac_core::command::Command; +use ratatui::{layout::Rect, Frame}; + +use crate::pages::{Eventful, Renderable}; + +pub trait EventfulRenderable: Renderable + Eventful + Debug {} +impl EventfulRenderable for K where K: Renderable + Eventful + Debug {} + +type Key = u8; + +#[derive(Debug)] +pub enum Navigate { + To(Key, Option>), + Up(Key, Option>), +} + +#[derive(Debug)] +pub struct Router { + routes: HashMap>>, + active_route: Key, + command_sender: Sender, + navigate_sender: Sender, + navigate_receiver: Receiver, + parent_navigator: Option>, +} + +impl Router { + pub fn new(command_sender: Sender) -> Self { + let (navigate_sender, navigate_receiver) = std::sync::mpsc::channel(); + Self { + routes: Default::default(), + active_route: Default::default(), + command_sender, + navigate_sender, + navigate_receiver, + parent_navigator: None, + } + } + + pub fn navigate_sender(&self) -> Sender { + self.navigate_sender.clone() + } + + pub fn add_route(&mut self, key: Key, mut route: Box>) { + route.attach_navigator(self.navigate_sender.clone()); + route.register_command_handler(self.command_sender.clone()); + self.routes.insert(key, route); + } + + pub fn handle_command(&mut self, command: Command) -> anyhow::Result<()> { + let route = self.get_active_route(); + route.handle_command(command)?; + Ok(()) + } + + fn get_active_route(&mut self) -> &mut Box> { + self.routes + .get_mut(&self.active_route) + .expect("active route doesn't exist on router") + } + + pub fn attach_parent_navigator(&mut self, navigator: Sender) { + self.parent_navigator = Some(navigator); + } +} + +impl Renderable for Router { + fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { + match self.navigate_receiver.try_recv() { + Ok(Navigate::To(route, data)) => { + self.active_route = route; + let route = self.get_active_route(); + route.update(data); + } + Ok(Navigate::Up(route, data)) if self.parent_navigator.is_some() => { + tracing::debug!(""); + self.parent_navigator + .as_mut() + .unwrap() + .send(Navigate::To(route, data)) + .expect("failed to send navigation command"); + } + _ => {} + } + + let route = self.get_active_route(); + route.draw(frame, size)?; + + Ok(()) + } + + fn resize(&mut self, new_size: Rect) { + let route = self.get_active_route(); + route.resize(new_size); + } +} + +impl Eventful for Router { + type Result = Command; + + fn handle_key_event(&mut self, key_event: crossterm::event::KeyEvent) -> anyhow::Result> { + let route = self.get_active_route(); + route.handle_key_event(key_event) + } +} diff --git a/hac-client/src/screen_manager.rs b/hac-client/src/screen_manager.rs index e60da9d..a0c8df4 100755 --- a/hac-client/src/screen_manager.rs +++ b/hac-client/src/screen_manager.rs @@ -1,20 +1,28 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::fmt::Debug; +use std::hash::Hash; +use std::rc::Rc; +use std::sync::mpsc::channel; +use std::sync::mpsc::Receiver; +use std::sync::mpsc::Sender; + use hac_core::command::Command; use hac_loader::collection_loader::CollectionMeta; +use ratatui::layout::Rect; +use ratatui::Frame; use crate::event_pool::Event; use crate::pages::collection_dashboard::CollectionDashboard; use crate::pages::collection_viewer::collection_store::CollectionStore; use crate::pages::collection_viewer::CollectionViewer; use crate::pages::terminal_too_small::TerminalTooSmall; -use crate::pages::{Eventful, Renderable}; - -use std::{cell::RefCell, rc::Rc}; - -use ratatui::{layout::Rect, Frame}; -use tokio::sync::mpsc::UnboundedSender; +use crate::pages::Eventful; +use crate::pages::Renderable; #[derive(Debug, Clone, PartialEq)] -pub enum Screens { +pub enum Routes { + CollectionList, CollectionDashboard, CollectionViewer, TerminalTooSmall, @@ -22,63 +30,31 @@ pub enum Screens { /// ScreenManager is responsible for redirecting the user to the screen it should /// be seeing at any point by the application, it is the entity behind navigation -pub struct ScreenManager<'sm> { - terminal_too_small: TerminalTooSmall<'sm>, - collection_list: CollectionDashboard<'sm>, - /// CollectionViewer is a option as we need a selected collection in order to build - /// all the components inside - collection_viewer: Option>, - - curr_screen: Screens, - /// we keep track of the previous screen, as when the terminal_too_small screen - /// is shown, we know where to redirect the user back - prev_screen: Screens, - - size: Rect, - colors: &'sm hac_colors::Colors, - config: &'sm hac_config::Config, - dry_run: bool, - - collection_store: Rc>, - - // we hold a copy of the sender so we can pass it to the editor when we first - // build one - sender: Option>, +pub struct Router { + active_route: Routes, + prev_route: Routes, + sender: Option>, } -impl<'sm> ScreenManager<'sm> { - pub fn new( - size: Rect, - colors: &'sm hac_colors::Colors, - collections: Vec, - config: &'sm hac_config::Config, - dry_run: bool, - ) -> anyhow::Result { +impl Router { + pub fn new(collection_list: Vec, size: Rect) -> anyhow::Result { Ok(Self { - curr_screen: Screens::CollectionDashboard, - prev_screen: Screens::CollectionDashboard, - collection_viewer: None, - terminal_too_small: TerminalTooSmall::new(colors), - collection_list: CollectionDashboard::new(size, colors, collections, dry_run)?, - collection_store: Rc::new(RefCell::new(CollectionStore::default())), - size, - colors, - config, + active_route: Routes::CollectionDashboard, + prev_route: Routes::CollectionDashboard, sender: None, - dry_run, }) } fn restore_screen(&mut self) { - std::mem::swap(&mut self.curr_screen, &mut self.prev_screen); + std::mem::swap(&mut self.active_route, &mut self.prev_route); } - fn switch_screen(&mut self, screen: Screens) { - if self.curr_screen == screen { + fn switch_screen(&mut self, screen: Routes) { + if self.active_route == screen { return; } - std::mem::swap(&mut self.curr_screen, &mut self.prev_screen); - self.curr_screen = screen; + std::mem::swap(&mut self.active_route, &mut self.prev_route); + self.active_route = screen; } // events can generate commands, which are sent back to the top level event loop through this @@ -88,25 +64,21 @@ impl<'sm> ScreenManager<'sm> { match command { Command::SelectCollection(collection) | Command::CreateCollection(collection) => { tracing::debug!("changing to api explorer: {}", collection.info.name); - self.switch_screen(Screens::CollectionViewer); - self.collection_store.borrow_mut().set_state(collection); - self.collection_viewer = Some(CollectionViewer::new( - self.size, - self.collection_store.clone(), - self.colors, - self.config, - self.dry_run, - )); - self.collection_viewer - .as_mut() - .unwrap() - .register_command_handler( - self.sender - .as_ref() - .expect("attempted to register the sender on collection_viewer but it was None") - .clone(), - ) - .ok(); + self.switch_screen(Routes::CollectionViewer); + // self.collection_store.borrow_mut().set_state(collection); + // self.collection_viewer = Some(CollectionViewer::new( + // self.size, + // self.collection_store.clone(), + // self.colors, + // self.config, + // self.dry_run, + // )); + // self.collection_viewer.as_mut().unwrap().register_command_handler( + // self.sender + // .as_ref() + // .expect("attempted to register the sender on collection_viewer but it was None") + // .clone(), + // ) } Command::Error(msg) => { //self.collection_list.display_error(msg); @@ -116,217 +88,68 @@ impl<'sm> ScreenManager<'sm> { } } -impl Renderable for ScreenManager<'_> { +impl Renderable for Router { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { match (size.width < 75, size.height < 22) { - (true, _) => self.switch_screen(Screens::TerminalTooSmall), - (_, true) => self.switch_screen(Screens::TerminalTooSmall), - (false, false) if self.curr_screen.eq(&Screens::TerminalTooSmall) => self.restore_screen(), + (true, _) => self.switch_screen(Routes::TerminalTooSmall), + (_, true) => self.switch_screen(Routes::TerminalTooSmall), + (false, false) if self.active_route.eq(&Routes::TerminalTooSmall) => self.restore_screen(), _ => {} } - match &self.curr_screen { - Screens::CollectionViewer => self - .collection_viewer - .as_mut() - .expect("should never be able to switch to editor screen without having a collection") - .draw(frame, frame.size())?, - Screens::CollectionDashboard => self.collection_list.draw(frame, frame.size())?, - Screens::TerminalTooSmall => self.terminal_too_small.draw(frame, frame.size())?, - }; + // match &self.active_route { + // // Routes::CollectionViewer => self + // // .collection_viewer + // // .as_mut() + // // .expect("should never be able to switch to editor screen without having a collection") + // // .draw(frame, frame.size())?, + // Routes::CollectionDashboard => (), // self.collection_list.draw(frame, frame.size())?), + // // Routes::TerminalTooSmall => self.terminal_too_small.draw(frame, frame.size())?, + // }; Ok(()) } - fn register_command_handler(&mut self, sender: UnboundedSender) -> anyhow::Result<()> { - self.sender = Some(sender.clone()); - self.collection_list.register_command_handler(sender.clone())?; - Ok(()) + fn register_command_handler(&mut self, sender: Sender) { + // self.sender = Some(sender.clone()); + // self.collection_list.register_command_handler(sender.clone())?; } fn resize(&mut self, new_size: Rect) { - self.size = new_size; - self.collection_list.resize(new_size); + // self.collection_list.resize(new_size); - if let Some(e) = self.collection_viewer.as_mut() { - e.resize(new_size) - } + // if let Some(e) = self.collection_viewer.as_mut() { + // e.resize(new_size) + // } } - fn handle_tick(&mut self) -> anyhow::Result<()> { + fn tick(&mut self) -> anyhow::Result<()> { // currently, only the editor cares about the ticks, used to determine // when to sync changes in disk - if let Screens::CollectionViewer = &self.curr_screen { - self.collection_viewer - .as_mut() - .expect("we are displaying the editor without having one") - .handle_tick()? - }; + // if let Routes::CollectionViewer = &self.active_route { + // self.collection_viewer + // .as_mut() + // .expect("we are displaying the editor without having one") + // .handle_tick()? + // }; Ok(()) } } -impl Eventful for ScreenManager<'_> { +impl Eventful for Router { type Result = Command; fn handle_event(&mut self, event: Option) -> anyhow::Result> { - match self.curr_screen { - Screens::CollectionViewer => self - .collection_viewer - .as_mut() - .expect("should never be able to switch to editor screen without having a collection") - .handle_event(event), - Screens::CollectionDashboard => self.collection_list.handle_event(event), - Screens::TerminalTooSmall => Ok(None), - } + // match self.active_route { + // Routes::CollectionViewer => self + // .collection_viewer + // .as_mut() + // .expect("should never be able to switch to editor screen without having a collection") + // .handle_event(event), + // Routes::CollectionDashboard => Ok(None), //self.collection_list.handle_event(event), + // Routes::TerminalTooSmall => Ok(None), + // } + Ok(None) } } - -//#[cfg(test)] -//mod tests { -// use super::*; -// use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -// use hac_core::collection::{self, types::*}; -// use ratatui::{backend::TestBackend, Terminal}; -// use std::{ -// fs::{create_dir, File}, -// io::Write, -// }; -// use tempfile::{tempdir, TempDir}; -// -// fn setup_temp_collections(amount: usize) -> (TempDir, String) { -// let tmp_data_dir = tempdir().expect("Failed to create temp data dir"); -// -// let tmp_dir = tmp_data_dir.path().join("collections"); -// create_dir(&tmp_dir).expect("Failed to create collections directory"); -// -// for i in 0..amount { -// let file_path = tmp_dir.join(format!("test_collection_{}.json", i)); -// let mut tmp_file = File::create(&file_path).expect("Failed to create file"); -// -// write!( -// tmp_file, -// r#"{{"info": {{ "name": "test_collection_{}", "description": "test_description_{}" }}}}"#, -// i, i -// ).expect("Failed to write to file"); -// -// tmp_file.flush().expect("Failed to flush file"); -// } -// -// (tmp_data_dir, tmp_dir.to_string_lossy().to_string()) -// } -// -// #[test] -// fn test_show_terminal_too_small_screen() { -// let small_in_width = Rect::new(0, 0, 79, 22); -// let small_in_height = Rect::new(0, 0, 100, 19); -// let colors = hac_colors::Colors::default(); -// let (_guard, path) = setup_temp_collections(10); -// let collections = collection::get_collections(path).unwrap(); -// let config = hac_config::load_config(); -// let mut sm = -// ScreenManager::new(small_in_width, &colors, collections, &config, false).unwrap(); -// let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); -// -// sm.draw(&mut terminal.get_frame(), small_in_width).unwrap(); -// assert_eq!(sm.curr_screen, Screens::TerminalTooSmall); -// -// sm.draw(&mut terminal.get_frame(), small_in_height).unwrap(); -// assert_eq!(sm.curr_screen, Screens::TerminalTooSmall); -// } -// -// #[test] -// fn test_restore_screeen() { -// let small = Rect::new(0, 0, 79, 22); -// let enough = Rect::new(0, 0, 80, 22); -// let colors = hac_colors::Colors::default(); -// let (_guard, path) = setup_temp_collections(10); -// let collections = collection::get_collections(path).unwrap(); -// let config = hac_config::load_config(); -// let mut sm = ScreenManager::new(small, &colors, collections, &config, false).unwrap(); -// let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); -// -// terminal.resize(small).unwrap(); -// sm.draw(&mut terminal.get_frame(), small).unwrap(); -// assert_eq!(sm.curr_screen, Screens::TerminalTooSmall); -// assert_eq!(sm.prev_screen, Screens::CollectionDashboard); -// -// terminal.resize(enough).unwrap(); -// sm.draw(&mut terminal.get_frame(), enough).unwrap(); -// assert_eq!(sm.curr_screen, Screens::CollectionDashboard); -// assert_eq!(sm.prev_screen, Screens::TerminalTooSmall); -// } -// -// #[test] -// fn test_resizing() { -// let initial = Rect::new(0, 0, 80, 22); -// let expected = Rect::new(0, 0, 100, 22); -// let colors = hac_colors::Colors::default(); -// let (_guard, path) = setup_temp_collections(10); -// let collection = collection::get_collections(path).unwrap(); -// let config = hac_config::load_config(); -// let mut sm = ScreenManager::new(initial, &colors, collection, &config, false).unwrap(); -// -// sm.resize(expected); -// -// assert_eq!(sm.size, expected); -// } -// -// #[test] -// fn test_switch_to_explorer_on_select() { -// let initial = Rect::new(0, 0, 80, 22); -// let colors = hac_colors::Colors::default(); -// let collection = Collection { -// info: Info { -// name: String::from("any_name"), -// description: None, -// }, -// path: "any_path".into(), -// requests: None, -// }; -// let command = Command::SelectCollection(collection.clone()); -// let (_guard, path) = setup_temp_collections(10); -// let collection = collection::get_collections(path).unwrap(); -// let config = hac_config::load_config(); -// let (tx, _) = tokio::sync::mpsc::unbounded_channel::(); -// let mut sm = ScreenManager::new(initial, &colors, collection, &config, false).unwrap(); -// _ = sm.register_command_handler(tx.clone()); -// assert_eq!(sm.curr_screen, Screens::CollectionDashboard); -// -// sm.handle_command(command); -// assert_eq!(sm.curr_screen, Screens::CollectionViewer); -// } -// -// #[test] -// fn test_register_command_sender_for_dashboard() { -// let initial = Rect::new(0, 0, 80, 22); -// let colors = hac_colors::Colors::default(); -// let (_guard, path) = setup_temp_collections(10); -// let collections = collection::get_collections(path).unwrap(); -// let config = hac_config::load_config(); -// let mut sm = ScreenManager::new(initial, &colors, collections, &config, false).unwrap(); -// -// let (tx, _) = tokio::sync::mpsc::unbounded_channel::(); -// -// sm.register_command_handler(tx.clone()).unwrap(); -// -// assert!(sm.collection_list.command_sender.is_some()); -// } -// -// #[test] -// fn test_quit_event() { -// let initial = Rect::new(0, 0, 80, 22); -// let colors = hac_colors::Colors::default(); -// let (_guard, path) = setup_temp_collections(10); -// let collections = collection::get_collections(path).unwrap(); -// let config = hac_config::load_config(); -// let mut sm = ScreenManager::new(initial, &colors, collections, &config, false).unwrap(); -// -// let event = Event::Key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)); -// -// let command = sm.handle_event(Some(event)).unwrap(); -// -// assert!(command.is_some()); -// } -//} diff --git a/.rustfmt.toml b/rustfmt.toml similarity index 89% rename from .rustfmt.toml rename to rustfmt.toml index ba69af2..e4668df 100644 --- a/.rustfmt.toml +++ b/rustfmt.toml @@ -1,11 +1,10 @@ edition = "2021" unstable_features = true use_field_init_shorthand = true -imports_granularity = "Module" +imports_granularity = "Item" group_imports = "StdExternalCrate" format_code_in_doc_comments = true reorder_impl_items = true single_line_let_else_max_width = 100 single_line_if_else_max_width = 100 max_width = 120 - From 25b4dbf149bf3cfb3e987db058bdabe4337e78e7 Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Sat, 21 Sep 2024 19:32:10 -0300 Subject: [PATCH 04/23] wip: new collection creation form --- .gitignore | 1 + .vscode/launch.json | 197 -------------- hac-client/src/app.rs | 14 +- hac-client/src/ascii.rs | 182 +++---------- hac-client/src/components/input.rs | 108 ++++++++ hac-client/src/components/list_itemm.rs | 96 +++++++ hac-client/src/components/mod.rs | 2 + hac-client/src/lib.rs | 15 +- .../collection_dashboard.rs | 25 +- .../pages/collection_list/collection_list.rs | 84 ++---- .../collection_list/create_collection.rs | 121 ++++++++- hac-client/src/pages/collection_list/mod.rs | 7 +- .../auth_editor/auth_kind_prompt.rs | 33 +-- .../headers_editor/headers_editor.rs | 95 ++----- .../headers_editor_delete_prompt.rs | 26 +- .../headers_editor_edit_form.rs | 50 ++-- .../collection_viewer/response_viewer.rs | 251 ++++-------------- .../sidebar/create_directory_form.rs | 20 +- .../sidebar/create_request_form.rs | 46 +--- .../sidebar/delete_item_prompt.rs | 41 +-- .../sidebar/directory_form.rs | 42 +-- .../sidebar/edit_directory_form.rs | 24 +- .../sidebar/edit_request_form.rs | 50 +--- .../collection_viewer/sidebar/request_form.rs | 50 ++-- .../sidebar/select_request_parent.rs | 41 +-- hac-client/src/pages/overlay.rs | 18 +- hac-client/src/pages/terminal_too_small.rs | 16 +- hac-client/src/router.rs | 137 ++++++++-- hac-config/src/config.rs | 6 +- rustfmt.toml | 2 +- 30 files changed, 776 insertions(+), 1024 deletions(-) delete mode 100644 .vscode/launch.json create mode 100644 hac-client/src/components/input.rs create mode 100644 hac-client/src/components/list_itemm.rs diff --git a/.gitignore b/.gitignore index 92162c1..27ce7e1 100755 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ hac.log tarpaulin-report.html mutants.out* +.vscode diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 5714ef2..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,197 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in library 'hac_config'", - "cargo": { - "args": [ - "test", - "--no-run", - "--lib", - "--package=hac-config" - ], - "filter": { - "name": "hac_config", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in library 'hac_core'", - "cargo": { - "args": [ - "test", - "--no-run", - "--lib", - "--package=hac-core" - ], - "filter": { - "name": "hac_core", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in library 'hac_client'", - "cargo": { - "args": [ - "test", - "--no-run", - "--lib", - "--package=hac-client" - ], - "filter": { - "name": "hac_client", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug executable 'hac'", - "cargo": { - "args": [ - "build", - "--bin=hac", - "--package=hac-client" - ], - "filter": { - "name": "hac", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in executable 'hac'", - "cargo": { - "args": [ - "test", - "--no-run", - "--bin=hac", - "--package=hac-client" - ], - "filter": { - "name": "hac", - "kind": "bin" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug benchmark 'collection_viewer_bench'", - "cargo": { - "args": [ - "test", - "--no-run", - "--bench=collection_viewer_bench", - "--package=hac-client" - ], - "filter": { - "name": "collection_viewer_bench", - "kind": "bench" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in library 'hac_cli'", - "cargo": { - "args": [ - "test", - "--no-run", - "--lib", - "--package=hac-cli" - ], - "filter": { - "name": "hac_cli", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in library 'hac_colors'", - "cargo": { - "args": [ - "test", - "--no-run", - "--lib", - "--package=hac-colors" - ], - "filter": { - "name": "hac_colors", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in library 'hac_loader'", - "cargo": { - "args": [ - "test", - "--no-run", - "--lib", - "--package=hac-loader" - ], - "filter": { - "name": "hac_loader", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - }, - { - "type": "lldb", - "request": "launch", - "name": "Debug unit tests in library 'hac_store'", - "cargo": { - "args": [ - "test", - "--no-run", - "--lib", - "--package=hac-store" - ], - "filter": { - "name": "hac_store", - "kind": "lib" - } - }, - "args": [], - "cwd": "${workspaceFolder}" - } - ] -} \ No newline at end of file diff --git a/hac-client/src/app.rs b/hac-client/src/app.rs index fcad060..cd36127 100755 --- a/hac-client/src/app.rs +++ b/hac-client/src/app.rs @@ -1,7 +1,6 @@ use std::io::Stdout; use std::rc::Rc; -use std::sync::mpsc::Receiver; -use std::sync::mpsc::Sender; +use std::sync::mpsc::{Receiver, Sender}; use std::sync::RwLock; use hac_colors::Colors; @@ -11,14 +10,11 @@ use hac_loader::collection_loader::CollectionMeta; use ratatui::backend::CrosstermBackend; use ratatui::Terminal; -use crate::event_pool::Event; -use crate::event_pool::EventPool; +use crate::event_pool::{Event, EventPool}; use crate::pages::collection_list::make_collection_list_router; -use crate::pages::Eventful; -use crate::pages::Renderable; +use crate::pages::{Eventful, Renderable}; use crate::router::Router; -use crate::HacColors; -use crate::HacConfig; +use crate::{HacColors, HacConfig}; #[derive(Default, Hash, Debug, PartialEq, Eq, Copy, Clone)] pub enum AppRoutes { @@ -54,7 +50,7 @@ impl App { let (command_sender, command_receiver) = std::sync::mpsc::channel(); let size = terminal.size()?; - let mut router = Router::new(command_sender.clone()); + let mut router = Router::new(command_sender.clone(), colors.clone()); let mut collection_list_router = make_collection_list_router( command_sender.clone(), diff --git a/hac-client/src/ascii.rs b/hac-client/src/ascii.rs index 20aaaf2..7cefe02 100755 --- a/hac-client/src/ascii.rs +++ b/hac-client/src/ascii.rs @@ -1,156 +1,40 @@ -pub static BIG_ERROR_ARTS: &[&[&str]] = &[ - &[ - r#" ,adPPYba, 8b,dPPYba, 8b,dPPYba, ,adPPYba, 8b,dPPYba, "#, - r#"a8P_____88 88P' "Y8 88P' "Y8 a8" "8a 88P' "Y8 "#, - r#"8PP""""""" 88 88 8b d8 88 "#, - r#""8b, ,aa 88 88 "8a, ,a8" 88 "#, - r#" `"Ybbd8"' 88 88 `"YbbdP"' 88 "#, - ], - &[ - r#".d88b. 888d888888d888 .d88b. 888d888 "#, - r#"d8P Y8b888P" 888P" d88""88b888P" "#, - r#"88888888888 888 888 888888 "#, - r#"Y8b. 888 888 Y88..88P888 "#, - r#" "Y8888 888 888 "Y88P" 888 "#, - ], - &[ - r#" __ ,-. __ ,-. ,---. __ ,-. "#, - r#" ,' ,'/ /|,' ,'/ /| ' ,'\ ,' ,'/ /| "#, - r#" ,---. ' | |' |' | |' | / / |' | |' | "#, - r#" / \ | | ,'| | ,'. ; ,. :| | ,' "#, - r#" / / |' : / ' : / ' | |: :' : / "#, - r#". ' / || | ' | | ' ' | .; :| | ' "#, - r#"' ; /|; : | ; : | | : |; : | "#, - r#"' | / || , ; | , ; \ \ / | , ; "#, - r#"| : | ---' ---' `----' ---' "#, - r#" \ \ / "#, - r#" `----' "#, - ], - &[ - r#" /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$ "#, - r#" /$$__ $$ /$$__ $$ /$$__ $$ /$$__ $$ /$$__ $$"#, - r#"| $$$$$$$$| $$ \__/| $$ \__/| $$ \ $$| $$ \__/"#, - r#"| $$_____/| $$ | $$ | $$ | $$| $$ "#, - r#"| $$$$$$$| $$ | $$ | $$$$$$/| $$ "#, - r#" \_______/|__/ |__/ \______/ |__/ "#, - ], - &[ - r#" .u . .u . u. .u . "#, - r#" .u .d88B :@8c .d88B :@8c ...ue888b .d88B :@8c "#, - r#" ud8888. ="8888f8888r ="8888f8888r 888R Y888r ="8888f8888r "#, - r#":888'8888. 4888>'88" 4888>'88" 888R I888> 4888>'88" "#, - r#"d888 '88%" 4888> ' 4888> ' 888R I888> 4888> ' "#, - r#"8888.+" 4888> 4888> 888R I888> 4888> "#, - r#"8888L .d888L .+ .d888L .+ u8888cJ888 .d888L .+ "#, - r#"'8888c. .+ ^"8888*" ^"8888*" "*888*P" ^"8888*" "#, - r#" "88888% "Y" "Y" 'Y" "Y" "#, - r#" "YP' "#, - ], +pub static BIG_ERROR_ART: &[&str] = &[ + r#" __ ,-. __ ,-. ,---. __ ,-. "#, + r#" ,' ,'/ /|,' ,'/ /| ' ,'\ ,' ,'/ /| "#, + r#" ,---. ' | |' |' | |' | / / |' | |' | "#, + r#" / \ | | ,'| | ,'. ; ,. :| | ,' "#, + r#" / / |' : / ' : / ' | |: :' : / "#, + r#". ' / || | ' | | ' ' | .; :| | ' "#, + r#"' ; /|; : | ; : | | : |; : | "#, + r#"' | / || , ; | , ; \ \ / | , ; "#, + r#"| : | ---' ---' `----' ---' "#, + r#" \ \ / "#, + r#" `----' "#, ]; -pub static SMALL_ERROR_ARTS: &[&[&str]] = &[ - &[ - r#" ___ _ __ _ __ ___ _ __ "#, - r#" / _ \ '__| '__/ _ \| '__|"#, - r#" | __/ | | | | (_) | | "#, - r#" \___|_| |_| \___/|_| "#, - ], - &[ - r#" __ _ __ _ __ ___ _ __ "#, - r#" /'__`\/\`'__\/\`'__\/ __`\/\`'__\"#, - r#"/\ __/\ \ \/ \ \ \//\ \L\ \ \ \/ "#, - r#"\ \____\\ \_\ \ \_\\ \____/\ \_\ "#, - r#" \/____/ \/_/ \/_/ \/___/ \/_/ "#, - ], - &[ - r#" dBBBP dBBBBBb dBBBBBb dBBBBP dBBBBBb"#, - r#" dBP dBP dBP.BP dBP"#, - r#" dBBP dBBBBK dBBBBK dBP.BP dBBBBK "#, - r#" dBP dBP BB dBP BB dBP.BP dBP BB "#, - r#" dBBBBP dBP dB' dBP dB' dBBBBP dBP dB' "#, - ], - &[ - r#".d88b 8d8b 8d8b .d8b. 8d8b "#, - r#"8.dP' 8P 8P 8' .8 8P "#, - r#"`Y88P 8 8 `Y8P' 8 "#, - ], - &[ - r#" ____ ____ ____ ___ ____ "#, - r#" / _ )/ ___) ___) _ \ / ___)"#, - r#"( (/ /| | | | | |_| | | "#, - r#" \____)_| |_| \___/|_| "#, - ], +pub static SMALL_ERROR_ART: &[&str] = &[ + r#" __ _ __ _ __ ___ _ __ "#, + r#" /'__`\/\`'__\/\`'__\/ __`\/\`'__\"#, + r#"/\ __/\ \ \/ \ \ \//\ \L\ \ \ \/ "#, + r#"\ \____\\ \_\ \ \_\\ \____/\ \_\ "#, + r#" \/____/ \/_/ \/_/ \/___/ \/_/ "#, ]; -pub static LOGO_ASCII: &[&[&str]] = &[ - &[ - r#" ,--, "#, - r#" ,--.'| ,---, ,----.. "#, - r#" ,--, | : ' .' \ / / \ "#, - r#",---.'| : ' / ; '. | : : "#, - r#"| | : _' |: : \ . | ;. / "#, - r#": : |.' |: | /\ \ . ; /--` "#, - r#"| ' ' ; :| : ' ;. : ; | ; "#, - r#"' | .'. || | ;/ \ \| : | "#, - r#"| | : | '' : | \ \ ,'. | '___ "#, - r#"' : | : ;| | ' '--' ' ; : .'| "#, - r#"| | ' ,/ | : : ' | '/ : "#, - r#"; : ;--' | | ,' | : / "#, - r#"| ,/ `--'' \ \ .' "#, - r#"'---' `---` "#, - ], - &[ - r#" ____ ____ ____ _____ "#, - r#"| | | | ____|\ \ ___|\ \ "#, - r#"| | | | / /\ \ / /\ \ "#, - r#"| |_| || | | || | | |"#, - r#"| .-. || |__| || | |____|"#, - r#"| | | || .--. || | ____ "#, - r#"| | | || | | || | | |"#, - r#"|____| |____||____| |____||\ ___\/ /|"#, - r#"| | | || | | || | /____/ |"#, - r#"|____| |____||____| |____| \|___| | /"#, - r#" \( )/ \( )/ \( |____|/ "#, - r#" ' ' ' ' ' )/ "#, - r#" ' "#, - ], - &[ - r#" .uef^" "#, - r#":d88E "#, - r#"`888E u . "#, - r#" 888E .z8k us888u. .udR88N "#, - r#" 888E~?888L .@88 "8888" <888'888k "#, - r#" 888E 888E 9888 9888 9888 'Y" "#, - r#" 888E 888E 9888 9888 9888 "#, - r#" 888E 888E 9888 9888 9888 "#, - r#" 888E 888E 9888 9888 ?8888u../ "#, - r#"m888N= 888> "888*""888" "8888P' "#, - r#" `Y" 888 ^Y" ^Y' "P' "#, - r#" J88" "#, - r#" @% "#, - r#" :" "#, - ], - &[ - r#" _ "#, - r#"| |__ __ _ ___ "#, - r#"| '_ \ / _` |/ __|"#, - r#"| | | | (_| | (__ "#, - r#"|_| |_|\__,_|\___|"#, - ], - &[ - r#" __ "#, - r#" / /_ ____ ______"#, - r#" / __ \/ __ `/ ___/"#, - r#" / / / / /_/ / /__ "#, - r#"/_/ /_/\__,_/\___/ "#, - ], - &[ - r#" __ __ ______ ______ "#, - r#"/\ \_\ \ /\ __ \ /\ ___\ "#, - r#"\ \ __ \ \ \ __ \ \ \ \____ "#, - r#" \ \_\ \_\ \ \_\ \_\ \ \_____\ "#, - r#" \/_/\/_/ \/_/\/_/ \/_____/ "#, - ], +pub static LOGO_ASCII: &[&str] = &[ + r#" ,--, "#, + r#" ,--.'| ,---, ,----.. "#, + r#" ,--, | : ' .' \ / / \ "#, + r#",---.'| : ' / ; '. | : : "#, + r#"| | : _' |: : \ . | ;. / "#, + r#": : |.' |: | /\ \ . ; /--` "#, + r#"| ' ' ; :| : ' ;. : ; | ; "#, + r#"' | .'. || | ;/ \ \| : | "#, + r#"| | : | '' : | \ \ ,'. | '___ "#, + r#"' : | : ;| | ' '--' ' ; : .'| "#, + r#"| | ' ,/ | : : ' | '/ : "#, + r#"; : ;--' | | ,' | : / "#, + r#"| ,/ `--'' \ \ .' "#, + r#"'---' `---` "#, ]; pub static UNDER_CONSTRUCTION: &[&str] = &[ diff --git a/hac-client/src/components/input.rs b/hac-client/src/components/input.rs new file mode 100644 index 0000000..094ffa1 --- /dev/null +++ b/hac-client/src/components/input.rs @@ -0,0 +1,108 @@ +use std::borrow::Cow; + +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::style::Style; +use ratatui::text::Line; +use ratatui::widgets::{Block, Borders, Paragraph, Widget}; + +use crate::HacColors; + +#[derive(Debug, Default)] +pub struct Input<'input> { + label: Option>, + label_style: Style, + value: Option>, + value_style: Style, + border_style: Style, + colors: HacColors, +} + +impl<'input> Input<'input> { + pub fn new(value: Option, label: Option, colors: HacColors) -> Self + where + T: Into>, + { + Self { + value: value.map(Into::into), + value_style: Style::default(), + label: label.map(Into::into), + label_style: Style::default(), + border_style: Style::default().fg(colors.normal.white).bg(colors.normal.black), + colors, + } + } + + pub fn label_style(self, style: Style) -> Self { + Self { + value: self.value, + value_style: self.value_style, + label: self.label, + label_style: style, + border_style: self.border_style, + colors: self.colors, + } + } + + pub fn value_style(self, style: Style) -> Self { + Self { + value: self.value, + value_style: style, + label: self.label, + label_style: self.label_style, + border_style: self.border_style, + colors: self.colors, + } + } + + pub fn border_style(self, style: Style) -> Self { + Self { + value: self.value, + value_style: self.value_style, + label: self.label, + label_style: self.label_style, + colors: self.colors, + border_style: style, + } + } + + fn has_label(&self) -> bool { + self.label.as_ref().is_some_and(|label| !label.is_empty()) + } +} + +impl Widget for Input<'_> { + fn render(self, mut area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let min_height = if self.has_label() { 4 } else { 3 }; + if area.height < min_height { + panic!("input needs at least a height of 4, but got {area:?}"); + } + + if self.has_label() { + let size = Rect::new(area.x, area.y, area.width, 1); + let label = self.label.as_ref().unwrap().to_string(); + let fill = size.width as usize - label.len(); + let label = format!("{}{}", label, " ".repeat(fill)); + let label = Line::from(label).style(self.label_style); + label.render(size, buf); + area.y += 1; + area.height -= 1; + } + + let block = Block::default() + .borders(Borders::ALL) + .border_style(self.border_style) + .style(self.label_style); + + let value = self.value.as_ref().map(|v| v.to_string()).unwrap_or(String::default()); + let size = Rect::new(area.x, area.y, area.width, 3); + let fill = size.width as usize - value.len(); + let value = format!("{}{}", value.to_string(), " ".repeat(fill)); + let value = Line::from(value).style(self.value_style); + Paragraph::new(value).block(block).render(size, buf); + return; + } +} diff --git a/hac-client/src/components/list_itemm.rs b/hac-client/src/components/list_itemm.rs new file mode 100644 index 0000000..30782ac --- /dev/null +++ b/hac-client/src/components/list_itemm.rs @@ -0,0 +1,96 @@ +use std::borrow::Cow; + +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::style::Style; +use ratatui::text::Line; +use ratatui::widgets::{Block, Borders, Padding, Paragraph, Widget}; + +use crate::HacColors; + +#[derive(Debug, Clone)] +pub struct ListItem<'item> { + title: Cow<'item, str>, + description: Option>, + title_style: Style, + desc_style: Style, + selected: bool, + colors: HacColors, +} + +impl<'item> ListItem<'item> { + pub fn new(title: T, description: Option, colors: HacColors) -> Self + where + T: Into>, + { + Self { + title: title.into(), + description: description.map(Into::into), + title_style: Style::default(), + desc_style: Style::default(), + selected: false, + colors, + } + } + + pub fn title_style(self, style: Style) -> Self { + Self { + title: self.title, + description: self.description, + title_style: style, + desc_style: self.desc_style, + selected: self.selected, + colors: self.colors, + } + } + + pub fn desc_style(self, style: Style) -> Self { + Self { + title: self.title, + description: self.description, + title_style: self.title_style, + desc_style: style, + selected: self.selected, + colors: self.colors, + } + } + + pub fn select(self) -> Self { + Self { + title: self.title, + description: self.description, + title_style: self.title_style, + desc_style: self.desc_style, + selected: true, + colors: self.colors, + } + } +} + +impl Widget for ListItem<'_> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let mut parts = Vec::with_capacity(2); + parts.push(Line::from(self.title.to_string()).style(self.title_style)); + + if let Some(description) = self.description { + parts.push(Line::from(description.to_string()).style(self.desc_style)); + } + + let mut block = Block::default().padding(Padding::left(1)); + if self.selected { + block = block + .borders(Borders::LEFT) + .border_style( + Style::default() + .bg(self.colors.primary.background) + .fg(self.colors.normal.white), + ) + .style(self.title_style); + } + + Paragraph::new(parts).block(block).render(area, buf); + } +} diff --git a/hac-client/src/components/mod.rs b/hac-client/src/components/mod.rs index 9b94ab1..3174a48 100644 --- a/hac-client/src/components/mod.rs +++ b/hac-client/src/components/mod.rs @@ -1,3 +1,5 @@ pub mod component_styles; +pub mod input; pub mod key_value_pair; pub mod list_item; +pub mod list_itemm; diff --git a/hac-client/src/lib.rs b/hac-client/src/lib.rs index e69cdf6..e26ad95 100755 --- a/hac-client/src/lib.rs +++ b/hac-client/src/lib.rs @@ -1,9 +1,3 @@ -use std::rc::Rc; -use std::sync::RwLock; - -use hac_colors::Colors; -use hac_config::Config; - pub mod app; mod ascii; mod components; @@ -13,5 +7,14 @@ mod router; pub mod screen_manager; pub mod utils; +use std::rc::Rc; +use std::sync::RwLock; + +use hac_colors::Colors; +use hac_config::Config; + +pub static MIN_WIDTH: u16 = 80; +pub static MIN_HEIGHT: u16 = 30; + pub type HacConfig = Rc>; pub type HacColors = Rc; diff --git a/hac-client/src/pages/collection_dashboard/collection_dashboard.rs b/hac-client/src/pages/collection_dashboard/collection_dashboard.rs index ddc5e8d..141693b 100755 --- a/hac-client/src/pages/collection_dashboard/collection_dashboard.rs +++ b/hac-client/src/pages/collection_dashboard/collection_dashboard.rs @@ -1,27 +1,26 @@ -use hac_core::command::Command; -use hac_loader::collection_loader::{CollectionMeta, ReadableByteSize}; -use hac_store::collection::Collection; - -use crate::pages::collection_dashboard::collection_list::{CollectionList, CollectionListState}; -use crate::pages::collection_dashboard::new_collection_form::{FormFocus, FormState, NewCollectionForm}; -use crate::pages::confirm_popup::ConfirmPopup; -use crate::pages::error_popup::ErrorPopup; -use crate::pages::overlay::{draw_overlay, make_overlay}; -use crate::pages::{Eventful, Renderable}; -use crate::router::{EventfulRenderable, Navigate}; - use std::fmt::Debug; use std::hash::Hash; use std::ops::{Add, Div, Not, Sub}; use std::sync::mpsc::Sender; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; +use hac_core::command::Command; +use hac_loader::collection_loader::{CollectionMeta, ReadableByteSize}; +use hac_store::collection::Collection; use ratatui::layout::{Alignment, Constraint, Direction, Flex, Layout, Rect}; use ratatui::style::{Style, Stylize}; use ratatui::text::Line; use ratatui::widgets::{Block, BorderType, Borders, Clear, Padding, Paragraph, StatefulWidget, Widget, Wrap}; use ratatui::Frame; +use crate::pages::collection_dashboard::collection_list::{CollectionList, CollectionListState}; +use crate::pages::collection_dashboard::new_collection_form::{FormFocus, FormState, NewCollectionForm}; +use crate::pages::confirm_popup::ConfirmPopup; +use crate::pages::error_popup::ErrorPopup; +use crate::pages::overlay::{draw_overlay_old, make_overlay_old}; +use crate::pages::{Eventful, Renderable}; +use crate::router::{EventfulRenderable, Navigate}; + #[derive(Debug, PartialEq)] struct DashboardLayout { collections_pane: Rect, diff --git a/hac-client/src/pages/collection_list/collection_list.rs b/hac-client/src/pages/collection_list/collection_list.rs index 85fefff..736fb80 100644 --- a/hac-client/src/pages/collection_list/collection_list.rs +++ b/hac-client/src/pages/collection_list/collection_list.rs @@ -1,42 +1,22 @@ use std::fmt::Debug; -use std::sync::mpsc::channel; -use std::sync::mpsc::Sender; +use std::sync::mpsc::{channel, Sender}; -use crossterm::event::KeyCode; -use crossterm::event::KeyEvent; -use crossterm::event::KeyModifiers; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::command::Command; -use hac_loader::collection_loader::CollectionMeta; -use hac_loader::collection_loader::ReadableByteSize; -use ratatui::layout::Alignment; -use ratatui::layout::Constraint; -use ratatui::layout::Direction; -use ratatui::layout::Flex; -use ratatui::layout::Layout; -use ratatui::layout::Rect; -use ratatui::style::Style; -use ratatui::style::Stylize; +use hac_loader::collection_loader::{CollectionMeta, ReadableByteSize}; +use ratatui::layout::{Alignment, Constraint, Direction, Flex, Layout, Rect}; +use ratatui::style::{Style, Stylize}; use ratatui::text::Line; -use ratatui::widgets::Block; -use ratatui::widgets::BorderType; -use ratatui::widgets::Borders; -use ratatui::widgets::Clear; -use ratatui::widgets::Padding; -use ratatui::widgets::Paragraph; -use ratatui::widgets::StatefulWidget; -use ratatui::widgets::Widget; -use ratatui::widgets::Wrap; +use ratatui::widgets::{Block, BorderType, Borders, Clear, Padding, Paragraph, StatefulWidget, Widget, Wrap}; use ratatui::Frame; use crate::app::AppRoutes; +use crate::components::list_itemm::ListItem; use crate::pages::collection_list::Routes; -use crate::pages::overlay::draw_overlay; -use crate::pages::overlay::make_overlay; -use crate::pages::Eventful; -use crate::pages::Renderable; +use crate::pages::overlay::{draw_overlay_old, make_overlay_old}; +use crate::pages::{Eventful, Renderable}; use crate::router::Navigate; -use crate::HacColors; -use crate::HacConfig; +use crate::{HacColors, HacConfig}; #[derive(Debug, PartialEq)] struct DashboardLayout { @@ -152,35 +132,23 @@ impl CollectionList { .enumerate() { let selected = i == self.selected; - - let item = match selected { - true => Paragraph::new(vec![ - Line::from(item.name().to_string().fg(self.colors.bright.red)), - Line::from(vec![ - item.modified().to_string().fg(self.colors.bright.black), - " - ".fg(self.colors.bright.black), - item.size().readable_byte_size().fg(self.colors.bright.black), - ]), - ]) - .block( - Block::new() - .borders(Borders::LEFT) - .fg(self.colors.bright.red) - .padding(Padding::left(1)), - ), - false => Paragraph::new(vec![ - Line::from(item.name().to_string().fg(self.colors.normal.white)), - Line::from(vec![ - item.modified().to_string().fg(self.colors.bright.black), - " - ".fg(self.colors.bright.black), - item.size().readable_byte_size().fg(self.colors.bright.black), - ]), - ]) - .block(Block::new().padding(Padding::left(2))), + let modified = item.modified(); + let size = item.size().readable_byte_size(); + let description = format!("{modified} - {size}"); + + let item = if selected { + ListItem::new(item.name(), Some(&description), self.colors.clone()) + .title_style(Style::new().fg(self.colors.normal.red)) + .desc_style(Style::new().fg(self.colors.bright.black)) + .select() + } else { + ListItem::new(item.name(), Some(&description), self.colors.clone()) + .title_style(Style::new().fg(self.colors.normal.white)) + .desc_style(Style::new().fg(self.colors.bright.black)) }; - let size = Rect::new(layout.x, layout.y + (*idx as u16 * 3), layout.width, 2); frame.render_widget(item, size); + *idx += 1; } @@ -189,7 +157,7 @@ impl CollectionList { fn draw_hint_text(&self, frame: &mut Frame) { let hint = vec![ - "j/k} ↑/↓".fg(self.colors.normal.green), + "j/k ↑/↓".fg(self.colors.normal.green), " - choose • ".fg(self.colors.bright.black), "n".fg(self.colors.normal.green), " - new • ".fg(self.colors.bright.black), @@ -286,7 +254,7 @@ impl Eventful for CollectionList { assert!(self.collections.len() > self.selected); let path = self.collections[self.selected].path().clone(); self.navigator - .send(Navigate::Up( + .send(Navigate::Leave( AppRoutes::CollectionViewerRouter.into(), Some(Box::new(path)), )) diff --git a/hac-client/src/pages/collection_list/create_collection.rs b/hac-client/src/pages/collection_list/create_collection.rs index ded6911..9bf0d59 100644 --- a/hac-client/src/pages/collection_list/create_collection.rs +++ b/hac-client/src/pages/collection_list/create_collection.rs @@ -1,25 +1,44 @@ -use std::sync::mpsc::channel; -use std::sync::mpsc::Sender; +use std::sync::mpsc::{channel, Sender}; -use crossterm::event::KeyCode; -use crossterm::event::KeyEvent; -use crossterm::event::KeyModifiers; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::command::Command; -use ratatui::layout::Rect; +use ratatui::layout::{Constraint, Direction, Flex, Layout, Rect}; +use ratatui::style::{Style, Stylize}; +use ratatui::text::Line; use ratatui::widgets::Paragraph; use ratatui::Frame; -use crate::pages::Eventful; -use crate::pages::Renderable; +use crate::ascii::LOGO_ASCII; +use crate::components::input::Input; +use crate::pages::overlay::make_overlay; +use crate::pages::{Eventful, Renderable}; use crate::router::Navigate; -use crate::HacColors; -use crate::HacConfig; +use crate::{HacColors, HacConfig, MIN_HEIGHT, MIN_WIDTH}; + +#[derive(Debug)] +struct CreateCollectionLayout { + name_input: Rect, + desc_input: Rect, + hint: Rect, + logo: Rect, +} + +#[derive(Debug, Default)] +enum Focus { + #[default] + Name, + Desc, +} #[derive(Debug)] pub struct CreateCollection { + name: String, + desc: String, size: Rect, colors: HacColors, + focus: Focus, config: HacConfig, + layout: CreateCollectionLayout, navigator: Sender, } @@ -30,6 +49,10 @@ impl CreateCollection { config, colors, size, + layout: build_layout(size), + name: Default::default(), + focus: Default::default(), + desc: Default::default(), navigator: dummy, } } @@ -37,13 +60,56 @@ impl CreateCollection { impl Renderable for CreateCollection { fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { - frame.render_widget(Paragraph::new("lol"), self.size); + make_overlay(self.colors.clone(), self.colors.normal.black, 0.05, frame); + + let label = String::from("Collection Name"); + let name_input = Input::new(Some(&self.name), Some(&label), self.colors.clone()) + .value_style(Style::default().fg(self.colors.normal.white)) + .label_style(Style::default().fg(self.colors.bright.black)); + + let label = String::from("Description"); + let desc_input = Input::new(Some(&self.desc), Some(&label), self.colors.clone()) + .value_style(Style::default().fg(self.colors.normal.white)) + .label_style(Style::default().fg(self.colors.bright.black)); + + let hint = vec![ + "Tab".fg(self.colors.bright.green).bold(), + " - Switch input • ".fg(self.colors.bright.black), + "Enter".fg(self.colors.bright.green).bold(), + " - Confirm ".fg(self.colors.bright.black), + "Esc".fg(self.colors.bright.green).bold(), + " - Cancel".fg(self.colors.bright.black), + ]; + + let logo = Paragraph::new( + LOGO_ASCII + .iter() + .map(|line| Line::from(line.to_string()).fg(self.colors.bright.red).centered()) + .collect::>(), + ); + + frame.render_widget(logo, self.layout.logo); + frame.render_widget(name_input, self.layout.name_input); + frame.render_widget(desc_input, self.layout.desc_input); + frame.render_widget(Line::from(hint), self.layout.hint); + + match self.focus { + Focus::Name => frame.set_cursor( + self.layout.name_input.x + 1 + self.name.len() as u16, + self.layout.name_input.y + 2, + ), + Focus::Desc => frame.set_cursor( + self.layout.desc_input.x + 1 + self.desc.len() as u16, + self.layout.desc_input.y + 2, + ), + } Ok(()) } fn resize(&mut self, new_size: Rect) { self.size = new_size; + self.layout = build_layout(new_size); } fn attach_navigator(&mut self, navigator: Sender) { @@ -62,3 +128,36 @@ impl Eventful for CreateCollection { Ok(None) } } + +fn build_layout(area: Rect) -> CreateCollectionLayout { + let [_, form, _] = Layout::default() + .flex(Flex::Center) + .constraints([Constraint::Fill(1), Constraint::Length(MIN_WIDTH), Constraint::Fill(1)]) + .direction(Direction::Horizontal) + .areas(area); + + let [_, form, _] = Layout::default() + .constraints([Constraint::Fill(1), Constraint::Length(MIN_HEIGHT), Constraint::Fill(1)]) + .direction(Direction::Vertical) + .areas(form); + + let [logo, _, name_input, _, desc_input, _, hint] = Layout::default() + .constraints([ + Constraint::Length(LOGO_ASCII.len() as u16), + Constraint::Length(1), + Constraint::Length(4), + Constraint::Length(1), + Constraint::Length(4), + Constraint::Length(1), + Constraint::Length(1), + ]) + .direction(Direction::Vertical) + .areas(form); + + CreateCollectionLayout { + name_input, + desc_input, + hint, + logo, + } +} diff --git a/hac-client/src/pages/collection_list/mod.rs b/hac-client/src/pages/collection_list/mod.rs index b8b9f8a..379d597 100644 --- a/hac-client/src/pages/collection_list/mod.rs +++ b/hac-client/src/pages/collection_list/mod.rs @@ -9,8 +9,7 @@ use hac_loader::collection_loader::CollectionMeta; use ratatui::layout::Rect; use crate::router::Router; -use crate::HacColors; -use crate::HacConfig; +use crate::{HacColors, HacConfig}; #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Default)] pub enum Routes { @@ -35,11 +34,11 @@ pub fn make_collection_list_router( config: HacConfig, colors: HacColors, ) -> Router { - let mut router = Router::new(command_sender); + let mut router = Router::new(command_sender, colors.clone()); let collection_list = CollectionList::new(collections, size, config.clone(), colors.clone()); let create_collection = CreateCollection::new(size, config.clone(), colors.clone()); router.add_route(Routes::ListCollections.into(), Box::new(collection_list)); - router.add_route(Routes::CreateCollection.into(), Box::new(create_collection)); + router.add_dialog(Routes::CreateCollection.into(), Box::new(create_collection)); router } diff --git a/hac-client/src/pages/collection_viewer/request_editor/auth_editor/auth_kind_prompt.rs b/hac-client/src/pages/collection_viewer/request_editor/auth_editor/auth_kind_prompt.rs index 535cf7b..77c4cdc 100644 --- a/hac-client/src/pages/collection_viewer/request_editor/auth_editor/auth_kind_prompt.rs +++ b/hac-client/src/pages/collection_viewer/request_editor/auth_editor/auth_kind_prompt.rs @@ -1,11 +1,4 @@ -use crate::ascii::LOGO_ASCII; -use crate::components::component_styles::ComponentBorder; -use crate::components::list_item::{list_item, ListItemKind}; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::{overlay::make_overlay, Eventful, Renderable}; - use std::cell::RefCell; -use std::ops::{Add, Div, Sub}; use std::rc::Rc; use crossterm::event::{KeyCode, KeyEvent}; @@ -17,6 +10,13 @@ use ratatui::text::{Line, Span}; use ratatui::widgets::Paragraph; use ratatui::Frame; +use crate::ascii::LOGO_ASCII; +use crate::components::component_styles::ComponentBorder; +use crate::components::list_item::{list_item, ListItemKind}; +use crate::pages::collection_viewer::collection_store::CollectionStore; +use crate::pages::overlay::make_overlay_old; +use crate::pages::{Eventful, Renderable}; + pub enum AuthKindPromptEvent { Confirm(AuthMethod), Cancel, @@ -27,39 +27,28 @@ pub struct AuthKindPrompt<'akp> { colors: &'akp hac_colors::Colors, collection_store: Rc>, selected_idx: usize, - logo_idx: usize, } impl<'akp> AuthKindPrompt<'akp> { - pub fn new( - colors: &'akp hac_colors::Colors, - collection_store: Rc>, - ) -> AuthKindPrompt { - let logo_idx = rand::thread_rng().gen_range(0..LOGO_ASCII.len()); - + pub fn new(colors: &'akp hac_colors::Colors, collection_store: Rc>) -> AuthKindPrompt { AuthKindPrompt { colors, collection_store, selected_idx: 0, - logo_idx, } } } impl Renderable for AuthKindPrompt<'_> { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { - make_overlay(self.colors, self.colors.normal.black, 0.1, frame); + make_overlay_old(self.colors, self.colors.normal.black, 0.1, frame); - let logo = LOGO_ASCII[self.logo_idx]; + let logo = LOGO_ASCII; let logo_size = logo.len() as u16; // adding size of the form + spacing + hint let [_, center, _] = Layout::default() - .constraints([ - Constraint::Fill(1), - Constraint::Min(80), - Constraint::Fill(1), - ]) + .constraints([Constraint::Fill(1), Constraint::Min(80), Constraint::Fill(1)]) .direction(Direction::Horizontal) .areas(size); diff --git a/hac-client/src/pages/collection_viewer/request_editor/headers_editor/headers_editor.rs b/hac-client/src/pages/collection_viewer/request_editor/headers_editor/headers_editor.rs index 64ba927..df4b1bd 100755 --- a/hac-client/src/pages/collection_viewer/request_editor/headers_editor/headers_editor.rs +++ b/hac-client/src/pages/collection_viewer/request_editor/headers_editor/headers_editor.rs @@ -1,10 +1,6 @@ -use crate::ascii::LOGO_ASCII; -use crate::pages::collection_viewer::collection_viewer::CollectionViewerOverlay; -use crate::pages::overlay::make_overlay; -use crate::pages::{collection_viewer::collection_store::CollectionStore, Eventful, Renderable}; - -use std::ops::{Div, Mul, Sub}; -use std::{cell::RefCell, ops::Add, rc::Rc}; +use std::cell::RefCell; +use std::ops::{Add, Div, Mul, Sub}; +use std::rc::Rc; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::collection::types::HeaderMap; @@ -12,15 +8,16 @@ use rand::Rng; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::{Style, Stylize}; use ratatui::text::Line; -use ratatui::widgets::{ - Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, -}; +use ratatui::widgets::{Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState}; use ratatui::Frame; -use super::headers_editor_delete_prompt::{ - HeadersEditorDeletePrompt, HeadersEditorDeletePromptEvent, -}; +use super::headers_editor_delete_prompt::{HeadersEditorDeletePrompt, HeadersEditorDeletePromptEvent}; use super::headers_editor_edit_form::{HeadersEditorForm, HeadersEditorFormEvent}; +use crate::ascii::LOGO_ASCII; +use crate::pages::collection_viewer::collection_store::CollectionStore; +use crate::pages::collection_viewer::collection_viewer::CollectionViewerOverlay; +use crate::pages::overlay::make_overlay_old; +use crate::pages::{Eventful, Renderable}; #[derive(Debug)] pub enum HeadersEditorEvent { @@ -46,7 +43,6 @@ pub struct HeadersEditor<'he> { row_height: u16, amount_on_view: usize, layout: HeadersEditorLayout, - logo_idx: usize, delete_prompt: HeadersEditorDeletePrompt<'he>, header_form: HeadersEditorForm<'he>, @@ -60,7 +56,6 @@ impl<'he> HeadersEditor<'he> { ) -> Self { let row_height = 2; let layout = build_layout(size, row_height); - let logo_idx = rand::thread_rng().gen_range(0..LOGO_ASCII.len()); HeadersEditor { delete_prompt: HeadersEditorDeletePrompt::new(colors), @@ -73,7 +68,6 @@ impl<'he> HeadersEditor<'he> { row_height, amount_on_view: layout.content_size.height.div_ceil(row_height).into(), layout, - logo_idx, } } @@ -115,14 +109,11 @@ impl<'he> HeadersEditor<'he> { w if w.le(&100) => "[j/k -> move down/up] [enter -> select] [space -> enable/disable] [? -> help]", _ => "[j/k -> move down/up] [enter -> select] [space -> enable/disable] [d -> delete] [? -> help]", }; - frame.render_widget( - Paragraph::new(hint).fg(self.colors.bright.black).centered(), - hint_size, - ); + frame.render_widget(Paragraph::new(hint).fg(self.colors.bright.black).centered(), hint_size); } fn draw_help_overlay(&self, frame: &mut Frame) { - make_overlay(self.colors, self.colors.normal.black, 0.1, frame); + make_overlay_old(self.colors, self.colors.normal.black, 0.1, frame); let lines = [ [ @@ -139,18 +130,15 @@ impl<'he> HeadersEditor<'he> { ], [ format!("space{}", " ".repeat(7)).fg(self.colors.normal.red), - format!("- enables or disabled header{}", " ".repeat(12)) - .fg(self.colors.normal.yellow), + format!("- enables or disabled header{}", " ".repeat(12)).fg(self.colors.normal.yellow), ], [ format!("enter{}", " ".repeat(7)).fg(self.colors.normal.red), - format!("- select header for editing{}", " ".repeat(13)) - .fg(self.colors.normal.yellow), + format!("- select header for editing{}", " ".repeat(13)).fg(self.colors.normal.yellow), ], [ format!("?{}", " ".repeat(11)).fg(self.colors.normal.red), - format!("- shows this help message{}", " ".repeat(15)) - .fg(self.colors.normal.yellow), + format!("- shows this help message{}", " ".repeat(15)).fg(self.colors.normal.yellow), ], ]; @@ -159,7 +147,7 @@ impl<'he> HeadersEditor<'he> { .map(|l| Line::from(l.into_iter().collect::>())) .collect(); - let mut logo = LOGO_ASCII[self.logo_idx]; + let mut logo = LOGO_ASCII; let size = frame.size(); let logo_size = logo.len(); // we are adding 2 spaces for the gap between the logo and the text @@ -187,12 +175,7 @@ impl<'he> HeadersEditor<'he> { .chain(lines) .collect::>(); - let hint_size = Rect::new( - popup_size.x, - popup_size.y.add(popup_size.height).add(1), - 40, - 1, - ); + let hint_size = Rect::new(popup_size.x, popup_size.y.add(popup_size.height).add(1), 40, 1); let hint = Line::from("press any key to close this dialog") .fg(self.colors.bright.black) @@ -205,22 +188,16 @@ impl<'he> HeadersEditor<'he> { pub fn draw_empty_message(&self, frame: &mut Frame) { let size = self.layout.content_size; let no_headers = "No headers method".fg(self.colors.bright.black); - let no_request = Paragraph::new(no_headers).centered().block( - Block::default() - .fg(self.colors.normal.white) - .borders(Borders::ALL), - ); + let no_request = Paragraph::new(no_headers) + .centered() + .block(Block::default().fg(self.colors.normal.white).borders(Borders::ALL)); let size = Rect::new(size.x.add(5), size.y.sub(2), size.width.sub(10), 3); frame.render_widget(no_request, size); self.draw_hint(frame); } - pub fn draw_overlay( - &mut self, - frame: &mut Frame, - overlay: CollectionViewerOverlay, - ) -> anyhow::Result<()> { + pub fn draw_overlay(&mut self, frame: &mut Frame, overlay: CollectionViewerOverlay) -> anyhow::Result<()> { match overlay { CollectionViewerOverlay::HeadersHelp => self.draw_help_overlay(frame), CollectionViewerOverlay::HeadersDelete => { @@ -254,16 +231,9 @@ impl Renderable for HeadersEditor<'_> { let headers = headers.unwrap(); let title_name = Paragraph::new("Name").fg(self.colors.normal.yellow).bold(); let title_value = Paragraph::new("Value").fg(self.colors.normal.yellow).bold(); - let title_enabled = Paragraph::new("Enabled") - .fg(self.colors.normal.yellow) - .bold(); + let title_enabled = Paragraph::new("Enabled").fg(self.colors.normal.yellow).bold(); - for (idx, header) in headers - .iter() - .skip(self.scroll) - .take(self.amount_on_view) - .enumerate() - { + for (idx, header) in headers.iter().skip(self.scroll).take(self.amount_on_view).enumerate() { let size = self.layout.content_size; let offset = self.row_height.mul(idx as u16); let size = Rect::new(size.x, size.y.add(offset), size.width, self.row_height); @@ -315,12 +285,7 @@ impl Renderable for HeadersEditor<'_> { fn resize(&mut self, new_size: Rect) { self.layout = build_layout(new_size, self.row_height); - self.amount_on_view = self - .layout - .content_size - .height - .div_ceil(self.row_height) - .into(); + self.amount_on_view = self.layout.content_size.height.div_ceil(self.row_height).into(); } } @@ -392,11 +357,7 @@ impl Eventful for HeadersEditor<'_> { let mut request = request.write().unwrap(); - let total_headers = request - .headers - .as_ref() - .map(|h| h.len()) - .unwrap_or_default(); + let total_headers = request.headers.as_ref().map(|h| h.len()).unwrap_or_default(); match key_event.code { KeyCode::Char('j') | KeyCode::Down => { @@ -461,9 +422,9 @@ impl Eventful for HeadersEditor<'_> { }; drop(request); - self.collection_store.borrow_mut().push_overlay( - CollectionViewerOverlay::HeadersForm(self.selected_row, false), - ); + self.collection_store + .borrow_mut() + .push_overlay(CollectionViewerOverlay::HeadersForm(self.selected_row, false)); } } KeyCode::Esc => return Ok(Some(HeadersEditorEvent::RemoveSelection)), diff --git a/hac-client/src/pages/collection_viewer/request_editor/headers_editor/headers_editor_delete_prompt.rs b/hac-client/src/pages/collection_viewer/request_editor/headers_editor/headers_editor_delete_prompt.rs index 21d122e..8ffa12e 100644 --- a/hac-client/src/pages/collection_viewer/request_editor/headers_editor/headers_editor_delete_prompt.rs +++ b/hac-client/src/pages/collection_viewer/request_editor/headers_editor/headers_editor_delete_prompt.rs @@ -1,14 +1,16 @@ use std::ops::{Add, Div}; -use crate::ascii::LOGO_ASCII; -use crate::pages::{overlay::make_overlay, Eventful, Renderable}; - use crossterm::event::{KeyCode, KeyEvent}; use rand::Rng; +use ratatui::layout::Rect; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::widgets::Paragraph; -use ratatui::{layout::Rect, Frame}; +use ratatui::Frame; + +use crate::ascii::LOGO_ASCII; +use crate::pages::overlay::make_overlay_old; +use crate::pages::{Eventful, Renderable}; /// set of events `HeadersEditorDeletePrompt` can emit to its parent #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -24,19 +26,17 @@ pub enum HeadersEditorDeletePromptEvent { #[derive(Debug)] pub struct HeadersEditorDeletePrompt<'hedp> { colors: &'hedp hac_colors::Colors, - logo_idx: usize, } impl<'hedp> HeadersEditorDeletePrompt<'hedp> { pub fn new(colors: &'hedp hac_colors::Colors) -> Self { - let logo_idx = rand::thread_rng().gen_range(0..LOGO_ASCII.len()); - HeadersEditorDeletePrompt { colors, logo_idx } + HeadersEditorDeletePrompt { colors } } } impl Renderable for HeadersEditorDeletePrompt<'_> { fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { - make_overlay(self.colors, self.colors.normal.black, 0.1, frame); + make_overlay_old(self.colors, self.colors.normal.black, 0.1, frame); let lines: Vec = vec![ "are you sure you want to delete this header?" @@ -52,7 +52,7 @@ impl Renderable for HeadersEditorDeletePrompt<'_> { .centered(), ]; - let logo = LOGO_ASCII[self.logo_idx]; + let logo = LOGO_ASCII; let size = frame.size(); let logo_size = logo.len(); // we are adding 2 spaces for the gap between the logo and the text @@ -85,12 +85,8 @@ impl Eventful for HeadersEditorDeletePrompt<'_> { fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { match key_event.code { - KeyCode::Char('y') | KeyCode::Char('Y') => { - Ok(Some(HeadersEditorDeletePromptEvent::Confirm)) - } - KeyCode::Char('n') | KeyCode::Char('N') => { - Ok(Some(HeadersEditorDeletePromptEvent::Cancel)) - } + KeyCode::Char('y') | KeyCode::Char('Y') => Ok(Some(HeadersEditorDeletePromptEvent::Confirm)), + KeyCode::Char('n') | KeyCode::Char('N') => Ok(Some(HeadersEditorDeletePromptEvent::Cancel)), _ => Ok(None), } } diff --git a/hac-client/src/pages/collection_viewer/request_editor/headers_editor/headers_editor_edit_form.rs b/hac-client/src/pages/collection_viewer/request_editor/headers_editor/headers_editor_edit_form.rs index 026e247..41e5adc 100644 --- a/hac-client/src/pages/collection_viewer/request_editor/headers_editor/headers_editor_edit_form.rs +++ b/hac-client/src/pages/collection_viewer/request_editor/headers_editor/headers_editor_edit_form.rs @@ -1,10 +1,3 @@ -use crate::ascii::LOGO_ASCII; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::collection_viewer::collection_viewer::CollectionViewerOverlay; -use crate::pages::input::Input; -use crate::pages::overlay::make_overlay; -use crate::pages::{Eventful, Renderable}; - use std::cell::RefCell; use std::ops::{Add, Div, Sub}; use std::rc::Rc; @@ -17,6 +10,13 @@ use ratatui::text::Line; use ratatui::widgets::Paragraph; use ratatui::Frame; +use crate::ascii::LOGO_ASCII; +use crate::pages::collection_viewer::collection_store::CollectionStore; +use crate::pages::collection_viewer::collection_viewer::CollectionViewerOverlay; +use crate::pages::input::Input; +use crate::pages::overlay::make_overlay_old; +use crate::pages::{Eventful, Renderable}; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HeadersEditorFormEvent { FinishEdit, @@ -44,24 +44,17 @@ pub struct HeadersEditorForm<'hef> { colors: &'hef hac_colors::Colors, collection_store: Rc>, header_idx: usize, - logo_idx: usize, focused_input: HeadersEditorFormInput, original_name: String, original_value: String, } impl<'hef> HeadersEditorForm<'hef> { - pub fn new( - colors: &'hef hac_colors::Colors, - collection_store: Rc>, - ) -> HeadersEditorForm { - let logo_idx = rand::thread_rng().gen_range(0..LOGO_ASCII.len()); - + pub fn new(colors: &'hef hac_colors::Colors, collection_store: Rc>) -> HeadersEditorForm { HeadersEditorForm { colors, header_idx: 0, collection_store, - logo_idx, focused_input: HeadersEditorFormInput::Name, original_name: String::default(), original_value: String::default(), @@ -89,9 +82,7 @@ impl<'hef> HeadersEditorForm<'hef> { anyhow::bail!("tried to display the header form without the proper overlay set"); }; - let header = headers - .get(idx) - .expect("selected a non-existing header to edit"); + let header = headers.get(idx).expect("selected a non-existing header to edit"); self.original_name = header.pair.0.to_string(); self.original_value = header.pair.1.to_string(); @@ -108,7 +99,7 @@ impl<'hef> HeadersEditorForm<'hef> { impl Renderable for HeadersEditorForm<'_> { #[tracing::instrument(skip_all, err)] fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { - make_overlay(self.colors, self.colors.normal.black, 0.1, frame); + make_overlay_old(self.colors, self.colors.normal.black, 0.1, frame); let store = self.collection_store.borrow_mut(); let Some(request) = store.get_selected_request() else { @@ -124,22 +115,17 @@ impl Renderable for HeadersEditorForm<'_> { anyhow::bail!("tried to display the header form without the proper overlay set"); }; - let header = headers - .get(idx) - .expect("selected a non-existing header to edit"); + let header = headers.get(idx).expect("selected a non-existing header to edit"); let size = frame.size(); - let mut logo = LOGO_ASCII[self.logo_idx]; + let mut logo = LOGO_ASCII; let mut logo_size = logo.len() as u16; let total_size = logo_size.add(11).add(2); let mut size = Rect::new( size.width.div(2).sub(25), - size.height - .div(2) - .saturating_sub(logo_size.div(2)) - .saturating_sub(5), + size.height.div(2).saturating_sub(logo_size.div(2)).saturating_sub(5), 50, logo_size.add(11), ); @@ -153,10 +139,8 @@ impl Renderable for HeadersEditorForm<'_> { let mut name_input = Input::new(self.colors, "Name".into()); let mut value_input = Input::new(self.colors, "Value".into()); - let hint = Paragraph::new( - "Press enter to confirm, press esc to cancel".fg(self.colors.bright.black), - ) - .centered(); + let hint = + Paragraph::new("Press enter to confirm, press esc to cancel".fg(self.colors.bright.black)).centered(); match self.focused_input { HeadersEditorFormInput::Name => name_input.focus(), @@ -193,9 +177,7 @@ impl Renderable for HeadersEditorForm<'_> { } HeadersEditorFormInput::Value => { frame.set_cursor( - value_size - .x - .add(header.pair.1.chars().count().add(1) as u16), + value_size.x.add(header.pair.1.chars().count().add(1) as u16), value_size.y.add(1), ); } diff --git a/hac-client/src/pages/collection_viewer/response_viewer.rs b/hac-client/src/pages/collection_viewer/response_viewer.rs index 2622ef1..c96bdcc 100755 --- a/hac-client/src/pages/collection_viewer/response_viewer.rs +++ b/hac-client/src/pages/collection_viewer/response_viewer.rs @@ -1,28 +1,28 @@ -use hac_core::net::request_manager::Response; -use hac_core::syntax::highlighter::HIGHLIGHTER; - -use crate::ascii::{BIG_ERROR_ARTS, LOGO_ASCII, SMALL_ERROR_ARTS}; -use crate::pages::collection_viewer::collection_viewer::PaneFocus; -use crate::pages::under_construction::UnderConstruction; -use crate::pages::{spinner::Spinner, Eventful, Renderable}; -use crate::utils::build_syntax_highlighted_lines; - use std::cell::RefCell; use std::iter; use std::ops::{Add, Sub}; use std::rc::Rc; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use hac_core::net::request_manager::Response; +use hac_core::syntax::highlighter::HIGHLIGHTER; use rand::Rng; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::{Style, Stylize}; use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph, Scrollbar}; -use ratatui::widgets::{ScrollbarOrientation, ScrollbarState, Tabs}; +use ratatui::widgets::{ + Block, Borders, Clear, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Tabs, +}; use ratatui::Frame; use tree_sitter::Tree; use super::collection_store::CollectionStore; +use crate::ascii::{BIG_ERROR_ART, LOGO_ASCII, SMALL_ERROR_ART}; +use crate::pages::collection_viewer::collection_viewer::PaneFocus; +use crate::pages::spinner::Spinner; +use crate::pages::under_construction::UnderConstruction; +use crate::pages::{Eventful, Renderable}; +use crate::utils::build_syntax_highlighted_lines; #[derive(Debug)] pub enum ResponseViewerEvent { @@ -148,12 +148,7 @@ impl<'a> ResponseViewer<'a> { pub fn update(&mut self, response: Option>>) { let body_str = response .as_ref() - .and_then(|res| { - res.borrow() - .pretty_body - .as_ref() - .map(|body| body.to_string()) - }) + .and_then(|res| res.borrow().pretty_body.as_ref().map(|body| body.to_string())) .unwrap_or_default(); if body_str.len().gt(&0) { @@ -173,24 +168,19 @@ impl<'a> ResponseViewer<'a> { .unwrap_or(String::default()); self.error_lines = Some( - get_error_ascii_art( - self.preview_layout.content_pane.width, - &mut rand::thread_rng(), - ) - .iter() - .map(|line| Line::from(line.to_string()).centered()) - .chain(vec!["".into()]) - .chain( - cause - .chars() - .collect::>() - .chunks(self.layout.content_pane.width.sub(3).into()) - .map(|chunk| { - Line::from(chunk.iter().collect::().fg(self.colors.normal.red)) - }) - .collect::>(), - ) - .collect::>(), + get_error_ascii_art(self.preview_layout.content_pane.width) + .iter() + .map(|line| Line::from(line.to_string()).centered()) + .chain(vec!["".into()]) + .chain( + cause + .chars() + .collect::>() + .chunks(self.layout.content_pane.width.sub(3).into()) + .map(|chunk| Line::from(chunk.iter().collect::().fg(self.colors.normal.red))) + .collect::>(), + ) + .collect::>(), ) }; @@ -249,10 +239,7 @@ impl<'a> ResponseViewer<'a> { .into_centered_line(); frame.render_widget(Clear, request_pane); - frame.render_widget( - Block::default().bg(self.colors.primary.background), - request_pane, - ); + frame.render_widget(Block::default().bg(self.colors.primary.background), request_pane); frame.render_widget(spinner, size); } @@ -261,10 +248,7 @@ impl<'a> ResponseViewer<'a> { let request_pane = self.preview_layout.content_pane; frame.render_widget(Clear, request_pane); - frame.render_widget( - Block::default().bg(self.colors.primary.background), - request_pane, - ); + frame.render_widget(Block::default().bg(self.colors.primary.background), request_pane); let center = request_pane .y @@ -288,10 +272,7 @@ impl<'a> ResponseViewer<'a> { fn draw_waiting_for_request(&self, frame: &mut Frame) { let request_pane = self.preview_layout.content_pane; frame.render_widget(Clear, request_pane); - frame.render_widget( - Block::default().bg(self.colors.primary.background), - request_pane, - ); + frame.render_widget(Block::default().bg(self.colors.primary.background), request_pane); let mut empty_message = self.empty_lines.clone(); @@ -318,19 +299,13 @@ impl<'a> ResponseViewer<'a> { ); frame.render_widget( - Paragraph::new(empty_message) - .fg(self.colors.normal.red) - .centered(), + Paragraph::new(empty_message).fg(self.colors.normal.red).centered(), size, ) } fn draw_current_tab(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { - if self - .response - .as_ref() - .is_some_and(|res| res.borrow().is_error) - { + if self.response.as_ref().is_some_and(|res| res.borrow().is_error) { self.draw_network_error(frame); }; @@ -338,11 +313,7 @@ impl<'a> ResponseViewer<'a> { self.draw_waiting_for_request(frame); } - if self - .response - .as_ref() - .is_some_and(|res| !res.borrow().is_error) - { + if self.response.as_ref().is_some_and(|res| !res.borrow().is_error) { match self.active_tab { ResViewerTabs::Preview => self.draw_pretty_response(frame, size), ResViewerTabs::Raw => self.draw_raw_response(frame, size), @@ -363,10 +334,8 @@ impl<'a> ResponseViewer<'a> { if let Some(headers) = response.borrow().headers.as_ref() { let mut longest_line: usize = 0; - let mut lines: Vec = vec![ - Line::from("Headers".fg(self.colors.normal.red).bold()), - Line::from(""), - ]; + let mut lines: Vec = + vec![Line::from("Headers".fg(self.colors.normal.red).bold()), Line::from("")]; for (name, value) in headers { if let Ok(value) = value.to_str() { @@ -382,10 +351,7 @@ impl<'a> ResponseViewer<'a> { .yellow(), )); lines.push(Line::from( - value - .chars() - .skip(self.headers_scroll_x) - .collect::(), + value.chars().skip(self.headers_scroll_x).collect::(), )); lines.push(Line::from("")); } @@ -403,21 +369,14 @@ impl<'a> ResponseViewer<'a> { self.headers_scroll_x = longest_line.saturating_sub(1); } - let [headers_pane, x_scrollbar_pane] = - build_horizontal_scrollbar(self.preview_layout.content_pane); - self.draw_scrollbar( - lines.len(), - self.headers_scroll_y, - frame, - self.preview_layout.scrollbar, - ); - - let lines_to_show = - if longest_line > self.preview_layout.content_pane.width as usize { - headers_pane.height - } else { - self.preview_layout.content_pane.height - }; + let [headers_pane, x_scrollbar_pane] = build_horizontal_scrollbar(self.preview_layout.content_pane); + self.draw_scrollbar(lines.len(), self.headers_scroll_y, frame, self.preview_layout.scrollbar); + + let lines_to_show = if longest_line > self.preview_layout.content_pane.width as usize { + headers_pane.height + } else { + self.preview_layout.content_pane.height + }; let lines = lines .into_iter() @@ -428,18 +387,10 @@ impl<'a> ResponseViewer<'a> { let block = Block::default().padding(Padding::left(1)); if longest_line > self.preview_layout.content_pane.width as usize { - self.draw_horizontal_scrollbar( - longest_line, - self.headers_scroll_x, - frame, - x_scrollbar_pane, - ); + self.draw_horizontal_scrollbar(longest_line, self.headers_scroll_x, frame, x_scrollbar_pane); frame.render_widget(Paragraph::new(lines).block(block), headers_pane) } else { - frame.render_widget( - Paragraph::new(lines).block(block), - self.preview_layout.content_pane, - ); + frame.render_widget(Paragraph::new(lines).block(block), self.preview_layout.content_pane); } } } @@ -467,12 +418,7 @@ impl<'a> ResponseViewer<'a> { self.raw_scroll = lines.len().saturating_sub(1); } - self.draw_scrollbar( - lines.len(), - self.raw_scroll, - frame, - self.preview_layout.scrollbar, - ); + self.draw_scrollbar(lines.len(), self.raw_scroll, frame, self.preview_layout.scrollbar); let lines_in_view = lines .into_iter() @@ -486,13 +432,7 @@ impl<'a> ResponseViewer<'a> { } } - fn draw_scrollbar( - &self, - total_lines: usize, - current_scroll: usize, - frame: &mut Frame, - size: Rect, - ) { + fn draw_scrollbar(&self, total_lines: usize, current_scroll: usize, frame: &mut Frame, size: Rect) { let mut scrollbar_state = ScrollbarState::new(total_lines).position(current_scroll); let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) @@ -503,13 +443,7 @@ impl<'a> ResponseViewer<'a> { frame.render_stateful_widget(scrollbar, size, &mut scrollbar_state); } - fn draw_horizontal_scrollbar( - &self, - total_columns: usize, - current_scroll: usize, - frame: &mut Frame, - size: Rect, - ) { + fn draw_horizontal_scrollbar(&self, total_columns: usize, current_scroll: usize, frame: &mut Frame, size: Rect) { let mut scrollbar_state = ScrollbarState::new(total_columns).position(current_scroll); let scrollbar = Scrollbar::new(ScrollbarOrientation::HorizontalBottom) @@ -526,18 +460,10 @@ impl<'a> ResponseViewer<'a> { self.pretty_scroll = self.lines.len().saturating_sub(1); } - self.draw_scrollbar( - self.lines.len(), - self.raw_scroll, - frame, - self.preview_layout.scrollbar, - ); + self.draw_scrollbar(self.lines.len(), self.raw_scroll, frame, self.preview_layout.scrollbar); - let lines = if self.lines.len().gt(&0) { - self.lines.clone() - } else { - vec![Line::from("No body").centered()] - }; + let lines = + if self.lines.len().gt(&0) { self.lines.clone() } else { vec![Line::from("No body").centered()] }; let lines_in_view = lines .into_iter() @@ -581,8 +507,7 @@ impl<'a> ResponseViewer<'a> { status, " ".into(), "Time: ".fg(self.colors.bright.black), - format!("{}ms", response.borrow().duration.as_millis()) - .fg(self.colors.normal.green), + format!("{}ms", response.borrow().duration.as_millis()).fg(self.colors.normal.green), " ".into(), ]; @@ -650,9 +575,7 @@ impl<'a> Eventful for ResponseViewer<'a> { KeyCode::Char('k') => match self.active_tab { ResViewerTabs::Preview => self.pretty_scroll = self.pretty_scroll.saturating_sub(1), ResViewerTabs::Raw => self.raw_scroll = self.raw_scroll.saturating_sub(1), - ResViewerTabs::Headers => { - self.headers_scroll_y = self.headers_scroll_y.saturating_sub(1) - } + ResViewerTabs::Headers => self.headers_scroll_y = self.headers_scroll_y.saturating_sub(1), ResViewerTabs::Cookies => {} }, KeyCode::Char('l') => { @@ -695,11 +618,7 @@ fn build_layout(size: Rect) -> ResViewerLayout { fn build_horizontal_scrollbar(size: Rect) -> [Rect; 2] { let [request_pane, _, scrollbar_pane] = Layout::default() .direction(Direction::Vertical) - .constraints([ - Constraint::Fill(1), - Constraint::Length(1), - Constraint::Length(1), - ]) + .constraints([Constraint::Fill(1), Constraint::Length(1), Constraint::Length(1)]) .areas(size); [request_pane, scrollbar_pane] @@ -708,11 +627,7 @@ fn build_horizontal_scrollbar(size: Rect) -> [Rect; 2] { fn build_preview_layout(size: Rect) -> PreviewLayout { let [content_pane, _, scrollbar] = Layout::default() .direction(Direction::Horizontal) - .constraints([ - Constraint::Fill(1), - Constraint::Length(1), - Constraint::Length(1), - ]) + .constraints([Constraint::Fill(1), Constraint::Length(1), Constraint::Length(1)]) .areas(size); PreviewLayout { @@ -721,28 +636,15 @@ fn build_preview_layout(size: Rect) -> PreviewLayout { } } -fn get_error_ascii_art(width: u16, rng: &mut R) -> &'static [&'static str] -where - R: Rng, -{ +fn get_error_ascii_art(width: u16) -> &'static [&'static str] { match width.gt(&60) { - false => { - let index = rng.gen_range(0..SMALL_ERROR_ARTS.len()); - SMALL_ERROR_ARTS[index] - } - true => { - let full_range_arts = BIG_ERROR_ARTS - .iter() - .chain(SMALL_ERROR_ARTS) - .collect::>(); - let index = rng.gen_range(0..full_range_arts.len()); - full_range_arts[index] - } + false => SMALL_ERROR_ART, + true => BIG_ERROR_ART, } } fn make_empty_ascii_art(colors: &hac_colors::Colors) -> Vec> { - LOGO_ASCII[0] + LOGO_ASCII .iter() .map(|line| line.to_string().into()) .chain(vec![ @@ -756,40 +658,3 @@ fn make_empty_ascii_art(colors: &hac_colors::Colors) -> Vec> { ]) .collect::>() } - -#[cfg(test)] -mod tests { - use rand::{rngs::StdRng, SeedableRng}; - - use super::*; - #[test] - fn test_ascii_with_size() { - let seed = [0u8; 32]; - let mut rng = StdRng::from_seed(seed); - - let too_small = 59; - let art = get_error_ascii_art(too_small, &mut rng); - - let expected = [ - r#" ____ ____ ____ ___ ____ "#, - r#" / _ )/ ___) ___) _ \ / ___)"#, - r#"( (/ /| | | | | |_| | | "#, - r#" \____)_| |_| \___/|_| "#, - ]; - - assert_eq!(art, expected); - - let expected = [ - r#" dBBBP dBBBBBb dBBBBBb dBBBBP dBBBBBb"#, - r#" dBP dBP dBP.BP dBP"#, - r#" dBBP dBBBBK dBBBBK dBP.BP dBBBBK "#, - r#" dBP dBP BB dBP BB dBP.BP dBP BB "#, - r#" dBBBBP dBP dB' dBP dB' dBBBBP dBP dB' "#, - ]; - - let big_enough = 100; - let art = get_error_ascii_art(big_enough, &mut rng); - - assert_eq!(art, expected); - } -} diff --git a/hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs b/hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs index 1d972ff..a4db2af 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs @@ -1,4 +1,10 @@ +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::{Arc, RwLock}; + +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::collection::types::*; +use rand::Rng; use super::directory_form::{DirectoryForm, DirectoryFormCreate, DirectoryFormEvent}; use crate::ascii::LOGO_ASCII; @@ -6,25 +12,15 @@ use crate::pages::collection_viewer::collection_store::CollectionStore; use crate::pages::collection_viewer::sidebar::DirectoryFormTrait; use crate::pages::Eventful; -use std::cell::RefCell; -use std::rc::Rc; -use std::sync::{Arc, RwLock}; - -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use rand::Rng; - impl<'df> DirectoryForm<'df, DirectoryFormCreate> { pub fn new( colors: &'df hac_colors::Colors, collection_store: Rc>, ) -> DirectoryForm<'df, DirectoryFormCreate> { - let logo_idx = rand::thread_rng().gen_range(0..LOGO_ASCII.len()); - DirectoryForm { colors, collection_store, dir_name: String::default(), - logo_idx, marker: std::marker::PhantomData, directory: None, } @@ -54,9 +50,7 @@ impl Eventful for DirectoryForm<'_, DirectoryFormCreate> { .expect("tried to create a request without a collection"); let mut collection = collection.borrow_mut(); - let requests = collection - .requests - .get_or_insert(Arc::new(RwLock::new(vec![]))); + let requests = collection.requests.get_or_insert(Arc::new(RwLock::new(vec![]))); let mut requests = requests.write().unwrap(); if self.dir_name.is_empty() { diff --git a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs index cc5b46b..b84d834 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs @@ -1,31 +1,23 @@ -use hac_core::collection::types::*; - -use super::request_form::FormField; -use super::request_form::RequestForm; -use super::request_form::RequestFormCreate; -use super::request_form::RequestFormEvent; -use super::select_request_parent::{SelectRequestParent, SelectRequestParentEvent}; -use super::RequestFormTrait; -use crate::ascii::LOGO_ASCII; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::collection_viewer::collection_viewer::CollectionViewerOverlay; -use crate::pages::{Eventful, Renderable}; - use std::cell::RefCell; use std::ops::Sub; use std::rc::Rc; use std::sync::{Arc, RwLock}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use hac_core::collection::types::*; use rand::Rng; use ratatui::Frame; +use super::request_form::{FormField, RequestForm, RequestFormCreate, RequestFormEvent}; +use super::select_request_parent::{SelectRequestParent, SelectRequestParentEvent}; +use super::RequestFormTrait; +use crate::ascii::LOGO_ASCII; +use crate::pages::collection_viewer::collection_store::CollectionStore; +use crate::pages::collection_viewer::collection_viewer::CollectionViewerOverlay; +use crate::pages::{Eventful, Renderable}; + impl<'rf> RequestFormTrait for RequestForm<'rf, RequestFormCreate> { - fn draw_overlay( - &mut self, - frame: &mut Frame, - overlay: CollectionViewerOverlay, - ) -> anyhow::Result<()> { + fn draw_overlay(&mut self, frame: &mut Frame, overlay: CollectionViewerOverlay) -> anyhow::Result<()> { if let CollectionViewerOverlay::SelectParentDir = overlay { self.parent_selector.draw(frame, frame.size())?; } @@ -35,17 +27,11 @@ impl<'rf> RequestFormTrait for RequestForm<'rf, RequestFormCreate> { } impl<'rf> RequestForm<'rf, RequestFormCreate> { - pub fn new( - colors: &'rf hac_colors::Colors, - collection_store: Rc>, - ) -> Self { - let logo_idx = rand::thread_rng().gen_range(0..LOGO_ASCII.len()); - + pub fn new(colors: &'rf hac_colors::Colors, collection_store: Rc>) -> Self { RequestForm { colors, parent_selector: SelectRequestParent::new(colors, collection_store.clone()), collection_store, - logo_idx, request_name: String::default(), request_method: RequestMethod::Get, parent_dir: None, @@ -118,9 +104,7 @@ impl Eventful for RequestForm<'_, RequestFormCreate> { .expect("tried to create a request without a collection"); let mut collection = collection.borrow_mut(); - let requests = collection - .requests - .get_or_insert(Arc::new(RwLock::new(vec![]))); + let requests = collection.requests.get_or_insert(Arc::new(RwLock::new(vec![]))); let mut requests = requests.write().unwrap(); if self.request_name.is_empty() { @@ -140,11 +124,7 @@ impl Eventful for RequestForm<'_, RequestFormCreate> { }))); if let Some((dir_id, _)) = self.parent_dir.as_ref() { - if let RequestKind::Nested(dir) = requests - .iter_mut() - .find(|req| req.get_id().eq(dir_id)) - .unwrap() - { + if let RequestKind::Nested(dir) = requests.iter_mut().find(|req| req.get_id().eq(dir_id)).unwrap() { dir.requests.write().unwrap().push(request); } } else { diff --git a/hac-client/src/pages/collection_viewer/sidebar/delete_item_prompt.rs b/hac-client/src/pages/collection_viewer/sidebar/delete_item_prompt.rs index 04e2400..b125985 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/delete_item_prompt.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/delete_item_prompt.rs @@ -1,8 +1,3 @@ -use crate::ascii::LOGO_ASCII; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::overlay::make_overlay; -use crate::pages::{Eventful, Renderable}; - use std::cell::RefCell; use std::ops::{Add, Div, Mul, Sub}; use std::rc::Rc; @@ -15,6 +10,11 @@ use ratatui::text::Line; use ratatui::widgets::Paragraph; use ratatui::Frame; +use crate::ascii::LOGO_ASCII; +use crate::pages::collection_viewer::collection_store::CollectionStore; +use crate::pages::overlay::make_overlay_old; +use crate::pages::{Eventful, Renderable}; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DeleteItemPromptEvent { Confirm, @@ -29,10 +29,7 @@ pub struct DeleteItemPrompt<'dip> { } impl<'dip> DeleteItemPrompt<'dip> { - pub fn new( - colors: &'dip hac_colors::Colors, - collection_store: Rc>, - ) -> Self { + pub fn new(colors: &'dip hac_colors::Colors, collection_store: Rc>) -> Self { let logo_idx = rand::thread_rng().gen_range(0..LOGO_ASCII.len()); DeleteItemPrompt { colors, @@ -44,7 +41,7 @@ impl<'dip> DeleteItemPrompt<'dip> { impl Renderable for DeleteItemPrompt<'_> { fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { - make_overlay(self.colors, self.colors.normal.black, 0.1, frame); + make_overlay_old(self.colors, self.colors.normal.black, 0.1, frame); let store = self.collection_store.borrow(); let Some(hovered_id) = store.get_hovered_request().as_ref().cloned() else { @@ -66,38 +63,26 @@ impl Renderable for DeleteItemPrompt<'_> { let mut lines = if is_dir { vec![ - Line::from( - "Are you sure you want to delete the directory?".fg(self.colors.normal.red), - ) - .centered(), - Line::from("This will delete all the requests inside".fg(self.colors.normal.red)) - .centered(), + Line::from("Are you sure you want to delete the directory?".fg(self.colors.normal.red)).centered(), + Line::from("This will delete all the requests inside".fg(self.colors.normal.red)).centered(), Line::from(""), ] } else { vec![ - Line::from( - "Are you sure you want to delete the request?".fg(self.colors.normal.red), - ) - .centered(), + Line::from("Are you sure you want to delete the request?".fg(self.colors.normal.red)).centered(), Line::from(""), ] }; - lines.push( - Line::from("[Confirm: Enter] [Cancel: Esc]".fg(self.colors.bright.black)).centered(), - ); + lines.push(Line::from("[Confirm: Enter] [Cancel: Esc]".fg(self.colors.bright.black)).centered()); - let logo = LOGO_ASCII[self.logo_idx]; + let logo = LOGO_ASCII; let logo_size = logo.len() as u16; let size = frame.size(); let size = Rect::new( size.width.div(2).sub(25), - size.height - .div(2) - .saturating_sub(logo_size.div(2)) - .saturating_sub(3), + size.height.div(2).saturating_sub(logo_size.div(2)).saturating_sub(3), 50, logo_size.add(7), ); diff --git a/hac-client/src/pages/collection_viewer/sidebar/directory_form.rs b/hac-client/src/pages/collection_viewer/sidebar/directory_form.rs index 6766369..38bd11d 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/directory_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/directory_form.rs @@ -1,9 +1,3 @@ -use crate::ascii::LOGO_ASCII; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::input::Input; -use crate::pages::overlay::make_overlay; -use crate::pages::Renderable; - use std::cell::RefCell; use std::ops::{Add, Div, Sub}; use std::rc::Rc; @@ -13,6 +7,12 @@ use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::widgets::Paragraph; +use crate::ascii::LOGO_ASCII; +use crate::pages::collection_viewer::collection_store::CollectionStore; +use crate::pages::input::Input; +use crate::pages::overlay::make_overlay_old; +use crate::pages::Renderable; + /// set of events `DirectoryForm` can send the parent to /// handle #[derive(Debug, PartialEq, Eq, Clone)] @@ -36,7 +36,6 @@ pub struct DirectoryForm<'df, State = DirectoryFormCreate> { pub colors: &'df hac_colors::Colors, pub dir_name: String, pub collection_store: Rc>, - pub logo_idx: usize, /// the id of the directory being edited, this is only used when editing a directory /// this is (dir_id, dir_name) pub directory: Option<(String, String)>, @@ -51,23 +50,16 @@ impl<'cdf, State> DirectoryForm<'cdf, State> { } impl Renderable for DirectoryForm<'_, State> { - fn draw( - &mut self, - frame: &mut ratatui::prelude::Frame, - _: ratatui::prelude::Rect, - ) -> anyhow::Result<()> { - make_overlay(self.colors, self.colors.normal.black, 0.1, frame); - - let logo = LOGO_ASCII[self.logo_idx]; + fn draw(&mut self, frame: &mut ratatui::prelude::Frame, _: ratatui::prelude::Rect) -> anyhow::Result<()> { + make_overlay_old(self.colors, self.colors.normal.black, 0.1, frame); + + let logo = LOGO_ASCII; let logo_size = logo.len() as u16; let size = frame.size(); let size = Rect::new( size.width.div(2).sub(25), - size.height - .div(2) - .saturating_sub(logo_size.div(2)) - .saturating_sub(2), + size.height.div(2).saturating_sub(logo_size.div(2)).saturating_sub(2), 50, logo_size.add(4), ); @@ -85,12 +77,7 @@ impl Renderable for DirectoryForm<'_, State> { .centered(); let logo_size = Rect::new(size.x, size.y, size.width, logo_size); - let input_size = Rect::new( - size.x, - logo_size.y.add(logo_size.height).add(1), - size.width, - 3, - ); + let input_size = Rect::new(size.x, logo_size.y.add(logo_size.height).add(1), size.width, 3); let hint_size = Rect::new(size.x, input_size.y.add(4), size.width, 1); frame.render_widget(Paragraph::new(logo), logo_size); @@ -98,10 +85,7 @@ impl Renderable for DirectoryForm<'_, State> { frame.render_widget(hint, hint_size); frame.set_cursor( - input_size - .x - .add(self.dir_name.chars().count() as u16) - .add(1), + input_size.x.add(self.dir_name.chars().count() as u16).add(1), input_size.y.add(1), ); diff --git a/hac-client/src/pages/collection_viewer/sidebar/edit_directory_form.rs b/hac-client/src/pages/collection_viewer/sidebar/edit_directory_form.rs index 407825a..a0ee2dc 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/edit_directory_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/edit_directory_form.rs @@ -1,4 +1,10 @@ +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::{Arc, RwLock}; + +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::collection::types::*; +use rand::Rng; use super::directory_form::{DirectoryForm, DirectoryFormEdit, DirectoryFormEvent}; use crate::ascii::LOGO_ASCII; @@ -6,30 +12,18 @@ use crate::pages::collection_viewer::collection_store::CollectionStore; use crate::pages::collection_viewer::sidebar::DirectoryFormTrait; use crate::pages::Eventful; -use std::cell::RefCell; -use std::rc::Rc; -use std::sync::{Arc, RwLock}; - -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use rand::Rng; - impl<'df> DirectoryForm<'df, DirectoryFormEdit> { pub fn new( colors: &'df hac_colors::Colors, collection_store: Rc>, directory: Option<(String, String)>, ) -> DirectoryForm<'df, DirectoryFormEdit> { - let logo_idx = rand::thread_rng().gen_range(0..LOGO_ASCII.len()); - let dir_name = directory - .as_ref() - .map(|dir| dir.1.clone()) - .unwrap_or_default(); + let dir_name = directory.as_ref().map(|dir| dir.1.clone()).unwrap_or_default(); DirectoryForm { colors, collection_store, dir_name, - logo_idx, marker: std::marker::PhantomData, directory, } @@ -59,9 +53,7 @@ impl Eventful for DirectoryForm<'_, DirectoryFormEdit> { .expect("tried to create a request without a collection"); let mut collection = collection.borrow_mut(); - let requests = collection - .requests - .get_or_insert(Arc::new(RwLock::new(vec![]))); + let requests = collection.requests.get_or_insert(Arc::new(RwLock::new(vec![]))); let mut requests = requests.write().unwrap(); if self.dir_name.is_empty() { diff --git a/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs index c8b4f71..553a518 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs @@ -1,31 +1,23 @@ -use hac_core::collection::types::*; - -use super::request_form::FormField; -use super::request_form::RequestForm; -use super::request_form::RequestFormEdit; -use super::request_form::RequestFormEvent; -use super::select_request_parent::{SelectRequestParent, SelectRequestParentEvent}; -use super::RequestFormTrait; -use crate::ascii::LOGO_ASCII; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::collection_viewer::collection_viewer::CollectionViewerOverlay; -use crate::pages::{Eventful, Renderable}; - use std::cell::RefCell; use std::ops::Sub; use std::rc::Rc; use std::sync::{Arc, RwLock}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use hac_core::collection::types::*; use rand::Rng; use ratatui::Frame; +use super::request_form::{FormField, RequestForm, RequestFormEdit, RequestFormEvent}; +use super::select_request_parent::{SelectRequestParent, SelectRequestParentEvent}; +use super::RequestFormTrait; +use crate::ascii::LOGO_ASCII; +use crate::pages::collection_viewer::collection_store::CollectionStore; +use crate::pages::collection_viewer::collection_viewer::CollectionViewerOverlay; +use crate::pages::{Eventful, Renderable}; + impl<'rf> RequestFormTrait for RequestForm<'rf, RequestFormEdit> { - fn draw_overlay( - &mut self, - frame: &mut Frame, - overlay: CollectionViewerOverlay, - ) -> anyhow::Result<()> { + fn draw_overlay(&mut self, frame: &mut Frame, overlay: CollectionViewerOverlay) -> anyhow::Result<()> { if let CollectionViewerOverlay::SelectParentDir = overlay { self.parent_selector.draw(frame, frame.size())?; } @@ -40,7 +32,6 @@ impl<'rf> RequestForm<'rf, RequestFormEdit> { collection_store: Rc>, request: Arc>, ) -> Self { - let logo_idx = rand::thread_rng().gen_range(0..LOGO_ASCII.len()); let request_method = request.read().unwrap().method.clone(); let request_name = request.read().unwrap().name.clone(); @@ -72,7 +63,6 @@ impl<'rf> RequestForm<'rf, RequestFormEdit> { colors, parent_selector: SelectRequestParent::new(colors, collection_store.clone()), collection_store, - logo_idx, request_name, request_method, parent_dir, @@ -150,31 +140,19 @@ impl Eventful for RequestForm<'_, RequestFormEdit> { .get_collection() .expect("tried to create a request without a collection"); let mut collection = collection.borrow_mut(); - let requests = collection - .requests - .get_or_insert(Arc::new(RwLock::new(vec![]))); + let requests = collection.requests.get_or_insert(Arc::new(RwLock::new(vec![]))); let mut requests = requests.write().unwrap(); requests.iter_mut().for_each(|req| { if let RequestKind::Nested(dir) = req { - dir.requests - .write() - .unwrap() - .retain(|req| req.get_id().ne(&request_id)); + dir.requests.write().unwrap().retain(|req| req.get_id().ne(&request_id)); } }); if let Some((dir_id, _)) = self.parent_dir.as_ref() { - if let RequestKind::Nested(dir) = requests - .iter_mut() - .find(|req| req.get_id().eq(dir_id)) - .unwrap() - { + if let RequestKind::Nested(dir) = requests.iter_mut().find(|req| req.get_id().eq(dir_id)).unwrap() { let request = self.request.as_ref().unwrap().clone(); - dir.requests - .write() - .unwrap() - .push(RequestKind::Single(request)); + dir.requests.write().unwrap().push(RequestKind::Single(request)); } } diff --git a/hac-client/src/pages/collection_viewer/sidebar/request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/request_form.rs index d3e670b..dfe461a 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/request_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/request_form.rs @@ -1,23 +1,22 @@ -use hac_core::collection::types::{Request, RequestMethod}; - -use crate::ascii::LOGO_ASCII; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::collection_viewer::sidebar::select_request_parent::SelectRequestParent; -use crate::pages::input::Input; -use crate::pages::overlay::make_overlay; -use crate::pages::Renderable; - use std::cell::RefCell; use std::ops::{Add, Div, Sub}; use std::rc::Rc; use std::sync::{Arc, RwLock}; +use hac_core::collection::types::{Request, RequestMethod}; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::widgets::{Block, Borders, Paragraph}; use ratatui::Frame; +use crate::ascii::LOGO_ASCII; +use crate::pages::collection_viewer::collection_store::CollectionStore; +use crate::pages::collection_viewer::sidebar::select_request_parent::SelectRequestParent; +use crate::pages::input::Input; +use crate::pages::overlay::make_overlay_old; +use crate::pages::Renderable; + #[derive(Debug)] pub enum RequestFormEvent { Confirm, @@ -60,9 +59,6 @@ pub struct RequestForm<'rf, State = RequestFormCreate> { pub colors: &'rf hac_colors::Colors, pub collection_store: Rc>, - /// when we construct this component, we randomly chose one of our available - /// logo arts to display, for a little fun touch - pub logo_idx: usize, /// the name of the current request being edited or created pub request_name: String, /// which method the request should have when finishing edition or creation @@ -102,9 +98,9 @@ impl<'rf, State> RequestForm<'rf, State> { impl<'rf, State> Renderable for RequestForm<'rf, State> { fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { - make_overlay(self.colors, self.colors.normal.black, 0.1, frame); + make_overlay_old(self.colors, self.colors.normal.black, 0.1, frame); - let mut logo = LOGO_ASCII[self.logo_idx]; + let mut logo = LOGO_ASCII; let mut logo_size = logo.len() as u16; // adding size of the form + spacing + hint let total_size = logo_size.add(11).add(2); @@ -112,10 +108,7 @@ impl<'rf, State> Renderable for RequestForm<'rf, State> { let size = frame.size(); let mut size = Rect::new( size.width.div(2).sub(32), - size.height - .div(2) - .saturating_sub(logo_size.div(2)) - .saturating_sub(6), + size.height.div(2).saturating_sub(logo_size.div(2)).saturating_sub(6), 65, logo_size.add(12), ); @@ -139,8 +132,7 @@ impl<'rf, State> Renderable for RequestForm<'rf, State> { let mut name_input = Input::new(self.colors, "Name".into()); let method_title = Paragraph::new("Method".fg(self.colors.normal.white)); - let hint = - "[Confirm: Enter] [Cancel: Esc] [Switch: Tab] [Select: Space] [Remove Parent: ]"; + let hint = "[Confirm: Enter] [Cancel: Esc] [Switch: Tab] [Select: Space] [Remove Parent: ]"; let hint_size = hint.len() as u16; let hint = Paragraph::new(hint.fg(self.colors.bright.black)).centered(); @@ -199,12 +191,7 @@ impl<'rf, State> Renderable for RequestForm<'rf, State> { }; let method = Paragraph::new(Line::from(vec![ format!(" {}", idx.add(1)).fg(self.colors.bright.black), - format!( - " {}{}", - method, - " ".repeat(10.sub(method.to_string().len())) - ) - .fg(self.colors.normal.white), + format!(" {}{}", method, " ".repeat(10.sub(method.to_string().len()))).fg(self.colors.normal.white), ])) .block(Block::default().borders(Borders::ALL).fg(border_color)); frame.render_widget(method, methods_items[idx]); @@ -218,10 +205,8 @@ impl<'rf, State> Renderable for RequestForm<'rf, State> { .no_available_parent_timer .is_some_and(|timer| timer.elapsed().as_secs().le(&3)) { - let warning = Paragraph::new( - "No available directory to select as a parent".fg(self.colors.normal.red), - ) - .centered(); + let warning = + Paragraph::new("No available directory to select as a parent".fg(self.colors.normal.red)).centered(); frame.render_widget(warning, hint_size); } else { frame.render_widget(hint, hint_size); @@ -236,10 +221,7 @@ impl<'rf, State> Renderable for RequestForm<'rf, State> { if self.focused_field.eq(&FormField::Name) { frame.set_cursor( - name_size - .x - .add(self.request_name.chars().count() as u16) - .add(1), + name_size.x.add(self.request_name.chars().count() as u16).add(1), name_size.y.add(1), ); } diff --git a/hac-client/src/pages/collection_viewer/sidebar/select_request_parent.rs b/hac-client/src/pages/collection_viewer/sidebar/select_request_parent.rs index 3749a4a..1b6ebf1 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/select_request_parent.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/select_request_parent.rs @@ -1,8 +1,3 @@ -use crate::ascii::LOGO_ASCII; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::overlay::make_overlay; -use crate::pages::{Eventful, Renderable}; - use std::cell::RefCell; use std::ops::{Add, Div, Mul, Sub}; use std::rc::Rc; @@ -15,6 +10,11 @@ use ratatui::text::Line; use ratatui::widgets::Paragraph; use ratatui::Frame; +use crate::ascii::LOGO_ASCII; +use crate::pages::collection_viewer::collection_store::CollectionStore; +use crate::pages::overlay::make_overlay_old; +use crate::pages::{Eventful, Renderable}; + #[derive(Debug, PartialEq, Eq, Clone)] pub enum SelectRequestParentEvent { /// user selected a directory, so we send back the id of the directory to be @@ -30,20 +30,15 @@ pub struct SelectRequestParent<'srp> { colors: &'srp hac_colors::Colors, collection_store: Rc>, selected_dir: usize, - logo_idx: usize, scroll: usize, } impl<'srp> SelectRequestParent<'srp> { - pub fn new( - colors: &'srp hac_colors::Colors, - collection_store: Rc>, - ) -> Self { + pub fn new(colors: &'srp hac_colors::Colors, collection_store: Rc>) -> Self { SelectRequestParent { colors, collection_store, selected_dir: 0, - logo_idx: rand::thread_rng().gen_range(0..LOGO_ASCII.len()), scroll: 0, } } @@ -51,7 +46,7 @@ impl<'srp> SelectRequestParent<'srp> { impl Renderable for SelectRequestParent<'_> { fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { - make_overlay(self.colors, self.colors.normal.black, 0.1, frame); + make_overlay_old(self.colors, self.colors.normal.black, 0.1, frame); let store = self.collection_store.borrow(); let collection = store @@ -68,7 +63,7 @@ impl Renderable for SelectRequestParent<'_> { .for_each(|dir| directories.push(dir.get_name())); }; - let mut logo = LOGO_ASCII[self.logo_idx]; + let mut logo = LOGO_ASCII; let size = frame.size(); let mut logo_size = logo.len() as u16; @@ -78,12 +73,7 @@ impl Renderable for SelectRequestParent<'_> { logo_size = 0; } - let size = Rect::new( - size.width.div(2).saturating_sub(25), - size.y.add(4), - 50, - size.height, - ); + let size = Rect::new(size.width.div(2).saturating_sub(25), size.y.add(4), 50, size.height); if !logo.is_empty() { let logo_size = Rect::new(size.x, size.y, size.width, logo_size); @@ -110,17 +100,8 @@ impl Renderable for SelectRequestParent<'_> { .skip(self.scroll) .take(amount_on_view.into()) { - let foreground = if self.selected_dir.eq(&idx) { - self.colors.normal.red - } else { - self.colors.normal.white - }; - let dir_size = Rect::new( - size.x, - size.y.add(dirs_start_y).add(idx.mul(2) as u16), - size.width, - 2, - ); + let foreground = if self.selected_dir.eq(&idx) { self.colors.normal.red } else { self.colors.normal.white }; + let dir_size = Rect::new(size.x, size.y.add(dirs_start_y).add(idx.mul(2) as u16), size.width, 2); let dir = Paragraph::new(dir.fg(foreground)); frame.render_widget(dir, dir_size); } diff --git a/hac-client/src/pages/overlay.rs b/hac-client/src/pages/overlay.rs index 812770d..a506b39 100755 --- a/hac-client/src/pages/overlay.rs +++ b/hac-client/src/pages/overlay.rs @@ -5,10 +5,11 @@ use ratatui::widgets::Paragraph; use ratatui::Frame; use crate::utils::blend_colors_multiply; +use crate::HacColors; /// draws a fullscreen overlay with the given fill text, many pages uses this to display /// "floating" information -pub fn draw_overlay(colors: &hac_colors::Colors, size: Rect, fill_text: &str, frame: &mut Frame) { +pub fn draw_overlay_old(colors: &hac_colors::Colors, size: Rect, fill_text: &str, frame: &mut Frame) { let lines: Vec> = vec![fill_text.repeat(size.width.into()).into(); size.height.into()]; let overlay = Paragraph::new(lines) @@ -19,7 +20,20 @@ pub fn draw_overlay(colors: &hac_colors::Colors, size: Rect, fill_text: &str, fr frame.render_widget(overlay, size); } -pub fn make_overlay(colors: &hac_colors::Colors, color: Color, alpha: f32, frame: &mut Frame) { +pub fn make_overlay_old(colors: &hac_colors::Colors, color: Color, alpha: f32, frame: &mut Frame) { + let buffer = frame.buffer_mut(); + let cells = &mut buffer.content; + + cells.iter_mut().for_each(|cell| { + let cell_fg = cell.style().fg.unwrap_or(colors.normal.white); + let cell_bg = cell.style().bg.unwrap_or(colors.primary.background); + + cell.set_fg(blend_colors_multiply(cell_fg, color, alpha)); + cell.set_bg(blend_colors_multiply(cell_bg, color, alpha)); + }); +} + +pub fn make_overlay(colors: HacColors, color: Color, alpha: f32, frame: &mut Frame) { let buffer = frame.buffer_mut(); let cells = &mut buffer.content; diff --git a/hac-client/src/pages/terminal_too_small.rs b/hac-client/src/pages/terminal_too_small.rs index 75c3275..8fba2f2 100755 --- a/hac-client/src/pages/terminal_too_small.rs +++ b/hac-client/src/pages/terminal_too_small.rs @@ -1,21 +1,23 @@ -use crate::pages::Renderable; - use ratatui::layout::{Alignment, Constraint, Direction, Flex, Layout, Rect}; use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::widgets::{Paragraph, Wrap}; use ratatui::Frame; +use crate::pages::Renderable; +use crate::{HacColors, MIN_HEIGHT, MIN_WIDTH}; + /// `TerminalTooSmall` as the name suggests is a screen rendered by the /// `screen_manager` when the terminal gets smaller than a certain threshold, /// this page will display over everything and will automatically be hidden /// when the terminal gets bigger than said threshold +#[derive(Debug)] pub struct TerminalTooSmall { - colors: hac_colors::Colors, + colors: HacColors, } impl TerminalTooSmall { - pub fn new(colors: hac_colors::Colors) -> Self { + pub fn new(colors: HacColors) -> Self { TerminalTooSmall { colors } } } @@ -33,7 +35,11 @@ impl Renderable for TerminalTooSmall { ]); let empty = Line::from(" "); let hint = Line::from("Minimum size needed:".bold().fg(self.colors.bright.black)); - let min_size = Line::from("Width = 80 Height = 22".bold().fg(self.colors.bright.black)); + let min_size = Line::from( + format!("Width = {MIN_WIDTH} Height = {MIN_HEIGHT}") + .bold() + .fg(self.colors.bright.black), + ); let text = Paragraph::new(vec![lines, curr_size, empty, hint, min_size]) .wrap(Wrap { trim: true }) diff --git a/hac-client/src/router.rs b/hac-client/src/router.rs index 4c56799..771042d 100644 --- a/hac-client/src/router.rs +++ b/hac-client/src/router.rs @@ -1,43 +1,67 @@ +use std::collections::{HashMap, VecDeque}; use std::fmt::Debug; -use std::sync::mpsc::Sender; -use std::{collections::HashMap, sync::mpsc::Receiver}; +use std::sync::mpsc::{Receiver, Sender}; use hac_core::command::Command; -use ratatui::{layout::Rect, Frame}; +use ratatui::layout::Rect; +use ratatui::Frame; +use crate::pages::terminal_too_small::TerminalTooSmall; use crate::pages::{Eventful, Renderable}; +use crate::{HacColors, MIN_HEIGHT, MIN_WIDTH}; pub trait EventfulRenderable: Renderable + Eventful + Debug {} impl EventfulRenderable for K where K: Renderable + Eventful + Debug {} type Key = u8; +/// This is how routes communicate with the router how then want to navigate +/// between screens, a router can have any number of dialogs, routes and +/// nested routers. #[derive(Debug)] pub enum Navigate { + /// Change the current route to a new route, or open a dialog if the key matches a + /// dialog rather than a route + /// Navigating to a route from a dialog will close that dialog To(Key, Option>), - Up(Key, Option>), + /// Routes can only communicate with the router that owns the route, therefore, + /// if a route in a nested router needs to navigate to an entirely different router + /// the `Leave` variant is responsible for that. It will direct the navigation event + /// to the parent router of the current active router. + Leave(Key, Option>), + /// Go back in the navigation history, this completely wipes the current router history, + /// and close every dialog thats visible + Back(Option>), } #[derive(Debug)] pub struct Router { routes: HashMap>>, + dialogs: HashMap>>, + dialog_stack: Vec, active_route: Key, + history: Vec, command_sender: Sender, navigate_sender: Sender, navigate_receiver: Receiver, parent_navigator: Option>, + too_small: TerminalTooSmall, } impl Router { - pub fn new(command_sender: Sender) -> Self { + pub fn new(command_sender: Sender, colors: HacColors) -> Self { let (navigate_sender, navigate_receiver) = std::sync::mpsc::channel(); Self { routes: Default::default(), + dialogs: Default::default(), + dialog_stack: Default::default(), active_route: Default::default(), + history: vec![Default::default()], command_sender, navigate_sender, navigate_receiver, parent_navigator: None, + too_small: TerminalTooSmall::new(colors.clone()), } } @@ -51,51 +75,128 @@ impl Router { self.routes.insert(key, route); } + pub fn add_dialog(&mut self, key: Key, mut dialog: Box>) { + dialog.attach_navigator(self.navigate_sender.clone()); + dialog.register_command_handler(self.command_sender.clone()); + self.dialogs.insert(key, dialog); + } + pub fn handle_command(&mut self, command: Command) -> anyhow::Result<()> { let route = self.get_active_route(); route.handle_command(command)?; Ok(()) } + pub fn attach_parent_navigator(&mut self, navigator: Sender) { + self.parent_navigator = Some(navigator); + } + fn get_active_route(&mut self) -> &mut Box> { self.routes .get_mut(&self.active_route) .expect("active route doesn't exist on router") } - pub fn attach_parent_navigator(&mut self, navigator: Sender) { - self.parent_navigator = Some(navigator); + fn get_active_dialog(&mut self) -> Option<&mut Box>> { + let key = self.dialog_stack.last()?; + Some( + self.dialogs + .get_mut(key) + .expect("tried to show a dialog that is not registered"), + ) } -} -impl Renderable for Router { - fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { - match self.navigate_receiver.try_recv() { - Ok(Navigate::To(route, data)) => { - self.active_route = route; - let route = self.get_active_route(); + fn navigate(&mut self, navigation: Navigate) { + match navigation { + Navigate::To(route, data) => { + if self.routes.contains_key(&route) { + // if navigating to a route, we clear the dialogs. + self.dialog_stack.clear(); + + self.active_route = route; + self.history.push(route); + let route = self.get_active_route(); + route.update(data); + } else if self.dialogs.contains_key(&route) { + self.dialog_stack.push(route); + let dialog = self + .get_active_dialog() + .expect("attempt to navigate to non registered dialog"); + dialog.update(data); + } else { + panic!("tried to navigate to an unknown route"); + } + } + Navigate::Back(data) => { + if self.history.len() <= 1 { + return; + } + + // SAFETY: we just checked if these exists + let curr = self.history.pop().unwrap(); + let prev = *self.history.last().unwrap(); + + // pop the dialog if we are currently displaying one + self.dialogs.contains_key(&curr).then(|| self.dialog_stack.pop()); + + let route; + if self.dialogs.contains_key(&prev) { + route = self + .get_active_dialog() + .expect("previous route is a dialog, but its not on the stack"); + } else { + route = self + .routes + .get_mut(&prev) + .expect("previous route is not registered... how?"); + } + route.update(data); } - Ok(Navigate::Up(route, data)) if self.parent_navigator.is_some() => { - tracing::debug!(""); + Navigate::Leave(route, data) if self.parent_navigator.is_some() => { + self.history.clear(); + self.dialog_stack.clear(); + self.parent_navigator .as_mut() .unwrap() .send(Navigate::To(route, data)) .expect("failed to send navigation command"); } + Navigate::Leave(_, _) => panic!("theres no parent navigator"), + } + } +} + +impl Renderable for Router { + fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { + if frame.size().height < MIN_HEIGHT || frame.size().width < MIN_WIDTH { + self.too_small.draw(frame, size); + return Ok(()); + } + + match self.navigate_receiver.try_recv() { + Ok(navigation) => self.navigate(navigation), _ => {} } let route = self.get_active_route(); route.draw(frame, size)?; + if let Some(dialog) = self.get_active_dialog() { + dialog.draw(frame, size)?; + } + Ok(()) } fn resize(&mut self, new_size: Rect) { let route = self.get_active_route(); route.resize(new_size); + + if let Some(dialog) = self.get_active_dialog() { + dialog.resize(new_size); + } } } @@ -103,6 +204,10 @@ impl Eventful for Router { type Result = Command; fn handle_key_event(&mut self, key_event: crossterm::event::KeyEvent) -> anyhow::Result> { + if let Some(dialog) = self.get_active_dialog() { + return dialog.handle_key_event(key_event); + } + let route = self.get_active_route(); route.handle_key_event(key_event) } diff --git a/hac-config/src/config.rs b/hac-config/src/config.rs index 387ddfa..3050549 100755 --- a/hac-config/src/config.rs +++ b/hac-config/src/config.rs @@ -1,11 +1,11 @@ -use crate::default_config::DEFAULT_CONFIG; -use crate::{EditorMode, APP_NAME, CONFIG_ENV_VAR, CONFIG_FILE, XDG_DEFAULTS, XDG_ENV_VARS}; - use std::collections::HashMap; use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; +use crate::default_config::DEFAULT_CONFIG; +use crate::{EditorMode, APP_NAME, CONFIG_ENV_VAR, CONFIG_FILE, XDG_DEFAULTS, XDG_ENV_VARS}; + #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub enum Action { Undo, diff --git a/rustfmt.toml b/rustfmt.toml index e4668df..64f0d34 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,7 +1,7 @@ edition = "2021" unstable_features = true use_field_init_shorthand = true -imports_granularity = "Item" +imports_granularity = "Module" group_imports = "StdExternalCrate" format_code_in_doc_comments = true reorder_impl_items = true From 310059d74299277e98b243ba3bef0cff8433b51e Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Tue, 24 Sep 2024 00:11:40 -0300 Subject: [PATCH 05/23] wip: new create collection form and changing types --- Cargo.lock | 111 ++ Justfile | 3 +- hac-client/src/app.rs | 22 +- hac-client/src/components/input.rs | 5 +- hac-client/src/components/list_itemm.rs | 2 +- hac-client/src/event_pool.rs | 10 +- hac-client/src/lib.rs | 5 +- hac-client/src/main.rs | 5 +- hac-client/src/pages.rs | 7 +- .../collection_dashboard.rs | 1156 ----------------- .../collection_dashboard/collection_list.rs | 279 ---- .../src/pages/collection_dashboard/mod.rs | 6 - .../new_collection_form.rs | 156 --- .../pages/collection_list/collection_list.rs | 292 ----- .../collection_list/create_collection.rs | 180 ++- hac-client/src/pages/collection_list/mod.rs | 415 +++++- .../sidebar/create_directory_form.rs | 9 +- hac-client/src/pages/confirm_popup.rs | 79 -- hac-client/src/pages/error_popup.rs | 130 -- hac-client/src/router.rs | 35 +- hac-client/src/screen_manager.rs | 155 --- hac-client/src/utils.rs | 5 +- hac-config/src/config.rs | 1 + hac-config/src/default_config.rs | 2 + hac-core/Cargo.toml | 1 + hac-core/src/collection/errors.rs | 22 - hac-core/src/collection/mod.rs | 75 -- hac-core/src/collection/types.rs | 289 ----- hac-core/src/command.rs | 2 +- hac-core/src/lib.rs | 1 - hac-core/src/net/request_client.rs | 12 +- hac-core/src/net/request_manager.rs | 18 +- hac-core/src/net/request_strategies.rs | 3 +- .../net/request_strategies/http_strategy.rs | 40 +- hac-core/src/text_object/cursor.rs | 18 +- hac-core/src/text_object/text_object.rs | 54 +- hac-loader/Cargo.toml | 1 + .../src/collection_loader/json_collection.rs | 19 +- hac-loader/src/collection_loader/mod.rs | 164 ++- 39 files changed, 909 insertions(+), 2880 deletions(-) delete mode 100755 hac-client/src/pages/collection_dashboard/collection_dashboard.rs delete mode 100755 hac-client/src/pages/collection_dashboard/collection_list.rs delete mode 100755 hac-client/src/pages/collection_dashboard/mod.rs delete mode 100755 hac-client/src/pages/collection_dashboard/new_collection_form.rs delete mode 100644 hac-client/src/pages/collection_list/collection_list.rs delete mode 100755 hac-client/src/pages/confirm_popup.rs delete mode 100755 hac-client/src/pages/error_popup.rs delete mode 100755 hac-client/src/screen_manager.rs delete mode 100755 hac-core/src/collection/errors.rs delete mode 100644 hac-core/src/collection/mod.rs delete mode 100755 hac-core/src/collection/types.rs diff --git a/Cargo.lock b/Cargo.lock index 06e46f9..4ecb4e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -412,6 +412,18 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + [[package]] name = "fnv" version = "1.0.7" @@ -427,6 +439,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures" version = "0.3.30" @@ -607,6 +628,7 @@ version = "0.2.0" dependencies = [ "anyhow", "hac-config", + "hac-store", "jsonxf", "lazy_static", "ratatui", @@ -629,6 +651,7 @@ dependencies = [ "dirs", "hac-config", "hac-store", + "notify", "serde", "serde_json", "tracing", @@ -808,6 +831,26 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -863,6 +906,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -883,6 +946,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.5.0", "libc", + "redox_syscall", ] [[package]] @@ -967,6 +1031,25 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc0287524726960e07b119cebd01678f852f147742ae0d925e6a520dca956126" +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.5.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1388,6 +1471,15 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.23" @@ -2020,6 +2112,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -2127,6 +2229,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Justfile b/Justfile index 5e868e8..1f52997 100755 --- a/Justfile +++ b/Justfile @@ -20,7 +20,7 @@ build-time: cargo +nightly build -Z timings fmt: - cargo fmt --check + cargo +nightly fmt --check lint: cargo clippy -- -D warnings @@ -130,4 +130,3 @@ cross: ### x86 just binary hac.exe x86_64-pc-windows-gnu just binary_static hac.exe x86_64-pc-windows-gnu - diff --git a/hac-client/src/app.rs b/hac-client/src/app.rs index cd36127..2493a3f 100755 --- a/hac-client/src/app.rs +++ b/hac-client/src/app.rs @@ -1,10 +1,6 @@ use std::io::Stdout; -use std::rc::Rc; use std::sync::mpsc::{Receiver, Sender}; -use std::sync::RwLock; -use hac_colors::Colors; -use hac_config::Config; use hac_core::command::Command; use hac_loader::collection_loader::CollectionMeta; use ratatui::backend::CrosstermBackend; @@ -23,11 +19,11 @@ pub enum AppRoutes { CollectionViewerRouter, } -impl Into for AppRoutes { - fn into(self) -> u8 { - match self { - Self::CollectionListRouter => 0, - Self::CollectionViewerRouter => 1, +impl From for u8 { + fn from(val: AppRoutes) -> Self { + match val { + AppRoutes::CollectionListRouter => 0, + AppRoutes::CollectionViewerRouter => 1, } } } @@ -39,9 +35,6 @@ pub struct App { command_receiver: Receiver, command_sender: Sender, router: Router, - - config: Rc>, - colors: Rc, } impl App { @@ -69,9 +62,6 @@ impl App { command_receiver, command_sender, terminal, - - config, - colors, }) } @@ -92,7 +82,7 @@ impl App { } } - if let Some(event) = self.event_pool.next().await { + if let Some(event) = self.event_pool.next() { match event { Event::Tick => self.router.tick()?, Event::Resize(new_size) => self.router.resize(new_size), diff --git a/hac-client/src/components/input.rs b/hac-client/src/components/input.rs index 094ffa1..22daa06 100644 --- a/hac-client/src/components/input.rs +++ b/hac-client/src/components/input.rs @@ -97,12 +97,11 @@ impl Widget for Input<'_> { .border_style(self.border_style) .style(self.label_style); - let value = self.value.as_ref().map(|v| v.to_string()).unwrap_or(String::default()); + let value = self.value.as_ref().map(|v| v.to_string()).unwrap_or_default(); let size = Rect::new(area.x, area.y, area.width, 3); let fill = size.width as usize - value.len(); - let value = format!("{}{}", value.to_string(), " ".repeat(fill)); + let value = format!("{}{}", value, " ".repeat(fill)); let value = Line::from(value).style(self.value_style); Paragraph::new(value).block(block).render(size, buf); - return; } } diff --git a/hac-client/src/components/list_itemm.rs b/hac-client/src/components/list_itemm.rs index 30782ac..0cc7138 100644 --- a/hac-client/src/components/list_itemm.rs +++ b/hac-client/src/components/list_itemm.rs @@ -86,7 +86,7 @@ impl Widget for ListItem<'_> { .border_style( Style::default() .bg(self.colors.primary.background) - .fg(self.colors.normal.white), + .fg(self.colors.bright.red), ) .style(self.title_style); } diff --git a/hac-client/src/event_pool.rs b/hac-client/src/event_pool.rs index 0c035e7..00fdf5e 100755 --- a/hac-client/src/event_pool.rs +++ b/hac-client/src/event_pool.rs @@ -15,15 +15,15 @@ pub enum Event { /// them over to be handled #[derive(Debug)] pub struct EventPool { - event_rx: tokio::sync::mpsc::UnboundedReceiver, - event_tx: tokio::sync::mpsc::UnboundedSender, + event_rx: std::sync::mpsc::Receiver, + event_tx: std::sync::mpsc::Sender, frame_rate: f64, tick_rate: f64, } impl EventPool { pub fn new(frame_rate: f64, tick_rate: f64) -> Self { - let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel(); + let (event_tx, event_rx) = std::sync::mpsc::channel(); EventPool { event_rx, @@ -75,7 +75,7 @@ impl EventPool { } #[cfg_attr(test, mutants::skip)] - pub async fn next(&mut self) -> Option { - self.event_rx.recv().await + pub fn next(&mut self) -> Option { + self.event_rx.recv().ok() } } diff --git a/hac-client/src/lib.rs b/hac-client/src/lib.rs index e26ad95..465b214 100755 --- a/hac-client/src/lib.rs +++ b/hac-client/src/lib.rs @@ -4,11 +4,10 @@ mod components; pub mod event_pool; pub mod pages; mod router; -pub mod screen_manager; pub mod utils; +use std::cell::RefCell; use std::rc::Rc; -use std::sync::RwLock; use hac_colors::Colors; use hac_config::Config; @@ -16,5 +15,5 @@ use hac_config::Config; pub static MIN_WIDTH: u16 = 80; pub static MIN_HEIGHT: u16 = 30; -pub type HacConfig = Rc>; +pub type HacConfig = Rc>; pub type HacColors = Rc; diff --git a/hac-client/src/main.rs b/hac-client/src/main.rs index e4521fd..eae30b2 100755 --- a/hac-client/src/main.rs +++ b/hac-client/src/main.rs @@ -49,9 +49,10 @@ async fn main() -> anyhow::Result<()> { let collections = hac_loader::collection_loader::collections_metadata()?; let colors = hac_colors::Colors::default(); - let config = hac_config::load_config(); + let mut config = hac_config::load_config(); + config.dry_run = dry_run; - let mut app = app::App::new(collections, Rc::new(RwLock::new(config)), Rc::new(colors))?; + let mut app = app::App::new(collections, Rc::new(RefCell::new(config)), Rc::new(colors))?; app.run().await?; Ok(()) diff --git a/hac-client/src/pages.rs b/hac-client/src/pages.rs index 246ee03..97d7d64 100755 --- a/hac-client/src/pages.rs +++ b/hac-client/src/pages.rs @@ -1,8 +1,5 @@ -pub mod collection_dashboard; pub mod collection_list; pub mod collection_viewer; -pub mod confirm_popup; -pub mod error_popup; pub mod input; pub mod overlay; mod spinner; @@ -16,16 +13,18 @@ use crossterm::event::KeyEvent; use hac_core::command::Command; use ratatui::{layout::Rect, Frame}; -/// A `Page` is anything that is a top level page and can be drawn to the screen +/// A `Renderable` is anything that is a top level page and can be drawn to the screen pub trait Renderable { #[allow(unused_variables)] fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { Ok(()) } + /// when navigating to a new route, you might need to pass in some data, here you do it #[allow(unused_variables)] fn update(&mut self, data: Option>) {} + /// each route can, (and must) have a navigator, this is how you attach one #[allow(unused_variables)] fn attach_navigator(&mut self, navigator: std::sync::mpsc::Sender) {} diff --git a/hac-client/src/pages/collection_dashboard/collection_dashboard.rs b/hac-client/src/pages/collection_dashboard/collection_dashboard.rs deleted file mode 100755 index 141693b..0000000 --- a/hac-client/src/pages/collection_dashboard/collection_dashboard.rs +++ /dev/null @@ -1,1156 +0,0 @@ -use std::fmt::Debug; -use std::hash::Hash; -use std::ops::{Add, Div, Not, Sub}; -use std::sync::mpsc::Sender; - -use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; -use hac_core::command::Command; -use hac_loader::collection_loader::{CollectionMeta, ReadableByteSize}; -use hac_store::collection::Collection; -use ratatui::layout::{Alignment, Constraint, Direction, Flex, Layout, Rect}; -use ratatui::style::{Style, Stylize}; -use ratatui::text::Line; -use ratatui::widgets::{Block, BorderType, Borders, Clear, Padding, Paragraph, StatefulWidget, Widget, Wrap}; -use ratatui::Frame; - -use crate::pages::collection_dashboard::collection_list::{CollectionList, CollectionListState}; -use crate::pages::collection_dashboard::new_collection_form::{FormFocus, FormState, NewCollectionForm}; -use crate::pages::confirm_popup::ConfirmPopup; -use crate::pages::error_popup::ErrorPopup; -use crate::pages::overlay::{draw_overlay_old, make_overlay_old}; -use crate::pages::{Eventful, Renderable}; -use crate::router::{EventfulRenderable, Navigate}; - -#[derive(Debug, PartialEq)] -struct DashboardLayout { - collections_pane: Rect, - hint_pane: Rect, - help_popup: Rect, - title_pane: Rect, - confirm_popup: Rect, - form_popup: Rect, - error_popup: Rect, -} - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -enum SortingKind { - #[default] - Recent, - Name, - Size, -} - -impl SortingKind { - pub fn next(&self) -> Self { - match self { - SortingKind::Recent => SortingKind::Name, - SortingKind::Name => SortingKind::Size, - SortingKind::Size => SortingKind::Recent, - } - } - - pub fn prev(&self) -> Self { - match self { - SortingKind::Recent => SortingKind::Size, - SortingKind::Name => SortingKind::Recent, - SortingKind::Size => SortingKind::Name, - } - } -} - -#[derive(Debug)] -pub struct CollectionDashboard<'a> { - colors: hac_colors::Colors, - collections: Vec, - sorting_kind: SortingKind, - selected: usize, - scroll: usize, - navigator: Option>, - - layout: DashboardLayout, - form_state: FormState, - filter: String, - pane_focus: PaneFocus, - pub command_sender: Option>, - error_message: String, - dry_run: bool, - _a: std::marker::PhantomData<&'a ()>, -} - -#[derive(Debug, PartialEq, Eq)] -enum PaneFocus { - List, - Form, - Error, - Prompt, - Help, - Filter, -} - -impl<'a> CollectionDashboard<'a> { - const LIST_ITEM_HEIGHT: u16 = 3; - - pub fn new(size: Rect, colors: hac_colors::Colors, collections: Vec, dry_run: bool) -> Self { - CollectionDashboard { - collections, - colors, - selected: 0, - scroll: 0, - sorting_kind: SortingKind::default(), - navigator: None, - - form_state: FormState::default(), - layout: build_layout(size), - filter: String::new(), - command_sender: None, - error_message: String::default(), - pane_focus: PaneFocus::List, - dry_run, - _a: std::marker::PhantomData, - } - } - - fn sort_list(&mut self) { - match self.sorting_kind { - SortingKind::Recent => self.collections.sort_by(|a, b| a.modified().cmp(b.modified())), - SortingKind::Name => self.collections.sort_by(|a, b| a.name().cmp(b.name())), - SortingKind::Size => self.collections.sort_by_key(|a| std::cmp::Reverse(a.size())), - } - } - - fn draw_title(&self, frame: &mut Frame) -> anyhow::Result<()> { - let selected_style = |kind: SortingKind| { - if kind == self.sorting_kind { - self.colors.bright.blue - } else { - self.colors.bright.black - } - }; - - let title = " HAC ".bg(self.colors.normal.red).fg(self.colors.normal.white); - let sorting = vec![ - "Most recent".fg(selected_style(SortingKind::Recent)), - " ❘ ".fg(self.colors.bright.black), - "By name".fg(selected_style(SortingKind::Name)), - " ❘ ".fg(self.colors.bright.black), - "By size".fg(selected_style(SortingKind::Size)), - ]; - - let lines = vec![Line::from(title), "".into(), Line::from(sorting)]; - frame.render_widget(Paragraph::new(lines), self.layout.title_pane); - - Ok(()) - } - - fn max_items_onscreen(&self) -> usize { - (self.layout.collections_pane.height / Self::LIST_ITEM_HEIGHT).into() - } - - fn draw_collection_list(&mut self, frame: &mut Frame) -> anyhow::Result<()> { - let layout = self.layout.collections_pane; - let max_items = self.max_items_onscreen(); - - for (ref mut idx, (i, item)) in self - .collections - .iter() - .enumerate() - .skip(self.scroll) - .take(max_items) - .enumerate() - { - let selected = i == self.selected; - - let item = match selected { - true => Paragraph::new(vec![ - Line::from(item.name().to_string().fg(self.colors.bright.red)), - Line::from(vec![ - item.modified().to_string().fg(self.colors.bright.black), - " - ".fg(self.colors.bright.black), - item.size().readable_byte_size().fg(self.colors.bright.black), - ]), - ]) - .block( - Block::new() - .borders(Borders::LEFT) - .fg(self.colors.bright.red) - .padding(Padding::left(1)), - ), - false => Paragraph::new(vec![ - Line::from(item.name().to_string().fg(self.colors.normal.white)), - Line::from(vec![ - item.modified().to_string().fg(self.colors.bright.black), - " - ".fg(self.colors.bright.black), - item.size().readable_byte_size().fg(self.colors.bright.black), - ]), - ]) - .block(Block::new().padding(Padding::left(2))), - }; - - let size = Rect::new(layout.x, layout.y + (*idx as u16 * 3), layout.width, 2); - frame.render_widget(item, size); - *idx += 1; - } - - Ok(()) - } - - fn draw_hint_text(&self, frame: &mut Frame) { - let hint = vec![ - "j/k} ↑/↓".fg(self.colors.normal.green), - " - choose • ".fg(self.colors.bright.black), - "n".fg(self.colors.normal.green), - " - new • ".fg(self.colors.bright.black), - "enter".fg(self.colors.normal.green), - " - select • ".fg(self.colors.bright.black), - "?".fg(self.colors.normal.green), - " - show more • ".fg(self.colors.bright.black), - "ctrl c".fg(self.colors.normal.green), - " - quit".fg(self.colors.bright.black), - ]; - - frame.render_widget(Line::from(hint), self.layout.hint_pane); - } - - fn draw_background(&self, size: Rect, frame: &mut Frame) { - frame.render_widget(Clear, size); - frame.render_widget(Block::default().bg(self.colors.primary.background), size); - } - - fn maybe_scroll_list(&mut self) { - if self.scroll > self.selected { - let offset = self.scroll - self.selected; - self.scroll -= offset; - return; - } - - let max_items = self.max_items_onscreen() - 1; - let normalized = self.selected - self.scroll; - if normalized >= max_items { - let offset = self.selected - max_items; - self.scroll = offset; - } - } - - //pub fn display_error(&mut self, message: String) { - // self.pane_focus = PaneFocus::Error; - // self.error_message = message; - //} - // - //fn filter_list(&mut self) { - // self.list_state.set_items( - // self.collections - // .clone() - // .into_iter() - // .filter(|s| s.info.name.contains(&self.filter)) - // .collect(), - // ); - // self.list_state.select(None); - //} - // - //fn handle_filter_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - // match (key_event.code, key_event.modifiers) { - // (KeyCode::Char('c'), KeyModifiers::CONTROL) | (KeyCode::Esc, _) => { - // self.pane_focus = PaneFocus::List; - // self.filter = String::new(); - // self.filter_list(); - // } - // (KeyCode::Backspace, _) => { - // if self.filter.is_empty() { - // self.pane_focus = PaneFocus::List; - // } - // self.filter.pop(); - // self.filter_list(); - // } - // (KeyCode::Enter, _) => { - // self.pane_focus = PaneFocus::List; - // self.filter_list(); - // } - // (KeyCode::Char(c), _) => { - // self.filter.push(c); - // self.filter_list(); - // } - // _ => {} - // }; - // - // Ok(None) - //} - // - //fn handle_list_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - // match key_event.code { - // KeyCode::Enter => { - // return Ok(self - // .list_state - // .items - // .is_empty() - // .not() - // .then(|| { - // self.list_state - // .selected() - // .and_then(|i| self.collections.get(i)) - // .expect( - // "user should never be allowed to select a non existing collection", - // ) - // }) - // .map(|collection| { - // tracing::debug!("selected collection: {}", collection.info.name); - // Command::SelectCollection(collection.clone()) - // })); - // } - // KeyCode::Char('d') => { - // if self.list_state.selected().is_some() { - // self.pane_focus = PaneFocus::Prompt; - // } - // } - // KeyCode::Char('n') | KeyCode::Char('c') => { - // self.pane_focus = PaneFocus::Form; - // } - // KeyCode::Char('h') | KeyCode::Left => { - // if !self.list_state.items.is_empty() { - // self.list_state.select( - // self.list_state - // .selected() - // .map(|i| i.saturating_sub(1)) - // .or(Some(0)), - // ); - // } - // } - // KeyCode::Char('j') | KeyCode::Down => { - // if !self.list_state.items.is_empty() { - // self.list_state.select( - // self.list_state - // .selected() - // .map(|i| { - // usize::min( - // self.list_state.items.len() - 1, - // i + self.list.items_per_row(&self.layout.collections_pane), - // ) - // }) - // .or(Some(0)), - // ); - // } - // } - // KeyCode::Char('k') | KeyCode::Up => { - // if !self.list_state.items.is_empty() { - // self.list_state.select( - // self.list_state - // .selected() - // .map(|i| { - // i.saturating_sub( - // self.list.items_per_row(&self.layout.collections_pane), - // ) - // }) - // .or(Some(0)), - // ); - // } - // } - // KeyCode::Char('l') | KeyCode::Right => { - // if !self.list_state.items.is_empty() { - // self.list_state.select( - // self.list_state - // .selected() - // .map(|i| usize::min(self.list_state.items.len().sub(1), i.add(1))) - // .or(Some(0)), - // ); - // } - // } - // KeyCode::Char('?') => self.pane_focus = PaneFocus::Help, - // KeyCode::Char('/') => self.pane_focus = PaneFocus::Filter, - // _ => {} - // }; - // Ok(None) - //} - // - //fn handle_form_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - // match (key_event.code, key_event.modifiers) { - // (KeyCode::Tab, _) => match self.form_state.focused_field { - // FormFocus::Name => self.form_state.focused_field = FormFocus::Description, - // FormFocus::Description => self.form_state.focused_field = FormFocus::Confirm, - // FormFocus::Confirm => self.form_state.focused_field = FormFocus::Cancel, - // FormFocus::Cancel => self.form_state.focused_field = FormFocus::Name, - // }, - // (KeyCode::Char(c), _) => match self.form_state.focused_field { - // FormFocus::Name => self.form_state.name.push(c), - // FormFocus::Description => self.form_state.description.push(c), - // _ => {} - // }, - // (KeyCode::Enter, _) => match self.form_state.focused_field { - // FormFocus::Confirm => { - // let name = self.form_state.name.clone(); - // let description = self.form_state.description.clone(); - // - // let sender_copy = self - // .command_sender - // .clone() - // .expect("should always have a sender at this point"); - // - // let dry_run = self.dry_run; - // - // tokio::spawn(async move { - // match hac_core::fs::create_collection(name, description, dry_run).await { - // Ok(collection) => { - // if sender_copy - // .send(Command::CreateCollection(collection)) - // .is_err() - // { - // tracing::error!("failed to send command through channel"); - // std::process::abort(); - // } - // } - // Err(e) => { - // if sender_copy.send(Command::Error(e.to_string())).is_err() { - // tracing::error!("failed to send error command through channel"); - // std::process::abort(); - // } - // } - // } - // }); - // } - // FormFocus::Cancel => { - // self.pane_focus = PaneFocus::List; - // self.form_state.reset(); - // } - // _ => {} - // }, - // (KeyCode::Backspace, _) => match self.form_state.focused_field { - // FormFocus::Name => { - // self.form_state.name.pop(); - // } - // FormFocus::Description => { - // self.form_state.description.pop(); - // } - // _ => {} - // }, - // _ => {} - // } - // Ok(None) - //} - // - //#[tracing::instrument(skip_all)] - //fn handle_confirm_popup_key_event( - // &mut self, - // key_event: KeyEvent, - //) -> anyhow::Result> { - // match key_event.code { - // KeyCode::Char('y') => { - // let selected = self - // .list_state - // .selected() - // .expect("deleting when nothing is selected should never happen"); - // let collection = self - // .collections - // .get(selected) - // .expect("should never attempt to delete a non existing item"); - // let path = collection.path.clone(); - // - // if !self.dry_run { - // tokio::spawn(async move { - // tracing::debug!("attempting to delete collection: {:?}", path); - // hac_core::fs::delete_collection(&path) - // .await - // .expect("failed to delete collection from filesystem"); - // }); - // } - // - // self.collections.remove(selected); - // self.list_state.set_items(self.collections.clone()); - // self.list_state.select(None); - // self.pane_focus = PaneFocus::List; - // } - // KeyCode::Char('n') => { - // self.pane_focus = PaneFocus::List; - // } - // _ => {} - // }; - // - // Ok(None) - //} - // - //fn handle_error_popup_key_event( - // &mut self, - // key_event: KeyEvent, - //) -> anyhow::Result> { - // match key_event.code { - // KeyCode::Char('o') | KeyCode::Esc | KeyCode::Enter => { - // self.pane_focus = PaneFocus::List; - // } - // _ => {} - // }; - // - // Ok(None) - //} - // - // - //fn draw_help_popup(&self, frame: &mut Frame) { - // make_overlay(self.colors, self.colors.primary.background, 0.2, frame); - // - // let lines = vec![ - // Line::from(vec![ - // "h/".fg(self.colors.bright.magenta), - // " - select left item".into(), - // ]), - // Line::from(vec![ - // "j/".fg(self.colors.bright.magenta), - // " - select item below".into(), - // ]), - // Line::from(vec![ - // "k/".fg(self.colors.bright.magenta), - // " - select item above".into(), - // ]), - // Line::from(vec![ - // "l/".fg(self.colors.bright.magenta), - // " - select right item".into(), - // ]), - // Line::from(vec![ - // "n/c".fg(self.colors.bright.magenta), - // " - creates a new collection".into(), - // ]), - // Line::from(vec![ - // "d".fg(self.colors.bright.magenta), - // " - deletes the selected collection".into(), - // ]), - // Line::from(vec![ - // "?".fg(self.colors.bright.magenta), - // " - toggle this help window".into(), - // ]), - // Line::from(vec![ - // "enter".fg(self.colors.bright.magenta), - // " - select item under cursor".into(), - // ]), - // Line::from(vec![ - // "/".fg(self.colors.bright.magenta), - // " - enter filter mode".into(), - // ]), - // Line::from(vec![ - // "".fg(self.colors.bright.magenta), - // " - quits the application".into(), - // ]), - // Line::from(""), - // Line::from("press any key to go back".fg(self.colors.normal.magenta)).centered(), - // ]; - // - // frame.render_widget( - // Paragraph::new(lines) - // .wrap(Wrap { trim: true }) - // .block(Block::default().padding(Padding::new(2, 2, 1, 1))), - // self.layout.help_popup, - // ); - //} - // - //fn draw_filter_prompt(&self, frame: &mut Frame) { - // let filter = Line::from(format!("/{}", self.filter)); - // frame.render_widget(filter, self.layout.hint_pane); - //} - // - // - //fn draw_no_matches_text(&self, frame: &mut Frame) -> anyhow::Result<()> { - // let layout = Layout::default() - // .direction(Direction::Vertical) - // .constraints([ - // Constraint::Fill(1), - // Constraint::Length(8), - // Constraint::Fill(1), - // ]) - // .split(self.layout.collections_pane)[1]; - // - // let no_matches = BigText::builder() - // .pixel_size(PixelSize::Quadrant) - // .style(Style::default().fg(self.colors.normal.magenta)) - // .lines(vec!["No matches".into()]) - // .alignment(Alignment::Center) - // .build()?; - // - // frame.render_widget(no_matches, layout); - // - // Ok(()) - //} - // - //fn draw_empty_message(&self, frame: &mut Frame) -> anyhow::Result<()> { - // let size = Layout::default() - // .direction(Direction::Vertical) - // .flex(Flex::Center) - // .constraints([ - // Constraint::Fill(1), - // Constraint::Length(8), - // Constraint::Fill(1), - // ]) - // .split(self.layout.collections_pane)[1]; - // - // let empty_message = BigText::builder() - // .pixel_size(PixelSize::Quadrant) - // .style(Style::default().fg(self.colors.normal.magenta)) - // .lines(vec!["No collections".into()]) - // .alignment(Alignment::Center) - // .build()?; - // - // frame.render_widget(empty_message, size); - // - // Ok(()) - //} - // - // - //fn draw_error_popup(&self, frame: &mut Frame) { - // let popup = ErrorPopup::new(self.error_message.clone(), self.colors); - // popup.render(self.layout.error_popup, frame.buffer_mut()); - //} - // - //fn draw_form_popup(&mut self, size: Rect, frame: &mut Frame) { - // self.draw_background(size, frame); - // draw_overlay(self.colors, size, "新", frame); - // - // let form = NewCollectionForm::new(self.colors); - // form.render( - // self.layout.form_popup, - // frame.buffer_mut(), - // &mut self.form_state, - // ); - //} - // - //fn draw_delete_prompt(&self, frame: &mut Frame) { - // let selected_index = self - // .list_state - // .selected() - // .expect("attempted to open confirm popup without an item selected"); - // let selected_item_name = &self - // .collections - // .get(selected_index) - // .expect("should never be able to have an out of bounds selection") - // .info - // .name; - // - // let confirm_popup = ConfirmPopup::new( - // format!( - // "You really want to delete collection {}?", - // selected_item_name - // ), - // self.colors, - // ); - // confirm_popup.render(self.layout.confirm_popup, frame.buffer_mut()); - //} - // -} - -impl Renderable for CollectionDashboard<'_> { - fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { - self.draw_background(size, frame); - self.draw_title(frame)?; - - self.draw_collection_list(frame)?; - - //match ( - // self.collections.is_empty(), - // self.list_state.items.is_empty(), - //) { - // (false, false) => self.draw_collection_list(frame), - // (false, true) => self.draw_no_matches_text(frame)?, - // (true, true) => self.draw_empty_message(frame)?, - // (true, false) => unreachable!(), - //} - // - match self.pane_focus { - //PaneFocus::Error => self.draw_error_popup(frame), - //PaneFocus::Form => self.draw_form_popup(size, frame), - //PaneFocus::Filter => self.draw_filter_prompt(frame), - //PaneFocus::Help => self.draw_help_popup(frame), - //PaneFocus::Prompt => self.draw_delete_prompt(frame), - PaneFocus::List => self.draw_hint_text(frame), - _ => todo!(), - } - - Ok(()) - } - - fn register_command_handler(&mut self, sender: Sender) { - self.command_sender = Some(sender.clone()); - } - - fn resize(&mut self, new_size: Rect) { - self.layout = build_layout(new_size); - } -} - -impl Eventful for CollectionDashboard<'_> { - type Result = Command; - - fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - return Ok(Some(Command::Quit)); - }; - - match key_event.code { - KeyCode::Char('j') | KeyCode::Down => { - self.selected = usize::min(self.selected + 1, self.collections.len() - 1); - self.maybe_scroll_list(); - } - KeyCode::Char('k') | KeyCode::Up => { - self.selected = self.selected.saturating_sub(1); - self.maybe_scroll_list(); - } - KeyCode::Tab => { - self.sorting_kind = self.sorting_kind.next(); - self.sort_list(); - } - KeyCode::BackTab => { - self.sorting_kind = self.sorting_kind.prev(); - self.sort_list(); - } - _ => {} - } - - //match self.pane_focus { - // PaneFocus::List => self.handle_list_key_event(key_event), - // PaneFocus::Form => self.handle_form_key_event(key_event), - // PaneFocus::Error => self.handle_error_popup_key_event(key_event), - // PaneFocus::Prompt => self.handle_confirm_popup_key_event(key_event), - // PaneFocus::Filter => self.handle_filter_key_event(key_event), - // PaneFocus::Help => { - // self.pane_focus = PaneFocus::List; - // Ok(None) - // } - //} - - Ok(None) - } -} - -fn build_layout(size: Rect) -> DashboardLayout { - let size = Rect::new(size.x + 1, size.y, size.width - 1, size.height); - let [top, help_pane] = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Fill(1), Constraint::Length(1)]) - .areas(size); - - let [_, title_pane, _, collections_pane] = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(1), - Constraint::Length(3), - Constraint::Length(1), - Constraint::Fill(1), - ]) - .areas(top); - - let help_popup = Rect::new( - size.width.div(2).saturating_sub(25), - size.height.div(2).saturating_sub(7), - 50, - 14, - ); - let confirm_popup = Rect::new( - size.width.div(4), - size.height.div(2).saturating_sub(4), - size.width.div(2), - 8, - ); - let form_popup = Rect::new( - size.width.div(4), - size.height.div(2).saturating_sub(7), - size.width.div(2), - 14, - ); - let error_popup = Rect::new( - size.width.div(4), - size.height.div(2).saturating_sub(10), - size.width.div(2), - 20, - ); - - DashboardLayout { - collections_pane, - hint_pane: help_pane, - title_pane, - help_popup, - confirm_popup, - form_popup, - error_popup, - } -} - -//#[cfg(test)] -//mod tests { -// use hac_core::collection; -// use ratatui::{backend::TestBackend, buffer::Cell, Terminal}; -// use std::{ -// fs::{create_dir, File}, -// io::Write, -// }; -// use tempfile::{tempdir, TempDir}; -// -// use super::*; -// -// fn setup_temp_collections(amount: usize) -> (TempDir, String) { -// let tmp_data_dir = tempdir().expect("Failed to create temp data dir"); -// -// let tmp_dir = tmp_data_dir.path().join("collections"); -// create_dir(&tmp_dir).expect("Failed to create collections directory"); -// -// for i in 0..amount { -// let file_path = tmp_dir.join(format!("test_collection_{}.json", i)); -// let mut tmp_file = File::create(&file_path).expect("Failed to create file"); -// -// write!( -// tmp_file, -// r#"{{"info": {{ "name": "test_collection_{}", "description": "test_description_{}" }}}}"#, -// i, i -// ).expect("Failed to write to file"); -// -// tmp_file.flush().expect("Failed to flush file"); -// } -// -// (tmp_data_dir, tmp_dir.to_string_lossy().to_string()) -// } -// -// fn feed_keys(dashboard: &mut CollectionDashboard, events: &[KeyEvent]) { -// for event in events { -// _ = dashboard.handle_key_event(*event); -// } -// } -// -// #[test] -// fn test_build_layout() { -// let size = Rect::new(0, 0, 80, 24); -// let expected = DashboardLayout { -// collections_pane: Rect::new(1, 6, 79, 17), -// hint_pane: Rect::new(1, 23, 79, 1), -// title_pane: Rect::new(1, 1, 79, 5), -// help_popup: Rect::new(14, 5, 50, 14), -// confirm_popup: Rect::new(19, 8, 39, 8), -// form_popup: Rect::new(19, 5, 39, 14), -// error_popup: Rect::new(19, 2, 39, 20), -// }; -// -// let layout = build_layout(size); -// -// assert_eq!(layout, expected); -// } -// -// #[test] -// fn test_open_close_help() { -// let size = Rect::new(0, 0, 80, 24); -// let colors = hac_colors::Colors::default(); -// let (_guard, path) = setup_temp_collections(1); -// let collection = collection::get_collections(path).unwrap(); -// -// let mut dashboard = CollectionDashboard::new(size, &colors, collection, false).unwrap(); -// -// assert_eq!(dashboard.collections.len(), 1); -// assert_eq!(dashboard.list_state.selected(), Some(0)); -// -// assert_eq!(dashboard.pane_focus, PaneFocus::List); -// -// feed_keys( -// &mut dashboard, -// &[KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE)], -// ); -// -// assert_eq!(dashboard.pane_focus, PaneFocus::Help); -// -// feed_keys( -// &mut dashboard, -// &[KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE)], -// ); -// -// assert_eq!(dashboard.pane_focus, PaneFocus::List); -// } -// -// #[test] -// fn test_actions_without_any_collections() { -// let size = Rect::new(0, 0, 80, 24); -// let colors = hac_colors::Colors::default(); -// let mut dashboard = CollectionDashboard::new(size, &colors, vec![], false).unwrap(); -// -// assert!(dashboard.collections.is_empty()); -// assert_eq!(dashboard.list_state.selected(), None); -// -// feed_keys( -// &mut dashboard, -// &[ -// KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('k'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE), -// ], -// ); -// -// assert!(dashboard.collections.is_empty()); -// assert_eq!(dashboard.list_state.selected(), None); -// } -// -// #[test] -// fn test_filtering_list() { -// let size = Rect::new(0, 0, 80, 24); -// let colors = hac_colors::Colors::default(); -// let (_guard, path) = setup_temp_collections(10); -// let collections = collection::get_collections(path).unwrap(); -// -// let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); -// -// assert_eq!(dashboard.collections.len(), 10); -// assert_eq!(dashboard.list_state.selected(), Some(0)); -// -// feed_keys( -// &mut dashboard, -// &[ -// // enter filtering - search for non-existing item -// KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE), -// ], -// ); -// -// assert_eq!(dashboard.pane_focus, PaneFocus::Filter); -// assert_eq!(dashboard.list_state.items.len(), 0); -// -// feed_keys( -// &mut dashboard, -// &[ -// // erase until filtering is cancelled -// KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), -// ], -// ); -// -// assert_eq!(dashboard.pane_focus, PaneFocus::List); -// assert_eq!(dashboard.list_state.items.len(), 10); -// -// feed_keys( -// &mut dashboard, -// &[ -// // enter filtering again and cancel with hotkey -// KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), -// KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE), -// ], -// ); -// -// assert_eq!(dashboard.pane_focus, PaneFocus::List); -// assert_eq!(dashboard.list_state.items.len(), 10); -// -// feed_keys( -// &mut dashboard, -// &[ -// // enter filtering again and actually filter the list -// KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), -// ], -// ); -// -// assert_eq!(dashboard.pane_focus, PaneFocus::List); -// assert_eq!(dashboard.list_state.items.len(), 1); -// } -// -// #[test] -// fn test_moving_out_of_bounds() { -// let size = Rect::new(0, 0, 80, 24); -// let colors = hac_colors::Colors::default(); -// let (_guard, path) = setup_temp_collections(3); -// let collections = collection::get_collections(path).unwrap(); -// -// let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); -// -// feed_keys( -// &mut dashboard, -// &[ -// // moving down until end is reached, twice more -// KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE), -// ], -// ); -// -// assert_eq!(dashboard.list_state.selected(), Some(2)); -// -// feed_keys( -// &mut dashboard, -// &[ -// // moving right until beginning is reached, twice more -// KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), -// ], -// ); -// -// assert_eq!(dashboard.list_state.selected(), Some(0)); -// } -// -// #[test] -// fn test_creating_new_collections() { -// let size = Rect::new(0, 0, 80, 24); -// let colors = hac_colors::Colors::default(); -// let (_guard, path) = setup_temp_collections(3); -// let collections = collection::get_collections(path).unwrap(); -// -// let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); -// -// feed_keys( -// &mut dashboard, -// &[KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE)], -// ); -// -// assert_eq!(dashboard.pane_focus, PaneFocus::Form); -// -// feed_keys( -// &mut dashboard, -// &[ -// // going to the cancel button, and closing the form, typing something to ensure -// // state reset -// KeyEvent::new(KeyCode::Char('h'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), -// ], -// ); -// -// assert_eq!(dashboard.pane_focus, PaneFocus::List); -// -// feed_keys( -// &mut dashboard, -// &[KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE)], -// ); -// -// assert_eq!(dashboard.pane_focus, PaneFocus::Form); -// -// feed_keys( -// &mut dashboard, -// &[ -// // filling in the name field -// KeyEvent::new(KeyCode::Char('H'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE), -// ], -// ); -// -// assert_eq!(dashboard.form_state.name, "Hello"); -// -// feed_keys( -// &mut dashboard, -// &[ -// // filling in the description -// KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('W'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('l'), KeyModifiers::NONE), -// KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE), -// ], -// ); -// -// assert_eq!(dashboard.form_state.name, "Hello"); -// assert_eq!(dashboard.form_state.description, "World"); -// } -// -// #[test] -// fn test_prompt_delete_collections() { -// let size = Rect::new(0, 0, 80, 24); -// let colors = hac_colors::Colors::default(); -// let (_guard, path) = setup_temp_collections(3); -// let collections = collection::get_collections(path).unwrap(); -// let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); -// -// feed_keys( -// &mut dashboard, -// &[KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE)], -// ); -// -// assert_eq!(dashboard.pane_focus, PaneFocus::Prompt); -// -// feed_keys( -// &mut dashboard, -// &[KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE)], -// ); -// -// assert_eq!(dashboard.pane_focus, PaneFocus::List); -// } -// -// #[test] -// fn test_display_error() { -// let size = Rect::new(0, 0, 80, 24); -// let colors = hac_colors::Colors::default(); -// let mut dashboard = CollectionDashboard::new(size, &colors, vec![], false).unwrap(); -// -// dashboard.display_error("any error message".into()); -// -// assert_eq!(dashboard.pane_focus, PaneFocus::Error); -// assert_eq!(dashboard.error_message, "any error message"); -// } -// -// #[test] -// fn test_draw_background() { -// let colors = hac_colors::Colors::default(); -// let size = Rect::new(0, 0, 80, 22); -// let dashboard = CollectionDashboard::new(size, &colors, vec![], false).unwrap(); -// -// let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); -// let mut frame = terminal.get_frame(); -// -// for cell in frame.buffer_mut().content.iter() { -// assert_eq!(cell, &Cell::default()); -// } -// -// dashboard.draw_background(size, &mut frame); -// -// for cell in frame.buffer_mut().content.iter() { -// assert_eq!(cell.bg, colors.primary.background); -// } -// } -// -// #[test] -// fn test_close_error_popup() { -// let colors = hac_colors::Colors::default(); -// let size = Rect::new(0, 0, 80, 22); -// let (_guard, path) = setup_temp_collections(3); -// let collections = collection::get_collections(path).unwrap(); -// let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); -// -// dashboard.display_error("any_error_message".into()); -// assert_eq!(dashboard.pane_focus, PaneFocus::Error); -// feed_keys( -// &mut dashboard, -// &[KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE)], -// ); -// -// assert_eq!(dashboard.pane_focus, PaneFocus::List); -// } -// -// #[test] -// fn test_resizing() { -// let colors = hac_colors::Colors::default(); -// let size = Rect::new(0, 0, 80, 22); -// let new_size = Rect::new(0, 0, 80, 24); -// let (_guard, path) = setup_temp_collections(3); -// let collections = collection::get_collections(path).unwrap(); -// let mut dashboard = CollectionDashboard::new(size, &colors, collections, false).unwrap(); -// let expected = DashboardLayout { -// collections_pane: Rect::new(1, 6, 79, 17), -// hint_pane: Rect::new(1, 23, 79, 1), -// title_pane: Rect::new(1, 1, 79, 5), -// help_popup: Rect::new(14, 5, 50, 14), -// confirm_popup: Rect::new(19, 8, 39, 8), -// form_popup: Rect::new(19, 5, 39, 14), -// error_popup: Rect::new(19, 2, 39, 20), -// }; -// -// dashboard.resize(new_size); -// assert_eq!(dashboard.layout, expected); -// } -//} diff --git a/hac-client/src/pages/collection_dashboard/collection_list.rs b/hac-client/src/pages/collection_dashboard/collection_list.rs deleted file mode 100755 index 15b1af5..0000000 --- a/hac-client/src/pages/collection_dashboard/collection_list.rs +++ /dev/null @@ -1,279 +0,0 @@ -use hac_store::collection::Collection; - -use std::collections::VecDeque; -use std::ops::{Add, Div, Mul}; - -use ratatui::buffer::Buffer; -use ratatui::layout::{Constraint, Direction, Flex, Layout, Rect}; -use ratatui::style::{Style, Stylize}; -use ratatui::widgets::{ - Block, BorderType, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, - StatefulWidget, Widget, -}; - -#[derive(Debug)] -pub struct CollectionListState<'a> { - selected: Option, - pub(super) items: &'a [Collection], - scroll: usize, -} - -impl<'a> CollectionListState<'a> { - pub fn new(items: &'a [Collection]) -> Self { - CollectionListState { - selected: None, - items, - scroll: 0, - } - } - - pub fn select(&mut self, index: Option) { - self.selected = index; - } - - pub fn selected(&self) -> Option { - self.selected - } - - pub fn set_items(&mut self, items: &'a [Collection]) { - self.items = items; - } -} - -#[derive(Debug, Clone)] -pub struct CollectionList<'cl> { - colors: &'cl hac_colors::Colors, - min_col_width: u16, - row_height: u16, -} - -impl<'a> CollectionList<'a> { - pub fn new(colors: &'a hac_colors::Colors) -> Self { - CollectionList { - colors, - min_col_width: 30, - row_height: 4, - } - } - - pub fn items_per_row(&self, size: &Rect) -> usize { - (size.width.saturating_sub(1).div(self.min_col_width)).into() - } - - pub fn total_rows(&self, size: &Rect) -> usize { - (size.height.div(self.row_height)).into() - } - - fn build_layout(&self, size: &Rect) -> VecDeque { - let total_rows = self.total_rows(size); - let items_per_row = self.items_per_row(size); - - (0..total_rows) - .flat_map(|row| { - Layout::default() - .direction(Direction::Horizontal) - .flex(Flex::SpaceAround) - .constraints((0..items_per_row).map(|_| Constraint::Min(self.min_col_width))) - .split(Rect::new( - size.x, - size.y + (self.row_height.mul(row as u16)), - size.width, - self.row_height, - )) - .to_vec() - }) - .collect::>() - } - - //fn build_card( - // &self, - // state: &CollectionListState, - // collection: &Collection, - // index: usize, - //) -> Paragraph<'_> { - //let lines = vec![ - // collection - // .info - // .name - // .clone() - // .fg(self.colors.normal.white) - // .into(), - // collection - // .info - // .description - // .clone() - // .unwrap_or_default() - // .fg(self.colors.bright.yellow) - // .into(), - //]; - // - //let border_color = if state - // .selected - // .is_some_and(|selected| selected.eq(&(index.add(state.scroll)))) - //{ - // self.colors.bright.magenta - //} else { - // self.colors.primary.hover - //}; - // - //Paragraph::new(lines).block( - // Block::default() - // .borders(Borders::ALL) - // .border_type(BorderType::Rounded) - // .border_style(Style::default().fg(border_color)), - //) - //} -} - -//impl StatefulWidget for CollectionList<'_> { -// type State = CollectionListState; -// -// fn render(self, size: Rect, buf: &mut Buffer, state: &mut Self::State) { -// let list_size = Rect::new(size.x, size.y, size.width.saturating_sub(3), size.height); -// let scrollbar_size = Rect::new(size.width.saturating_sub(1), size.y, 1, size.height); -// let mut rects = self.build_layout(&list_size); -// -// let mut scrollbar_state = -// ScrollbarState::new(state.items.len().div(self.items_per_row(&size))) -// .position(state.scroll); -// -// let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) -// .style(Style::default().fg(self.colors.normal.magenta)) -// .begin_symbol(Some("↑")) -// .end_symbol(Some("↓")); -// -// let items_on_display = self -// .items_per_row(&list_size) -// .mul(self.total_rows(&list_size)); -// if let Some(index) = state.selected { -// index -// .gt(&items_on_display.saturating_sub(1).add(state.scroll)) -// .then(|| state.scroll = state.scroll.add(self.items_per_row(&list_size))); -// -// state.scroll.gt(&0).then(|| { -// index -// .add(1) -// .saturating_sub(state.scroll) -// .eq(&0) -// .then(|| state.scroll = state.scroll.saturating_sub(self.items_per_row(&size))); -// }); -// }; -// -// state -// .items -// .iter() -// .skip(state.scroll) -// .take(rects.len()) -// .enumerate() -// .map(|(i, collection)| self.build_card(state, collection, i)) -// .for_each(|card| card.render(rects.pop_front().unwrap(), buf)); -// -// scrollbar.render(scrollbar_size, buf, &mut scrollbar_state); -// } -//} - -//#[cfg(test)] -//mod tests { -// use std::ops::Sub; -// -// use super::*; -// use hac_core::collection::types::*; -// use ratatui::{backend::TestBackend, buffer::Cell, Terminal}; -// -// fn sample_collection() -> Collection { -// Collection { -// info: Info { -// name: String::from("any_name"), -// description: None, -// }, -// path: "any_path".into(), -// requests: None, -// } -// } -// -// #[test] -// fn test_build_layout() { -// let colors = hac_colors::Colors::default(); -// let collection_list = CollectionList::new(&colors); -// let size = Rect::new(0, 0, 31, 10); -// -// let layout = collection_list.build_layout(&size); -// -// assert!(!layout.is_empty()); -// assert_eq!(layout.len(), 2); -// } -// -// #[test] -// fn test_items_per_row() { -// let colors = hac_colors::Colors::default(); -// let collection_list = CollectionList::new(&colors); -// let zero_items = Rect::new(0, 0, 30, 10); -// let one_item = Rect::new(0, 0, 31, 10); -// -// let amount = collection_list.items_per_row(&zero_items); -// assert_eq!(amount, 0); -// -// let amount = collection_list.items_per_row(&one_item); -// assert_eq!(amount, 1); -// } -// -// #[test] -// fn test_build_card() { -// let colors = hac_colors::Colors::default(); -// let collection_list = CollectionList::new(&colors); -// let collections = vec![Collection { -// info: Info { -// name: String::from("any_name"), -// description: None, -// }, -// path: "any_path".into(), -// requests: None, -// }]; -// let state = CollectionListState::new(collections.clone()); -// -// let lines = vec![ -// "any_name".fg(colors.normal.white).into(), -// "".fg(colors.bright.yellow).into(), -// ]; -// let expected = Paragraph::new(lines).block( -// Block::default() -// .borders(Borders::ALL) -// .border_type(BorderType::Rounded) -// .border_style(Style::default().fg(colors.primary.hover)), -// ); -// -// let card = collection_list.build_card(&state, &collections[0], 0); -// -// assert_eq!(card, expected); -// } -// -// #[test] -// fn test_rendering() { -// let colors = hac_colors::Colors::default(); -// let collections = (0..100).map(|_| sample_collection()).collect::>(); -// -// let backend = TestBackend::new(80, 22); -// let mut terminal = Terminal::new(backend).unwrap(); -// let size = terminal.size().unwrap(); -// let mut frame = terminal.get_frame(); -// -// let mut state = CollectionListState::new(collections.clone()); -// let collection_list = CollectionList::new(&colors); -// -// for cell in &frame.buffer_mut().content { -// assert_eq!(cell, &Cell::default()); -// } -// -// collection_list.render(size, frame.buffer_mut(), &mut state); -// -// for cell in frame -// .buffer_mut() -// .content -// .iter() -// .skip(size.width.sub(1).into()) -// .step_by(size.width.into()) -// { -// assert!(cell.symbol().ne(" ")); -// } -// } -//} diff --git a/hac-client/src/pages/collection_dashboard/mod.rs b/hac-client/src/pages/collection_dashboard/mod.rs deleted file mode 100755 index b9e9817..0000000 --- a/hac-client/src/pages/collection_dashboard/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[allow(clippy::module_inception)] -mod collection_dashboard; -mod collection_list; -mod new_collection_form; - -pub use collection_dashboard::CollectionDashboard; diff --git a/hac-client/src/pages/collection_dashboard/new_collection_form.rs b/hac-client/src/pages/collection_dashboard/new_collection_form.rs deleted file mode 100755 index 2529772..0000000 --- a/hac-client/src/pages/collection_dashboard/new_collection_form.rs +++ /dev/null @@ -1,156 +0,0 @@ -use crate::pages::input::Input; - -use ratatui::buffer::Buffer; -use ratatui::layout::{Constraint, Direction, Flex, Layout, Rect}; -use ratatui::style::{Style, Stylize}; -use ratatui::text::Line; -use ratatui::widgets::{ - Block, BorderType, Borders, Clear, Padding, Paragraph, StatefulWidget, Widget, -}; - -#[derive(Debug, Default, PartialEq, Eq)] -pub enum FormFocus { - #[default] - Name, - Description, - Confirm, - Cancel, -} - -struct FormLayout { - name_input: Rect, - desc_input: Rect, - confirm_button: Rect, - cancel_button: Rect, - hint: Rect, -} - -#[derive(Debug, Default)] -pub struct FormState { - pub name: String, - pub description: String, - pub focused_field: FormFocus, -} - -impl FormState { - pub fn reset(&mut self) { - self.name = String::default(); - self.description = String::default(); - self.focused_field = FormFocus::Name; - } -} - -#[derive(Debug)] -pub struct NewCollectionForm<'a> { - colors: &'a hac_colors::Colors, -} - -impl<'a> NewCollectionForm<'a> { - pub fn new(colors: &'a hac_colors::Colors) -> Self { - NewCollectionForm { colors } - } - - fn build_layout(&self, size: &Rect) -> FormLayout { - let size = Rect { - x: size.x + 2, - y: size.y + 1, - width: size.width.saturating_sub(4), - height: size.height.saturating_sub(2), - }; - let [name_input, desc_input, _, buttons, _, hint] = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(1), - Constraint::Length(3), - Constraint::Length(1), - Constraint::Length(1), - ]) - .areas(size); - - let [confirm_button, _, cancel_button] = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Length(10), - Constraint::Length(1), - Constraint::Length(10), - ]) - .flex(Flex::Center) - .areas(buttons); - - FormLayout { - name_input, - desc_input, - confirm_button, - cancel_button, - hint, - } - } -} - -impl StatefulWidget for NewCollectionForm<'_> { - type State = FormState; - - fn render(self, size: Rect, buf: &mut Buffer, state: &mut Self::State) { - let layout = self.build_layout(&size); - Clear.render(size, buf); - - let mut name_input = - Input::new(self.colors, "Name".into()).placeholder("My awesome API".into()); - - let mut desc_input = - Input::new(self.colors, "Description".into()).placeholder("Request testing".into()); - - match state.focused_field { - FormFocus::Name => name_input.focus(), - FormFocus::Description => desc_input.focus(), - _ => {} - }; - - let cancel_text = if state.focused_field.eq(&FormFocus::Cancel) { - "Cancel" - .fg(self.colors.normal.white) - .bg(self.colors.normal.red) - } else { - "Cancel".fg(self.colors.normal.white) - }; - - let cancel_button = Paragraph::new(Line::from(cancel_text).centered()).block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(self.colors.bright.red)) - .border_type(BorderType::Rounded), - ); - - let confirm_text = if state.focused_field.eq(&FormFocus::Confirm) { - "Create" - .fg(self.colors.normal.white) - .bg(self.colors.normal.magenta) - } else { - "Create".fg(self.colors.normal.white) - }; - - let confirm_button = Paragraph::new(Line::from(confirm_text).centered()).block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(self.colors.bright.magenta)) - .border_type(BorderType::Rounded), - ); - - let full_block = Block::default() - .padding(Padding::uniform(1)) - .style(Style::default().bg(self.colors.primary.background)); - - let hint = Paragraph::new("[Tab] to switch focus [Enter] to select a button") - .centered() - .fg(self.colors.normal.magenta); - - full_block.render(size, buf); - name_input.render(layout.name_input, buf, &mut state.name); - desc_input.render(layout.desc_input, buf, &mut state.description); - cancel_button.render(layout.cancel_button, buf); - confirm_button.render(layout.confirm_button, buf); - hint.render(layout.hint, buf); - } -} diff --git a/hac-client/src/pages/collection_list/collection_list.rs b/hac-client/src/pages/collection_list/collection_list.rs deleted file mode 100644 index 736fb80..0000000 --- a/hac-client/src/pages/collection_list/collection_list.rs +++ /dev/null @@ -1,292 +0,0 @@ -use std::fmt::Debug; -use std::sync::mpsc::{channel, Sender}; - -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use hac_core::command::Command; -use hac_loader::collection_loader::{CollectionMeta, ReadableByteSize}; -use ratatui::layout::{Alignment, Constraint, Direction, Flex, Layout, Rect}; -use ratatui::style::{Style, Stylize}; -use ratatui::text::Line; -use ratatui::widgets::{Block, BorderType, Borders, Clear, Padding, Paragraph, StatefulWidget, Widget, Wrap}; -use ratatui::Frame; - -use crate::app::AppRoutes; -use crate::components::list_itemm::ListItem; -use crate::pages::collection_list::Routes; -use crate::pages::overlay::{draw_overlay_old, make_overlay_old}; -use crate::pages::{Eventful, Renderable}; -use crate::router::Navigate; -use crate::{HacColors, HacConfig}; - -#[derive(Debug, PartialEq)] -struct DashboardLayout { - collections_pane: Rect, - hint_pane: Rect, - title_pane: Rect, -} - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -enum SortingKind { - #[default] - Recent, - Name, - Size, -} - -impl SortingKind { - pub fn next(&self) -> Self { - match self { - SortingKind::Recent => SortingKind::Name, - SortingKind::Name => SortingKind::Size, - SortingKind::Size => SortingKind::Recent, - } - } - - pub fn prev(&self) -> Self { - match self { - SortingKind::Recent => SortingKind::Size, - SortingKind::Name => SortingKind::Recent, - SortingKind::Size => SortingKind::Name, - } - } -} - -#[derive(Debug)] -pub struct CollectionList { - config: HacConfig, - colors: HacColors, - sorting_kind: SortingKind, - collections: Vec, - selected: usize, - scroll: usize, - navigator: Sender, - layout: DashboardLayout, - pub command_sender: Option>, -} - -impl CollectionList { - const LIST_ITEM_HEIGHT: u16 = 3; - - pub fn new(collections: Vec, size: Rect, config: HacConfig, colors: HacColors) -> Self { - let (dummy, _) = channel(); - Self { - colors, - config, - - selected: 0, - scroll: 0, - sorting_kind: SortingKind::default(), - navigator: dummy, - collections, - layout: build_layout(size), - command_sender: None, - } - } - - fn sort_list(&mut self) { - match self.sorting_kind { - SortingKind::Name => self.collections.sort_by(|a, b| a.name().cmp(b.name())), - SortingKind::Recent => self.collections.sort_by(|a, b| a.modified().cmp(b.modified())), - SortingKind::Size => self.collections.sort_by_key(|a| std::cmp::Reverse(a.size())), - } - } - - fn draw_title(&self, frame: &mut Frame) -> anyhow::Result<()> { - let selected_style = |kind: SortingKind| { - if kind == self.sorting_kind { - self.colors.bright.blue - } else { - self.colors.bright.black - } - }; - - let title = " HAC ".bg(self.colors.normal.red).fg(self.colors.normal.white); - let sorting = vec![ - "Most recent".fg(selected_style(SortingKind::Recent)), - " ❘ ".fg(self.colors.bright.black), - "By name".fg(selected_style(SortingKind::Name)), - " ❘ ".fg(self.colors.bright.black), - "By size".fg(selected_style(SortingKind::Size)), - ]; - - let lines = vec![Line::from(title), "".into(), Line::from(sorting)]; - frame.render_widget(Paragraph::new(lines), self.layout.title_pane); - - Ok(()) - } - - fn max_items_onscreen(&self) -> usize { - (self.layout.collections_pane.height / Self::LIST_ITEM_HEIGHT).into() - } - - fn draw_collection_list(&mut self, frame: &mut Frame) -> anyhow::Result<()> { - let layout = self.layout.collections_pane; - let max_items = self.max_items_onscreen(); - - for (ref mut idx, (i, item)) in self - .collections - .iter() - .enumerate() - .skip(self.scroll) - .take(max_items) - .enumerate() - { - let selected = i == self.selected; - let modified = item.modified(); - let size = item.size().readable_byte_size(); - let description = format!("{modified} - {size}"); - - let item = if selected { - ListItem::new(item.name(), Some(&description), self.colors.clone()) - .title_style(Style::new().fg(self.colors.normal.red)) - .desc_style(Style::new().fg(self.colors.bright.black)) - .select() - } else { - ListItem::new(item.name(), Some(&description), self.colors.clone()) - .title_style(Style::new().fg(self.colors.normal.white)) - .desc_style(Style::new().fg(self.colors.bright.black)) - }; - let size = Rect::new(layout.x, layout.y + (*idx as u16 * 3), layout.width, 2); - frame.render_widget(item, size); - - *idx += 1; - } - - Ok(()) - } - - fn draw_hint_text(&self, frame: &mut Frame) { - let hint = vec![ - "j/k ↑/↓".fg(self.colors.normal.green), - " - choose • ".fg(self.colors.bright.black), - "n".fg(self.colors.normal.green), - " - new • ".fg(self.colors.bright.black), - "enter".fg(self.colors.normal.green), - " - select • ".fg(self.colors.bright.black), - "?".fg(self.colors.normal.green), - " - show more • ".fg(self.colors.bright.black), - "ctrl c".fg(self.colors.normal.green), - " - quit".fg(self.colors.bright.black), - ]; - - frame.render_widget(Line::from(hint), self.layout.hint_pane); - } - - fn draw_background(&self, size: Rect, frame: &mut Frame) { - frame.render_widget(Clear, size); - frame.render_widget(Block::default().bg(self.colors.primary.background), size); - } - - fn maybe_scroll_list(&mut self) { - if self.scroll > self.selected { - let offset = self.scroll - self.selected; - self.scroll -= offset; - return; - } - - let max_items = self.max_items_onscreen() - 1; - let normalized = self.selected - self.scroll; - if normalized >= max_items { - let offset = self.selected - max_items; - self.scroll = offset; - } - } -} - -impl Renderable for CollectionList { - fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { - self.draw_background(size, frame); - self.draw_title(frame)?; - - self.draw_collection_list(frame)?; - self.draw_hint_text(frame); - - Ok(()) - } - - fn attach_navigator(&mut self, navigator: std::sync::mpsc::Sender) { - self.navigator = navigator; - } - - fn register_command_handler(&mut self, sender: Sender) { - self.command_sender = Some(sender.clone()); - } - - fn resize(&mut self, new_size: Rect) { - self.layout = build_layout(new_size); - } -} - -impl Eventful for CollectionList { - type Result = Command; - - fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - return Ok(Some(Command::Quit)); - }; - - match key_event.code { - KeyCode::Char('j') | KeyCode::Down => { - self.selected = usize::min(self.selected + 1, self.collections.len() - 1); - self.maybe_scroll_list(); - } - KeyCode::Char('k') | KeyCode::Up => { - self.selected = self.selected.saturating_sub(1); - self.maybe_scroll_list(); - } - KeyCode::Tab => { - self.sorting_kind = self.sorting_kind.next(); - self.sort_list(); - } - KeyCode::BackTab => { - self.sorting_kind = self.sorting_kind.prev(); - self.sort_list(); - } - KeyCode::Char('n') => { - self.navigator - .send(Navigate::To(Routes::CreateCollection.into(), None)) - .expect("failed to send navigation message"); - } - KeyCode::Enter => { - if self.collections.is_empty() { - return Ok(None); - } - assert!(self.collections.len() > self.selected); - let path = self.collections[self.selected].path().clone(); - self.navigator - .send(Navigate::Leave( - AppRoutes::CollectionViewerRouter.into(), - Some(Box::new(path)), - )) - .expect("failed to send navigation message"); - } - _ => {} - } - - Ok(None) - } -} - -fn build_layout(size: Rect) -> DashboardLayout { - let size = Rect::new(size.x + 1, size.y, size.width - 1, size.height); - let [top, help_pane] = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Fill(1), Constraint::Length(1)]) - .areas(size); - - let [_, title_pane, _, collections_pane] = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(1), - Constraint::Length(3), - Constraint::Length(1), - Constraint::Fill(1), - ]) - .areas(top); - - DashboardLayout { - collections_pane, - hint_pane: help_pane, - title_pane, - } -} diff --git a/hac-client/src/pages/collection_list/create_collection.rs b/hac-client/src/pages/collection_list/create_collection.rs index 9bf0d59..e53f262 100644 --- a/hac-client/src/pages/collection_list/create_collection.rs +++ b/hac-client/src/pages/collection_list/create_collection.rs @@ -2,12 +2,16 @@ use std::sync::mpsc::{channel, Sender}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::command::Command; -use ratatui::layout::{Constraint, Direction, Flex, Layout, Rect}; +use hac_core::text_object::cursor::Cursor; +use hac_core::text_object::{TextObject, Write}; +use hac_loader::collection_loader::CollectionMeta; +use ratatui::layout::{Constraint, Flex, Layout, Rect}; use ratatui::style::{Style, Stylize}; use ratatui::text::Line; use ratatui::widgets::Paragraph; use ratatui::Frame; +use super::Routes; use crate::ascii::LOGO_ASCII; use crate::components::input::Input; use crate::pages::overlay::make_overlay; @@ -18,7 +22,6 @@ use crate::{HacColors, HacConfig, MIN_HEIGHT, MIN_WIDTH}; #[derive(Debug)] struct CreateCollectionLayout { name_input: Rect, - desc_input: Rect, hint: Rect, logo: Rect, } @@ -27,15 +30,16 @@ struct CreateCollectionLayout { enum Focus { #[default] Name, - Desc, } #[derive(Debug)] pub struct CreateCollection { - name: String, - desc: String, + name: TextObject, + desc: TextObject, + collections: Vec, size: Rect, colors: HacColors, + name_cursor: Cursor, focus: Focus, config: HacConfig, layout: CreateCollectionLayout, @@ -50,33 +54,36 @@ impl CreateCollection { colors, size, layout: build_layout(size), - name: Default::default(), + name: TextObject::::default(), + desc: TextObject::::default(), + name_cursor: Cursor::default(), focus: Default::default(), - desc: Default::default(), + collections: Default::default(), navigator: dummy, } } + + fn reset(&mut self) { + self.name = Default::default(); + self.desc = Default::default(); + self.name_cursor = Default::default(); + self.focus = Default::default(); + } } impl Renderable for CreateCollection { fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { - make_overlay(self.colors.clone(), self.colors.normal.black, 0.05, frame); + make_overlay(self.colors.clone(), self.colors.normal.black, 0.2, frame); let label = String::from("Collection Name"); - let name_input = Input::new(Some(&self.name), Some(&label), self.colors.clone()) - .value_style(Style::default().fg(self.colors.normal.white)) - .label_style(Style::default().fg(self.colors.bright.black)); - - let label = String::from("Description"); - let desc_input = Input::new(Some(&self.desc), Some(&label), self.colors.clone()) + let name = self.name.to_string(); + let name_input = Input::new(Some(&name), Some(&label), self.colors.clone()) .value_style(Style::default().fg(self.colors.normal.white)) .label_style(Style::default().fg(self.colors.bright.black)); let hint = vec![ - "Tab".fg(self.colors.bright.green).bold(), - " - Switch input • ".fg(self.colors.bright.black), "Enter".fg(self.colors.bright.green).bold(), - " - Confirm ".fg(self.colors.bright.black), + " - Confirm • ".fg(self.colors.bright.black), "Esc".fg(self.colors.bright.green).bold(), " - Cancel".fg(self.colors.bright.black), ]; @@ -90,23 +97,29 @@ impl Renderable for CreateCollection { frame.render_widget(logo, self.layout.logo); frame.render_widget(name_input, self.layout.name_input); - frame.render_widget(desc_input, self.layout.desc_input); frame.render_widget(Line::from(hint), self.layout.hint); - match self.focus { - Focus::Name => frame.set_cursor( - self.layout.name_input.x + 1 + self.name.len() as u16, - self.layout.name_input.y + 2, - ), - Focus::Desc => frame.set_cursor( - self.layout.desc_input.x + 1 + self.desc.len() as u16, - self.layout.desc_input.y + 2, - ), - } + frame.set_cursor( + self.layout.name_input.x + 1 + self.name_cursor.col() as u16, + self.layout.name_input.y + 2 + self.name_cursor.row() as u16, + ); Ok(()) } + fn update(&mut self, data: Option>) { + if let Some(data) = data { + let data = data + .downcast::<(String, Vec)>() + .expect("wrong kind of data sent to CreateCollection"); + let name = data.0; + let collections = data.1; + self.collections = collections; + self.name = TextObject::from(&name).with_write(); + self.name_cursor.move_to_col(name.len()); + } + } + fn resize(&mut self, new_size: Rect) { self.size = new_size; self.layout = build_layout(new_size); @@ -125,39 +138,102 @@ impl Eventful for CreateCollection { return Ok(Some(Command::Quit)); } + match key_event.code { + KeyCode::Enter => { + let collections = hac_loader::collection_loader::create_collection( + self.name.to_string(), + self.collections.clone(), + &self.config, + )?; + let name = self.name.to_string(); + self.navigator + .send(Navigate::To( + Routes::ListCollections.into(), + Some(Box::new((Some(name), collections))), + )) + .expect("failed to send navigate message"); + } + KeyCode::Esc => { + self.reset(); + self.navigator + .send(Navigate::To(Routes::ListCollections.into(), None)) + .expect("failed to send navigate message"); + } + KeyCode::Char('b') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { + self.name_cursor.move_left(1); + } + KeyCode::Left if matches!(key_event.modifiers, KeyModifiers::ALT) => { + let (col, row) = self.name.find_char_before_whitespace(&self.name_cursor); + self.name_cursor.move_to(col, row); + } + KeyCode::Right if matches!(key_event.modifiers, KeyModifiers::ALT) => { + let (col, row) = self.name.find_char_after_whitespace(&self.name_cursor); + self.name_cursor.move_to(col, row); + } + KeyCode::Left => { + self.name_cursor.move_left(1); + } + KeyCode::Char('e') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { + self.name_cursor.move_to_line_end(self.name.line_len(0)); + } + KeyCode::Down => { + self.name_cursor.move_to_line_end(self.name.line_len(0)); + } + KeyCode::Char('a') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { + self.name_cursor.move_to_line_start(); + } + KeyCode::Up => { + self.name_cursor.move_to_line_start(); + } + KeyCode::Char('f') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { + self.name_cursor.move_right(1); + } + KeyCode::Right => { + self.name_cursor.move_right(1); + self.name_cursor.maybe_snap_to_col(self.name.line_len(0)); + } + KeyCode::Char('d') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { + self.name.erase_current_char(&self.name_cursor); + } + KeyCode::Char('u') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { + self.name.clear_line(&self.name_cursor); + self.name_cursor.move_to_line_start(); + } + KeyCode::Backspace => { + self.name.erase_previous_char(&self.name_cursor); + self.name_cursor.move_left(1); + } + KeyCode::Char(c) => { + self.name.insert_char(c, &self.name_cursor); + self.name_cursor.move_right(1); + } + _ => {} + } + Ok(None) } } fn build_layout(area: Rect) -> CreateCollectionLayout { - let [_, form, _] = Layout::default() + let [_, form, _] = Layout::horizontal([Constraint::Fill(1), Constraint::Length(MIN_WIDTH), Constraint::Fill(1)]) .flex(Flex::Center) - .constraints([Constraint::Fill(1), Constraint::Length(MIN_WIDTH), Constraint::Fill(1)]) - .direction(Direction::Horizontal) .areas(area); - let [_, form, _] = Layout::default() - .constraints([Constraint::Fill(1), Constraint::Length(MIN_HEIGHT), Constraint::Fill(1)]) - .direction(Direction::Vertical) + let [_, form, _] = Layout::vertical([Constraint::Fill(1), Constraint::Length(MIN_HEIGHT), Constraint::Fill(1)]) + .flex(Flex::End) .areas(form); - let [logo, _, name_input, _, desc_input, _, hint] = Layout::default() - .constraints([ - Constraint::Length(LOGO_ASCII.len() as u16), - Constraint::Length(1), - Constraint::Length(4), - Constraint::Length(1), - Constraint::Length(4), - Constraint::Length(1), - Constraint::Length(1), - ]) - .direction(Direction::Vertical) - .areas(form); + let form = form.inner(&ratatui::layout::Margin::new(2, 0)); - CreateCollectionLayout { - name_input, - desc_input, - hint, - logo, - } + let [logo, _, name_input, _, hint] = Layout::vertical([ + Constraint::Length(LOGO_ASCII.len() as u16), + Constraint::Length(1), + Constraint::Length(4), + Constraint::Length(1), + Constraint::Length(1), + ]) + .flex(Flex::Center) + .areas(form); + + CreateCollectionLayout { name_input, hint, logo } } diff --git a/hac-client/src/pages/collection_list/mod.rs b/hac-client/src/pages/collection_list/mod.rs index 379d597..d1e936e 100644 --- a/hac-client/src/pages/collection_list/mod.rs +++ b/hac-client/src/pages/collection_list/mod.rs @@ -1,12 +1,22 @@ -mod collection_list; mod create_collection; -use std::sync::mpsc::Sender; -use collection_list::CollectionList; -use create_collection::CreateCollection; +use std::fmt::Debug; +use std::sync::mpsc::{channel, Sender}; + +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::command::Command; -use hac_loader::collection_loader::CollectionMeta; -use ratatui::layout::Rect; +use hac_loader::collection_loader::{CollectionMeta, ReadableByteSize}; +use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::style::{Style, Stylize}; +use ratatui::text::Line; +use ratatui::widgets::{Block, Clear, Paragraph}; +use ratatui::Frame; + +use crate::app::AppRoutes; +use crate::components::list_itemm::ListItem; +use crate::pages::{Eventful, Renderable}; +use crate::router::Navigate; +use create_collection::CreateCollection; use crate::router::Router; use crate::{HacColors, HacConfig}; @@ -18,11 +28,11 @@ pub enum Routes { CreateCollection, } -impl Into for Routes { - fn into(self) -> u8 { - match self { - Self::ListCollections => 0, - Self::CreateCollection => 1, +impl From for u8 { + fn from(val: Routes) -> Self { + match val { + Routes::ListCollections => 0, + Routes::CreateCollection => 1, } } } @@ -35,10 +45,391 @@ pub fn make_collection_list_router( colors: HacColors, ) -> Router { let mut router = Router::new(command_sender, colors.clone()); - let collection_list = CollectionList::new(collections, size, config.clone(), colors.clone()); + let collection_list = CollectionList::new(collections, size, colors.clone()); let create_collection = CreateCollection::new(size, config.clone(), colors.clone()); router.add_route(Routes::ListCollections.into(), Box::new(collection_list)); router.add_dialog(Routes::CreateCollection.into(), Box::new(create_collection)); router } + +#[derive(Debug, PartialEq)] +struct DashboardLayout { + collections_pane: Rect, + hint_pane: Rect, + title_pane: Rect, + total_size: Rect, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +enum SortingKind { + #[default] + Recent, + Name, + Size, +} + +impl SortingKind { + pub fn next(&self) -> Self { + match self { + SortingKind::Recent => SortingKind::Name, + SortingKind::Name => SortingKind::Size, + SortingKind::Size => SortingKind::Recent, + } + } + + pub fn prev(&self) -> Self { + match self { + SortingKind::Recent => SortingKind::Size, + SortingKind::Name => SortingKind::Recent, + SortingKind::Size => SortingKind::Name, + } + } +} + +#[derive(Debug)] +pub struct CollectionList { + colors: HacColors, + sorting_kind: SortingKind, + collections: Vec, + selected: usize, + scroll: usize, + navigator: Sender, + layout: DashboardLayout, + pub command_sender: Option>, + extended_hint: bool, +} + +impl CollectionList { + const LIST_ITEM_HEIGHT: u16 = 3; + + pub fn new(collections: Vec, size: Rect, colors: HacColors) -> Self { + let (dummy, _) = channel(); + Self { + colors, + + selected: 0, + scroll: 0, + sorting_kind: SortingKind::default(), + navigator: dummy, + collections, + layout: build_layout(size, false), + command_sender: None, + extended_hint: false, + } + } + + fn sort_list(&mut self) { + match self.sorting_kind { + SortingKind::Name => self.collections.sort_by(|a, b| a.name().cmp(b.name())), + SortingKind::Recent => self.collections.sort_by(|a, b| b.modified().cmp(a.modified())), + SortingKind::Size => self.collections.sort_by_key(|a| std::cmp::Reverse(a.size())), + } + } + + fn draw_title(&self, frame: &mut Frame) -> anyhow::Result<()> { + let selected_style = |kind: SortingKind| { + if kind == self.sorting_kind { + self.colors.bright.blue + } else { + self.colors.bright.black + } + }; + + let title = " HAC ".bg(self.colors.normal.red).fg(self.colors.normal.white); + let sorting = vec![ + "Most recent".fg(selected_style(SortingKind::Recent)), + " ❘ ".fg(self.colors.bright.black), + "By name".fg(selected_style(SortingKind::Name)), + " ❘ ".fg(self.colors.bright.black), + "By size".fg(selected_style(SortingKind::Size)), + ]; + + let lines = vec![Line::from(title), "".into(), Line::from(sorting)]; + frame.render_widget(Paragraph::new(lines), self.layout.title_pane); + + Ok(()) + } + + fn max_items_onscreen(&self) -> usize { + (self.layout.collections_pane.height / Self::LIST_ITEM_HEIGHT).into() + } + + fn draw_collection_list(&mut self, frame: &mut Frame) -> anyhow::Result<()> { + let layout = self.layout.collections_pane; + let max_items = self.max_items_onscreen(); + + for (ref mut idx, (i, item)) in self + .collections + .iter() + .enumerate() + .skip(self.scroll) + .take(max_items) + .enumerate() + { + let selected = i == self.selected; + let modified = item.modified(); + let size = item.size().readable_byte_size(); + let description = format!("{modified} - {size}"); + + let item = if selected { + ListItem::new(item.name(), Some(&description), self.colors.clone()) + .title_style(Style::new().fg(self.colors.normal.red)) + .desc_style(Style::new().fg(self.colors.bright.black)) + .select() + } else { + ListItem::new(item.name(), Some(&description), self.colors.clone()) + .title_style(Style::new().fg(self.colors.normal.white)) + .desc_style(Style::new().fg(self.colors.bright.black)) + }; + let size = Rect::new(layout.x, layout.y + (*idx as u16 * 3), layout.width, 2); + frame.render_widget(item, size); + + *idx += 1; + } + + Ok(()) + } + + fn draw_hint_text(&self, frame: &mut Frame) { + if self.extended_hint { + let lines = vec![ + Line::from(vec![ + "j/k ↑/↓".fg(self.colors.normal.green), + " - choose • ".fg(self.colors.bright.black), + "n".fg(self.colors.normal.green), + " - new • ".fg(self.colors.bright.black), + "enter".fg(self.colors.normal.green), + " - select".fg(self.colors.bright.black), + ]), + Line::from(vec![ + "?".fg(self.colors.normal.green), + " - show more • ".fg(self.colors.bright.black), + "ctrl c".fg(self.colors.normal.green), + " - quit • ".fg(self.colors.bright.black), + "d".fg(self.colors.normal.green), + " - delete".fg(self.colors.bright.black), + ]), + Line::from(vec![ + "r".fg(self.colors.normal.green), + " - refresh • ".fg(self.colors.bright.black), + "tab".fg(self.colors.normal.green), + " - change sorting".fg(self.colors.bright.black), + ]), + ]; + frame.render_widget(Paragraph::new(lines), self.layout.hint_pane); + } else { + let hint = vec![ + "j/k ↑/↓".fg(self.colors.normal.green), + " - choose • ".fg(self.colors.bright.black), + "n".fg(self.colors.normal.green), + " - new • ".fg(self.colors.bright.black), + "enter".fg(self.colors.normal.green), + " - select • ".fg(self.colors.bright.black), + "?".fg(self.colors.normal.green), + " - show more • ".fg(self.colors.bright.black), + "ctrl c".fg(self.colors.normal.green), + " - quit".fg(self.colors.bright.black), + ]; + frame.render_widget(Line::from(hint), self.layout.hint_pane); + }; + } + + fn draw_background(&self, size: Rect, frame: &mut Frame) { + frame.render_widget(Clear, size); + frame.render_widget(Block::default().bg(self.colors.primary.background), size); + } + + fn maybe_scroll_list(&mut self) { + if self.scroll > self.selected { + let offset = self.scroll - self.selected; + self.scroll -= offset; + return; + } + + let max_items = self.max_items_onscreen() - 1; + let normalized = self.selected - self.scroll; + if normalized >= max_items { + let offset = self.selected - max_items; + self.scroll = offset; + } + } +} + +impl Renderable for CollectionList { + fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { + self.draw_background(size, frame); + self.draw_title(frame)?; + + self.draw_collection_list(frame)?; + self.draw_hint_text(frame); + + Ok(()) + } + + fn tick(&mut self) -> anyhow::Result<()> { + if hac_loader::collection_loader::has_changes() { + self.collections = hac_loader::collection_loader::collections_metadata()?; + self.sort_list(); + } + Ok(()) + } + + fn attach_navigator(&mut self, navigator: std::sync::mpsc::Sender) { + self.navigator = navigator; + } + + fn register_command_handler(&mut self, sender: Sender) { + self.command_sender = Some(sender.clone()); + } + + fn resize(&mut self, new_size: Rect) { + self.layout = build_layout(new_size, self.extended_hint); + } + + fn update(&mut self, data: Option>) { + if let Some(data) = data { + let data = data + .downcast::<(Option, Vec)>() + .expect("wrong kind of data provided to CollectionList"); + self.collections = data.1; + self.sort_list(); + + if let Some(name) = data.0 { + let selected_idx = self + .collections + .iter() + .position(|c| c.name() == name) + .expect("received invalid name after creation"); + self.selected = selected_idx; + } + } + } +} + +impl Eventful for CollectionList { + type Result = Command; + + fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { + return Ok(Some(Command::Quit)); + }; + + match key_event.code { + KeyCode::Char('?') => { + self.extended_hint = !self.extended_hint; + self.layout = build_layout(self.layout.total_size, self.extended_hint); + } + KeyCode::Char('j') | KeyCode::Down => { + self.selected = usize::min(self.selected + 1, self.collections.len() - 1); + self.maybe_scroll_list(); + } + KeyCode::Char('k') | KeyCode::Up => { + self.selected = self.selected.saturating_sub(1); + self.maybe_scroll_list(); + } + KeyCode::PageDown => { + let half = self.layout.collections_pane.height / 2; + self.selected = usize::min(self.selected + half as usize, self.collections.len() - 1); + self.maybe_scroll_list(); + } + KeyCode::PageUp => { + let half = self.layout.collections_pane.height / 2; + self.selected = self.selected.saturating_sub(half.into()); + self.maybe_scroll_list(); + } + KeyCode::Char('r') => { + self.collections = hac_loader::collection_loader::collections_metadata()?; + self.sort_list(); + } + KeyCode::Char('d') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { + let half = self.layout.collections_pane.height / 2; + self.selected = usize::min(self.selected + half as usize, self.collections.len() - 1); + self.maybe_scroll_list(); + } + KeyCode::Char('u') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { + self.selected = 0; + self.maybe_scroll_list(); + } + KeyCode::Char('G') => { + self.selected = self.collections.len() - 1; + self.maybe_scroll_list(); + } + KeyCode::Char('g') => { + let half = self.layout.collections_pane.height / 2; + self.selected = self.selected.saturating_sub(half.into()); + self.maybe_scroll_list(); + } + KeyCode::Tab => { + self.sorting_kind = self.sorting_kind.next(); + self.sort_list(); + } + KeyCode::BackTab => { + self.sorting_kind = self.sorting_kind.prev(); + self.sort_list(); + } + KeyCode::Char('n') => { + let collections = self.collections.clone(); + self.navigator + .send(Navigate::To( + Routes::CreateCollection.into(), + Some(Box::new((String::default(), collections))), + )) + .expect("failed to send navigation message"); + } + KeyCode::Char('e') => { + if self.collections.is_empty() { + return Ok(None); + } + let name = self.collections[self.selected].name().to_string(); + self.navigator + .send(Navigate::To(Routes::CreateCollection.into(), Some(Box::new(name)))) + .expect("failed to send navigation message"); + } + KeyCode::Enter => { + if self.collections.is_empty() { + return Ok(None); + } + assert!(self.collections.len() > self.selected); + let path = self.collections[self.selected].path().clone(); + self.navigator + .send(Navigate::Leave( + AppRoutes::CollectionViewerRouter.into(), + Some(Box::new(path)), + )) + .expect("failed to send navigation message"); + } + _ => {} + } + + Ok(None) + } +} + +fn build_layout(total_size: Rect, extended_hint: bool) -> DashboardLayout { + let size = Rect::new(total_size.x + 1, total_size.y, total_size.width - 1, total_size.height); + let [top, help_pane] = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Fill(1), + Constraint::Length(if extended_hint { 4 } else { 2 }), + ]) + .areas(size); + + let [_, title_pane, _, collections_pane] = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(1), + Constraint::Length(3), + Constraint::Length(1), + Constraint::Fill(1), + ]) + .areas(top); + + DashboardLayout { + collections_pane, + hint_pane: help_pane, + title_pane, + total_size, + } +} diff --git a/hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs b/hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs index a4db2af..8a4c547 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs @@ -3,11 +3,9 @@ use std::rc::Rc; use std::sync::{Arc, RwLock}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use hac_core::collection::types::*; -use rand::Rng; +use hac_store::collection::{Folder, ReqTree}; use super::directory_form::{DirectoryForm, DirectoryFormCreate, DirectoryFormEvent}; -use crate::ascii::LOGO_ASCII; use crate::pages::collection_viewer::collection_store::CollectionStore; use crate::pages::collection_viewer::sidebar::DirectoryFormTrait; use crate::pages::Eventful; @@ -57,10 +55,9 @@ impl Eventful for DirectoryForm<'_, DirectoryFormCreate> { self.dir_name = "unnamed directory".into(); } - requests.push(RequestKind::Nested(Directory { - id: uuid::Uuid::new_v4().to_string(), + requests.push(ReqTree::Folder(Folder { name: self.dir_name.clone(), - requests: Arc::new(RwLock::new(vec![])), + requests: vec![], })); drop(store); diff --git a/hac-client/src/pages/confirm_popup.rs b/hac-client/src/pages/confirm_popup.rs deleted file mode 100755 index c4801b8..0000000 --- a/hac-client/src/pages/confirm_popup.rs +++ /dev/null @@ -1,79 +0,0 @@ -use ratatui::buffer::Buffer; -use ratatui::layout::Rect; -use ratatui::style::{Style, Stylize}; -use ratatui::text::Line; -use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap}; - -pub struct ConfirmPopup<'a> { - message: String, - colors: &'a hac_colors::Colors, -} - -impl<'a> ConfirmPopup<'a> { - pub fn new(message: String, colors: &'a hac_colors::Colors) -> Self { - ConfirmPopup { message, colors } - } - - fn build_popup(&self) -> Paragraph<'_> { - let lines = vec![ - self.message.clone().fg(self.colors.normal.yellow).into(), - "".into(), - Line::from(vec![ - "(y)es".fg(self.colors.normal.green), - " ".into(), - "(n)o".fg(self.colors.normal.red), - ]) - .centered(), - ]; - Paragraph::new(lines).wrap(Wrap { trim: true }).block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(self.colors.bright.black)) - .padding(Padding::new(2, 2, 1, 1)) - .bg(self.colors.normal.black), - ) - } -} - -impl Widget for ConfirmPopup<'_> { - fn render(self, size: Rect, buf: &mut Buffer) - where - Self: Sized, - { - Clear.render(size, buf); - let popup = self.build_popup(); - popup.render(size, buf); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_build_with_correct_message() { - let colors = hac_colors::Colors::default(); - let popup = ConfirmPopup::new("my confirmation message".into(), &colors); - let lines = vec![ - "my confirmation message".fg(colors.normal.yellow).into(), - "".into(), - Line::from(vec![ - "(y)es".fg(colors.normal.green), - " ".into(), - "(n)o".fg(colors.normal.red), - ]) - .centered(), - ]; - let expected = Paragraph::new(lines).wrap(Wrap { trim: true }).block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(colors.bright.black)) - .padding(Padding::new(2, 2, 1, 1)) - .bg(colors.normal.black), - ); - - let content = popup.build_popup(); - - assert_eq!(expected, content); - } -} diff --git a/hac-client/src/pages/error_popup.rs b/hac-client/src/pages/error_popup.rs deleted file mode 100755 index b0b7c8c..0000000 --- a/hac-client/src/pages/error_popup.rs +++ /dev/null @@ -1,130 +0,0 @@ -use ratatui::buffer::Buffer; -use ratatui::layout::{Constraint, Direction, Flex, Layout, Rect}; -use ratatui::style::{Style, Stylize}; -use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap}; - -#[derive(Debug, PartialEq)] -pub struct ErrorPopupLayout { - message_pane: Rect, - confirmation_pane: Rect, -} - -pub struct ErrorPopup<'a> { - message: String, - colors: &'a hac_colors::Colors, -} - -impl<'a> ErrorPopup<'a> { - pub fn new(message: String, colors: &'a hac_colors::Colors) -> Self { - ErrorPopup { message, colors } - } - - fn build_popup(&self) -> (Paragraph<'_>, Paragraph<'_>) { - let message = Paragraph::new(self.message.clone().fg(self.colors.normal.red)) - .wrap(Wrap { trim: true }); - - let confirmation = Paragraph::new("(O)k".fg(self.colors.normal.green).into_centered_line()) - .wrap(Wrap { trim: true }); - - (message, confirmation) - } - - fn layout(&self, size: &Rect) -> ErrorPopupLayout { - let size = Rect::new( - size.x + 2, - size.y + 2, - size.width.saturating_sub(4), - size.height.saturating_sub(4), - ); - - let [message_pane, confirmation_pane] = Layout::default() - .direction(Direction::Vertical) - .flex(Flex::SpaceBetween) - .constraints([Constraint::Fill(1), Constraint::Length(1)]) - .areas(size); - - ErrorPopupLayout { - message_pane, - confirmation_pane, - } - } - - fn build_container(&self) -> Block<'_> { - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(self.colors.bright.black)) - .padding(Padding::new(2, 2, 1, 1)) - .bg(self.colors.normal.black) - } -} - -impl Widget for ErrorPopup<'_> { - fn render(self, size: Rect, buf: &mut Buffer) - where - Self: Sized, - { - Clear.render(size, buf); - let layout = self.layout(&size); - let (message, confirmation) = self.build_popup(); - let full_block = self.build_container(); - - full_block.render(size, buf); - message.render(layout.message_pane, buf); - confirmation.render(layout.confirmation_pane, buf); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_build_with_correct_message() { - let colors = hac_colors::Colors::default(); - let popup = ErrorPopup::new("my error message".into(), &colors); - - let (message, confirmation) = popup.build_popup(); - - assert_eq!( - message, - Paragraph::new("my error message".fg(colors.normal.red)).wrap(Wrap { trim: true }) - ); - - assert_eq!( - confirmation, - Paragraph::new("(O)k".fg(colors.normal.green).into_centered_line()) - .wrap(Wrap { trim: true }) - ); - } - - #[test] - fn test_build_layout_correctly() { - let colors = hac_colors::Colors::default(); - let popup = ErrorPopup::new("my error message".into(), &colors); - let rect = Rect::new(0, 0, 10, 10); - let expected = ErrorPopupLayout { - message_pane: Rect::new(2, 2, 6, 5), - confirmation_pane: Rect::new(2, 7, 6, 1), - }; - - let layout = popup.layout(&rect); - - assert_eq!(layout, expected); - } - - #[test] - fn test_build_container_correctly() { - let colors = hac_colors::Colors::default(); - let popup = ErrorPopup::new("my error message".into(), &colors); - - let expected = Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(colors.bright.black)) - .padding(Padding::new(2, 2, 1, 1)) - .bg(colors.normal.black); - - let block = popup.build_container(); - - assert_eq!(expected, block); - } -} diff --git a/hac-client/src/router.rs b/hac-client/src/router.rs index 771042d..51e02b7 100644 --- a/hac-client/src/router.rs +++ b/hac-client/src/router.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, VecDeque}; +use std::collections::HashMap; use std::fmt::Debug; use std::sync::mpsc::{Receiver, Sender}; @@ -139,17 +139,14 @@ impl Router { // pop the dialog if we are currently displaying one self.dialogs.contains_key(&curr).then(|| self.dialog_stack.pop()); - let route; - if self.dialogs.contains_key(&prev) { - route = self - .get_active_dialog() - .expect("previous route is a dialog, but its not on the stack"); + let route = if self.dialogs.contains_key(&prev) { + self.get_active_dialog() + .expect("previous route is a dialog, but its not on the stack") } else { - route = self - .routes + self.routes .get_mut(&prev) - .expect("previous route is not registered... how?"); - } + .expect("previous route is not registered... how?") + }; route.update(data); } @@ -171,13 +168,12 @@ impl Router { impl Renderable for Router { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { if frame.size().height < MIN_HEIGHT || frame.size().width < MIN_WIDTH { - self.too_small.draw(frame, size); + self.too_small.draw(frame, size)?; return Ok(()); } - match self.navigate_receiver.try_recv() { - Ok(navigation) => self.navigate(navigation), - _ => {} + if let Ok(navigation) = self.navigate_receiver.try_recv() { + self.navigate(navigation) } let route = self.get_active_route(); @@ -190,6 +186,17 @@ impl Renderable for Router { Ok(()) } + fn tick(&mut self) -> anyhow::Result<()> { + let route = self.get_active_route(); + route.tick()?; + + if let Some(dialog) = self.get_active_dialog() { + dialog.tick()?; + } + + Ok(()) + } + fn resize(&mut self, new_size: Rect) { let route = self.get_active_route(); route.resize(new_size); diff --git a/hac-client/src/screen_manager.rs b/hac-client/src/screen_manager.rs deleted file mode 100755 index a0c8df4..0000000 --- a/hac-client/src/screen_manager.rs +++ /dev/null @@ -1,155 +0,0 @@ -use std::cell::RefCell; -use std::collections::HashMap; -use std::fmt::Debug; -use std::hash::Hash; -use std::rc::Rc; -use std::sync::mpsc::channel; -use std::sync::mpsc::Receiver; -use std::sync::mpsc::Sender; - -use hac_core::command::Command; -use hac_loader::collection_loader::CollectionMeta; -use ratatui::layout::Rect; -use ratatui::Frame; - -use crate::event_pool::Event; -use crate::pages::collection_dashboard::CollectionDashboard; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::collection_viewer::CollectionViewer; -use crate::pages::terminal_too_small::TerminalTooSmall; -use crate::pages::Eventful; -use crate::pages::Renderable; - -#[derive(Debug, Clone, PartialEq)] -pub enum Routes { - CollectionList, - CollectionDashboard, - CollectionViewer, - TerminalTooSmall, -} - -/// ScreenManager is responsible for redirecting the user to the screen it should -/// be seeing at any point by the application, it is the entity behind navigation -pub struct Router { - active_route: Routes, - prev_route: Routes, - sender: Option>, -} - -impl Router { - pub fn new(collection_list: Vec, size: Rect) -> anyhow::Result { - Ok(Self { - active_route: Routes::CollectionDashboard, - prev_route: Routes::CollectionDashboard, - sender: None, - }) - } - - fn restore_screen(&mut self) { - std::mem::swap(&mut self.active_route, &mut self.prev_route); - } - - fn switch_screen(&mut self, screen: Routes) { - if self.active_route == screen { - return; - } - std::mem::swap(&mut self.active_route, &mut self.prev_route); - self.active_route = screen; - } - - // events can generate commands, which are sent back to the top level event loop through this - // channel, and goes back down the chain of components as many components may be interested - // in such command - pub fn handle_command(&mut self, command: Command) { - match command { - Command::SelectCollection(collection) | Command::CreateCollection(collection) => { - tracing::debug!("changing to api explorer: {}", collection.info.name); - self.switch_screen(Routes::CollectionViewer); - // self.collection_store.borrow_mut().set_state(collection); - // self.collection_viewer = Some(CollectionViewer::new( - // self.size, - // self.collection_store.clone(), - // self.colors, - // self.config, - // self.dry_run, - // )); - // self.collection_viewer.as_mut().unwrap().register_command_handler( - // self.sender - // .as_ref() - // .expect("attempted to register the sender on collection_viewer but it was None") - // .clone(), - // ) - } - Command::Error(msg) => { - //self.collection_list.display_error(msg); - } - _ => {} - } - } -} - -impl Renderable for Router { - fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { - match (size.width < 75, size.height < 22) { - (true, _) => self.switch_screen(Routes::TerminalTooSmall), - (_, true) => self.switch_screen(Routes::TerminalTooSmall), - (false, false) if self.active_route.eq(&Routes::TerminalTooSmall) => self.restore_screen(), - _ => {} - } - - // match &self.active_route { - // // Routes::CollectionViewer => self - // // .collection_viewer - // // .as_mut() - // // .expect("should never be able to switch to editor screen without having a collection") - // // .draw(frame, frame.size())?, - // Routes::CollectionDashboard => (), // self.collection_list.draw(frame, frame.size())?), - // // Routes::TerminalTooSmall => self.terminal_too_small.draw(frame, frame.size())?, - // }; - - Ok(()) - } - - fn register_command_handler(&mut self, sender: Sender) { - // self.sender = Some(sender.clone()); - // self.collection_list.register_command_handler(sender.clone())?; - } - - fn resize(&mut self, new_size: Rect) { - // self.collection_list.resize(new_size); - - // if let Some(e) = self.collection_viewer.as_mut() { - // e.resize(new_size) - // } - } - - fn tick(&mut self) -> anyhow::Result<()> { - // currently, only the editor cares about the ticks, used to determine - // when to sync changes in disk - // if let Routes::CollectionViewer = &self.active_route { - // self.collection_viewer - // .as_mut() - // .expect("we are displaying the editor without having one") - // .handle_tick()? - // }; - - Ok(()) - } -} - -impl Eventful for Router { - type Result = Command; - - fn handle_event(&mut self, event: Option) -> anyhow::Result> { - // match self.active_route { - // Routes::CollectionViewer => self - // .collection_viewer - // .as_mut() - // .expect("should never be able to switch to editor screen without having a collection") - // .handle_event(event), - // Routes::CollectionDashboard => Ok(None), //self.collection_list.handle_event(event), - // Routes::TerminalTooSmall => Ok(None), - // } - Ok(None) - } -} diff --git a/hac-client/src/utils.rs b/hac-client/src/utils.rs index 6415ef6..17f4552 100755 --- a/hac-client/src/utils.rs +++ b/hac-client/src/utils.rs @@ -21,10 +21,7 @@ pub fn build_syntax_highlighted_lines( // `HIGHLIGHTER` returns a vector of `ColorInfo`, which contains information about // which kind of token that is, and the style to apply to it - let mut highlights = HIGHLIGHTER - .read() - .unwrap() - .apply(content, tree, &colors.tokens); + let mut highlights = HIGHLIGHTER.read().unwrap().apply(content, tree, &colors.tokens); // these are helper variables to collect each line into styled spans based on the // token it contains diff --git a/hac-config/src/config.rs b/hac-config/src/config.rs index 3050549..f5a350b 100755 --- a/hac-config/src/config.rs +++ b/hac-config/src/config.rs @@ -60,6 +60,7 @@ pub struct Config { pub editor_keys: Keys, #[serde(default)] pub collection_ext: CollectionExtensions, + pub dry_run: bool, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] diff --git a/hac-config/src/default_config.rs b/hac-config/src/default_config.rs index 3ab566b..851e505 100755 --- a/hac-config/src/default_config.rs +++ b/hac-config/src/default_config.rs @@ -1,4 +1,6 @@ pub static DEFAULT_CONFIG: &str = r##" +dry_run = false + [editor_keys.normal] "u" = "Undo" "n" = "FindNext" diff --git a/hac-core/Cargo.toml b/hac-core/Cargo.toml index 03a393b..9720484 100755 --- a/hac-core/Cargo.toml +++ b/hac-core/Cargo.toml @@ -15,6 +15,7 @@ workspace = "../" [dependencies] hac-config.workspace = true +hac-store.workspace = true anyhow.workspace = true tracing.workspace = true diff --git a/hac-core/src/collection/errors.rs b/hac-core/src/collection/errors.rs deleted file mode 100755 index 8cc1df0..0000000 --- a/hac-core/src/collection/errors.rs +++ /dev/null @@ -1,22 +0,0 @@ -pub enum CollectionError { - Unknown(String), -} - -impl std::fmt::Display for CollectionError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - CollectionError::Unknown(msg) => write!(f, "{}", msg), - } - } -} - -impl From for CollectionError -where - E: Into, -{ - fn from(err: E) -> Self { - let err: anyhow::Error = err.into(); - let msg = err.to_string(); - CollectionError::Unknown(msg) - } -} diff --git a/hac-core/src/collection/mod.rs b/hac-core/src/collection/mod.rs deleted file mode 100644 index 0dbb258..0000000 --- a/hac-core/src/collection/mod.rs +++ /dev/null @@ -1,75 +0,0 @@ -pub mod types; -pub use types::Collection; -mod errors; - -use crate::collection::types::Info; - -use std::path::Path; -use std::time::{self, UNIX_EPOCH}; - -//#[tracing::instrument(err)] -//pub fn get_collections_from_config() -> anyhow::Result> { -// let collections_dir = hac_config::get_or_create_collections_dir(); -// get_collections(collections_dir) -//} -// -//#[tracing::instrument(skip(collections_dir), err)] -//pub fn get_collections

(collections_dir: P) -> anyhow::Result> -//where -// P: AsRef, -//{ -// let items = std::fs::read_dir(&collections_dir)?; -// -// let mut collections = vec![]; -// -// for item in items.into_iter().flatten() { -// let file_name = item.file_name(); -// let collection_name = collections_dir.as_ref().join(file_name); -// let file = std::fs::read_to_string(&collection_name)?; -// let mut collection: Collection = serde_json::from_str(&file)?; -// collection.path = collection_name; -// collections.push(collection); -// } -// -// collections.sort_by(|a, b| a.info.name.cmp(&b.info.name)); -// -// Ok(collections) -//} -// -//pub fn create_from_form(name: String, description: String) -> Collection { -// let name = if name.is_empty() { -// let now = time::SystemTime::now() -// .duration_since(UNIX_EPOCH) -// .unwrap() -// .as_millis(); -// format!("Unnamed Collection {}", now) -// } else { -// name -// }; -// -// let collections_dir = hac_config::get_collections_dir(); -// let name_as_file_name = name.to_lowercase().replace(' ', "_"); -// let collection_name = collections_dir.join(name_as_file_name); -// -// Collection { -// info: Info { -// name, -// description: Some(description), -// }, -// requests: None, -// path: format!("{}.json", collection_name.to_string_lossy()).into(), -// } -//} -// -//#[cfg(test)] -//mod tests { -// use super::*; -// #[test] -// fn test_creating_from_form() { -// let collection = create_from_form("any valid name".into(), "any desctiption".into()); -// -// assert!(collection.path.to_string_lossy().ends_with(".json")); -// assert!(collection.info.name.eq("any valid name")); -// assert!(collection.info.description.is_some()) -// } -//} diff --git a/hac-core/src/collection/types.rs b/hac-core/src/collection/types.rs deleted file mode 100755 index 31f65a7..0000000 --- a/hac-core/src/collection/types.rs +++ /dev/null @@ -1,289 +0,0 @@ -use std::hash::Hash; -use std::path::PathBuf; -use std::sync::{Arc, RwLock}; - -use serde::{Deserialize, Serialize}; - -/// a collection is represented as a file on the file system and holds every -/// request and metadata -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Collection { - /// basic information about the collection such as name and description - pub info: Info, - /// maybe a vector of `RequestKind` that are part of the collection - pub requests: Option>>>, - /// path is a virtual field used only during runtime to know where to - /// sync the file, this will be the absolute path to the file on the - /// users computer - #[serde(skip)] - pub path: PathBuf, -} - -/// we store requests on a collection and on directories as a enum that could -/// be either an request or a directory. This enables us to have nested -/// directories, although we don't support that now and might not ever support. -/// -/// Single means its a request -/// Nested means its a directory -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(untagged)] -pub enum RequestKind { - Single(Arc>), - Nested(Directory), -} - -impl RequestKind { - /// helper method to get the name of either a request or a directory without - /// needing to narrow the type - pub fn get_name(&self) -> String { - match self { - RequestKind::Single(req) => req.read().unwrap().name.to_string(), - RequestKind::Nested(dir) => dir.name.to_string(), - } - } - - /// helper method to know wether a request kind is a directory or not - pub fn is_dir(&self) -> bool { - match self { - RequestKind::Single(_) => false, - RequestKind::Nested(_) => true, - } - } - - /// helper method to get the id of either a request or a directory without - /// needing to narrow the type - pub fn get_id(&self) -> String { - match self { - RequestKind::Single(req) => req.read().unwrap().id.to_string(), - RequestKind::Nested(dir) => dir.id.to_string(), - } - } -} - -/// we store headers as a simple struct which is composed by a pair which -/// represents name/value of a header, and wether it is enabled or not. -/// -/// disabled headers should not be sent on requests -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct HeaderMap { - pub pair: (String, String), - pub enabled: bool, -} - -/// set of methods we currently support on HTTP requests -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)] -#[serde(rename_all = "UPPERCASE")] -pub enum RequestMethod { - Get, - Post, - Put, - Patch, - Delete, -} - -impl TryFrom for RequestMethod { - type Error = anyhow::Error; - - fn try_from(value: usize) -> anyhow::Result { - match value { - 0 => Ok(RequestMethod::Get), - 1 => Ok(RequestMethod::Post), - 2 => Ok(RequestMethod::Put), - 3 => Ok(RequestMethod::Patch), - 4 => Ok(RequestMethod::Delete), - _ => anyhow::bail!("invalid request method index"), - } - } -} - -/// this is a cyclic implementation of `next` and `prev` that are used -/// by the UI to cycle through elements, usually by using tab -impl RequestMethod { - pub fn next(&self) -> Self { - match self { - RequestMethod::Get => RequestMethod::Post, - RequestMethod::Post => RequestMethod::Put, - RequestMethod::Put => RequestMethod::Patch, - RequestMethod::Patch => RequestMethod::Delete, - RequestMethod::Delete => RequestMethod::Get, - } - } - - pub fn prev(&self) -> Self { - match self { - RequestMethod::Get => RequestMethod::Delete, - RequestMethod::Post => RequestMethod::Get, - RequestMethod::Put => RequestMethod::Post, - RequestMethod::Patch => RequestMethod::Put, - RequestMethod::Delete => RequestMethod::Patch, - } - } -} - -impl std::fmt::Display for RequestMethod { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Get => f.write_str("GET"), - Self::Post => f.write_str("POST"), - Self::Put => f.write_str("PUT"), - Self::Patch => f.write_str("PATCH"), - Self::Delete => f.write_str("DELETE"), - } - } -} - -// custom iterator implementation for RequestMethod to be able to map over -// its variants without writing a lot of boilerplate everytime -// -// NOTE: if this kind of behavior repeats a lot we might want to introduce -// a helper crate like strum -impl RequestMethod { - pub fn iter() -> std::slice::Iter<'static, RequestMethod> { - [ - RequestMethod::Get, - RequestMethod::Post, - RequestMethod::Put, - RequestMethod::Patch, - RequestMethod::Delete, - ] - .iter() - } -} - -/// This is how we store a request on the system, basically this stores all -/// needed information about a request to be able to perform any actions we -/// allow. -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Request { - /// we store an uuid on each request to be able to easily identify them - /// as identifying by name is - pub id: String, - /// each request has to have a method, this is the HTTP method that will - /// be used when realizing requests - pub method: RequestMethod, - /// name of the request that will be displayed on the sidebar - pub name: String, - /// uri that the request will be sent against - pub uri: String, - /// all headers used on given request, sometimes, we may include additional - /// headers if required to make a request - pub headers: Option>, - /// auth method used by the request, eg: Bearer or basic auth - pub auth_method: Option, - /// if this request lives as a children of a directory, the uuid of given - /// directory will be stored here, this is mainly used to know where to - /// insert or move the request - pub parent: Option, - /// body of the request, this will only be sent in methods that accept a - /// body, like POST or PUT, for example - pub body: Option, - #[serde(rename = "bodyType")] - /// the type of the body to be used, like `application/json` or any other - /// accepted body type - pub body_type: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum AuthMethod { - Bearer, - None, - A, - B, - C, - D, - E, -} - -#[derive(Default)] -pub struct AuthKindIter { - inner: u8, -} - -impl std::fmt::Display for AuthMethod { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - AuthMethod::None => write!(f, "None"), - AuthMethod::Bearer => write!(f, "Bearer"), - AuthMethod::A => write!(f, "Bearer"), - AuthMethod::B => write!(f, "Bearer"), - AuthMethod::C => write!(f, "Bearer"), - AuthMethod::D => write!(f, "Bearer"), - AuthMethod::E => write!(f, "Bearer"), - } - } -} - -impl From for AuthMethod { - fn from(value: usize) -> Self { - match value { - 0 => AuthMethod::None, - 1 => AuthMethod::Bearer, - 2 => AuthMethod::A, - 3 => AuthMethod::B, - 4 => AuthMethod::C, - 5 => AuthMethod::D, - 6 => AuthMethod::E, - _ => AuthMethod::None, - } - } -} - -impl AuthMethod { - pub fn iter() -> AuthKindIter { - AuthKindIter::default() - } - - pub fn len() -> usize { - AuthKindIter::default().count() - } -} - -impl Iterator for AuthKindIter { - type Item = AuthMethod; - - fn next(&mut self) -> Option { - let variant = match self.inner { - 0 => Some(AuthMethod::None), - 1 => Some(AuthMethod::Bearer), - 2 => Some(AuthMethod::A), - 3 => Some(AuthMethod::B), - 4 => Some(AuthMethod::C), - 5 => Some(AuthMethod::D), - 6 => Some(AuthMethod::E), - _ => None, - }; - self.inner += 1; - variant - } -} - -/// a collection of all available body types we support. -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] -pub enum BodyType { - #[serde(rename = "json")] - Json, -} - -/// a directory can hold a vector of requests, which will be -/// displayed as a tree-like view in the sidebar -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct Directory { - /// as it is the same with requests, directories have an id to make - /// easier to know relations without having to rely on references - /// and lifetimes - pub id: String, - /// name of the directory that will be used in the display on the - /// sidebar - pub name: String, - /// vector of requests that are children of this directory - pub requests: Arc>>, -} - -/// basic information about a colleciton -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] -pub struct Info { - /// name of the collection that will be displayed onscreen - pub name: String, - /// a optional description in case it is useful - pub description: Option, -} diff --git a/hac-core/src/command.rs b/hac-core/src/command.rs index 0737f88..8d28a47 100755 --- a/hac-core/src/command.rs +++ b/hac-core/src/command.rs @@ -1,4 +1,4 @@ -use crate::collection::Collection; +use hac_store::collection::Collection; #[derive(Debug)] pub enum Command { diff --git a/hac-core/src/lib.rs b/hac-core/src/lib.rs index 7acc170..db78815 100755 --- a/hac-core/src/lib.rs +++ b/hac-core/src/lib.rs @@ -1,4 +1,3 @@ -pub mod collection; pub mod command; pub mod fs; pub mod net; diff --git a/hac-core/src/net/request_client.rs b/hac-core/src/net/request_client.rs index 4401408..a6e1e5c 100644 --- a/hac-core/src/net/request_client.rs +++ b/hac-core/src/net/request_client.rs @@ -1,4 +1,4 @@ -use crate::collection::types::Request; +use hac_store::collection::Request; #[derive(Debug)] pub struct RequestClient { @@ -42,12 +42,10 @@ impl RequestClient { request: &Request, mut request_builder: reqwest::RequestBuilder, ) -> reqwest::RequestBuilder { - if let Some(ref headers) = request.headers { - for header in headers.iter().filter(|header| header.enabled) { - let header_name = header.pair.0.clone(); - let header_value = header.pair.1.clone(); - request_builder = request_builder.header(header_name, header_value); - } + for header in request.headers.iter().filter(|header| header.enabled) { + let header_name = header.key.clone(); + let header_value = header.val.clone(); + request_builder = request_builder.header(header_name, header_value); } request_builder diff --git a/hac-core/src/net/request_manager.rs b/hac-core/src/net/request_manager.rs index faad89f..27e1b0c 100755 --- a/hac-core/src/net/request_manager.rs +++ b/hac-core/src/net/request_manager.rs @@ -1,6 +1,6 @@ -use crate::collection::types::{BodyType, Request}; use crate::net::request_strategies::{http_strategy::HttpResponse, RequestStrategy}; use crate::text_object::{Readonly, TextObject}; +use hac_store::collection::{BodyKind, Request}; use std::sync::{Arc, RwLock}; use std::time::Duration; @@ -58,20 +58,14 @@ impl From<&str> for ContentType { } #[tracing::instrument(skip_all)] -pub fn handle_request(request: &Arc>, response_tx: UnboundedSender) { - let request = request.read().unwrap().clone(); +pub fn handle_request(request: Request, response_tx: UnboundedSender) { tokio::spawn(async move { - let response = match request.body_type.as_ref() { + let response = match request.body_kind { // if we dont have a body type, this is a GET request, so we use HTTP strategy - None => RequestManager::handle(HttpResponse, request).await, - Some(body_type) => match body_type { - BodyType::Json => RequestManager::handle(HttpResponse, request).await, - }, + BodyKind::NoBody => RequestManager::handle(HttpResponse, request).await, + BodyKind::Json => RequestManager::handle(HttpResponse, request).await, }; - response_tx - .send(response) - .is_err() - .then(|| std::process::abort()); + response_tx.send(response).is_err().then(|| std::process::abort()); }); } diff --git a/hac-core/src/net/request_strategies.rs b/hac-core/src/net/request_strategies.rs index f5ea4ef..92601be 100755 --- a/hac-core/src/net/request_strategies.rs +++ b/hac-core/src/net/request_strategies.rs @@ -2,7 +2,8 @@ pub mod http_strategy; use std::future::Future; -use crate::{collection::types::Request, net::request_manager::Response}; +use crate::net::request_manager::Response; +use hac_store::collection::Request; pub trait RequestStrategy { fn handle(&self, request: Request) -> impl Future; diff --git a/hac-core/src/net/request_strategies/http_strategy.rs b/hac-core/src/net/request_strategies/http_strategy.rs index 7b9be5b..242503e 100755 --- a/hac-core/src/net/request_strategies/http_strategy.rs +++ b/hac-core/src/net/request_strategies/http_strategy.rs @@ -1,8 +1,8 @@ -use crate::collection::types::{Request, RequestMethod}; use crate::net::request_client::RequestClient; use crate::net::request_manager::Response; use crate::net::request_strategies::RequestStrategy; use crate::net::response_decoders::{decoder_from_headers, ResponseDecoder}; +use hac_store::collection::{ReqMethod, Request}; pub struct HttpResponse; @@ -11,11 +11,11 @@ impl RequestStrategy for HttpResponse { let client = RequestClient::default(); match request.method { - RequestMethod::Get => self.handle_get_request(client, request).await, - RequestMethod::Post => self.handle_post_request(client, request).await, - RequestMethod::Put => self.handle_put_request(client, request).await, - RequestMethod::Patch => self.handle_patch_request(client, request).await, - RequestMethod::Delete => self.handle_delete_request(client, request).await, + ReqMethod::Get => self.handle_get_request(client, request).await, + ReqMethod::Post => self.handle_post_request(client, request).await, + ReqMethod::Put => self.handle_put_request(client, request).await, + ReqMethod::Patch => self.handle_patch_request(client, request).await, + ReqMethod::Delete => self.handle_delete_request(client, request).await, } } } @@ -45,12 +45,7 @@ impl HttpResponse { async fn handle_post_request(&self, client: RequestClient, request: Request) -> Response { let now = std::time::Instant::now(); - match client - .post(&request) - .json(&request.body.unwrap_or_default()) - .send() - .await - { + match client.post(&request).json(&request.body).send().await { Ok(response) => { let decoder = decoder_from_headers(response.headers()); decoder.decode(response, now).await @@ -72,12 +67,7 @@ impl HttpResponse { async fn handle_put_request(&self, client: RequestClient, request: Request) -> Response { let now = std::time::Instant::now(); - match client - .put(&request) - .json(&request.body.unwrap_or_default()) - .send() - .await - { + match client.put(&request).json(&request.body).send().await { Ok(response) => { let decoder = decoder_from_headers(response.headers()); decoder.decode(response, now).await @@ -99,12 +89,7 @@ impl HttpResponse { async fn handle_patch_request(&self, client: RequestClient, request: Request) -> Response { let now = std::time::Instant::now(); - match client - .patch(&request) - .json(&request.body.unwrap_or_default()) - .send() - .await - { + match client.patch(&request).json(&request.body).send().await { Ok(response) => { let decoder = decoder_from_headers(response.headers()); decoder.decode(response, now).await @@ -126,12 +111,7 @@ impl HttpResponse { async fn handle_delete_request(&self, client: RequestClient, request: Request) -> Response { let now = std::time::Instant::now(); - match client - .delete(&request) - .json(&request.body.unwrap_or_default()) - .send() - .await - { + match client.delete(&request).json(&request.body).send().await { Ok(response) => { let decoder = decoder_from_headers(response.headers()); decoder.decode(response, now).await diff --git a/hac-core/src/text_object/cursor.rs b/hac-core/src/text_object/cursor.rs index 63b8bcc..803b17b 100755 --- a/hac-core/src/text_object/cursor.rs +++ b/hac-core/src/text_object/cursor.rs @@ -42,10 +42,15 @@ impl Cursor { } pub fn move_to_line_end(&mut self, line_len: usize) { - self.col = line_len.saturating_sub(1); + self.col = line_len + 1; self.snapback_col = self.col; } + pub fn move_to(&mut self, col: usize, row: usize) { + self.move_to_row(row); + self.move_to_col(col); + } + pub fn move_to_col(&mut self, col: usize) { self.col = col; self.snapback_col = col; @@ -91,16 +96,11 @@ impl Cursor { // when moving into a bigger line (line_len > cursor.col) we make the col snap back // to the min col between line_len and the current expand_col position. pub fn maybe_snap_to_col(&mut self, line_len: usize) { - match line_len.saturating_sub(1).cmp(&self.col) { + match line_len.cmp(&self.col) { std::cmp::Ordering::Less => { - if self.snapback_col.eq(&self.col) { - self.snapback_col = self.col; - } - self.col = line_len.saturating_sub(1); - } - std::cmp::Ordering::Greater => { - self.col = self.snapback_col.min(line_len.saturating_sub(1)) + self.col = line_len + 1; } + std::cmp::Ordering::Greater => self.col = self.snapback_col.min(line_len.saturating_sub(1)), // if both expand_col and col are the same we dont have to do nothing std::cmp::Ordering::Equal => {} } diff --git a/hac-core/src/text_object/text_object.rs b/hac-core/src/text_object/text_object.rs index ed966ef..f3a8cbb 100755 --- a/hac-core/src/text_object/text_object.rs +++ b/hac-core/src/text_object/text_object.rs @@ -1,11 +1,12 @@ -use crate::{syntax::highlighter::Highlighter, text_object::cursor::Cursor}; - use std::collections::HashMap; use std::ops::{Add, Sub}; use ropey::Rope; use tree_sitter::Tree; +use crate::syntax::highlighter::Highlighter; +use crate::text_object::cursor::Cursor; + #[derive(Debug, PartialEq, Clone)] pub enum LineBreak { Lf, @@ -46,7 +47,7 @@ impl Default for TextObject { fn default() -> Self { let content = String::default(); - TextObject { + Self { content: Rope::from_str(&content), state: std::marker::PhantomData, line_break: LineBreak::Lf, @@ -87,8 +88,7 @@ impl TextObject { pub fn insert_newline(&mut self, cursor: &Cursor) { let line = self.content.line_to_char(cursor.row()); let col_offset = line + cursor.col(); - self.content - .insert(col_offset, &self.line_break.to_string()); + self.content.insert(col_offset, &self.line_break.to_string()); } pub fn erase_backwards_up_to_line_start(&mut self, cursor: &Cursor) { @@ -97,17 +97,13 @@ impl TextObject { } let line = self.content.line_to_char(cursor.row()); let col_offset = line + cursor.col(); - self.content - .try_remove(col_offset.saturating_sub(1)..col_offset) - .ok(); + self.content.try_remove(col_offset.saturating_sub(1)..col_offset).ok(); } pub fn erase_previous_char(&mut self, cursor: &Cursor) { let line = self.content.line_to_char(cursor.row()); let col_offset = line + cursor.col(); - self.content - .try_remove(col_offset.saturating_sub(1)..col_offset) - .ok(); + self.content.try_remove(col_offset.saturating_sub(1)..col_offset).ok(); } pub fn erase_current_char(&mut self, cursor: &Cursor) { @@ -140,9 +136,12 @@ impl TextObject { let line = self.content.line_to_char(cursor.row()); let next_line = self.content.line_to_char(cursor.row().add(1)); let col_offset = line + cursor.col(); - self.content - .try_remove(col_offset..next_line.saturating_sub(1)) - .ok(); + self.content.try_remove(col_offset..next_line.saturating_sub(1)).ok(); + } + + pub fn clear_line(&mut self, cursor: &Cursor) { + let line = self.content.line_to_char(cursor.row()); + self.content.try_remove(line..line + cursor.col()).ok(); } pub fn find_char_after_whitespace(&self, cursor: &Cursor) -> (usize, usize) { @@ -198,11 +197,7 @@ impl TextObject { if let Some(initial_char) = self.content.get_char(start_idx) { for char in self.content.chars_at(start_idx) { - match ( - initial_char.is_alphanumeric(), - char.is_alphanumeric(), - found_newline, - ) { + match (initial_char.is_alphanumeric(), char.is_alphanumeric(), found_newline) { (_, _, true) if !char.is_whitespace() => break, (false, true, _) => break, (true, false, _) => break, @@ -232,11 +227,7 @@ impl TextObject { for _ in (0..start_idx.saturating_sub(1)).rev() { let char = self.content.char(end_idx); - match ( - initial_char.is_alphanumeric(), - char.is_alphanumeric(), - found_newline, - ) { + match (initial_char.is_alphanumeric(), char.is_alphanumeric(), found_newline) { (_, _, true) if !self.line_break.to_string().contains(char) => break, (false, true, _) => break, (true, false, _) => break, @@ -392,20 +383,12 @@ impl TextObject { _ => {} } - let range = if look_forward { - start_idx.add(1)..self.content.len_chars() - } else { - 0..start_idx - }; + let range = if look_forward { start_idx.add(1)..self.content.len_chars() } else { 0..start_idx }; for i in range { let char = self .content - .get_char(if look_forward { - i - } else { - start_idx - walked - 1 - }) + .get_char(if look_forward { i } else { start_idx - walked - 1 }) .unwrap_or_default(); if token_to_search.eq(&char::default()) { @@ -419,8 +402,7 @@ impl TextObject { char.eq(combinations.get(&token_to_search).unwrap()) .then(|| curr_open = curr_open.add(1)); - char.eq(&token_to_search) - .then(|| curr_open = curr_open.sub(1)); + char.eq(&token_to_search).then(|| curr_open = curr_open.sub(1)); walked = walked.add(1); diff --git a/hac-loader/Cargo.toml b/hac-loader/Cargo.toml index dc4fa1c..17346d7 100644 --- a/hac-loader/Cargo.toml +++ b/hac-loader/Cargo.toml @@ -13,3 +13,4 @@ anyhow.workspace = true tracing.workspace = true dirs = "5.0.1" chrono = "0.4.38" +notify = "6.1.1" diff --git a/hac-loader/src/collection_loader/json_collection.rs b/hac-loader/src/collection_loader/json_collection.rs index 694aa2c..1399318 100644 --- a/hac-loader/src/collection_loader/json_collection.rs +++ b/hac-loader/src/collection_loader/json_collection.rs @@ -1,9 +1,9 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Default)] -struct JsonInfo { - name: String, - path: std::path::PathBuf, +pub struct JsonInfo { + pub name: String, + pub path: std::path::PathBuf, } #[derive(Debug, Serialize, Deserialize)] @@ -14,6 +14,19 @@ pub struct JsonCollection { pub file_info: JsonInfo, } +impl JsonCollection { + pub fn new(name: String, description: String, file_name: String, path: &std::path::PathBuf) -> Self { + Self { + info: JsonCollectionInfo { name, description }, + file_info: JsonInfo { + name: file_name, + path: path.clone(), + }, + requests: Default::default(), + } + } +} + #[derive(Debug, Serialize, Deserialize)] pub struct JsonCollectionInfo { pub name: String, diff --git a/hac-loader/src/collection_loader/mod.rs b/hac-loader/src/collection_loader/mod.rs index 1c4e1f2..1c0e0a1 100644 --- a/hac-loader/src/collection_loader/mod.rs +++ b/hac-loader/src/collection_loader/mod.rs @@ -5,8 +5,15 @@ use chrono::{Datelike, Timelike}; use hac_config::config::CollectionExtensions; use hac_store::collection::Collection; use json_loader::JsonLoader; +use notify::{RecursiveMode, Watcher}; +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::SystemTime; +static HAS_CHANGES: AtomicBool = AtomicBool::new(false); +static HAS_WATCHER: AtomicBool = AtomicBool::new(false); + pub trait IntoCollection { fn into_collection(self) -> hac_store::collection::Collection; } @@ -32,6 +39,103 @@ pub fn load_collection>( } } +fn sanitize_filename(name: &str) -> String { + // TODO: find a better way to do this + let forbidden_chars = ['/', '\\', '?', '%', '*', ':', '|', '"', '<', '>', '.']; + name.chars() + .map(|c| if forbidden_chars.contains(&c) { '_' } else { c }) + .collect() +} + +fn create_persistent_colletion(name: String) -> anyhow::Result<()> { + let file_name = format!("{}.json", sanitize_filename(&name)); + let path = super::collections_dir().join(&file_name); + tracing::debug!("{path:?}"); + let collection = json_collection::JsonCollection::new(name, Default::default(), file_name, &path); + std::fs::write(&path, serde_json::to_string_pretty(&collection)?)?; + Ok(()) +} + +pub fn has_changes() -> bool { + HAS_CHANGES.load(Ordering::Relaxed) +} + +fn set_watcher() -> anyhow::Result<()> { + if HAS_WATCHER.load(Ordering::Relaxed) { + return Ok(()); + } + + std::thread::spawn(move || { + let mut watcher = notify::recommended_watcher(|res| match res { + Ok(_) => HAS_CHANGES.store(true, Ordering::Relaxed), + Err(_) => todo!(), + }) + .expect("failed to set watcher"); + let collections_dir = super::collections_dir(); + watcher + .watch(&collections_dir, RecursiveMode::NonRecursive) + .expect("failed to watch collections dir"); + + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + } + }); + + Ok(()) +} + +fn create_virtual_collection( + name: String, + mut collections: Vec, +) -> anyhow::Result> { + let file_name = format!("{}.json", sanitize_filename(&name)); + let size = name.len(); + let metadata = CollectionMeta::new(name, file_name.into(), size as u64); + collections.push(metadata); + Ok(collections) +} + +pub fn create_collection( + name: String, + collections: Vec, + config: &Rc>, +) -> anyhow::Result> { + match config.borrow().dry_run { + false => { + create_persistent_colletion(name)?; + collections_metadata() + } + true => create_virtual_collection(name, collections), + } +} + +fn delete_persistent_collection(file_name: String, collections: Vec) -> anyhow::Result<()> { + let path = collections + .iter() + .find(|c| c.path().to_string_lossy().contains(&file_name)) + .expect("attempted to delete non-existing collection") + .path(); + std::fs::remove_file(path)?; + Ok(()) +} + +pub fn delete_collection( + file_name: String, + mut collections: Vec, + config: &Rc>, +) -> anyhow::Result> { + match config.borrow().dry_run { + false => { + delete_persistent_collection(file_name, collections)?; + collections_metadata() + } + true => { + collections.retain(|c| !c.path().to_string_lossy().contains(&file_name)); + Ok(collections) + } + } +} + // TODO: i probably want to introduce some metadata storage, or caching to know // total requests, total saved responses, and other infos without having to read // the entire collection. @@ -53,6 +157,18 @@ pub struct CollectionModifiedMeta { system_time: SystemTime, } +impl CollectionModifiedMeta { + pub fn new() -> Self { + std::time::SystemTime::now().into() + } +} + +impl Default for CollectionModifiedMeta { + fn default() -> Self { + Self::new() + } +} + impl PartialOrd for CollectionModifiedMeta { fn partial_cmp(&self, other: &Self) -> Option { Some(self.system_time.cmp(&other.system_time)) @@ -76,6 +192,15 @@ impl std::fmt::Display for CollectionModifiedMeta { } impl CollectionMeta { + pub fn new(name: String, path: std::path::PathBuf, size: u64) -> Self { + Self { + name, + path, + size, + modified: CollectionModifiedMeta::new(), + } + } + pub fn name(&self) -> &str { &self.name } @@ -119,32 +244,35 @@ impl ReadableByteSize for u64 { } } -fn format_modified_date(modified_time: SystemTime) -> anyhow::Result { - let datetime = chrono::DateTime::::from(modified_time); - let year = datetime.year(); - let month = datetime.day(); - let day = datetime.month(); - let hour = datetime.hour(); - let minutes = datetime.minute(); - Ok(CollectionModifiedMeta { - year, - month, - day, - hour, - minutes, - system_time: modified_time, - }) +impl From for CollectionModifiedMeta { + fn from(value: SystemTime) -> Self { + let datetime = chrono::DateTime::::from(value); + let year = datetime.year(); + let month = datetime.day(); + let day = datetime.month(); + let hour = datetime.hour(); + let minutes = datetime.minute(); + + CollectionModifiedMeta { + year, + month, + day, + hour, + minutes, + system_time: value, + } + } } pub fn collections_metadata() -> anyhow::Result> { + set_watcher()?; let entries = std::fs::read_dir(super::collections_dir())?.flatten(); - let mut collections = vec![]; for entry in entries { let name = entry.file_name().to_string_lossy().to_string(); let path = entry.path(); let metadata = entry.metadata()?; - let modified = format_modified_date(metadata.modified()?)?; + let modified = metadata.modified()?.into(); let size = metadata.len(); collections.push(CollectionMeta { name, @@ -154,5 +282,7 @@ pub fn collections_metadata() -> anyhow::Result> { }); } + HAS_CHANGES.store(false, Ordering::Relaxed); + Ok(collections) } From ecac463859f7052114646feac64bd31c5e367b1e Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Tue, 24 Sep 2024 09:35:54 -0300 Subject: [PATCH 06/23] wip: omiting errors --- hac-client/src/pages.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hac-client/src/pages.rs b/hac-client/src/pages.rs index 97d7d64..30397ae 100755 --- a/hac-client/src/pages.rs +++ b/hac-client/src/pages.rs @@ -1,5 +1,5 @@ pub mod collection_list; -pub mod collection_viewer; +//pub mod collection_viewer; pub mod input; pub mod overlay; mod spinner; From cdae6de7e1712dd6b0d74d373c1513aac2aa3a43 Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Wed, 25 Sep 2024 11:18:29 -0300 Subject: [PATCH 07/23] refactor: finally getting the router to a state I like --- hac-client/Cargo.toml | 4 - hac-client/benches/collection_viewer_bench.rs | 156 --------------- hac-client/src/pages.rs | 15 +- .../collection_list/create_collection.rs | 34 ++-- hac-client/src/pages/collection_list/mod.rs | 66 ++++--- hac-client/src/pages/terminal_too_small.rs | 7 + hac-client/src/pages/under_construction.rs | 19 +- hac-client/src/router.rs | 181 +++++++++++++----- 8 files changed, 216 insertions(+), 266 deletions(-) delete mode 100755 hac-client/benches/collection_viewer_bench.rs diff --git a/hac-client/Cargo.toml b/hac-client/Cargo.toml index 49107c4..8d831bb 100644 --- a/hac-client/Cargo.toml +++ b/hac-client/Cargo.toml @@ -47,7 +47,3 @@ name = "hac" test = false bench = false path = "src/main.rs" - -[[bench]] -name = "collection_viewer_bench" -harness = false diff --git a/hac-client/benches/collection_viewer_bench.rs b/hac-client/benches/collection_viewer_bench.rs deleted file mode 100755 index 6af468b..0000000 --- a/hac-client/benches/collection_viewer_bench.rs +++ /dev/null @@ -1,156 +0,0 @@ -use hac_core::collection::types::{BodyType, Info, Request, RequestKind, RequestMethod}; -use hac_core::collection::Collection; -use hac_core::syntax::highlighter::Highlighter; - -use hac_client::pages::collection_viewer::{collection_store::CollectionStore, CollectionViewer}; -use hac_client::pages::{Eventful, Renderable}; -use hac_client::utils::build_syntax_highlighted_lines; - -use std::cell::RefCell; -use std::rc::Rc; -use std::sync::{Arc, RwLock}; - -use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers}; -use lazy_static::lazy_static; -use ratatui::{backend::TestBackend, layout::Rect, Terminal}; -use tree_sitter::Tree; - -fn main() { - divan::main(); -} - -fn create_sample_collection() -> Collection { - Collection { - info: Info { - name: "sample collection".to_string(), - description: None, - }, - path: "any_path".into(), - requests: Some(Arc::new(RwLock::new(vec![ - RequestKind::Single(Arc::new(RwLock::new(Request { - id: "any id".to_string(), - headers: None, - name: "testing".to_string(), - parent: None, - auth_method: None, - uri: "https://jsonplaceholder.typicode.com/users".to_string(), - method: RequestMethod::Get, - body: Some("[\r\n {\r\n \"id\": 1,\r\n \"name\": \"Leanne Graham\",\r\n \"username\": \"Bret\",\r\n \"email\": \"Sincere@april.biz\",\r\n \"address\": {\r\n \"street\": \"Kulas Light\",\r\n \"suite\": \"Apt. 556\",\r\n \"city\": \"Gwenborough\",\r\n \"zipcode\": \"92998-3874\",\r\n \"geo\": {\r\n \"lat\": \"-37.3159\",\r\n \"lng\": \"81.1496\"\r\n }\r\n },\r\n \"phone\": \"1-770-736-8031 x56442\",\r\n \"website\": \"hildegard.org\",\r\n \"company\": {\r\n \"name\": \"Romaguera-Crona\",\r\n \"catchPhrase\": \"Multi-layered client-server neural-net\",\r\n \"bs\": \"harness real-time e-markets\"\r\n }\r\n },\r\n {\r\n \"id\": 2,\r\n \"name\": \"Ervin Howell\",\r\n \"username\": \"Antonette\",\r\n \"email\": \"Shanna@melissa.tv\",\r\n \"address\": {\r\n \"street\": \"Victor Plains\",\r\n \"suite\": \"Suite 879\",\r\n \"city\": \"Wisokyburgh\",\r\n \"zipcode\": \"90566-7771\",\r\n \"geo\": {\r\n \"lat\": \"-43.9509\",\r\n \"lng\": \"-34.4618\"\r\n }\r\n },\r\n \"phone\": \"010-692-6593 x09125\",\r\n \"website\": \"anastasia.net\",\r\n \"company\": {\r\n \"name\": \"Deckow-Crist\",\r\n \"catchPhrase\": \"Proactive didactic contingency\",\r\n \"bs\": \"synergize scalable supply-chains\"\r\n }\r\n },\r\n {\r\n \"id\": 3,\r\n \"name\": \"Clementine Bauch\",\r\n \"username\": \"Samantha\",\r\n \"email\": \"Nathan@yesenia.net\",\r\n \"address\": {\r\n \"street\": \"Douglas Extension\",\r\n \"suite\": \"Suite 847\",\r\n \"city\": \"McKenziehaven\",\r\n \"zipcode\": \"59590-4157\",\r\n \"geo\": {\r\n \"lat\": \"-68.6102\",\r\n \"lng\": \"-47.0653\"\r\n }\r\n },\r\n \"phone\": \"1-463-123-4447\",\r\n \"website\": \"ramiro.info\",\r\n \"company\": {\r\n \"name\": \"Romaguera-Jacobson\",\r\n \"catchPhrase\": \"Face to face bifurcated interface\",\r\n \"bs\": \"e-enable strategic applications\"\r\n }\r\n },\r\n {\r\n \"id\": 4,\r\n \"name\": \"Patricia Lebsack\",\r\n \"username\": \"Karianne\",\r\n \"email\": \"Julianne.OConner@kory.org\",\r\n \"address\": {\r\n \"street\": \"Hoeger Mall\",\r\n \"suite\": \"Apt. 692\",\r\n \"city\": \"South Elvis\",\r\n \"zipcode\": \"53919-4257\",\r\n \"geo\": {\r\n \"lat\": \"29.4572\",\r\n \"lng\": \"-164.2990\"\r\n }\r\n },\r\n \"phone\": \"493-170-9623 x156\",\r\n \"website\": \"kale.biz\",\r\n \"company\": {\r\n \"name\": \"Robel-Corkery\",\r\n \"catchPhrase\": \"Multi-tiered zero tolerance productivity\",\r\n \"bs\": \"transition cutting-edge web services\"\r\n }\r\n },\r\n {\r\n \"id\": 5,\r\n \"name\": \"Chelsey Dietrich\",\r\n \"username\": \"Kamren\",\r\n \"email\": \"Lucio_Hettinger@annie.ca\",\r\n \"address\": {\r\n \"street\": \"Skiles Walks\",\r\n \"suite\": \"Suite 351\",\r\n \"city\": \"Roscoeview\",\r\n \"zipcode\": \"33263\",\r\n \"geo\": {\r\n \"lat\": \"-31.8129\",\r\n \"lng\": \"62.5342\"\r\n }\r\n },\r\n \"phone\": \"(254)954-1289\",\r\n \"website\": \"demarco.info\",\r\n \"company\": {\r\n \"name\": \"Keebler LLC\",\r\n \"catchPhrase\": \"User-centric fault-tolerant solution\",\r\n \"bs\": \"revolutionize end-to-end systems\"\r\n }\r\n },\r\n {\r\n \"id\": 6,\r\n \"name\": \"Mrs. Dennis Schulist\",\r\n \"username\": \"Leopoldo_Corkery\",\r\n \"email\": \"Karley_Dach@jasper.info\",\r\n \"address\": {\r\n \"street\": \"Norberto Crossing\",\r\n \"suite\": \"Apt. 950\",\r\n \"city\": \"South Christy\",\r\n \"zipcode\": \"23505-1337\",\r\n \"geo\": {\r\n \"lat\": \"-71.4197\",\r\n \"lng\": \"71.7478\"\r\n }\r\n },\r\n \"phone\": \"1-477-935-8478 x6430\",\r\n \"website\": \"ola.org\",\r\n \"company\": {\r\n \"name\": \"Considine-Lockman\",\r\n \"catchPhrase\": \"Synchronised bottom-line interface\",\r\n \"bs\": \"e-enable innovative applications\"\r\n }\r\n },\r\n {\r\n \"id\": 7,\r\n \"name\": \"Kurtis Weissnat\",\r\n \"username\": \"Elwyn.Skiles\",\r\n \"email\": \"Telly.Hoeger@billy.biz\",\r\n \"address\": {\r\n \"street\": \"Rex Trail\",\r\n \"suite\": \"Suite 280\",\r\n \"city\": \"Howemouth\",\r\n \"zipcode\": \"58804-1099\",\r\n \"geo\": {\r\n \"lat\": \"24.8918\",\r\n \"lng\": \"21.8984\"\r\n }\r\n },\r\n \"phone\": \"210.067.6132\",\r\n \"website\": \"elvis.io\",\r\n \"company\": {\r\n \"name\": \"Johns Group\",\r\n \"catchPhrase\": \"Configurable multimedia task-force\",\r\n \"bs\": \"generate enterprise e-tailers\"\r\n }\r\n },\r\n {\r\n \"id\": 8,\r\n \"name\": \"Nicholas Runolfsdottir V\",\r\n \"username\": \"Maxime_Nienow\",\r\n \"email\": \"Sherwood@rosamond.me\",\r\n \"address\": {\r\n \"street\": \"Ellsworth Summit\",\r\n \"suite\": \"Suite 729\",\r\n \"city\": \"Aliyaview\",\r\n \"zipcode\": \"45169\",\r\n \"geo\": {\r\n \"lat\": \"-14.3990\",\r\n \"lng\": \"-120.7677\"\r\n }\r\n },\r\n \"phone\": \"586.493.6943 x140\",\r\n \"website\": \"jacynthe.com\",\r\n \"company\": {\r\n \"name\": \"Abernathy Group\",\r\n \"catchPhrase\": \"Implemented secondary concept\",\r\n \"bs\": \"e-enable extensible e-tailers\"\r\n }\r\n },\r\n {\r\n \"id\": 9,\r\n \"name\": \"Glenna Reichert\",\r\n \"username\": \"Delphine\",\r\n \"email\": \"Chaim_McDermott@dana.io\",\r\n \"address\": {\r\n \"street\": \"Dayna Park\",\r\n \"suite\": \"Suite 449\",\r\n \"city\": \"Bartholomebury\",\r\n \"zipcode\": \"76495-3109\",\r\n \"geo\": {\r\n \"lat\": \"24.6463\",\r\n \"lng\": \"-168.8889\"\r\n }\r\n },\r\n \"phone\": \"(775)976-6794 x41206\",\r\n \"website\": \"conrad.com\",\r\n \"company\": {\r\n \"name\": \"Yost and Sons\",\r\n \"catchPhrase\": \"Switchable contextually-based project\",\r\n \"bs\": \"aggregate real-time technologies\"\r\n }\r\n },\r\n {\r\n \"id\": 10,\r\n \"name\": \"Clementina DuBuque\",\r\n \"username\": \"Moriah.Stanton\",\r\n \"email\": \"Rey.Padberg@karina.biz\",\r\n \"address\": {\r\n \"street\": \"Kattie Turnpike\",\r\n \"suite\": \"Suite 198\",\r\n \"city\": \"Lebsackbury\",\r\n \"zipcode\": \"31428-2261\",\r\n \"geo\": {\r\n \"lat\": \"-38.2386\",\r\n \"lng\": \"57.2232\"\r\n }\r\n },\r\n \"phone\": \"024-648-3804\",\r\n \"website\": \"ambrose.net\",\r\n \"company\": {\r\n \"name\": \"Hoeger LLC\",\r\n \"catchPhrase\": \"Centralized empowering task-force\",\r\n \"bs\": \"target end-to-end models\"\r\n }\r\n }\r\n]".to_string()), - body_type: Some(BodyType::Json), - }))), - RequestKind::Single(Arc::new(RwLock::new(Request { - id: "any_other_id".to_string(), - name: "testing".to_string(), - auth_method: None, - uri: "https://jsonplaceholder.typicode.com/users".to_string(), - method: RequestMethod::Get, - parent: None, - headers: None, - body: Some("[\r\n {\r\n \"id\": 1,\r\n \"name\": \"Leanne Graham\",\r\n \"username\": \"Bret\",\r\n \"email\": \"Sincere@april.biz\",\r\n \"address\": {\r\n \"street\": \"Kulas Light\",\r\n \"suite\": \"Apt. 556\",\r\n \"city\": \"Gwenborough\",\r\n \"zipcode\": \"92998-3874\",\r\n \"geo\": {\r\n \"lat\": \"-37.3159\",\r\n \"lng\": \"81.1496\"\r\n }\r\n },\r\n \"phone\": \"1-770-736-8031 x56442\",\r\n \"website\": \"hildegard.org\",\r\n \"company\": {\r\n \"name\": \"Romaguera-Crona\",\r\n \"catchPhrase\": \"Multi-layered client-server neural-net\",\r\n \"bs\": \"harness real-time e-markets\"\r\n }\r\n },\r\n {\r\n \"id\": 2,\r\n \"name\": \"Ervin Howell\",\r\n \"username\": \"Antonette\",\r\n \"email\": \"Shanna@melissa.tv\",\r\n \"address\": {\r\n \"street\": \"Victor Plains\",\r\n \"suite\": \"Suite 879\",\r\n \"city\": \"Wisokyburgh\",\r\n \"zipcode\": \"90566-7771\",\r\n \"geo\": {\r\n \"lat\": \"-43.9509\",\r\n \"lng\": \"-34.4618\"\r\n }\r\n },\r\n \"phone\": \"010-692-6593 x09125\",\r\n \"website\": \"anastasia.net\",\r\n \"company\": {\r\n \"name\": \"Deckow-Crist\",\r\n \"catchPhrase\": \"Proactive didactic contingency\",\r\n \"bs\": \"synergize scalable supply-chains\"\r\n }\r\n },\r\n {\r\n \"id\": 3,\r\n \"name\": \"Clementine Bauch\",\r\n \"username\": \"Samantha\",\r\n \"email\": \"Nathan@yesenia.net\",\r\n \"address\": {\r\n \"street\": \"Douglas Extension\",\r\n \"suite\": \"Suite 847\",\r\n \"city\": \"McKenziehaven\",\r\n \"zipcode\": \"59590-4157\",\r\n \"geo\": {\r\n \"lat\": \"-68.6102\",\r\n \"lng\": \"-47.0653\"\r\n }\r\n },\r\n \"phone\": \"1-463-123-4447\",\r\n \"website\": \"ramiro.info\",\r\n \"company\": {\r\n \"name\": \"Romaguera-Jacobson\",\r\n \"catchPhrase\": \"Face to face bifurcated interface\",\r\n \"bs\": \"e-enable strategic applications\"\r\n }\r\n },\r\n {\r\n \"id\": 4,\r\n \"name\": \"Patricia Lebsack\",\r\n \"username\": \"Karianne\",\r\n \"email\": \"Julianne.OConner@kory.org\",\r\n \"address\": {\r\n \"street\": \"Hoeger Mall\",\r\n \"suite\": \"Apt. 692\",\r\n \"city\": \"South Elvis\",\r\n \"zipcode\": \"53919-4257\",\r\n \"geo\": {\r\n \"lat\": \"29.4572\",\r\n \"lng\": \"-164.2990\"\r\n }\r\n },\r\n \"phone\": \"493-170-9623 x156\",\r\n \"website\": \"kale.biz\",\r\n \"company\": {\r\n \"name\": \"Robel-Corkery\",\r\n \"catchPhrase\": \"Multi-tiered zero tolerance productivity\",\r\n \"bs\": \"transition cutting-edge web services\"\r\n }\r\n },\r\n {\r\n \"id\": 5,\r\n \"name\": \"Chelsey Dietrich\",\r\n \"username\": \"Kamren\",\r\n \"email\": \"Lucio_Hettinger@annie.ca\",\r\n \"address\": {\r\n \"street\": \"Skiles Walks\",\r\n \"suite\": \"Suite 351\",\r\n \"city\": \"Roscoeview\",\r\n \"zipcode\": \"33263\",\r\n \"geo\": {\r\n \"lat\": \"-31.8129\",\r\n \"lng\": \"62.5342\"\r\n }\r\n },\r\n \"phone\": \"(254)954-1289\",\r\n \"website\": \"demarco.info\",\r\n \"company\": {\r\n \"name\": \"Keebler LLC\",\r\n \"catchPhrase\": \"User-centric fault-tolerant solution\",\r\n \"bs\": \"revolutionize end-to-end systems\"\r\n }\r\n },\r\n {\r\n \"id\": 6,\r\n \"name\": \"Mrs. Dennis Schulist\",\r\n \"username\": \"Leopoldo_Corkery\",\r\n \"email\": \"Karley_Dach@jasper.info\",\r\n \"address\": {\r\n \"street\": \"Norberto Crossing\",\r\n \"suite\": \"Apt. 950\",\r\n \"city\": \"South Christy\",\r\n \"zipcode\": \"23505-1337\",\r\n \"geo\": {\r\n \"lat\": \"-71.4197\",\r\n \"lng\": \"71.7478\"\r\n }\r\n },\r\n \"phone\": \"1-477-935-8478 x6430\",\r\n \"website\": \"ola.org\",\r\n \"company\": {\r\n \"name\": \"Considine-Lockman\",\r\n \"catchPhrase\": \"Synchronised bottom-line interface\",\r\n \"bs\": \"e-enable innovative applications\"\r\n }\r\n },\r\n {\r\n \"id\": 7,\r\n \"name\": \"Kurtis Weissnat\",\r\n \"username\": \"Elwyn.Skiles\",\r\n \"email\": \"Telly.Hoeger@billy.biz\",\r\n \"address\": {\r\n \"street\": \"Rex Trail\",\r\n \"suite\": \"Suite 280\",\r\n \"city\": \"Howemouth\",\r\n \"zipcode\": \"58804-1099\",\r\n \"geo\": {\r\n \"lat\": \"24.8918\",\r\n \"lng\": \"21.8984\"\r\n }\r\n },\r\n \"phone\": \"210.067.6132\",\r\n \"website\": \"elvis.io\",\r\n \"company\": {\r\n \"name\": \"Johns Group\",\r\n \"catchPhrase\": \"Configurable multimedia task-force\",\r\n \"bs\": \"generate enterprise e-tailers\"\r\n }\r\n },\r\n {\r\n \"id\": 8,\r\n \"name\": \"Nicholas Runolfsdottir V\",\r\n \"username\": \"Maxime_Nienow\",\r\n \"email\": \"Sherwood@rosamond.me\",\r\n \"address\": {\r\n \"street\": \"Ellsworth Summit\",\r\n \"suite\": \"Suite 729\",\r\n \"city\": \"Aliyaview\",\r\n \"zipcode\": \"45169\",\r\n \"geo\": {\r\n \"lat\": \"-14.3990\",\r\n \"lng\": \"-120.7677\"\r\n }\r\n },\r\n \"phone\": \"586.493.6943 x140\",\r\n \"website\": \"jacynthe.com\",\r\n \"company\": {\r\n \"name\": \"Abernathy Group\",\r\n \"catchPhrase\": \"Implemented secondary concept\",\r\n \"bs\": \"e-enable extensible e-tailers\"\r\n }\r\n },\r\n {\r\n \"id\": 9,\r\n \"name\": \"Glenna Reichert\",\r\n \"username\": \"Delphine\",\r\n \"email\": \"Chaim_McDermott@dana.io\",\r\n \"address\": {\r\n \"street\": \"Dayna Park\",\r\n \"suite\": \"Suite 449\",\r\n \"city\": \"Bartholomebury\",\r\n \"zipcode\": \"76495-3109\",\r\n \"geo\": {\r\n \"lat\": \"24.6463\",\r\n \"lng\": \"-168.8889\"\r\n }\r\n },\r\n \"phone\": \"(775)976-6794 x41206\",\r\n \"website\": \"conrad.com\",\r\n \"company\": {\r\n \"name\": \"Yost and Sons\",\r\n \"catchPhrase\": \"Switchable contextually-based project\",\r\n \"bs\": \"aggregate real-time technologies\"\r\n }\r\n },\r\n {\r\n \"id\": 10,\r\n \"name\": \"Clementina DuBuque\",\r\n \"username\": \"Moriah.Stanton\",\r\n \"email\": \"Rey.Padberg@karina.biz\",\r\n \"address\": {\r\n \"street\": \"Kattie Turnpike\",\r\n \"suite\": \"Suite 198\",\r\n \"city\": \"Lebsackbury\",\r\n \"zipcode\": \"31428-2261\",\r\n \"geo\": {\r\n \"lat\": \"-38.2386\",\r\n \"lng\": \"57.2232\"\r\n }\r\n },\r\n \"phone\": \"024-648-3804\",\r\n \"website\": \"ambrose.net\",\r\n \"company\": {\r\n \"name\": \"Hoeger LLC\",\r\n \"catchPhrase\": \"Centralized empowering task-force\",\r\n \"bs\": \"target end-to-end models\"\r\n }\r\n }\r\n]".to_string()), - body_type: Some(BodyType::Json), - }))), - ]))) - } -} - -fn feed_keys(widget: &mut CollectionViewer, key_codes: Vec) { - key_codes.into_iter().for_each(|code| { - widget - .handle_key_event(KeyEvent { - code, - modifiers: KeyModifiers::NONE, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) - .unwrap(); - }); -} - -#[divan::bench] -fn handling_key_events() { - let colors = hac_colors::Colors::default(); - let collection = create_sample_collection(); - let size = Rect::new(0, 0, 80, 24); - let config = hac_config::load_config(); - let mut store = CollectionStore::default(); - store.set_state(collection); - let mut api_explorer = - CollectionViewer::new(size, Rc::new(RefCell::new(store)), &colors, &config, false); - let mut terminal = Terminal::new(TestBackend::new(size.width, size.height)).unwrap(); - let mut frame = terminal.get_frame(); - - api_explorer.draw(&mut frame, size).unwrap(); - feed_keys( - &mut api_explorer, - vec![ - KeyCode::Char('j'), - KeyCode::Char('j'), - KeyCode::Char('j'), - KeyCode::Char('j'), - KeyCode::Char('j'), - KeyCode::Char('j'), - KeyCode::Char('j'), - ], - ); - api_explorer.draw(&mut frame, size).unwrap(); -} - -#[divan::bench] -fn creating_with_highlight() { - let colors = hac_colors::Colors::default(); - let collection = create_sample_collection(); - let size = Rect::new(0, 0, 80, 24); - let config = hac_config::load_config(); - let mut store = CollectionStore::default(); - store.set_state(collection); - let mut api_explorer = - CollectionViewer::new(size, Rc::new(RefCell::new(store)), &colors, &config, false); - let mut terminal = Terminal::new(TestBackend::new(size.width, size.height)).unwrap(); - let _frame = terminal.get_frame(); - - // we are simulating changing the active request 12 times, which will rebuild the highlight - // tree. - feed_keys( - &mut api_explorer, - vec![ - KeyCode::Tab, - KeyCode::Tab, - KeyCode::Tab, - KeyCode::Char('j'), - KeyCode::Enter, - KeyCode::Char('k'), - KeyCode::Enter, - KeyCode::Char('j'), - KeyCode::Enter, - KeyCode::Char('k'), - KeyCode::Enter, - KeyCode::Char('j'), - KeyCode::Enter, - KeyCode::Char('k'), - KeyCode::Enter, - KeyCode::Char('j'), - KeyCode::Enter, - KeyCode::Char('k'), - KeyCode::Enter, - KeyCode::Char('j'), - KeyCode::Enter, - KeyCode::Char('k'), - KeyCode::Enter, - KeyCode::Char('j'), - KeyCode::Enter, - KeyCode::Char('k'), - KeyCode::Enter, - ], - ); -} - -lazy_static! { - static ref BODY: &'static str = "[\r\n {\r\n \"id\": 1,\r\n \"name\": \"Leanne Graham\",\r\n \"username\": \"Bret\",\r\n \"email\": \"Sincere@april.biz\",\r\n \"address\": {\r\n \"street\": \"Kulas Light\",\r\n \"suite\": \"Apt. 556\",\r\n \"city\": \"Gwenborough\",\r\n \"zipcode\": \"92998-3874\",\r\n \"geo\": {\r\n \"lat\": \"-37.3159\",\r\n \"lng\": \"81.1496\"\r\n }\r\n },\r\n \"phone\": \"1-770-736-8031 x56442\",\r\n \"website\": \"hildegard.org\",\r\n \"company\": {\r\n \"name\": \"Romaguera-Crona\",\r\n \"catchPhrase\": \"Multi-layered client-server neural-net\",\r\n \"bs\": \"harness real-time e-markets\"\r\n }\r\n },\r\n {\r\n \"id\": 2,\r\n \"name\": \"Ervin Howell\",\r\n \"username\": \"Antonette\",\r\n \"email\": \"Shanna@melissa.tv\",\r\n \"address\": {\r\n \"street\": \"Victor Plains\",\r\n \"suite\": \"Suite 879\",\r\n \"city\": \"Wisokyburgh\",\r\n \"zipcode\": \"90566-7771\",\r\n \"geo\": {\r\n \"lat\": \"-43.9509\",\r\n \"lng\": \"-34.4618\"\r\n }\r\n },\r\n \"phone\": \"010-692-6593 x09125\",\r\n \"website\": \"anastasia.net\",\r\n \"company\": {\r\n \"name\": \"Deckow-Crist\",\r\n \"catchPhrase\": \"Proactive didactic contingency\",\r\n \"bs\": \"synergize scalable supply-chains\"\r\n }\r\n },\r\n {\r\n \"id\": 3,\r\n \"name\": \"Clementine Bauch\",\r\n \"username\": \"Samantha\",\r\n \"email\": \"Nathan@yesenia.net\",\r\n \"address\": {\r\n \"street\": \"Douglas Extension\",\r\n \"suite\": \"Suite 847\",\r\n \"city\": \"McKenziehaven\",\r\n \"zipcode\": \"59590-4157\",\r\n \"geo\": {\r\n \"lat\": \"-68.6102\",\r\n \"lng\": \"-47.0653\"\r\n }\r\n },\r\n \"phone\": \"1-463-123-4447\",\r\n \"website\": \"ramiro.info\",\r\n \"company\": {\r\n \"name\": \"Romaguera-Jacobson\",\r\n \"catchPhrase\": \"Face to face bifurcated interface\",\r\n \"bs\": \"e-enable strategic applications\"\r\n }\r\n },\r\n {\r\n \"id\": 4,\r\n \"name\": \"Patricia Lebsack\",\r\n \"username\": \"Karianne\",\r\n \"email\": \"Julianne.OConner@kory.org\",\r\n \"address\": {\r\n \"street\": \"Hoeger Mall\",\r\n \"suite\": \"Apt. 692\",\r\n \"city\": \"South Elvis\",\r\n \"zipcode\": \"53919-4257\",\r\n \"geo\": {\r\n \"lat\": \"29.4572\",\r\n \"lng\": \"-164.2990\"\r\n }\r\n },\r\n \"phone\": \"493-170-9623 x156\",\r\n \"website\": \"kale.biz\",\r\n \"company\": {\r\n \"name\": \"Robel-Corkery\",\r\n \"catchPhrase\": \"Multi-tiered zero tolerance productivity\",\r\n \"bs\": \"transition cutting-edge web services\"\r\n }\r\n },\r\n {\r\n \"id\": 5,\r\n \"name\": \"Chelsey Dietrich\",\r\n \"username\": \"Kamren\",\r\n \"email\": \"Lucio_Hettinger@annie.ca\",\r\n \"address\": {\r\n \"street\": \"Skiles Walks\",\r\n \"suite\": \"Suite 351\",\r\n \"city\": \"Roscoeview\",\r\n \"zipcode\": \"33263\",\r\n \"geo\": {\r\n \"lat\": \"-31.8129\",\r\n \"lng\": \"62.5342\"\r\n }\r\n },\r\n \"phone\": \"(254)954-1289\",\r\n \"website\": \"demarco.info\",\r\n \"company\": {\r\n \"name\": \"Keebler LLC\",\r\n \"catchPhrase\": \"User-centric fault-tolerant solution\",\r\n \"bs\": \"revolutionize end-to-end systems\"\r\n }\r\n },\r\n {\r\n \"id\": 6,\r\n \"name\": \"Mrs. Dennis Schulist\",\r\n \"username\": \"Leopoldo_Corkery\",\r\n \"email\": \"Karley_Dach@jasper.info\",\r\n \"address\": {\r\n \"street\": \"Norberto Crossing\",\r\n \"suite\": \"Apt. 950\",\r\n \"city\": \"South Christy\",\r\n \"zipcode\": \"23505-1337\",\r\n \"geo\": {\r\n \"lat\": \"-71.4197\",\r\n \"lng\": \"71.7478\"\r\n }\r\n },\r\n \"phone\": \"1-477-935-8478 x6430\",\r\n \"website\": \"ola.org\",\r\n \"company\": {\r\n \"name\": \"Considine-Lockman\",\r\n \"catchPhrase\": \"Synchronised bottom-line interface\",\r\n \"bs\": \"e-enable innovative applications\"\r\n }\r\n },\r\n {\r\n \"id\": 7,\r\n \"name\": \"Kurtis Weissnat\",\r\n \"username\": \"Elwyn.Skiles\",\r\n \"email\": \"Telly.Hoeger@billy.biz\",\r\n \"address\": {\r\n \"street\": \"Rex Trail\",\r\n \"suite\": \"Suite 280\",\r\n \"city\": \"Howemouth\",\r\n \"zipcode\": \"58804-1099\",\r\n \"geo\": {\r\n \"lat\": \"24.8918\",\r\n \"lng\": \"21.8984\"\r\n }\r\n },\r\n \"phone\": \"210.067.6132\",\r\n \"website\": \"elvis.io\",\r\n \"company\": {\r\n \"name\": \"Johns Group\",\r\n \"catchPhrase\": \"Configurable multimedia task-force\",\r\n \"bs\": \"generate enterprise e-tailers\"\r\n }\r\n },\r\n {\r\n \"id\": 8,\r\n \"name\": \"Nicholas Runolfsdottir V\",\r\n \"username\": \"Maxime_Nienow\",\r\n \"email\": \"Sherwood@rosamond.me\",\r\n \"address\": {\r\n \"street\": \"Ellsworth Summit\",\r\n \"suite\": \"Suite 729\",\r\n \"city\": \"Aliyaview\",\r\n \"zipcode\": \"45169\",\r\n \"geo\": {\r\n \"lat\": \"-14.3990\",\r\n \"lng\": \"-120.7677\"\r\n }\r\n },\r\n \"phone\": \"586.493.6943 x140\",\r\n \"website\": \"jacynthe.com\",\r\n \"company\": {\r\n \"name\": \"Abernathy Group\",\r\n \"catchPhrase\": \"Implemented secondary concept\",\r\n \"bs\": \"e-enable extensible e-tailers\"\r\n }\r\n },\r\n {\r\n \"id\": 9,\r\n \"name\": \"Glenna Reichert\",\r\n \"username\": \"Delphine\",\r\n \"email\": \"Chaim_McDermott@dana.io\",\r\n \"address\": {\r\n \"street\": \"Dayna Park\",\r\n \"suite\": \"Suite 449\",\r\n \"city\": \"Bartholomebury\",\r\n \"zipcode\": \"76495-3109\",\r\n \"geo\": {\r\n \"lat\": \"24.6463\",\r\n \"lng\": \"-168.8889\"\r\n }\r\n },\r\n \"phone\": \"(775)976-6794 x41206\",\r\n \"website\": \"conrad.com\",\r\n \"company\": {\r\n \"name\": \"Yost and Sons\",\r\n \"catchPhrase\": \"Switchable contextually-based project\",\r\n \"bs\": \"aggregate real-time technologies\"\r\n }\r\n },\r\n {\r\n \"id\": 10,\r\n \"name\": \"Clementina DuBuque\",\r\n \"username\": \"Moriah.Stanton\",\r\n \"email\": \"Rey.Padberg@karina.biz\",\r\n \"address\": {\r\n \"street\": \"Kattie Turnpike\",\r\n \"suite\": \"Suite 198\",\r\n \"city\": \"Lebsackbury\",\r\n \"zipcode\": \"31428-2261\",\r\n \"geo\": {\r\n \"lat\": \"-38.2386\",\r\n \"lng\": \"57.2232\"\r\n }\r\n },\r\n \"phone\": \"024-648-3804\",\r\n \"website\": \"ambrose.net\",\r\n \"company\": {\r\n \"name\": \"Hoeger LLC\",\r\n \"catchPhrase\": \"Centralized empowering task-force\",\r\n \"bs\": \"target end-to-end models\"\r\n }\r\n }\r\n]"; - static ref TREE: Option = Highlighter::default().parse(&BODY); -} - -#[divan::bench] -fn benchmarking_building_content() { - let colors = hac_colors::Colors::default(); - build_syntax_highlighted_lines(&BODY, TREE.as_ref(), &colors); -} diff --git a/hac-client/src/pages.rs b/hac-client/src/pages.rs index 30397ae..b055936 100755 --- a/hac-client/src/pages.rs +++ b/hac-client/src/pages.rs @@ -8,13 +8,18 @@ mod under_construction; use std::sync::mpsc::Sender; -use crate::event_pool::Event; use crossterm::event::KeyEvent; use hac_core::command::Command; -use ratatui::{layout::Rect, Frame}; +use ratatui::layout::Rect; +use ratatui::Frame; + +use crate::event_pool::Event; /// A `Renderable` is anything that is a top level page and can be drawn to the screen pub trait Renderable { + type Input: 'static; + type Output: 'static; + #[allow(unused_variables)] fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { Ok(()) @@ -22,7 +27,9 @@ pub trait Renderable { /// when navigating to a new route, you might need to pass in some data, here you do it #[allow(unused_variables)] - fn update(&mut self, data: Option>) {} + fn update(&mut self, data: Self::Input) {} + + fn data(&self) -> Self::Output; /// each route can, (and must) have a navigator, this is how you attach one #[allow(unused_variables)] @@ -53,7 +60,7 @@ pub trait Renderable { /// An `Eventful` page is a page that can handle key events, and mouse events /// when support for them gets added. pub trait Eventful { - type Result: std::any::Any; + type Result; /// the top level event loop doesnt differentiate between kinds of events, so this is what /// delegate each kind of events to the responsible function diff --git a/hac-client/src/pages/collection_list/create_collection.rs b/hac-client/src/pages/collection_list/create_collection.rs index e53f262..c9bce0a 100644 --- a/hac-client/src/pages/collection_list/create_collection.rs +++ b/hac-client/src/pages/collection_list/create_collection.rs @@ -72,6 +72,11 @@ impl CreateCollection { } impl Renderable for CreateCollection { + type Input = (); + type Output = (); + + fn data(&self) -> Self::Output {} + fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { make_overlay(self.colors.clone(), self.colors.normal.black, 0.2, frame); @@ -107,17 +112,17 @@ impl Renderable for CreateCollection { Ok(()) } - fn update(&mut self, data: Option>) { - if let Some(data) = data { - let data = data - .downcast::<(String, Vec)>() - .expect("wrong kind of data sent to CreateCollection"); - let name = data.0; - let collections = data.1; - self.collections = collections; - self.name = TextObject::from(&name).with_write(); - self.name_cursor.move_to_col(name.len()); - } + fn update(&mut self, data: Self::Input) { + //if let Some(data) = data { + // let data = data + // .downcast::<(String, Vec)>() + // .expect("wrong kind of data sent to CreateCollection"); + // let name = data.0; + // let collections = data.1; + // self.collections = collections; + // self.name = TextObject::from(&name).with_write(); + // self.name_cursor.move_to_col(name.len()); + //} } fn resize(&mut self, new_size: Rect) { @@ -147,16 +152,13 @@ impl Eventful for CreateCollection { )?; let name = self.name.to_string(); self.navigator - .send(Navigate::To( - Routes::ListCollections.into(), - Some(Box::new((Some(name), collections))), - )) + .send(Navigate::To(Routes::ListCollections.into())) .expect("failed to send navigate message"); } KeyCode::Esc => { self.reset(); self.navigator - .send(Navigate::To(Routes::ListCollections.into(), None)) + .send(Navigate::To(Routes::ListCollections.into())) .expect("failed to send navigate message"); } KeyCode::Char('b') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { diff --git a/hac-client/src/pages/collection_list/mod.rs b/hac-client/src/pages/collection_list/mod.rs index d1e936e..beb0169 100644 --- a/hac-client/src/pages/collection_list/mod.rs +++ b/hac-client/src/pages/collection_list/mod.rs @@ -3,6 +3,7 @@ mod create_collection; use std::fmt::Debug; use std::sync::mpsc::{channel, Sender}; +use create_collection::CreateCollection; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::command::Command; use hac_loader::collection_loader::{CollectionMeta, ReadableByteSize}; @@ -15,10 +16,7 @@ use ratatui::Frame; use crate::app::AppRoutes; use crate::components::list_itemm::ListItem; use crate::pages::{Eventful, Renderable}; -use crate::router::Navigate; -use create_collection::CreateCollection; - -use crate::router::Router; +use crate::router::{Navigate, Router}; use crate::{HacColors, HacConfig}; #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Default)] @@ -257,6 +255,18 @@ impl CollectionList { } impl Renderable for CollectionList { + type Input = (String, Vec); + type Output = (String, Vec); + + fn data(&self) -> Self::Input { + if self.collections.is_empty() { + (String::default(), self.collections.clone()) + } else { + let name = self.collections[self.selected].name().to_string(); + (name, self.collections.clone()) + } + } + fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { self.draw_background(size, frame); self.draw_title(frame)?; @@ -287,23 +297,23 @@ impl Renderable for CollectionList { self.layout = build_layout(new_size, self.extended_hint); } - fn update(&mut self, data: Option>) { - if let Some(data) = data { - let data = data - .downcast::<(Option, Vec)>() - .expect("wrong kind of data provided to CollectionList"); - self.collections = data.1; - self.sort_list(); - - if let Some(name) = data.0 { - let selected_idx = self - .collections - .iter() - .position(|c| c.name() == name) - .expect("received invalid name after creation"); - self.selected = selected_idx; - } - } + fn update(&mut self, _data: Self::Input) { + //if let Some(data) = data { + // let data = data + // .downcast::<(Option, Vec)>() + // .expect("wrong kind of data provided to CollectionList"); + // self.collections = data.1; + // self.sort_list(); + // + // if let Some(name) = data.0 { + // let selected_idx = self + // .collections + // .iter() + // .position(|c| c.name() == name) + // .expect("received invalid name after creation"); + // self.selected = selected_idx; + // } + //} } } @@ -371,10 +381,7 @@ impl Eventful for CollectionList { KeyCode::Char('n') => { let collections = self.collections.clone(); self.navigator - .send(Navigate::To( - Routes::CreateCollection.into(), - Some(Box::new((String::default(), collections))), - )) + .send(Navigate::To(Routes::CreateCollection.into())) .expect("failed to send navigation message"); } KeyCode::Char('e') => { @@ -383,7 +390,7 @@ impl Eventful for CollectionList { } let name = self.collections[self.selected].name().to_string(); self.navigator - .send(Navigate::To(Routes::CreateCollection.into(), Some(Box::new(name)))) + .send(Navigate::To(Routes::CreateCollection.into())) .expect("failed to send navigation message"); } KeyCode::Enter => { @@ -391,12 +398,9 @@ impl Eventful for CollectionList { return Ok(None); } assert!(self.collections.len() > self.selected); - let path = self.collections[self.selected].path().clone(); + //let path = self.collections[self.selected].path().clone(); self.navigator - .send(Navigate::Leave( - AppRoutes::CollectionViewerRouter.into(), - Some(Box::new(path)), - )) + .send(Navigate::Leave()) .expect("failed to send navigation message"); } _ => {} diff --git a/hac-client/src/pages/terminal_too_small.rs b/hac-client/src/pages/terminal_too_small.rs index 8fba2f2..b81c84a 100755 --- a/hac-client/src/pages/terminal_too_small.rs +++ b/hac-client/src/pages/terminal_too_small.rs @@ -23,6 +23,9 @@ impl TerminalTooSmall { } impl Renderable for TerminalTooSmall { + type Input = (); + type Output = (); + fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { let layout = build_layout(size); @@ -54,6 +57,10 @@ impl Renderable for TerminalTooSmall { // we purposefully don't do nothing here, as this page automatically adapts to the // size of the window when rendering fn resize(&mut self, _new_size: Rect) {} + + fn data(&self) -> Self::Output { + todo!() + } } fn build_layout(size: Rect) -> Rect { diff --git a/hac-client/src/pages/under_construction.rs b/hac-client/src/pages/under_construction.rs index 7603f2a..229f42f 100755 --- a/hac-client/src/pages/under_construction.rs +++ b/hac-client/src/pages/under_construction.rs @@ -1,9 +1,13 @@ -use crate::ascii::UNDER_CONSTRUCTION; -use crate::pages::Renderable; - use std::ops::{Add, Sub}; -use ratatui::{layout::Rect, style::Stylize, text::Line, widgets::Paragraph, Frame}; +use ratatui::layout::Rect; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::widgets::Paragraph; +use ratatui::Frame; + +use crate::ascii::UNDER_CONSTRUCTION; +use crate::pages::Renderable; pub struct UnderConstruction<'uc> { colors: &'uc hac_colors::colors::Colors, @@ -16,6 +20,9 @@ impl<'uc> UnderConstruction<'uc> { } impl Renderable for UnderConstruction<'_> { + type Input = (); + type Output = (); + fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { let icon_lines = UNDER_CONSTRUCTION .iter() @@ -49,4 +56,8 @@ impl Renderable for UnderConstruction<'_> { Ok(()) } + + fn data(&self) -> Self::Output { + todo!() + } } diff --git a/hac-client/src/router.rs b/hac-client/src/router.rs index 51e02b7..1f7a050 100644 --- a/hac-client/src/router.rs +++ b/hac-client/src/router.rs @@ -1,18 +1,18 @@ +use std::any::{Any, TypeId}; use std::collections::HashMap; use std::fmt::Debug; use std::sync::mpsc::{Receiver, Sender}; +use crossterm::event::KeyEvent; use hac_core::command::Command; use ratatui::layout::Rect; use ratatui::Frame; +use crate::event_pool::Event; use crate::pages::terminal_too_small::TerminalTooSmall; use crate::pages::{Eventful, Renderable}; use crate::{HacColors, MIN_HEIGHT, MIN_WIDTH}; -pub trait EventfulRenderable: Renderable + Eventful + Debug {} -impl EventfulRenderable for K where K: Renderable + Eventful + Debug {} - type Key = u8; /// This is how routes communicate with the router how then want to navigate @@ -23,21 +23,93 @@ pub enum Navigate { /// Change the current route to a new route, or open a dialog if the key matches a /// dialog rather than a route /// Navigating to a route from a dialog will close that dialog - To(Key, Option>), + To(Key), /// Routes can only communicate with the router that owns the route, therefore, /// if a route in a nested router needs to navigate to an entirely different router /// the `Leave` variant is responsible for that. It will direct the navigation event /// to the parent router of the current active router. - Leave(Key, Option>), + Leave(), /// Go back in the navigation history, this completely wipes the current router history, /// and close every dialog thats visible - Back(Option>), + Back(), +} + +pub trait AnyCommand {} +impl AnyCommand for Command {} + +pub trait AnyRenderable: Debug { + type Ev: AnyCommand; + + fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()>; + + fn data(&self) -> Box; + + fn update(&mut self, input: Box); + + fn attach_navigator(&mut self, navigator: std::sync::mpsc::Sender); + + fn resize(&mut self, new_size: Rect); + + fn register_command_handler(&mut self, sender: Sender); + + fn handle_command(&mut self, command: Command) -> anyhow::Result<()>; + + fn tick(&mut self) -> anyhow::Result<()>; + + fn handle_event(&mut self, event: Option) -> anyhow::Result>; + + fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result>; +} + +impl + Debug> AnyRenderable for T { + type Ev = K; + + fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { + ::draw(self, frame, size) + } + + fn data(&self) -> Box { + Box::new(::data(self)) + } + + fn update(&mut self, input: Box) { + let input = input.downcast::().expect("wrong data passed to route"); + ::update(self, *input); + } + + fn attach_navigator(&mut self, navigator: std::sync::mpsc::Sender) { + ::attach_navigator(self, navigator); + } + + fn resize(&mut self, new_size: Rect) { + ::resize(self, new_size); + } + + fn register_command_handler(&mut self, sender: Sender) { + ::register_command_handler(self, sender); + } + + fn handle_command(&mut self, command: Command) -> anyhow::Result<()> { + ::handle_command(self, command) + } + + fn tick(&mut self) -> anyhow::Result<()> { + ::tick(self) + } + + fn handle_event(&mut self, event: Option) -> anyhow::Result> { + ::handle_event(self, event) + } + + fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + ::handle_key_event(self, key_event) + } } #[derive(Debug)] pub struct Router { - routes: HashMap>>, - dialogs: HashMap>>, + routes: HashMap>>, + dialogs: HashMap>>, dialog_stack: Vec, active_route: Key, history: Vec, @@ -69,13 +141,13 @@ impl Router { self.navigate_sender.clone() } - pub fn add_route(&mut self, key: Key, mut route: Box>) { + pub fn add_route(&mut self, key: Key, mut route: Box>) { route.attach_navigator(self.navigate_sender.clone()); route.register_command_handler(self.command_sender.clone()); self.routes.insert(key, route); } - pub fn add_dialog(&mut self, key: Key, mut dialog: Box>) { + pub fn add_dialog(&mut self, key: Key, mut dialog: Box>) { dialog.attach_navigator(self.navigate_sender.clone()); dialog.register_command_handler(self.command_sender.clone()); self.dialogs.insert(key, dialog); @@ -91,13 +163,13 @@ impl Router { self.parent_navigator = Some(navigator); } - fn get_active_route(&mut self) -> &mut Box> { + fn get_active_route(&mut self) -> &mut Box> { self.routes .get_mut(&self.active_route) .expect("active route doesn't exist on router") } - fn get_active_dialog(&mut self) -> Option<&mut Box>> { + fn get_active_dialog(&mut self) -> Option<&mut Box>> { let key = self.dialog_stack.last()?; Some( self.dialogs @@ -108,15 +180,20 @@ impl Router { fn navigate(&mut self, navigation: Navigate) { match navigation { - Navigate::To(route, data) => { + Navigate::To(route) => { + let curr_route = if self.get_active_dialog().is_some() { + // SAFETY: we just checked if its some + self.get_active_dialog().unwrap() + } else { + self.get_active_route() + }; + let data = curr_route.data(); if self.routes.contains_key(&route) { - // if navigating to a route, we clear the dialogs. self.dialog_stack.clear(); - self.active_route = route; self.history.push(route); let route = self.get_active_route(); - route.update(data); + route.update(data) } else if self.dialogs.contains_key(&route) { self.dialog_stack.push(route); let dialog = self @@ -127,48 +204,48 @@ impl Router { panic!("tried to navigate to an unknown route"); } } - Navigate::Back(data) => { - if self.history.len() <= 1 { - return; - } - - // SAFETY: we just checked if these exists - let curr = self.history.pop().unwrap(); - let prev = *self.history.last().unwrap(); - - // pop the dialog if we are currently displaying one - self.dialogs.contains_key(&curr).then(|| self.dialog_stack.pop()); - - let route = if self.dialogs.contains_key(&prev) { - self.get_active_dialog() - .expect("previous route is a dialog, but its not on the stack") - } else { - self.routes - .get_mut(&prev) - .expect("previous route is not registered... how?") - }; - - route.update(data); - } - Navigate::Leave(route, data) if self.parent_navigator.is_some() => { - self.history.clear(); - self.dialog_stack.clear(); - - self.parent_navigator - .as_mut() - .unwrap() - .send(Navigate::To(route, data)) - .expect("failed to send navigation command"); - } - Navigate::Leave(_, _) => panic!("theres no parent navigator"), + Navigate::Back() => todo!(), + //if self.history.len() <= 1 { + // return; + //} + // + //// SAFETY: we just checked if these exists + //let curr = self.history.pop().unwrap(); + //let prev = *self.history.last().unwrap(); + // + //// pop the dialog if we are currently displaying one + //self.dialogs.contains_key(&curr).then(|| self.dialog_stack.pop()); + // + //let route = if self.dialogs.contains_key(&prev) { + // self.get_active_dialog() + // .expect("previous route is a dialog, but its not on the stack") + //} else { + // self.routes + // .get_mut(&prev) + // .expect("previous route is not registered... how?") + //}; + // + //route.update(data); + Navigate::Leave() => todo!(), + //self.history.clear(); + //self.dialog_stack.clear(); + // + //self.parent_navigator + // .as_mut() + // .unwrap() + // .send(Navigate::To(route, data)) + // .expect("failed to send navigation command"); } } } impl Renderable for Router { + type Input = (); + type Output = (); + fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { if frame.size().height < MIN_HEIGHT || frame.size().width < MIN_WIDTH { - self.too_small.draw(frame, size)?; + Renderable::draw(&mut self.too_small, frame, size)?; return Ok(()); } @@ -205,6 +282,8 @@ impl Renderable for Router { dialog.resize(new_size); } } + + fn data(&self) -> Self::Input {} } impl Eventful for Router { From 525fed20f2dcfe2b28949e12587692e8f3b9e82f Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Thu, 26 Sep 2024 01:55:07 -0300 Subject: [PATCH 08/23] wip: initial implementation of edit collection --- hac-client/src/pages.rs | 5 +- .../collection_list/create_collection.rs | 187 ++++-------------- .../pages/collection_list/edit_collection.rs | 119 +++++++++++ .../src/pages/collection_list/form_shared.rs | 132 +++++++++++++ hac-client/src/pages/collection_list/mod.rs | 103 +++++----- hac-client/src/pages/terminal_too_small.rs | 4 +- hac-client/src/pages/under_construction.rs | 4 +- hac-client/src/router.rs | 111 ++++++----- hac-loader/src/collection_loader/mod.rs | 9 +- 9 files changed, 416 insertions(+), 258 deletions(-) create mode 100644 hac-client/src/pages/collection_list/edit_collection.rs create mode 100644 hac-client/src/pages/collection_list/form_shared.rs diff --git a/hac-client/src/pages.rs b/hac-client/src/pages.rs index b055936..faec882 100755 --- a/hac-client/src/pages.rs +++ b/hac-client/src/pages.rs @@ -14,6 +14,7 @@ use ratatui::layout::Rect; use ratatui::Frame; use crate::event_pool::Event; +use crate::router::{Navigate, RouterMessage}; /// A `Renderable` is anything that is a top level page and can be drawn to the screen pub trait Renderable { @@ -29,11 +30,11 @@ pub trait Renderable { #[allow(unused_variables)] fn update(&mut self, data: Self::Input) {} - fn data(&self) -> Self::Output; + fn data(&self, requester: u8) -> Self::Output; /// each route can, (and must) have a navigator, this is how you attach one #[allow(unused_variables)] - fn attach_navigator(&mut self, navigator: std::sync::mpsc::Sender) {} + fn attach_navigator(&mut self, navigator: Sender, messager: Sender) {} /// pages need to adapt to change of sizes on the application, this function is called /// by the top level event loop whenever a resize event is produced diff --git a/hac-client/src/pages/collection_list/create_collection.rs b/hac-client/src/pages/collection_list/create_collection.rs index c9bce0a..56c7ef1 100644 --- a/hac-client/src/pages/collection_list/create_collection.rs +++ b/hac-client/src/pages/collection_list/create_collection.rs @@ -5,32 +5,17 @@ use hac_core::command::Command; use hac_core::text_object::cursor::Cursor; use hac_core::text_object::{TextObject, Write}; use hac_loader::collection_loader::CollectionMeta; -use ratatui::layout::{Constraint, Flex, Layout, Rect}; -use ratatui::style::{Style, Stylize}; -use ratatui::text::Line; -use ratatui::widgets::Paragraph; +use ratatui::layout::Rect; use ratatui::Frame; -use super::Routes; -use crate::ascii::LOGO_ASCII; -use crate::components::input::Input; +use super::form_shared::{ + build_form_layout, draw_form_layout, handle_form_key_event, set_form_cursor, FormEvent, FormLayout, +}; +use super::{CollectionListData, Routes}; use crate::pages::overlay::make_overlay; use crate::pages::{Eventful, Renderable}; -use crate::router::Navigate; -use crate::{HacColors, HacConfig, MIN_HEIGHT, MIN_WIDTH}; - -#[derive(Debug)] -struct CreateCollectionLayout { - name_input: Rect, - hint: Rect, - logo: Rect, -} - -#[derive(Debug, Default)] -enum Focus { - #[default] - Name, -} +use crate::router::{Navigate, RouterMessage}; +use crate::{HacColors, HacConfig}; #[derive(Debug)] pub struct CreateCollection { @@ -39,99 +24,66 @@ pub struct CreateCollection { collections: Vec, size: Rect, colors: HacColors, - name_cursor: Cursor, - focus: Focus, + cursor: Cursor, config: HacConfig, - layout: CreateCollectionLayout, + layout: FormLayout, navigator: Sender, + messager: Sender, } impl CreateCollection { pub fn new(size: Rect, config: HacConfig, colors: HacColors) -> Self { - let (dummy, _) = channel(); Self { config, colors, size, - layout: build_layout(size), + layout: build_form_layout(size), name: TextObject::::default(), desc: TextObject::::default(), - name_cursor: Cursor::default(), - focus: Default::default(), + cursor: Cursor::default(), collections: Default::default(), - navigator: dummy, + navigator: channel().0, + messager: channel().0, } } fn reset(&mut self) { self.name = Default::default(); self.desc = Default::default(); - self.name_cursor = Default::default(); - self.focus = Default::default(); + self.cursor = Default::default(); } } impl Renderable for CreateCollection { - type Input = (); - type Output = (); + type Input = CollectionListData; + type Output = (String, Vec); - fn data(&self) -> Self::Output {} + fn data(&self, _requester: u8) -> Self::Output { + (self.name.to_string(), self.collections.clone()) + } fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { make_overlay(self.colors.clone(), self.colors.normal.black, 0.2, frame); - - let label = String::from("Collection Name"); - let name = self.name.to_string(); - let name_input = Input::new(Some(&name), Some(&label), self.colors.clone()) - .value_style(Style::default().fg(self.colors.normal.white)) - .label_style(Style::default().fg(self.colors.bright.black)); - - let hint = vec![ - "Enter".fg(self.colors.bright.green).bold(), - " - Confirm • ".fg(self.colors.bright.black), - "Esc".fg(self.colors.bright.green).bold(), - " - Cancel".fg(self.colors.bright.black), - ]; - - let logo = Paragraph::new( - LOGO_ASCII - .iter() - .map(|line| Line::from(line.to_string()).fg(self.colors.bright.red).centered()) - .collect::>(), - ); - - frame.render_widget(logo, self.layout.logo); - frame.render_widget(name_input, self.layout.name_input); - frame.render_widget(Line::from(hint), self.layout.hint); - - frame.set_cursor( - self.layout.name_input.x + 1 + self.name_cursor.col() as u16, - self.layout.name_input.y + 2 + self.name_cursor.row() as u16, - ); + draw_form_layout(self.layout, self.name.to_string(), &self.colors, frame); + set_form_cursor(self.layout, &self.cursor, frame); Ok(()) } fn update(&mut self, data: Self::Input) { - //if let Some(data) = data { - // let data = data - // .downcast::<(String, Vec)>() - // .expect("wrong kind of data sent to CreateCollection"); - // let name = data.0; - // let collections = data.1; - // self.collections = collections; - // self.name = TextObject::from(&name).with_write(); - // self.name_cursor.move_to_col(name.len()); - //} + if let CollectionListData::CreateCollection(data) = data { + self.collections = data; + } } fn resize(&mut self, new_size: Rect) { self.size = new_size; - self.layout = build_layout(new_size); + self.layout = build_form_layout(new_size); } - fn attach_navigator(&mut self, navigator: Sender) { + fn attach_navigator(&mut self, navigator: Sender, messager: Sender) { self.navigator = navigator; + self.messager = messager; } } @@ -143,99 +95,26 @@ impl Eventful for CreateCollection { return Ok(Some(Command::Quit)); } - match key_event.code { - KeyCode::Enter => { - let collections = hac_loader::collection_loader::create_collection( + match handle_form_key_event(key_event, &mut self.name, &mut self.cursor)? { + Some(FormEvent::Confirm) => { + self.collections = hac_loader::collection_loader::create_collection( self.name.to_string(), self.collections.clone(), &self.config, )?; - let name = self.name.to_string(); self.navigator - .send(Navigate::To(Routes::ListCollections.into())) + .send(Navigate::Back(Routes::ListCollections.into())) .expect("failed to send navigate message"); } - KeyCode::Esc => { + Some(FormEvent::Cancel) => { self.reset(); self.navigator .send(Navigate::To(Routes::ListCollections.into())) .expect("failed to send navigate message"); } - KeyCode::Char('b') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { - self.name_cursor.move_left(1); - } - KeyCode::Left if matches!(key_event.modifiers, KeyModifiers::ALT) => { - let (col, row) = self.name.find_char_before_whitespace(&self.name_cursor); - self.name_cursor.move_to(col, row); - } - KeyCode::Right if matches!(key_event.modifiers, KeyModifiers::ALT) => { - let (col, row) = self.name.find_char_after_whitespace(&self.name_cursor); - self.name_cursor.move_to(col, row); - } - KeyCode::Left => { - self.name_cursor.move_left(1); - } - KeyCode::Char('e') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { - self.name_cursor.move_to_line_end(self.name.line_len(0)); - } - KeyCode::Down => { - self.name_cursor.move_to_line_end(self.name.line_len(0)); - } - KeyCode::Char('a') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { - self.name_cursor.move_to_line_start(); - } - KeyCode::Up => { - self.name_cursor.move_to_line_start(); - } - KeyCode::Char('f') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { - self.name_cursor.move_right(1); - } - KeyCode::Right => { - self.name_cursor.move_right(1); - self.name_cursor.maybe_snap_to_col(self.name.line_len(0)); - } - KeyCode::Char('d') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { - self.name.erase_current_char(&self.name_cursor); - } - KeyCode::Char('u') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { - self.name.clear_line(&self.name_cursor); - self.name_cursor.move_to_line_start(); - } - KeyCode::Backspace => { - self.name.erase_previous_char(&self.name_cursor); - self.name_cursor.move_left(1); - } - KeyCode::Char(c) => { - self.name.insert_char(c, &self.name_cursor); - self.name_cursor.move_right(1); - } _ => {} } Ok(None) } } - -fn build_layout(area: Rect) -> CreateCollectionLayout { - let [_, form, _] = Layout::horizontal([Constraint::Fill(1), Constraint::Length(MIN_WIDTH), Constraint::Fill(1)]) - .flex(Flex::Center) - .areas(area); - - let [_, form, _] = Layout::vertical([Constraint::Fill(1), Constraint::Length(MIN_HEIGHT), Constraint::Fill(1)]) - .flex(Flex::End) - .areas(form); - - let form = form.inner(&ratatui::layout::Margin::new(2, 0)); - - let [logo, _, name_input, _, hint] = Layout::vertical([ - Constraint::Length(LOGO_ASCII.len() as u16), - Constraint::Length(1), - Constraint::Length(4), - Constraint::Length(1), - Constraint::Length(1), - ]) - .flex(Flex::Center) - .areas(form); - - CreateCollectionLayout { name_input, hint, logo } -} diff --git a/hac-client/src/pages/collection_list/edit_collection.rs b/hac-client/src/pages/collection_list/edit_collection.rs new file mode 100644 index 0000000..984a6d9 --- /dev/null +++ b/hac-client/src/pages/collection_list/edit_collection.rs @@ -0,0 +1,119 @@ +use std::sync::mpsc::{channel, Sender}; + +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use hac_core::command::Command; +use hac_core::text_object::cursor::Cursor; +use hac_core::text_object::{TextObject, Write}; +use hac_loader::collection_loader::CollectionMeta; +use ratatui::layout::Rect; +use ratatui::Frame; + +use super::form_shared::{ + build_form_layout, draw_form_layout, handle_form_key_event, set_form_cursor, FormEvent, FormLayout, +}; +use super::{CollectionListData, Routes}; +use crate::pages::overlay::make_overlay; +use crate::pages::{Eventful, Renderable}; +use crate::router::{Navigate, RouterMessage}; +use crate::{HacColors, HacConfig}; + +#[derive(Debug)] +pub struct EditCollection { + name: TextObject, + desc: TextObject, + collections: Vec, + collection_idx: usize, + size: Rect, + colors: HacColors, + cursor: Cursor, + config: HacConfig, + layout: FormLayout, + navigator: Sender, + messager: Sender, +} + +impl EditCollection { + pub fn new(size: Rect, config: HacConfig, colors: HacColors) -> Self { + Self { + config, + colors, + size, + layout: build_form_layout(size), + name: TextObject::::default(), + desc: TextObject::::default(), + cursor: Cursor::default(), + collections: Default::default(), + collection_idx: 0, + navigator: channel().0, + messager: channel().0, + } + } + + fn reset(&mut self) { + self.name = Default::default(); + self.desc = Default::default(); + self.cursor = Default::default(); + } +} + +impl Renderable for EditCollection { + type Input = CollectionListData; + type Output = (String, Vec); + + fn data(&self, _requester: u8) -> Self::Output { + (self.name.to_string(), self.collections.clone()) + } + + fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { + make_overlay(self.colors.clone(), self.colors.normal.black, 0.2, frame); + draw_form_layout(self.layout, self.name.to_string(), &self.colors, frame); + set_form_cursor(self.layout, &self.cursor, frame); + Ok(()) + } + + fn update(&mut self, data: Self::Input) { + if let CollectionListData::EditCollection(idx, data) = data { + self.collections = data; + self.collection_idx = idx; + self.name = TextObject::from(self.collections[idx].name()).with_write(); + self.cursor.move_to_col(self.name.line_len(0) + 1); + } + } + + fn resize(&mut self, new_size: Rect) { + self.size = new_size; + self.layout = build_form_layout(new_size); + } + + fn attach_navigator(&mut self, navigator: Sender, messager: Sender) { + self.navigator = navigator; + self.messager = messager; + } +} + +impl Eventful for EditCollection { + type Result = Command; + + fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { + return Ok(Some(Command::Quit)); + } + + match handle_form_key_event(key_event, &mut self.name, &mut self.cursor)? { + Some(FormEvent::Confirm) => { + self.navigator + .send(Navigate::To(Routes::ListCollections.into())) + .expect("failed to send navigate message"); + } + Some(FormEvent::Cancel) => { + self.reset(); + self.navigator + .send(Navigate::To(Routes::ListCollections.into())) + .expect("failed to send navigate message"); + } + _ => {} + } + + Ok(None) + } +} diff --git a/hac-client/src/pages/collection_list/form_shared.rs b/hac-client/src/pages/collection_list/form_shared.rs new file mode 100644 index 0000000..a9c901e --- /dev/null +++ b/hac-client/src/pages/collection_list/form_shared.rs @@ -0,0 +1,132 @@ +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use hac_core::text_object::cursor::Cursor; +use hac_core::text_object::{TextObject, Write}; +use ratatui::layout::{Constraint, Flex, Layout, Rect}; +use ratatui::style::{Style, Stylize}; +use ratatui::text::Line; +use ratatui::widgets::Paragraph; +use ratatui::Frame; + +use crate::ascii::LOGO_ASCII; +use crate::components::input::Input; +use crate::{HacColors, MIN_HEIGHT, MIN_WIDTH}; + +#[derive(Debug, Clone, Copy)] +pub struct FormLayout { + pub name_input: Rect, + pub hint: Rect, + pub logo: Rect, +} + +#[derive(Debug)] +pub enum FormEvent { + Confirm, + Cancel, +} + +pub fn build_form_layout(area: Rect) -> FormLayout { + let [_, form, _] = Layout::horizontal([Constraint::Fill(1), Constraint::Length(MIN_WIDTH), Constraint::Fill(1)]) + .flex(Flex::Center) + .areas(area); + + let [_, form, _] = Layout::vertical([Constraint::Fill(1), Constraint::Length(MIN_HEIGHT), Constraint::Fill(1)]) + .flex(Flex::End) + .areas(form); + + let form = form.inner(&ratatui::layout::Margin::new(2, 0)); + + let [logo, _, name_input, _, hint] = Layout::vertical([ + Constraint::Length(LOGO_ASCII.len() as u16), + Constraint::Length(1), + Constraint::Length(4), + Constraint::Length(1), + Constraint::Length(1), + ]) + .flex(Flex::Center) + .areas(form); + + FormLayout { name_input, hint, logo } +} + +pub fn handle_form_key_event( + key_event: KeyEvent, + text_object: &mut TextObject, + cursor: &mut Cursor, +) -> anyhow::Result> { + match key_event.code { + KeyCode::Enter => return Ok(Some(FormEvent::Confirm)), + KeyCode::Esc => return Ok(Some(FormEvent::Cancel)), + KeyCode::Char('b') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => cursor.move_left(1), + KeyCode::Left if matches!(key_event.modifiers, KeyModifiers::ALT) => { + let (col, row) = text_object.find_char_before_whitespace(cursor); + cursor.move_to(col, row); + } + KeyCode::Right if matches!(key_event.modifiers, KeyModifiers::ALT) => { + let (col, row) = text_object.find_char_after_whitespace(cursor); + cursor.move_to(col, row); + } + KeyCode::Left => cursor.move_left(1), + KeyCode::Char('e') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { + cursor.move_to_line_end(text_object.line_len(0)) + } + KeyCode::Down => cursor.move_to_line_end(text_object.line_len(0)), + KeyCode::Char('a') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => cursor.move_to_line_start(), + KeyCode::Up => cursor.move_to_line_start(), + KeyCode::Char('f') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => cursor.move_right(1), + KeyCode::Right => { + cursor.move_right(1); + cursor.maybe_snap_to_col(text_object.line_len(0)); + } + KeyCode::Char('d') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { + text_object.erase_current_char(cursor) + } + KeyCode::Char('u') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { + text_object.clear_line(cursor); + cursor.move_to_line_start(); + } + KeyCode::Backspace => { + text_object.erase_previous_char(cursor); + cursor.move_left(1); + } + KeyCode::Char(c) => { + text_object.insert_char(c, cursor); + cursor.move_right(1); + } + _ => {} + } + + Ok(None) +} + +pub fn draw_form_layout(layout: FormLayout, name: String, colors: &HacColors, frame: &mut Frame) { + let label = String::from("Collection Name"); + let name = name.to_string(); + let name_input = Input::new(Some(&name), Some(&label), colors.clone()) + .value_style(Style::default().fg(colors.normal.white)) + .label_style(Style::default().fg(colors.bright.black)); + + let hint = vec![ + "Enter".fg(colors.bright.green).bold(), + " - Confirm • ".fg(colors.bright.black), + "Esc".fg(colors.bright.green).bold(), + " - Cancel".fg(colors.bright.black), + ]; + + let logo = Paragraph::new( + LOGO_ASCII + .iter() + .map(|line| Line::from(line.to_string()).fg(colors.bright.red).centered()) + .collect::>(), + ); + + frame.render_widget(logo, layout.logo); + frame.render_widget(name_input, layout.name_input); + frame.render_widget(Line::from(hint), layout.hint); +} + +pub fn set_form_cursor(layout: FormLayout, cursor: &Cursor, frame: &mut Frame) { + frame.set_cursor( + layout.name_input.x + 1 + cursor.col() as u16, + layout.name_input.y + 2 + cursor.row() as u16, + ); +} diff --git a/hac-client/src/pages/collection_list/mod.rs b/hac-client/src/pages/collection_list/mod.rs index beb0169..ba2a44f 100644 --- a/hac-client/src/pages/collection_list/mod.rs +++ b/hac-client/src/pages/collection_list/mod.rs @@ -1,10 +1,13 @@ mod create_collection; +mod edit_collection; +mod form_shared; use std::fmt::Debug; use std::sync::mpsc::{channel, Sender}; use create_collection::CreateCollection; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use edit_collection::EditCollection; use hac_core::command::Command; use hac_loader::collection_loader::{CollectionMeta, ReadableByteSize}; use ratatui::layout::{Constraint, Direction, Layout, Rect}; @@ -13,10 +16,9 @@ use ratatui::text::Line; use ratatui::widgets::{Block, Clear, Paragraph}; use ratatui::Frame; -use crate::app::AppRoutes; use crate::components::list_itemm::ListItem; use crate::pages::{Eventful, Renderable}; -use crate::router::{Navigate, Router}; +use crate::router::{Navigate, Router, RouterMessage}; use crate::{HacColors, HacConfig}; #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Default)] @@ -24,6 +26,18 @@ pub enum Routes { #[default] ListCollections, CreateCollection, + EditCollection, +} + +impl From for Routes { + fn from(value: u8) -> Self { + match value { + 0 => Routes::ListCollections, + 1 => Routes::CreateCollection, + 2 => Routes::EditCollection, + _ => panic!("invalid route number"), + } + } } impl From for u8 { @@ -31,6 +45,7 @@ impl From for u8 { match val { Routes::ListCollections => 0, Routes::CreateCollection => 1, + Routes::EditCollection => 2, } } } @@ -43,11 +58,8 @@ pub fn make_collection_list_router( colors: HacColors, ) -> Router { let mut router = Router::new(command_sender, colors.clone()); - let collection_list = CollectionList::new(collections, size, colors.clone()); - let create_collection = CreateCollection::new(size, config.clone(), colors.clone()); - + let collection_list = CollectionList::new(collections, size, config, colors.clone()); router.add_route(Routes::ListCollections.into(), Box::new(collection_list)); - router.add_dialog(Routes::CreateCollection.into(), Box::new(create_collection)); router } @@ -91,8 +103,10 @@ pub struct CollectionList { sorting_kind: SortingKind, collections: Vec, selected: usize, + config: HacConfig, scroll: usize, navigator: Sender, + messager: Sender, layout: DashboardLayout, pub command_sender: Option>, extended_hint: bool, @@ -101,20 +115,22 @@ pub struct CollectionList { impl CollectionList { const LIST_ITEM_HEIGHT: u16 = 3; - pub fn new(collections: Vec, size: Rect, colors: HacColors) -> Self { - let (dummy, _) = channel(); - Self { + pub fn new(collections: Vec, size: Rect, config: HacConfig, colors: HacColors) -> Self { + let mut list = Self { colors, - + config, selected: 0, scroll: 0, sorting_kind: SortingKind::default(), - navigator: dummy, + navigator: channel().0, + messager: channel().0, collections, layout: build_layout(size, false), command_sender: None, extended_hint: false, - } + }; + list.sort_list(); + list } fn sort_list(&mut self) { @@ -254,23 +270,27 @@ impl CollectionList { } } +#[derive(Debug)] +pub enum CollectionListData { + CreateCollection(Vec), + EditCollection(usize, Vec), +} + impl Renderable for CollectionList { type Input = (String, Vec); - type Output = (String, Vec); + type Output = CollectionListData; - fn data(&self) -> Self::Input { - if self.collections.is_empty() { - (String::default(), self.collections.clone()) - } else { - let name = self.collections[self.selected].name().to_string(); - (name, self.collections.clone()) + fn data(&self, requester: u8) -> Self::Output { + match Routes::from(requester) { + Routes::CreateCollection => CollectionListData::CreateCollection(self.collections.clone()), + Routes::ListCollections => CollectionListData::CreateCollection(self.collections.clone()), + Routes::EditCollection => CollectionListData::EditCollection(self.selected, self.collections.clone()), } } fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { self.draw_background(size, frame); self.draw_title(frame)?; - self.draw_collection_list(frame)?; self.draw_hint_text(frame); @@ -285,8 +305,9 @@ impl Renderable for CollectionList { Ok(()) } - fn attach_navigator(&mut self, navigator: std::sync::mpsc::Sender) { + fn attach_navigator(&mut self, navigator: Sender, messager: Sender) { self.navigator = navigator; + self.messager = messager; } fn register_command_handler(&mut self, sender: Sender) { @@ -297,23 +318,14 @@ impl Renderable for CollectionList { self.layout = build_layout(new_size, self.extended_hint); } - fn update(&mut self, _data: Self::Input) { - //if let Some(data) = data { - // let data = data - // .downcast::<(Option, Vec)>() - // .expect("wrong kind of data provided to CollectionList"); - // self.collections = data.1; - // self.sort_list(); - // - // if let Some(name) = data.0 { - // let selected_idx = self - // .collections - // .iter() - // .position(|c| c.name() == name) - // .expect("received invalid name after creation"); - // self.selected = selected_idx; - // } - //} + fn update(&mut self, data: Self::Input) { + self.collections = data.1; + self.sort_list(); + self.selected = self + .collections + .iter() + .position(|col| col.path().to_string_lossy().contains(&data.0)) + .expect("collection to select doesn't exist"); } } @@ -379,18 +391,20 @@ impl Eventful for CollectionList { self.sort_list(); } KeyCode::Char('n') => { - let collections = self.collections.clone(); + let create_form = + CreateCollection::new(self.layout.total_size, self.config.clone(), self.colors.clone()); + let message = RouterMessage::AddDialog(Routes::CreateCollection.into(), Box::new(create_form)); + self.messager.send(message).expect("failed to create new route"); self.navigator .send(Navigate::To(Routes::CreateCollection.into())) .expect("failed to send navigation message"); } KeyCode::Char('e') => { - if self.collections.is_empty() { - return Ok(None); - } - let name = self.collections[self.selected].name().to_string(); + let edit_form = EditCollection::new(self.layout.total_size, self.config.clone(), self.colors.clone()); + let message = RouterMessage::AddDialog(Routes::EditCollection.into(), Box::new(edit_form)); + self.messager.send(message).expect("failed to create new route"); self.navigator - .send(Navigate::To(Routes::CreateCollection.into())) + .send(Navigate::To(Routes::EditCollection.into())) .expect("failed to send navigation message"); } KeyCode::Enter => { @@ -398,7 +412,6 @@ impl Eventful for CollectionList { return Ok(None); } assert!(self.collections.len() > self.selected); - //let path = self.collections[self.selected].path().clone(); self.navigator .send(Navigate::Leave()) .expect("failed to send navigation message"); diff --git a/hac-client/src/pages/terminal_too_small.rs b/hac-client/src/pages/terminal_too_small.rs index b81c84a..0a2080f 100755 --- a/hac-client/src/pages/terminal_too_small.rs +++ b/hac-client/src/pages/terminal_too_small.rs @@ -58,9 +58,7 @@ impl Renderable for TerminalTooSmall { // size of the window when rendering fn resize(&mut self, _new_size: Rect) {} - fn data(&self) -> Self::Output { - todo!() - } + fn data(&self, _requester: u8) -> Self::Output {} } fn build_layout(size: Rect) -> Rect { diff --git a/hac-client/src/pages/under_construction.rs b/hac-client/src/pages/under_construction.rs index 229f42f..ef4b8fb 100755 --- a/hac-client/src/pages/under_construction.rs +++ b/hac-client/src/pages/under_construction.rs @@ -57,7 +57,5 @@ impl Renderable for UnderConstruction<'_> { Ok(()) } - fn data(&self) -> Self::Output { - todo!() - } + fn data(&self, _requester: u8) -> Self::Output {} } diff --git a/hac-client/src/router.rs b/hac-client/src/router.rs index 1f7a050..26d7c3c 100644 --- a/hac-client/src/router.rs +++ b/hac-client/src/router.rs @@ -31,7 +31,7 @@ pub enum Navigate { Leave(), /// Go back in the navigation history, this completely wipes the current router history, /// and close every dialog thats visible - Back(), + Back(Key), } pub trait AnyCommand {} @@ -41,23 +41,14 @@ pub trait AnyRenderable: Debug { type Ev: AnyCommand; fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()>; - - fn data(&self) -> Box; - + fn data(&self, requester: Key) -> Box; fn update(&mut self, input: Box); - - fn attach_navigator(&mut self, navigator: std::sync::mpsc::Sender); - + fn attach_navigator(&mut self, navigator: Sender, messager: Sender); fn resize(&mut self, new_size: Rect); - fn register_command_handler(&mut self, sender: Sender); - fn handle_command(&mut self, command: Command) -> anyhow::Result<()>; - fn tick(&mut self) -> anyhow::Result<()>; - fn handle_event(&mut self, event: Option) -> anyhow::Result>; - fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result>; } @@ -68,8 +59,8 @@ impl + Debug> AnyRenderable ::draw(self, frame, size) } - fn data(&self) -> Box { - Box::new(::data(self)) + fn data(&self, requester: Key) -> Box { + Box::new(::data(self, requester)) } fn update(&mut self, input: Box) { @@ -77,8 +68,8 @@ impl + Debug> AnyRenderable ::update(self, *input); } - fn attach_navigator(&mut self, navigator: std::sync::mpsc::Sender) { - ::attach_navigator(self, navigator); + fn attach_navigator(&mut self, navigator: Sender, messager: Sender) { + ::attach_navigator(self, navigator, messager); } fn resize(&mut self, new_size: Rect) { @@ -106,6 +97,13 @@ impl + Debug> AnyRenderable } } +#[derive(Debug)] +pub enum RouterMessage { + AddRoute(Key, Box>), + AddDialog(Key, Box>), + DelRoute(Key), +} + #[derive(Debug)] pub struct Router { routes: HashMap>>, @@ -114,6 +112,8 @@ pub struct Router { active_route: Key, history: Vec, command_sender: Sender, + message_receiver: Receiver, + message_sender: Sender, navigate_sender: Sender, navigate_receiver: Receiver, parent_navigator: Option>, @@ -123,12 +123,15 @@ pub struct Router { impl Router { pub fn new(command_sender: Sender, colors: HacColors) -> Self { let (navigate_sender, navigate_receiver) = std::sync::mpsc::channel(); + let (message_sender, message_receiver) = std::sync::mpsc::channel(); Self { routes: Default::default(), dialogs: Default::default(), dialog_stack: Default::default(), active_route: Default::default(), history: vec![Default::default()], + message_receiver, + message_sender, command_sender, navigate_sender, navigate_receiver, @@ -142,13 +145,13 @@ impl Router { } pub fn add_route(&mut self, key: Key, mut route: Box>) { - route.attach_navigator(self.navigate_sender.clone()); + route.attach_navigator(self.navigate_sender.clone(), self.message_sender.clone()); route.register_command_handler(self.command_sender.clone()); self.routes.insert(key, route); } pub fn add_dialog(&mut self, key: Key, mut dialog: Box>) { - dialog.attach_navigator(self.navigate_sender.clone()); + dialog.attach_navigator(self.navigate_sender.clone(), self.message_sender.clone()); dialog.register_command_handler(self.command_sender.clone()); self.dialogs.insert(key, dialog); } @@ -181,13 +184,12 @@ impl Router { fn navigate(&mut self, navigation: Navigate) { match navigation { Navigate::To(route) => { - let curr_route = if self.get_active_dialog().is_some() { - // SAFETY: we just checked if its some - self.get_active_dialog().unwrap() - } else { - self.get_active_route() + tracing::trace!("navigating to route with key: {route}"); + let curr_route = match self.get_active_dialog() { + Some(dialog) => dialog, + None => self.get_active_route(), }; - let data = curr_route.data(); + let data = curr_route.data(route); if self.routes.contains_key(&route) { self.dialog_stack.clear(); self.active_route = route; @@ -204,28 +206,36 @@ impl Router { panic!("tried to navigate to an unknown route"); } } - Navigate::Back() => todo!(), - //if self.history.len() <= 1 { - // return; - //} - // - //// SAFETY: we just checked if these exists - //let curr = self.history.pop().unwrap(); - //let prev = *self.history.last().unwrap(); - // - //// pop the dialog if we are currently displaying one - //self.dialogs.contains_key(&curr).then(|| self.dialog_stack.pop()); - // - //let route = if self.dialogs.contains_key(&prev) { - // self.get_active_dialog() - // .expect("previous route is a dialog, but its not on the stack") - //} else { - // self.routes - // .get_mut(&prev) - // .expect("previous route is not registered... how?") - //}; - // - //route.update(data); + Navigate::Back(route) => { + tracing::trace!("navigating back from route: {route}"); + if self.history.len() <= 1 { + return; + } + + let curr_route = match self.get_active_dialog() { + Some(dialog) => dialog, + None => self.get_active_route(), + }; + let data = curr_route.data(route); + + // SAFETY: we just checked if these exists + let curr = self.history.pop().unwrap(); + let prev = *self.history.last().unwrap(); + + self.dialogs.contains_key(&curr).then(|| self.dialog_stack.pop()); + + let route = match self.dialogs.contains_key(&prev) { + true => self + .get_active_dialog() + .expect("previous route is a dialog, but its not on the stack"), + false => self + .routes + .get_mut(&prev) + .expect("previous route is not registered... how?"), + }; + + route.update(data); + } Navigate::Leave() => todo!(), //self.history.clear(); //self.dialog_stack.clear(); @@ -249,6 +259,13 @@ impl Renderable for Router { return Ok(()); } + match self.message_receiver.try_recv() { + Ok(RouterMessage::AddRoute(key, route)) => self.add_route(key, route), + Ok(RouterMessage::AddDialog(key, route)) => self.add_dialog(key, route), + Ok(RouterMessage::DelRoute(key)) => _ = self.routes.remove(&key), + Err(_) => {} + }; + if let Ok(navigation) = self.navigate_receiver.try_recv() { self.navigate(navigation) } @@ -283,7 +300,7 @@ impl Renderable for Router { } } - fn data(&self) -> Self::Input {} + fn data(&self, _requester: u8) -> Self::Input {} } impl Eventful for Router { diff --git a/hac-loader/src/collection_loader/mod.rs b/hac-loader/src/collection_loader/mod.rs index 1c0e0a1..20c8825 100644 --- a/hac-loader/src/collection_loader/mod.rs +++ b/hac-loader/src/collection_loader/mod.rs @@ -1,15 +1,16 @@ mod json_collection; mod json_loader; +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::SystemTime; + use chrono::{Datelike, Timelike}; use hac_config::config::CollectionExtensions; use hac_store::collection::Collection; use json_loader::JsonLoader; use notify::{RecursiveMode, Watcher}; -use std::cell::RefCell; -use std::rc::Rc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::SystemTime; static HAS_CHANGES: AtomicBool = AtomicBool::new(false); static HAS_WATCHER: AtomicBool = AtomicBool::new(false); From f7b9192d0b3e47e7e4dc660e8923c7140d1fca6c Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Fri, 27 Sep 2024 00:18:41 -0300 Subject: [PATCH 09/23] feat: better delete collection screen --- hac-client/src/app.rs | 4 +- hac-client/src/event_pool.rs | 5 +- hac-client/src/main.rs | 19 +- hac-client/src/pages.rs | 4 +- .../collection_list/create_collection.rs | 19 +- .../collection_list/delete_collection.rs | 170 ++++++++++++++++++ .../pages/collection_list/edit_collection.rs | 26 ++- .../src/pages/collection_list/form_shared.rs | 4 +- hac-client/src/pages/collection_list/mod.rs | 70 +++++--- hac-client/src/pages/under_construction.rs | 4 +- hac-client/src/router.rs | 128 +++++++------ hac-core/src/net/request_manager.rs | 10 +- .../src/collection_loader/json_collection.rs | 9 +- hac-loader/src/collection_loader/mod.rs | 30 ++++ 14 files changed, 380 insertions(+), 122 deletions(-) create mode 100644 hac-client/src/pages/collection_list/delete_collection.rs diff --git a/hac-client/src/app.rs b/hac-client/src/app.rs index 2493a3f..db3929c 100755 --- a/hac-client/src/app.rs +++ b/hac-client/src/app.rs @@ -52,7 +52,7 @@ impl App { config.clone(), colors.clone(), ); - collection_list_router.attach_parent_navigator(router.navigate_sender()); + collection_list_router.attach_parent_navigator(router.message_sender()); router.add_route(AppRoutes::CollectionListRouter.into(), Box::new(collection_list_router)); Ok(Self { @@ -82,7 +82,7 @@ impl App { } } - if let Some(event) = self.event_pool.next() { + if let Some(event) = self.event_pool.next_event() { match event { Event::Tick => self.router.tick()?, Event::Resize(new_size) => self.router.resize(new_size), diff --git a/hac-client/src/event_pool.rs b/hac-client/src/event_pool.rs index 00fdf5e..15a6f9b 100755 --- a/hac-client/src/event_pool.rs +++ b/hac-client/src/event_pool.rs @@ -1,7 +1,8 @@ +use std::ops::Div; + use crossterm::event::{Event as CrosstermEvent, KeyEventKind}; use futures::{FutureExt, StreamExt}; use ratatui::layout::Rect; -use std::ops::Div; #[derive(Debug, Clone, PartialEq)] pub enum Event { @@ -75,7 +76,7 @@ impl EventPool { } #[cfg_attr(test, mutants::skip)] - pub fn next(&mut self) -> Option { + pub fn next_event(&mut self) -> Option { self.event_rx.recv().ok() } } diff --git a/hac-client/src/main.rs b/hac-client/src/main.rs index eae30b2..4edd42e 100755 --- a/hac-client/src/main.rs +++ b/hac-client/src/main.rs @@ -1,10 +1,8 @@ use std::cell::RefCell; use std::rc::Rc; -use std::sync::RwLock; use hac_cli::RuntimeBehavior; use hac_client::app; -use hac_config::Config; fn setup_tracing() -> anyhow::Result { let logfile = hac_config::LOGFILE; @@ -26,23 +24,9 @@ fn setup_tracing() -> anyhow::Result anyhow::Result<()> { let runtime_behavior = hac_cli::Cli::parse_args(); - //match runtime_behavior { - // RuntimeBehavior::PrintConfigPath => hac_cli::Cli::print_config_path( - // hac_config::get_config_dir_path(), - // hac_config::get_usual_path(), - // ), - // RuntimeBehavior::PrintDataPath => { - // hac_cli::Cli::print_data_path(hac_config::get_collections_dir()) - // } - // RuntimeBehavior::DumpDefaultConfig => { - // hac_cli::Cli::print_default_config(hac_config::default_as_str()) - // } - // _ => {} - //} - let dry_run = runtime_behavior.eq(&RuntimeBehavior::DryRun); - let _guard = setup_tracing()?; + let guard = setup_tracing()?; hac_loader::get_or_create_data_dir(); hac_loader::get_or_create_collections_dir(); @@ -55,5 +39,6 @@ async fn main() -> anyhow::Result<()> { let mut app = app::App::new(collections, Rc::new(RefCell::new(config)), Rc::new(colors))?; app.run().await?; + _ = guard; Ok(()) } diff --git a/hac-client/src/pages.rs b/hac-client/src/pages.rs index faec882..51ce5ee 100755 --- a/hac-client/src/pages.rs +++ b/hac-client/src/pages.rs @@ -14,7 +14,7 @@ use ratatui::layout::Rect; use ratatui::Frame; use crate::event_pool::Event; -use crate::router::{Navigate, RouterMessage}; +use crate::router::RouterMessage; /// A `Renderable` is anything that is a top level page and can be drawn to the screen pub trait Renderable { @@ -34,7 +34,7 @@ pub trait Renderable { /// each route can, (and must) have a navigator, this is how you attach one #[allow(unused_variables)] - fn attach_navigator(&mut self, navigator: Sender, messager: Sender) {} + fn attach_navigator(&mut self, messager: Sender) {} /// pages need to adapt to change of sizes on the application, this function is called /// by the top level event loop whenever a resize event is produced diff --git a/hac-client/src/pages/collection_list/create_collection.rs b/hac-client/src/pages/collection_list/create_collection.rs index 56c7ef1..684074f 100644 --- a/hac-client/src/pages/collection_list/create_collection.rs +++ b/hac-client/src/pages/collection_list/create_collection.rs @@ -27,7 +27,6 @@ pub struct CreateCollection { cursor: Cursor, config: HacConfig, layout: FormLayout, - navigator: Sender, messager: Sender, } @@ -42,7 +41,6 @@ impl CreateCollection { desc: TextObject::::default(), cursor: Cursor::default(), collections: Default::default(), - navigator: channel().0, messager: channel().0, } } @@ -81,8 +79,7 @@ impl Renderable for CreateCollection { self.layout = build_form_layout(new_size); } - fn attach_navigator(&mut self, navigator: Sender, messager: Sender) { - self.navigator = navigator; + fn attach_navigator(&mut self, messager: Sender) { self.messager = messager; } } @@ -102,15 +99,21 @@ impl Eventful for CreateCollection { self.collections.clone(), &self.config, )?; - self.navigator - .send(Navigate::Back(Routes::ListCollections.into())) + self.messager + .send(RouterMessage::Navigate(Navigate::Back)) .expect("failed to send navigate message"); + self.messager + .send(RouterMessage::DelDialog(Routes::CreateCollection.into())) + .expect("failed to send router message"); } Some(FormEvent::Cancel) => { self.reset(); - self.navigator - .send(Navigate::To(Routes::ListCollections.into())) + self.messager + .send(RouterMessage::Navigate(Navigate::Back)) .expect("failed to send navigate message"); + self.messager + .send(RouterMessage::DelDialog(Routes::CreateCollection.into())) + .expect("failed to send router message"); } _ => {} } diff --git a/hac-client/src/pages/collection_list/delete_collection.rs b/hac-client/src/pages/collection_list/delete_collection.rs new file mode 100644 index 0000000..9c20f0a --- /dev/null +++ b/hac-client/src/pages/collection_list/delete_collection.rs @@ -0,0 +1,170 @@ +use std::sync::mpsc::{channel, Sender}; + +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use hac_core::command::Command; +use hac_loader::collection_loader::CollectionMeta; +use ratatui::layout::{Constraint, Flex, Layout, Margin, Rect}; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::widgets::{Block, Borders, Paragraph, Wrap}; +use ratatui::Frame; + +use super::{CollectionListData, Routes}; +use crate::ascii::LOGO_ASCII; +use crate::pages::overlay::make_overlay; +use crate::pages::{Eventful, Renderable}; +use crate::router::{Navigate, RouterMessage}; +use crate::{HacColors, HacConfig, MIN_HEIGHT, MIN_WIDTH}; + +#[derive(Debug)] +struct DeleteCollectionLayout { + logo: Rect, + left_button: Rect, + right_button: Rect, + name: Rect, +} + +#[derive(Debug)] +pub struct DeleteCollection { + layout: DeleteCollectionLayout, + colors: HacColors, + selected_idx: usize, + collections: Vec, + messager: Sender, + config: HacConfig, +} + +impl DeleteCollection { + pub fn new(size: Rect, colors: HacColors, config: HacConfig) -> Self { + Self { + config, + colors, + selected_idx: 0, + messager: channel().0, + layout: build_layout(size), + collections: Default::default(), + } + } +} + +impl Renderable for DeleteCollection { + type Input = CollectionListData; + type Output = (String, Vec); + + fn draw(&mut self, frame: &mut Frame, _size: Rect) -> anyhow::Result<()> { + make_overlay(self.colors.clone(), self.colors.normal.black, 0.15, frame); + + let logo = Paragraph::new( + LOGO_ASCII + .iter() + .map(|line| Line::from(line.to_string()).fg(self.colors.bright.red).centered()) + .collect::>(), + ); + + let name = self.collections[self.selected_idx].name().to_string(); + let name = format!("are you sure you want to delete {name}?"); + let name = Paragraph::new(Line::from(name).fg(self.colors.normal.red).centered()).wrap(Wrap { trim: true }); + + let left_button = Paragraph::new(Line::from("[ENTER] CONFIRM").centered()) + .block(Block::default().borders(Borders::ALL).fg(self.colors.bright.red)) + .fg(self.colors.normal.black) + .bg(self.colors.bright.red) + .bold(); + + let right_button = Paragraph::new(Line::from("[ESC] CANCEL").centered()) + .block(Block::default().borders(Borders::ALL).fg(self.colors.normal.blue)) + .fg(self.colors.bright.red) + .bg(self.colors.normal.blue) + .bold(); + + frame.render_widget(logo, self.layout.logo); + frame.render_widget(name, self.layout.name); + frame.render_widget(left_button, self.layout.left_button); + frame.render_widget(right_button, self.layout.right_button); + + Ok(()) + } + + fn data(&self, _requester: u8) -> Self::Output { + let collections = self.collections.clone(); + let name = collections[self.selected_idx].name().to_string(); + (name, collections) + } + + fn update(&mut self, data: Self::Input) { + if let CollectionListData::DeleteCollection(selected_idx, collections) = data { + self.collections = collections; + self.selected_idx = selected_idx; + } + } + + fn attach_navigator(&mut self, messager: Sender) { + self.messager = messager; + } +} + +impl Eventful for DeleteCollection { + type Result = Command; + + fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { + return Ok(Some(Command::Quit)); + } + + match key_event.code { + KeyCode::Esc => { + self.messager + .send(RouterMessage::Navigate(Navigate::Back)) + .expect("failed to send router message"); + self.messager + .send(RouterMessage::DelDialog(Routes::DeleteCollection.into())) + .expect("failed to send router message"); + } + KeyCode::Char('o') | KeyCode::Char('y') | KeyCode::Enter => { + let name = self.collections[self.selected_idx].name().to_string(); + self.collections = + hac_loader::collection_loader::delete_collection(name, self.collections.clone(), &self.config)?; + self.messager + .send(RouterMessage::Navigate(Navigate::Back)) + .expect("failed to send router message"); + self.messager + .send(RouterMessage::DelDialog(Routes::DeleteCollection.into())) + .expect("failed to send router message"); + } + _ => {} + } + + Ok(None) + } +} + +fn build_layout(total_size: Rect) -> DeleteCollectionLayout { + let [_, form, _] = Layout::horizontal([Constraint::Fill(1), Constraint::Length(MIN_WIDTH), Constraint::Fill(1)]) + .flex(Flex::Center) + .areas(total_size); + + let [_, form, _] = Layout::vertical([Constraint::Fill(1), Constraint::Length(MIN_HEIGHT), Constraint::Fill(1)]) + .flex(Flex::End) + .areas(form); + + let form = form.inner(&Margin::new(2, 0)); + let [logo, _, name, _, buttons] = Layout::vertical([ + Constraint::Length(LOGO_ASCII.len() as u16), + Constraint::Length(1), + Constraint::Length(3), + Constraint::Length(1), + Constraint::Length(3), + ]) + .flex(Flex::Center) + .areas(form); + + let [left_button, _, right_button] = + Layout::horizontal([Constraint::Fill(1), Constraint::Length(1), Constraint::Fill(1)]).areas(buttons); + + DeleteCollectionLayout { + name, + logo, + left_button, + right_button, + } +} diff --git a/hac-client/src/pages/collection_list/edit_collection.rs b/hac-client/src/pages/collection_list/edit_collection.rs index 984a6d9..fc10f5b 100644 --- a/hac-client/src/pages/collection_list/edit_collection.rs +++ b/hac-client/src/pages/collection_list/edit_collection.rs @@ -28,7 +28,6 @@ pub struct EditCollection { cursor: Cursor, config: HacConfig, layout: FormLayout, - navigator: Sender, messager: Sender, } @@ -44,7 +43,6 @@ impl EditCollection { cursor: Cursor::default(), collections: Default::default(), collection_idx: 0, - navigator: channel().0, messager: channel().0, } } @@ -73,6 +71,7 @@ impl Renderable for EditCollection { fn update(&mut self, data: Self::Input) { if let CollectionListData::EditCollection(idx, data) = data { + tracing::debug!("new update {data:?}"); self.collections = data; self.collection_idx = idx; self.name = TextObject::from(self.collections[idx].name()).with_write(); @@ -85,8 +84,7 @@ impl Renderable for EditCollection { self.layout = build_form_layout(new_size); } - fn attach_navigator(&mut self, navigator: Sender, messager: Sender) { - self.navigator = navigator; + fn attach_navigator(&mut self, messager: Sender) { self.messager = messager; } } @@ -101,15 +99,27 @@ impl Eventful for EditCollection { match handle_form_key_event(key_event, &mut self.name, &mut self.cursor)? { Some(FormEvent::Confirm) => { - self.navigator - .send(Navigate::To(Routes::ListCollections.into())) + self.collections = hac_loader::collection_loader::edit_collection( + self.name.to_string(), + self.collections.clone(), + self.collection_idx, + &self.config, + )?; + self.messager + .send(RouterMessage::Navigate(Navigate::Back)) .expect("failed to send navigate message"); + self.messager + .send(RouterMessage::DelDialog(Routes::EditCollection.into())) + .expect("failed to send router message"); } Some(FormEvent::Cancel) => { self.reset(); - self.navigator - .send(Navigate::To(Routes::ListCollections.into())) + self.messager + .send(RouterMessage::Navigate(Navigate::Back)) .expect("failed to send navigate message"); + self.messager + .send(RouterMessage::DelDialog(Routes::EditCollection.into())) + .expect("failed to send router message"); } _ => {} } diff --git a/hac-client/src/pages/collection_list/form_shared.rs b/hac-client/src/pages/collection_list/form_shared.rs index a9c901e..cf53397 100644 --- a/hac-client/src/pages/collection_list/form_shared.rs +++ b/hac-client/src/pages/collection_list/form_shared.rs @@ -1,7 +1,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::text_object::cursor::Cursor; use hac_core::text_object::{TextObject, Write}; -use ratatui::layout::{Constraint, Flex, Layout, Rect}; +use ratatui::layout::{Constraint, Flex, Layout, Margin, Rect}; use ratatui::style::{Style, Stylize}; use ratatui::text::Line; use ratatui::widgets::Paragraph; @@ -33,7 +33,7 @@ pub fn build_form_layout(area: Rect) -> FormLayout { .flex(Flex::End) .areas(form); - let form = form.inner(&ratatui::layout::Margin::new(2, 0)); + let form = form.inner(&Margin::new(2, 0)); let [logo, _, name_input, _, hint] = Layout::vertical([ Constraint::Length(LOGO_ASCII.len() as u16), diff --git a/hac-client/src/pages/collection_list/mod.rs b/hac-client/src/pages/collection_list/mod.rs index ba2a44f..f4c277a 100644 --- a/hac-client/src/pages/collection_list/mod.rs +++ b/hac-client/src/pages/collection_list/mod.rs @@ -1,4 +1,5 @@ mod create_collection; +mod delete_collection; mod edit_collection; mod form_shared; @@ -7,6 +8,7 @@ use std::sync::mpsc::{channel, Sender}; use create_collection::CreateCollection; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use delete_collection::DeleteCollection; use edit_collection::EditCollection; use hac_core::command::Command; use hac_loader::collection_loader::{CollectionMeta, ReadableByteSize}; @@ -27,15 +29,30 @@ pub enum Routes { ListCollections, CreateCollection, EditCollection, + DeleteCollection, } -impl From for Routes { - fn from(value: u8) -> Self { +#[derive(Debug)] +pub struct InvalidRouteNumber(u8); + +impl std::error::Error for InvalidRouteNumber {} + +impl std::fmt::Display for InvalidRouteNumber { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "number {} is not a valid route number", self.0) + } +} + +impl TryFrom for Routes { + type Error = InvalidRouteNumber; + + fn try_from(value: u8) -> Result { match value { - 0 => Routes::ListCollections, - 1 => Routes::CreateCollection, - 2 => Routes::EditCollection, - _ => panic!("invalid route number"), + 0 => Ok(Routes::ListCollections), + 1 => Ok(Routes::CreateCollection), + 2 => Ok(Routes::EditCollection), + 3 => Ok(Routes::DeleteCollection), + _ => Err(InvalidRouteNumber(value)), } } } @@ -46,6 +63,7 @@ impl From for u8 { Routes::ListCollections => 0, Routes::CreateCollection => 1, Routes::EditCollection => 2, + Routes::DeleteCollection => 3, } } } @@ -105,7 +123,6 @@ pub struct CollectionList { selected: usize, config: HacConfig, scroll: usize, - navigator: Sender, messager: Sender, layout: DashboardLayout, pub command_sender: Option>, @@ -122,7 +139,6 @@ impl CollectionList { selected: 0, scroll: 0, sorting_kind: SortingKind::default(), - navigator: channel().0, messager: channel().0, collections, layout: build_layout(size, false), @@ -274,6 +290,7 @@ impl CollectionList { pub enum CollectionListData { CreateCollection(Vec), EditCollection(usize, Vec), + DeleteCollection(usize, Vec), } impl Renderable for CollectionList { @@ -281,10 +298,14 @@ impl Renderable for CollectionList { type Output = CollectionListData; fn data(&self, requester: u8) -> Self::Output { - match Routes::from(requester) { - Routes::CreateCollection => CollectionListData::CreateCollection(self.collections.clone()), - Routes::ListCollections => CollectionListData::CreateCollection(self.collections.clone()), - Routes::EditCollection => CollectionListData::EditCollection(self.selected, self.collections.clone()), + match Routes::try_from(requester) { + Ok(Routes::CreateCollection) => CollectionListData::CreateCollection(self.collections.clone()), + Ok(Routes::EditCollection) => CollectionListData::EditCollection(self.selected, self.collections.clone()), + Ok(Routes::DeleteCollection) => { + CollectionListData::DeleteCollection(self.selected, self.collections.clone()) + } + Ok(Routes::ListCollections) => unreachable!(), + Err(_) => unreachable!(), } } @@ -305,8 +326,7 @@ impl Renderable for CollectionList { Ok(()) } - fn attach_navigator(&mut self, navigator: Sender, messager: Sender) { - self.navigator = navigator; + fn attach_navigator(&mut self, messager: Sender) { self.messager = messager; } @@ -319,6 +339,7 @@ impl Renderable for CollectionList { } fn update(&mut self, data: Self::Input) { + tracing::debug!("{data:?}"); self.collections = data.1; self.sort_list(); self.selected = self @@ -369,6 +390,15 @@ impl Eventful for CollectionList { self.selected = usize::min(self.selected + half as usize, self.collections.len() - 1); self.maybe_scroll_list(); } + KeyCode::Char('d') => { + let delete_form = + DeleteCollection::new(self.layout.total_size, self.colors.clone(), self.config.clone()); + let message = RouterMessage::AddDialog(Routes::DeleteCollection.into(), Box::new(delete_form)); + self.messager.send(message).expect("failed to create new route"); + self.messager + .send(RouterMessage::Navigate(Navigate::To(Routes::DeleteCollection.into()))) + .expect("failed to send navigation message"); + } KeyCode::Char('u') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { self.selected = 0; self.maybe_scroll_list(); @@ -395,16 +425,16 @@ impl Eventful for CollectionList { CreateCollection::new(self.layout.total_size, self.config.clone(), self.colors.clone()); let message = RouterMessage::AddDialog(Routes::CreateCollection.into(), Box::new(create_form)); self.messager.send(message).expect("failed to create new route"); - self.navigator - .send(Navigate::To(Routes::CreateCollection.into())) + self.messager + .send(RouterMessage::Navigate(Navigate::To(Routes::CreateCollection.into()))) .expect("failed to send navigation message"); } KeyCode::Char('e') => { let edit_form = EditCollection::new(self.layout.total_size, self.config.clone(), self.colors.clone()); let message = RouterMessage::AddDialog(Routes::EditCollection.into(), Box::new(edit_form)); self.messager.send(message).expect("failed to create new route"); - self.navigator - .send(Navigate::To(Routes::EditCollection.into())) + self.messager + .send(RouterMessage::Navigate(Navigate::To(Routes::EditCollection.into()))) .expect("failed to send navigation message"); } KeyCode::Enter => { @@ -412,8 +442,8 @@ impl Eventful for CollectionList { return Ok(None); } assert!(self.collections.len() > self.selected); - self.navigator - .send(Navigate::Leave()) + self.messager + .send(RouterMessage::Navigate(Navigate::Leave())) .expect("failed to send navigation message"); } _ => {} diff --git a/hac-client/src/pages/under_construction.rs b/hac-client/src/pages/under_construction.rs index ef4b8fb..7d26bd3 100755 --- a/hac-client/src/pages/under_construction.rs +++ b/hac-client/src/pages/under_construction.rs @@ -34,11 +34,11 @@ impl Renderable for UnderConstruction<'_> { let half_height = size.height.div_ceil(2); let starting_y = half_height.saturating_sub(icon_half_height as u16).saturating_sub(1); - let icon_size = Rect::new(size.x, size.y.add(starting_y), size.width, icon_height as u16); + let _icon_size = Rect::new(size.x, size.y.add(starting_y), size.width, icon_height as u16); let message = Line::from("Hold on, we're cooking up something new!").fg(self.colors.normal.red); - let message_size = Rect::new( + let _message_size = Rect::new( size.x, size.y.add(starting_y).add(icon_height as u16).add(1), size.width, diff --git a/hac-client/src/router.rs b/hac-client/src/router.rs index 26d7c3c..409edf1 100644 --- a/hac-client/src/router.rs +++ b/hac-client/src/router.rs @@ -1,4 +1,4 @@ -use std::any::{Any, TypeId}; +use std::any::Any; use std::collections::HashMap; use std::fmt::Debug; use std::sync::mpsc::{Receiver, Sender}; @@ -31,7 +31,7 @@ pub enum Navigate { Leave(), /// Go back in the navigation history, this completely wipes the current router history, /// and close every dialog thats visible - Back(Key), + Back, } pub trait AnyCommand {} @@ -43,7 +43,7 @@ pub trait AnyRenderable: Debug { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()>; fn data(&self, requester: Key) -> Box; fn update(&mut self, input: Box); - fn attach_navigator(&mut self, navigator: Sender, messager: Sender); + fn attach_navigator(&mut self, messager: Sender); fn resize(&mut self, new_size: Rect); fn register_command_handler(&mut self, sender: Sender); fn handle_command(&mut self, command: Command) -> anyhow::Result<()>; @@ -68,8 +68,8 @@ impl + Debug> AnyRenderable ::update(self, *input); } - fn attach_navigator(&mut self, navigator: Sender, messager: Sender) { - ::attach_navigator(self, navigator, messager); + fn attach_navigator(&mut self, messager: Sender) { + ::attach_navigator(self, messager); } fn resize(&mut self, new_size: Rect) { @@ -102,6 +102,8 @@ pub enum RouterMessage { AddRoute(Key, Box>), AddDialog(Key, Box>), DelRoute(Key), + DelDialog(Key), + Navigate(Navigate), } #[derive(Debug)] @@ -114,15 +116,12 @@ pub struct Router { command_sender: Sender, message_receiver: Receiver, message_sender: Sender, - navigate_sender: Sender, - navigate_receiver: Receiver, - parent_navigator: Option>, + parent_messager: Option>, too_small: TerminalTooSmall, } impl Router { pub fn new(command_sender: Sender, colors: HacColors) -> Self { - let (navigate_sender, navigate_receiver) = std::sync::mpsc::channel(); let (message_sender, message_receiver) = std::sync::mpsc::channel(); Self { routes: Default::default(), @@ -133,25 +132,23 @@ impl Router { message_receiver, message_sender, command_sender, - navigate_sender, - navigate_receiver, - parent_navigator: None, + parent_messager: None, too_small: TerminalTooSmall::new(colors.clone()), } } - pub fn navigate_sender(&self) -> Sender { - self.navigate_sender.clone() + pub fn message_sender(&self) -> Sender { + self.message_sender.clone() } pub fn add_route(&mut self, key: Key, mut route: Box>) { - route.attach_navigator(self.navigate_sender.clone(), self.message_sender.clone()); + route.attach_navigator(self.message_sender.clone()); route.register_command_handler(self.command_sender.clone()); self.routes.insert(key, route); } pub fn add_dialog(&mut self, key: Key, mut dialog: Box>) { - dialog.attach_navigator(self.navigate_sender.clone(), self.message_sender.clone()); + dialog.attach_navigator(self.message_sender.clone()); dialog.register_command_handler(self.command_sender.clone()); self.dialogs.insert(key, dialog); } @@ -162,8 +159,8 @@ impl Router { Ok(()) } - pub fn attach_parent_navigator(&mut self, navigator: Sender) { - self.parent_navigator = Some(navigator); + pub fn attach_parent_navigator(&mut self, messager: Sender) { + self.parent_messager = Some(messager); } fn get_active_route(&mut self) -> &mut Box> { @@ -198,6 +195,8 @@ impl Router { route.update(data) } else if self.dialogs.contains_key(&route) { self.dialog_stack.push(route); + self.history.push(route); + tracing::debug!("navigating to -> {} {}", self.history.len(), self.dialog_stack.len()); let dialog = self .get_active_dialog() .expect("attempt to navigate to non registered dialog"); @@ -206,45 +205,63 @@ impl Router { panic!("tried to navigate to an unknown route"); } } - Navigate::Back(route) => { - tracing::trace!("navigating back from route: {route}"); + Navigate::Back => { if self.history.len() <= 1 { return; } - let curr_route = match self.get_active_dialog() { - Some(dialog) => dialog, - None => self.get_active_route(), - }; - let data = curr_route.data(route); - // SAFETY: we just checked if these exists let curr = self.history.pop().unwrap(); let prev = *self.history.last().unwrap(); - self.dialogs.contains_key(&curr).then(|| self.dialog_stack.pop()); - - let route = match self.dialogs.contains_key(&prev) { - true => self - .get_active_dialog() - .expect("previous route is a dialog, but its not on the stack"), - false => self - .routes - .get_mut(&prev) - .expect("previous route is not registered... how?"), - }; - - route.update(data); + // there are only 3 valid scenarios here. + // 1. we are currently on a route, navigating to a previous route; + // 2. we are currently on a dialog, navigating to a previous route; + // 3. we are currently on a dialog, navigating to a previous dialog. + // + // navigating from a route back to a dialog is invalid as we always close + // dialogs when navigating to routes. + + let curr_is_dialog = self.dialogs.contains_key(&curr); + let prev_is_dialog = self.dialogs.contains_key(&prev); + tracing::debug!("im really confused, {} {}", self.history.len(), self.dialog_stack.len()); + + match (curr_is_dialog, prev_is_dialog) { + // we are on a route, navigating to a route + (false, false) => { + let curr_route = self.get_active_route(); + let data = curr_route.data(prev); + self.active_route = prev; + let new_route = self.get_active_route(); + new_route.update(data); + } + // we are on a dialog, navigating to a route + (true, false) => { + let dialog = self + .get_active_dialog() + .expect("invalid navigation from dialog without displaying a dialog"); + let data = dialog.data(prev); + self.dialog_stack.pop(); + self.active_route = prev; + let new_route = self.get_active_route(); + new_route.update(data); + } + // we are on a dialog, navigating to a dialog + (true, true) => { + let curr_dialog = self + .get_active_dialog() + .expect("invalid navigation from dialog without displaying a dialog"); + let data = curr_dialog.data(prev); + self.dialog_stack.pop(); + let new_dialog = self + .get_active_dialog() + .expect("invalid navigation from dialog to dialog without a previous dialog"); + new_dialog.update(data); + } + (false, true) => panic!("invalid navigation from route to dialog"), + } } Navigate::Leave() => todo!(), - //self.history.clear(); - //self.dialog_stack.clear(); - // - //self.parent_navigator - // .as_mut() - // .unwrap() - // .send(Navigate::To(route, data)) - // .expect("failed to send navigation command"); } } } @@ -262,14 +279,21 @@ impl Renderable for Router { match self.message_receiver.try_recv() { Ok(RouterMessage::AddRoute(key, route)) => self.add_route(key, route), Ok(RouterMessage::AddDialog(key, route)) => self.add_dialog(key, route), - Ok(RouterMessage::DelRoute(key)) => _ = self.routes.remove(&key), + Ok(RouterMessage::DelRoute(key)) => { + tracing::trace!("dropping route with key: {key}"); + self.history.retain(|&k| k != key); + self.routes.remove(&key); + } + Ok(RouterMessage::DelDialog(key)) => { + tracing::trace!("dropping dialog with key: {key}"); + self.dialog_stack.retain(|&k| k != key); + self.history.retain(|&k| k != key); + self.dialogs.remove(&key); + } + Ok(RouterMessage::Navigate(navigation)) => self.navigate(navigation), Err(_) => {} }; - if let Ok(navigation) = self.navigate_receiver.try_recv() { - self.navigate(navigation) - } - let route = self.get_active_route(); route.draw(frame, size)?; diff --git a/hac-core/src/net/request_manager.rs b/hac-core/src/net/request_manager.rs index 27e1b0c..a49b90d 100755 --- a/hac-core/src/net/request_manager.rs +++ b/hac-core/src/net/request_manager.rs @@ -1,13 +1,13 @@ -use crate::net::request_strategies::{http_strategy::HttpResponse, RequestStrategy}; -use crate::text_object::{Readonly, TextObject}; -use hac_store::collection::{BodyKind, Request}; - -use std::sync::{Arc, RwLock}; use std::time::Duration; +use hac_store::collection::{BodyKind, Request}; use reqwest::header::{HeaderMap, HeaderValue}; use tokio::sync::mpsc::UnboundedSender; +use crate::net::request_strategies::http_strategy::HttpResponse; +use crate::net::request_strategies::RequestStrategy; +use crate::text_object::{Readonly, TextObject}; + #[derive(Debug, PartialEq)] pub struct Response { pub body: Option, diff --git a/hac-loader/src/collection_loader/json_collection.rs b/hac-loader/src/collection_loader/json_collection.rs index 1399318..49398c1 100644 --- a/hac-loader/src/collection_loader/json_collection.rs +++ b/hac-loader/src/collection_loader/json_collection.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Default)] @@ -15,12 +17,15 @@ pub struct JsonCollection { } impl JsonCollection { - pub fn new(name: String, description: String, file_name: String, path: &std::path::PathBuf) -> Self { + pub fn new

(name: String, description: String, file_name: String, path: P) -> Self + where + P: AsRef, + { Self { info: JsonCollectionInfo { name, description }, file_info: JsonInfo { name: file_name, - path: path.clone(), + path: path.as_ref().to_path_buf(), }, requests: Default::default(), } diff --git a/hac-loader/src/collection_loader/mod.rs b/hac-loader/src/collection_loader/mod.rs index 20c8825..635b0be 100644 --- a/hac-loader/src/collection_loader/mod.rs +++ b/hac-loader/src/collection_loader/mod.rs @@ -110,6 +110,36 @@ pub fn create_collection( } } +fn edit_persistent_collection(entry: &mut CollectionMeta) -> anyhow::Result<()> { + let original_path = entry.path().clone(); + let mut new_path = entry.path().clone(); + new_path.pop(); + let new_path = new_path.join(entry.name()); + std::fs::rename(original_path, new_path)?; + Ok(()) +} + +pub fn edit_collection( + name: String, + mut collections: Vec, + item_idx: usize, + config: &Rc>, +) -> anyhow::Result> { + let entry = &mut collections[item_idx]; + match config.borrow().dry_run { + false => { + edit_persistent_collection(entry)?; + collections_metadata() + } + true => { + entry.path.pop(); + entry.path = entry.path.join(&name); + entry.name = name; + Ok(collections) + } + } +} + fn delete_persistent_collection(file_name: String, collections: Vec) -> anyhow::Result<()> { let path = collections .iter() From 19952813568631d6d2f613d762c44b8bd8523da2 Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Fri, 27 Sep 2024 20:15:23 -0300 Subject: [PATCH 10/23] feat: moving collection metadata to store --- Cargo.lock | 2 +- hac-client/src/app.rs | 14 +- hac-client/src/lib.rs | 1 + hac-client/src/main.rs | 5 +- .../collection_list/create_collection.rs | 21 +- .../collection_list/delete_collection.rs | 60 ++-- .../pages/collection_list/edit_collection.rs | 29 +- hac-client/src/pages/collection_list/mod.rs | 185 +++++------ .../collection_viewer/collection_store.rs | 72 ++-- .../collection_viewer/collection_viewer.rs | 20 +- hac-client/src/pages/mod.rs | 7 + hac-client/src/pages/terminal_too_small.rs | 2 +- hac-client/src/pages/under_construction.rs | 2 +- hac-client/src/{pages.rs => renderable.rs} | 8 - hac-client/src/router.rs | 6 +- hac-core/src/fs.rs | 5 - hac-core/src/fs/error.rs | 20 -- hac-core/src/fs/fs.rs | 64 ---- hac-core/src/lib.rs | 1 - hac-core/src/{net.rs => net/mod.rs} | 0 .../src/net/response_decoders/json_decoder.rs | 10 +- hac-core/src/{syntax.rs => syntax/mod.rs} | 0 hac-core/src/text_object.rs | 5 - .../text_object/{text_object.rs => mod.rs} | 10 +- hac-loader/Cargo.toml | 1 - hac-loader/src/collection_loader/error.rs | 24 ++ .../src/collection_loader/json_loader.rs | 11 +- hac-loader/src/collection_loader/mod.rs | 313 +++++------------- hac-store/Cargo.toml | 1 + hac-store/src/collection_meta.rs | 220 ++++++++++++ hac-store/src/lib.rs | 32 ++ hac-store/src/slab.rs | 10 +- 32 files changed, 550 insertions(+), 611 deletions(-) create mode 100755 hac-client/src/pages/mod.rs rename hac-client/src/{pages.rs => renderable.rs} (94%) mode change 100755 => 100644 delete mode 100755 hac-core/src/fs.rs delete mode 100755 hac-core/src/fs/error.rs delete mode 100755 hac-core/src/fs/fs.rs rename hac-core/src/{net.rs => net/mod.rs} (100%) mode change 100755 => 100644 rename hac-core/src/{syntax.rs => syntax/mod.rs} (100%) mode change 100755 => 100644 delete mode 100755 hac-core/src/text_object.rs rename hac-core/src/text_object/{text_object.rs => mod.rs} (98%) mode change 100755 => 100644 create mode 100644 hac-loader/src/collection_loader/error.rs create mode 100644 hac-store/src/collection_meta.rs diff --git a/Cargo.lock b/Cargo.lock index 4ecb4e9..123c00e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -647,7 +647,6 @@ name = "hac-loader" version = "0.2.0" dependencies = [ "anyhow", - "chrono", "dirs", "hac-config", "hac-store", @@ -661,6 +660,7 @@ dependencies = [ name = "hac-store" version = "0.2.0" dependencies = [ + "chrono", "serde", ] diff --git a/hac-client/src/app.rs b/hac-client/src/app.rs index db3929c..72b15ed 100755 --- a/hac-client/src/app.rs +++ b/hac-client/src/app.rs @@ -2,13 +2,12 @@ use std::io::Stdout; use std::sync::mpsc::{Receiver, Sender}; use hac_core::command::Command; -use hac_loader::collection_loader::CollectionMeta; use ratatui::backend::CrosstermBackend; use ratatui::Terminal; use crate::event_pool::{Event, EventPool}; use crate::pages::collection_list::make_collection_list_router; -use crate::pages::{Eventful, Renderable}; +use crate::renderable::{Eventful, Renderable}; use crate::router::Router; use crate::{HacColors, HacConfig}; @@ -38,20 +37,15 @@ pub struct App { } impl App { - pub fn new(collections: Vec, config: HacConfig, colors: HacColors) -> anyhow::Result { + pub fn new(config: HacConfig, colors: HacColors) -> anyhow::Result { let terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?; let (command_sender, command_receiver) = std::sync::mpsc::channel(); let size = terminal.size()?; let mut router = Router::new(command_sender.clone(), colors.clone()); - let mut collection_list_router = make_collection_list_router( - command_sender.clone(), - collections.clone(), - size, - config.clone(), - colors.clone(), - ); + let mut collection_list_router = + make_collection_list_router(command_sender.clone(), size, config.clone(), colors.clone()); collection_list_router.attach_parent_navigator(router.message_sender()); router.add_route(AppRoutes::CollectionListRouter.into(), Box::new(collection_list_router)); diff --git a/hac-client/src/lib.rs b/hac-client/src/lib.rs index 465b214..ac765f3 100755 --- a/hac-client/src/lib.rs +++ b/hac-client/src/lib.rs @@ -3,6 +3,7 @@ mod ascii; mod components; pub mod event_pool; pub mod pages; +mod renderable; mod router; pub mod utils; diff --git a/hac-client/src/main.rs b/hac-client/src/main.rs index 4edd42e..bba722a 100755 --- a/hac-client/src/main.rs +++ b/hac-client/src/main.rs @@ -29,14 +29,13 @@ async fn main() -> anyhow::Result<()> { let guard = setup_tracing()?; hac_loader::get_or_create_data_dir(); hac_loader::get_or_create_collections_dir(); - - let collections = hac_loader::collection_loader::collections_metadata()?; + hac_loader::collection_loader::get_collections_metadata()?; let colors = hac_colors::Colors::default(); let mut config = hac_config::load_config(); config.dry_run = dry_run; - let mut app = app::App::new(collections, Rc::new(RefCell::new(config)), Rc::new(colors))?; + let mut app = app::App::new(Rc::new(RefCell::new(config)), Rc::new(colors))?; app.run().await?; _ = guard; diff --git a/hac-client/src/pages/collection_list/create_collection.rs b/hac-client/src/pages/collection_list/create_collection.rs index 684074f..e7fea1f 100644 --- a/hac-client/src/pages/collection_list/create_collection.rs +++ b/hac-client/src/pages/collection_list/create_collection.rs @@ -4,7 +4,6 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::command::Command; use hac_core::text_object::cursor::Cursor; use hac_core::text_object::{TextObject, Write}; -use hac_loader::collection_loader::CollectionMeta; use ratatui::layout::Rect; use ratatui::Frame; @@ -13,7 +12,7 @@ use super::form_shared::{ }; use super::{CollectionListData, Routes}; use crate::pages::overlay::make_overlay; -use crate::pages::{Eventful, Renderable}; +use crate::renderable::{Eventful, Renderable}; use crate::router::{Navigate, RouterMessage}; use crate::{HacColors, HacConfig}; @@ -21,7 +20,6 @@ use crate::{HacColors, HacConfig}; pub struct CreateCollection { name: TextObject, desc: TextObject, - collections: Vec, size: Rect, colors: HacColors, cursor: Cursor, @@ -40,7 +38,6 @@ impl CreateCollection { name: TextObject::::default(), desc: TextObject::::default(), cursor: Cursor::default(), - collections: Default::default(), messager: channel().0, } } @@ -54,10 +51,10 @@ impl CreateCollection { impl Renderable for CreateCollection { type Input = CollectionListData; - type Output = (String, Vec); + type Output = String; fn data(&self, _requester: u8) -> Self::Output { - (self.name.to_string(), self.collections.clone()) + self.name.to_string() } fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { @@ -68,11 +65,7 @@ impl Renderable for CreateCollection { Ok(()) } - fn update(&mut self, data: Self::Input) { - if let CollectionListData::CreateCollection(data) = data { - self.collections = data; - } - } + fn update(&mut self, _data: Self::Input) {} fn resize(&mut self, new_size: Rect) { self.size = new_size; @@ -94,11 +87,7 @@ impl Eventful for CreateCollection { match handle_form_key_event(key_event, &mut self.name, &mut self.cursor)? { Some(FormEvent::Confirm) => { - self.collections = hac_loader::collection_loader::create_collection( - self.name.to_string(), - self.collections.clone(), - &self.config, - )?; + hac_loader::collection_loader::create_collection(self.name.to_string(), &self.config)?; self.messager .send(RouterMessage::Navigate(Navigate::Back)) .expect("failed to send navigate message"); diff --git a/hac-client/src/pages/collection_list/delete_collection.rs b/hac-client/src/pages/collection_list/delete_collection.rs index 9c20f0a..efb6d26 100644 --- a/hac-client/src/pages/collection_list/delete_collection.rs +++ b/hac-client/src/pages/collection_list/delete_collection.rs @@ -2,7 +2,7 @@ use std::sync::mpsc::{channel, Sender}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::command::Command; -use hac_loader::collection_loader::CollectionMeta; +use hac_store::collection_meta::CollectionMeta; use ratatui::layout::{Constraint, Flex, Layout, Margin, Rect}; use ratatui::style::Stylize; use ratatui::text::Line; @@ -12,7 +12,7 @@ use ratatui::Frame; use super::{CollectionListData, Routes}; use crate::ascii::LOGO_ASCII; use crate::pages::overlay::make_overlay; -use crate::pages::{Eventful, Renderable}; +use crate::renderable::{Eventful, Renderable}; use crate::router::{Navigate, RouterMessage}; use crate::{HacColors, HacConfig, MIN_HEIGHT, MIN_WIDTH}; @@ -29,7 +29,6 @@ pub struct DeleteCollection { layout: DeleteCollectionLayout, colors: HacColors, selected_idx: usize, - collections: Vec, messager: Sender, config: HacConfig, } @@ -42,14 +41,13 @@ impl DeleteCollection { selected_idx: 0, messager: channel().0, layout: build_layout(size), - collections: Default::default(), } } } impl Renderable for DeleteCollection { type Input = CollectionListData; - type Output = (String, Vec); + type Output = String; fn draw(&mut self, frame: &mut Frame, _size: Rect) -> anyhow::Result<()> { make_overlay(self.colors.clone(), self.colors.normal.black, 0.15, frame); @@ -61,39 +59,41 @@ impl Renderable for DeleteCollection { .collect::>(), ); - let name = self.collections[self.selected_idx].name().to_string(); - let name = format!("are you sure you want to delete {name}?"); - let name = Paragraph::new(Line::from(name).fg(self.colors.normal.red).centered()).wrap(Wrap { trim: true }); + let drawer = |meta: &CollectionMeta| { + let name = meta.name().to_string(); + let name = format!("are you sure you want to delete {name}?"); + let name = Paragraph::new(Line::from(name).fg(self.colors.normal.red).centered()).wrap(Wrap { trim: true }); - let left_button = Paragraph::new(Line::from("[ENTER] CONFIRM").centered()) - .block(Block::default().borders(Borders::ALL).fg(self.colors.bright.red)) - .fg(self.colors.normal.black) - .bg(self.colors.bright.red) - .bold(); + let left_button = Paragraph::new(Line::from("[ENTER] CONFIRM").centered()) + .block(Block::default().borders(Borders::ALL).fg(self.colors.bright.red)) + .fg(self.colors.normal.black) + .bg(self.colors.bright.red) + .bold(); - let right_button = Paragraph::new(Line::from("[ESC] CANCEL").centered()) - .block(Block::default().borders(Borders::ALL).fg(self.colors.normal.blue)) - .fg(self.colors.bright.red) - .bg(self.colors.normal.blue) - .bold(); + let right_button = Paragraph::new(Line::from("[ESC] CANCEL").centered()) + .block(Block::default().borders(Borders::ALL).fg(self.colors.normal.blue)) + .fg(self.colors.bright.red) + .bg(self.colors.normal.blue) + .bold(); - frame.render_widget(logo, self.layout.logo); - frame.render_widget(name, self.layout.name); - frame.render_widget(left_button, self.layout.left_button); - frame.render_widget(right_button, self.layout.right_button); + frame.render_widget(logo, self.layout.logo); + frame.render_widget(name, self.layout.name); + frame.render_widget(left_button, self.layout.left_button); + frame.render_widget(right_button, self.layout.right_button); + }; + + hac_store::collection_meta::get_collection_meta(self.selected_idx, drawer); Ok(()) } fn data(&self, _requester: u8) -> Self::Output { - let collections = self.collections.clone(); - let name = collections[self.selected_idx].name().to_string(); - (name, collections) + let name = hac_store::collection_meta::get_collection_meta(self.selected_idx, |meta| meta.name().to_string()); + name } fn update(&mut self, data: Self::Input) { - if let CollectionListData::DeleteCollection(selected_idx, collections) = data { - self.collections = collections; + if let CollectionListData::DeleteCollection(selected_idx) = data { self.selected_idx = selected_idx; } } @@ -121,9 +121,9 @@ impl Eventful for DeleteCollection { .expect("failed to send router message"); } KeyCode::Char('o') | KeyCode::Char('y') | KeyCode::Enter => { - let name = self.collections[self.selected_idx].name().to_string(); - self.collections = - hac_loader::collection_loader::delete_collection(name, self.collections.clone(), &self.config)?; + let name = + hac_store::collection_meta::get_collection_meta(self.selected_idx, |meta| meta.name().to_string()); + hac_loader::collection_loader::delete_collection(name, &self.config)?; self.messager .send(RouterMessage::Navigate(Navigate::Back)) .expect("failed to send router message"); diff --git a/hac-client/src/pages/collection_list/edit_collection.rs b/hac-client/src/pages/collection_list/edit_collection.rs index fc10f5b..9a85bac 100644 --- a/hac-client/src/pages/collection_list/edit_collection.rs +++ b/hac-client/src/pages/collection_list/edit_collection.rs @@ -4,7 +4,6 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::command::Command; use hac_core::text_object::cursor::Cursor; use hac_core::text_object::{TextObject, Write}; -use hac_loader::collection_loader::CollectionMeta; use ratatui::layout::Rect; use ratatui::Frame; @@ -13,15 +12,13 @@ use super::form_shared::{ }; use super::{CollectionListData, Routes}; use crate::pages::overlay::make_overlay; -use crate::pages::{Eventful, Renderable}; +use crate::renderable::{Eventful, Renderable}; use crate::router::{Navigate, RouterMessage}; use crate::{HacColors, HacConfig}; #[derive(Debug)] pub struct EditCollection { name: TextObject, - desc: TextObject, - collections: Vec, collection_idx: usize, size: Rect, colors: HacColors, @@ -39,27 +36,20 @@ impl EditCollection { size, layout: build_form_layout(size), name: TextObject::::default(), - desc: TextObject::::default(), cursor: Cursor::default(), - collections: Default::default(), collection_idx: 0, messager: channel().0, } } - - fn reset(&mut self) { - self.name = Default::default(); - self.desc = Default::default(); - self.cursor = Default::default(); - } } impl Renderable for EditCollection { type Input = CollectionListData; - type Output = (String, Vec); + type Output = String; fn data(&self, _requester: u8) -> Self::Output { - (self.name.to_string(), self.collections.clone()) + tracing::debug!("{:?}", self.name); + self.name.to_string() } fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { @@ -70,11 +60,12 @@ impl Renderable for EditCollection { } fn update(&mut self, data: Self::Input) { - if let CollectionListData::EditCollection(idx, data) = data { + if let CollectionListData::EditCollection(idx) = data { tracing::debug!("new update {data:?}"); - self.collections = data; self.collection_idx = idx; - self.name = TextObject::from(self.collections[idx].name()).with_write(); + self.name = + hac_store::collection_meta::get_collection_meta(idx, |meta| TextObject::from(meta.name()).with_write()); + tracing::debug!("{:?}", self.name); self.cursor.move_to_col(self.name.line_len(0) + 1); } } @@ -99,9 +90,8 @@ impl Eventful for EditCollection { match handle_form_key_event(key_event, &mut self.name, &mut self.cursor)? { Some(FormEvent::Confirm) => { - self.collections = hac_loader::collection_loader::edit_collection( + hac_loader::collection_loader::edit_collection( self.name.to_string(), - self.collections.clone(), self.collection_idx, &self.config, )?; @@ -113,7 +103,6 @@ impl Eventful for EditCollection { .expect("failed to send router message"); } Some(FormEvent::Cancel) => { - self.reset(); self.messager .send(RouterMessage::Navigate(Navigate::Back)) .expect("failed to send navigate message"); diff --git a/hac-client/src/pages/collection_list/mod.rs b/hac-client/src/pages/collection_list/mod.rs index f4c277a..cbecc6b 100644 --- a/hac-client/src/pages/collection_list/mod.rs +++ b/hac-client/src/pages/collection_list/mod.rs @@ -11,7 +11,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use delete_collection::DeleteCollection; use edit_collection::EditCollection; use hac_core::command::Command; -use hac_loader::collection_loader::{CollectionMeta, ReadableByteSize}; +use hac_store::collection_meta::{CollectionMeta, CollectionMetaSorting, ReadableByteSize}; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::{Style, Stylize}; use ratatui::text::Line; @@ -19,7 +19,7 @@ use ratatui::widgets::{Block, Clear, Paragraph}; use ratatui::Frame; use crate::components::list_itemm::ListItem; -use crate::pages::{Eventful, Renderable}; +use crate::renderable::{Eventful, Renderable}; use crate::router::{Navigate, Router, RouterMessage}; use crate::{HacColors, HacConfig}; @@ -70,13 +70,12 @@ impl From for u8 { pub fn make_collection_list_router( command_sender: Sender, - collections: Vec, size: Rect, config: HacConfig, colors: HacColors, ) -> Router { let mut router = Router::new(command_sender, colors.clone()); - let collection_list = CollectionList::new(collections, size, config, colors.clone()); + let collection_list = CollectionList::new(size, config, colors.clone()); router.add_route(Routes::ListCollections.into(), Box::new(collection_list)); router } @@ -89,37 +88,10 @@ struct DashboardLayout { total_size: Rect, } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -enum SortingKind { - #[default] - Recent, - Name, - Size, -} - -impl SortingKind { - pub fn next(&self) -> Self { - match self { - SortingKind::Recent => SortingKind::Name, - SortingKind::Name => SortingKind::Size, - SortingKind::Size => SortingKind::Recent, - } - } - - pub fn prev(&self) -> Self { - match self { - SortingKind::Recent => SortingKind::Size, - SortingKind::Name => SortingKind::Recent, - SortingKind::Size => SortingKind::Name, - } - } -} - #[derive(Debug)] pub struct CollectionList { colors: HacColors, - sorting_kind: SortingKind, - collections: Vec, + sorting_kind: CollectionMetaSorting, selected: usize, config: HacConfig, scroll: usize, @@ -132,15 +104,14 @@ pub struct CollectionList { impl CollectionList { const LIST_ITEM_HEIGHT: u16 = 3; - pub fn new(collections: Vec, size: Rect, config: HacConfig, colors: HacColors) -> Self { + pub fn new(size: Rect, config: HacConfig, colors: HacColors) -> Self { let mut list = Self { colors, config, selected: 0, scroll: 0, - sorting_kind: SortingKind::default(), + sorting_kind: CollectionMetaSorting::default(), messager: channel().0, - collections, layout: build_layout(size, false), command_sender: None, extended_hint: false, @@ -150,15 +121,11 @@ impl CollectionList { } fn sort_list(&mut self) { - match self.sorting_kind { - SortingKind::Name => self.collections.sort_by(|a, b| a.name().cmp(b.name())), - SortingKind::Recent => self.collections.sort_by(|a, b| b.modified().cmp(a.modified())), - SortingKind::Size => self.collections.sort_by_key(|a| std::cmp::Reverse(a.size())), - } + hac_store::collection_meta::sort_collection_meta(self.sorting_kind); } fn draw_title(&self, frame: &mut Frame) -> anyhow::Result<()> { - let selected_style = |kind: SortingKind| { + let selected_style = |kind: CollectionMetaSorting| { if kind == self.sorting_kind { self.colors.bright.blue } else { @@ -168,11 +135,11 @@ impl CollectionList { let title = " HAC ".bg(self.colors.normal.red).fg(self.colors.normal.white); let sorting = vec![ - "Most recent".fg(selected_style(SortingKind::Recent)), + "Most recent".fg(selected_style(CollectionMetaSorting::Recent)), " ❘ ".fg(self.colors.bright.black), - "By name".fg(selected_style(SortingKind::Name)), + "By name".fg(selected_style(CollectionMetaSorting::Name)), " ❘ ".fg(self.colors.bright.black), - "By size".fg(selected_style(SortingKind::Size)), + "By size".fg(selected_style(CollectionMetaSorting::Size)), ]; let lines = vec![Line::from(title), "".into(), Line::from(sorting)]; @@ -189,34 +156,37 @@ impl CollectionList { let layout = self.layout.collections_pane; let max_items = self.max_items_onscreen(); - for (ref mut idx, (i, item)) in self - .collections - .iter() - .enumerate() - .skip(self.scroll) - .take(max_items) - .enumerate() - { - let selected = i == self.selected; - let modified = item.modified(); - let size = item.size().readable_byte_size(); - let description = format!("{modified} - {size}"); - - let item = if selected { - ListItem::new(item.name(), Some(&description), self.colors.clone()) - .title_style(Style::new().fg(self.colors.normal.red)) - .desc_style(Style::new().fg(self.colors.bright.black)) - .select() - } else { - ListItem::new(item.name(), Some(&description), self.colors.clone()) - .title_style(Style::new().fg(self.colors.normal.white)) - .desc_style(Style::new().fg(self.colors.bright.black)) - }; - let size = Rect::new(layout.x, layout.y + (*idx as u16 * 3), layout.width, 2); - frame.render_widget(item, size); - - *idx += 1; - } + let drawer = |collections_meta: &[CollectionMeta]| { + for (ref mut idx, (i, item)) in collections_meta + .iter() + .enumerate() + .skip(self.scroll) + .take(max_items) + .enumerate() + { + let selected = i == self.selected; + let modified = item.modified(); + let size = item.size().readable_byte_size(); + let description = format!("{modified} - {size}"); + + let item = if selected { + ListItem::new(item.name(), Some(&description), self.colors.clone()) + .title_style(Style::new().fg(self.colors.normal.red)) + .desc_style(Style::new().fg(self.colors.bright.black)) + .select() + } else { + ListItem::new(item.name(), Some(&description), self.colors.clone()) + .title_style(Style::new().fg(self.colors.normal.white)) + .desc_style(Style::new().fg(self.colors.bright.black)) + }; + let size = Rect::new(layout.x, layout.y + (*idx as u16 * 3), layout.width, 2); + frame.render_widget(item, size); + + *idx += 1; + } + }; + + hac_store::collection_meta::collections_meta(drawer); Ok(()) } @@ -288,22 +258,20 @@ impl CollectionList { #[derive(Debug)] pub enum CollectionListData { - CreateCollection(Vec), - EditCollection(usize, Vec), - DeleteCollection(usize, Vec), + CreateCollection, + EditCollection(usize), + DeleteCollection(usize), } impl Renderable for CollectionList { - type Input = (String, Vec); + type Input = String; type Output = CollectionListData; fn data(&self, requester: u8) -> Self::Output { match Routes::try_from(requester) { - Ok(Routes::CreateCollection) => CollectionListData::CreateCollection(self.collections.clone()), - Ok(Routes::EditCollection) => CollectionListData::EditCollection(self.selected, self.collections.clone()), - Ok(Routes::DeleteCollection) => { - CollectionListData::DeleteCollection(self.selected, self.collections.clone()) - } + Ok(Routes::CreateCollection) => CollectionListData::CreateCollection, + Ok(Routes::EditCollection) => CollectionListData::EditCollection(self.selected), + Ok(Routes::DeleteCollection) => CollectionListData::DeleteCollection(self.selected), Ok(Routes::ListCollections) => unreachable!(), Err(_) => unreachable!(), } @@ -320,7 +288,7 @@ impl Renderable for CollectionList { fn tick(&mut self) -> anyhow::Result<()> { if hac_loader::collection_loader::has_changes() { - self.collections = hac_loader::collection_loader::collections_metadata()?; + hac_loader::collection_loader::get_collections_metadata()?; self.sort_list(); } Ok(()) @@ -339,14 +307,13 @@ impl Renderable for CollectionList { } fn update(&mut self, data: Self::Input) { - tracing::debug!("{data:?}"); - self.collections = data.1; self.sort_list(); - self.selected = self - .collections - .iter() - .position(|col| col.path().to_string_lossy().contains(&data.0)) - .expect("collection to select doesn't exist"); + hac_store::collection_meta::collections_meta(|collections_meta| { + self.selected = collections_meta + .iter() + .position(|col| col.path().to_string_lossy().contains(&data)) + .expect("collection to select doesn't exist"); + }) } } @@ -363,32 +330,34 @@ impl Eventful for CollectionList { self.extended_hint = !self.extended_hint; self.layout = build_layout(self.layout.total_size, self.extended_hint); } - KeyCode::Char('j') | KeyCode::Down => { - self.selected = usize::min(self.selected + 1, self.collections.len() - 1); + KeyCode::Char('j') | KeyCode::Down => hac_store::collection_meta::collections_meta(|collections_meta| { + self.selected = usize::min(self.selected + 1, collections_meta.len() - 1); self.maybe_scroll_list(); - } + }), KeyCode::Char('k') | KeyCode::Up => { self.selected = self.selected.saturating_sub(1); self.maybe_scroll_list(); } - KeyCode::PageDown => { + KeyCode::PageDown => hac_store::collection_meta::collections_meta(|collections_meta| { let half = self.layout.collections_pane.height / 2; - self.selected = usize::min(self.selected + half as usize, self.collections.len() - 1); + self.selected = usize::min(self.selected + half as usize, collections_meta.len() - 1); self.maybe_scroll_list(); - } + }), KeyCode::PageUp => { let half = self.layout.collections_pane.height / 2; self.selected = self.selected.saturating_sub(half.into()); self.maybe_scroll_list(); } KeyCode::Char('r') => { - self.collections = hac_loader::collection_loader::collections_metadata()?; + hac_loader::collection_loader::get_collections_metadata()?; self.sort_list(); } KeyCode::Char('d') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { - let half = self.layout.collections_pane.height / 2; - self.selected = usize::min(self.selected + half as usize, self.collections.len() - 1); - self.maybe_scroll_list(); + hac_store::collection_meta::collections_meta(|collections_meta| { + let half = self.layout.collections_pane.height / 2; + self.selected = usize::min(self.selected + half as usize, collections_meta.len() - 1); + self.maybe_scroll_list(); + }); } KeyCode::Char('d') => { let delete_form = @@ -403,10 +372,10 @@ impl Eventful for CollectionList { self.selected = 0; self.maybe_scroll_list(); } - KeyCode::Char('G') => { - self.selected = self.collections.len() - 1; + KeyCode::Char('G') => hac_store::collection_meta::collections_meta(|collections_meta| { + self.selected = collections_meta.len() - 1; self.maybe_scroll_list(); - } + }), KeyCode::Char('g') => { let half = self.layout.collections_pane.height / 2; self.selected = self.selected.saturating_sub(half.into()); @@ -437,15 +406,15 @@ impl Eventful for CollectionList { .send(RouterMessage::Navigate(Navigate::To(Routes::EditCollection.into()))) .expect("failed to send navigation message"); } - KeyCode::Enter => { - if self.collections.is_empty() { - return Ok(None); + KeyCode::Enter => hac_store::collection_meta::collections_meta(|collections_meta| { + if collections_meta.is_empty() { + return; } - assert!(self.collections.len() > self.selected); + assert!(collections_meta.len() > self.selected); self.messager - .send(RouterMessage::Navigate(Navigate::Leave())) + .send(RouterMessage::Navigate(Navigate::Leave)) .expect("failed to send navigation message"); - } + }), _ => {} } diff --git a/hac-client/src/pages/collection_viewer/collection_store.rs b/hac-client/src/pages/collection_viewer/collection_store.rs index 551fb29..cd8758e 100755 --- a/hac-client/src/pages/collection_viewer/collection_store.rs +++ b/hac-client/src/pages/collection_viewer/collection_store.rs @@ -1,14 +1,10 @@ -use hac_core::collection::types::{Request, RequestKind}; -use hac_core::collection::Collection; - -use crate::pages::collection_viewer::collection_viewer::CollectionViewerOverlay; -use crate::pages::collection_viewer::collection_viewer::PaneFocus; - use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use std::sync::{Arc, RwLock}; +use crate::pages::collection_viewer::collection_viewer::{CollectionViewerOverlay, PaneFocus}; + #[derive(Debug)] pub struct CollectionState { collection: Rc>, @@ -73,9 +69,7 @@ impl CollectionStore { pub fn dispatch(&mut self, action: CollectionStoreAction) { if let Some(ref state) = self.state { match action { - CollectionStoreAction::SetSelectedRequest(maybe_req) => { - state.borrow_mut().selected_request = maybe_req - } + CollectionStoreAction::SetSelectedRequest(maybe_req) => state.borrow_mut().selected_request = maybe_req, CollectionStoreAction::SetHoveredRequest(maybe_req_id) => { state.borrow_mut().hovered_request = maybe_req_id } @@ -98,12 +92,8 @@ impl CollectionStore { let entry = dirs.entry(dir_id).or_insert(false); *entry = !*entry; } - CollectionStoreAction::SetFocusedPane(pane) => { - state.borrow_mut().focused_pane = pane - } - CollectionStoreAction::SetSelectedPane(pane) => { - state.borrow_mut().selected_pane = pane - } + CollectionStoreAction::SetFocusedPane(pane) => state.borrow_mut().focused_pane = pane, + CollectionStoreAction::SetSelectedPane(pane) => state.borrow_mut().selected_pane = pane, CollectionStoreAction::SetPendingRequest(is_pending) => { state.borrow_mut().has_pending_request = is_pending; } @@ -138,15 +128,11 @@ impl CollectionStore { } pub fn get_collection(&self) -> Option>> { - self.state - .as_ref() - .map(|state| state.borrow().collection.clone()) + self.state.as_ref().map(|state| state.borrow().collection.clone()) } pub fn get_dirs_expanded(&mut self) -> Option>>> { - self.state - .as_mut() - .map(|state| state.borrow().dirs_expanded.clone()) + self.state.as_mut().map(|state| state.borrow().dirs_expanded.clone()) } pub fn push_overlay(&mut self, overlay: CollectionViewerOverlay) { @@ -182,15 +168,9 @@ impl CollectionStore { } pub fn get_requests(&self) -> Option>>> { - self.state.as_ref().and_then(|state| { - state - .borrow() - .collection - .borrow() - .requests - .as_ref() - .cloned() - }) + self.state + .as_ref() + .and_then(|state| state.borrow().collection.borrow().requests.as_ref().cloned()) } pub fn has_pending_request(&self) -> bool { @@ -218,9 +198,7 @@ impl CollectionStore { &self.get_dirs_expanded().unwrap().borrow(), &id, ) { - self.dispatch(CollectionStoreAction::SetHoveredRequest(Some( - next.get_id(), - ))); + self.dispatch(CollectionStoreAction::SetHoveredRequest(Some(next.get_id()))); }; } } @@ -242,9 +220,7 @@ impl CollectionStore { &self.get_dirs_expanded().unwrap().borrow(), &id, ) { - self.dispatch(CollectionStoreAction::SetHoveredRequest(Some( - next.get_id(), - ))); + self.dispatch(CollectionStoreAction::SetHoveredRequest(Some(next.get_id()))); }; }; } @@ -343,29 +319,17 @@ fn traverse( false } -fn get_request_by_id( - tree: &[RequestKind], - dirs_expanded: &HashMap, - id: &str, -) -> RequestKind { +fn get_request_by_id(tree: &[RequestKind], dirs_expanded: &HashMap, id: &str) -> RequestKind { let mut found = false; let mut path = vec![]; for node in tree { - if traverse( - &mut found, - &VisitNode::Curr, - dirs_expanded, - node, - id, - &mut path, - ) { + if traverse(&mut found, &VisitNode::Curr, dirs_expanded, node, id, &mut path) { break; } } - path.pop() - .expect("attempting to find an unexisting request") + path.pop().expect("attempting to find an unexisting request") } fn find_next_entry( @@ -388,10 +352,12 @@ fn find_next_entry( #[cfg(test)] mod tests { - use super::*; - use hac_core::collection::types::{Directory, Request, RequestMethod}; use std::collections::HashMap; + use hac_core::collection::types::{Directory, Request, RequestMethod}; + + use super::*; + fn create_root_one() -> RequestKind { RequestKind::Single(Arc::new(RwLock::new(Request { id: "root".to_string(), diff --git a/hac-client/src/pages/collection_viewer/collection_viewer.rs b/hac-client/src/pages/collection_viewer/collection_viewer.rs index 313407e..46436f6 100755 --- a/hac-client/src/pages/collection_viewer/collection_viewer.rs +++ b/hac-client/src/pages/collection_viewer/collection_viewer.rs @@ -1,14 +1,3 @@ -use hac_core::collection::types::*; -use hac_core::command::Command; -use hac_core::net::request_manager::Response; - -use crate::pages::collection_viewer::collection_store::{CollectionStore, CollectionStoreAction}; -use crate::pages::collection_viewer::request_editor::{RequestEditor, RequestEditorEvent}; -use crate::pages::collection_viewer::request_uri::{RequestUri, RequestUriEvent}; -use crate::pages::collection_viewer::response_viewer::{ResponseViewer, ResponseViewerEvent}; -use crate::pages::collection_viewer::sidebar::{self, Sidebar, SidebarEvent}; -use crate::pages::{Eventful, Renderable}; - use std::cell::RefCell; use std::collections::HashMap; use std::ops::{Add, Div}; @@ -16,12 +5,21 @@ use std::rc::Rc; use std::sync::mpsc::Sender; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use hac_core::command::Command; +use hac_core::net::request_manager::Response; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::Stylize; use ratatui::widgets::{Block, Clear}; use ratatui::Frame; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; +use crate::pages::collection_viewer::collection_store::{CollectionStore, CollectionStoreAction}; +use crate::pages::collection_viewer::request_editor::{RequestEditor, RequestEditorEvent}; +use crate::pages::collection_viewer::request_uri::{RequestUri, RequestUriEvent}; +use crate::pages::collection_viewer::response_viewer::{ResponseViewer, ResponseViewerEvent}; +use crate::pages::collection_viewer::sidebar::{self, Sidebar, SidebarEvent}; +use crate::pages::{Eventful, Renderable}; + #[derive(Debug, PartialEq)] pub struct ExplorerLayout { pub hint_pane: Rect, diff --git a/hac-client/src/pages/mod.rs b/hac-client/src/pages/mod.rs new file mode 100755 index 0000000..67e3ef2 --- /dev/null +++ b/hac-client/src/pages/mod.rs @@ -0,0 +1,7 @@ +pub mod collection_list; +//pub mod collection_viewer; +pub mod input; +pub mod overlay; +mod spinner; +pub mod terminal_too_small; +mod under_construction; diff --git a/hac-client/src/pages/terminal_too_small.rs b/hac-client/src/pages/terminal_too_small.rs index 0a2080f..0b7eeab 100755 --- a/hac-client/src/pages/terminal_too_small.rs +++ b/hac-client/src/pages/terminal_too_small.rs @@ -4,7 +4,7 @@ use ratatui::text::Line; use ratatui::widgets::{Paragraph, Wrap}; use ratatui::Frame; -use crate::pages::Renderable; +use crate::renderable::Renderable; use crate::{HacColors, MIN_HEIGHT, MIN_WIDTH}; /// `TerminalTooSmall` as the name suggests is a screen rendered by the diff --git a/hac-client/src/pages/under_construction.rs b/hac-client/src/pages/under_construction.rs index 7d26bd3..90286cf 100755 --- a/hac-client/src/pages/under_construction.rs +++ b/hac-client/src/pages/under_construction.rs @@ -7,7 +7,7 @@ use ratatui::widgets::Paragraph; use ratatui::Frame; use crate::ascii::UNDER_CONSTRUCTION; -use crate::pages::Renderable; +use crate::renderable::Renderable; pub struct UnderConstruction<'uc> { colors: &'uc hac_colors::colors::Colors, diff --git a/hac-client/src/pages.rs b/hac-client/src/renderable.rs old mode 100755 new mode 100644 similarity index 94% rename from hac-client/src/pages.rs rename to hac-client/src/renderable.rs index 51ce5ee..2e7e8e8 --- a/hac-client/src/pages.rs +++ b/hac-client/src/renderable.rs @@ -1,11 +1,3 @@ -pub mod collection_list; -//pub mod collection_viewer; -pub mod input; -pub mod overlay; -mod spinner; -pub mod terminal_too_small; -mod under_construction; - use std::sync::mpsc::Sender; use crossterm::event::KeyEvent; diff --git a/hac-client/src/router.rs b/hac-client/src/router.rs index 409edf1..31cba5f 100644 --- a/hac-client/src/router.rs +++ b/hac-client/src/router.rs @@ -10,7 +10,7 @@ use ratatui::Frame; use crate::event_pool::Event; use crate::pages::terminal_too_small::TerminalTooSmall; -use crate::pages::{Eventful, Renderable}; +use crate::renderable::{Eventful, Renderable}; use crate::{HacColors, MIN_HEIGHT, MIN_WIDTH}; type Key = u8; @@ -28,7 +28,7 @@ pub enum Navigate { /// if a route in a nested router needs to navigate to an entirely different router /// the `Leave` variant is responsible for that. It will direct the navigation event /// to the parent router of the current active router. - Leave(), + Leave, /// Go back in the navigation history, this completely wipes the current router history, /// and close every dialog thats visible Back, @@ -261,7 +261,7 @@ impl Router { (false, true) => panic!("invalid navigation from route to dialog"), } } - Navigate::Leave() => todo!(), + Navigate::Leave => todo!(), } } } diff --git a/hac-core/src/fs.rs b/hac-core/src/fs.rs deleted file mode 100755 index cfcbd8b..0000000 --- a/hac-core/src/fs.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod error; -#[allow(clippy::module_inception)] -mod fs; - -pub use fs::*; diff --git a/hac-core/src/fs/error.rs b/hac-core/src/fs/error.rs deleted file mode 100755 index 9c03588..0000000 --- a/hac-core/src/fs/error.rs +++ /dev/null @@ -1,20 +0,0 @@ -#[derive(Debug)] -pub enum FsError { - SerializationError(String), - IOError(String), - CollectionAlreadyExists(String), - Unknown, -} - -impl std::fmt::Display for FsError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - FsError::CollectionAlreadyExists(path) => { - write!(f, "collection {:?} already exists", path) - } - FsError::Unknown => write!(f, "unknown error"), - FsError::SerializationError(msg) => write!(f, "{}", msg), - FsError::IOError(msg) => write!(f, "{}", msg), - } - } -} diff --git a/hac-core/src/fs/fs.rs b/hac-core/src/fs/fs.rs deleted file mode 100755 index f6b963c..0000000 --- a/hac-core/src/fs/fs.rs +++ /dev/null @@ -1,64 +0,0 @@ -//use crate::collection::{create_from_form, Collection}; -use crate::fs::error::FsError; - -use std::path::Path; - -#[tracing::instrument(err, skip_all)] -pub async fn delete_collection

(path: P) -> anyhow::Result<(), FsError> -where - P: AsRef, -{ - let path = path.as_ref(); - tokio::fs::remove_file(path) - .await - .map_err(|_| FsError::IOError(format!("failed to delete collection: {:?}", path)))?; - - tracing::debug!("sucessfully deleted collection: {:?}", path); - Ok(()) -} - -//#[tracing::instrument(err)] -//pub async fn create_collection( -// name: String, -// description: String, -// dry_run: bool, -//) -> anyhow::Result { -// let collection = create_from_form(name, description); -// -// if collection.path.exists() { -// return Err(FsError::CollectionAlreadyExists( -// collection.path.to_string_lossy().to_string(), -// )); -// } -// -// let serialized_collection = serde_json::to_string(&collection) -// .map_err(|e| FsError::SerializationError(e.to_string()))?; -// -// // if we are on a dry_run, we skip syncing -// if !dry_run { -// tokio::fs::write(&collection.path, serialized_collection) -// .await -// .map_err(|e| FsError::IOError(format!("failed to write collection: {:?}", e)))?; -// } -// -// tracing::debug!("successfully created new collection: {:?}", collection.path); -// Ok(collection) -//} - -//pub async fn sync_collection(collection: Collection) -> anyhow::Result<(), FsError> { -// let collection_str = serde_json::to_string(&collection) -// .map_err(|e| FsError::SerializationError(e.to_string()))?; -// -// tokio::fs::write(&collection.path, collection_str) -// .await -// .map_err(|_| { -// FsError::IOError(format!( -// "failed to synchronize collection {:?}", -// collection.path -// )) -// })?; -// -// tracing::debug!("synchronization of collection: {:?}", collection.path); -// -// Ok(()) -//} diff --git a/hac-core/src/lib.rs b/hac-core/src/lib.rs index db78815..2f6406f 100755 --- a/hac-core/src/lib.rs +++ b/hac-core/src/lib.rs @@ -1,5 +1,4 @@ pub mod command; -pub mod fs; pub mod net; pub mod syntax; pub mod text_object; diff --git a/hac-core/src/net.rs b/hac-core/src/net/mod.rs old mode 100755 new mode 100644 similarity index 100% rename from hac-core/src/net.rs rename to hac-core/src/net/mod.rs diff --git a/hac-core/src/net/response_decoders/json_decoder.rs b/hac-core/src/net/response_decoders/json_decoder.rs index 545eaf8..cb7679c 100755 --- a/hac-core/src/net/response_decoders/json_decoder.rs +++ b/hac-core/src/net/response_decoders/json_decoder.rs @@ -1,7 +1,9 @@ -use crate::net::{request_manager::Response, response_decoders::ResponseDecoder}; -use crate::text_object::TextObject; +use std::ops::Add; +use std::time::Instant; -use std::{ops::Add, time::Instant}; +use crate::net::request_manager::Response; +use crate::net::response_decoders::ResponseDecoder; +use crate::text_object::TextObject; pub struct JsonDecoder; @@ -22,7 +24,7 @@ impl ResponseDecoder for JsonDecoder { if response.content_length().is_some_and(|len| len.gt(&0)) { if let Ok(body_str) = response.text().await { let pretty_body_str = jsonxf::pretty_print(&body_str).unwrap_or_default(); - pretty_body = Some(TextObject::from(&pretty_body_str)); + pretty_body = Some(TextObject::from(pretty_body_str.as_str())); body = Some(body_str); }; } diff --git a/hac-core/src/syntax.rs b/hac-core/src/syntax/mod.rs old mode 100755 new mode 100644 similarity index 100% rename from hac-core/src/syntax.rs rename to hac-core/src/syntax/mod.rs diff --git a/hac-core/src/text_object.rs b/hac-core/src/text_object.rs deleted file mode 100755 index c96231c..0000000 --- a/hac-core/src/text_object.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod cursor; -#[allow(clippy::module_inception)] -mod text_object; - -pub use text_object::{Readonly, TextObject, Write}; diff --git a/hac-core/src/text_object/text_object.rs b/hac-core/src/text_object/mod.rs old mode 100755 new mode 100644 similarity index 98% rename from hac-core/src/text_object/text_object.rs rename to hac-core/src/text_object/mod.rs index f3a8cbb..479f6a2 --- a/hac-core/src/text_object/text_object.rs +++ b/hac-core/src/text_object/mod.rs @@ -1,3 +1,5 @@ +pub mod cursor; + use std::collections::HashMap; use std::ops::{Add, Sub}; @@ -55,8 +57,14 @@ impl Default for TextObject { } } +impl> From for TextObject { + fn from(value: T) -> Self { + Self::new(value.as_ref()) + } +} + impl TextObject { - pub fn from(content: &str) -> TextObject { + pub fn new(content: &str) -> TextObject { let content = Rope::from_str(content); let line_break = match content.line(0).to_string().contains("\r\n") { true => LineBreak::Crlf, diff --git a/hac-loader/Cargo.toml b/hac-loader/Cargo.toml index 17346d7..680af6a 100644 --- a/hac-loader/Cargo.toml +++ b/hac-loader/Cargo.toml @@ -12,5 +12,4 @@ serde_json.workspace = true anyhow.workspace = true tracing.workspace = true dirs = "5.0.1" -chrono = "0.4.38" notify = "6.1.1" diff --git a/hac-loader/src/collection_loader/error.rs b/hac-loader/src/collection_loader/error.rs new file mode 100644 index 0000000..644f40e --- /dev/null +++ b/hac-loader/src/collection_loader/error.rs @@ -0,0 +1,24 @@ +pub type Result = std::result::Result; + +#[derive(Debug)] +pub enum CollectionLoaderError { + Create(String), + Rename(String), + Remove(String), + ReadDir(String), + Read(String), +} + +impl std::error::Error for CollectionLoaderError {} + +impl std::fmt::Display for CollectionLoaderError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Create(message) => write!(f, "{message}"), + Self::Rename(message) => write!(f, "{message}"), + Self::Remove(message) => write!(f, "{message}"), + Self::ReadDir(message) => write!(f, "{message}"), + Self::Read(message) => write!(f, "{message}"), + } + } +} diff --git a/hac-loader/src/collection_loader/json_loader.rs b/hac-loader/src/collection_loader/json_loader.rs index 2acf336..e7eee09 100644 --- a/hac-loader/src/collection_loader/json_loader.rs +++ b/hac-loader/src/collection_loader/json_loader.rs @@ -1,11 +1,10 @@ +use hac_store::collection::{BodyKind, Collection, CollectionInfo, Folder, HeaderEntry, KeyKind, ReqMethod, Request}; +use hac_store::slab::{Key, Slab}; + use super::json_collection::{ JsonBodyKind, JsonCollection, JsonCollectionInfo, JsonFolder, JsonHeaderEntry, JsonReqMethod, JsonRequest, ReqKind, }; -use super::IntoCollection; -use hac_store::{ - collection::{BodyKind, Collection, CollectionInfo, Folder, HeaderEntry, KeyKind, ReqMethod, Request}, - slab::{Key, Slab}, -}; +use super::{IntoCollection, Result}; pub struct JsonLoader(JsonCollection); @@ -16,7 +15,7 @@ impl IntoCollection for JsonLoader { } impl JsonLoader { - pub fn parse(content: &str) -> anyhow::Result { + pub fn parse(content: &str) -> Result { match serde_json::from_str(content) { Ok(collection) => Ok(Self(collection)), Err(e) => todo!("{e:?}"), diff --git a/hac-loader/src/collection_loader/mod.rs b/hac-loader/src/collection_loader/mod.rs index 635b0be..c24707f 100644 --- a/hac-loader/src/collection_loader/mod.rs +++ b/hac-loader/src/collection_loader/mod.rs @@ -1,14 +1,16 @@ +mod error; mod json_collection; mod json_loader; use std::cell::RefCell; +use std::fs; use std::rc::Rc; use std::sync::atomic::{AtomicBool, Ordering}; -use std::time::SystemTime; -use chrono::{Datelike, Timelike}; +use error::{CollectionLoaderError, Result}; use hac_config::config::CollectionExtensions; use hac_store::collection::Collection; +use hac_store::collection_meta::{CollectionMeta, CollectionModifiedMeta}; use json_loader::JsonLoader; use notify::{RecursiveMode, Watcher}; @@ -19,10 +21,10 @@ pub trait IntoCollection { fn into_collection(self) -> hac_store::collection::Collection; } -pub fn read_collection_file(file_path: F, parser: P) -> anyhow::Result +pub fn read_collection_file(file_path: F, parser: P) -> Result where F: AsRef, - P: FnOnce(&str) -> anyhow::Result, + P: FnOnce(&str) -> Result, T: IntoCollection, { match std::fs::read_to_string(file_path.as_ref()) { @@ -31,10 +33,7 @@ where } } -pub fn load_collection>( - file_path: F, - config: &hac_config::Config, -) -> anyhow::Result { +pub fn load_collection>(file_path: F, config: &hac_config::Config) -> Result { match config.collection_ext { CollectionExtensions::Json => Ok(read_collection_file(file_path, JsonLoader::parse)?), } @@ -48,12 +47,15 @@ fn sanitize_filename(name: &str) -> String { .collect() } -fn create_persistent_colletion(name: String) -> anyhow::Result<()> { +fn create_persistent_colletion(name: String) -> Result<()> { + // TODO: make this dynamic let file_name = format!("{}.json", sanitize_filename(&name)); + tracing::debug!("creating new collection {file_name} on disk"); let path = super::collections_dir().join(&file_name); - tracing::debug!("{path:?}"); let collection = json_collection::JsonCollection::new(name, Default::default(), file_name, &path); - std::fs::write(&path, serde_json::to_string_pretty(&collection)?)?; + let strigified = serde_json::to_string_pretty(&collection).expect("invalid collection format to be stringified"); + fs::write(&path, &strigified) + .map_err(|_| CollectionLoaderError::Create("failed to write collection to disk".into()))?; Ok(()) } @@ -61,259 +63,102 @@ pub fn has_changes() -> bool { HAS_CHANGES.load(Ordering::Relaxed) } -fn set_watcher() -> anyhow::Result<()> { +fn set_watcher() { if HAS_WATCHER.load(Ordering::Relaxed) { - return Ok(()); - } - - std::thread::spawn(move || { - let mut watcher = notify::recommended_watcher(|res| match res { - Ok(_) => HAS_CHANGES.store(true, Ordering::Relaxed), - Err(_) => todo!(), - }) - .expect("failed to set watcher"); - let collections_dir = super::collections_dir(); - watcher - .watch(&collections_dir, RecursiveMode::NonRecursive) - .expect("failed to watch collections dir"); - - loop { - std::thread::sleep(std::time::Duration::from_secs(1)); - } - }); - - Ok(()) + return; + } + + if let Ok(mut watcher) = notify::recommended_watcher(|res| match res { + Ok(_) => HAS_CHANGES.store(true, Ordering::Relaxed), + Err(_) => tracing::error!("failed to get changes from watcher"), + }) { + std::thread::spawn(move || { + let collections_dir = super::collections_dir(); + if let Err(e) = watcher.watch(&collections_dir, RecursiveMode::NonRecursive) { + tracing::error!("failed to watch collections dir: {}", e); + return; + } + loop { + std::thread::sleep(std::time::Duration::from_secs(1)); + } + }); + }; } -fn create_virtual_collection( - name: String, - mut collections: Vec, -) -> anyhow::Result> { +fn create_virtual_collection(name: String) { let file_name = format!("{}.json", sanitize_filename(&name)); let size = name.len(); - let metadata = CollectionMeta::new(name, file_name.into(), size as u64); - collections.push(metadata); - Ok(collections) + let collection_meta = CollectionMeta::new(name, file_name.into(), size as u64, CollectionModifiedMeta::new()); + hac_store::collection_meta::push_collection_meta(collection_meta) } -pub fn create_collection( - name: String, - collections: Vec, - config: &Rc>, -) -> anyhow::Result> { +pub fn create_collection(name: String, config: &Rc>) -> Result<()> { match config.borrow().dry_run { - false => { - create_persistent_colletion(name)?; - collections_metadata() - } - true => create_virtual_collection(name, collections), + false => create_persistent_colletion(name)?, + true => create_virtual_collection(name), } + Ok(()) } -fn edit_persistent_collection(entry: &mut CollectionMeta) -> anyhow::Result<()> { +fn edit_persistent_collection(entry: &mut CollectionMeta) -> Result<()> { let original_path = entry.path().clone(); - let mut new_path = entry.path().clone(); - new_path.pop(); - let new_path = new_path.join(entry.name()); - std::fs::rename(original_path, new_path)?; + entry.path_mut().pop(); + let name = entry.name().to_string(); + let new_path = entry.path_mut().join(name); + std::fs::rename(original_path, new_path) + .map_err(|_| CollectionLoaderError::Rename("failed to rename collection".into()))?; Ok(()) } -pub fn edit_collection( - name: String, - mut collections: Vec, - item_idx: usize, - config: &Rc>, -) -> anyhow::Result> { - let entry = &mut collections[item_idx]; - match config.borrow().dry_run { - false => { - edit_persistent_collection(entry)?; - collections_metadata() - } - true => { - entry.path.pop(); - entry.path = entry.path.join(&name); - entry.name = name; - Ok(collections) +pub fn edit_collection(name: String, item_idx: usize, config: &Rc>) -> Result<()> { + hac_store::collection_meta::get_collection_meta_mut(item_idx, |entry| -> Result<()> { + match config.borrow().dry_run { + false => edit_persistent_collection(entry), + true => { + entry.path_mut().pop(); + *entry.path_mut() = entry.path().join(&name); + *entry.name_mut() = name; + Ok(()) + } } - } + }) } -fn delete_persistent_collection(file_name: String, collections: Vec) -> anyhow::Result<()> { - let path = collections - .iter() - .find(|c| c.path().to_string_lossy().contains(&file_name)) - .expect("attempted to delete non-existing collection") - .path(); - std::fs::remove_file(path)?; +fn delete_persistent_collection(file_name: String) -> Result<()> { + let collection_meta = hac_store::collection_meta::remove_collection_meta(file_name, |meta| meta); + std::fs::remove_file(collection_meta.path()) + .map_err(|_| CollectionLoaderError::Remove("failed to remove collection".into()))?; Ok(()) } -pub fn delete_collection( - file_name: String, - mut collections: Vec, - config: &Rc>, -) -> anyhow::Result> { +pub fn delete_collection(file_name: String, config: &Rc>) -> Result<()> { match config.borrow().dry_run { - false => { - delete_persistent_collection(file_name, collections)?; - collections_metadata() - } - true => { - collections.retain(|c| !c.path().to_string_lossy().contains(&file_name)); - Ok(collections) - } - } -} - -// TODO: i probably want to introduce some metadata storage, or caching to know -// total requests, total saved responses, and other infos without having to read -// the entire collection. -#[derive(Debug, Clone)] -pub struct CollectionMeta { - name: String, - path: std::path::PathBuf, - size: u64, - modified: CollectionModifiedMeta, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CollectionModifiedMeta { - year: i32, - month: u32, - day: u32, - hour: u32, - minutes: u32, - system_time: SystemTime, -} - -impl CollectionModifiedMeta { - pub fn new() -> Self { - std::time::SystemTime::now().into() - } -} - -impl Default for CollectionModifiedMeta { - fn default() -> Self { - Self::new() - } -} - -impl PartialOrd for CollectionModifiedMeta { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.system_time.cmp(&other.system_time)) - } -} - -impl Ord for CollectionModifiedMeta { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.system_time.cmp(&other.system_time) - } -} - -impl std::fmt::Display for CollectionModifiedMeta { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}-{}-{} {}:{}", - self.year, self.month, self.day, self.hour, self.minutes, - ) - } -} - -impl CollectionMeta { - pub fn new(name: String, path: std::path::PathBuf, size: u64) -> Self { - Self { - name, - path, - size, - modified: CollectionModifiedMeta::new(), - } - } - - pub fn name(&self) -> &str { - &self.name - } - - pub fn path(&self) -> &std::path::PathBuf { - &self.path - } - - pub fn modified(&self) -> &CollectionModifiedMeta { - &self.modified - } - - pub fn size(&self) -> u64 { - self.size - } -} - -pub trait ReadableByteSize { - fn readable_byte_size(&self) -> String; -} - -fn readable_byte_size(val: N) -> String -where - N: Copy + Into, -{ - let units = ["B", "KB", "MB", "GB", "TB", "PB"]; - let mut size = val.into() as f64; - let mut unit_index = 0; - - while size >= 1024.0 && unit_index < units.len() - 1 { - size /= 1024.0; - unit_index += 1; - } - - format!("{:.2}{}", size, units[unit_index]) -} - -impl ReadableByteSize for u64 { - fn readable_byte_size(&self) -> String { - readable_byte_size(*self) - } -} - -impl From for CollectionModifiedMeta { - fn from(value: SystemTime) -> Self { - let datetime = chrono::DateTime::::from(value); - let year = datetime.year(); - let month = datetime.day(); - let day = datetime.month(); - let hour = datetime.hour(); - let minutes = datetime.minute(); - - CollectionModifiedMeta { - year, - month, - day, - hour, - minutes, - system_time: value, - } + false => delete_persistent_collection(file_name)?, + true => hac_store::collection_meta::remove_collection_meta(file_name, |_| ()), } + Ok(()) } -pub fn collections_metadata() -> anyhow::Result> { - set_watcher()?; - let entries = std::fs::read_dir(super::collections_dir())?.flatten(); +pub fn get_collections_metadata() -> Result<()> { + set_watcher(); + let entries = std::fs::read_dir(super::collections_dir()) + .map_err(|_| CollectionLoaderError::ReadDir("failed to read collections directory".into()))? + .flatten(); let mut collections = vec![]; for entry in entries { let name = entry.file_name().to_string_lossy().to_string(); let path = entry.path(); - let metadata = entry.metadata()?; - let modified = metadata.modified()?.into(); + let metadata = entry + .metadata() + .map_err(|_| CollectionLoaderError::Read("failed to read collection metadata".into()))?; + let modified = metadata + .modified() + .map_err(|_| CollectionLoaderError::Read("failed to read collection metadata".into()))? + .into(); let size = metadata.len(); - collections.push(CollectionMeta { - name, - path, - modified, - size, - }); + collections.push(CollectionMeta::new(name, path, size, modified)); } - + hac_store::collection_meta::set_collections_meta(collections); HAS_CHANGES.store(false, Ordering::Relaxed); - - Ok(collections) + Ok(()) } diff --git a/hac-store/Cargo.toml b/hac-store/Cargo.toml index bfaa145..48119a3 100644 --- a/hac-store/Cargo.toml +++ b/hac-store/Cargo.toml @@ -5,3 +5,4 @@ edition = "2021" [dependencies] serde.workspace = true +chrono = "0.4.38" diff --git a/hac-store/src/collection_meta.rs b/hac-store/src/collection_meta.rs new file mode 100644 index 0000000..677bb98 --- /dev/null +++ b/hac-store/src/collection_meta.rs @@ -0,0 +1,220 @@ +use std::path::PathBuf; +use std::time::SystemTime; + +use chrono::{Datelike, Timelike}; + +use crate::HAC_STORE; + +// TODO: i probably want to introduce some metadata storage, or caching to know +// total requests, total saved responses, and other infos without having to read +// the entire collection. +#[derive(Debug, Clone)] +pub struct CollectionMeta { + name: String, + path: std::path::PathBuf, + size: u64, + modified: CollectionModifiedMeta, +} + +impl CollectionMeta { + pub fn new(name: String, path: std::path::PathBuf, size: u64, modified: CollectionModifiedMeta) -> Self { + Self { + name, + path, + size, + modified, + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn name_mut(&mut self) -> &mut String { + &mut self.name + } + + pub fn path(&self) -> &PathBuf { + &self.path + } + + pub fn path_mut(&mut self) -> &mut PathBuf { + &mut self.path + } + + pub fn modified(&self) -> &CollectionModifiedMeta { + &self.modified + } + + pub fn size(&self) -> u64 { + self.size + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CollectionModifiedMeta { + year: i32, + month: u32, + day: u32, + hour: u32, + minutes: u32, + system_time: SystemTime, +} + +impl CollectionModifiedMeta { + pub fn new() -> Self { + std::time::SystemTime::now().into() + } +} + +impl Default for CollectionModifiedMeta { + fn default() -> Self { + Self::new() + } +} + +impl PartialOrd for CollectionModifiedMeta { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.system_time.cmp(&other.system_time)) + } +} + +impl Ord for CollectionModifiedMeta { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.system_time.cmp(&other.system_time) + } +} + +impl std::fmt::Display for CollectionModifiedMeta { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}-{}-{} {}:{}", + self.year, self.month, self.day, self.hour, self.minutes, + ) + } +} + +pub trait ReadableByteSize { + fn readable_byte_size(&self) -> String; +} + +fn readable_byte_size(val: N) -> String +where + N: Copy + Into, +{ + let units = ["B", "KB", "MB", "GB", "TB", "PB"]; + let mut size = val.into() as f64; + let mut unit_index = 0; + + while size >= 1024.0 && unit_index < units.len() - 1 { + size /= 1024.0; + unit_index += 1; + } + + format!("{:.2}{}", size, units[unit_index]) +} + +impl ReadableByteSize for u64 { + fn readable_byte_size(&self) -> String { + readable_byte_size(*self) + } +} + +impl From for CollectionModifiedMeta { + fn from(value: SystemTime) -> Self { + let datetime = chrono::DateTime::::from(value); + let year = datetime.year(); + let month = datetime.day(); + let day = datetime.month(); + let hour = datetime.hour(); + let minutes = datetime.minute(); + + CollectionModifiedMeta { + year, + month, + day, + hour, + minutes, + system_time: value, + } + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum CollectionMetaSorting { + #[default] + Recent, + Name, + Size, +} + +impl CollectionMetaSorting { + pub fn next(&self) -> Self { + match self { + CollectionMetaSorting::Recent => CollectionMetaSorting::Name, + CollectionMetaSorting::Name => CollectionMetaSorting::Size, + CollectionMetaSorting::Size => CollectionMetaSorting::Recent, + } + } + + pub fn prev(&self) -> Self { + match self { + CollectionMetaSorting::Recent => CollectionMetaSorting::Size, + CollectionMetaSorting::Name => CollectionMetaSorting::Recent, + CollectionMetaSorting::Size => CollectionMetaSorting::Name, + } + } +} + +pub fn set_collections_meta(collections_meta: Vec) { + HAC_STORE.with_borrow_mut(|store| store.collections_meta = collections_meta); +} + +pub fn sort_collection_meta(sorting_kind: CollectionMetaSorting) { + HAC_STORE.with_borrow_mut(|store| match sorting_kind { + CollectionMetaSorting::Name => store.collections_meta.sort_by(|a, b| a.name().cmp(b.name())), + CollectionMetaSorting::Recent => store.collections_meta.sort_by(|a, b| b.modified().cmp(a.modified())), + CollectionMetaSorting::Size => store.collections_meta.sort_by_key(|a| std::cmp::Reverse(a.size())), + }) +} + +pub fn collections_meta(f: F) +where + F: FnOnce(&[CollectionMeta]), +{ + HAC_STORE.with_borrow(|store| f(&store.collections_meta)) +} + +pub fn get_collection_meta(idx: usize, f: F) -> R +where + F: FnOnce(&CollectionMeta) -> R, +{ + HAC_STORE.with_borrow(|store| f(&store.collections_meta[idx])) +} + +pub fn get_collection_meta_mut(idx: usize, f: F) -> R +where + F: FnOnce(&mut CollectionMeta) -> R, +{ + HAC_STORE.with_borrow_mut(|store| f(&mut store.collections_meta[idx])) +} + +pub fn push_collection_meta(collection_meta: CollectionMeta) { + HAC_STORE.with_borrow_mut(|store| store.collections_meta.push(collection_meta)) +} + +pub fn remove_collection_meta(file_name: String, f: F) -> R +where + F: FnOnce(CollectionMeta) -> R, +{ + HAC_STORE.with_borrow_mut(|store| { + let index = store + .collections_meta + .iter() + .position(|meta| !meta.path().to_string_lossy().contains(&file_name)) + .expect("no collection metadata with file name"); + let collection_meta = store.collections_meta.remove(index); + f(collection_meta) + }) +} diff --git a/hac-store/src/lib.rs b/hac-store/src/lib.rs index 8a33a95..a08e09b 100644 --- a/hac-store/src/lib.rs +++ b/hac-store/src/lib.rs @@ -1,2 +1,34 @@ +use std::cell::RefCell; + +use collection::Collection; + pub mod collection; +pub mod collection_meta; pub mod slab; + +use collection_meta::CollectionMeta; + +thread_local! { + pub(crate) static HAC_STORE: RefCell = const { RefCell::new(HacStore::new()) }; +} + +#[derive(Debug)] +pub struct HacStore { + collection: Option, + collections_meta: Vec, +} + +impl HacStore { + pub const fn new() -> Self { + Self { + collection: None, + collections_meta: vec![], + } + } +} + +impl Default for HacStore { + fn default() -> Self { + Self::new() + } +} diff --git a/hac-store/src/slab.rs b/hac-store/src/slab.rs index ba868f8..2d90d3f 100644 --- a/hac-store/src/slab.rs +++ b/hac-store/src/slab.rs @@ -3,7 +3,7 @@ pub type Key = usize; #[derive(Debug)] pub enum Entry { Full(T), - Free(Option), + Free(Option), } impl Entry { @@ -12,7 +12,7 @@ impl Entry { std::mem::swap(self, &mut Self::full(val)) } - pub fn free(idx: Option) -> Self { + pub fn free(idx: Option) -> Self { Self::Free(idx) } @@ -24,11 +24,11 @@ impl Entry { #[derive(Debug, Default)] pub struct Slab { inner: Vec>, - next_idx: Option, + next_idx: Option, } impl Slab { - pub fn new() -> Self { + pub const fn new() -> Self { Self { inner: vec![], next_idx: None, @@ -111,8 +111,8 @@ impl Slab { } impl IntoIterator for Slab { - type Item = T; type IntoIter = std::vec::IntoIter; + type Item = T; fn into_iter(self) -> Self::IntoIter { self.inner From 922461d5ccc0e3231f75cbfd5b1b4fd5e402b827 Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Fri, 27 Sep 2024 21:31:12 -0300 Subject: [PATCH 11/23] wip: collection viewer --- hac-client/src/app.rs | 52 +- hac-client/src/pages/collection_list/mod.rs | 51 +- .../collection_viewer/collection_store.rs | 525 -------------- .../collection_viewer/collection_viewer.rs | 685 +++++++++--------- hac-client/src/pages/collection_viewer/mod.rs | 9 +- hac-client/src/pages/mod.rs | 2 +- hac-client/src/router.rs | 17 +- 7 files changed, 404 insertions(+), 937 deletions(-) delete mode 100755 hac-client/src/pages/collection_viewer/collection_store.rs diff --git a/hac-client/src/app.rs b/hac-client/src/app.rs index 72b15ed..1ac791e 100755 --- a/hac-client/src/app.rs +++ b/hac-client/src/app.rs @@ -11,18 +11,50 @@ use crate::renderable::{Eventful, Renderable}; use crate::router::Router; use crate::{HacColors, HacConfig}; -#[derive(Default, Hash, Debug, PartialEq, Eq, Copy, Clone)] -pub enum AppRoutes { +#[derive(Debug)] +pub struct InvalidRouteNumber(u8); + +impl std::error::Error for InvalidRouteNumber {} + +impl std::fmt::Display for InvalidRouteNumber { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "number {} is not a valid route number", self.0) + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Default)] +pub enum Routes { #[default] - CollectionListRouter, - CollectionViewerRouter, + ListCollections, + CreateCollection, + EditCollection, + DeleteCollection, + CollectionViewer, +} + +impl TryFrom for Routes { + type Error = InvalidRouteNumber; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Routes::ListCollections), + 1 => Ok(Routes::CreateCollection), + 2 => Ok(Routes::EditCollection), + 3 => Ok(Routes::DeleteCollection), + 4 => Ok(Routes::CollectionViewer), + _ => Err(InvalidRouteNumber(value)), + } + } } -impl From for u8 { - fn from(val: AppRoutes) -> Self { +impl From for u8 { + fn from(val: Routes) -> Self { match val { - AppRoutes::CollectionListRouter => 0, - AppRoutes::CollectionViewerRouter => 1, + Routes::ListCollections => 0, + Routes::CreateCollection => 1, + Routes::EditCollection => 2, + Routes::DeleteCollection => 3, + Routes::CollectionViewer => 4, } } } @@ -42,12 +74,10 @@ impl App { let (command_sender, command_receiver) = std::sync::mpsc::channel(); let size = terminal.size()?; - let mut router = Router::new(command_sender.clone(), colors.clone()); + let router = Router::new(command_sender.clone(), colors.clone()); let mut collection_list_router = make_collection_list_router(command_sender.clone(), size, config.clone(), colors.clone()); - collection_list_router.attach_parent_navigator(router.message_sender()); - router.add_route(AppRoutes::CollectionListRouter.into(), Box::new(collection_list_router)); Ok(Self { event_pool: EventPool::new(60f64, 30f64), diff --git a/hac-client/src/pages/collection_list/mod.rs b/hac-client/src/pages/collection_list/mod.rs index cbecc6b..d6a9cfd 100644 --- a/hac-client/src/pages/collection_list/mod.rs +++ b/hac-client/src/pages/collection_list/mod.rs @@ -18,56 +18,12 @@ use ratatui::text::Line; use ratatui::widgets::{Block, Clear, Paragraph}; use ratatui::Frame; +use crate::app::Routes; use crate::components::list_itemm::ListItem; use crate::renderable::{Eventful, Renderable}; use crate::router::{Navigate, Router, RouterMessage}; use crate::{HacColors, HacConfig}; -#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Default)] -pub enum Routes { - #[default] - ListCollections, - CreateCollection, - EditCollection, - DeleteCollection, -} - -#[derive(Debug)] -pub struct InvalidRouteNumber(u8); - -impl std::error::Error for InvalidRouteNumber {} - -impl std::fmt::Display for InvalidRouteNumber { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "number {} is not a valid route number", self.0) - } -} - -impl TryFrom for Routes { - type Error = InvalidRouteNumber; - - fn try_from(value: u8) -> Result { - match value { - 0 => Ok(Routes::ListCollections), - 1 => Ok(Routes::CreateCollection), - 2 => Ok(Routes::EditCollection), - 3 => Ok(Routes::DeleteCollection), - _ => Err(InvalidRouteNumber(value)), - } - } -} - -impl From for u8 { - fn from(val: Routes) -> Self { - match val { - Routes::ListCollections => 0, - Routes::CreateCollection => 1, - Routes::EditCollection => 2, - Routes::DeleteCollection => 3, - } - } -} - pub fn make_collection_list_router( command_sender: Sender, size: Rect, @@ -272,6 +228,7 @@ impl Renderable for CollectionList { Ok(Routes::CreateCollection) => CollectionListData::CreateCollection, Ok(Routes::EditCollection) => CollectionListData::EditCollection(self.selected), Ok(Routes::DeleteCollection) => CollectionListData::DeleteCollection(self.selected), + Ok(Routes::CollectionViewer) => CollectionListData::DeleteCollection(self.selected), Ok(Routes::ListCollections) => unreachable!(), Err(_) => unreachable!(), } @@ -412,7 +369,9 @@ impl Eventful for CollectionList { } assert!(collections_meta.len() > self.selected); self.messager - .send(RouterMessage::Navigate(Navigate::Leave)) + .send(RouterMessage::Navigate(Navigate::Leave( + Routes::CollectionViewer.into(), + ))) .expect("failed to send navigation message"); }), _ => {} diff --git a/hac-client/src/pages/collection_viewer/collection_store.rs b/hac-client/src/pages/collection_viewer/collection_store.rs deleted file mode 100755 index cd8758e..0000000 --- a/hac-client/src/pages/collection_viewer/collection_store.rs +++ /dev/null @@ -1,525 +0,0 @@ -use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; -use std::sync::{Arc, RwLock}; - -use crate::pages::collection_viewer::collection_viewer::{CollectionViewerOverlay, PaneFocus}; - -#[derive(Debug)] -pub struct CollectionState { - collection: Rc>, - hovered_request: Option, - selected_request: Option>>, - dirs_expanded: Rc>>, - selected_pane: Option, - focused_pane: PaneFocus, - has_pending_request: bool, - overlay_stack: Vec, -} - -#[derive(Debug, Default)] -pub struct CollectionStore { - state: Option>>, -} - -#[derive(Debug)] -pub enum CollectionStoreAction { - SetSelectedRequest(Option>>), - SetHoveredRequest(Option), - InsertRequest(RequestKind), - HoverPrev, - HoverNext, - ToggleDirectory(String), - SetFocusedPane(PaneFocus), - SetSelectedPane(Option), - SetPendingRequest(bool), -} - -impl CollectionStore { - pub fn set_state(&mut self, collection: Collection) { - let selected_request = collection.requests.as_ref().and_then(|requests| { - requests.read().unwrap().first().and_then(|req| { - if let RequestKind::Single(req) = req { - Some(req.clone()) - } else { - None - } - }) - }); - - let hovered_request = collection - .requests - .as_ref() - .and_then(|items| items.read().unwrap().first().map(|item| item.get_id())); - - let state = CollectionState { - selected_request, - hovered_request, - dirs_expanded: Rc::new(RefCell::new(HashMap::default())), - collection: Rc::new(RefCell::new(collection)), - focused_pane: PaneFocus::Sidebar, - selected_pane: None, - has_pending_request: false, - overlay_stack: vec![], - }; - - self.state = Some(Rc::new(RefCell::new(state))); - } - - pub fn dispatch(&mut self, action: CollectionStoreAction) { - if let Some(ref state) = self.state { - match action { - CollectionStoreAction::SetSelectedRequest(maybe_req) => state.borrow_mut().selected_request = maybe_req, - CollectionStoreAction::SetHoveredRequest(maybe_req_id) => { - state.borrow_mut().hovered_request = maybe_req_id - } - CollectionStoreAction::InsertRequest(request_kind) => { - state - .borrow_mut() - .collection - .borrow_mut() - .requests - .get_or_insert_with(|| Arc::new(RwLock::new(vec![]))) - .write() - .unwrap() - .push(request_kind); - } - CollectionStoreAction::HoverPrev => self.maybe_hover_prev(), - CollectionStoreAction::HoverNext => self.maybe_hover_next(), - CollectionStoreAction::ToggleDirectory(dir_id) => { - let state = state.borrow_mut(); - let mut dirs = state.dirs_expanded.borrow_mut(); - let entry = dirs.entry(dir_id).or_insert(false); - *entry = !*entry; - } - CollectionStoreAction::SetFocusedPane(pane) => state.borrow_mut().focused_pane = pane, - CollectionStoreAction::SetSelectedPane(pane) => state.borrow_mut().selected_pane = pane, - CollectionStoreAction::SetPendingRequest(is_pending) => { - state.borrow_mut().has_pending_request = is_pending; - } - } - } - } - - pub fn get_selected_request(&self) -> Option>> { - self.state - .as_ref() - .and_then(|state| state.borrow().selected_request.clone()) - } - - pub fn get_focused_pane(&self) -> PaneFocus { - self.state - .as_ref() - .map(|state| state.borrow().focused_pane) - .expect("tried to get the focused pane without a state") - } - - pub fn get_selected_pane(&self) -> Option { - self.state - .as_ref() - .map(|state| state.borrow().selected_pane) - .expect("tried to get the selected pane without a state") - } - - pub fn get_hovered_request(&self) -> Option { - self.state - .as_ref() - .and_then(|state| state.borrow().hovered_request.clone()) - } - - pub fn get_collection(&self) -> Option>> { - self.state.as_ref().map(|state| state.borrow().collection.clone()) - } - - pub fn get_dirs_expanded(&mut self) -> Option>>> { - self.state.as_mut().map(|state| state.borrow().dirs_expanded.clone()) - } - - pub fn push_overlay(&mut self, overlay: CollectionViewerOverlay) { - if let Some(state) = self.state.as_mut() { - state.borrow_mut().overlay_stack.push(overlay) - } - } - - pub fn pop_overlay(&mut self) -> Option { - self.state - .as_mut() - .and_then(|state| state.borrow_mut().overlay_stack.pop()) - } - - pub fn peek_overlay(&self) -> CollectionViewerOverlay { - self.state - .as_ref() - .and_then(|state| state.borrow().overlay_stack.last().cloned()) - .unwrap_or(CollectionViewerOverlay::None) - } - - pub fn has_overlay(&self) -> bool { - self.state - .as_ref() - .map(|state| !state.borrow().overlay_stack.is_empty()) - .unwrap_or(false) - } - - pub fn clear_overlay(&mut self) { - if let Some(state) = self.state.as_mut() { - state.borrow_mut().overlay_stack.clear() - } - } - - pub fn get_requests(&self) -> Option>>> { - self.state - .as_ref() - .and_then(|state| state.borrow().collection.borrow().requests.as_ref().cloned()) - } - - pub fn has_pending_request(&self) -> bool { - self.state - .as_ref() - .is_some_and(|state| state.borrow().has_pending_request) - } - - fn maybe_hover_prev(&mut self) { - if self.get_requests().is_some() { - let requests = self.get_requests().unwrap(); - - let Some(id) = self.get_hovered_request() else { - tracing::debug!("{:?}", self.get_hovered_request()); - self.dispatch(CollectionStoreAction::SetHoveredRequest( - requests.read().unwrap().first().map(|req| req.get_id()), - )); - tracing::debug!("{:?}", self.get_hovered_request()); - return; - }; - - if let Some(next) = find_next_entry( - &requests.read().unwrap(), - VisitNode::Prev, - &self.get_dirs_expanded().unwrap().borrow(), - &id, - ) { - self.dispatch(CollectionStoreAction::SetHoveredRequest(Some(next.get_id()))); - }; - } - } - - fn maybe_hover_next(&mut self) { - if self.get_requests().is_some() { - let requests = self.get_requests().unwrap(); - - let Some(id) = self.get_hovered_request() else { - self.dispatch(CollectionStoreAction::SetHoveredRequest( - requests.read().unwrap().first().map(|req| req.get_id()), - )); - return; - }; - - if let Some(next) = find_next_entry( - &requests.read().unwrap(), - VisitNode::Next, - &self.get_dirs_expanded().unwrap().borrow(), - &id, - ) { - self.dispatch(CollectionStoreAction::SetHoveredRequest(Some(next.get_id()))); - }; - }; - } - - pub fn find_hovered_request(&mut self) -> RequestKind { - get_request_by_id( - &self.get_requests().as_ref().unwrap().read().unwrap(), - &self.get_dirs_expanded().unwrap().borrow(), - self.get_hovered_request().as_ref().unwrap(), - ) - } - - pub fn remove_item(&mut self, item_id: String) { - if let Some(request) = self.get_selected_request() { - if request.read().unwrap().id.eq(&item_id) { - self.dispatch(CollectionStoreAction::SetSelectedRequest(None)); - } - } - let mut requests = self.get_requests(); - let mut requests = requests.as_mut().unwrap().write().unwrap(); - requests.retain(|req| req.get_id().ne(&item_id)); - requests.iter_mut().for_each(|req| { - if let RequestKind::Nested(dir) = req { - dir.requests - .write() - .unwrap() - .retain(|child| child.get_id().ne(&item_id)); - } - }); - self.dispatch(CollectionStoreAction::SetHoveredRequest( - requests.first().map(|req| req.get_id()), - )); - } -} - -#[derive(PartialEq)] -enum VisitNode { - Next, - Prev, - Curr, -} - -fn traverse( - found: &mut bool, - visit: &VisitNode, - dirs_expanded: &HashMap, - current: &RequestKind, - needle: &str, - path: &mut Vec, -) -> bool { - let node_match = current.get_id().eq(needle); - - match (&visit, node_match, &found) { - // We are looking for the next item and we already found the current one (needle), so the - // current item must be the next... we add it to the path and return found = true - (VisitNode::Next, false, true) => { - path.push(current.clone()); - return *found; - } - // We are looking for the previous item and we just found the current one (needle), so we - // simply return found = true as we dont want the current one on the path - (VisitNode::Prev, true, false) => { - *found = true; - return *found; - } - // We are looking for the next and just found the current one, so we set the flag to - // true in order to know when to return the next. - (VisitNode::Next, true, false) => *found = true, - // we are looking for the current node and we found it, so we set the flag to true - // and return immediatly - (VisitNode::Curr, true, _) => { - path.push(current.clone()); - *found = true; - return *found; - } - _ => {} - } - - // visit the node in order to have the full traversed path... - path.push(current.clone()); - - if let RequestKind::Nested(dir) = current { - // if we are on a collapsed directory we should not recurse into its children - if !dirs_expanded.get(&dir.id).unwrap() { - return false; - } - - // recurse into children when expanded - for node in dir.requests.read().unwrap().iter() { - if traverse(found, visit, dirs_expanded, node, needle, path) { - return true; - } - } - } - - false -} - -fn get_request_by_id(tree: &[RequestKind], dirs_expanded: &HashMap, id: &str) -> RequestKind { - let mut found = false; - let mut path = vec![]; - - for node in tree { - if traverse(&mut found, &VisitNode::Curr, dirs_expanded, node, id, &mut path) { - break; - } - } - - path.pop().expect("attempting to find an unexisting request") -} - -fn find_next_entry( - tree: &[RequestKind], - visit: VisitNode, - dirs_expanded: &HashMap, - needle: &str, -) -> Option { - let mut found = false; - let mut path = vec![]; - - for node in tree { - if traverse(&mut found, &visit, dirs_expanded, node, needle, &mut path) { - break; - } - } - - found.then(|| path.pop()).flatten() -} - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - - use hac_core::collection::types::{Directory, Request, RequestMethod}; - - use super::*; - - fn create_root_one() -> RequestKind { - RequestKind::Single(Arc::new(RwLock::new(Request { - id: "root".to_string(), - method: RequestMethod::Get, - name: "Root1".to_string(), - auth_method: None, - parent: None, - headers: None, - uri: "/root1".to_string(), - body_type: None, - body: None, - }))) - } - - fn create_child_one() -> RequestKind { - RequestKind::Single(Arc::new(RwLock::new(Request { - id: "child_one".to_string(), - auth_method: None, - parent: Some(String::from("dir")), - method: RequestMethod::Post, - name: "Child1".to_string(), - uri: "/nested1/child1".to_string(), - headers: None, - body_type: None, - body: None, - }))) - } - - fn create_child_two() -> RequestKind { - RequestKind::Single(Arc::new(RwLock::new(Request { - id: "child_two".to_string(), - method: RequestMethod::Put, - auth_method: None, - name: "Child2".to_string(), - headers: None, - parent: Some(String::from("dir")), - uri: "/nested1/child2".to_string(), - body_type: None, - body: None, - }))) - } - - fn create_not_used() -> RequestKind { - RequestKind::Single(Arc::new(RwLock::new(Request { - id: "not_used".to_string(), - method: RequestMethod::Put, - name: "NotUsed".to_string(), - parent: None, - auth_method: None, - headers: None, - uri: "/not/used".to_string(), - body_type: None, - body: None, - }))) - } - - fn create_dir() -> Directory { - Directory { - id: "dir".to_string(), - name: "Nested1".to_string(), - requests: Arc::new(RwLock::new(vec![create_child_one(), create_child_two()])), - } - } - - fn create_nested() -> RequestKind { - RequestKind::Nested(create_dir()) - } - - fn create_root_two() -> RequestKind { - RequestKind::Single(Arc::new(RwLock::new(Request { - id: "root_two".to_string(), - method: RequestMethod::Delete, - auth_method: None, - headers: None, - parent: None, - name: "Root2".to_string(), - uri: "/root2".to_string(), - body_type: None, - body: None, - }))) - } - - fn create_test_tree() -> Vec { - vec![create_root_one(), create_nested(), create_root_two()] - } - - #[test] - fn test_visit_next_no_expanded() { - let tree = create_test_tree(); - let mut dirs_expanded = HashMap::new(); - dirs_expanded.insert(create_dir().id, false); - let needle = create_nested(); - let expected = create_root_two(); - - let next = find_next_entry(&tree, VisitNode::Next, &dirs_expanded, &needle.get_id()); - - assert!(next.is_some()); - assert_eq!(next.unwrap().get_id(), expected.get_id()); - } - - #[test] - fn test_visit_node_nested_next() { - let tree = create_test_tree(); - let mut dirs_expanded = HashMap::new(); - dirs_expanded.insert(create_dir().id, true); - let needle = create_nested(); - let expected = create_child_one(); - - let next = find_next_entry(&tree, VisitNode::Next, &dirs_expanded, &needle.get_id()); - - assert!(next.is_some()); - assert_eq!(next.unwrap().get_id(), expected.get_id()); - } - - #[test] - fn test_visit_node_no_match() { - let tree = create_test_tree(); - let mut dirs_expanded = HashMap::new(); - dirs_expanded.insert(create_dir().id, true); - let needle = create_not_used(); - - let next = find_next_entry(&tree, VisitNode::Next, &dirs_expanded, &needle.get_id()); - - assert!(next.is_none()); - } - - #[test] - fn test_visit_node_nested_prev() { - let tree = create_test_tree(); - let mut dirs_expanded = HashMap::new(); - dirs_expanded.insert(create_dir().id, true); - let needle = create_child_one(); - let expected = create_nested(); - - let next = find_next_entry(&tree, VisitNode::Prev, &dirs_expanded, &needle.get_id()); - - assert!(next.is_some()); - assert_eq!(next.unwrap().get_id(), expected.get_id()); - } - - #[test] - fn test_visit_prev_into_nested() { - let tree = create_test_tree(); - let mut dirs_expanded = HashMap::new(); - dirs_expanded.insert(create_dir().id, true); - let needle = create_root_two(); - let expected = create_child_two(); - - let next = find_next_entry(&tree, VisitNode::Prev, &dirs_expanded, &needle.get_id()); - - assert!(next.is_some()); - assert_eq!(next.unwrap().get_id(), expected.get_id()); - } - - #[test] - fn test_empty_tree() { - let tree = vec![]; - let dirs_expanded = HashMap::new(); - let needle = create_root_two(); - - let next = find_next_entry(&tree, VisitNode::Next, &dirs_expanded, &needle.get_id()); - - assert!(next.is_none()); - } -} diff --git a/hac-client/src/pages/collection_viewer/collection_viewer.rs b/hac-client/src/pages/collection_viewer/collection_viewer.rs index 46436f6..ea897e4 100755 --- a/hac-client/src/pages/collection_viewer/collection_viewer.rs +++ b/hac-client/src/pages/collection_viewer/collection_viewer.rs @@ -13,12 +13,13 @@ use ratatui::widgets::{Block, Clear}; use ratatui::Frame; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; -use crate::pages::collection_viewer::collection_store::{CollectionStore, CollectionStoreAction}; -use crate::pages::collection_viewer::request_editor::{RequestEditor, RequestEditorEvent}; -use crate::pages::collection_viewer::request_uri::{RequestUri, RequestUriEvent}; -use crate::pages::collection_viewer::response_viewer::{ResponseViewer, ResponseViewerEvent}; -use crate::pages::collection_viewer::sidebar::{self, Sidebar, SidebarEvent}; -use crate::pages::{Eventful, Renderable}; +//use crate::pages::collection_viewer::collection_store::{CollectionStore, CollectionStoreAction}; +//use crate::pages::collection_viewer::request_editor::{RequestEditor, RequestEditorEvent}; +//use crate::pages::collection_viewer::request_uri::{RequestUri, RequestUriEvent}; +//use crate::pages::collection_viewer::response_viewer::{ResponseViewer, ResponseViewerEvent}; +//use crate::pages::collection_viewer::sidebar::{self, Sidebar, SidebarEvent}; +use crate::renderable::{Eventful, Renderable}; +use crate::{HacColors, HacConfig}; #[derive(Debug, PartialEq)] pub struct ExplorerLayout { @@ -74,18 +75,16 @@ impl PaneFocus { } #[derive(Debug)] -pub struct CollectionViewer<'cv> { - response_viewer: ResponseViewer<'cv>, - request_editor: RequestEditor<'cv>, - request_uri: RequestUri<'cv>, - sidebar: Sidebar<'cv>, - - colors: &'cv hac_colors::Colors, - config: &'cv hac_config::Config, +pub struct CollectionViewer { + //response_viewer: ResponseViewer<'cv>, + //request_editor: RequestEditor<'cv>, + //request_uri: RequestUri<'cv>, + //sidebar: Sidebar<'cv>, + colors: HacColors, + config: HacConfig, layout: ExplorerLayout, global_command_sender: Option>, collection_sync_timer: std::time::Instant, - collection_store: Rc>, responses_map: HashMap>>, response_rx: UnboundedReceiver, @@ -94,30 +93,24 @@ pub struct CollectionViewer<'cv> { dry_run: bool, } -impl<'cv> CollectionViewer<'cv> { - pub fn new( - size: Rect, - collection_store: Rc>, - colors: &'cv hac_colors::Colors, - config: &'cv hac_config::Config, - dry_run: bool, - ) -> Self { +impl CollectionViewer { + pub fn new(size: Rect, colors: HacColors, config: HacConfig, dry_run: bool) -> Self { let layout = build_layout(size); let (request_tx, response_rx) = unbounded_channel::(); - let sidebar = sidebar::Sidebar::new(colors, collection_store.clone()); - - let request_editor = RequestEditor::new(colors, config, collection_store.clone(), layout.req_editor); - - let response_viewer = ResponseViewer::new(colors, collection_store.clone(), None, layout.response_preview); - - let request_uri = RequestUri::new(colors, collection_store.clone(), layout.req_uri); + //let sidebar = sidebar::Sidebar::new(colors, collection_store.clone()); + // + //let request_editor = RequestEditor::new(colors, config, collection_store.clone(), layout.req_editor); + // + //let response_viewer = ResponseViewer::new(colors, collection_store.clone(), None, layout.response_preview); + // + //let request_uri = RequestUri::new(colors, collection_store.clone(), layout.req_uri); CollectionViewer { - request_editor, - response_viewer, - sidebar, - request_uri, + //request_editor, + //response_viewer, + //sidebar, + //request_uri, colors, layout, config, @@ -127,144 +120,148 @@ impl<'cv> CollectionViewer<'cv> { response_rx, request_tx, dry_run, - collection_store, } } - fn rebuild_everything(&mut self) { - self.sidebar = sidebar::Sidebar::new(self.colors, self.collection_store.clone()); - self.request_editor = RequestEditor::new( - self.colors, - self.config, - self.collection_store.clone(), - self.layout.req_editor, - ); - self.response_viewer = ResponseViewer::new( - self.colors, - self.collection_store.clone(), - None, - self.layout.response_preview, - ); - self.request_uri = RequestUri::new(self.colors, self.collection_store.clone(), self.layout.req_uri); - } - - fn focus_next(&mut self) { - let next_pane = self.collection_store.borrow().get_focused_pane().next(); - self.update_focus(next_pane); - } - - fn focus_prev(&mut self) { - let prev_pane = self.collection_store.borrow().get_focused_pane().prev(); - self.update_focus(prev_pane); - } + //fn rebuild_everything(&mut self) { + // self.sidebar = sidebar::Sidebar::new(self.colors, self.collection_store.clone()); + // self.request_editor = RequestEditor::new( + // self.colors, + // self.config, + // self.collection_store.clone(), + // self.layout.req_editor, + // ); + // self.response_viewer = ResponseViewer::new( + // self.colors, + // self.collection_store.clone(), + // None, + // self.layout.response_preview, + // ); + // self.request_uri = RequestUri::new(self.colors, self.collection_store.clone(), self.layout.req_uri); + //} + + //fn focus_next(&mut self) { + // let next_pane = self.collection_store.borrow().get_focused_pane().next(); + // self.update_focus(next_pane); + //} + // + //fn focus_prev(&mut self) { + // let prev_pane = self.collection_store.borrow().get_focused_pane().prev(); + // self.update_focus(prev_pane); + //} // collect all pending responses from the channel. Here, I don't see a way we // may have more than one response on this channel at any point, but it shouldn't matter // if we have, so we can drain all the responses and update accordingly - fn drain_responses_channel(&mut self) { - while let Ok(res) = self.response_rx.try_recv() { - let res = Rc::new(RefCell::new(res)); - self.collection_store - .borrow() - .get_selected_request() - .as_ref() - .and_then(|req| { - self.responses_map - .insert(req.read().unwrap().id.to_string(), Rc::clone(&res)) - }); - self.response_viewer.update(Some(Rc::clone(&res))); - self.response_rx.is_empty().then(|| { - self.collection_store - .borrow_mut() - .dispatch(CollectionStoreAction::SetPendingRequest(false)); - }); - } - } - - fn sync_collection_changes(&mut self) { - let sender = self - .global_command_sender - .as_ref() - .expect("should have a sender at this point") - .clone(); - - let mut collection = self - .collection_store - .borrow() - .get_collection() - .clone() - .expect("tried to sync collection to disk without having a collection") - .borrow() - .clone(); - if let Some(request) = self.collection_store.borrow().get_selected_request() { - let request = request.clone(); - let body = self.request_editor.body().to_string(); - // this is not the best idea for when we start implementing other kinds of - // body types like GraphQL - if !body.is_empty() { - request.write().unwrap().body = Some(body); - request.write().unwrap().body_type = Some(BodyType::Json) - } - - // we might later on decide to keep track of the actual dir/request index - // so we dont have to go over all the possible requests, this might be a - // problem for huge collections, but I haven't tested - collection - .requests - .as_mut() - .expect("no requests on collection, but we have a selected request") - .write() - .unwrap() - .iter_mut() - .for_each(|other| match other { - RequestKind::Single(inner) => { - if request.read().unwrap().id.eq(&inner.read().unwrap().id) { - *inner = request.clone(); - } - } - RequestKind::Nested(dir) => dir.requests.write().unwrap().iter_mut().for_each(|other| { - if let RequestKind::Single(inner) = other { - if request.read().unwrap().id.eq(&inner.read().unwrap().id) { - *inner = request.clone(); - } - } - }), - }); - } - - self.collection_sync_timer = std::time::Instant::now(); - - //if self.dry_run { - // return; - //} - - //tokio::spawn(async move { - // match hac_core::fs::sync_collection(collection).await { - // Ok(_) => {} - // Err(e) => { - // if sender.send(Command::Error(e.to_string())).is_err() { - // tracing::error!("failed to send error command through channel"); - // std::process::abort(); - // } - // } - // } - //}); - } + //fn drain_responses_channel(&mut self) { + // while let Ok(res) = self.response_rx.try_recv() { + // let res = Rc::new(RefCell::new(res)); + // self.collection_store + // .borrow() + // .get_selected_request() + // .as_ref() + // .and_then(|req| { + // self.responses_map + // .insert(req.read().unwrap().id.to_string(), Rc::clone(&res)) + // }); + // self.response_viewer.update(Some(Rc::clone(&res))); + // self.response_rx.is_empty().then(|| { + // self.collection_store + // .borrow_mut() + // .dispatch(CollectionStoreAction::SetPendingRequest(false)); + // }); + // } + //} + + //fn sync_collection_changes(&mut self) { + // let sender = self + // .global_command_sender + // .as_ref() + // .expect("should have a sender at this point") + // .clone(); + // + // let mut collection = self + // .collection_store + // .borrow() + // .get_collection() + // .clone() + // .expect("tried to sync collection to disk without having a collection") + // .borrow() + // .clone(); + // if let Some(request) = self.collection_store.borrow().get_selected_request() { + // let request = request.clone(); + // let body = self.request_editor.body().to_string(); + // // this is not the best idea for when we start implementing other kinds of + // // body types like GraphQL + // if !body.is_empty() { + // request.write().unwrap().body = Some(body); + // request.write().unwrap().body_type = Some(BodyType::Json) + // } + // + // // we might later on decide to keep track of the actual dir/request index + // // so we dont have to go over all the possible requests, this might be a + // // problem for huge collections, but I haven't tested + // collection + // .requests + // .as_mut() + // .expect("no requests on collection, but we have a selected request") + // .write() + // .unwrap() + // .iter_mut() + // .for_each(|other| match other { + // RequestKind::Single(inner) => { + // if request.read().unwrap().id.eq(&inner.read().unwrap().id) { + // *inner = request.clone(); + // } + // } + // RequestKind::Nested(dir) => dir.requests.write().unwrap().iter_mut().for_each(|other| { + // if let RequestKind::Single(inner) = other { + // if request.read().unwrap().id.eq(&inner.read().unwrap().id) { + // *inner = request.clone(); + // } + // } + // }), + // }); + // } + + //self.collection_sync_timer = std::time::Instant::now(); + + //if self.dry_run { + // return; + //} + + //tokio::spawn(async move { + // match hac_core::fs::sync_collection(collection).await { + // Ok(_) => {} + // Err(e) => { + // if sender.send(Command::Error(e.to_string())).is_err() { + // tracing::error!("failed to send error command through channel"); + // std::process::abort(); + // } + // } + // } + //}); + //} + + //fn update_selection(&mut self, pane_to_select: Option) { + // self.collection_store + // .borrow_mut() + // .dispatch(CollectionStoreAction::SetSelectedPane(pane_to_select)); + //} + // + //fn update_focus(&mut self, pane_to_focus: PaneFocus) { + // self.collection_store + // .borrow_mut() + // .dispatch(CollectionStoreAction::SetFocusedPane(pane_to_focus)); + //} +} - fn update_selection(&mut self, pane_to_select: Option) { - self.collection_store - .borrow_mut() - .dispatch(CollectionStoreAction::SetSelectedPane(pane_to_select)); - } +impl Renderable for CollectionViewer { + type Input = (); + type Output = (); - fn update_focus(&mut self, pane_to_focus: PaneFocus) { - self.collection_store - .borrow_mut() - .dispatch(CollectionStoreAction::SetFocusedPane(pane_to_focus)); - } -} + fn data(&self, requester: u8) -> Self::Output {} -impl Renderable for CollectionViewer<'_> { #[tracing::instrument(skip_all)] fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { // we redraw the background to prevent weird "transparent" spots when popups are @@ -272,83 +269,83 @@ impl Renderable for CollectionViewer<'_> { frame.render_widget(Clear, size); frame.render_widget(Block::default().bg(self.colors.primary.background), size); - self.drain_responses_channel(); - - self.sidebar.draw(frame, self.layout.sidebar)?; - self.response_viewer.draw(frame, self.layout.response_preview)?; - self.request_editor.draw(frame, self.layout.req_editor)?; - self.request_uri.draw(frame, self.layout.req_uri)?; - - let overlay = self.collection_store.borrow().peek_overlay(); - match overlay { - CollectionViewerOverlay::CreateRequest => { - self.sidebar.draw_overlay(frame, overlay)?; - } - CollectionViewerOverlay::CreateDirectory => { - self.sidebar.draw_overlay(frame, overlay)?; - } - CollectionViewerOverlay::SelectParentDir => { - self.sidebar.draw_overlay(frame, overlay)?; - } - CollectionViewerOverlay::EditRequest => { - self.sidebar.draw_overlay(frame, overlay)?; - } - CollectionViewerOverlay::EditDirectory => { - self.sidebar.draw_overlay(frame, overlay)?; - } - CollectionViewerOverlay::DeleteSidebarItem(_) => { - self.sidebar.draw_overlay(frame, overlay)?; - } - CollectionViewerOverlay::HeadersHelp => { - self.request_editor.draw_overlay(frame, overlay)?; - } - CollectionViewerOverlay::HeadersDelete => { - self.request_editor.draw_overlay(frame, overlay)?; - } - CollectionViewerOverlay::HeadersForm(_, _) => { - self.request_editor.draw_overlay(frame, overlay)?; - } - CollectionViewerOverlay::ChangeAuthMethod => { - self.request_editor.draw_overlay(frame, overlay)?; - } - CollectionViewerOverlay::None => {} - } + //self.drain_responses_channel(); + // + //self.sidebar.draw(frame, self.layout.sidebar)?; + //self.response_viewer.draw(frame, self.layout.response_preview)?; + //self.request_editor.draw(frame, self.layout.req_editor)?; + //self.request_uri.draw(frame, self.layout.req_uri)?; + + //let overlay = self.collection_store.borrow().peek_overlay(); + //match overlay { + // CollectionViewerOverlay::CreateRequest => { + // self.sidebar.draw_overlay(frame, overlay)?; + // } + // CollectionViewerOverlay::CreateDirectory => { + // self.sidebar.draw_overlay(frame, overlay)?; + // } + // CollectionViewerOverlay::SelectParentDir => { + // self.sidebar.draw_overlay(frame, overlay)?; + // } + // CollectionViewerOverlay::EditRequest => { + // self.sidebar.draw_overlay(frame, overlay)?; + // } + // CollectionViewerOverlay::EditDirectory => { + // self.sidebar.draw_overlay(frame, overlay)?; + // } + // CollectionViewerOverlay::DeleteSidebarItem(_) => { + // self.sidebar.draw_overlay(frame, overlay)?; + // } + // CollectionViewerOverlay::HeadersHelp => { + // self.request_editor.draw_overlay(frame, overlay)?; + // } + // CollectionViewerOverlay::HeadersDelete => { + // self.request_editor.draw_overlay(frame, overlay)?; + // } + // CollectionViewerOverlay::HeadersForm(_, _) => { + // self.request_editor.draw_overlay(frame, overlay)?; + // } + // CollectionViewerOverlay::ChangeAuthMethod => { + // self.request_editor.draw_overlay(frame, overlay)?; + // } + // CollectionViewerOverlay::None => {} + //} - if self - .collection_store - .borrow() - .get_selected_pane() - .as_ref() - .is_some_and(|pane| pane.eq(&PaneFocus::Editor)) - { - self.request_editor.maybe_draw_cursor(frame); - } + //if self + // .collection_store + // .borrow() + // .get_selected_pane() + // .as_ref() + // .is_some_and(|pane| pane.eq(&PaneFocus::Editor)) + //{ + // self.request_editor.maybe_draw_cursor(frame); + //} - if self - .collection_store - .borrow() - .get_selected_pane() - .as_ref() - .is_some_and(|pane| pane.eq(&PaneFocus::ReqUri)) - { - if let Some(request) = self.collection_store.borrow().get_selected_request().as_ref() { - frame.set_cursor( - self.layout - .req_uri - .x - .add(request.read().unwrap().uri.chars().count() as u16) - .add(1), - self.layout.req_uri.y.add(1), - ) - } - } + //if self + // .collection_store + // .borrow() + // .get_selected_pane() + // .as_ref() + // .is_some_and(|pane| pane.eq(&PaneFocus::ReqUri)) + //{ + // if let Some(request) = self.collection_store.borrow().get_selected_request().as_ref() { + // frame.set_cursor( + // self.layout + // .req_uri + // .x + // .add(request.read().unwrap().uri.chars().count() as u16) + // .add(1), + // self.layout.req_uri.y.add(1), + // ) + // } + //} Ok(()) } fn tick(&mut self) -> anyhow::Result<()> { if self.collection_sync_timer.elapsed().as_secs().ge(&5) { - self.sync_collection_changes(); + //self.sync_collection_changes(); } Ok(()) } @@ -359,128 +356,128 @@ impl Renderable for CollectionViewer<'_> { fn resize(&mut self, new_size: Rect) { let new_layout = build_layout(new_size); - self.request_editor.resize(new_layout.req_editor); - self.response_viewer.resize(new_layout.response_preview); + //self.request_editor.resize(new_layout.req_editor); + //self.response_viewer.resize(new_layout.response_preview); self.layout = new_layout; } } -impl Eventful for CollectionViewer<'_> { +impl Eventful for CollectionViewer { type Result = Command; fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - if let ( - None, - KeyEvent { - code: KeyCode::Char('c'), - modifiers: KeyModifiers::CONTROL, - .. - }, - ) = (self.collection_store.borrow().get_selected_pane(), key_event) - { - return Ok(Some(Command::Quit)); - } + //if let ( + // None, + // KeyEvent { + // code: KeyCode::Char('c'), + // modifiers: KeyModifiers::CONTROL, + // .. + // }, + //) = (self.collection_store.borrow().get_selected_pane(), key_event) + //{ + // return Ok(Some(Command::Quit)); + //} - if self.collection_store.borrow().get_selected_pane().is_none() { - match key_event.code { - KeyCode::Char('r') => { - self.update_focus(PaneFocus::Sidebar); - self.update_selection(Some(PaneFocus::Sidebar)); - } - KeyCode::Char('u') => { - self.update_focus(PaneFocus::ReqUri); - self.update_selection(Some(PaneFocus::ReqUri)); - } - KeyCode::Char('p') => { - self.update_focus(PaneFocus::Preview); - self.update_selection(Some(PaneFocus::Preview)); - } - KeyCode::Char('e') => { - self.update_focus(PaneFocus::Editor); - self.update_selection(Some(PaneFocus::Editor)); - } - KeyCode::Tab => self.focus_next(), - KeyCode::BackTab => self.focus_prev(), - KeyCode::Enter => { - let curr_pane = self.collection_store.borrow().get_focused_pane(); - self.update_selection(Some(curr_pane)); - } - _ => {} - } - return Ok(None); - } + //if self.collection_store.borrow().get_selected_pane().is_none() { + // match key_event.code { + // KeyCode::Char('r') => { + // self.update_focus(PaneFocus::Sidebar); + // self.update_selection(Some(PaneFocus::Sidebar)); + // } + // KeyCode::Char('u') => { + // self.update_focus(PaneFocus::ReqUri); + // self.update_selection(Some(PaneFocus::ReqUri)); + // } + // KeyCode::Char('p') => { + // self.update_focus(PaneFocus::Preview); + // self.update_selection(Some(PaneFocus::Preview)); + // } + // KeyCode::Char('e') => { + // self.update_focus(PaneFocus::Editor); + // self.update_selection(Some(PaneFocus::Editor)); + // } + // KeyCode::Tab => self.focus_next(), + // KeyCode::BackTab => self.focus_prev(), + // KeyCode::Enter => { + // let curr_pane = self.collection_store.borrow().get_focused_pane(); + // self.update_selection(Some(curr_pane)); + // } + // _ => {} + // } + // return Ok(None); + //} - let selected_pane = self.collection_store.borrow().get_selected_pane(); - if let Some(curr_pane) = selected_pane { - match curr_pane { - PaneFocus::Sidebar => match self.sidebar.handle_key_event(key_event)? { - Some(SidebarEvent::CreateRequest) => self - .collection_store - .borrow_mut() - .push_overlay(CollectionViewerOverlay::CreateRequest), - Some(SidebarEvent::EditRequest) => self - .collection_store - .borrow_mut() - .push_overlay(CollectionViewerOverlay::EditRequest), - Some(SidebarEvent::EditDirectory) => self - .collection_store - .borrow_mut() - .push_overlay(CollectionViewerOverlay::EditDirectory), - Some(SidebarEvent::CreateDirectory) => self - .collection_store - .borrow_mut() - .push_overlay(CollectionViewerOverlay::CreateDirectory), - Some(SidebarEvent::DeleteItem(item_id)) => self - .collection_store - .borrow_mut() - .push_overlay(CollectionViewerOverlay::DeleteSidebarItem(item_id)), - Some(SidebarEvent::RemoveSelection) => self.update_selection(None), - Some(SidebarEvent::SelectNext) => { - self.update_selection(None); - self.focus_next(); - } - Some(SidebarEvent::SelectPrev) => { - self.update_selection(None); - self.focus_prev(); - } - Some(SidebarEvent::SyncCollection) => self.sync_collection_changes(), - Some(SidebarEvent::Quit) => return Ok(Some(Command::Quit)), - Some(SidebarEvent::RebuildView) => self.rebuild_everything(), - // when theres no event we do nothing - None => {} - }, - PaneFocus::ReqUri => match self.request_uri.handle_key_event(key_event)? { - Some(RequestUriEvent::Quit) => return Ok(Some(Command::Quit)), - Some(RequestUriEvent::SendRequest) => hac_core::net::handle_request( - self.collection_store.borrow().get_selected_request().as_ref().unwrap(), - self.request_tx.clone(), - ), - Some(RequestUriEvent::RemoveSelection) => self.update_selection(None), - Some(RequestUriEvent::SelectNext) => { - self.update_selection(None); - self.focus_next(); - } - Some(RequestUriEvent::SelectPrev) => { - self.update_selection(None); - self.focus_prev(); - } - // when theres no event we do nothing - None => {} - }, - PaneFocus::Preview => match self.response_viewer.handle_key_event(key_event)? { - Some(ResponseViewerEvent::RemoveSelection) => self.update_selection(None), - Some(ResponseViewerEvent::Quit) => return Ok(Some(Command::Quit)), - // when theres no event we do nothing - None => {} - }, - PaneFocus::Editor => match self.request_editor.handle_key_event(key_event)? { - Some(RequestEditorEvent::RemoveSelection) => self.update_selection(None), - Some(RequestEditorEvent::Quit) => return Ok(Some(Command::Quit)), - // when theres no event we do nothing - None => {} - }, - }; - } + //let selected_pane = self.collection_store.borrow().get_selected_pane(); + //if let Some(curr_pane) = selected_pane { + // match curr_pane { + // PaneFocus::Sidebar => match self.sidebar.handle_key_event(key_event)? { + // Some(SidebarEvent::CreateRequest) => self + // .collection_store + // .borrow_mut() + // .push_overlay(CollectionViewerOverlay::CreateRequest), + // Some(SidebarEvent::EditRequest) => self + // .collection_store + // .borrow_mut() + // .push_overlay(CollectionViewerOverlay::EditRequest), + // Some(SidebarEvent::EditDirectory) => self + // .collection_store + // .borrow_mut() + // .push_overlay(CollectionViewerOverlay::EditDirectory), + // Some(SidebarEvent::CreateDirectory) => self + // .collection_store + // .borrow_mut() + // .push_overlay(CollectionViewerOverlay::CreateDirectory), + // Some(SidebarEvent::DeleteItem(item_id)) => self + // .collection_store + // .borrow_mut() + // .push_overlay(CollectionViewerOverlay::DeleteSidebarItem(item_id)), + // Some(SidebarEvent::RemoveSelection) => self.update_selection(None), + // Some(SidebarEvent::SelectNext) => { + // self.update_selection(None); + // self.focus_next(); + // } + // Some(SidebarEvent::SelectPrev) => { + // self.update_selection(None); + // self.focus_prev(); + // } + // Some(SidebarEvent::SyncCollection) => self.sync_collection_changes(), + // Some(SidebarEvent::Quit) => return Ok(Some(Command::Quit)), + // Some(SidebarEvent::RebuildView) => self.rebuild_everything(), + // // when theres no event we do nothing + // None => {} + // }, + // PaneFocus::ReqUri => match self.request_uri.handle_key_event(key_event)? { + // Some(RequestUriEvent::Quit) => return Ok(Some(Command::Quit)), + // Some(RequestUriEvent::SendRequest) => hac_core::net::handle_request( + // self.collection_store.borrow().get_selected_request().as_ref().unwrap(), + // self.request_tx.clone(), + // ), + // Some(RequestUriEvent::RemoveSelection) => self.update_selection(None), + // Some(RequestUriEvent::SelectNext) => { + // self.update_selection(None); + // self.focus_next(); + // } + // Some(RequestUriEvent::SelectPrev) => { + // self.update_selection(None); + // self.focus_prev(); + // } + // // when theres no event we do nothing + // None => {} + // }, + // PaneFocus::Preview => match self.response_viewer.handle_key_event(key_event)? { + // Some(ResponseViewerEvent::RemoveSelection) => self.update_selection(None), + // Some(ResponseViewerEvent::Quit) => return Ok(Some(Command::Quit)), + // // when theres no event we do nothing + // None => {} + // }, + // PaneFocus::Editor => match self.request_editor.handle_key_event(key_event)? { + // Some(RequestEditorEvent::RemoveSelection) => self.update_selection(None), + // Some(RequestEditorEvent::Quit) => return Ok(Some(Command::Quit)), + // // when theres no event we do nothing + // None => {} + // }, + // }; + //} Ok(None) } diff --git a/hac-client/src/pages/collection_viewer/mod.rs b/hac-client/src/pages/collection_viewer/mod.rs index c5b2353..06df887 100755 --- a/hac-client/src/pages/collection_viewer/mod.rs +++ b/hac-client/src/pages/collection_viewer/mod.rs @@ -1,9 +1,8 @@ -pub mod collection_store; #[allow(clippy::module_inception)] pub mod collection_viewer; -mod request_editor; -mod request_uri; -mod response_viewer; -mod sidebar; +//mod request_editor; +//mod request_uri; +//mod response_viewer; +//mod sidebar; pub use collection_viewer::CollectionViewer; diff --git a/hac-client/src/pages/mod.rs b/hac-client/src/pages/mod.rs index 67e3ef2..38b306a 100755 --- a/hac-client/src/pages/mod.rs +++ b/hac-client/src/pages/mod.rs @@ -1,5 +1,5 @@ pub mod collection_list; -//pub mod collection_viewer; +pub mod collection_viewer; pub mod input; pub mod overlay; mod spinner; diff --git a/hac-client/src/router.rs b/hac-client/src/router.rs index 31cba5f..6903668 100644 --- a/hac-client/src/router.rs +++ b/hac-client/src/router.rs @@ -24,14 +24,14 @@ pub enum Navigate { /// dialog rather than a route /// Navigating to a route from a dialog will close that dialog To(Key), + /// Go back in the navigation history, this completely wipes the current router history, + /// and close every dialog thats visible + Back, /// Routes can only communicate with the router that owns the route, therefore, /// if a route in a nested router needs to navigate to an entirely different router /// the `Leave` variant is responsible for that. It will direct the navigation event /// to the parent router of the current active router. - Leave, - /// Go back in the navigation history, this completely wipes the current router history, - /// and close every dialog thats visible - Back, + Leave(Key), } pub trait AnyCommand {} @@ -261,7 +261,14 @@ impl Router { (false, true) => panic!("invalid navigation from route to dialog"), } } - Navigate::Leave => todo!(), + Navigate::Leave(route) => { + tracing::debug!("leaving to: {route}"); + if let Some(messenger) = self.parent_messager.as_mut() { + messenger + .send(RouterMessage::Navigate(Navigate::To(route))) + .expect("lol"); + } + } } } } From 0d3f17b12a97a15a1529c702ed12818c8e33715e Mon Sep 17 00:00:00 2001 From: wiru Date: Sat, 28 Sep 2024 22:20:32 -0300 Subject: [PATCH 12/23] feat: rendering collection viewer --- Cargo.lock | 1 + hac-client/src/app.rs | 9 +- hac-client/src/pages/collection_list/mod.rs | 49 ++--- .../collection_viewer/collection_viewer.rs | 38 ++-- hac-client/src/pages/collection_viewer/mod.rs | 2 +- .../pages/collection_viewer/request_uri.rs | 174 ++++++++---------- hac-client/src/router.rs | 13 -- hac-loader/src/collection_loader/error.rs | 8 +- hac-loader/src/collection_loader/mod.rs | 22 +-- hac-store/Cargo.toml | 1 + hac-store/src/collection.rs | 19 ++ hac-store/src/collection_meta.rs | 4 +- 12 files changed, 166 insertions(+), 174 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 123c00e..c1358e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -662,6 +662,7 @@ version = "0.2.0" dependencies = [ "chrono", "serde", + "tracing", ] [[package]] diff --git a/hac-client/src/app.rs b/hac-client/src/app.rs index 1ac791e..639af16 100755 --- a/hac-client/src/app.rs +++ b/hac-client/src/app.rs @@ -6,7 +6,7 @@ use ratatui::backend::CrosstermBackend; use ratatui::Terminal; use crate::event_pool::{Event, EventPool}; -use crate::pages::collection_list::make_collection_list_router; +use crate::pages::collection_list::CollectionList; use crate::renderable::{Eventful, Renderable}; use crate::router::Router; use crate::{HacColors, HacConfig}; @@ -74,10 +74,9 @@ impl App { let (command_sender, command_receiver) = std::sync::mpsc::channel(); let size = terminal.size()?; - let router = Router::new(command_sender.clone(), colors.clone()); - - let mut collection_list_router = - make_collection_list_router(command_sender.clone(), size, config.clone(), colors.clone()); + let mut router = Router::new(command_sender.clone(), colors.clone()); + let collection_list = CollectionList::new(size, config.clone(), colors.clone()); + router.add_route(Routes::ListCollections.into(), Box::new(collection_list)); Ok(Self { event_pool: EventPool::new(60f64, 30f64), diff --git a/hac-client/src/pages/collection_list/mod.rs b/hac-client/src/pages/collection_list/mod.rs index d6a9cfd..c869e9c 100644 --- a/hac-client/src/pages/collection_list/mod.rs +++ b/hac-client/src/pages/collection_list/mod.rs @@ -20,22 +20,11 @@ use ratatui::Frame; use crate::app::Routes; use crate::components::list_itemm::ListItem; +use crate::pages::collection_viewer::CollectionViewer; use crate::renderable::{Eventful, Renderable}; -use crate::router::{Navigate, Router, RouterMessage}; +use crate::router::{Navigate, RouterMessage}; use crate::{HacColors, HacConfig}; -pub fn make_collection_list_router( - command_sender: Sender, - size: Rect, - config: HacConfig, - colors: HacColors, -) -> Router { - let mut router = Router::new(command_sender, colors.clone()); - let collection_list = CollectionList::new(size, config, colors.clone()); - router.add_route(Routes::ListCollections.into(), Box::new(collection_list)); - router -} - #[derive(Debug, PartialEq)] struct DashboardLayout { collections_pane: Rect, @@ -217,6 +206,7 @@ pub enum CollectionListData { CreateCollection, EditCollection(usize), DeleteCollection(usize), + CollectionViewer, } impl Renderable for CollectionList { @@ -363,17 +353,34 @@ impl Eventful for CollectionList { .send(RouterMessage::Navigate(Navigate::To(Routes::EditCollection.into()))) .expect("failed to send navigation message"); } - KeyCode::Enter => hac_store::collection_meta::collections_meta(|collections_meta| { - if collections_meta.is_empty() { - return; - } - assert!(collections_meta.len() > self.selected); + KeyCode::Enter => { + let Some(path) = hac_store::collection_meta::collections_meta(|collections_meta| { + if collections_meta.is_empty() { + return None; + } + assert!(collections_meta.len() > self.selected); + let selected = &collections_meta[self.selected]; + Some(selected.path().clone()) + }) else { + return Ok(None); + }; + let collection_viewer = + CollectionViewer::new(self.layout.total_size, self.colors.clone(), self.config.clone()); + let collection = match hac_loader::collection_loader::load_collection(path, &self.config) { + Ok(collection) => collection, + Err(_) => todo!(), + }; + hac_store::collection::set_collection(Some(collection)); self.messager - .send(RouterMessage::Navigate(Navigate::Leave( + .send(RouterMessage::AddRoute( Routes::CollectionViewer.into(), - ))) + Box::new(collection_viewer), + )) + .expect("failed to send router message"); + self.messager + .send(RouterMessage::Navigate(Navigate::To(Routes::CollectionViewer.into()))) .expect("failed to send navigation message"); - }), + } _ => {} } diff --git a/hac-client/src/pages/collection_viewer/collection_viewer.rs b/hac-client/src/pages/collection_viewer/collection_viewer.rs index ea897e4..b640402 100755 --- a/hac-client/src/pages/collection_viewer/collection_viewer.rs +++ b/hac-client/src/pages/collection_viewer/collection_viewer.rs @@ -1,18 +1,21 @@ use std::cell::RefCell; use std::collections::HashMap; -use std::ops::{Add, Div}; +use std::ops::Div; use std::rc::Rc; use std::sync::mpsc::Sender; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::command::Command; use hac_core::net::request_manager::Response; +use hac_store::slab::Key; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::Stylize; use ratatui::widgets::{Block, Clear}; use ratatui::Frame; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; +use super::request_uri::RequestUri; +use crate::pages::collection_list::CollectionListData; //use crate::pages::collection_viewer::collection_store::{CollectionStore, CollectionStoreAction}; //use crate::pages::collection_viewer::request_editor::{RequestEditor, RequestEditorEvent}; //use crate::pages::collection_viewer::request_uri::{RequestUri, RequestUriEvent}; @@ -76,9 +79,12 @@ impl PaneFocus { #[derive(Debug)] pub struct CollectionViewer { + hovered_request: Key, + selected_request: Option, + request_uri: RequestUri, + //response_viewer: ResponseViewer<'cv>, //request_editor: RequestEditor<'cv>, - //request_uri: RequestUri<'cv>, //sidebar: Sidebar<'cv>, colors: HacColors, config: HacConfig, @@ -89,12 +95,10 @@ pub struct CollectionViewer { responses_map: HashMap>>, response_rx: UnboundedReceiver, request_tx: UnboundedSender, - - dry_run: bool, } impl CollectionViewer { - pub fn new(size: Rect, colors: HacColors, config: HacConfig, dry_run: bool) -> Self { + pub fn new(size: Rect, colors: HacColors, config: HacConfig) -> Self { let layout = build_layout(size); let (request_tx, response_rx) = unbounded_channel::(); @@ -103,14 +107,17 @@ impl CollectionViewer { //let request_editor = RequestEditor::new(colors, config, collection_store.clone(), layout.req_editor); // //let response_viewer = ResponseViewer::new(colors, collection_store.clone(), None, layout.response_preview); - // - //let request_uri = RequestUri::new(colors, collection_store.clone(), layout.req_uri); + + let request_uri = RequestUri::new(colors.clone(), layout.req_uri, Some(0)); CollectionViewer { + request_uri, + hovered_request: 0, + selected_request: Some(0), + //request_editor, //response_viewer, //sidebar, - //request_uri, colors, layout, config, @@ -119,7 +126,6 @@ impl CollectionViewer { responses_map: HashMap::default(), response_rx, request_tx, - dry_run, } } @@ -257,11 +263,9 @@ impl CollectionViewer { } impl Renderable for CollectionViewer { - type Input = (); + type Input = CollectionListData; type Output = (); - fn data(&self, requester: u8) -> Self::Output {} - #[tracing::instrument(skip_all)] fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { // we redraw the background to prevent weird "transparent" spots when popups are @@ -270,11 +274,11 @@ impl Renderable for CollectionViewer { frame.render_widget(Block::default().bg(self.colors.primary.background), size); //self.drain_responses_channel(); - // + self.request_uri.draw(frame, self.layout.req_uri)?; + //self.sidebar.draw(frame, self.layout.sidebar)?; //self.response_viewer.draw(frame, self.layout.response_preview)?; //self.request_editor.draw(frame, self.layout.req_editor)?; - //self.request_uri.draw(frame, self.layout.req_uri)?; //let overlay = self.collection_store.borrow().peek_overlay(); //match overlay { @@ -350,6 +354,8 @@ impl Renderable for CollectionViewer { Ok(()) } + fn data(&self, _requester: u8) -> Self::Output {} + fn register_command_handler(&mut self, sender: Sender) { self.global_command_sender = Some(sender); } @@ -366,6 +372,10 @@ impl Eventful for CollectionViewer { type Result = Command; fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { + return Ok(Some(Command::Quit)); + } + //if let ( // None, // KeyEvent { diff --git a/hac-client/src/pages/collection_viewer/mod.rs b/hac-client/src/pages/collection_viewer/mod.rs index 06df887..f4c6a1d 100755 --- a/hac-client/src/pages/collection_viewer/mod.rs +++ b/hac-client/src/pages/collection_viewer/mod.rs @@ -1,7 +1,7 @@ #[allow(clippy::module_inception)] pub mod collection_viewer; //mod request_editor; -//mod request_uri; +mod request_uri; //mod response_viewer; //mod sidebar; diff --git a/hac-client/src/pages/collection_viewer/request_uri.rs b/hac-client/src/pages/collection_viewer/request_uri.rs index 1e651c3..9935899 100755 --- a/hac-client/src/pages/collection_viewer/request_uri.rs +++ b/hac-client/src/pages/collection_viewer/request_uri.rs @@ -1,17 +1,13 @@ -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::collection_viewer::collection_store::CollectionStoreAction; -use crate::pages::collection_viewer::collection_viewer::PaneFocus; -use crate::pages::{Eventful, Renderable}; - -use std::cell::RefCell; -use std::rc::Rc; - use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use hac_store::slab::Key; use ratatui::layout::Rect; use ratatui::style::{Style, Stylize}; use ratatui::widgets::{Block, Borders, Paragraph}; use ratatui::Frame; +use crate::renderable::{Eventful, Renderable}; +use crate::HacColors; + /// Set of events RequestUri can send back to the caller when handling key_events #[derive(Debug)] pub enum RequestUriEvent { @@ -30,131 +26,105 @@ pub enum RequestUriEvent { } #[derive(Debug)] -pub struct RequestUri<'ru> { - colors: &'ru hac_colors::Colors, - collection_store: Rc>, +pub struct RequestUri { + colors: HacColors, size: Rect, + selected_request: Option, + is_focused: bool, + is_selected: bool, } -impl<'ru> RequestUri<'ru> { - pub fn new( - colors: &'ru hac_colors::Colors, - collection_store: Rc>, - size: Rect, - ) -> Self { +impl RequestUri { + pub fn new(colors: HacColors, size: Rect, selected_request: Option) -> Self { Self { colors, - collection_store, size, + selected_request, + is_focused: true, + is_selected: true, } } } -impl<'a> Renderable for RequestUri<'a> { +impl Renderable for RequestUri { + type Input = (); + type Output = (); + + fn data(&self, _requester: u8) -> Self::Output {} + fn resize(&mut self, new_size: Rect) { self.size = new_size; } fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { - let is_focused = self - .collection_store - .borrow() - .get_focused_pane() - .eq(&PaneFocus::ReqUri); - let is_selected = self - .collection_store - .borrow() - .get_selected_pane() - .is_some_and(|pane| pane.eq(&PaneFocus::ReqUri)); - - let block_border = match (is_focused, is_selected) { + let block_border = match (self.is_focused, self.is_selected) { (true, false) => Style::default().fg(self.colors.bright.blue), (true, true) => Style::default().fg(self.colors.normal.red), (false, _) => Style::default().fg(self.colors.bright.black), }; let uri = self - .collection_store - .borrow() - .get_selected_request() - .as_ref() - .map(|req| req.read().unwrap().uri.to_string()) + .selected_request + .and_then(|key| hac_store::collection::get_request(key, |req| Some(req.uri.to_string()))) .unwrap_or_default(); - frame.render_widget( - Paragraph::new(uri).fg(self.colors.normal.white).block( - Block::default() - .borders(Borders::ALL) - .border_style(block_border) - .title(vec![ - "U".fg(self.colors.normal.red).bold(), - "ri".fg(self.colors.bright.black), - ]), - ), - size, - ); + let uri = Paragraph::new(uri) + .fg(self.colors.normal.white) + .block(Block::default().borders(Borders::ALL).border_style(block_border)); + + frame.render_widget(uri, size); Ok(()) } } -impl Eventful for RequestUri<'_> { +impl Eventful for RequestUri { type Result = RequestUriEvent; fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - let is_selected = self - .collection_store - .borrow() - .get_selected_pane() - .is_some_and(|pane| pane.eq(&PaneFocus::ReqUri)); - - assert!( - is_selected, - "handled an event to the request uri while it was not selected" - ); - - if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - return Ok(Some(RequestUriEvent::Quit)); - } - - match key_event.code { - KeyCode::Esc => return Ok(Some(RequestUriEvent::RemoveSelection)), - KeyCode::Tab => return Ok(Some(RequestUriEvent::SelectNext)), - KeyCode::BackTab => return Ok(Some(RequestUriEvent::SelectPrev)), - KeyCode::Char(c) => { - if let Some(req) = self - .collection_store - .borrow() - .get_selected_request() - .as_mut() - { - req.write().unwrap().uri.push(c); - } - } - KeyCode::Backspace => { - if let Some(req) = self - .collection_store - .borrow() - .get_selected_request() - .as_mut() - { - req.write().unwrap().uri.pop(); - } - } - KeyCode::Enter => { - let mut store = self.collection_store.borrow_mut(); - if store - .get_selected_request() - .as_ref() - .is_some_and(|_| !store.has_pending_request()) - { - store.dispatch(CollectionStoreAction::SetPendingRequest(true)); - return Ok(Some(RequestUriEvent::SendRequest)); - } - } - _ => {} - } - + //let is_selected = self + // .collection_store + // .borrow() + // .get_selected_pane() + // .is_some_and(|pane| pane.eq(&PaneFocus::ReqUri)); + // + //assert!( + // is_selected, + // "handled an event to the request uri while it was not selected" + //); + // + //if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { + // return Ok(Some(RequestUriEvent::Quit)); + //} + // + //match key_event.code { + // KeyCode::Esc => return Ok(Some(RequestUriEvent::RemoveSelection)), + // KeyCode::Tab => return Ok(Some(RequestUriEvent::SelectNext)), + // KeyCode::BackTab => return Ok(Some(RequestUriEvent::SelectPrev)), + // KeyCode::Char(c) => { + // if let Some(req) = self.collection_store.borrow().get_selected_request().as_mut() { + // req.write().unwrap().uri.push(c); + // } + // } + // KeyCode::Backspace => { + // if let Some(req) = self.collection_store.borrow().get_selected_request().as_mut() { + // req.write().unwrap().uri.pop(); + // } + // } + // KeyCode::Enter => { + // let mut store = self.collection_store.borrow_mut(); + // if store + // .get_selected_request() + // .as_ref() + // .is_some_and(|_| !store.has_pending_request()) + // { + // store.dispatch(CollectionStoreAction::SetPendingRequest(true)); + // return Ok(Some(RequestUriEvent::SendRequest)); + // } + // } + // _ => {} + //} + // Ok(None) } } diff --git a/hac-client/src/router.rs b/hac-client/src/router.rs index 6903668..19f90bf 100644 --- a/hac-client/src/router.rs +++ b/hac-client/src/router.rs @@ -27,11 +27,6 @@ pub enum Navigate { /// Go back in the navigation history, this completely wipes the current router history, /// and close every dialog thats visible Back, - /// Routes can only communicate with the router that owns the route, therefore, - /// if a route in a nested router needs to navigate to an entirely different router - /// the `Leave` variant is responsible for that. It will direct the navigation event - /// to the parent router of the current active router. - Leave(Key), } pub trait AnyCommand {} @@ -261,14 +256,6 @@ impl Router { (false, true) => panic!("invalid navigation from route to dialog"), } } - Navigate::Leave(route) => { - tracing::debug!("leaving to: {route}"); - if let Some(messenger) = self.parent_messager.as_mut() { - messenger - .send(RouterMessage::Navigate(Navigate::To(route))) - .expect("lol"); - } - } } } } diff --git a/hac-loader/src/collection_loader/error.rs b/hac-loader/src/collection_loader/error.rs index 644f40e..cfdde28 100644 --- a/hac-loader/src/collection_loader/error.rs +++ b/hac-loader/src/collection_loader/error.rs @@ -1,7 +1,7 @@ -pub type Result = std::result::Result; +pub type Result = std::result::Result; #[derive(Debug)] -pub enum CollectionLoaderError { +pub enum Error { Create(String), Rename(String), Remove(String), @@ -9,9 +9,9 @@ pub enum CollectionLoaderError { Read(String), } -impl std::error::Error for CollectionLoaderError {} +impl std::error::Error for Error {} -impl std::fmt::Display for CollectionLoaderError { +impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Create(message) => write!(f, "{message}"), diff --git a/hac-loader/src/collection_loader/mod.rs b/hac-loader/src/collection_loader/mod.rs index c24707f..7058bbd 100644 --- a/hac-loader/src/collection_loader/mod.rs +++ b/hac-loader/src/collection_loader/mod.rs @@ -4,10 +4,11 @@ mod json_loader; use std::cell::RefCell; use std::fs; +use std::path::Path; use std::rc::Rc; use std::sync::atomic::{AtomicBool, Ordering}; -use error::{CollectionLoaderError, Result}; +pub use error::{Error, Result}; use hac_config::config::CollectionExtensions; use hac_store::collection::Collection; use hac_store::collection_meta::{CollectionMeta, CollectionModifiedMeta}; @@ -33,8 +34,8 @@ where } } -pub fn load_collection>(file_path: F, config: &hac_config::Config) -> Result { - match config.collection_ext { +pub fn load_collection>(file_path: F, config: &Rc>) -> Result { + match config.borrow().collection_ext { CollectionExtensions::Json => Ok(read_collection_file(file_path, JsonLoader::parse)?), } } @@ -54,8 +55,7 @@ fn create_persistent_colletion(name: String) -> Result<()> { let path = super::collections_dir().join(&file_name); let collection = json_collection::JsonCollection::new(name, Default::default(), file_name, &path); let strigified = serde_json::to_string_pretty(&collection).expect("invalid collection format to be stringified"); - fs::write(&path, &strigified) - .map_err(|_| CollectionLoaderError::Create("failed to write collection to disk".into()))?; + fs::write(&path, strigified).map_err(|_| Error::Create("failed to write collection to disk".into()))?; Ok(()) } @@ -105,8 +105,7 @@ fn edit_persistent_collection(entry: &mut CollectionMeta) -> Result<()> { entry.path_mut().pop(); let name = entry.name().to_string(); let new_path = entry.path_mut().join(name); - std::fs::rename(original_path, new_path) - .map_err(|_| CollectionLoaderError::Rename("failed to rename collection".into()))?; + std::fs::rename(original_path, new_path).map_err(|_| Error::Rename("failed to rename collection".into()))?; Ok(()) } @@ -126,8 +125,7 @@ pub fn edit_collection(name: String, item_idx: usize, config: &Rc Result<()> { let collection_meta = hac_store::collection_meta::remove_collection_meta(file_name, |meta| meta); - std::fs::remove_file(collection_meta.path()) - .map_err(|_| CollectionLoaderError::Remove("failed to remove collection".into()))?; + std::fs::remove_file(collection_meta.path()).map_err(|_| Error::Remove("failed to remove collection".into()))?; Ok(()) } @@ -142,7 +140,7 @@ pub fn delete_collection(file_name: String, config: &Rc Result<()> { set_watcher(); let entries = std::fs::read_dir(super::collections_dir()) - .map_err(|_| CollectionLoaderError::ReadDir("failed to read collections directory".into()))? + .map_err(|_| Error::ReadDir("failed to read collections directory".into()))? .flatten(); let mut collections = vec![]; for entry in entries { @@ -150,10 +148,10 @@ pub fn get_collections_metadata() -> Result<()> { let path = entry.path(); let metadata = entry .metadata() - .map_err(|_| CollectionLoaderError::Read("failed to read collection metadata".into()))?; + .map_err(|_| Error::Read("failed to read collection metadata".into()))?; let modified = metadata .modified() - .map_err(|_| CollectionLoaderError::Read("failed to read collection metadata".into()))? + .map_err(|_| Error::Read("failed to read collection metadata".into()))? .into(); let size = metadata.len(); collections.push(CollectionMeta::new(name, path, size, modified)); diff --git a/hac-store/Cargo.toml b/hac-store/Cargo.toml index 48119a3..f7e4d2f 100644 --- a/hac-store/Cargo.toml +++ b/hac-store/Cargo.toml @@ -6,3 +6,4 @@ edition = "2021" [dependencies] serde.workspace = true chrono = "0.4.38" +tracing.workspace = true diff --git a/hac-store/src/collection.rs b/hac-store/src/collection.rs index cec0920..38758d0 100644 --- a/hac-store/src/collection.rs +++ b/hac-store/src/collection.rs @@ -1,4 +1,5 @@ use crate::slab::{Key, Slab}; +use crate::HAC_STORE; #[derive(Debug)] pub enum ReqTree { @@ -64,3 +65,21 @@ pub struct Folder { pub name: String, pub requests: Vec, } + +pub fn set_collection(collection: Option) { + HAC_STORE.with_borrow_mut(|store| store.collection = collection); +} + +pub fn get_request(key: Key, f: F) -> Option +where + F: FnOnce(&Request) -> Option, +{ + HAC_STORE.with_borrow(|store| { + if let Some(ref collection) = store.collection { + let request = collection.root_requests.get(key); + return f(request); + }; + + None + }) +} diff --git a/hac-store/src/collection_meta.rs b/hac-store/src/collection_meta.rs index 677bb98..b827197 100644 --- a/hac-store/src/collection_meta.rs +++ b/hac-store/src/collection_meta.rs @@ -179,9 +179,9 @@ pub fn sort_collection_meta(sorting_kind: CollectionMetaSorting) { }) } -pub fn collections_meta(f: F) +pub fn collections_meta(f: F) -> R where - F: FnOnce(&[CollectionMeta]), + F: FnOnce(&[CollectionMeta]) -> R, { HAC_STORE.with_borrow(|store| f(&store.collections_meta)) } From 65a433827eb11d250bcfb96d0a04b4357e796a9b Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Mon, 30 Sep 2024 18:13:25 -0300 Subject: [PATCH 13/23] feat: initial rendering of items on the sidebar --- hac-client/src/icons.rs | 6 + hac-client/src/lib.rs | 1 + .../collection_viewer/collection_viewer.rs | 306 +++---- hac-client/src/pages/collection_viewer/mod.rs | 2 +- .../pages/collection_viewer/request_uri.rs | 101 ++- .../src/pages/collection_viewer/sidebar.rs | 760 ++++++++---------- hac-config/src/config.rs | 2 + hac-config/src/default_config.rs | 2 + .../src/collection_loader/json_collection.rs | 2 +- .../src/collection_loader/json_loader.rs | 46 +- hac-store/src/collection.rs | 187 ++++- hac-store/src/slab.rs | 24 +- 12 files changed, 767 insertions(+), 672 deletions(-) create mode 100644 hac-client/src/icons.rs diff --git a/hac-client/src/icons.rs b/hac-client/src/icons.rs new file mode 100644 index 0000000..dd4ca93 --- /dev/null +++ b/hac-client/src/icons.rs @@ -0,0 +1,6 @@ +pub struct Icons; + +#[rustfmt::skip] +impl Icons { + pub const FOLDER: char = '\u{e5ff}'; //  +} diff --git a/hac-client/src/lib.rs b/hac-client/src/lib.rs index ac765f3..1314b93 100755 --- a/hac-client/src/lib.rs +++ b/hac-client/src/lib.rs @@ -2,6 +2,7 @@ pub mod app; mod ascii; mod components; pub mod event_pool; +mod icons; pub mod pages; mod renderable; mod router; diff --git a/hac-client/src/pages/collection_viewer/collection_viewer.rs b/hac-client/src/pages/collection_viewer/collection_viewer.rs index b640402..4be64ef 100755 --- a/hac-client/src/pages/collection_viewer/collection_viewer.rs +++ b/hac-client/src/pages/collection_viewer/collection_viewer.rs @@ -7,14 +7,14 @@ use std::sync::mpsc::Sender; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::command::Command; use hac_core::net::request_manager::Response; -use hac_store::slab::Key; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::Stylize; use ratatui::widgets::{Block, Clear}; use ratatui::Frame; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; -use super::request_uri::RequestUri; +use super::request_uri::{RequestUri, RequestUriEvent}; +use super::sidebar::{Sidebar, SidebarEvent}; use crate::pages::collection_list::CollectionListData; //use crate::pages::collection_viewer::collection_store::{CollectionStore, CollectionStoreAction}; //use crate::pages::collection_viewer::request_editor::{RequestEditor, RequestEditorEvent}; @@ -79,13 +79,13 @@ impl PaneFocus { #[derive(Debug)] pub struct CollectionViewer { - hovered_request: Key, - selected_request: Option, + focus: PaneFocus, + selection: Option, request_uri: RequestUri, + sidebar: Sidebar, //response_viewer: ResponseViewer<'cv>, //request_editor: RequestEditor<'cv>, - //sidebar: Sidebar<'cv>, colors: HacColors, config: HacConfig, layout: ExplorerLayout, @@ -102,22 +102,21 @@ impl CollectionViewer { let layout = build_layout(size); let (request_tx, response_rx) = unbounded_channel::(); - //let sidebar = sidebar::Sidebar::new(colors, collection_store.clone()); // //let request_editor = RequestEditor::new(colors, config, collection_store.clone(), layout.req_editor); // //let response_viewer = ResponseViewer::new(colors, collection_store.clone(), None, layout.response_preview); - - let request_uri = RequestUri::new(colors.clone(), layout.req_uri, Some(0)); + let mut sidebar = Sidebar::new(colors.clone(), config.clone()); + sidebar.focus(); CollectionViewer { - request_uri, - hovered_request: 0, - selected_request: Some(0), + focus: PaneFocus::Sidebar, + selection: None, + request_uri: RequestUri::new(colors.clone(), layout.req_uri), + sidebar, //request_editor, //response_viewer, - //sidebar, colors, layout, config, @@ -146,16 +145,6 @@ impl CollectionViewer { // self.request_uri = RequestUri::new(self.colors, self.collection_store.clone(), self.layout.req_uri); //} - //fn focus_next(&mut self) { - // let next_pane = self.collection_store.borrow().get_focused_pane().next(); - // self.update_focus(next_pane); - //} - // - //fn focus_prev(&mut self) { - // let prev_pane = self.collection_store.borrow().get_focused_pane().prev(); - // self.update_focus(prev_pane); - //} - // collect all pending responses from the channel. Here, I don't see a way we // may have more than one response on this channel at any point, but it shouldn't matter // if we have, so we can drain all the responses and update accordingly @@ -249,17 +238,70 @@ impl CollectionViewer { //}); //} - //fn update_selection(&mut self, pane_to_select: Option) { - // self.collection_store - // .borrow_mut() - // .dispatch(CollectionStoreAction::SetSelectedPane(pane_to_select)); - //} - // - //fn update_focus(&mut self, pane_to_focus: PaneFocus) { - // self.collection_store - // .borrow_mut() - // .dispatch(CollectionStoreAction::SetFocusedPane(pane_to_focus)); - //} + /// updating selection will always also update the focus, as we cannot + /// select something that isn't also focused, unless pane_to_select is + /// None, in which case we won't change focus at all + #[inline] + fn update_selection(&mut self, pane_to_select: Option) { + if hac_store::collection::is_empty() { + return; + } + + match self.selection { + Some(PaneFocus::Sidebar) => self.sidebar.deselect(), + Some(PaneFocus::ReqUri) => self.request_uri.deselect(), + Some(PaneFocus::Preview) => {} + Some(PaneFocus::Editor) => {} + None => {} + } + match pane_to_select { + Some(pane @ PaneFocus::Sidebar) => { + self.sidebar.select(); + self.focus = pane; + } + Some(pane @ PaneFocus::ReqUri) => { + self.request_uri.select(); + self.focus = pane; + } + Some(pane @ PaneFocus::Preview) => { + self.focus = pane; + } + Some(pane @ PaneFocus::Editor) => { + self.focus = pane; + } + None => {} + } + self.selection = pane_to_select; + } + + #[inline] + fn update_focus(&mut self, pane_to_focus: PaneFocus) { + match self.focus { + PaneFocus::Sidebar => self.sidebar.blur(), + PaneFocus::ReqUri => self.request_uri.blur(), + PaneFocus::Preview => {} + PaneFocus::Editor => {} + } + match pane_to_focus { + PaneFocus::Sidebar => self.sidebar.focus(), + PaneFocus::ReqUri => self.request_uri.focus(), + PaneFocus::Preview => {} + PaneFocus::Editor => {} + } + self.focus = pane_to_focus; + } + + #[inline] + fn focus_next(&mut self) { + let next = self.focus.next(); + self.update_focus(next); + } + + #[inline] + fn focus_prev(&mut self) { + let prev = self.focus.prev(); + self.update_focus(prev); + } } impl Renderable for CollectionViewer { @@ -268,15 +310,14 @@ impl Renderable for CollectionViewer { #[tracing::instrument(skip_all)] fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { - // we redraw the background to prevent weird "transparent" spots when popups are - // cleared from the buffer frame.render_widget(Clear, size); frame.render_widget(Block::default().bg(self.colors.primary.background), size); //self.drain_responses_channel(); + self.request_uri.draw(frame, self.layout.req_uri)?; + self.sidebar.draw(frame, self.layout.sidebar)?; - //self.sidebar.draw(frame, self.layout.sidebar)?; //self.response_viewer.draw(frame, self.layout.response_preview)?; //self.request_editor.draw(frame, self.layout.req_editor)?; @@ -376,118 +417,89 @@ impl Eventful for CollectionViewer { return Ok(Some(Command::Quit)); } - //if let ( - // None, - // KeyEvent { - // code: KeyCode::Char('c'), - // modifiers: KeyModifiers::CONTROL, - // .. - // }, - //) = (self.collection_store.borrow().get_selected_pane(), key_event) - //{ - // return Ok(Some(Command::Quit)); - //} - - //if self.collection_store.borrow().get_selected_pane().is_none() { - // match key_event.code { - // KeyCode::Char('r') => { - // self.update_focus(PaneFocus::Sidebar); - // self.update_selection(Some(PaneFocus::Sidebar)); - // } - // KeyCode::Char('u') => { - // self.update_focus(PaneFocus::ReqUri); - // self.update_selection(Some(PaneFocus::ReqUri)); - // } - // KeyCode::Char('p') => { - // self.update_focus(PaneFocus::Preview); - // self.update_selection(Some(PaneFocus::Preview)); - // } - // KeyCode::Char('e') => { - // self.update_focus(PaneFocus::Editor); - // self.update_selection(Some(PaneFocus::Editor)); - // } - // KeyCode::Tab => self.focus_next(), - // KeyCode::BackTab => self.focus_prev(), - // KeyCode::Enter => { - // let curr_pane = self.collection_store.borrow().get_focused_pane(); - // self.update_selection(Some(curr_pane)); - // } - // _ => {} - // } - // return Ok(None); - //} + if self.selection.is_none() { + match key_event.code { + KeyCode::Char('r') => self.update_selection(Some(PaneFocus::Sidebar)), + KeyCode::Char('u') => self.update_selection(Some(PaneFocus::ReqUri)), + KeyCode::Char('p') => self.update_selection(Some(PaneFocus::Preview)), + KeyCode::Char('e') => self.update_selection(Some(PaneFocus::Editor)), + KeyCode::Tab => self.focus_next(), + KeyCode::BackTab => self.focus_prev(), + KeyCode::Enter => self.update_selection(Some(self.focus)), + _ => {} + } + return Ok(None); + } - //let selected_pane = self.collection_store.borrow().get_selected_pane(); - //if let Some(curr_pane) = selected_pane { - // match curr_pane { - // PaneFocus::Sidebar => match self.sidebar.handle_key_event(key_event)? { - // Some(SidebarEvent::CreateRequest) => self - // .collection_store - // .borrow_mut() - // .push_overlay(CollectionViewerOverlay::CreateRequest), - // Some(SidebarEvent::EditRequest) => self - // .collection_store - // .borrow_mut() - // .push_overlay(CollectionViewerOverlay::EditRequest), - // Some(SidebarEvent::EditDirectory) => self - // .collection_store - // .borrow_mut() - // .push_overlay(CollectionViewerOverlay::EditDirectory), - // Some(SidebarEvent::CreateDirectory) => self - // .collection_store - // .borrow_mut() - // .push_overlay(CollectionViewerOverlay::CreateDirectory), - // Some(SidebarEvent::DeleteItem(item_id)) => self - // .collection_store - // .borrow_mut() - // .push_overlay(CollectionViewerOverlay::DeleteSidebarItem(item_id)), - // Some(SidebarEvent::RemoveSelection) => self.update_selection(None), - // Some(SidebarEvent::SelectNext) => { - // self.update_selection(None); - // self.focus_next(); - // } - // Some(SidebarEvent::SelectPrev) => { - // self.update_selection(None); - // self.focus_prev(); - // } - // Some(SidebarEvent::SyncCollection) => self.sync_collection_changes(), - // Some(SidebarEvent::Quit) => return Ok(Some(Command::Quit)), - // Some(SidebarEvent::RebuildView) => self.rebuild_everything(), - // // when theres no event we do nothing - // None => {} - // }, - // PaneFocus::ReqUri => match self.request_uri.handle_key_event(key_event)? { - // Some(RequestUriEvent::Quit) => return Ok(Some(Command::Quit)), - // Some(RequestUriEvent::SendRequest) => hac_core::net::handle_request( - // self.collection_store.borrow().get_selected_request().as_ref().unwrap(), - // self.request_tx.clone(), - // ), - // Some(RequestUriEvent::RemoveSelection) => self.update_selection(None), - // Some(RequestUriEvent::SelectNext) => { - // self.update_selection(None); - // self.focus_next(); - // } - // Some(RequestUriEvent::SelectPrev) => { - // self.update_selection(None); - // self.focus_prev(); - // } - // // when theres no event we do nothing - // None => {} - // }, - // PaneFocus::Preview => match self.response_viewer.handle_key_event(key_event)? { - // Some(ResponseViewerEvent::RemoveSelection) => self.update_selection(None), - // Some(ResponseViewerEvent::Quit) => return Ok(Some(Command::Quit)), - // // when theres no event we do nothing - // None => {} - // }, - // PaneFocus::Editor => match self.request_editor.handle_key_event(key_event)? { - // Some(RequestEditorEvent::RemoveSelection) => self.update_selection(None), - // Some(RequestEditorEvent::Quit) => return Ok(Some(Command::Quit)), - // // when theres no event we do nothing - // None => {} - // }, - // }; - //} + if let Some(pane) = self.selection { + match pane { + PaneFocus::ReqUri => match self.request_uri.handle_key_event(key_event)? { + Some(RequestUriEvent::Quit) => return Ok(Some(Command::Quit)), + Some(RequestUriEvent::RemoveSelection) => self.update_selection(None), + Some(RequestUriEvent::SendRequest) => todo!(), + Some(RequestUriEvent::SelectNext) => { + self.update_selection(None); + self.focus_next(); + } + Some(RequestUriEvent::SelectPrev) => { + self.update_selection(None); + self.focus_prev(); + } + None => {} + }, + PaneFocus::Sidebar => match self.sidebar.handle_key_event(key_event)? { + Some(SidebarEvent::SelectNext) => { + self.update_selection(None); + self.focus_next(); + } + Some(SidebarEvent::SelectPrev) => { + self.update_selection(None); + self.focus_prev(); + } + Some(SidebarEvent::RemoveSelection) => self.update_selection(None), + Some(e) => tracing::trace!("{e:?}"), + None => (), + //Some(SidebarEvent::CreateRequest) => self + // .collection_store + // .borrow_mut() + // .push_overlay(CollectionViewerOverlay::CreateRequest), + //Some(SidebarEvent::EditRequest) => self + // .collection_store + // .borrow_mut() + // .push_overlay(CollectionViewerOverlay::EditRequest), + //Some(SidebarEvent::EditDirectory) => self + // .collection_store + // .borrow_mut() + // .push_overlay(CollectionViewerOverlay::EditDirectory), + //Some(SidebarEvent::CreateDirectory) => self + // .collection_store + // .borrow_mut() + // .push_overlay(CollectionViewerOverlay::CreateDirectory), + //Some(SidebarEvent::DeleteItem(item_id)) => self + // .collection_store + // .borrow_mut() + // .push_overlay(CollectionViewerOverlay::DeleteSidebarItem(item_id)), + //Some(SidebarEvent::SyncCollection) => self.sync_collection_changes(), + //Some(SidebarEvent::Quit) => return Ok(Some(Command::Quit)), + //Some(SidebarEvent::RebuildView) => self.rebuild_everything(), + //// when theres no event we do nothing + //None => {} + }, + // PaneFocus::Preview => match self.response_viewer.handle_key_event(key_event)? { + // Some(ResponseViewerEvent::RemoveSelection) => self.update_selection(None), + // Some(ResponseViewerEvent::Quit) => return Ok(Some(Command::Quit)), + // // when theres no event we do nothing + // None => {} + // }, + // PaneFocus::Editor => match self.request_editor.handle_key_event(key_event)? { + // Some(RequestEditorEvent::RemoveSelection) => self.update_selection(None), + // Some(RequestEditorEvent::Quit) => return Ok(Some(Command::Quit)), + // // when theres no event we do nothing + // None => {} + // }, + _ => todo!(), + }; + } Ok(None) } diff --git a/hac-client/src/pages/collection_viewer/mod.rs b/hac-client/src/pages/collection_viewer/mod.rs index f4c6a1d..c3b042e 100755 --- a/hac-client/src/pages/collection_viewer/mod.rs +++ b/hac-client/src/pages/collection_viewer/mod.rs @@ -3,6 +3,6 @@ pub mod collection_viewer; //mod request_editor; mod request_uri; //mod response_viewer; -//mod sidebar; +mod sidebar; pub use collection_viewer::CollectionViewer; diff --git a/hac-client/src/pages/collection_viewer/request_uri.rs b/hac-client/src/pages/collection_viewer/request_uri.rs index 9935899..7db865a 100755 --- a/hac-client/src/pages/collection_viewer/request_uri.rs +++ b/hac-client/src/pages/collection_viewer/request_uri.rs @@ -1,5 +1,4 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use hac_store::slab::Key; use ratatui::layout::Rect; use ratatui::style::{Style, Stylize}; use ratatui::widgets::{Block, Borders, Paragraph}; @@ -29,21 +28,35 @@ pub enum RequestUriEvent { pub struct RequestUri { colors: HacColors, size: Rect, - selected_request: Option, - is_focused: bool, - is_selected: bool, + focused: bool, + selected: bool, } impl RequestUri { - pub fn new(colors: HacColors, size: Rect, selected_request: Option) -> Self { + pub fn new(colors: HacColors, size: Rect) -> Self { Self { colors, size, - selected_request, - is_focused: true, - is_selected: true, + focused: false, + selected: false, } } + + pub fn focus(&mut self) { + self.focused = true; + } + + pub fn blur(&mut self) { + self.focused = false; + } + + pub fn select(&mut self) { + self.selected = true; + } + + pub fn deselect(&mut self) { + self.selected = false; + } } impl Renderable for RequestUri { @@ -57,16 +70,14 @@ impl Renderable for RequestUri { } fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { - let block_border = match (self.is_focused, self.is_selected) { + let block_border = match (self.focused, self.selected) { (true, false) => Style::default().fg(self.colors.bright.blue), (true, true) => Style::default().fg(self.colors.normal.red), (false, _) => Style::default().fg(self.colors.bright.black), }; - let uri = self - .selected_request - .and_then(|key| hac_store::collection::get_request(key, |req| Some(req.uri.to_string()))) - .unwrap_or_default(); + let uri = hac_store::collection::get_selected_request(|req| Some(req.uri.to_string())).unwrap_or_default(); + let len = uri.chars().count() as u16; let uri = Paragraph::new(uri) .fg(self.colors.normal.white) @@ -74,6 +85,11 @@ impl Renderable for RequestUri { frame.render_widget(uri, size); + if self.selected { + let x = self.size.x + len + 1; + frame.set_cursor(x, self.size.y + 1); + } + Ok(()) } } @@ -82,49 +98,22 @@ impl Eventful for RequestUri { type Result = RequestUriEvent; fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - //let is_selected = self - // .collection_store - // .borrow() - // .get_selected_pane() - // .is_some_and(|pane| pane.eq(&PaneFocus::ReqUri)); - // - //assert!( - // is_selected, - // "handled an event to the request uri while it was not selected" - //); - // - //if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - // return Ok(Some(RequestUriEvent::Quit)); - //} - // - //match key_event.code { - // KeyCode::Esc => return Ok(Some(RequestUriEvent::RemoveSelection)), - // KeyCode::Tab => return Ok(Some(RequestUriEvent::SelectNext)), - // KeyCode::BackTab => return Ok(Some(RequestUriEvent::SelectPrev)), - // KeyCode::Char(c) => { - // if let Some(req) = self.collection_store.borrow().get_selected_request().as_mut() { - // req.write().unwrap().uri.push(c); - // } - // } - // KeyCode::Backspace => { - // if let Some(req) = self.collection_store.borrow().get_selected_request().as_mut() { - // req.write().unwrap().uri.pop(); - // } - // } - // KeyCode::Enter => { - // let mut store = self.collection_store.borrow_mut(); - // if store - // .get_selected_request() - // .as_ref() - // .is_some_and(|_| !store.has_pending_request()) - // { - // store.dispatch(CollectionStoreAction::SetPendingRequest(true)); - // return Ok(Some(RequestUriEvent::SendRequest)); - // } - // } - // _ => {} - //} - // + assert!(self.selected); + + if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { + return Ok(Some(RequestUriEvent::Quit)); + } + + match key_event.code { + KeyCode::Esc => return Ok(Some(RequestUriEvent::RemoveSelection)), + KeyCode::Tab => return Ok(Some(RequestUriEvent::SelectNext)), + KeyCode::BackTab => return Ok(Some(RequestUriEvent::SelectPrev)), + KeyCode::Char(c) => hac_store::collection::get_selected_request_mut(|req| req.uri.push(c)), + KeyCode::Backspace => hac_store::collection::get_selected_request_mut(|req| _ = req.uri.pop()), + KeyCode::Enter => return Ok(Some(RequestUriEvent::SendRequest)), + _ => {} + }; + Ok(None) } } diff --git a/hac-client/src/pages/collection_viewer/sidebar.rs b/hac-client/src/pages/collection_viewer/sidebar.rs index e34ac49..37d206a 100755 --- a/hac-client/src/pages/collection_viewer/sidebar.rs +++ b/hac-client/src/pages/collection_viewer/sidebar.rs @@ -1,35 +1,25 @@ -mod create_directory_form; -mod create_request_form; -mod delete_item_prompt; -mod directory_form; -mod edit_directory_form; -mod edit_request_form; -mod request_form; -mod select_request_parent; - -use hac_core::collection::types::{Request, RequestKind, RequestMethod}; - -use super::sidebar::delete_item_prompt::{DeleteItemPrompt, DeleteItemPromptEvent}; -use super::sidebar::directory_form::{DirectoryForm, DirectoryFormEvent}; -use super::sidebar::directory_form::{DirectoryFormCreate, DirectoryFormEdit}; -use super::sidebar::request_form::{RequestForm, RequestFormEvent}; -use super::sidebar::request_form::{RequestFormCreate, RequestFormEdit}; -use crate::pages::collection_viewer::collection_store::{CollectionStore, CollectionStoreAction}; -use crate::pages::collection_viewer::collection_viewer::{CollectionViewerOverlay, PaneFocus}; -use crate::pages::{Eventful, Renderable}; - -use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; -use std::sync::{Arc, RwLock}; +// mod create_directory_form; +// mod create_request_form; +// mod delete_item_prompt; +// mod directory_form; +// mod edit_directory_form; +// mod edit_request_form; +// mod request_form; +// mod select_request_parent; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use hac_store::collection::{EntryStatus, ReqMethod, ReqTreeNode, WhichSlab}; use ratatui::layout::Rect; use ratatui::style::{Style, Styled, Stylize}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Paragraph}; use ratatui::Frame; +use crate::icons::Icons; +use crate::pages::collection_viewer::collection_viewer::CollectionViewerOverlay; +use crate::renderable::{Eventful, Renderable}; +use crate::{HacColors, HacConfig}; + /// set of events Sidebar can emit to the caller when handling events. #[derive(Debug, PartialEq, Eq, Clone)] pub enum SidebarEvent { @@ -66,476 +56,366 @@ pub enum SidebarEvent { } #[derive(Debug)] -enum DirectoryFormVariant<'sbar> { - Create(DirectoryForm<'sbar, DirectoryFormCreate>), - Edit(DirectoryForm<'sbar, DirectoryFormEdit>), +pub struct Sidebar { + colors: HacColors, + config: HacConfig, + selected: bool, + focused: bool, + //request_form: RequestFormVariant<'sbar>, + //directory_form: DirectoryFormVariant<'sbar>, + //delete_item_prompt: DeleteItemPrompt<'sbar>, } -impl DirectoryFormVariant<'_> { - pub fn inner(&mut self) -> &mut dyn DirectoryFormTrait { - match self { - DirectoryFormVariant::Create(form) => form, - DirectoryFormVariant::Edit(form) => form, - } - } -} - -#[derive(Debug)] -enum RequestFormVariant<'sbar> { - Create(RequestForm<'sbar, RequestFormCreate>), - Edit(RequestForm<'sbar, RequestFormEdit>), -} +impl Sidebar { + pub fn new(colors: HacColors, config: HacConfig) -> Self { + let mut sidebar = Self { + colors, + config, + selected: false, + focused: false, + //request_form: RequestFormVariant::Create(RequestForm::::new( + // colors, + // collection_store.clone(), + //)), + //directory_form: DirectoryFormVariant::Create(DirectoryForm::::new( + // colors, + // collection_store.clone(), + //)), + //delete_item_prompt: DeleteItemPrompt::new(colors, collection_store.clone()), + }; -/// this is just a helper trait to be able to return the inner reference of the form -/// from the enum as we cannot return it like: -/// `&mut dyn Renderable + Eventful;` -pub trait DirectoryFormTrait: Renderable + Eventful {} + sidebar.rebuild_tree_view(); -/// this is just a helper trait to be able to return the inner reference of the form -/// from the enum as we cannot return it like: -/// `&mut dyn Renderable + Eventful;` -#[allow(unused_variables)] -pub trait RequestFormTrait: Renderable + Eventful { - fn draw_overlay( - &mut self, - frame: &mut Frame, - overlay: CollectionViewerOverlay, - ) -> anyhow::Result<()> { - Ok(()) + sidebar } -} -impl RequestFormVariant<'_> { - pub fn inner(&mut self) -> &mut dyn RequestFormTrait { - match self { - RequestFormVariant::Create(form) => form, - RequestFormVariant::Edit(form) => form, - } + pub fn focus(&mut self) { + self.focused = true; } -} - -#[derive(Debug)] -pub struct Sidebar<'sbar> { - colors: &'sbar hac_colors::Colors, - lines: Vec>, - collection_store: Rc>, - request_form: RequestFormVariant<'sbar>, - directory_form: DirectoryFormVariant<'sbar>, - delete_item_prompt: DeleteItemPrompt<'sbar>, -} -impl<'sbar> Sidebar<'sbar> { - pub fn new( - colors: &'sbar hac_colors::Colors, - collection_store: Rc>, - ) -> Self { - let mut sidebar = Self { - colors, - request_form: RequestFormVariant::Create(RequestForm::::new( - colors, - collection_store.clone(), - )), - directory_form: DirectoryFormVariant::Create( - DirectoryForm::::new(colors, collection_store.clone()), - ), - delete_item_prompt: DeleteItemPrompt::new(colors, collection_store.clone()), - lines: vec![], - collection_store, - }; + pub fn blur(&mut self) { + self.focused = false; + } - sidebar.rebuild_tree_view(); + pub fn select(&mut self) { + self.selected = true; + } - sidebar + pub fn deselect(&mut self) { + self.selected = false; } pub fn rebuild_tree_view(&mut self) { - let mut collection_store = self.collection_store.borrow_mut(); - self.lines = build_lines( - collection_store.get_requests(), - 0, - collection_store.get_selected_request(), - collection_store.get_hovered_request(), - collection_store.get_dirs_expanded().unwrap().clone(), - self.colors, - ); + //let mut collection_store = self.collection_store.borrow_mut(); + //self.lines = build_lines( + // collection_store.get_requests(), + // 0, + // collection_store.get_selected_request(), + // collection_store.get_hovered_request(), + // collection_store.get_dirs_expanded().unwrap().clone(), + // self.colors, + //); } - pub fn draw_overlay( - &mut self, - frame: &mut Frame, - overlay: CollectionViewerOverlay, - ) -> anyhow::Result<()> { - match overlay { - CollectionViewerOverlay::CreateRequest => { - self.request_form.inner().draw(frame, frame.size())?; - } - CollectionViewerOverlay::EditRequest => { - self.request_form.inner().draw(frame, frame.size())?; - } - CollectionViewerOverlay::SelectParentDir => { - self.request_form.inner().draw_overlay(frame, overlay)?; - } - CollectionViewerOverlay::CreateDirectory => { - self.directory_form.inner().draw(frame, frame.size())?; - } - CollectionViewerOverlay::EditDirectory => { - self.directory_form.inner().draw(frame, frame.size())?; - } - CollectionViewerOverlay::DeleteSidebarItem(_) => { - self.delete_item_prompt.draw(frame, frame.size())?; - } - _ => {} - }; + pub fn draw_overlay(&mut self, _frame: &mut Frame, _overlay: CollectionViewerOverlay) -> anyhow::Result<()> { + //match overlay { + // CollectionViewerOverlay::CreateRequest => { + // self.request_form.inner().draw(frame, frame.size())?; + // } + // CollectionViewerOverlay::EditRequest => { + // self.request_form.inner().draw(frame, frame.size())?; + // } + // CollectionViewerOverlay::SelectParentDir => { + // self.request_form.inner().draw_overlay(frame, overlay)?; + // } + // CollectionViewerOverlay::CreateDirectory => { + // self.directory_form.inner().draw(frame, frame.size())?; + // } + // CollectionViewerOverlay::EditDirectory => { + // self.directory_form.inner().draw(frame, frame.size())?; + // } + // CollectionViewerOverlay::DeleteSidebarItem(_) => { + // self.delete_item_prompt.draw(frame, frame.size())?; + // } + // _ => {} + //}; Ok(()) } } -impl<'sbar> Renderable for Sidebar<'sbar> { +impl Renderable for Sidebar { + type Input = (); + type Output = (); + fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { - let is_focused = self - .collection_store - .borrow() - .get_focused_pane() - .eq(&PaneFocus::Sidebar); - let is_selected = self - .collection_store - .borrow() - .get_selected_pane() - .is_some_and(|pane| pane.eq(&PaneFocus::Sidebar)); + let layout = hac_store::collection::tree_layout(); + let mut lines = vec![]; + layout.nodes.into_iter().for_each(|node| match node { + ReqTreeNode::Req(key) => hac_store::collection::get_root_request(key, |req, status| { + let name = req.name.clone(); + let method = &req.method; + + let style = match status { + EntryStatus::None => Style::default().fg(self.colors.normal.white), + EntryStatus::Hovered => Style::default() + .fg(self.colors.normal.white) + .bg(self.colors.primary.hover), + EntryStatus::Selected => Style::default() + .fg(self.colors.normal.white) + .bg(self.colors.normal.blue), + EntryStatus::Both => Style::default() + .fg(self.colors.normal.yellow) + .bg(self.colors.normal.blue), + }; - let mut requests_size = Rect::new(size.x + 1, size.y, size.width.saturating_sub(2), 1); + lines.push(Line::from(vec![ + colored_method(method, &self.colors), + name.set_style(style), + ])); + }), + ReqTreeNode::Folder(folder_key, requests) => { + let mut folder_name = String::default(); + hac_store::collection::get_folder(folder_key, |folder| folder_name.push_str(&folder.name)); + let folder_name = if self.config.borrow().enable_icons { + format!("{} {}", Icons::FOLDER, folder_name) + } else { + folder_name + }; + lines.push(Line::from(folder_name).bold().fg(self.colors.normal.yellow)); + for request in requests { + hac_store::collection::get_request(request, |req| { + let name = req.name.clone(); + let method = &req.method; + lines.push(Line::from(vec![ + " ".repeat(self.config.borrow().tab_size).into(), + colored_method(method, &self.colors), + name.fg(self.colors.normal.white), + ])); + }); + } + } + }); - let block_border = match (is_focused, is_selected) { + let block_border = match (self.focused, self.selected) { (true, false) => Style::default().fg(self.colors.bright.blue), (true, true) => Style::default().fg(self.colors.normal.red), (false, _) => Style::default().fg(self.colors.bright.black), }; - - let block = Block::default() - .borders(Borders::ALL) - .title(vec![ - "R".fg(self.colors.normal.red).bold(), - "equests".fg(self.colors.bright.black), - ]) - .border_style(block_border); - + let block = Block::default().borders(Borders::ALL).border_style(block_border); frame.render_widget(block, size); - self.lines.clone().into_iter().for_each(|req| { - requests_size.y += 1; - frame.render_widget(req, requests_size); - }); + let size = Rect::new(size.x + 1, size.y + 1, size.width - 2, size.height - 2); + frame.render_widget(Paragraph::new(lines), size); Ok(()) } + fn data(&self, _requester: u8) -> Self::Output {} + fn resize(&mut self, _new_size: Rect) {} } -impl<'a> Eventful for Sidebar<'a> { +impl Eventful for Sidebar { type Result = SidebarEvent; fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - let is_selected = self - .collection_store - .borrow() - .get_selected_pane() - .is_some_and(|pane| pane.eq(&PaneFocus::Sidebar)); - assert!( - is_selected, - "handled an event to the sidebar while it was not selected" - ); - - let overlay = self.collection_store.borrow_mut().peek_overlay(); - - match overlay { - CollectionViewerOverlay::CreateRequest => { - match self.request_form.inner().handle_key_event(key_event)? { - Some(RequestFormEvent::Confirm) => { - let mut store = self.collection_store.borrow_mut(); - store.pop_overlay(); - drop(store); - self.rebuild_tree_view(); - return Ok(Some(SidebarEvent::SyncCollection)); - } - Some(RequestFormEvent::Cancel) => { - let mut store = self.collection_store.borrow_mut(); - store.pop_overlay(); - drop(store); - self.rebuild_tree_view(); - return Ok(None); - } - None => return Ok(None), - } - } - CollectionViewerOverlay::SelectParentDir => { - let result = self.request_form.inner().handle_key_event(key_event)?; - assert!( - result.is_none(), - "should never return an event when selecting parent dir" - ); - return Ok(None); - } - CollectionViewerOverlay::CreateDirectory => { - match self.directory_form.inner().handle_key_event(key_event)? { - Some(DirectoryFormEvent::Confirm) => { - let mut store = self.collection_store.borrow_mut(); - store.pop_overlay(); - drop(store); - self.rebuild_tree_view(); - return Ok(Some(SidebarEvent::SyncCollection)); - } - Some(DirectoryFormEvent::Cancel) => { - let mut store = self.collection_store.borrow_mut(); - store.pop_overlay(); - drop(store); - self.rebuild_tree_view(); - return Ok(None); - } - None => return Ok(None), - } - } - CollectionViewerOverlay::EditDirectory => { - match self.directory_form.inner().handle_key_event(key_event)? { - Some(DirectoryFormEvent::Confirm) => { - let mut store = self.collection_store.borrow_mut(); - store.pop_overlay(); - drop(store); - self.rebuild_tree_view(); - return Ok(Some(SidebarEvent::SyncCollection)); - } - Some(DirectoryFormEvent::Cancel) => { - let mut store = self.collection_store.borrow_mut(); - store.pop_overlay(); - drop(store); - self.rebuild_tree_view(); - return Ok(None); - } - None => return Ok(None), - } - } - CollectionViewerOverlay::EditRequest => { - // when editing, we setup the form to display the current header information. - match self.request_form.inner().handle_key_event(key_event)? { - Some(RequestFormEvent::Confirm) => { - let mut store = self.collection_store.borrow_mut(); - store.pop_overlay(); - drop(store); - self.rebuild_tree_view(); - return Ok(Some(SidebarEvent::SyncCollection)); - } - Some(RequestFormEvent::Cancel) => { - let mut store = self.collection_store.borrow_mut(); - store.pop_overlay(); - drop(store); - self.rebuild_tree_view(); - return Ok(None); - } - None => return Ok(None), - } - } - CollectionViewerOverlay::DeleteSidebarItem(item_id) => { - match self.delete_item_prompt.handle_key_event(key_event)? { - Some(DeleteItemPromptEvent::Confirm) => { - let mut store = self.collection_store.borrow_mut(); - let changed_selection = store - .get_selected_request() - .is_some_and(|req| req.read().unwrap().id.eq(&item_id)); - store.remove_item(item_id); - store.pop_overlay(); - drop(store); - self.rebuild_tree_view(); - - if changed_selection { - return Ok(Some(SidebarEvent::RebuildView)); - } else { - return Ok(None); - } - } - Some(DeleteItemPromptEvent::Cancel) => { - let mut store = self.collection_store.borrow_mut(); - store.pop_overlay(); - drop(store); - self.rebuild_tree_view(); - return Ok(None); - } - None => return Ok(None), - } - } - _ => {} - }; - if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { return Ok(Some(SidebarEvent::Quit)); } - let mut store = self.collection_store.borrow_mut(); + //let is_selected = self + // .collection_store + // .borrow() + // .get_selected_pane() + // .is_some_and(|pane| pane.eq(&PaneFocus::Sidebar)); + //assert!(is_selected, "handled an event to the sidebar while it was not selected"); + // + //let overlay = self.collection_store.borrow_mut().peek_overlay(); + // + //match overlay { + // CollectionViewerOverlay::CreateRequest => match self.request_form.inner().handle_key_event(key_event)? { + // Some(RequestFormEvent::Confirm) => { + // let mut store = self.collection_store.borrow_mut(); + // store.pop_overlay(); + // drop(store); + // self.rebuild_tree_view(); + // return Ok(Some(SidebarEvent::SyncCollection)); + // } + // Some(RequestFormEvent::Cancel) => { + // let mut store = self.collection_store.borrow_mut(); + // store.pop_overlay(); + // drop(store); + // self.rebuild_tree_view(); + // return Ok(None); + // } + // None => return Ok(None), + // }, + // CollectionViewerOverlay::SelectParentDir => { + // let result = self.request_form.inner().handle_key_event(key_event)?; + // assert!( + // result.is_none(), + // "should never return an event when selecting parent dir" + // ); + // return Ok(None); + // } + // CollectionViewerOverlay::CreateDirectory => { + // match self.directory_form.inner().handle_key_event(key_event)? { + // Some(DirectoryFormEvent::Confirm) => { + // let mut store = self.collection_store.borrow_mut(); + // store.pop_overlay(); + // drop(store); + // self.rebuild_tree_view(); + // return Ok(Some(SidebarEvent::SyncCollection)); + // } + // Some(DirectoryFormEvent::Cancel) => { + // let mut store = self.collection_store.borrow_mut(); + // store.pop_overlay(); + // drop(store); + // self.rebuild_tree_view(); + // return Ok(None); + // } + // None => return Ok(None), + // } + // } + // CollectionViewerOverlay::EditDirectory => match self.directory_form.inner().handle_key_event(key_event)? { + // Some(DirectoryFormEvent::Confirm) => { + // let mut store = self.collection_store.borrow_mut(); + // store.pop_overlay(); + // drop(store); + // self.rebuild_tree_view(); + // return Ok(Some(SidebarEvent::SyncCollection)); + // } + // Some(DirectoryFormEvent::Cancel) => { + // let mut store = self.collection_store.borrow_mut(); + // store.pop_overlay(); + // drop(store); + // self.rebuild_tree_view(); + // return Ok(None); + // } + // None => return Ok(None), + // }, + // CollectionViewerOverlay::EditRequest => { + // // when editing, we setup the form to display the current header information. + // match self.request_form.inner().handle_key_event(key_event)? { + // Some(RequestFormEvent::Confirm) => { + // let mut store = self.collection_store.borrow_mut(); + // store.pop_overlay(); + // drop(store); + // self.rebuild_tree_view(); + // return Ok(Some(SidebarEvent::SyncCollection)); + // } + // Some(RequestFormEvent::Cancel) => { + // let mut store = self.collection_store.borrow_mut(); + // store.pop_overlay(); + // drop(store); + // self.rebuild_tree_view(); + // return Ok(None); + // } + // None => return Ok(None), + // } + // } + // CollectionViewerOverlay::DeleteSidebarItem(item_id) => { + // match self.delete_item_prompt.handle_key_event(key_event)? { + // Some(DeleteItemPromptEvent::Confirm) => { + // let mut store = self.collection_store.borrow_mut(); + // let changed_selection = store + // .get_selected_request() + // .is_some_and(|req| req.read().unwrap().id.eq(&item_id)); + // store.remove_item(item_id); + // store.pop_overlay(); + // drop(store); + // self.rebuild_tree_view(); + // + // if changed_selection { + // return Ok(Some(SidebarEvent::RebuildView)); + // } else { + // return Ok(None); + // } + // } + // Some(DeleteItemPromptEvent::Cancel) => { + // let mut store = self.collection_store.borrow_mut(); + // store.pop_overlay(); + // drop(store); + // self.rebuild_tree_view(); + // return Ok(None); + // } + // None => return Ok(None), + // } + // } + // _ => {} + //}; match key_event.code { - KeyCode::Enter => { - if store.get_requests().is_none() || store.get_hovered_request().is_none() { - return Ok(None); - } - - let request = store.find_hovered_request(); - match request { - RequestKind::Nested(_) => { - store.dispatch(CollectionStoreAction::ToggleDirectory(request.get_id())); - } - RequestKind::Single(req) => { - store.dispatch(CollectionStoreAction::SetSelectedRequest(Some(req))); - return Ok(Some(SidebarEvent::RebuildView)); - } - } - } - KeyCode::Char('j') | KeyCode::Down => store.dispatch(CollectionStoreAction::HoverNext), - KeyCode::Char('k') | KeyCode::Up => store.dispatch(CollectionStoreAction::HoverPrev), - KeyCode::Char('n') => { - self.request_form = - RequestFormVariant::Create(RequestForm::::new( - self.colors, - self.collection_store.clone(), - )); - return Ok(Some(SidebarEvent::CreateRequest)); - } - KeyCode::Char('e') => { - let hovered_request = store.find_hovered_request(); - drop(store); - match hovered_request { - RequestKind::Single(req) => { - self.request_form = - RequestFormVariant::Edit(RequestForm::::new( - self.colors, - self.collection_store.clone(), - req.clone(), - )); - return Ok(Some(SidebarEvent::EditRequest)); - } - RequestKind::Nested(dir) => { - self.directory_form = - DirectoryFormVariant::Edit(DirectoryForm::::new( - self.colors, - self.collection_store.clone(), - Some((dir.id.clone(), dir.name.clone())), - )); - return Ok(Some(SidebarEvent::EditDirectory)); - } - } - } KeyCode::Tab => return Ok(Some(SidebarEvent::SelectNext)), KeyCode::BackTab => return Ok(Some(SidebarEvent::SelectPrev)), - KeyCode::Char('D') => { - if let Some(item_id) = store.get_hovered_request() { - return Ok(Some(SidebarEvent::DeleteItem(item_id))); - } - } - KeyCode::Char('d') => return Ok(Some(SidebarEvent::CreateDirectory)), KeyCode::Esc => return Ok(Some(SidebarEvent::RemoveSelection)), - _ => {} + KeyCode::Char('j') | KeyCode::Down => hac_store::collection::hover_next(), + KeyCode::Char('k') | KeyCode::Up => hac_store::collection::hover_prev(), + _ => (), + // KeyCode::Enter => { + // if store.get_requests().is_none() || store.get_hovered_request().is_none() { + // return Ok(None); + // } + // + // let request = store.find_hovered_request(); + // match request { + // RequestKind::Nested(_) => { + // store.dispatch(CollectionStoreAction::ToggleDirectory(request.get_id())); + // } + // RequestKind::Single(req) => { + // store.dispatch(CollectionStoreAction::SetSelectedRequest(Some(req))); + // return Ok(Some(SidebarEvent::RebuildView)); + // } + // } + // } + // KeyCode::Char('n') => { + // self.request_form = RequestFormVariant::Create(RequestForm::::new( + // self.colors, + // self.collection_store.clone(), + // )); + // return Ok(Some(SidebarEvent::CreateRequest)); + // } + // KeyCode::Char('e') => { + // let hovered_request = store.find_hovered_request(); + // drop(store); + // match hovered_request { + // RequestKind::Single(req) => { + // self.request_form = RequestFormVariant::Edit(RequestForm::::new( + // self.colors, + // self.collection_store.clone(), + // req.clone(), + // )); + // return Ok(Some(SidebarEvent::EditRequest)); + // } + // RequestKind::Nested(dir) => { + // self.directory_form = DirectoryFormVariant::Edit(DirectoryForm::::new( + // self.colors, + // self.collection_store.clone(), + // Some((dir.id.clone(), dir.name.clone())), + // )); + // return Ok(Some(SidebarEvent::EditDirectory)); + // } + // } + // } + // KeyCode::Char('D') => { + // if let Some(item_id) = store.get_hovered_request() { + // return Ok(Some(SidebarEvent::DeleteItem(item_id))); + // } + // } + // KeyCode::Char('d') => return Ok(Some(SidebarEvent::CreateDirectory)), } - drop(store); - self.rebuild_tree_view(); - Ok(None) } } -pub fn build_lines( - requests: Option>>>, - level: usize, - selected_request: Option>>, - hovered_request: Option, - dirs_expanded: Rc>>, - colors: &hac_colors::Colors, -) -> Vec> { - requests - .unwrap_or(Arc::new(RwLock::new(vec![]))) - .read() - .unwrap() - .iter() - .flat_map(|item| match item { - RequestKind::Nested(dir) => { - let is_hovered = hovered_request - .as_ref() - .is_some_and(|id| id.eq(&item.get_id())); - let mut dirs = dirs_expanded.borrow_mut(); - let is_expanded = dirs.entry(dir.id.to_string()).or_insert(false); - - let dir_style = match is_hovered { - true => Style::default() - .fg(colors.normal.white) - .bg(colors.primary.hover) - .bold(), - false => Style::default().fg(colors.normal.white).bold(), - }; - - let gap = " ".repeat(level * 2); - let chevron = if *is_expanded { "v" } else { ">" }; - let line = vec![Paragraph::new(format!( - "{}{} {}/", - gap, - chevron, - dir.name.to_lowercase().replace(' ', "-") - )) - .set_style(dir_style)]; - - let nested_lines = if *is_expanded { - build_lines( - Some(dir.requests.clone()), - level + 1, - selected_request.clone(), - hovered_request.clone(), - dirs_expanded.clone(), - colors, - ) - } else { - vec![] - }; - line.into_iter().chain(nested_lines).collect::>() - } - RequestKind::Single(req) => { - let gap = " ".repeat(level * 2); - let is_selected = selected_request.as_ref().is_some_and(|selected| { - selected.read().unwrap().id.eq(&req.read().unwrap().id) - }); - let is_hovered = hovered_request - .as_ref() - .is_some_and(|id| id.eq(&item.get_id())); - - let req_style = match (is_selected, is_hovered) { - (true, true) => Style::default() - .fg(colors.normal.yellow) - .bg(colors.normal.blue), - (true, _) => Style::default() - .fg(colors.normal.white) - .bg(colors.normal.blue), - (_, true) => Style::default() - .fg(colors.normal.white) - .bg(colors.primary.hover), - (false, false) => Style::default().fg(colors.normal.white), - }; - - let line: Line<'_> = vec![ - Span::from(gap.clone()), - colored_method(req.read().unwrap().method.clone(), colors), - Span::from(format!(" {}", req.read().unwrap().name.clone())), - ] - .into(); - - vec![Paragraph::new(line).set_style(req_style)] - } - }) - .collect() -} - -fn colored_method(method: RequestMethod, colors: &hac_colors::Colors) -> Span<'static> { +fn colored_method(method: &ReqMethod, colors: &HacColors) -> Span<'static> { match method { - RequestMethod::Get => "GET ".fg(colors.normal.green).bold(), - RequestMethod::Post => "POST ".fg(colors.normal.magenta).bold(), - RequestMethod::Put => "PUT ".fg(colors.normal.yellow).bold(), - RequestMethod::Patch => "PATCH ".fg(colors.normal.orange).bold(), - RequestMethod::Delete => "DELETE".fg(colors.normal.red).bold(), + ReqMethod::Get => format!("{method} ").fg(colors.normal.green).bold(), + ReqMethod::Post => format!("{method} ").fg(colors.normal.magenta).bold(), + ReqMethod::Put => format!("{method} ").fg(colors.normal.yellow).bold(), + ReqMethod::Patch => format!("{method} ").fg(colors.normal.orange).bold(), + ReqMethod::Delete => format!("{method}").fg(colors.normal.red).bold(), } } diff --git a/hac-config/src/config.rs b/hac-config/src/config.rs index f5a350b..cfc3df9 100755 --- a/hac-config/src/config.rs +++ b/hac-config/src/config.rs @@ -61,6 +61,8 @@ pub struct Config { #[serde(default)] pub collection_ext: CollectionExtensions, pub dry_run: bool, + pub tab_size: usize, + pub enable_icons: bool, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] diff --git a/hac-config/src/default_config.rs b/hac-config/src/default_config.rs index 851e505..cea421a 100755 --- a/hac-config/src/default_config.rs +++ b/hac-config/src/default_config.rs @@ -1,5 +1,7 @@ pub static DEFAULT_CONFIG: &str = r##" dry_run = false +tab_size = 4 +enable_icons = true [editor_keys.normal] "u" = "Undo" diff --git a/hac-loader/src/collection_loader/json_collection.rs b/hac-loader/src/collection_loader/json_collection.rs index 49398c1..d275a49 100644 --- a/hac-loader/src/collection_loader/json_collection.rs +++ b/hac-loader/src/collection_loader/json_collection.rs @@ -93,5 +93,5 @@ pub enum JsonAuthKind { #[derive(Debug, Serialize, Deserialize)] pub struct JsonFolder { pub name: String, - pub requests: Vec, + pub requests: Vec, } diff --git a/hac-loader/src/collection_loader/json_loader.rs b/hac-loader/src/collection_loader/json_loader.rs index e7eee09..de432b0 100644 --- a/hac-loader/src/collection_loader/json_loader.rs +++ b/hac-loader/src/collection_loader/json_loader.rs @@ -1,4 +1,6 @@ -use hac_store::collection::{BodyKind, Collection, CollectionInfo, Folder, HeaderEntry, KeyKind, ReqMethod, Request}; +use hac_store::collection::{ + BodyKind, Collection, CollectionInfo, Folder, HeaderEntry, ReqMethod, ReqTree, ReqTreeNode, Request, WhichSlab, +}; use hac_store::slab::{Key, Slab}; use super::json_collection::{ @@ -35,11 +37,37 @@ fn from_file_to_collection(file_collection: JsonCollection) -> Collection { }; } + let selected_request = match (root_requests.is_empty(), folders.is_empty()) { + (true, true) => None, + (false, _) => Some((WhichSlab::RootRequests, 0)), + (true, false) => { + let folder = folders.get(0); + match folder.requests.is_empty() { + true => Some((WhichSlab::Folders, 0)), + false => Some((WhichSlab::Requests, folder.requests[0])), + } + } + }; + + let mut nodes: Vec = (0..root_requests.len()).map(ReqTreeNode::Req).collect(); + nodes.extend( + folders + .iter() + .enumerate() + .map(|(idx, folder)| ReqTreeNode::Folder(idx, folder.requests.clone())) + .collect::>(), + ); + + let layout = ReqTree { nodes }; + Collection { info: file_collection.info.into(), folders, requests, root_requests, + layout, + hovered_request: selected_request, + selected_request, } } @@ -49,18 +77,10 @@ fn collect_folder(file_folder: JsonFolder, requests: &mut Slab, folders let mut childrens = vec![]; for item in file_folder.requests { - match item { - ReqKind::Req(inner) => { - let mut req: Request = inner.into(); - req.parent = Some(idx); - let req_idx = requests.push(req); - childrens.push(KeyKind::Req(req_idx)); - } - ReqKind::Folder(inner) => { - let folder_idx = collect_folder(inner, requests, folders); - childrens.push(KeyKind::Folder(folder_idx)); - } - } + let mut req: Request = item.into(); + req.parent = Some(idx); + let req_idx = requests.push(req); + childrens.push(req_idx); } let folder = folders.get_mut(idx); diff --git a/hac-store/src/collection.rs b/hac-store/src/collection.rs index 38758d0..022babe 100644 --- a/hac-store/src/collection.rs +++ b/hac-store/src/collection.rs @@ -1,10 +1,24 @@ +use std::fmt::Display; + use crate::slab::{Key, Slab}; use crate::HAC_STORE; -#[derive(Debug)] -pub enum ReqTree { +#[derive(Debug, Clone)] +pub enum ReqTreeNode { Req(Key), - Folder(Vec), + Folder(Key, Vec), +} + +#[derive(Debug, Clone)] +pub struct ReqTree { + pub nodes: Vec, +} + +#[derive(Debug, Clone, Copy)] +pub enum WhichSlab { + Requests, + Folders, + RootRequests, } #[derive(Debug)] @@ -13,6 +27,15 @@ pub struct Collection { pub requests: Slab, pub root_requests: Slab, pub folders: Slab, + pub layout: ReqTree, + pub selected_request: Option<(WhichSlab, Key)>, + pub hovered_request: Option<(WhichSlab, Key)>, +} + +impl Collection { + pub fn is_empty(&self) -> bool { + self.requests.is_empty() && self.root_requests.is_empty() + } } #[derive(Debug)] @@ -30,6 +53,18 @@ pub enum ReqMethod { Delete, } +impl Display for ReqMethod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ReqMethod::Get => f.write_str("GET"), + ReqMethod::Post => f.write_str("POST"), + ReqMethod::Put => f.write_str("PUT"), + ReqMethod::Patch => f.write_str("PATCH"), + ReqMethod::Delete => f.write_str("DELETE"), + } + } +} + #[derive(Debug)] pub struct HeaderEntry { pub key: String, @@ -54,32 +89,158 @@ pub struct Request { pub body_kind: BodyKind, } -#[derive(Debug)] -pub enum KeyKind { - Req(Key), - Folder(Key), -} - #[derive(Debug, Default)] pub struct Folder { pub name: String, - pub requests: Vec, + pub requests: Vec, } pub fn set_collection(collection: Option) { HAC_STORE.with_borrow_mut(|store| store.collection = collection); } -pub fn get_request(key: Key, f: F) -> Option +pub fn get_selected_request(f: F) -> Option where F: FnOnce(&Request) -> Option, { HAC_STORE.with_borrow(|store| { if let Some(ref collection) = store.collection { - let request = collection.root_requests.get(key); - return f(request); + if let Some((slab, key)) = collection.selected_request { + if let Some(request) = match slab { + WhichSlab::Requests => Some(collection.requests.get(key)), + WhichSlab::RootRequests => Some(collection.root_requests.get(key)), + WhichSlab::Folders => None, + } { + return f(request); + } + } }; None }) } + +pub fn get_selected_request_mut(f: F) +where + F: FnOnce(&mut Request), +{ + HAC_STORE.with_borrow_mut(|store| { + if let Some(collection) = &mut store.collection { + if let Some((slab, key)) = collection.selected_request { + if let Some(request) = match slab { + WhichSlab::Requests => Some(collection.requests.get_mut(key)), + WhichSlab::RootRequests => Some(collection.root_requests.get_mut(key)), + WhichSlab::Folders => None, + } { + f(request); + } + } + }; + }) +} + +pub fn is_empty() -> bool { + HAC_STORE.with_borrow(|store| { + store + .collection + .as_ref() + .is_some_and(|collection| collection.is_empty()) + }) +} + +pub fn tree_layout() -> ReqTree { + let mut nodes = ReqTree { nodes: vec![] }; + + HAC_STORE.with_borrow(|store| { + if let Some(ref collection) = store.collection { + nodes = collection.layout.clone(); + } + }); + + nodes +} + +#[derive(Debug)] +pub enum EntryStatus { + None, + Hovered, + Selected, + Both, +} + +impl From<(bool, bool)> for EntryStatus { + fn from((hovered, selected): (bool, bool)) -> Self { + match (hovered, selected) { + (false, false) => Self::None, + (true, false) => Self::Hovered, + (false, true) => Self::Selected, + (true, true) => Self::Both, + } + } +} + +pub fn get_root_request(key: Key, f: F) +where + F: FnOnce(&Request, EntryStatus), +{ + HAC_STORE.with_borrow(|store| { + if let Some(ref collection) = store.collection { + let req = collection.root_requests.get(key); + let is_hovered = collection + .hovered_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::RootRequests) && key == k); + let is_selected = collection + .selected_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::RootRequests) && key == k); + + f(req, (is_hovered, is_selected).into()); + } + }) +} + +pub fn get_request(key: Key, f: F) +where + F: FnOnce(&Request), +{ + HAC_STORE.with_borrow(|store| { + if let Some(ref collection) = store.collection { + let req = collection.requests.get(key); + f(req); + } + }) +} + +pub fn get_folder(key: Key, f: F) +where + F: FnOnce(&Folder), +{ + HAC_STORE.with_borrow(|store| { + if let Some(ref collection) = store.collection { + let folder = collection.folders.get(key); + f(folder); + } + }) +} + +pub fn hover_next() { + HAC_STORE.with_borrow_mut(|store| { + if let Some(collection) = &mut store.collection { + if let Some((which, key)) = collection.hovered_request { + match which { + WhichSlab::Requests => todo!(), + WhichSlab::Folders => todo!(), + WhichSlab::RootRequests => { + if key >= collection.root_requests.len() - 1 { + collection.hovered_request = collection.folders.try_get(0).map(|_| (WhichSlab::Folders, 0)); + return; + } + + collection.hovered_request = Some((WhichSlab::RootRequests, key + 1)) + } + } + } + } + }) +} + +pub fn hover_prev() {} diff --git a/hac-store/src/slab.rs b/hac-store/src/slab.rs index 2d90d3f..a512265 100644 --- a/hac-store/src/slab.rs +++ b/hac-store/src/slab.rs @@ -80,6 +80,28 @@ impl Slab { val } + pub fn try_get(&self, idx: Key) -> Option<&T> { + if let Some(entry) = self.inner.get(idx) { + let Entry::Full(val) = entry else { + panic!("attempted to get an empty entry"); + }; + Some(val) + } else { + None + } + } + + pub fn try_get_mut(&mut self, idx: Key) -> Option<&mut T> { + if let Some(entry) = self.inner.get_mut(idx) { + let Entry::Full(val) = entry else { + panic!("attempted to get an empty entry"); + }; + Some(val) + } else { + None + } + } + pub fn len(&self) -> usize { self.inner.len() } @@ -95,7 +117,7 @@ impl Slab { } } - pub fn iter(&mut self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { self.inner.iter().filter_map(|e| match e { Entry::Full(val) => Some(val), Entry::Free(_) => None, From d3f625a0ad56ca970654f126ca222f5691165fcd Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Mon, 30 Sep 2024 18:56:51 -0300 Subject: [PATCH 14/23] feat: better hovering styles --- .../src/pages/collection_viewer/sidebar.rs | 26 ++--- .../src/collection_loader/json_loader.rs | 1 + hac-store/src/collection.rs | 99 ++++++++++++++----- 3 files changed, 90 insertions(+), 36 deletions(-) diff --git a/hac-client/src/pages/collection_viewer/sidebar.rs b/hac-client/src/pages/collection_viewer/sidebar.rs index 37d206a..b1dc353 100755 --- a/hac-client/src/pages/collection_viewer/sidebar.rs +++ b/hac-client/src/pages/collection_viewer/sidebar.rs @@ -157,16 +157,10 @@ impl Renderable for Sidebar { let method = &req.method; let style = match status { - EntryStatus::None => Style::default().fg(self.colors.normal.white), - EntryStatus::Hovered => Style::default() - .fg(self.colors.normal.white) - .bg(self.colors.primary.hover), - EntryStatus::Selected => Style::default() - .fg(self.colors.normal.white) - .bg(self.colors.normal.blue), - EntryStatus::Both => Style::default() - .fg(self.colors.normal.yellow) - .bg(self.colors.normal.blue), + EntryStatus::None => Style::new().fg(self.colors.normal.white), + EntryStatus::Hovered => Style::new().fg(self.colors.bright.blue).underlined().italic(), + EntryStatus::Selected => Style::new().fg(self.colors.normal.red).bold(), + EntryStatus::Both => Style::new().fg(self.colors.normal.red).underlined().italic().bold(), }; lines.push(Line::from(vec![ @@ -184,13 +178,21 @@ impl Renderable for Sidebar { }; lines.push(Line::from(folder_name).bold().fg(self.colors.normal.yellow)); for request in requests { - hac_store::collection::get_request(request, |req| { + hac_store::collection::get_request(request, |req, status| { let name = req.name.clone(); let method = &req.method; + + let style = match status { + EntryStatus::None => Style::new().fg(self.colors.normal.white), + EntryStatus::Hovered => Style::new().fg(self.colors.bright.blue).underlined().italic(), + EntryStatus::Selected => Style::new().fg(self.colors.normal.red).bold(), + EntryStatus::Both => Style::new().fg(self.colors.normal.red).underlined().italic().bold(), + }; + lines.push(Line::from(vec![ " ".repeat(self.config.borrow().tab_size).into(), colored_method(method, &self.colors), - name.fg(self.colors.normal.white), + name.set_style(style), ])); }); } diff --git a/hac-loader/src/collection_loader/json_loader.rs b/hac-loader/src/collection_loader/json_loader.rs index de432b0..a88e366 100644 --- a/hac-loader/src/collection_loader/json_loader.rs +++ b/hac-loader/src/collection_loader/json_loader.rs @@ -148,6 +148,7 @@ impl From for Folder { Self { name: file_folder.name, requests: vec![], + collapsed: false, } } } diff --git a/hac-store/src/collection.rs b/hac-store/src/collection.rs index 022babe..f844c6f 100644 --- a/hac-store/src/collection.rs +++ b/hac-store/src/collection.rs @@ -14,6 +14,25 @@ pub struct ReqTree { pub nodes: Vec, } +#[derive(Debug)] +pub enum EntryStatus { + None, + Hovered, + Selected, + Both, +} + +impl From<(bool, bool)> for EntryStatus { + fn from((hovered, selected): (bool, bool)) -> Self { + match (hovered, selected) { + (false, false) => Self::None, + (true, false) => Self::Hovered, + (false, true) => Self::Selected, + (true, true) => Self::Both, + } + } +} + #[derive(Debug, Clone, Copy)] pub enum WhichSlab { Requests, @@ -93,6 +112,7 @@ pub struct Request { pub struct Folder { pub name: String, pub requests: Vec, + pub collapsed: bool, } pub fn set_collection(collection: Option) { @@ -160,25 +180,6 @@ pub fn tree_layout() -> ReqTree { nodes } -#[derive(Debug)] -pub enum EntryStatus { - None, - Hovered, - Selected, - Both, -} - -impl From<(bool, bool)> for EntryStatus { - fn from((hovered, selected): (bool, bool)) -> Self { - match (hovered, selected) { - (false, false) => Self::None, - (true, false) => Self::Hovered, - (false, true) => Self::Selected, - (true, true) => Self::Both, - } - } -} - pub fn get_root_request(key: Key, f: F) where F: FnOnce(&Request, EntryStatus), @@ -200,12 +201,18 @@ where pub fn get_request(key: Key, f: F) where - F: FnOnce(&Request), + F: FnOnce(&Request, EntryStatus), { HAC_STORE.with_borrow(|store| { if let Some(ref collection) = store.collection { let req = collection.requests.get(key); - f(req); + let is_hovered = collection + .hovered_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Requests) && key == k); + let is_selected = collection + .selected_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Requests) && key == k); + f(req, (is_hovered, is_selected).into()); } }) } @@ -227,15 +234,59 @@ pub fn hover_next() { if let Some(collection) = &mut store.collection { if let Some((which, key)) = collection.hovered_request { match which { - WhichSlab::Requests => todo!(), - WhichSlab::Folders => todo!(), + WhichSlab::Requests => { + let req = collection.requests.get(key); + let parent_key = req.parent.expect("nested request has no parent"); + let parent = collection.folders.get(parent_key); + let pos = parent + .requests + .iter() + .position(|&req| req == key) + .expect("nested request not listed on the parent"); + + // when the current nested request is not the last on the folder, we hover + // the next request on that folder + if pos < parent.requests.len() - 1 { + collection.hovered_request = Some((WhichSlab::Requests, parent.requests[pos + 1])); + return; + } + + // when it is the last one, we try to move to the next folder if possible + if let Some(next) = collection + .folders + .try_get(parent_key + 1) + .map(|_| (WhichSlab::Folders, parent_key + 1)) + { + collection.hovered_request = Some(next) + }; + } + WhichSlab::Folders => { + let folder = collection.folders.get(key); + // when the folder is collapsed or has no requests, and its not the + // last folder on the collection, we can hover the next folder + if (folder.requests.is_empty() || folder.collapsed) && key < collection.folders.len() - 1 { + collection.hovered_request = Some((WhichSlab::Folders, key + 1)); + return; + } + + // when the folder has requests, we hover the first one + if !folder.requests.is_empty() { + collection.hovered_request = Some((WhichSlab::Requests, folder.requests[0])); + } + + // when the folder has no requests and its the last folder, we don't do + // anything + } WhichSlab::RootRequests => { + // when the last root_request is hovered, we try to hover the first folder + // as no request with a parent can appear before its parent if key >= collection.root_requests.len() - 1 { collection.hovered_request = collection.folders.try_get(0).map(|_| (WhichSlab::Folders, 0)); return; } - collection.hovered_request = Some((WhichSlab::RootRequests, key + 1)) + // when we are not at the last one, we can simply hover the next + collection.hovered_request = Some((WhichSlab::RootRequests, key + 1)); } } } From 83104ac899e31b06f7948024f3e34c345e29645a Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Mon, 30 Sep 2024 19:56:38 -0300 Subject: [PATCH 15/23] feat: hovering up and down --- .../src/pages/collection_viewer/sidebar.rs | 20 +++--- hac-config/src/default_config.rs | 2 +- hac-store/src/collection.rs | 71 ++++++++++++++++++- 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/hac-client/src/pages/collection_viewer/sidebar.rs b/hac-client/src/pages/collection_viewer/sidebar.rs index b1dc353..c753589 100755 --- a/hac-client/src/pages/collection_viewer/sidebar.rs +++ b/hac-client/src/pages/collection_viewer/sidebar.rs @@ -169,14 +169,18 @@ impl Renderable for Sidebar { ])); }), ReqTreeNode::Folder(folder_key, requests) => { - let mut folder_name = String::default(); - hac_store::collection::get_folder(folder_key, |folder| folder_name.push_str(&folder.name)); - let folder_name = if self.config.borrow().enable_icons { - format!("{} {}", Icons::FOLDER, folder_name) - } else { - folder_name - }; - lines.push(Line::from(folder_name).bold().fg(self.colors.normal.yellow)); + hac_store::collection::get_folder(folder_key, |folder, status| { + let mut folder_name = folder.name.clone(); + if self.config.borrow().enable_icons { + folder_name = format!("{} {}", Icons::FOLDER, folder_name); + } + let style = match status { + EntryStatus::None => Style::new().fg(self.colors.normal.yellow).bold(), + _ => Style::new().fg(self.colors.normal.yellow).underlined().italic().bold(), + }; + let name = Line::from(folder_name).set_style(style); + lines.push(name); + }); for request in requests { hac_store::collection::get_request(request, |req, status| { let name = req.name.clone(); diff --git a/hac-config/src/default_config.rs b/hac-config/src/default_config.rs index cea421a..fb823cb 100755 --- a/hac-config/src/default_config.rs +++ b/hac-config/src/default_config.rs @@ -1,6 +1,6 @@ pub static DEFAULT_CONFIG: &str = r##" dry_run = false -tab_size = 4 +tab_size = 2 enable_icons = true [editor_keys.normal] diff --git a/hac-store/src/collection.rs b/hac-store/src/collection.rs index f844c6f..d60a225 100644 --- a/hac-store/src/collection.rs +++ b/hac-store/src/collection.rs @@ -219,12 +219,18 @@ where pub fn get_folder(key: Key, f: F) where - F: FnOnce(&Folder), + F: FnOnce(&Folder, EntryStatus), { HAC_STORE.with_borrow(|store| { if let Some(ref collection) = store.collection { let folder = collection.folders.get(key); - f(folder); + let is_hovered = collection + .hovered_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Folders) && key == k); + let is_selected = collection + .selected_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Folders) && key == k); + f(folder, (is_hovered, is_selected).into()); } }) } @@ -294,4 +300,63 @@ pub fn hover_next() { }) } -pub fn hover_prev() {} +pub fn hover_prev() { + HAC_STORE.with_borrow_mut(|store| { + if let Some(collection) = &mut store.collection { + if let Some((which, key)) = collection.hovered_request { + match which { + WhichSlab::Requests => { + let req = collection.requests.get(key); + let parent_key = req.parent.expect("nested request has no parent"); + let parent = collection.folders.get(parent_key); + let pos = parent + .requests + .iter() + .position(|&req| req == key) + .expect("nested request not listed on the parent"); + + // when the current nested request is not the first on the folder, we hover + // the previous request on that folder + if pos > 0 { + collection.hovered_request = Some((WhichSlab::Requests, parent.requests[pos - 1])); + return; + } + + // when it is the first one, we move to the parent folder itself + collection.hovered_request = Some((WhichSlab::Folders, parent_key)) + } + WhichSlab::Folders => { + // when we are hovering a folder, we try to hover the previous folders's last + // request if it exists, otherwise we select the previous folder itself + if key > 0 { + let folder = collection.folders.get(key - 1); + if let Some(last) = folder.requests.last() { + collection.hovered_request = Some((WhichSlab::Requests, *last)); + return; + }; + + collection.hovered_request = Some((WhichSlab::Folders, key - 1)); + return; + } + + // when there are no previous folders we try to hover the last root request + if !collection.root_requests.is_empty() { + let key = collection.root_requests.len() - 1; + collection.hovered_request = Some((WhichSlab::RootRequests, key)); + } + + // when there are no root requests or previou folders, we do nothing + } + WhichSlab::RootRequests => { + // when we are hovering on a root request, we either hover the preivous or + // do nothing + + if key > 0 { + collection.hovered_request = Some((WhichSlab::RootRequests, key - 1)); + } + } + } + } + } + }) +} From 7b2756703ced347c40eeef7ce7b3ed56af305ca5 Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Wed, 2 Oct 2024 21:45:55 -0300 Subject: [PATCH 16/23] feat: almost done with creation form --- hac-client/src/app.rs | 8 +- hac-client/src/components/component_styles.rs | 31 -- hac-client/src/components/key_value_pair.rs | 37 -- hac-client/src/components/list_item.rs | 131 ++++-- hac-client/src/components/list_itemm.rs | 96 ----- hac-client/src/components/mod.rs | 3 - .../src/pages/collection_list/form_shared.rs | 1 - hac-client/src/pages/collection_list/mod.rs | 5 +- .../collection_viewer/collection_viewer.rs | 25 +- .../sidebar/create_request_form.rs | 392 ++++++++++-------- .../{sidebar.rs => sidebar/mod.rs} | 95 ++--- .../collection_viewer/sidebar/request_form.rs | 8 +- hac-client/src/pages/overlay.rs | 31 +- hac-client/src/router.rs | 26 +- hac-client/src/utils.rs | 5 +- .../src/collection_loader/json_collection.rs | 10 +- .../src/collection_loader/json_loader.rs | 14 +- hac-loader/src/collection_loader/mod.rs | 2 +- hac-store/src/collection.rs | 116 +++++- 19 files changed, 500 insertions(+), 536 deletions(-) delete mode 100644 hac-client/src/components/component_styles.rs delete mode 100644 hac-client/src/components/key_value_pair.rs delete mode 100644 hac-client/src/components/list_itemm.rs rename hac-client/src/pages/collection_viewer/{sidebar.rs => sidebar/mod.rs} (85%) diff --git a/hac-client/src/app.rs b/hac-client/src/app.rs index 639af16..6d3028c 100755 --- a/hac-client/src/app.rs +++ b/hac-client/src/app.rs @@ -11,6 +11,9 @@ use crate::renderable::{Eventful, Renderable}; use crate::router::Router; use crate::{HacColors, HacConfig}; +static FRAME_RATE: f64 = 60f64; +static TICK_RATE: f64 = 30f64; + #[derive(Debug)] pub struct InvalidRouteNumber(u8); @@ -30,6 +33,7 @@ pub enum Routes { EditCollection, DeleteCollection, CollectionViewer, + CreateRequest, } impl TryFrom for Routes { @@ -42,6 +46,7 @@ impl TryFrom for Routes { 2 => Ok(Routes::EditCollection), 3 => Ok(Routes::DeleteCollection), 4 => Ok(Routes::CollectionViewer), + 5 => Ok(Routes::CreateRequest), _ => Err(InvalidRouteNumber(value)), } } @@ -55,6 +60,7 @@ impl From for u8 { Routes::EditCollection => 2, Routes::DeleteCollection => 3, Routes::CollectionViewer => 4, + Routes::CreateRequest => 5, } } } @@ -79,7 +85,7 @@ impl App { router.add_route(Routes::ListCollections.into(), Box::new(collection_list)); Ok(Self { - event_pool: EventPool::new(60f64, 30f64), + event_pool: EventPool::new(FRAME_RATE, TICK_RATE), should_quit: false, router, command_receiver, diff --git a/hac-client/src/components/component_styles.rs b/hac-client/src/components/component_styles.rs deleted file mode 100644 index e8df6f4..0000000 --- a/hac-client/src/components/component_styles.rs +++ /dev/null @@ -1,31 +0,0 @@ -#[derive(Debug, Clone, Copy)] -pub enum ComponentBorder { - All, - Below, - None, -} - -#[derive(Debug, Clone, Copy)] -pub enum ComponentFocus { - Unfocused, - Focused, -} - -pub fn color_from_focus( - focus: ComponentFocus, - colors: &hac_colors::Colors, -) -> ratatui::style::Color { - match focus { - ComponentFocus::Focused => colors.normal.red, - ComponentFocus::Unfocused => colors.normal.white, - } -} - -impl From for ComponentFocus { - fn from(value: bool) -> Self { - match value { - true => ComponentFocus::Focused, - false => ComponentFocus::Unfocused, - } - } -} diff --git a/hac-client/src/components/key_value_pair.rs b/hac-client/src/components/key_value_pair.rs deleted file mode 100644 index ba2e4c5..0000000 --- a/hac-client/src/components/key_value_pair.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::borrow::Cow; - -use ratatui::style::Style; -use ratatui::widgets::{Block, Borders, Paragraph}; - -use super::component_styles::{color_from_focus, ComponentBorder, ComponentFocus}; - -pub fn key_value_pair<'a, T>( - key: T, - value: T, - border_kind: ComponentBorder, - focus: ComponentFocus, - colors: &hac_colors::Colors, -) -> (Paragraph<'a>, Paragraph<'a>) -where - T: Into>, -{ - let block = Block::default(); - let block = block.borders(match border_kind { - ComponentBorder::None => Borders::NONE, - ComponentBorder::All => Borders::ALL, - ComponentBorder::Below => Borders::BOTTOM, - }); - let block = block.border_style(match border_kind { - ComponentBorder::None => Style::default(), - ComponentBorder::All => Style::default().fg(color_from_focus(focus, colors)), - ComponentBorder::Below => Style::default().fg(color_from_focus(focus, colors)), - }); - - let style = Style::default(); - let style = style.fg(color_from_focus(focus, colors)); - - let key = Paragraph::new(key.into()).block(block.clone()).style(style); - let value = Paragraph::new(value.into()).block(block).style(style); - - (key, value) -} diff --git a/hac-client/src/components/list_item.rs b/hac-client/src/components/list_item.rs index ca86a79..0cc7138 100644 --- a/hac-client/src/components/list_item.rs +++ b/hac-client/src/components/list_item.rs @@ -1,53 +1,96 @@ use std::borrow::Cow; -use ratatui::style::{Style, Stylize}; -use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, Borders, Paragraph}; +use ratatui::buffer::Buffer; +use ratatui::layout::Rect; +use ratatui::style::Style; +use ratatui::text::Line; +use ratatui::widgets::{Block, Borders, Padding, Paragraph, Widget}; -use crate::components::component_styles::{color_from_focus, ComponentBorder, ComponentFocus}; +use crate::HacColors; -pub enum ListItemKind { - _Regular, - Enumerated(usize), +#[derive(Debug, Clone)] +pub struct ListItem<'item> { + title: Cow<'item, str>, + description: Option>, + title_style: Style, + desc_style: Style, + selected: bool, + colors: HacColors, } -pub fn list_item<'a, T>( - text: T, - focus: ComponentFocus, - border_kind: ComponentBorder, - kind: ListItemKind, - colors: &hac_colors::Colors, -) -> Paragraph<'a> -where - T: Into>, -{ - let block = Block::default(); - let block = block.borders(match border_kind { - ComponentBorder::None => Borders::NONE, - ComponentBorder::All => Borders::ALL, - ComponentBorder::Below => Borders::BOTTOM, - }); - let block = block.border_style(match border_kind { - ComponentBorder::None => Style::default(), - ComponentBorder::All => Style::default().fg(color_from_focus(focus, colors)), - ComponentBorder::Below => Style::default().fg(color_from_focus(focus, colors)), - }); - - let style = Style::default(); - let style = style.fg(color_from_focus(focus, colors)); - let style = style.bg(colors.normal.black); - - let text = match kind { - ListItemKind::_Regular => Line::from(Span::from(text.into())), - ListItemKind::Enumerated(idx) => { - let text = format!("{}{}", text.into(), " ".repeat(80)); - Line::from(vec![ - Span::from(idx.to_string()).fg(colors.normal.red), - Span::from(" "), - Span::from(text), - ]) +impl<'item> ListItem<'item> { + pub fn new(title: T, description: Option, colors: HacColors) -> Self + where + T: Into>, + { + Self { + title: title.into(), + description: description.map(Into::into), + title_style: Style::default(), + desc_style: Style::default(), + selected: false, + colors, } - }; + } - Paragraph::new(text).block(block).style(style) + pub fn title_style(self, style: Style) -> Self { + Self { + title: self.title, + description: self.description, + title_style: style, + desc_style: self.desc_style, + selected: self.selected, + colors: self.colors, + } + } + + pub fn desc_style(self, style: Style) -> Self { + Self { + title: self.title, + description: self.description, + title_style: self.title_style, + desc_style: style, + selected: self.selected, + colors: self.colors, + } + } + + pub fn select(self) -> Self { + Self { + title: self.title, + description: self.description, + title_style: self.title_style, + desc_style: self.desc_style, + selected: true, + colors: self.colors, + } + } +} + +impl Widget for ListItem<'_> { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let mut parts = Vec::with_capacity(2); + parts.push(Line::from(self.title.to_string()).style(self.title_style)); + + if let Some(description) = self.description { + parts.push(Line::from(description.to_string()).style(self.desc_style)); + } + + let mut block = Block::default().padding(Padding::left(1)); + if self.selected { + block = block + .borders(Borders::LEFT) + .border_style( + Style::default() + .bg(self.colors.primary.background) + .fg(self.colors.bright.red), + ) + .style(self.title_style); + } + + Paragraph::new(parts).block(block).render(area, buf); + } } diff --git a/hac-client/src/components/list_itemm.rs b/hac-client/src/components/list_itemm.rs deleted file mode 100644 index 0cc7138..0000000 --- a/hac-client/src/components/list_itemm.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::borrow::Cow; - -use ratatui::buffer::Buffer; -use ratatui::layout::Rect; -use ratatui::style::Style; -use ratatui::text::Line; -use ratatui::widgets::{Block, Borders, Padding, Paragraph, Widget}; - -use crate::HacColors; - -#[derive(Debug, Clone)] -pub struct ListItem<'item> { - title: Cow<'item, str>, - description: Option>, - title_style: Style, - desc_style: Style, - selected: bool, - colors: HacColors, -} - -impl<'item> ListItem<'item> { - pub fn new(title: T, description: Option, colors: HacColors) -> Self - where - T: Into>, - { - Self { - title: title.into(), - description: description.map(Into::into), - title_style: Style::default(), - desc_style: Style::default(), - selected: false, - colors, - } - } - - pub fn title_style(self, style: Style) -> Self { - Self { - title: self.title, - description: self.description, - title_style: style, - desc_style: self.desc_style, - selected: self.selected, - colors: self.colors, - } - } - - pub fn desc_style(self, style: Style) -> Self { - Self { - title: self.title, - description: self.description, - title_style: self.title_style, - desc_style: style, - selected: self.selected, - colors: self.colors, - } - } - - pub fn select(self) -> Self { - Self { - title: self.title, - description: self.description, - title_style: self.title_style, - desc_style: self.desc_style, - selected: true, - colors: self.colors, - } - } -} - -impl Widget for ListItem<'_> { - fn render(self, area: Rect, buf: &mut Buffer) - where - Self: Sized, - { - let mut parts = Vec::with_capacity(2); - parts.push(Line::from(self.title.to_string()).style(self.title_style)); - - if let Some(description) = self.description { - parts.push(Line::from(description.to_string()).style(self.desc_style)); - } - - let mut block = Block::default().padding(Padding::left(1)); - if self.selected { - block = block - .borders(Borders::LEFT) - .border_style( - Style::default() - .bg(self.colors.primary.background) - .fg(self.colors.bright.red), - ) - .style(self.title_style); - } - - Paragraph::new(parts).block(block).render(area, buf); - } -} diff --git a/hac-client/src/components/mod.rs b/hac-client/src/components/mod.rs index 3174a48..b3910f3 100644 --- a/hac-client/src/components/mod.rs +++ b/hac-client/src/components/mod.rs @@ -1,5 +1,2 @@ -pub mod component_styles; pub mod input; -pub mod key_value_pair; pub mod list_item; -pub mod list_itemm; diff --git a/hac-client/src/pages/collection_list/form_shared.rs b/hac-client/src/pages/collection_list/form_shared.rs index cf53397..7c71022 100644 --- a/hac-client/src/pages/collection_list/form_shared.rs +++ b/hac-client/src/pages/collection_list/form_shared.rs @@ -100,7 +100,6 @@ pub fn handle_form_key_event( pub fn draw_form_layout(layout: FormLayout, name: String, colors: &HacColors, frame: &mut Frame) { let label = String::from("Collection Name"); - let name = name.to_string(); let name_input = Input::new(Some(&name), Some(&label), colors.clone()) .value_style(Style::default().fg(colors.normal.white)) .label_style(Style::default().fg(colors.bright.black)); diff --git a/hac-client/src/pages/collection_list/mod.rs b/hac-client/src/pages/collection_list/mod.rs index c869e9c..4da1844 100644 --- a/hac-client/src/pages/collection_list/mod.rs +++ b/hac-client/src/pages/collection_list/mod.rs @@ -19,7 +19,7 @@ use ratatui::widgets::{Block, Clear, Paragraph}; use ratatui::Frame; use crate::app::Routes; -use crate::components::list_itemm::ListItem; +use crate::components::list_item::ListItem; use crate::pages::collection_viewer::CollectionViewer; use crate::renderable::{Eventful, Renderable}; use crate::router::{Navigate, RouterMessage}; @@ -219,8 +219,7 @@ impl Renderable for CollectionList { Ok(Routes::EditCollection) => CollectionListData::EditCollection(self.selected), Ok(Routes::DeleteCollection) => CollectionListData::DeleteCollection(self.selected), Ok(Routes::CollectionViewer) => CollectionListData::DeleteCollection(self.selected), - Ok(Routes::ListCollections) => unreachable!(), - Err(_) => unreachable!(), + _ => unreachable!(), } } diff --git a/hac-client/src/pages/collection_viewer/collection_viewer.rs b/hac-client/src/pages/collection_viewer/collection_viewer.rs index 4be64ef..cccc789 100755 --- a/hac-client/src/pages/collection_viewer/collection_viewer.rs +++ b/hac-client/src/pages/collection_viewer/collection_viewer.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; use std::collections::HashMap; use std::ops::Div; use std::rc::Rc; -use std::sync::mpsc::Sender; +use std::sync::mpsc::{channel, Sender}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::command::Command; @@ -14,7 +14,9 @@ use ratatui::Frame; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use super::request_uri::{RequestUri, RequestUriEvent}; +use super::sidebar::create_request_form::CreateRequestForm; use super::sidebar::{Sidebar, SidebarEvent}; +use crate::app::Routes; use crate::pages::collection_list::CollectionListData; //use crate::pages::collection_viewer::collection_store::{CollectionStore, CollectionStoreAction}; //use crate::pages::collection_viewer::request_editor::{RequestEditor, RequestEditorEvent}; @@ -22,7 +24,8 @@ use crate::pages::collection_list::CollectionListData; //use crate::pages::collection_viewer::response_viewer::{ResponseViewer, ResponseViewerEvent}; //use crate::pages::collection_viewer::sidebar::{self, Sidebar, SidebarEvent}; use crate::renderable::{Eventful, Renderable}; -use crate::{HacColors, HacConfig}; +use crate::router::RouterMessage; +use crate::{router_add_dialog, router_navigate_to, HacColors, HacConfig}; #[derive(Debug, PartialEq)] pub struct ExplorerLayout { @@ -32,6 +35,7 @@ pub struct ExplorerLayout { pub req_editor: Rect, pub response_preview: Rect, pub create_req_form: Rect, + pub total_size: Rect, } #[derive(Debug, PartialEq, Eq, Clone)] @@ -83,6 +87,7 @@ pub struct CollectionViewer { selection: Option, request_uri: RequestUri, sidebar: Sidebar, + messager: Sender, //response_viewer: ResponseViewer<'cv>, //request_editor: RequestEditor<'cv>, @@ -114,6 +119,7 @@ impl CollectionViewer { selection: None, request_uri: RequestUri::new(colors.clone(), layout.req_uri), sidebar, + messager: channel().0, //request_editor, //response_viewer, @@ -407,6 +413,10 @@ impl Renderable for CollectionViewer { //self.response_viewer.resize(new_layout.response_preview); self.layout = new_layout; } + + fn attach_navigator(&mut self, messager: Sender) { + self.messager = messager; + } } impl Eventful for CollectionViewer { @@ -456,13 +466,15 @@ impl Eventful for CollectionViewer { self.update_selection(None); self.focus_prev(); } + Some(SidebarEvent::CreateRequest) => { + tracing::trace!("opening create request form from sidebar"); + let create_request_form = CreateRequestForm::new(self.colors.clone(), self.layout.total_size); + router_add_dialog!(&self.messager, Routes::CreateRequest, create_request_form); + router_navigate_to!(&self.messager, Routes::CreateRequest); + } Some(SidebarEvent::RemoveSelection) => self.update_selection(None), Some(e) => tracing::trace!("{e:?}"), None => (), - //Some(SidebarEvent::CreateRequest) => self - // .collection_store - // .borrow_mut() - // .push_overlay(CollectionViewerOverlay::CreateRequest), //Some(SidebarEvent::EditRequest) => self // .collection_store // .borrow_mut() @@ -547,5 +559,6 @@ pub fn build_layout(size: Rect) -> ExplorerLayout { req_editor, response_preview, create_req_form, + total_size: size, } } diff --git a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs index b84d834..4243a39 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs @@ -1,212 +1,248 @@ -use std::cell::RefCell; -use std::ops::Sub; use std::rc::Rc; -use std::sync::{Arc, RwLock}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use hac_core::collection::types::*; -use rand::Rng; +use hac_core::command::Command; +use hac_store::collection::ReqMethod; +use ratatui::layout::{Constraint, Flex, Layout, Margin, Rect}; +use ratatui::style::{Style, Stylize}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Block, Borders, Paragraph}; use ratatui::Frame; -use super::request_form::{FormField, RequestForm, RequestFormCreate, RequestFormEvent}; -use super::select_request_parent::{SelectRequestParent, SelectRequestParentEvent}; -use super::RequestFormTrait; use crate::ascii::LOGO_ASCII; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::collection_viewer::collection_viewer::CollectionViewerOverlay; -use crate::pages::{Eventful, Renderable}; - -impl<'rf> RequestFormTrait for RequestForm<'rf, RequestFormCreate> { - fn draw_overlay(&mut self, frame: &mut Frame, overlay: CollectionViewerOverlay) -> anyhow::Result<()> { - if let CollectionViewerOverlay::SelectParentDir = overlay { - self.parent_selector.draw(frame, frame.size())?; - } +use crate::components::input::Input; +use crate::pages::overlay::make_overlay; +use crate::renderable::{Eventful, Renderable}; +use crate::{HacColors, MIN_HEIGHT, MIN_WIDTH}; + +#[derive(Debug)] +struct CreateReqFormLayout { + name: Rect, + hint: Rect, + logo: Rect, + parent: Rect, + methods: Rc<[Rect]>, +} - Ok(()) - } +#[derive(Debug, Default, PartialEq, Eq)] +enum FieldFocus { + #[default] + Name, + Methods, + Parent, } -impl<'rf> RequestForm<'rf, RequestFormCreate> { - pub fn new(colors: &'rf hac_colors::Colors, collection_store: Rc>) -> Self { - RequestForm { - colors, - parent_selector: SelectRequestParent::new(colors, collection_store.clone()), - collection_store, - request_name: String::default(), - request_method: RequestMethod::Get, - parent_dir: None, - focused_field: FormField::Name, - marker: std::marker::PhantomData, - request: None, - no_available_parent_timer: None, +impl FieldFocus { + pub fn next(&mut self) { + match self { + FieldFocus::Name => *self = FieldFocus::Methods, + FieldFocus::Methods => *self = FieldFocus::Parent, + FieldFocus::Parent => *self = FieldFocus::Name, } } -} - -impl Eventful for RequestForm<'_, RequestFormCreate> { - type Result = RequestFormEvent; - #[tracing::instrument(skip_all, err)] - fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - let overlay = self.collection_store.borrow_mut().peek_overlay(); - - if overlay.eq(&CollectionViewerOverlay::SelectParentDir) { - match self.parent_selector.handle_key_event(key_event)? { - Some(SelectRequestParentEvent::Confirm(dir_id)) => { - let mut store = self.collection_store.borrow_mut(); - let collection = store - .get_collection() - .expect("tried attach a parent to a request without having a collection"); - let collection = collection.borrow(); - let requests = collection - .requests - .as_ref() - .expect("tried to attach a parent to a request with empty collection"); - let dir_name = requests - .read() - .unwrap() - .iter() - .find(|req| req.get_id().eq(&dir_id)) - .as_ref() - // its safe to unwrap here as to have an id we for sure have the directory - .unwrap() - .get_name(); - self.parent_dir = Some((dir_id, dir_name)); - store.pop_overlay(); - } - Some(SelectRequestParentEvent::Cancel) => { - self.collection_store.borrow_mut().pop_overlay(); - } - None => {} - } - return Ok(None); + pub fn prev(&mut self) { + match self { + FieldFocus::Name => *self = FieldFocus::Parent, + FieldFocus::Methods => *self = FieldFocus::Name, + FieldFocus::Parent => *self = FieldFocus::Methods, } + } +} - if let KeyCode::Tab = key_event.code { - self.focused_field = self.focused_field.next(); - return Ok(None); - } +#[derive(Debug)] +pub struct CreateRequestForm { + colors: HacColors, + layout: CreateReqFormLayout, + name: String, + method: ReqMethod, + focus: FieldFocus, +} - if let KeyCode::BackTab = key_event.code { - self.focused_field = self.focused_field.prev(); - return Ok(None); +impl CreateRequestForm { + pub fn new(colors: HacColors, area: Rect) -> Self { + Self { + colors, + layout: build_layout(area), + name: Default::default(), + method: Default::default(), + focus: Default::default(), } + } - if let (KeyCode::Char('p'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - self.parent_dir = None; - return Ok(None); + fn make_contextual_hint(&self) -> impl Iterator { + match self.focus { + FieldFocus::Name => vec![ + "Enter".fg(self.colors.bright.green).bold(), + " - Confirm • ".fg(self.colors.bright.black), + "Esc".fg(self.colors.bright.green).bold(), + " - Cancel • ".fg(self.colors.bright.black), + "Tab".fg(self.colors.bright.green).bold(), + " - Next • ".fg(self.colors.bright.black), + "S-Tab".fg(self.colors.bright.green).bold(), + " - Prev • ".fg(self.colors.bright.black), + "Ctrl p".fg(self.colors.bright.green).bold(), + " - Parent".fg(self.colors.bright.black), + ] + .into_iter(), + FieldFocus::Methods => vec![ + "Enter".fg(self.colors.bright.green).bold(), + " - Confirm • ".fg(self.colors.bright.black), + "Esc".fg(self.colors.bright.green).bold(), + " - Cancel • ".fg(self.colors.bright.black), + "Tab".fg(self.colors.bright.green).bold(), + " - Next • ".fg(self.colors.bright.black), + "S-Tab".fg(self.colors.bright.green).bold(), + " - Prev • ".fg(self.colors.bright.black), + "1-5".fg(self.colors.bright.green).bold(), + " - Method".fg(self.colors.bright.black), + ] + .into_iter(), + FieldFocus::Parent => vec![ + "Enter".fg(self.colors.bright.green).bold(), + " - Confirm • ".fg(self.colors.bright.black), + "Esc".fg(self.colors.bright.green).bold(), + " - Cancel • ".fg(self.colors.bright.black), + "1-5".fg(self.colors.bright.green).bold(), + " - Change Method • ".fg(self.colors.bright.black), + "p".fg(self.colors.bright.green).bold(), + " - Select Parent".fg(self.colors.bright.black), + ] + .into_iter(), } + } +} - if let KeyCode::Enter = key_event.code { - let store = self.collection_store.borrow_mut(); - let collection = store - .get_collection() - .expect("tried to create a request without a collection"); - - let mut collection = collection.borrow_mut(); - let requests = collection.requests.get_or_insert(Arc::new(RwLock::new(vec![]))); - let mut requests = requests.write().unwrap(); - - if self.request_name.is_empty() { - self.request_name = String::from("unnamed request"); +impl Renderable for CreateRequestForm { + type Input = (); + type Output = (); + + fn data(&self, _: u8) -> Self::Output {} + + fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { + make_overlay(self.colors.clone(), self.colors.normal.black, 0.15, frame); + + let border_style = match self.focus == FieldFocus::Name { + true => Style::new().fg(self.colors.normal.white), + false => Style::new().fg(self.colors.bright.black), + }; + let label = String::from("Request Name"); + let name_input = Input::new(Some(&self.name), Some(&label), self.colors.clone()) + .border_style(border_style) + .value_style(Style::default().fg(self.colors.normal.white)) + .label_style(Style::default().fg(self.colors.bright.black)); + + let hint = self.make_contextual_hint(); + + let logo = Paragraph::new( + LOGO_ASCII + .iter() + .map(|line| Line::from(line.to_string()).fg(self.colors.bright.red).centered()) + .collect::>(), + ); + + for (idx, method) in ReqMethod::iter().enumerate() { + let selected = method == self.method; + let number_color = match selected { + true => self.colors.bright.red, + false => self.colors.bright.black, + }; + let area = self.layout.methods[idx]; + let method = method.to_string(); + let remaining_width = area.width as usize - 3 - method.len(); + let left_pad = remaining_width / 2; + + let parts = vec![ + (idx + 1).to_string().fg(number_color), + " ".repeat(left_pad).into(), + method.fg(self.colors.normal.white), + ]; + + let mut block = Block::new() + .borders(Borders::ALL) + .border_style(Style::new().fg(self.colors.bright.black)); + if let FieldFocus::Methods = self.focus { + block = block.border_style(Style::new().fg(self.colors.normal.white)); } - - let request = RequestKind::Single(Arc::new(RwLock::new(Request { - id: uuid::Uuid::new_v4().to_string(), - auth_method: None, - body: None, - body_type: None, - parent: self.parent_dir.as_ref().map(|(id, _)| id.clone()), - headers: None, - method: self.request_method.clone(), - name: self.request_name.clone(), - uri: String::default(), - }))); - - if let Some((dir_id, _)) = self.parent_dir.as_ref() { - if let RequestKind::Nested(dir) = requests.iter_mut().find(|req| req.get_id().eq(dir_id)).unwrap() { - dir.requests.write().unwrap().push(request); - } - } else { - requests.push(request); + if selected { + block = block.border_style(Style::new().fg(self.colors.bright.red)); } - drop(store); - self.reset(); - return Ok(Some(RequestFormEvent::Confirm)); + frame.render_widget(Paragraph::new(Line::from(parts)).block(block), area); } - if let KeyCode::Esc = key_event.code { - self.reset(); - return Ok(Some(RequestFormEvent::Cancel)); + frame.render_widget(name_input, self.layout.name); + frame.render_widget(logo, self.layout.logo); + frame.render_widget(Line::from(hint.collect::>()).centered(), self.layout.hint); + + if let FieldFocus::Name = self.focus { + frame.set_cursor(self.layout.name.x + 1 + self.name.len() as u16, self.layout.name.y + 2); } + Ok(()) + } +} + +impl Eventful for CreateRequestForm { + type Result = Command; + + fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - self.reset(); - return Ok(Some(RequestFormEvent::Cancel)); + return Ok(Some(Command::Quit)); } - match self.focused_field { - FormField::Name => match key_event.code { - KeyCode::Char(c) => { - self.request_name.push(c); - } - KeyCode::Backspace => { - self.request_name.pop(); - } - _ => {} - }, - FormField::Method => match key_event.code { - KeyCode::Char(c @ '1'..='5') => { - self.request_method = (c.to_digit(10).unwrap() as usize).sub(1).try_into()?; - } - KeyCode::Left => self.request_method = self.request_method.prev(), - KeyCode::Down => self.request_method = 4.try_into()?, - KeyCode::Up => self.request_method = 0.try_into()?, - KeyCode::Right => self.request_method = self.request_method.next(), - KeyCode::Char('h') => self.request_method = self.request_method.prev(), - KeyCode::Char('j') => self.request_method = 4.try_into()?, - KeyCode::Char('k') => self.request_method = 0.try_into()?, - KeyCode::Char('l') => self.request_method = self.request_method.next(), - _ => {} - }, - FormField::Parent => { - if let KeyCode::Char(' ') = key_event.code { - let mut store = self.collection_store.borrow_mut(); - let collection = store - .get_collection() - .expect("tried to select a parent without a collection"); - let collection = collection.borrow(); - - let Some(requests) = collection.requests.as_ref() else { - drop(store); - self.set_no_parent_timer(); - return Ok(None); - }; - - let requests = requests.read().unwrap(); - if requests.is_empty() { - drop(store); - self.set_no_parent_timer(); - return Ok(None); - } - - if requests - .iter() - .filter(|req| req.is_dir()) - .collect::>() - .is_empty() - { - drop(store); - self.set_no_parent_timer(); - return Ok(None); - } - - store.push_overlay(CollectionViewerOverlay::SelectParentDir); - } + match key_event.code { + KeyCode::Char(ch) if matches!(self.focus, FieldFocus::Name) => self.name.push(ch), + KeyCode::Backspace if matches!(self.focus, FieldFocus::Name) => _ = self.name.pop(), + + KeyCode::Char(ch @ '1'..='5') if matches!(self.focus, FieldFocus::Methods) => { + self.method = ReqMethod::from(ch) } - } + KeyCode::Left if matches!(self.focus, FieldFocus::Methods) => self.method.set_prev(), + KeyCode::Right if matches!(self.focus, FieldFocus::Methods) => self.method.set_next(), + KeyCode::Up if matches!(self.focus, FieldFocus::Methods) => self.method.set_first(), + KeyCode::Down if matches!(self.focus, FieldFocus::Methods) => self.method.set_last(), + + KeyCode::Tab => self.focus.next(), + KeyCode::BackTab => self.focus.prev(), + _ => {} + }; Ok(None) } } + +fn build_layout(area: Rect) -> CreateReqFormLayout { + let [_, form, _] = Layout::horizontal([Constraint::Fill(1), Constraint::Length(MIN_WIDTH), Constraint::Fill(1)]) + .flex(Flex::Center) + .areas(area); + + let [_, form, _] = Layout::vertical([Constraint::Fill(1), Constraint::Length(MIN_HEIGHT), Constraint::Fill(1)]) + .flex(Flex::End) + .areas(form); + + let form = form.inner(&Margin::new(2, 0)); + + let [logo, _, name, _, methods, _, parent, hint] = Layout::vertical([ + Constraint::Length(LOGO_ASCII.len() as u16), + Constraint::Length(1), + Constraint::Length(4), + Constraint::Length(1), + Constraint::Length(3), + Constraint::Length(1), + Constraint::Length(3), + Constraint::Length(1), + ]) + .flex(Flex::Center) + .areas(form); + + let methods = Layout::horizontal((0..ReqMethod::size()).map(|_| Constraint::Fill(1))).split(methods); + + CreateReqFormLayout { + name, + hint, + logo, + methods, + parent, + } +} diff --git a/hac-client/src/pages/collection_viewer/sidebar.rs b/hac-client/src/pages/collection_viewer/sidebar/mod.rs similarity index 85% rename from hac-client/src/pages/collection_viewer/sidebar.rs rename to hac-client/src/pages/collection_viewer/sidebar/mod.rs index c753589..c962226 100755 --- a/hac-client/src/pages/collection_viewer/sidebar.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/mod.rs @@ -1,5 +1,5 @@ +pub mod create_request_form; // mod create_directory_form; -// mod create_request_form; // mod delete_item_prompt; // mod directory_form; // mod edit_directory_form; @@ -8,7 +8,7 @@ // mod select_request_parent; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use hac_store::collection::{EntryStatus, ReqMethod, ReqTreeNode, WhichSlab}; +use hac_store::collection::{self, EntryStatus, ReqMethod, ReqTreeNode, WhichSlab}; use ratatui::layout::Rect; use ratatui::style::{Style, Styled, Stylize}; use ratatui::text::{Line, Span}; @@ -170,36 +170,43 @@ impl Renderable for Sidebar { }), ReqTreeNode::Folder(folder_key, requests) => { hac_store::collection::get_folder(folder_key, |folder, status| { - let mut folder_name = folder.name.clone(); - if self.config.borrow().enable_icons { - folder_name = format!("{} {}", Icons::FOLDER, folder_name); - } + let folder_name = folder.name.clone(); let style = match status { EntryStatus::None => Style::new().fg(self.colors.normal.yellow).bold(), _ => Style::new().fg(self.colors.normal.yellow).underlined().italic().bold(), }; - let name = Line::from(folder_name).set_style(style); + let name = Line::from(vec![ + format!("{} ", Icons::FOLDER).bold().fg(self.colors.normal.yellow), + folder_name.set_style(style), + ]); lines.push(name); + + if folder.collapsed { + return; + } + + for request in requests { + hac_store::collection::get_request(request, |req, status| { + let name = req.name.clone(); + let method = &req.method; + + let style = match status { + EntryStatus::None => Style::new().fg(self.colors.normal.white), + EntryStatus::Hovered => Style::new().fg(self.colors.bright.blue).underlined().italic(), + EntryStatus::Selected => Style::new().fg(self.colors.normal.red).bold(), + EntryStatus::Both => { + Style::new().fg(self.colors.normal.red).underlined().italic().bold() + } + }; + + lines.push(Line::from(vec![ + " ".repeat(self.config.borrow().tab_size).into(), + colored_method(method, &self.colors), + name.set_style(style), + ])); + }); + } }); - for request in requests { - hac_store::collection::get_request(request, |req, status| { - let name = req.name.clone(); - let method = &req.method; - - let style = match status { - EntryStatus::None => Style::new().fg(self.colors.normal.white), - EntryStatus::Hovered => Style::new().fg(self.colors.bright.blue).underlined().italic(), - EntryStatus::Selected => Style::new().fg(self.colors.normal.red).bold(), - EntryStatus::Both => Style::new().fg(self.colors.normal.red).underlined().italic().bold(), - }; - - lines.push(Line::from(vec![ - " ".repeat(self.config.borrow().tab_size).into(), - colored_method(method, &self.colors), - name.set_style(style), - ])); - }); - } } }); @@ -356,32 +363,18 @@ impl Eventful for Sidebar { KeyCode::Tab => return Ok(Some(SidebarEvent::SelectNext)), KeyCode::BackTab => return Ok(Some(SidebarEvent::SelectPrev)), KeyCode::Esc => return Ok(Some(SidebarEvent::RemoveSelection)), - KeyCode::Char('j') | KeyCode::Down => hac_store::collection::hover_next(), - KeyCode::Char('k') | KeyCode::Up => hac_store::collection::hover_prev(), + KeyCode::Char('j') | KeyCode::Down => collection::hover_next(), + KeyCode::Char('k') | KeyCode::Up => collection::hover_prev(), + KeyCode::Char('n') => return Ok(Some(SidebarEvent::CreateRequest)), + KeyCode::Enter => { + if let Some((which, key)) = collection::get_hovered_request(|req| req).flatten() { + match which { + WhichSlab::Requests | WhichSlab::RootRequests => collection::select_request((which, key)), + WhichSlab::Folders => collection::toggle_dir(key), + } + }; + } _ => (), - // KeyCode::Enter => { - // if store.get_requests().is_none() || store.get_hovered_request().is_none() { - // return Ok(None); - // } - // - // let request = store.find_hovered_request(); - // match request { - // RequestKind::Nested(_) => { - // store.dispatch(CollectionStoreAction::ToggleDirectory(request.get_id())); - // } - // RequestKind::Single(req) => { - // store.dispatch(CollectionStoreAction::SetSelectedRequest(Some(req))); - // return Ok(Some(SidebarEvent::RebuildView)); - // } - // } - // } - // KeyCode::Char('n') => { - // self.request_form = RequestFormVariant::Create(RequestForm::::new( - // self.colors, - // self.collection_store.clone(), - // )); - // return Ok(Some(SidebarEvent::CreateRequest)); - // } // KeyCode::Char('e') => { // let hovered_request = store.find_hovered_request(); // drop(store); diff --git a/hac-client/src/pages/collection_viewer/sidebar/request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/request_form.rs index dfe461a..a657a62 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/request_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/request_form.rs @@ -1,9 +1,6 @@ -use std::cell::RefCell; use std::ops::{Add, Div, Sub}; -use std::rc::Rc; use std::sync::{Arc, RwLock}; -use hac_core::collection::types::{Request, RequestMethod}; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::Stylize; use ratatui::text::Line; @@ -11,11 +8,9 @@ use ratatui::widgets::{Block, Borders, Paragraph}; use ratatui::Frame; use crate::ascii::LOGO_ASCII; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::collection_viewer::sidebar::select_request_parent::SelectRequestParent; use crate::pages::input::Input; use crate::pages::overlay::make_overlay_old; -use crate::pages::Renderable; +use crate::renderable::Renderable; #[derive(Debug)] pub enum RequestFormEvent { @@ -57,7 +52,6 @@ pub struct RequestFormEdit; #[derive(Debug)] pub struct RequestForm<'rf, State = RequestFormCreate> { pub colors: &'rf hac_colors::Colors, - pub collection_store: Rc>, /// the name of the current request being edited or created pub request_name: String, diff --git a/hac-client/src/pages/overlay.rs b/hac-client/src/pages/overlay.rs index a506b39..1456740 100755 --- a/hac-client/src/pages/overlay.rs +++ b/hac-client/src/pages/overlay.rs @@ -1,38 +1,9 @@ -use ratatui::layout::Rect; -use ratatui::style::{Color, Stylize}; -use ratatui::text::Line; -use ratatui::widgets::Paragraph; +use ratatui::style::Color; use ratatui::Frame; use crate::utils::blend_colors_multiply; use crate::HacColors; -/// draws a fullscreen overlay with the given fill text, many pages uses this to display -/// "floating" information -pub fn draw_overlay_old(colors: &hac_colors::Colors, size: Rect, fill_text: &str, frame: &mut Frame) { - let lines: Vec> = vec![fill_text.repeat(size.width.into()).into(); size.height.into()]; - - let overlay = Paragraph::new(lines) - .fg(colors.primary.hover) - .bg(colors.primary.background) - .bold(); - - frame.render_widget(overlay, size); -} - -pub fn make_overlay_old(colors: &hac_colors::Colors, color: Color, alpha: f32, frame: &mut Frame) { - let buffer = frame.buffer_mut(); - let cells = &mut buffer.content; - - cells.iter_mut().for_each(|cell| { - let cell_fg = cell.style().fg.unwrap_or(colors.normal.white); - let cell_bg = cell.style().bg.unwrap_or(colors.primary.background); - - cell.set_fg(blend_colors_multiply(cell_fg, color, alpha)); - cell.set_bg(blend_colors_multiply(cell_bg, color, alpha)); - }); -} - pub fn make_overlay(colors: HacColors, color: Color, alpha: f32, frame: &mut Frame) { let buffer = frame.buffer_mut(); let cells = &mut buffer.content; diff --git a/hac-client/src/router.rs b/hac-client/src/router.rs index 19f90bf..28e6e0f 100644 --- a/hac-client/src/router.rs +++ b/hac-client/src/router.rs @@ -29,6 +29,24 @@ pub enum Navigate { Back, } +#[macro_export] +macro_rules! router_add_dialog { + ($messager:expr, $route:expr, $renderable:expr) => { + $messager + .send(RouterMessage::AddDialog($route.into(), Box::new($renderable))) + .expect("failed to send router message"); + }; +} + +#[macro_export] +macro_rules! router_navigate_to { + ($messager:expr, $route:expr) => { + $messager + .send(RouterMessage::Navigate($crate::router::Navigate::To($route.into()))) + .expect("failed to send router message"); + }; +} + pub trait AnyCommand {} impl AnyCommand for Command {} @@ -96,7 +114,6 @@ impl + Debug> AnyRenderable pub enum RouterMessage { AddRoute(Key, Box>), AddDialog(Key, Box>), - DelRoute(Key), DelDialog(Key), Navigate(Navigate), } @@ -191,7 +208,6 @@ impl Router { } else if self.dialogs.contains_key(&route) { self.dialog_stack.push(route); self.history.push(route); - tracing::debug!("navigating to -> {} {}", self.history.len(), self.dialog_stack.len()); let dialog = self .get_active_dialog() .expect("attempt to navigate to non registered dialog"); @@ -219,7 +235,6 @@ impl Router { let curr_is_dialog = self.dialogs.contains_key(&curr); let prev_is_dialog = self.dialogs.contains_key(&prev); - tracing::debug!("im really confused, {} {}", self.history.len(), self.dialog_stack.len()); match (curr_is_dialog, prev_is_dialog) { // we are on a route, navigating to a route @@ -273,11 +288,6 @@ impl Renderable for Router { match self.message_receiver.try_recv() { Ok(RouterMessage::AddRoute(key, route)) => self.add_route(key, route), Ok(RouterMessage::AddDialog(key, route)) => self.add_dialog(key, route), - Ok(RouterMessage::DelRoute(key)) => { - tracing::trace!("dropping route with key: {key}"); - self.history.retain(|&k| k != key); - self.routes.remove(&key); - } Ok(RouterMessage::DelDialog(key)) => { tracing::trace!("dropping dialog with key: {key}"); self.dialog_stack.retain(|&k| k != key); diff --git a/hac-client/src/utils.rs b/hac-client/src/utils.rs index 17f4552..70e0d39 100755 --- a/hac-client/src/utils.rs +++ b/hac-client/src/utils.rs @@ -1,7 +1,6 @@ -use hac_core::syntax::highlighter::HIGHLIGHTER; - use std::ops::Add; +use hac_core::syntax::highlighter::HIGHLIGHTER; use ratatui::style::{Color, Stylize}; use ratatui::text::{Line, Span}; use tree_sitter::Tree; @@ -152,8 +151,8 @@ pub fn build_syntax_highlighted_lines( /// eg: /// /// ```rust -/// use ratatui::prelude::Color; /// use hac_client::utils::blend_colors_multiply; +/// use ratatui::prelude::Color; /// /// blend_colors_multiply(Color::Rgb(255, 255, 25), Color::Rgb(0, 0, 0), 0.5); /// ``` diff --git a/hac-loader/src/collection_loader/json_collection.rs b/hac-loader/src/collection_loader/json_collection.rs index d275a49..59de473 100644 --- a/hac-loader/src/collection_loader/json_collection.rs +++ b/hac-loader/src/collection_loader/json_collection.rs @@ -10,19 +10,17 @@ pub struct JsonInfo { #[derive(Debug, Serialize, Deserialize)] pub struct JsonCollection { - pub info: JsonCollectionInfo, pub requests: Vec, #[serde(skip)] pub file_info: JsonInfo, } impl JsonCollection { - pub fn new

(name: String, description: String, file_name: String, path: P) -> Self + pub fn new

(file_name: String, path: P) -> Self where P: AsRef, { Self { - info: JsonCollectionInfo { name, description }, file_info: JsonInfo { name: file_name, path: path.as_ref().to_path_buf(), @@ -32,12 +30,6 @@ impl JsonCollection { } } -#[derive(Debug, Serialize, Deserialize)] -pub struct JsonCollectionInfo { - pub name: String, - pub description: String, -} - #[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] pub enum ReqKind { diff --git a/hac-loader/src/collection_loader/json_loader.rs b/hac-loader/src/collection_loader/json_loader.rs index a88e366..810edf1 100644 --- a/hac-loader/src/collection_loader/json_loader.rs +++ b/hac-loader/src/collection_loader/json_loader.rs @@ -1,10 +1,10 @@ use hac_store::collection::{ - BodyKind, Collection, CollectionInfo, Folder, HeaderEntry, ReqMethod, ReqTree, ReqTreeNode, Request, WhichSlab, + BodyKind, Collection, Folder, HeaderEntry, ReqMethod, ReqTree, ReqTreeNode, Request, WhichSlab, }; use hac_store::slab::{Key, Slab}; use super::json_collection::{ - JsonBodyKind, JsonCollection, JsonCollectionInfo, JsonFolder, JsonHeaderEntry, JsonReqMethod, JsonRequest, ReqKind, + JsonBodyKind, JsonCollection, JsonFolder, JsonHeaderEntry, JsonReqMethod, JsonRequest, ReqKind, }; use super::{IntoCollection, Result}; @@ -61,7 +61,6 @@ fn from_file_to_collection(file_collection: JsonCollection) -> Collection { let layout = ReqTree { nodes }; Collection { - info: file_collection.info.into(), folders, requests, root_requests, @@ -134,15 +133,6 @@ impl From for Request { } } -impl From for CollectionInfo { - fn from(collection_info: JsonCollectionInfo) -> Self { - Self { - name: collection_info.name, - description: collection_info.description, - } - } -} - impl From for Folder { fn from(file_folder: JsonFolder) -> Self { Self { diff --git a/hac-loader/src/collection_loader/mod.rs b/hac-loader/src/collection_loader/mod.rs index 7058bbd..6436307 100644 --- a/hac-loader/src/collection_loader/mod.rs +++ b/hac-loader/src/collection_loader/mod.rs @@ -53,7 +53,7 @@ fn create_persistent_colletion(name: String) -> Result<()> { let file_name = format!("{}.json", sanitize_filename(&name)); tracing::debug!("creating new collection {file_name} on disk"); let path = super::collections_dir().join(&file_name); - let collection = json_collection::JsonCollection::new(name, Default::default(), file_name, &path); + let collection = json_collection::JsonCollection::new(file_name, &path); let strigified = serde_json::to_string_pretty(&collection).expect("invalid collection format to be stringified"); fs::write(&path, strigified).map_err(|_| Error::Create("failed to write collection to disk".into()))?; Ok(()) diff --git a/hac-store/src/collection.rs b/hac-store/src/collection.rs index d60a225..772bd62 100644 --- a/hac-store/src/collection.rs +++ b/hac-store/src/collection.rs @@ -42,7 +42,6 @@ pub enum WhichSlab { #[derive(Debug)] pub struct Collection { - pub info: CollectionInfo, pub requests: Slab, pub root_requests: Slab, pub folders: Slab, @@ -57,14 +56,9 @@ impl Collection { } } -#[derive(Debug)] -pub struct CollectionInfo { - pub name: String, - pub description: String, -} - -#[derive(Debug)] +#[derive(Debug, Default, PartialEq, Eq)] pub enum ReqMethod { + #[default] Get, Post, Put, @@ -72,6 +66,64 @@ pub enum ReqMethod { Delete, } +impl ReqMethod { + pub fn size() -> usize { + 5 + } + + pub fn iter() -> impl Iterator { + [ + ReqMethod::Get, + ReqMethod::Post, + ReqMethod::Put, + ReqMethod::Patch, + ReqMethod::Delete, + ] + .into_iter() + } + + pub fn set_first(&mut self) { + *self = ReqMethod::Get; + } + + pub fn set_last(&mut self) { + *self = ReqMethod::Delete; + } + + pub fn set_next(&mut self) { + match self { + ReqMethod::Get => *self = ReqMethod::Post, + ReqMethod::Post => *self = ReqMethod::Put, + ReqMethod::Put => *self = ReqMethod::Patch, + ReqMethod::Patch => *self = ReqMethod::Delete, + ReqMethod::Delete => *self = ReqMethod::Get, + } + } + + pub fn set_prev(&mut self) { + match self { + ReqMethod::Get => *self = ReqMethod::Delete, + ReqMethod::Post => *self = ReqMethod::Get, + ReqMethod::Put => *self = ReqMethod::Post, + ReqMethod::Patch => *self = ReqMethod::Put, + ReqMethod::Delete => *self = ReqMethod::Patch, + } + } +} + +impl From for ReqMethod { + fn from(value: char) -> Self { + match value { + '1' => ReqMethod::Get, + '2' => ReqMethod::Post, + '3' => ReqMethod::Put, + '4' => ReqMethod::Patch, + '5' => ReqMethod::Delete, + _ => unreachable!(), + } + } +} + impl Display for ReqMethod { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -275,8 +327,9 @@ pub fn hover_next() { return; } - // when the folder has requests, we hover the first one - if !folder.requests.is_empty() { + // when the folder has requests, we hover the first one, unless the folder + // is collapsed + if !folder.requests.is_empty() && !folder.collapsed { collection.hovered_request = Some((WhichSlab::Requests, folder.requests[0])); } @@ -327,13 +380,17 @@ pub fn hover_prev() { } WhichSlab::Folders => { // when we are hovering a folder, we try to hover the previous folders's last - // request if it exists, otherwise we select the previous folder itself + // request if it exists and the folder is not collapsed, otherwise we select + // the previous folder itself if key > 0 { let folder = collection.folders.get(key - 1); - if let Some(last) = folder.requests.last() { - collection.hovered_request = Some((WhichSlab::Requests, *last)); - return; - }; + + if !folder.collapsed { + if let Some(last) = folder.requests.last() { + collection.hovered_request = Some((WhichSlab::Requests, *last)); + return; + }; + } collection.hovered_request = Some((WhichSlab::Folders, key - 1)); return; @@ -360,3 +417,32 @@ pub fn hover_prev() { } }) } + +pub fn get_hovered_request(f: F) -> Option +where + F: FnOnce(Option<(WhichSlab, Key)>) -> R, +{ + HAC_STORE.with_borrow(|store| { + if let Some(ref collection) = store.collection { + return Some(f(collection.hovered_request)); + }; + None + }) +} + +pub fn select_request((which, key): (WhichSlab, Key)) { + HAC_STORE.with_borrow_mut(|store| { + if let Some(collection) = &mut store.collection { + collection.selected_request = Some((which, key)); + } + }); +} + +pub fn toggle_dir(key: Key) { + HAC_STORE.with_borrow_mut(|store| { + if let Some(collection) = &mut store.collection { + let folder = collection.folders.get_mut(key); + folder.collapsed = !folder.collapsed; + } + }); +} From 28e2ccadb246cb1fd6bde93278bad0066765d313 Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Thu, 3 Oct 2024 21:45:30 -0300 Subject: [PATCH 17/23] feat: blending list for parent selection --- hac-client/src/app.rs | 40 +++++-- hac-client/src/components/blending_list.rs | 65 +++++++++++ hac-client/src/components/input.rs | 19 ++- hac-client/src/components/mod.rs | 1 + hac-client/src/event_pool.rs | 38 ++---- hac-client/src/icons.rs | 3 +- .../collection_list/create_collection.rs | 3 - .../sidebar/create_request_form.rs | 109 ++++++++++++++---- .../pages/collection_viewer/sidebar/mod.rs | 15 +-- hac-client/src/pages/input.rs | 6 +- hac-client/src/pages/overlay.rs | 6 +- hac-client/src/utils.rs | 2 +- hac-colors/src/colors.rs | 13 +-- hac-store/src/collection.rs | 11 ++ 14 files changed, 229 insertions(+), 102 deletions(-) create mode 100644 hac-client/src/components/blending_list.rs diff --git a/hac-client/src/app.rs b/hac-client/src/app.rs index 6d3028c..c193653 100755 --- a/hac-client/src/app.rs +++ b/hac-client/src/app.rs @@ -1,5 +1,10 @@ use std::io::Stdout; use std::sync::mpsc::{Receiver, Sender}; +use std::time::{Duration, Instant}; + +pub trait A { + fn asda() -> T; +} use hac_core::command::Command; use ratatui::backend::CrosstermBackend; @@ -85,7 +90,7 @@ impl App { router.add_route(Routes::ListCollections.into(), Box::new(collection_list)); Ok(Self { - event_pool: EventPool::new(FRAME_RATE, TICK_RATE), + event_pool: EventPool::default(), should_quit: false, router, command_receiver, @@ -98,10 +103,16 @@ impl App { /// to pass them down the chain, and render the terminal screen pub async fn run(&mut self) -> anyhow::Result<()> { self.event_pool.start(); - startup()?; + let render_delay = Duration::from_secs_f64(1.0 / FRAME_RATE); + let tick_delay = Duration::from_secs_f64(1.0 / TICK_RATE); + let frame_delta = Instant::now(); + let mut tick_delta = Instant::now(); + loop { + let frame_start = Instant::now(); + { while let Ok(command) = self.command_receiver.try_recv() { match command { @@ -113,7 +124,6 @@ impl App { if let Some(event) = self.event_pool.next_event() { match event { - Event::Tick => self.router.tick()?, Event::Resize(new_size) => self.router.resize(new_size), Event::Key(_) => { if let Some(command) = self.router.handle_event(Some(event.clone()))? { @@ -122,18 +132,24 @@ impl App { .expect("failed to send command through channel") } } - Event::Render => { - self.terminal.draw(|f| { - if let Err(e) = self.router.draw(f, f.size()) { - self.command_sender - .send(Command::Error(format!("Failed to draw: {:?}", e))) - .expect("failed to send command through channel"); - } - })?; - } } } + if frame_start.duration_since(tick_delta) >= tick_delay { + self.router.tick()?; + tick_delta = frame_start; + } + + if frame_start.duration_since(frame_delta) >= render_delay { + self.terminal.draw(|f| { + if let Err(e) = self.router.draw(f, f.size()) { + self.command_sender + .send(Command::Error(format!("Failed to draw: {:?}", e))) + .expect("failed to send command through channel"); + } + })?; + } + if self.should_quit { break; } diff --git a/hac-client/src/components/blending_list.rs b/hac-client/src/components/blending_list.rs new file mode 100644 index 0000000..f2b03b7 --- /dev/null +++ b/hac-client/src/components/blending_list.rs @@ -0,0 +1,65 @@ +use std::rc::Rc; + +use ratatui::layout::{Constraint, Layout, Rect}; +use ratatui::style::Stylize; +use ratatui::widgets::{Block, Padding, Paragraph}; +use ratatui::Frame; + +use crate::utils::alpha_blend_multiply; +use crate::HacColors; + +#[derive(Debug)] +pub struct BlendingList { + selected: usize, + scroll: usize, + amount: usize, + colors: HacColors, + padding: u16, +} + +impl BlendingList { + pub fn new(selected: usize, amount: usize, padding: u16, colors: HacColors) -> Self { + Self { + selected, + amount, + scroll: 0, + colors, + padding, + } + } + + pub fn draw_with<'a, T: 'a, E: Fn(&'a T) -> O, O: AsRef>( + &mut self, + frame: &mut Frame, + items: impl Iterator, + extractor: E, + area: Rect, + ) { + let areas = Layout::vertical((0..self.amount).map(|_| Constraint::Length(1 + self.padding))).split(area); + let blend_max = 0.2; + let blend_step = 0.8 / self.amount as f32; + + for (i, item) in items.skip(self.scroll).take(self.amount).enumerate() { + let name = extractor(item); + let mut color = alpha_blend_multiply( + self.colors.normal.white, + self.colors.normal.black, + f32::max(1.0 - blend_step * i as f32, blend_max), + ); + + if self.selected == i { + color = self.colors.normal.red; + } + + let mut block = Block::new(); + if self.padding > 0 { + block = block.padding(Padding::top(self.padding)); + } + + frame.render_widget( + Paragraph::new(name.as_ref().fg(color)).centered().block(block), + areas[i], + ); + } + } +} diff --git a/hac-client/src/components/input.rs b/hac-client/src/components/input.rs index 22daa06..97f00dd 100644 --- a/hac-client/src/components/input.rs +++ b/hac-client/src/components/input.rs @@ -3,8 +3,8 @@ use std::borrow::Cow; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::Style; -use ratatui::text::Line; -use ratatui::widgets::{Block, Borders, Paragraph, Widget}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Block, Borders, Clear, Paragraph, Widget}; use crate::HacColors; @@ -25,10 +25,10 @@ impl<'input> Input<'input> { { Self { value: value.map(Into::into), - value_style: Style::default(), + value_style: Style::default().fg(colors.normal.white), label: label.map(Into::into), - label_style: Style::default(), - border_style: Style::default().fg(colors.normal.white).bg(colors.normal.black), + label_style: Style::default().fg(colors.bright.black), + border_style: Style::default().fg(colors.normal.white), colors, } } @@ -84,9 +84,7 @@ impl Widget for Input<'_> { if self.has_label() { let size = Rect::new(area.x, area.y, area.width, 1); let label = self.label.as_ref().unwrap().to_string(); - let fill = size.width as usize - label.len(); - let label = format!("{}{}", label, " ".repeat(fill)); - let label = Line::from(label).style(self.label_style); + let label = Line::from(Span::from(label).style(self.label_style)); label.render(size, buf); area.y += 1; area.height -= 1; @@ -99,9 +97,8 @@ impl Widget for Input<'_> { let value = self.value.as_ref().map(|v| v.to_string()).unwrap_or_default(); let size = Rect::new(area.x, area.y, area.width, 3); - let fill = size.width as usize - value.len(); - let value = format!("{}{}", value, " ".repeat(fill)); - let value = Line::from(value).style(self.value_style); + let value = Line::from(Span::from(value).style(self.value_style)); + Clear.render(size, buf); Paragraph::new(value).block(block).render(size, buf); } } diff --git a/hac-client/src/components/mod.rs b/hac-client/src/components/mod.rs index b3910f3..8a4654f 100644 --- a/hac-client/src/components/mod.rs +++ b/hac-client/src/components/mod.rs @@ -1,2 +1,3 @@ +pub mod blending_list; pub mod input; pub mod list_item; diff --git a/hac-client/src/event_pool.rs b/hac-client/src/event_pool.rs index 15a6f9b..187ec8d 100755 --- a/hac-client/src/event_pool.rs +++ b/hac-client/src/event_pool.rs @@ -1,5 +1,3 @@ -use std::ops::Div; - use crossterm::event::{Event as CrosstermEvent, KeyEventKind}; use futures::{FutureExt, StreamExt}; use ratatui::layout::Rect; @@ -8,8 +6,6 @@ use ratatui::layout::Rect; pub enum Event { Key(crossterm::event::KeyEvent), Resize(Rect), - Tick, - Render, } /// Core component responsible for pooling events from crossterm and sending @@ -18,36 +14,28 @@ pub enum Event { pub struct EventPool { event_rx: std::sync::mpsc::Receiver, event_tx: std::sync::mpsc::Sender, - frame_rate: f64, - tick_rate: f64, +} + +impl Default for EventPool { + fn default() -> Self { + Self::new() + } } impl EventPool { - pub fn new(frame_rate: f64, tick_rate: f64) -> Self { + pub fn new() -> Self { let (event_tx, event_rx) = std::sync::mpsc::channel(); - - EventPool { - event_rx, - event_tx, - frame_rate, - tick_rate, - } + EventPool { event_rx, event_tx } } #[cfg_attr(test, mutants::skip)] pub fn start(&mut self) { - let render_delay = std::time::Duration::from_secs_f64(1.0.div(self.frame_rate)); - let tick_delay = std::time::Duration::from_secs_f64(1.0.div(self.tick_rate)); - let event_tx = self.event_tx.clone(); + tokio::spawn(async move { let mut reader = crossterm::event::EventStream::new(); - let mut render_interval = tokio::time::interval(render_delay); - let mut tick_interval = tokio::time::interval(tick_delay); loop { - let render_delay = render_interval.tick(); - let tick_delay = tick_interval.tick(); let crossterm_event = reader.next().fuse(); tokio::select! { @@ -64,12 +52,6 @@ impl EventPool { _ => {} } } - _ = tick_delay => { - event_tx.send(Event::Tick).expect("failed to send event through channel"); - }, - _ = render_delay => { - event_tx.send(Event::Render).expect("failed to send event through channel"); - }, } } }); @@ -77,6 +59,6 @@ impl EventPool { #[cfg_attr(test, mutants::skip)] pub fn next_event(&mut self) -> Option { - self.event_rx.recv().ok() + self.event_rx.try_recv().ok() } } diff --git a/hac-client/src/icons.rs b/hac-client/src/icons.rs index dd4ca93..6e3dc49 100644 --- a/hac-client/src/icons.rs +++ b/hac-client/src/icons.rs @@ -2,5 +2,6 @@ pub struct Icons; #[rustfmt::skip] impl Icons { - pub const FOLDER: char = '\u{e5ff}'; //  + pub const FOLDER: char = '\u{f07b}'; //  + pub const FOLDER_OPEN: char = '\u{f07c}'; //  } diff --git a/hac-client/src/pages/collection_list/create_collection.rs b/hac-client/src/pages/collection_list/create_collection.rs index e7fea1f..ed47d53 100644 --- a/hac-client/src/pages/collection_list/create_collection.rs +++ b/hac-client/src/pages/collection_list/create_collection.rs @@ -61,12 +61,9 @@ impl Renderable for CreateCollection { make_overlay(self.colors.clone(), self.colors.normal.black, 0.2, frame); draw_form_layout(self.layout, self.name.to_string(), &self.colors, frame); set_form_cursor(self.layout, &self.cursor, frame); - Ok(()) } - fn update(&mut self, _data: Self::Input) {} - fn resize(&mut self, new_size: Rect) { self.size = new_size; self.layout = build_form_layout(new_size); diff --git a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs index 4243a39..b2087fb 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs @@ -2,14 +2,16 @@ use std::rc::Rc; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::command::Command; -use hac_store::collection::ReqMethod; +use hac_store::collection::{Folder, ReqMethod, ReqTreeNode}; +use hac_store::slab::Key; use ratatui::layout::{Constraint, Flex, Layout, Margin, Rect}; -use ratatui::style::{Style, Stylize}; +use ratatui::style::{Color, Style, Stylize}; use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, Borders, Paragraph}; +use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph}; use ratatui::Frame; use crate::ascii::LOGO_ASCII; +use crate::components::blending_list::BlendingList; use crate::components::input::Input; use crate::pages::overlay::make_overlay; use crate::renderable::{Eventful, Renderable}; @@ -22,6 +24,7 @@ struct CreateReqFormLayout { logo: Rect, parent: Rect, methods: Rc<[Rect]>, + parent_listing: Rect, } #[derive(Debug, Default, PartialEq, Eq)] @@ -50,6 +53,12 @@ impl FieldFocus { } } +#[derive(Debug)] +enum FormStep { + MainForm, + ParentSelector, +} + #[derive(Debug)] pub struct CreateRequestForm { colors: HacColors, @@ -57,16 +66,22 @@ pub struct CreateRequestForm { name: String, method: ReqMethod, focus: FieldFocus, + parent: Option, + form_step: FormStep, + parent_listing: BlendingList, } impl CreateRequestForm { pub fn new(colors: HacColors, area: Rect) -> Self { Self { - colors, layout: build_layout(area), name: Default::default(), method: Default::default(), focus: Default::default(), + parent: None, + form_step: FormStep::MainForm, + parent_listing: BlendingList::new(0, 6, 1, colors.clone()), + colors, } } @@ -105,23 +120,14 @@ impl CreateRequestForm { " - Cancel • ".fg(self.colors.bright.black), "1-5".fg(self.colors.bright.green).bold(), " - Change Method • ".fg(self.colors.bright.black), - "p".fg(self.colors.bright.green).bold(), - " - Select Parent".fg(self.colors.bright.black), + "Ctrl p".fg(self.colors.bright.green).bold(), + " - Parent".fg(self.colors.bright.black), ] .into_iter(), } } -} - -impl Renderable for CreateRequestForm { - type Input = (); - type Output = (); - - fn data(&self, _: u8) -> Self::Output {} - - fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { - make_overlay(self.colors.clone(), self.colors.normal.black, 0.15, frame); + fn draw_main_form(&mut self, frame: &mut Frame) { let border_style = match self.focus == FieldFocus::Name { true => Style::new().fg(self.colors.normal.white), false => Style::new().fg(self.colors.bright.black), @@ -132,8 +138,6 @@ impl Renderable for CreateRequestForm { .value_style(Style::default().fg(self.colors.normal.white)) .label_style(Style::default().fg(self.colors.bright.black)); - let hint = self.make_contextual_hint(); - let logo = Paragraph::new( LOGO_ASCII .iter() @@ -171,16 +175,62 @@ impl Renderable for CreateRequestForm { frame.render_widget(Paragraph::new(Line::from(parts)).block(block), area); } + let hint = self.make_contextual_hint(); + let parent_name = "No Parent".to_string(); + let parent = Paragraph::new(Line::from(parent_name).centered()) + .block(Block::new().borders(Borders::ALL).fg(self.colors.normal.white)); + frame.render_widget(name_input, self.layout.name); frame.render_widget(logo, self.layout.logo); + frame.render_widget(Clear, self.layout.parent); + frame.render_widget(parent, self.layout.parent); frame.render_widget(Line::from(hint.collect::>()).centered(), self.layout.hint); if let FieldFocus::Name = self.focus { - frame.set_cursor(self.layout.name.x + 1 + self.name.len() as u16, self.layout.name.y + 2); + frame.set_cursor( + self.layout.name.x + 1 + self.name.chars().count() as u16, + self.layout.name.y + 2, + ); + } + } + + fn draw_parent_selector(&mut self, frame: &mut Frame) { + let logo = Paragraph::new( + LOGO_ASCII + .iter() + .map(|line| Line::from(line.to_string()).fg(self.colors.bright.red).centered()) + .collect::>(), + ); + + let mut folders = vec![]; + hac_store::collection::folders(|folder| folders.push(folder.name.clone())); + self.parent_listing + .draw_with(frame, folders.iter(), |name| name, self.layout.parent_listing); + + frame.render_widget(logo, self.layout.logo); + } +} + +impl Renderable for CreateRequestForm { + type Input = (); + type Output = (); + + fn data(&self, _: u8) -> Self::Output {} + + fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { + make_overlay(self.colors.clone(), self.colors.normal.black, 0.15, frame); + + match self.form_step { + FormStep::MainForm => self.draw_main_form(frame), + FormStep::ParentSelector => self.draw_parent_selector(frame), } Ok(()) } + + fn resize(&mut self, new_size: Rect) { + self.layout = build_layout(new_size); + } } impl Eventful for CreateRequestForm { @@ -192,16 +242,20 @@ impl Eventful for CreateRequestForm { } match key_event.code { + KeyCode::Char('p') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { + self.form_step = FormStep::ParentSelector; + } + KeyCode::Char(ch) if matches!(self.focus, FieldFocus::Name) => self.name.push(ch), KeyCode::Backspace if matches!(self.focus, FieldFocus::Name) => _ = self.name.pop(), - KeyCode::Char(ch @ '1'..='5') if matches!(self.focus, FieldFocus::Methods) => { - self.method = ReqMethod::from(ch) - } KeyCode::Left if matches!(self.focus, FieldFocus::Methods) => self.method.set_prev(), KeyCode::Right if matches!(self.focus, FieldFocus::Methods) => self.method.set_next(), KeyCode::Up if matches!(self.focus, FieldFocus::Methods) => self.method.set_first(), KeyCode::Down if matches!(self.focus, FieldFocus::Methods) => self.method.set_last(), + KeyCode::Char(ch @ '1'..='5') if matches!(self.focus, FieldFocus::Methods) => { + self.method = ReqMethod::from(ch) + } KeyCode::Tab => self.focus.next(), KeyCode::BackTab => self.focus.prev(), @@ -236,6 +290,16 @@ fn build_layout(area: Rect) -> CreateReqFormLayout { .flex(Flex::Center) .areas(form); + let [_, _, parent_listing, _, _] = Layout::vertical([ + Constraint::Length(LOGO_ASCII.len() as u16), + Constraint::Length(1), + Constraint::Length(13), + Constraint::Length(1), + Constraint::Length(1), + ]) + .flex(Flex::Center) + .areas(form); + let methods = Layout::horizontal((0..ReqMethod::size()).map(|_| Constraint::Fill(1))).split(methods); CreateReqFormLayout { @@ -244,5 +308,6 @@ fn build_layout(area: Rect) -> CreateReqFormLayout { logo, methods, parent, + parent_listing, } } diff --git a/hac-client/src/pages/collection_viewer/sidebar/mod.rs b/hac-client/src/pages/collection_viewer/sidebar/mod.rs index c962226..84a941b 100755 --- a/hac-client/src/pages/collection_viewer/sidebar/mod.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/mod.rs @@ -163,10 +163,7 @@ impl Renderable for Sidebar { EntryStatus::Both => Style::new().fg(self.colors.normal.red).underlined().italic().bold(), }; - lines.push(Line::from(vec![ - colored_method(method, &self.colors), - name.set_style(style), - ])); + lines.push(Line::default().spans([colored_method(method, &self.colors), name.set_style(style)])); }), ReqTreeNode::Folder(folder_key, requests) => { hac_store::collection::get_folder(folder_key, |folder, status| { @@ -175,8 +172,12 @@ impl Renderable for Sidebar { EntryStatus::None => Style::new().fg(self.colors.normal.yellow).bold(), _ => Style::new().fg(self.colors.normal.yellow).underlined().italic().bold(), }; - let name = Line::from(vec![ - format!("{} ", Icons::FOLDER).bold().fg(self.colors.normal.yellow), + let icon = match folder.collapsed { + true => Icons::FOLDER, + false => Icons::FOLDER_OPEN, + }; + let name = Line::default().spans([ + format!("{icon} ").bold().fg(self.colors.normal.yellow), folder_name.set_style(style), ]); lines.push(name); @@ -199,7 +200,7 @@ impl Renderable for Sidebar { } }; - lines.push(Line::from(vec![ + lines.push(Line::default().spans([ " ".repeat(self.config.borrow().tab_size).into(), colored_method(method, &self.colors), name.set_style(style), diff --git a/hac-client/src/pages/input.rs b/hac-client/src/pages/input.rs index 7abdef3..305b6d0 100755 --- a/hac-client/src/pages/input.rs +++ b/hac-client/src/pages/input.rs @@ -50,11 +50,7 @@ impl<'a> Input<'a> { (value, color) }; - let without_space = format!( - "{value}{}", - " ".repeat(size.width.saturating_sub(value.len() as u16) as usize) - ); - Paragraph::new(without_space) + Paragraph::new(value) .block( Block::default() .title(self.name.clone()) diff --git a/hac-client/src/pages/overlay.rs b/hac-client/src/pages/overlay.rs index 1456740..a6fad21 100755 --- a/hac-client/src/pages/overlay.rs +++ b/hac-client/src/pages/overlay.rs @@ -1,7 +1,7 @@ use ratatui::style::Color; use ratatui::Frame; -use crate::utils::blend_colors_multiply; +use crate::utils::alpha_blend_multiply; use crate::HacColors; pub fn make_overlay(colors: HacColors, color: Color, alpha: f32, frame: &mut Frame) { @@ -12,7 +12,7 @@ pub fn make_overlay(colors: HacColors, color: Color, alpha: f32, frame: &mut Fra let cell_fg = cell.style().fg.unwrap_or(colors.normal.white); let cell_bg = cell.style().bg.unwrap_or(colors.primary.background); - cell.set_fg(blend_colors_multiply(cell_fg, color, alpha)); - cell.set_bg(blend_colors_multiply(cell_bg, color, alpha)); + cell.set_fg(alpha_blend_multiply(cell_fg, color, alpha)); + cell.set_bg(alpha_blend_multiply(cell_bg, color, alpha)); }); } diff --git a/hac-client/src/utils.rs b/hac-client/src/utils.rs index 70e0d39..cdad85c 100755 --- a/hac-client/src/utils.rs +++ b/hac-client/src/utils.rs @@ -163,7 +163,7 @@ pub fn build_syntax_highlighted_lines( /// use ratatui::prelude::Color; /// Color::Rgb(128, 128, 128); /// ``` -pub fn blend_colors_multiply(original: Color, overlay: Color, alpha: f32) -> Color { +pub fn alpha_blend_multiply(original: Color, overlay: Color, alpha: f32) -> Color { let blend_component = |fg, bg| (alpha * fg as f32 + (1.0 - alpha) * bg as f32) as u8; let Some(foreground) = color_to_rgb(original) else { diff --git a/hac-colors/src/colors.rs b/hac-colors/src/colors.rs index ee3a2a5..17cca83 100755 --- a/hac-colors/src/colors.rs +++ b/hac-colors/src/colors.rs @@ -61,14 +61,8 @@ fn token_highlight() -> HashMap { tokens.insert("boolean".into(), Style::new().fg(colors.red)); tokens.insert("number".into(), Style::new().fg(colors.magenta)); tokens.insert("property".into(), Style::new().fg(colors.yellow)); - tokens.insert( - "punctuation.bracket".into(), - Style::new().fg(colors.magenta), - ); - tokens.insert( - "punctuation.delimiter".into(), - Style::new().fg(colors.magenta), - ); + tokens.insert("punctuation.bracket".into(), Style::new().fg(colors.magenta)); + tokens.insert("punctuation.delimiter".into(), Style::new().fg(colors.magenta)); tokens.insert("string".into(), Style::new().fg(colors.green)); tokens @@ -78,7 +72,8 @@ impl Default for PrimaryColors { fn default() -> Self { PrimaryColors { foreground: Color::Rgb(0x0F, 0x14, 0x19), - background: Color::Rgb(0x18, 0x16, 0x16), + //background: Color::Rgb(0x18, 0x16, 0x16), + background: Color::Reset, accent: Color::Rgb(0xb6, 0x92, 0x7b), hover: Color::Rgb(0x38, 0x38, 0x38), } diff --git a/hac-store/src/collection.rs b/hac-store/src/collection.rs index 772bd62..35efaff 100644 --- a/hac-store/src/collection.rs +++ b/hac-store/src/collection.rs @@ -269,6 +269,17 @@ where }) } +pub fn folders(f: F) +where + F: FnMut(&Folder), +{ + HAC_STORE.with_borrow(|store| { + if let Some(collection) = store.collection.as_ref() { + collection.folders.iter().for_each(f) + } + }) +} + pub fn get_folder(key: Key, f: F) where F: FnOnce(&Folder, EntryStatus), From a8e9b78592fdda4edb9284f8ba134a9e60222fee Mon Sep 17 00:00:00 2001 From: wiru Date: Sat, 5 Oct 2024 21:07:07 -0300 Subject: [PATCH 18/23] feat: creating new requests --- hac-client/src/components/blending_list.rs | 48 ++- .../collection_viewer/collection_viewer.rs | 1 + .../sidebar/create_request_form.rs | 142 ++++-- .../pages/collection_viewer/sidebar/mod.rs | 3 +- hac-client/src/pages/input.rs | 2 +- hac-client/src/router.rs | 28 +- hac-store/src/collection.rs | 406 ++++++++++-------- 7 files changed, 392 insertions(+), 238 deletions(-) diff --git a/hac-client/src/components/blending_list.rs b/hac-client/src/components/blending_list.rs index f2b03b7..0f40e3a 100644 --- a/hac-client/src/components/blending_list.rs +++ b/hac-client/src/components/blending_list.rs @@ -1,7 +1,6 @@ -use std::rc::Rc; - use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::style::Stylize; +use ratatui::text::Line; use ratatui::widgets::{Block, Padding, Paragraph}; use ratatui::Frame; @@ -10,24 +9,46 @@ use crate::HacColors; #[derive(Debug)] pub struct BlendingList { - selected: usize, + pub selected: usize, scroll: usize, - amount: usize, + page_size: usize, + total: usize, colors: HacColors, padding: u16, } impl BlendingList { - pub fn new(selected: usize, amount: usize, padding: u16, colors: HacColors) -> Self { + pub fn new(selected: usize, total: usize, page_size: usize, padding: u16, colors: HacColors) -> Self { Self { selected, - amount, + total, + page_size, scroll: 0, colors, padding, } } + pub fn reset(&mut self) { + self.selected = 0; + self.scroll = 0; + } + + pub fn select_up(&mut self) { + self.selected = self.selected.saturating_sub(1); + if self.selected.saturating_sub(self.scroll) == 0 { + self.scroll = self.scroll - (self.scroll - self.selected); + } + } + + pub fn select_down(&mut self) { + self.selected = usize::min(self.selected + 1, self.total - 1); + if self.selected - self.scroll >= self.page_size.saturating_sub(1) { + let diff = usize::abs_diff(self.selected - self.scroll, self.page_size.saturating_sub(1)); + self.scroll += diff; + } + } + pub fn draw_with<'a, T: 'a, E: Fn(&'a T) -> O, O: AsRef>( &mut self, frame: &mut Frame, @@ -35,19 +56,22 @@ impl BlendingList { extractor: E, area: Rect, ) { - let areas = Layout::vertical((0..self.amount).map(|_| Constraint::Length(1 + self.padding))).split(area); + let areas = Layout::vertical((0..self.page_size).map(|_| Constraint::Length(1 + self.padding))).split(area); let blend_max = 0.2; - let blend_step = 0.8 / self.amount as f32; + let blend_step = 1.0 / self.page_size as f32; - for (i, item) in items.skip(self.scroll).take(self.amount).enumerate() { + for (i, item) in items.skip(self.scroll).take(self.page_size).enumerate() { + let diff_from_selected = usize::abs_diff(i, self.selected - self.scroll); + let blend_step = diff_from_selected as f32 * blend_step; let name = extractor(item); + let mut color = alpha_blend_multiply( self.colors.normal.white, self.colors.normal.black, - f32::max(1.0 - blend_step * i as f32, blend_max), + f32::max(1.0 - blend_step, blend_max), ); - if self.selected == i { + if self.selected - self.scroll == i { color = self.colors.normal.red; } @@ -57,7 +81,7 @@ impl BlendingList { } frame.render_widget( - Paragraph::new(name.as_ref().fg(color)).centered().block(block), + Paragraph::new(Line::from(name.as_ref().fg(color)).centered()).block(block), areas[i], ); } diff --git a/hac-client/src/pages/collection_viewer/collection_viewer.rs b/hac-client/src/pages/collection_viewer/collection_viewer.rs index cccc789..afdcbff 100755 --- a/hac-client/src/pages/collection_viewer/collection_viewer.rs +++ b/hac-client/src/pages/collection_viewer/collection_viewer.rs @@ -5,6 +5,7 @@ use std::rc::Rc; use std::sync::mpsc::{channel, Sender}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use futures::stream::SelectNextSome; use hac_core::command::Command; use hac_core::net::request_manager::Response; use ratatui::layout::{Constraint, Direction, Layout, Rect}; diff --git a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs index b2087fb..935094b 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs @@ -1,21 +1,24 @@ use std::rc::Rc; +use std::sync::mpsc::{channel, Sender}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::command::Command; -use hac_store::collection::{Folder, ReqMethod, ReqTreeNode}; +use hac_store::collection::ReqMethod; use hac_store::slab::Key; use ratatui::layout::{Constraint, Flex, Layout, Margin, Rect}; -use ratatui::style::{Color, Style, Stylize}; +use ratatui::style::{Style, Stylize}; use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph}; +use ratatui::widgets::{Block, Borders, Clear, Paragraph}; use ratatui::Frame; +use crate::app::Routes; use crate::ascii::LOGO_ASCII; use crate::components::blending_list::BlendingList; use crate::components::input::Input; use crate::pages::overlay::make_overlay; use crate::renderable::{Eventful, Renderable}; -use crate::{HacColors, MIN_HEIGHT, MIN_WIDTH}; +use crate::router::RouterMessage; +use crate::{router_drop_dialog, HacColors, MIN_HEIGHT, MIN_WIDTH}; #[derive(Debug)] struct CreateReqFormLayout { @@ -25,6 +28,7 @@ struct CreateReqFormLayout { parent: Rect, methods: Rc<[Rect]>, parent_listing: Rect, + parent_hint: Rect, } #[derive(Debug, Default, PartialEq, Eq)] @@ -69,6 +73,7 @@ pub struct CreateRequestForm { parent: Option, form_step: FormStep, parent_listing: BlendingList, + messager: Sender, } impl CreateRequestForm { @@ -80,8 +85,9 @@ impl CreateRequestForm { focus: Default::default(), parent: None, form_step: FormStep::MainForm, - parent_listing: BlendingList::new(0, 6, 1, colors.clone()), + parent_listing: BlendingList::new(0, hac_store::collection::len_folders(), 13, 0, colors.clone()), colors, + messager: channel().0, } } @@ -118,10 +124,10 @@ impl CreateRequestForm { " - Confirm • ".fg(self.colors.bright.black), "Esc".fg(self.colors.bright.green).bold(), " - Cancel • ".fg(self.colors.bright.black), - "1-5".fg(self.colors.bright.green).bold(), - " - Change Method • ".fg(self.colors.bright.black), "Ctrl p".fg(self.colors.bright.green).bold(), - " - Parent".fg(self.colors.bright.black), + " - Parent • ".fg(self.colors.bright.black), + "Backspace".fg(self.colors.bright.green).bold(), + " - Remove parent".fg(self.colors.bright.black), ] .into_iter(), } @@ -129,7 +135,7 @@ impl CreateRequestForm { fn draw_main_form(&mut self, frame: &mut Frame) { let border_style = match self.focus == FieldFocus::Name { - true => Style::new().fg(self.colors.normal.white), + true => Style::new().fg(self.colors.bright.red), false => Style::new().fg(self.colors.bright.black), }; let label = String::from("Request Name"); @@ -148,7 +154,7 @@ impl CreateRequestForm { for (idx, method) in ReqMethod::iter().enumerate() { let selected = method == self.method; let number_color = match selected { - true => self.colors.bright.red, + true => self.colors.bright.blue, false => self.colors.bright.black, }; let area = self.layout.methods[idx]; @@ -159,26 +165,33 @@ impl CreateRequestForm { let parts = vec![ (idx + 1).to_string().fg(number_color), " ".repeat(left_pad).into(), - method.fg(self.colors.normal.white), + method.fg(number_color), ]; let mut block = Block::new() .borders(Borders::ALL) .border_style(Style::new().fg(self.colors.bright.black)); if let FieldFocus::Methods = self.focus { - block = block.border_style(Style::new().fg(self.colors.normal.white)); + block = block.border_style(Style::new().fg(self.colors.bright.red)); } if selected { - block = block.border_style(Style::new().fg(self.colors.bright.red)); + block = block.border_style(Style::new().fg(self.colors.bright.blue)); } frame.render_widget(Paragraph::new(Line::from(parts)).block(block), area); } let hint = self.make_contextual_hint(); - let parent_name = "No Parent".to_string(); + let mut parent_name = "No Parent".to_string(); + if let Some(parent) = self.parent { + hac_store::collection::get_folder(parent, |folder, _| parent_name.clone_from(&folder.name)); + }; + + let parent_color = + if self.focus == FieldFocus::Parent { self.colors.bright.red } else { self.colors.bright.black }; + let parent = Paragraph::new(Line::from(parent_name).centered()) - .block(Block::new().borders(Borders::ALL).fg(self.colors.normal.white)); + .block(Block::new().borders(Borders::ALL).fg(parent_color)); frame.render_widget(name_input, self.layout.name); frame.render_widget(logo, self.layout.logo); @@ -207,16 +220,80 @@ impl CreateRequestForm { self.parent_listing .draw_with(frame, folders.iter(), |name| name, self.layout.parent_listing); + let hint = vec![ + "Enter".fg(self.colors.bright.green).bold(), + " - Confirm • ".fg(self.colors.bright.black), + "Esc".fg(self.colors.bright.green).bold(), + " - Cancel • ".fg(self.colors.bright.black), + "j/k ↑/↓".fg(self.colors.normal.green), + " - Choose".fg(self.colors.bright.black), + ]; + + frame.render_widget(Line::from(hint).centered(), self.layout.parent_hint); frame.render_widget(logo, self.layout.logo); } + + fn main_form_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + match key_event.code { + KeyCode::Char('p') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { + self.form_step = FormStep::ParentSelector; + } + + KeyCode::Char(ch) if matches!(self.focus, FieldFocus::Name) => self.name.push(ch), + KeyCode::Backspace if matches!(self.focus, FieldFocus::Name) => _ = self.name.pop(), + + KeyCode::Left if matches!(self.focus, FieldFocus::Methods) => self.method.set_prev(), + KeyCode::Right if matches!(self.focus, FieldFocus::Methods) => self.method.set_next(), + KeyCode::Up if matches!(self.focus, FieldFocus::Methods) => self.method.set_first(), + KeyCode::Down if matches!(self.focus, FieldFocus::Methods) => self.method.set_last(), + KeyCode::Char('h') if matches!(self.focus, FieldFocus::Methods) => self.method.set_prev(), + KeyCode::Char('j') if matches!(self.focus, FieldFocus::Methods) => self.method.set_last(), + KeyCode::Char('k') if matches!(self.focus, FieldFocus::Methods) => self.method.set_first(), + KeyCode::Char('l') if matches!(self.focus, FieldFocus::Methods) => self.method.set_next(), + KeyCode::Char(ch @ '1'..='5') if matches!(self.focus, FieldFocus::Methods) => { + self.method = ReqMethod::from(ch) + } + + KeyCode::Backspace if matches!(self.focus, FieldFocus::Parent) => self.parent = None, + + KeyCode::Tab => self.focus.next(), + KeyCode::BackTab => self.focus.prev(), + KeyCode::Esc => { + router_drop_dialog!(&self.messager, Routes::CreateRequest.into()); + } + KeyCode::Enter => { + let request = hac_store::collection::Request::new(self.name.clone(), self.method, self.parent); + hac_store::collection::push_request(request, self.parent); + router_drop_dialog!(&self.messager, Routes::CreateRequest.into()); + } + _ => {} + }; + + Ok(None) + } + + fn parent_selector_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + match key_event.code { + KeyCode::Char('j') | KeyCode::Down => self.parent_listing.select_down(), + KeyCode::Char('k') | KeyCode::Up => self.parent_listing.select_up(), + KeyCode::Enter => { + if hac_store::collection::has_folders() { + self.parent = Some(self.parent_listing.selected); + self.form_step = FormStep::MainForm; + self.parent_listing.reset() + } + } + _ => {} + }; + + Ok(None) + } } impl Renderable for CreateRequestForm { type Input = (); type Output = (); - fn data(&self, _: u8) -> Self::Output {} - fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { make_overlay(self.colors.clone(), self.colors.normal.black, 0.15, frame); @@ -228,6 +305,12 @@ impl Renderable for CreateRequestForm { Ok(()) } + fn data(&self, _: u8) -> Self::Output {} + + fn attach_navigator(&mut self, messager: Sender) { + self.messager = messager; + } + fn resize(&mut self, new_size: Rect) { self.layout = build_layout(new_size); } @@ -241,25 +324,9 @@ impl Eventful for CreateRequestForm { return Ok(Some(Command::Quit)); } - match key_event.code { - KeyCode::Char('p') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { - self.form_step = FormStep::ParentSelector; - } - - KeyCode::Char(ch) if matches!(self.focus, FieldFocus::Name) => self.name.push(ch), - KeyCode::Backspace if matches!(self.focus, FieldFocus::Name) => _ = self.name.pop(), - - KeyCode::Left if matches!(self.focus, FieldFocus::Methods) => self.method.set_prev(), - KeyCode::Right if matches!(self.focus, FieldFocus::Methods) => self.method.set_next(), - KeyCode::Up if matches!(self.focus, FieldFocus::Methods) => self.method.set_first(), - KeyCode::Down if matches!(self.focus, FieldFocus::Methods) => self.method.set_last(), - KeyCode::Char(ch @ '1'..='5') if matches!(self.focus, FieldFocus::Methods) => { - self.method = ReqMethod::from(ch) - } - - KeyCode::Tab => self.focus.next(), - KeyCode::BackTab => self.focus.prev(), - _ => {} + match self.form_step { + FormStep::MainForm => self.main_form_key_event(key_event)?, + FormStep::ParentSelector => self.parent_selector_key_event(key_event)?, }; Ok(None) @@ -290,7 +357,7 @@ fn build_layout(area: Rect) -> CreateReqFormLayout { .flex(Flex::Center) .areas(form); - let [_, _, parent_listing, _, _] = Layout::vertical([ + let [_, _, parent_listing, _, parent_hint] = Layout::vertical([ Constraint::Length(LOGO_ASCII.len() as u16), Constraint::Length(1), Constraint::Length(13), @@ -309,5 +376,6 @@ fn build_layout(area: Rect) -> CreateReqFormLayout { methods, parent, parent_listing, + parent_hint, } } diff --git a/hac-client/src/pages/collection_viewer/sidebar/mod.rs b/hac-client/src/pages/collection_viewer/sidebar/mod.rs index 84a941b..5e2a319 100755 --- a/hac-client/src/pages/collection_viewer/sidebar/mod.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/mod.rs @@ -151,6 +151,7 @@ impl Renderable for Sidebar { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { let layout = hac_store::collection::tree_layout(); let mut lines = vec![]; + layout.nodes.into_iter().for_each(|node| match node { ReqTreeNode::Req(key) => hac_store::collection::get_root_request(key, |req, status| { let name = req.name.clone(); @@ -368,7 +369,7 @@ impl Eventful for Sidebar { KeyCode::Char('k') | KeyCode::Up => collection::hover_prev(), KeyCode::Char('n') => return Ok(Some(SidebarEvent::CreateRequest)), KeyCode::Enter => { - if let Some((which, key)) = collection::get_hovered_request(|req| req).flatten() { + if let Some((which, key)) = collection::get_hovered_request(|req| req) { match which { WhichSlab::Requests | WhichSlab::RootRequests => collection::select_request((which, key)), WhichSlab::Folders => collection::toggle_dir(key), diff --git a/hac-client/src/pages/input.rs b/hac-client/src/pages/input.rs index 305b6d0..00b077b 100755 --- a/hac-client/src/pages/input.rs +++ b/hac-client/src/pages/input.rs @@ -35,7 +35,7 @@ impl<'a> Input<'a> { self.focused = true; } - fn build_input(&self, value: String, size: Rect) -> Paragraph<'_> { + fn build_input(&self, value: String, _: Rect) -> Paragraph<'_> { let border_color = if self.focused { Style::default().fg(self.colors.normal.red) } else { diff --git a/hac-client/src/router.rs b/hac-client/src/router.rs index 28e6e0f..14c42ad 100644 --- a/hac-client/src/router.rs +++ b/hac-client/src/router.rs @@ -38,6 +38,15 @@ macro_rules! router_add_dialog { }; } +#[macro_export] +macro_rules! router_drop_dialog { + ($messager:expr, $dialog:expr) => { + $messager + .send(RouterMessage::DelDialog($dialog)) + .expect("failed to send router message"); + }; +} + #[macro_export] macro_rules! router_navigate_to { ($messager:expr, $route:expr) => { @@ -47,6 +56,15 @@ macro_rules! router_navigate_to { }; } +#[macro_export] +macro_rules! router_navigate_back { + ($messager:expr) => { + $messager + .send(RouterMessage::Navigate($crate::router::Navigate::Back)) + .expect("failed to send router message"); + }; +} + pub trait AnyCommand {} impl AnyCommand for Command {} @@ -128,7 +146,6 @@ pub struct Router { command_sender: Sender, message_receiver: Receiver, message_sender: Sender, - parent_messager: Option>, too_small: TerminalTooSmall, } @@ -144,15 +161,10 @@ impl Router { message_receiver, message_sender, command_sender, - parent_messager: None, too_small: TerminalTooSmall::new(colors.clone()), } } - pub fn message_sender(&self) -> Sender { - self.message_sender.clone() - } - pub fn add_route(&mut self, key: Key, mut route: Box>) { route.attach_navigator(self.message_sender.clone()); route.register_command_handler(self.command_sender.clone()); @@ -171,10 +183,6 @@ impl Router { Ok(()) } - pub fn attach_parent_navigator(&mut self, messager: Sender) { - self.parent_messager = Some(messager); - } - fn get_active_route(&mut self) -> &mut Box> { self.routes .get_mut(&self.active_route) diff --git a/hac-store/src/collection.rs b/hac-store/src/collection.rs index 35efaff..830bd3b 100644 --- a/hac-store/src/collection.rs +++ b/hac-store/src/collection.rs @@ -56,7 +56,7 @@ impl Collection { } } -#[derive(Debug, Default, PartialEq, Eq)] +#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] pub enum ReqMethod { #[default] Get, @@ -143,23 +143,35 @@ pub struct HeaderEntry { pub enabled: bool, } -#[derive(Debug)] +#[derive(Debug, Default)] pub enum BodyKind { - Json, + #[default] NoBody, + Json, } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Request { - pub parent: Option, - pub method: ReqMethod, pub name: String, + pub method: ReqMethod, + pub parent: Option, pub uri: String, pub headers: Vec, pub body: String, pub body_kind: BodyKind, } +impl Request { + pub fn new(name: String, method: ReqMethod, parent: Option) -> Self { + Self { + name, + method, + parent, + ..Default::default() + } + } +} + #[derive(Debug, Default)] pub struct Folder { pub name: String, @@ -176,17 +188,17 @@ where F: FnOnce(&Request) -> Option, { HAC_STORE.with_borrow(|store| { - if let Some(ref collection) = store.collection { - if let Some((slab, key)) = collection.selected_request { - if let Some(request) = match slab { - WhichSlab::Requests => Some(collection.requests.get(key)), - WhichSlab::RootRequests => Some(collection.root_requests.get(key)), - WhichSlab::Folders => None, - } { - return f(request); - } + assert!(store.collection.is_some()); + let collection = store.collection.as_ref().unwrap(); + if let Some((slab, key)) = collection.selected_request { + if let Some(request) = match slab { + WhichSlab::Requests => Some(collection.requests.get(key)), + WhichSlab::RootRequests => Some(collection.root_requests.get(key)), + WhichSlab::Folders => None, + } { + return f(request); } - }; + } None }) @@ -197,17 +209,17 @@ where F: FnOnce(&mut Request), { HAC_STORE.with_borrow_mut(|store| { - if let Some(collection) = &mut store.collection { - if let Some((slab, key)) = collection.selected_request { - if let Some(request) = match slab { - WhichSlab::Requests => Some(collection.requests.get_mut(key)), - WhichSlab::RootRequests => Some(collection.root_requests.get_mut(key)), - WhichSlab::Folders => None, - } { - f(request); - } + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + if let Some((slab, key)) = collection.selected_request { + if let Some(request) = match slab { + WhichSlab::Requests => Some(collection.requests.get_mut(key)), + WhichSlab::RootRequests => Some(collection.root_requests.get_mut(key)), + WhichSlab::Folders => None, + } { + f(request); } - }; + } }) } @@ -224,9 +236,9 @@ pub fn tree_layout() -> ReqTree { let mut nodes = ReqTree { nodes: vec![] }; HAC_STORE.with_borrow(|store| { - if let Some(ref collection) = store.collection { - nodes = collection.layout.clone(); - } + assert!(store.collection.is_some()); + let collection = store.collection.as_ref().unwrap(); + nodes = collection.layout.clone(); }); nodes @@ -237,17 +249,17 @@ where F: FnOnce(&Request, EntryStatus), { HAC_STORE.with_borrow(|store| { - if let Some(ref collection) = store.collection { - let req = collection.root_requests.get(key); - let is_hovered = collection - .hovered_request - .is_some_and(|(slab, k)| matches!(slab, WhichSlab::RootRequests) && key == k); - let is_selected = collection - .selected_request - .is_some_and(|(slab, k)| matches!(slab, WhichSlab::RootRequests) && key == k); - - f(req, (is_hovered, is_selected).into()); - } + assert!(store.collection.is_some()); + let collection = store.collection.as_ref().unwrap(); + let req = collection.root_requests.get(key); + let is_hovered = collection + .hovered_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::RootRequests) && key == k); + let is_selected = collection + .selected_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::RootRequests) && key == k); + + f(req, (is_hovered, is_selected).into()); }) } @@ -256,16 +268,16 @@ where F: FnOnce(&Request, EntryStatus), { HAC_STORE.with_borrow(|store| { - if let Some(ref collection) = store.collection { - let req = collection.requests.get(key); - let is_hovered = collection - .hovered_request - .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Requests) && key == k); - let is_selected = collection - .selected_request - .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Requests) && key == k); - f(req, (is_hovered, is_selected).into()); - } + assert!(store.collection.is_some()); + let collection = store.collection.as_ref().unwrap(); + let req = collection.requests.get(key); + let is_hovered = collection + .hovered_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Requests) && key == k); + let is_selected = collection + .selected_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Requests) && key == k); + f(req, (is_hovered, is_selected).into()); }) } @@ -274,90 +286,98 @@ where F: FnMut(&Folder), { HAC_STORE.with_borrow(|store| { - if let Some(collection) = store.collection.as_ref() { - collection.folders.iter().for_each(f) - } + assert!(store.collection.is_some()); + let collection = store.collection.as_ref().unwrap(); + collection.folders.iter().for_each(f) }) } +pub fn len_folders() -> usize { + HAC_STORE.with_borrow(|store| store.collection.as_ref().map(|c| c.folders.len()).unwrap_or_default()) +} + +pub fn has_folders() -> bool { + len_folders() != 0 +} + pub fn get_folder(key: Key, f: F) where F: FnOnce(&Folder, EntryStatus), { HAC_STORE.with_borrow(|store| { - if let Some(ref collection) = store.collection { - let folder = collection.folders.get(key); - let is_hovered = collection - .hovered_request - .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Folders) && key == k); - let is_selected = collection - .selected_request - .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Folders) && key == k); - f(folder, (is_hovered, is_selected).into()); - } + assert!(store.collection.is_some()); + let collection = store.collection.as_ref().unwrap(); + let folder = collection.folders.get(key); + let is_hovered = collection + .hovered_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Folders) && key == k); + let is_selected = collection + .selected_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Folders) && key == k); + f(folder, (is_hovered, is_selected).into()); }) } pub fn hover_next() { HAC_STORE.with_borrow_mut(|store| { - if let Some(collection) = &mut store.collection { - if let Some((which, key)) = collection.hovered_request { - match which { - WhichSlab::Requests => { - let req = collection.requests.get(key); - let parent_key = req.parent.expect("nested request has no parent"); - let parent = collection.folders.get(parent_key); - let pos = parent - .requests - .iter() - .position(|&req| req == key) - .expect("nested request not listed on the parent"); - - // when the current nested request is not the last on the folder, we hover - // the next request on that folder - if pos < parent.requests.len() - 1 { - collection.hovered_request = Some((WhichSlab::Requests, parent.requests[pos + 1])); - return; - } - - // when it is the last one, we try to move to the next folder if possible - if let Some(next) = collection - .folders - .try_get(parent_key + 1) - .map(|_| (WhichSlab::Folders, parent_key + 1)) - { - collection.hovered_request = Some(next) - }; + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + if let Some((which, key)) = collection.hovered_request { + match which { + WhichSlab::Requests => { + let req = collection.requests.get(key); + let parent_key = req.parent.expect("nested request has no parent"); + let parent = collection.folders.get(parent_key); + let pos = parent + .requests + .iter() + .position(|&req| req == key) + .expect("nested request not listed on the parent"); + + // when the current nested request is not the last on the folder, we hover + // the next request on that folder + if pos < parent.requests.len() - 1 { + collection.hovered_request = Some((WhichSlab::Requests, parent.requests[pos + 1])); + return; } - WhichSlab::Folders => { - let folder = collection.folders.get(key); - // when the folder is collapsed or has no requests, and its not the - // last folder on the collection, we can hover the next folder - if (folder.requests.is_empty() || folder.collapsed) && key < collection.folders.len() - 1 { - collection.hovered_request = Some((WhichSlab::Folders, key + 1)); - return; - } - // when the folder has requests, we hover the first one, unless the folder - // is collapsed - if !folder.requests.is_empty() && !folder.collapsed { - collection.hovered_request = Some((WhichSlab::Requests, folder.requests[0])); - } + // when it is the last one, we try to move to the next folder if possible + if let Some(next) = collection + .folders + .try_get(parent_key + 1) + .map(|_| (WhichSlab::Folders, parent_key + 1)) + { + collection.hovered_request = Some(next) + }; + } + WhichSlab::Folders => { + let folder = collection.folders.get(key); + // when the folder is collapsed or has no requests, and its not the + // last folder on the collection, we can hover the next folder + if (folder.requests.is_empty() || folder.collapsed) && key < collection.folders.len() - 1 { + collection.hovered_request = Some((WhichSlab::Folders, key + 1)); + return; + } - // when the folder has no requests and its the last folder, we don't do - // anything + // when the folder has requests, we hover the first one, unless the folder + // is collapsed + if !folder.requests.is_empty() && !folder.collapsed { + collection.hovered_request = Some((WhichSlab::Requests, folder.requests[0])); } - WhichSlab::RootRequests => { - // when the last root_request is hovered, we try to hover the first folder - // as no request with a parent can appear before its parent - if key >= collection.root_requests.len() - 1 { - collection.hovered_request = collection.folders.try_get(0).map(|_| (WhichSlab::Folders, 0)); - return; - } - // when we are not at the last one, we can simply hover the next - collection.hovered_request = Some((WhichSlab::RootRequests, key + 1)); + // when the folder has no requests and its the last folder, we don't do + // anything + } + WhichSlab::RootRequests => { + // when the last root_request is hovered, we try to hover the first folder + // as no request with a parent can appear before its parent + if key >= collection.root_requests.len() - 1 { + collection.hovered_request = collection.folders.try_get(0).map(|_| (WhichSlab::Folders, 0)); + return; } + + // when we are not at the last one, we can simply hover the next + collection.hovered_request = Some((WhichSlab::RootRequests, key + 1)); } } } @@ -366,62 +386,62 @@ pub fn hover_next() { pub fn hover_prev() { HAC_STORE.with_borrow_mut(|store| { - if let Some(collection) = &mut store.collection { - if let Some((which, key)) = collection.hovered_request { - match which { - WhichSlab::Requests => { - let req = collection.requests.get(key); - let parent_key = req.parent.expect("nested request has no parent"); - let parent = collection.folders.get(parent_key); - let pos = parent - .requests - .iter() - .position(|&req| req == key) - .expect("nested request not listed on the parent"); - - // when the current nested request is not the first on the folder, we hover - // the previous request on that folder - if pos > 0 { - collection.hovered_request = Some((WhichSlab::Requests, parent.requests[pos - 1])); - return; - } - - // when it is the first one, we move to the parent folder itself - collection.hovered_request = Some((WhichSlab::Folders, parent_key)) + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + if let Some((which, key)) = collection.hovered_request { + match which { + WhichSlab::Requests => { + let req = collection.requests.get(key); + let parent_key = req.parent.expect("nested request has no parent"); + let parent = collection.folders.get(parent_key); + let pos = parent + .requests + .iter() + .position(|&req| req == key) + .expect("nested request not listed on the parent"); + + // when the current nested request is not the first on the folder, we hover + // the previous request on that folder + if pos > 0 { + collection.hovered_request = Some((WhichSlab::Requests, parent.requests[pos - 1])); + return; } - WhichSlab::Folders => { - // when we are hovering a folder, we try to hover the previous folders's last - // request if it exists and the folder is not collapsed, otherwise we select - // the previous folder itself - if key > 0 { - let folder = collection.folders.get(key - 1); - - if !folder.collapsed { - if let Some(last) = folder.requests.last() { - collection.hovered_request = Some((WhichSlab::Requests, *last)); - return; - }; - } - - collection.hovered_request = Some((WhichSlab::Folders, key - 1)); - return; - } - // when there are no previous folders we try to hover the last root request - if !collection.root_requests.is_empty() { - let key = collection.root_requests.len() - 1; - collection.hovered_request = Some((WhichSlab::RootRequests, key)); + // when it is the first one, we move to the parent folder itself + collection.hovered_request = Some((WhichSlab::Folders, parent_key)) + } + WhichSlab::Folders => { + // when we are hovering a folder, we try to hover the previous folders's last + // request if it exists and the folder is not collapsed, otherwise we select + // the previous folder itself + if key > 0 { + let folder = collection.folders.get(key - 1); + + if !folder.collapsed { + if let Some(last) = folder.requests.last() { + collection.hovered_request = Some((WhichSlab::Requests, *last)); + return; + }; } - // when there are no root requests or previou folders, we do nothing + collection.hovered_request = Some((WhichSlab::Folders, key - 1)); + return; } - WhichSlab::RootRequests => { - // when we are hovering on a root request, we either hover the preivous or - // do nothing - if key > 0 { - collection.hovered_request = Some((WhichSlab::RootRequests, key - 1)); - } + // when there are no previous folders we try to hover the last root request + if !collection.root_requests.is_empty() { + let key = collection.root_requests.len() - 1; + collection.hovered_request = Some((WhichSlab::RootRequests, key)); + } + + // when there are no root requests or previou folders, we do nothing + } + WhichSlab::RootRequests => { + // when we are hovering on a root request, we either hover the preivous or + // do nothing + + if key > 0 { + collection.hovered_request = Some((WhichSlab::RootRequests, key - 1)); } } } @@ -429,31 +449,63 @@ pub fn hover_prev() { }) } -pub fn get_hovered_request(f: F) -> Option +pub fn get_hovered_request(f: F) -> R where F: FnOnce(Option<(WhichSlab, Key)>) -> R, { HAC_STORE.with_borrow(|store| { - if let Some(ref collection) = store.collection { - return Some(f(collection.hovered_request)); - }; - None + assert!(store.collection.is_some()); + let collection = store.collection.as_ref().unwrap(); + f(collection.hovered_request) }) } pub fn select_request((which, key): (WhichSlab, Key)) { HAC_STORE.with_borrow_mut(|store| { - if let Some(collection) = &mut store.collection { - collection.selected_request = Some((which, key)); - } + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + collection.selected_request = Some((which, key)); }); } pub fn toggle_dir(key: Key) { HAC_STORE.with_borrow_mut(|store| { - if let Some(collection) = &mut store.collection { - let folder = collection.folders.get_mut(key); - folder.collapsed = !folder.collapsed; - } + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + let folder = collection.folders.get_mut(key); + folder.collapsed = !folder.collapsed; }); } + +pub fn push_request(request: Request, parent: Option) { + HAC_STORE.with_borrow_mut(|store| { + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + match parent { + Some(key) => { + let new_key = collection.requests.push(request); + let folder = collection.folders.get_mut(key); + folder.requests.push(new_key); + } + None => _ = collection.root_requests.push(request), + }; + }); + rebuild_tree_layout(); +} + +fn rebuild_tree_layout() { + HAC_STORE.with_borrow_mut(|store| { + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + let mut nodes: Vec = (0..collection.root_requests.len()).map(ReqTreeNode::Req).collect(); + nodes.extend( + collection + .folders + .iter() + .enumerate() + .map(|(idx, folder)| ReqTreeNode::Folder(idx, folder.requests.clone())) + .collect::>(), + ); + collection.layout = ReqTree { nodes }; + }) +} From ff77ed87960abf685820d260d615c8d37183e0d6 Mon Sep 17 00:00:00 2001 From: wiru Date: Sat, 5 Oct 2024 22:26:32 -0300 Subject: [PATCH 19/23] feat: showing better sidebar hints --- .../collection_list/delete_collection.rs | 11 +- .../src/pages/collection_list/form_shared.rs | 2 +- hac-client/src/pages/collection_list/mod.rs | 14 +- .../collection_viewer/collection_viewer.rs | 14 +- .../sidebar/create_request_form.rs | 13 +- .../pages/collection_viewer/sidebar/mod.rs | 296 +++++------------- 6 files changed, 115 insertions(+), 235 deletions(-) diff --git a/hac-client/src/pages/collection_list/delete_collection.rs b/hac-client/src/pages/collection_list/delete_collection.rs index efb6d26..f785946 100644 --- a/hac-client/src/pages/collection_list/delete_collection.rs +++ b/hac-client/src/pages/collection_list/delete_collection.rs @@ -5,7 +5,7 @@ use hac_core::command::Command; use hac_store::collection_meta::CollectionMeta; use ratatui::layout::{Constraint, Flex, Layout, Margin, Rect}; use ratatui::style::Stylize; -use ratatui::text::Line; +use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Paragraph, Wrap}; use ratatui::Frame; @@ -61,12 +61,15 @@ impl Renderable for DeleteCollection { let drawer = |meta: &CollectionMeta| { let name = meta.name().to_string(); - let name = format!("are you sure you want to delete {name}?"); + let name = vec![ + Span::from("are you sure you want to delete "), + Span::from(name).bold().underlined(), + "?".into(), + ]; let name = Paragraph::new(Line::from(name).fg(self.colors.normal.red).centered()).wrap(Wrap { trim: true }); - let left_button = Paragraph::new(Line::from("[ENTER] CONFIRM").centered()) + let left_button = Paragraph::new(Line::from("[ENTER] CONFIRM").centered().fg(self.colors.normal.black)) .block(Block::default().borders(Borders::ALL).fg(self.colors.bright.red)) - .fg(self.colors.normal.black) .bg(self.colors.bright.red) .bold(); diff --git a/hac-client/src/pages/collection_list/form_shared.rs b/hac-client/src/pages/collection_list/form_shared.rs index 7c71022..1f576d3 100644 --- a/hac-client/src/pages/collection_list/form_shared.rs +++ b/hac-client/src/pages/collection_list/form_shared.rs @@ -120,7 +120,7 @@ pub fn draw_form_layout(layout: FormLayout, name: String, colors: &HacColors, fr frame.render_widget(logo, layout.logo); frame.render_widget(name_input, layout.name_input); - frame.render_widget(Line::from(hint), layout.hint); + frame.render_widget(Line::from(hint).centered(), layout.hint); } pub fn set_form_cursor(layout: FormLayout, cursor: &Cursor, frame: &mut Frame) { diff --git a/hac-client/src/pages/collection_list/mod.rs b/hac-client/src/pages/collection_list/mod.rs index 4da1844..12b0dd4 100644 --- a/hac-client/src/pages/collection_list/mod.rs +++ b/hac-client/src/pages/collection_list/mod.rs @@ -166,15 +166,15 @@ impl CollectionList { } else { let hint = vec![ "j/k ↑/↓".fg(self.colors.normal.green), - " - choose • ".fg(self.colors.bright.black), + " - Choose • ".fg(self.colors.bright.black), "n".fg(self.colors.normal.green), - " - new • ".fg(self.colors.bright.black), - "enter".fg(self.colors.normal.green), - " - select • ".fg(self.colors.bright.black), + " - New • ".fg(self.colors.bright.black), + "Enter".fg(self.colors.normal.green), + " - Select • ".fg(self.colors.bright.black), "?".fg(self.colors.normal.green), - " - show more • ".fg(self.colors.bright.black), - "ctrl c".fg(self.colors.normal.green), - " - quit".fg(self.colors.bright.black), + " - Show more • ".fg(self.colors.bright.black), + "Ctrl c".fg(self.colors.normal.green), + " - Quit".fg(self.colors.bright.black), ]; frame.render_widget(Line::from(hint), self.layout.hint_pane); }; diff --git a/hac-client/src/pages/collection_viewer/collection_viewer.rs b/hac-client/src/pages/collection_viewer/collection_viewer.rs index afdcbff..3749211 100755 --- a/hac-client/src/pages/collection_viewer/collection_viewer.rs +++ b/hac-client/src/pages/collection_viewer/collection_viewer.rs @@ -105,7 +105,7 @@ pub struct CollectionViewer { impl CollectionViewer { pub fn new(size: Rect, colors: HacColors, config: HacConfig) -> Self { - let layout = build_layout(size); + let layout = build_layout(size, 1); let (request_tx, response_rx) = unbounded_channel::(); // @@ -409,7 +409,7 @@ impl Renderable for CollectionViewer { } fn resize(&mut self, new_size: Rect) { - let new_layout = build_layout(new_size); + let new_layout = build_layout(new_size, 1); //self.request_editor.resize(new_layout.req_editor); //self.response_viewer.resize(new_layout.response_preview); self.layout = new_layout; @@ -459,11 +459,14 @@ impl Eventful for CollectionViewer { None => {} }, PaneFocus::Sidebar => match self.sidebar.handle_key_event(key_event)? { + Some(SidebarEvent::Quit) => return Ok(Some(Command::Quit)), Some(SidebarEvent::SelectNext) => { + self.layout = build_layout(self.layout.total_size, 1); self.update_selection(None); self.focus_next(); } Some(SidebarEvent::SelectPrev) => { + self.layout = build_layout(self.layout.total_size, 1); self.update_selection(None); self.focus_prev(); } @@ -474,7 +477,8 @@ impl Eventful for CollectionViewer { router_navigate_to!(&self.messager, Routes::CreateRequest); } Some(SidebarEvent::RemoveSelection) => self.update_selection(None), - Some(e) => tracing::trace!("{e:?}"), + Some(SidebarEvent::ShowExtendedHint) => self.layout = build_layout(self.layout.total_size, 3), + Some(SidebarEvent::HideExtendedHint) => self.layout = build_layout(self.layout.total_size, 1), None => (), //Some(SidebarEvent::EditRequest) => self // .collection_store @@ -518,10 +522,10 @@ impl Eventful for CollectionViewer { } } -pub fn build_layout(size: Rect) -> ExplorerLayout { +pub fn build_layout(size: Rect, hint_size: u16) -> ExplorerLayout { let [top_pane, hint_pane] = Layout::default() .direction(Direction::Vertical) - .constraints([Constraint::Fill(1), Constraint::Length(1)]) + .constraints([Constraint::Fill(1), Constraint::Length(hint_size)]) .areas(size); let [sidebar, right_pane] = Layout::default() diff --git a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs index 935094b..69d29c3 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs @@ -91,7 +91,7 @@ impl CreateRequestForm { } } - fn make_contextual_hint(&self) -> impl Iterator { + fn make_contextual_hint(&self) -> Vec { match self.focus { FieldFocus::Name => vec![ "Enter".fg(self.colors.bright.green).bold(), @@ -104,8 +104,7 @@ impl CreateRequestForm { " - Prev • ".fg(self.colors.bright.black), "Ctrl p".fg(self.colors.bright.green).bold(), " - Parent".fg(self.colors.bright.black), - ] - .into_iter(), + ], FieldFocus::Methods => vec![ "Enter".fg(self.colors.bright.green).bold(), " - Confirm • ".fg(self.colors.bright.black), @@ -117,8 +116,7 @@ impl CreateRequestForm { " - Prev • ".fg(self.colors.bright.black), "1-5".fg(self.colors.bright.green).bold(), " - Method".fg(self.colors.bright.black), - ] - .into_iter(), + ], FieldFocus::Parent => vec![ "Enter".fg(self.colors.bright.green).bold(), " - Confirm • ".fg(self.colors.bright.black), @@ -128,8 +126,7 @@ impl CreateRequestForm { " - Parent • ".fg(self.colors.bright.black), "Backspace".fg(self.colors.bright.green).bold(), " - Remove parent".fg(self.colors.bright.black), - ] - .into_iter(), + ], } } @@ -197,7 +194,7 @@ impl CreateRequestForm { frame.render_widget(logo, self.layout.logo); frame.render_widget(Clear, self.layout.parent); frame.render_widget(parent, self.layout.parent); - frame.render_widget(Line::from(hint.collect::>()).centered(), self.layout.hint); + frame.render_widget(Line::from(hint).centered(), self.layout.hint); if let FieldFocus::Name = self.focus { frame.set_cursor( diff --git a/hac-client/src/pages/collection_viewer/sidebar/mod.rs b/hac-client/src/pages/collection_viewer/sidebar/mod.rs index 5e2a319..3e6823f 100755 --- a/hac-client/src/pages/collection_viewer/sidebar/mod.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/mod.rs @@ -16,42 +16,17 @@ use ratatui::widgets::{Block, Borders, Paragraph}; use ratatui::Frame; use crate::icons::Icons; -use crate::pages::collection_viewer::collection_viewer::CollectionViewerOverlay; use crate::renderable::{Eventful, Renderable}; use crate::{HacColors, HacConfig}; -/// set of events Sidebar can emit to the caller when handling events. #[derive(Debug, PartialEq, Eq, Clone)] pub enum SidebarEvent { - /// user pressed `CreateRequest (n)` hotkey, which should notify the caller to open - /// the create_request_form and properly handle the creation of a new request - CreateRequest, - /// user pressed `Edit (e)` hotkey on a request, which should notify the caller to open - /// the edit_request_form and properly handle the editing of the existing request - EditRequest, - /// user pressed `Edit (e)` hotkey on a directory, which should notify the caller to - /// open the `edit_request_form` and properly handle the editing of the current directory - EditDirectory, - /// user pressed `CreateDirectory (d)` hotkey, which should notify the caller to open - /// the `create_directory_form` overlay to create a new directory on the collection - CreateDirectory, - /// user pressed `Esc` so we notify the caller to remove the selection from - /// this pane, essentially bubbling the key handling scope to the caller - RemoveSelection, - /// tells the parent to move selection to the next pane - SelectNext, - /// tells the parent to move selection to the previous pane + ShowExtendedHint, + HideExtendedHint, SelectPrev, - /// event to force a full rebuild of the view, when a request is deleted - RebuildView, - /// this event is used when a request or directory is created, this notify the parent - /// to sync changes with the file system. - SyncCollection, - /// user pressed `DeleteItem (D)` hotkey, which should notify the caller to open the - /// delete_item_prompt to ask the user for confirmation - DeleteItem(String), - /// user pressed a hotkey to quit the application, so we bubble up so the caller - /// can do a few things before bubbling the quit request further up + SelectNext, + RemoveSelection, + CreateRequest, Quit, } @@ -61,32 +36,18 @@ pub struct Sidebar { config: HacConfig, selected: bool, focused: bool, - //request_form: RequestFormVariant<'sbar>, - //directory_form: DirectoryFormVariant<'sbar>, - //delete_item_prompt: DeleteItemPrompt<'sbar>, + show_extended_hint: bool, } impl Sidebar { pub fn new(colors: HacColors, config: HacConfig) -> Self { - let mut sidebar = Self { + Self { colors, config, selected: false, focused: false, - //request_form: RequestFormVariant::Create(RequestForm::::new( - // colors, - // collection_store.clone(), - //)), - //directory_form: DirectoryFormVariant::Create(DirectoryForm::::new( - // colors, - // collection_store.clone(), - //)), - //delete_item_prompt: DeleteItemPrompt::new(colors, collection_store.clone()), - }; - - sidebar.rebuild_tree_view(); - - sidebar + show_extended_hint: false, + } } pub fn focus(&mut self) { @@ -105,42 +66,66 @@ impl Sidebar { self.selected = false; } - pub fn rebuild_tree_view(&mut self) { - //let mut collection_store = self.collection_store.borrow_mut(); - //self.lines = build_lines( - // collection_store.get_requests(), - // 0, - // collection_store.get_selected_request(), - // collection_store.get_hovered_request(), - // collection_store.get_dirs_expanded().unwrap().clone(), - // self.colors, - //); - } - - pub fn draw_overlay(&mut self, _frame: &mut Frame, _overlay: CollectionViewerOverlay) -> anyhow::Result<()> { - //match overlay { - // CollectionViewerOverlay::CreateRequest => { - // self.request_form.inner().draw(frame, frame.size())?; - // } - // CollectionViewerOverlay::EditRequest => { - // self.request_form.inner().draw(frame, frame.size())?; - // } - // CollectionViewerOverlay::SelectParentDir => { - // self.request_form.inner().draw_overlay(frame, overlay)?; - // } - // CollectionViewerOverlay::CreateDirectory => { - // self.directory_form.inner().draw(frame, frame.size())?; - // } - // CollectionViewerOverlay::EditDirectory => { - // self.directory_form.inner().draw(frame, frame.size())?; - // } - // CollectionViewerOverlay::DeleteSidebarItem(_) => { - // self.delete_item_prompt.draw(frame, frame.size())?; - // } - // _ => {} - //}; + fn draw_hint(&self, frame: &mut Frame, size: Rect) { + if !self.selected { + let hint = vec![ + "Enter".fg(self.colors.bright.green).bold(), + " - Select • ".fg(self.colors.bright.black), + "Tab".fg(self.colors.bright.green).bold(), + " - Next • ".fg(self.colors.bright.black), + "S-Tab".fg(self.colors.bright.green).bold(), + " - Prev".fg(self.colors.bright.black), + ]; + let size = Rect::new(size.x, size.height - 1, size.width, 1); + frame.render_widget(Line::from(hint).centered(), size); + return; + } - Ok(()) + if self.show_extended_hint { + let lines = vec![ + Line::from(vec![ + "j/k ↑/↓".fg(self.colors.normal.green), + " - Choose • ".fg(self.colors.bright.black), + "n".fg(self.colors.normal.green), + " - New request • ".fg(self.colors.bright.black), + "Enter".fg(self.colors.normal.green), + " - Select request".fg(self.colors.bright.black), + ]), + Line::from(vec![ + "?".fg(self.colors.normal.green), + " - Show less • ".fg(self.colors.bright.black), + "Ctrl c".fg(self.colors.normal.green), + " - Quit • ".fg(self.colors.bright.black), + "d".fg(self.colors.normal.green), + " - Delete item".fg(self.colors.bright.black), + ]), + Line::from(vec![ + "e".fg(self.colors.normal.green), + " - Edit • ".fg(self.colors.bright.black), + "Tab".fg(self.colors.normal.green), + " - Next • ".fg(self.colors.bright.black), + "S-Tab".fg(self.colors.normal.green), + " - Prev".fg(self.colors.bright.black), + ]), + ]; + let size = Rect::new(size.x + 1, size.height - 3, size.width - 2, 3); + frame.render_widget(Paragraph::new(lines), size); + } else { + let hint = vec![ + "Enter".fg(self.colors.bright.green).bold(), + " - Select Request • ".fg(self.colors.bright.black), + "Esc".fg(self.colors.bright.green).bold(), + " - Deselect • ".fg(self.colors.bright.black), + "Tab".fg(self.colors.bright.green).bold(), + " - Next • ".fg(self.colors.bright.black), + "S-Tab".fg(self.colors.bright.green).bold(), + " - Prev • ".fg(self.colors.bright.black), + "?".fg(self.colors.bright.green).bold(), + " - Help".fg(self.colors.bright.black), + ]; + let size = Rect::new(size.x, size.height - 1, size.width, 1); + frame.render_widget(Line::from(hint).centered(), size); + } } } @@ -223,6 +208,11 @@ impl Renderable for Sidebar { let size = Rect::new(size.x + 1, size.y + 1, size.width - 2, size.height - 2); frame.render_widget(Paragraph::new(lines), size); + if self.selected || self.focused { + let size = frame.size(); + self.draw_hint(frame, size); + } + Ok(()) } @@ -239,128 +229,6 @@ impl Eventful for Sidebar { return Ok(Some(SidebarEvent::Quit)); } - //let is_selected = self - // .collection_store - // .borrow() - // .get_selected_pane() - // .is_some_and(|pane| pane.eq(&PaneFocus::Sidebar)); - //assert!(is_selected, "handled an event to the sidebar while it was not selected"); - // - //let overlay = self.collection_store.borrow_mut().peek_overlay(); - // - //match overlay { - // CollectionViewerOverlay::CreateRequest => match self.request_form.inner().handle_key_event(key_event)? { - // Some(RequestFormEvent::Confirm) => { - // let mut store = self.collection_store.borrow_mut(); - // store.pop_overlay(); - // drop(store); - // self.rebuild_tree_view(); - // return Ok(Some(SidebarEvent::SyncCollection)); - // } - // Some(RequestFormEvent::Cancel) => { - // let mut store = self.collection_store.borrow_mut(); - // store.pop_overlay(); - // drop(store); - // self.rebuild_tree_view(); - // return Ok(None); - // } - // None => return Ok(None), - // }, - // CollectionViewerOverlay::SelectParentDir => { - // let result = self.request_form.inner().handle_key_event(key_event)?; - // assert!( - // result.is_none(), - // "should never return an event when selecting parent dir" - // ); - // return Ok(None); - // } - // CollectionViewerOverlay::CreateDirectory => { - // match self.directory_form.inner().handle_key_event(key_event)? { - // Some(DirectoryFormEvent::Confirm) => { - // let mut store = self.collection_store.borrow_mut(); - // store.pop_overlay(); - // drop(store); - // self.rebuild_tree_view(); - // return Ok(Some(SidebarEvent::SyncCollection)); - // } - // Some(DirectoryFormEvent::Cancel) => { - // let mut store = self.collection_store.borrow_mut(); - // store.pop_overlay(); - // drop(store); - // self.rebuild_tree_view(); - // return Ok(None); - // } - // None => return Ok(None), - // } - // } - // CollectionViewerOverlay::EditDirectory => match self.directory_form.inner().handle_key_event(key_event)? { - // Some(DirectoryFormEvent::Confirm) => { - // let mut store = self.collection_store.borrow_mut(); - // store.pop_overlay(); - // drop(store); - // self.rebuild_tree_view(); - // return Ok(Some(SidebarEvent::SyncCollection)); - // } - // Some(DirectoryFormEvent::Cancel) => { - // let mut store = self.collection_store.borrow_mut(); - // store.pop_overlay(); - // drop(store); - // self.rebuild_tree_view(); - // return Ok(None); - // } - // None => return Ok(None), - // }, - // CollectionViewerOverlay::EditRequest => { - // // when editing, we setup the form to display the current header information. - // match self.request_form.inner().handle_key_event(key_event)? { - // Some(RequestFormEvent::Confirm) => { - // let mut store = self.collection_store.borrow_mut(); - // store.pop_overlay(); - // drop(store); - // self.rebuild_tree_view(); - // return Ok(Some(SidebarEvent::SyncCollection)); - // } - // Some(RequestFormEvent::Cancel) => { - // let mut store = self.collection_store.borrow_mut(); - // store.pop_overlay(); - // drop(store); - // self.rebuild_tree_view(); - // return Ok(None); - // } - // None => return Ok(None), - // } - // } - // CollectionViewerOverlay::DeleteSidebarItem(item_id) => { - // match self.delete_item_prompt.handle_key_event(key_event)? { - // Some(DeleteItemPromptEvent::Confirm) => { - // let mut store = self.collection_store.borrow_mut(); - // let changed_selection = store - // .get_selected_request() - // .is_some_and(|req| req.read().unwrap().id.eq(&item_id)); - // store.remove_item(item_id); - // store.pop_overlay(); - // drop(store); - // self.rebuild_tree_view(); - // - // if changed_selection { - // return Ok(Some(SidebarEvent::RebuildView)); - // } else { - // return Ok(None); - // } - // } - // Some(DeleteItemPromptEvent::Cancel) => { - // let mut store = self.collection_store.borrow_mut(); - // store.pop_overlay(); - // drop(store); - // self.rebuild_tree_view(); - // return Ok(None); - // } - // None => return Ok(None), - // } - // } - // _ => {} - //}; - match key_event.code { KeyCode::Tab => return Ok(Some(SidebarEvent::SelectNext)), KeyCode::BackTab => return Ok(Some(SidebarEvent::SelectPrev)), @@ -368,6 +236,14 @@ impl Eventful for Sidebar { KeyCode::Char('j') | KeyCode::Down => collection::hover_next(), KeyCode::Char('k') | KeyCode::Up => collection::hover_prev(), KeyCode::Char('n') => return Ok(Some(SidebarEvent::CreateRequest)), + KeyCode::Char('?') => { + self.show_extended_hint = !self.show_extended_hint; + if self.show_extended_hint { + return Ok(Some(SidebarEvent::ShowExtendedHint)); + } else { + return Ok(Some(SidebarEvent::HideExtendedHint)); + } + } KeyCode::Enter => { if let Some((which, key)) = collection::get_hovered_request(|req| req) { match which { @@ -413,10 +289,10 @@ impl Eventful for Sidebar { fn colored_method(method: &ReqMethod, colors: &HacColors) -> Span<'static> { match method { - ReqMethod::Get => format!("{method} ").fg(colors.normal.green).bold(), - ReqMethod::Post => format!("{method} ").fg(colors.normal.magenta).bold(), - ReqMethod::Put => format!("{method} ").fg(colors.normal.yellow).bold(), - ReqMethod::Patch => format!("{method} ").fg(colors.normal.orange).bold(), - ReqMethod::Delete => format!("{method}").fg(colors.normal.red).bold(), + ReqMethod::Get => format!("{method} ").fg(colors.normal.green).bold(), + ReqMethod::Post => format!("{method} ").fg(colors.normal.magenta).bold(), + ReqMethod::Put => format!("{method} ").fg(colors.normal.yellow).bold(), + ReqMethod::Patch => format!("{method} ").fg(colors.normal.orange).bold(), + ReqMethod::Delete => format!("{method} ").fg(colors.normal.red).bold(), } } From 81582e7429495b12f8632f0f0a5c5df5a0a7a3ba Mon Sep 17 00:00:00 2001 From: wiru Date: Sun, 6 Oct 2024 05:35:41 -0300 Subject: [PATCH 20/23] refactor: removing code duplication for edit request --- hac-client/src/app.rs | 2 + .../collection_viewer/collection_viewer.rs | 41 ++- .../pages/collection_viewer/request_uri.rs | 15 +- .../sidebar/create_request_form.rs | 262 ++------------ .../sidebar/edit_request_form.rs | 334 +++++++----------- .../pages/collection_viewer/sidebar/mod.rs | 31 +- .../sidebar/request_form_layout.rs | 251 +++++++++++++ hac-store/src/collection.rs | 1 - 8 files changed, 472 insertions(+), 465 deletions(-) create mode 100644 hac-client/src/pages/collection_viewer/sidebar/request_form_layout.rs diff --git a/hac-client/src/app.rs b/hac-client/src/app.rs index c193653..a9f7069 100755 --- a/hac-client/src/app.rs +++ b/hac-client/src/app.rs @@ -39,6 +39,7 @@ pub enum Routes { DeleteCollection, CollectionViewer, CreateRequest, + EditRequest, } impl TryFrom for Routes { @@ -66,6 +67,7 @@ impl From for u8 { Routes::DeleteCollection => 3, Routes::CollectionViewer => 4, Routes::CreateRequest => 5, + Routes::EditRequest => 6, } } } diff --git a/hac-client/src/pages/collection_viewer/collection_viewer.rs b/hac-client/src/pages/collection_viewer/collection_viewer.rs index 3749211..07e979e 100755 --- a/hac-client/src/pages/collection_viewer/collection_viewer.rs +++ b/hac-client/src/pages/collection_viewer/collection_viewer.rs @@ -5,9 +5,9 @@ use std::rc::Rc; use std::sync::mpsc::{channel, Sender}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use futures::stream::SelectNextSome; use hac_core::command::Command; use hac_core::net::request_manager::Response; +use hac_store::collection::WhichSlab; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::Stylize; use ratatui::widgets::{Block, Clear}; @@ -19,6 +19,7 @@ use super::sidebar::create_request_form::CreateRequestForm; use super::sidebar::{Sidebar, SidebarEvent}; use crate::app::Routes; use crate::pages::collection_list::CollectionListData; +use crate::pages::collection_viewer::sidebar::edit_request_form::EditRequestForm; //use crate::pages::collection_viewer::collection_store::{CollectionStore, CollectionStoreAction}; //use crate::pages::collection_viewer::request_editor::{RequestEditor, RequestEditorEvent}; //use crate::pages::collection_viewer::request_uri::{RequestUri, RequestUriEvent}; @@ -460,6 +461,9 @@ impl Eventful for CollectionViewer { }, PaneFocus::Sidebar => match self.sidebar.handle_key_event(key_event)? { Some(SidebarEvent::Quit) => return Ok(Some(Command::Quit)), + Some(SidebarEvent::RemoveSelection) => self.update_selection(None), + Some(SidebarEvent::ShowExtendedHint) => self.layout = build_layout(self.layout.total_size, 3), + Some(SidebarEvent::HideExtendedHint) => self.layout = build_layout(self.layout.total_size, 1), Some(SidebarEvent::SelectNext) => { self.layout = build_layout(self.layout.total_size, 1); self.update_selection(None); @@ -476,9 +480,38 @@ impl Eventful for CollectionViewer { router_add_dialog!(&self.messager, Routes::CreateRequest, create_request_form); router_navigate_to!(&self.messager, Routes::CreateRequest); } - Some(SidebarEvent::RemoveSelection) => self.update_selection(None), - Some(SidebarEvent::ShowExtendedHint) => self.layout = build_layout(self.layout.total_size, 3), - Some(SidebarEvent::HideExtendedHint) => self.layout = build_layout(self.layout.total_size, 1), + Some(SidebarEvent::EditRequest) => { + let entry = hac_store::collection::get_hovered_request(|entry| entry); + if let Some((which, key)) = entry { + match which { + WhichSlab::Requests => hac_store::collection::get_request(key, |req, _| { + let edit_request_form = EditRequestForm::new( + req.name.clone(), + req.method, + key, + req.parent, + self.colors.clone(), + self.layout.total_size, + ); + router_add_dialog!(&self.messager, Routes::EditRequest, edit_request_form); + router_navigate_to!(&self.messager, Routes::EditRequest); + }), + WhichSlab::RootRequests => hac_store::collection::get_root_request(key, |req, _| { + let edit_request_form = EditRequestForm::new( + req.name.clone(), + req.method, + key, + req.parent, + self.colors.clone(), + self.layout.total_size, + ); + router_add_dialog!(&self.messager, Routes::EditRequest, edit_request_form); + router_navigate_to!(&self.messager, Routes::EditRequest); + }), + _ => {} + }; + } + } None => (), //Some(SidebarEvent::EditRequest) => self // .collection_store diff --git a/hac-client/src/pages/collection_viewer/request_uri.rs b/hac-client/src/pages/collection_viewer/request_uri.rs index 7db865a..74f9085 100755 --- a/hac-client/src/pages/collection_viewer/request_uri.rs +++ b/hac-client/src/pages/collection_viewer/request_uri.rs @@ -108,8 +108,21 @@ impl Eventful for RequestUri { KeyCode::Esc => return Ok(Some(RequestUriEvent::RemoveSelection)), KeyCode::Tab => return Ok(Some(RequestUriEvent::SelectNext)), KeyCode::BackTab => return Ok(Some(RequestUriEvent::SelectPrev)), - KeyCode::Char(c) => hac_store::collection::get_selected_request_mut(|req| req.uri.push(c)), KeyCode::Backspace => hac_store::collection::get_selected_request_mut(|req| _ = req.uri.pop()), + KeyCode::Char('w') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { + hac_store::collection::get_selected_request_mut(|req| { + let len = req.uri.len().saturating_sub(1); + let position = req + .uri + .chars() + .rev() + .skip(1) + .position(|ch| !matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9')) + .unwrap_or(len); + req.uri.truncate(len - position); + }) + } + KeyCode::Char(c) => hac_store::collection::get_selected_request_mut(|req| req.uri.push(c)), KeyCode::Enter => return Ok(Some(RequestUriEvent::SendRequest)), _ => {} }; diff --git a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs index 69d29c3..6d04f30 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs @@ -1,72 +1,26 @@ -use std::rc::Rc; use std::sync::mpsc::{channel, Sender}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::command::Command; use hac_store::collection::ReqMethod; use hac_store::slab::Key; -use ratatui::layout::{Constraint, Flex, Layout, Margin, Rect}; -use ratatui::style::{Style, Stylize}; -use ratatui::text::{Line, Span}; -use ratatui::widgets::{Block, Borders, Clear, Paragraph}; +use ratatui::layout::Rect; use ratatui::Frame; +use super::request_form_layout::{ + build_req_form_layout, draw_main_form, draw_parent_selector, FieldFocus, FormStep, ReqFormLayout, +}; use crate::app::Routes; -use crate::ascii::LOGO_ASCII; use crate::components::blending_list::BlendingList; -use crate::components::input::Input; use crate::pages::overlay::make_overlay; use crate::renderable::{Eventful, Renderable}; use crate::router::RouterMessage; -use crate::{router_drop_dialog, HacColors, MIN_HEIGHT, MIN_WIDTH}; - -#[derive(Debug)] -struct CreateReqFormLayout { - name: Rect, - hint: Rect, - logo: Rect, - parent: Rect, - methods: Rc<[Rect]>, - parent_listing: Rect, - parent_hint: Rect, -} - -#[derive(Debug, Default, PartialEq, Eq)] -enum FieldFocus { - #[default] - Name, - Methods, - Parent, -} - -impl FieldFocus { - pub fn next(&mut self) { - match self { - FieldFocus::Name => *self = FieldFocus::Methods, - FieldFocus::Methods => *self = FieldFocus::Parent, - FieldFocus::Parent => *self = FieldFocus::Name, - } - } - - pub fn prev(&mut self) { - match self { - FieldFocus::Name => *self = FieldFocus::Parent, - FieldFocus::Methods => *self = FieldFocus::Name, - FieldFocus::Parent => *self = FieldFocus::Methods, - } - } -} - -#[derive(Debug)] -enum FormStep { - MainForm, - ParentSelector, -} +use crate::{router_drop_dialog, HacColors}; #[derive(Debug)] pub struct CreateRequestForm { colors: HacColors, - layout: CreateReqFormLayout, + layout: ReqFormLayout, name: String, method: ReqMethod, focus: FieldFocus, @@ -79,7 +33,7 @@ pub struct CreateRequestForm { impl CreateRequestForm { pub fn new(colors: HacColors, area: Rect) -> Self { Self { - layout: build_layout(area), + layout: build_req_form_layout(area), name: Default::default(), method: Default::default(), focus: Default::default(), @@ -91,145 +45,6 @@ impl CreateRequestForm { } } - fn make_contextual_hint(&self) -> Vec { - match self.focus { - FieldFocus::Name => vec![ - "Enter".fg(self.colors.bright.green).bold(), - " - Confirm • ".fg(self.colors.bright.black), - "Esc".fg(self.colors.bright.green).bold(), - " - Cancel • ".fg(self.colors.bright.black), - "Tab".fg(self.colors.bright.green).bold(), - " - Next • ".fg(self.colors.bright.black), - "S-Tab".fg(self.colors.bright.green).bold(), - " - Prev • ".fg(self.colors.bright.black), - "Ctrl p".fg(self.colors.bright.green).bold(), - " - Parent".fg(self.colors.bright.black), - ], - FieldFocus::Methods => vec![ - "Enter".fg(self.colors.bright.green).bold(), - " - Confirm • ".fg(self.colors.bright.black), - "Esc".fg(self.colors.bright.green).bold(), - " - Cancel • ".fg(self.colors.bright.black), - "Tab".fg(self.colors.bright.green).bold(), - " - Next • ".fg(self.colors.bright.black), - "S-Tab".fg(self.colors.bright.green).bold(), - " - Prev • ".fg(self.colors.bright.black), - "1-5".fg(self.colors.bright.green).bold(), - " - Method".fg(self.colors.bright.black), - ], - FieldFocus::Parent => vec![ - "Enter".fg(self.colors.bright.green).bold(), - " - Confirm • ".fg(self.colors.bright.black), - "Esc".fg(self.colors.bright.green).bold(), - " - Cancel • ".fg(self.colors.bright.black), - "Ctrl p".fg(self.colors.bright.green).bold(), - " - Parent • ".fg(self.colors.bright.black), - "Backspace".fg(self.colors.bright.green).bold(), - " - Remove parent".fg(self.colors.bright.black), - ], - } - } - - fn draw_main_form(&mut self, frame: &mut Frame) { - let border_style = match self.focus == FieldFocus::Name { - true => Style::new().fg(self.colors.bright.red), - false => Style::new().fg(self.colors.bright.black), - }; - let label = String::from("Request Name"); - let name_input = Input::new(Some(&self.name), Some(&label), self.colors.clone()) - .border_style(border_style) - .value_style(Style::default().fg(self.colors.normal.white)) - .label_style(Style::default().fg(self.colors.bright.black)); - - let logo = Paragraph::new( - LOGO_ASCII - .iter() - .map(|line| Line::from(line.to_string()).fg(self.colors.bright.red).centered()) - .collect::>(), - ); - - for (idx, method) in ReqMethod::iter().enumerate() { - let selected = method == self.method; - let number_color = match selected { - true => self.colors.bright.blue, - false => self.colors.bright.black, - }; - let area = self.layout.methods[idx]; - let method = method.to_string(); - let remaining_width = area.width as usize - 3 - method.len(); - let left_pad = remaining_width / 2; - - let parts = vec![ - (idx + 1).to_string().fg(number_color), - " ".repeat(left_pad).into(), - method.fg(number_color), - ]; - - let mut block = Block::new() - .borders(Borders::ALL) - .border_style(Style::new().fg(self.colors.bright.black)); - if let FieldFocus::Methods = self.focus { - block = block.border_style(Style::new().fg(self.colors.bright.red)); - } - if selected { - block = block.border_style(Style::new().fg(self.colors.bright.blue)); - } - - frame.render_widget(Paragraph::new(Line::from(parts)).block(block), area); - } - - let hint = self.make_contextual_hint(); - let mut parent_name = "No Parent".to_string(); - if let Some(parent) = self.parent { - hac_store::collection::get_folder(parent, |folder, _| parent_name.clone_from(&folder.name)); - }; - - let parent_color = - if self.focus == FieldFocus::Parent { self.colors.bright.red } else { self.colors.bright.black }; - - let parent = Paragraph::new(Line::from(parent_name).centered()) - .block(Block::new().borders(Borders::ALL).fg(parent_color)); - - frame.render_widget(name_input, self.layout.name); - frame.render_widget(logo, self.layout.logo); - frame.render_widget(Clear, self.layout.parent); - frame.render_widget(parent, self.layout.parent); - frame.render_widget(Line::from(hint).centered(), self.layout.hint); - - if let FieldFocus::Name = self.focus { - frame.set_cursor( - self.layout.name.x + 1 + self.name.chars().count() as u16, - self.layout.name.y + 2, - ); - } - } - - fn draw_parent_selector(&mut self, frame: &mut Frame) { - let logo = Paragraph::new( - LOGO_ASCII - .iter() - .map(|line| Line::from(line.to_string()).fg(self.colors.bright.red).centered()) - .collect::>(), - ); - - let mut folders = vec![]; - hac_store::collection::folders(|folder| folders.push(folder.name.clone())); - self.parent_listing - .draw_with(frame, folders.iter(), |name| name, self.layout.parent_listing); - - let hint = vec![ - "Enter".fg(self.colors.bright.green).bold(), - " - Confirm • ".fg(self.colors.bright.black), - "Esc".fg(self.colors.bright.green).bold(), - " - Cancel • ".fg(self.colors.bright.black), - "j/k ↑/↓".fg(self.colors.normal.green), - " - Choose".fg(self.colors.bright.black), - ]; - - frame.render_widget(Line::from(hint).centered(), self.layout.parent_hint); - frame.render_widget(logo, self.layout.logo); - } - fn main_form_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { match key_event.code { KeyCode::Char('p') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { @@ -295,8 +110,18 @@ impl Renderable for CreateRequestForm { make_overlay(self.colors.clone(), self.colors.normal.black, 0.15, frame); match self.form_step { - FormStep::MainForm => self.draw_main_form(frame), - FormStep::ParentSelector => self.draw_parent_selector(frame), + FormStep::MainForm => draw_main_form( + &self.name, + self.method, + self.parent, + self.focus, + &self.layout, + &self.colors, + frame, + ), + FormStep::ParentSelector => { + draw_parent_selector(&mut self.parent_listing, &self.layout, &self.colors, frame) + } } Ok(()) @@ -309,7 +134,7 @@ impl Renderable for CreateRequestForm { } fn resize(&mut self, new_size: Rect) { - self.layout = build_layout(new_size); + self.layout = build_req_form_layout(new_size); } } @@ -329,50 +154,3 @@ impl Eventful for CreateRequestForm { Ok(None) } } - -fn build_layout(area: Rect) -> CreateReqFormLayout { - let [_, form, _] = Layout::horizontal([Constraint::Fill(1), Constraint::Length(MIN_WIDTH), Constraint::Fill(1)]) - .flex(Flex::Center) - .areas(area); - - let [_, form, _] = Layout::vertical([Constraint::Fill(1), Constraint::Length(MIN_HEIGHT), Constraint::Fill(1)]) - .flex(Flex::End) - .areas(form); - - let form = form.inner(&Margin::new(2, 0)); - - let [logo, _, name, _, methods, _, parent, hint] = Layout::vertical([ - Constraint::Length(LOGO_ASCII.len() as u16), - Constraint::Length(1), - Constraint::Length(4), - Constraint::Length(1), - Constraint::Length(3), - Constraint::Length(1), - Constraint::Length(3), - Constraint::Length(1), - ]) - .flex(Flex::Center) - .areas(form); - - let [_, _, parent_listing, _, parent_hint] = Layout::vertical([ - Constraint::Length(LOGO_ASCII.len() as u16), - Constraint::Length(1), - Constraint::Length(13), - Constraint::Length(1), - Constraint::Length(1), - ]) - .flex(Flex::Center) - .areas(form); - - let methods = Layout::horizontal((0..ReqMethod::size()).map(|_| Constraint::Fill(1))).split(methods); - - CreateReqFormLayout { - name, - hint, - logo, - methods, - parent, - parent_listing, - parent_hint, - } -} diff --git a/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs index 553a518..67c0a50 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs @@ -1,241 +1,151 @@ -use std::cell::RefCell; -use std::ops::Sub; -use std::rc::Rc; -use std::sync::{Arc, RwLock}; +use std::sync::mpsc::{channel, Sender}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use hac_core::collection::types::*; -use rand::Rng; +use hac_core::command::Command; +use hac_store::collection::ReqMethod; +use hac_store::slab::Key; +use ratatui::layout::Rect; use ratatui::Frame; -use super::request_form::{FormField, RequestForm, RequestFormEdit, RequestFormEvent}; -use super::select_request_parent::{SelectRequestParent, SelectRequestParentEvent}; -use super::RequestFormTrait; -use crate::ascii::LOGO_ASCII; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::collection_viewer::collection_viewer::CollectionViewerOverlay; -use crate::pages::{Eventful, Renderable}; - -impl<'rf> RequestFormTrait for RequestForm<'rf, RequestFormEdit> { - fn draw_overlay(&mut self, frame: &mut Frame, overlay: CollectionViewerOverlay) -> anyhow::Result<()> { - if let CollectionViewerOverlay::SelectParentDir = overlay { - self.parent_selector.draw(frame, frame.size())?; - } - - Ok(()) - } +use super::request_form_layout::{ + build_req_form_layout, draw_main_form, draw_parent_selector, FieldFocus, FormStep, ReqFormLayout, +}; +use crate::app::Routes; +use crate::components::blending_list::BlendingList; +use crate::pages::overlay::make_overlay; +use crate::renderable::{Eventful, Renderable}; +use crate::router::RouterMessage; +use crate::{router_drop_dialog, HacColors}; + +#[derive(Debug)] +pub struct EditRequestForm { + colors: HacColors, + layout: ReqFormLayout, + name: String, + method: ReqMethod, + focus: FieldFocus, + parent: Option, + prev_parent: Option, + key: Key, + form_step: FormStep, + parent_listing: BlendingList, + messager: Sender, } -impl<'rf> RequestForm<'rf, RequestFormEdit> { - pub fn new( - colors: &'rf hac_colors::Colors, - collection_store: Rc>, - request: Arc>, - ) -> Self { - let request_method = request.read().unwrap().method.clone(); - let request_name = request.read().unwrap().name.clone(); - - let parent_dir = if request.read().unwrap().parent.is_some() { - let store = collection_store.borrow(); - let Some(collection) = store.get_collection() else { - unreachable!(); - }; - let collection = collection.borrow(); - let Some(requests) = collection.requests.as_ref() else { - unreachable!(); - }; - - let dir_id = request.read().unwrap().parent.clone().unwrap(); - let dir_name = requests - .read() - .unwrap() - .iter() - .find(|req| req.get_id().eq(&dir_id)) - .unwrap() - .get_name(); - - Some((dir_id, dir_name)) - } else { - None - }; - - RequestForm { +impl EditRequestForm { + pub fn new(name: String, method: ReqMethod, key: Key, parent: Option, colors: HacColors, area: Rect) -> Self { + Self { + layout: build_req_form_layout(area), + name, + method, + parent, + prev_parent: parent, + key, + focus: Default::default(), + form_step: FormStep::MainForm, + parent_listing: BlendingList::new(0, hac_store::collection::len_folders(), 13, 0, colors.clone()), colors, - parent_selector: SelectRequestParent::new(colors, collection_store.clone()), - collection_store, - request_name, - request_method, - parent_dir, - focused_field: FormField::Name, - marker: std::marker::PhantomData, - request: Some(request), - no_available_parent_timer: None, + messager: channel().0, } } -} - -impl Eventful for RequestForm<'_, RequestFormEdit> { - type Result = RequestFormEvent; - #[tracing::instrument(skip_all, err)] - fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - let overlay = self.collection_store.borrow().peek_overlay(); - if let CollectionViewerOverlay::SelectParentDir = overlay { - match self.parent_selector.handle_key_event(key_event)? { - Some(SelectRequestParentEvent::Confirm(dir_id)) => { - let mut store = self.collection_store.borrow_mut(); - let collection = store - .get_collection() - .expect("tried attach a parent to a request without having a collection"); - let collection = collection.borrow(); - let requests = collection - .requests - .as_ref() - .expect("tried to attach a parent to a request with empty collection"); - let dir_name = requests - .read() - .unwrap() - .iter() - .find(|req| req.get_id().eq(&dir_id)) - .as_ref() - // its safe to unwrap here as to have an id we for sure have the directory - .unwrap() - .get_name(); - tracing::debug!("before: {:?}", self.parent_dir); - self.parent_dir = Some((dir_id, dir_name)); - store.pop_overlay(); - } - Some(SelectRequestParentEvent::Cancel) => { - self.collection_store.borrow_mut().pop_overlay(); - } - None => {} + fn main_form_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + match key_event.code { + KeyCode::Char('p') if matches!(key_event.modifiers, KeyModifiers::CONTROL) => { + self.form_step = FormStep::ParentSelector; } - return Ok(None); - } - - if let KeyCode::Tab = key_event.code { - self.focused_field = self.focused_field.next(); - return Ok(None); - } - - if let KeyCode::BackTab = key_event.code { - self.focused_field = self.focused_field.prev(); - return Ok(None); - } - - if let KeyCode::Enter = key_event.code { - let request = self.request.as_mut().unwrap(); - let mut request = request.write().unwrap(); - - request.name.clone_from(&self.request_name); - request.method.clone_from(&self.request_method); - request.parent = self.parent_dir.as_ref().map(|(id, _)| id.clone()); - let request_id = request.id.clone(); + KeyCode::Char(ch) if matches!(self.focus, FieldFocus::Name) => self.name.push(ch), + KeyCode::Backspace if matches!(self.focus, FieldFocus::Name) => _ = self.name.pop(), + + KeyCode::Left if matches!(self.focus, FieldFocus::Methods) => self.method.set_prev(), + KeyCode::Right if matches!(self.focus, FieldFocus::Methods) => self.method.set_next(), + KeyCode::Up if matches!(self.focus, FieldFocus::Methods) => self.method.set_first(), + KeyCode::Down if matches!(self.focus, FieldFocus::Methods) => self.method.set_last(), + KeyCode::Char('h') if matches!(self.focus, FieldFocus::Methods) => self.method.set_prev(), + KeyCode::Char('j') if matches!(self.focus, FieldFocus::Methods) => self.method.set_last(), + KeyCode::Char('k') if matches!(self.focus, FieldFocus::Methods) => self.method.set_first(), + KeyCode::Char('l') if matches!(self.focus, FieldFocus::Methods) => self.method.set_next(), + KeyCode::Char(ch @ '1'..='5') if matches!(self.focus, FieldFocus::Methods) => { + self.method = ReqMethod::from(ch) + } - drop(request); + KeyCode::Backspace if matches!(self.focus, FieldFocus::Parent) => self.parent = None, - let store = self.collection_store.borrow_mut(); - let collection = store - .get_collection() - .expect("tried to create a request without a collection"); - let mut collection = collection.borrow_mut(); - let requests = collection.requests.get_or_insert(Arc::new(RwLock::new(vec![]))); - let mut requests = requests.write().unwrap(); + KeyCode::Tab => self.focus.next(), + KeyCode::BackTab => self.focus.prev(), + KeyCode::Esc => { + router_drop_dialog!(&self.messager, Routes::CreateRequest.into()); + } + KeyCode::Enter => { + let request = hac_store::collection::Request::new(self.name.clone(), self.method, self.parent); + hac_store::collection::push_request(request, self.parent); + router_drop_dialog!(&self.messager, Routes::CreateRequest.into()); + } + _ => {} + }; - requests.iter_mut().for_each(|req| { - if let RequestKind::Nested(dir) = req { - dir.requests.write().unwrap().retain(|req| req.get_id().ne(&request_id)); - } - }); + Ok(None) + } - if let Some((dir_id, _)) = self.parent_dir.as_ref() { - if let RequestKind::Nested(dir) = requests.iter_mut().find(|req| req.get_id().eq(dir_id)).unwrap() { - let request = self.request.as_ref().unwrap().clone(); - dir.requests.write().unwrap().push(RequestKind::Single(request)); + fn parent_selector_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + match key_event.code { + KeyCode::Char('j') | KeyCode::Down => self.parent_listing.select_down(), + KeyCode::Char('k') | KeyCode::Up => self.parent_listing.select_up(), + KeyCode::Enter => { + if hac_store::collection::has_folders() { + self.parent = Some(self.parent_listing.selected); + self.form_step = FormStep::MainForm; + self.parent_listing.reset() } } + _ => {} + }; - drop(store); - self.reset(); - return Ok(Some(RequestFormEvent::Confirm)); - } + Ok(None) + } +} - if let KeyCode::Esc = key_event.code { - self.reset(); - return Ok(Some(RequestFormEvent::Cancel)); +impl Renderable for EditRequestForm { + type Input = (); + type Output = (); + + fn data(&self, _requester: u8) -> Self::Output {} + + fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { + make_overlay(self.colors.clone(), self.colors.normal.black, 0.15, frame); + + match self.form_step { + FormStep::MainForm => draw_main_form( + &self.name, + self.method, + self.parent, + self.focus, + &self.layout, + &self.colors, + frame, + ), + FormStep::ParentSelector => { + draw_parent_selector(&mut self.parent_listing, &self.layout, &self.colors, frame) + } } - if let (KeyCode::Char('p'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - self.parent_dir = None; - return Ok(Some(RequestFormEvent::Cancel)); - } + Ok(()) + } +} + +impl Eventful for EditRequestForm { + type Result = Command; + fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - self.reset(); - return Ok(None); + return Ok(Some(Command::Quit)); } - match self.focused_field { - FormField::Name => match key_event.code { - KeyCode::Char(c) => { - self.request_name.push(c); - } - KeyCode::Backspace => { - self.request_name.pop(); - } - _ => {} - }, - FormField::Method => match key_event.code { - KeyCode::Char(c @ '1'..='5') => { - self.request_method = (c.to_digit(10).unwrap() as usize).sub(1).try_into()?; - } - KeyCode::Left => self.request_method = self.request_method.prev(), - KeyCode::Down => self.request_method = 4.try_into()?, - KeyCode::Up => self.request_method = 0.try_into()?, - KeyCode::Right => self.request_method = self.request_method.next(), - KeyCode::Char('h') => self.request_method = self.request_method.prev(), - KeyCode::Char('j') => self.request_method = 4.try_into()?, - KeyCode::Char('k') => self.request_method = 0.try_into()?, - KeyCode::Char('l') => self.request_method = self.request_method.next(), - _ => {} - }, - FormField::Parent => { - if let KeyCode::Char(' ') = key_event.code { - let mut store = self.collection_store.borrow_mut(); - let collection = store - .get_collection() - .expect("tried to select a parent without a collection"); - let collection = collection.borrow(); - - let Some(requests) = collection.requests.as_ref() else { - drop(store); - self.set_no_parent_timer(); - return Ok(None); - }; - - let requests = requests.read().unwrap(); - if requests.is_empty() { - drop(store); - self.set_no_parent_timer(); - return Ok(None); - } - - if requests - .iter() - .filter(|req| req.is_dir()) - .collect::>() - .is_empty() - { - drop(store); - self.set_no_parent_timer(); - return Ok(None); - } - - store.push_overlay(CollectionViewerOverlay::SelectParentDir); - } - } - } + match self.form_step { + FormStep::MainForm => self.main_form_key_event(key_event)?, + FormStep::ParentSelector => self.parent_selector_key_event(key_event)?, + }; Ok(None) } diff --git a/hac-client/src/pages/collection_viewer/sidebar/mod.rs b/hac-client/src/pages/collection_viewer/sidebar/mod.rs index 3e6823f..22f7bf6 100755 --- a/hac-client/src/pages/collection_viewer/sidebar/mod.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/mod.rs @@ -1,15 +1,16 @@ pub mod create_request_form; +pub mod edit_request_form; +mod request_form_layout; // mod create_directory_form; // mod delete_item_prompt; // mod directory_form; // mod edit_directory_form; -// mod edit_request_form; // mod request_form; // mod select_request_parent; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_store::collection::{self, EntryStatus, ReqMethod, ReqTreeNode, WhichSlab}; -use ratatui::layout::Rect; +use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::style::{Style, Styled, Stylize}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Paragraph}; @@ -17,7 +18,7 @@ use ratatui::Frame; use crate::icons::Icons; use crate::renderable::{Eventful, Renderable}; -use crate::{HacColors, HacConfig}; +use crate::{HacColors, HacConfig, MIN_WIDTH}; #[derive(Debug, PartialEq, Eq, Clone)] pub enum SidebarEvent { @@ -27,6 +28,7 @@ pub enum SidebarEvent { SelectNext, RemoveSelection, CreateRequest, + EditRequest, Quit, } @@ -108,7 +110,13 @@ impl Sidebar { " - Prev".fg(self.colors.bright.black), ]), ]; - let size = Rect::new(size.x + 1, size.height - 3, size.width - 2, 3); + let size = Rect::new(size.x, size.height - 3, size.width, 3); + let [_, size, _] = Layout::horizontal([ + Constraint::Fill(1), + Constraint::Length(MIN_WIDTH - 2), + Constraint::Fill(1), + ]) + .areas(size); frame.render_widget(Paragraph::new(lines), size); } else { let hint = vec![ @@ -163,7 +171,7 @@ impl Renderable for Sidebar { false => Icons::FOLDER_OPEN, }; let name = Line::default().spans([ - format!("{icon} ").bold().fg(self.colors.normal.yellow), + format!("{icon} ").bold().fg(self.colors.normal.yellow), folder_name.set_style(style), ]); lines.push(name); @@ -235,6 +243,19 @@ impl Eventful for Sidebar { KeyCode::Esc => return Ok(Some(SidebarEvent::RemoveSelection)), KeyCode::Char('j') | KeyCode::Down => collection::hover_next(), KeyCode::Char('k') | KeyCode::Up => collection::hover_prev(), + KeyCode::Char('e') => { + return collection::get_hovered_request(|entry| { + if let Some((which, _)) = entry { + match which { + WhichSlab::Requests | WhichSlab::RootRequests => { + return Ok(Some(SidebarEvent::EditRequest)) + } + WhichSlab::Folders => todo!(), + }; + }; + Ok(None) + }); + } KeyCode::Char('n') => return Ok(Some(SidebarEvent::CreateRequest)), KeyCode::Char('?') => { self.show_extended_hint = !self.show_extended_hint; diff --git a/hac-client/src/pages/collection_viewer/sidebar/request_form_layout.rs b/hac-client/src/pages/collection_viewer/sidebar/request_form_layout.rs new file mode 100644 index 0000000..49c7dbc --- /dev/null +++ b/hac-client/src/pages/collection_viewer/sidebar/request_form_layout.rs @@ -0,0 +1,251 @@ +use std::rc::Rc; + +use hac_store::collection::ReqMethod; +use hac_store::slab::Key; +use ratatui::layout::{Constraint, Flex, Layout, Margin, Rect}; +use ratatui::style::{Style, Stylize}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Block, Borders, Clear, Paragraph}; +use ratatui::Frame; + +use crate::ascii::LOGO_ASCII; +use crate::components::blending_list::BlendingList; +use crate::components::input::Input; +use crate::{HacColors, MIN_HEIGHT, MIN_WIDTH}; + +#[derive(Debug)] +pub enum FormStep { + MainForm, + ParentSelector, +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +pub enum FieldFocus { + #[default] + Name, + Methods, + Parent, +} + +impl FieldFocus { + pub fn next(&mut self) { + match self { + FieldFocus::Name => *self = FieldFocus::Methods, + FieldFocus::Methods => *self = FieldFocus::Parent, + FieldFocus::Parent => *self = FieldFocus::Name, + } + } + + pub fn prev(&mut self) { + match self { + FieldFocus::Name => *self = FieldFocus::Parent, + FieldFocus::Methods => *self = FieldFocus::Name, + FieldFocus::Parent => *self = FieldFocus::Methods, + } + } +} + +#[derive(Debug)] +pub struct ReqFormLayout { + pub name: Rect, + pub hint: Rect, + pub logo: Rect, + pub parent: Rect, + pub methods: Rc<[Rect]>, + pub parent_listing: Rect, + pub parent_hint: Rect, +} + +pub fn make_contextual_hint(focus: FieldFocus, colors: &HacColors) -> Vec { + match focus { + FieldFocus::Name => vec![ + "Enter".fg(colors.bright.green).bold(), + " - Confirm • ".fg(colors.bright.black), + "Esc".fg(colors.bright.green).bold(), + " - Cancel • ".fg(colors.bright.black), + "Tab".fg(colors.bright.green).bold(), + " - Next • ".fg(colors.bright.black), + "S-Tab".fg(colors.bright.green).bold(), + " - Prev • ".fg(colors.bright.black), + "Ctrl p".fg(colors.bright.green).bold(), + " - Parent".fg(colors.bright.black), + ], + FieldFocus::Methods => vec![ + "Enter".fg(colors.bright.green).bold(), + " - Confirm • ".fg(colors.bright.black), + "Esc".fg(colors.bright.green).bold(), + " - Cancel • ".fg(colors.bright.black), + "Tab".fg(colors.bright.green).bold(), + " - Next • ".fg(colors.bright.black), + "S-Tab".fg(colors.bright.green).bold(), + " - Prev • ".fg(colors.bright.black), + "1-5".fg(colors.bright.green).bold(), + " - Method".fg(colors.bright.black), + ], + FieldFocus::Parent => vec![ + "Enter".fg(colors.bright.green).bold(), + " - Confirm • ".fg(colors.bright.black), + "Esc".fg(colors.bright.green).bold(), + " - Cancel • ".fg(colors.bright.black), + "Ctrl p".fg(colors.bright.green).bold(), + " - Parent • ".fg(colors.bright.black), + "Backspace".fg(colors.bright.green).bold(), + " - Remove parent".fg(colors.bright.black), + ], + } +} + +pub fn draw_main_form( + name: &str, + method: ReqMethod, + parent: Option, + focus: FieldFocus, + layout: &ReqFormLayout, + colors: &HacColors, + frame: &mut Frame, +) { + let border_style = match focus == FieldFocus::Name { + true => Style::new().fg(colors.bright.red), + false => Style::new().fg(colors.bright.black), + }; + let label = String::from("Request Name"); + let name_input = Input::new(Some(name), Some(&label), colors.clone()) + .border_style(border_style) + .value_style(Style::default().fg(colors.normal.white)) + .label_style(Style::default().fg(colors.bright.black)); + + let logo = Paragraph::new( + LOGO_ASCII + .iter() + .map(|line| Line::from(line.to_string()).fg(colors.bright.red).centered()) + .collect::>(), + ); + + for (idx, m) in ReqMethod::iter().enumerate() { + let selected = m == method; + let number_color = match selected { + true => colors.bright.blue, + false => colors.bright.black, + }; + let area = layout.methods[idx]; + let method = method.to_string(); + let remaining_width = area.width as usize - 3 - method.len(); + let left_pad = remaining_width / 2; + + let parts = vec![ + (idx + 1).to_string().fg(number_color), + " ".repeat(left_pad).into(), + method.fg(number_color), + ]; + + let mut block = Block::new() + .borders(Borders::ALL) + .border_style(Style::new().fg(colors.bright.black)); + if let FieldFocus::Methods = focus { + block = block.border_style(Style::new().fg(colors.bright.red)); + } + if selected { + block = block.border_style(Style::new().fg(colors.bright.blue)); + } + + frame.render_widget(Paragraph::new(Line::from(parts)).block(block), area); + } + + let hint = make_contextual_hint(focus, colors); + let mut parent_name = "No Parent".to_string(); + if let Some(parent) = parent { + hac_store::collection::get_folder(parent, |folder, _| parent_name.clone_from(&folder.name)); + }; + + let parent_color = if focus == FieldFocus::Parent { colors.bright.red } else { colors.bright.black }; + + let parent = + Paragraph::new(Line::from(parent_name).centered()).block(Block::new().borders(Borders::ALL).fg(parent_color)); + + frame.render_widget(name_input, layout.name); + frame.render_widget(logo, layout.logo); + frame.render_widget(Clear, layout.parent); + frame.render_widget(parent, layout.parent); + frame.render_widget(Line::from(hint).centered(), layout.hint); + + if let FieldFocus::Name = focus { + frame.set_cursor(layout.name.x + 1 + name.chars().count() as u16, layout.name.y + 2); + } +} + +pub fn draw_parent_selector( + parent_listing: &mut BlendingList, + layout: &ReqFormLayout, + colors: &HacColors, + frame: &mut Frame, +) { + let logo = Paragraph::new( + LOGO_ASCII + .iter() + .map(|line| Line::from(line.to_string()).fg(colors.bright.red).centered()) + .collect::>(), + ); + + let mut folders = vec![]; + hac_store::collection::folders(|folder| folders.push(folder.name.clone())); + parent_listing.draw_with(frame, folders.iter(), |name| name, layout.parent_listing); + + let hint = vec![ + "Enter".fg(colors.bright.green).bold(), + " - Confirm • ".fg(colors.bright.black), + "Esc".fg(colors.bright.green).bold(), + " - Cancel • ".fg(colors.bright.black), + "j/k ↑/↓".fg(colors.normal.green), + " - Choose".fg(colors.bright.black), + ]; + + frame.render_widget(Line::from(hint).centered(), layout.parent_hint); + frame.render_widget(logo, layout.logo); +} + +pub fn build_req_form_layout(area: Rect) -> ReqFormLayout { + let [_, form, _] = Layout::horizontal([Constraint::Fill(1), Constraint::Length(MIN_WIDTH), Constraint::Fill(1)]) + .flex(Flex::Center) + .areas(area); + + let [_, form, _] = Layout::vertical([Constraint::Fill(1), Constraint::Length(MIN_HEIGHT), Constraint::Fill(1)]) + .flex(Flex::End) + .areas(form); + + let form = form.inner(&Margin::new(2, 0)); + + let [logo, _, name, _, methods, _, parent, hint] = Layout::vertical([ + Constraint::Length(LOGO_ASCII.len() as u16), + Constraint::Length(1), + Constraint::Length(4), + Constraint::Length(1), + Constraint::Length(3), + Constraint::Length(1), + Constraint::Length(3), + Constraint::Length(1), + ]) + .flex(Flex::Center) + .areas(form); + + let [_, _, parent_listing, _, parent_hint] = Layout::vertical([ + Constraint::Length(LOGO_ASCII.len() as u16), + Constraint::Length(1), + Constraint::Length(13), + Constraint::Length(1), + Constraint::Length(1), + ]) + .flex(Flex::Center) + .areas(form); + + let methods = Layout::horizontal((0..ReqMethod::size()).map(|_| Constraint::Fill(1))).split(methods); + + ReqFormLayout { + name, + hint, + logo, + methods, + parent, + parent_listing, + parent_hint, + } +} diff --git a/hac-store/src/collection.rs b/hac-store/src/collection.rs index 830bd3b..adaeb2b 100644 --- a/hac-store/src/collection.rs +++ b/hac-store/src/collection.rs @@ -258,7 +258,6 @@ where let is_selected = collection .selected_request .is_some_and(|(slab, k)| matches!(slab, WhichSlab::RootRequests) && key == k); - f(req, (is_hovered, is_selected).into()); }) } From c38b66ac34f1f7d4e6b715827e8bf18f28658850 Mon Sep 17 00:00:00 2001 From: wiru Date: Sun, 6 Oct 2024 17:45:16 -0300 Subject: [PATCH 21/23] feat: editing requests from sidebar --- .../sidebar/create_request_form.rs | 2 + .../sidebar/edit_request_form.rs | 81 +++++++++++-- .../pages/collection_viewer/sidebar/mod.rs | 18 +-- hac-store/src/collection.rs | 112 ++++++++++++++++-- hac-store/src/slab.rs | 10 +- 5 files changed, 194 insertions(+), 29 deletions(-) diff --git a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs index 6d04f30..bbafded 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs @@ -76,6 +76,7 @@ impl CreateRequestForm { KeyCode::Enter => { let request = hac_store::collection::Request::new(self.name.clone(), self.method, self.parent); hac_store::collection::push_request(request, self.parent); + hac_store::collection::rebuild_tree_layout(); router_drop_dialog!(&self.messager, Routes::CreateRequest.into()); } _ => {} @@ -88,6 +89,7 @@ impl CreateRequestForm { match key_event.code { KeyCode::Char('j') | KeyCode::Down => self.parent_listing.select_down(), KeyCode::Char('k') | KeyCode::Up => self.parent_listing.select_up(), + KeyCode::Esc => self.form_step = FormStep::MainForm, KeyCode::Enter => { if hac_store::collection::has_folders() { self.parent = Some(self.parent_listing.selected); diff --git a/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs index 67c0a50..3171321 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs @@ -2,7 +2,7 @@ use std::sync::mpsc::{channel, Sender}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::command::Command; -use hac_store::collection::ReqMethod; +use hac_store::collection::{EntryStatus, ReqMethod, WhichSlab}; use hac_store::slab::Key; use ratatui::layout::Rect; use ratatui::Frame; @@ -75,12 +75,74 @@ impl EditRequestForm { KeyCode::Tab => self.focus.next(), KeyCode::BackTab => self.focus.prev(), KeyCode::Esc => { - router_drop_dialog!(&self.messager, Routes::CreateRequest.into()); + router_drop_dialog!(&self.messager, Routes::EditRequest.into()); } KeyCode::Enter => { - let request = hac_store::collection::Request::new(self.name.clone(), self.method, self.parent); - hac_store::collection::push_request(request, self.parent); - router_drop_dialog!(&self.messager, Routes::CreateRequest.into()); + // editing a request gives us a few possible scenarios: + // 1. parent of the request didn't change, either if it had no parent and still has + // no parent, or if it had one and it wasn't changed. + // - in this case, we just need to update the request on the current slab it + // lives in + // 2. parent of the request changed from not having a parent to having a parent. + // - in this case we need to remove the request from root_requests and move it + // to requests, also updating the new parent to hold the key to its new request + // 3. parent of the request changed from having a parent to not having a parent. + // - similar to the previous one, we need to remove it from requests, and also + // remove the key from the current parent, and then add it into root_requests. + // 4. parent of the request changed to another parent, so we only need to update + // both folders + match (self.prev_parent, self.parent) { + (Some(a), Some(b)) if a == b => hac_store::collection::get_request_mut(self.key, |req, _| { + req.name.clone_from(&self.name); + req.method = self.method; + req.parent = self.parent; + }), + (None, None) => hac_store::collection::get_root_request_mut(self.key, |req, _| { + req.name.clone_from(&self.name); + req.method = self.method; + req.parent = self.parent; + }), + (None, Some(new_parent)) => { + let status = hac_store::collection::get_root_request(self.key, |_, status| status); + let is_selected = matches!(status, EntryStatus::Selected | EntryStatus::Both); + let mut req = hac_store::collection::remove_root_request(self.key); + req.name.clone_from(&self.name); + req.method = self.method; + req.parent = self.parent; + let key = hac_store::collection::push_request(req, Some(new_parent)); + hac_store::collection::set_hovered_request(Some((WhichSlab::Requests, key))); + if is_selected { + hac_store::collection::set_selected_request(Some((WhichSlab::Requests, key))); + } + } + (Some(_), None) => { + let status = hac_store::collection::get_request(self.key, |_, status| status); + let is_selected = matches!(status, EntryStatus::Selected | EntryStatus::Both); + let mut req = hac_store::collection::remove_request(self.key); + req.name.clone_from(&self.name); + req.method = self.method; + req.parent = self.parent; + let key = hac_store::collection::push_request(req, None); + hac_store::collection::set_hovered_request(Some((WhichSlab::RootRequests, key))); + if is_selected { + hac_store::collection::set_selected_request(Some((WhichSlab::RootRequests, key))); + } + } + (Some(curr_parent), Some(new_parent)) => { + hac_store::collection::get_request_mut(self.key, |req, _| { + req.name.clone_from(&self.name); + req.method = self.method; + req.parent = self.parent; + }); + hac_store::collection::get_folder_mut(curr_parent, |folder, _| { + folder.requests.retain(|r| *r != self.key) + }); + hac_store::collection::get_folder_mut(new_parent, |folder, _| folder.requests.push(self.key)); + } + } + + hac_store::collection::rebuild_tree_layout(); + router_drop_dialog!(&self.messager, Routes::EditRequest.into()); } _ => {} }; @@ -92,6 +154,7 @@ impl EditRequestForm { match key_event.code { KeyCode::Char('j') | KeyCode::Down => self.parent_listing.select_down(), KeyCode::Char('k') | KeyCode::Up => self.parent_listing.select_up(), + KeyCode::Esc => self.form_step = FormStep::MainForm, KeyCode::Enter => { if hac_store::collection::has_folders() { self.parent = Some(self.parent_listing.selected); @@ -110,8 +173,6 @@ impl Renderable for EditRequestForm { type Input = (); type Output = (); - fn data(&self, _requester: u8) -> Self::Output {} - fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { make_overlay(self.colors.clone(), self.colors.normal.black, 0.15, frame); @@ -132,6 +193,12 @@ impl Renderable for EditRequestForm { Ok(()) } + + fn data(&self, _requester: u8) -> Self::Output {} + + fn attach_navigator(&mut self, messager: Sender) { + self.messager = messager; + } } impl Eventful for EditRequestForm { diff --git a/hac-client/src/pages/collection_viewer/sidebar/mod.rs b/hac-client/src/pages/collection_viewer/sidebar/mod.rs index 22f7bf6..12a3851 100755 --- a/hac-client/src/pages/collection_viewer/sidebar/mod.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/mod.rs @@ -86,27 +86,27 @@ impl Sidebar { if self.show_extended_hint { let lines = vec![ Line::from(vec![ - "j/k ↑/↓".fg(self.colors.normal.green), + "j/k ↑/↓".fg(self.colors.bright.green).bold(), " - Choose • ".fg(self.colors.bright.black), - "n".fg(self.colors.normal.green), + "n".fg(self.colors.bright.green).bold(), " - New request • ".fg(self.colors.bright.black), - "Enter".fg(self.colors.normal.green), + "Enter".fg(self.colors.bright.green).bold(), " - Select request".fg(self.colors.bright.black), ]), Line::from(vec![ - "?".fg(self.colors.normal.green), + "?".fg(self.colors.bright.green).bold(), " - Show less • ".fg(self.colors.bright.black), - "Ctrl c".fg(self.colors.normal.green), + "Ctrl c".fg(self.colors.bright.green).bold(), " - Quit • ".fg(self.colors.bright.black), - "d".fg(self.colors.normal.green), + "d".fg(self.colors.bright.green).bold(), " - Delete item".fg(self.colors.bright.black), ]), Line::from(vec![ - "e".fg(self.colors.normal.green), + "e".fg(self.colors.bright.green).bold(), " - Edit • ".fg(self.colors.bright.black), - "Tab".fg(self.colors.normal.green), + "Tab".fg(self.colors.bright.green).bold(), " - Next • ".fg(self.colors.bright.black), - "S-Tab".fg(self.colors.normal.green), + "S-Tab".fg(self.colors.bright.green).bold(), " - Prev".fg(self.colors.bright.black), ]), ]; diff --git a/hac-store/src/collection.rs b/hac-store/src/collection.rs index adaeb2b..abed43f 100644 --- a/hac-store/src/collection.rs +++ b/hac-store/src/collection.rs @@ -14,7 +14,7 @@ pub struct ReqTree { pub nodes: Vec, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum EntryStatus { None, Hovered, @@ -244,14 +244,52 @@ pub fn tree_layout() -> ReqTree { nodes } -pub fn get_root_request(key: Key, f: F) +pub fn remove_request(key: Key) -> Request { + HAC_STORE.with_borrow_mut(|store| { + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + let req = collection.requests.remove(key); + assert!(req.parent.is_some()); + let folder = collection.folders.get_mut(req.parent.unwrap()); + folder.requests.retain(|r| *r != key); + req + }) +} + +pub fn remove_root_request(key: Key) -> Request { + HAC_STORE.with_borrow_mut(|store| { + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + collection.root_requests.remove(key) + }) +} + +pub fn get_root_request(key: Key, f: F) -> R where - F: FnOnce(&Request, EntryStatus), + F: FnOnce(&Request, EntryStatus) -> R, { HAC_STORE.with_borrow(|store| { assert!(store.collection.is_some()); let collection = store.collection.as_ref().unwrap(); let req = collection.root_requests.get(key); + let is_hovered = collection + .hovered_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::RootRequests) && key == k); + let is_selected = collection + .selected_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::RootRequests) && key == k); + f(req, (is_hovered, is_selected).into()) + }) +} + +pub fn get_root_request_mut(key: Key, f: F) +where + F: FnOnce(&mut Request, EntryStatus), +{ + HAC_STORE.with_borrow_mut(|store| { + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + let req = collection.root_requests.get_mut(key); let is_hovered = collection .hovered_request .is_some_and(|(slab, k)| matches!(slab, WhichSlab::RootRequests) && key == k); @@ -262,14 +300,32 @@ where }) } -pub fn get_request(key: Key, f: F) +pub fn get_request(key: Key, f: F) -> R where - F: FnOnce(&Request, EntryStatus), + F: FnOnce(&Request, EntryStatus) -> R, { HAC_STORE.with_borrow(|store| { assert!(store.collection.is_some()); let collection = store.collection.as_ref().unwrap(); let req = collection.requests.get(key); + let is_hovered = collection + .hovered_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Requests) && key == k); + let is_selected = collection + .selected_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Requests) && key == k); + f(req, (is_hovered, is_selected).into()) + }) +} + +pub fn get_request_mut(key: Key, f: F) +where + F: FnOnce(&mut Request, EntryStatus), +{ + HAC_STORE.with_borrow_mut(|store| { + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + let req = collection.requests.get_mut(key); let is_hovered = collection .hovered_request .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Requests) && key == k); @@ -317,6 +373,24 @@ where }) } +pub fn get_folder_mut(key: Key, f: F) +where + F: FnOnce(&mut Folder, EntryStatus), +{ + HAC_STORE.with_borrow_mut(|store| { + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + let folder = collection.folders.get_mut(key); + let is_hovered = collection + .hovered_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Folders) && key == k); + let is_selected = collection + .selected_request + .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Folders) && key == k); + f(folder, (is_hovered, is_selected).into()); + }) +} + pub fn hover_next() { HAC_STORE.with_borrow_mut(|store| { assert!(store.collection.is_some()); @@ -448,6 +522,22 @@ pub fn hover_prev() { }) } +pub fn set_selected_request(new_hovered: Option<(WhichSlab, Key)>) { + HAC_STORE.with_borrow_mut(|store| { + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + collection.selected_request = new_hovered; + }) +} + +pub fn set_hovered_request(new_hovered: Option<(WhichSlab, Key)>) { + HAC_STORE.with_borrow_mut(|store| { + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + collection.hovered_request = new_hovered; + }) +} + pub fn get_hovered_request(f: F) -> R where F: FnOnce(Option<(WhichSlab, Key)>) -> R, @@ -476,7 +566,7 @@ pub fn toggle_dir(key: Key) { }); } -pub fn push_request(request: Request, parent: Option) { +pub fn push_request(request: Request, parent: Option) -> Key { HAC_STORE.with_borrow_mut(|store| { assert!(store.collection.is_some()); let collection = store.collection.as_mut().unwrap(); @@ -485,14 +575,14 @@ pub fn push_request(request: Request, parent: Option) { let new_key = collection.requests.push(request); let folder = collection.folders.get_mut(key); folder.requests.push(new_key); + new_key } - None => _ = collection.root_requests.push(request), - }; - }); - rebuild_tree_layout(); + None => collection.root_requests.push(request), + } + }) } -fn rebuild_tree_layout() { +pub fn rebuild_tree_layout() { HAC_STORE.with_borrow_mut(|store| { assert!(store.collection.is_some()); let collection = store.collection.as_mut().unwrap(); diff --git a/hac-store/src/slab.rs b/hac-store/src/slab.rs index a512265..8ec8aa4 100644 --- a/hac-store/src/slab.rs +++ b/hac-store/src/slab.rs @@ -103,11 +103,17 @@ impl Slab { } pub fn len(&self) -> usize { - self.inner.len() + self.inner + .iter() + .filter_map(|e| match e { + Entry::Full(val) => Some(val), + Entry::Free(_) => None, + }) + .count() } pub fn is_empty(&self) -> bool { - self.inner.is_empty() + self.len() == 0 } pub fn next_idx(&self) -> Key { From 175eba8f5f57f6bc887581dea44d8692ab938595 Mon Sep 17 00:00:00 2001 From: wiru Date: Sun, 6 Oct 2024 19:59:19 -0300 Subject: [PATCH 22/23] feat: editing and deleting requests, needs refactor --- hac-client/src/app.rs | 4 + .../collection_list/delete_collection.rs | 20 +- .../collection_viewer/collection_viewer.rs | 6 + .../sidebar/create_directory_form.rs | 74 ------ .../sidebar/delete_item_form.rs | 190 +++++++++++++++ .../sidebar/delete_item_prompt.rs | 121 ---------- .../sidebar/directory_form.rs | 94 -------- .../sidebar/edit_directory_form.rs | 81 ------- .../pages/collection_viewer/sidebar/mod.rs | 37 +-- .../collection_viewer/sidebar/request_form.rs | 225 ------------------ .../sidebar/request_form_layout.rs | 2 +- .../sidebar/select_request_parent.rs | 160 ------------- hac-client/src/pages/mod.rs | 4 +- hac-client/src/router.rs | 6 - hac-store/src/collection.rs | 65 +++-- hac-store/src/slab.rs | 37 ++- 16 files changed, 293 insertions(+), 833 deletions(-) delete mode 100644 hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs create mode 100644 hac-client/src/pages/collection_viewer/sidebar/delete_item_form.rs delete mode 100644 hac-client/src/pages/collection_viewer/sidebar/delete_item_prompt.rs delete mode 100644 hac-client/src/pages/collection_viewer/sidebar/directory_form.rs delete mode 100644 hac-client/src/pages/collection_viewer/sidebar/edit_directory_form.rs delete mode 100644 hac-client/src/pages/collection_viewer/sidebar/request_form.rs delete mode 100644 hac-client/src/pages/collection_viewer/sidebar/select_request_parent.rs diff --git a/hac-client/src/app.rs b/hac-client/src/app.rs index a9f7069..05db92b 100755 --- a/hac-client/src/app.rs +++ b/hac-client/src/app.rs @@ -40,6 +40,7 @@ pub enum Routes { CollectionViewer, CreateRequest, EditRequest, + DeleteItem, } impl TryFrom for Routes { @@ -53,6 +54,8 @@ impl TryFrom for Routes { 3 => Ok(Routes::DeleteCollection), 4 => Ok(Routes::CollectionViewer), 5 => Ok(Routes::CreateRequest), + 6 => Ok(Routes::EditRequest), + 7 => Ok(Routes::DeleteItem), _ => Err(InvalidRouteNumber(value)), } } @@ -68,6 +71,7 @@ impl From for u8 { Routes::CollectionViewer => 4, Routes::CreateRequest => 5, Routes::EditRequest => 6, + Routes::DeleteItem => 7, } } } diff --git a/hac-client/src/pages/collection_list/delete_collection.rs b/hac-client/src/pages/collection_list/delete_collection.rs index f785946..23b328a 100644 --- a/hac-client/src/pages/collection_list/delete_collection.rs +++ b/hac-client/src/pages/collection_list/delete_collection.rs @@ -13,8 +13,8 @@ use super::{CollectionListData, Routes}; use crate::ascii::LOGO_ASCII; use crate::pages::overlay::make_overlay; use crate::renderable::{Eventful, Renderable}; -use crate::router::{Navigate, RouterMessage}; -use crate::{HacColors, HacConfig, MIN_HEIGHT, MIN_WIDTH}; +use crate::router::RouterMessage; +use crate::{router_drop_dialog, router_navigate_back, HacColors, HacConfig, MIN_HEIGHT, MIN_WIDTH}; #[derive(Debug)] struct DeleteCollectionLayout { @@ -116,23 +116,15 @@ impl Eventful for DeleteCollection { match key_event.code { KeyCode::Esc => { - self.messager - .send(RouterMessage::Navigate(Navigate::Back)) - .expect("failed to send router message"); - self.messager - .send(RouterMessage::DelDialog(Routes::DeleteCollection.into())) - .expect("failed to send router message"); + router_navigate_back!(&self.messager); + router_drop_dialog!(&self.messager, Routes::DeleteCollection.into()); } KeyCode::Char('o') | KeyCode::Char('y') | KeyCode::Enter => { let name = hac_store::collection_meta::get_collection_meta(self.selected_idx, |meta| meta.name().to_string()); hac_loader::collection_loader::delete_collection(name, &self.config)?; - self.messager - .send(RouterMessage::Navigate(Navigate::Back)) - .expect("failed to send router message"); - self.messager - .send(RouterMessage::DelDialog(Routes::DeleteCollection.into())) - .expect("failed to send router message"); + router_navigate_back!(&self.messager); + router_drop_dialog!(&self.messager, Routes::DeleteCollection.into()); } _ => {} } diff --git a/hac-client/src/pages/collection_viewer/collection_viewer.rs b/hac-client/src/pages/collection_viewer/collection_viewer.rs index 07e979e..799b66b 100755 --- a/hac-client/src/pages/collection_viewer/collection_viewer.rs +++ b/hac-client/src/pages/collection_viewer/collection_viewer.rs @@ -16,6 +16,7 @@ use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use super::request_uri::{RequestUri, RequestUriEvent}; use super::sidebar::create_request_form::CreateRequestForm; +use super::sidebar::delete_item_form::DeleteItemForm; use super::sidebar::{Sidebar, SidebarEvent}; use crate::app::Routes; use crate::pages::collection_list::CollectionListData; @@ -474,6 +475,11 @@ impl Eventful for CollectionViewer { self.update_selection(None); self.focus_prev(); } + Some(SidebarEvent::DeleteItem) => { + let delete_item_form = DeleteItemForm::new(self.colors.clone(), self.layout.total_size); + router_add_dialog!(&self.messager, Routes::DeleteItem, delete_item_form); + router_navigate_to!(&self.messager, Routes::DeleteItem); + } Some(SidebarEvent::CreateRequest) => { tracing::trace!("opening create request form from sidebar"); let create_request_form = CreateRequestForm::new(self.colors.clone(), self.layout.total_size); diff --git a/hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs b/hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs deleted file mode 100644 index 8a4c547..0000000 --- a/hac-client/src/pages/collection_viewer/sidebar/create_directory_form.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::cell::RefCell; -use std::rc::Rc; -use std::sync::{Arc, RwLock}; - -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use hac_store::collection::{Folder, ReqTree}; - -use super::directory_form::{DirectoryForm, DirectoryFormCreate, DirectoryFormEvent}; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::collection_viewer::sidebar::DirectoryFormTrait; -use crate::pages::Eventful; - -impl<'df> DirectoryForm<'df, DirectoryFormCreate> { - pub fn new( - colors: &'df hac_colors::Colors, - collection_store: Rc>, - ) -> DirectoryForm<'df, DirectoryFormCreate> { - DirectoryForm { - colors, - collection_store, - dir_name: String::default(), - marker: std::marker::PhantomData, - directory: None, - } - } -} - -impl DirectoryFormTrait for DirectoryForm<'_, DirectoryFormCreate> {} - -impl Eventful for DirectoryForm<'_, DirectoryFormCreate> { - type Result = DirectoryFormEvent; - - fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - self.reset(); - return Ok(Some(DirectoryFormEvent::Cancel)); - } - - match key_event.code { - KeyCode::Esc => { - self.reset(); - return Ok(Some(DirectoryFormEvent::Cancel)); - } - KeyCode::Enter => { - let store = self.collection_store.borrow_mut(); - let collection = store - .get_collection() - .expect("tried to create a request without a collection"); - - let mut collection = collection.borrow_mut(); - let requests = collection.requests.get_or_insert(Arc::new(RwLock::new(vec![]))); - let mut requests = requests.write().unwrap(); - - if self.dir_name.is_empty() { - self.dir_name = "unnamed directory".into(); - } - - requests.push(ReqTree::Folder(Folder { - name: self.dir_name.clone(), - requests: vec![], - })); - - drop(store); - self.reset(); - return Ok(Some(DirectoryFormEvent::Confirm)); - } - KeyCode::Char(c) => self.dir_name.push(c), - KeyCode::Backspace => _ = self.dir_name.pop(), - _ => {} - } - - Ok(None) - } -} diff --git a/hac-client/src/pages/collection_viewer/sidebar/delete_item_form.rs b/hac-client/src/pages/collection_viewer/sidebar/delete_item_form.rs new file mode 100644 index 0000000..b3aa5bb --- /dev/null +++ b/hac-client/src/pages/collection_viewer/sidebar/delete_item_form.rs @@ -0,0 +1,190 @@ +use std::sync::mpsc::{channel, Sender}; + +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use hac_core::command::Command; +use hac_store::collection::WhichSlab; +use ratatui::layout::{Constraint, Flex, Layout, Margin, Rect}; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::widgets::{Block, Borders, Paragraph, Wrap}; +use ratatui::Frame; + +use crate::app::Routes; +use crate::ascii::LOGO_ASCII; +use crate::pages::overlay::make_overlay; +use crate::renderable::{Eventful, Renderable}; +use crate::router::RouterMessage; +use crate::{router_drop_dialog, HacColors, MIN_HEIGHT, MIN_WIDTH}; + +#[derive(Debug)] +struct DeleteItemLayout { + logo: Rect, + message: Rect, + left_button: Rect, + right_button: Rect, +} + +#[derive(Debug)] +pub struct DeleteItemForm { + colors: HacColors, + layout: DeleteItemLayout, + messager: Sender, +} + +impl DeleteItemForm { + pub fn new(colors: HacColors, size: Rect) -> Self { + DeleteItemForm { + colors, + layout: build_layout(size), + messager: channel().0, + } + } +} + +impl Renderable for DeleteItemForm { + type Input = (); + type Output = (); + + fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { + make_overlay(self.colors.clone(), self.colors.normal.black, 0.15, frame); + + let (slab, key) = + hac_store::collection::get_hovered_request(|entry| entry).expect("deleting a non-existing request??"); + + let message = match slab { + WhichSlab::Requests => hac_store::collection::get_request(key, |req, _| { + vec![ + "are you sure you want to delete the request " + .fg(self.colors.bright.red) + .not_bold(), + req.name.clone().fg(self.colors.bright.red).bold().underlined(), + "?".fg(self.colors.bright.red).not_bold(), + ] + }), + WhichSlab::RootRequests => hac_store::collection::get_root_request(key, |req, _| { + vec![ + "are you sure you want to delete the request " + .fg(self.colors.bright.red) + .not_bold(), + req.name.clone().fg(self.colors.bright.red).bold().underlined(), + "?".fg(self.colors.bright.red).not_bold(), + ] + }), + WhichSlab::Folders => hac_store::collection::get_folder(key, |folder, _| { + vec![ + "are you sure you want to delete the folder " + .fg(self.colors.bright.red) + .not_bold(), + folder.name.clone().fg(self.colors.bright.red).bold().underlined(), + "? This will also delete every request inside of this folder" + .fg(self.colors.bright.red) + .not_bold(), + ] + }), + }; + + let message = Paragraph::new(Line::from(message).centered()).wrap(Wrap { trim: true }); + + let left_button = Paragraph::new(Line::from("[ENTER] CONFIRM").centered().fg(self.colors.normal.black)) + .block(Block::default().borders(Borders::ALL).fg(self.colors.bright.red)) + .bg(self.colors.bright.red) + .bold(); + + let right_button = Paragraph::new(Line::from("[ESC] CANCEL").centered()) + .block(Block::default().borders(Borders::ALL).fg(self.colors.normal.blue)) + .fg(self.colors.bright.red) + .bg(self.colors.normal.blue) + .bold(); + + frame.render_widget(message, self.layout.message); + frame.render_widget(left_button, self.layout.left_button); + frame.render_widget(right_button, self.layout.right_button); + + let logo = LOGO_ASCII + .iter() + .map(|line| Line::from(line.fg(self.colors.normal.red)).centered()) + .collect::>(); + frame.render_widget(Paragraph::new(logo), self.layout.logo); + + Ok(()) + } + + fn attach_navigator(&mut self, messager: Sender) { + self.messager = messager; + } + + fn data(&self, _: u8) -> Self::Output {} +} + +impl Eventful for DeleteItemForm { + type Result = Command; + + fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { + return Ok(Some(Command::Quit)); + } + + match key_event.code { + KeyCode::Esc => { + router_drop_dialog!(&self.messager, Routes::DeleteItem.into()); + } + KeyCode::Char('o') | KeyCode::Char('y') | KeyCode::Enter => { + let hovered_entry = hac_store::collection::get_hovered_request(|entry| entry) + .expect("should have hovered request to delete"); + let selected_entry = hac_store::collection::get_selected_entry(); + + hac_store::collection::hover_prev(); + + match hovered_entry.0 { + WhichSlab::Requests => _ = hac_store::collection::remove_request(hovered_entry.1), + WhichSlab::RootRequests => _ = hac_store::collection::remove_root_request(hovered_entry.1), + WhichSlab::Folders => _ = hac_store::collection::remove_folder(hovered_entry.1), + }; + + if let Some((slab, key)) = selected_entry { + if slab == hovered_entry.0 && key == hovered_entry.1 { + hac_store::collection::set_selected_request(None); + } + } + + hac_store::collection::rebuild_tree_layout(); + router_drop_dialog!(&self.messager, Routes::DeleteItem.into()); + } + _ => {} + } + + Ok(None) + } +} + +fn build_layout(area: Rect) -> DeleteItemLayout { + let [_, form, _] = Layout::horizontal([Constraint::Fill(1), Constraint::Length(MIN_WIDTH), Constraint::Fill(1)]) + .flex(Flex::Center) + .areas(area); + + let [_, form, _] = Layout::vertical([Constraint::Fill(1), Constraint::Length(MIN_HEIGHT), Constraint::Fill(1)]) + .flex(Flex::End) + .areas(form); + + let form = form.inner(&Margin::new(2, 0)); + + let [logo, _, message, _, buttons] = Layout::vertical([ + Constraint::Length(LOGO_ASCII.len() as u16), + Constraint::Length(1), + Constraint::Length(3), + Constraint::Length(1), + Constraint::Length(3), + ]) + .flex(Flex::Center) + .areas(form); + + let [left_button, _, right_button] = + Layout::horizontal([Constraint::Fill(1), Constraint::Length(1), Constraint::Fill(1)]).areas(buttons); + + DeleteItemLayout { + logo, + message, + left_button, + right_button, + } +} diff --git a/hac-client/src/pages/collection_viewer/sidebar/delete_item_prompt.rs b/hac-client/src/pages/collection_viewer/sidebar/delete_item_prompt.rs deleted file mode 100644 index b125985..0000000 --- a/hac-client/src/pages/collection_viewer/sidebar/delete_item_prompt.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::cell::RefCell; -use std::ops::{Add, Div, Mul, Sub}; -use std::rc::Rc; - -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use rand::Rng; -use ratatui::layout::Rect; -use ratatui::style::Stylize; -use ratatui::text::Line; -use ratatui::widgets::Paragraph; -use ratatui::Frame; - -use crate::ascii::LOGO_ASCII; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::overlay::make_overlay_old; -use crate::pages::{Eventful, Renderable}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DeleteItemPromptEvent { - Confirm, - Cancel, -} - -#[derive(Debug)] -pub struct DeleteItemPrompt<'dip> { - colors: &'dip hac_colors::Colors, - collection_store: Rc>, - logo_idx: usize, -} - -impl<'dip> DeleteItemPrompt<'dip> { - pub fn new(colors: &'dip hac_colors::Colors, collection_store: Rc>) -> Self { - let logo_idx = rand::thread_rng().gen_range(0..LOGO_ASCII.len()); - DeleteItemPrompt { - colors, - logo_idx, - collection_store, - } - } -} - -impl Renderable for DeleteItemPrompt<'_> { - fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { - make_overlay_old(self.colors, self.colors.normal.black, 0.1, frame); - - let store = self.collection_store.borrow(); - let Some(hovered_id) = store.get_hovered_request().as_ref().cloned() else { - unreachable!(); - }; - let Some(collection) = store.get_collection() else { - unreachable!(); - }; - let collection = collection.borrow(); - let Some(ref requests) = collection.requests else { - unreachable!(); - }; - let is_dir = requests - .read() - .unwrap() - .iter() - .find(|req| req.get_id().eq(&hovered_id)) - .is_some_and(|req| req.is_dir()); - - let mut lines = if is_dir { - vec![ - Line::from("Are you sure you want to delete the directory?".fg(self.colors.normal.red)).centered(), - Line::from("This will delete all the requests inside".fg(self.colors.normal.red)).centered(), - Line::from(""), - ] - } else { - vec![ - Line::from("Are you sure you want to delete the request?".fg(self.colors.normal.red)).centered(), - Line::from(""), - ] - }; - - lines.push(Line::from("[Confirm: Enter] [Cancel: Esc]".fg(self.colors.bright.black)).centered()); - - let logo = LOGO_ASCII; - let logo_size = logo.len() as u16; - let size = frame.size(); - - let size = Rect::new( - size.width.div(2).sub(25), - size.height.div(2).saturating_sub(logo_size.div(2)).saturating_sub(3), - 50, - logo_size.add(7), - ); - - let logo_size = Rect::new(size.x, size.y, size.width, logo_size); - let logo = logo - .iter() - .map(|line| Line::from(line.fg(self.colors.normal.red)).centered()) - .collect::>(); - frame.render_widget(Paragraph::new(logo), logo_size); - - for (idx, line) in lines.into_iter().enumerate() { - let y_offset = logo_size.height.add(2).add(idx.mul(1) as u16); - let line_size = Rect::new(size.x, size.y.add(y_offset), size.width, 1); - frame.render_widget(Paragraph::new(line), line_size); - } - - Ok(()) - } -} - -impl Eventful for DeleteItemPrompt<'_> { - type Result = DeleteItemPromptEvent; - - fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - return Ok(Some(DeleteItemPromptEvent::Cancel)); - } - - match key_event.code { - KeyCode::Enter => Ok(Some(DeleteItemPromptEvent::Confirm)), - KeyCode::Esc => Ok(Some(DeleteItemPromptEvent::Cancel)), - _ => Ok(None), - } - } -} diff --git a/hac-client/src/pages/collection_viewer/sidebar/directory_form.rs b/hac-client/src/pages/collection_viewer/sidebar/directory_form.rs deleted file mode 100644 index 38bd11d..0000000 --- a/hac-client/src/pages/collection_viewer/sidebar/directory_form.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::cell::RefCell; -use std::ops::{Add, Div, Sub}; -use std::rc::Rc; - -use ratatui::layout::Rect; -use ratatui::style::Stylize; -use ratatui::text::Line; -use ratatui::widgets::Paragraph; - -use crate::ascii::LOGO_ASCII; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::input::Input; -use crate::pages::overlay::make_overlay_old; -use crate::pages::Renderable; - -/// set of events `DirectoryForm` can send the parent to -/// handle -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum DirectoryFormEvent { - /// when user confirms the directory creation we should notify - /// the parent to properly handle the event - Confirm, - /// when the user cancels the creation, we should also notify - /// the parent to properly clean up things - Cancel, -} - -#[derive(Debug)] -pub struct DirectoryFormCreate; - -#[derive(Debug)] -pub struct DirectoryFormEdit; - -#[derive(Debug)] -pub struct DirectoryForm<'df, State = DirectoryFormCreate> { - pub colors: &'df hac_colors::Colors, - pub dir_name: String, - pub collection_store: Rc>, - /// the id of the directory being edited, this is only used when editing a directory - /// this is (dir_id, dir_name) - pub directory: Option<(String, String)>, - - pub marker: std::marker::PhantomData, -} - -impl<'cdf, State> DirectoryForm<'cdf, State> { - pub fn reset(&mut self) { - self.dir_name.clear(); - } -} - -impl Renderable for DirectoryForm<'_, State> { - fn draw(&mut self, frame: &mut ratatui::prelude::Frame, _: ratatui::prelude::Rect) -> anyhow::Result<()> { - make_overlay_old(self.colors, self.colors.normal.black, 0.1, frame); - - let logo = LOGO_ASCII; - let logo_size = logo.len() as u16; - - let size = frame.size(); - let size = Rect::new( - size.width.div(2).sub(25), - size.height.div(2).saturating_sub(logo_size.div(2)).saturating_sub(2), - 50, - logo_size.add(4), - ); - - let logo = logo - .iter() - .map(|line| Line::from(line.to_string().fg(self.colors.normal.red)).centered()) - .collect::>(); - - let mut input = Input::new(self.colors, "Name".into()); - input.focus(); - - let hint = Line::from("[Confirm: Enter] [Cancel: Esc]") - .fg(self.colors.bright.black) - .centered(); - - let logo_size = Rect::new(size.x, size.y, size.width, logo_size); - let input_size = Rect::new(size.x, logo_size.y.add(logo_size.height).add(1), size.width, 3); - let hint_size = Rect::new(size.x, input_size.y.add(4), size.width, 1); - - frame.render_widget(Paragraph::new(logo), logo_size); - frame.render_stateful_widget(input, input_size, &mut self.dir_name); - frame.render_widget(hint, hint_size); - - frame.set_cursor( - input_size.x.add(self.dir_name.chars().count() as u16).add(1), - input_size.y.add(1), - ); - - Ok(()) - } -} diff --git a/hac-client/src/pages/collection_viewer/sidebar/edit_directory_form.rs b/hac-client/src/pages/collection_viewer/sidebar/edit_directory_form.rs deleted file mode 100644 index a0ee2dc..0000000 --- a/hac-client/src/pages/collection_viewer/sidebar/edit_directory_form.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::cell::RefCell; -use std::rc::Rc; -use std::sync::{Arc, RwLock}; - -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use hac_core::collection::types::*; -use rand::Rng; - -use super::directory_form::{DirectoryForm, DirectoryFormEdit, DirectoryFormEvent}; -use crate::ascii::LOGO_ASCII; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::collection_viewer::sidebar::DirectoryFormTrait; -use crate::pages::Eventful; - -impl<'df> DirectoryForm<'df, DirectoryFormEdit> { - pub fn new( - colors: &'df hac_colors::Colors, - collection_store: Rc>, - directory: Option<(String, String)>, - ) -> DirectoryForm<'df, DirectoryFormEdit> { - let dir_name = directory.as_ref().map(|dir| dir.1.clone()).unwrap_or_default(); - - DirectoryForm { - colors, - collection_store, - dir_name, - marker: std::marker::PhantomData, - directory, - } - } -} - -impl DirectoryFormTrait for DirectoryForm<'_, DirectoryFormEdit> {} - -impl Eventful for DirectoryForm<'_, DirectoryFormEdit> { - type Result = DirectoryFormEvent; - - fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - self.reset(); - return Ok(Some(DirectoryFormEvent::Cancel)); - } - - match key_event.code { - KeyCode::Esc => { - self.reset(); - return Ok(Some(DirectoryFormEvent::Cancel)); - } - KeyCode::Enter => { - let store = self.collection_store.borrow_mut(); - let collection = store - .get_collection() - .expect("tried to create a request without a collection"); - - let mut collection = collection.borrow_mut(); - let requests = collection.requests.get_or_insert(Arc::new(RwLock::new(vec![]))); - let mut requests = requests.write().unwrap(); - - if self.dir_name.is_empty() { - self.dir_name = "unnamed directory".into(); - } - - if let Some(RequestKind::Nested(dir)) = requests - .iter_mut() - .find(|req| req.get_id().eq(&self.directory.as_ref().unwrap().0)) - { - dir.name.clone_from(&self.dir_name); - } - - drop(store); - self.reset(); - return Ok(Some(DirectoryFormEvent::Confirm)); - } - KeyCode::Char(c) => self.dir_name.push(c), - KeyCode::Backspace => _ = self.dir_name.pop(), - _ => {} - } - - Ok(None) - } -} diff --git a/hac-client/src/pages/collection_viewer/sidebar/mod.rs b/hac-client/src/pages/collection_viewer/sidebar/mod.rs index 12a3851..2173484 100755 --- a/hac-client/src/pages/collection_viewer/sidebar/mod.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/mod.rs @@ -1,12 +1,7 @@ pub mod create_request_form; +pub mod delete_item_form; pub mod edit_request_form; mod request_form_layout; -// mod create_directory_form; -// mod delete_item_prompt; -// mod directory_form; -// mod edit_directory_form; -// mod request_form; -// mod select_request_parent; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_store::collection::{self, EntryStatus, ReqMethod, ReqTreeNode, WhichSlab}; @@ -29,6 +24,7 @@ pub enum SidebarEvent { RemoveSelection, CreateRequest, EditRequest, + DeleteItem, Quit, } @@ -256,6 +252,7 @@ impl Eventful for Sidebar { Ok(None) }); } + KeyCode::Char('d') => return Ok(Some(SidebarEvent::DeleteItem)), KeyCode::Char('n') => return Ok(Some(SidebarEvent::CreateRequest)), KeyCode::Char('?') => { self.show_extended_hint = !self.show_extended_hint; @@ -274,34 +271,6 @@ impl Eventful for Sidebar { }; } _ => (), - // KeyCode::Char('e') => { - // let hovered_request = store.find_hovered_request(); - // drop(store); - // match hovered_request { - // RequestKind::Single(req) => { - // self.request_form = RequestFormVariant::Edit(RequestForm::::new( - // self.colors, - // self.collection_store.clone(), - // req.clone(), - // )); - // return Ok(Some(SidebarEvent::EditRequest)); - // } - // RequestKind::Nested(dir) => { - // self.directory_form = DirectoryFormVariant::Edit(DirectoryForm::::new( - // self.colors, - // self.collection_store.clone(), - // Some((dir.id.clone(), dir.name.clone())), - // )); - // return Ok(Some(SidebarEvent::EditDirectory)); - // } - // } - // } - // KeyCode::Char('D') => { - // if let Some(item_id) = store.get_hovered_request() { - // return Ok(Some(SidebarEvent::DeleteItem(item_id))); - // } - // } - // KeyCode::Char('d') => return Ok(Some(SidebarEvent::CreateDirectory)), } Ok(None) diff --git a/hac-client/src/pages/collection_viewer/sidebar/request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/request_form.rs deleted file mode 100644 index a657a62..0000000 --- a/hac-client/src/pages/collection_viewer/sidebar/request_form.rs +++ /dev/null @@ -1,225 +0,0 @@ -use std::ops::{Add, Div, Sub}; -use std::sync::{Arc, RwLock}; - -use ratatui::layout::{Constraint, Direction, Layout, Rect}; -use ratatui::style::Stylize; -use ratatui::text::Line; -use ratatui::widgets::{Block, Borders, Paragraph}; -use ratatui::Frame; - -use crate::ascii::LOGO_ASCII; -use crate::pages::input::Input; -use crate::pages::overlay::make_overlay_old; -use crate::renderable::Renderable; - -#[derive(Debug)] -pub enum RequestFormEvent { - Confirm, - Cancel, -} - -#[derive(Debug, PartialEq)] -pub enum FormField { - Name, - Method, - Parent, -} - -impl FormField { - pub fn next(&self) -> Self { - match self { - FormField::Name => FormField::Method, - FormField::Method => FormField::Parent, - FormField::Parent => FormField::Name, - } - } - - pub fn prev(&self) -> Self { - match self { - FormField::Name => FormField::Parent, - FormField::Method => FormField::Name, - FormField::Parent => FormField::Method, - } - } -} - -#[derive(Debug)] -pub struct RequestFormCreate; - -#[derive(Debug)] -pub struct RequestFormEdit; - -#[derive(Debug)] -pub struct RequestForm<'rf, State = RequestFormCreate> { - pub colors: &'rf hac_colors::Colors, - - /// the name of the current request being edited or created - pub request_name: String, - /// which method the request should have when finishing edition or creation - pub request_method: RequestMethod, - /// we store the parent dir uuid so its easier to find it and we dont need - /// lifetimes or to Rc our way to hell, along with it we also store the name - /// for displaying purposes - pub parent_dir: Option<(String, String)>, - /// which form field is currently focused, so we can direct interactions - /// accordingly - pub focused_field: FormField, - /// `request` is only used when editing a request so we can update it directly - pub request: Option>>, - /// parent selector is the form used to select a parent for the current edited - /// or created request - pub parent_selector: SelectRequestParent<'rf>, - /// when the user tries to select a parent for a given request but there are - /// no directories on the collection, we use this timer to show a message for - /// a short duration, alerting the user - pub no_available_parent_timer: Option, - - pub marker: std::marker::PhantomData, -} - -impl<'rf, State> RequestForm<'rf, State> { - pub fn reset(&mut self) { - self.request_name = String::default(); - self.request_method = RequestMethod::Get; - self.focused_field = FormField::Name; - self.parent_dir = None; - } - - pub fn set_no_parent_timer(&mut self) { - self.no_available_parent_timer = Some(std::time::Instant::now()); - } -} - -impl<'rf, State> Renderable for RequestForm<'rf, State> { - fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { - make_overlay_old(self.colors, self.colors.normal.black, 0.1, frame); - - let mut logo = LOGO_ASCII; - let mut logo_size = logo.len() as u16; - // adding size of the form + spacing + hint - let total_size = logo_size.add(11).add(2); - - let size = frame.size(); - let mut size = Rect::new( - size.width.div(2).sub(32), - size.height.div(2).saturating_sub(logo_size.div(2)).saturating_sub(6), - 65, - logo_size.add(12), - ); - - if total_size.ge(&frame.size().height) { - logo = &[]; - logo_size = 0; - size.height = 12; - size.y = frame.size().height.div(2).saturating_sub(5); - } - - if !logo.is_empty() { - let logo = logo - .iter() - .map(|line| Line::from(line.fg(self.colors.normal.red)).centered()) - .collect::>(); - - let logo_size = Rect::new(size.x, size.y, size.width, logo_size); - frame.render_widget(Paragraph::new(logo), logo_size); - } - - let mut name_input = Input::new(self.colors, "Name".into()); - let method_title = Paragraph::new("Method".fg(self.colors.normal.white)); - let hint = "[Confirm: Enter] [Cancel: Esc] [Switch: Tab] [Select: Space] [Remove Parent: ]"; - let hint_size = hint.len() as u16; - let hint = Paragraph::new(hint.fg(self.colors.bright.black)).centered(); - - if self.focused_field.eq(&FormField::Name) { - name_input.focus(); - } - - let name_size = Rect::new(size.x, size.y.add(logo_size).add(1), size.width, 3); - let method_title_size = Rect::new(size.x, name_size.y.add(3), size.width, 1); - let methods_size = Rect::new(size.x, method_title_size.y.add(1), size.width, 3); - let parent_size = Rect::new(size.x, methods_size.y.add(3), size.width, 3); - let hint_size = Rect::new( - frame.size().width.div(2).saturating_sub(hint_size.div(2)), - parent_size.y.add(4), - hint_size, - 1, - ); - - let methods_items = Layout::default() - .direction(Direction::Horizontal) - .constraints((0..5).map(|_| Constraint::Length(13))) - .split(methods_size); - - let parent_name = if self.parent_dir.is_none() { - format!( - "{}None{}", - " ".repeat(parent_size.width.div(2).sub(2).into()), - " ".repeat(parent_size.width.div(2).sub(2).into()) - ) - .fg(self.colors.bright.black) - } else { - format!( - "{}{}{}", - " ".repeat(parent_size.width.div(2).sub(2).into()), - self.parent_dir.as_ref().unwrap().1.clone(), - " ".repeat(parent_size.width.div(2).sub(2).into()) - ) - .fg(self.colors.normal.white) - }; - let parent = Paragraph::new(parent_name).centered().block( - Block::default() - .title("Parent".fg(self.colors.normal.white)) - .borders(Borders::ALL) - .fg(if self.focused_field.eq(&FormField::Parent) { - self.colors.normal.red - } else { - self.colors.bright.black - }), - ); - - for (idx, method) in RequestMethod::iter().enumerate() { - let border_color = match (&self.request_method, &self.focused_field) { - (m, FormField::Method) if m.eq(method) => self.colors.normal.red, - (m, _) if m.eq(method) => self.colors.bright.blue, - _ => self.colors.bright.black, - }; - let method = Paragraph::new(Line::from(vec![ - format!(" {}", idx.add(1)).fg(self.colors.bright.black), - format!(" {}{}", method, " ".repeat(10.sub(method.to_string().len()))).fg(self.colors.normal.white), - ])) - .block(Block::default().borders(Borders::ALL).fg(border_color)); - frame.render_widget(method, methods_items[idx]); - } - - frame.render_stateful_widget(name_input, name_size, &mut self.request_name); - frame.render_widget(method_title, method_title_size); - frame.render_widget(parent, parent_size); - - if self - .no_available_parent_timer - .is_some_and(|timer| timer.elapsed().as_secs().le(&3)) - { - let warning = - Paragraph::new("No available directory to select as a parent".fg(self.colors.normal.red)).centered(); - frame.render_widget(warning, hint_size); - } else { - frame.render_widget(hint, hint_size); - } - - if self - .no_available_parent_timer - .is_some_and(|timer| timer.elapsed().as_secs().gt(&3)) - { - self.no_available_parent_timer = None; - } - - if self.focused_field.eq(&FormField::Name) { - frame.set_cursor( - name_size.x.add(self.request_name.chars().count() as u16).add(1), - name_size.y.add(1), - ); - } - - Ok(()) - } -} diff --git a/hac-client/src/pages/collection_viewer/sidebar/request_form_layout.rs b/hac-client/src/pages/collection_viewer/sidebar/request_form_layout.rs index 49c7dbc..062cd59 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/request_form_layout.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/request_form_layout.rs @@ -128,7 +128,7 @@ pub fn draw_main_form( false => colors.bright.black, }; let area = layout.methods[idx]; - let method = method.to_string(); + let method = m.to_string(); let remaining_width = area.width as usize - 3 - method.len(); let left_pad = remaining_width / 2; diff --git a/hac-client/src/pages/collection_viewer/sidebar/select_request_parent.rs b/hac-client/src/pages/collection_viewer/sidebar/select_request_parent.rs deleted file mode 100644 index 1b6ebf1..0000000 --- a/hac-client/src/pages/collection_viewer/sidebar/select_request_parent.rs +++ /dev/null @@ -1,160 +0,0 @@ -use std::cell::RefCell; -use std::ops::{Add, Div, Mul, Sub}; -use std::rc::Rc; - -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use rand::Rng; -use ratatui::layout::Rect; -use ratatui::style::Stylize; -use ratatui::text::Line; -use ratatui::widgets::Paragraph; -use ratatui::Frame; - -use crate::ascii::LOGO_ASCII; -use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::overlay::make_overlay_old; -use crate::pages::{Eventful, Renderable}; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum SelectRequestParentEvent { - /// user selected a directory, so we send back the id of the directory to be - /// linked to the request. - Confirm(String), - /// user canceled the parent selection, so nothing will be linked to the - /// request - Cancel, -} - -#[derive(Debug)] -pub struct SelectRequestParent<'srp> { - colors: &'srp hac_colors::Colors, - collection_store: Rc>, - selected_dir: usize, - scroll: usize, -} - -impl<'srp> SelectRequestParent<'srp> { - pub fn new(colors: &'srp hac_colors::Colors, collection_store: Rc>) -> Self { - SelectRequestParent { - colors, - collection_store, - selected_dir: 0, - scroll: 0, - } - } -} - -impl Renderable for SelectRequestParent<'_> { - fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { - make_overlay_old(self.colors, self.colors.normal.black, 0.1, frame); - - let store = self.collection_store.borrow(); - let collection = store - .get_collection() - .expect("trying to select a parent directory without a collection"); - - let mut directories = vec![]; - if let Some(ref requests) = collection.borrow().requests { - requests - .read() - .unwrap() - .iter() - .filter(|req| req.is_dir()) - .for_each(|dir| directories.push(dir.get_name())); - }; - - let mut logo = LOGO_ASCII; - let size = frame.size(); - let mut logo_size = logo.len() as u16; - - // if the logo makes the screen have 10 or less height, we hide it - if size.height.sub(logo_size).le(&10) { - logo = &[]; - logo_size = 0; - } - - let size = Rect::new(size.width.div(2).saturating_sub(25), size.y.add(4), 50, size.height); - - if !logo.is_empty() { - let logo_size = Rect::new(size.x, size.y, size.width, logo_size); - let logo = logo - .iter() - .map(|line| Line::from(line.fg(self.colors.normal.red)).centered()) - .collect::>(); - - frame.render_widget(Paragraph::new(logo), logo_size); - } - - let item_height = 2; - let remaining_space = size.height.sub(logo_size).sub(1); - let amount_on_view = remaining_space.div(item_height); - let dirs_start_y = logo_size.add(3); - - let header = Paragraph::new("Available directories".fg(self.colors.normal.yellow).bold()); - let header_size = Rect::new(size.x, size.y.add(logo_size).add(1), size.width, 2); - frame.render_widget(header, header_size); - - for (idx, dir) in directories - .into_iter() - .enumerate() - .skip(self.scroll) - .take(amount_on_view.into()) - { - let foreground = if self.selected_dir.eq(&idx) { self.colors.normal.red } else { self.colors.normal.white }; - let dir_size = Rect::new(size.x, size.y.add(dirs_start_y).add(idx.mul(2) as u16), size.width, 2); - let dir = Paragraph::new(dir.fg(foreground)); - frame.render_widget(dir, dir_size); - } - - Ok(()) - } -} - -impl Eventful for SelectRequestParent<'_> { - type Result = SelectRequestParentEvent; - - fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - return Ok(Some(SelectRequestParentEvent::Cancel)); - } - - let store = self.collection_store.borrow(); - let collection = store - .get_collection() - .expect("trying to select a parent directory without a collection"); - - let mut directories = vec![]; - if let Some(ref requests) = collection.borrow().requests { - requests - .read() - .unwrap() - .iter() - .filter(|req| req.is_dir()) - .for_each(|dir| directories.push(dir.get_id())) - }; - let total_dirs = directories.len(); - - match key_event.code { - KeyCode::Enter => { - let (_, dir_id) = directories - .into_iter() - .enumerate() - .find(|(idx, _)| idx.eq(&self.selected_dir)) - .unwrap(); - return Ok(Some(SelectRequestParentEvent::Confirm(dir_id))); - } - KeyCode::Esc => { - return Ok(Some(SelectRequestParentEvent::Cancel)); - } - KeyCode::Down | KeyCode::Tab | KeyCode::Char('j') => { - self.selected_dir = usize::min(self.selected_dir.add(1), total_dirs.sub(1)); - } - KeyCode::Up | KeyCode::BackTab | KeyCode::Char('k') => { - self.selected_dir = self.selected_dir.saturating_sub(1); - } - _ => {} - } - - Ok(None) - } -} diff --git a/hac-client/src/pages/mod.rs b/hac-client/src/pages/mod.rs index 38b306a..570b65a 100755 --- a/hac-client/src/pages/mod.rs +++ b/hac-client/src/pages/mod.rs @@ -2,6 +2,6 @@ pub mod collection_list; pub mod collection_viewer; pub mod input; pub mod overlay; -mod spinner; +// mod spinner; +// mod under_construction; pub mod terminal_too_small; -mod under_construction; diff --git a/hac-client/src/router.rs b/hac-client/src/router.rs index 14c42ad..e0de1fc 100644 --- a/hac-client/src/router.rs +++ b/hac-client/src/router.rs @@ -8,7 +8,6 @@ use hac_core::command::Command; use ratatui::layout::Rect; use ratatui::Frame; -use crate::event_pool::Event; use crate::pages::terminal_too_small::TerminalTooSmall; use crate::renderable::{Eventful, Renderable}; use crate::{HacColors, MIN_HEIGHT, MIN_WIDTH}; @@ -79,7 +78,6 @@ pub trait AnyRenderable: Debug { fn register_command_handler(&mut self, sender: Sender); fn handle_command(&mut self, command: Command) -> anyhow::Result<()>; fn tick(&mut self) -> anyhow::Result<()>; - fn handle_event(&mut self, event: Option) -> anyhow::Result>; fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result>; } @@ -119,10 +117,6 @@ impl + Debug> AnyRenderable ::tick(self) } - fn handle_event(&mut self, event: Option) -> anyhow::Result> { - ::handle_event(self, event) - } - fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { ::handle_key_event(self, key_event) } diff --git a/hac-store/src/collection.rs b/hac-store/src/collection.rs index abed43f..76c90ec 100644 --- a/hac-store/src/collection.rs +++ b/hac-store/src/collection.rs @@ -33,7 +33,7 @@ impl From<(bool, bool)> for EntryStatus { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WhichSlab { Requests, Folders, @@ -183,6 +183,14 @@ pub fn set_collection(collection: Option) { HAC_STORE.with_borrow_mut(|store| store.collection = collection); } +pub fn get_selected_entry() -> Option<(WhichSlab, Key)> { + HAC_STORE.with_borrow(|store| { + assert!(store.collection.is_some()); + let collection = store.collection.as_ref().unwrap(); + collection.selected_request + }) +} + pub fn get_selected_request(f: F) -> Option where F: FnOnce(&Request) -> Option, @@ -264,6 +272,18 @@ pub fn remove_root_request(key: Key) -> Request { }) } +pub fn remove_folder(key: Key) -> Folder { + HAC_STORE.with_borrow_mut(|store| { + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + let folder = collection.folders.remove(key); + for req in folder.requests.iter() { + collection.requests.remove(*req); + } + folder + }) +} + pub fn get_root_request(key: Key, f: F) -> R where F: FnOnce(&Request, EntryStatus) -> R, @@ -355,9 +375,9 @@ pub fn has_folders() -> bool { len_folders() != 0 } -pub fn get_folder(key: Key, f: F) +pub fn get_folder(key: Key, f: F) -> R where - F: FnOnce(&Folder, EntryStatus), + F: FnOnce(&Folder, EntryStatus) -> R, { HAC_STORE.with_borrow(|store| { assert!(store.collection.is_some()); @@ -369,7 +389,7 @@ where let is_selected = collection .selected_request .is_some_and(|(slab, k)| matches!(slab, WhichSlab::Folders) && key == k); - f(folder, (is_hovered, is_selected).into()); + f(folder, (is_hovered, is_selected).into()) }) } @@ -391,12 +411,21 @@ where }) } +// TODO: this sucks, rewrite it probably pub fn hover_next() { HAC_STORE.with_borrow_mut(|store| { assert!(store.collection.is_some()); let collection = store.collection.as_mut().unwrap(); - if let Some((which, key)) = collection.hovered_request { - match which { + match collection.hovered_request { + None => { + if let Some(node) = collection.layout.nodes.first() { + match node { + ReqTreeNode::Req(key) => collection.hovered_request = Some((WhichSlab::RootRequests, *key)), + ReqTreeNode::Folder(key, _) => collection.hovered_request = Some((WhichSlab::Folders, *key)), + } + } + } + Some((which, key)) => match which { WhichSlab::Requests => { let req = collection.requests.get(key); let parent_key = req.parent.expect("nested request has no parent"); @@ -428,7 +457,8 @@ pub fn hover_next() { // when the folder is collapsed or has no requests, and its not the // last folder on the collection, we can hover the next folder if (folder.requests.is_empty() || folder.collapsed) && key < collection.folders.len() - 1 { - collection.hovered_request = Some((WhichSlab::Folders, key + 1)); + let key = collection.folders.next_key(key); + collection.hovered_request = Some((WhichSlab::Folders, key)); return; } @@ -452,7 +482,7 @@ pub fn hover_next() { // when we are not at the last one, we can simply hover the next collection.hovered_request = Some((WhichSlab::RootRequests, key + 1)); } - } + }, } }) } @@ -487,8 +517,13 @@ pub fn hover_prev() { // when we are hovering a folder, we try to hover the previous folders's last // request if it exists and the folder is not collapsed, otherwise we select // the previous folder itself + + // TODO: have a method that instead tries to find the previous key rather than + // doing index math, as indexes are unrealiable if key > 0 { - let folder = collection.folders.get(key - 1); + let Some(folder) = collection.folders.try_get(key - 1) else { + return; + }; if !folder.collapsed { if let Some(last) = folder.requests.last() { @@ -505,17 +540,18 @@ pub fn hover_prev() { if !collection.root_requests.is_empty() { let key = collection.root_requests.len() - 1; collection.hovered_request = Some((WhichSlab::RootRequests, key)); + return; } - // when there are no root requests or previou folders, we do nothing + collection.hovered_request = None; } WhichSlab::RootRequests => { - // when we are hovering on a root request, we either hover the preivous or - // do nothing - if key > 0 { collection.hovered_request = Some((WhichSlab::RootRequests, key - 1)); } + if collection.root_requests.is_empty() { + collection.hovered_request = None; + } } } } @@ -590,8 +626,7 @@ pub fn rebuild_tree_layout() { nodes.extend( collection .folders - .iter() - .enumerate() + .enumerated_iter() .map(|(idx, folder)| ReqTreeNode::Folder(idx, folder.requests.clone())) .collect::>(), ); diff --git a/hac-store/src/slab.rs b/hac-store/src/slab.rs index 8ec8aa4..37f8b06 100644 --- a/hac-store/src/slab.rs +++ b/hac-store/src/slab.rs @@ -55,6 +55,28 @@ impl Slab { } } + pub fn prev_key(&self, key: Key) -> Key { + assert!(key == 0, "cant call prev key on first key"); + tracing::error!("{key}"); + + for i in 0..key - 1 { + if let Entry::Full(_) = &self.inner[i] { + return i; + } + } + key + } + + pub fn next_key(&self, key: Key) -> Key { + assert!(key < self.inner.len() - 1, "cant call next key on last key"); + for i in (key + 1)..self.inner.len() { + if let Entry::Full(_) = &self.inner[i] { + return i; + } + } + key + } + pub fn remove(&mut self, idx: Key) -> T { let mut entry = Entry::free(self.next_idx.take()); self.next_idx = Some(idx); @@ -82,9 +104,7 @@ impl Slab { pub fn try_get(&self, idx: Key) -> Option<&T> { if let Some(entry) = self.inner.get(idx) { - let Entry::Full(val) = entry else { - panic!("attempted to get an empty entry"); - }; + let Entry::Full(val) = entry else { return None }; Some(val) } else { None @@ -93,9 +113,7 @@ impl Slab { pub fn try_get_mut(&mut self, idx: Key) -> Option<&mut T> { if let Some(entry) = self.inner.get_mut(idx) { - let Entry::Full(val) = entry else { - panic!("attempted to get an empty entry"); - }; + let Entry::Full(val) = entry else { return None }; Some(val) } else { None @@ -123,6 +141,13 @@ impl Slab { } } + pub fn enumerated_iter(&self) -> impl Iterator { + self.inner.iter().enumerate().filter_map(|(i, e)| match e { + Entry::Full(val) => Some((i, val)), + Entry::Free(_) => None, + }) + } + pub fn iter(&self) -> impl Iterator { self.inner.iter().filter_map(|e| match e { Entry::Full(val) => Some(val), From b5ccf1a68e575a8d9c4dd8840029e03e5696c017 Mon Sep 17 00:00:00 2001 From: wiru Date: Thu, 17 Oct 2024 23:16:37 -0300 Subject: [PATCH 23/23] feat: making response viewer work again --- hac-client/src/ascii.rs | 16 - hac-client/src/components/mod.rs | 1 + .../src/{pages => components}/spinner.rs | 0 .../collection_viewer/collection_viewer.rs | 198 +++--------- hac-client/src/pages/collection_viewer/mod.rs | 2 +- .../pages/collection_viewer/request_uri.rs | 6 - .../collection_viewer/response_viewer.rs | 282 ++++++------------ .../pages/collection_viewer/sidebar/mod.rs | 7 +- hac-client/src/pages/mod.rs | 2 - hac-client/src/pages/under_construction.rs | 61 ---- hac-config/src/config.rs | 1 + hac-config/src/default_config.rs | 1 + hac-core/src/net/request_manager.rs | 16 +- hac-core/src/net/request_strategies.rs | 2 +- .../net/request_strategies/http_strategy.rs | 12 +- .../src/net/response_decoders/json_decoder.rs | 2 +- hac-store/src/collection.rs | 31 +- hac-store/src/slab.rs | 70 ++++- 18 files changed, 248 insertions(+), 462 deletions(-) rename hac-client/src/{pages => components}/spinner.rs (100%) delete mode 100755 hac-client/src/pages/under_construction.rs diff --git a/hac-client/src/ascii.rs b/hac-client/src/ascii.rs index 7cefe02..c89b24d 100755 --- a/hac-client/src/ascii.rs +++ b/hac-client/src/ascii.rs @@ -36,19 +36,3 @@ pub static LOGO_ASCII: &[&str] = &[ r#"| ,/ `--'' \ \ .' "#, r#"'---' `---` "#, ]; - -pub static UNDER_CONSTRUCTION: &[&str] = &[ - r#"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"#, - r#"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"#, - r#"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"#, - r#"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"#, - r#"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣤⣄⣀⣀⣠⣤⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"#, - r#"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"#, - r#"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠻⠿⢿⣿⣿⡿⠿⠟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"#, - r#"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"#, - r#"⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣶⣤⣤⣤⣤⣤⣤⣶⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀"#, - r#"⠀⠀⠀⠀⣀⣠⣤⡖⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⢶⣤⣄⣀⠀⠀⠀⠀"#, - r#"⠀⠀⠀⠉⠙⠻⢿⣿⡀⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠛⢀⣿⡿⠟⠋⠉⠀⠀⠀"#, - r#"⠀⠀⠀⠀⠀⠀⠀⠀⠉⠁⠢⠤⣤⣀⣈⣁⣀⣤⠤⠔⠈⠉⠀⠀⠀⠀⠀⠀⠀⠀"#, - r#"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"#, -]; diff --git a/hac-client/src/components/mod.rs b/hac-client/src/components/mod.rs index 8a4654f..f8bd870 100644 --- a/hac-client/src/components/mod.rs +++ b/hac-client/src/components/mod.rs @@ -1,3 +1,4 @@ pub mod blending_list; pub mod input; pub mod list_item; +pub mod spinner; diff --git a/hac-client/src/pages/spinner.rs b/hac-client/src/components/spinner.rs similarity index 100% rename from hac-client/src/pages/spinner.rs rename to hac-client/src/components/spinner.rs diff --git a/hac-client/src/pages/collection_viewer/collection_viewer.rs b/hac-client/src/pages/collection_viewer/collection_viewer.rs index 799b66b..18af9f3 100755 --- a/hac-client/src/pages/collection_viewer/collection_viewer.rs +++ b/hac-client/src/pages/collection_viewer/collection_viewer.rs @@ -7,7 +7,8 @@ use std::sync::mpsc::{channel, Sender}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::command::Command; use hac_core::net::request_manager::Response; -use hac_store::collection::WhichSlab; +use hac_store::collection::{Request, WhichSlab}; +use hac_store::slab::EntryRef; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::Stylize; use ratatui::widgets::{Block, Clear}; @@ -24,7 +25,7 @@ use crate::pages::collection_viewer::sidebar::edit_request_form::EditRequestForm //use crate::pages::collection_viewer::collection_store::{CollectionStore, CollectionStoreAction}; //use crate::pages::collection_viewer::request_editor::{RequestEditor, RequestEditorEvent}; //use crate::pages::collection_viewer::request_uri::{RequestUri, RequestUriEvent}; -//use crate::pages::collection_viewer::response_viewer::{ResponseViewer, ResponseViewerEvent}; +use crate::pages::collection_viewer::response_viewer::{ResponseViewer, ResponseViewerEvent}; //use crate::pages::collection_viewer::sidebar::{self, Sidebar, SidebarEvent}; use crate::renderable::{Eventful, Renderable}; use crate::router::RouterMessage; @@ -92,7 +93,8 @@ pub struct CollectionViewer { sidebar: Sidebar, messager: Sender, - //response_viewer: ResponseViewer<'cv>, + response_viewer: ResponseViewer, + //request_editor: RequestEditor<'cv>, colors: HacColors, config: HacConfig, @@ -101,19 +103,18 @@ pub struct CollectionViewer { collection_sync_timer: std::time::Instant, responses_map: HashMap>>, - response_rx: UnboundedReceiver, - request_tx: UnboundedSender, + + response_rx: UnboundedReceiver<(EntryRef, Response)>, + response_tx: UnboundedSender<(EntryRef, Response)>, } impl CollectionViewer { pub fn new(size: Rect, colors: HacColors, config: HacConfig) -> Self { let layout = build_layout(size, 1); - let (request_tx, response_rx) = unbounded_channel::(); + let (response_tx, response_rx) = unbounded_channel::<(EntryRef, Response)>(); - // //let request_editor = RequestEditor::new(colors, config, collection_store.clone(), layout.req_editor); - // - //let response_viewer = ResponseViewer::new(colors, collection_store.clone(), None, layout.response_preview); + let mut sidebar = Sidebar::new(colors.clone(), config.clone()); sidebar.focus(); @@ -123,9 +124,9 @@ impl CollectionViewer { request_uri: RequestUri::new(colors.clone(), layout.req_uri), sidebar, messager: channel().0, + response_viewer: ResponseViewer::new(colors.clone(), layout.response_preview), //request_editor, - //response_viewer, colors, layout, config, @@ -133,49 +134,17 @@ impl CollectionViewer { collection_sync_timer: std::time::Instant::now(), responses_map: HashMap::default(), response_rx, - request_tx, + response_tx, } } - //fn rebuild_everything(&mut self) { - // self.sidebar = sidebar::Sidebar::new(self.colors, self.collection_store.clone()); - // self.request_editor = RequestEditor::new( - // self.colors, - // self.config, - // self.collection_store.clone(), - // self.layout.req_editor, - // ); - // self.response_viewer = ResponseViewer::new( - // self.colors, - // self.collection_store.clone(), - // None, - // self.layout.response_preview, - // ); - // self.request_uri = RequestUri::new(self.colors, self.collection_store.clone(), self.layout.req_uri); - //} - - // collect all pending responses from the channel. Here, I don't see a way we - // may have more than one response on this channel at any point, but it shouldn't matter - // if we have, so we can drain all the responses and update accordingly - //fn drain_responses_channel(&mut self) { - // while let Ok(res) = self.response_rx.try_recv() { - // let res = Rc::new(RefCell::new(res)); - // self.collection_store - // .borrow() - // .get_selected_request() - // .as_ref() - // .and_then(|req| { - // self.responses_map - // .insert(req.read().unwrap().id.to_string(), Rc::clone(&res)) - // }); - // self.response_viewer.update(Some(Rc::clone(&res))); - // self.response_rx.is_empty().then(|| { - // self.collection_store - // .borrow_mut() - // .dispatch(CollectionStoreAction::SetPendingRequest(false)); - // }); - // } - //} + fn drain_responses_channel(&mut self) { + while let Ok((entry_ref, response)) = self.response_rx.try_recv() { + hac_store::collection::restore_request(entry_ref); + self.response_viewer.update(Some(response)); + self.response_viewer.pending_request = false; + } + } //fn sync_collection_changes(&mut self) { // let sender = self @@ -227,13 +196,10 @@ impl CollectionViewer { // }), // }); // } - //self.collection_sync_timer = std::time::Instant::now(); - //if self.dry_run { // return; //} - //tokio::spawn(async move { // match hac_core::fs::sync_collection(collection).await { // Ok(_) => {} @@ -247,10 +213,6 @@ impl CollectionViewer { //}); //} - /// updating selection will always also update the focus, as we cannot - /// select something that isn't also focused, unless pane_to_select is - /// None, in which case we won't change focus at all - #[inline] fn update_selection(&mut self, pane_to_select: Option) { if hac_store::collection::is_empty() { return; @@ -259,7 +221,7 @@ impl CollectionViewer { match self.selection { Some(PaneFocus::Sidebar) => self.sidebar.deselect(), Some(PaneFocus::ReqUri) => self.request_uri.deselect(), - Some(PaneFocus::Preview) => {} + Some(PaneFocus::Preview) => self.response_viewer.deselect(), Some(PaneFocus::Editor) => {} None => {} } @@ -273,6 +235,7 @@ impl CollectionViewer { self.focus = pane; } Some(pane @ PaneFocus::Preview) => { + self.response_viewer.select(); self.focus = pane; } Some(pane @ PaneFocus::Editor) => { @@ -283,18 +246,17 @@ impl CollectionViewer { self.selection = pane_to_select; } - #[inline] fn update_focus(&mut self, pane_to_focus: PaneFocus) { match self.focus { PaneFocus::Sidebar => self.sidebar.blur(), PaneFocus::ReqUri => self.request_uri.blur(), - PaneFocus::Preview => {} + PaneFocus::Preview => self.response_viewer.blur(), PaneFocus::Editor => {} } match pane_to_focus { PaneFocus::Sidebar => self.sidebar.focus(), PaneFocus::ReqUri => self.request_uri.focus(), - PaneFocus::Preview => {} + PaneFocus::Preview => self.response_viewer.focus(), PaneFocus::Editor => {} } self.focus = pane_to_focus; @@ -322,83 +284,18 @@ impl Renderable for CollectionViewer { frame.render_widget(Clear, size); frame.render_widget(Block::default().bg(self.colors.primary.background), size); - //self.drain_responses_channel(); + self.drain_responses_channel(); self.request_uri.draw(frame, self.layout.req_uri)?; self.sidebar.draw(frame, self.layout.sidebar)?; - - //self.response_viewer.draw(frame, self.layout.response_preview)?; + self.response_viewer.draw(frame, self.layout.response_preview)?; //self.request_editor.draw(frame, self.layout.req_editor)?; - //let overlay = self.collection_store.borrow().peek_overlay(); - //match overlay { - // CollectionViewerOverlay::CreateRequest => { - // self.sidebar.draw_overlay(frame, overlay)?; - // } - // CollectionViewerOverlay::CreateDirectory => { - // self.sidebar.draw_overlay(frame, overlay)?; - // } - // CollectionViewerOverlay::SelectParentDir => { - // self.sidebar.draw_overlay(frame, overlay)?; - // } - // CollectionViewerOverlay::EditRequest => { - // self.sidebar.draw_overlay(frame, overlay)?; - // } - // CollectionViewerOverlay::EditDirectory => { - // self.sidebar.draw_overlay(frame, overlay)?; - // } - // CollectionViewerOverlay::DeleteSidebarItem(_) => { - // self.sidebar.draw_overlay(frame, overlay)?; - // } - // CollectionViewerOverlay::HeadersHelp => { - // self.request_editor.draw_overlay(frame, overlay)?; - // } - // CollectionViewerOverlay::HeadersDelete => { - // self.request_editor.draw_overlay(frame, overlay)?; - // } - // CollectionViewerOverlay::HeadersForm(_, _) => { - // self.request_editor.draw_overlay(frame, overlay)?; - // } - // CollectionViewerOverlay::ChangeAuthMethod => { - // self.request_editor.draw_overlay(frame, overlay)?; - // } - // CollectionViewerOverlay::None => {} - //} - - //if self - // .collection_store - // .borrow() - // .get_selected_pane() - // .as_ref() - // .is_some_and(|pane| pane.eq(&PaneFocus::Editor)) - //{ - // self.request_editor.maybe_draw_cursor(frame); - //} - - //if self - // .collection_store - // .borrow() - // .get_selected_pane() - // .as_ref() - // .is_some_and(|pane| pane.eq(&PaneFocus::ReqUri)) - //{ - // if let Some(request) = self.collection_store.borrow().get_selected_request().as_ref() { - // frame.set_cursor( - // self.layout - // .req_uri - // .x - // .add(request.read().unwrap().uri.chars().count() as u16) - // .add(1), - // self.layout.req_uri.y.add(1), - // ) - // } - //} - Ok(()) } fn tick(&mut self) -> anyhow::Result<()> { - if self.collection_sync_timer.elapsed().as_secs().ge(&5) { + if self.collection_sync_timer.elapsed().as_secs() > self.config.borrow().sync_delay { //self.sync_collection_changes(); } Ok(()) @@ -412,8 +309,8 @@ impl Renderable for CollectionViewer { fn resize(&mut self, new_size: Rect) { let new_layout = build_layout(new_size, 1); + self.response_viewer.resize(new_layout.response_preview); //self.request_editor.resize(new_layout.req_editor); - //self.response_viewer.resize(new_layout.response_preview); self.layout = new_layout; } @@ -447,9 +344,12 @@ impl Eventful for CollectionViewer { if let Some(pane) = self.selection { match pane { PaneFocus::ReqUri => match self.request_uri.handle_key_event(key_event)? { - Some(RequestUriEvent::Quit) => return Ok(Some(Command::Quit)), Some(RequestUriEvent::RemoveSelection) => self.update_selection(None), - Some(RequestUriEvent::SendRequest) => todo!(), + Some(RequestUriEvent::SendRequest) => { + let request_ref = hac_store::collection::borrow_selected_request(); + hac_core::net::handle_request(request_ref, self.response_tx.clone()); + self.response_viewer.pending_request = true; + } Some(RequestUriEvent::SelectNext) => { self.update_selection(None); self.focus_next(); @@ -461,7 +361,6 @@ impl Eventful for CollectionViewer { None => {} }, PaneFocus::Sidebar => match self.sidebar.handle_key_event(key_event)? { - Some(SidebarEvent::Quit) => return Ok(Some(Command::Quit)), Some(SidebarEvent::RemoveSelection) => self.update_selection(None), Some(SidebarEvent::ShowExtendedHint) => self.layout = build_layout(self.layout.total_size, 3), Some(SidebarEvent::HideExtendedHint) => self.layout = build_layout(self.layout.total_size, 1), @@ -518,35 +417,12 @@ impl Eventful for CollectionViewer { }; } } - None => (), - //Some(SidebarEvent::EditRequest) => self - // .collection_store - // .borrow_mut() - // .push_overlay(CollectionViewerOverlay::EditRequest), - //Some(SidebarEvent::EditDirectory) => self - // .collection_store - // .borrow_mut() - // .push_overlay(CollectionViewerOverlay::EditDirectory), - //Some(SidebarEvent::CreateDirectory) => self - // .collection_store - // .borrow_mut() - // .push_overlay(CollectionViewerOverlay::CreateDirectory), - //Some(SidebarEvent::DeleteItem(item_id)) => self - // .collection_store - // .borrow_mut() - // .push_overlay(CollectionViewerOverlay::DeleteSidebarItem(item_id)), - //Some(SidebarEvent::SyncCollection) => self.sync_collection_changes(), - //Some(SidebarEvent::Quit) => return Ok(Some(Command::Quit)), - //Some(SidebarEvent::RebuildView) => self.rebuild_everything(), - //// when theres no event we do nothing - //None => {} + None => {} + }, + PaneFocus::Preview => match self.response_viewer.handle_key_event(key_event)? { + Some(ResponseViewerEvent::RemoveSelection) => self.update_selection(None), + _ => {} }, - // PaneFocus::Preview => match self.response_viewer.handle_key_event(key_event)? { - // Some(ResponseViewerEvent::RemoveSelection) => self.update_selection(None), - // Some(ResponseViewerEvent::Quit) => return Ok(Some(Command::Quit)), - // // when theres no event we do nothing - // None => {} - // }, // PaneFocus::Editor => match self.request_editor.handle_key_event(key_event)? { // Some(RequestEditorEvent::RemoveSelection) => self.update_selection(None), // Some(RequestEditorEvent::Quit) => return Ok(Some(Command::Quit)), diff --git a/hac-client/src/pages/collection_viewer/mod.rs b/hac-client/src/pages/collection_viewer/mod.rs index c3b042e..f0deab1 100755 --- a/hac-client/src/pages/collection_viewer/mod.rs +++ b/hac-client/src/pages/collection_viewer/mod.rs @@ -2,7 +2,7 @@ pub mod collection_viewer; //mod request_editor; mod request_uri; -//mod response_viewer; +mod response_viewer; mod sidebar; pub use collection_viewer::CollectionViewer; diff --git a/hac-client/src/pages/collection_viewer/request_uri.rs b/hac-client/src/pages/collection_viewer/request_uri.rs index 74f9085..291cac0 100755 --- a/hac-client/src/pages/collection_viewer/request_uri.rs +++ b/hac-client/src/pages/collection_viewer/request_uri.rs @@ -20,8 +20,6 @@ pub enum RequestUriEvent { SelectNext, /// requests the parent to select the previous pane SelectPrev, - /// user pressed `C-c` hotkey so we bubble up the event for the parent to handle - Quit, } #[derive(Debug)] @@ -100,10 +98,6 @@ impl Eventful for RequestUri { fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { assert!(self.selected); - if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - return Ok(Some(RequestUriEvent::Quit)); - } - match key_event.code { KeyCode::Esc => return Ok(Some(RequestUriEvent::RemoveSelection)), KeyCode::Tab => return Ok(Some(RequestUriEvent::SelectNext)), diff --git a/hac-client/src/pages/collection_viewer/response_viewer.rs b/hac-client/src/pages/collection_viewer/response_viewer.rs index c96bdcc..42cb96d 100755 --- a/hac-client/src/pages/collection_viewer/response_viewer.rs +++ b/hac-client/src/pages/collection_viewer/response_viewer.rs @@ -1,12 +1,11 @@ -use std::cell::RefCell; use std::iter; use std::ops::{Add, Sub}; -use std::rc::Rc; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use crate::HacColors; + +use crate::components::spinner::Spinner; +use crossterm::event::{KeyCode, KeyEvent}; use hac_core::net::request_manager::Response; -use hac_core::syntax::highlighter::HIGHLIGHTER; -use rand::Rng; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::{Style, Stylize}; use ratatui::text::{Line, Span}; @@ -16,18 +15,12 @@ use ratatui::widgets::{ use ratatui::Frame; use tree_sitter::Tree; -use super::collection_store::CollectionStore; -use crate::ascii::{BIG_ERROR_ART, LOGO_ASCII, SMALL_ERROR_ART}; -use crate::pages::collection_viewer::collection_viewer::PaneFocus; -use crate::pages::spinner::Spinner; -use crate::pages::under_construction::UnderConstruction; -use crate::pages::{Eventful, Renderable}; -use crate::utils::build_syntax_highlighted_lines; +use crate::ascii::{BIG_ERROR_ART, SMALL_ERROR_ART}; +use crate::renderable::{Eventful, Renderable}; #[derive(Debug)] pub enum ResponseViewerEvent { RemoveSelection, - Quit, } #[derive(Debug, Clone, PartialEq)] @@ -82,53 +75,37 @@ struct PreviewLayout { scrollbar: Rect, } -#[derive(Debug, Clone)] -pub struct ResponseViewer<'a> { - colors: &'a hac_colors::Colors, - response: Option>>, +#[derive(Debug)] +pub struct ResponseViewer { + colors: HacColors, + response: Option, tree: Option, lines: Vec>, error_lines: Option>>, - empty_lines: Vec>, preview_layout: PreviewLayout, layout: ResViewerLayout, - collection_store: Rc>, active_tab: ResViewerTabs, raw_scroll: usize, headers_scroll_y: usize, headers_scroll_x: usize, pretty_scroll: usize, -} -impl<'a> ResponseViewer<'a> { - pub fn new( - colors: &'a hac_colors::Colors, - collection_store: Rc>, - response: Option>>, - size: Rect, - ) -> Self { - let tree = response.as_ref().and_then(|response| { - if let Some(ref pretty_body) = response.borrow().pretty_body { - let pretty_body = pretty_body.to_string(); - let mut highlighter = HIGHLIGHTER.write().unwrap(); - highlighter.parse(&pretty_body) - } else { - None - } - }); + pub pending_request: bool, + focused: bool, + selected: bool, +} +impl ResponseViewer { + pub fn new(colors: HacColors, size: Rect) -> Self { let layout = build_layout(size); let preview_layout = build_preview_layout(layout.content_pane); - let empty_lines = make_empty_ascii_art(colors); - ResponseViewer { colors, - response, - tree, + response: None, + tree: None, lines: vec![], error_lines: None, - empty_lines, preview_layout, layout, active_tab: ResViewerTabs::Preview, @@ -136,32 +113,50 @@ impl<'a> ResponseViewer<'a> { headers_scroll_y: 0, headers_scroll_x: 0, pretty_scroll: 0, - collection_store, + + pending_request: false, + focused: false, + selected: false, } } + pub fn focus(&mut self) { + self.focused = true; + } + + pub fn blur(&mut self) { + self.focused = false; + } + + pub fn select(&mut self) { + self.selected = true; + } + + pub fn deselect(&mut self) { + self.selected = false; + } + pub fn resize(&mut self, new_size: Rect) { self.layout = build_layout(new_size); self.preview_layout = build_preview_layout(self.layout.content_pane); } - pub fn update(&mut self, response: Option>>) { - let body_str = response - .as_ref() - .and_then(|res| res.borrow().pretty_body.as_ref().map(|body| body.to_string())) - .unwrap_or_default(); - - if body_str.len().gt(&0) { - self.tree = HIGHLIGHTER.write().unwrap().parse(&body_str); - self.lines = build_syntax_highlighted_lines(&body_str, self.tree.as_ref(), self.colors); - } else { - self.tree = None; - self.lines = vec![]; - } + pub fn update(&mut self, response: Option) { + //let body_str = response + // .as_ref() + // .and_then(|res| res.pretty_body.as_ref().map(|body| body.to_string())) + // .unwrap_or_default(); + + //if body_str.len().gt(&0) { + // self.tree = HIGHLIGHTER.write().unwrap().parse(&body_str); + // self.lines = build_syntax_highlighted_lines(&body_str, self.tree.as_ref(), self.colors); + //} else { + // self.tree = None; + // self.lines = vec![]; + //} if let Some(res) = response.as_ref() { let cause: String = res - .borrow() .cause .as_ref() .map(|cause| cause.to_string()) @@ -184,36 +179,16 @@ impl<'a> ResponseViewer<'a> { ) }; - self.empty_lines = make_empty_ascii_art(self.colors); self.response = response; } fn draw_container(&self, size: Rect, frame: &mut Frame) { - let is_focused = self - .collection_store - .borrow() - .get_focused_pane() - .eq(&PaneFocus::Preview); - let is_selected = self - .collection_store - .borrow() - .get_selected_pane() - .is_some_and(|pane| pane.eq(&PaneFocus::Preview)); - - let block_border = match (is_focused, is_selected) { + let block_border = match (self.focused, self.selected) { (true, false) => Style::default().fg(self.colors.bright.blue), (true, true) => Style::default().fg(self.colors.normal.red), (_, _) => Style::default().fg(self.colors.bright.black), }; - - let block = Block::default() - .borders(Borders::ALL) - .title(vec![ - "P".fg(self.colors.normal.red).bold(), - "review".fg(self.colors.bright.black), - ]) - .border_style(block_border); - + let block = Block::default().borders(Borders::ALL).border_style(block_border); frame.render_widget(block, size); } @@ -269,69 +244,26 @@ impl<'a> ResponseViewer<'a> { } } - fn draw_waiting_for_request(&self, frame: &mut Frame) { - let request_pane = self.preview_layout.content_pane; - frame.render_widget(Clear, request_pane); - frame.render_widget(Block::default().bg(self.colors.primary.background), request_pane); - - let mut empty_message = self.empty_lines.clone(); - - if self.empty_lines.len() >= request_pane.height.into() { - empty_message = vec![ - "your handy API client".fg(self.colors.normal.red).into(), - "".into(), - "make a request and the result will appear here" - .fg(self.colors.normal.red) - .into(), - ]; - } - - let center = request_pane - .y - .add(request_pane.height.div_ceil(2)) - .sub(empty_message.len().div_ceil(2) as u16); - - let size = Rect::new( - request_pane.x.add(1), - center, - request_pane.width, - self.empty_lines.len() as u16, - ); - - frame.render_widget( - Paragraph::new(empty_message).fg(self.colors.normal.red).centered(), - size, - ) - } - fn draw_current_tab(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { - if self.response.as_ref().is_some_and(|res| res.borrow().is_error) { + if self.response.as_ref().is_some_and(|res| res.is_error) { self.draw_network_error(frame); }; - if self.response.is_none() { - self.draw_waiting_for_request(frame); - } - - if self.response.as_ref().is_some_and(|res| !res.borrow().is_error) { + if self.response.as_ref().is_some_and(|res| !res.is_error) { match self.active_tab { ResViewerTabs::Preview => self.draw_pretty_response(frame, size), ResViewerTabs::Raw => self.draw_raw_response(frame, size), ResViewerTabs::Headers => self.draw_response_headers(frame), - ResViewerTabs::Cookies => UnderConstruction::new(self.colors).draw(frame, size)?, + ResViewerTabs::Cookies => (), } } - if self.collection_store.borrow().has_pending_request() { - self.draw_spinner(frame); - } - Ok(()) } fn draw_response_headers(&mut self, frame: &mut Frame) { if let Some(response) = self.response.as_ref() { - if let Some(headers) = response.borrow().headers.as_ref() { + if let Some(headers) = response.headers.as_ref() { let mut longest_line: usize = 0; let mut lines: Vec = @@ -398,9 +330,8 @@ impl<'a> ResponseViewer<'a> { fn draw_raw_response(&mut self, frame: &mut Frame, size: Rect) { if let Some(response) = self.response.as_ref() { - let lines = if response.borrow().body.is_some() { + let lines = if response.body.is_some() { response - .borrow() .body .as_ref() .unwrap() @@ -454,42 +385,37 @@ impl<'a> ResponseViewer<'a> { frame.render_stateful_widget(scrollbar, size, &mut scrollbar_state); } - fn draw_pretty_response(&mut self, frame: &mut Frame, size: Rect) { - if self.response.as_ref().is_some() { - if self.pretty_scroll.ge(&self.lines.len().saturating_sub(1)) { - self.pretty_scroll = self.lines.len().saturating_sub(1); - } - - self.draw_scrollbar(self.lines.len(), self.raw_scroll, frame, self.preview_layout.scrollbar); - - let lines = - if self.lines.len().gt(&0) { self.lines.clone() } else { vec![Line::from("No body").centered()] }; - - let lines_in_view = lines - .into_iter() - .skip(self.pretty_scroll) - .chain(iter::repeat(Line::from("~".fg(self.colors.bright.black)))) - .take(size.height.into()) - .collect::>(); - - let pretty_response = Paragraph::new(lines_in_view); - frame.render_widget(pretty_response, self.preview_layout.content_pane); - } + fn draw_pretty_response(&mut self, _frame: &mut Frame, _size: Rect) { + //if self.response.as_ref().is_some() { + // if self.pretty_scroll.ge(&self.lines.len().saturating_sub(1)) { + // self.pretty_scroll = self.lines.len().saturating_sub(1); + // } + // + // self.draw_scrollbar(self.lines.len(), self.raw_scroll, frame, self.preview_layout.scrollbar); + // + // let lines = + // if self.lines.len().gt(&0) { self.lines.clone() } else { vec![Line::from("No body").centered()] }; + // + // let lines_in_view = lines + // .into_iter() + // .skip(self.pretty_scroll) + // .chain(iter::repeat(Line::from("~".fg(self.colors.bright.black)))) + // .take(size.height.into()) + // .collect::>(); + // + // let pretty_response = Paragraph::new(lines_in_view); + // frame.render_widget(pretty_response, self.preview_layout.content_pane); + //} } fn draw_summary(&self, frame: &mut Frame, size: Rect) { if let Some(ref response) = self.response { - let status_color = match response - .borrow() - .status - .map(|status| status.as_u16()) - .unwrap_or_default() - { + let status_color = match response.status.map(|status| status.as_u16()).unwrap_or_default() { s if s < 400 => self.colors.normal.green, _ => self.colors.normal.red, }; - let status = match response.borrow().status { + let status = match response.status { Some(status) if size.width.gt(&50) => format!( "{} ({})", status.as_str(), @@ -507,11 +433,11 @@ impl<'a> ResponseViewer<'a> { status, " ".into(), "Time: ".fg(self.colors.bright.black), - format!("{}ms", response.borrow().duration.as_millis()).fg(self.colors.normal.green), + format!("{}ms", response.duration.as_millis()).fg(self.colors.normal.green), " ".into(), ]; - if let Some(size) = response.borrow().size { + if let Some(size) = response.size { pieces.push("Size: ".fg(self.colors.bright.black)); pieces.push(format!("{} B", size).fg(self.colors.normal.green)) }; @@ -521,8 +447,17 @@ impl<'a> ResponseViewer<'a> { } } -impl<'a> Renderable for ResponseViewer<'a> { +impl Renderable for ResponseViewer { + type Input = (); + type Output = (); + + fn data(&self, _: u8) -> Self::Output {} + fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { + if self.pending_request { + self.draw_spinner(frame); + } + self.draw_tabs(frame, self.layout.tabs_pane); self.draw_current_tab(frame, self.layout.content_pane)?; self.draw_summary(frame, self.layout.summary_pane); @@ -534,18 +469,10 @@ impl<'a> Renderable for ResponseViewer<'a> { fn resize(&mut self, _new_size: Rect) {} } -impl<'a> Eventful for ResponseViewer<'a> { +impl Eventful for ResponseViewer { type Result = ResponseViewerEvent; fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - return Ok(Some(ResponseViewerEvent::Quit)); - } - - if let KeyCode::Esc = key_event.code { - return Ok(Some(ResponseViewerEvent::RemoveSelection)); - } - if let KeyCode::Tab = key_event.code { self.active_tab = ResViewerTabs::next(&self.active_tab); } @@ -554,6 +481,10 @@ impl<'a> Eventful for ResponseViewer<'a> { self.active_tab = ResViewerTabs::prev(&self.active_tab); } + if let KeyCode::Esc = key_event.code { + return Ok(Some(ResponseViewerEvent::RemoveSelection)); + } + match key_event.code { KeyCode::Char('0') if self.active_tab.eq(&ResViewerTabs::Headers) => { self.headers_scroll_x = 0; @@ -585,7 +516,6 @@ impl<'a> Eventful for ResponseViewer<'a> { } _ => {} } - Ok(None) } } @@ -642,19 +572,3 @@ fn get_error_ascii_art(width: u16) -> &'static [&'static str] { true => BIG_ERROR_ART, } } - -fn make_empty_ascii_art(colors: &hac_colors::Colors) -> Vec> { - LOGO_ASCII - .iter() - .map(|line| line.to_string().into()) - .chain(vec![ - "".into(), - "your handy API client".fg(colors.bright.blue).into(), - "".into(), - "".into(), - "make a request and the result will appear here" - .fg(colors.bright.black) - .into(), - ]) - .collect::>() -} diff --git a/hac-client/src/pages/collection_viewer/sidebar/mod.rs b/hac-client/src/pages/collection_viewer/sidebar/mod.rs index 2173484..b01c491 100755 --- a/hac-client/src/pages/collection_viewer/sidebar/mod.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/mod.rs @@ -3,7 +3,7 @@ pub mod delete_item_form; pub mod edit_request_form; mod request_form_layout; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use crossterm::event::{KeyCode, KeyEvent}; use hac_store::collection::{self, EntryStatus, ReqMethod, ReqTreeNode, WhichSlab}; use ratatui::layout::{Constraint, Layout, Rect}; use ratatui::style::{Style, Styled, Stylize}; @@ -25,7 +25,6 @@ pub enum SidebarEvent { CreateRequest, EditRequest, DeleteItem, - Quit, } #[derive(Debug)] @@ -229,10 +228,6 @@ impl Eventful for Sidebar { type Result = SidebarEvent; fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - return Ok(Some(SidebarEvent::Quit)); - } - match key_event.code { KeyCode::Tab => return Ok(Some(SidebarEvent::SelectNext)), KeyCode::BackTab => return Ok(Some(SidebarEvent::SelectPrev)), diff --git a/hac-client/src/pages/mod.rs b/hac-client/src/pages/mod.rs index 570b65a..f68a874 100755 --- a/hac-client/src/pages/mod.rs +++ b/hac-client/src/pages/mod.rs @@ -2,6 +2,4 @@ pub mod collection_list; pub mod collection_viewer; pub mod input; pub mod overlay; -// mod spinner; -// mod under_construction; pub mod terminal_too_small; diff --git a/hac-client/src/pages/under_construction.rs b/hac-client/src/pages/under_construction.rs deleted file mode 100755 index 90286cf..0000000 --- a/hac-client/src/pages/under_construction.rs +++ /dev/null @@ -1,61 +0,0 @@ -use std::ops::{Add, Sub}; - -use ratatui::layout::Rect; -use ratatui::style::Stylize; -use ratatui::text::Line; -use ratatui::widgets::Paragraph; -use ratatui::Frame; - -use crate::ascii::UNDER_CONSTRUCTION; -use crate::renderable::Renderable; - -pub struct UnderConstruction<'uc> { - colors: &'uc hac_colors::colors::Colors, -} - -impl<'uc> UnderConstruction<'uc> { - pub fn new(colors: &'uc hac_colors::colors::Colors) -> Self { - UnderConstruction { colors } - } -} - -impl Renderable for UnderConstruction<'_> { - type Input = (); - type Output = (); - - fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { - let icon_lines = UNDER_CONSTRUCTION - .iter() - .map(|line| Line::from(line.to_string()).fg(self.colors.normal.red)) - .collect::>(); - - let icon_height = icon_lines.len(); - let icon_half_height = icon_height.div_ceil(2); - let half_height = size.height.div_ceil(2); - let starting_y = half_height.saturating_sub(icon_half_height as u16).saturating_sub(1); - - let _icon_size = Rect::new(size.x, size.y.add(starting_y), size.width, icon_height as u16); - - let message = Line::from("Hold on, we're cooking up something new!").fg(self.colors.normal.red); - - let _message_size = Rect::new( - size.x, - size.y.add(starting_y).add(icon_height as u16).add(1), - size.width, - 1, - ); - - if icon_height >= (size.height - 3).into() { - let rect = Rect::new(size.x, size.y.add(size.height.div_ceil(2).sub(1)), size.width, 1); - frame.render_widget(Paragraph::new(message).centered(), rect); - return Ok(()); - } - - // frame.render_widget(Paragraph::new(icon_lines).centered(), icon_size); - // frame.render_widget(Paragraph::new(message).centered(), message_size); - - Ok(()) - } - - fn data(&self, _requester: u8) -> Self::Output {} -} diff --git a/hac-config/src/config.rs b/hac-config/src/config.rs index cfc3df9..1021b26 100755 --- a/hac-config/src/config.rs +++ b/hac-config/src/config.rs @@ -63,6 +63,7 @@ pub struct Config { pub dry_run: bool, pub tab_size: usize, pub enable_icons: bool, + pub sync_delay: u64, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] diff --git a/hac-config/src/default_config.rs b/hac-config/src/default_config.rs index fb823cb..dcb85cc 100755 --- a/hac-config/src/default_config.rs +++ b/hac-config/src/default_config.rs @@ -2,6 +2,7 @@ pub static DEFAULT_CONFIG: &str = r##" dry_run = false tab_size = 2 enable_icons = true +sync_delay = 5 [editor_keys.normal] "u" = "Undo" diff --git a/hac-core/src/net/request_manager.rs b/hac-core/src/net/request_manager.rs index a49b90d..f7386d6 100755 --- a/hac-core/src/net/request_manager.rs +++ b/hac-core/src/net/request_manager.rs @@ -1,6 +1,7 @@ use std::time::Duration; use hac_store::collection::{BodyKind, Request}; +use hac_store::slab::EntryRef; use reqwest::header::{HeaderMap, HeaderValue}; use tokio::sync::mpsc::UnboundedSender; @@ -25,7 +26,7 @@ pub struct Response { pub struct RequestManager; impl RequestManager { - pub async fn handle(strategy: S, request: Request) -> Response + pub async fn handle(strategy: S, request: &Request) -> Response where S: RequestStrategy, { @@ -58,14 +59,17 @@ impl From<&str> for ContentType { } #[tracing::instrument(skip_all)] -pub fn handle_request(request: Request, response_tx: UnboundedSender) { +pub fn handle_request(request: EntryRef, response_tx: UnboundedSender<(EntryRef, Response)>) { tokio::spawn(async move { - let response = match request.body_kind { + let response = match request.inner.body_kind { // if we dont have a body type, this is a GET request, so we use HTTP strategy - BodyKind::NoBody => RequestManager::handle(HttpResponse, request).await, - BodyKind::Json => RequestManager::handle(HttpResponse, request).await, + BodyKind::NoBody => RequestManager::handle(HttpResponse, &request.inner).await, + BodyKind::Json => RequestManager::handle(HttpResponse, &request.inner).await, }; - response_tx.send(response).is_err().then(|| std::process::abort()); + response_tx + .send((request, response)) + .is_err() + .then(|| std::process::abort()); }); } diff --git a/hac-core/src/net/request_strategies.rs b/hac-core/src/net/request_strategies.rs index 92601be..df2e6f9 100755 --- a/hac-core/src/net/request_strategies.rs +++ b/hac-core/src/net/request_strategies.rs @@ -6,5 +6,5 @@ use crate::net::request_manager::Response; use hac_store::collection::Request; pub trait RequestStrategy { - fn handle(&self, request: Request) -> impl Future; + fn handle(&self, request: &Request) -> impl Future; } diff --git a/hac-core/src/net/request_strategies/http_strategy.rs b/hac-core/src/net/request_strategies/http_strategy.rs index 242503e..dd67958 100755 --- a/hac-core/src/net/request_strategies/http_strategy.rs +++ b/hac-core/src/net/request_strategies/http_strategy.rs @@ -7,7 +7,7 @@ use hac_store::collection::{ReqMethod, Request}; pub struct HttpResponse; impl RequestStrategy for HttpResponse { - async fn handle(&self, request: Request) -> Response { + async fn handle(&self, request: &Request) -> Response { let client = RequestClient::default(); match request.method { @@ -21,7 +21,7 @@ impl RequestStrategy for HttpResponse { } impl HttpResponse { - async fn handle_get_request(&self, client: RequestClient, request: Request) -> Response { + async fn handle_get_request(&self, client: RequestClient, request: &Request) -> Response { let now = std::time::Instant::now(); match client.get(&request).send().await { Ok(response) => { @@ -43,7 +43,7 @@ impl HttpResponse { } } - async fn handle_post_request(&self, client: RequestClient, request: Request) -> Response { + async fn handle_post_request(&self, client: RequestClient, request: &Request) -> Response { let now = std::time::Instant::now(); match client.post(&request).json(&request.body).send().await { Ok(response) => { @@ -65,7 +65,7 @@ impl HttpResponse { } } - async fn handle_put_request(&self, client: RequestClient, request: Request) -> Response { + async fn handle_put_request(&self, client: RequestClient, request: &Request) -> Response { let now = std::time::Instant::now(); match client.put(&request).json(&request.body).send().await { Ok(response) => { @@ -87,7 +87,7 @@ impl HttpResponse { } } - async fn handle_patch_request(&self, client: RequestClient, request: Request) -> Response { + async fn handle_patch_request(&self, client: RequestClient, request: &Request) -> Response { let now = std::time::Instant::now(); match client.patch(&request).json(&request.body).send().await { Ok(response) => { @@ -109,7 +109,7 @@ impl HttpResponse { } } - async fn handle_delete_request(&self, client: RequestClient, request: Request) -> Response { + async fn handle_delete_request(&self, client: RequestClient, request: &Request) -> Response { let now = std::time::Instant::now(); match client.delete(&request).json(&request.body).send().await { Ok(response) => { diff --git a/hac-core/src/net/response_decoders/json_decoder.rs b/hac-core/src/net/response_decoders/json_decoder.rs index cb7679c..e814914 100755 --- a/hac-core/src/net/response_decoders/json_decoder.rs +++ b/hac-core/src/net/response_decoders/json_decoder.rs @@ -21,7 +21,7 @@ impl ResponseDecoder for JsonDecoder { let mut body: Option = None; let mut pretty_body = None; - if response.content_length().is_some_and(|len| len.gt(&0)) { + if response.content_length().is_some_and(|len| len > 0) { if let Ok(body_str) = response.text().await { let pretty_body_str = jsonxf::pretty_print(&body_str).unwrap_or_default(); pretty_body = Some(TextObject::from(pretty_body_str.as_str())); diff --git a/hac-store/src/collection.rs b/hac-store/src/collection.rs index 76c90ec..a54ab04 100644 --- a/hac-store/src/collection.rs +++ b/hac-store/src/collection.rs @@ -1,6 +1,6 @@ use std::fmt::Display; -use crate::slab::{Key, Slab}; +use crate::slab::{EntryRef, Key, Slab}; use crate::HAC_STORE; #[derive(Debug, Clone)] @@ -633,3 +633,32 @@ pub fn rebuild_tree_layout() { collection.layout = ReqTree { nodes }; }) } + +pub fn borrow_selected_request() -> EntryRef { + HAC_STORE.with_borrow_mut(|store| { + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + let Some((slab, key)) = collection.selected_request else { + panic!("trying to borrow a request without having one?"); + }; + let request_ref = match slab { + WhichSlab::Requests => collection.requests.borrow(key, slab), + WhichSlab::RootRequests => collection.root_requests.borrow(key, slab), + _ => unreachable!(), + }; + request_ref + }) +} + +pub fn restore_request(request_ref: EntryRef) { + HAC_STORE.with_borrow_mut(|store| { + assert!(store.collection.is_some()); + let collection = store.collection.as_mut().unwrap(); + + match request_ref.id() { + WhichSlab::Requests => collection.requests.restore(request_ref), + WhichSlab::RootRequests => collection.root_requests.restore(request_ref), + _ => unreachable!(), + }; + }) +} diff --git a/hac-store/src/slab.rs b/hac-store/src/slab.rs index 37f8b06..8bc0bf5 100644 --- a/hac-store/src/slab.rs +++ b/hac-store/src/slab.rs @@ -1,9 +1,24 @@ +use crate::collection::WhichSlab; +use std::sync::Arc; pub type Key = usize; #[derive(Debug)] pub enum Entry { Full(T), Free(Option), + Borrowed(Arc), +} + +pub struct EntryRef { + pub inner: Arc, + pub key: Key, + id: WhichSlab, +} + +impl EntryRef { + pub fn id(&self) -> WhichSlab { + self.id + } } impl Entry { @@ -77,6 +92,39 @@ impl Slab { key } + pub fn restore(&mut self, entry_ref: EntryRef) { + let key = entry_ref.key; + let mut entry = Entry::free(None); + std::mem::swap(&mut self.inner[key], &mut entry); + drop(entry_ref); + + let Entry::Borrowed(entry) = entry else { + panic!("attempting to restore a non-borrowed value"); + }; + + let Ok(entry) = Arc::try_unwrap(entry) else { + panic!("restoring reference with more than one borrower"); + }; + + let mut entry = Entry::full(entry); + std::mem::swap(&mut self.inner[key], &mut entry); + } + + pub fn borrow(&mut self, key: Key, id: WhichSlab) -> EntryRef { + let mut entry = Entry::free(None); + std::mem::swap(&mut self.inner[key], &mut entry); + + let val = match entry { + Entry::Full(val) => val, + Entry::Free(_) | Entry::Borrowed(_) => panic!("cannot borrow a free or borrowed entry"), + }; + + let inner = Arc::new(val); + let mut entry = Entry::Borrowed(inner.clone()); + std::mem::swap(&mut self.inner[key], &mut entry); + EntryRef { inner, key, id } + } + pub fn remove(&mut self, idx: Key) -> T { let mut entry = Entry::free(self.next_idx.take()); self.next_idx = Some(idx); @@ -84,15 +132,16 @@ impl Slab { match entry { Entry::Full(val) => val, - Entry::Free(_) => panic!("cannot remove a free entry"), + Entry::Free(_) | Entry::Borrowed(_) => panic!("cannot remove a free or borrowed entry"), } } pub fn get(&self, idx: Key) -> &T { - let Entry::Full(val) = &self.inner[idx] else { - panic!("attempted to get an empty entry"); - }; - val + match &self.inner[idx] { + Entry::Full(val) => val, + Entry::Borrowed(val) => val, + _ => panic!("attempted to get an empty entry"), + } } pub fn get_mut(&mut self, idx: Key) -> &mut T { @@ -124,7 +173,8 @@ impl Slab { self.inner .iter() .filter_map(|e| match e { - Entry::Full(val) => Some(val), + Entry::Full(_) => Some(()), + Entry::Borrowed(_) => Some(()), Entry::Free(_) => None, }) .count() @@ -144,21 +194,21 @@ impl Slab { pub fn enumerated_iter(&self) -> impl Iterator { self.inner.iter().enumerate().filter_map(|(i, e)| match e { Entry::Full(val) => Some((i, val)), - Entry::Free(_) => None, + Entry::Free(_) | Entry::Borrowed(_) => None, }) } pub fn iter(&self) -> impl Iterator { self.inner.iter().filter_map(|e| match e { Entry::Full(val) => Some(val), - Entry::Free(_) => None, + Entry::Free(_) | Entry::Borrowed(_) => None, }) } pub fn iter_mut(&mut self) -> impl Iterator { self.inner.iter_mut().filter_map(|e| match e { Entry::Full(val) => Some(val), - Entry::Free(_) => None, + Entry::Free(_) | Entry::Borrowed(_) => None, }) } } @@ -172,7 +222,7 @@ impl IntoIterator for Slab { .into_iter() .filter_map(|e| match e { Entry::Full(val) => Some(val), - Entry::Free(_) => None, + Entry::Free(_) | Entry::Borrowed(_) => None, }) .collect::>() .into_iter()