aura-gui 0.3.5

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! Tabla de atajos con contexto y secuencias de varias teclas.

use crate::core::Action;
use crate::view::key_context::{ContextPredicate, KeyContext, PredicateError};
use std::collections::HashMap;

#[derive(Clone, Debug)]
pub struct Binding {
    pub keystrokes: Vec<String>,
    /// `None` desactiva un atajo heredado (`"mod-p": null` en keymap.json).
    pub action: Option<Action>,
    pub context: Option<ContextPredicate>,
    /// El predicado tal como se escribió, para mostrarlo y volver a guardarlo.
    pub context_source: Option<String>,
}

#[derive(Debug, PartialEq, Eq)]
pub enum KeymapMatch {
    /// La secuencia completa coincide con un atajo activo.
    Action(Action),
    /// La secuencia es prefijo de algún atajo: hay que esperar la siguiente tecla.
    Pending,
    None,
}

#[derive(Default)]
pub struct Keymap {
    bindings: Vec<Binding>,
    known_actions: HashMap<&'static str, Action>,
}

impl Keymap {
    /// Los atajos añadidos después tienen prioridad en caso de empate.
    pub fn bind(
        &mut self,
        keystrokes: &str,
        action: Option<Action>,
        context: Option<&str>,
    ) -> Result<(), PredicateError> {
        let context_source = context.map(str::to_owned);
        let context = context.map(ContextPredicate::parse).transpose()?;
        let keystrokes = keystrokes.split_whitespace().map(str::to_owned).collect();
        self.bindings.push(Binding { keystrokes, action, context, context_source });
        Ok(())
    }

    pub fn register_actions(&mut self, actions: &[Action]) {
        for action in actions {
            self.known_actions.insert(action.0, *action);
        }
    }

    pub fn action_by_name(&self, name: &str) -> Option<Action> {
        self.known_actions.get(name).copied()
    }

    /// Todas las acciones conocidas, ordenadas por nombre (para el editor de atajos).
    pub fn known_actions(&self) -> Vec<Action> {
        let mut actions: Vec<Action> = self.known_actions.values().copied().collect();
        actions.sort_by_key(|action| action.0);
        actions
    }

    /// Atajos vigentes de una acción: `(teclas, contexto)`. Se descartan los que un atajo posterior con las mismas
    /// teclas y contexto anula o reasigna.
    pub fn bindings_for(&self, action: Action) -> Vec<(String, Option<String>)> {
        let mut found: Vec<(String, Option<String>)> = Vec::new();
        for (index, binding) in self.bindings.iter().enumerate() {
            if binding.action != Some(action) {
                continue;
            }
            let overridden = self.bindings[index + 1..].iter().any(|later| {
                later.keystrokes == binding.keystrokes
                    && later.context_source == binding.context_source
                    && later.action != Some(action)
            });
            let entry = (binding.keystrokes.join(" "), binding.context_source.clone());
            if !overridden && !found.contains(&entry) {
                found.push(entry);
            }
        }
        found
    }

    /// Profundidad máxima en que el contexto del atajo coincide; `Some(0)` sin contexto.
    fn match_depth(binding: &Binding, contexts: &[KeyContext]) -> Option<usize> {
        match &binding.context {
            None => Some(0),
            Some(predicate) => (0..contexts.len()).rev().find(|depth| predicate.matches(contexts, *depth)),
        }
    }

    pub fn resolve(&self, sequence: &[String], contexts: &[KeyContext]) -> KeymapMatch {
        let mut best: Option<(usize, usize)> = None;
        let mut pending = false;
        for (index, binding) in self.bindings.iter().enumerate() {
            let is_prefix = binding.keystrokes.len() > sequence.len() && binding.keystrokes.starts_with(sequence);
            if binding.keystrokes != sequence && !is_prefix {
                continue;
            }
            let Some(depth) = Self::match_depth(binding, contexts) else { continue };
            if is_prefix {
                pending |= binding.action.is_some();
            } else if best.is_none_or(|(best_depth, _)| depth >= best_depth) {
                best = Some((depth, index));
            }
        }
        if pending {
            return KeymapMatch::Pending;
        }
        match best.and_then(|(_, index)| self.bindings[index].action) {
            Some(action) => KeymapMatch::Action(action),
            None => KeymapMatch::None,
        }
    }

