Skip to main content

fresh/input/
key_translator.rs

1//! Key translation layer for terminal input calibration
2//!
3//! This module provides a translation layer that sits between the terminal and the keymap.
4//! It allows broken terminal key sequences to be normalized to expected key codes.
5//!
6//! The translation happens BEFORE keybinding resolution, so:
7//! 1. Terminal sends raw KeyEvent
8//! 2. KeyTranslator.translate(raw) → normalized KeyEvent
9//! 3. KeybindingResolver.resolve(normalized) → Action
10//!
11//! This keeps calibration separate from keymap customization.
12
13use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::path::Path;
17
18/// Key translation table that normalizes terminal input
19///
20/// Maps raw terminal events to expected/normalized events.
21/// For example, if a terminal sends Char('\x7f') for backspace,
22/// this translator can map it to KeyCode::Backspace.
23#[derive(Debug, Clone, Default)]
24pub struct KeyTranslator {
25    /// Translation table: raw event → normalized event
26    translations: HashMap<KeyEventKey, KeyEventKey>,
27}
28
29/// A serializable key event (simplified version of crossterm::KeyEvent)
30#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
31pub struct KeyEventKey {
32    /// The key code
33    pub code: SerializableKeyCode,
34    /// Key modifiers (Shift, Ctrl, Alt, etc.)
35    pub modifiers: u8, // KeyModifiers as bits
36}
37
38impl KeyEventKey {
39    /// Create a KeyEventKey from a crossterm KeyEvent
40    pub fn from_key_event(event: &KeyEvent) -> Self {
41        Self {
42            code: SerializableKeyCode::from_key_code(&event.code),
43            modifiers: event.modifiers.bits(),
44        }
45    }
46
47    /// Convert to a crossterm KeyEvent
48    pub fn to_key_event(&self) -> KeyEvent {
49        KeyEvent::new(
50            self.code.to_key_code(),
51            KeyModifiers::from_bits_truncate(self.modifiers),
52        )
53    }
54}
55
56/// A serializable key code
57#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
58#[serde(tag = "type", content = "value")]
59pub enum SerializableKeyCode {
60    /// Backspace key
61    Backspace,
62    /// Enter key
63    Enter,
64    /// Left arrow
65    Left,
66    /// Right arrow
67    Right,
68    /// Up arrow
69    Up,
70    /// Down arrow
71    Down,
72    /// Home key
73    Home,
74    /// End key
75    End,
76    /// Page up
77    PageUp,
78    /// Page down
79    PageDown,
80    /// Tab key
81    Tab,
82    /// Shift+Tab
83    BackTab,
84    /// Delete key
85    Delete,
86    /// Insert key
87    Insert,
88    /// Function key (1-12)
89    F(u8),
90    /// A character key
91    Char(char),
92    /// Null character (Ctrl+Space, etc.)
93    Null,
94    /// Escape key
95    Esc,
96    /// Caps Lock
97    CapsLock,
98    /// Scroll Lock
99    ScrollLock,
100    /// Num Lock
101    NumLock,
102    /// Print Screen
103    PrintScreen,
104    /// Pause key
105    Pause,
106    /// Menu key
107    Menu,
108    /// Modifier key only
109    Modifier(String),
110    /// Unknown key
111    Unknown,
112}
113
114impl SerializableKeyCode {
115    /// Convert from crossterm KeyCode
116    pub fn from_key_code(code: &KeyCode) -> Self {
117        match code {
118            KeyCode::Backspace => SerializableKeyCode::Backspace,
119            KeyCode::Enter => SerializableKeyCode::Enter,
120            KeyCode::Left => SerializableKeyCode::Left,
121            KeyCode::Right => SerializableKeyCode::Right,
122            KeyCode::Up => SerializableKeyCode::Up,
123            KeyCode::Down => SerializableKeyCode::Down,
124            KeyCode::Home => SerializableKeyCode::Home,
125            KeyCode::End => SerializableKeyCode::End,
126            KeyCode::PageUp => SerializableKeyCode::PageUp,
127            KeyCode::PageDown => SerializableKeyCode::PageDown,
128            KeyCode::Tab => SerializableKeyCode::Tab,
129            KeyCode::BackTab => SerializableKeyCode::BackTab,
130            KeyCode::Delete => SerializableKeyCode::Delete,
131            KeyCode::Insert => SerializableKeyCode::Insert,
132            KeyCode::F(n) => SerializableKeyCode::F(*n),
133            KeyCode::Char(c) => SerializableKeyCode::Char(*c),
134            KeyCode::Null => SerializableKeyCode::Null,
135            KeyCode::Esc => SerializableKeyCode::Esc,
136            KeyCode::CapsLock => SerializableKeyCode::CapsLock,
137            KeyCode::ScrollLock => SerializableKeyCode::ScrollLock,
138            KeyCode::NumLock => SerializableKeyCode::NumLock,
139            KeyCode::PrintScreen => SerializableKeyCode::PrintScreen,
140            KeyCode::Pause => SerializableKeyCode::Pause,
141            KeyCode::Menu => SerializableKeyCode::Menu,
142            KeyCode::Modifier(m) => SerializableKeyCode::Modifier(format!("{:?}", m)),
143            _ => SerializableKeyCode::Unknown,
144        }
145    }
146
147    /// Convert to crossterm KeyCode
148    pub fn to_key_code(&self) -> KeyCode {
149        match self {
150            SerializableKeyCode::Backspace => KeyCode::Backspace,
151            SerializableKeyCode::Enter => KeyCode::Enter,
152            SerializableKeyCode::Left => KeyCode::Left,
153            SerializableKeyCode::Right => KeyCode::Right,
154            SerializableKeyCode::Up => KeyCode::Up,
155            SerializableKeyCode::Down => KeyCode::Down,
156            SerializableKeyCode::Home => KeyCode::Home,
157            SerializableKeyCode::End => KeyCode::End,
158            SerializableKeyCode::PageUp => KeyCode::PageUp,
159            SerializableKeyCode::PageDown => KeyCode::PageDown,
160            SerializableKeyCode::Tab => KeyCode::Tab,
161            SerializableKeyCode::BackTab => KeyCode::BackTab,
162            SerializableKeyCode::Delete => KeyCode::Delete,
163            SerializableKeyCode::Insert => KeyCode::Insert,
164            SerializableKeyCode::F(n) => KeyCode::F(*n),
165            SerializableKeyCode::Char(c) => KeyCode::Char(*c),
166            SerializableKeyCode::Null => KeyCode::Null,
167            SerializableKeyCode::Esc => KeyCode::Esc,
168            SerializableKeyCode::CapsLock => KeyCode::CapsLock,
169            SerializableKeyCode::ScrollLock => KeyCode::ScrollLock,
170            SerializableKeyCode::NumLock => KeyCode::NumLock,
171            SerializableKeyCode::PrintScreen => KeyCode::PrintScreen,
172            SerializableKeyCode::Pause => KeyCode::Pause,
173            SerializableKeyCode::Menu => KeyCode::Menu,
174            SerializableKeyCode::Modifier(_) | SerializableKeyCode::Unknown => KeyCode::Null,
175        }
176    }
177}
178
179/// JSON format for the calibration file
180#[derive(Debug, Serialize, Deserialize)]
181struct CalibrationFile {
182    /// Comment explaining the file format
183    #[serde(rename = "_comment")]
184    comment: String,
185    /// Format description
186    #[serde(rename = "_format")]
187    format: String,
188    /// Translation mappings
189    translations: Vec<TranslationEntry>,
190}
191
192/// A single translation entry in the calibration file
193#[derive(Debug, Serialize, Deserialize)]
194struct TranslationEntry {
195    /// The raw key that the terminal sends
196    raw: KeyEventKey,
197    /// The expected/normalized key
198    expected: KeyEventKey,
199}
200
201impl KeyTranslator {
202    /// Create a new empty translator (no translations)
203    pub fn new() -> Self {
204        Self {
205            translations: HashMap::new(),
206        }
207    }
208
209    /// Translate a raw terminal event to a normalized event
210    ///
211    /// If a translation exists for the raw event, returns the normalized event.
212    /// Otherwise, returns the raw event unchanged.
213    pub fn translate(&self, raw: KeyEvent) -> KeyEvent {
214        let key = KeyEventKey::from_key_event(&raw);
215        if let Some(normalized) = self.translations.get(&key) {
216            normalized.to_key_event()
217        } else {
218            raw
219        }
220    }
221
222    /// Check if a translation exists for the given key
223    pub fn has_translation(&self, raw: &KeyEvent) -> bool {
224        let key = KeyEventKey::from_key_event(raw);
225        self.translations.contains_key(&key)
226    }
227
228    /// Add a translation mapping
229    pub fn add_translation(&mut self, raw: KeyEvent, expected: KeyEvent) {
230        let raw_key = KeyEventKey::from_key_event(&raw);
231        let expected_key = KeyEventKey::from_key_event(&expected);
232        self.translations.insert(raw_key, expected_key);
233    }
234
235    /// Remove a translation mapping
236    pub fn remove_translation(&mut self, raw: &KeyEvent) {
237        let key = KeyEventKey::from_key_event(raw);
238        self.translations.remove(&key);
239    }
240
241    /// Get the number of translations
242    pub fn len(&self) -> usize {
243        self.translations.len()
244    }
245
246    /// Check if there are no translations
247    pub fn is_empty(&self) -> bool {
248        self.translations.is_empty()
249    }
250
251    /// Clear all translations
252    pub fn clear(&mut self) {
253        self.translations.clear();
254    }
255
256    /// Load translations from a JSON file
257    ///
258    /// Returns an empty translator if the file doesn't exist.
259    /// Returns an error if the file exists but is invalid.
260    pub fn load_from_file(path: &Path) -> Result<Self, std::io::Error> {
261        if !path.exists() {
262            return Ok(Self::new());
263        }
264
265        let content = std::fs::read_to_string(path)?;
266        let file: CalibrationFile = serde_json::from_str(&content).map_err(|e| {
267            std::io::Error::new(
268                std::io::ErrorKind::InvalidData,
269                format!("Invalid calibration file: {}", e),
270            )
271        })?;
272
273        let mut translator = Self::new();
274        for entry in file.translations {
275            translator.translations.insert(entry.raw, entry.expected);
276        }
277
278        tracing::info!(
279            "Loaded {} key translations from {}",
280            translator.len(),
281            path.display()
282        );
283
284        Ok(translator)
285    }
286
287    /// Save translations to a JSON file
288    pub fn save_to_file(&self, path: &Path) -> Result<(), std::io::Error> {
289        // Ensure parent directory exists
290        if let Some(parent) = path.parent() {
291            std::fs::create_dir_all(parent)?;
292        }
293
294        let entries: Vec<TranslationEntry> = self
295            .translations
296            .iter()
297            .map(|(raw, expected)| TranslationEntry {
298                raw: raw.clone(),
299                expected: expected.clone(),
300            })
301            .collect();
302
303        let file = CalibrationFile {
304            comment: "Generated by 'Calibrate Input Keys' wizard".to_string(),
305            format: "raw_key → expected_key".to_string(),
306            translations: entries,
307        };
308
309        let content = serde_json::to_string_pretty(&file)?;
310        std::fs::write(path, content)?;
311
312        tracing::info!(
313            "Saved {} key translations to {}",
314            self.len(),
315            path.display()
316        );
317
318        Ok(())
319    }
320
321    /// Get the calibration file path for a given config directory.
322    pub fn calibration_path(config_dir: &std::path::Path) -> std::path::PathBuf {
323        config_dir.join("key_calibration.json")
324    }
325
326    /// Load from the config directory.
327    pub fn load_from_config_dir(config_dir: &std::path::Path) -> Result<Self, std::io::Error> {
328        let path = Self::calibration_path(config_dir);
329        Self::load_from_file(&path)
330    }
331
332    /// Save to the config directory.
333    pub fn save_to_config_dir(&self, config_dir: &std::path::Path) -> Result<(), std::io::Error> {
334        let path = Self::calibration_path(config_dir);
335        self.save_to_file(&path)
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn test_translator_empty() {
345        let translator = KeyTranslator::new();
346        assert!(translator.is_empty());
347
348        // Raw events pass through unchanged
349        let raw = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
350        let result = translator.translate(raw);
351        assert_eq!(result.code, KeyCode::Backspace);
352    }
353
354    #[test]
355    fn test_translator_with_mapping() {
356        let mut translator = KeyTranslator::new();
357
358        // Map Char('\x7f') to Backspace
359        let raw = KeyEvent::new(KeyCode::Char('\x7f'), KeyModifiers::NONE);
360        let expected = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
361        translator.add_translation(raw, expected);
362
363        assert_eq!(translator.len(), 1);
364        assert!(translator.has_translation(&raw));
365
366        // Translation works
367        let result = translator.translate(raw);
368        assert_eq!(result.code, KeyCode::Backspace);
369    }
370
371    #[test]
372    fn test_translator_preserves_unmapped() {
373        let mut translator = KeyTranslator::new();
374
375        // Add one mapping
376        let mapped_raw = KeyEvent::new(KeyCode::Char('\x7f'), KeyModifiers::NONE);
377        let mapped_expected = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
378        translator.add_translation(mapped_raw, mapped_expected);
379
380        // Unmapped keys pass through
381        let unmapped = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
382        let result = translator.translate(unmapped);
383        assert_eq!(result.code, KeyCode::Char('a'));
384    }
385
386    #[test]
387    fn test_translator_with_modifiers() {
388        let mut translator = KeyTranslator::new();
389
390        // Map Alt+b to Alt+Left
391        let raw = KeyEvent::new(KeyCode::Char('b'), KeyModifiers::ALT);
392        let expected = KeyEvent::new(KeyCode::Left, KeyModifiers::ALT);
393        translator.add_translation(raw, expected);
394
395        let result = translator.translate(raw);
396        assert_eq!(result.code, KeyCode::Left);
397        assert!(result.modifiers.contains(KeyModifiers::ALT));
398    }
399
400    #[test]
401    fn test_key_event_key_serialization() {
402        let key = KeyEventKey {
403            code: SerializableKeyCode::Backspace,
404            modifiers: KeyModifiers::NONE.bits(),
405        };
406
407        let json = serde_json::to_string(&key).unwrap();
408        let deserialized: KeyEventKey = serde_json::from_str(&json).unwrap();
409
410        assert_eq!(key, deserialized);
411    }
412
413    #[test]
414    fn test_roundtrip_key_event() {
415        let original = KeyEvent::new(KeyCode::Home, KeyModifiers::SHIFT);
416        let key = KeyEventKey::from_key_event(&original);
417        let restored = key.to_key_event();
418
419        assert_eq!(original.code, restored.code);
420        assert_eq!(original.modifiers, restored.modifiers);
421    }
422}