use ratatui::Frame;
use ratatui::layout::{Constraint, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::Span;
use ratatui::widgets::{Block, Borders, Cell, Paragraph, Row, Table};
use crate::intercept;
use super::format::truncate;
use super::state::State;
pub fn draw(f: &mut Frame, area: Rect, _state: &State) {
let pending = intercept::global_store().snapshot();
let mode_on = intercept::intercept_mode_enabled();
let title = format!(
" Intercept · {} pending · MODE: {} ",
pending.len(),
if mode_on { "ON" } else { "OFF" },
);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(if mode_on {
Color::LightGreen
} else {
Color::DarkGray
}))
.title(Span::styled(
title,
Style::default()
.fg(Color::LightCyan)
.add_modifier(Modifier::BOLD),
));
if pending.is_empty() {
let inner = block.inner(area);
f.render_widget(block, area);
let p = Paragraph::new(if mode_on {
"(no requests parked yet — proxy something through this address)"
} else {
"(intercept mode is OFF — press `i` to toggle ON)"
})
.style(crate::tui::style::DIM);
f.render_widget(p, inner);
return;
}
let header = Row::new(vec![
Cell::from("ID"),
Cell::from("METHOD"),
Cell::from("PATH"),
Cell::from("HOST"),
Cell::from("WAITING"),
])
.style(
Style::default()
.fg(Color::LightCyan)
.add_modifier(Modifier::BOLD),
);
let now = std::time::Instant::now();
let rows: Vec<Row> = pending
.iter()
.map(|p| {
let waiting_secs = now.duration_since(p.since).as_secs();
let waiting_color = match waiting_secs {
0..=4 => Color::Green,
5..=14 => Color::Yellow,
_ => Color::Red,
};
Row::new(vec![
Cell::from(p.id.to_string()).style(crate::tui::style::DIM),
Cell::from(p.method.clone()).style(Style::default().fg(Color::Cyan)),
Cell::from(truncate(&p.path, 36).to_string())
.style(Style::default().fg(Color::White)),
Cell::from(truncate(&p.host, 28).to_string())
.style(Style::default().fg(Color::Yellow)),
Cell::from(format!("{waiting_secs}s")).style(Style::default().fg(waiting_color)),
])
})
.collect();
let widths = [
Constraint::Length(6),
Constraint::Length(7),
Constraint::Percentage(40),
Constraint::Percentage(35),
Constraint::Length(8),
];
let table = Table::new(rows, widths).header(header).block(block);
f.render_widget(table, area);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::intercept::{InterceptStore, set_intercept_mode};
#[tokio::test]
async fn snapshot_appears_in_global_store() {
let store = InterceptStore::new();
let (_, _rx) = store.register("h", "GET", "/");
let snap = store.snapshot();
assert_eq!(snap.len(), 1);
let _ = snap; }
#[test]
fn intercept_mode_toggle_round_trips() {
let initial = intercept::intercept_mode_enabled();
set_intercept_mode(false);
assert!(!intercept::intercept_mode_enabled());
intercept::toggle_intercept_mode();
assert!(intercept::intercept_mode_enabled());
intercept::toggle_intercept_mode();
assert!(!intercept::intercept_mode_enabled());
set_intercept_mode(initial);
}
use ratatui::Terminal;
use ratatui::backend::TestBackend;
fn render(width: u16, height: u16) -> String {
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).expect("backend");
let state = State::new();
terminal
.draw(|f| {
let area = f.area();
draw(f, area, &state);
})
.expect("draw");
let buffer = terminal.backend().buffer().clone();
let mut out = String::new();
for y in 0..buffer.area().height {
let mut line = String::new();
for x in 0..buffer.area().width {
line.push_str(buffer[(x, y)].symbol());
}
out.push_str(line.trim_end());
out.push('\n');
}
out
}
fn serial_guard() -> std::sync::MutexGuard<'static, ()> {
use std::sync::{Mutex, OnceLock};
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[test]
fn empty_state_with_mode_on_shows_hint_to_proxy() {
let _g = serial_guard();
let prior = intercept::intercept_mode_enabled();
set_intercept_mode(true);
let buf = render(120, 6);
assert!(buf.contains("MODE: ON"), "mode banner missing: {buf}");
assert!(buf.contains("no requests parked"));
set_intercept_mode(prior);
}
#[test]
fn empty_state_with_mode_off_shows_hint_to_toggle() {
let _g = serial_guard();
let prior = intercept::intercept_mode_enabled();
set_intercept_mode(false);
let buf = render(120, 6);
assert!(buf.contains("MODE: OFF"));
assert!(buf.contains("intercept mode is OFF"));
set_intercept_mode(prior);
}
#[test]
fn empty_state_title_shows_zero_pending() {
let _g = serial_guard();
let prior = intercept::intercept_mode_enabled();
set_intercept_mode(true);
let buf = render(120, 6);
assert!(buf.contains("Intercept"));
assert!(buf.contains("pending"));
set_intercept_mode(prior);
}
#[test]
fn render_does_not_panic_on_narrow_width() {
let _g = serial_guard();
let prior = intercept::intercept_mode_enabled();
set_intercept_mode(false);
let _ = render(30, 6);
set_intercept_mode(prior);
}
#[test]
fn render_does_not_panic_on_tall_height() {
let _g = serial_guard();
let prior = intercept::intercept_mode_enabled();
set_intercept_mode(true);
let _ = render(120, 50);
set_intercept_mode(prior);
}
#[test]
fn waiting_color_bands_are_well_defined() {
for secs in 0..=4 {
let color = match secs {
0..=4 => Color::Green,
5..=14 => Color::Yellow,
_ => Color::Red,
};
assert_eq!(color, Color::Green, "secs {secs} should be green");
}
for secs in 5..=14 {
let color = match secs {
0..=4 => Color::Green,
5..=14 => Color::Yellow,
_ => Color::Red,
};
assert_eq!(color, Color::Yellow, "secs {secs} should be yellow");
}
for secs in [15_u64, 30, 60, 300] {
let color = match secs {
0..=4 => Color::Green,
5..=14 => Color::Yellow,
_ => Color::Red,
};
assert_eq!(color, Color::Red, "secs {secs} should be red");
}
}
#[test]
fn snapshot_distinguishes_concurrent_intercepts() {
let store = InterceptStore::new();
let (_id1, _rx1) = store.register("h1", "GET", "/a");
let (_id2, _rx2) = store.register("h2", "POST", "/b");
let snap = store.snapshot();
assert_eq!(snap.len(), 2);
let methods: Vec<&str> = snap.iter().map(|p| p.method.as_str()).collect();
assert!(methods.contains(&"GET"));
assert!(methods.contains(&"POST"));
}
#[test]
fn empty_snapshot_on_fresh_store() {
let store = InterceptStore::new();
assert!(store.snapshot().is_empty());
}
#[test]
fn mode_toggle_initial_round_trip_preserves_invariant() {
let _g = serial_guard();
let initial = intercept::intercept_mode_enabled();
for value in [false, true, false, true] {
set_intercept_mode(value);
assert_eq!(intercept::intercept_mode_enabled(), value);
}
set_intercept_mode(initial);
}
}