diff --git a/Cargo.lock b/Cargo.lock index 2e5bb10..a921c72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1053,6 +1053,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" version = "1.0.81" @@ -1071,6 +1077,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "ratatui" version = "0.26.2" @@ -1842,6 +1878,7 @@ dependencies = [ "tracing-subscriber", "tree-sitter", "tui-big-text", + "uuid", ] [[package]] @@ -1900,6 +1937,28 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom", + "rand", + "uuid-macro-internal", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9881bea7cbe687e36c9ab3b778c36cd0487402e270304e8b1296d5085303c1a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/reqtui/src/net/request_manager.rs b/reqtui/src/net/request_manager.rs index 85f0f6c..d1bd1ba 100644 --- a/reqtui/src/net/request_manager.rs +++ b/reqtui/src/net/request_manager.rs @@ -20,7 +20,7 @@ pub enum ReqtuiNetRequest { Error(String), } -#[tracing::instrument(skip(response_tx))] +#[tracing::instrument(skip_all)] pub fn handle_request(request: Request, response_tx: UnboundedSender) { tracing::debug!("starting to handle user request"); tokio::spawn(async move { diff --git a/reqtui/src/schema/types.rs b/reqtui/src/schema/types.rs index 43072d9..97da660 100644 --- a/reqtui/src/schema/types.rs +++ b/reqtui/src/schema/types.rs @@ -74,6 +74,7 @@ impl RequestMethod { #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] pub struct Request { + pub id: String, pub method: RequestMethod, pub name: String, pub uri: String, diff --git a/tui/Cargo.toml b/tui/Cargo.toml index 61bb09d..7dabb1f 100644 --- a/tui/Cargo.toml +++ b/tui/Cargo.toml @@ -23,6 +23,7 @@ futures = "0.3.30" tui-big-text = { version = "0.4.3" } tracing-subscriber = { version = "0.3.18" } tracing-appender = "0.2.3" +uuid = { version = "1.8.0", features = ["v4", "fast-rng", "macro-diagnostics"] } [dev-dependencies] tempfile = "3.10.1" diff --git a/tui/benches/api_explorer.rs b/tui/benches/api_explorer.rs index 130cd27..a2bd7eb 100644 --- a/tui/benches/api_explorer.rs +++ b/tui/benches/api_explorer.rs @@ -27,6 +27,7 @@ fn create_sample_schema() -> Schema { path: "any_path".into(), requests: Some(vec![ RequestKind::Single(Request { + id: "any id".to_string(), name: "testing".to_string(), uri: "https://jsonplaceholder.typicode.com/users".to_string(), method: RequestMethod::Get, @@ -34,6 +35,7 @@ fn create_sample_schema() -> Schema { body_type: Some("application/json".to_string()), }), RequestKind::Single(Request { + id: "any_other_id".to_string(), name: "testing".to_string(), uri: "https://jsonplaceholder.typicode.com/users".to_string(), method: RequestMethod::Get, diff --git a/tui/src/app.rs b/tui/src/app.rs index 21c9512..641cd26 100644 --- a/tui/src/app.rs +++ b/tui/src/app.rs @@ -24,7 +24,7 @@ impl<'app> App<'app> { let terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?; Ok(Self { screen_manager: ScreenManager::new(terminal.size()?, colors, schemas, config)?, - event_pool: EventPool::new(60f64), + event_pool: EventPool::new(60f64, 30f64), should_quit: false, terminal, }) @@ -42,7 +42,7 @@ impl<'app> App<'app> { loop { if let Some(event) = self.event_pool.next().await { match event { - Event::Tick => {} + Event::Tick => self.screen_manager.handle_tick()?, Event::Resize(new_size) => self.screen_manager.resize(new_size), Event::Render => { self.terminal.draw(|f| { diff --git a/tui/src/components.rs b/tui/src/components.rs index 47b5b4e..78b9e74 100644 --- a/tui/src/components.rs +++ b/tui/src/components.rs @@ -38,4 +38,8 @@ pub trait Component { fn register_command_handler(&mut self, sender: UnboundedSender) -> anyhow::Result<()> { Ok(()) } + + fn handle_tick(&mut self) -> anyhow::Result<()> { + Ok(()) + } } diff --git a/tui/src/components/api_explorer/api_explorer.rs b/tui/src/components/api_explorer/api_explorer.rs index e330ab6..65b3d8a 100644 --- a/tui/src/components/api_explorer/api_explorer.rs +++ b/tui/src/components/api_explorer/api_explorer.rs @@ -144,6 +144,7 @@ pub struct ApiExplorer<'ae> { curr_overlay: Overlays, create_req_form_state: CreateReqFormState, + sync_interval: std::time::Instant, editor: ReqEditor<'ae>, editor_tab: ReqEditorTabs, @@ -161,7 +162,7 @@ impl<'ae> ApiExplorer<'ae> { ) -> Self { let layout = build_layout(size); - let selected_request = schema.requests.as_ref().and_then(|requests| { + let mut selected_request = schema.requests.as_ref().and_then(|requests| { requests.first().and_then(|req| { if let RequestKind::Single(req) = req { Some(Rc::new(RefCell::new(req.clone()))) @@ -185,7 +186,7 @@ impl<'ae> ApiExplorer<'ae> { colors, config, - editor: ReqEditor::new(colors, selected_request.clone(), layout.req_editor, config), + editor: ReqEditor::new(colors, selected_request.as_mut(), layout.req_editor, config), editor_tab: ReqEditorTabs::Request, res_viewer: ResViewer::new(colors, None), @@ -206,6 +207,7 @@ impl<'ae> ApiExplorer<'ae> { curr_overlay: Overlays::None, create_req_form_state: CreateReqFormState::default(), + sync_interval: std::time::Instant::now(), response_rx, request_tx, layout, @@ -230,7 +232,7 @@ impl<'ae> ApiExplorer<'ae> { self.selected_request = Some(Rc::new(RefCell::new(req.clone()))); self.editor = ReqEditor::new( self.colors, - self.selected_request.clone(), + self.selected_request.as_mut(), self.layout.req_editor, self.config, ); @@ -746,6 +748,7 @@ impl<'ae> ApiExplorer<'ae> { let form_state = &self.create_req_form_state; let new_request = match form_state.req_kind { CreateReqKind::Request => RequestKind::Single(Request { + id: uuid::Uuid::new_v4().to_string(), name: form_state.req_name.clone(), method: form_state.method.clone(), uri: String::default(), @@ -758,19 +761,58 @@ impl<'ae> ApiExplorer<'ae> { }), }; - if let Some(requests) = self.schema.requests.as_mut() { - requests.push(new_request); - } + self.schema + .requests + .get_or_insert_with(Vec::new) + .push(new_request); + + self.create_req_form_state = CreateReqFormState::default(); + self.curr_overlay = Overlays::None; + self.sync_schema_changes(); + } + + fn sync_schema_changes(&mut self) { let sender = self .sender .as_ref() .expect("should have a sender at this point") .clone(); - let schema = self.schema.clone(); - self.create_req_form_state = CreateReqFormState::default(); - self.curr_overlay = Overlays::None; + let mut schema = self.schema.clone(); + if let Some(request) = self.selected_request.as_ref() { + let mut request = request.borrow().clone(); + let body = self.editor.body().to_string(); + if !body.is_empty() { + request.body = Some(body); + request.body_type = Some("application/json".into()); + } + + schema + .requests + .as_mut() + .expect("no requests on schema, but we have a selected request") + .iter_mut() + .for_each(|other| match other { + RequestKind::Single(inner) => { + tracing::debug!("inner: {}, request: {}", inner.id, request.id); + request + .id + .eq(&inner.id) + .then(|| std::mem::swap(inner, &mut request)); + } + RequestKind::Nested(dir) => dir.requests.iter_mut().for_each(|other| { + if let RequestKind::Single(inner) = other { + request + .id + .eq(&inner.id) + .then(|| std::mem::swap(inner, &mut request)); + } + }), + }); + } + + self.sync_interval = std::time::Instant::now(); tokio::spawn(async move { match reqtui::fs::sync_schema(schema).await { Ok(_) => {} @@ -838,6 +880,13 @@ impl Component for ApiExplorer<'_> { Ok(()) } + fn handle_tick(&mut self) -> anyhow::Result<()> { + if self.sync_interval.elapsed().as_secs().ge(&5) { + self.sync_schema_changes(); + } + Ok(()) + } + fn register_command_handler(&mut self, sender: UnboundedSender) -> anyhow::Result<()> { self.sender = Some(sender); Ok(()) @@ -1035,6 +1084,7 @@ mod tests { fn create_root_one() -> RequestKind { RequestKind::Single(Request { + id: "any id".to_string(), method: RequestMethod::Get, name: "Root1".to_string(), uri: "/root1".to_string(), @@ -1045,6 +1095,7 @@ mod tests { fn create_child_one() -> RequestKind { RequestKind::Single(Request { + id: "any id".to_string(), method: RequestMethod::Post, name: "Child1".to_string(), uri: "/nested1/child1".to_string(), @@ -1055,6 +1106,7 @@ mod tests { fn create_child_two() -> RequestKind { RequestKind::Single(Request { + id: "any id".to_string(), method: RequestMethod::Put, name: "Child2".to_string(), uri: "/nested1/child2".to_string(), @@ -1065,6 +1117,7 @@ mod tests { fn create_not_used() -> RequestKind { RequestKind::Single(Request { + id: "any id".to_string(), method: RequestMethod::Put, name: "NotUsed".to_string(), uri: "/not/used".to_string(), @@ -1086,6 +1139,7 @@ mod tests { fn create_root_two() -> RequestKind { RequestKind::Single(Request { + id: "any id".to_string(), method: RequestMethod::Delete, name: "Root2".to_string(), uri: "/root2".to_string(), diff --git a/tui/src/components/api_explorer/req_editor.rs b/tui/src/components/api_explorer/req_editor.rs index da84dcc..f84b74a 100644 --- a/tui/src/components/api_explorer/req_editor.rs +++ b/tui/src/components/api_explorer/req_editor.rs @@ -106,7 +106,7 @@ pub struct ReqEditor<'re> { impl<'re> ReqEditor<'re> { pub fn new( colors: &'re colors::Colors, - request: Option>>, + request: Option<&mut Rc>>, size: Rect, config: &'re config::Config, ) -> Self { @@ -140,6 +140,10 @@ impl<'re> ReqEditor<'re> { } } + pub fn body(&self) -> &TextObject { + &self.body + } + pub fn layout(&self) -> &ReqEditorLayout { &self.layout } diff --git a/tui/src/components/input.rs b/tui/src/components/input.rs index 1b4c25f..cfc6bc0 100644 --- a/tui/src/components/input.rs +++ b/tui/src/components/input.rs @@ -73,8 +73,6 @@ impl StatefulWidget for Input<'_> { #[cfg(test)] mod tests { - use ratatui::widgets::BorderType; - use super::*; #[test] diff --git a/tui/src/event_pool.rs b/tui/src/event_pool.rs index 5df01fa..7ade13f 100644 --- a/tui/src/event_pool.rs +++ b/tui/src/event_pool.rs @@ -20,10 +20,11 @@ pub struct EventPool { event_tx: tokio::sync::mpsc::UnboundedSender, task: JoinHandle<()>, frame_rate: f64, + tick_rate: f64, } impl EventPool { - pub fn new(frame_rate: f64) -> Self { + pub fn new(frame_rate: f64, tick_rate: f64) -> Self { let (event_tx, event_rx) = tokio::sync::mpsc::unbounded_channel(); let task = tokio::spawn(async {}); @@ -32,17 +33,20 @@ impl EventPool { event_tx, task, frame_rate, + tick_rate, } } #[cfg_attr(test, mutants::skip)] pub fn start(&mut self) { let render_delay = std::time::Duration::from_secs_f64(1.0.div(self.frame_rate)); + let tick_delay = std::time::Duration::from_secs_f64(1.0.div(self.tick_rate)); let event_tx = self.event_tx.clone(); self.task = tokio::spawn(async move { let mut reader = crossterm::event::EventStream::new(); let mut render_interval = tokio::time::interval(render_delay); + let mut tick_interval = tokio::time::interval(tick_delay); event_tx .send(Event::Init) @@ -50,6 +54,7 @@ impl EventPool { loop { let render_delay = render_interval.tick(); + let tick_delay = tick_interval.tick(); let crossterm_event = reader.next().fuse(); tokio::select! { @@ -68,6 +73,9 @@ impl EventPool { None => {} } } + _ = tick_delay => { + event_tx.send(Event::Tick).unwrap(); + }, _ = render_delay => { event_tx.send(Event::Render).unwrap(); }, diff --git a/tui/src/screen_manager.rs b/tui/src/screen_manager.rs index 4d990da..f449b82 100644 --- a/tui/src/screen_manager.rs +++ b/tui/src/screen_manager.rs @@ -127,6 +127,19 @@ impl Component for ScreenManager<'_> { e.resize(new_size) } } + + fn handle_tick(&mut self) -> anyhow::Result<()> { + // currently, only the editor cares about the ticks, used to determine + // when to sync changes in disk + if let Screens::Editor = &self.curr_screen { + self.api_explorer + .as_mut() + .expect("we are displaying the editor without having one") + .handle_tick()? + }; + + Ok(()) + } } impl Eventful for ScreenManager<'_> {