Skip to main content

escriba_input/
lib.rs

1//! `escriba-input` — translate madori input events into escriba's key
2//! abstraction.
3//!
4//! Two layers:
5//!   - [`translate_key`] takes a `madori::KeyEvent` (press only) and returns
6//!     the corresponding `escriba_keymap::Key` — Ctrl/Alt modifiers preserved,
7//!     printable chars passed through.
8//!   - [`EditorInput`] is the full frame-level dispatch — handles window
9//!     resizes, focus changes, IME, mouse events, and key events uniformly.
10
11use escriba_keymap::Key;
12use madori::AppEvent;
13use madori::event::{KeyCode, KeyEvent};
14
15/// Translate a single `madori::KeyEvent` into an `escriba_keymap::Key`.
16///
17/// Returns `None` for non-printable keys with no match (modifiers-only
18/// presses, unknown keycodes, releases).
19#[must_use]
20pub fn translate_key(event: &KeyEvent) -> Option<Key> {
21    if !event.pressed {
22        return None;
23    }
24    let mods = event.modifiers;
25    let key = match event.key {
26        KeyCode::Enter => Key::Enter,
27        KeyCode::Escape => Key::Esc,
28        KeyCode::Backspace => Key::Backspace,
29        KeyCode::Tab => Key::Tab,
30        KeyCode::Up => Key::Up,
31        KeyCode::Down => Key::Down,
32        KeyCode::Left => Key::Left,
33        KeyCode::Right => Key::Right,
34        KeyCode::Home => Key::Home,
35        KeyCode::End => Key::End,
36        KeyCode::PageUp => Key::PageUp,
37        KeyCode::PageDown => Key::PageDown,
38        KeyCode::Space => Key::Char(' '),
39        KeyCode::Char(c) => {
40            if mods.ctrl && !mods.alt {
41                return Some(Key::Ctrl(c.to_ascii_lowercase()));
42            }
43            if mods.alt && !mods.ctrl {
44                return Some(Key::Alt(c.to_ascii_lowercase()));
45            }
46            Key::Char(c)
47        }
48        // `Delete` is a real prompt verb (`Action::PromptDelete`), not noise —
49        // dropping it here is why `<Del>` was documented but unreachable.
50        KeyCode::Delete => Key::Delete,
51        KeyCode::F(_) | KeyCode::Unknown => return None,
52    };
53    Some(key)
54}
55
56/// What the runtime should do after translating an event.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum InputOutcome {
59    /// A fully-translated escriba key.
60    Key(Key),
61    /// Window was resized.
62    Resized { width: u32, height: u32 },
63    /// Focus changed.
64    Focus(bool),
65    /// Exit requested (Cmd-Q / close button).
66    Quit,
67    /// Nothing translatable — ignore.
68    None,
69}
70
71#[must_use]
72pub fn translate_app_event(event: &AppEvent) -> InputOutcome {
73    match event {
74        AppEvent::Key(ke) => translate_key(ke).map_or(InputOutcome::None, InputOutcome::Key),
75        AppEvent::Resized { width, height } => InputOutcome::Resized {
76            width: *width,
77            height: *height,
78        },
79        AppEvent::Focused(f) => InputOutcome::Focus(*f),
80        AppEvent::CloseRequested => InputOutcome::Quit,
81        // IME / Mouse / RedrawRequested — phase 1 doesn't route them.
82        _ => InputOutcome::None,
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use madori::event::Modifiers;
90
91    fn key(kc: KeyCode, mods: Modifiers) -> KeyEvent {
92        KeyEvent {
93            key: kc,
94            pressed: true,
95            modifiers: mods,
96            text: None,
97        }
98    }
99
100    #[test]
101    fn char_without_modifiers() {
102        let k = translate_key(&key(KeyCode::Char('h'), Modifiers::default())).unwrap();
103        assert_eq!(k, Key::Char('h'));
104    }
105
106    #[test]
107    fn ctrl_char_becomes_ctrl_key() {
108        let mods = Modifiers {
109            ctrl: true,
110            ..Modifiers::default()
111        };
112        let k = translate_key(&key(KeyCode::Char('R'), mods)).unwrap();
113        assert_eq!(k, Key::Ctrl('r'));
114    }
115
116    #[test]
117    fn alt_char_becomes_alt_key() {
118        let mods = Modifiers {
119            alt: true,
120            ..Modifiers::default()
121        };
122        let k = translate_key(&key(KeyCode::Char('f'), mods)).unwrap();
123        assert_eq!(k, Key::Alt('f'));
124    }
125
126    #[test]
127    fn named_keys_map() {
128        assert_eq!(
129            translate_key(&key(KeyCode::Escape, Modifiers::default())).unwrap(),
130            Key::Esc
131        );
132        assert_eq!(
133            translate_key(&key(KeyCode::Enter, Modifiers::default())).unwrap(),
134            Key::Enter
135        );
136        assert_eq!(
137            translate_key(&key(KeyCode::Space, Modifiers::default())).unwrap(),
138            Key::Char(' ')
139        );
140    }
141
142    #[test]
143    fn release_returns_none() {
144        let mut e = key(KeyCode::Char('a'), Modifiers::default());
145        e.pressed = false;
146        assert!(translate_key(&e).is_none());
147    }
148
149    #[test]
150    fn close_event_quits() {
151        assert_eq!(
152            translate_app_event(&AppEvent::CloseRequested),
153            InputOutcome::Quit
154        );
155    }
156}