use crate::core::app::ui_state::UiMode;
use crate::core::app::AppActionDispatcher;
use crate::ui::chat_loop::{AppHandle, KeyLoopAction};
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub enum KeyResult {
Continue,
Exit,
Handled,
NotHandled,
}
impl From<KeyLoopAction> for KeyResult {
fn from(action: KeyLoopAction) -> Self {
match action {
KeyLoopAction::Continue => KeyResult::Continue,
KeyLoopAction::Break => KeyResult::Exit,
}
}
}
impl From<bool> for KeyResult {
fn from(handled: bool) -> Self {
if handled {
KeyResult::Handled
} else {
KeyResult::NotHandled
}
}
}
#[async_trait::async_trait]
pub trait KeyHandler: Send + Sync {
async fn handle(
&self,
app: &AppHandle,
dispatcher: &AppActionDispatcher,
key: &KeyEvent,
term_width: u16,
term_height: u16,
last_input_layout_update: Option<std::time::Instant>,
) -> KeyResult;
}
pub struct KeyExecutionContext<'a> {
pub app: &'a AppHandle,
pub dispatcher: &'a AppActionDispatcher,
}
#[derive(Debug, Clone, Copy)]
pub struct KeyHandlingContext {
pub term_width: u16,
pub term_height: u16,
pub last_input_layout_update: Option<std::time::Instant>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct KeyPattern {
pub code: KeyCode,
pub modifiers: KeyModifiers,
}
impl KeyPattern {
pub fn simple(code: KeyCode) -> Self {
Self {
code,
modifiers: KeyModifiers::NONE,
}
}
pub fn ctrl(code: KeyCode) -> Self {
Self {
code,
modifiers: KeyModifiers::CONTROL,
}
}
pub fn with_modifiers(code: KeyCode, modifiers: KeyModifiers) -> Self {
Self { code, modifiers }
}
pub fn any() -> Self {
Self {
code: KeyCode::Null, modifiers: KeyModifiers::ALT, }
}
pub fn matches(&self, key: &KeyEvent) -> bool {
if self.code == KeyCode::Null && self.modifiers == KeyModifiers::ALT {
return true;
}
self.code == key.code && self.modifiers == key.modifiers
}
}
impl From<&KeyEvent> for KeyPattern {
fn from(key: &KeyEvent) -> Self {
Self {
code: key.code,
modifiers: key.modifiers,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum KeyContext {
Typing,
EditSelect,
BlockSelect,
InPlaceEdit,
FilePrompt,
ToolPrompt,
McpPromptInput,
Picker,
}
impl KeyContext {
pub fn from_ui_mode(ui_mode: &UiMode, picker_open: bool) -> Self {
if picker_open {
return KeyContext::Picker;
}
match ui_mode {
UiMode::Typing => KeyContext::Typing,
UiMode::EditSelect { .. } => KeyContext::EditSelect,
UiMode::BlockSelect { .. } => KeyContext::BlockSelect,
UiMode::InPlaceEdit { .. } => KeyContext::InPlaceEdit,
UiMode::FilePrompt(_) => KeyContext::FilePrompt,
UiMode::ToolPrompt(_) => KeyContext::ToolPrompt,
UiMode::McpPromptInput(_) => KeyContext::McpPromptInput,
}
}
}
pub struct ModeAwareRegistry {
handlers: HashMap<KeyContext, HashMap<KeyPattern, Box<dyn KeyHandler>>>,
}
impl ModeAwareRegistry {
pub fn new() -> Self {
Self {
handlers: HashMap::new(),
}
}
pub fn register_for_context(
&mut self,
context: KeyContext,
pattern: KeyPattern,
handler: Box<dyn KeyHandler>,
) {
self.handlers
.entry(context)
.or_default()
.insert(pattern, handler);
}
pub fn should_handle_as_text_input(&self, key: &KeyEvent, context: &KeyContext) -> bool {
match context {
KeyContext::Typing => {
if let KeyCode::Char(c) = key.code {
if key.modifiers.contains(KeyModifiers::CONTROL) {
return !matches!(
c,
'c' | 'l'
| 'd'
| 'b'
| 'p'
| 'j'
| 'r'
| 't'
| 'a'
| 'e'
| 'n'
| 'o'
| 'x'
);
}
return true;
}
false
}
KeyContext::ToolPrompt => false,
KeyContext::McpPromptInput => {
match key.code {
KeyCode::Esc => false,
KeyCode::Enter => false,
KeyCode::Char(c) if key.modifiers.contains(KeyModifiers::CONTROL) => !matches!(
c,
'b' | 'p' | 'j' | 'r' | 't' | 'c' | 'l' | 'd' | 'n' | 'o' | 'x'
),
KeyCode::F(4) => false,
_ if key.modifiers.contains(KeyModifiers::ALT)
&& key.code == KeyCode::Enter =>
{
false
}
_ => true,
}
}
KeyContext::InPlaceEdit => {
match key.code {
KeyCode::Left
| KeyCode::Right
| KeyCode::Up
| KeyCode::Down
| KeyCode::Home
| KeyCode::End
| KeyCode::PageUp
| KeyCode::PageDown => false,
KeyCode::Esc => false,
KeyCode::Enter => false,
KeyCode::Char(c) if key.modifiers.contains(KeyModifiers::CONTROL) => !matches!(
c,
'b' | 'p' | 'j' | 'r' | 't' | 'c' | 'l' | 'd' | 'n' | 'o' | 'x'
),
KeyCode::F(4) => false,
_ if key.modifiers.contains(KeyModifiers::ALT)
&& key.code == KeyCode::Enter =>
{
false
}
_ => true,
}
}
KeyContext::FilePrompt => {
match key.code {
KeyCode::Esc => false,
KeyCode::Enter => false,
KeyCode::Char(c) if key.modifiers.contains(KeyModifiers::CONTROL) => {
!matches!(c, 'b' | 'p' | 'j' | 'r' | 't' | 'c' | 'l' | 'd' | 'n' | 'x')
}
KeyCode::F(4) => false,
_ if key.modifiers.contains(KeyModifiers::ALT)
&& key.code == KeyCode::Enter =>
{
false
}
_ => true,
}
}
_ => false,
}
}
pub async fn handle_key_event(
&self,
key: &KeyEvent,
context: KeyContext,
execution: KeyExecutionContext<'_>,
handling: KeyHandlingContext,
) -> ModeAwareResult {
if let Some(context_handlers) = self.handlers.get(&context) {
for (pattern, handler) in context_handlers {
if pattern.matches(key) && !is_wildcard_pattern(pattern) {
let result = handler
.handle(
execution.app,
execution.dispatcher,
key,
handling.term_width,
handling.term_height,
handling.last_input_layout_update,
)
.await;
if result != KeyResult::NotHandled {
return ModeAwareResult {
result,
updated_layout_time: handling.last_input_layout_update,
};
}
}
}
for (pattern, handler) in context_handlers {
if pattern.matches(key) && is_wildcard_pattern(pattern) {
let result = handler
.handle(
execution.app,
execution.dispatcher,
key,
handling.term_width,
handling.term_height,
handling.last_input_layout_update,
)
.await;
if result != KeyResult::NotHandled {
return ModeAwareResult {
result,
updated_layout_time: handling.last_input_layout_update,
};
}
}
}
}
ModeAwareResult {
result: KeyResult::NotHandled,
updated_layout_time: None,
}
}
}
pub struct ModeAwareResult {
pub result: KeyResult,
pub updated_layout_time: Option<std::time::Instant>,
}
fn is_wildcard_pattern(pattern: &KeyPattern) -> bool {
pattern.code == KeyCode::Null
}
impl Default for ModeAwareRegistry {
fn default() -> Self {
Self::new()
}
}
pub struct ModeAwareBuilder {
registry: ModeAwareRegistry,
}
impl ModeAwareBuilder {
pub fn new() -> Self {
Self {
registry: ModeAwareRegistry::new(),
}
}
pub fn build(self) -> ModeAwareRegistry {
self.registry
}
pub fn register_for_context(
mut self,
context: KeyContext,
pattern: KeyPattern,
handler: Box<dyn KeyHandler>,
) -> Self {
self.registry
.register_for_context(context, pattern, handler);
self
}
}
impl Default for ModeAwareBuilder {
fn default() -> Self {
Self::new()
}
}