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            // The shorthands carry exactly ONE modifier. Anything richer —
41            // two modifiers, Super, or Shift as a modifier rather than a
42            // capital letter — goes through `Chord`, which carries the fleet
43            // type. Before this, `shift` and `meta` were computed here and
44            // then dropped for want of anywhere to put them.
45            let extra = mods.meta || (mods.ctrl && mods.alt);
46            if extra {
47                return chord_of(c, mods).map(Key::Chord);
48            }
49            if mods.ctrl {
50                return Some(Key::Ctrl(c.to_ascii_lowercase()));
51            }
52            if mods.alt {
53                return Some(Key::Alt(c.to_ascii_lowercase()));
54            }
55            Key::Char(c)
56        }
57        // `Delete` is a real prompt verb (`Action::PromptDelete`), not noise —
58        // dropping it here is why `<Del>` was documented but unreachable.
59        KeyCode::Delete => Key::Delete,
60        // F-keys used to die here — declared in an rc, never delivered.
61        KeyCode::F(n) => Key::F(n),
62        KeyCode::Unknown => return None,
63    };
64    Some(key)
65}
66
67/// Build a fleet chord from a character plus every modifier that is set.
68///
69/// Only reached when the single-modifier shorthands cannot express the
70/// combination — see the call site.
71fn chord_of(c: char, mods: madori::event::Modifiers) -> Option<awase::Hotkey> {
72    use awase::Modifiers as M;
73    let mut m = M::NONE;
74    if mods.ctrl {
75        m = m | M::CTRL;
76    }
77    if mods.alt {
78        m = m | M::ALT;
79    }
80    if mods.shift {
81        m = m | M::SHIFT;
82    }
83    if mods.meta {
84        m = m | M::CMD;
85    }
86    let key = awase::Key::from_name(&c.to_ascii_lowercase().to_string())?;
87    Some(awase::Hotkey::new(m, key))
88}
89
90/// What the runtime should do after translating an event.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum InputOutcome {
93    /// A fully-translated escriba key.
94    Key(Key),
95    /// Window was resized.
96    Resized { width: u32, height: u32 },
97    /// Focus changed.
98    Focus(bool),
99    /// Exit requested (Cmd-Q / close button).
100    Quit,
101    /// Nothing translatable — ignore.
102    None,
103}
104
105#[must_use]
106pub fn translate_app_event(event: &AppEvent) -> InputOutcome {
107    match event {
108        AppEvent::Key(ke) => translate_key(ke).map_or(InputOutcome::None, InputOutcome::Key),
109        AppEvent::Resized { width, height } => InputOutcome::Resized {
110            width: *width,
111            height: *height,
112        },
113        AppEvent::Focused(f) => InputOutcome::Focus(*f),
114        AppEvent::CloseRequested => InputOutcome::Quit,
115        // IME / Mouse / RedrawRequested — phase 1 doesn't route them.
116        _ => InputOutcome::None,
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use madori::event::Modifiers;
124
125    fn key(kc: KeyCode, mods: Modifiers) -> KeyEvent {
126        KeyEvent {
127            key: kc,
128            pressed: true,
129            modifiers: mods,
130            text: None,
131        }
132    }
133
134    #[test]
135    fn char_without_modifiers() {
136        let k = translate_key(&key(KeyCode::Char('h'), Modifiers::default())).unwrap();
137        assert_eq!(k, Key::Char('h'));
138    }
139
140    #[test]
141    fn ctrl_char_becomes_ctrl_key() {
142        let mods = Modifiers {
143            ctrl: true,
144            ..Modifiers::default()
145        };
146        let k = translate_key(&key(KeyCode::Char('R'), mods)).unwrap();
147        assert_eq!(k, Key::Ctrl('r'));
148    }
149
150    #[test]
151    fn alt_char_becomes_alt_key() {
152        let mods = Modifiers {
153            alt: true,
154            ..Modifiers::default()
155        };
156        let k = translate_key(&key(KeyCode::Char('f'), mods)).unwrap();
157        assert_eq!(k, Key::Alt('f'));
158    }
159
160    #[test]
161    fn named_keys_map() {
162        assert_eq!(
163            translate_key(&key(KeyCode::Escape, Modifiers::default())).unwrap(),
164            Key::Esc
165        );
166        assert_eq!(
167            translate_key(&key(KeyCode::Enter, Modifiers::default())).unwrap(),
168            Key::Enter
169        );
170        assert_eq!(
171            translate_key(&key(KeyCode::Space, Modifiers::default())).unwrap(),
172            Key::Char(' ')
173        );
174    }
175
176    #[test]
177    fn release_returns_none() {
178        let mut e = key(KeyCode::Char('a'), Modifiers::default());
179        e.pressed = false;
180        assert!(translate_key(&e).is_none());
181    }
182
183    #[test]
184    fn close_event_quits() {
185        assert_eq!(
186            translate_app_event(&AppEvent::CloseRequested),
187            InputOutcome::Quit
188        );
189    }
190}