From a35d5055b83068b72be532a641deecc22a6eb3db Mon Sep 17 00:00:00 2001 From: Federico Minaya Date: Sun, 3 Nov 2024 14:34:19 -0300 Subject: [PATCH] chore: refactor ui mod --- src/app/mod.rs | 19 ++- src/cli.rs | 4 +- src/ui/mod.rs | 308 +++--------------------------------------- src/ui/popup.rs | 158 ++++++++++++++++++++++ src/ui/request_tab.rs | 159 ++++++++++++++++++++++ 5 files changed, 356 insertions(+), 292 deletions(-) create mode 100644 src/ui/popup.rs create mode 100644 src/ui/request_tab.rs diff --git a/src/app/mod.rs b/src/app/mod.rs index aafe5f4..658e93c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -3,7 +3,7 @@ pub mod form; use crate::event::input::Input; use clap::ValueEnum; use form::Form; -use std::collections::HashMap; +use std::{collections::HashMap, str::FromStr}; use tokio::sync::mpsc::{channel, Receiver, Sender}; @@ -15,7 +15,7 @@ pub enum InputMode { Insert, } -#[derive(Clone, PartialEq, ValueEnum)] +#[derive(Clone, PartialEq)] pub enum RequestMethod { Get, Post, @@ -36,6 +36,21 @@ impl ToString for RequestMethod { } } +impl FromStr for RequestMethod { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "get" => Ok(Self::Get), + "post" => Ok(Self::Post), + "put" => Ok(Self::Put), + "delete" => Ok(Self::Delete), + "patch" => Ok(Self::Patch), + _ => Err("Invalid method".to_string()), + } + } +} + pub trait OrderNavigation: Clone + PartialEq { fn get_order(&self) -> Vec where diff --git a/src/cli.rs b/src/cli.rs index f66da8a..9cd4bcb 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,10 +3,10 @@ use clap::Parser; use crate::app::RequestMethod; #[derive(Parser)] -#[command(version, about, long_about=None)] +#[command(version, about, long_about=None, author)] pub struct Cli { pub endpoint: Option, - #[arg(value_enum, default_value = "get", short, long)] + #[arg(default_value = "get", short = 'X', long, value_parser = clap::value_parser!(RequestMethod))] pub method: Option, } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index bacff65..559d186 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,22 +1,23 @@ mod input; +mod popup; +mod request_tab; mod syntax; -use std::{io::Stdout, iter::once}; +use std::io::Stdout; +use popup::render_popup; use ratatui::{ - prelude::{Alignment, Constraint, CrosstermBackend, Direction, Layout, Rect}, + prelude::{Alignment, Constraint, CrosstermBackend, Direction, Layout}, style::{Color, Style}, text::Span, - widgets::{Block, Borders, Clear, Paragraph, Row, Table, TableState, Tabs}, + widgets::{Block, Borders, Paragraph, Tabs}, Frame, }; +use request_tab::render_request_tab; -use crate::app::{ - App, AppBlock, AppPopup, BodyContentType, BodyType, InputMode, OrderNavigation, RequestMethod, - RequestTab, -}; +use crate::app::{App, AppBlock, InputMode, OrderNavigation, RequestMethod}; -use self::input::{create_input, create_textarea}; +use self::input::create_input; fn selectable_block(block: AppBlock, app: &App) -> Block { let is_selected = block == app.selected_block && app.popup.is_none(); @@ -113,146 +114,22 @@ pub fn draw(frame: &mut Frame>, app: &mut App) { frame.render_widget(tab, request_chunks[0]); - match app.request_tab { - RequestTab::Body => { - let body_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(0), Constraint::Length(3)]) - .split(request_chunks[1]); - - let content_type_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(body_chunks[1]); - - let content_type_mode_p = Paragraph::new(match app.body_content_type { - BodyContentType::Text(_) => "Text", - BodyContentType::Form => "Form", - }) - .block(selectable_block(AppBlock::RequestContent, app).title("Content")) - .alignment(Alignment::Center); - - frame.render_widget( - content_type_mode_p, - match app.body_content_type { - BodyContentType::Text(_) => content_type_chunks[0], - BodyContentType::Form => body_chunks[1], - }, - ); - - if let BodyContentType::Text(body_type) = app.body_content_type.clone() { - let content_type_format_p = Paragraph::new(match body_type { - BodyType::Json => "JSON", - BodyType::Raw => "Raw", - BodyType::Xml => "XML", - }) - .block(selectable_block(AppBlock::RequestContent, app).title("Type")) - .style(Style::default().fg(Color::Yellow)) - .alignment(Alignment::Center); - - frame.render_widget(content_type_format_p, content_type_chunks[1]); - - let raw_body_input = create_textarea(&app.raw_body, app) - .block(selectable_block(AppBlock::RequestContent, app).title("Body")); - - frame.render_widget(raw_body_input, body_chunks[0]); - } else { - let rows: Vec = app - .body_form - .iter() - .map(|(key, value)| { - Row::new(vec![key.clone(), value.clone()]) - .style(Style::default().fg(Color::White)) - }) - .collect(); - - let table = Table::new(rows) - .header( - Row::new(vec!["Key", "Value"]) - .style(Style::default().fg(Color::Yellow)) - .bottom_margin(1), - ) - .widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]) - .highlight_style(Style::default().fg(Color::Green)) - .highlight_symbol(">> ") - .block( - selectable_block(AppBlock::RequestContent, app) - .title("Body") - .padding(ratatui::widgets::Padding::new(1, 1, 1, 1)), - ); - - let mut state = TableState::default(); - - state.select(Some(app.selected_form_field.into())); - - frame.render_stateful_widget(table, body_chunks[0], &mut state); - } - } - RequestTab::Headers => { - let rows: Vec = app - .headers - .iter() - .map(|(key, value)| { - Row::new(vec![key.clone(), value.clone()]) - .style(Style::default().fg(Color::White)) - }) - .collect(); - - let table = Table::new(rows) - .header( - Row::new(vec!["Key", "Value"]) - .style(Style::default().fg(Color::Yellow)) - .bottom_margin(1), - ) - .widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]) - .highlight_style(Style::default().fg(Color::Green)) - .highlight_symbol(">> ") - .block( - selectable_block(AppBlock::RequestContent, app) - .title("Headers") - .padding(ratatui::widgets::Padding::new(1, 1, 1, 1)), - ); + render_request_tab(app, frame, request_chunks.to_vec()); - let mut state = TableState::default(); + render_response(app, frame, response_chunks.to_vec()); - state.select(Some(app.selected_header.into())); - - frame.render_stateful_widget(table, request_chunks[1], &mut state); - } - RequestTab::Query => { - let rows: Vec = app - .query_params - .iter() - .map(|(key, value)| { - Row::new(vec![key.clone(), value.clone()]) - .style(Style::default().fg(Color::White)) - }) - .collect(); - - let table = Table::new(rows) - .header( - Row::new(vec!["Key", "Value"]) - .style(Style::default().fg(Color::Yellow)) - .bottom_margin(1), - ) - .widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]) - .highlight_style(Style::default().fg(Color::Green)) - .highlight_symbol(">> ") - .block( - selectable_block(AppBlock::RequestContent, app) - .title("Query Parameters") - .padding(ratatui::widgets::Padding::new(1, 1, 1, 1)), - ); - - let mut state = TableState::default(); - - state.select(Some(app.selected_query_param.into())); + frame.render_widget(help_p, main_chunks[2]); - frame.render_stateful_widget(table, request_chunks[1], &mut state); - } - _ => {} + if let Some(_) = app.popup { + render_popup(app, frame); } +} +fn render_response( + app: &mut App, + frame: &mut Frame>, + response_chunks: Vec, +) { match app.response.as_ref() { Some(r) => { let lines_count = u16::try_from(r.text.lines().count()).unwrap_or(1); @@ -318,149 +195,4 @@ pub fn draw(frame: &mut Frame>, app: &mut App) { frame.render_widget(status_blank, response_chunks[1]); } } - - frame.render_widget(help_p, main_chunks[2]); - - match app.popup.as_ref() { - Some(AppPopup::ChangeMethod) => { - let block = Block::default() - .title("Select method") - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::White)); - - let methods = app.method.get_order(); - - let methods_blocks = methods.iter().enumerate().map(|(i, method)| { - let cloned_method = method.clone(); - - let border_style = match cloned_method == app.method.clone() { - true => Style::default().fg(Color::Green), - false => Style::default().fg(Color::White), - }; - - let style = Style::default().fg(match cloned_method { - RequestMethod::Get => Color::Green, - RequestMethod::Post => Color::Blue, - RequestMethod::Put => Color::Yellow, - RequestMethod::Delete => Color::Red, - RequestMethod::Patch => Color::Magenta, - }); - - let block = Paragraph::new(cloned_method.to_string()) - .style(style) - .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(border_style), - ); - - (i, block) - }); - - let height = app.method.get_order().len() as u16 * 3; - - let width = 40; - - let area = centered_rect(width, height + 4, frame.size()); - - frame.render_widget(Clear, area); - frame.render_widget(block, area); - - methods_blocks.for_each(|(index, p)| { - frame.render_widget( - p, - Rect::new(area.x + 2, area.y + index as u16 * 3 + 1, width - 4, 3), - ); - }); - - let help_p = Paragraph::new("Use j/k to navigate, Enter to select") - .style(Style::default().fg(Color::White)) - .alignment(Alignment::Center); - - frame.render_widget( - help_p, - Rect::new(area.x + 2, area.y + height + 2, width - 4, 1), - ); - } - Some(AppPopup::FormPopup(form)) => { - let block = Block::default() - .title(form.title.clone()) - .title_alignment(Alignment::Center) - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Blue)); - - let visible_fields = form.visible_fields(); - - let height = visible_fields.len() * 3 + 4; - - let area = centered_rect(70, height as u16, frame.size()); - - let inputs = visible_fields.iter().enumerate().map(|(index, field)| { - let input = create_input(&field.input, &app, index == form.selected_field as usize) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg( - if index == form.selected_field - && app.input_mode == InputMode::Insert - { - Color::Green - } else if index == form.selected_field { - Color::Blue - } else { - Color::White - }, - )) - .title(field.label.clone()), - ); - - (index, input) - }); - - frame.render_widget(Clear, area); - frame.render_widget(block, area); - - inputs.for_each(|(index, p)| { - frame.render_widget( - p, - Rect::new(area.x + 2, area.y + index as u16 * 3 + 1, area.width - 4, 3), - ); - }); - - frame.render_widget( - Paragraph::new("Press Enter to Accept Changes") - .style(Style::default().fg(Color::White)) - .alignment(Alignment::Center), - Rect::new(area.x + 2, area.y + height as u16 - 2, area.width - 4, 1), - ); - } - None => {} - } -} - -fn centered_rect(width: u16, height: u16, r: Rect) -> Rect { - let popup_layout = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length((r.height - height) / 2), - Constraint::Length(height), - Constraint::Length((r.height - height) / 2), - ] - .as_ref(), - ) - .split(r); - - Layout::default() - .direction(Direction::Horizontal) - .constraints( - [ - Constraint::Length((r.width - width) / 2), - Constraint::Length(width), - Constraint::Length((r.width - width) / 2), - ] - .as_ref(), - ) - .split(popup_layout[1])[1] } diff --git a/src/ui/popup.rs b/src/ui/popup.rs new file mode 100644 index 0000000..6a983fe --- /dev/null +++ b/src/ui/popup.rs @@ -0,0 +1,158 @@ +use std::io::Stdout; + +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout, Rect}, + prelude::CrosstermBackend, + style::{Color, Style}, + widgets::{Block, Borders, Clear, Paragraph}, + Frame, +}; + +use crate::app::{App, AppPopup, InputMode, OrderNavigation, RequestMethod}; + +use super::input::create_input; + +pub fn render_popup(app: &App, frame: &mut Frame<'_, CrosstermBackend>) { + match app.popup.as_ref() { + Some(AppPopup::ChangeMethod) => { + let block = Block::default() + .title("Select method") + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::White)); + + let methods = app.method.get_order(); + + let methods_blocks = methods.iter().enumerate().map(|(i, method)| { + let cloned_method = method.clone(); + + let border_style = match cloned_method == app.method.clone() { + true => Style::default().fg(Color::Green), + false => Style::default().fg(Color::White), + }; + + let style = Style::default().fg(match cloned_method { + RequestMethod::Get => Color::Green, + RequestMethod::Post => Color::Blue, + RequestMethod::Put => Color::Yellow, + RequestMethod::Delete => Color::Red, + RequestMethod::Patch => Color::Magenta, + }); + + let block = Paragraph::new(cloned_method.to_string()) + .style(style) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(border_style), + ); + + (i, block) + }); + + let height = app.method.get_order().len() as u16 * 3; + + let width = 40; + + let area = centered_rect(width, height + 4, frame.size()); + + frame.render_widget(Clear, area); + frame.render_widget(block, area); + + methods_blocks.for_each(|(index, p)| { + frame.render_widget( + p, + Rect::new(area.x + 2, area.y + index as u16 * 3 + 1, width - 4, 3), + ); + }); + + let help_p = Paragraph::new("Use j/k to navigate, Enter to select") + .style(Style::default().fg(Color::White)) + .alignment(Alignment::Center); + + frame.render_widget( + help_p, + Rect::new(area.x + 2, area.y + height + 2, width - 4, 1), + ); + } + Some(AppPopup::FormPopup(form)) => { + let block = Block::default() + .title(form.title.clone()) + .title_alignment(Alignment::Center) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Blue)); + + let visible_fields = form.visible_fields(); + + let height = visible_fields.len() * 3 + 4; + + let area = centered_rect(70, height as u16, frame.size()); + + let inputs = visible_fields.iter().enumerate().map(|(index, field)| { + let input = create_input(&field.input, &app, index == form.selected_field as usize) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg( + if index == form.selected_field + && app.input_mode == InputMode::Insert + { + Color::Green + } else if index == form.selected_field { + Color::Blue + } else { + Color::White + }, + )) + .title(field.label.clone()), + ); + + (index, input) + }); + + frame.render_widget(Clear, area); + frame.render_widget(block, area); + + inputs.for_each(|(index, p)| { + frame.render_widget( + p, + Rect::new(area.x + 2, area.y + index as u16 * 3 + 1, area.width - 4, 3), + ); + }); + + frame.render_widget( + Paragraph::new("Press Enter to Accept Changes") + .style(Style::default().fg(Color::White)) + .alignment(Alignment::Center), + Rect::new(area.x + 2, area.y + height as u16 - 2, area.width - 4, 1), + ); + } + None => {} + } +} + +fn centered_rect(width: u16, height: u16, r: Rect) -> Rect { + let popup_layout = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length((r.height - height) / 2), + Constraint::Length(height), + Constraint::Length((r.height - height) / 2), + ] + .as_ref(), + ) + .split(r); + + Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Length((r.width - width) / 2), + Constraint::Length(width), + Constraint::Length((r.width - width) / 2), + ] + .as_ref(), + ) + .split(popup_layout[1])[1] +} diff --git a/src/ui/request_tab.rs b/src/ui/request_tab.rs new file mode 100644 index 0000000..d2d0dbc --- /dev/null +++ b/src/ui/request_tab.rs @@ -0,0 +1,159 @@ +use std::io::Stdout; + +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout, Rect}, + prelude::CrosstermBackend, + style::{Color, Style}, + widgets::{Paragraph, Row, Table, TableState}, + Frame, +}; + +use crate::app::{App, AppBlock, BodyContentType, BodyType, RequestTab}; + +use super::{input::create_textarea, selectable_block}; + +pub fn render_request_tab( + app: &App, + frame: &mut Frame<'_, CrosstermBackend>, + request_chunks: Vec, +) { + match app.request_tab { + RequestTab::Body => { + let body_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(0), Constraint::Length(3)]) + .split(request_chunks[1]); + + let content_type_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(body_chunks[1]); + + let content_type_mode_p = Paragraph::new(match app.body_content_type { + BodyContentType::Text(_) => "Text", + BodyContentType::Form => "Form", + }) + .block(selectable_block(AppBlock::RequestContent, app).title("Content")) + .alignment(Alignment::Center); + + frame.render_widget( + content_type_mode_p, + match app.body_content_type { + BodyContentType::Text(_) => content_type_chunks[0], + BodyContentType::Form => body_chunks[1], + }, + ); + + if let BodyContentType::Text(body_type) = app.body_content_type.clone() { + let content_type_format_p = Paragraph::new(match body_type { + BodyType::Json => "JSON", + BodyType::Raw => "Raw", + BodyType::Xml => "XML", + }) + .block(selectable_block(AppBlock::RequestContent, app).title("Type")) + .style(Style::default().fg(Color::Yellow)) + .alignment(Alignment::Center); + + frame.render_widget(content_type_format_p, content_type_chunks[1]); + + let raw_body_input = create_textarea(&app.raw_body, app) + .block(selectable_block(AppBlock::RequestContent, app).title("Body")); + + frame.render_widget(raw_body_input, body_chunks[0]); + } else { + let rows: Vec = app + .body_form + .iter() + .map(|(key, value)| { + Row::new(vec![key.clone(), value.clone()]) + .style(Style::default().fg(Color::White)) + }) + .collect(); + + let table = Table::new(rows) + .header( + Row::new(vec!["Key", "Value"]) + .style(Style::default().fg(Color::Yellow)) + .bottom_margin(1), + ) + .widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]) + .highlight_style(Style::default().fg(Color::Green)) + .highlight_symbol(">> ") + .block( + selectable_block(AppBlock::RequestContent, app) + .title("Body") + .padding(ratatui::widgets::Padding::new(1, 1, 1, 1)), + ); + + let mut state = TableState::default(); + + state.select(Some(app.selected_form_field.into())); + + frame.render_stateful_widget(table, body_chunks[0], &mut state); + } + } + RequestTab::Headers => { + let rows: Vec = app + .headers + .iter() + .map(|(key, value)| { + Row::new(vec![key.clone(), value.clone()]) + .style(Style::default().fg(Color::White)) + }) + .collect(); + + let table = Table::new(rows) + .header( + Row::new(vec!["Key", "Value"]) + .style(Style::default().fg(Color::Yellow)) + .bottom_margin(1), + ) + .widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]) + .highlight_style(Style::default().fg(Color::Green)) + .highlight_symbol(">> ") + .block( + selectable_block(AppBlock::RequestContent, app) + .title("Headers") + .padding(ratatui::widgets::Padding::new(1, 1, 1, 1)), + ); + + let mut state = TableState::default(); + + state.select(Some(app.selected_header.into())); + + frame.render_stateful_widget(table, request_chunks[1], &mut state); + } + RequestTab::Query => { + let rows: Vec = app + .query_params + .iter() + .map(|(key, value)| { + Row::new(vec![key.clone(), value.clone()]) + .style(Style::default().fg(Color::White)) + }) + .collect(); + + let table = Table::new(rows) + .header( + Row::new(vec!["Key", "Value"]) + .style(Style::default().fg(Color::Yellow)) + .bottom_margin(1), + ) + .widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]) + .highlight_style(Style::default().fg(Color::Green)) + .highlight_symbol(">> ") + .block( + selectable_block(AppBlock::RequestContent, app) + .title("Query Parameters") + .padding(ratatui::widgets::Padding::new(1, 1, 1, 1)), + ); + + let mut state = TableState::default(); + + state.select(Some(app.selected_query_param.into())); + + frame.render_stateful_widget(table, request_chunks[1], &mut state); + } + _ => {} + } +}