use std::{collections::HashMap, path::Path};
use crate::prelude::*;
use commands::CommandId;
use input::KeyEvent;
use shortcuts::parser::parse_key_combo;
pub struct ShortcutRegistry {
pub(crate) global_shortcut_file: std::path::PathBuf,
pub(crate) project_shortcut_file: std::path::PathBuf,
pub(crate) shortcuts: HashMap<KeyEvent, Vec<CommandId>>,
}
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct ShortcutYaml {
shortcut: String,
command: String,
}
impl ShortcutRegistry {
pub fn new(project_settings_path: &Path) -> Result<Self> {
let mut registry = Self {
global_shortcut_file: directories::ProjectDirs::from("com", "cyloncore", "oo")
.ok_or_else(|| anyhow!("Failed to retrieve project dir."))?
.config_dir()
.join("shortcuts.yaml"),
project_shortcut_file: project_settings_path.join("shortcuts.yaml"),
shortcuts: HashMap::new(),
};
registry.load_shortcuts()?;
Ok(registry)
}
fn register(&mut self, key_combo: KeyEvent, command_id: CommandId) {
self.shortcuts
.entry(key_combo)
.or_default()
.push(command_id)
}
pub fn find_commands(&self, combo: &KeyEvent) -> Option<&Vec<CommandId>> {
log::debug!(
"Looking for shortcut for {combo:?} in {:?} {:?}",
self.shortcuts,
self.shortcuts.contains_key(combo)
);
self.shortcuts.get(combo)
}
pub fn load_shortcuts(&mut self) -> Result<()> {
self.shortcuts = Default::default();
self.load_yaml(include_str!("../../data/default_shortcuts.yaml"))?;
if self.global_shortcut_file.exists() {
let data = std::fs::read_to_string(&self.global_shortcut_file)?;
self.load_yaml(&data)?;
} else {
log::info!("No global shortcuts file {:?}", self.global_shortcut_file);
}
if self.project_shortcut_file.exists() {
let data = std::fs::read_to_string(&self.project_shortcut_file)?;
self.load_yaml(&data)?;
} else {
log::info!("No project shortcuts file {:?}", self.project_shortcut_file);
}
Ok(())
}
pub fn load_yaml(&mut self, content: &str) -> Result<()> {
let shortcuts_yaml: Vec<ShortcutYaml> = serde_saphyr::from_str(content)?;
for s in shortcuts_yaml {
let key_combo = parse_key_combo(&s.shortcut)?;
self.register(key_combo, s.command.parse()?);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::input::{Key, Modifiers};
fn create_test_registry() -> (ShortcutRegistry, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let project_config = dir.path().join("shortcuts.yaml");
let registry = ShortcutRegistry {
global_shortcut_file: dir.path().join("global_shortcuts.yaml"),
project_shortcut_file: project_config,
shortcuts: HashMap::new(),
};
(registry, dir)
}
#[test]
fn test_register_single_shortcut() {
let (mut registry, _dir) = create_test_registry();
let key_event = KeyEvent {
modifiers: Modifiers::CTRL,
key: Key::Char('s'),
};
let command_id: CommandId = "editor.save".parse().unwrap();
registry.register(key_event, command_id);
assert_eq!(registry.shortcuts.len(), 1);
assert!(registry.shortcuts.contains_key(&key_event));
}
#[test]
fn test_find_commands_found() {
let (mut registry, _dir) = create_test_registry();
let key_event = KeyEvent {
modifiers: Modifiers::CTRL,
key: Key::Char('s'),
};
let command_id: CommandId = "editor.save".parse().unwrap();
registry.register(key_event, command_id.clone());
let found = registry.find_commands(&key_event);
assert!(found.is_some());
assert_eq!(found.unwrap().len(), 1);
assert_eq!(found.unwrap()[0], command_id);
}
#[test]
fn test_find_commands_not_found() {
let (registry, _dir) = create_test_registry();
let key_event = KeyEvent {
modifiers: Modifiers::CTRL,
key: Key::Char('x'),
};
let found = registry.find_commands(&key_event);
assert!(found.is_none());
}
#[test]
fn test_multiple_commands_for_same_shortcut() {
let (mut registry, _dir) = create_test_registry();
let key_event = KeyEvent {
modifiers: Modifiers::empty(),
key: Key::F(1),
};
let cmd1: CommandId = "group.cmd1".parse().unwrap();
let cmd2: CommandId = "group.cmd2".parse().unwrap();
registry.register(key_event, cmd1);
registry.register(key_event, cmd2);
let found = registry.find_commands(&key_event).unwrap();
assert_eq!(found.len(), 2);
}
#[test]
fn test_load_yaml_simple() {
let (mut registry, _dir) = create_test_registry();
let yaml = r#"
- shortcut: "Ctrl+s"
command: "editor.save"
- shortcut: "Ctrl+o"
command: "editor.open"
"#;
registry.load_yaml(yaml).unwrap();
assert_eq!(registry.shortcuts.len(), 2);
}
#[test]
fn test_load_yaml_with_all_modifier_combinations() {
let (mut registry, _dir) = create_test_registry();
let yaml = r#"
- shortcut: "a"
command: "cmd.a"
- shortcut: "Ctrl+b"
command: "cmd.b"
- shortcut: "Shift+c"
command: "cmd.c"
- shortcut: "Alt+d"
command: "cmd.d"
- shortcut: "Ctrl+Shift+e"
command: "cmd.e"
"#;
registry.load_yaml(yaml).unwrap();
assert_eq!(registry.shortcuts.len(), 5);
}
#[test]
fn test_load_yaml_replaces_existing() {
let (mut registry, _dir) = create_test_registry();
let yaml1 = r#"
- shortcut: "Ctrl+a"
command: "cmd.a"
"#;
registry.load_yaml(yaml1).unwrap();
assert_eq!(registry.shortcuts.len(), 1);
let yaml2 = r#"
- shortcut: "Ctrl+b"
command: "cmd.b"
"#;
registry.load_yaml(yaml2).unwrap();
assert_eq!(registry.shortcuts.len(), 2);
}
#[test]
fn test_load_yaml_invalid_yaml() {
let (mut registry, _dir) = create_test_registry();
let invalid_yaml = "not a valid yaml { [ }";
let result = registry.load_yaml(invalid_yaml);
assert!(result.is_err());
}
#[test]
fn test_load_yaml_missing_shortcut_field() {
let (mut registry, _dir) = create_test_registry();
let yaml = r#"
- command: "cmd.a"
"#;
let result = registry.load_yaml(yaml);
assert!(result.is_err());
}
#[test]
fn test_load_yaml_invalid_command() {
let (mut registry, _dir) = create_test_registry();
let yaml = r#"
- shortcut: "a"
command: "!!invalid"
"#;
let result = registry.load_yaml(yaml);
assert!(result.is_err());
}
#[test]
fn test_load_yaml_with_function_keys() {
let (mut registry, _dir) = create_test_registry();
let yaml = r#"
- shortcut: "F1"
command: "help.show"
- shortcut: "F12"
command: "debug.toggle"
"#;
registry.load_yaml(yaml).unwrap();
assert_eq!(registry.shortcuts.len(), 2);
}
#[test]
fn test_load_yaml_with_special_keys() {
let (mut registry, _dir) = create_test_registry();
let yaml = r#"
- shortcut: "Enter"
command: "editor.confirm"
- shortcut: "Escape"
command: "editor.cancel"
- shortcut: "Tab"
command: "editor.indent"
- shortcut: "Backspace"
command: "editor.backspace"
- shortcut: "Delete"
command: "editor.delete"
- shortcut: "ArrowUp"
command: "editor.up"
- shortcut: "ArrowDown"
command: "editor.down"
"#;
registry.load_yaml(yaml).unwrap();
assert_eq!(registry.shortcuts.len(), 7);
}
}