diff --git a/hac-client/benches/collection_viewer_bench.rs b/hac-client/benches/collection_viewer_bench.rs index ae0fcc0..6c427fc 100755 --- a/hac-client/benches/collection_viewer_bench.rs +++ b/hac-client/benches/collection_viewer_bench.rs @@ -29,6 +29,7 @@ fn create_sample_collection() -> Collection { requests: Some(Arc::new(RwLock::new(vec![ RequestKind::Single(Arc::new(RwLock::new(Request { id: "any id".to_string(), + headers: None, name: "testing".to_string(), uri: "https://jsonplaceholder.typicode.com/users".to_string(), method: RequestMethod::Get, @@ -40,6 +41,7 @@ fn create_sample_collection() -> Collection { name: "testing".to_string(), uri: "https://jsonplaceholder.typicode.com/users".to_string(), method: RequestMethod::Get, + headers: None, body: Some("[\r\n {\r\n \"id\": 1,\r\n \"name\": \"Leanne Graham\",\r\n \"username\": \"Bret\",\r\n \"email\": \"Sincere@april.biz\",\r\n \"address\": {\r\n \"street\": \"Kulas Light\",\r\n \"suite\": \"Apt. 556\",\r\n \"city\": \"Gwenborough\",\r\n \"zipcode\": \"92998-3874\",\r\n \"geo\": {\r\n \"lat\": \"-37.3159\",\r\n \"lng\": \"81.1496\"\r\n }\r\n },\r\n \"phone\": \"1-770-736-8031 x56442\",\r\n \"website\": \"hildegard.org\",\r\n \"company\": {\r\n \"name\": \"Romaguera-Crona\",\r\n \"catchPhrase\": \"Multi-layered client-server neural-net\",\r\n \"bs\": \"harness real-time e-markets\"\r\n }\r\n },\r\n {\r\n \"id\": 2,\r\n \"name\": \"Ervin Howell\",\r\n \"username\": \"Antonette\",\r\n \"email\": \"Shanna@melissa.tv\",\r\n \"address\": {\r\n \"street\": \"Victor Plains\",\r\n \"suite\": \"Suite 879\",\r\n \"city\": \"Wisokyburgh\",\r\n \"zipcode\": \"90566-7771\",\r\n \"geo\": {\r\n \"lat\": \"-43.9509\",\r\n \"lng\": \"-34.4618\"\r\n }\r\n },\r\n \"phone\": \"010-692-6593 x09125\",\r\n \"website\": \"anastasia.net\",\r\n \"company\": {\r\n \"name\": \"Deckow-Crist\",\r\n \"catchPhrase\": \"Proactive didactic contingency\",\r\n \"bs\": \"synergize scalable supply-chains\"\r\n }\r\n },\r\n {\r\n \"id\": 3,\r\n \"name\": \"Clementine Bauch\",\r\n \"username\": \"Samantha\",\r\n \"email\": \"Nathan@yesenia.net\",\r\n \"address\": {\r\n \"street\": \"Douglas Extension\",\r\n \"suite\": \"Suite 847\",\r\n \"city\": \"McKenziehaven\",\r\n \"zipcode\": \"59590-4157\",\r\n \"geo\": {\r\n \"lat\": \"-68.6102\",\r\n \"lng\": \"-47.0653\"\r\n }\r\n },\r\n \"phone\": \"1-463-123-4447\",\r\n \"website\": \"ramiro.info\",\r\n \"company\": {\r\n \"name\": \"Romaguera-Jacobson\",\r\n \"catchPhrase\": \"Face to face bifurcated interface\",\r\n \"bs\": \"e-enable strategic applications\"\r\n }\r\n },\r\n {\r\n \"id\": 4,\r\n \"name\": \"Patricia Lebsack\",\r\n \"username\": \"Karianne\",\r\n \"email\": \"Julianne.OConner@kory.org\",\r\n \"address\": {\r\n \"street\": \"Hoeger Mall\",\r\n \"suite\": \"Apt. 692\",\r\n \"city\": \"South Elvis\",\r\n \"zipcode\": \"53919-4257\",\r\n \"geo\": {\r\n \"lat\": \"29.4572\",\r\n \"lng\": \"-164.2990\"\r\n }\r\n },\r\n \"phone\": \"493-170-9623 x156\",\r\n \"website\": \"kale.biz\",\r\n \"company\": {\r\n \"name\": \"Robel-Corkery\",\r\n \"catchPhrase\": \"Multi-tiered zero tolerance productivity\",\r\n \"bs\": \"transition cutting-edge web services\"\r\n }\r\n },\r\n {\r\n \"id\": 5,\r\n \"name\": \"Chelsey Dietrich\",\r\n \"username\": \"Kamren\",\r\n \"email\": \"Lucio_Hettinger@annie.ca\",\r\n \"address\": {\r\n \"street\": \"Skiles Walks\",\r\n \"suite\": \"Suite 351\",\r\n \"city\": \"Roscoeview\",\r\n \"zipcode\": \"33263\",\r\n \"geo\": {\r\n \"lat\": \"-31.8129\",\r\n \"lng\": \"62.5342\"\r\n }\r\n },\r\n \"phone\": \"(254)954-1289\",\r\n \"website\": \"demarco.info\",\r\n \"company\": {\r\n \"name\": \"Keebler LLC\",\r\n \"catchPhrase\": \"User-centric fault-tolerant solution\",\r\n \"bs\": \"revolutionize end-to-end systems\"\r\n }\r\n },\r\n {\r\n \"id\": 6,\r\n \"name\": \"Mrs. Dennis Schulist\",\r\n \"username\": \"Leopoldo_Corkery\",\r\n \"email\": \"Karley_Dach@jasper.info\",\r\n \"address\": {\r\n \"street\": \"Norberto Crossing\",\r\n \"suite\": \"Apt. 950\",\r\n \"city\": \"South Christy\",\r\n \"zipcode\": \"23505-1337\",\r\n \"geo\": {\r\n \"lat\": \"-71.4197\",\r\n \"lng\": \"71.7478\"\r\n }\r\n },\r\n \"phone\": \"1-477-935-8478 x6430\",\r\n \"website\": \"ola.org\",\r\n \"company\": {\r\n \"name\": \"Considine-Lockman\",\r\n \"catchPhrase\": \"Synchronised bottom-line interface\",\r\n \"bs\": \"e-enable innovative applications\"\r\n }\r\n },\r\n {\r\n \"id\": 7,\r\n \"name\": \"Kurtis Weissnat\",\r\n \"username\": \"Elwyn.Skiles\",\r\n \"email\": \"Telly.Hoeger@billy.biz\",\r\n \"address\": {\r\n \"street\": \"Rex Trail\",\r\n \"suite\": \"Suite 280\",\r\n \"city\": \"Howemouth\",\r\n \"zipcode\": \"58804-1099\",\r\n \"geo\": {\r\n \"lat\": \"24.8918\",\r\n \"lng\": \"21.8984\"\r\n }\r\n },\r\n \"phone\": \"210.067.6132\",\r\n \"website\": \"elvis.io\",\r\n \"company\": {\r\n \"name\": \"Johns Group\",\r\n \"catchPhrase\": \"Configurable multimedia task-force\",\r\n \"bs\": \"generate enterprise e-tailers\"\r\n }\r\n },\r\n {\r\n \"id\": 8,\r\n \"name\": \"Nicholas Runolfsdottir V\",\r\n \"username\": \"Maxime_Nienow\",\r\n \"email\": \"Sherwood@rosamond.me\",\r\n \"address\": {\r\n \"street\": \"Ellsworth Summit\",\r\n \"suite\": \"Suite 729\",\r\n \"city\": \"Aliyaview\",\r\n \"zipcode\": \"45169\",\r\n \"geo\": {\r\n \"lat\": \"-14.3990\",\r\n \"lng\": \"-120.7677\"\r\n }\r\n },\r\n \"phone\": \"586.493.6943 x140\",\r\n \"website\": \"jacynthe.com\",\r\n \"company\": {\r\n \"name\": \"Abernathy Group\",\r\n \"catchPhrase\": \"Implemented secondary concept\",\r\n \"bs\": \"e-enable extensible e-tailers\"\r\n }\r\n },\r\n {\r\n \"id\": 9,\r\n \"name\": \"Glenna Reichert\",\r\n \"username\": \"Delphine\",\r\n \"email\": \"Chaim_McDermott@dana.io\",\r\n \"address\": {\r\n \"street\": \"Dayna Park\",\r\n \"suite\": \"Suite 449\",\r\n \"city\": \"Bartholomebury\",\r\n \"zipcode\": \"76495-3109\",\r\n \"geo\": {\r\n \"lat\": \"24.6463\",\r\n \"lng\": \"-168.8889\"\r\n }\r\n },\r\n \"phone\": \"(775)976-6794 x41206\",\r\n \"website\": \"conrad.com\",\r\n \"company\": {\r\n \"name\": \"Yost and Sons\",\r\n \"catchPhrase\": \"Switchable contextually-based project\",\r\n \"bs\": \"aggregate real-time technologies\"\r\n }\r\n },\r\n {\r\n \"id\": 10,\r\n \"name\": \"Clementina DuBuque\",\r\n \"username\": \"Moriah.Stanton\",\r\n \"email\": \"Rey.Padberg@karina.biz\",\r\n \"address\": {\r\n \"street\": \"Kattie Turnpike\",\r\n \"suite\": \"Suite 198\",\r\n \"city\": \"Lebsackbury\",\r\n \"zipcode\": \"31428-2261\",\r\n \"geo\": {\r\n \"lat\": \"-38.2386\",\r\n \"lng\": \"57.2232\"\r\n }\r\n },\r\n \"phone\": \"024-648-3804\",\r\n \"website\": \"ambrose.net\",\r\n \"company\": {\r\n \"name\": \"Hoeger LLC\",\r\n \"catchPhrase\": \"Centralized empowering task-force\",\r\n \"bs\": \"target end-to-end models\"\r\n }\r\n }\r\n]".to_string()), body_type: Some(BodyType::Json), }))), diff --git a/hac-client/src/pages/collection_viewer/request_editor.rs b/hac-client/src/pages/collection_viewer/request_editor.rs index d356348..2f3b609 100755 --- a/hac-client/src/pages/collection_viewer/request_editor.rs +++ b/hac-client/src/pages/collection_viewer/request_editor.rs @@ -129,7 +129,11 @@ impl<'re> RequestEditor<'re> { collection_store.clone(), layout.content_pane, ), - headers_editor: HeadersEditor::new(colors, collection_store.clone()), + headers_editor: HeadersEditor::new( + colors, + collection_store.clone(), + layout.content_pane, + ), auth_editor: AuthEditor::new(colors), layout, curr_tab, diff --git a/hac-client/src/pages/collection_viewer/request_editor/headers_editor.rs b/hac-client/src/pages/collection_viewer/request_editor/headers_editor.rs index f1a32c8..a7e78c1 100755 --- a/hac-client/src/pages/collection_viewer/request_editor/headers_editor.rs +++ b/hac-client/src/pages/collection_viewer/request_editor/headers_editor.rs @@ -1,64 +1,90 @@ +use hac_core::collection::types::HeaderMap; + use crate::pages::{collection_viewer::collection_store::CollectionStore, Eventful, Renderable}; -use std::{cell::RefCell, ops::Div, rc::Rc}; +use std::ops::{Div, Sub}; +use std::{cell::RefCell, ops::Add, rc::Rc}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use hac_core::collection::types::HeaderMap; -use ratatui::{ - layout::{Constraint, Direction, Layout, Rect}, - style::Stylize, - text::Line, - widgets::{Block, Borders, Cell, Paragraph, Row, Table}, - Frame, -}; +use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::style::{Style, Stylize}; +use ratatui::widgets::{Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState}; +use ratatui::Frame; #[derive(Debug)] pub enum HeadersEditorEvent { Quit, } +#[derive(Debug)] +struct HeadersEditorLayout { + name_header_size: Rect, + value_header_size: Rect, + enabled_header_size: Rect, + content_size: Rect, + scrollbar_size: Rect, +} + #[derive(Debug)] pub struct HeadersEditor<'he> { colors: &'he hac_colors::colors::Colors, collection_store: Rc>, scroll: usize, + selected_row: usize, + row_height: u16, + amount_on_view: usize, + layout: HeadersEditorLayout, } impl<'he> HeadersEditor<'he> { pub fn new( colors: &'he hac_colors::colors::Colors, collection_store: Rc>, + size: Rect, ) -> Self { + let row_height = 2; + let layout = build_layout(size, row_height); HeadersEditor { colors, collection_store, scroll: 0, + selected_row: 5, + row_height, + amount_on_view: layout.content_size.height.div(row_height).into(), + layout, } } - fn draw_row(&self, (row, header): (Rc<[Rect]>, &HeaderMap), frame: &mut Frame) { - let enabled = self.colors.normal.yellow; + fn draw_row(&self, (row, header): (Vec, &HeaderMap), frame: &mut Frame, row_idx: usize) { let disabled = self.colors.bright.black; - let make_paragraph = |text: &str| { - Paragraph::new(text.to_string()) - .fg(if header.enabled { enabled } else { disabled }) - .block( - Block::default() - .fg(self.colors.normal.white) - .borders(Borders::BOTTOM), - ) + let normal = self.colors.normal.white; + let selected = self.colors.normal.red; + let is_selected = row_idx.eq(&self.selected_row.saturating_sub(self.scroll)); + + let text_color = match (is_selected, header.enabled) { + (true, _) => selected, + (false, true) => normal, + (false, false) => disabled, }; + let make_paragraph = |text: &str| Paragraph::new(text.to_string()).fg(text_color); + let name = make_paragraph(&header.pair.0); let value = make_paragraph(&header.pair.1); - frame.render_widget(name, row[0]); - frame.render_widget(value, row[1]); + let decor_fg = if is_selected { selected } else { normal }; + let checkbox = if header.enabled { "[x]" } else { "[ ]" }; + let chevron = if is_selected { ">" } else { " " }; + + frame.render_widget(Paragraph::new(chevron).fg(decor_fg), row[0]); + frame.render_widget(name, row[1]); + frame.render_widget(value, row[2]); + frame.render_widget(Paragraph::new(checkbox).fg(decor_fg).centered(), row[3]); } } impl Renderable for HeadersEditor<'_> { - fn draw(&mut self, frame: &mut Frame, size: Rect) -> anyhow::Result<()> { + fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { let Some(request) = self.collection_store.borrow().get_selected_request() else { return Ok(()); }; @@ -68,43 +94,65 @@ impl Renderable for HeadersEditor<'_> { return Ok(()); }; - let size = build_layout(size); - let row_height = 2; - let [titles_size, headers_size] = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Length(row_height), Constraint::Fill(1)]) - .areas(size); - - let [name_title_size, value_title_size] = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .areas(titles_size); - - let title_name = Paragraph::new("Name").fg(self.colors.normal.red).bold(); - let title_value = Paragraph::new("Value").fg(self.colors.normal.red).bold(); - - let pane_height = headers_size.height; - let items_fitting_onscreen = pane_height.div(row_height) as usize; + let title_name = Paragraph::new("Name").fg(self.colors.normal.yellow).bold(); + let title_value = Paragraph::new("Value").fg(self.colors.normal.yellow).bold(); + let title_enabled = Paragraph::new("Enabled") + .fg(self.colors.normal.yellow) + .bold(); Layout::default() - .constraints((0..items_fitting_onscreen).map(|_| Constraint::Length(row_height))) + .constraints((0..self.amount_on_view).map(|_| Constraint::Length(self.row_height))) .direction(Direction::Vertical) - .split(headers_size) + .split(self.layout.content_size) .iter() .map(|row| { Layout::default() - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .constraints([ + Constraint::Length(2), + Constraint::Fill(1), + Constraint::Fill(1), + Constraint::Length(1), + Constraint::Length(7), + ]) .direction(Direction::Horizontal) .split(*row) + .iter() + .enumerate() + // we are removing the empty space we just created between vallue and + // the enabled checkbox the idea is to have something like this: + // + // Name Value Enabled + // > Header-Name Header-Value [x] + // Header-Name Header-Value [x] + // + .filter(|(idx, _)| idx.ne(&3)) + .map(|(_, rect)| *rect) + .collect::>() }) - .zip(headers.iter()) - .for_each(|pair| self.draw_row(pair, frame)); + .zip(headers.iter().skip(self.scroll).take(self.amount_on_view)) + .enumerate() + .for_each(|(idx, pair)| self.draw_row(pair, frame, idx)); + + let mut scrollbar_state = ScrollbarState::new(headers.len()) + .content_length(self.row_height.into()) + .position(self.scroll); - frame.render_widget(title_name, name_title_size); - frame.render_widget(title_value, value_title_size); + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .style(Style::default().fg(self.colors.normal.red)) + .begin_symbol(Some("↑")) + .end_symbol(Some("↓")); + + frame.render_stateful_widget(scrollbar, self.layout.scrollbar_size, &mut scrollbar_state); + frame.render_widget(title_name, self.layout.name_header_size); + frame.render_widget(title_value, self.layout.value_header_size); + frame.render_widget(title_enabled, self.layout.enabled_header_size); Ok(()) } + + fn resize(&mut self, new_size: Rect) { + self.layout = build_layout(new_size, self.row_height); + } } impl Eventful for HeadersEditor<'_> { @@ -115,19 +163,75 @@ impl Eventful for HeadersEditor<'_> { return Ok(Some(HeadersEditorEvent::Quit)); } + let Some(req) = self.collection_store.borrow().get_selected_request() else { + return Ok(None); + }; + + let Some(ref headers) = req.read().unwrap().headers else { + return Ok(None); + }; + + let total_headers = headers.len(); + + match key_event.code { + KeyCode::Char('j') => { + self.selected_row = usize::min(self.selected_row.add(1), total_headers.sub(1)) + } + KeyCode::Char('k') => { + self.selected_row = self.selected_row.saturating_sub(1); + } + _ => {} + } + + if self + .selected_row + .saturating_sub(self.scroll) + .ge(&self.amount_on_view.sub(1)) + { + self.scroll = self.selected_row.saturating_sub(self.amount_on_view.sub(1)); + } + + if self.selected_row.saturating_sub(self.scroll).eq(&0) { + self.scroll = self + .scroll + .saturating_sub(self.scroll.saturating_sub(self.selected_row)); + } + Ok(None) } } -fn build_layout(size: Rect) -> Rect { - let [_, content, _] = Layout::default() +fn build_layout(size: Rect, row_height: u16) -> HeadersEditorLayout { + let [_, content, _, scrollbar_size] = Layout::default() .constraints([ Constraint::Length(1), Constraint::Fill(1), Constraint::Length(1), + Constraint::Length(1), ]) .direction(Direction::Horizontal) .areas(size); - content + let [headers_size, content_size] = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(row_height), Constraint::Fill(1)]) + .areas(content); + + let [_, name_header_size, value_header_size, enabled_header_size] = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Length(2), + Constraint::Fill(1), + Constraint::Fill(1), + Constraint::Length(7), + ]) + .areas(headers_size); + + HeadersEditorLayout { + name_header_size, + value_header_size, + enabled_header_size, + content_size, + scrollbar_size, + } }