    /// Cómo puede seguir un atajo incompleto en estos contextos: `(teclas que faltan, acción)`, sin repetidos y
    /// quedándose con el atajo más específico cuando varios usan las mismas teclas.
    pub fn continuations(&self, sequence: &[String], contexts: &[KeyContext]) -> Vec<(String, Action)> {
        let mut best: Vec<(Vec<String>, usize, Option<Action>)> = Vec::new();
        for binding in &self.bindings {
            if binding.keystrokes.len() <= sequence.len() || !binding.keystrokes.starts_with(sequence) {
                continue;
            }
            let Some(depth) = Self::match_depth(binding, contexts) else { continue };
            let rest = binding.keystrokes[sequence.len()..].to_vec();
            match best.iter_mut().find(|(keys, _, _)| *keys == rest) {
                Some(entry) if depth >= entry.1 => *entry = (rest, depth, binding.action),
                Some(_) => {}
                None => best.push((rest, depth, binding.action)),
            }
        }
        best.into_iter().filter_map(|(keys, _, action)| Some((keys.join(" "), action?))).collect()
    }

    /// Atajos de una acción (para mostrarlos en menús y en la paleta).
    pub fn keystrokes_for(&self, action: Action) -> Option<String> {
        self.bindings
            .iter()
            .rev()
            .find(|binding| binding.action == Some(action))
            .map(|binding| binding.keystrokes.join(" "))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const SAVE: Action = Action("workspace::Save");
    const PICKER_CONFIRM: Action = Action("picker::Confirm");
    const NEWLINE: Action = Action("editor::Newline");

    fn keys(source: &str) -> Vec<String> {
        source.split_whitespace().map(str::to_owned).collect()
    }

    fn contexts(levels: &[&str]) -> Vec<KeyContext> {
        levels.iter().map(|level| KeyContext::parse(level)).collect()
    }

    #[test]
    fn deeper_context_wins() {
        let mut keymap = Keymap::default();
        keymap.bind("enter", Some(NEWLINE), Some("Editor")).expect("válido");
        keymap.bind("enter", Some(PICKER_CONFIRM), Some("Picker")).expect("válido");
        let stack = contexts(&["Workspace", "Editor", "Picker"]);
        assert_eq!(keymap.resolve(&keys("enter"), &stack), KeymapMatch::Action(PICKER_CONFIRM));
        assert_eq!(keymap.resolve(&keys("enter"), &stack[..2]), KeymapMatch::Action(NEWLINE));
    }

    #[test]
    fn sequences_wait_for_the_next_keystroke() {
        let mut keymap = Keymap::default();
        keymap.bind("mod-k mod-s", Some(SAVE), None).expect("válido");
        let stack = contexts(&["Workspace"]);
        assert_eq!(keymap.resolve(&keys("mod-k"), &stack), KeymapMatch::Pending);
        let next = keymap.continuations(&keys("mod-k"), &stack);
        assert!(!next.is_empty() && next.iter().all(|(rest, _)| !rest.is_empty()), "{next:?}");
        assert_eq!(keymap.resolve(&keys("mod-k mod-s"), &stack), KeymapMatch::Action(SAVE));
    }

    #[test]
    fn bindings_for_skips_overridden_ones() {
        let mut keymap = Keymap::default();
        keymap.bind("mod-s", Some(SAVE), Some("Workspace")).expect("válido");
        keymap.bind("ctrl-s", Some(SAVE), Some("Editor")).expect("válido");
        keymap.bind("mod-s", None, Some("Workspace")).expect("válido");
        assert_eq!(keymap.bindings_for(SAVE), vec![("ctrl-s".to_owned(), Some("Editor".to_owned()))]);
    }

    #[test]
    fn null_bindings_disable_inherited_ones() {
        let mut keymap = Keymap::default();
        keymap.bind("mod-s", Some(SAVE), None).expect("válido");
        keymap.bind("mod-s", None, None).expect("válido");
        assert_eq!(keymap.resolve(&keys("mod-s"), &contexts(&["Workspace"])), KeymapMatch::None);
    }
}