diff --git a/hac-client/benches/collection_viewer_bench.rs b/hac-client/benches/collection_viewer_bench.rs index df88996..90524f0 100644 --- a/hac-client/benches/collection_viewer_bench.rs +++ b/hac-client/benches/collection_viewer_bench.rs @@ -2,7 +2,7 @@ use std::sync::{Arc, RwLock}; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers}; use hac_client::{ - pages::{collection_viewer::CollectionViewer, Component, Eventful}, + pages::{collection_viewer::CollectionViewer, Eventful, Page}, utils::build_syntax_highlighted_lines, }; use hac_core::{ diff --git a/hac-client/src/app.rs b/hac-client/src/app.rs index 01fd3e1..c369f1f 100644 --- a/hac-client/src/app.rs +++ b/hac-client/src/app.rs @@ -1,6 +1,6 @@ use crate::{ event_pool::{Event, EventPool}, - pages::{Component, Eventful}, + pages::{Eventful, Page}, screen_manager::ScreenManager, }; use hac_core::{collection::Collection, command::Command}; @@ -49,6 +49,15 @@ impl<'app> App<'app> { .register_command_handler(command_tx.clone())?; loop { + { + while let Ok(command) = command_rx.try_recv() { + match command { + Command::Quit => self.should_quit = true, + _ => self.screen_manager.handle_command(command), + } + } + } + if let Some(event) = self.event_pool.next().await { match event { Event::Tick => self.screen_manager.handle_tick()?, @@ -75,13 +84,6 @@ impl<'app> App<'app> { }; } - while let Ok(command) = command_rx.try_recv() { - match command { - Command::Quit => self.should_quit = true, - _ => self.screen_manager.handle_command(command), - } - } - if self.should_quit { break; } diff --git a/hac-client/src/pages.rs b/hac-client/src/pages.rs index 54385c5..24f7357 100644 --- a/hac-client/src/pages.rs +++ b/hac-client/src/pages.rs @@ -4,7 +4,7 @@ pub mod confirm_popup; pub mod error_popup; pub mod input; mod overlay; -pub mod spinner; +mod spinner; pub mod terminal_too_small; use crate::event_pool::Event; @@ -14,7 +14,7 @@ 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 Component { +pub trait Page { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()>; /// pages need to adapt to change of sizes on the application, this function is called @@ -29,31 +29,61 @@ pub trait Component { Ok(()) } - /// tick is a smaller interval than the one used by the render cycle, it is mainly used - /// for actions that rely on time, such as auto-syncing the edited request on the - /// CollectionViewer + /// 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<()> { Ok(()) } } -/// An `Eventful` component is a component that can handle key events, and mouse events +/// 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; + /// the top level event loop doesnt differentiate between kinds of events, so this is what /// delegate each kind of events to the responsible function - fn handle_event(&mut self, event: Option) -> anyhow::Result> { - let action = match event { - Some(Event::Key(key_event)) => self.handle_key_event(key_event)?, - _ => None, - }; - - Ok(action) + fn handle_event(&mut self, event: Option) -> anyhow::Result> { + match event { + Some(Event::Key(key_event)) => self.handle_key_event(key_event), + _ => Ok(None), + } } /// when we get a key_event, this will be called for the eventful component to handle it #[allow(unused_variables)] - fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + Ok(None) + } +} + +/// An `EventfulWithContext` component is a component that can handle events but requires +/// additional information which can be specified by its implementer. Such as defining a +/// associate type required to re-render properly or information that it needs to decide +/// on how to behave +/// +/// besides the contextful behavior, this is exactly as `Eventful` +pub trait EventfulWithContext { + type Result; + type Context; + + fn handle_event( + &mut self, + event: Option, + context: Self::Context, + ) -> anyhow::Result> { + match event { + Some(Event::Key(key_event)) => self.handle_key_event(key_event, context), + _ => Ok(None), + } + } + + #[allow(unused_variables)] + fn handle_key_event( + &mut self, + key_event: KeyEvent, + context: Self::Context, + ) -> anyhow::Result> { Ok(None) } } diff --git a/hac-client/src/pages/collection_dashboard/collection_dashboard.rs b/hac-client/src/pages/collection_dashboard/collection_dashboard.rs index 15962bd..a7981c2 100644 --- a/hac-client/src/pages/collection_dashboard/collection_dashboard.rs +++ b/hac-client/src/pages/collection_dashboard/collection_dashboard.rs @@ -6,7 +6,7 @@ use crate::pages::{ confirm_popup::ConfirmPopup, error_popup::ErrorPopup, overlay::draw_overlay, - Component, Eventful, + Eventful, Page, }; use hac_core::{collection::types::Collection, command::Command}; @@ -520,7 +520,7 @@ impl<'a> CollectionDashboard<'a> { } } -impl Component for CollectionDashboard<'_> { +impl Page for CollectionDashboard<'_> { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { self.draw_background(size, frame); self.draw_title(frame)?; @@ -558,6 +558,8 @@ impl Component for CollectionDashboard<'_> { } 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)); diff --git a/hac-client/src/pages/collection_viewer/collection_store.rs b/hac-client/src/pages/collection_viewer/collection_store.rs new file mode 100644 index 0000000..77ccba3 --- /dev/null +++ b/hac-client/src/pages/collection_viewer/collection_store.rs @@ -0,0 +1,277 @@ +use std::{ + cell::RefCell, + collections::HashMap, + rc::Rc, + sync::{Arc, RwLock}, +}; + +use hac_core::collection::{ + types::{Request, RequestKind}, + Collection, +}; + +#[derive(Debug)] +pub struct CollectionState { + collection: Rc>, + hovered_request: Option, + selected_request: Option>>, + dirs_expanded: Rc>>, +} + +#[derive(Debug, Default)] +pub struct CollectionStore { + state: Option>>, +} + +pub enum CollectionStoreAction { + SetSelectedRequest(Option>>), + SetHoveredRequest(Option), + InsertRequest(RequestKind), + HoverPrev, + HoverNext, + ToggleDirectory(String), +} + +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(Arc::clone(req)) + } 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)), + }; + + 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.hover_prev(), + CollectionStoreAction::HoverNext => self.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; + } + } + } + } + + pub fn get_selected_request(&self) -> Option>> { + self.state + .as_ref() + .and_then(|state| state.borrow().selected_request.clone()) + } + + 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 get_requests(&self) -> Option>>> { + self.state.as_ref().and_then(|state| { + state + .borrow() + .collection + .borrow() + .requests + .as_ref() + .cloned() + }) + } + + fn hover_prev(&mut self) { + if self.get_hovered_request().is_some() && self.get_requests().is_some() { + let id = self.get_hovered_request().unwrap(); + let requests = self.get_requests().unwrap(); + + 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 hover_next(&mut self) { + if self.get_hovered_request().is_some() && self.get_requests().is_some() { + let id = self.get_hovered_request().unwrap(); + let requests = self.get_requests().unwrap(); + + 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(), + ) + } +} + +#[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, + (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() +} diff --git a/hac-client/src/pages/collection_viewer/collection_viewer.rs b/hac-client/src/pages/collection_viewer/collection_viewer.rs index 0d707b0..e5c193a 100644 --- a/hac-client/src/pages/collection_viewer/collection_viewer.rs +++ b/hac-client/src/pages/collection_viewer/collection_viewer.rs @@ -2,13 +2,14 @@ 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::req_uri::{ReqUri, ReqUriState}; use crate::pages::collection_viewer::request_editor::{ReqEditor, ReqEditorState}; use crate::pages::collection_viewer::response_viewer::ResViewer; use crate::pages::collection_viewer::sidebar::Sidebar; use crate::pages::input::Input; use crate::pages::overlay::draw_overlay; -use crate::pages::{Component, Eventful}; +use crate::pages::{Eventful, Page}; use std::cell::RefCell; use std::collections::HashMap; @@ -16,16 +17,14 @@ use std::ops::{Add, Div, Sub}; use std::rc::Rc; use std::sync::{Arc, RwLock}; -use anyhow::Context; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use hac_config::EditorMode; use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::style::{Style, Stylize}; use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph, StatefulWidget}; use ratatui::Frame; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; -use super::sidebar; +use super::sidebar::{self, SidebarEvent}; #[derive(Debug, PartialEq)] pub struct ExplorerLayout { @@ -44,14 +43,7 @@ pub enum Overlays { RequestMethod, } -#[derive(PartialEq)] -enum VisitNode { - Next, - Prev, - Curr, -} - -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum PaneFocus { Sidebar, ReqUri, @@ -59,6 +51,26 @@ pub enum PaneFocus { Editor, } +impl PaneFocus { + fn next(&self) -> Self { + match self { + PaneFocus::Sidebar => PaneFocus::ReqUri, + PaneFocus::ReqUri => PaneFocus::Editor, + PaneFocus::Editor => PaneFocus::Preview, + PaneFocus::Preview => PaneFocus::Sidebar, + } + } + + fn prev(&self) -> Self { + match self { + PaneFocus::Sidebar => PaneFocus::Preview, + PaneFocus::ReqUri => PaneFocus::Sidebar, + PaneFocus::Editor => PaneFocus::ReqUri, + PaneFocus::Preview => PaneFocus::Editor, + } + } +} + #[derive(Debug, Default, PartialEq)] pub enum FormFocus { #[default] @@ -127,12 +139,8 @@ pub struct CollectionViewer<'cv> { config: &'cv hac_config::Config, layout: ExplorerLayout, global_command_sender: Option>, - collection: Collection, collection_sync_timer: std::time::Instant, - - selected_request: Option>>, - hovered_request: Option, - dirs_expanded: HashMap, + collection_store: Rc>, focused_pane: PaneFocus, selected_pane: Option, @@ -151,7 +159,7 @@ pub struct CollectionViewer<'cv> { impl<'cv> CollectionViewer<'cv> { pub fn new( size: Rect, - collection: Collection, + collection_store: Rc>, colors: &'cv hac_colors::Colors, config: &'cv hac_config::Config, dry_run: bool, @@ -159,54 +167,42 @@ impl<'cv> CollectionViewer<'cv> { let layout = build_layout(size); let (request_tx, response_rx) = unbounded_channel::(); - let selected_request = collection.requests.as_ref().and_then(|requests| { - requests.first().and_then(|req| { - if let RequestKind::Single(req) = req { - Some(Arc::clone(req)) - } else { - None - } - }) - }); - - let hovered_request = collection - .requests - .as_ref() - .and_then(|items| items.first().map(|item| item.get_id())); + //let selected_request = collection.requests.as_ref().and_then(|requests| { + // requests.read().unwrap().first().and_then(|req| { + // if let RequestKind::Single(req) = req { + // Some(Arc::clone(req)) + // } else { + // None + // } + // }) + //}); + // + //let dirs_expanded = Rc::new(RefCell::new(HashMap::default())); + // + //let hovered_request = collection.requests.as_ref().and_then(|items| { + // items + // .read() + // .unwrap() + // .first() + // .map(|item| Rc::new(RefCell::new(item.get_id()))) + //}); CollectionViewer { request_editor: ReqEditor::new( colors, config, - selected_request.as_ref().cloned(), + collection_store.clone(), layout.req_editor, ), response_viewer: ResViewer::new(colors, None, layout.response_preview), - sidebar: Sidebar::new( - colors, - true, - false, - sidebar::build_lines( - collection.requests.as_ref(), - 0, - &selected_request.as_ref(), - hovered_request.as_ref(), - &mut HashMap::new(), - colors, - ), - ), + sidebar: sidebar::Sidebar::new(colors, true, false, collection_store.clone()), colors, config, layout, global_command_sender: None, - collection, collection_sync_timer: std::time::Instant::now(), - selected_request, - hovered_request, - dirs_expanded: HashMap::default(), - focused_pane: PaneFocus::Sidebar, selected_pane: None, @@ -219,106 +215,11 @@ impl<'cv> CollectionViewer<'cv> { request_tx, dry_run, + collection_store, } } - #[tracing::instrument(skip_all, err)] - fn handle_sidebar_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - if let KeyCode::Esc = key_event.code { - self.selected_pane = None; - self.update_selection(); - return Ok(None); - } - - if self - .selected_pane - .as_ref() - .is_some_and(|pane| pane.eq(&PaneFocus::Sidebar)) - { - match key_event.code { - KeyCode::Enter => { - if let Some(id) = self.hovered_request.as_ref() { - let req = get_request_by_id( - self.collection.requests.as_ref().unwrap(), - &self.dirs_expanded, - id, - ); - match req { - RequestKind::Nested(dir) => { - let entry = - self.dirs_expanded.entry(dir.id.clone()).or_insert(false); - *entry = !*entry; - } - RequestKind::Single(req) => { - self.selected_request = Some(Arc::clone(&req)); - self.response_viewer.update(None); - self.request_editor = ReqEditor::new( - self.colors, - self.config, - self.selected_request.clone(), - self.layout.req_editor, - ); - } - } - self.build_sidebar_treeview(); - } - } - KeyCode::Char('j') => { - if let Some(id) = self.hovered_request.as_mut() { - if let Some(next) = find_next_entry( - self.collection.requests.as_ref().context( - "should never have a selected request without any requests on collection", - )?, - VisitNode::Next, - &self.dirs_expanded, - id, - ) { - *id = next.get_id(); - self.build_sidebar_treeview(); - }; - } - } - KeyCode::Char('k') => { - if let Some(id) = self.hovered_request.as_mut() { - if let Some(next) = find_next_entry( - self.collection.requests.as_ref().expect( - "should never have a selected request without any requests on collection", - ), - VisitNode::Prev, - &self.dirs_expanded, - id, - ) { - *id = next.get_id(); - self.build_sidebar_treeview(); - } - } - } - KeyCode::Char('n') => self.curr_overlay = Overlays::CreateRequest, - _ => {} - } - } - - Ok(None) - } - - fn build_sidebar_treeview(&mut self) { - self.sidebar.set_lines(sidebar::build_lines( - self.collection.requests.as_ref(), - 0, - &self.selected_request.as_ref(), - self.hovered_request.as_ref(), - &mut HashMap::new(), - self.colors, - )); - } - fn handle_req_uri_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - if let KeyCode::Esc = key_event.code { - self.selected_pane = None; - self.update_selection(); - return Ok(None); - } - if self .selected_pane .as_ref() @@ -326,25 +227,41 @@ impl<'cv> CollectionViewer<'cv> { { match key_event.code { KeyCode::Char(c) => { - if let Some(req) = self.selected_request.as_mut() { + 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.selected_request.as_mut() { + if let Some(req) = self + .collection_store + .borrow() + .get_selected_request() + .as_mut() + { req.write().unwrap().uri.pop(); } } KeyCode::Enter => { if self - .selected_request + .collection_store + .borrow() + .get_selected_request() .as_ref() .is_some_and(|_| !self.has_pending_request) { self.has_pending_request = true; self.response_viewer.set_pending_request(true); hac_core::net::handle_request( - self.selected_request.as_ref().unwrap(), + self.collection_store + .borrow() + .get_selected_request() + .as_ref() + .unwrap(), self.request_tx.clone(), ) } @@ -356,14 +273,6 @@ impl<'cv> CollectionViewer<'cv> { } fn handle_editor_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - match (key_event.code, &self.selected_pane) { - (KeyCode::Esc, Some(_)) if self.request_editor.mode().eq(&EditorMode::Normal) => { - self.selected_pane = None; - self.update_selection(); - return Ok(None); - } - _ => {} - } if key_event.code.eq(&KeyCode::Enter) && self.selected_pane.is_none() { return Ok(None); } @@ -371,8 +280,9 @@ impl<'cv> CollectionViewer<'cv> { } fn draw_req_uri(&mut self, frame: &mut Frame) { + let selected_request = self.collection_store.borrow().get_selected_request(); let mut state = ReqUriState::new( - &self.selected_request, + &selected_request, self.focused_pane.eq(&PaneFocus::ReqUri), self.selected_pane .as_ref() @@ -399,10 +309,14 @@ impl<'cv> CollectionViewer<'cv> { fn drain_responses_channel(&mut self) { while let Ok(res) = self.response_rx.try_recv() { let res = Rc::new(RefCell::new(res)); - self.selected_request.as_ref().and_then(|req| { - self.responses_map - .insert(req.read().unwrap().id.to_string(), Rc::clone(&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.has_pending_request = false; @@ -411,20 +325,6 @@ impl<'cv> CollectionViewer<'cv> { } } - fn handle_preview_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - match key_event.code { - KeyCode::Esc => { - self.selected_pane = None; - self.update_selection(); - } - _ => { - self.response_viewer.handle_key_event(key_event)?; - } - } - - Ok(None) - } - fn draw_req_uri_hint(&self, frame: &mut Frame) { let hint = "[type anything -> edit] [enter -> execute request] [ -> quit]" .fg(self.colors.normal.magenta) @@ -742,30 +642,28 @@ impl<'cv> CollectionViewer<'cv> { CreateReqKind::Directory => RequestKind::Nested(Directory { id: uuid::Uuid::new_v4().to_string(), name: form_state.req_name.clone(), - requests: vec![], + requests: Arc::new(RwLock::new(vec![])), }), }; + let mut collection_store_mut = self.collection_store.borrow_mut(); if let RequestKind::Single(ref req) = new_request { - self.selected_request = Some(Arc::clone(req)); - self.hovered_request = Some(new_request.get_id()); - self.sidebar.set_lines(sidebar::build_lines( - self.collection.requests.as_ref(), - 0, - &self.selected_request.as_ref(), - self.hovered_request.as_ref(), - &mut HashMap::new(), - self.colors, - )); + collection_store_mut + .dispatch(CollectionStoreAction::SetSelectedRequest(Some(req.clone()))); + collection_store_mut.dispatch(CollectionStoreAction::SetHoveredRequest(Some( + new_request.get_id(), + ))); + self.sidebar.rebuild_tree_view(); } - self.collection - .requests - .get_or_insert_with(Vec::new) - .push(new_request); + collection_store_mut.dispatch(CollectionStoreAction::InsertRequest(new_request)); + // dropping the borrow so we can sync the changes + drop(collection_store_mut); self.create_req_form_state = CreateReqFormState::default(); self.curr_overlay = Overlays::None; + + // TODO: maybe the collection store should be responsible for syncing to disk self.sync_collection_changes(); } @@ -776,8 +674,15 @@ impl<'cv> CollectionViewer<'cv> { .expect("should have a sender at this point") .clone(); - let mut collection = self.collection.clone(); - if let Some(request) = self.selected_request.as_ref() { + 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 @@ -794,6 +699,8 @@ impl<'cv> CollectionViewer<'cv> { .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) => { @@ -801,13 +708,15 @@ impl<'cv> CollectionViewer<'cv> { *inner = request.clone(); } } - RequestKind::Nested(dir) => dir.requests.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(); + } } - } - }), + }) + } }); } @@ -830,19 +739,21 @@ impl<'cv> CollectionViewer<'cv> { }); } - fn update_selection(&mut self) { - self.response_viewer - .maybe_select(self.selected_pane.as_ref()); - self.sidebar.maybe_select(self.selected_pane.as_ref()); + fn update_selection(&mut self, pane_to_select: Option) { + self.selected_pane = pane_to_select; + + self.response_viewer.maybe_select(pane_to_select.as_ref()); + self.sidebar.maybe_select(pane_to_select.as_ref()); } - fn update_focus(&mut self) { + fn update_focus(&mut self, pane_to_focus: PaneFocus) { + self.focused_pane = pane_to_focus; self.response_viewer.maybe_focus(&self.focused_pane); self.sidebar.maybe_focus(&self.focused_pane); } } -impl Component for CollectionViewer<'_> { +impl Page 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 @@ -906,7 +817,12 @@ impl Component for CollectionViewer<'_> { .as_ref() .is_some_and(|pane| pane.eq(&PaneFocus::ReqUri)) { - if let Some(request) = self.selected_request.as_ref() { + if let Some(request) = self + .collection_store + .borrow() + .get_selected_request() + .as_ref() + { frame.set_cursor( self.layout .req_uri @@ -942,18 +858,9 @@ impl Component for CollectionViewer<'_> { } impl Eventful for CollectionViewer<'_> { - 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.sync_collection_changes(); - return Ok(Some(Command::Quit)); - }; - - if let (KeyCode::Enter, None) = (key_event.code, self.selected_pane.as_ref()) { - self.selected_pane = Some(self.focused_pane.clone()); - self.update_selection(); - return Ok(None); - } + type Result = Command; + fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { if self.curr_overlay.ne(&Overlays::None) { match self.curr_overlay { Overlays::CreateRequest => return self.handle_create_request_key_event(key_event), @@ -967,91 +874,49 @@ impl Eventful for CollectionViewer<'_> { if self.selected_pane.is_none() { match key_event.code { KeyCode::Char('r') => { - self.focused_pane = PaneFocus::Sidebar; - self.selected_pane = Some(PaneFocus::Sidebar); - self.update_selection(); - return Ok(None); + self.update_focus(PaneFocus::Sidebar); + self.update_selection(Some(PaneFocus::Sidebar)); } KeyCode::Char('u') => { - self.focused_pane = PaneFocus::ReqUri; - self.selected_pane = Some(PaneFocus::ReqUri); - self.update_selection(); - return Ok(None); + self.update_focus(PaneFocus::ReqUri); + self.update_selection(Some(PaneFocus::ReqUri)); } KeyCode::Char('p') => { - self.focused_pane = PaneFocus::Preview; - self.selected_pane = Some(PaneFocus::Preview); - self.update_selection(); - return Ok(None); + self.update_focus(PaneFocus::Preview); + self.update_selection(Some(PaneFocus::Preview)); } KeyCode::Char('e') => { - self.focused_pane = PaneFocus::Editor; - self.selected_pane = Some(PaneFocus::Editor); - self.update_selection(); - return Ok(None); - } - _ => (), - } - } - - if let KeyCode::Tab = key_event.code { - match (&self.focused_pane, &self.selected_pane, key_event.modifiers) { - (PaneFocus::Sidebar, None, KeyModifiers::NONE) => { - self.focused_pane = PaneFocus::ReqUri; - self.update_focus(); - } - (PaneFocus::ReqUri, None, KeyModifiers::NONE) => { - self.focused_pane = PaneFocus::Editor; - self.update_focus(); - } - (PaneFocus::Editor, None, KeyModifiers::NONE) => { - self.focused_pane = PaneFocus::Preview; - self.update_focus(); - } - (PaneFocus::Preview, None, KeyModifiers::NONE) => { - self.focused_pane = PaneFocus::Sidebar; - self.update_focus(); - } - (PaneFocus::Editor, Some(_), KeyModifiers::NONE) => { - self.request_editor.handle_key_event(key_event)?; - } - (PaneFocus::Preview, Some(_), _) => { - self.handle_preview_key_event(key_event)?; + self.update_focus(PaneFocus::Editor); + self.update_selection(Some(PaneFocus::Editor)); } + KeyCode::Tab => self.update_focus(self.focused_pane.next()), + KeyCode::BackTab => self.update_focus(self.focused_pane.prev()), + KeyCode::Enter => self.update_selection(Some(self.focused_pane)), _ => {} } return Ok(None); } - if let KeyCode::BackTab = key_event.code { - match (&self.focused_pane, &self.selected_pane, key_event.modifiers) { - (PaneFocus::Sidebar, None, KeyModifiers::SHIFT) => { - self.focused_pane = PaneFocus::Preview; - self.update_focus(); - } - (PaneFocus::ReqUri, None, KeyModifiers::SHIFT) => { - self.focused_pane = PaneFocus::Sidebar; - self.update_focus(); - } - (PaneFocus::Editor, None, KeyModifiers::SHIFT) => { - self.focused_pane = PaneFocus::ReqUri; - self.update_focus(); - } - (PaneFocus::Preview, None, KeyModifiers::SHIFT) => { - self.focused_pane = PaneFocus::Editor; - self.update_focus(); - } - _ => {} + match self.focused_pane { + PaneFocus::Sidebar => match self.sidebar.handle_key_event(key_event)? { + Some(SidebarEvent::CreateRequest) => self.curr_overlay = Overlays::CreateRequest, + Some(SidebarEvent::Quit) => return Ok(Some(Command::Quit)), + // no event so we do nothing + None => {} + }, + PaneFocus::ReqUri => { + self.handle_req_uri_key_event(key_event)?; } - return Ok(None); - } + PaneFocus::Preview => match self.response_viewer.handle_key_event(key_event)? { + Some(_) => todo!(), + None => todo!(), + }, + PaneFocus::Editor => { + self.handle_editor_key_event(key_event)?; + } + }; - match self.focused_pane { - PaneFocus::Sidebar => self.handle_sidebar_key_event(key_event), - PaneFocus::ReqUri => self.handle_req_uri_key_event(key_event), - PaneFocus::Preview => self.handle_preview_key_event(key_event), - PaneFocus::Editor => self.handle_editor_key_event(key_event), - } + Ok(None) } } @@ -1100,103 +965,6 @@ pub fn build_layout(size: Rect) -> ExplorerLayout { } } -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, - (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.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 super::*; diff --git a/hac-client/src/pages/collection_viewer/mod.rs b/hac-client/src/pages/collection_viewer/mod.rs index c07a30d..dcfd74d 100644 --- a/hac-client/src/pages/collection_viewer/mod.rs +++ b/hac-client/src/pages/collection_viewer/mod.rs @@ -1,3 +1,4 @@ +pub mod collection_store; #[allow(clippy::module_inception)] mod collection_viewer; mod req_uri; diff --git a/hac-client/src/pages/collection_viewer/request_editor.rs b/hac-client/src/pages/collection_viewer/request_editor.rs index 563b6a2..98d849b 100644 --- a/hac-client/src/pages/collection_viewer/request_editor.rs +++ b/hac-client/src/pages/collection_viewer/request_editor.rs @@ -1,3 +1,4 @@ +use crate::pages::collection_viewer::collection_store::CollectionStore; use crate::{pages::Eventful, utils::build_syntax_highlighted_lines}; use hac_config::{Action, EditorMode, KeyAction}; @@ -6,8 +7,10 @@ use hac_core::command::Command; use hac_core::syntax::highlighter::HIGHLIGHTER; use hac_core::text_object::{cursor::Cursor, TextObject, Write}; +use std::cell::RefCell; use std::fmt::Display; use std::ops::{Add, Div, Mul, Sub}; +use std::rc::Rc; use std::sync::{Arc, RwLock}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; @@ -92,10 +95,10 @@ impl<'re> ReqEditor<'re> { pub fn new( colors: &'re hac_colors::Colors, config: &'re hac_config::Config, - request: Option>>, + collection_store: Rc>, size: Rect, ) -> Self { - let (body, tree) = if let Some(request) = request.as_ref() { + let (body, tree) = if let Some(request) = collection_store.borrow().get_selected_request() { if let Some(body) = request.read().unwrap().body.as_ref() { let mut highlighter = HIGHLIGHTER.write().unwrap(); let tree = highlighter.parse(body); @@ -122,7 +125,9 @@ impl<'re> ReqEditor<'re> { row_scroll: 0, col_scroll: 0, layout: build_layout(size), - curr_tab: request + curr_tab: collection_store + .borrow() + .get_selected_request() .as_ref() .map(request_has_no_body) .unwrap_or(false) @@ -644,6 +649,8 @@ impl<'re> ReqEditor<'re> { } impl Eventful for ReqEditor<'_> { + type Result = Command; + fn handle_key_event( &mut self, key_event: KeyEvent, diff --git a/hac-client/src/pages/collection_viewer/response_viewer.rs b/hac-client/src/pages/collection_viewer/response_viewer.rs index d30ea9c..47f1e5f 100644 --- a/hac-client/src/pages/collection_viewer/response_viewer.rs +++ b/hac-client/src/pages/collection_viewer/response_viewer.rs @@ -4,7 +4,7 @@ use hac_core::syntax::highlighter::HIGHLIGHTER; use crate::ascii::{BIG_ERROR_ARTS, LOGO_ART, SMALL_ERROR_ARTS}; use crate::pages::collection_viewer::collection_viewer::PaneFocus; -use crate::pages::{spinner::Spinner, Component, Eventful}; +use crate::pages::{spinner::Spinner, Eventful, Page}; use crate::utils::build_syntax_highlighted_lines; use std::cell::RefCell; @@ -223,6 +223,7 @@ impl<'a> ResViewer<'a> { let size = Rect::new(request_pane.x, center, request_pane.width, 1); let spinner = Spinner::default() .with_label("Sending request".fg(self.colors.bright.black)) + .with_style(Style::default().fg(self.colors.normal.red)) .into_centered_line(); frame.render_widget(Clear, request_pane); @@ -571,7 +572,7 @@ impl<'a> ResViewer<'a> { } } -impl<'a> Component for ResViewer<'a> { +impl<'a> Page for ResViewer<'a> { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { self.draw_tabs(frame, self.layout.tabs_pane); self.draw_current_tab(frame, self.layout.content_pane); @@ -585,6 +586,8 @@ impl<'a> Component for ResViewer<'a> { } impl<'a> Eventful for ResViewer<'a> { + type Result = Command; + fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { if let KeyCode::Tab = key_event.code { self.active_tab = ResViewerTabs::next(&self.active_tab); diff --git a/hac-client/src/pages/collection_viewer/sidebar.rs b/hac-client/src/pages/collection_viewer/sidebar.rs index 139cc20..4eeac57 100644 --- a/hac-client/src/pages/collection_viewer/sidebar.rs +++ b/hac-client/src/pages/collection_viewer/sidebar.rs @@ -1,8 +1,12 @@ +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use hac_core::collection::types::{Request, RequestKind, RequestMethod}; -use crate::pages::Component; +use crate::pages::collection_viewer::collection_store::{CollectionStore, CollectionStoreAction}; +use crate::pages::{Eventful, Page}; +use std::cell::RefCell; use std::collections::HashMap; +use std::rc::Rc; use std::sync::{Arc, RwLock}; use ratatui::layout::Rect; @@ -14,30 +18,32 @@ use ratatui::Frame; use super::collection_viewer::PaneFocus; #[derive(Debug)] -pub struct Sidebar<'s> { - colors: &'s hac_colors::Colors, +pub struct Sidebar<'sbar> { + colors: &'sbar hac_colors::Colors, is_focused: bool, is_selected: bool, lines: Vec>, + collection_store: Rc>, } -impl<'s> Sidebar<'s> { +impl<'sbar> Sidebar<'sbar> { pub fn new( - colors: &'s hac_colors::Colors, + colors: &'sbar hac_colors::Colors, is_focused: bool, is_selected: bool, - lines: Vec>, + collection_store: Rc>, ) -> Self { - Self { + let mut sidebar = Self { colors, is_focused, is_selected, - lines, - } - } + lines: vec![], + collection_store, + }; - pub fn set_lines(&mut self, lines: Vec>) { - self.lines = lines; + sidebar.rebuild_tree_view(); + + sidebar } pub fn maybe_select(&mut self, selected_pane: Option<&PaneFocus>) { @@ -47,9 +53,21 @@ impl<'s> Sidebar<'s> { pub fn maybe_focus(&mut self, focused_pane: &PaneFocus) { self.is_focused = focused_pane.eq(&PaneFocus::Sidebar); } + + 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, + ); + } } -impl<'s> Component for Sidebar<'s> { +impl<'sbar> Page for Sidebar<'sbar> { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { let mut requests_size = Rect::new(size.x + 1, size.y, size.width.saturating_sub(2), 1); @@ -80,21 +98,88 @@ impl<'s> Component for Sidebar<'s> { fn resize(&mut self, _new_size: Rect) {} } +#[derive(Debug)] +pub enum SidebarEvent { + CreateRequest, + Quit, +} + +impl<'a> Eventful for Sidebar<'a> { + type Result = SidebarEvent; + + fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + assert!( + self.is_selected, + "handled an event to the sidebar while it was not selected" + ); + + 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(); + match key_event.code { + KeyCode::Enter => { + if store.get_hovered_request().is_some() && store.get_requests().is_some() { + let request = store.find_hovered_request(); + match request { + RequestKind::Nested(_) => { + store + .dispatch(CollectionStoreAction::ToggleDirectory(request.get_id())); + drop(store); + self.rebuild_tree_view(); + } + RequestKind::Single(req) => { + store.dispatch(CollectionStoreAction::SetSelectedRequest(Some(req))); + drop(store); + self.rebuild_tree_view(); + } + } + } + } + KeyCode::Char('j') | KeyCode::Down => { + if store.get_hovered_request().is_some() && store.get_requests().is_some() { + store.dispatch(CollectionStoreAction::HoverNext); + drop(store); + self.rebuild_tree_view(); + } + } + KeyCode::Char('k') | KeyCode::Up => { + if store.get_hovered_request().is_some() && store.get_requests().is_some() { + store.dispatch(CollectionStoreAction::HoverPrev); + drop(store); + self.rebuild_tree_view(); + } + } + + KeyCode::Char('n') => return Ok(Some(SidebarEvent::CreateRequest)), + _ => {} + } + + Ok(None) + } +} + pub fn build_lines( - requests: Option<&Vec>, + requests: Option>>>, level: usize, - selected_request: &Option<&Arc>>, - hovered_request: Option<&String>, - dirs_expanded: &mut HashMap, + selected_request: Option>>, + hovered_request: Option, + dirs_expanded: Rc>>, colors: &hac_colors::Colors, ) -> Vec> { requests - .unwrap_or(&vec![]) + .unwrap_or(Arc::new(RwLock::new(vec![]))) + .read() + .unwrap() .iter() .flat_map(|item| match item { RequestKind::Nested(dir) => { - let is_hovered = hovered_request.is_some_and(|id| id.eq(&item.get_id())); - let is_expanded = dirs_expanded.entry(dir.id.to_string()).or_insert(false); + 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() @@ -116,11 +201,11 @@ pub fn build_lines( let nested_lines = if *is_expanded { build_lines( - Some(&dir.requests), + Some(dir.requests.clone()), level + 1, - selected_request, - hovered_request, - dirs_expanded, + selected_request.clone(), + hovered_request.clone(), + dirs_expanded.clone(), colors, ) } else { @@ -133,7 +218,9 @@ pub fn build_lines( 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.is_some_and(|id| id.eq(&item.get_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() diff --git a/hac-client/src/pages/terminal_too_small.rs b/hac-client/src/pages/terminal_too_small.rs index f4be65e..17e4c7f 100644 --- a/hac-client/src/pages/terminal_too_small.rs +++ b/hac-client/src/pages/terminal_too_small.rs @@ -1,4 +1,4 @@ -use crate::pages::Component; +use crate::pages::Page; use ratatui::{ layout::{Alignment, Constraint, Direction, Flex, Layout, Rect}, style::Stylize, @@ -21,7 +21,7 @@ impl<'a> TerminalTooSmall<'a> { } } -impl Component for TerminalTooSmall<'_> { +impl Page for TerminalTooSmall<'_> { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { let layout = build_layout(size); diff --git a/hac-client/src/screen_manager.rs b/hac-client/src/screen_manager.rs index c581ce2..33e355b 100644 --- a/hac-client/src/screen_manager.rs +++ b/hac-client/src/screen_manager.rs @@ -1,10 +1,11 @@ -use crate::{ - event_pool::Event, - pages::{ - collection_dashboard::CollectionDashboard, collection_viewer::CollectionViewer, - terminal_too_small::TerminalTooSmall, Component, Eventful, - }, -}; +use std::{cell::RefCell, rc::Rc}; + +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, Page}; use hac_core::{collection::Collection, command::Command}; use ratatui::{layout::Rect, Frame}; @@ -36,6 +37,8 @@ pub struct ScreenManager<'sm> { 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>, @@ -55,6 +58,7 @@ impl<'sm> ScreenManager<'sm> { 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, @@ -83,14 +87,15 @@ impl<'sm> ScreenManager<'sm> { Command::SelectCollection(collection) | Command::CreateCollection(collection) => { tracing::debug!("changing to api explorer: {}", collection.info.name); self.switch_screen(Screens::CollectionViewer); - let mut collection_viewer = CollectionViewer::new( + self.collection_store.borrow_mut().set_state(collection); + self.collection_viewer = Some(CollectionViewer::new( self.size, - collection, + self.collection_store.clone(), self.colors, self.config, self.dry_run, - ); - collection_viewer + )); + self.collection_viewer.as_mut().unwrap() .register_command_handler( self.sender .as_ref() @@ -98,7 +103,6 @@ impl<'sm> ScreenManager<'sm> { .clone(), ) .ok(); - self.collection_viewer = Some(collection_viewer); } Command::Error(msg) => { self.collection_list.display_error(msg); @@ -108,7 +112,7 @@ impl<'sm> ScreenManager<'sm> { } } -impl Component for ScreenManager<'_> { +impl Page for ScreenManager<'_> { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { match (size.width < 80, size.height < 22) { (true, _) => self.switch_screen(Screens::TerminalTooSmall), @@ -165,6 +169,8 @@ impl Component for ScreenManager<'_> { } impl Eventful for ScreenManager<'_> { + type Result = Command; + fn handle_event(&mut self, event: Option) -> anyhow::Result> { match self.curr_screen { Screens::CollectionViewer => self diff --git a/hac-client/tests/collection_dashboard.rs b/hac-client/tests/collection_dashboard.rs index e84226f..178b9c9 100644 --- a/hac-client/tests/collection_dashboard.rs +++ b/hac-client/tests/collection_dashboard.rs @@ -1,5 +1,5 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use hac_client::pages::{collection_dashboard::CollectionDashboard, Component, Eventful}; +use hac_client::pages::{collection_dashboard::CollectionDashboard, Eventful, Page}; use hac_core::collection; use ratatui::{backend::TestBackend, layout::Rect, Frame, Terminal}; use std::{ diff --git a/hac-core/src/collection/types.rs b/hac-core/src/collection/types.rs index 245ef05..e57976a 100644 --- a/hac-core/src/collection/types.rs +++ b/hac-core/src/collection/types.rs @@ -9,11 +9,17 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Collection { pub info: Info, - pub requests: Option>, + pub requests: Option>>>, #[serde(skip)] pub path: PathBuf, } +impl AsRef for Collection { + fn as_ref(&self) -> &Collection { + self + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(untagged)] pub enum RequestKind { @@ -110,7 +116,7 @@ impl Hash for Request { pub struct Directory { pub id: String, pub name: String, - pub requests: Vec, + pub requests: Arc>>, } #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]