use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crate::app::mode; use crate::app::runtime::actions::Action; use crate::app::runtime::commands::apply_command; use crate::app::state::rest::AppStateRest; use crate::app::state::types::Overlay; use crate::controller::command::parse_command; pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec { // While Editor overlay is active, route input directly to the editor handler if state.misc.overlay == Overlay::Editor { match key.code { KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { return vec![Action::QuitConfirm]; } KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => { if let Some(ref ed) = state.misc.editor.clone() { let content = ed.as_string(); if let Err(e) = std::fs::write(&ed.path, &content) { state.push_toast(crate::app::state::types::Toast::new( crate::app::state::types::ToastKind::Error, format!("Save failed: {}", e), )); } else { state.push_toast(crate::app::state::types::Toast::new( crate::app::state::types::ToastKind::Success, format!("Saved {}", ed.path), )); } state.dirty = true; } return vec![]; } KeyCode::Esc => { crate::app::mode::editor::handle_editor_dismiss(state); return vec![]; } KeyCode::Backspace => { if let Some(ref mut ed) = state.misc.editor { ed.delete_left(); state.dirty = true; } return vec![]; } KeyCode::Enter => { crate::app::mode::editor::handle_editor_input(state, "\n".to_string()); return vec![]; } KeyCode::Char(c) => { crate::app::mode::editor::handle_editor_input(state, c.to_string()); return vec![]; } _ => return vec![], } } match key.code { KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { vec![Action::QuitConfirm] } KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => { vec![Action::CloseOverlay] } KeyCode::Enter => { if state.input.autocomplete_visible { state.input.select_autocomplete(); state.dirty = true; return Vec::new(); } if state.misc.overlay.is_active() { return handle_overlay_enter(state); } let text = state.input.buffer.clone(); if text.starts_with('/') { return apply_command(parse_command(&text)); } vec![Action::SubmitInput(text)] } KeyCode::Backspace => { if state.input.autocomplete_visible { state.input.close_autocomplete(); state.dirty = true; return Vec::new(); } vec![Action::DeleteChar] } KeyCode::Delete => { if state.input.autocomplete_visible { state.input.close_autocomplete(); state.dirty = true; return Vec::new(); } vec![Action::DeleteCharRight] } KeyCode::Left => { vec![Action::CursorLeft] } KeyCode::Right => { vec![Action::CursorRight] } KeyCode::Up => { if state.input.autocomplete_visible { state.input.cycle_autocomplete(false); state.dirty = true; Vec::new() } else if key.modifiers.contains(KeyModifiers::CONTROL) { vec![Action::HistoryUp] } else if state.misc.overlay == Overlay::Effort { mode::effort::cycle_effort(state); Vec::new() } else if state.misc.overlay == Overlay::Rewind { let n = mode::rewind::rewind_count(state); state.misc.selected_index = if state.misc.selected_index == 0 { n.saturating_sub(1) } else { state.misc.selected_index - 1 }; state.dirty = true; Vec::new() } else if state.misc.overlay == Overlay::ModelSelector { let n = state.app_config.providers.len(); state.misc.selected_index = if state.misc.selected_index == 0 { n.saturating_sub(1) } else { state.misc.selected_index - 1 }; state.dirty = true; Vec::new() } else { vec![Action::ScrollUp] } } KeyCode::Down => { if state.input.autocomplete_visible { state.input.cycle_autocomplete(true); state.dirty = true; Vec::new() } else if key.modifiers.contains(KeyModifiers::CONTROL) { vec![Action::HistoryDown] } else if state.misc.overlay == Overlay::Effort { mode::effort::cycle_effort(state); Vec::new() } else if state.misc.overlay == Overlay::Rewind { let n = mode::rewind::rewind_count(state); state.misc.selected_index = if n == 0 { 0 } else { (state.misc.selected_index + 1) % n }; state.dirty = true; Vec::new() } else if state.misc.overlay == Overlay::ModelSelector { let n = state.app_config.providers.len(); state.misc.selected_index = if n == 0 { 0 } else { (state.misc.selected_index + 1) % n }; state.dirty = true; Vec::new() } else { vec![Action::ScrollDown] } } KeyCode::PageUp => { vec![Action::ScrollUp] } KeyCode::PageDown => { vec![Action::ScrollDown] } KeyCode::Esc => { if state.turn_in_flight() { vec![Action::AbortTurn] } else if state.input.autocomplete_visible { state.input.close_autocomplete(); state.dirty = true; Vec::new() } else if state.misc.overlay.is_active() { vec![Action::CloseOverlay] } else { Vec::new() } } KeyCode::Tab => { if state.input.buffer.starts_with('/') { if state.input.autocomplete_visible { state.input.cycle_autocomplete(true); } else { state.input.tab_complete(); } state.dirty = true; } Vec::new() } KeyCode::Char(c) => { if state.input.autocomplete_visible { state.input.close_autocomplete(); state.dirty = true; } vec![Action::InsertChar(c)] } _ => Vec::new(), } } fn handle_overlay_enter(state: &mut AppStateRest) -> Vec { match state.misc.overlay { Overlay::Bash => { let command = state.input.buffer.clone(); mode::bash::handle_bash_submit(state, command); Vec::new() } Overlay::Settings => { mode::settings::cycle_internet_mode(&mut state.settings); state.dirty = true; Vec::new() } Overlay::Todo => { mode::todo::handle_todo_toggle(state); Vec::new() } Overlay::QuitConfirm => { vec![mode::quit_confirm::handle_quit_confirm(true)] } Overlay::KeyInput => { let text = state.input.buffer.clone(); mode::key_input::handle_key_text(state, text.clone()); if text.is_empty() { state.settings.api_keys.remove(&state.settings.provider); } else { state.settings.api_keys.insert(state.settings.provider.clone(), text.clone()); } let _ = state.settings.save(); state.input.buffer.clear(); state.input.cursor = 0; state.misc.overlay = Overlay::None; state.push_toast(crate::app::state::types::Toast::new( crate::app::state::types::ToastKind::Success, "API key saved".to_string(), )); state.dirty = true; Vec::new() } Overlay::Mcp => { mode::mcp::connect_mcp(state, ""); Vec::new() } Overlay::Rewind => { let idx = state.misc.selected_index; mode::rewind::rewind_to(state, idx); Vec::new() } Overlay::ModelSelector => { let providers: Vec = state.app_config.providers.keys().cloned().collect(); if let Some(provider) = providers.get(state.misc.selected_index) { if let Some(cfg) = state.app_config.providers.get(provider) { let model = cfg.default_model.clone().unwrap_or_else(|| { tracing::warn!("[input] provider '{}' has no default_model, using 'claude-opus-4-8'", provider); "claude-opus-4-8".to_string() }); state.settings.provider = provider.clone(); state.settings.model = model.clone(); if let Some(ref key) = cfg.default_api_key { state.settings.api_keys.insert(provider.clone(), key.clone()); } else if let Some(env_key) = cfg.api_key_env.as_ref().and_then(|env| std::env::var(env).ok()) { state.settings.api_keys.insert(provider.clone(), env_key); } let _ = state.settings.save(); state.push_toast(crate::app::state::types::Toast::new( crate::app::state::types::ToastKind::Success, format!("Switched to {} / {}", provider, model), )); } } state.misc.overlay = Overlay::None; state.dirty = true; Vec::new() } Overlay::ClearConfirm => { state.push_toast(crate::app::state::types::Toast::new( crate::app::state::types::ToastKind::Info, "Transcript cleared".to_string(), )); state.misc.overlay = Overlay::None; state.dirty = true; Vec::new() } _ => Vec::new(), } }