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