1use ratatui::layout::{Constraint, Direction, Layout, Rect};
2use ratatui::widgets::{Block, Borders, Paragraph};
3use ratatui::Frame;
4
5use crate::tui::openapi::ApiEndpoint;
6
7pub struct ExecuteScreen {
9 pub endpoint: ApiEndpoint,
10 pub path_params: Vec<(String, String)>,
11 pub path_param_input_idx: usize,
12 pub query_params: Vec<(String, String)>,
13 pub body_text: String,
14 pub focused_field: FocusedField,
15 pub param_input_idx: usize,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub enum FocusedField {
20 PathParams,
21 QueryParams,
22 Body,
23}
24
25struct MiddleAreas {
26 path: Option<Rect>,
27 query: Rect,
28 body: Option<Rect>,
29}
30
31impl ExecuteScreen {
32 pub fn new(endpoint: ApiEndpoint) -> Self {
33 let path_params: Vec<(String, String)> = endpoint
34 .path_params
35 .iter()
36 .map(|p| (p.name.clone(), p.default.clone().unwrap_or_default()))
37 .collect();
38
39 let query_params: Vec<(String, String)> = endpoint
40 .query_params
41 .iter()
42 .map(|p| (p.name.clone(), p.default.clone().unwrap_or_default()))
43 .collect();
44
45 let focused_field = if !path_params.is_empty() {
46 FocusedField::PathParams
47 } else {
48 FocusedField::QueryParams
49 };
50
51 Self {
52 endpoint,
53 path_params,
54 path_param_input_idx: 0,
55 query_params,
56 body_text: String::new(),
57 focused_field,
58 param_input_idx: 0,
59 }
60 }
61
62 pub fn get_path_params(&self) -> Vec<(String, String)> {
63 self.path_params
64 .iter()
65 .filter(|(_, v)| !v.is_empty())
66 .cloned()
67 .collect()
68 }
69
70 pub fn get_query_params(&self) -> Vec<(String, String)> {
71 self.query_params
72 .iter()
73 .filter(|(_, v)| !v.is_empty())
74 .cloned()
75 .collect()
76 }
77
78 fn middle_areas(&self, mid: Rect) -> MiddleAreas {
79 let has_path = !self.path_params.is_empty();
80
81 if !has_path {
82 if self.endpoint.has_body {
83 let q = Layout::default()
84 .direction(Direction::Horizontal)
85 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
86 .split(mid);
87 return MiddleAreas {
88 path: None,
89 query: q[0],
90 body: Some(q[1]),
91 };
92 }
93 return MiddleAreas {
94 path: None,
95 query: mid,
96 body: None,
97 };
98 }
99
100 let path_lines = self.path_params.len() as u16;
101 let path_h = (path_lines + 2).clamp(3, mid.height.saturating_sub(4));
102 let chunks = Layout::default()
103 .direction(Direction::Vertical)
104 .constraints([Constraint::Length(path_h), Constraint::Min(3)])
105 .split(mid);
106 let path_r = chunks[0];
107 let bottom = chunks[1];
108
109 if self.endpoint.has_body {
110 let q = Layout::default()
111 .direction(Direction::Horizontal)
112 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
113 .split(bottom);
114 MiddleAreas {
115 path: Some(path_r),
116 query: q[0],
117 body: Some(q[1]),
118 }
119 } else {
120 MiddleAreas {
121 path: Some(path_r),
122 query: bottom,
123 body: None,
124 }
125 }
126 }
127
128 pub fn next_field(&mut self) {
129 match self.focused_field {
130 FocusedField::PathParams => {
131 if self.path_param_input_idx + 1 < self.path_params.len() {
132 self.path_param_input_idx += 1;
133 } else if !self.query_params.is_empty() {
134 self.focused_field = FocusedField::QueryParams;
135 self.param_input_idx = 0;
136 } else if self.endpoint.has_body {
137 self.focused_field = FocusedField::Body;
138 } else {
139 self.path_param_input_idx = 0;
140 }
141 }
142 FocusedField::QueryParams => {
143 if self.param_input_idx + 1 < self.query_params.len() {
144 self.param_input_idx += 1;
145 } else if self.endpoint.has_body {
146 self.focused_field = FocusedField::Body;
147 } else if !self.path_params.is_empty() {
148 self.focused_field = FocusedField::PathParams;
149 self.path_param_input_idx = 0;
150 } else {
151 self.param_input_idx = 0;
152 }
153 }
154 FocusedField::Body => {
155 if !self.path_params.is_empty() {
156 self.focused_field = FocusedField::PathParams;
157 self.path_param_input_idx = 0;
158 } else {
159 self.focused_field = FocusedField::QueryParams;
160 self.param_input_idx = 0;
161 }
162 }
163 }
164 }
165
166 pub fn previous_field(&mut self) {
167 match self.focused_field {
168 FocusedField::PathParams => {
169 if self.path_param_input_idx > 0 {
170 self.path_param_input_idx -= 1;
171 } else if self.endpoint.has_body {
172 self.focused_field = FocusedField::Body;
173 } else if !self.query_params.is_empty() {
174 self.focused_field = FocusedField::QueryParams;
175 self.param_input_idx = self.query_params.len().saturating_sub(1);
176 } else {
177 self.path_param_input_idx = self.path_params.len().saturating_sub(1);
178 }
179 }
180 FocusedField::QueryParams => {
181 if self.param_input_idx > 0 {
182 self.param_input_idx -= 1;
183 } else if !self.path_params.is_empty() {
184 self.focused_field = FocusedField::PathParams;
185 self.path_param_input_idx = self.path_params.len().saturating_sub(1);
186 } else if self.endpoint.has_body {
187 self.focused_field = FocusedField::Body;
188 } else {
189 self.param_input_idx = self.query_params.len().saturating_sub(1);
190 }
191 }
192 FocusedField::Body => {
193 if !self.query_params.is_empty() {
194 self.focused_field = FocusedField::QueryParams;
195 self.param_input_idx = self.query_params.len().saturating_sub(1);
196 } else if !self.path_params.is_empty() {
197 self.focused_field = FocusedField::PathParams;
198 self.path_param_input_idx = self.path_params.len().saturating_sub(1);
199 }
200 }
201 }
202 }
203
204 pub fn add_char_to_focused(&mut self, c: char) {
205 match self.focused_field {
206 FocusedField::PathParams => {
207 if let Some((_, ref mut v)) = self.path_params.get_mut(self.path_param_input_idx) {
208 v.push(c);
209 }
210 }
211 FocusedField::QueryParams => {
212 if let Some((_, ref mut v)) = self.query_params.get_mut(self.param_input_idx) {
213 v.push(c);
214 }
215 }
216 FocusedField::Body => {
217 self.body_text.push(c);
218 }
219 }
220 }
221
222 pub fn delete_char_from_focused(&mut self) {
223 match self.focused_field {
224 FocusedField::PathParams => {
225 if let Some((_, ref mut v)) = self.path_params.get_mut(self.path_param_input_idx) {
226 v.pop();
227 }
228 }
229 FocusedField::QueryParams => {
230 if let Some((_, ref mut v)) = self.query_params.get_mut(self.param_input_idx) {
231 v.pop();
232 }
233 }
234 FocusedField::Body => {
235 self.body_text.pop();
236 }
237 }
238 }
239
240 pub fn cursor_position(&self, area: Rect) -> Option<(u16, u16)> {
241 let chunks = Layout::default()
242 .constraints([
243 Constraint::Length(3),
244 Constraint::Min(5),
245 Constraint::Length(3),
246 ])
247 .split(area);
248
249 let mid = self.middle_areas(chunks[1]);
250
251 match self.focused_field {
252 FocusedField::PathParams => mid.path.and_then(|pa| self.cursor_in_path_params(pa)),
253 FocusedField::QueryParams => self.cursor_in_query_params(mid.query),
254 FocusedField::Body => mid.body.and_then(|ba| self.cursor_in_body(ba)),
255 }
256 }
257
258 fn cursor_in_path_params(&self, area: Rect) -> Option<(u16, u16)> {
259 if area.width < 3 || area.height < 3 {
260 return None;
261 }
262
263 let idx = self
264 .path_param_input_idx
265 .min(self.path_params.len().saturating_sub(1));
266 let y = area.y + 1 + idx as u16;
267 if y >= area.y + area.height - 1 {
268 return None;
269 }
270
271 let (name, value) = self.path_params.get(idx)?;
272 let param = self.endpoint.path_params.get(idx);
273 let required = param.map(|p| p.required).unwrap_or(false);
274 let param_type = param
275 .map(|p| p.param_type.clone())
276 .unwrap_or_else(|| "string".to_string());
277 let marker = if required { "*" } else { " " };
278
279 let line = format!("{marker} {name} ({param_type}) = {value}");
280 let max_x = area.x + area.width - 2;
281 let mut x = area.x + 1 + line.chars().count() as u16;
282 if x > max_x {
283 x = max_x;
284 }
285
286 Some((x, y))
287 }
288
289 fn cursor_in_query_params(&self, area: Rect) -> Option<(u16, u16)> {
290 if area.width < 3 || area.height < 3 {
291 return None;
292 }
293
294 let idx = self
295 .param_input_idx
296 .min(self.query_params.len().saturating_sub(1));
297 let y = area.y + 1 + idx as u16;
298 if y >= area.y + area.height - 1 {
299 return None;
300 }
301
302 let (name, value) = self.query_params.get(idx)?;
303 let param = self.endpoint.query_params.get(idx);
304 let required = param.map(|p| p.required).unwrap_or(false);
305 let param_type = param
306 .map(|p| p.param_type.clone())
307 .unwrap_or_else(|| "string".to_string());
308 let marker = if required { "*" } else { " " };
309
310 let line = format!("{marker} {name} ({param_type}) = {value}");
311 let max_x = area.x + area.width - 2;
312 let mut x = area.x + 1 + line.chars().count() as u16;
313 if x > max_x {
314 x = max_x;
315 }
316
317 Some((x, y))
318 }
319
320 fn cursor_in_body(&self, area: Rect) -> Option<(u16, u16)> {
321 if area.width < 3 || area.height < 3 {
322 return None;
323 }
324
325 let lines: Vec<&str> = self.body_text.split('\n').collect();
326 let last_line = lines.last().copied().unwrap_or("");
327 let line_idx = lines.len().saturating_sub(1);
328
329 let max_visible_lines = (area.height - 2) as usize;
330 let visible_line_idx = line_idx.min(max_visible_lines.saturating_sub(1));
331
332 let y = area.y + 1 + visible_line_idx as u16;
333 let max_x = area.x + area.width - 2;
334 let mut x = area.x + 1 + last_line.chars().count() as u16;
335 if x > max_x {
336 x = max_x;
337 }
338
339 Some((x, y))
340 }
341
342 pub fn render(&self, f: &mut Frame, area: ratatui::layout::Rect) {
343 let chunks = Layout::default()
344 .constraints([
345 Constraint::Length(3),
346 Constraint::Min(5),
347 Constraint::Length(3),
348 ])
349 .split(area);
350
351 let info_text = format!("{} {}", self.endpoint.method, self.endpoint.path);
352 let info = Paragraph::new(info_text)
353 .block(Block::default().title("Endpoint").borders(Borders::ALL));
354 f.render_widget(info, chunks[0]);
355
356 let mid = self.middle_areas(chunks[1]);
357 if let Some(pa) = mid.path {
358 self.render_path_params(f, pa);
359 }
360 self.render_query_params(f, mid.query);
361 if let Some(ba) = mid.body {
362 self.render_body(f, ba);
363 }
364
365 let help_text = "Tab: Next field | Backspace: Delete | Enter: Execute | Esc: Back";
366 let help = Paragraph::new(help_text).block(Block::default().borders(Borders::ALL));
367 f.render_widget(help, chunks[2]);
368 }
369
370 fn render_path_params(&self, f: &mut Frame, area: Rect) {
371 let items: Vec<String> = self
372 .path_params
373 .iter()
374 .enumerate()
375 .map(|(idx, (name, value))| {
376 let param = self.endpoint.path_params.get(idx);
377 let required = param.map(|p| p.required).unwrap_or(false);
378 let param_type = param
379 .map(|p| p.param_type.clone())
380 .unwrap_or_else(|| "string".to_string());
381 let marker = if required { "*" } else { " " };
382 format!("{} {} ({}) = {}", marker, name, param_type, value)
383 })
384 .collect();
385
386 let text = items.join("\n");
387 let mut paragraph = Paragraph::new(text).block(
388 Block::default()
389 .title("Path parameters")
390 .borders(Borders::ALL),
391 );
392
393 if self.focused_field == FocusedField::PathParams {
394 paragraph =
395 paragraph.style(ratatui::style::Style::default().fg(ratatui::style::Color::Yellow));
396 }
397
398 f.render_widget(paragraph, area);
399 }
400
401 fn render_query_params(&self, f: &mut Frame, area: Rect) {
402 let items: Vec<String> = self
403 .query_params
404 .iter()
405 .enumerate()
406 .map(|(idx, (name, value))| {
407 let param = self.endpoint.query_params.get(idx);
408 let required = param.map(|p| p.required).unwrap_or(false);
409 let param_type = param
410 .map(|p| p.param_type.clone())
411 .unwrap_or_else(|| "string".to_string());
412 let marker = if required { "*" } else { " " };
413 format!("{} {} ({}) = {}", marker, name, param_type, value)
414 })
415 .collect();
416
417 let text = items.join("\n");
418 let mut paragraph = Paragraph::new(text).block(
419 Block::default()
420 .title("Query Parameters")
421 .borders(Borders::ALL),
422 );
423
424 if self.focused_field == FocusedField::QueryParams {
425 paragraph =
426 paragraph.style(ratatui::style::Style::default().fg(ratatui::style::Color::Yellow));
427 }
428
429 f.render_widget(paragraph, area);
430 }
431
432 fn render_body(&self, f: &mut Frame, area: Rect) {
433 let mut paragraph = Paragraph::new(self.body_text.as_str())
434 .block(
435 Block::default()
436 .title("Request Body (JSON)")
437 .borders(Borders::ALL),
438 )
439 .wrap(ratatui::widgets::Wrap { trim: true });
440
441 if self.focused_field == FocusedField::Body {
442 paragraph =
443 paragraph.style(ratatui::style::Style::default().fg(ratatui::style::Color::Yellow));
444 }
445
446 f.render_widget(paragraph, area);
447 }
448}