Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub enum TransportType {
pub enum InputMode {
Normal,
EditingTarget,
FilteringRequests,
}

#[derive(Debug, Clone, PartialEq)]
Expand Down Expand Up @@ -72,6 +73,7 @@ pub struct PendingRequest {
pub struct App {
pub exchanges: Vec<JsonRpcExchange>,
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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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();
}
}
Expand Down
36 changes: 33 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 10 additions & 1 deletion src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,20 @@ impl ProxyServer {
target_url: String,
message_sender: mpsc::UnboundedSender<JsonRpcMessage>,
) -> 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,
}
}
Expand Down
40 changes: 34 additions & 6 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -587,6 +601,7 @@ fn get_keybinds_for_mode(app: &App) -> Vec<KeybindInfo> {
// 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),
Expand Down Expand Up @@ -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
Expand All @@ -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(""),
Expand All @@ -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 });
Expand Down Expand Up @@ -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
Expand Down
131 changes: 131 additions & 0 deletions tests/app_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}