diff --git a/src/app.rs b/src/app.rs index f1c2228..94a0f65 100644 --- a/src/app.rs +++ b/src/app.rs @@ -43,6 +43,7 @@ pub enum TransportType { pub enum InputMode { Normal, EditingTarget, + FilteringRequests, } #[derive(Debug, Clone, PartialEq)] @@ -72,6 +73,7 @@ pub struct PendingRequest { pub struct App { pub exchanges: Vec, pub selected_exchange: usize, + pub filter_text: String, pub table_state: TableState, pub details_scroll: usize, pub intercept_details_scroll: usize, // New field for intercept details scrolling @@ -108,6 +110,7 @@ impl App { Self { exchanges: Vec::new(), selected_exchange: 0, + filter_text: String::new(), table_state, details_scroll: 0, intercept_details_scroll: 0, @@ -134,6 +137,7 @@ impl App { Self { exchanges: Vec::new(), selected_exchange: 0, + filter_text: String::new(), table_state, details_scroll: 0, intercept_details_scroll: 0, @@ -320,6 +324,23 @@ impl App { } } + // Filtering requests methods + pub fn start_filtering_requests(&mut self) { + self.input_mode = InputMode::FilteringRequests; + self.input_buffer.clear(); + } + + pub fn cancel_filtering(&mut self) { + self.input_mode = InputMode::Normal; + self.input_buffer.clear(); + } + + pub fn apply_filter(&mut self) { + self.filter_text = self.input_buffer.clone(); + self.input_mode = InputMode::Normal; + self.input_buffer.clear(); + } + // Get content lines for proper scrolling calculations // Target editing methods pub fn start_editing_target(&mut self) { @@ -341,13 +362,17 @@ impl App { } pub fn handle_input_char(&mut self, c: char) { - if self.input_mode == InputMode::EditingTarget { + if self.input_mode == InputMode::EditingTarget + || self.input_mode == InputMode::FilteringRequests + { self.input_buffer.push(c); } } pub fn handle_backspace(&mut self) { - if self.input_mode == InputMode::EditingTarget { + if self.input_mode == InputMode::EditingTarget + || self.input_mode == InputMode::FilteringRequests + { self.input_buffer.pop(); } } diff --git a/src/main.rs b/src/main.rs index 73bf978..f13a89e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -179,6 +179,24 @@ async fn run_app( if let Event::Key(key) = event::read()? { // Handle input modes first match app.input_mode { + app::InputMode::FilteringRequests => { + match key.code { + KeyCode::Enter => { + app.apply_filter(); + } + KeyCode::Esc => { + app.cancel_filtering(); + } + KeyCode::Backspace => { + app.handle_backspace(); + } + KeyCode::Char(c) => { + app.handle_input_char(c); + } + _ => {} + } + continue; + } app::InputMode::EditingTarget => { match key.code { KeyCode::Enter => { @@ -332,18 +350,30 @@ async fn run_app( } }, KeyCode::Char('t') => { - // Edit target URL app.start_editing_target(); } + KeyCode::Char('/') => { + app.start_filtering_requests(); + } KeyCode::Char('n') if key.modifiers.contains(event::KeyModifiers::CONTROL) => { - app.select_next() + match app.app_mode { + app::AppMode::Normal => app.select_next(), + app::AppMode::Paused | app::AppMode::Intercepting => { + app.select_next_pending() + } + } } KeyCode::Char('p') if key.modifiers.contains(event::KeyModifiers::CONTROL) => { - app.select_previous() + match app.app_mode { + app::AppMode::Normal => app.select_previous(), + app::AppMode::Paused | app::AppMode::Intercepting => { + app.select_previous_pending() + } + } } KeyCode::Char('s') => { if app.is_running { diff --git a/src/proxy.rs b/src/proxy.rs index 25e841f..9f13e34 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -32,11 +32,20 @@ impl ProxyServer { target_url: String, message_sender: mpsc::UnboundedSender, ) -> Self { + // Configure client for higher concurrency + let client = Client::builder() + .pool_max_idle_per_host(50) // More idle connections + .pool_idle_timeout(std::time::Duration::from_secs(30)) + .http2_max_frame_size(Some(16384)) // Larger frame size + .http2_keep_alive_interval(Some(std::time::Duration::from_secs(10))) + .build() + .unwrap_or_else(|_| Client::new()); // Fallback to default if config fails + Self { listen_port, target_url, message_sender, - client: Client::new(), + client, proxy_state: None, } } diff --git a/src/ui.rs b/src/ui.rs index d44f35f..274fb81 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -174,7 +174,9 @@ pub fn draw(f: &mut Frame, app: &App) { // Draw input dialogs if app.input_mode == InputMode::EditingTarget { - draw_input_dialog(f, app); + draw_input_dialog(f, app, "Edit Target URL", "Target URL"); + } else if app.input_mode == InputMode::FilteringRequests { + draw_input_dialog(f, app, "Filter Requests", "Filter"); } } @@ -209,8 +211,8 @@ fn draw_header(f: &mut Frame, area: Rect, app: &App) { .add_modifier(Modifier::BOLD), ), Span::raw(format!( - " | Port: {} | Target: {}", - app.proxy_config.listen_port, app.proxy_config.target_url + " | Port: {} | Target: {} | Filter: {}", + app.proxy_config.listen_port, app.proxy_config.target_url, app.filter_text )), Span::styled( mode_text, @@ -273,6 +275,18 @@ fn draw_message_list(f: &mut Frame, area: Rect, app: &App) { .exchanges .iter() .enumerate() + .filter(|(_, exchange)| { + if app.filter_text.is_empty() { + true + } else { + // TODO: Filter by id, params, result, error, etc. + exchange + .method + .as_deref() + .unwrap_or("") + .contains(&app.filter_text) + } + }) .map(|(i, exchange)| { let transport_symbol = match exchange.transport { TransportType::Http => "HTTP", @@ -587,6 +601,7 @@ fn get_keybinds_for_mode(app: &App) -> Vec { // Navigation keybinds (priority 2) KeybindInfo::new("^n/^p", "navigate", 2), KeybindInfo::new("t", "edit target", 2), + KeybindInfo::new("/", "filter", 2), KeybindInfo::new("p", "pause", 2), // Advanced keybinds (priority 3) KeybindInfo::new("j/k/d/u/G/g", "scroll details", 3), @@ -701,7 +716,7 @@ fn draw_footer(f: &mut Frame, area: Rect, app: &App) { f.render_widget(footer, area); } -fn draw_input_dialog(f: &mut Frame, app: &App) { +fn draw_input_dialog(f: &mut Frame, app: &App, title: &str, label: &str) { let area = f.size(); // Create a centered popup @@ -725,7 +740,7 @@ fn draw_input_dialog(f: &mut Frame, app: &App) { let input_text = vec![ Line::from(""), Line::from(vec![ - Span::raw("Target URL: "), + Span::raw(format!("{}: ", label)), Span::styled(&app.input_buffer, Style::default().fg(Color::Green)), ]), Line::from(""), @@ -740,7 +755,7 @@ fn draw_input_dialog(f: &mut Frame, app: &App) { .block( Block::default() .borders(Borders::ALL) - .title("Edit Target URL") + .title(title) .style(Style::default().fg(Color::White).bg(Color::DarkGray)), ) .wrap(Wrap { trim: true }); @@ -785,6 +800,19 @@ fn draw_pending_requests(f: &mut Frame, area: Rect, app: &App) { .pending_requests .iter() .enumerate() + .filter(|(_, pending)| { + if app.filter_text.is_empty() { + true + } else { + // Filter pending requests by method name (same as main list) + pending + .original_request + .method + .as_deref() + .unwrap_or("") + .contains(&app.filter_text) + } + }) .map(|(i, pending)| { let method = pending .original_request diff --git a/tests/app_tests.rs b/tests/app_tests.rs index cdd07f1..2814c28 100644 --- a/tests/app_tests.rs +++ b/tests/app_tests.rs @@ -253,3 +253,134 @@ fn test_proxy_config() { assert_eq!(config.target_url, "https://example.com"); assert!(matches!(config.transport, TransportType::Http)); } + +#[test] +fn test_filtering_functionality() { + let mut app = App::new(); + + // Add test exchanges with different methods + let methods = [ + "eth_getBalance", + "eth_sendTransaction", + "net_version", + "eth_blockNumber", + ]; + + for (i, method) in methods.iter().enumerate() { + let test_message = JsonRpcMessage { + id: Some(serde_json::Value::Number(serde_json::Number::from( + i as i64, + ))), + method: Some(method.to_string()), + params: Some(serde_json::json!({"test": format!("value_{}", i)})), + result: None, + error: None, + timestamp: std::time::SystemTime::now(), + direction: MessageDirection::Request, + transport: TransportType::Http, + headers: None, + }; + app.add_message(test_message); + } + + // Test initial state - no filter + assert_eq!(app.filter_text, ""); + assert_eq!(app.exchanges.len(), 4); + + // Test filter methods + app.start_filtering_requests(); + assert_eq!(app.input_mode, InputMode::FilteringRequests); + assert_eq!(app.input_buffer, ""); // Should start empty + + // Simulate typing "eth" + app.handle_input_char('e'); + app.handle_input_char('t'); + app.handle_input_char('h'); + assert_eq!(app.input_buffer, "eth"); + + // Apply the filter + app.apply_filter(); + assert_eq!(app.filter_text, "eth"); + assert_eq!(app.input_mode, InputMode::Normal); + assert_eq!(app.input_buffer, ""); + + // Test that filtering logic would work (this tests the filter logic conceptually) + let filtered_count = app + .exchanges + .iter() + .filter(|exchange| { + if app.filter_text.is_empty() { + true + } else { + exchange + .method + .as_deref() + .unwrap_or("") + .contains(&app.filter_text) + } + }) + .count(); + + // Should match 3 exchanges: eth_getBalance, eth_sendTransaction, eth_blockNumber + assert_eq!(filtered_count, 3); + + // Test cancel filtering + app.start_filtering_requests(); + app.handle_input_char('n'); + app.handle_input_char('e'); + app.handle_input_char('t'); + app.cancel_filtering(); + assert_eq!(app.filter_text, "eth"); // Should keep previous filter + assert_eq!(app.input_mode, InputMode::Normal); + assert_eq!(app.input_buffer, ""); + + // Test clearing filter + app.start_filtering_requests(); + app.apply_filter(); // Apply empty filter + assert_eq!(app.filter_text, ""); + + // All exchanges should match when filter is empty + let all_count = app + .exchanges + .iter() + .filter(|exchange| { + if app.filter_text.is_empty() { + true + } else { + exchange + .method + .as_deref() + .unwrap_or("") + .contains(&app.filter_text) + } + }) + .count(); + assert_eq!(all_count, 4); + + // Test case-insensitive filtering (if implemented) + app.start_filtering_requests(); + app.handle_input_char('E'); + app.handle_input_char('T'); + app.handle_input_char('H'); + app.apply_filter(); + assert_eq!(app.filter_text, "ETH"); + + // This would test case-insensitive matching if implemented + let case_insensitive_count = app + .exchanges + .iter() + .filter(|exchange| { + if app.filter_text.is_empty() { + true + } else { + exchange + .method + .as_deref() + .unwrap_or("") + .to_lowercase() + .contains(&app.filter_text.to_lowercase()) + } + }) + .count(); + assert_eq!(case_insensitive_count, 3); +}