oo-ide 0.0.4

∞ is a terminal IDE focused on low distraction, high usability.
Documentation
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)
    }
    /// Find commands for the given combo
    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();

        // First load
        let yaml1 = r#"
- shortcut: "Ctrl+a"
  command: "cmd.a"
"#;
        registry.load_yaml(yaml1).unwrap();
        assert_eq!(registry.shortcuts.len(), 1);

        // Load again via load_shortcuts which calls load_yaml multiple times
        // For this test, directly test the merge behavior by loading twice
        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);
    }
}