diff --git a/hac-client/src/pages/collection_viewer/collection_viewer.rs b/hac-client/src/pages/collection_viewer/collection_viewer.rs index 40f2393..1a76996 100755 --- a/hac-client/src/pages/collection_viewer/collection_viewer.rs +++ b/hac-client/src/pages/collection_viewer/collection_viewer.rs @@ -7,20 +7,17 @@ use crate::pages::collection_viewer::request_editor::{RequestEditor, RequestEdit use crate::pages::collection_viewer::request_uri::{RequestUri, RequestUriEvent}; use crate::pages::collection_viewer::response_viewer::{ResponseViewer, ResponseViewerEvent}; use crate::pages::collection_viewer::sidebar::{self, Sidebar, SidebarEvent}; -use crate::pages::input::Input; -use crate::pages::overlay::draw_overlay; use crate::pages::{Eventful, Renderable}; use std::cell::RefCell; use std::collections::HashMap; -use std::ops::{Add, Div, Sub}; +use std::ops::{Add, Div}; use std::rc::Rc; -use std::sync::{Arc, RwLock}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::layout::{Constraint, Direction, Layout, Rect}; -use ratatui::style::{Style, Stylize}; -use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph}; +use ratatui::style::Stylize; +use ratatui::widgets::{Block, Clear}; use ratatui::Frame; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; @@ -34,12 +31,12 @@ pub struct ExplorerLayout { pub create_req_form: Rect, } -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, Clone)] pub enum CollectionViewerOverlay { None, CreateRequest, + EditRequest, CreateDirectory, - RequestMethod, HeadersHelp, HeadersDelete, HeadersForm(usize), @@ -73,64 +70,6 @@ impl PaneFocus { } } -#[derive(Debug, Default, PartialEq)] -pub enum FormFocus { - #[default] - NameInput, - ReqButton, - DirButton, - ConfirmButton, - CancelButton, -} - -impl FormFocus { - fn prev(&self) -> FormFocus { - match self { - Self::NameInput => FormFocus::CancelButton, - Self::ReqButton => FormFocus::NameInput, - Self::DirButton => FormFocus::ReqButton, - Self::ConfirmButton => FormFocus::DirButton, - Self::CancelButton => FormFocus::ConfirmButton, - } - } - - fn next(&self) -> FormFocus { - match self { - Self::NameInput => FormFocus::ReqButton, - Self::ReqButton => FormFocus::DirButton, - Self::DirButton => FormFocus::ConfirmButton, - Self::ConfirmButton => FormFocus::CancelButton, - Self::CancelButton => FormFocus::NameInput, - } - } -} - -#[derive(Debug)] -struct CreateReqFormState { - pub req_kind: CreateReqKind, - pub req_name: String, - pub focus: FormFocus, - pub method: RequestMethod, -} - -impl Default for CreateReqFormState { - fn default() -> Self { - CreateReqFormState { - req_kind: CreateReqKind::default(), - req_name: String::new(), - focus: FormFocus::default(), - method: RequestMethod::Get, - } - } -} - -#[derive(Debug, Default, PartialEq)] -pub enum CreateReqKind { - #[default] - Request, - Directory, -} - #[derive(Debug)] pub struct CollectionViewer<'cv> { response_viewer: ResponseViewer<'cv>, @@ -144,8 +83,6 @@ pub struct CollectionViewer<'cv> { collection_sync_timer: std::time::Instant, collection_store: Rc>, - create_req_form_state: CreateReqFormState, - responses_map: HashMap>>, response_rx: UnboundedReceiver, request_tx: UnboundedSender, @@ -187,7 +124,6 @@ impl<'cv> CollectionViewer<'cv> { layout, global_command_sender: None, collection_sync_timer: std::time::Instant::now(), - create_req_form_state: CreateReqFormState::default(), responses_map: HashMap::default(), response_rx, request_tx, @@ -219,299 +155,6 @@ impl<'cv> CollectionViewer<'cv> { } } - fn draw_create_request_form(&mut self, frame: &mut Frame) { - let size = self.layout.create_req_form; - let item_height = 3; - let name_input_size = - Rect::new(size.x.add(1), size.y.add(1), size.width.sub(1), item_height); - - let req_button_size = Rect::new( - size.x.add(1), - size.y.add(item_height).add(1), - size.width.sub(2).div(2), - item_height, - ); - - let dir_button_size = Rect::new( - size.x.add(size.width.div(2)), - size.y.add(item_height).add(1), - size.width.div_ceil(2), - item_height, - ); - - let confirm_button_size = Rect::new( - size.x.add(1), - size.y.add(size.height.sub(4)), - size.width.sub(2).div(2), - item_height, - ); - - let cancel_button_size = Rect::new( - size.x.add(size.width.div(2)), - size.y.add(size.height.sub(4)), - size.width.div_ceil(2), - item_height, - ); - - let mut input = Input::new(self.colors, "Name".into()); - if self.create_req_form_state.focus.eq(&FormFocus::NameInput) { - input.focus(); - } - - let req_button_border_style = match ( - &self.create_req_form_state.focus, - &self.create_req_form_state.req_kind, - ) { - (FormFocus::ReqButton, _) => Style::default().fg(self.colors.bright.magenta), - (_, CreateReqKind::Request) => Style::default().fg(self.colors.normal.red), - (_, _) => Style::default().fg(self.colors.bright.black), - }; - - let dir_button_border_style = match ( - &self.create_req_form_state.focus, - &self.create_req_form_state.req_kind, - ) { - (FormFocus::DirButton, _) => Style::default().fg(self.colors.bright.magenta), - (_, CreateReqKind::Directory) => Style::default().fg(self.colors.normal.red), - (_, _) => Style::default().fg(self.colors.bright.black), - }; - - let confirm_button_border_style = match self.create_req_form_state.focus { - FormFocus::ConfirmButton => Style::default().fg(self.colors.bright.magenta), - _ => Style::default().fg(self.colors.bright.black), - }; - let cancel_button_border_style = match self.create_req_form_state.focus { - FormFocus::CancelButton => Style::default().fg(self.colors.bright.magenta), - _ => Style::default().fg(self.colors.bright.black), - }; - - let req_button = - Paragraph::new("Request".fg(self.colors.normal.white).into_centered_line()).block( - Block::default() - .borders(Borders::ALL) - .border_style(req_button_border_style), - ); - - let dir_button = Paragraph::new( - "Directory" - .fg(self.colors.normal.white) - .into_centered_line(), - ) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(dir_button_border_style), - ); - - let confirm_button = - Paragraph::new("Confirm".fg(self.colors.normal.green).into_centered_line()).block( - Block::default() - .borders(Borders::ALL) - .border_style(confirm_button_border_style), - ); - - let cancel_button = - Paragraph::new("Cancel".fg(self.colors.normal.red).into_centered_line()).block( - Block::default() - .borders(Borders::ALL) - .border_style(cancel_button_border_style), - ); - - let full_block = Block::default() - .padding(Padding::uniform(1)) - .style(Style::default().bg(self.colors.primary.background)); - - draw_overlay(self.colors, frame.size(), "新", frame); - frame.render_widget(Clear, size); - frame.render_widget(full_block, size); - frame.render_widget(req_button, req_button_size); - frame.render_widget(dir_button, dir_button_size); - frame.render_widget(confirm_button, confirm_button_size); - frame.render_widget(cancel_button, cancel_button_size); - - frame.render_stateful_widget( - input, - name_input_size, - &mut self.create_req_form_state.req_name, - ); - } - - fn handle_create_request_key_event( - &mut self, - key_event: KeyEvent, - ) -> anyhow::Result> { - match ( - key_event.code, - key_event.modifiers, - &self.create_req_form_state.focus, - ) { - (KeyCode::Tab, KeyModifiers::NONE, _) => { - self.create_req_form_state.focus = - FormFocus::next(&self.create_req_form_state.focus); - } - (KeyCode::BackTab, KeyModifiers::SHIFT, _) => { - self.create_req_form_state.focus = - FormFocus::prev(&self.create_req_form_state.focus); - } - (KeyCode::Char(c), _, FormFocus::NameInput) => { - self.create_req_form_state.req_name.push(c); - } - (KeyCode::Backspace, _, FormFocus::NameInput) => { - self.create_req_form_state.req_name.pop(); - } - (KeyCode::Enter, _, FormFocus::ReqButton) => { - self.create_req_form_state.req_kind = CreateReqKind::Request; - } - (KeyCode::Enter, _, FormFocus::DirButton) => { - self.create_req_form_state.req_kind = CreateReqKind::Directory; - } - (KeyCode::Enter, _, FormFocus::ConfirmButton) => { - self.create_or_ask_for_request_method() - } - (KeyCode::Enter, _, FormFocus::CancelButton) | (KeyCode::Esc, _, _) => { - self.create_req_form_state = CreateReqFormState::default(); - self.collection_store.borrow_mut().clear_overlay(); - } - _ => {} - } - - Ok(None) - } - - fn handle_request_method_key_event( - &mut self, - key_event: KeyEvent, - ) -> anyhow::Result> { - 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(); - } - (KeyCode::Enter, _) => { - self.create_and_sync_request(); - } - (KeyCode::Esc, _) => { - self.create_req_form_state = CreateReqFormState::default(); - self.collection_store.borrow_mut().clear_overlay(); - } - _ => {} - } - - Ok(None) - } - - fn draw_request_method_form(&mut self, frame: &mut Frame) { - let size = self.layout.create_req_form; - - let item_height = 3; - let mut buttons = vec![]; - let reqs = vec![ - RequestMethod::Get, - RequestMethod::Post, - RequestMethod::Put, - RequestMethod::Patch, - RequestMethod::Delete, - ]; - - for item in reqs { - let border_style = if self.create_req_form_state.method == item { - Style::default().fg(self.colors.bright.magenta) - } else { - Style::default().fg(self.colors.bright.black) - }; - - buttons.push( - Paragraph::new( - item.to_string() - .fg(self.colors.normal.white) - .into_centered_line(), - ) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(border_style), - ), - ); - } - - let full_block = Block::default() - .padding(Padding::uniform(1)) - .style(Style::default().bg(self.colors.primary.background)); - - draw_overlay(self.colors, frame.size(), "新", frame); - frame.render_widget(Clear, size); - frame.render_widget(full_block, size); - - let expand_last = buttons.len() % 2 != 0; - let right_half = size.width.div(2); - let buttons_len = buttons.len(); - for (i, button) in buttons.into_iter().enumerate() { - let padding = if i % 2 != 0 { right_half } else { 0 }; - let width = if i.eq(&buttons_len.sub(1)) && expand_last { - size.width.sub(1) - } else { - size.width.div(2) - }; - let button_size = Rect::new( - size.x.add(padding).add(1), - size.y.add(1).add(item_height * (i / 2) as u16), - width, - item_height, - ); - - frame.render_widget(button, button_size); - } - } - - fn create_or_ask_for_request_method(&mut self) { - let form_state = &self.create_req_form_state; - if form_state.req_kind.eq(&CreateReqKind::Request) { - self.collection_store - .borrow_mut() - .push_overlay(CollectionViewerOverlay::RequestMethod); - return; - } - self.create_and_sync_request(); - } - - fn create_and_sync_request(&mut self) { - let form_state = &self.create_req_form_state; - let new_request = match form_state.req_kind { - CreateReqKind::Request => RequestKind::Single(Arc::new(RwLock::new(Request { - id: uuid::Uuid::new_v4().to_string(), - name: form_state.req_name.clone(), - headers: None, - method: form_state.method.clone(), - uri: String::default(), - body: None, - body_type: None, - }))), - CreateReqKind::Directory => RequestKind::Nested(Directory { - id: uuid::Uuid::new_v4().to_string(), - name: form_state.req_name.clone(), - requests: Arc::new(RwLock::new(vec![])), - }), - }; - - let mut store_mut = self.collection_store.borrow_mut(); - if let RequestKind::Single(ref req) = new_request { - store_mut.dispatch(CollectionStoreAction::SetSelectedRequest(Some(req.clone()))); - store_mut.dispatch(CollectionStoreAction::SetHoveredRequest(Some( - new_request.get_id(), - ))); - } - - store_mut.dispatch(CollectionStoreAction::InsertRequest(new_request)); - // dropping the borrow so we can sync the changes - drop(store_mut); - - self.sidebar.rebuild_tree_view(); - self.create_req_form_state = CreateReqFormState::default(); - self.collection_store.borrow_mut().clear_overlay(); - - // TODO: maybe the collection store should be responsible for syncing to disk - self.sync_collection_changes(); - } - fn sync_collection_changes(&mut self) { let sender = self .global_command_sender @@ -621,7 +264,9 @@ impl Renderable for CollectionViewer<'_> { CollectionViewerOverlay::CreateDirectory => { self.sidebar.draw_overlay(frame, overlay)?; } - CollectionViewerOverlay::RequestMethod => self.draw_request_method_form(frame), + CollectionViewerOverlay::EditRequest => { + self.sidebar.draw_overlay(frame, overlay)?; + } CollectionViewerOverlay::HeadersHelp => { self.request_editor.draw_overlay(frame, overlay)? } @@ -752,11 +397,16 @@ impl Eventful for CollectionViewer<'_> { .collection_store .borrow_mut() .push_overlay(CollectionViewerOverlay::CreateRequest), + Some(SidebarEvent::EditRequest) => self + .collection_store + .borrow_mut() + .push_overlay(CollectionViewerOverlay::EditRequest), Some(SidebarEvent::CreateDirectory) => self .collection_store .borrow_mut() .push_overlay(CollectionViewerOverlay::CreateDirectory), Some(SidebarEvent::RemoveSelection) => self.update_selection(None), + Some(SidebarEvent::SyncCollection) => self.sync_collection_changes(), Some(SidebarEvent::Quit) => return Ok(Some(Command::Quit)), // when theres no event we do nothing None => {} 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 3c427c4..4e2d351 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 @@ -301,12 +301,12 @@ impl Eventful for HeadersEditor<'_> { fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { let overlay = self.collection_store.borrow().peek_overlay(); - if overlay.eq(&CollectionViewerOverlay::HeadersHelp) { + if let CollectionViewerOverlay::HeadersHelp = overlay { self.collection_store.borrow_mut().pop_overlay(); return Ok(None); } - if overlay.eq(&CollectionViewerOverlay::HeadersDelete) { + if let CollectionViewerOverlay::HeadersDelete = overlay { match self.delete_prompt.handle_key_event(key_event)? { Some(HeadersEditorDeletePromptEvent::Cancel) => { self.collection_store.borrow_mut().pop_overlay(); @@ -379,7 +379,7 @@ impl Eventful for HeadersEditor<'_> { drop(request); let mut store = self.collection_store.borrow_mut(); let overlay = store.peek_overlay(); - if overlay.eq(&CollectionViewerOverlay::HeadersHelp) { + if let CollectionViewerOverlay::HeadersHelp = overlay { store.clear_overlay(); } else { store.push_overlay(CollectionViewerOverlay::HeadersHelp); diff --git a/hac-client/src/pages/collection_viewer/sidebar.rs b/hac-client/src/pages/collection_viewer/sidebar.rs index 1611a40..c413a6c 100755 --- a/hac-client/src/pages/collection_viewer/sidebar.rs +++ b/hac-client/src/pages/collection_viewer/sidebar.rs @@ -1,11 +1,15 @@ mod create_request_form; +mod edit_request_form; +mod request_form; use hac_core::collection::types::{Request, RequestKind, RequestMethod}; use crate::pages::collection_viewer::collection_store::{CollectionStore, CollectionStoreAction}; use crate::pages::collection_viewer::collection_viewer::{CollectionViewerOverlay, PaneFocus}; -use crate::pages::collection_viewer::sidebar::create_request_form::CreateRequestForm; -use crate::pages::collection_viewer::sidebar::create_request_form::CreateRequestFormEvent; +use crate::pages::collection_viewer::sidebar::request_form::RequestForm; +use crate::pages::collection_viewer::sidebar::request_form::RequestFormCreate; +use crate::pages::collection_viewer::sidebar::request_form::RequestFormEdit; +use crate::pages::collection_viewer::sidebar::request_form::RequestFormEvent; use crate::pages::{Eventful, Renderable}; use std::cell::RefCell; @@ -26,23 +30,51 @@ pub enum SidebarEvent { /// user pressed `CreateRequest (n)` hotkey, which should notify the caller to open /// the create_request_form and properly handle the creation of a new request CreateRequest, + /// user pressed `EditRequest (e)` hotkey, which should notify the caller to open + /// the create_request_form and propery handle the editing of the existing request + EditRequest, /// user pressed `CreateDirectory (d)` hotkey, which should notify the caller to open /// the `create_directory_form` overlay to create a new directory on the collection CreateDirectory, /// user pressed `Esc` so we notify the caller to remove the selection from /// this pane, essentially bubbling the key handling scope to the caller RemoveSelection, + /// this event is used when a request or directory is created, this notify the parent + /// to sync changes with the file system. + SyncCollection, /// user pressed a hotkey to quit the application, so we bubble up so the caller /// can do a few things before bubbling the quit request further up Quit, } +#[derive(Debug)] +enum FormVariant<'sbar> { + Create(RequestForm<'sbar, RequestFormCreate>), + Edit(RequestForm<'sbar, RequestFormEdit>), +} + +/// this is just a helper trait to be able to return the inner reference of the form +/// from the enum as we cannot return it like: +/// ```rust +/// &mut dyn Renderable + Eventful; +/// ``` +pub trait RequestFormTrait: Renderable + Eventful {} + +impl FormVariant<'_> { + pub fn inner(&mut self) -> &mut dyn RequestFormTrait { + match self { + FormVariant::Create(form) => form, + FormVariant::Edit(form) => form, + } + } +} + #[derive(Debug)] pub struct Sidebar<'sbar> { colors: &'sbar hac_colors::Colors, lines: Vec>, collection_store: Rc>, - create_request_form: CreateRequestForm<'sbar>, + request_form: FormVariant<'sbar>, } impl<'sbar> Sidebar<'sbar> { @@ -52,7 +84,10 @@ impl<'sbar> Sidebar<'sbar> { ) -> Self { let mut sidebar = Self { colors, - create_request_form: CreateRequestForm::new(colors, collection_store.clone()), + request_form: FormVariant::Create(RequestForm::::new( + colors, + collection_store.clone(), + )), lines: vec![], collection_store, }; @@ -81,7 +116,10 @@ impl<'sbar> Sidebar<'sbar> { ) -> anyhow::Result<()> { match overlay { CollectionViewerOverlay::CreateRequest => { - self.create_request_form.draw(frame, frame.size())?; + self.request_form.inner().draw(frame, frame.size())?; + } + CollectionViewerOverlay::EditRequest => { + self.request_form.inner().draw(frame, frame.size())?; } CollectionViewerOverlay::CreateDirectory => {} _ => {} @@ -147,17 +185,49 @@ impl<'a> Eventful for Sidebar<'a> { "handled an event to the sidebar while it was not selected" ); - let mut store = self.collection_store.borrow_mut(); + let overlay = self.collection_store.borrow_mut().peek_overlay(); - match store.peek_overlay() { + match overlay { CollectionViewerOverlay::CreateRequest => { - match self.create_request_form.handle_key_event(key_event)? { - Some(CreateRequestFormEvent::Confirm) => return Ok(None), - Some(CreateRequestFormEvent::Cancel) => _ = store.pop_overlay(), + match self.request_form.inner().handle_key_event(key_event)? { + Some(RequestFormEvent::Confirm) => { + let mut store = self.collection_store.borrow_mut(); + store.pop_overlay(); + drop(store); + self.rebuild_tree_view(); + return Ok(Some(SidebarEvent::SyncCollection)); + } + Some(RequestFormEvent::Cancel) => { + let mut store = self.collection_store.borrow_mut(); + store.pop_overlay(); + drop(store); + self.rebuild_tree_view(); + return Ok(None); + } None => return Ok(None), } } CollectionViewerOverlay::CreateDirectory => todo!(), + CollectionViewerOverlay::EditRequest => { + // when editing, we setup the form to display the current header information. + match self.request_form.inner().handle_key_event(key_event)? { + Some(RequestFormEvent::Confirm) => { + let mut store = self.collection_store.borrow_mut(); + store.pop_overlay(); + drop(store); + self.rebuild_tree_view(); + return Ok(Some(SidebarEvent::SyncCollection)); + } + Some(RequestFormEvent::Cancel) => { + let mut store = self.collection_store.borrow_mut(); + store.pop_overlay(); + drop(store); + self.rebuild_tree_view(); + return Ok(None); + } + None => return Ok(None), + } + } _ => {} }; @@ -165,6 +235,8 @@ impl<'a> Eventful for Sidebar<'a> { return Ok(Some(SidebarEvent::Quit)); } + let mut store = self.collection_store.borrow_mut(); + match key_event.code { KeyCode::Enter => { if store.get_requests().is_none() || store.get_hovered_request().is_none() { @@ -183,7 +255,24 @@ impl<'a> Eventful for Sidebar<'a> { } KeyCode::Char('j') | KeyCode::Down => store.dispatch(CollectionStoreAction::HoverNext), KeyCode::Char('k') | KeyCode::Up => store.dispatch(CollectionStoreAction::HoverPrev), - KeyCode::Char('n') => return Ok(Some(SidebarEvent::CreateRequest)), + KeyCode::Char('n') => { + self.request_form = FormVariant::Create(RequestForm::::new( + self.colors, + self.collection_store.clone(), + )); + return Ok(Some(SidebarEvent::CreateRequest)); + } + KeyCode::Char('e') => { + let RequestKind::Single(request) = store.find_hovered_request() else { + return Ok(None); + }; + self.request_form = FormVariant::Edit(RequestForm::::new( + self.colors, + self.collection_store.clone(), + request.clone(), + )); + return Ok(Some(SidebarEvent::EditRequest)); + } KeyCode::Char('d') => return Ok(Some(SidebarEvent::CreateDirectory)), KeyCode::Esc => return Ok(Some(SidebarEvent::RemoveSelection)), _ => {} diff --git a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs index 4848cfc..94971d9 100644 --- a/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs +++ b/hac-client/src/pages/collection_viewer/sidebar/create_request_form.rs @@ -1,68 +1,32 @@ -use hac_core::collection::types::RequestMethod; +use hac_core::collection::types::*; use crate::ascii::LOGO_ASCII; use crate::pages::collection_viewer::collection_store::CollectionStore; -use crate::pages::input::Input; -use crate::pages::overlay::make_overlay; -use crate::pages::{Eventful, Renderable}; +use crate::pages::collection_viewer::sidebar::request_form::FormField; +use crate::pages::collection_viewer::sidebar::request_form::RequestForm; +use crate::pages::collection_viewer::sidebar::request_form::RequestFormCreate; +use crate::pages::collection_viewer::sidebar::request_form::RequestFormEvent; +use crate::pages::collection_viewer::sidebar::RequestFormTrait; +use crate::pages::Eventful; use std::cell::RefCell; -use std::ops::{Add, Div, Sub}; +use std::ops::Sub; use std::rc::Rc; +use std::sync::{Arc, RwLock}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use rand::Rng; -use ratatui::layout::{Constraint, Direction, Layout, Rect}; -use ratatui::style::Stylize; -use ratatui::text::Line; -use ratatui::widgets::{Block, Borders, Paragraph}; -use ratatui::Frame; - -#[derive(Debug, PartialEq)] -enum FormField { - Name, - Method, - Parent, -} - -impl FormField { - fn next(&self) -> Self { - match self { - FormField::Name => FormField::Method, - FormField::Method => FormField::Parent, - FormField::Parent => FormField::Name, - } - } - - fn prev(&self) -> Self { - match self { - FormField::Name => FormField::Parent, - FormField::Method => FormField::Name, - FormField::Parent => FormField::Method, - } - } -} -#[derive(Debug)] -pub struct CreateRequestForm<'crf> { - colors: &'crf hac_colors::Colors, - collection_store: Rc>, - logo_idx: usize, - request_name: String, - request_method: RequestMethod, - /// we store the parent dir uuid so its easier to find it. - parent_dir: Option, - focused_field: FormField, -} +impl<'rf> RequestFormTrait for RequestForm<'rf, RequestFormCreate> {} -impl<'crf> CreateRequestForm<'crf> { +impl<'rf> RequestForm<'rf, RequestFormCreate> { pub fn new( - colors: &'crf hac_colors::Colors, + colors: &'rf hac_colors::Colors, collection_store: Rc>, ) -> Self { let logo_idx = rand::thread_rng().gen_range(0..LOGO_ASCII.len()); - CreateRequestForm { + RequestForm { colors, collection_store, logo_idx, @@ -70,134 +34,17 @@ impl<'crf> CreateRequestForm<'crf> { request_method: RequestMethod::Get, parent_dir: None, focused_field: FormField::Name, + marker: std::marker::PhantomData, + request: None, } } - - fn reset(&mut self) { - self.request_name = String::default(); - self.request_method = RequestMethod::Get; - self.parent_dir = None; - } -} - -impl Renderable for CreateRequestForm<'_> { - fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { - make_overlay(self.colors, self.colors.normal.black, 0.1, frame); - - let mut logo = LOGO_ASCII[self.logo_idx]; - let mut logo_size = logo.len() as u16; - // adding size of the form + spacing + hint - let total_size = logo_size.add(11).add(2); - - let size = frame.size(); - let mut size = Rect::new( - size.width.div(2).sub(32), - size.height - .div(2) - .saturating_sub(logo_size.div(2)) - .saturating_sub(6), - 65, - logo_size.add(12), - ); - - if total_size.ge(&frame.size().height) { - logo = &[]; - logo_size = 0; - size.height = 12; - size.y = size.height.div(2).saturating_sub(5); - } - - if !logo.is_empty() { - let logo = logo - .iter() - .map(|line| Line::from(line.fg(self.colors.normal.red)).centered()) - .collect::>(); - - let logo_size = Rect::new(size.x, size.y, size.width, logo_size); - frame.render_widget(Paragraph::new(logo), logo_size); - } - - let mut name_input = Input::new(self.colors, "Name".into()); - let method_title = Paragraph::new("Method"); - let hint = Paragraph::new( - "[Confirm: Enter] [Cancel: Esc] [Switch field: Tab]".fg(self.colors.bright.black), - ) - .centered(); - - if self.focused_field.eq(&FormField::Name) { - name_input.focus(); - } - - let name_size = Rect::new(size.x, size.y.add(logo_size).add(1), size.width, 3); - let method_title_size = Rect::new(size.x, name_size.y.add(3), size.width, 1); - let methods_size = Rect::new(size.x, method_title_size.y.add(1), size.width, 3); - let parent_size = Rect::new(size.x, methods_size.y.add(3), size.width, 3); - let hint_size = Rect::new(size.x, parent_size.y.add(4), size.width, 1); - - let methods_items = Layout::default() - .direction(Direction::Horizontal) - .constraints((0..5).map(|_| Constraint::Length(13))) - .split(methods_size); - - let parent_name = format!( - "{}None{}", - " ".repeat(parent_size.width.div(2).sub(2).into()), - " ".repeat(parent_size.width.div(2).sub(2).into()) - ); - let parent = Paragraph::new(parent_name) - .centered() - .block( - Block::default() - .title("Parent".fg(self.colors.normal.white)) - .borders(Borders::ALL) - .fg(if self.focused_field.eq(&FormField::Parent) { - self.colors.normal.red - } else { - self.colors.bright.black - }), - ) - .fg(self.colors.bright.black); - - for (idx, method) in RequestMethod::iter().enumerate() { - let border_color = match (&self.request_method, &self.focused_field) { - (m, FormField::Method) if m.eq(method) => self.colors.normal.red, - (m, _) if m.eq(method) => self.colors.bright.blue, - _ => self.colors.bright.black, - }; - let method = Paragraph::new(Line::from(vec![ - format!(" {}", idx.add(1)).fg(self.colors.bright.black), - format!( - " {}{}", - method, - " ".repeat(10.sub(method.to_string().len())) - ) - .fg(self.colors.normal.white), - ])) - .block(Block::default().borders(Borders::ALL).fg(border_color)); - frame.render_widget(method, methods_items[idx]); - } - - frame.render_stateful_widget(name_input, name_size, &mut self.request_name); - frame.render_widget(method_title, method_title_size); - frame.render_widget(parent, parent_size); - frame.render_widget(hint, hint_size); - - Ok(()) - } } -pub enum CreateRequestFormEvent { - Confirm, - Cancel, -} - -impl Eventful for CreateRequestForm<'_> { - type Result = CreateRequestFormEvent; +impl Eventful for RequestForm<'_, RequestFormCreate> { + type Result = RequestFormEvent; #[tracing::instrument(skip_all, err)] fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { - tracing::debug!("{key_event:#?}"); - if let KeyCode::Tab = key_event.code { self.focused_field = self.focused_field.next(); return Ok(None); @@ -209,18 +56,40 @@ impl Eventful for CreateRequestForm<'_> { } if let KeyCode::Enter = key_event.code { + let store = self.collection_store.borrow_mut(); + let collection = store + .get_collection() + .expect("tried to create a request without a collection"); + + let mut collection = collection.borrow_mut(); + let requests = collection + .requests + .get_or_insert(Arc::new(RwLock::new(vec![]))); + let mut requests = requests.write().unwrap(); + + requests.push(RequestKind::Single(Arc::new(RwLock::new(Request { + id: uuid::Uuid::new_v4().to_string(), + body: None, + body_type: None, + headers: None, + method: self.request_method.clone(), + name: self.request_name.clone(), + uri: String::default(), + })))); + + drop(store); self.reset(); - return Ok(Some(CreateRequestFormEvent::Confirm)); + return Ok(Some(RequestFormEvent::Confirm)); } if let KeyCode::Esc = key_event.code { self.reset(); - return Ok(Some(CreateRequestFormEvent::Cancel)); + return Ok(Some(RequestFormEvent::Cancel)); } if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { self.reset(); - return Ok(Some(CreateRequestFormEvent::Cancel)); + return Ok(Some(RequestFormEvent::Cancel)); } match self.focused_field { diff --git a/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs new file mode 100644 index 0000000..aff30e7 --- /dev/null +++ b/hac-client/src/pages/collection_viewer/sidebar/edit_request_form.rs @@ -0,0 +1,112 @@ +use hac_core::collection::types::*; + +use crate::ascii::LOGO_ASCII; +use crate::pages::collection_viewer::collection_store::CollectionStore; +use crate::pages::collection_viewer::sidebar::request_form::FormField; +use crate::pages::collection_viewer::sidebar::request_form::RequestForm; +use crate::pages::collection_viewer::sidebar::request_form::RequestFormEdit; +use crate::pages::collection_viewer::sidebar::request_form::RequestFormEvent; +use crate::pages::collection_viewer::sidebar::RequestFormTrait; +use crate::pages::Eventful; + +use std::cell::RefCell; +use std::ops::Sub; +use std::rc::Rc; +use std::sync::{Arc, RwLock}; + +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use rand::Rng; + +impl<'rf> RequestFormTrait for RequestForm<'rf, RequestFormEdit> {} + +impl<'rf> RequestForm<'rf, RequestFormEdit> { + pub fn new( + colors: &'rf hac_colors::Colors, + collection_store: Rc>, + request: Arc>, + ) -> Self { + let logo_idx = rand::thread_rng().gen_range(0..LOGO_ASCII.len()); + let request_method = request.read().unwrap().method.clone(); + let request_name = request.read().unwrap().name.clone(); + + RequestForm { + colors, + collection_store, + logo_idx, + request_name, + request_method, + parent_dir: None, + focused_field: FormField::Name, + marker: std::marker::PhantomData, + request: Some(request), + } + } +} + +impl Eventful for RequestForm<'_, RequestFormEdit> { + type Result = RequestFormEvent; + + #[tracing::instrument(skip_all, err)] + fn handle_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + if let KeyCode::Tab = key_event.code { + self.focused_field = self.focused_field.next(); + return Ok(None); + } + + if let KeyCode::BackTab = key_event.code { + self.focused_field = self.focused_field.prev(); + return Ok(None); + } + + if let KeyCode::Enter = key_event.code { + let request = self.request.as_mut().unwrap(); + let mut request = request.write().unwrap(); + + request.name.clone_from(&self.request_name); + request.method.clone_from(&self.request_method); + + drop(request); + self.reset(); + return Ok(Some(RequestFormEvent::Confirm)); + } + + if let KeyCode::Esc = key_event.code { + self.reset(); + return Ok(Some(RequestFormEvent::Cancel)); + } + + if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (key_event.code, key_event.modifiers) { + self.reset(); + return Ok(Some(RequestFormEvent::Cancel)); + } + + match self.focused_field { + FormField::Name => match key_event.code { + KeyCode::Char(c) => { + self.request_name.push(c); + } + KeyCode::Backspace => { + self.request_name.pop(); + } + _ => {} + }, + FormField::Method => match key_event.code { + KeyCode::Char(c @ '1'..='5') => { + self.request_method = (c.to_digit(10).unwrap() as usize).sub(1).try_into()?; + } + KeyCode::Left => self.request_method = self.request_method.prev(), + KeyCode::Down => self.request_method = 4.try_into()?, + KeyCode::Up => self.request_method = 0.try_into()?, + KeyCode::Right => self.request_method = self.request_method.next(), + KeyCode::Char('h') => self.request_method = self.request_method.prev(), + KeyCode::Char('j') => self.request_method = 4.try_into()?, + KeyCode::Char('k') => self.request_method = 0.try_into()?, + KeyCode::Char('l') => self.request_method = self.request_method.next(), + _ => {} + }, + FormField::Parent => {} + } + + Ok(None) + } +} diff --git a/hac-client/src/pages/collection_viewer/sidebar/request_form.rs b/hac-client/src/pages/collection_viewer/sidebar/request_form.rs new file mode 100644 index 0000000..31ba65d --- /dev/null +++ b/hac-client/src/pages/collection_viewer/sidebar/request_form.rs @@ -0,0 +1,194 @@ +use hac_core::collection::types::{Request, RequestMethod}; + +use crate::ascii::LOGO_ASCII; +use crate::pages::collection_viewer::collection_store::CollectionStore; +use crate::pages::input::Input; +use crate::pages::overlay::make_overlay; +use crate::pages::Renderable; + +use std::cell::RefCell; +use std::ops::{Add, Div, Sub}; +use std::rc::Rc; +use std::sync::{Arc, RwLock}; + +use ratatui::layout::{Constraint, Direction, Layout, Rect}; +use ratatui::style::Stylize; +use ratatui::text::Line; +use ratatui::widgets::{Block, Borders, Paragraph}; +use ratatui::Frame; + +#[derive(Debug)] +pub enum RequestFormEvent { + Confirm, + Cancel, +} + +#[derive(Debug, PartialEq)] +pub enum FormField { + Name, + Method, + Parent, +} + +impl FormField { + pub fn next(&self) -> Self { + match self { + FormField::Name => FormField::Method, + FormField::Method => FormField::Parent, + FormField::Parent => FormField::Name, + } + } + + pub fn prev(&self) -> Self { + match self { + FormField::Name => FormField::Parent, + FormField::Method => FormField::Name, + FormField::Parent => FormField::Method, + } + } +} + +#[derive(Debug)] +pub struct RequestFormCreate; + +#[derive(Debug)] +pub struct RequestFormEdit; + +#[derive(Debug)] +pub struct RequestForm<'rf, State = RequestFormCreate> { + pub colors: &'rf hac_colors::Colors, + pub collection_store: Rc>, + pub logo_idx: usize, + pub request_name: String, + pub request_method: RequestMethod, + /// we store the parent dir uuid so its easier to find it and we dont need + /// lifetimes or to Rc our way to hell + pub parent_dir: Option, + pub focused_field: FormField, + pub marker: std::marker::PhantomData, + /// `request` is only used when editing a request so we can update it directly + pub request: Option>>, +} + +impl<'rf, State> RequestForm<'rf, State> { + pub fn reset(&mut self) { + self.request_name = String::default(); + self.request_method = RequestMethod::Get; + self.focused_field = FormField::Name; + self.parent_dir = None; + } +} + +impl<'rf, State> Renderable for RequestForm<'rf, State> { + fn draw(&mut self, frame: &mut Frame, _: Rect) -> anyhow::Result<()> { + make_overlay(self.colors, self.colors.normal.black, 0.1, frame); + + let mut logo = LOGO_ASCII[self.logo_idx]; + let mut logo_size = logo.len() as u16; + // adding size of the form + spacing + hint + let total_size = logo_size.add(11).add(2); + + let size = frame.size(); + let mut size = Rect::new( + size.width.div(2).sub(32), + size.height + .div(2) + .saturating_sub(logo_size.div(2)) + .saturating_sub(6), + 65, + logo_size.add(12), + ); + + if total_size.ge(&frame.size().height) { + logo = &[]; + logo_size = 0; + size.height = 12; + size.y = frame.size().height.div(2).saturating_sub(5); + } + + if !logo.is_empty() { + let logo = logo + .iter() + .map(|line| Line::from(line.fg(self.colors.normal.red)).centered()) + .collect::>(); + + let logo_size = Rect::new(size.x, size.y, size.width, logo_size); + frame.render_widget(Paragraph::new(logo), logo_size); + } + + let mut name_input = Input::new(self.colors, "Name".into()); + let method_title = Paragraph::new("Method".fg(self.colors.normal.white)); + let hint = Paragraph::new( + "[Confirm: Enter] [Cancel: Esc] [Switch: Tab] [Select: Space]" + .fg(self.colors.bright.black), + ) + .centered(); + + if self.focused_field.eq(&FormField::Name) { + name_input.focus(); + } + + let name_size = Rect::new(size.x, size.y.add(logo_size).add(1), size.width, 3); + let method_title_size = Rect::new(size.x, name_size.y.add(3), size.width, 1); + let methods_size = Rect::new(size.x, method_title_size.y.add(1), size.width, 3); + let parent_size = Rect::new(size.x, methods_size.y.add(3), size.width, 3); + let hint_size = Rect::new(size.x, parent_size.y.add(4), size.width, 1); + + let methods_items = Layout::default() + .direction(Direction::Horizontal) + .constraints((0..5).map(|_| Constraint::Length(13))) + .split(methods_size); + + let parent_name = format!( + "{}None{}", + " ".repeat(parent_size.width.div(2).sub(2).into()), + " ".repeat(parent_size.width.div(2).sub(2).into()) + ); + let parent = Paragraph::new(parent_name) + .centered() + .block( + Block::default() + .title("Parent".fg(self.colors.normal.white)) + .borders(Borders::ALL) + .fg(if self.focused_field.eq(&FormField::Parent) { + self.colors.normal.red + } else { + self.colors.bright.black + }), + ) + .fg(self.colors.bright.black); + + for (idx, method) in RequestMethod::iter().enumerate() { + let border_color = match (&self.request_method, &self.focused_field) { + (m, FormField::Method) if m.eq(method) => self.colors.normal.red, + (m, _) if m.eq(method) => self.colors.bright.blue, + _ => self.colors.bright.black, + }; + let method = Paragraph::new(Line::from(vec![ + format!(" {}", idx.add(1)).fg(self.colors.bright.black), + format!( + " {}{}", + method, + " ".repeat(10.sub(method.to_string().len())) + ) + .fg(self.colors.normal.white), + ])) + .block(Block::default().borders(Borders::ALL).fg(border_color)); + frame.render_widget(method, methods_items[idx]); + } + + frame.render_stateful_widget(name_input, name_size, &mut self.request_name); + frame.render_widget(method_title, method_title_size); + frame.render_widget(parent, parent_size); + frame.render_widget(hint, hint_size); + + if self.focused_field.eq(&FormField::Name) { + frame.set_cursor( + name_size.x.add(self.request_name.len() as u16).add(1), + name_size.y.add(1), + ); + } + + Ok(()) + } +}