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