scrin 0.1.80

A terminal UI toolkit with panes, widgets, overlays, animations, and Aisling-powered effects/loaders.
Documentation
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent};
use std::collections::HashMap;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AppAction {
    Quit,
    Next,
    Previous,
    Select,
    Back,
    OpenCommandPalette,
    Copy,
    Paste,
    ScrollUp,
    ScrollDown,
    ScrollLeft,
    ScrollRight,
    TabNext,
    TabPrev,
    FocusUp,
    FocusDown,
    FocusLeft,
    FocusRight,
    Resize,
    Tick,
    Custom(String),
}

#[derive(Debug, Clone)]
pub struct KeyBinding {
    pub key: KeyCode,
    pub modifiers: KeyModifiers,
    pub action: AppAction,
    pub description: String,
}

pub struct EventRouter {
    bindings: Vec<KeyBinding>,
    mouse_bindings: HashMap<(MouseButton, bool), AppAction>,
    pending_actions: Vec<AppAction>,
}

impl EventRouter {
    pub fn new() -> Self {
        let mut router = Self {
            bindings: Vec::new(),
            mouse_bindings: HashMap::new(),
            pending_actions: Vec::new(),
        };
        router.add_default_bindings();
        router
    }

    fn add_default_bindings(&mut self) {
        self.bind(
            KeyModifiers::CONTROL,
            KeyCode::Char('c'),
            AppAction::Quit,
            "Quit",
        );
        self.bind(
            KeyModifiers::CONTROL,
            KeyCode::Char('q'),
            AppAction::Quit,
            "Quit",
        );
        self.bind(
            KeyModifiers::NONE,
            KeyCode::Up,
            AppAction::FocusUp,
            "Focus Up",
        );
        self.bind(
            KeyModifiers::NONE,
            KeyCode::Down,
            AppAction::FocusDown,
            "Focus Down",
        );
        self.bind(
            KeyModifiers::NONE,
            KeyCode::Left,
            AppAction::FocusLeft,
            "Focus Left",
        );
        self.bind(
            KeyModifiers::NONE,
            KeyCode::Right,
            AppAction::FocusRight,
            "Focus Right",
        );
        self.bind(
            KeyModifiers::NONE,
            KeyCode::Tab,
            AppAction::TabNext,
            "Next Tab",
        );
        self.bind(
            KeyModifiers::SHIFT,
            KeyCode::BackTab,
            AppAction::TabPrev,
            "Previous Tab",
        );
        self.bind(
            KeyModifiers::NONE,
            KeyCode::Enter,
            AppAction::Select,
            "Select",
        );
        self.bind(KeyModifiers::NONE, KeyCode::Esc, AppAction::Back, "Back");
        self.bind(
            KeyModifiers::CONTROL,
            KeyCode::Char('p'),
            AppAction::OpenCommandPalette,
            "Command Palette",
        );
        self.bind(
            KeyModifiers::CONTROL,
            KeyCode::Char('k'),
            AppAction::OpenCommandPalette,
            "Command Palette",
        );
        self.bind_key(KeyCode::PageUp, AppAction::ScrollUp, "Scroll Up");
        self.bind_key(KeyCode::PageDown, AppAction::ScrollDown, "Scroll Down");
        self.bind_key(KeyCode::Home, AppAction::ScrollLeft, "Scroll Start");
        self.bind_key(KeyCode::End, AppAction::ScrollRight, "Scroll End");
    }

    pub fn bind(
        &mut self,
        modifiers: KeyModifiers,
        key: KeyCode,
        action: AppAction,
        description: &str,
    ) {
        self.bindings.push(KeyBinding {
            key,
            modifiers,
            action,
            description: description.to_string(),
        });
    }

    pub fn bind_key(&mut self, key: KeyCode, action: AppAction, description: &str) {
        self.bind(KeyModifiers::NONE, key, action, description);
    }

    pub fn bind_mouse(&mut self, button: MouseButton, ctrl: bool, action: AppAction) {
        self.mouse_bindings.insert((button, ctrl), action);
    }

    pub fn handle_event(&mut self, event: &Event) -> Option<AppAction> {
        match event {
            Event::Key(key_event) => self.match_key(key_event),
            Event::Mouse(mouse_event) => self.match_mouse(mouse_event),
            Event::Resize(_, _) => Some(AppAction::Resize),
            _ => None,
        }
    }

    fn match_key(&self, key_event: &KeyEvent) -> Option<AppAction> {
        for binding in &self.bindings {
            if binding.key == key_event.code && binding.modifiers == key_event.modifiers {
                return Some(binding.action.clone());
            }
        }
        None
    }

    fn match_mouse(&self, mouse_event: &MouseEvent) -> Option<AppAction> {
        let ctrl = mouse_event
            .modifiers
            .contains(crossterm::event::KeyModifiers::CONTROL);
        match mouse_event.kind {
            crossterm::event::MouseEventKind::Down(button) => {
                self.mouse_bindings.get(&(button, ctrl)).cloned()
            }
            crossterm::event::MouseEventKind::ScrollUp => Some(AppAction::ScrollUp),
            crossterm::event::MouseEventKind::ScrollDown => Some(AppAction::ScrollDown),
            _ => None,
        }
    }

    pub fn bindings(&self) -> &[KeyBinding] {
        &self.bindings
    }

    pub fn push_action(&mut self, action: AppAction) {
        self.pending_actions.push(action);
    }

    pub fn drain_actions(&mut self) -> Vec<AppAction> {
        std::mem::take(&mut self.pending_actions)
    }
}

impl Default for EventRouter {
    fn default() -> Self {
        Self::new()
    }
}