use super::keys::{parse_key_id, KeyId};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumIter, strum::Display)]
pub enum Action {
CursorLeft,
CursorRight,
CursorWordLeft,
CursorWordRight,
CursorLineStart,
CursorLineEnd,
DeleteCharBackward,
DeleteCharForward,
DeleteWordBackward,
DeleteWordForward,
DeleteToLineStart,
DeleteToLineEnd,
Undo,
Submit,
NewLine,
Tab,
CycleThinking,
ScrollUp,
ScrollDown,
ScrollPageUp,
ScrollPageDown,
HistoryUp,
HistoryDown,
Quit,
Cancel,
OpenModelSelect,
OpenProviderSetup,
ToggleRouting,
ToggleQueue,
CopyCodeBlock,
OpenImage,
CompletionNext,
CompletionPrev,
CompletionDismiss,
CompletionAccept,
}
pub struct KeybindingsManager {
defaults: HashMap<Action, Vec<KeyId>>,
user_overrides: HashMap<Action, Vec<KeyId>>,
resolved: HashMap<Action, Vec<KeyId>>,
key_to_action: HashMap<KeyId, Action>,
}
impl fmt::Debug for KeybindingsManager {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("KeybindingsManager")
.field("resolved_count", &self.resolved.len())
.field("key_to_action_count", &self.key_to_action.len())
.finish()
}
}
impl KeybindingsManager {
pub fn new() -> Self {
let mut mgr = Self {
defaults: HashMap::new(),
user_overrides: HashMap::new(),
resolved: HashMap::new(),
key_to_action: HashMap::new(),
};
mgr.init_defaults();
mgr.rebuild();
mgr
}
pub fn set_user_bindings(&mut self, config: &HashMap<String, Vec<String>>) {
self.user_overrides.clear();
for (action_name, key_strings) in config {
if let Some(action) = parse_action(action_name) {
let keys: Vec<KeyId> = key_strings.iter().filter_map(|s| parse_key_id(s)).collect();
if !keys.is_empty() {
self.user_overrides.insert(action, keys);
}
}
}
self.rebuild();
}
pub fn match_action(&self, key_id: &KeyId) -> Option<Action> {
self.key_to_action.get(key_id).copied()
}
pub fn keys_for_action(&self, action: Action) -> &[KeyId] {
self.resolved.get(&action).map_or(&[], |v| v)
}
pub fn primary_key_for(&self, action: Action) -> Option<&KeyId> {
self.resolved.get(&action).and_then(|v| v.first())
}
pub fn all_bindings(&self) -> &HashMap<Action, Vec<KeyId>> {
&self.resolved
}
fn rebuild(&mut self) {
self.resolved = self.defaults.clone();
for (action, keys) in &self.user_overrides {
self.resolved.insert(*action, keys.clone());
}
self.key_to_action.clear();
for (action, keys) in &self.resolved {
for key in keys {
self.key_to_action.entry(key.clone()).or_insert(*action);
}
}
}
fn init_defaults(&mut self) {
use Action::*;
let defaults: Vec<(Action, Vec<&str>)> = vec![
(CursorLeft, vec!["Left"]),
(CursorRight, vec!["Right"]),
(CursorWordLeft, vec!["Ctrl+Left"]),
(CursorWordRight, vec!["Ctrl+Right"]),
(CursorLineStart, vec!["Home"]),
(CursorLineEnd, vec!["End"]),
(DeleteCharBackward, vec!["Backspace"]),
(DeleteCharForward, vec!["Delete"]),
(DeleteWordBackward, vec!["Ctrl+Backspace"]),
(DeleteWordForward, vec!["Ctrl+Delete"]),
(DeleteToLineStart, vec!["Ctrl+u"]),
(DeleteToLineEnd, vec!["Ctrl+k"]),
(Undo, vec!["Ctrl+z"]),
(Submit, vec!["Enter"]),
(NewLine, vec!["Alt+Enter", "Ctrl+j"]),
(Tab, vec!["Tab"]),
(CycleThinking, vec!["BackTab"]),
(ScrollUp, vec!["Up"]),
(ScrollDown, vec!["Down"]),
(ScrollPageUp, vec!["PageUp"]),
(ScrollPageDown, vec!["PageDown"]),
(HistoryUp, vec![]), (HistoryDown, vec![]), (Quit, vec!["Ctrl+c"]),
(Cancel, vec!["Esc"]),
(OpenModelSelect, vec!["Ctrl+m"]),
(OpenProviderSetup, vec!["Ctrl+o"]),
(ToggleRouting, vec!["Ctrl+r"]),
(ToggleQueue, vec!["Ctrl+q"]),
(CopyCodeBlock, vec!["Ctrl+y"]),
(OpenImage, vec!["Ctrl+i"]),
(CompletionNext, vec![]), (CompletionPrev, vec![]),
(CompletionDismiss, vec![]), (CompletionAccept, vec![]),
];
for (action, key_strings) in defaults {
let keys: Vec<KeyId> = key_strings.into_iter().filter_map(parse_key_id).collect();
self.defaults.insert(action, keys);
}
}
}
impl Default for KeybindingsManager {
fn default() -> Self {
Self::new()
}
}
fn parse_action(s: &str) -> Option<Action> {
use Action::*;
match s.to_ascii_lowercase().as_str() {
"cursorleft" => Some(CursorLeft),
"cursorright" => Some(CursorRight),
"cursorwordleft" => Some(CursorWordLeft),
"cursorwordright" => Some(CursorWordRight),
"cursorlinestart" => Some(CursorLineStart),
"cursorlineend" => Some(CursorLineEnd),
"deletecharbackward" => Some(DeleteCharBackward),
"deletecharforward" => Some(DeleteCharForward),
"deletewordbackward" => Some(DeleteWordBackward),
"deletewordforward" => Some(DeleteWordForward),
"deletetolinestart" => Some(DeleteToLineStart),
"deletetolineend" => Some(DeleteToLineEnd),
"undo" => Some(Undo),
"submit" => Some(Submit),
"newline" => Some(NewLine),
"tab" => Some(Tab),
"cyclethinking" => Some(CycleThinking),
"scrollup" => Some(ScrollUp),
"scrolldown" => Some(ScrollDown),
"scrollpageup" => Some(ScrollPageUp),
"scrollpagedown" => Some(ScrollPageDown),
"historyup" => Some(HistoryUp),
"historydown" => Some(HistoryDown),
"quit" => Some(Quit),
"cancel" => Some(Cancel),
"openmodelselect" => Some(OpenModelSelect),
"openprovidersetup" => Some(OpenProviderSetup),
"togglerouting" => Some(ToggleRouting),
"togglequeue" => Some(ToggleQueue),
"copycodeblock" => Some(CopyCodeBlock),
"openimage" => Some(OpenImage),
"completionnext" => Some(CompletionNext),
"completionprev" => Some(CompletionPrev),
"completiondismiss" => Some(CompletionDismiss),
"completionaccept" => Some(CompletionAccept),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_bindings_exist() {
let mgr = KeybindingsManager::new();
use strum::IntoEnumIterator;
for action in Action::iter() {
let _ = mgr.keys_for_action(action);
}
}
#[test]
fn test_match_ctrl_c() {
let mgr = KeybindingsManager::new();
let key = parse_key_id("Ctrl+c").unwrap();
assert_eq!(mgr.match_action(&key), Some(Action::Quit));
}
#[test]
fn test_match_enter() {
let mgr = KeybindingsManager::new();
let key = parse_key_id("Enter").unwrap();
assert_eq!(mgr.match_action(&key), Some(Action::Submit));
}
#[test]
fn test_user_override() {
let mut mgr = KeybindingsManager::new();
let ctrl_c = parse_key_id("Ctrl+c").unwrap();
assert_eq!(mgr.match_action(&ctrl_c), Some(Action::Quit));
let mut config = HashMap::new();
config.insert("Quit".to_string(), vec!["Ctrl+x".to_string()]);
config.insert("Cancel".to_string(), vec!["Ctrl+c".to_string()]);
mgr.set_user_bindings(&config);
assert_eq!(mgr.match_action(&ctrl_c), Some(Action::Cancel));
let ctrl_x = parse_key_id("Ctrl+x").unwrap();
assert_eq!(mgr.match_action(&ctrl_x), Some(Action::Quit));
}
#[test]
fn test_parse_action() {
assert_eq!(parse_action("Quit"), Some(Action::Quit));
assert_eq!(parse_action("quit"), Some(Action::Quit));
assert_eq!(parse_action("QUIT"), Some(Action::Quit));
assert_eq!(parse_action("CursorLeft"), Some(Action::CursorLeft));
assert_eq!(parse_action("Unknown"), None);
}
#[test]
fn test_primary_key_display() {
let mgr = KeybindingsManager::new();
let key = mgr.primary_key_for(Action::Quit);
assert!(key.is_some());
assert_eq!(format!("{}", key.unwrap()), "Ctrl+c");
}
}