tui-canvas 0.7.5

Form/textarea for TUI
Documentation
pub mod action;
pub mod builtin;
pub mod key_sequence;
pub mod preset;

use std::collections::HashMap;
use std::time::{Duration, Instant};

use crate::canvas::actions::CanvasAction;
use crate::canvas::modes::AppMode;
#[cfg(feature = "keybindings")]
use crate::editor::behavior::KeybindingParadigm;

pub use action::CanvasKeyAction;
pub use builtin::{
    builtin_emacs_preset, builtin_helix_preset, builtin_vim_preset,
    default_builtin_action_bindings, default_emacs_action_bindings,
    default_helix_action_bindings, default_vim_action_bindings, emacs_preset_toml,
    helix_preset_toml, vim_preset_toml, BuiltinCanvasKeybindingPreset,
};
pub use key_sequence::{
    parse_binding, try_parse_binding, try_parse_key, KeyStroke, ParseKeyError,
};
pub use preset::{
    CanvasKeybindingPreset, CanvasKeybindingPresetBinding, CanvasKeybindingPresetError,
    CanvasKeybindingPresetIssue, CanvasKeybindingPresetSection,
};

#[derive(Clone, Debug)]
struct Binding {
    action: CanvasKeyAction,
    sequence: Vec<KeyStroke>,
}

#[derive(Clone, Debug, Default)]
pub struct CanvasKeyBindings {
    ro: Vec<Binding>,
    edit: Vec<Binding>,
    hl: Vec<Binding>,
    pub(crate) paradigm: Option<KeybindingParadigm>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CanvasActionKeyBinding {
    pub mode: AppMode,
    pub action: CanvasAction,
    pub sequence: Vec<KeyStroke>,
}

pub type CanvasActionBinding = CanvasActionKeyBinding;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyEventOutcome {
    Consumed(Option<String>),
    Pending,
    NotMatched,
    /// Reached top boundary while navigating upward.
    ExitTop,
    /// Reached bottom boundary while navigating downward.
    ExitBottom,
}

#[derive(Debug, Clone)]
pub struct KeySequenceTracker {
    sequence: Vec<KeyStroke>,
    last_key_time: Instant,
    timeout: Duration,
}

impl KeySequenceTracker {
    pub fn new(timeout_ms: u64) -> Self {
        Self {
            sequence: Vec::new(),
            last_key_time: Instant::now(),
            timeout: Duration::from_millis(timeout_ms),
        }
    }

    pub fn reset(&mut self) {
        self.sequence.clear();
        self.last_key_time = Instant::now();
    }

    pub fn add_key(&mut self, stroke: KeyStroke) {
        let now = Instant::now();
        if now.duration_since(self.last_key_time) > self.timeout {
            self.reset();
        }
        self.sequence
            .push(key_sequence::normalize_stroke(stroke));
        self.last_key_time = now;
    }

    pub fn sequence(&self) -> &[KeyStroke] {
        &self.sequence
    }
}

impl CanvasKeyBindings {
    pub fn vim_defaults() -> Self {
        Self::from_builtin_preset(BuiltinCanvasKeybindingPreset::Vim)
    }

    pub fn helix_defaults() -> Self {
        Self::from_builtin_preset(BuiltinCanvasKeybindingPreset::Helix)
    }

    pub fn emacs_defaults() -> Self {
        Self::from_builtin_preset(BuiltinCanvasKeybindingPreset::Emacs)
    }

    pub fn from_builtin_preset(preset: BuiltinCanvasKeybindingPreset) -> Self {
        let mut bindings = Self::from_preset(&preset.preset());
        bindings.paradigm = Some(match preset {
            BuiltinCanvasKeybindingPreset::Vim => KeybindingParadigm::Vim,
            BuiltinCanvasKeybindingPreset::Helix => KeybindingParadigm::Helix,
            BuiltinCanvasKeybindingPreset::Emacs => KeybindingParadigm::Emacs,
        });
        bindings
    }

    pub fn from_preset(preset: &CanvasKeybindingPreset) -> Self {
        Self::try_from_preset(preset).expect("canvas keybinding preset was validated")
    }

