use bytes::Bytes;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use proxyapi::{InterceptConfig, InterceptDecision};
use proxyapi_models::ProxiedRequest;
use std::sync::Arc;
use tokio::sync::mpsc;
use super::state::{AppState, EditAction, EditSession};
pub fn handle_key_event(
key: KeyEvent,
state: &mut AppState,
intercept: &Arc<InterceptConfig>,
replay_tx: &mpsc::Sender<ProxiedRequest>,
) -> bool {
if state.show_help {
if matches!(key.code, KeyCode::Char('?') | KeyCode::Esc) {
state.show_help = false;
}
return false;
}
if let Some(ref mut session) = state.edit_session {
if session.typing {
match session.handle_key(key) {
EditAction::None => {}
EditAction::StageEdits => {
session.typing = false;
}
EditAction::Discard => {
state.edit_session = None;
}
}
return false;
}
if key.code == KeyCode::Esc {
state.edit_session = None;
return false;
}
}
if state.filter_mode {
match key.code {
KeyCode::Esc => {
state.filter_mode = false;
state.filter_input.clear();
state.filter = None;
}
KeyCode::Enter => {
state.filter_mode = false;
if state.filter_input.is_empty() {
state.filter = None;
} else {
state.filter = Some(state.filter_input.clone());
}
}
KeyCode::Backspace => {
state.filter_input.pop();
}
KeyCode::Char(c) => {
state.filter_input.push(c);
}
_ => {}
}
return false;
}
if state.detail_focused {
match key.code {
KeyCode::Char('j') | KeyCode::Down => {
state.frames_follow = false;
state.detail_scroll = state.detail_scroll.saturating_add(1);
}
KeyCode::Char('k') | KeyCode::Up => {
state.frames_follow = false;
state.detail_scroll = state.detail_scroll.saturating_sub(1);
}
KeyCode::Tab => state.toggle_tab(),
KeyCode::Enter | KeyCode::Esc => {
state.detail_focused = false;
}
KeyCode::Char('q' | 'Q') => return true,
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return true,
_ => {}
}
return false;
}
match key.code {
KeyCode::Char('q' | 'Q') => return true,
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => return true,
KeyCode::Char('j') | KeyCode::Down => state.select_next(),
KeyCode::Char('k') | KeyCode::Up => state.select_prev(),
KeyCode::Char('g') => state.select_first(),
KeyCode::Char('G') => state.select_last(),
KeyCode::Enter => {
if state.detail_open {
state.detail_focused = true;
} else {
state.detail_open = true;
}
}
KeyCode::Tab => state.toggle_tab(),
KeyCode::Char('/') => {
state.filter_mode = true;
state.filter_input.clear();
}
KeyCode::Esc => {
if state.detail_open {
state.detail_open = false;
state.detail_focused = false;
} else {
state.filter = None;
}
}
KeyCode::Char('c') => state.clear(),
KeyCode::Char('i') => {
state.intercept_enabled = intercept.toggle();
}
KeyCode::Char('f') => {
if let Some(ref session) = state.edit_session {
let id = session.id;
let text = session.to_text();
match parse_raw_http_request(&text) {
Ok((method, uri, headers, body)) => {
state.edit_session = None;
intercept.resolve(
id,
InterceptDecision::Modified {
method,
uri,
headers,
body,
},
);
}
Err(_) => {
let s = state.edit_session.as_mut().unwrap();
s.parse_error = true;
s.typing = true;
}
}
} else if let Some(id) = state.selected_pending_id() {
intercept.resolve(id, InterceptDecision::Forward);
}
}
KeyCode::Char('d') => {
let id = state
.edit_session
.take()
.map(|s| s.id)
.or_else(|| state.selected_pending_id());
if let Some(id) = id {
intercept.resolve(
id,
InterceptDecision::Block {
status: 504,
body: Bytes::from_static(b"Blocked by Proxelar intercept"),
},
);
state.remove_pending_by_id(id);
}
}
KeyCode::Char('e') => {
if let Some(ref mut session) = state.edit_session {
session.typing = true;
session.parse_error = false;
state.detail_open = true;
} else if let Some((id, req)) = state.selected_pending_request() {
let (text, binary_body) = request_to_text(req);
let mut session = EditSession::new(id, &text);
session.binary_body = binary_body;
state.edit_session = Some(session);
state.detail_open = true;
}
}
KeyCode::Char('?') => state.show_help = true,
KeyCode::Char('r') => {
if let Some(req) = state.selected_request() {
if replay_tx.try_send(req.clone()).is_err() {
tracing::warn!("Replay channel full, dropping request");
}
}
}
_ => {}
}
false
}
fn request_to_text(req: &proxyapi_models::ProxiedRequest) -> (String, bool) {
let mut text = format!("{} {} {:?}\n", req.method(), req.uri(), req.version());
for (name, value) in req.headers() {
text.push_str(&format!(
"{}: {}\n",
name,
String::from_utf8_lossy(value.as_bytes())
));
}
text.push('\n');
let binary_body = !req.body().is_empty() && std::str::from_utf8(req.body()).is_err();
if !req.body().is_empty() {
text.push_str(&String::from_utf8_lossy(req.body()));
}
(text, binary_body)
}
fn parse_raw_http_request(text: &str) -> Result<(String, String, http::HeaderMap, Bytes), String> {
let normalised = text.replace("\r\n", "\n");
let mut parts = normalised.splitn(2, "\n\n");
let header_section = parts.next().unwrap_or("");
let body_str = parts.next().unwrap_or("");
let mut header_lines = header_section.lines();
let request_line = header_lines
.next()
.ok_or("Missing request line")?
.trim_end();
let mut fields = request_line.splitn(3, ' ');
let method = fields.next().ok_or("Missing method")?.trim().to_uppercase();
let uri = fields.next().ok_or("Missing URI")?.trim().to_string();
let mut headers = http::HeaderMap::new();
for line in header_lines {
let line = line.trim_end();
if line.is_empty() {
break;
}
if let Some((name, value)) = line.split_once(':') {
let name = name.trim();
let value = value.trim();
if let (Ok(n), Ok(v)) = (
http::header::HeaderName::from_bytes(name.as_bytes()),
http::header::HeaderValue::from_str(value),
) {
headers.append(n, v);
}
}
}
let body = Bytes::copy_from_slice(body_str.as_bytes());
Ok((method, uri, headers, body))
}