use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::collections::HashMap;
use std::fs;
use crate::config::{dir_for, prune_empty_parents, global_config_path, local_config_path};
use super::edit::{self, Applied, Edit};
use super::{App, Focus, Mode, PairKind, PendingAction, RequestDraft, ResponseKind, pair_field, profile_field};
use super::tree::{VisibleRow, visible_rows, build_tree, load_entries};
pub(super) fn handle_key_new_request(app: &mut App, key: KeyEvent) -> bool {
match key.code {
KeyCode::Tab => {
if let Mode::NewRequest { draft, .. } = &mut app.mode {
let next = (draft.focused + 1) % draft.rows();
draft.focus(next);
}
}
KeyCode::BackTab => {
if let Mode::NewRequest { draft, .. } = &mut app.mode {
let rows = draft.rows();
let prev = draft.focused.checked_sub(1).unwrap_or(rows - 1);
draft.focus(prev);
}
}
KeyCode::Char('g') if key.modifiers.contains(KeyModifiers::CONTROL) => {
if let Mode::NewRequest { draft, .. } = &mut app.mode {
draft.global = !draft.global;
}
}
KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => {
let draft = match &app.mode {
Mode::NewRequest { draft, .. } => Some(draft.clone()),
_ => None,
};
if let Some(draft) = draft {
app.mode = Mode::ProfileList { draft, selected: 0 };
}
}
KeyCode::Char('e' | 'h') if key.modifiers.contains(KeyModifiers::CONTROL) => {
if let Mode::NewRequest { draft, .. } = &app.mode {
app.mode = Mode::edit_pairs(PairKind::Headers, draft.clone());
}
}
KeyCode::Char('q') if key.modifiers.contains(KeyModifiers::CONTROL) => {
if let Mode::NewRequest { draft, .. } = &app.mode {
app.mode = Mode::edit_pairs(PairKind::Query, draft.clone());
}
}
KeyCode::Enter => {
let target = match &app.mode {
Mode::NewRequest { draft, .. } => draft.focus_target(),
_ => return false,
};
match target {
Focus::Field(_) => app.save_new_request(),
Focus::Global => {
if let Mode::NewRequest { draft, .. } = &mut app.mode {
draft.global = !draft.global;
}
}
Focus::Headers | Focus::Query | Focus::Profiles => {
let Mode::NewRequest { draft, .. } = &app.mode else { return false };
let draft = draft.clone();
app.mode = match target {
Focus::Headers => Mode::edit_pairs(PairKind::Headers, draft),
Focus::Query => Mode::edit_pairs(PairKind::Query, draft),
_ => Mode::ProfileList { draft, selected: 0 },
};
}
}
}
_ => {
let keys = app.keys;
let mut leaving = false;
if let Mode::NewRequest { draft, error } = &mut app.mode {
match draft.focus_target() {
Focus::Field(i) => {
match edit::apply(&mut draft.fields[i].1, &mut draft.edit, key, keys) {
Applied::Yes => *error = None,
Applied::Exit => leaving = true,
Applied::No => {}
}
}
_ => leaving = key.code == KeyCode::Esc,
}
}
if leaving {
app.mode = Mode::Browse;
}
}
}
false
}
pub(super) fn handle_key_import_curl(app: &mut App, key: KeyEvent) -> bool {
match key.code {
KeyCode::Esc => app.mode = Mode::Browse,
KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.confirm_import_curl();
}
KeyCode::Enter => {
if let Mode::ImportCurl { buffer, error } = &mut app.mode {
buffer.push('\n');
*error = None;
}
}
KeyCode::Char(c) => {
if let Mode::ImportCurl { buffer, error } = &mut app.mode {
buffer.push(c);
*error = None;
}
}
KeyCode::Backspace => {
if let Mode::ImportCurl { buffer, error } = &mut app.mode {
buffer.pop();
*error = None;
}
}
_ => {}
}
false
}
pub(super) fn handle_key_edit_pairs(app: &mut App, key: KeyEvent) -> bool {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
match key.code {
KeyCode::Tab => {
if let Mode::EditPairs { pairs, focused, edit, .. } = &mut app.mode {
let total = 2 * pairs.len();
if total > 0 {
*focused = (*focused + 1) % total;
*edit = caret_for(pair_field(pairs, *focused));
}
}
}
KeyCode::BackTab => {
if let Mode::EditPairs { pairs, focused, edit, .. } = &mut app.mode {
let total = 2 * pairs.len();
if total > 0 {
*focused = focused.checked_sub(1).unwrap_or(total - 1);
*edit = caret_for(pair_field(pairs, *focused));
}
}
}
KeyCode::Char('a') if ctrl => {
if let Mode::EditPairs { pairs, focused, edit, error, .. } = &mut app.mode {
*focused = 2 * pairs.len();
pairs.push((String::new(), String::new()));
*edit = Edit::default();
*error = None;
}
}
KeyCode::Char('d') if ctrl => {
if let Mode::EditPairs { pairs, focused, edit, error, .. } = &mut app.mode {
let idx = *focused / 2;
if idx < pairs.len() {
pairs.remove(idx);
*focused = (*focused).min((2 * pairs.len()).saturating_sub(1));
*edit = caret_for(pair_field(pairs, *focused));
*error = None;
}
}
}
KeyCode::Enter => {
let taken = match &app.mode {
Mode::EditPairs { kind, draft, pairs, .. } => {
Some((*kind, draft.clone(), pairs.clone()))
}
_ => None,
};
let Some((kind, mut draft, pairs)) = taken else { return false };
match kind.apply(&mut draft, pairs) {
Ok(()) => app.mode = Mode::NewRequest { draft, error: None },
Err(message) => {
if let Mode::EditPairs { error, .. } = &mut app.mode {
*error = Some(message);
}
}
}
}
_ => {
let keys = app.keys;
let mut leaving = None;
if let Mode::EditPairs { draft, pairs, focused, edit, error, .. } = &mut app.mode {
let focused = *focused;
if let Some(value) = pair_field(pairs, focused) {
match edit::apply(value, edit, key, keys) {
Applied::Yes => *error = None,
Applied::Exit => leaving = Some(draft.clone()),
Applied::No => {}
}
} else if matches!(key.code, KeyCode::Esc) {
leaving = Some(draft.clone());
}
}
if let Some(draft) = leaving {
app.mode = Mode::NewRequest { draft, error: None };
}
}
}
false
}
pub(super) fn handle_key_profile_list(app: &mut App, key: KeyEvent) -> bool {
let Mode::ProfileList { draft, selected } = &mut app.mode else { return false };
let new_row = draft.profiles.len();
match key.code {
KeyCode::Esc => {
app.mode = Mode::NewRequest { draft: draft.clone(), error: None };
}
KeyCode::Char('j') | KeyCode::Down => {
if *selected < new_row {
*selected += 1;
}
}
KeyCode::Char('k') | KeyCode::Up => {
*selected = selected.saturating_sub(1);
}
KeyCode::Char('n') => {
app.mode = Mode::new_profile(draft.clone(), None, String::new(), Vec::new());
}
KeyCode::Char('d') => {
if *selected < new_row {
draft.profiles.remove(*selected);
*selected = (*selected).min(draft.profiles.len());
}
}
KeyCode::Enter => {
let idx = *selected;
let draft = draft.clone();
let picked = draft.profiles.get(idx).map(|profile| {
let mut params: Vec<(String, String)> = profile
.params
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
params.sort_by(|a, b| a.0.cmp(&b.0));
(profile.name.clone(), params)
});
app.mode = match picked {
Some((name, params)) => Mode::new_profile(draft, Some(idx), name, params),
None => Mode::new_profile(draft, None, String::new(), Vec::new()),
};
}
_ => {}
}
false
}
pub(super) fn handle_key_new_profile(app: &mut App, key: KeyEvent) -> bool {
match key.code {
KeyCode::Tab => {
if let Mode::NewProfile { name, params, focused, edit, .. } = &mut app.mode {
let total = 1 + 2 * params.len();
*focused = (*focused + 1) % total.max(1);
*edit = caret_for(profile_field(name, params, *focused));
}
}
KeyCode::BackTab => {
if let Mode::NewProfile { name, params, focused, edit, .. } = &mut app.mode {
let total = 1 + 2 * params.len();
*focused = focused.checked_sub(1).unwrap_or(total.saturating_sub(1));
*edit = caret_for(profile_field(name, params, *focused));
}
}
KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
if let Mode::NewProfile { params, focused, edit, .. } = &mut app.mode {
*focused = 1 + 2 * params.len();
params.push((String::new(), String::new()));
*edit = Edit::default();
}
}
KeyCode::Enter => {
app.save_new_profile();
}
_ => {
let keys = app.keys;
let mut leaving = None;
if let Mode::NewProfile { draft, editing, name, params, focused, edit, error } =
&mut app.mode
{
let focused = *focused;
if let Some(value) = profile_field(name, params, focused) {
match edit::apply(value, edit, key, keys) {
Applied::Yes => *error = None,
Applied::Exit => {
leaving = Some((draft.clone(), editing.unwrap_or(draft.profiles.len())))
}
Applied::No => {}
}
}
}
if let Some((draft, selected)) = leaving {
app.mode = Mode::ProfileList { draft, selected };
}
}
}
false
}
fn caret_for(value: Option<&mut String>) -> Edit {
value.map_or_else(Edit::default, |v| Edit::at_end(v))
}
pub(super) fn handle_key_profile_select(app: &mut App, code: KeyCode) -> bool {
match code {
KeyCode::Esc => {
app.mode = Mode::Browse;
}
KeyCode::Char('j') | KeyCode::Down => {
if let Mode::ProfileSelect { profiles, selected, .. } = &mut app.mode {
*selected = (*selected + 1).min(profiles.len());
}
}
KeyCode::Char('k') | KeyCode::Up => {
if let Mode::ProfileSelect { selected, .. } = &mut app.mode {
*selected = selected.saturating_sub(1);
}
}
KeyCode::Enter => {
app.confirm_profile_select();
}
_ => {}
}
false
}
pub(super) fn handle_key_browse(app: &mut App, code: KeyCode) -> bool {
let was_pending_g = app.pending_g;
app.pending_g = false;
match code {
KeyCode::Char('q') => return true,
KeyCode::Char('j') | KeyCode::Down => app.move_down(),
KeyCode::Char('k') | KeyCode::Up => app.move_up(),
KeyCode::Char('G') => app.move_bottom(),
KeyCode::Char('g') => {
if was_pending_g {
app.move_top();
} else {
app.pending_g = true;
}
}
KeyCode::Char(' ') if app.using_tree() => {
if let Some(pos) = app.list_state.selected() {
let rows = visible_rows(&app.tree, &app.collapsed_folders);
if let Some(VisibleRow::Folder { path, .. }) = rows.get(pos) {
app.toggle_folder(path.clone());
}
}
}
KeyCode::Char(' ') => {}
KeyCode::Char('R') => app.reload_entries(None),
KeyCode::Char('r') | KeyCode::Enter => {
if app.using_tree()
&& let Some(pos) = app.list_state.selected() {
let rows = visible_rows(&app.tree, &app.collapsed_folders);
if let Some(VisibleRow::Folder { path, .. }) = rows.get(pos) {
app.toggle_folder(path.clone());
return false;
}
}
app.try_run_selected()
}
KeyCode::Char('t') => app.try_test_selected(),
KeyCode::Char('y') => app.try_copy_curl_selected(),
KeyCode::Char('n') => {
app.mode = Mode::NewRequest { draft: RequestDraft::blank(), error: None };
}
KeyCode::Char('i') => {
app.mode = Mode::ImportCurl { buffer: String::new(), error: None };
}
KeyCode::Char('e') => app.edit_selected(),
KeyCode::Char('c') => app.clone_selected(),
KeyCode::Char('d') => {
let Some(idx) = app.selected_entry_index() else { return false };
let entry = &app.entries[idx];
app.mode = Mode::ConfirmDelete { entry_name: entry.name.clone(), global: entry.global };
}
KeyCode::Char('f') => {
app.filter_active = true;
let count = app.filtered_indices().len();
app.list_state.select(if count == 0 { None } else { Some(0) });
}
KeyCode::Char('?') => app.show_help = true,
_ => {}
}
false
}
pub(super) fn handle_key_filter(app: &mut App, key: KeyEvent) -> bool {
match key.code {
KeyCode::Esc => {
app.filter_active = false;
app.filter.clear();
let count = app.visible_count();
app.list_state.select(if count == 0 { None } else { Some(0) });
}
KeyCode::Enter => {
app.filter_active = false;
}
KeyCode::Char(c) => {
app.filter.push(c);
let count = app.visible_count();
app.list_state.select(if count == 0 { None } else { Some(0) });
}
KeyCode::Backspace => {
app.filter.pop();
let count = app.visible_count();
app.list_state.select(if count == 0 { None } else { Some(0) });
}
_ => {}
}
false
}
pub(super) fn handle_key_response_filter(app: &mut App, key: KeyEvent) -> bool {
if let Mode::Response {
response_filter, response_filter_active, scroll, cursor, anchor, ..
} = &mut app.mode
{
let mut reset = || {
*scroll = 0;
*cursor = 0;
*anchor = None;
};
match key.code {
KeyCode::Esc => {
response_filter.clear();
*response_filter_active = false;
reset();
}
KeyCode::Enter => {
*response_filter_active = false;
}
KeyCode::Char(c) => {
response_filter.push(c);
reset();
}
KeyCode::Backspace => {
response_filter.pop();
reset();
}
_ => {}
}
}
false
}
pub(super) fn handle_key_confirm_delete(app: &mut App, code: KeyCode) -> bool {
match code {
KeyCode::Char('y') | KeyCode::Enter => {
let (entry_name, global) = match &app.mode {
Mode::ConfirmDelete { entry_name , global} => (entry_name.clone(), *global),
_ => return false,
};
let path = if global {
global_config_path(&entry_name)
} else {
local_config_path(&entry_name)
};
let _ = fs::remove_file(&path);
prune_empty_parents(&path, &dir_for(global));
if let Ok(entries) = load_entries() {
app.tree = build_tree(&entries);
app.entries = entries;
let count = app.visible_count();
let new_pos = app.list_state.selected()
.map(|i| i.min(count.saturating_sub(1)));
app.list_state.select(if count == 0 { None } else { new_pos });
}
app.mode = Mode::Browse;
}
KeyCode::Char('n') | KeyCode::Esc => {
app.mode = Mode::Browse;
}
_ => {}
}
false
}
pub(super) fn handle_key_var_input(app: &mut App, key: KeyEvent) -> bool {
match key.code {
KeyCode::Enter => {
let (entry_name, var_map, action) = match &app.mode {
Mode::VarInput { entry_name, vars, action, .. } => (
entry_name.clone(),
vars.iter().map(|(k, v)| (k.clone(), v.clone())).collect::<HashMap<_, _>>(),
*action,
),
_ => return false,
};
match action {
PendingAction::Curl => app.render_curl(&entry_name, &var_map),
_ if app.is_chain_entry(&entry_name) => app.execute_chain(&entry_name, &var_map),
_ => app.execute_request(&entry_name, &var_map),
}
}
KeyCode::Tab => {
if let Mode::VarInput { vars, focused, edit, .. } = &mut app.mode {
let len = vars.len();
*focused = (*focused + 1) % len;
*edit = Edit::at_end(&vars[*focused].1);
}
}
KeyCode::BackTab => {
if let Mode::VarInput { vars, focused, edit, .. } = &mut app.mode {
let len = vars.len();
*focused = focused.checked_sub(1).unwrap_or(len - 1);
*edit = Edit::at_end(&vars[*focused].1);
}
}
_ => {
let keys = app.keys;
let mut leaving = false;
if let Mode::VarInput { vars, focused, edit, .. } = &mut app.mode {
let focused = *focused;
if let Applied::Exit = edit::apply(&mut vars[focused].1, edit, key, keys) {
leaving = true;
}
}
if leaving {
app.mode = Mode::Browse;
}
}
}
false
}
pub(super) fn handle_key_test_input(app: &mut App, key: KeyEvent) -> bool {
match key.code {
KeyCode::Enter => {
let (entry_name, var_map, iterations) = match &app.mode {
Mode::TestInput { entry_name, vars, iterations, .. } => (
entry_name.clone(),
vars.iter().map(|(k, v)| (k.clone(), v.clone())).collect::<HashMap<_, _>>(),
iterations.parse::<usize>().unwrap_or(10),
),
_ => return false,
};
app.execute_test(&entry_name, &var_map, iterations);
}
KeyCode::Tab => {
if let Mode::TestInput { vars, focused, iterations, edit, .. } = &mut app.mode {
let len = vars.len() + 1;
*focused = (*focused + 1) % len;
*edit = Edit::at_end(test_field(vars, iterations, *focused));
}
}
KeyCode::BackTab => {
if let Mode::TestInput { vars, focused, iterations, edit, .. } = &mut app.mode {
let len = vars.len() + 1;
*focused = focused.checked_sub(1).unwrap_or(len - 1);
*edit = Edit::at_end(test_field(vars, iterations, *focused));
}
}
_ => {
let keys = app.keys;
let mut leaving = false;
if let Mode::TestInput { vars, focused, iterations, edit, .. } = &mut app.mode {
let focused = *focused;
let typing_non_digit = edit.insert
&& matches!(key.code, KeyCode::Char(c) if !c.is_ascii_digit());
let value = if focused < vars.len() {
Some(&mut vars[focused].1)
} else if typing_non_digit {
None
} else {
Some(iterations)
};
if let Some(value) = value
&& let Applied::Exit = edit::apply(value, edit, key, keys)
{
leaving = true;
}
}
if leaving {
app.mode = Mode::Browse;
}
}
}
false
}
fn test_field<'a>(
vars: &'a [(String, String)],
iterations: &'a str,
focused: usize,
) -> &'a str {
vars.get(focused).map_or(iterations, |(_, value)| value.as_str())
}
pub(super) fn handle_key_response(app: &mut App, code: KeyCode) -> bool {
use super::render::format_test_results_text;
use super::render::copy_to_clipboard;
let was_pending_g = app.pending_g;
app.pending_g = false;
let view_height = app.response_view_height;
if let Mode::Response { status, .. } = &mut app.mode {
*status = None;
}
match code {
KeyCode::Char('q') => return true,
KeyCode::Esc => {
let selecting = matches!(app.mode, Mode::Response { anchor: Some(_), .. });
if selecting {
if let Mode::Response { anchor, .. } = &mut app.mode {
*anchor = None;
}
} else {
app.mode = Mode::Browse;
}
}
KeyCode::Char('j') | KeyCode::Down => move_cursor(app, 1, view_height),
KeyCode::Char('k') | KeyCode::Up => move_cursor(app, -1, view_height),
KeyCode::Char('g') => {
if was_pending_g {
if let Mode::Response { scroll, cursor, .. } = &mut app.mode {
*cursor = 0;
*scroll = 0;
}
} else {
app.pending_g = true;
}
}
KeyCode::Char('G') => {
if let Mode::Response { scroll, cursor, body, response_filter, .. } = &mut app.mode {
let count = super::visible_response_lines(body, response_filter).len();
*cursor = count.saturating_sub(1);
let last = count.min(u16::MAX as usize) as u16;
*scroll = last.saturating_sub(view_height);
}
}
KeyCode::Char('V') => {
if let Mode::Response { anchor, cursor, .. } = &mut app.mode {
*anchor = match anchor {
Some(_) => None,
None => Some(*cursor),
};
}
}
KeyCode::Char('y') => {
let picked = match &app.mode {
Mode::Response { body, response_filter, cursor, anchor, .. } => {
let lines = super::visible_response_lines(body, response_filter);
let (from, to) = super::selection_range(*cursor, *anchor);
lines
.get(from..=to.min(lines.len().saturating_sub(1)))
.map(|slice| (slice.join("\n"), slice.len()))
}
_ => None,
};
let Some((text, count)) = picked else { return false };
match copy_to_clipboard(&text) {
Ok(()) => {
if let Mode::Response { anchor, status, .. } = &mut app.mode {
*anchor = None;
*status = Some(format!(
"copied {} line{}",
count,
if count == 1 { "" } else { "s" }
));
}
}
Err(e) => app.show_message(ResponseKind::Error, format!("Copy failed: {e}")),
}
}
KeyCode::Char('c') => {
let text = match &app.mode {
Mode::Response { body, response_filter, .. } => {
super::visible_response_lines(body, response_filter).join("\n")
}
Mode::TestResponse { results } => format_test_results_text(results),
_ => return false,
};
match copy_to_clipboard(&text) {
Ok(()) => {
if let Mode::Response { status, response_filter, .. } = &mut app.mode {
*status = Some(if response_filter.is_empty() {
"copied response".to_string()
} else {
"copied filtered lines".to_string()
});
}
}
Err(e) => app.show_message(ResponseKind::Error, format!("Copy failed: {e}")),
}
}
KeyCode::Char('f') => {
if let Mode::Response { response_filter_active, .. } = &mut app.mode {
*response_filter_active = true;
}
}
_ => {}
}
false
}
fn move_cursor(app: &mut App, delta: isize, view_height: u16) {
let Mode::Response { body, response_filter, cursor, scroll, .. } = &mut app.mode else {
return;
};
let count = super::visible_response_lines(body, response_filter).len();
if count == 0 {
return;
}
let last = count - 1;
*cursor = cursor.saturating_add_signed(delta).min(last);
let view = view_height.max(1) as usize;
let top = *scroll as usize;
if *cursor < top {
*scroll = *cursor as u16;
} else if *cursor >= top + view {
*scroll = (*cursor + 1 - view).min(u16::MAX as usize) as u16;
}
}