1use ratatui::{
2 layout::{Constraint, Direction, Layout, Margin, Rect},
3 style::{Color, Modifier, Style},
4 text::{Line, Span},
5 widgets::{
6 Block, Borders, Cell, Clear, List, ListItem, Paragraph, Row, Table, TableState, Wrap,
7 },
8 Frame,
9};
10
11use crate::app::{App, AppMode, InputMode, TransportType};
12
13fn format_json_with_highlighting(json_value: &serde_json::Value) -> Vec<Line<'static>> {
15 let json_str = match serde_json::to_string_pretty(json_value) {
17 Ok(s) => s,
18 Err(_) => return vec![Line::from("Failed to format JSON")],
19 };
20
21 let mut lines = Vec::new();
22
23 for (line_num, line) in json_str.lines().enumerate() {
24 if line_num > 1000 {
26 lines.push(Line::from(Span::styled(
27 "... (content truncated)",
28 Style::default().fg(Color::Gray),
29 )));
30 break;
31 }
32
33 let mut spans = Vec::new();
35 let mut chars = line.chars().peekable();
36 let mut current_token = String::new();
37
38 while let Some(ch) = chars.next() {
39 match ch {
40 '"' => {
41 if !current_token.is_empty() {
43 spans.push(Span::raw(current_token.clone()));
44 current_token.clear();
45 }
46
47 let mut string_content = String::from("\"");
49 for string_ch in chars.by_ref() {
50 string_content.push(string_ch);
51 if string_ch == '"' && !string_content.ends_with("\\\"") {
52 break;
53 }
54 }
55
56 let peek_chars = chars.clone();
58 let mut found_colon = false;
59 for peek_ch in peek_chars {
60 if peek_ch == ':' {
61 found_colon = true;
62 break;
63 } else if !peek_ch.is_whitespace() {
64 break;
65 }
66 }
67
68 if found_colon {
69 spans.push(Span::styled(
71 string_content,
72 Style::default()
73 .fg(Color::Cyan)
74 .add_modifier(Modifier::BOLD),
75 ));
76 } else {
77 spans.push(Span::styled(
79 string_content,
80 Style::default().fg(Color::Green),
81 ));
82 }
83 }
84 ':' => {
85 if !current_token.is_empty() {
86 spans.push(Span::raw(current_token.clone()));
87 current_token.clear();
88 }
89 spans.push(Span::styled(":", Style::default().fg(Color::White)));
90 }
91 ',' => {
92 if !current_token.is_empty() {
93 spans.push(Span::raw(current_token.clone()));
94 current_token.clear();
95 }
96 spans.push(Span::styled(",", Style::default().fg(Color::White)));
97 }
98 '{' | '}' | '[' | ']' => {
99 if !current_token.is_empty() {
100 spans.push(Span::raw(current_token.clone()));
101 current_token.clear();
102 }
103 spans.push(Span::styled(
104 ch.to_string(),
105 Style::default()
106 .fg(Color::Yellow)
107 .add_modifier(Modifier::BOLD),
108 ));
109 }
110 _ => {
111 current_token.push(ch);
113 }
114 }
115 }
116
117 if !current_token.is_empty() {
119 let trimmed_token = current_token.trim();
120 if trimmed_token == "true" || trimmed_token == "false" {
121 spans.push(Span::styled(
122 current_token,
123 Style::default().fg(Color::Magenta),
124 ));
125 } else if trimmed_token == "null" {
126 spans.push(Span::styled(current_token, Style::default().fg(Color::Red)));
127 } else if trimmed_token.parse::<f64>().is_ok() {
128 spans.push(Span::styled(
129 current_token,
130 Style::default().fg(Color::Blue),
131 ));
132 } else {
133 spans.push(Span::raw(current_token));
135 }
136 }
137
138 lines.push(Line::from(spans));
139 }
140
141 lines
142}
143
144pub fn draw(f: &mut Frame, app: &App) {
145 let keybinds = get_keybinds_for_mode(app);
147 let available_width = f.size().width as usize;
148 let line_spans = arrange_keybinds_responsive(keybinds, available_width);
149 let footer_height = (line_spans.len() + 2).max(3); let chunks = Layout::default()
152 .direction(Direction::Vertical)
153 .constraints([
154 Constraint::Length(3), Constraint::Min(10), Constraint::Length(footer_height as u16), Constraint::Length(1), ])
159 .split(f.size());
160
161 draw_header(f, chunks[0], app);
162
163 match app.app_mode {
165 AppMode::Normal => {
166 draw_main_content(f, chunks[1], app);
167 }
168 AppMode::Paused | AppMode::Intercepting => {
169 draw_intercept_content(f, chunks[1], app);
170 }
171 }
172
173 draw_footer(f, chunks[2], app);
174
175 if app.input_mode == InputMode::EditingTarget {
177 draw_input_dialog(f, app, "Edit Target URL", "Target URL");
178 } else if app.input_mode == InputMode::FilteringRequests {
179 draw_input_dialog(f, app, "Filter Requests", "Filter");
180 }
181}
182
183fn draw_header(f: &mut Frame, area: Rect, app: &App) {
184 let status = if app.is_running { "RUNNING" } else { "STOPPED" };
185 let status_color = if app.is_running {
186 Color::Green
187 } else {
188 Color::Red
189 };
190
191 let mode_text = match app.app_mode {
192 AppMode::Normal => String::new(),
193 AppMode::Paused => " | Mode: PAUSED".to_string(),
194 AppMode::Intercepting => format!(
195 " | Mode: INTERCEPTING ({} pending)",
196 app.pending_requests.len()
197 ),
198 };
199 let mode_color = match app.app_mode {
200 AppMode::Normal => Color::White,
201 AppMode::Paused => Color::Yellow,
202 AppMode::Intercepting => Color::Red,
203 };
204
205 let header_text = vec![Line::from(vec![
206 Span::raw("JSON-RPC Debugger | Status: "),
207 Span::styled(
208 status,
209 Style::default()
210 .fg(status_color)
211 .add_modifier(Modifier::BOLD),
212 ),
213 Span::raw(format!(
214 " | Port: {} | Target: {} | Filter: {}",
215 app.proxy_config.listen_port, app.proxy_config.target_url, app.filter_text
216 )),
217 Span::styled(
218 mode_text,
219 Style::default().fg(mode_color).add_modifier(Modifier::BOLD),
220 ),
221 ])];
222
223 let header =
224 Paragraph::new(header_text).block(Block::default().borders(Borders::ALL).title("Status"));
225
226 f.render_widget(header, area);
227}
228
229fn draw_main_content(f: &mut Frame, area: Rect, app: &App) {
230 let chunks = Layout::default()
231 .direction(Direction::Horizontal)
232 .constraints([
233 Constraint::Percentage(50), Constraint::Percentage(50), ])
236 .split(area);
237
238 draw_message_list(f, chunks[0], app);
239 draw_message_details(f, chunks[1], app);
240}
241
242fn draw_message_list(f: &mut Frame, area: Rect, app: &App) {
243 if app.exchanges.is_empty() {
244 let empty_message = if app.is_running {
245 format!(
246 "Proxy is running on port {}. Waiting for JSON-RPC requests...",
247 app.proxy_config.listen_port
248 )
249 } else {
250 "Press 's' to start the proxy and begin capturing messages".to_string()
251 };
252
253 let paragraph = Paragraph::new(empty_message.as_str())
254 .block(Block::default().borders(Borders::ALL).title("JSON-RPC"))
255 .style(Style::default().fg(Color::Gray))
256 .wrap(Wrap { trim: true });
257
258 f.render_widget(paragraph, area);
259 return;
260 }
261
262 let header = Row::new(vec![
264 Cell::from("Status"),
265 Cell::from("Transport"),
266 Cell::from("Method"),
267 Cell::from("ID"),
268 Cell::from("Duration"),
269 ])
270 .style(Style::default().add_modifier(Modifier::BOLD))
271 .height(1);
272
273 let rows: Vec<Row> = app
275 .exchanges
276 .iter()
277 .enumerate()
278 .filter(|(_, exchange)| {
279 if app.filter_text.is_empty() {
280 true
281 } else {
282 exchange
284 .method
285 .as_deref()
286 .unwrap_or("")
287 .contains(&app.filter_text)
288 }
289 })
290 .map(|(i, exchange)| {
291 let transport_symbol = match exchange.transport {
292 TransportType::Http => "HTTP",
293 TransportType::WebSocket => "WS",
294 };
295
296 let method = exchange.method.as_deref().unwrap_or("unknown");
297 let id = exchange
298 .id
299 .as_ref()
300 .map(|v| match v {
301 serde_json::Value::String(s) => s.clone(),
302 serde_json::Value::Number(n) => n.to_string(),
303 _ => v.to_string(),
304 })
305 .unwrap_or_else(|| "null".to_string());
306
307 let (status_symbol, status_color) = if exchange.response.is_none() {
309 ("⏳ Pending", Color::Yellow)
310 } else if let Some(response) = &exchange.response {
311 if response.error.is_some() {
312 ("✗ Error", Color::Red)
313 } else {
314 ("✓ Success", Color::Green)
315 }
316 } else {
317 ("? Unknown", Color::Gray)
318 };
319
320 let duration_text =
322 if let (Some(request), Some(response)) = (&exchange.request, &exchange.response) {
323 match response.timestamp.duration_since(request.timestamp) {
324 Ok(duration) => {
325 let millis = duration.as_millis();
326 if millis < 1000 {
327 format!("{}ms", millis)
328 } else {
329 format!("{:.2}s", duration.as_secs_f64())
330 }
331 }
332 Err(_) => "-".to_string(),
333 }
334 } else {
335 "-".to_string()
336 };
337
338 let style = if i == app.selected_exchange {
339 Style::default()
340 .bg(Color::Cyan)
341 .fg(Color::Black)
342 .add_modifier(Modifier::BOLD)
343 } else {
344 Style::default()
345 };
346
347 Row::new(vec![
348 Cell::from(status_symbol).style(Style::default().fg(status_color)),
349 Cell::from(transport_symbol).style(Style::default().fg(Color::Blue)),
350 Cell::from(method).style(Style::default().fg(Color::Red)),
351 Cell::from(id).style(Style::default().fg(Color::Gray)),
352 Cell::from(duration_text).style(Style::default().fg(Color::Magenta)),
353 ])
354 .style(style)
355 .height(1)
356 })
357 .collect();
358
359 let table = Table::new(
360 rows,
361 [
362 Constraint::Length(12), Constraint::Length(9), Constraint::Min(15), Constraint::Length(12), Constraint::Length(10), ],
368 )
369 .header(header)
370 .block(Block::default().borders(Borders::ALL).title("JSON-RPC"))
371 .highlight_style(
372 Style::default()
373 .bg(Color::Cyan)
374 .fg(Color::Black)
375 .add_modifier(Modifier::BOLD),
376 )
377 .highlight_symbol("→ ");
378
379 let mut table_state = TableState::default();
380 table_state.select(Some(app.selected_exchange));
381 f.render_stateful_widget(table, area, &mut table_state);
382}
383
384fn draw_message_details(f: &mut Frame, area: Rect, app: &App) {
385 let content = if let Some(exchange) = app.get_selected_exchange() {
386 let mut lines = Vec::new();
387
388 lines.push(Line::from(vec![
390 Span::styled("Transport: ", Style::default().add_modifier(Modifier::BOLD)),
391 Span::raw(format!("{:?}", exchange.transport)),
392 ]));
393
394 if let Some(method) = &exchange.method {
395 lines.push(Line::from(vec![
396 Span::styled("Method: ", Style::default().add_modifier(Modifier::BOLD)),
397 Span::raw(method.clone()),
398 ]));
399 }
400
401 if let Some(id) = &exchange.id {
402 lines.push(Line::from(vec![
403 Span::styled("ID: ", Style::default().add_modifier(Modifier::BOLD)),
404 Span::raw(id.to_string()),
405 ]));
406 }
407
408 if let Some(request) = &exchange.request {
410 lines.push(Line::from(""));
411 lines.push(Line::from(Span::styled(
412 "REQUEST:",
413 Style::default()
414 .add_modifier(Modifier::BOLD)
415 .fg(Color::Green),
416 )));
417
418 if let Some(headers) = &request.headers {
420 lines.push(Line::from(""));
421 lines.push(Line::from("HTTP Headers:"));
422 for (key, value) in headers {
423 lines.push(Line::from(format!(" {}: {}", key, value)));
424 }
425 }
426
427 lines.push(Line::from(""));
429 lines.push(Line::from(Span::styled(
430 "JSON-RPC Request:",
431 Style::default()
432 .fg(Color::Green)
433 .add_modifier(Modifier::BOLD),
434 )));
435 lines.push(Line::from(""));
436 let mut request_json = serde_json::Map::new();
437 request_json.insert(
438 "jsonrpc".to_string(),
439 serde_json::Value::String("2.0".to_string()),
440 );
441
442 if let Some(id) = &request.id {
443 request_json.insert("id".to_string(), id.clone());
444 }
445 if let Some(method) = &request.method {
446 request_json.insert(
447 "method".to_string(),
448 serde_json::Value::String(method.clone()),
449 );
450 }
451 if let Some(params) = &request.params {
452 request_json.insert("params".to_string(), params.clone());
453 }
454
455 let request_json_value = serde_json::Value::Object(request_json);
456 let request_json_lines = format_json_with_highlighting(&request_json_value);
457 for line in request_json_lines {
458 lines.push(line);
459 }
460 }
461
462 if let Some(response) = &exchange.response {
464 lines.push(Line::from(""));
465 lines.push(Line::from(Span::styled(
466 "RESPONSE:",
467 Style::default()
468 .add_modifier(Modifier::BOLD)
469 .fg(Color::Blue),
470 )));
471
472 if let Some(headers) = &response.headers {
474 lines.push(Line::from(""));
475 lines.push(Line::from("HTTP Headers:"));
476 for (key, value) in headers {
477 lines.push(Line::from(format!(" {}: {}", key, value)));
478 }
479 }
480
481 lines.push(Line::from(""));
483 lines.push(Line::from(Span::styled(
484 "JSON-RPC Response:",
485 Style::default()
486 .fg(Color::Blue)
487 .add_modifier(Modifier::BOLD),
488 )));
489 lines.push(Line::from(""));
490 let mut response_json = serde_json::Map::new();
491 response_json.insert(
492 "jsonrpc".to_string(),
493 serde_json::Value::String("2.0".to_string()),
494 );
495
496 if let Some(id) = &response.id {
497 response_json.insert("id".to_string(), id.clone());
498 }
499 if let Some(result) = &response.result {
500 response_json.insert("result".to_string(), result.clone());
501 }
502 if let Some(error) = &response.error {
503 response_json.insert("error".to_string(), error.clone());
504 }
505
506 let response_json_value = serde_json::Value::Object(response_json);
507 let response_json_lines = format_json_with_highlighting(&response_json_value);
508 for line in response_json_lines {
509 lines.push(line);
510 }
511 } else {
512 lines.push(Line::from(""));
513 lines.push(Line::from(Span::styled(
514 "RESPONSE: Pending...",
515 Style::default()
516 .add_modifier(Modifier::BOLD)
517 .fg(Color::Yellow),
518 )));
519 }
520
521 lines
522 } else {
523 vec![Line::from("No request selected")]
524 };
525
526 let inner_area = area.inner(&Margin {
528 vertical: 1,
529 horizontal: 1,
530 });
531 let visible_lines = inner_area.height as usize;
532 let total_lines = content.len();
533
534 let start_line = app.details_scroll;
536 let end_line = std::cmp::min(start_line + visible_lines, total_lines);
537 let visible_content = if start_line < total_lines {
538 content[start_line..end_line].to_vec()
539 } else {
540 vec![]
541 };
542
543 let scroll_info = if total_lines > visible_lines {
545 let progress =
546 ((app.details_scroll as f32 / (total_lines - visible_lines) as f32) * 100.0) as u8;
547 format!("Details ({}% - vim: j/k/d/u/G/g)", progress)
548 } else {
549 "Details".to_string()
550 };
551
552 let details = Paragraph::new(visible_content)
553 .block(Block::default().borders(Borders::ALL).title(scroll_info))
554 .wrap(Wrap { trim: false });
555
556 f.render_widget(details, area);
557}
558
559#[derive(Clone)]
561struct KeybindInfo {
562 key: String,
563 description: String,
564 priority: u8, }
566
567impl KeybindInfo {
568 fn new(key: &str, description: &str, priority: u8) -> Self {
569 Self {
570 key: key.to_string(),
571 description: description.to_string(),
572 priority,
573 }
574 }
575
576 fn display_width(&self) -> usize {
578 self.key.len() + 1 + self.description.len() + 3 }
580
581 fn to_spans(&self) -> Vec<Span<'static>> {
583 vec![
584 Span::styled(
585 self.key.clone(),
586 Style::default()
587 .fg(Color::Yellow)
588 .add_modifier(Modifier::BOLD),
589 ),
590 Span::raw(format!(" {} | ", self.description)),
591 ]
592 }
593}
594
595fn get_keybinds_for_mode(app: &App) -> Vec<KeybindInfo> {
596 let mut keybinds = vec![
597 KeybindInfo::new("q", "quit", 1),
599 KeybindInfo::new("↑↓", "navigate", 1),
600 KeybindInfo::new("s", "start/stop proxy", 1),
601 KeybindInfo::new("^n/^p", "navigate", 2),
603 KeybindInfo::new("t", "edit target", 2),
604 KeybindInfo::new("/", "filter", 2),
605 KeybindInfo::new("p", "pause", 2),
606 KeybindInfo::new("j/k/d/u/G/g", "scroll details", 3),
608 ];
609
610 match app.app_mode {
612 AppMode::Paused | AppMode::Intercepting => {
613 if !app.pending_requests.is_empty() {
615 keybinds.extend(vec![
616 KeybindInfo::new("a", "allow", 4),
617 KeybindInfo::new("e", "edit", 4),
618 KeybindInfo::new("h", "headers", 4),
619 KeybindInfo::new("c", "complete", 4),
620 KeybindInfo::new("b", "block", 4),
621 KeybindInfo::new("r", "resume", 4),
622 ]);
623 }
624 }
625 AppMode::Normal => {
626 keybinds.push(KeybindInfo::new("c", "create request", 4));
627 }
628 }
629
630 keybinds
631}
632
633fn arrange_keybinds_responsive(
634 keybinds: Vec<KeybindInfo>,
635 available_width: usize,
636) -> Vec<Vec<Span<'static>>> {
637 let mut lines = Vec::new();
638 let mut current_line_spans = Vec::new();
639 let mut current_line_width = 0;
640
641 let usable_width = available_width.saturating_sub(4);
643
644 let mut sorted_keybinds = keybinds;
646 sorted_keybinds.sort_by_key(|k| k.priority);
647
648 for (i, keybind) in sorted_keybinds.iter().enumerate() {
649 let keybind_width = keybind.display_width();
650 let is_last = i == sorted_keybinds.len() - 1;
651
652 let width_needed = if is_last {
654 keybind_width - 3 } else {
656 keybind_width
657 };
658
659 if current_line_width + width_needed <= usable_width || current_line_spans.is_empty() {
660 let mut spans = keybind.to_spans();
662 if is_last {
663 if let Some(last_span) = spans.last_mut() {
665 if let Some(content) = last_span.content.strip_suffix(" | ") {
666 *last_span = Span::raw(content.to_string());
667 }
668 }
669 }
670 current_line_spans.extend(spans);
671 current_line_width += width_needed;
672 } else {
673 if let Some(last_span) = current_line_spans.last_mut() {
676 if let Some(content) = last_span.content.strip_suffix(" | ") {
677 *last_span = Span::raw(content.to_string());
678 }
679 }
680
681 lines.push(current_line_spans);
682 current_line_spans = keybind.to_spans();
683 current_line_width = keybind_width;
684
685 if is_last {
687 if let Some(last_span) = current_line_spans.last_mut() {
688 if let Some(content) = last_span.content.strip_suffix(" | ") {
689 *last_span = Span::raw(content.to_string());
690 }
691 }
692 }
693 }
694 }
695
696 if !current_line_spans.is_empty() {
698 lines.push(current_line_spans);
699 }
700
701 lines
702}
703
704fn draw_footer(f: &mut Frame, area: Rect, app: &App) {
705 let keybinds = get_keybinds_for_mode(app);
706 let available_width = area.width as usize;
707
708 let line_spans = arrange_keybinds_responsive(keybinds, available_width);
709
710 let footer_text: Vec<Line> = line_spans.into_iter().map(Line::from).collect();
712
713 let footer =
714 Paragraph::new(footer_text).block(Block::default().borders(Borders::ALL).title("Controls"));
715
716 f.render_widget(footer, area);
717}
718
719fn draw_input_dialog(f: &mut Frame, app: &App, title: &str, label: &str) {
720 let area = f.size();
721
722 let popup_area = Rect {
724 x: area.width / 4,
725 y: area.height / 2 - 3,
726 width: area.width / 2,
727 height: 7,
728 };
729
730 f.render_widget(Clear, area);
732
733 let background = Block::default().style(Style::default().bg(Color::Black));
735 f.render_widget(background, area);
736
737 f.render_widget(Clear, popup_area);
739
740 let input_text = vec![
741 Line::from(""),
742 Line::from(vec![
743 Span::raw(format!("{}: ", label)),
744 Span::styled(&app.input_buffer, Style::default().fg(Color::Green)),
745 ]),
746 Line::from(""),
747 Line::from(Span::styled(
748 "Press Enter to confirm, Esc to cancel",
749 Style::default().fg(Color::Gray),
750 )),
751 Line::from(""),
752 ];
753
754 let input_dialog = Paragraph::new(input_text)
755 .block(
756 Block::default()
757 .borders(Borders::ALL)
758 .title(title)
759 .style(Style::default().fg(Color::White).bg(Color::DarkGray)),
760 )
761 .wrap(Wrap { trim: true });
762
763 f.render_widget(input_dialog, popup_area);
764}
765
766fn draw_intercept_content(f: &mut Frame, area: Rect, app: &App) {
767 let chunks = Layout::default()
768 .direction(Direction::Horizontal)
769 .constraints([
770 Constraint::Percentage(50), Constraint::Percentage(50), ])
773 .split(area);
774
775 draw_pending_requests(f, chunks[0], app);
776 draw_request_details(f, chunks[1], app);
777}
778
779fn draw_pending_requests(f: &mut Frame, area: Rect, app: &App) {
780 if app.pending_requests.is_empty() {
781 let mode_text = match app.app_mode {
782 AppMode::Paused => "Pause mode active. New requests will be intercepted.",
783 _ => "No pending requests.",
784 };
785
786 let paragraph = Paragraph::new(mode_text)
787 .block(
788 Block::default()
789 .borders(Borders::ALL)
790 .title("Pending Requests"),
791 )
792 .style(Style::default().fg(Color::Yellow))
793 .wrap(Wrap { trim: true });
794
795 f.render_widget(paragraph, area);
796 return;
797 }
798
799 let requests: Vec<ListItem> = app
800 .pending_requests
801 .iter()
802 .enumerate()
803 .filter(|(_, pending)| {
804 if app.filter_text.is_empty() {
805 true
806 } else {
807 pending
809 .original_request
810 .method
811 .as_deref()
812 .unwrap_or("")
813 .contains(&app.filter_text)
814 }
815 })
816 .map(|(i, pending)| {
817 let method = pending
818 .original_request
819 .method
820 .as_deref()
821 .unwrap_or("unknown");
822 let id = pending
823 .original_request
824 .id
825 .as_ref()
826 .map(|v| v.to_string())
827 .unwrap_or_else(|| "null".to_string());
828
829 let style = if i == app.selected_pending {
830 Style::default()
831 .bg(Color::Cyan)
832 .fg(Color::Black)
833 .add_modifier(Modifier::BOLD)
834 } else {
835 Style::default()
836 };
837
838 let (icon, icon_color) =
840 if pending.modified_request.is_some() || pending.modified_headers.is_some() {
841 ("✏ ", Color::Blue) } else {
843 ("⏸ ", Color::Red) };
845
846 let mut modification_labels = Vec::new();
847 if pending.modified_request.is_some() {
848 modification_labels.push("BODY");
849 }
850 if pending.modified_headers.is_some() {
851 modification_labels.push("HEADERS");
852 }
853 let modification_text = if !modification_labels.is_empty() {
854 format!(" [{}]", modification_labels.join("+"))
855 } else {
856 String::new()
857 };
858
859 ListItem::new(Line::from(vec![
860 Span::styled(icon, Style::default().fg(icon_color)),
861 Span::styled(format!("{} ", method), Style::default().fg(Color::Red)),
862 Span::styled(format!("(id: {})", id), Style::default().fg(Color::Gray)),
863 if !modification_text.is_empty() {
864 Span::styled(
865 modification_text,
866 Style::default()
867 .fg(Color::Blue)
868 .add_modifier(Modifier::BOLD),
869 )
870 } else {
871 Span::raw("")
872 },
873 ]))
874 .style(style)
875 })
876 .collect();
877
878 let requests_list = List::new(requests)
879 .block(
880 Block::default()
881 .borders(Borders::ALL)
882 .title(format!("Pending Requests ({})", app.pending_requests.len())),
883 )
884 .highlight_style(
885 Style::default()
886 .bg(Color::Cyan)
887 .fg(Color::Black)
888 .add_modifier(Modifier::BOLD),
889 );
890
891 f.render_widget(requests_list, area);
892}
893
894fn draw_request_details(f: &mut Frame, area: Rect, app: &App) {
895 let content = if let Some(pending) = app.get_selected_pending() {
896 let mut lines = Vec::new();
897
898 if pending.modified_request.is_some() || pending.modified_headers.is_some() {
899 lines.push(Line::from(Span::styled(
900 "MODIFIED REQUEST:",
901 Style::default()
902 .add_modifier(Modifier::BOLD)
903 .fg(Color::Blue),
904 )));
905 } else {
906 lines.push(Line::from(Span::styled(
907 "INTERCEPTED REQUEST:",
908 Style::default().add_modifier(Modifier::BOLD).fg(Color::Red),
909 )));
910 }
911 lines.push(Line::from(""));
912
913 lines.push(Line::from(Span::styled(
915 "HTTP Headers:",
916 Style::default()
917 .add_modifier(Modifier::BOLD)
918 .fg(Color::Green),
919 )));
920 let headers_to_show = pending
921 .modified_headers
922 .as_ref()
923 .or(pending.original_request.headers.as_ref());
924
925 if let Some(headers) = headers_to_show {
926 for (key, value) in headers {
927 lines.push(Line::from(format!(" {}: {}", key, value)));
928 }
929 if pending.modified_headers.is_some() {
930 lines.push(Line::from(Span::styled(
931 " [Headers have been modified]",
932 Style::default()
933 .fg(Color::Blue)
934 .add_modifier(Modifier::ITALIC),
935 )));
936 }
937 } else {
938 lines.push(Line::from(" No headers"));
939 }
940 lines.push(Line::from(""));
941
942 lines.push(Line::from(Span::styled(
944 "JSON-RPC Request:",
945 Style::default()
946 .add_modifier(Modifier::BOLD)
947 .fg(Color::Green),
948 )));
949
950 let json_to_show = if let Some(ref modified_json) = pending.modified_request {
952 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(modified_json) {
953 parsed
954 } else {
955 let mut request_json = serde_json::Map::new();
957 request_json.insert(
958 "jsonrpc".to_string(),
959 serde_json::Value::String("2.0".to_string()),
960 );
961
962 if let Some(id) = &pending.original_request.id {
963 request_json.insert("id".to_string(), id.clone());
964 }
965 if let Some(method) = &pending.original_request.method {
966 request_json.insert(
967 "method".to_string(),
968 serde_json::Value::String(method.clone()),
969 );
970 }
971 if let Some(params) = &pending.original_request.params {
972 request_json.insert("params".to_string(), params.clone());
973 }
974
975 serde_json::Value::Object(request_json)
976 }
977 } else {
978 let mut request_json = serde_json::Map::new();
980 request_json.insert(
981 "jsonrpc".to_string(),
982 serde_json::Value::String("2.0".to_string()),
983 );
984
985 if let Some(id) = &pending.original_request.id {
986 request_json.insert("id".to_string(), id.clone());
987 }
988 if let Some(method) = &pending.original_request.method {
989 request_json.insert(
990 "method".to_string(),
991 serde_json::Value::String(method.clone()),
992 );
993 }
994 if let Some(params) = &pending.original_request.params {
995 request_json.insert("params".to_string(), params.clone());
996 }
997
998 serde_json::Value::Object(request_json)
999 };
1000
1001 let request_json_lines = format_json_with_highlighting(&json_to_show);
1002 for line in request_json_lines {
1003 lines.push(line);
1004 }
1005
1006 lines.push(Line::from(""));
1007 lines.push(Line::from(Span::styled(
1008 "Actions:",
1009 Style::default().add_modifier(Modifier::BOLD),
1010 )));
1011 lines.push(Line::from("• Press 'a' to Allow request"));
1012 lines.push(Line::from("• Press 'e' to Edit request body"));
1013 lines.push(Line::from("• Press 'h' to Edit headers"));
1014 lines.push(Line::from("• Press 'c' to Complete with custom response"));
1015 lines.push(Line::from("• Press 'b' to Block request"));
1016 lines.push(Line::from("• Press 'r' to Resume all requests"));
1017
1018 lines
1019 } else {
1020 vec![Line::from("No request selected")]
1021 };
1022
1023 let inner_area = area.inner(&Margin {
1025 vertical: 1,
1026 horizontal: 1,
1027 });
1028 let visible_lines = inner_area.height as usize;
1029 let total_lines = content.len();
1030
1031 let start_line = app.intercept_details_scroll;
1033 let end_line = std::cmp::min(start_line + visible_lines, total_lines);
1034 let visible_content = if start_line < total_lines {
1035 content[start_line..end_line].to_vec()
1036 } else {
1037 vec![]
1038 };
1039
1040 let scroll_info = if total_lines > visible_lines {
1042 let progress = ((app.intercept_details_scroll as f32
1043 / (total_lines - visible_lines) as f32)
1044 * 100.0) as u8;
1045 format!("Request Details ({}% - vim: j/k/d/u/G/g)", progress)
1046 } else {
1047 "Request Details".to_string()
1048 };
1049
1050 let details = Paragraph::new(visible_content)
1051 .block(Block::default().borders(Borders::ALL).title(scroll_info))
1052 .wrap(Wrap { trim: false });
1053
1054 f.render_widget(details, area);
1055}