diff --git a/hac-client/benches/collection_viewer_bench.rs b/hac-client/benches/collection_viewer_bench.rs index 90524f0..df88996 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, Eventful, Page}, + pages::{collection_viewer::CollectionViewer, Component, Eventful}, utils::build_syntax_highlighted_lines, }; use hac_core::{ diff --git a/hac-client/src/app.rs b/hac-client/src/app.rs index 706a2ca..01fd3e1 100644 --- a/hac-client/src/app.rs +++ b/hac-client/src/app.rs @@ -1,6 +1,6 @@ use crate::{ event_pool::{Event, EventPool}, - pages::{Eventful, Page}, + pages::{Component, Eventful}, screen_manager::ScreenManager, }; use hac_core::{collection::Collection, command::Command}; diff --git a/hac-client/src/pages.rs b/hac-client/src/pages.rs index 0168311..54385c5 100644 --- a/hac-client/src/pages.rs +++ b/hac-client/src/pages.rs @@ -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 Page { +pub trait Component { 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 diff --git a/hac-client/src/pages/collection_dashboard/collection_dashboard.rs b/hac-client/src/pages/collection_dashboard/collection_dashboard.rs index 1209016..15962bd 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, - Eventful, Page, + Component, Eventful, }; use hac_core::{collection::types::Collection, command::Command}; @@ -520,7 +520,7 @@ impl<'a> CollectionDashboard<'a> { } } -impl Page for CollectionDashboard<'_> { +impl Component for CollectionDashboard<'_> { fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { self.draw_background(size, frame); self.draw_title(frame)?; diff --git a/hac-client/src/pages/collection_viewer/collection_viewer.rs b/hac-client/src/pages/collection_viewer/collection_viewer.rs index 61bef39..4fbd4b8 100644 --- a/hac-client/src/pages/collection_viewer/collection_viewer.rs +++ b/hac-client/src/pages/collection_viewer/collection_viewer.rs @@ -1,35 +1,28 @@ -use crate::pages::{ - collection_viewer::{ - req_editor::{ReqEditor, ReqEditorState}, - req_uri::{ReqUri, ReqUriState}, - res_viewer::{ResViewer, ResViewerState, ResViewerTabs}, - sidebar::{Sidebar, SidebarState}, - }, - input::Input, - overlay::draw_overlay, - Eventful, Page, -}; +use hac_core::collection::types::*; +use hac_core::command::Command; +use hac_core::net::request_manager::Response; + +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, SidebarState}; +use crate::pages::input::Input; +use crate::pages::overlay::draw_overlay; +use crate::pages::{Component, Eventful}; + +use std::cell::RefCell; +use std::collections::HashMap; +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 hac_core::{ - collection::types::{BodyType, Collection, Directory, Request, RequestKind, RequestMethod}, - command::Command, - net::request_manager::Response, -}; -use ratatui::{ - layout::{Constraint, Direction, Layout, Rect}, - style::{Style, Stylize}, - widgets::{Block, Borders, Clear, Padding, Paragraph, StatefulWidget}, - Frame, -}; -use std::{ - cell::RefCell, - collections::HashMap, - ops::{Add, Div, Sub}, - rc::Rc, - sync::{Arc, RwLock}, -}; +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}; #[derive(Debug, PartialEq)] @@ -56,7 +49,7 @@ enum VisitNode { } #[derive(Debug, PartialEq)] -enum PaneFocus { +pub enum PaneFocus { Sidebar, ReqUri, Preview, @@ -123,36 +116,32 @@ pub enum CreateReqKind { #[derive(Debug)] pub struct CollectionViewer<'cv> { - collection: Collection, + response_viewer: ResViewer<'cv>, + request_editor: ReqEditor<'cv>, + colors: &'cv hac_colors::Colors, config: &'cv hac_config::Config, layout: ExplorerLayout, - response_rx: UnboundedReceiver, - request_tx: UnboundedSender, + global_command_sender: Option>, + collection: Collection, + collection_sync_timer: std::time::Instant, + selected_request: Option>>, hovered_request: Option, dirs_expanded: HashMap, + focused_pane: PaneFocus, selected_pane: Option, - res_viewer: ResViewer<'cv>, - preview_tab: ResViewerTabs, - raw_preview_scroll: usize, - preview_header_scroll_y: usize, - preview_header_scroll_x: usize, - pretty_preview_scroll: usize, - curr_overlay: Overlays, create_req_form_state: CreateReqFormState, - sync_interval: std::time::Instant, - editor: ReqEditor<'cv>, + has_pending_request: bool, + responses_map: HashMap>>, + response_rx: UnboundedReceiver, + request_tx: UnboundedSender, - sender: Option>, - pending_request: bool, dry_run: bool, - - responses_map: HashMap>>, } impl<'cv> CollectionViewer<'cv> { @@ -164,6 +153,7 @@ impl<'cv> CollectionViewer<'cv> { dry_run: bool, ) -> Self { 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| { @@ -180,55 +170,43 @@ impl<'cv> CollectionViewer<'cv> { .as_ref() .and_then(|requests| requests.first().cloned()); - let (request_tx, response_rx) = unbounded_channel::(); - CollectionViewer { - collection, - focused_pane: PaneFocus::Sidebar, - selected_pane: None, - colors, - config, - - editor: ReqEditor::new( + request_editor: ReqEditor::new( colors, + config, selected_request.as_ref().cloned(), layout.req_editor, - config, ), - res_viewer: ResViewer::new(colors, None, layout.response_preview), + response_viewer: ResViewer::new(colors, None, layout.response_preview), + + colors, + config, + layout, + global_command_sender: None, + collection, + collection_sync_timer: std::time::Instant::now(), - hovered_request, selected_request, + hovered_request, dirs_expanded: HashMap::default(), - responses_map: HashMap::default(), - - sender: None, - pending_request: false, - preview_tab: ResViewerTabs::Preview, - raw_preview_scroll: 0, - preview_header_scroll_y: 0, - preview_header_scroll_x: 0, - pretty_preview_scroll: 0, + focused_pane: PaneFocus::Sidebar, + selected_pane: None, curr_overlay: Overlays::None, create_req_form_state: CreateReqFormState::default(), - sync_interval: std::time::Instant::now(), + has_pending_request: false, + responses_map: HashMap::default(), response_rx, request_tx, - layout, + dry_run, } } #[tracing::instrument(skip_all, err)] fn handle_sidebar_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - self.before_quit(); - return Ok(Some(Command::Quit)); - }; - if let KeyCode::Esc = key_event.code { self.selected_pane = None; return Ok(None); @@ -255,12 +233,12 @@ impl<'cv> CollectionViewer<'cv> { } RequestKind::Single(req) => { self.selected_request = Some(Arc::clone(req)); - self.res_viewer.update(None); - self.editor = ReqEditor::new( + self.response_viewer.update(None); + self.request_editor = ReqEditor::new( self.colors, + self.config, self.selected_request.clone(), self.layout.req_editor, - self.config, ); } } @@ -305,11 +283,6 @@ impl<'cv> CollectionViewer<'cv> { } fn handle_req_uri_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - self.before_quit(); - return Ok(Some(Command::Quit)); - }; - if let KeyCode::Esc = key_event.code { self.selected_pane = None; return Ok(None); @@ -340,9 +313,10 @@ impl<'cv> CollectionViewer<'cv> { if self .selected_request .as_ref() - .is_some_and(|_| !self.pending_request) + .is_some_and(|_| !self.has_pending_request) { - self.pending_request = true; + self.has_pending_request = true; + self.response_viewer.set_pending_request(true); hac_core::net::handle_request( self.selected_request.as_ref().unwrap(), self.request_tx.clone(), @@ -361,7 +335,7 @@ impl<'cv> CollectionViewer<'cv> { self.selected_pane = Some(PaneFocus::Editor); return Ok(None); } - (KeyCode::Esc, Some(_)) if self.editor.mode().eq(&EditorMode::Normal) => { + (KeyCode::Esc, Some(_)) if self.request_editor.mode().eq(&EditorMode::Normal) => { self.selected_pane = None; return Ok(None); } @@ -370,12 +344,7 @@ impl<'cv> CollectionViewer<'cv> { if key_event.code.eq(&KeyCode::Enter) && self.selected_pane.is_none() { return Ok(None); } - self.editor.handle_key_event(key_event) - } - - 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); + self.request_editor.handle_key_event(key_event) } fn draw_sidebar(&mut self, frame: &mut Frame) { @@ -404,29 +373,6 @@ impl<'cv> CollectionViewer<'cv> { ReqUri::new(self.colors).render(self.layout.req_uri, frame.buffer_mut(), &mut state); } - fn draw_res_viewer(&mut self, frame: &mut Frame) { - let mut state = ResViewerState { - is_focused: self.focused_pane.eq(&PaneFocus::Preview), - is_selected: self - .selected_pane - .as_ref() - .map(|sel| sel.eq(&PaneFocus::Preview)) - .unwrap_or(false), - curr_tab: &self.preview_tab, - raw_scroll: &mut self.raw_preview_scroll, - pretty_scroll: &mut self.pretty_preview_scroll, - headers_scroll_y: &mut self.preview_header_scroll_y, - headers_scroll_x: &mut self.preview_header_scroll_x, - pending_request: self.pending_request, - }; - - frame.render_stateful_widget( - self.res_viewer.clone(), - self.layout.response_preview, - &mut state, - ) - } - fn draw_req_editor(&mut self, frame: &mut Frame) { let mut state = ReqEditorState::new( self.focused_pane.eq(&PaneFocus::Editor), @@ -435,78 +381,34 @@ impl<'cv> CollectionViewer<'cv> { .map(|sel| sel.eq(&PaneFocus::Editor)) .unwrap_or(false), ); - self.editor + self.request_editor .get_components(self.layout.req_editor, frame, &mut state); } - fn drain_response_rx(&mut self) { + // 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.selected_request.as_ref().and_then(|req| { self.responses_map .insert(req.read().unwrap().id.to_string(), Rc::clone(&res)) }); - self.res_viewer.update(Some(Rc::clone(&res))); - - self.response_rx - .is_empty() - .then(|| self.pending_request = false); + self.response_viewer.update(Some(Rc::clone(&res))); + self.response_rx.is_empty().then(|| { + self.has_pending_request = false; + self.response_viewer.set_pending_request(false); + }); } } - fn before_quit(&mut self) { - self.sync_collection_changes(); - } - fn handle_preview_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - self.before_quit(); - return Ok(Some(Command::Quit)); - }; - match key_event.code { KeyCode::Enter => self.selected_pane = Some(PaneFocus::Preview), - KeyCode::Tab => self.preview_tab = ResViewerTabs::next(&self.preview_tab), - KeyCode::Esc => self.selected_pane = None, - KeyCode::Char('0') if self.preview_tab.eq(&ResViewerTabs::Headers) => { - self.preview_header_scroll_x = 0; - } - KeyCode::Char('$') if self.preview_tab.eq(&ResViewerTabs::Headers) => { - self.preview_header_scroll_x = usize::MAX; - } - KeyCode::Char('h') => { - if let ResViewerTabs::Headers = self.preview_tab { - self.preview_header_scroll_x = self.preview_header_scroll_x.saturating_sub(1) - } - } - KeyCode::Char('j') => match self.preview_tab { - ResViewerTabs::Preview => { - self.pretty_preview_scroll = self.pretty_preview_scroll.add(1) - } - ResViewerTabs::Raw => self.raw_preview_scroll = self.raw_preview_scroll.add(1), - ResViewerTabs::Headers => { - self.preview_header_scroll_y = self.preview_header_scroll_y.add(1) - } - ResViewerTabs::Cookies => {} - }, - KeyCode::Char('k') => match self.preview_tab { - ResViewerTabs::Preview => { - self.pretty_preview_scroll = self.pretty_preview_scroll.saturating_sub(1) - } - ResViewerTabs::Raw => { - self.raw_preview_scroll = self.raw_preview_scroll.saturating_sub(1) - } - ResViewerTabs::Headers => { - self.preview_header_scroll_y = self.preview_header_scroll_y.saturating_sub(1) - } - ResViewerTabs::Cookies => {} - }, - KeyCode::Char('l') => { - if let ResViewerTabs::Headers = self.preview_tab { - self.preview_header_scroll_x = self.preview_header_scroll_x.add(1) - } + _ => { + self.response_viewer.handle_key_event(key_event)?; } - _ => {} } Ok(None) @@ -684,11 +586,6 @@ impl<'cv> CollectionViewer<'cv> { &mut self, key_event: KeyEvent, ) -> anyhow::Result> { - if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - self.before_quit(); - return Ok(Some(Command::Quit)); - }; - match ( key_event.code, key_event.modifiers, @@ -731,11 +628,6 @@ impl<'cv> CollectionViewer<'cv> { &mut self, key_event: KeyEvent, ) -> anyhow::Result> { - if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { - self.before_quit(); - return Ok(Some(Command::Quit)); - }; - match (key_event.code, &self.create_req_form_state.method) { (KeyCode::Tab, _) => { self.create_req_form_state.method = self.create_req_form_state.method.next(); @@ -860,7 +752,7 @@ impl<'cv> CollectionViewer<'cv> { fn sync_collection_changes(&mut self) { let sender = self - .sender + .global_command_sender .as_ref() .expect("should have a sender at this point") .clone(); @@ -868,7 +760,7 @@ impl<'cv> CollectionViewer<'cv> { let mut collection = self.collection.clone(); if let Some(request) = self.selected_request.as_ref() { let request = request.clone(); - let body = self.editor.body().to_string(); + 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() { @@ -900,7 +792,7 @@ impl<'cv> CollectionViewer<'cv> { }); } - self.sync_interval = std::time::Instant::now(); + self.collection_sync_timer = std::time::Instant::now(); if self.dry_run { return; @@ -918,16 +810,34 @@ impl<'cv> CollectionViewer<'cv> { } }); } + + fn update_selection(&mut self) { + assert!( + self.selected_pane.is_some(), + "update selection can only be called when a pane is selected" + ); + + self.response_viewer + .maybe_select(self.selected_pane.as_ref().unwrap()); + } + + fn update_focus(&mut self) { + self.response_viewer.maybe_focus(&self.focused_pane); + } } -impl Page for CollectionViewer<'_> { +impl Component for CollectionViewer<'_> { #[tracing::instrument(skip_all)] fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { - self.draw_background(size, frame); + // 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_response_rx(); + self.drain_responses_channel(); - self.draw_res_viewer(frame); + self.response_viewer + .draw(frame, self.layout.response_preview)?; self.draw_req_editor(frame); self.draw_req_uri(frame); self.draw_sidebar(frame); @@ -950,21 +860,26 @@ impl Page for CollectionViewer<'_> { .as_ref() .is_some_and(|pane| pane.eq(&PaneFocus::Editor)) { - let mut editor_position = self.editor.layout().content_pane; - editor_position.height = editor_position.height.sub(2); - let cursor = self.editor.cursor(); + // the editor status bar occupies 1 row, so we have to subtract it to prevent the + // cursor from going out of the intended spacing, we also subtract the bottom border. + let mut editor_position = self.request_editor.layout().content_pane; + let statusbar_size = 1; + let border_size = 1; + editor_position.height = editor_position.height.sub(statusbar_size).sub(border_size); + + let cursor = self.request_editor.cursor(); let row_with_offset = u16::min( editor_position .y .add(cursor.row_with_offset() as u16) - .saturating_sub(self.editor.row_scroll() as u16), + .saturating_sub(self.request_editor.row_scroll() as u16), editor_position.y.add(editor_position.height), ); let col_with_offset = u16::min( editor_position .x .add(cursor.col_with_offset() as u16) - .saturating_sub(self.editor.col_scroll() as u16), + .saturating_sub(self.request_editor.col_scroll() as u16), editor_position.x.add(editor_position.width), ); frame.set_cursor(col_with_offset, row_with_offset); @@ -991,27 +906,32 @@ impl Page for CollectionViewer<'_> { } fn handle_tick(&mut self) -> anyhow::Result<()> { - if self.sync_interval.elapsed().as_secs().ge(&5) { + 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<()> { - self.sender = Some(sender); + self.global_command_sender = Some(sender); Ok(()) } fn resize(&mut self, new_size: Rect) { let new_layout = build_layout(new_size); - self.editor.resize(new_layout.req_editor); - self.res_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<'_> { 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 self.curr_overlay.ne(&Overlays::None) { match self.curr_overlay { Overlays::CreateRequest => return self.handle_create_request_key_event(key_event), @@ -1027,21 +947,25 @@ impl Eventful for CollectionViewer<'_> { KeyCode::Char('r') => { self.focused_pane = PaneFocus::Sidebar; self.selected_pane = Some(PaneFocus::Sidebar); + self.update_selection(); return Ok(None); } KeyCode::Char('u') => { self.focused_pane = PaneFocus::ReqUri; self.selected_pane = Some(PaneFocus::ReqUri); + self.update_selection(); return Ok(None); } KeyCode::Char('p') => { self.focused_pane = PaneFocus::Preview; self.selected_pane = Some(PaneFocus::Preview); + self.update_selection(); return Ok(None); } KeyCode::Char('e') => { self.focused_pane = PaneFocus::Editor; self.selected_pane = Some(PaneFocus::Editor); + self.update_selection(); return Ok(None); } _ => (), @@ -1051,19 +975,23 @@ impl Eventful for CollectionViewer<'_> { 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.focused_pane = PaneFocus::ReqUri; + self.update_focus(); } (PaneFocus::ReqUri, None, KeyModifiers::NONE) => { - self.focused_pane = PaneFocus::Editor + self.focused_pane = PaneFocus::Editor; + self.update_focus(); } (PaneFocus::Editor, None, KeyModifiers::NONE) => { - self.focused_pane = PaneFocus::Preview + self.focused_pane = PaneFocus::Preview; + self.update_focus(); } (PaneFocus::Preview, None, KeyModifiers::NONE) => { - self.focused_pane = PaneFocus::Sidebar + self.focused_pane = PaneFocus::Sidebar; + self.update_focus(); } (PaneFocus::Editor, Some(_), KeyModifiers::NONE) => { - self.editor.handle_key_event(key_event)?; + self.request_editor.handle_key_event(key_event)?; } (PaneFocus::Preview, Some(_), _) => { self.handle_preview_key_event(key_event)?; diff --git a/hac-client/src/pages/collection_viewer/mod.rs b/hac-client/src/pages/collection_viewer/mod.rs index cce6b4d..c07a30d 100644 --- a/hac-client/src/pages/collection_viewer/mod.rs +++ b/hac-client/src/pages/collection_viewer/mod.rs @@ -1,8 +1,8 @@ #[allow(clippy::module_inception)] mod collection_viewer; -mod req_editor; mod req_uri; -mod res_viewer; +mod request_editor; +mod response_viewer; mod sidebar; pub use collection_viewer::CollectionViewer; diff --git a/hac-client/src/pages/collection_viewer/req_editor.rs b/hac-client/src/pages/collection_viewer/request_editor.rs similarity index 95% rename from hac-client/src/pages/collection_viewer/req_editor.rs rename to hac-client/src/pages/collection_viewer/request_editor.rs index 9024ad0..26982cd 100644 --- a/hac-client/src/pages/collection_viewer/req_editor.rs +++ b/hac-client/src/pages/collection_viewer/request_editor.rs @@ -1,25 +1,24 @@ +mod editor_tab; + use crate::{pages::Eventful, utils::build_syntax_highlighted_lines}; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + use hac_config::{Action, EditorMode, KeyAction}; -use hac_core::{ - collection::types::{Request, RequestMethod}, - command::Command, - syntax::highlighter::HIGHLIGHTER, - text_object::{cursor::Cursor, TextObject, Write}, -}; -use ratatui::{ - buffer::Buffer, - layout::{Constraint, Direction, Layout, Rect}, - style::{Style, Stylize}, - text::{Line, Span}, - widgets::{Block, Borders, Paragraph, Tabs, Widget}, - Frame, -}; -use std::{ - fmt::Display, - ops::{Add, Div, Mul, Sub}, - sync::{Arc, RwLock}, -}; +use hac_core::collection::types::{Request, RequestMethod}; +use hac_core::command::Command; +use hac_core::syntax::highlighter::HIGHLIGHTER; +use hac_core::text_object::{cursor::Cursor, TextObject, Write}; + +use std::fmt::Display; +use std::ops::{Add, Div, Mul, Sub}; +use std::sync::{Arc, RwLock}; + +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use ratatui::buffer::Buffer; +use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::style::{Style, Stylize}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Block, Borders, Paragraph, Tabs, Widget}; +use ratatui::Frame; use tree_sitter::Tree; #[derive(Debug, Default, Clone)] @@ -95,10 +94,9 @@ pub struct ReqEditor<'re> { impl<'re> ReqEditor<'re> { pub fn new( colors: &'re hac_colors::Colors, + config: &'re hac_config::Config, request: Option>>, - size: Rect, - config: &'re hac_config::Config, ) -> Self { let (body, tree) = if let Some(request) = request.as_ref() { if let Some(body) = request.read().unwrap().body.as_ref() { @@ -243,29 +241,12 @@ impl<'re> ReqEditor<'re> { } fn draw_tabs(&self, buf: &mut Buffer, size: Rect) { - let (tabs, active) = if self - .request - .as_ref() - .map(request_has_no_body) - .unwrap_or(true) - { - let tabs = vec!["Headers", "Query", "Auth"]; - let active = match self.curr_tab { - ReqEditorTabs::Headers => 0, - ReqEditorTabs::_Query => 1, - ReqEditorTabs::_Auth => 2, - _ => 0, - }; - (tabs, active) - } else { - let tabs = vec!["Body", "Headers", "Query", "Auth"]; - let active = match self.curr_tab { - ReqEditorTabs::Body => 0, - ReqEditorTabs::Headers => 1, - ReqEditorTabs::_Query => 2, - ReqEditorTabs::_Auth => 3, - }; - (tabs, active) + let tabs = vec!["Body", "Headers", "Query", "Auth"]; + let active = match self.curr_tab { + ReqEditorTabs::Body => 0, + ReqEditorTabs::Headers => 1, + ReqEditorTabs::_Query => 2, + ReqEditorTabs::_Auth => 3, }; Tabs::new(tabs) diff --git a/hac-client/src/pages/collection_viewer/request_editor/editor_tab.rs b/hac-client/src/pages/collection_viewer/request_editor/editor_tab.rs new file mode 100644 index 0000000..139597f --- /dev/null +++ b/hac-client/src/pages/collection_viewer/request_editor/editor_tab.rs @@ -0,0 +1,2 @@ + + diff --git a/hac-client/src/pages/collection_viewer/res_viewer.rs b/hac-client/src/pages/collection_viewer/response_viewer.rs similarity index 70% rename from hac-client/src/pages/collection_viewer/res_viewer.rs rename to hac-client/src/pages/collection_viewer/response_viewer.rs index b28b106..a68367d 100644 --- a/hac-client/src/pages/collection_viewer/res_viewer.rs +++ b/hac-client/src/pages/collection_viewer/response_viewer.rs @@ -1,40 +1,29 @@ -use hac_core::{net::request_manager::Response, syntax::highlighter::HIGHLIGHTER}; -use rand::Rng; +use hac_core::command::Command; +use hac_core::net::request_manager::Response; +use hac_core::syntax::highlighter::HIGHLIGHTER; -use crate::{ - ascii::{BIG_ERROR_ARTS, LOGO_ART, SMALL_ERROR_ARTS}, - pages::spinner::Spinner, - utils::build_syntax_highlighted_lines, -}; -use ratatui::{ - buffer::Buffer, - layout::{Constraint, Direction, Layout, Rect}, - style::{Style, Stylize}, - text::{Line, Span}, - widgets::{ - Block, Borders, Clear, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, - StatefulWidget, Tabs, Widget, - }, -}; -use std::{ - cell::RefCell, - iter, - ops::{Add, Deref, Sub}, - rc::Rc, +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::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}; +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, ScrollbarOrientation, ScrollbarState, + Tabs, }; +use ratatui::Frame; use tree_sitter::Tree; -pub struct ResViewerState<'a> { - pub is_focused: bool, - pub is_selected: bool, - pub curr_tab: &'a ResViewerTabs, - pub raw_scroll: &'a mut usize, - pub pretty_scroll: &'a mut usize, - pub headers_scroll_y: &'a mut usize, - pub headers_scroll_x: &'a mut usize, - pub pending_request: bool, -} - #[derive(Debug, Clone, PartialEq)] pub enum ResViewerTabs { Preview, @@ -88,6 +77,15 @@ pub struct ResViewer<'a> { empty_lines: Vec>, preview_layout: PreviewLayout, layout: ResViewerLayout, + + active_tab: ResViewerTabs, + raw_scroll: usize, + headers_scroll_y: usize, + headers_scroll_x: usize, + pretty_scroll: usize, + is_focused: bool, + is_selected: bool, + pending_request: bool, } impl<'a> ResViewer<'a> { @@ -120,6 +118,14 @@ impl<'a> ResViewer<'a> { empty_lines, preview_layout, layout, + active_tab: ResViewerTabs::Preview, + raw_scroll: 0, + headers_scroll_y: 0, + headers_scroll_x: 0, + pretty_scroll: 0, + is_focused: false, + is_selected: false, + pending_request: false, } } @@ -181,8 +187,8 @@ impl<'a> ResViewer<'a> { self.response = response; } - fn draw_container(&self, size: Rect, buf: &mut Buffer, state: &mut ResViewerState) { - let block_border = match (state.is_focused, state.is_selected) { + fn draw_container(&self, size: Rect, frame: &mut Frame) { + 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), (_, _) => Style::default().fg(self.colors.bright.black), @@ -196,22 +202,22 @@ impl<'a> ResViewer<'a> { ]) .border_style(block_border); - block.render(size, buf); + frame.render_widget(block, size); } - fn draw_tabs(&self, buf: &mut Buffer, state: &ResViewerState, size: Rect) { + fn draw_tabs(&self, frame: &mut Frame, size: Rect) { let tabs = Tabs::new(["Pretty", "Raw", "Headers", "Cookies"]) .style(Style::default().fg(self.colors.bright.black)) - .select(state.curr_tab.clone().into()) + .select(self.active_tab.clone().into()) .highlight_style( Style::default() .fg(self.colors.normal.white) .bg(self.colors.normal.blue), ); - tabs.render(size, buf); + frame.render_widget(tabs, size); } - fn draw_spinner(&self, buf: &mut Buffer) { + fn draw_spinner(&self, frame: &mut Frame) { let request_pane = self.preview_layout.content_pane; let center = request_pane.y.add(request_pane.height.div_ceil(2)); let size = Rect::new(request_pane.x, center, request_pane.width, 1); @@ -219,23 +225,22 @@ impl<'a> ResViewer<'a> { .with_label("Sending request".fg(self.colors.bright.black)) .into_centered_line(); - Widget::render(Clear, request_pane, buf); - Widget::render( + frame.render_widget(Clear, request_pane); + frame.render_widget( Block::default().bg(self.colors.primary.background), request_pane, - buf, ); - Widget::render(spinner, size, buf); + frame.render_widget(spinner, size); } - fn draw_network_error(&self, buf: &mut Buffer) { + fn draw_network_error(&self, frame: &mut Frame) { if self.response.as_ref().is_some() { let request_pane = self.preview_layout.content_pane; - Widget::render(Clear, request_pane, buf); - Widget::render( + + frame.render_widget(Clear, request_pane); + frame.render_widget( Block::default().bg(self.colors.primary.background), request_pane, - buf, ); let center = request_pane @@ -250,19 +255,19 @@ impl<'a> ResViewer<'a> { self.error_lines.as_ref().unwrap().len() as u16, ); - Paragraph::new(self.error_lines.clone().unwrap()) - .fg(self.colors.bright.black) - .render(size, buf); + frame.render_widget( + Paragraph::new(self.error_lines.clone().unwrap()).fg(self.colors.bright.black), + size, + ) } } - fn draw_waiting_for_request(&self, buf: &mut Buffer) { + fn draw_waiting_for_request(&self, frame: &mut Frame) { let request_pane = self.preview_layout.content_pane; - Widget::render(Clear, request_pane, buf); - Widget::render( + frame.render_widget(Clear, request_pane); + frame.render_widget( Block::default().bg(self.colors.primary.background), request_pane, - buf, ); let center = request_pane @@ -277,23 +282,25 @@ impl<'a> ResViewer<'a> { self.empty_lines.len() as u16, ); - Paragraph::new(self.empty_lines.clone()) - .fg(self.colors.normal.red) - .centered() - .render(size, buf); + frame.render_widget( + Paragraph::new(self.empty_lines.clone()) + .fg(self.colors.normal.red) + .centered(), + size, + ) } - fn draw_current_tab(&self, state: &mut ResViewerState, buf: &mut Buffer, size: Rect) { + fn draw_current_tab(&mut self, frame: &mut Frame, size: Rect) { if self .response .as_ref() .is_some_and(|res| res.borrow().is_error) { - self.draw_network_error(buf); + self.draw_network_error(frame); }; if self.response.is_none() { - self.draw_waiting_for_request(buf); + self.draw_waiting_for_request(frame); } if self @@ -301,20 +308,20 @@ impl<'a> ResViewer<'a> { .as_ref() .is_some_and(|res| !res.borrow().is_error) { - match state.curr_tab { - ResViewerTabs::Preview => self.draw_pretty_response(state, buf, size), - ResViewerTabs::Raw => self.draw_raw_response(state, buf, size), - ResViewerTabs::Headers => self.draw_response_headers(state, buf), + 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 => {} } } - if state.pending_request { - self.draw_spinner(buf); + if self.pending_request { + self.draw_spinner(frame); } } - fn draw_response_headers(&self, state: &mut ResViewerState, buf: &mut Buffer) { + 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() { let mut longest_line: usize = 0; @@ -332,7 +339,7 @@ impl<'a> ResViewer<'a> { lines.push(Line::from( name_string .chars() - .skip(*state.headers_scroll_x) + .skip(self.headers_scroll_x) .collect::() .bold() .yellow(), @@ -340,36 +347,31 @@ impl<'a> ResViewer<'a> { lines.push(Line::from( value .chars() - .skip(*state.headers_scroll_x) + .skip(self.headers_scroll_x) .collect::(), )); lines.push(Line::from("")); } } - if state + if self .headers_scroll_y - .deref() // we add a blank line after every entry, we account for that here .ge(&lines.len().saturating_sub(2)) { - *state.headers_scroll_y = lines.len().saturating_sub(2); + self.headers_scroll_y = lines.len().saturating_sub(2); } - if state - .headers_scroll_x - .deref() - .ge(&longest_line.saturating_sub(1)) - { - *state.headers_scroll_x = longest_line.saturating_sub(1); + if self.headers_scroll_x.ge(&longest_line.saturating_sub(1)) { + 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(), - *state.headers_scroll_y, - buf, + self.headers_scroll_y, + frame, self.preview_layout.scrollbar, ); @@ -382,7 +384,7 @@ impl<'a> ResViewer<'a> { let lines = lines .into_iter() - .skip(*state.headers_scroll_y) + .skip(self.headers_scroll_y) .chain(iter::repeat(Line::from("~".fg(self.colors.bright.black)))) .take(lines_to_show as usize) .collect::>(); @@ -391,21 +393,22 @@ impl<'a> ResViewer<'a> { if longest_line > self.preview_layout.content_pane.width as usize { self.draw_horizontal_scrollbar( longest_line, - *state.headers_scroll_x, - buf, + self.headers_scroll_x, + frame, x_scrollbar_pane, ); - Paragraph::new(lines).block(block).render(headers_pane, buf); + frame.render_widget(Paragraph::new(lines).block(block), headers_pane) } else { - Paragraph::new(lines) - .block(block) - .render(self.preview_layout.content_pane, buf); + frame.render_widget( + Paragraph::new(lines).block(block), + self.preview_layout.content_pane, + ); } } } } - fn draw_raw_response(&self, state: &mut ResViewerState, buf: &mut Buffer, size: Rect) { + 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() { response @@ -423,26 +426,26 @@ impl<'a> ResViewer<'a> { vec![Line::from("No body").centered()] }; // allow for scrolling down until theres only one line left into view - if state.raw_scroll.deref().ge(&lines.len().saturating_sub(1)) { - *state.raw_scroll = lines.len().saturating_sub(1); + if self.raw_scroll.ge(&lines.len().saturating_sub(1)) { + self.raw_scroll = lines.len().saturating_sub(1); } self.draw_scrollbar( lines.len(), - *state.raw_scroll, - buf, + self.raw_scroll, + frame, self.preview_layout.scrollbar, ); let lines_in_view = lines .into_iter() - .skip(*state.raw_scroll) + .skip(self.raw_scroll) .chain(iter::repeat(Line::from("~".fg(self.colors.bright.black)))) .take(size.height.into()) .collect::>(); let raw_response = Paragraph::new(lines_in_view); - raw_response.render(self.preview_layout.content_pane, buf); + frame.render_widget(raw_response, self.preview_layout.content_pane); } } @@ -450,7 +453,7 @@ impl<'a> ResViewer<'a> { &self, total_lines: usize, current_scroll: usize, - buf: &mut Buffer, + frame: &mut Frame, size: Rect, ) { let mut scrollbar_state = ScrollbarState::new(total_lines).position(current_scroll); @@ -460,14 +463,14 @@ impl<'a> ResViewer<'a> { .begin_symbol(Some("↑")) .end_symbol(Some("↓")); - scrollbar.render(size, buf, &mut scrollbar_state); + frame.render_stateful_widget(scrollbar, size, &mut scrollbar_state); } fn draw_horizontal_scrollbar( &self, total_columns: usize, current_scroll: usize, - buf: &mut Buffer, + frame: &mut Frame, size: Rect, ) { let mut scrollbar_state = ScrollbarState::new(total_columns).position(current_scroll); @@ -477,23 +480,19 @@ impl<'a> ResViewer<'a> { .begin_symbol(Some("←")) .end_symbol(Some("→")); - scrollbar.render(size, buf, &mut scrollbar_state); + frame.render_stateful_widget(scrollbar, size, &mut scrollbar_state); } - fn draw_pretty_response(&self, state: &mut ResViewerState, buf: &mut Buffer, size: Rect) { + fn draw_pretty_response(&mut self, frame: &mut Frame, size: Rect) { if self.response.as_ref().is_some() { - if state - .pretty_scroll - .deref() - .ge(&self.lines.len().saturating_sub(1)) - { - *state.pretty_scroll = self.lines.len().saturating_sub(1); + 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(), - *state.raw_scroll, - buf, + self.raw_scroll, + frame, self.preview_layout.scrollbar, ); @@ -505,17 +504,17 @@ impl<'a> ResViewer<'a> { let lines_in_view = lines .into_iter() - .skip(*state.pretty_scroll) + .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); - pretty_response.render(self.preview_layout.content_pane, buf); + frame.render_widget(pretty_response, self.preview_layout.content_pane); } } - fn draw_summary(&self, buf: &mut Buffer, size: Rect) { + fn draw_summary(&self, frame: &mut Frame, size: Rect) { if let Some(ref response) = self.response { let status_color = match response .borrow() @@ -555,19 +554,78 @@ impl<'a> ResViewer<'a> { pieces.push(format!("{} B", size).fg(self.colors.normal.green)) }; - Line::from(pieces).render(size, buf); + frame.render_widget(Line::from(pieces), size); } } + + pub fn maybe_select(&mut self, selected_pane: &PaneFocus) { + self.is_selected = selected_pane.eq(&PaneFocus::Preview); + self.is_focused = selected_pane.eq(&PaneFocus::Preview); + } + + pub fn maybe_focus(&mut self, focused_pane: &PaneFocus) { + self.is_focused = focused_pane.eq(&PaneFocus::Preview); + } + + pub fn set_pending_request(&mut self, is_pending: bool) { + self.pending_request = is_pending; + } } -impl<'a> StatefulWidget for ResViewer<'a> { - type State = ResViewerState<'a>; +impl<'a> Component 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); + self.draw_summary(frame, self.layout.summary_pane); + self.draw_container(size, frame); + + Ok(()) + } + + fn resize(&mut self, _new_size: Rect) {} +} + +impl<'a> Eventful for ResViewer<'a> { + 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); + } + + match key_event.code { + KeyCode::Char('0') if self.active_tab.eq(&ResViewerTabs::Headers) => { + self.headers_scroll_x = 0; + } + KeyCode::Char('$') if self.active_tab.eq(&ResViewerTabs::Headers) => { + self.headers_scroll_x = usize::MAX; + } + KeyCode::Char('h') => { + if let ResViewerTabs::Headers = self.active_tab { + self.headers_scroll_x = self.headers_scroll_x.saturating_sub(1) + } + } + KeyCode::Char('j') => match self.active_tab { + ResViewerTabs::Preview => self.pretty_scroll = self.pretty_scroll.add(1), + ResViewerTabs::Raw => self.raw_scroll = self.raw_scroll.add(1), + ResViewerTabs::Headers => self.headers_scroll_y = self.headers_scroll_y.add(1), + ResViewerTabs::Cookies => {} + }, + 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::Cookies => {} + }, + KeyCode::Char('l') => { + if let ResViewerTabs::Headers = self.active_tab { + self.headers_scroll_x = self.headers_scroll_x.add(1) + } + } + _ => {} + } - fn render(self, size: Rect, buf: &mut Buffer, state: &mut Self::State) { - self.draw_tabs(buf, state, self.layout.tabs_pane); - self.draw_current_tab(state, buf, self.layout.content_pane); - self.draw_summary(buf, self.layout.summary_pane); - self.draw_container(size, buf, state); + Ok(None) } } diff --git a/hac-client/src/pages/terminal_too_small.rs b/hac-client/src/pages/terminal_too_small.rs index 17e4c7f..f4be65e 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::Page; +use crate::pages::Component; use ratatui::{ layout::{Alignment, Constraint, Direction, Flex, Layout, Rect}, style::Stylize, @@ -21,7 +21,7 @@ impl<'a> TerminalTooSmall<'a> { } } -impl Page for TerminalTooSmall<'_> { +impl Component 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 fe74562..c581ce2 100644 --- a/hac-client/src/screen_manager.rs +++ b/hac-client/src/screen_manager.rs @@ -2,7 +2,7 @@ use crate::{ event_pool::Event, pages::{ collection_dashboard::CollectionDashboard, collection_viewer::CollectionViewer, - terminal_too_small::TerminalTooSmall, Eventful, Page, + terminal_too_small::TerminalTooSmall, Component, Eventful, }, }; use hac_core::{collection::Collection, command::Command}; @@ -108,7 +108,7 @@ impl<'sm> ScreenManager<'sm> { } } -impl Page for ScreenManager<'_> { +impl Component 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), diff --git a/hac-client/tests/collection_dashboard.rs b/hac-client/tests/collection_dashboard.rs index 178b9c9..e84226f 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, Eventful, Page}; +use hac_client::pages::{collection_dashboard::CollectionDashboard, Component, Eventful}; use hac_core::collection; use ratatui::{backend::TestBackend, layout::Rect, Frame, Terminal}; use std::{