jsonrpc_debugger/
ui.rs

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
14// Helper function to format JSON with syntax highlighting and 2-space indentation
15fn format_json_with_highlighting(json_value: &serde_json::Value) -> Vec<Line<'static>> {
16    // Use the standard pretty formatter
17    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        // Limit total lines to prevent UI issues
26        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        // Don't trim the line - work with it as-is to preserve indentation
35        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                    // Flush any accumulated token (including spaces)
43                    if !current_token.is_empty() {
44                        spans.push(Span::raw(current_token.clone()));
45                        current_token.clear();
46                    }
47
48                    // Collect the entire string
49                    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                    // Check if this is a key (followed by colon)
58                    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                        // This is a key
71                        spans.push(Span::styled(
72                            string_content,
73                            Style::default()
74                                .fg(Color::Cyan)
75                                .add_modifier(Modifier::BOLD),
76                        ));
77                    } else {
78                        // This is a string value
79                        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                    // Accumulate all other characters including spaces
113                    current_token.push(ch);
114                }
115            }
116        }
117
118        // Handle any remaining token (including trailing spaces)
119        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                // This includes spaces and other whitespace - preserve as-is
135                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    // Calculate footer height dynamically
147    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); // +2 for borders, minimum 3
151
152    let chunks = Layout::default()
153        .direction(Direction::Vertical)
154        .constraints([
155            Constraint::Length(3),                    // Header
156            Constraint::Min(10),                      // Main content
157            Constraint::Length(footer_height as u16), // Dynamic footer height
158            Constraint::Length(1),                    // Input dialog
159        ])
160        .split(f.size());
161
162    draw_header(f, chunks[0], app);
163
164    // Choose layout based on app mode
165    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    // Draw input dialogs
177    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), // Message list
235            Constraint::Percentage(50), // Message details
236        ])
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    // Create table headers
264    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    // Create table rows
275    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                // TODO: Filter by id, params, result, error, etc.
284                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            // Determine status
309            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            // Calculate duration if we have both request and response
322            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), // Status
364            Constraint::Length(9),  // Transport
365            Constraint::Min(15),    // Method (flexible)
366            Constraint::Length(12), // ID
367            Constraint::Length(10), // Duration
368        ],
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        // Basic exchange info
426        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        // Request details
446        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            // Show HTTP headers if available
456            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            // Build and show the complete JSON-RPC request object
465            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        // Response details
500        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            // Show HTTP headers if available
510            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            // Build and show the complete JSON-RPC response object
519            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    // Calculate visible area for scrolling
564    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    // Apply scrolling offset
572    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    // Create title with scroll indicator
581    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// Helper struct to represent a keybind with its display information
616#[derive(Clone)]
617struct KeybindInfo {
618    key: String,
619    description: String,
620    priority: u8, // Lower number = higher priority
621}
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    // Calculate the display width of this keybind (key + description + separators)
633    fn display_width(&self) -> usize {
634        self.key.len() + 1 + self.description.len() + 3 // " | " separator
635    }
636
637    // Convert to spans for rendering
638    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        // Essential keybinds (priority 1)
654        KeybindInfo::new("q", "quit", 1),
655        KeybindInfo::new("↑↓", "navigate", 1),
656        KeybindInfo::new("s", "start/stop proxy", 1),
657        // Navigation keybinds (priority 2)
658        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        // Advanced keybinds (priority 3)
663        KeybindInfo::new("j/k/d/u/G/g", "scroll details", 3),
664    ];
665
666    // Add context-specific keybinds (priority 4)
667    match app.app_mode {
668        AppMode::Paused | AppMode::Intercepting => {
669            // Only show intercept controls if there are pending requests
670            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    // Account for border padding (2 chars for left/right borders)
698    let usable_width = available_width.saturating_sub(4);
699
700    // Sort keybinds by priority
701    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        // Check if this keybind fits on the current line
709        let width_needed = if is_last {
710            keybind_width - 3 // Remove " | " from last item
711        } else {
712            keybind_width
713        };
714
715        if current_line_width + width_needed <= usable_width || current_line_spans.is_empty() {
716            // Add to current line
717            let mut spans = keybind.to_spans();
718            if is_last {
719                // Remove the trailing " | " from the last keybind
720                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            // Start a new line
730            // Remove trailing " | " from the last span of the current line
731            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 this is the last keybind, remove trailing separator
742            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    // Add the last line if it has content
753    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    // Convert spans to Lines
767    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    // Create a centered popup
779    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    // Clear the entire screen first
787    f.render_widget(Clear, area);
788
789    // Render a black background
790    let background = Block::default().style(Style::default().bg(Color::Black));
791    f.render_widget(background, area);
792
793    // Clear the popup area specifically
794    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), // Pending requests list
827            Constraint::Percentage(50), // Request details/editor
828        ])
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                // Filter pending requests by method name (same as main list)
864                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            // Show different icon if request has been modified
895            let (icon, icon_color) =
896                if pending.modified_request.is_some() || pending.modified_headers.is_some() {
897                    ("✏ ", Color::Blue) // Modified
898                } else {
899                    ("⏸ ", Color::Red) // Paused/Intercepted
900                };
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        // Show headers section
970        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        // Show JSON-RPC body section
999        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        // Show the modified request if available, otherwise show original
1007        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                // Fallback to original if modified JSON is invalid
1012                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            // Show original request
1035            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    // Calculate visible area for scrolling
1080    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    // Apply scrolling offset
1088    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    // Create title with scroll indicator
1097    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}