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 chunks = Layout::default()
146 .direction(Direction::Vertical)
147 .constraints([
148 Constraint::Length(3), Constraint::Min(10), Constraint::Length(3), ])
152 .split(f.size());
153
154 draw_header(f, chunks[0], app);
155
156 match app.app_mode {
158 AppMode::Normal => {
159 draw_main_content(f, chunks[1], app);
160 }
161 AppMode::Paused | AppMode::Intercepting => {
162 draw_intercept_content(f, chunks[1], app);
163 }
164 }
165
166 draw_footer(f, chunks[2], app);
167
168 if app.input_mode == InputMode::EditingTarget {
170 draw_input_dialog(f, app);
171 }
172}
173
174fn draw_header(f: &mut Frame, area: Rect, app: &App) {
175 let status = if app.is_running { "RUNNING" } else { "STOPPED" };
176 let status_color = if app.is_running {
177 Color::Green
178 } else {
179 Color::Red
180 };
181
182 let mode_text = match app.app_mode {
183 AppMode::Normal => String::new(),
184 AppMode::Paused => " | Mode: PAUSED".to_string(),
185 AppMode::Intercepting => format!(
186 " | Mode: INTERCEPTING ({} pending)",
187 app.pending_requests.len()
188 ),
189 };
190 let mode_color = match app.app_mode {
191 AppMode::Normal => Color::White,
192 AppMode::Paused => Color::Yellow,
193 AppMode::Intercepting => Color::Red,
194 };
195
196 let header_text = vec![Line::from(vec![
197 Span::raw("JSON-RPC Debugger | Status: "),
198 Span::styled(
199 status,
200 Style::default()
201 .fg(status_color)
202 .add_modifier(Modifier::BOLD),
203 ),
204 Span::raw(format!(
205 " | Port: {} | Target: {}",
206 app.proxy_config.listen_port, app.proxy_config.target_url
207 )),
208 Span::styled(
209 mode_text,
210 Style::default().fg(mode_color).add_modifier(Modifier::BOLD),
211 ),
212 ])];
213
214 let header =
215 Paragraph::new(header_text).block(Block::default().borders(Borders::ALL).title("Status"));
216
217 f.render_widget(header, area);
218}
219
220fn draw_main_content(f: &mut Frame, area: Rect, app: &App) {
221 let chunks = Layout::default()
222 .direction(Direction::Horizontal)
223 .constraints([
224 Constraint::Percentage(50), Constraint::Percentage(50), ])
227 .split(area);
228
229 draw_message_list(f, chunks[0], app);
230 draw_message_details(f, chunks[1], app);
231}
232
233fn draw_message_list(f: &mut Frame, area: Rect, app: &App) {
234 if app.exchanges.is_empty() {
235 let empty_message = if app.is_running {
236 format!(
237 "Proxy is running on port {}. Waiting for JSON-RPC requests...",
238 app.proxy_config.listen_port
239 )
240 } else {
241 "Press 's' to start the proxy and begin capturing messages".to_string()
242 };
243
244 let paragraph = Paragraph::new(empty_message.as_str())
245 .block(Block::default().borders(Borders::ALL).title("JSON-RPC"))
246 .style(Style::default().fg(Color::Gray))
247 .wrap(Wrap { trim: true });
248
249 f.render_widget(paragraph, area);
250 return;
251 }
252
253 let header = Row::new(vec![
255 Cell::from("Status"),
256 Cell::from("Transport"),
257 Cell::from("Method"),
258 Cell::from("ID"),
259 Cell::from("Duration"),
260 ])
261 .style(Style::default().add_modifier(Modifier::BOLD))
262 .height(1);
263
264 let rows: Vec<Row> = app
266 .exchanges
267 .iter()
268 .enumerate()
269 .map(|(i, exchange)| {
270 let transport_symbol = match exchange.transport {
271 TransportType::Http => "HTTP",
272 TransportType::WebSocket => "WS",
273 };
274
275 let method = exchange.method.as_deref().unwrap_or("unknown");
276 let id = exchange
277 .id
278 .as_ref()
279 .map(|v| match v {
280 serde_json::Value::String(s) => s.clone(),
281 serde_json::Value::Number(n) => n.to_string(),
282 _ => v.to_string(),
283 })
284 .unwrap_or_else(|| "null".to_string());
285
286 let (status_symbol, status_color) = if exchange.response.is_none() {
288 ("⏳ Pending", Color::Yellow)
289 } else if let Some(response) = &exchange.response {
290 if response.error.is_some() {
291 ("✗ Error", Color::Red)
292 } else {
293 ("✓ Success", Color::Green)
294 }
295 } else {
296 ("? Unknown", Color::Gray)
297 };
298
299 let duration_text =
301 if let (Some(request), Some(response)) = (&exchange.request, &exchange.response) {
302 match response.timestamp.duration_since(request.timestamp) {
303 Ok(duration) => {
304 let millis = duration.as_millis();
305 if millis < 1000 {
306 format!("{}ms", millis)
307 } else {
308 format!("{:.2}s", duration.as_secs_f64())
309 }
310 }
311 Err(_) => "-".to_string(),
312 }
313 } else {
314 "-".to_string()
315 };
316
317 let style = if i == app.selected_exchange {
318 Style::default()
319 .bg(Color::Cyan)
320 .fg(Color::Black)
321 .add_modifier(Modifier::BOLD)
322 } else {
323 Style::default()
324 };
325
326 Row::new(vec![
327 Cell::from(status_symbol).style(Style::default().fg(status_color)),
328 Cell::from(transport_symbol).style(Style::default().fg(Color::Blue)),
329 Cell::from(method).style(Style::default().fg(Color::Red)),
330 Cell::from(id).style(Style::default().fg(Color::Gray)),
331 Cell::from(duration_text).style(Style::default().fg(Color::Magenta)),
332 ])
333 .style(style)
334 .height(1)
335 })
336 .collect();
337
338 let table = Table::new(
339 rows,
340 [
341 Constraint::Length(12), Constraint::Length(9), Constraint::Min(15), Constraint::Length(12), Constraint::Length(10), ],
347 )
348 .header(header)
349 .block(Block::default().borders(Borders::ALL).title("JSON-RPC"))
350 .highlight_style(
351 Style::default()
352 .bg(Color::Cyan)
353 .fg(Color::Black)
354 .add_modifier(Modifier::BOLD),
355 )
356 .highlight_symbol("→ ");
357
358 let mut table_state = TableState::default();
359 table_state.select(Some(app.selected_exchange));
360 f.render_stateful_widget(table, area, &mut table_state);
361}
362
363fn draw_message_details(f: &mut Frame, area: Rect, app: &App) {
364 let content = if let Some(exchange) = app.get_selected_exchange() {
365 let mut lines = Vec::new();
366
367 lines.push(Line::from(vec![
369 Span::styled("Transport: ", Style::default().add_modifier(Modifier::BOLD)),
370 Span::raw(format!("{:?}", exchange.transport)),
371 ]));
372
373 if let Some(method) = &exchange.method {
374 lines.push(Line::from(vec![
375 Span::styled("Method: ", Style::default().add_modifier(Modifier::BOLD)),
376 Span::raw(method.clone()),
377 ]));
378 }
379
380 if let Some(id) = &exchange.id {
381 lines.push(Line::from(vec![
382 Span::styled("ID: ", Style::default().add_modifier(Modifier::BOLD)),
383 Span::raw(id.to_string()),
384 ]));
385 }
386
387 if let Some(request) = &exchange.request {
389 lines.push(Line::from(""));
390 lines.push(Line::from(Span::styled(
391 "REQUEST:",
392 Style::default()
393 .add_modifier(Modifier::BOLD)
394 .fg(Color::Green),
395 )));
396
397 if let Some(headers) = &request.headers {
399 lines.push(Line::from(""));
400 lines.push(Line::from("HTTP Headers:"));
401 for (key, value) in headers {
402 lines.push(Line::from(format!(" {}: {}", key, value)));
403 }
404 }
405
406 lines.push(Line::from(""));
408 lines.push(Line::from(Span::styled(
409 "JSON-RPC Request:",
410 Style::default()
411 .fg(Color::Green)
412 .add_modifier(Modifier::BOLD),
413 )));
414 lines.push(Line::from(""));
415 let mut request_json = serde_json::Map::new();
416 request_json.insert(
417 "jsonrpc".to_string(),
418 serde_json::Value::String("2.0".to_string()),
419 );
420
421 if let Some(id) = &request.id {
422 request_json.insert("id".to_string(), id.clone());
423 }
424 if let Some(method) = &request.method {
425 request_json.insert(
426 "method".to_string(),
427 serde_json::Value::String(method.clone()),
428 );
429 }
430 if let Some(params) = &request.params {
431 request_json.insert("params".to_string(), params.clone());
432 }
433
434 let request_json_value = serde_json::Value::Object(request_json);
435 let request_json_lines = format_json_with_highlighting(&request_json_value);
436 for line in request_json_lines {
437 lines.push(line);
438 }
439 }
440
441 if let Some(response) = &exchange.response {
443 lines.push(Line::from(""));
444 lines.push(Line::from(Span::styled(
445 "RESPONSE:",
446 Style::default()
447 .add_modifier(Modifier::BOLD)
448 .fg(Color::Blue),
449 )));
450
451 if let Some(headers) = &response.headers {
453 lines.push(Line::from(""));
454 lines.push(Line::from("HTTP Headers:"));
455 for (key, value) in headers {
456 lines.push(Line::from(format!(" {}: {}", key, value)));
457 }
458 }
459
460 lines.push(Line::from(""));
462 lines.push(Line::from(Span::styled(
463 "JSON-RPC Response:",
464 Style::default()
465 .fg(Color::Blue)
466 .add_modifier(Modifier::BOLD),
467 )));
468 lines.push(Line::from(""));
469 let mut response_json = serde_json::Map::new();
470 response_json.insert(
471 "jsonrpc".to_string(),
472 serde_json::Value::String("2.0".to_string()),
473 );
474
475 if let Some(id) = &response.id {
476 response_json.insert("id".to_string(), id.clone());
477 }
478 if let Some(result) = &response.result {
479 response_json.insert("result".to_string(), result.clone());
480 }
481 if let Some(error) = &response.error {
482 response_json.insert("error".to_string(), error.clone());
483 }
484
485 let response_json_value = serde_json::Value::Object(response_json);
486 let response_json_lines = format_json_with_highlighting(&response_json_value);
487 for line in response_json_lines {
488 lines.push(line);
489 }
490 } else {
491 lines.push(Line::from(""));
492 lines.push(Line::from(Span::styled(
493 "RESPONSE: Pending...",
494 Style::default()
495 .add_modifier(Modifier::BOLD)
496 .fg(Color::Yellow),
497 )));
498 }
499
500 lines
501 } else {
502 vec![Line::from("No request selected")]
503 };
504
505 let inner_area = area.inner(&Margin {
507 vertical: 1,
508 horizontal: 1,
509 });
510 let visible_lines = inner_area.height as usize;
511 let total_lines = content.len();
512
513 let start_line = app.details_scroll;
515 let end_line = std::cmp::min(start_line + visible_lines, total_lines);
516 let visible_content = if start_line < total_lines {
517 content[start_line..end_line].to_vec()
518 } else {
519 vec![]
520 };
521
522 let scroll_info = if total_lines > visible_lines {
524 let progress =
525 ((app.details_scroll as f32 / (total_lines - visible_lines) as f32) * 100.0) as u8;
526 format!("Details ({}% - vim: j/k/d/u/G/g)", progress)
527 } else {
528 "Details".to_string()
529 };
530
531 let details = Paragraph::new(visible_content)
532 .block(Block::default().borders(Borders::ALL).title(scroll_info))
533 .wrap(Wrap { trim: false });
534
535 f.render_widget(details, area);
536}
537
538fn draw_footer(f: &mut Frame, area: Rect, app: &App) {
539 let mut footer_spans = vec![
540 Span::styled(
541 "q",
542 Style::default()
543 .fg(Color::Yellow)
544 .add_modifier(Modifier::BOLD),
545 ),
546 Span::raw(" quit | "),
547 Span::styled(
548 "↑↓",
549 Style::default()
550 .fg(Color::Yellow)
551 .add_modifier(Modifier::BOLD),
552 ),
553 Span::raw("/"),
554 Span::styled(
555 "^n/^p",
556 Style::default()
557 .fg(Color::Yellow)
558 .add_modifier(Modifier::BOLD),
559 ),
560 Span::raw(" navigate | "),
561 Span::styled(
562 "j/k/d/u/G/g",
563 Style::default()
564 .fg(Color::Yellow)
565 .add_modifier(Modifier::BOLD),
566 ),
567 Span::raw(" scroll details | "),
568 Span::styled(
569 "s",
570 Style::default()
571 .fg(Color::Yellow)
572 .add_modifier(Modifier::BOLD),
573 ),
574 Span::raw(" start/stop proxy | "),
575 Span::styled(
576 "t",
577 Style::default()
578 .fg(Color::Yellow)
579 .add_modifier(Modifier::BOLD),
580 ),
581 Span::raw(" edit target | "),
582 Span::styled(
583 "p",
584 Style::default()
585 .fg(Color::Yellow)
586 .add_modifier(Modifier::BOLD),
587 ),
588 Span::raw(" pause"),
589 ];
590
591 match app.app_mode {
593 AppMode::Paused | AppMode::Intercepting => {
594 footer_spans.extend(vec![
595 Span::raw(" | "),
596 Span::styled(
597 "a/e/h/c/b/r",
598 Style::default()
599 .fg(Color::Yellow)
600 .add_modifier(Modifier::BOLD),
601 ),
602 Span::raw(" allow/edit/headers/complete/block/resume"),
603 ]);
604 }
605 AppMode::Normal => {
606 footer_spans.extend(vec![
607 Span::raw(" | "),
608 Span::styled(
609 "c",
610 Style::default()
611 .fg(Color::Yellow)
612 .add_modifier(Modifier::BOLD),
613 ),
614 Span::raw(" create request"),
615 ]);
616 }
617 }
618
619 let footer_text = vec![Line::from(footer_spans)];
620
621 let footer =
622 Paragraph::new(footer_text).block(Block::default().borders(Borders::ALL).title("Controls"));
623
624 f.render_widget(footer, area);
625}
626
627fn draw_input_dialog(f: &mut Frame, app: &App) {
628 let area = f.size();
629
630 let popup_area = Rect {
632 x: area.width / 4,
633 y: area.height / 2 - 3,
634 width: area.width / 2,
635 height: 7,
636 };
637
638 f.render_widget(Clear, area);
640
641 let background = Block::default().style(Style::default().bg(Color::Black));
643 f.render_widget(background, area);
644
645 f.render_widget(Clear, popup_area);
647
648 let input_text = vec![
649 Line::from(""),
650 Line::from(vec![
651 Span::raw("Target URL: "),
652 Span::styled(&app.input_buffer, Style::default().fg(Color::Green)),
653 ]),
654 Line::from(""),
655 Line::from(Span::styled(
656 "Press Enter to confirm, Esc to cancel",
657 Style::default().fg(Color::Gray),
658 )),
659 Line::from(""),
660 ];
661
662 let input_dialog = Paragraph::new(input_text)
663 .block(
664 Block::default()
665 .borders(Borders::ALL)
666 .title("Edit Target URL")
667 .style(Style::default().fg(Color::White).bg(Color::DarkGray)),
668 )
669 .wrap(Wrap { trim: true });
670
671 f.render_widget(input_dialog, popup_area);
672}
673
674fn draw_intercept_content(f: &mut Frame, area: Rect, app: &App) {
675 let chunks = Layout::default()
676 .direction(Direction::Horizontal)
677 .constraints([
678 Constraint::Percentage(50), Constraint::Percentage(50), ])
681 .split(area);
682
683 draw_pending_requests(f, chunks[0], app);
684 draw_request_details(f, chunks[1], app);
685}
686
687fn draw_pending_requests(f: &mut Frame, area: Rect, app: &App) {
688 if app.pending_requests.is_empty() {
689 let mode_text = match app.app_mode {
690 AppMode::Paused => "Pause mode active. New requests will be intercepted.",
691 _ => "No pending requests.",
692 };
693
694 let paragraph = Paragraph::new(mode_text)
695 .block(
696 Block::default()
697 .borders(Borders::ALL)
698 .title("Pending Requests"),
699 )
700 .style(Style::default().fg(Color::Yellow))
701 .wrap(Wrap { trim: true });
702
703 f.render_widget(paragraph, area);
704 return;
705 }
706
707 let requests: Vec<ListItem> = app
708 .pending_requests
709 .iter()
710 .enumerate()
711 .map(|(i, pending)| {
712 let method = pending
713 .original_request
714 .method
715 .as_deref()
716 .unwrap_or("unknown");
717 let id = pending
718 .original_request
719 .id
720 .as_ref()
721 .map(|v| v.to_string())
722 .unwrap_or_else(|| "null".to_string());
723
724 let style = if i == app.selected_pending {
725 Style::default()
726 .bg(Color::Cyan)
727 .fg(Color::Black)
728 .add_modifier(Modifier::BOLD)
729 } else {
730 Style::default()
731 };
732
733 let (icon, icon_color) =
735 if pending.modified_request.is_some() || pending.modified_headers.is_some() {
736 ("✏ ", Color::Blue) } else {
738 ("⏸ ", Color::Red) };
740
741 let mut modification_labels = Vec::new();
742 if pending.modified_request.is_some() {
743 modification_labels.push("BODY");
744 }
745 if pending.modified_headers.is_some() {
746 modification_labels.push("HEADERS");
747 }
748 let modification_text = if !modification_labels.is_empty() {
749 format!(" [{}]", modification_labels.join("+"))
750 } else {
751 String::new()
752 };
753
754 ListItem::new(Line::from(vec![
755 Span::styled(icon, Style::default().fg(icon_color)),
756 Span::styled(format!("{} ", method), Style::default().fg(Color::Red)),
757 Span::styled(format!("(id: {})", id), Style::default().fg(Color::Gray)),
758 if !modification_text.is_empty() {
759 Span::styled(
760 modification_text,
761 Style::default()
762 .fg(Color::Blue)
763 .add_modifier(Modifier::BOLD),
764 )
765 } else {
766 Span::raw("")
767 },
768 ]))
769 .style(style)
770 })
771 .collect();
772
773 let requests_list = List::new(requests)
774 .block(
775 Block::default()
776 .borders(Borders::ALL)
777 .title(format!("Pending Requests ({})", app.pending_requests.len())),
778 )
779 .highlight_style(
780 Style::default()
781 .bg(Color::Cyan)
782 .fg(Color::Black)
783 .add_modifier(Modifier::BOLD),
784 );
785
786 f.render_widget(requests_list, area);
787}
788
789fn draw_request_details(f: &mut Frame, area: Rect, app: &App) {
790 let content = if let Some(pending) = app.get_selected_pending() {
791 let mut lines = Vec::new();
792
793 if pending.modified_request.is_some() || pending.modified_headers.is_some() {
794 lines.push(Line::from(Span::styled(
795 "MODIFIED REQUEST:",
796 Style::default()
797 .add_modifier(Modifier::BOLD)
798 .fg(Color::Blue),
799 )));
800 } else {
801 lines.push(Line::from(Span::styled(
802 "INTERCEPTED REQUEST:",
803 Style::default().add_modifier(Modifier::BOLD).fg(Color::Red),
804 )));
805 }
806 lines.push(Line::from(""));
807
808 lines.push(Line::from(Span::styled(
810 "HTTP Headers:",
811 Style::default()
812 .add_modifier(Modifier::BOLD)
813 .fg(Color::Green),
814 )));
815 let headers_to_show = pending
816 .modified_headers
817 .as_ref()
818 .or(pending.original_request.headers.as_ref());
819
820 if let Some(headers) = headers_to_show {
821 for (key, value) in headers {
822 lines.push(Line::from(format!(" {}: {}", key, value)));
823 }
824 if pending.modified_headers.is_some() {
825 lines.push(Line::from(Span::styled(
826 " [Headers have been modified]",
827 Style::default()
828 .fg(Color::Blue)
829 .add_modifier(Modifier::ITALIC),
830 )));
831 }
832 } else {
833 lines.push(Line::from(" No headers"));
834 }
835 lines.push(Line::from(""));
836
837 lines.push(Line::from(Span::styled(
839 "JSON-RPC Request:",
840 Style::default()
841 .add_modifier(Modifier::BOLD)
842 .fg(Color::Green),
843 )));
844
845 let json_to_show = if let Some(ref modified_json) = pending.modified_request {
847 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(modified_json) {
848 parsed
849 } else {
850 let mut request_json = serde_json::Map::new();
852 request_json.insert(
853 "jsonrpc".to_string(),
854 serde_json::Value::String("2.0".to_string()),
855 );
856
857 if let Some(id) = &pending.original_request.id {
858 request_json.insert("id".to_string(), id.clone());
859 }
860 if let Some(method) = &pending.original_request.method {
861 request_json.insert(
862 "method".to_string(),
863 serde_json::Value::String(method.clone()),
864 );
865 }
866 if let Some(params) = &pending.original_request.params {
867 request_json.insert("params".to_string(), params.clone());
868 }
869
870 serde_json::Value::Object(request_json)
871 }
872 } else {
873 let mut request_json = serde_json::Map::new();
875 request_json.insert(
876 "jsonrpc".to_string(),
877 serde_json::Value::String("2.0".to_string()),
878 );
879
880 if let Some(id) = &pending.original_request.id {
881 request_json.insert("id".to_string(), id.clone());
882 }
883 if let Some(method) = &pending.original_request.method {
884 request_json.insert(
885 "method".to_string(),
886 serde_json::Value::String(method.clone()),
887 );
888 }
889 if let Some(params) = &pending.original_request.params {
890 request_json.insert("params".to_string(), params.clone());
891 }
892
893 serde_json::Value::Object(request_json)
894 };
895
896 let request_json_lines = format_json_with_highlighting(&json_to_show);
897 for line in request_json_lines {
898 lines.push(line);
899 }
900
901 lines.push(Line::from(""));
902 lines.push(Line::from(Span::styled(
903 "Actions:",
904 Style::default().add_modifier(Modifier::BOLD),
905 )));
906 lines.push(Line::from("• Press 'a' to Allow request"));
907 lines.push(Line::from("• Press 'e' to Edit request body"));
908 lines.push(Line::from("• Press 'h' to Edit headers"));
909 lines.push(Line::from("• Press 'c' to Complete with custom response"));
910 lines.push(Line::from("• Press 'b' to Block request"));
911 lines.push(Line::from("• Press 'r' to Resume all requests"));
912
913 lines
914 } else {
915 vec![Line::from("No request selected")]
916 };
917
918 let inner_area = area.inner(&Margin {
920 vertical: 1,
921 horizontal: 1,
922 });
923 let visible_lines = inner_area.height as usize;
924 let total_lines = content.len();
925
926 let start_line = app.intercept_details_scroll;
928 let end_line = std::cmp::min(start_line + visible_lines, total_lines);
929 let visible_content = if start_line < total_lines {
930 content[start_line..end_line].to_vec()
931 } else {
932 vec![]
933 };
934
935 let scroll_info = if total_lines > visible_lines {
937 let progress = ((app.intercept_details_scroll as f32
938 / (total_lines - visible_lines) as f32)
939 * 100.0) as u8;
940 format!("Request Details ({}% - vim: j/k/d/u/G/g)", progress)
941 } else {
942 "Request Details".to_string()
943 };
944
945 let details = Paragraph::new(visible_content)
946 .block(Block::default().borders(Borders::ALL).title(scroll_info))
947 .wrap(Wrap { trim: false });
948
949 f.render_widget(details, area);
950}