Skip to content

Commit

Permalink
feat: selecting and moving around headers
Browse files Browse the repository at this point in the history
  • Loading branch information
wllfaria committed Jun 5, 2024
1 parent dbfa1a3 commit 68273d1
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 50 deletions.
2 changes: 2 additions & 0 deletions hac-client/benches/collection_viewer_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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\": \"[email protected]\",\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\": \"[email protected]\",\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\": \"[email protected]\",\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\": \"[email protected]\",\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\": \"[email protected]\",\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\": \"[email protected]\",\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\": \"[email protected]\",\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\": \"[email protected]\",\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\": \"[email protected]\",\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\": \"[email protected]\",\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),
}))),
Expand Down
6 changes: 5 additions & 1 deletion hac-client/src/pages/collection_viewer/request_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
202 changes: 153 additions & 49 deletions hac-client/src/pages/collection_viewer/request_editor/headers_editor.rs
Original file line number Diff line number Diff line change
@@ -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<RefCell<CollectionStore>>,
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<RefCell<CollectionStore>>,
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<Rect>, &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(());
};
Expand All @@ -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::<Vec<_>>()
})
.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<'_> {
Expand All @@ -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,
}
}

0 comments on commit 68273d1

Please sign in to comment.