2026-07-11 13:16:10 +07:00
|
|
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
|
|
|
|
|
2026-07-11 20:21:59 +07:00
|
|
|
use crate::app::mode;
|
2026-07-11 13:16:10 +07:00
|
|
|
use crate::app::runtime::actions::Action;
|
2026-07-11 18:23:01 +07:00
|
|
|
use crate::app::runtime::commands::apply_command;
|
2026-07-11 13:16:10 +07:00
|
|
|
use crate::app::state::rest::AppStateRest;
|
|
|
|
|
use crate::app::state::types::Overlay;
|
2026-07-11 18:23:01 +07:00
|
|
|
use crate::controller::command::parse_command;
|
2026-07-11 13:16:10 +07:00
|
|
|
|
2026-07-11 20:21:59 +07:00
|
|
|
pub fn handle_key(key: KeyEvent, state: &mut AppStateRest) -> Vec<Action> {
|
2026-07-12 01:25:52 +07:00
|
|
|
// 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![],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 13:16:10 +07:00
|
|
|
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.misc.overlay.is_active() {
|
|
|
|
|
return handle_overlay_enter(state);
|
|
|
|
|
}
|
|
|
|
|
let text = state.input.buffer.clone();
|
|
|
|
|
if text.starts_with('/') {
|
2026-07-11 18:23:01 +07:00
|
|
|
return apply_command(parse_command(&text));
|
2026-07-11 13:16:10 +07:00
|
|
|
}
|
|
|
|
|
vec![Action::SubmitInput(text)]
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Backspace => {
|
|
|
|
|
vec![Action::DeleteChar]
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Delete => {
|
|
|
|
|
vec![Action::DeleteCharRight]
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Left => {
|
|
|
|
|
vec![Action::CursorLeft]
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Right => {
|
|
|
|
|
vec![Action::CursorRight]
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Up => {
|
2026-07-11 22:16:47 +07:00
|
|
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
|
|
|
vec![Action::HistoryUp]
|
2026-07-12 01:25:52 +07:00
|
|
|
} else if state.misc.overlay == Overlay::Effort {
|
|
|
|
|
mode::effort::cycle_effort(state);
|
|
|
|
|
Vec::new()
|
|
|
|
|
} else if state.misc.overlay == Overlay::OnboardProvider {
|
|
|
|
|
let n = mode::onboard_provider::PROVIDERS.len();
|
|
|
|
|
state.misc.selected_index = if state.misc.selected_index == 0 { n - 1 } else { state.misc.selected_index - 1 };
|
|
|
|
|
state.dirty = true;
|
|
|
|
|
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()
|
2026-07-11 22:16:47 +07:00
|
|
|
} else {
|
|
|
|
|
vec![Action::ScrollUp]
|
|
|
|
|
}
|
2026-07-11 13:16:10 +07:00
|
|
|
}
|
|
|
|
|
KeyCode::Down => {
|
2026-07-11 22:16:47 +07:00
|
|
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
|
|
|
vec![Action::HistoryDown]
|
2026-07-12 01:25:52 +07:00
|
|
|
} else if state.misc.overlay == Overlay::Effort {
|
|
|
|
|
mode::effort::cycle_effort(state);
|
|
|
|
|
Vec::new()
|
|
|
|
|
} else if state.misc.overlay == Overlay::OnboardProvider {
|
|
|
|
|
let n = mode::onboard_provider::PROVIDERS.len();
|
|
|
|
|
state.misc.selected_index = (state.misc.selected_index + 1) % n;
|
|
|
|
|
state.dirty = true;
|
|
|
|
|
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()
|
2026-07-11 22:16:47 +07:00
|
|
|
} else {
|
|
|
|
|
vec![Action::ScrollDown]
|
|
|
|
|
}
|
2026-07-11 13:16:10 +07:00
|
|
|
}
|
|
|
|
|
KeyCode::PageUp => {
|
|
|
|
|
vec![Action::ScrollUp]
|
|
|
|
|
}
|
|
|
|
|
KeyCode::PageDown => {
|
|
|
|
|
vec![Action::ScrollDown]
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('h') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
|
|
|
vec![Action::OpenOverlay(Overlay::Help)]
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
|
|
|
vec![Action::OpenOverlay(Overlay::Settings)]
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('a') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
|
|
|
vec![Action::ToggleYoloArm]
|
|
|
|
|
}
|
2026-07-11 20:21:59 +07:00
|
|
|
KeyCode::Char('m') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
|
|
|
vec![Action::CycleAgentMode]
|
|
|
|
|
}
|
2026-07-11 13:16:10 +07:00
|
|
|
KeyCode::Char('b') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
|
|
|
vec![Action::OpenOverlay(Overlay::Bash)]
|
|
|
|
|
}
|
2026-07-11 20:21:59 +07:00
|
|
|
KeyCode::Char('S') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
|
|
|
vec![Action::OpenOverlay(Overlay::Security)]
|
2026-07-11 13:16:10 +07:00
|
|
|
}
|
|
|
|
|
KeyCode::Char('t') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
|
|
|
vec![Action::OpenOverlay(Overlay::Todo)]
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('w') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
|
|
|
vec![Action::OpenOverlay(Overlay::Workflow)]
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
|
|
|
vec![Action::OpenOverlay(Overlay::KeyInput)]
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Esc => {
|
|
|
|
|
if state.misc.overlay.is_active() {
|
|
|
|
|
vec![Action::CloseOverlay]
|
|
|
|
|
} else {
|
|
|
|
|
Vec::new()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Tab => {
|
2026-07-11 22:10:17 +07:00
|
|
|
if state.input.buffer.starts_with('/') {
|
|
|
|
|
state.input.tab_complete();
|
|
|
|
|
state.dirty = true;
|
|
|
|
|
}
|
2026-07-11 13:16:10 +07:00
|
|
|
Vec::new()
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('l') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
|
|
|
vec![Action::OpenOverlay(Overlay::Learning)]
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
|
|
|
vec![Action::OpenOverlay(Overlay::Usage)]
|
|
|
|
|
}
|
|
|
|
|
KeyCode::Char(c) => {
|
|
|
|
|
vec![Action::InsertChar(c)]
|
|
|
|
|
}
|
|
|
|
|
_ => Vec::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-11 20:21:59 +07:00
|
|
|
fn handle_overlay_enter(state: &mut AppStateRest) -> Vec<Action> {
|
2026-07-11 13:16:10 +07:00
|
|
|
match state.misc.overlay {
|
2026-07-11 20:21:59 +07:00
|
|
|
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::Security => {
|
|
|
|
|
mode::security::handle_security_action(state, &Action::ToggleYoloArm);
|
|
|
|
|
Vec::new()
|
|
|
|
|
}
|
2026-07-11 13:16:10 +07:00
|
|
|
Overlay::QuitConfirm => {
|
2026-07-11 20:21:59 +07:00
|
|
|
vec![mode::quit_confirm::handle_quit_confirm(true)]
|
2026-07-11 13:16:10 +07:00
|
|
|
}
|
2026-07-12 01:25:52 +07:00
|
|
|
Overlay::KeyInput => {
|
|
|
|
|
let text = state.input.buffer.clone();
|
|
|
|
|
mode::key_input::handle_key_text(state, text.clone());
|
|
|
|
|
state.settings.api_key = if text.is_empty() { None } else { Some(text) };
|
|
|
|
|
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::Onboard => {
|
|
|
|
|
state.misc.overlay = Overlay::OnboardProvider;
|
|
|
|
|
state.misc.selected_index = 0;
|
|
|
|
|
state.dirty = true;
|
|
|
|
|
Vec::new()
|
|
|
|
|
}
|
|
|
|
|
Overlay::OnboardProvider => {
|
|
|
|
|
let idx = state.misc.selected_index.min(mode::onboard_provider::PROVIDERS.len() - 1);
|
|
|
|
|
let provider = mode::onboard_provider::PROVIDERS[idx];
|
|
|
|
|
mode::onboard_provider::set_provider(&mut state.settings, provider);
|
|
|
|
|
state.push_toast(crate::app::state::types::Toast::new(
|
|
|
|
|
crate::app::state::types::ToastKind::Info,
|
|
|
|
|
format!("Provider set to {}", provider),
|
|
|
|
|
));
|
|
|
|
|
state.misc.overlay = Overlay::KeyInput;
|
|
|
|
|
state.input.buffer.clear();
|
|
|
|
|
state.input.cursor = 0;
|
|
|
|
|
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()
|
|
|
|
|
}
|
2026-07-11 13:16:10 +07:00
|
|
|
_ => Vec::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|