Skip to main content

fresh/app/
event_debug.rs

1//! Event Debug Dialog
2//!
3//! A dialog for debugging terminal key events. Shows raw key codes and modifiers
4//! as they are received from the terminal, helping diagnose keybinding issues.
5
6use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
7
8/// Maximum number of events to display in the history
9const MAX_HISTORY: usize = 10;
10
11/// A recorded key event with display information
12#[derive(Debug, Clone)]
13pub struct RecordedEvent {
14    /// The raw key event
15    pub event: KeyEvent,
16    /// Human-readable description of the raw event
17    pub description: String,
18    /// Human-readable description of the key *after* `normalize_key` — i.e.
19    /// the form keybinding lookup actually matches against. `None` when
20    /// normalization leaves the event unchanged (no extra line to show).
21    pub normalized: Option<String>,
22}
23
24impl RecordedEvent {
25    fn new(event: KeyEvent) -> Self {
26        let description = format_key_event(&event);
27        let (norm_code, norm_mods) =
28            crate::input::keybindings::normalize_key(event.code, event.modifiers);
29        let normalized = if norm_code == event.code && norm_mods == event.modifiers {
30            None
31        } else {
32            let norm_event = KeyEvent::new(norm_code, norm_mods);
33            Some(format_key_event(&norm_event))
34        };
35        Self {
36            event,
37            description,
38            normalized,
39        }
40    }
41}
42
43/// Format a key event for display
44fn format_key_event(event: &KeyEvent) -> String {
45    let mut parts = Vec::new();
46
47    // Build modifier string
48    if event.modifiers.contains(KeyModifiers::CONTROL) {
49        parts.push("Ctrl");
50    }
51    if event.modifiers.contains(KeyModifiers::ALT) {
52        parts.push("Alt");
53    }
54    if event.modifiers.contains(KeyModifiers::SHIFT) {
55        parts.push("Shift");
56    }
57    if event.modifiers.contains(KeyModifiers::SUPER) {
58        parts.push("Super");
59    }
60    if event.modifiers.contains(KeyModifiers::HYPER) {
61        parts.push("Hyper");
62    }
63    if event.modifiers.contains(KeyModifiers::META) {
64        parts.push("Meta");
65    }
66
67    // Format key code
68    let key_str = match event.code {
69        KeyCode::Backspace => "Backspace".to_string(),
70        KeyCode::Enter => "Enter".to_string(),
71        KeyCode::Left => "Left".to_string(),
72        KeyCode::Right => "Right".to_string(),
73        KeyCode::Up => "Up".to_string(),
74        KeyCode::Down => "Down".to_string(),
75        KeyCode::Home => "Home".to_string(),
76        KeyCode::End => "End".to_string(),
77        KeyCode::PageUp => "PageUp".to_string(),
78        KeyCode::PageDown => "PageDown".to_string(),
79        KeyCode::Tab => "Tab".to_string(),
80        KeyCode::BackTab => "BackTab".to_string(),
81        KeyCode::Delete => "Delete".to_string(),
82        KeyCode::Insert => "Insert".to_string(),
83        KeyCode::F(n) => format!("F{}", n),
84        KeyCode::Char(c) => {
85            if c == ' ' {
86                "Space".to_string()
87            } else if c.is_control() {
88                format!("0x{:02x}", c as u8)
89            } else {
90                format!("'{}'", c)
91            }
92        }
93        KeyCode::Null => "Null".to_string(),
94        KeyCode::Esc => "Esc".to_string(),
95        KeyCode::CapsLock => "CapsLock".to_string(),
96        KeyCode::ScrollLock => "ScrollLock".to_string(),
97        KeyCode::NumLock => "NumLock".to_string(),
98        KeyCode::PrintScreen => "PrintScreen".to_string(),
99        KeyCode::Pause => "Pause".to_string(),
100        KeyCode::Menu => "Menu".to_string(),
101        KeyCode::KeypadBegin => "KeypadBegin".to_string(),
102        KeyCode::Modifier(m) => format!("Modifier({:?})", m),
103        KeyCode::Media(m) => format!("Media({:?})", m),
104    };
105
106    parts.push(&key_str);
107
108    // Join with + separator, or just key if no modifiers
109    if parts.len() > 1 {
110        parts.join("+")
111    } else {
112        key_str
113    }
114}
115
116/// The event debug dialog state
117#[derive(Debug)]
118pub struct EventDebug {
119    /// History of recorded events (newest first)
120    pub history: Vec<RecordedEvent>,
121    /// Whether the dialog is active
122    pub active: bool,
123}
124
125impl EventDebug {
126    /// Create a new event debug dialog
127    pub fn new() -> Self {
128        Self {
129            history: Vec::new(),
130            active: true,
131        }
132    }
133
134    /// Record a new key event
135    pub fn record_event(&mut self, event: KeyEvent) {
136        // Check for close keys first
137        if event.modifiers == KeyModifiers::NONE {
138            match event.code {
139                KeyCode::Char('q') | KeyCode::Esc => {
140                    self.active = false;
141                    return;
142                }
143                KeyCode::Char('c') => {
144                    // Clear history
145                    self.history.clear();
146                    return;
147                }
148                _ => {}
149            }
150        }
151
152        // Record the event
153        let recorded = RecordedEvent::new(event);
154        self.history.insert(0, recorded);
155
156        // Trim history to max size
157        if self.history.len() > MAX_HISTORY {
158            self.history.truncate(MAX_HISTORY);
159        }
160    }
161
162    /// Check if the dialog should be closed
163    pub fn should_close(&self) -> bool {
164        !self.active
165    }
166
167    /// Get the raw details for the most recent event
168    pub fn last_event_details(&self) -> Option<String> {
169        self.history.first().map(|e| {
170            format!(
171                "code={:?}, modifiers={:?} (bits=0x{:02x}), kind={:?}, state={:?}",
172                e.event.code,
173                e.event.modifiers,
174                e.event.modifiers.bits(),
175                e.event.kind,
176                e.event.state
177            )
178        })
179    }
180}
181
182impl Default for EventDebug {
183    fn default() -> Self {
184        Self::new()
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_event_debug_creation() {
194        let debug = EventDebug::new();
195        assert!(debug.active);
196        assert!(debug.history.is_empty());
197    }
198
199    #[test]
200    fn test_record_event() {
201        let mut debug = EventDebug::new();
202        let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
203        debug.record_event(event);
204
205        assert_eq!(debug.history.len(), 1);
206        assert_eq!(debug.history[0].description, "Ctrl+'a'");
207    }
208
209    #[test]
210    fn test_close_with_q() {
211        let mut debug = EventDebug::new();
212        let event = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE);
213        debug.record_event(event);
214
215        assert!(debug.should_close());
216    }
217
218    #[test]
219    fn test_close_with_esc() {
220        let mut debug = EventDebug::new();
221        let event = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
222        debug.record_event(event);
223
224        assert!(debug.should_close());
225    }
226
227    #[test]
228    fn test_clear_with_c() {
229        let mut debug = EventDebug::new();
230
231        // Add some events
232        debug.record_event(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
233        debug.record_event(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
234        assert_eq!(debug.history.len(), 2);
235
236        // Clear with 'c'
237        debug.record_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE));
238        assert!(debug.history.is_empty());
239        assert!(debug.active); // Should not close
240    }
241
242    #[test]
243    fn test_max_history() {
244        let mut debug = EventDebug::new();
245
246        // Add more than MAX_HISTORY events
247        for i in 0..15 {
248            debug.record_event(KeyEvent::new(
249                KeyCode::Char((b'a' + i) as char),
250                KeyModifiers::NONE,
251            ));
252        }
253
254        assert_eq!(debug.history.len(), MAX_HISTORY);
255    }
256
257    #[test]
258    fn test_format_modifiers() {
259        let mut debug = EventDebug::new();
260
261        // Ctrl+Shift+Home
262        let event = KeyEvent::new(KeyCode::Home, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
263        debug.record_event(event);
264
265        assert_eq!(debug.history[0].description, "Ctrl+Shift+Home");
266    }
267}