use crossterm::event::{KeyCode, KeyModifiers};
use super::mode::InputMode;
use super::views::TuiView;
#[derive(Debug, Clone)]
pub struct Keybinding {
pub code: KeyCode,
pub modifiers: KeyModifiers,
pub description: &'static str,
pub category: KeyCategory,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyCategory {
Global,
ViewNav,
PanelNav,
Mode,
Scroll,
Action,
Chat,
Monitor,
}
impl KeyCategory {
pub fn label(&self) -> &'static str {
match self {
KeyCategory::Global => "Global",
KeyCategory::ViewNav => "View Navigation",
KeyCategory::PanelNav => "Panel Focus",
KeyCategory::Mode => "Mode",
KeyCategory::Scroll => "Scroll",
KeyCategory::Action => "Actions",
KeyCategory::Chat => "Chat",
KeyCategory::Monitor => "Monitor",
}
}
}
pub fn keybindings_for_context(view: TuiView, mode: InputMode) -> Vec<Keybinding> {
let mut bindings = Vec::new();
bindings.push(Keybinding {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
description: "Copy / Quit (2x)",
category: KeyCategory::Global,
});
bindings.push(Keybinding {
code: KeyCode::Char('?'),
modifiers: KeyModifiers::NONE,
description: "Help",
category: KeyCategory::Global,
});
if mode == InputMode::Normal {
bindings.push(Keybinding {
code: KeyCode::Char('1'),
modifiers: KeyModifiers::NONE,
description: "Studio (Browser+Editor+DAG)",
category: KeyCategory::ViewNav,
});
bindings.push(Keybinding {
code: KeyCode::Char('2'),
modifiers: KeyModifiers::NONE,
description: "Command view",
category: KeyCategory::ViewNav,
});
bindings.push(Keybinding {
code: KeyCode::Char('3'),
modifiers: KeyModifiers::NONE,
description: "Control view",
category: KeyCategory::ViewNav,
});
}
bindings.push(Keybinding {
code: KeyCode::Tab,
modifiers: KeyModifiers::NONE,
description: "Next panel",
category: KeyCategory::PanelNav,
});
bindings.push(Keybinding {
code: KeyCode::BackTab,
modifiers: KeyModifiers::SHIFT,
description: "Previous panel",
category: KeyCategory::PanelNav,
});
if view == TuiView::Command && mode == InputMode::Normal {
bindings.push(Keybinding {
code: KeyCode::Char('i'),
modifiers: KeyModifiers::NONE,
description: "Enter Insert mode",
category: KeyCategory::Mode,
});
}
if mode == InputMode::Insert {
bindings.push(Keybinding {
code: KeyCode::Esc,
modifiers: KeyModifiers::NONE,
description: "Return to Normal mode",
category: KeyCategory::Mode,
});
}
if mode == InputMode::Normal {
bindings.push(Keybinding {
code: KeyCode::Char('j'),
modifiers: KeyModifiers::NONE,
description: "Scroll down",
category: KeyCategory::Scroll,
});
bindings.push(Keybinding {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::NONE,
description: "Scroll up",
category: KeyCategory::Scroll,
});
bindings.push(Keybinding {
code: KeyCode::Char('g'),
modifiers: KeyModifiers::NONE,
description: "Go to top",
category: KeyCategory::Scroll,
});
bindings.push(Keybinding {
code: KeyCode::Char('G'),
modifiers: KeyModifiers::SHIFT,
description: "Go to bottom",
category: KeyCategory::Scroll,
});
}
match view {
TuiView::Command => {
if mode == InputMode::Insert {
bindings.push(Keybinding {
code: KeyCode::Enter,
modifiers: KeyModifiers::NONE,
description: "Send message",
category: KeyCategory::Chat,
});
bindings.push(Keybinding {
code: KeyCode::Char('/'),
modifiers: KeyModifiers::NONE,
description: "Slash commands (at start)",
category: KeyCategory::Chat,
});
bindings.push(Keybinding {
code: KeyCode::Char('@'),
modifiers: KeyModifiers::NONE,
description: "File mention (@filename)",
category: KeyCategory::Chat,
});
}
bindings.push(Keybinding {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
description: "Command palette",
category: KeyCategory::Chat,
});
bindings.push(Keybinding {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
description: "Provider Modal",
category: KeyCategory::Chat,
});
bindings.push(Keybinding {
code: KeyCode::Char('t'),
modifiers: KeyModifiers::CONTROL,
description: "Toggle deep thinking",
category: KeyCategory::Chat,
});
bindings.push(Keybinding {
code: KeyCode::Char('m'),
modifiers: KeyModifiers::CONTROL,
description: "Toggle Infer/Agent mode",
category: KeyCategory::Chat,
});
bindings.push(Keybinding {
code: KeyCode::Char('f'),
modifiers: KeyModifiers::CONTROL,
description: "Search conversation",
category: KeyCategory::Chat,
});
bindings.push(Keybinding {
code: KeyCode::Char('r'),
modifiers: KeyModifiers::CONTROL,
description: "Retry failed MCP",
category: KeyCategory::Chat,
});
bindings.push(Keybinding {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::CONTROL,
description: "Cursor to start",
category: KeyCategory::Chat,
});
bindings.push(Keybinding {
code: KeyCode::Char('e'),
modifiers: KeyModifiers::CONTROL,
description: "Cursor to end",
category: KeyCategory::Chat,
});
bindings.push(Keybinding {
code: KeyCode::Left,
modifiers: KeyModifiers::CONTROL,
description: "Word left",
category: KeyCategory::Chat,
});
bindings.push(Keybinding {
code: KeyCode::Right,
modifiers: KeyModifiers::CONTROL,
description: "Word right",
category: KeyCategory::Chat,
});
bindings.push(Keybinding {
code: KeyCode::Backspace,
modifiers: KeyModifiers::CONTROL,
description: "Delete word",
category: KeyCategory::Chat,
});
bindings.push(Keybinding {
code: KeyCode::Char('v'),
modifiers: KeyModifiers::CONTROL,
description: "Paste from clipboard",
category: KeyCategory::Chat,
});
if mode == InputMode::Normal {
bindings.push(Keybinding {
code: KeyCode::Char(' '),
modifiers: KeyModifiers::NONE,
description: "Toggle pause",
category: KeyCategory::Monitor,
});
}
}
TuiView::Studio => {
if mode == InputMode::Normal {
bindings.push(Keybinding {
code: KeyCode::Enter,
modifiers: KeyModifiers::NONE,
description: "Open file",
category: KeyCategory::Action,
});
bindings.push(Keybinding {
code: KeyCode::F(5),
modifiers: KeyModifiers::NONE,
description: "Run workflow",
category: KeyCategory::Action,
});
bindings.push(Keybinding {
code: KeyCode::Char('/'),
modifiers: KeyModifiers::NONE,
description: "Fuzzy search",
category: KeyCategory::Action,
});
bindings.push(Keybinding {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
description: "Fuzzy search (Ctrl+P)",
category: KeyCategory::Action,
});
bindings.push(Keybinding {
code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL,
description: "Save file",
category: KeyCategory::Action,
});
}
}
TuiView::Control => {
}
}
bindings
}
pub fn format_key(code: KeyCode, modifiers: KeyModifiers) -> String {
let mut parts = Vec::new();
if modifiers.contains(KeyModifiers::CONTROL) {
parts.push("Ctrl");
}
if modifiers.contains(KeyModifiers::ALT) {
parts.push("Alt");
}
if modifiers.contains(KeyModifiers::SHIFT) {
parts.push("Shift");
}
let key = match code {
KeyCode::Char(' ') => "Space".to_string(),
KeyCode::Char(c) => c.to_string(),
KeyCode::Enter => "Enter".to_string(),
KeyCode::Esc => "Esc".to_string(),
KeyCode::Tab => "Tab".to_string(),
KeyCode::BackTab => "Tab".to_string(), KeyCode::Backspace => "Backspace".to_string(),
KeyCode::Delete => "Del".to_string(),
KeyCode::Up => "↑".to_string(),
KeyCode::Down => "↓".to_string(),
KeyCode::Left => "←".to_string(),
KeyCode::Right => "→".to_string(),
KeyCode::Home => "Home".to_string(),
KeyCode::End => "End".to_string(),
KeyCode::PageUp => "PgUp".to_string(),
KeyCode::PageDown => "PgDn".to_string(),
KeyCode::F(n) => format!("F{}", n),
_ => "?".to_string(),
};
parts.push(&key);
parts.join("+")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_keybindings_for_chat_normal() {
let bindings = keybindings_for_context(TuiView::Command, InputMode::Normal);
assert!(bindings.iter().any(|b| b.code == KeyCode::Char('i')));
assert!(bindings
.iter()
.any(|b| b.code == KeyCode::Char('c') && b.modifiers == KeyModifiers::CONTROL));
assert!(bindings
.iter()
.any(|b| b.code == KeyCode::Char('k') && b.modifiers == KeyModifiers::CONTROL));
assert!(bindings
.iter()
.any(|b| b.code == KeyCode::Char('t') && b.modifiers == KeyModifiers::CONTROL));
assert!(bindings
.iter()
.any(|b| b.code == KeyCode::Char('m') && b.modifiers == KeyModifiers::CONTROL));
}
#[test]
fn test_keybindings_for_chat_insert() {
let bindings = keybindings_for_context(TuiView::Command, InputMode::Insert);
assert!(bindings.iter().any(|b| b.code == KeyCode::Esc));
assert!(bindings.iter().any(|b| b.code == KeyCode::Enter));
assert!(bindings
.iter()
.any(|b| b.code == KeyCode::Char('k') && b.modifiers == KeyModifiers::CONTROL));
}
#[test]
fn test_format_key_simple() {
assert_eq!(format_key(KeyCode::Char('q'), KeyModifiers::NONE), "q");
assert_eq!(format_key(KeyCode::Enter, KeyModifiers::NONE), "Enter");
}
#[test]
fn test_format_key_with_modifiers() {
assert_eq!(
format_key(KeyCode::Char('c'), KeyModifiers::CONTROL),
"Ctrl+c"
);
assert_eq!(format_key(KeyCode::Tab, KeyModifiers::SHIFT), "Shift+Tab");
}
#[test]
fn test_key_category_labels() {
assert_eq!(KeyCategory::Global.label(), "Global");
assert_eq!(KeyCategory::Mode.label(), "Mode");
assert_eq!(KeyCategory::ViewNav.label(), "View Navigation");
}
}