From ba32aebf26428d53102bc186f83a6dee7a254cdf Mon Sep 17 00:00:00 2001 From: Willians Faria Date: Wed, 22 May 2024 23:24:28 -0300 Subject: [PATCH] feat: adding better comments througout the app --- config/src/data.rs | 2 +- config/src/lib.rs | 2 +- reqtui/src/schema/schema.rs | 5 +- tui/Cargo.toml | 2 +- ...explorer.rs => collection_viewer_bench.rs} | 12 +- tui/src/app.rs | 32 +++-- tui/src/components.rs | 45 ++++--- .../components/api_explorer/api_explorer.rs | 12 +- tui/src/components/api_explorer/mod.rs | 2 +- tui/src/components/api_explorer/req_editor.rs | 13 +- tui/src/components/api_explorer/res_viewer.rs | 5 +- tui/src/components/dashboard/dashboard.rs | 34 +++--- tui/src/components/dashboard/mod.rs | 2 +- tui/src/components/input.rs | 2 + tui/src/components/overlay.rs | 2 + tui/src/components/terminal_too_small.rs | 18 ++- tui/src/event_pool.rs | 22 +--- tui/src/screen_manager.rs | 114 ++++++++++-------- tui/src/utils.rs | 28 ++++- tui/tests/dashboard.rs | 24 ++-- 20 files changed, 219 insertions(+), 159 deletions(-) rename tui/benches/{api_explorer.rs => collection_viewer_bench.rs} (98%) diff --git a/config/src/data.rs b/config/src/data.rs index abdd4ed..a073f37 100644 --- a/config/src/data.rs +++ b/config/src/data.rs @@ -23,7 +23,7 @@ fn get_data_dir() -> PathBuf { } #[tracing::instrument(err)] -pub fn get_schemas_dir() -> anyhow::Result { +pub fn get_collections_dir() -> anyhow::Result { let data_dir = get_data_dir(); let schemas_dir = data_dir.join(SCHEMAS_DIR); diff --git a/config/src/lib.rs b/config/src/lib.rs index 68310fc..91f7793 100644 --- a/config/src/lib.rs +++ b/config/src/lib.rs @@ -3,7 +3,7 @@ mod data; mod default_config; pub use config::{load_config, Action, Config, KeyAction}; -pub use data::{get_schemas_dir, setup_data_dir}; +pub use data::{get_collections_dir, setup_data_dir}; use serde::{Deserialize, Serialize}; #[derive(PartialEq, Deserialize, Serialize, Debug, Clone)] diff --git a/reqtui/src/schema/schema.rs b/reqtui/src/schema/schema.rs index 0728788..9207314 100644 --- a/reqtui/src/schema/schema.rs +++ b/reqtui/src/schema/schema.rs @@ -10,7 +10,7 @@ use super::{ #[tracing::instrument(err)] pub fn get_schemas_from_config() -> anyhow::Result> { - let schemas_dir = config::get_schemas_dir()?; + let schemas_dir = config::get_collections_dir()?; get_schemas(schemas_dir) } @@ -48,7 +48,8 @@ pub fn create_from_form(name: String, description: String) -> anyhow::Result Schema { } } -fn feed_keys(widget: &mut ApiExplorer, key_codes: Vec) { +fn feed_keys(widget: &mut CollectionViewer, key_codes: Vec) { key_codes.into_iter().for_each(|code| { widget .handle_key_event(KeyEvent { @@ -65,7 +65,7 @@ fn handling_key_events() { let schema = create_sample_schema(); let size = Rect::new(0, 0, 80, 24); let config = config::load_config(); - let mut api_explorer = ApiExplorer::new(size, schema, &colors, &config); + let mut api_explorer = CollectionViewer::new(size, schema, &colors, &config); let mut terminal = Terminal::new(TestBackend::new(size.width, size.height)).unwrap(); let mut frame = terminal.get_frame(); @@ -93,7 +93,7 @@ fn creating_with_highlight() { let schema = create_sample_schema(); let size = Rect::new(0, 0, 80, 24); let config = config::load_config(); - let mut api_explorer = ApiExplorer::new(size, schema, &colors, &config); + let mut api_explorer = CollectionViewer::new(size, schema, &colors, &config); let mut terminal = Terminal::new(TestBackend::new(size.width, size.height)).unwrap(); let _frame = terminal.get_frame(); @@ -141,5 +141,5 @@ lazy_static! { #[divan::bench] fn benchmarking_building_content() { let colors = colors::Colors::default(); - build_styled_content(&BODY, TREE.as_ref(), &colors); + build_syntax_highlighted_lines(&BODY, TREE.as_ref(), &colors); } diff --git a/tui/src/app.rs b/tui/src/app.rs index 641cd26..e45ddbc 100644 --- a/tui/src/app.rs +++ b/tui/src/app.rs @@ -1,5 +1,5 @@ use crate::{ - components::{Component, Eventful}, + components::{Eventful, Page}, event_pool::{Event, EventPool}, screen_manager::ScreenManager, }; @@ -30,6 +30,8 @@ impl<'app> App<'app> { }) } + /// 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(); @@ -54,22 +56,22 @@ impl<'app> App<'app> { } })?; } - _ => {} + 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 let Some(command) = self.screen_manager.handle_event(Some(event.clone()))? { - command_tx - .send(command) - .expect("failed to send command through channel") - } } while let Ok(command) = command_rx.try_recv() { match command { Command::Quit => self.should_quit = true, - Command::SelectSchema(_) => self.screen_manager.handle_command(command), - Command::CreateSchema(_) => self.screen_manager.handle_command(command), - Command::Error(_) => self.screen_manager.handle_command(command), + _ => self.screen_manager.handle_command(command), } } @@ -83,14 +85,18 @@ impl<'app> App<'app> { } } +/// before initializing the app, we must setup the terminal to enable all the features +/// we need, such as raw mode and entering the alternate screen fn startup() -> anyhow::Result<()> { crossterm::terminal::enable_raw_mode()?; - crossterm::execute!(std::io::stdout(), crossterm::terminal::EnterAlternateScreen,)?; + crossterm::execute!(std::io::stdout(), crossterm::terminal::EnterAlternateScreen)?; Ok(()) } +/// before shutting down we must reverse the changes we made to the users terminal, allowing +/// them have a usable terminal fn shutdown() -> anyhow::Result<()> { crossterm::terminal::disable_raw_mode()?; - crossterm::execute!(std::io::stdout(), crossterm::terminal::LeaveAlternateScreen,)?; + crossterm::execute!(std::io::stdout(), crossterm::terminal::LeaveAlternateScreen)?; Ok(()) } diff --git a/tui/src/components.rs b/tui/src/components.rs index 78b9e74..5d7d291 100644 --- a/tui/src/components.rs +++ b/tui/src/components.rs @@ -12,7 +12,35 @@ use ratatui::{layout::Rect, Frame}; use reqtui::command::Command; use tokio::sync::mpsc::UnboundedSender; +/// A `Page` is anything that is a top level page and can be drawn to the screen +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 + /// by the top level event loop whenever a resize event is produced + #[allow(unused_variables)] + fn resize(&mut self, new_size: Rect); + + /// 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<()> { + 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 + fn handle_tick(&mut self) -> anyhow::Result<()> { + Ok(()) + } +} + +/// An `Eventful` component is a component that can handle key events, and mouse events +/// when support for them gets added. pub trait Eventful { + /// 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)?, @@ -22,24 +50,9 @@ pub trait Eventful { Ok(action) } + /// 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> { Ok(None) } } - -pub trait Component { - fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()>; - - #[allow(unused_variables)] - fn resize(&mut self, new_size: Rect) {} - - #[allow(unused_variables)] - fn register_command_handler(&mut self, sender: UnboundedSender) -> anyhow::Result<()> { - Ok(()) - } - - fn handle_tick(&mut self) -> anyhow::Result<()> { - Ok(()) - } -} diff --git a/tui/src/components/api_explorer/api_explorer.rs b/tui/src/components/api_explorer/api_explorer.rs index 5f9fd00..fb4f7ae 100644 --- a/tui/src/components/api_explorer/api_explorer.rs +++ b/tui/src/components/api_explorer/api_explorer.rs @@ -7,7 +7,7 @@ use crate::components::{ }, input::Input, overlay::draw_overlay, - Component, Eventful, + Eventful, Page, }; use anyhow::Context; use config::EditorMode; @@ -121,7 +121,7 @@ pub enum CreateReqKind { } #[derive(Debug)] -pub struct ApiExplorer<'ae> { +pub struct CollectionViewer<'ae> { schema: Schema, colors: &'ae colors::Colors, config: &'ae config::Config, @@ -153,7 +153,7 @@ pub struct ApiExplorer<'ae> { responses_map: HashMap>>, } -impl<'ae> ApiExplorer<'ae> { +impl<'ae> CollectionViewer<'ae> { pub fn new( size: Rect, schema: Schema, @@ -179,7 +179,7 @@ impl<'ae> ApiExplorer<'ae> { let (request_tx, response_rx) = unbounded_channel::(); - ApiExplorer { + CollectionViewer { schema, focused_pane: PaneFocus::ReqUri, selected_pane: None, @@ -825,7 +825,7 @@ impl<'ae> ApiExplorer<'ae> { } } -impl Component for ApiExplorer<'_> { +impl Page for CollectionViewer<'_> { #[tracing::instrument(skip_all, target = "api_explorer")] fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { self.draw_background(size, frame); @@ -897,7 +897,7 @@ impl Component for ApiExplorer<'_> { } } -impl Eventful for ApiExplorer<'_> { +impl Eventful for CollectionViewer<'_> { fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { if self.curr_overlay.ne(&Overlays::None) { match self.curr_overlay { diff --git a/tui/src/components/api_explorer/mod.rs b/tui/src/components/api_explorer/mod.rs index 332f75c..6dc0c10 100644 --- a/tui/src/components/api_explorer/mod.rs +++ b/tui/src/components/api_explorer/mod.rs @@ -5,4 +5,4 @@ mod req_uri; mod res_viewer; mod sidebar; -pub use api_explorer::ApiExplorer; +pub use api_explorer::CollectionViewer; diff --git a/tui/src/components/api_explorer/req_editor.rs b/tui/src/components/api_explorer/req_editor.rs index 3e4df08..fef273e 100644 --- a/tui/src/components/api_explorer/req_editor.rs +++ b/tui/src/components/api_explorer/req_editor.rs @@ -1,4 +1,4 @@ -use crate::{components::Eventful, utils::build_styled_content}; +use crate::{components::Eventful, utils::build_syntax_highlighted_lines}; use config::{Action, EditorMode, KeyAction}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::{ @@ -122,7 +122,7 @@ impl<'re> ReqEditor<'re> { }; let content = body.to_string(); - let styled_display = build_styled_content(&content, tree.as_ref(), colors); + let styled_display = build_syntax_highlighted_lines(&content, tree.as_ref(), colors); Self { colors, @@ -670,8 +670,11 @@ impl Eventful for ReqEditor<'_> { } self.tree = HIGHLIGHTER.write().unwrap().parse(&self.body.to_string()); - self.styled_display = - build_styled_content(&self.body.to_string(), self.tree.as_ref(), self.colors); + self.styled_display = build_syntax_highlighted_lines( + &self.body.to_string(), + self.tree.as_ref(), + self.colors, + ); return Ok(None); } @@ -706,7 +709,7 @@ impl Eventful for ReqEditor<'_> { self.tree = HIGHLIGHTER.write().unwrap().parse(&self.body.to_string()); self.styled_display = - build_styled_content(&self.body.to_string(), self.tree.as_ref(), self.colors); + build_syntax_highlighted_lines(&self.body.to_string(), self.tree.as_ref(), self.colors); Ok(None) } diff --git a/tui/src/components/api_explorer/res_viewer.rs b/tui/src/components/api_explorer/res_viewer.rs index c6a3e18..a631292 100644 --- a/tui/src/components/api_explorer/res_viewer.rs +++ b/tui/src/components/api_explorer/res_viewer.rs @@ -1,6 +1,6 @@ use reqtui::{net::request_manager::ReqtuiResponse, syntax::highlighter::HIGHLIGHTER}; -use crate::utils::build_styled_content; +use crate::utils::build_syntax_highlighted_lines; use ratatui::{ buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, @@ -119,7 +119,8 @@ impl<'a> ResViewer<'a> { if let Some(ref res) = response { let pretty_body = res.borrow().pretty_body.to_string(); - self.lines = build_styled_content(&pretty_body, self.tree.as_ref(), self.colors); + self.lines = + build_syntax_highlighted_lines(&pretty_body, self.tree.as_ref(), self.colors); } self.response = response; diff --git a/tui/src/components/dashboard/dashboard.rs b/tui/src/components/dashboard/dashboard.rs index e1f2d61..c28c66d 100644 --- a/tui/src/components/dashboard/dashboard.rs +++ b/tui/src/components/dashboard/dashboard.rs @@ -6,7 +6,7 @@ use crate::components::{ }, error_popup::ErrorPopup, overlay::draw_overlay, - Component, Eventful, + Eventful, Page, }; use reqtui::{command::Command, schema::types::Schema}; @@ -34,7 +34,7 @@ struct DashboardLayout { } #[derive(Debug)] -pub struct Dashboard<'a> { +pub struct CollectionList<'a> { layout: DashboardLayout, schemas: Vec, @@ -58,7 +58,7 @@ enum PaneFocus { Filter, } -impl<'a> Dashboard<'a> { +impl<'a> CollectionList<'a> { pub fn new( size: Rect, colors: &'a colors::Colors, @@ -67,7 +67,7 @@ impl<'a> Dashboard<'a> { let mut list_state = SchemaListState::new(schemas.clone()); schemas.is_empty().not().then(|| list_state.select(Some(0))); - Ok(Dashboard { + Ok(CollectionList { list_state, form_state: FormState::default(), colors, @@ -503,7 +503,7 @@ impl<'a> Dashboard<'a> { } } -impl Component for Dashboard<'_> { +impl Page for CollectionList<'_> { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { self.draw_background(size, frame); self.draw_title(frame)?; @@ -537,7 +537,7 @@ impl Component for Dashboard<'_> { } } -impl Eventful for Dashboard<'_> { +impl Eventful for CollectionList<'_> { 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)); @@ -643,7 +643,7 @@ mod tests { (tmp_data_dir, tmp_dir.to_string_lossy().to_string()) } - fn feed_keys(dashboard: &mut Dashboard, events: &[KeyEvent]) { + fn feed_keys(dashboard: &mut CollectionList, events: &[KeyEvent]) { for event in events { _ = dashboard.handle_key_event(*event); } @@ -674,7 +674,7 @@ mod tests { let (_guard, path) = setup_temp_schemas(1); let schemas = schema::schema::get_schemas(path).unwrap(); - let mut dashboard = Dashboard::new(size, &colors, schemas).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, schemas).unwrap(); assert_eq!(dashboard.schemas.len(), 1); assert_eq!(dashboard.list_state.selected(), Some(0)); @@ -700,7 +700,7 @@ mod tests { fn test_actions_without_any_schemas() { let size = Rect::new(0, 0, 80, 24); let colors = colors::Colors::default(); - let mut dashboard = Dashboard::new(size, &colors, vec![]).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, vec![]).unwrap(); assert!(dashboard.schemas.is_empty()); assert_eq!(dashboard.list_state.selected(), None); @@ -727,7 +727,7 @@ mod tests { let (_guard, path) = setup_temp_schemas(10); let schemas = schema::schema::get_schemas(path).unwrap(); - let mut dashboard = Dashboard::new(size, &colors, schemas).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, schemas).unwrap(); assert_eq!(dashboard.schemas.len(), 10); assert_eq!(dashboard.list_state.selected(), Some(0)); @@ -795,7 +795,7 @@ mod tests { let (_guard, path) = setup_temp_schemas(3); let schemas = schema::schema::get_schemas(path).unwrap(); - let mut dashboard = Dashboard::new(size, &colors, schemas).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, schemas).unwrap(); feed_keys( &mut dashboard, @@ -833,7 +833,7 @@ mod tests { let (_guard, path) = setup_temp_schemas(3); let schemas = schema::schema::get_schemas(path).unwrap(); - let mut dashboard = Dashboard::new(size, &colors, schemas).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, schemas).unwrap(); feed_keys( &mut dashboard, @@ -909,7 +909,7 @@ mod tests { let colors = colors::Colors::default(); let (_guard, path) = setup_temp_schemas(3); let schemas = schema::schema::get_schemas(path).unwrap(); - let mut dashboard = Dashboard::new(size, &colors, schemas).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, schemas).unwrap(); feed_keys( &mut dashboard, @@ -930,7 +930,7 @@ mod tests { fn test_display_error() { let size = Rect::new(0, 0, 80, 24); let colors = colors::Colors::default(); - let mut dashboard = Dashboard::new(size, &colors, vec![]).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, vec![]).unwrap(); dashboard.display_error("any error message".into()); @@ -942,7 +942,7 @@ mod tests { fn test_draw_background() { let colors = colors::Colors::default(); let size = Rect::new(0, 0, 80, 22); - let dashboard = Dashboard::new(size, &colors, vec![]).unwrap(); + let dashboard = CollectionList::new(size, &colors, vec![]).unwrap(); let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); let mut frame = terminal.get_frame(); @@ -964,7 +964,7 @@ mod tests { let size = Rect::new(0, 0, 80, 22); let (_guard, path) = setup_temp_schemas(3); let schemas = schema::schema::get_schemas(path).unwrap(); - let mut dashboard = Dashboard::new(size, &colors, schemas).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, schemas).unwrap(); dashboard.display_error("any_error_message".into()); assert_eq!(dashboard.pane_focus, PaneFocus::Error); @@ -983,7 +983,7 @@ mod tests { let new_size = Rect::new(0, 0, 80, 24); let (_guard, path) = setup_temp_schemas(3); let schemas = schema::schema::get_schemas(path).unwrap(); - let mut dashboard = Dashboard::new(size, &colors, schemas).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, schemas).unwrap(); let expected = DashboardLayout { schemas_pane: Rect::new(1, 6, 79, 17), hint_pane: Rect::new(1, 23, 79, 1), diff --git a/tui/src/components/dashboard/mod.rs b/tui/src/components/dashboard/mod.rs index fea1658..9fcf926 100644 --- a/tui/src/components/dashboard/mod.rs +++ b/tui/src/components/dashboard/mod.rs @@ -3,4 +3,4 @@ mod dashboard; mod new_collection_form; mod schema_list; -pub use dashboard::Dashboard; +pub use dashboard::CollectionList; diff --git a/tui/src/components/input.rs b/tui/src/components/input.rs index cfc6bc0..ad6ec58 100644 --- a/tui/src/components/input.rs +++ b/tui/src/components/input.rs @@ -5,6 +5,8 @@ use ratatui::{ widgets::{Block, Borders, Paragraph, StatefulWidget, Widget}, }; +/// input component used in forms and everywhere else that the user can +/// input text to a single, named field pub struct Input<'a> { colors: &'a colors::Colors, focused: bool, diff --git a/tui/src/components/overlay.rs b/tui/src/components/overlay.rs index 4c16c09..bd108c2 100644 --- a/tui/src/components/overlay.rs +++ b/tui/src/components/overlay.rs @@ -1,5 +1,7 @@ use ratatui::{layout::Rect, style::Stylize, text::Line, widgets::Paragraph, Frame}; +/// draws a fullscreen overlay with the given fill text, many pages uses this to display +/// "floating" information pub fn draw_overlay(colors: &colors::Colors, size: Rect, fill_text: &str, frame: &mut Frame) { let lines: Vec> = vec![fill_text.repeat(size.width.into()).into(); size.height.into()]; diff --git a/tui/src/components/terminal_too_small.rs b/tui/src/components/terminal_too_small.rs index d3a80cb..28bc294 100644 --- a/tui/src/components/terminal_too_small.rs +++ b/tui/src/components/terminal_too_small.rs @@ -1,4 +1,4 @@ -use crate::components::Component; +use crate::components::Page; use ratatui::{ layout::{Alignment, Constraint, Direction, Flex, Layout, Rect}, style::Stylize, @@ -7,19 +7,21 @@ use ratatui::{ Frame, }; -pub struct TerminalTooSmall<'a> { - colors: &'a colors::Colors, +/// `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 +pub struct TerminalTooSmall<'tts> { + colors: &'tts colors::Colors, } -pub struct TerminalTooSmallLayout {} - impl<'a> TerminalTooSmall<'a> { pub fn new(colors: &'a colors::Colors) -> Self { TerminalTooSmall { colors } } } -impl Component for TerminalTooSmall<'_> { +impl Page for TerminalTooSmall<'_> { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { let layout = build_layout(size); @@ -43,6 +45,10 @@ impl Component for TerminalTooSmall<'_> { Ok(()) } + + // 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 build_layout(size: Rect) -> Rect { diff --git a/tui/src/event_pool.rs b/tui/src/event_pool.rs index 7ade13f..0c035e7 100644 --- a/tui/src/event_pool.rs +++ b/tui/src/event_pool.rs @@ -2,23 +2,21 @@ use crossterm::event::{Event as CrosstermEvent, KeyEventKind}; use futures::{FutureExt, StreamExt}; use ratatui::layout::Rect; use std::ops::Div; -use tokio::task::JoinHandle; #[derive(Debug, Clone, PartialEq)] pub enum Event { Key(crossterm::event::KeyEvent), - Mouse(crossterm::event::MouseEvent), Resize(Rect), - Init, Tick, Render, } +/// Core component responsible for pooling events from crossterm and sending +/// them over to be handled #[derive(Debug)] pub struct EventPool { event_rx: tokio::sync::mpsc::UnboundedReceiver, event_tx: tokio::sync::mpsc::UnboundedSender, - task: JoinHandle<()>, frame_rate: f64, tick_rate: f64, } @@ -26,12 +24,10 @@ pub struct EventPool { impl EventPool { pub fn new(frame_rate: f64, tick_rate: f64) -> Self { let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel(); - let task = tokio::spawn(async {}); EventPool { event_rx, event_tx, - task, frame_rate, tick_rate, } @@ -43,15 +39,11 @@ impl EventPool { let tick_delay = std::time::Duration::from_secs_f64(1.0.div(self.tick_rate)); let event_tx = self.event_tx.clone(); - self.task = tokio::spawn(async move { + 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); - event_tx - .send(Event::Init) - .expect("failed to send event through channel"); - loop { let render_delay = render_interval.tick(); let tick_delay = tick_interval.tick(); @@ -68,16 +60,14 @@ impl EventPool { Some(Ok(CrosstermEvent::Resize(width, height))) => event_tx .send(Event::Resize(Rect::new(0, 0, width, height))) .expect("failed to send event through channel"), - Some(Err(_)) => {} - Some(_) => {} - None => {} + _ => {} } } _ = tick_delay => { - event_tx.send(Event::Tick).unwrap(); + event_tx.send(Event::Tick).expect("failed to send event through channel"); }, _ = render_delay => { - event_tx.send(Event::Render).unwrap(); + event_tx.send(Event::Render).expect("failed to send event through channel"); }, } } diff --git a/tui/src/screen_manager.rs b/tui/src/screen_manager.rs index f449b82..f4722fd 100644 --- a/tui/src/screen_manager.rs +++ b/tui/src/screen_manager.rs @@ -1,32 +1,42 @@ use crate::{ components::{ - api_explorer::ApiExplorer, dashboard::Dashboard, terminal_too_small::TerminalTooSmall, - Component, Eventful, + api_explorer::CollectionViewer, dashboard::CollectionList, + terminal_too_small::TerminalTooSmall, Eventful, Page, }, event_pool::Event, }; use reqtui::{command::Command, schema::Schema}; -use anyhow::Context; use ratatui::{layout::Rect, Frame}; use tokio::sync::mpsc::UnboundedSender; #[derive(Debug, Clone, PartialEq)] pub enum Screens { - Editor, - Dashboard, + CollectionList, + 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 ScreenManager<'sm> { - curr_screen: Screens, - api_explorer: Option>, - dashboard: Dashboard<'sm>, terminal_too_small: TerminalTooSmall<'sm>, + collection_list: CollectionList<'sm>, + /// CollectionViewer is a option as we need a selected schema 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 colors::Colors, config: &'sm config::Config, + + // we hold a copy of the sender so we can pass it to the editor when we first + // build one sender: Option>, } @@ -38,11 +48,11 @@ impl<'sm> ScreenManager<'sm> { config: &'sm config::Config, ) -> anyhow::Result { Ok(Self { - curr_screen: Screens::Dashboard, - prev_screen: Screens::Dashboard, - api_explorer: None, + curr_screen: Screens::CollectionList, + prev_screen: Screens::CollectionList, + collection_viewer: None, terminal_too_small: TerminalTooSmall::new(colors), - dashboard: Dashboard::new(size, colors, schemas)?, + collection_list: CollectionList::new(size, colors, schemas)?, size, colors, config, @@ -51,62 +61,63 @@ impl<'sm> ScreenManager<'sm> { } fn restore_screen(&mut self) { - if self.curr_screen.ne(&Screens::TerminalTooSmall) { - return; - } - - let temp = self.curr_screen.clone(); - self.curr_screen = self.prev_screen.clone(); - self.prev_screen = temp; + std::mem::swap(&mut self.curr_screen, &mut self.prev_screen); } fn switch_screen(&mut self, screen: Screens) { if self.curr_screen == screen { return; } - - self.prev_screen = self.curr_screen.clone(); + std::mem::swap(&mut self.curr_screen, &mut self.prev_screen); self.curr_screen = 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::SelectSchema(schema) | Command::CreateSchema(schema) => { tracing::debug!("changing to api explorer: {}", schema.info.name); - self.switch_screen(Screens::Editor); - let mut api_explorer = - ApiExplorer::new(self.size, schema, self.colors, self.config); - _ = api_explorer.register_command_handler( - self.sender - .as_ref() - .expect("should have a command sender at this point") - .clone(), - ); - self.api_explorer = Some(api_explorer); + self.switch_screen(Screens::CollectionViewer); + let mut collection_viewer = + CollectionViewer::new(self.size, schema, self.colors, self.config); + collection_viewer + .register_command_handler( + self.sender + .as_ref() + .expect("attempted to register the sender on collection_viewer but it was None") + .clone(), + ) + .ok(); + self.collection_viewer = Some(collection_viewer); } Command::Error(msg) => { - self.dashboard.display_error(msg); + self.collection_list.display_error(msg); } _ => {} } } } -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), (_, true) => self.switch_screen(Screens::TerminalTooSmall), - (false, false) => self.restore_screen(), + (false, false) if self.curr_screen.eq(&Screens::TerminalTooSmall) => { + self.restore_screen() + } + _ => {} } match &self.curr_screen { - Screens::Editor => self - .api_explorer + Screens::CollectionViewer => self + .collection_viewer .as_mut() - .context("should never be able to switch to editor screen without having a schema")? + .expect("should never be able to switch to editor screen without having a schema") .draw(frame, frame.size())?, - Screens::Dashboard => self.dashboard.draw(frame, frame.size())?, + Screens::CollectionList => self.collection_list.draw(frame, frame.size())?, Screens::TerminalTooSmall => self.terminal_too_small.draw(frame, frame.size())?, }; @@ -115,15 +126,16 @@ impl Component for ScreenManager<'_> { fn register_command_handler(&mut self, sender: UnboundedSender) -> anyhow::Result<()> { self.sender = Some(sender.clone()); - self.dashboard.register_command_handler(sender.clone())?; + self.collection_list + .register_command_handler(sender.clone())?; Ok(()) } fn resize(&mut self, new_size: Rect) { self.size = new_size; - self.dashboard.resize(new_size); + self.collection_list.resize(new_size); - if let Some(e) = self.api_explorer.as_mut() { + if let Some(e) = self.collection_viewer.as_mut() { e.resize(new_size) } } @@ -131,8 +143,8 @@ impl Component for ScreenManager<'_> { fn handle_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::Editor = &self.curr_screen { - self.api_explorer + if let Screens::CollectionViewer = &self.curr_screen { + self.collection_viewer .as_mut() .expect("we are displaying the editor without having one") .handle_tick()? @@ -145,12 +157,12 @@ impl Component for ScreenManager<'_> { impl Eventful for ScreenManager<'_> { fn handle_event(&mut self, event: Option) -> anyhow::Result> { match self.curr_screen { - Screens::Editor => self - .api_explorer + Screens::CollectionViewer => self + .collection_viewer .as_mut() .expect("should never be able to switch to editor screen without having a schema") .handle_event(event), - Screens::Dashboard => self.dashboard.handle_event(event), + Screens::CollectionList => self.collection_list.handle_event(event), Screens::TerminalTooSmall => Ok(None), } } @@ -222,11 +234,11 @@ mod tests { 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::Dashboard); + assert_eq!(sm.prev_screen, Screens::CollectionList); terminal.resize(enough).unwrap(); sm.draw(&mut terminal.get_frame(), enough).unwrap(); - assert_eq!(sm.curr_screen, Screens::Dashboard); + assert_eq!(sm.curr_screen, Screens::CollectionList); assert_eq!(sm.prev_screen, Screens::TerminalTooSmall); } @@ -264,10 +276,10 @@ mod tests { let (tx, _) = tokio::sync::mpsc::unbounded_channel::(); let mut sm = ScreenManager::new(initial, &colors, schemas, &config).unwrap(); _ = sm.register_command_handler(tx.clone()); - assert_eq!(sm.curr_screen, Screens::Dashboard); + assert_eq!(sm.curr_screen, Screens::CollectionList); sm.handle_command(command); - assert_eq!(sm.curr_screen, Screens::Editor); + assert_eq!(sm.curr_screen, Screens::CollectionViewer); } #[test] @@ -283,7 +295,7 @@ mod tests { sm.register_command_handler(tx.clone()).unwrap(); - assert!(sm.dashboard.command_sender.is_some()); + assert!(sm.collection_list.command_sender.is_some()); } #[test] diff --git a/tui/src/utils.rs b/tui/src/utils.rs index d9e9da2..5390a0d 100644 --- a/tui/src/utils.rs +++ b/tui/src/utils.rs @@ -11,22 +11,34 @@ fn is_endline(c: char) -> bool { matches!(c, '\n' | '\r') } -pub fn build_styled_content( +/// Builds a vector of `Lines` to be rendered with syntax highlight from treesitter +pub fn build_syntax_highlighted_lines( content: &str, tree: Option<&Tree>, colors: &colors::Colors, ) -> Vec> { + // we collect every line into this vector, and return it at the end + let mut styled_lines: Vec = vec![]; + + // `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 styled_lines: Vec = vec![]; + // these are helper variables to collect each line into styled spans based on the + // token it contains let mut current_line: Vec = vec![]; + // we collect tokens on each line into a string, and when we reach a whitespace, we + // convert this string into a styled span let mut current_token = String::default(); + // current capture holds the next token on the queue of tokens that treesitter produced + // we use this to check if we are on a token and thus should style it accordingly let mut current_capture = highlights.pop_front(); // when handling CRLF line endings, we skip the second 'newline' to prevent an empty line + // to be rendered to the terminal let mut skip_next = false; for (i, c) in content.chars().enumerate() { @@ -40,12 +52,19 @@ pub fn build_styled_content( current_token.push(c); continue; } + + // we reached the start of a new capture, and we have something on the current token, + // we push it to the current line with the default style if i == capture.start && !current_token.is_empty() { current_line.push(Span::from(current_token.clone()).fg(colors.normal.white)); current_token.clear(); current_token.push(c); continue; } + + // we reached a capture end that also ends a line, common on cases like curly braces in this + // case we add our current token to the current line, and push the line into the vector + // of styled lines before continuing if i == capture.end && is_endline(c) { current_token.push(c); current_line.push(Span::styled(current_token.clone(), capture.style)); @@ -63,6 +82,8 @@ pub fn build_styled_content( continue; } + // we reached the end of a capture, which means we have to push our current token + // to the current line before continuing if i == capture.end { current_line.push(Span::styled(current_token.clone(), capture.style)); current_token.clear(); @@ -91,6 +112,9 @@ pub fn build_styled_content( continue; } + // when we end iterating over all the captures, we might still have tokens to collect when + // they are not valid or tree sitter couldn't parse them due to previous errors or any + // other possible occurrence, so we finish collecting all the tokens if !current_token.is_empty() && !is_endline(c) { current_line.push(Span::from(current_token.clone()).fg(colors.normal.white)); current_token.clear(); diff --git a/tui/tests/dashboard.rs b/tui/tests/dashboard.rs index 05b4860..6c51e96 100644 --- a/tui/tests/dashboard.rs +++ b/tui/tests/dashboard.rs @@ -6,7 +6,7 @@ use std::{ io::Write, }; use tempfile::{tempdir, TempDir}; -use tui::components::{dashboard::Dashboard, Component, Eventful}; +use tui::components::{dashboard::CollectionList, Eventful, Page}; fn setup_temp_schemas(amount: usize) -> (TempDir, String) { let tmp_data_dir = tempdir().expect("Failed to create temp data dir"); @@ -30,7 +30,7 @@ fn setup_temp_schemas(amount: usize) -> (TempDir, String) { (tmp_data_dir, tmp_dir.to_string_lossy().to_string()) } -fn feed_keys(dashboard: &mut Dashboard, events: &[KeyEvent]) { +fn feed_keys(dashboard: &mut CollectionList, events: &[KeyEvent]) { for event in events { _ = dashboard.handle_key_event(*event); } @@ -49,7 +49,7 @@ fn get_rendered_from_buffer(frame: &mut Frame, size: Rect) -> Vec { fn test_draw_empty_message() { let colors = colors::Colors::default(); let size = Rect::new(0, 0, 80, 22); - let mut dashboard = Dashboard::new(size, &colors, vec![]).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, vec![]).unwrap(); let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); let mut frame = terminal.get_frame(); @@ -80,7 +80,7 @@ fn test_draw_no_matches_message() { let size = Rect::new(0, 0, 80, 22); let (_guard, path) = setup_temp_schemas(3); let schemas = schema::schema::get_schemas(path).unwrap(); - let mut dashboard = Dashboard::new(size, &colors, schemas).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, schemas).unwrap(); let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); let mut frame = terminal.get_frame(); @@ -130,7 +130,7 @@ fn draw_hint_text() { let size = Rect::new(0, 0, 80, 22); let (_guard, path) = setup_temp_schemas(3); let schemas = schema::schema::get_schemas(path).unwrap(); - let mut dashboard = Dashboard::new(size, &colors, schemas).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, schemas).unwrap(); let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); let mut frame = terminal.get_frame(); @@ -150,7 +150,7 @@ fn draw_filter_prompt() { let size = Rect::new(0, 0, 80, 22); let (_guard, path) = setup_temp_schemas(3); let schemas = schema::schema::get_schemas(path).unwrap(); - let mut dashboard = Dashboard::new(size, &colors, schemas).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, schemas).unwrap(); let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); let mut frame = terminal.get_frame(); let expected = @@ -184,7 +184,7 @@ fn draw_filter_prompt() { fn test_draw_title() { let colors = colors::Colors::default(); let size = Rect::new(0, 0, 80, 22); - let mut dashboard = Dashboard::new(size, &colors, vec![]).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, vec![]).unwrap(); let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); let mut frame = terminal.get_frame(); @@ -213,7 +213,7 @@ fn test_draw_title() { fn test_draw_error() { let colors = colors::Colors::default(); let size = Rect::new(0, 0, 80, 22); - let mut dashboard = Dashboard::new(size, &colors, vec![]).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, vec![]).unwrap(); let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); let mut frame = terminal.get_frame(); @@ -259,7 +259,7 @@ fn test_draw_error() { fn test_draw_help() { let colors = colors::Colors::default(); let size = Rect::new(0, 0, 80, 22); - let mut dashboard = Dashboard::new(size, &colors, vec![]).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, vec![]).unwrap(); let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); let mut frame = terminal.get_frame(); @@ -308,7 +308,7 @@ fn test_draw_help() { fn test_draw_form_popup() { let colors = colors::Colors::default(); let size = Rect::new(0, 0, 80, 22); - let mut dashboard = Dashboard::new(size, &colors, vec![]).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, vec![]).unwrap(); let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); let mut frame = terminal.get_frame(); @@ -361,7 +361,7 @@ fn test_draw_delete_prompt() { let size = Rect::new(0, 0, 80, 22); let (_guard, path) = setup_temp_schemas(3); let schemas = schema::schema::get_schemas(path).unwrap(); - let mut dashboard = Dashboard::new(size, &colors, schemas).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, schemas).unwrap(); let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); let mut frame = terminal.get_frame(); @@ -414,7 +414,7 @@ fn test_draw_schema_list() { let size = Rect::new(0, 0, 80, 22); let (_guard, path) = setup_temp_schemas(3); let schemas = schema::schema::get_schemas(path).unwrap(); - let mut dashboard = Dashboard::new(size, &colors, schemas).unwrap(); + let mut dashboard = CollectionList::new(size, &colors, schemas).unwrap(); let mut terminal = Terminal::new(TestBackend::new(80, 22)).unwrap(); let mut frame = terminal.get_frame();