    pub fn try_from_preset(
        preset: &CanvasKeybindingPreset,
    ) -> Result<Self, CanvasKeybindingPresetError> {
        preset.validate()?;

        let mut bindings = Self::default();
        for section in preset.sections() {
            let section_bindings = section
                .bindings
                .iter()
                .flat_map(|binding| {
                    binding.keys.iter().map(|key| {
                        try_parse_binding(key).map(|sequence| Binding {
                            action: binding.action.clone(),
                            sequence,
                        })
                    })
                })
                .collect::<Result<Vec<_>, _>>()
                .expect("canvas keybinding preset was validated");

            match section.mode {
                AppMode::Nor => bindings.ro.extend(section_bindings),
                AppMode::Ins => bindings.edit.extend(section_bindings),
                AppMode::Sel => bindings.hl.extend(section_bindings),
                _ => {}
            }
        }
        Ok(bindings)
    }

    pub fn from_mode_maps(
        read_only: &HashMap<String, Vec<String>>,
        edit: &HashMap<String, Vec<String>>,
        highlight: &HashMap<String, Vec<String>>,
    ) -> Self {
        let mut bindings = Self::default();
        bindings.ro = collect_bindings(read_only);
        bindings.edit = collect_bindings(edit);
        bindings.hl = collect_bindings(highlight);
        bindings
    }

    pub fn try_from_mode_maps(
        read_only: &HashMap<String, Vec<String>>,
        edit: &HashMap<String, Vec<String>>,
        highlight: &HashMap<String, Vec<String>>,
    ) -> Result<Self, CanvasKeybindingPresetError> {
        let preset = CanvasKeybindingPreset::from_mode_maps(read_only, edit, highlight)?;
        Self::try_from_preset(&preset)
    }

    pub fn lookup_action(
        &self,
        mode: AppMode,
        seq: &[KeyStroke],
    ) -> (Option<&CanvasKeyAction>, bool) {
        let bindings = match mode {
            AppMode::Nor => &self.ro,
            AppMode::Ins => &self.edit,
            AppMode::Sel => &self.hl,
            _ => return (None, false),
        };

        if seq.is_empty() {
            return (None, false);
        }

        for binding in bindings {
            if binding.sequence == seq {
                return (Some(&binding.action), false);
            }
        }

        for binding in bindings {
            if seq.len() < binding.sequence.len() && binding.sequence.starts_with(seq) {
                return (None, true);
            }
        }

        (None, false)
    }

    pub fn lookup(&self, mode: AppMode, seq: &[KeyStroke]) -> (Option<&str>, bool) {
        let (action, is_prefix) = self.lookup_action(mode, seq);
        (action.map(|action| action.as_str()), is_prefix)
    }
}

fn collect_bindings(mode_map: &HashMap<String, Vec<String>>) -> Vec<Binding> {
    let mut out = Vec::new();
    for (action, list) in mode_map {
        for binding_str in list {
            if let Some(sequence) = parse_binding(binding_str) {
                out.push(Binding {
                    action: CanvasKeyAction::from_name(action),
                    sequence,
                });
            }
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use crossterm::event::{KeyCode, KeyModifiers};

    #[test]
    fn strict_mode_maps_support_named_undo_and_redo() {
        let mut read_only = HashMap::new();
        read_only.insert("undo".to_string(), vec!["u".to_string()]);
        read_only.insert("redo".to_string(), vec!["ctrl+r".to_string()]);

        let keybindings = CanvasKeyBindings::try_from_mode_maps(
            &read_only,
            &HashMap::new(),
            &HashMap::new(),
        )
        .unwrap();

        let undo = [KeyStroke {
            code: KeyCode::Char('u'),
            modifiers: KeyModifiers::empty(),
        }];
        let redo = [KeyStroke {
            code: KeyCode::Char('r'),
            modifiers: KeyModifiers::CONTROL,
        }];

        assert_eq!(
            keybindings.lookup_action(AppMode::Nor, &undo).0,
            Some(&CanvasKeyAction::Undo)
        );
        assert_eq!(
            keybindings.lookup_action(AppMode::Nor, &redo).0,
            Some(&CanvasKeyAction::Redo)
        );
    }
}