From 4fbb31e1f2f3c221f9bffc8d4416e76fe252b2da Mon Sep 17 00:00:00 2001 From: Shane Date: Mon, 6 Oct 2025 21:02:15 -0400 Subject: [PATCH 1/4] Restore detail layout with per-panel tabs --- src/app.rs | 193 +++++++++++++++++++++++++------------- src/main.rs | 10 ++ src/ui.rs | 261 +++++++++++++++++++++++++++++++++------------------- 3 files changed, 303 insertions(+), 161 deletions(-) diff --git a/src/app.rs b/src/app.rs index 94a0f65..38c3f66 100644 --- a/src/app.rs +++ b/src/app.rs @@ -76,6 +76,9 @@ pub struct App { pub filter_text: String, pub table_state: TableState, pub details_scroll: usize, + pub details_tab: usize, + pub request_details_tab: usize, + pub response_details_tab: usize, pub intercept_details_scroll: usize, // New field for intercept details scrolling pub proxy_config: ProxyConfig, pub is_running: bool, @@ -113,6 +116,9 @@ impl App { filter_text: String::new(), table_state, details_scroll: 0, + details_tab: 0, + request_details_tab: 0, + response_details_tab: 0, intercept_details_scroll: 0, proxy_config: ProxyConfig { listen_port: 8080, @@ -140,6 +146,9 @@ impl App { filter_text: String::new(), table_state, details_scroll: 0, + details_tab: 0, + request_details_tab: 0, + response_details_tab: 0, intercept_details_scroll: 0, proxy_config: ProxyConfig { listen_port: 8080, @@ -231,6 +240,9 @@ impl App { self.selected_exchange = (self.selected_exchange + 1) % self.exchanges.len(); self.table_state.select(Some(self.selected_exchange)); self.reset_details_scroll(); + self.details_tab = 0; + self.request_details_tab = 0; + self.response_details_tab = 0; } } @@ -243,6 +255,9 @@ impl App { }; self.table_state.select(Some(self.selected_exchange)); self.reset_details_scroll(); + self.details_tab = 0; + self.request_details_tab = 0; + self.response_details_tab = 0; } } @@ -266,6 +281,34 @@ impl App { self.details_scroll = 0; } + pub fn next_details_tab(&mut self) { + self.details_tab = (self.details_tab + 1) % 4; + match self.details_tab { + 0 => self.request_details_tab = 0, + 1 => self.request_details_tab = 1, + 2 => self.response_details_tab = 0, + 3 => self.response_details_tab = 1, + _ => {} + } + self.reset_details_scroll(); + } + + pub fn previous_details_tab(&mut self) { + if self.details_tab == 0 { + self.details_tab = 3; + } else { + self.details_tab -= 1; + } + match self.details_tab { + 0 => self.request_details_tab = 0, + 1 => self.request_details_tab = 1, + 2 => self.response_details_tab = 0, + 3 => self.response_details_tab = 1, + _ => {} + } + self.reset_details_scroll(); + } + // Intercept details scrolling methods pub fn scroll_intercept_details_up(&mut self) { if self.intercept_details_scroll > 0 { @@ -379,87 +422,105 @@ impl App { pub fn get_details_content_lines(&self) -> usize { if let Some(exchange) = self.get_selected_exchange() { - let mut line_count = 0; + let mut line_count = 1; // Transport line - // Basic info lines - line_count += 3; // Transport, Method, ID + if exchange.method.is_some() { + line_count += 1; + } + if exchange.id.is_some() { + line_count += 1; + } // Request section - if let Some(request) = &exchange.request { - line_count += 2; // Empty line + "REQUEST:" header - - if let Some(headers) = &request.headers { - line_count += 2; // Empty line + "HTTP Headers:" - line_count += headers.len(); - } - - line_count += 2; // Empty line + "JSON-RPC Request:" + line_count += 1; // Blank line before section + line_count += 1; // Section header + line_count += 1; // Tabs line - // Estimate JSON lines (rough calculation) - let mut request_json = serde_json::Map::new(); - request_json.insert( - "jsonrpc".to_string(), - serde_json::Value::String("2.0".to_string()), - ); - if let Some(id) = &request.id { - request_json.insert("id".to_string(), id.clone()); - } - if let Some(method) = &request.method { - request_json.insert( - "method".to_string(), - serde_json::Value::String(method.clone()), - ); - } - if let Some(params) = &request.params { - request_json.insert("params".to_string(), params.clone()); - } - - if let Ok(json_str) = - serde_json::to_string_pretty(&serde_json::Value::Object(request_json)) - { - line_count += json_str.lines().count(); + if let Some(request) = &exchange.request { + match self.request_details_tab { + 0 => match &request.headers { + Some(headers) if !headers.is_empty() => { + line_count += headers.len(); + } + Some(_) | None => { + line_count += 1; + } + }, + _ => { + let mut request_json = serde_json::Map::new(); + request_json.insert( + "jsonrpc".to_string(), + serde_json::Value::String("2.0".to_string()), + ); + if let Some(id) = &request.id { + request_json.insert("id".to_string(), id.clone()); + } + if let Some(method) = &request.method { + request_json.insert( + "method".to_string(), + serde_json::Value::String(method.clone()), + ); + } + if let Some(params) = &request.params { + request_json.insert("params".to_string(), params.clone()); + } + + if let Ok(json_str) = + serde_json::to_string_pretty(&serde_json::Value::Object(request_json)) + { + line_count += json_str.lines().count(); + } + } } + } else { + line_count += 1; } // Response section - if let Some(response) = &exchange.response { - line_count += 2; // Empty line + "RESPONSE:" header - - if let Some(headers) = &response.headers { - line_count += 2; // Empty line + "HTTP Headers:" - line_count += headers.len(); - } + line_count += 1; // Blank line before section + line_count += 1; // Section header + line_count += 1; // Tabs line - line_count += 2; // Empty line + "JSON-RPC Response:" - - // Estimate JSON lines - let mut response_json = serde_json::Map::new(); - response_json.insert( - "jsonrpc".to_string(), - serde_json::Value::String("2.0".to_string()), - ); - if let Some(id) = &response.id { - response_json.insert("id".to_string(), id.clone()); - } - if let Some(result) = &response.result { - response_json.insert("result".to_string(), result.clone()); - } - if let Some(error) = &response.error { - response_json.insert("error".to_string(), error.clone()); - } - - if let Ok(json_str) = - serde_json::to_string_pretty(&serde_json::Value::Object(response_json)) - { - line_count += json_str.lines().count(); + if let Some(response) = &exchange.response { + match self.response_details_tab { + 0 => match &response.headers { + Some(headers) if !headers.is_empty() => { + line_count += headers.len(); + } + Some(_) | None => { + line_count += 1; + } + }, + _ => { + let mut response_json = serde_json::Map::new(); + response_json.insert( + "jsonrpc".to_string(), + serde_json::Value::String("2.0".to_string()), + ); + if let Some(id) = &response.id { + response_json.insert("id".to_string(), id.clone()); + } + if let Some(result) = &response.result { + response_json.insert("result".to_string(), result.clone()); + } + if let Some(error) = &response.error { + response_json.insert("error".to_string(), error.clone()); + } + + if let Ok(json_str) = + serde_json::to_string_pretty(&serde_json::Value::Object(response_json)) + { + line_count += json_str.lines().count(); + } + } } } else { - line_count += 2; // Empty line + "RESPONSE: Pending..." + line_count += 1; } line_count } else { - 1 // "No exchange selected" + 1 } } diff --git a/src/main.rs b/src/main.rs index f13a89e..246f866 100644 --- a/src/main.rs +++ b/src/main.rs @@ -276,6 +276,16 @@ async fn run_app( app.select_next_pending() } }, + KeyCode::Left => { + if app.app_mode == app::AppMode::Normal { + app.previous_details_tab(); + } + } + KeyCode::Right => { + if app.app_mode == app::AppMode::Normal { + app.next_details_tab(); + } + } KeyCode::Char('k') => match app.app_mode { app::AppMode::Normal => app.scroll_details_up(), app::AppMode::Paused | app::AppMode::Intercepting => { diff --git a/src/ui.rs b/src/ui.rs index a59f91d..14d37ef 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -142,6 +142,56 @@ fn format_json_with_highlighting(json_value: &serde_json::Value) -> Vec Line<'static> { + let mut spans = Vec::new(); + + for (index, label) in labels.iter().enumerate() { + let is_selected = index == selected; + let style = if is_selected { + let mut style = Style::default(); + if is_enabled { + style = style + .fg(if is_active { + Color::Yellow + } else { + Color::White + }) + .add_modifier(Modifier::BOLD); + } else { + style = style.fg(Color::DarkGray); + } + style + } else if is_enabled { + Style::default().fg(if is_active { + Color::Gray + } else { + Color::DarkGray + }) + } else { + Style::default().fg(Color::DarkGray) + }; + + let text = if is_selected { + format!("[{}]", label) + } else { + format!(" {} ", label) + }; + + spans.push(Span::styled(text, style)); + + if index < labels.len() - 1 { + spans.push(Span::raw(" ")); + } + } + + Line::from(spans) +} + pub fn draw(f: &mut Frame, app: &App) { // Calculate footer height dynamically let keybinds = get_keybinds_for_mode(app); @@ -442,116 +492,137 @@ fn draw_message_details(f: &mut Frame, area: Rect, app: &App) { ])); } - // Request details - if let Some(request) = &exchange.request { - lines.push(Line::from("")); - lines.push(Line::from(Span::styled( - "REQUEST:", - Style::default() - .add_modifier(Modifier::BOLD) - .fg(Color::Green), - ))); + // Request section with tabs + lines.push(Line::from("")); + lines.push(Line::from(Span::styled( + "REQUEST:", + Style::default() + .add_modifier(Modifier::BOLD) + .fg(Color::Green), + ))); + let request_active = matches!(app.details_tab, 0 | 1); + lines.push(build_tab_line( + &["Headers", "Body"], + app.request_details_tab, + request_active, + exchange.request.is_some(), + )); - // Show HTTP headers if available - if let Some(headers) = &request.headers { - lines.push(Line::from("")); - lines.push(Line::from("HTTP Headers:")); - for (key, value) in headers { - lines.push(Line::from(format!(" {}: {}", key, value))); + if let Some(request) = &exchange.request { + match app.request_details_tab { + 0 => { + lines.push(Line::from("")); + match &request.headers { + Some(headers) if !headers.is_empty() => { + for (key, value) in headers { + lines.push(Line::from(format!(" {}: {}", key, value))); + } + } + Some(_) => { + lines.push(Line::from(" No headers")); + } + None => { + lines.push(Line::from(" No headers captured")); + } + } } - } - - // Build and show the complete JSON-RPC request object - lines.push(Line::from("")); - lines.push(Line::from(Span::styled( - "JSON-RPC Request:", - Style::default() - .fg(Color::Green) - .add_modifier(Modifier::BOLD), - ))); - lines.push(Line::from("")); - let mut request_json = serde_json::Map::new(); - request_json.insert( - "jsonrpc".to_string(), - serde_json::Value::String("2.0".to_string()), - ); + _ => { + lines.push(Line::from("")); + let mut request_json = serde_json::Map::new(); + request_json.insert( + "jsonrpc".to_string(), + serde_json::Value::String("2.0".to_string()), + ); - if let Some(id) = &request.id { - request_json.insert("id".to_string(), id.clone()); - } - if let Some(method) = &request.method { - request_json.insert( - "method".to_string(), - serde_json::Value::String(method.clone()), - ); - } - if let Some(params) = &request.params { - request_json.insert("params".to_string(), params.clone()); - } + if let Some(id) = &request.id { + request_json.insert("id".to_string(), id.clone()); + } + if let Some(method) = &request.method { + request_json.insert( + "method".to_string(), + serde_json::Value::String(method.clone()), + ); + } + if let Some(params) = &request.params { + request_json.insert("params".to_string(), params.clone()); + } - let request_json_value = serde_json::Value::Object(request_json); - let request_json_lines = format_json_with_highlighting(&request_json_value); - for line in request_json_lines { - lines.push(line); + let request_json_value = serde_json::Value::Object(request_json); + let request_json_lines = format_json_with_highlighting(&request_json_value); + for line in request_json_lines { + lines.push(line); + } + } } + } else { + lines.push(Line::from("")); + lines.push(Line::from("Request not captured yet")); } - // Response details - if let Some(response) = &exchange.response { - lines.push(Line::from("")); - lines.push(Line::from(Span::styled( - "RESPONSE:", - Style::default() - .add_modifier(Modifier::BOLD) - .fg(Color::Blue), - ))); + // Response section with tabs + lines.push(Line::from("")); + lines.push(Line::from(Span::styled( + "RESPONSE:", + Style::default() + .add_modifier(Modifier::BOLD) + .fg(Color::Blue), + ))); + let response_active = matches!(app.details_tab, 2 | 3); + lines.push(build_tab_line( + &["Headers", "Body"], + app.response_details_tab, + response_active, + exchange.response.is_some(), + )); - // Show HTTP headers if available - if let Some(headers) = &response.headers { - lines.push(Line::from("")); - lines.push(Line::from("HTTP Headers:")); - for (key, value) in headers { - lines.push(Line::from(format!(" {}: {}", key, value))); + if let Some(response) = &exchange.response { + match app.response_details_tab { + 0 => { + lines.push(Line::from("")); + match &response.headers { + Some(headers) if !headers.is_empty() => { + for (key, value) in headers { + lines.push(Line::from(format!(" {}: {}", key, value))); + } + } + Some(_) => { + lines.push(Line::from(" No headers")); + } + None => { + lines.push(Line::from(" No headers captured")); + } + } } - } - - // Build and show the complete JSON-RPC response object - lines.push(Line::from("")); - lines.push(Line::from(Span::styled( - "JSON-RPC Response:", - Style::default() - .fg(Color::Blue) - .add_modifier(Modifier::BOLD), - ))); - lines.push(Line::from("")); - let mut response_json = serde_json::Map::new(); - response_json.insert( - "jsonrpc".to_string(), - serde_json::Value::String("2.0".to_string()), - ); + _ => { + lines.push(Line::from("")); + let mut response_json = serde_json::Map::new(); + response_json.insert( + "jsonrpc".to_string(), + serde_json::Value::String("2.0".to_string()), + ); - if let Some(id) = &response.id { - response_json.insert("id".to_string(), id.clone()); - } - if let Some(result) = &response.result { - response_json.insert("result".to_string(), result.clone()); - } - if let Some(error) = &response.error { - response_json.insert("error".to_string(), error.clone()); - } + if let Some(id) = &response.id { + response_json.insert("id".to_string(), id.clone()); + } + if let Some(result) = &response.result { + response_json.insert("result".to_string(), result.clone()); + } + if let Some(error) = &response.error { + response_json.insert("error".to_string(), error.clone()); + } - let response_json_value = serde_json::Value::Object(response_json); - let response_json_lines = format_json_with_highlighting(&response_json_value); - for line in response_json_lines { - lines.push(line); + let response_json_value = serde_json::Value::Object(response_json); + let response_json_lines = format_json_with_highlighting(&response_json_value); + for line in response_json_lines { + lines.push(line); + } + } } } else { lines.push(Line::from("")); lines.push(Line::from(Span::styled( - "RESPONSE: Pending...", - Style::default() - .add_modifier(Modifier::BOLD) - .fg(Color::Yellow), + "Response pending...", + Style::default().fg(Color::Yellow), ))); } From 8eb45198ae31701b7ebdd605205ff4fb1601e530 Mon Sep 17 00:00:00 2001 From: Shane Jonas Date: Fri, 10 Oct 2025 09:00:37 -0400 Subject: [PATCH 2/4] fix: split pane on right --- src/app.rs | 230 +++++++++++++++++++++++++++++---- src/main.rs | 217 ++++++++++++++++++++++++------- src/ui.rs | 361 +++++++++++++++++++++++++++++++++++----------------- 3 files changed, 615 insertions(+), 193 deletions(-) diff --git a/src/app.rs b/src/app.rs index 38c3f66..8b3c2df 100644 --- a/src/app.rs +++ b/src/app.rs @@ -46,6 +46,13 @@ pub enum InputMode { FilteringRequests, } +#[derive(Debug, Clone, PartialEq)] +pub enum Focus { + MessageList, + RequestSection, + ResponseSection, +} + #[derive(Debug, Clone, PartialEq)] pub enum AppMode { Normal, // Regular proxy mode @@ -76,6 +83,8 @@ pub struct App { pub filter_text: String, pub table_state: TableState, pub details_scroll: usize, + pub request_details_scroll: usize, + pub response_details_scroll: usize, pub details_tab: usize, pub request_details_tab: usize, pub response_details_tab: usize, @@ -89,6 +98,9 @@ pub struct App { pub pending_requests: Vec, // New field pub selected_pending: usize, // New field pub request_editor_buffer: String, // New field + pub focus: Focus, // New field for tracking which element is active + pub request_tab: usize, // 0 = Headers, 1 = Body + pub response_tab: usize, // 0 = Headers, 1 = Body } #[derive(Debug)] @@ -116,6 +128,8 @@ impl App { filter_text: String::new(), table_state, details_scroll: 0, + request_details_scroll: 0, + response_details_scroll: 0, details_tab: 0, request_details_tab: 0, response_details_tab: 0, @@ -133,6 +147,9 @@ impl App { pending_requests: Vec::new(), selected_pending: 0, request_editor_buffer: String::new(), + focus: Focus::MessageList, + request_tab: 1, // Body selected by default + response_tab: 1, // Body selected by default } } @@ -146,6 +163,8 @@ impl App { filter_text: String::new(), table_state, details_scroll: 0, + request_details_scroll: 0, + response_details_scroll: 0, details_tab: 0, request_details_tab: 0, response_details_tab: 0, @@ -163,6 +182,9 @@ impl App { pending_requests: Vec::new(), selected_pending: 0, request_editor_buffer: String::new(), + focus: Focus::MessageList, + request_tab: 1, // Body selected by default + response_tab: 1, // Body selected by default } } @@ -240,6 +262,8 @@ impl App { self.selected_exchange = (self.selected_exchange + 1) % self.exchanges.len(); self.table_state.select(Some(self.selected_exchange)); self.reset_details_scroll(); + self.request_details_scroll = 0; + self.response_details_scroll = 0; self.details_tab = 0; self.request_details_tab = 0; self.response_details_tab = 0; @@ -255,6 +279,8 @@ impl App { }; self.table_state.select(Some(self.selected_exchange)); self.reset_details_scroll(); + self.request_details_scroll = 0; + self.response_details_scroll = 0; self.details_tab = 0; self.request_details_tab = 0; self.response_details_tab = 0; @@ -281,33 +307,7 @@ impl App { self.details_scroll = 0; } - pub fn next_details_tab(&mut self) { - self.details_tab = (self.details_tab + 1) % 4; - match self.details_tab { - 0 => self.request_details_tab = 0, - 1 => self.request_details_tab = 1, - 2 => self.response_details_tab = 0, - 3 => self.response_details_tab = 1, - _ => {} - } - self.reset_details_scroll(); - } - pub fn previous_details_tab(&mut self) { - if self.details_tab == 0 { - self.details_tab = 3; - } else { - self.details_tab -= 1; - } - match self.details_tab { - 0 => self.request_details_tab = 0, - 1 => self.request_details_tab = 1, - 2 => self.response_details_tab = 0, - 3 => self.response_details_tab = 1, - _ => {} - } - self.reset_details_scroll(); - } // Intercept details scrolling methods pub fn scroll_intercept_details_up(&mut self) { @@ -326,8 +326,8 @@ impl App { self.intercept_details_scroll = 0; } - pub fn page_down_intercept_details(&mut self, visible_lines: usize) { - let page_size = visible_lines / 2; // Half page + pub fn page_down_intercept_details(&mut self) { + let page_size = 10; // Half page self.intercept_details_scroll += page_size; } @@ -367,6 +367,64 @@ impl App { } } + pub fn switch_focus(&mut self) { + self.focus = match self.focus { + Focus::MessageList => Focus::RequestSection, + Focus::RequestSection => Focus::ResponseSection, + Focus::ResponseSection => Focus::MessageList, + }; + self.reset_details_scroll(); + self.request_details_scroll = 0; + self.response_details_scroll = 0; + } + + pub fn switch_focus_reverse(&mut self) { + self.focus = match self.focus { + Focus::MessageList => Focus::ResponseSection, + Focus::RequestSection => Focus::MessageList, + Focus::ResponseSection => Focus::RequestSection, + }; + self.reset_details_scroll(); + self.request_details_scroll = 0; + self.response_details_scroll = 0; + } + + + + pub fn is_message_list_focused(&self) -> bool { + matches!(self.focus, Focus::MessageList) + } + + pub fn is_request_section_focused(&self) -> bool { + matches!(self.focus, Focus::RequestSection) + } + + pub fn is_response_section_focused(&self) -> bool { + matches!(self.focus, Focus::ResponseSection) + } + + + + pub fn next_request_tab(&mut self) { + self.request_tab = 1 - self.request_tab; // Toggle between 0 and 1 + self.reset_details_scroll(); + } + + pub fn previous_request_tab(&mut self) { + self.request_tab = 1 - self.request_tab; // Toggle between 0 and 1 + self.reset_details_scroll(); + } + + pub fn next_response_tab(&mut self) { + self.response_tab = 1 - self.response_tab; // Toggle between 0 and 1 + self.reset_details_scroll(); + } + + pub fn previous_response_tab(&mut self) { + self.response_tab = 1 - self.response_tab; // Toggle between 0 and 1 + self.reset_details_scroll(); + } + // Filtering requests methods pub fn start_filtering_requests(&mut self) { self.input_mode = InputMode::FilteringRequests; @@ -524,6 +582,122 @@ impl App { } } + pub fn get_request_details_content_lines(&self) -> usize { + if let Some(exchange) = self.get_selected_exchange() { + let mut line_count = 0; + + // Basic exchange info + line_count += 1; // Transport line + + if exchange.method.is_some() { + line_count += 1; + } + if exchange.id.is_some() { + line_count += 1; + } + + // Request section + line_count += 1; // Blank line before section + line_count += 1; // Section header + line_count += 1; // Tabs line + + if let Some(request) = &exchange.request { + match self.request_details_tab { + 0 => match &request.headers { + Some(headers) if !headers.is_empty() => { + line_count += headers.len(); + } + Some(_) | None => { + line_count += 1; + } + }, + _ => { + let mut request_json = serde_json::Map::new(); + request_json.insert( + "jsonrpc".to_string(), + serde_json::Value::String("2.0".to_string()), + ); + if let Some(id) = &request.id { + request_json.insert("id".to_string(), id.clone()); + } + if let Some(method) = &request.method { + request_json.insert( + "method".to_string(), + serde_json::Value::String(method.clone()), + ); + } + if let Some(params) = &request.params { + request_json.insert("params".to_string(), params.clone()); + } + + if let Ok(json_str) = + serde_json::to_string_pretty(&serde_json::Value::Object(request_json)) + { + line_count += json_str.lines().count(); + } + } + } + } else { + line_count += 1; + } + + line_count + } else { + 1 + } + } + + pub fn get_response_details_content_lines(&self) -> usize { + if let Some(exchange) = self.get_selected_exchange() { + let mut line_count = 0; + + // Response section + line_count += 1; // Section header + line_count += 1; // Tabs line + + if let Some(response) = &exchange.response { + match self.response_details_tab { + 0 => match &response.headers { + Some(headers) if !headers.is_empty() => { + line_count += headers.len(); + } + Some(_) | None => { + line_count += 1; + } + }, + _ => { + let mut response_json = serde_json::Map::new(); + response_json.insert( + "jsonrpc".to_string(), + serde_json::Value::String("2.0".to_string()), + ); + if let Some(id) = &response.id { + response_json.insert("id".to_string(), id.clone()); + } + if let Some(result) = &response.result { + response_json.insert("result".to_string(), result.clone()); + } + if let Some(error) = &response.error { + response_json.insert("error".to_string(), error.clone()); + } + + if let Ok(json_str) = + serde_json::to_string_pretty(&serde_json::Value::Object(response_json)) + { + line_count += json_str.lines().count(); + } + } + } + } else { + line_count += 1; + } + + line_count + } else { + 1 + } + } + // Pause/Intercept functionality pub fn toggle_pause_mode(&mut self) { self.app_mode = match self.app_mode { diff --git a/src/main.rs b/src/main.rs index 246f866..801d63e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -265,29 +265,91 @@ async fn run_app( return Ok(()); } KeyCode::Up => match app.app_mode { - app::AppMode::Normal => app.select_previous(), + app::AppMode::Normal => { + if app.is_message_list_focused() { + app.select_previous(); + } else if app.is_request_section_focused() { + if app.request_details_scroll > 0 { + app.request_details_scroll -= 1; + } + } else if app.is_response_section_focused() { + if app.response_details_scroll > 0 { + app.response_details_scroll -= 1; + } + } + } app::AppMode::Paused | app::AppMode::Intercepting => { app.select_previous_pending() } }, KeyCode::Down => match app.app_mode { - app::AppMode::Normal => app.select_next(), + app::AppMode::Normal => { + if app.is_message_list_focused() { + app.select_next(); + } else if app.is_request_section_focused() { + if app.get_selected_exchange().is_some() { + app.request_details_scroll += 1; // Allow unlimited scrolling, UI will clamp + } + } else if app.is_response_section_focused() { + if app.get_selected_exchange().is_some() { + app.response_details_scroll += 1; // Allow unlimited scrolling, UI will clamp + } + } + } app::AppMode::Paused | app::AppMode::Intercepting => { app.select_next_pending() } }, KeyCode::Left => { if app.app_mode == app::AppMode::Normal { - app.previous_details_tab(); + if app.is_request_section_focused() { + app.previous_request_tab(); + } else if app.is_response_section_focused() { + app.previous_response_tab(); + } else if app.is_message_list_focused() { + app.select_previous(); + } } } KeyCode::Right => { if app.app_mode == app::AppMode::Normal { - app.next_details_tab(); + if app.is_request_section_focused() { + app.next_request_tab(); + } else if app.is_response_section_focused() { + app.next_response_tab(); + } else if app.is_message_list_focused() { + app.select_next(); + } + } + } + KeyCode::Tab => { + if app.app_mode == app::AppMode::Normal { + app.switch_focus(); + } + // Don't process any other key handling for Tab + continue; + } + KeyCode::BackTab => { + if app.app_mode == app::AppMode::Normal { + app.switch_focus_reverse(); } + // Don't process any other key handling for Shift+Tab + continue; } KeyCode::Char('k') => match app.app_mode { - app::AppMode::Normal => app.scroll_details_up(), + app::AppMode::Normal => { + if app.is_message_list_focused() { + app.select_previous(); + } else if app.is_request_section_focused() { + if app.request_details_scroll > 0 { + app.request_details_scroll -= 1; + } + } else if app.is_response_section_focused() { + if app.response_details_scroll > 0 { + app.response_details_scroll -= 1; + } + } + } app::AppMode::Paused | app::AppMode::Intercepting => { app.scroll_intercept_details_up() } @@ -295,57 +357,66 @@ async fn run_app( KeyCode::Char('j') => { match app.app_mode { app::AppMode::Normal => { - // Calculate proper max lines for scrolling - if app.get_selected_exchange().is_some() { - let max_lines = app.get_details_content_lines(); - app.scroll_details_down(max_lines, 20); // 20 is rough visible lines estimate + if app.is_message_list_focused() { + app.select_next(); + } else if app.is_request_section_focused() { + if app.get_selected_exchange().is_some() { + app.request_details_scroll += 1; // Allow unlimited scrolling, UI will clamp + } + } else if app.is_response_section_focused() { + if app.get_selected_exchange().is_some() { + app.response_details_scroll += 1; // Allow unlimited scrolling, UI will clamp + } } } app::AppMode::Paused | app::AppMode::Intercepting => { - // For intercept mode, we need to calculate content lines differently - // For now, use a large number as max_lines since we don't have a helper method yet - app.scroll_intercept_details_down(1000, 20); + app.intercept_details_scroll += 1; // Allow unlimited scrolling, UI will clamp } } - } - // Vim-style page navigation for details - KeyCode::Char('d') - if key.modifiers.contains(event::KeyModifiers::CONTROL) => - { - match app.app_mode { - app::AppMode::Normal => app.page_down_details(20), - app::AppMode::Paused | app::AppMode::Intercepting => { - app.page_down_intercept_details(20) - } - } - } - KeyCode::Char('u') - if key.modifiers.contains(event::KeyModifiers::CONTROL) => - { - match app.app_mode { - app::AppMode::Normal => app.page_up_details(), - app::AppMode::Paused | app::AppMode::Intercepting => { - app.page_up_intercept_details() + }, + KeyCode::Char('u') => match app.app_mode { + app::AppMode::Normal => { + if app.is_request_section_focused() { + let page_size = 10; + app.request_details_scroll = app.request_details_scroll.saturating_sub(page_size); + } else if app.is_response_section_focused() { + let page_size = 10; + app.response_details_scroll = app.response_details_scroll.saturating_sub(page_size); } + // u does nothing when message list is focused } - } - KeyCode::Char('d') => match app.app_mode { - app::AppMode::Normal => app.page_down_details(20), app::AppMode::Paused | app::AppMode::Intercepting => { - app.page_down_intercept_details(20) + app.page_up_intercept_details() } }, - KeyCode::Char('u') => match app.app_mode { - app::AppMode::Normal => app.page_up_details(), + KeyCode::Char('d') => match app.app_mode { + app::AppMode::Normal => { + if app.is_request_section_focused() { + let page_size = 10; + app.request_details_scroll += page_size; + } else if app.is_response_section_focused() { + let page_size = 10; + app.response_details_scroll += page_size; + } + // d does nothing when message list is focused + } app::AppMode::Paused | app::AppMode::Intercepting => { - app.page_up_intercept_details() + app.page_down_intercept_details(); } }, KeyCode::Char('G') => { match app.app_mode { app::AppMode::Normal => { - let max_lines = app.get_details_content_lines(); - app.goto_bottom_details(max_lines, 20); + if app.is_request_section_focused() { + if app.get_selected_exchange().is_some() { + app.request_details_scroll = 10000; // Large number, UI will clamp to actual bottom + } + } else if app.is_response_section_focused() { + if app.get_selected_exchange().is_some() { + app.response_details_scroll = 10000; // Large number, UI will clamp to actual bottom + } + } + // G does nothing when message list is focused } app::AppMode::Paused | app::AppMode::Intercepting => { // For intercept mode, use a large number as max_lines @@ -354,7 +425,14 @@ async fn run_app( } } KeyCode::Char('g') => match app.app_mode { - app::AppMode::Normal => app.goto_top_details(), + app::AppMode::Normal => { + if app.is_request_section_focused() { + app.request_details_scroll = 0; + } else if app.is_response_section_focused() { + app.response_details_scroll = 0; + } + // g does nothing when message list is focused + } app::AppMode::Paused | app::AppMode::Intercepting => { app.goto_top_intercept_details() } @@ -369,7 +447,21 @@ async fn run_app( if key.modifiers.contains(event::KeyModifiers::CONTROL) => { match app.app_mode { - app::AppMode::Normal => app.select_next(), + app::AppMode::Normal => { + if app.is_message_list_focused() { + app.select_next(); + } else if app.is_request_section_focused() { + // Request details - scroll down + if app.get_selected_exchange().is_some() { + app.request_details_scroll += 1; // Allow unlimited scrolling, UI will clamp + } + } else if app.is_response_section_focused() { + // Response details - scroll down + if app.get_selected_exchange().is_some() { + app.response_details_scroll += 1; // Allow unlimited scrolling, UI will clamp + } + } + } app::AppMode::Paused | app::AppMode::Intercepting => { app.select_next_pending() } @@ -379,7 +471,21 @@ async fn run_app( if key.modifiers.contains(event::KeyModifiers::CONTROL) => { match app.app_mode { - app::AppMode::Normal => app.select_previous(), + app::AppMode::Normal => { + if app.is_message_list_focused() { + app.select_previous(); + } else if app.is_request_section_focused() { + // Request details - scroll up + if app.request_details_scroll > 0 { + app.request_details_scroll -= 1; + } + } else if app.is_response_section_focused() { + // Response details - scroll up + if app.response_details_scroll > 0 { + app.response_details_scroll -= 1; + } + } + } app::AppMode::Paused | app::AppMode::Intercepting => { app.select_previous_pending() } @@ -462,8 +568,10 @@ async fn run_app( } } KeyCode::Char('h') => { - // Edit selected pending request headers with external editor - if let Some(headers_content) = app.get_pending_request_headers() { + // Edit selected pending request headers with external editor (intercept mode) + if (app.app_mode == app::AppMode::Paused || app.app_mode == app::AppMode::Intercepting) + && app.get_pending_request_headers().is_some() { + let headers_content = app.get_pending_request_headers().unwrap(); // Temporarily exit TUI mode disable_raw_mode()?; execute!( @@ -498,6 +606,15 @@ async fn run_app( )?; terminal.clear()?; } + // Navigate tabs left in normal mode + if app.app_mode == app::AppMode::Normal + && (app.is_request_section_focused() || app.is_response_section_focused()) { + if app.is_request_section_focused() { + app.previous_request_tab(); + } else if app.is_response_section_focused() { + app.previous_response_tab(); + } + } } KeyCode::Char('c') => { // Check if we have a selected pending request (in either Paused or Intercepting mode) @@ -598,6 +715,16 @@ async fn run_app( app.resume_all_requests(); terminal.clear()?; } + KeyCode::Char('l') => { + if app.app_mode == app::AppMode::Normal + && (app.is_request_section_focused() || app.is_response_section_focused()) { + if app.is_request_section_focused() { + app.next_request_tab(); + } else if app.is_response_section_focused() { + app.next_response_tab(); + } + } + } _ => {} } diff --git a/src/ui.rs b/src/ui.rs index 14d37ef..6a443b3 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,7 +9,7 @@ use ratatui::{ Frame, }; -use crate::app::{App, AppMode, InputMode, TransportType}; +use crate::app::{App, AppMode, InputMode, TransportType, Focus}; // Helper function to format JSON with syntax highlighting and 2-space indentation fn format_json_with_highlighting(json_value: &serde_json::Value) -> Vec> { @@ -143,7 +143,7 @@ fn format_json_with_highlighting(json_value: &serde_json::Value) -> Vec { - lines.push(Line::from("")); - match &request.headers { - Some(headers) if !headers.is_empty() => { - for (key, value) in headers { - lines.push(Line::from(format!(" {}: {}", key, value))); - } - } - Some(_) => { - lines.push(Line::from(" No headers")); - } - None => { - lines.push(Line::from(" No headers captured")); + if app.request_tab == 0 { + // Show headers regardless of focus state + lines.push(Line::from("")); + match &request.headers { + Some(headers) if !headers.is_empty() => { + for (key, value) in headers { + lines.push(Line::from(format!(" {}: {}", key, value))); } } + Some(_) => { + lines.push(Line::from(" No headers")); + } + None => { + lines.push(Line::from(" No headers captured")); + } } - _ => { - lines.push(Line::from("")); - let mut request_json = serde_json::Map::new(); + } else { + // Show body regardless of focus state + lines.push(Line::from("")); + let mut request_json = serde_json::Map::new(); + request_json.insert( + "jsonrpc".to_string(), + serde_json::Value::String("2.0".to_string()), + ); + + if let Some(id) = &request.id { + request_json.insert("id".to_string(), id.clone()); + } + if let Some(method) = &request.method { request_json.insert( - "jsonrpc".to_string(), - serde_json::Value::String("2.0".to_string()), + "method".to_string(), + serde_json::Value::String(method.clone()), ); + } + if let Some(params) = &request.params { + request_json.insert("params".to_string(), params.clone()); + } - if let Some(id) = &request.id { - request_json.insert("id".to_string(), id.clone()); - } - if let Some(method) = &request.method { - request_json.insert( - "method".to_string(), - serde_json::Value::String(method.clone()), - ); - } - if let Some(params) = &request.params { - request_json.insert("params".to_string(), params.clone()); - } - - let request_json_value = serde_json::Value::Object(request_json); - let request_json_lines = format_json_with_highlighting(&request_json_value); - for line in request_json_lines { - lines.push(line); - } + let request_json_value = serde_json::Value::Object(request_json); + let request_json_lines = format_json_with_highlighting(&request_json_value); + for line in request_json_lines { + lines.push(line); } } } else { @@ -559,63 +567,145 @@ fn draw_message_details(f: &mut Frame, area: Rect, app: &App) { lines.push(Line::from("Request not captured yet")); } + lines + } else { + vec![Line::from("No request selected")] + }; + + // Calculate visible area for scrolling + let inner_area = area.inner(&Margin { + vertical: 1, + horizontal: 1, + }); + let visible_lines = inner_area.height as usize; + let total_lines = content.len(); + + // Apply scrolling offset + let start_line = app.request_details_scroll; + let end_line = std::cmp::min(start_line + visible_lines, total_lines); + let visible_content = if start_line < total_lines { + content[start_line..end_line].to_vec() + } else { + vec![] + }; + + // Create title with scroll indicator + let base_title = "Request Details"; + + let scroll_info = if total_lines > visible_lines { + let progress = + ((app.request_details_scroll as f32 / (total_lines - visible_lines) as f32) * 100.0) as u8; + format!("{} ({}% - vim: j/k/d/u/G/g)", base_title, progress) + } else { + base_title.to_string() + }; + + let details_block = if matches!(app.focus, Focus::RequestSection) { + Block::default() + .borders(Borders::ALL) + .title(scroll_info) + .border_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + } else { + Block::default().borders(Borders::ALL).title(scroll_info) + }; + + let details = Paragraph::new(visible_content) + .block(details_block) + .wrap(Wrap { trim: false }); + + f.render_widget(details, area); + + if total_lines > visible_lines { + let mut scrollbar_state = ScrollbarState::new(total_lines).position(app.request_details_scroll); + + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .begin_symbol(None) + .end_symbol(None) + .track_symbol(None) + .thumb_symbol("▐"); + + f.render_stateful_widget( + scrollbar, + area.inner(&Margin { + vertical: 1, + horizontal: 0, + }), + &mut scrollbar_state, + ); + } +} + +fn draw_details_split(f: &mut Frame, area: Rect, app: &App) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Percentage(50), // Request details + Constraint::Percentage(50), // Response details + ]) + .split(area); + + draw_request_details(f, chunks[0], app); + draw_response_details(f, chunks[1], app); +} + +fn draw_response_details(f: &mut Frame, area: Rect, app: &App) { + let content = if let Some(exchange) = app.get_selected_exchange() { + let mut lines = Vec::new(); + // Response section with tabs - lines.push(Line::from("")); lines.push(Line::from(Span::styled( "RESPONSE:", Style::default() .add_modifier(Modifier::BOLD) .fg(Color::Blue), ))); - let response_active = matches!(app.details_tab, 2 | 3); lines.push(build_tab_line( &["Headers", "Body"], - app.response_details_tab, - response_active, + app.response_tab, + matches!(app.focus, Focus::ResponseSection), exchange.response.is_some(), )); if let Some(response) = &exchange.response { - match app.response_details_tab { - 0 => { - lines.push(Line::from("")); - match &response.headers { - Some(headers) if !headers.is_empty() => { - for (key, value) in headers { - lines.push(Line::from(format!(" {}: {}", key, value))); - } - } - Some(_) => { - lines.push(Line::from(" No headers")); - } - None => { - lines.push(Line::from(" No headers captured")); + if app.response_tab == 0 { + // Show headers regardless of focus state + lines.push(Line::from("")); + match &response.headers { + Some(headers) if !headers.is_empty() => { + for (key, value) in headers { + lines.push(Line::from(format!(" {}: {}", key, value))); } } - } - _ => { - lines.push(Line::from("")); - let mut response_json = serde_json::Map::new(); - response_json.insert( - "jsonrpc".to_string(), - serde_json::Value::String("2.0".to_string()), - ); - - if let Some(id) = &response.id { - response_json.insert("id".to_string(), id.clone()); + Some(_) => { + lines.push(Line::from(" No headers")); } - if let Some(result) = &response.result { - response_json.insert("result".to_string(), result.clone()); - } - if let Some(error) = &response.error { - response_json.insert("error".to_string(), error.clone()); + None => { + lines.push(Line::from(" No headers captured")); } + } + } else { + // Show body regardless of focus state + lines.push(Line::from("")); + let mut response_json = serde_json::Map::new(); + response_json.insert( + "jsonrpc".to_string(), + serde_json::Value::String("2.0".to_string()), + ); - let response_json_value = serde_json::Value::Object(response_json); - let response_json_lines = format_json_with_highlighting(&response_json_value); - for line in response_json_lines { - lines.push(line); - } + if let Some(id) = &response.id { + response_json.insert("id".to_string(), id.clone()); + } + if let Some(result) = &response.result { + response_json.insert("result".to_string(), result.clone()); + } + if let Some(error) = &response.error { + response_json.insert("error".to_string(), error.clone()); + } + + let response_json_value = serde_json::Value::Object(response_json); + let response_json_lines = format_json_with_highlighting(&response_json_value); + for line in response_json_lines { + lines.push(line); } } } else { @@ -640,7 +730,7 @@ fn draw_message_details(f: &mut Frame, area: Rect, app: &App) { let total_lines = content.len(); // Apply scrolling offset - let start_line = app.details_scroll; + let start_line = app.response_details_scroll; let end_line = std::cmp::min(start_line + visible_lines, total_lines); let visible_content = if start_line < total_lines { content[start_line..end_line].to_vec() @@ -649,22 +739,33 @@ fn draw_message_details(f: &mut Frame, area: Rect, app: &App) { }; // Create title with scroll indicator + let base_title = "Response Details"; + let scroll_info = if total_lines > visible_lines { let progress = - ((app.details_scroll as f32 / (total_lines - visible_lines) as f32) * 100.0) as u8; - format!("Details ({}% - vim: j/k/d/u/G/g)", progress) + ((app.response_details_scroll as f32 / (total_lines - visible_lines) as f32) * 100.0) as u8; + format!("{} ({}% - vim: j/k/d/u/G/g)", base_title, progress) } else { - "Details".to_string() + base_title.to_string() + }; + + let details_block = if matches!(app.focus, Focus::ResponseSection) { + Block::default() + .borders(Borders::ALL) + .title(scroll_info) + .border_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + } else { + Block::default().borders(Borders::ALL).title(scroll_info) }; let details = Paragraph::new(visible_content) - .block(Block::default().borders(Borders::ALL).title(scroll_info)) + .block(details_block) .wrap(Wrap { trim: false }); f.render_widget(details, area); if total_lines > visible_lines { - let mut scrollbar_state = ScrollbarState::new(total_lines).position(app.details_scroll); + let mut scrollbar_state = ScrollbarState::new(total_lines).position(app.response_details_scroll); let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) .begin_symbol(None) @@ -683,6 +784,8 @@ fn draw_message_details(f: &mut Frame, area: Rect, app: &App) { } } + + // Helper struct to represent a keybind with its display information #[derive(Clone)] struct KeybindInfo { @@ -726,12 +829,14 @@ fn get_keybinds_for_mode(app: &App) -> Vec { KeybindInfo::new("↑↓", "navigate", 1), KeybindInfo::new("s", "start/stop proxy", 1), // Navigation keybinds (priority 2) + KeybindInfo::new("Tab/Shift+Tab", "navigate", 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), + KeybindInfo::new("h/l", "navigate tabs", 3), ]; // Add context-specific keybinds (priority 4) @@ -900,7 +1005,7 @@ fn draw_intercept_content(f: &mut Frame, area: Rect, app: &App) { .split(area); draw_pending_requests(f, chunks[0], app); - draw_request_details(f, chunks[1], app); + draw_intercept_request_details(f, chunks[1], app); } fn draw_pending_requests(f: &mut Frame, area: Rect, app: &App) { @@ -1002,12 +1107,19 @@ fn draw_pending_requests(f: &mut Frame, area: Rect, app: &App) { }) .collect(); + let pending_block = if matches!(app.app_mode, AppMode::Paused | AppMode::Intercepting) { + Block::default() + .borders(Borders::ALL) + .title(format!("Pending Requests ({})", app.pending_requests.len())) + .border_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + } else { + Block::default() + .borders(Borders::ALL) + .title(format!("Pending Requests ({})", app.pending_requests.len())) + }; + let requests_list = List::new(requests) - .block( - Block::default() - .borders(Borders::ALL) - .title(format!("Pending Requests ({})", app.pending_requests.len())), - ) + .block(pending_block) .highlight_style( Style::default() .bg(Color::Cyan) @@ -1018,7 +1130,7 @@ fn draw_pending_requests(f: &mut Frame, area: Rect, app: &App) { f.render_widget(requests_list, area); } -fn draw_request_details(f: &mut Frame, area: Rect, app: &App) { +fn draw_intercept_request_details(f: &mut Frame, area: Rect, app: &App) { let content = if let Some(pending) = app.get_selected_pending() { let mut lines = Vec::new(); @@ -1174,8 +1286,17 @@ fn draw_request_details(f: &mut Frame, area: Rect, app: &App) { "Request Details".to_string() }; + let details_block = if matches!(app.app_mode, AppMode::Paused | AppMode::Intercepting) { + Block::default() + .borders(Borders::ALL) + .title(scroll_info) + .border_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + } else { + Block::default().borders(Borders::ALL).title(scroll_info) + }; + let details = Paragraph::new(visible_content) - .block(Block::default().borders(Borders::ALL).title(scroll_info)) + .block(details_block) .wrap(Wrap { trim: false }); f.render_widget(details, area); From 2ba16e42d1ad72518ef3b7db47a44ddde56004e0 Mon Sep 17 00:00:00 2001 From: Shane Jonas Date: Fri, 10 Oct 2025 09:02:08 -0400 Subject: [PATCH 3/4] fix: cargo fmt --- src/app.rs | 8 +----- src/main.rs | 41 ++++++++++++++++++------------ src/ui.rs | 72 ++++++++++++++++++++++++++++++++--------------------- 3 files changed, 69 insertions(+), 52 deletions(-) diff --git a/src/app.rs b/src/app.rs index 8b3c2df..91de542 100644 --- a/src/app.rs +++ b/src/app.rs @@ -98,7 +98,7 @@ pub struct App { pub pending_requests: Vec, // New field pub selected_pending: usize, // New field pub request_editor_buffer: String, // New field - pub focus: Focus, // New field for tracking which element is active + pub focus: Focus, // New field for tracking which element is active pub request_tab: usize, // 0 = Headers, 1 = Body pub response_tab: usize, // 0 = Headers, 1 = Body } @@ -307,8 +307,6 @@ impl App { self.details_scroll = 0; } - - // Intercept details scrolling methods pub fn scroll_intercept_details_up(&mut self) { if self.intercept_details_scroll > 0 { @@ -389,8 +387,6 @@ impl App { self.response_details_scroll = 0; } - - pub fn is_message_list_focused(&self) -> bool { matches!(self.focus, Focus::MessageList) } @@ -403,8 +399,6 @@ impl App { matches!(self.focus, Focus::ResponseSection) } - - pub fn next_request_tab(&mut self) { self.request_tab = 1 - self.request_tab; // Toggle between 0 and 1 self.reset_details_scroll(); diff --git a/src/main.rs b/src/main.rs index 801d63e..1974529 100644 --- a/src/main.rs +++ b/src/main.rs @@ -286,11 +286,11 @@ async fn run_app( app::AppMode::Normal => { if app.is_message_list_focused() { app.select_next(); - } else if app.is_request_section_focused() { - if app.get_selected_exchange().is_some() { - app.request_details_scroll += 1; // Allow unlimited scrolling, UI will clamp - } - } else if app.is_response_section_focused() { + } else if app.is_request_section_focused() { + if app.get_selected_exchange().is_some() { + app.request_details_scroll += 1; // Allow unlimited scrolling, UI will clamp + } + } else if app.is_response_section_focused() { if app.get_selected_exchange().is_some() { app.response_details_scroll += 1; // Allow unlimited scrolling, UI will clamp } @@ -373,15 +373,17 @@ async fn run_app( app.intercept_details_scroll += 1; // Allow unlimited scrolling, UI will clamp } } - }, + } KeyCode::Char('u') => match app.app_mode { app::AppMode::Normal => { if app.is_request_section_focused() { let page_size = 10; - app.request_details_scroll = app.request_details_scroll.saturating_sub(page_size); + app.request_details_scroll = + app.request_details_scroll.saturating_sub(page_size); } else if app.is_response_section_focused() { let page_size = 10; - app.response_details_scroll = app.response_details_scroll.saturating_sub(page_size); + app.response_details_scroll = + app.response_details_scroll.saturating_sub(page_size); } // u does nothing when message list is focused } @@ -409,11 +411,12 @@ async fn run_app( app::AppMode::Normal => { if app.is_request_section_focused() { if app.get_selected_exchange().is_some() { - app.request_details_scroll = 10000; // Large number, UI will clamp to actual bottom + app.request_details_scroll = 10000; // Large number, UI will clamp to actual bottom } } else if app.is_response_section_focused() { if app.get_selected_exchange().is_some() { - app.response_details_scroll = 10000; // Large number, UI will clamp to actual bottom + app.response_details_scroll = 10000; + // Large number, UI will clamp to actual bottom } } // G does nothing when message list is focused @@ -453,12 +456,12 @@ async fn run_app( } else if app.is_request_section_focused() { // Request details - scroll down if app.get_selected_exchange().is_some() { - app.request_details_scroll += 1; // Allow unlimited scrolling, UI will clamp + app.request_details_scroll += 1; // Allow unlimited scrolling, UI will clamp } } else if app.is_response_section_focused() { // Response details - scroll down if app.get_selected_exchange().is_some() { - app.response_details_scroll += 1; // Allow unlimited scrolling, UI will clamp + app.response_details_scroll += 1; // Allow unlimited scrolling, UI will clamp } } } @@ -569,8 +572,10 @@ async fn run_app( } KeyCode::Char('h') => { // Edit selected pending request headers with external editor (intercept mode) - if (app.app_mode == app::AppMode::Paused || app.app_mode == app::AppMode::Intercepting) - && app.get_pending_request_headers().is_some() { + if (app.app_mode == app::AppMode::Paused + || app.app_mode == app::AppMode::Intercepting) + && app.get_pending_request_headers().is_some() + { let headers_content = app.get_pending_request_headers().unwrap(); // Temporarily exit TUI mode disable_raw_mode()?; @@ -608,7 +613,9 @@ async fn run_app( } // Navigate tabs left in normal mode if app.app_mode == app::AppMode::Normal - && (app.is_request_section_focused() || app.is_response_section_focused()) { + && (app.is_request_section_focused() + || app.is_response_section_focused()) + { if app.is_request_section_focused() { app.previous_request_tab(); } else if app.is_response_section_focused() { @@ -717,7 +724,9 @@ async fn run_app( } KeyCode::Char('l') => { if app.app_mode == app::AppMode::Normal - && (app.is_request_section_focused() || app.is_response_section_focused()) { + && (app.is_request_section_focused() + || app.is_response_section_focused()) + { if app.is_request_section_focused() { app.next_request_tab(); } else if app.is_response_section_focused() { diff --git a/src/ui.rs b/src/ui.rs index 6a443b3..1361584 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,7 +9,7 @@ use ratatui::{ Frame, }; -use crate::app::{App, AppMode, InputMode, TransportType, Focus}; +use crate::app::{App, AppMode, Focus, InputMode, TransportType}; // Helper function to format JSON with syntax highlighting and 2-space indentation fn format_json_with_highlighting(json_value: &serde_json::Value) -> Vec> { @@ -152,23 +152,19 @@ fn build_tab_line( for (index, label) in labels.iter().enumerate() { let is_selected = index == selected; - + if is_selected { // Active tab - use a more prominent style like modern tab designs let mut style = Style::default(); if is_enabled { style = style .fg(Color::Black) - .bg(if is_active { - Color::Cyan - } else { - Color::White - }) + .bg(if is_active { Color::Cyan } else { Color::White }) .add_modifier(Modifier::BOLD); } else { style = style.fg(Color::DarkGray).bg(Color::DarkGray); } - + spans.push(Span::styled(format!(" {} ", *label), style)); } else if is_enabled { // Inactive tab - subtle background @@ -412,7 +408,11 @@ fn draw_message_list(f: &mut Frame, area: Rect, app: &App) { Block::default() .borders(Borders::ALL) .title(table_title) - .border_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + .border_style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) } else { Block::default().borders(Borders::ALL).title(table_title) }; @@ -593,8 +593,8 @@ fn draw_request_details(f: &mut Frame, area: Rect, app: &App) { let base_title = "Request Details"; let scroll_info = if total_lines > visible_lines { - let progress = - ((app.request_details_scroll as f32 / (total_lines - visible_lines) as f32) * 100.0) as u8; + let progress = ((app.request_details_scroll as f32 / (total_lines - visible_lines) as f32) + * 100.0) as u8; format!("{} ({}% - vim: j/k/d/u/G/g)", base_title, progress) } else { base_title.to_string() @@ -604,7 +604,11 @@ fn draw_request_details(f: &mut Frame, area: Rect, app: &App) { Block::default() .borders(Borders::ALL) .title(scroll_info) - .border_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + .border_style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) } else { Block::default().borders(Borders::ALL).title(scroll_info) }; @@ -616,7 +620,8 @@ fn draw_request_details(f: &mut Frame, area: Rect, app: &App) { f.render_widget(details, area); if total_lines > visible_lines { - let mut scrollbar_state = ScrollbarState::new(total_lines).position(app.request_details_scroll); + let mut scrollbar_state = + ScrollbarState::new(total_lines).position(app.request_details_scroll); let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) .begin_symbol(None) @@ -742,8 +747,8 @@ fn draw_response_details(f: &mut Frame, area: Rect, app: &App) { let base_title = "Response Details"; let scroll_info = if total_lines > visible_lines { - let progress = - ((app.response_details_scroll as f32 / (total_lines - visible_lines) as f32) * 100.0) as u8; + let progress = ((app.response_details_scroll as f32 / (total_lines - visible_lines) as f32) + * 100.0) as u8; format!("{} ({}% - vim: j/k/d/u/G/g)", base_title, progress) } else { base_title.to_string() @@ -753,7 +758,11 @@ fn draw_response_details(f: &mut Frame, area: Rect, app: &App) { Block::default() .borders(Borders::ALL) .title(scroll_info) - .border_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + .border_style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) } else { Block::default().borders(Borders::ALL).title(scroll_info) }; @@ -765,7 +774,8 @@ fn draw_response_details(f: &mut Frame, area: Rect, app: &App) { f.render_widget(details, area); if total_lines > visible_lines { - let mut scrollbar_state = ScrollbarState::new(total_lines).position(app.response_details_scroll); + let mut scrollbar_state = + ScrollbarState::new(total_lines).position(app.response_details_scroll); let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) .begin_symbol(None) @@ -784,8 +794,6 @@ fn draw_response_details(f: &mut Frame, area: Rect, app: &App) { } } - - // Helper struct to represent a keybind with its display information #[derive(Clone)] struct KeybindInfo { @@ -1111,21 +1119,23 @@ fn draw_pending_requests(f: &mut Frame, area: Rect, app: &App) { Block::default() .borders(Borders::ALL) .title(format!("Pending Requests ({})", app.pending_requests.len())) - .border_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + .border_style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) } else { Block::default() .borders(Borders::ALL) .title(format!("Pending Requests ({})", app.pending_requests.len())) }; - let requests_list = List::new(requests) - .block(pending_block) - .highlight_style( - Style::default() - .bg(Color::Cyan) - .fg(Color::Black) - .add_modifier(Modifier::BOLD), - ); + let requests_list = List::new(requests).block(pending_block).highlight_style( + Style::default() + .bg(Color::Cyan) + .fg(Color::Black) + .add_modifier(Modifier::BOLD), + ); f.render_widget(requests_list, area); } @@ -1290,7 +1300,11 @@ fn draw_intercept_request_details(f: &mut Frame, area: Rect, app: &App) { Block::default() .borders(Borders::ALL) .title(scroll_info) - .border_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + .border_style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) } else { Block::default().borders(Borders::ALL).title(scroll_info) }; From 3f1e492f194e02645bdd629a8227da53dbb32e22 Mon Sep 17 00:00:00 2001 From: Shane Jonas Date: Fri, 10 Oct 2025 09:24:20 -0400 Subject: [PATCH 4/4] refactor: improve code readability and scrolling logic --- src/app.rs | 1 + src/main.rs | 42 +++++++++++++++++++++--------------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/app.rs b/src/app.rs index 91de542..5ada25f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -117,6 +117,7 @@ impl Default for App { } } +#[allow(dead_code)] impl App { pub fn new() -> Self { let mut table_state = TableState::default(); diff --git a/src/main.rs b/src/main.rs index 1974529..ed27e92 100644 --- a/src/main.rs +++ b/src/main.rs @@ -272,10 +272,10 @@ async fn run_app( if app.request_details_scroll > 0 { app.request_details_scroll -= 1; } - } else if app.is_response_section_focused() { - if app.response_details_scroll > 0 { - app.response_details_scroll -= 1; - } + } else if app.is_response_section_focused() + && app.response_details_scroll > 0 + { + app.response_details_scroll -= 1; } } app::AppMode::Paused | app::AppMode::Intercepting => { @@ -290,10 +290,10 @@ async fn run_app( if app.get_selected_exchange().is_some() { app.request_details_scroll += 1; // Allow unlimited scrolling, UI will clamp } - } else if app.is_response_section_focused() { - if app.get_selected_exchange().is_some() { - app.response_details_scroll += 1; // Allow unlimited scrolling, UI will clamp - } + } else if app.is_response_section_focused() + && app.get_selected_exchange().is_some() + { + app.response_details_scroll += 1; // Allow unlimited scrolling, UI will clamp } } app::AppMode::Paused | app::AppMode::Intercepting => { @@ -344,10 +344,10 @@ async fn run_app( if app.request_details_scroll > 0 { app.request_details_scroll -= 1; } - } else if app.is_response_section_focused() { - if app.response_details_scroll > 0 { - app.response_details_scroll -= 1; - } + } else if app.is_response_section_focused() + && app.response_details_scroll > 0 + { + app.response_details_scroll -= 1; } } app::AppMode::Paused | app::AppMode::Intercepting => { @@ -363,10 +363,10 @@ async fn run_app( if app.get_selected_exchange().is_some() { app.request_details_scroll += 1; // Allow unlimited scrolling, UI will clamp } - } else if app.is_response_section_focused() { - if app.get_selected_exchange().is_some() { - app.response_details_scroll += 1; // Allow unlimited scrolling, UI will clamp - } + } else if app.is_response_section_focused() + && app.get_selected_exchange().is_some() + { + app.response_details_scroll += 1; // Allow unlimited scrolling, UI will clamp } } app::AppMode::Paused | app::AppMode::Intercepting => { @@ -413,11 +413,11 @@ async fn run_app( if app.get_selected_exchange().is_some() { app.request_details_scroll = 10000; // Large number, UI will clamp to actual bottom } - } else if app.is_response_section_focused() { - if app.get_selected_exchange().is_some() { - app.response_details_scroll = 10000; - // Large number, UI will clamp to actual bottom - } + } else if app.is_response_section_focused() + && app.get_selected_exchange().is_some() + { + app.response_details_scroll = 10000; + // Large number, UI will clamp to actual bottom } // G does nothing when message list is focused }