Files
zesdex/src/controller/input.rs
T

282 lines
11 KiB
Rust
Raw Normal View History

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<Action> {
// 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<Action> {
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<String> = 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(),
}
}