Skip to main content

endpoint_validator/tui/
app.rs

1use crate::parser::EndpointMetadata;
2use crate::parser::services::ConvertValue;
3use crate::tui::state::{AppState, EndpointField, JsonViewMode};
4use crate::tui::ui::draw_ui;
5use anyhow::{Context, Result};
6use crossterm::event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode};
7use crossterm::execute;
8use crossterm::terminal::{
9    EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode,
10};
11use ratatui::Terminal;
12use ratatui::backend::CrosstermBackend;
13use std::collections::HashMap;
14use std::io;
15use std::sync::Arc;
16use tokio::sync::Mutex;
17use tokio::time::{self, Duration};
18
19pub async fn run(
20    endpoint_names: Vec<String>,
21    endpoint_data: HashMap<String, EndpointMetadata>,
22    param_defaults: HashMap<String, HashMap<String, String>>,
23) -> Result<()> {
24    // Set up terminal in raw mode
25    enable_raw_mode()?;
26    let mut stdout = io::stdout();
27    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
28    let backend = CrosstermBackend::new(stdout);
29    let terminal = Arc::new(Mutex::new(Terminal::new(backend)?));
30
31    // Initialize app state with shared state
32    let app_state = Arc::new(Mutex::new(AppState::new(
33        endpoint_names,
34        endpoint_data,
35        param_defaults,
36    )));
37
38    // Spawn a task to handle TUI updates
39    let terminal_clone = Arc::clone(&terminal);
40    let app_state_clone = Arc::clone(&app_state);
41    tokio::spawn(async move {
42        let mut ticker = time::interval(Duration::from_millis(500));
43        loop {
44            ticker.tick().await; // Wait for the next tick
45            let mut app_state_guard = app_state_clone.lock().await;
46            let mut terminal_guard = terminal_clone.lock().await;
47            if let Err(e) = terminal_guard.draw(|f| draw_ui(f, &mut *app_state_guard)) {
48                eprintln!("Error drawing UI: {}", e);
49                break; // Exit the loop if drawing fails
50            }
51        }
52    });
53
54    // Spawn a task to handle input events
55    let terminal_clone = Arc::clone(&terminal);
56    let handle_event_task = tokio::spawn(async move {
57        loop {
58            if let Err(e) = handle_event(&app_state, &terminal_clone).await {
59                eprintln!("Error handling event: {}", e);
60                break;
61            }
62        }
63    });
64
65    // Wait for the handle_event_task to finish, which will happen when Esc is pressed
66    handle_event_task.await?;
67
68    // Perform terminal cleanup in the async context
69    {
70        let mut terminal = terminal.lock().await;
71        disable_raw_mode()?;
72        execute!(
73            terminal.backend_mut(),
74            LeaveAlternateScreen,
75            DisableMouseCapture
76        )?;
77        terminal.show_cursor()?;
78    }
79
80    Ok(())
81}
82
83async fn handle_event(
84    app_state: &Arc<Mutex<AppState>>,
85    terminal: &Arc<Mutex<Terminal<CrosstermBackend<std::io::Stdout>>>>,
86) -> Result<()> {
87    // Poll for events with a short timeout
88    if event::poll(Duration::from_millis(100))? {
89        if let Event::Key(key) = event::read()? {
90            let mut needs_redraw = false;
91
92            match key.code {
93                KeyCode::Char(c) => {
94                    let mut app_state_guard = app_state.lock().await;
95                    app_state_guard.update_input(c);
96                    needs_redraw = true;
97                }
98                KeyCode::Backspace => {
99                    let mut app_state_guard = app_state.lock().await;
100                    app_state_guard.delete_last_char();
101                    needs_redraw = true;
102                }
103                KeyCode::Tab => {
104                    let mut app_state_guard = app_state.lock().await;
105                    app_state_guard.switch_block();
106                    needs_redraw = true;
107                }
108                KeyCode::Left => {
109                    let mut app_state_guard = app_state.lock().await;
110                    app_state_guard.scroll_response_left();
111                    needs_redraw = true;
112                }
113                KeyCode::Right => {
114                    let mut app_state_guard = app_state.lock().await;
115                    app_state_guard.scroll_response_right();
116                    needs_redraw = true;
117                }
118                KeyCode::Down => {
119                    let mut app_state_guard = app_state.lock().await;
120                    app_state_guard.next_field();
121                    needs_redraw = true;
122                }
123                KeyCode::Up => {
124                    let mut app_state_guard = app_state.lock().await;
125                    app_state_guard.previous_field();
126                    needs_redraw = true;
127                }
128                KeyCode::Enter => {
129                    let app_state_clone = Arc::clone(&app_state);
130
131                    // Lock the app_state and check the focused field
132                    let focused_field = {
133                        let state = app_state_clone.lock().await;
134                        state.focused_endpoint_field.clone()
135                    };
136
137                    // Now move the app_state_clone and start the connection
138                    if let Some(EndpointField::ConnectButton) = focused_field {
139                        if let Err(e) = connect_and_listen(app_state_clone).await {
140                            eprintln!("Error connecting and listening: {}", e);
141                        }
142                    } else {
143                        let mut app_state_guard = app_state.lock().await;
144                        if let Err(err) = app_state_guard.handle_enter().await {
145                            app_state_guard.json_data = Some(format!("Error: {}", err));
146                        }
147                    }
148                    needs_redraw = true;
149                }
150                KeyCode::Esc => {
151                    // Return an error to indicate that the program should exit
152                    return Err(anyhow::anyhow!("Esc pressed, exiting program"));
153                }
154                _ => {}
155            }
156
157            // Redraw TUI after handling key events only if necessary
158            if needs_redraw {
159                let mut app_state_guard = app_state.lock().await;
160                let mut terminal_guard = terminal.lock().await;
161                if let Err(e) = terminal_guard.draw(|f| draw_ui(f, &mut *app_state_guard)) {
162                    eprintln!("Error drawing UI: {}", e);
163                }
164            }
165        }
166    }
167
168    Ok(())
169}
170
171async fn connect_and_listen(app_state: Arc<Mutex<AppState>>) -> Result<()> {
172    tokio::spawn(async move {
173        let (method_id, converted_params, is_stream) = {
174            let state = app_state.lock().await;
175
176            // Extract necessary data while holding the lock
177            let method_id = state
178                .method_id
179                .ok_or_else(|| anyhow::anyhow!("Method ID is missing"))?;
180            let is_stream = state.is_stream;
181
182            let converted_params = state
183                .params
184                .iter()
185                .zip(state.param_values.iter())
186                .map(|(param, value)| {
187                    param.ty.convert_value(value).context(format!(
188                        "Failed to convert value for parameter: {}",
189                        param.name
190                    ))
191                })
192                .collect::<Result<Vec<_>>>()?;
193
194            (method_id, converted_params, is_stream)
195        };
196
197        {
198            // Send the request to the WebSocket
199            let mut state = app_state.lock().await;
200            let client = state
201                .client
202                .as_mut()
203                .context("WebSocket client is not connected")?;
204            client
205                .send_req(method_id, converted_params)
206                .await
207                .context("Failed to send request to WebSocket")?;
208        }
209
210        // Enter the receiving loop
211        loop {
212            let raw_response_result = {
213                let mut state = app_state.lock().await;
214                let client = state
215                    .client
216                    .as_mut()
217                    .context("WebSocket client is not connected")?;
218
219                client.recv_raw().await
220            };
221
222            match raw_response_result {
223                Ok(raw_response) => {
224                    let resp = {
225                        let state = app_state.lock().await;
226                        match state.json_view_mode {
227                            JsonViewMode::Pretty => serde_json::to_string_pretty(&raw_response)
228                                .context("Failed to format JSON as pretty"),
229                            JsonViewMode::Raw => serde_json::to_string(&raw_response)
230                                .context("Failed to format JSON as raw"),
231                        }
232                    };
233
234                    let mut state = app_state.lock().await;
235                    match resp {
236                        Ok(formatted_json) => {
237                            state.json_data = Some(formatted_json);
238                            state.endpoint_connected = true;
239                        }
240                        Err(err) => {
241                            state.json_data = Some(format!("Error: {}", err));
242                        }
243                    }
244                }
245                Err(e) => {
246                    let mut state = app_state.lock().await;
247                    state.json_data = Some(format!("Error receiving data: {:?}", e));
248                    break;
249                }
250            }
251
252            if !is_stream {
253                // If not streaming, break after receiving one response
254                break;
255            }
256
257            // Optional: sleep before the next iteration if streaming
258            tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
259        }
260
261        Ok::<(), anyhow::Error>(())
262    });
263
264    Ok(())
265}