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 default calibration file path
322    pub fn default_path() -> Option<std::path::PathBuf> {
323        dirs::config_dir().map(|p| p.join("fresh").join("key_calibration.json"))
324    }
325
326    /// Load from the default config location
327    pub fn load_default() -> Result<Self, std::io::Error> {
328        if let Some(path) = Self::default_path() {
329            Self::load_from_file(&path)
330        } else {
331            Ok(Self::new())
332        }
333    }
334
335    /// Save to the default config location
336    pub fn save_default(&self) -> Result<(), std::io::Error> {
337        if let Some(path) = Self::default_path() {
338            self.save_to_file(&path)
339        } else {
340            Err(std::io::Error::new(
341                std::io::ErrorKind::NotFound,
342                "Could not determine config directory",
343            ))
344        }
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn test_translator_empty() {
354        let translator = KeyTranslator::new();
355        assert!(translator.is_empty());
356
357        // Raw events pass through unchanged
358        let raw = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
359        let result = translator.translate(raw);
360        assert_eq!(result.code, KeyCode::Backspace);
361    }
362
363    #[test]
364    fn test_translator_with_mapping() {
365        let mut translator = KeyTranslator::new();
366
367        // Map Char('\x7f') to Backspace
368        let raw = KeyEvent::new(KeyCode::Char('\x7f'), KeyModifiers::NONE);
369        let expected = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
370        translator.add_translation(raw, expected);
371
372        assert_eq!(translator.len(), 1);
373        assert!(translator.has_translation(&raw));
374
375        // Translation works
376        let result = translator.translate(raw);
377        assert_eq!(result.code, KeyCode::Backspace);
378    }
379
380    #[test]
381    fn test_translator_preserves_unmapped() {
382        let mut translator = KeyTranslator::new();
383
384        // Add one mapping
385        let mapped_raw = KeyEvent::new(KeyCode::Char('\x7f'), KeyModifiers::NONE);
386        let mapped_expected = KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE);
387        translator.add_translation(mapped_raw, mapped_expected);
388
389        // Unmapped keys pass through
390        let unmapped = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE);
391        let result = translator.translate(unmapped);
392        assert_eq!(result.code, KeyCode::Char('a'));
393    }
394
395    #[test]
396    fn test_translator_with_modifiers() {
397        let mut translator = KeyTranslator::new();
398
399        // Map Alt+b to Alt+Left
400        let raw = KeyEvent::new(KeyCode::Char('b'), KeyModifiers::ALT);
401        let expected = KeyEvent::new(KeyCode::Left, KeyModifiers::ALT);
402        translator.add_translation(raw, expected);
403
404        let result = translator.translate(raw);
405        assert_eq!(result.code, KeyCode::Left);
406        assert!(result.modifiers.contains(KeyModifiers::ALT));
407    }
408
409    #[test]
410    fn test_key_event_key_serialization() {
411        let key = KeyEventKey {
412            code: SerializableKeyCode::Backspace,
413            modifiers: KeyModifiers::NONE.bits(),
414        };
415
416        let json = serde_json::to_string(&key).unwrap();
417        let deserialized: KeyEventKey = serde_json::from_str(&json).unwrap();
418
419        assert_eq!(key, deserialized);
420    }
421
422    #[test]
423    fn test_roundtrip_key_event() {
424        let original = KeyEvent::new(KeyCode::Home, KeyModifiers::SHIFT);
425        let key = KeyEventKey::from_key_event(&original);
426        let restored = key.to_key_event();
427
428        assert_eq!(original.code, restored.code);
429        assert_eq!(original.modifiers, restored.modifiers);
430    }
431}