Skip to main content

null_e/tui/
event.rs

1//! Event handling for TUI
2//!
3//! Handles keyboard and terminal events in a separate thread.
4
5use crossterm::event::{self, Event as CrosstermEvent, KeyCode, KeyEvent, KeyModifiers, MouseEventKind};
6use std::sync::mpsc;
7use std::thread;
8use std::time::{Duration, Instant};
9
10/// Terminal events
11#[derive(Debug, Clone)]
12pub enum Event {
13    /// Terminal tick (for animations/updates)
14    Tick,
15    /// Key press
16    Key(KeyEvent),
17    /// Mouse event
18    Mouse(crossterm::event::MouseEvent),
19    /// Terminal resize
20    Resize(u16, u16),
21}
22
23/// Event handler that polls for terminal events
24pub struct EventHandler {
25    /// Event receiver
26    rx: mpsc::Receiver<Event>,
27    /// Event sender (kept for potential future use)
28    _tx: mpsc::Sender<Event>,
29}
30
31impl EventHandler {
32    /// Create a new event handler with specified tick rate
33    pub fn new(tick_rate: Duration) -> Self {
34        let (tx, rx) = mpsc::channel();
35        let _tx = tx.clone();
36
37        thread::spawn(move || {
38            let mut last_tick = Instant::now();
39            loop {
40                // Calculate timeout for next tick
41                let timeout = tick_rate
42                    .checked_sub(last_tick.elapsed())
43                    .unwrap_or(Duration::ZERO);
44
45                // Poll for events
46                if event::poll(timeout).unwrap_or(false) {
47                    match event::read() {
48                        Ok(CrosstermEvent::Key(key)) => {
49                            // Filter out release events on some terminals
50                            if key.kind == crossterm::event::KeyEventKind::Press {
51                                if tx.send(Event::Key(key)).is_err() {
52                                    return;
53                                }
54                            }
55                        }
56                        Ok(CrosstermEvent::Mouse(mouse)) => {
57                            if tx.send(Event::Mouse(mouse)).is_err() {
58                                return;
59                            }
60                        }
61                        Ok(CrosstermEvent::Resize(w, h)) => {
62                            if tx.send(Event::Resize(w, h)).is_err() {
63                                return;
64                            }
65                        }
66                        _ => {}
67                    }
68                }
69
70                // Send tick event if enough time has passed
71                if last_tick.elapsed() >= tick_rate {
72                    if tx.send(Event::Tick).is_err() {
73                        return;
74                    }
75                    last_tick = Instant::now();
76                }
77            }
78        });
79
80        Self { rx, _tx }
81    }
82
83    /// Get the next event, blocking until one is available
84    pub fn next(&self) -> Result<Event, mpsc::RecvError> {
85        self.rx.recv()
86    }
87}
88
89/// Key bindings for the TUI
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum Action {
92    /// Quit the application
93    Quit,
94    /// Move selection up
95    Up,
96    /// Move selection down
97    Down,
98    /// Toggle selection of current item
99    ToggleSelect,
100    /// Expand/collapse current item
101    ToggleExpand,
102    /// Expand current item
103    Expand,
104    /// Collapse current item
105    Collapse,
106    /// Select all items
107    SelectAll,
108    /// Deselect all items
109    DeselectAll,
110    /// Delete selected items
111    Delete,
112    /// Confirm action
113    Confirm,
114    /// Cancel action
115    Cancel,
116    /// Show help
117    Help,
118    /// Start scanning
119    Scan,
120    /// Page up
121    PageUp,
122    /// Page down
123    PageDown,
124    /// Go to top
125    Top,
126    /// Go to bottom
127    Bottom,
128    /// Search/filter
129    Search,
130    /// Tab to next category
131    NextTab,
132    /// Tab to previous category
133    PrevTab,
134    /// Refresh
135    Refresh,
136    /// Scroll up (mouse)
137    ScrollUp,
138    /// Scroll down (mouse)
139    ScrollDown,
140    /// Go back to menu
141    Back,
142    /// Toggle permanent delete mode
143    TogglePermanent,
144    /// No action
145    None,
146}
147
148impl Action {
149    /// Convert a key event to an action
150    pub fn from_key(key: KeyEvent) -> Self {
151        match key.code {
152            // Quit
153            KeyCode::Char('q') => Action::Quit,
154            KeyCode::Esc => Action::Cancel,
155            KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => Action::Quit,
156
157            // Navigation
158            KeyCode::Up | KeyCode::Char('k') => Action::Up,
159            KeyCode::Down | KeyCode::Char('j') => Action::Down,
160            KeyCode::PageUp | KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => Action::PageUp,
161            KeyCode::PageDown | KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => Action::PageDown,
162            KeyCode::Home | KeyCode::Char('g') if key.modifiers.contains(KeyModifiers::NONE) => Action::Top,
163            KeyCode::End | KeyCode::Char('G') => Action::Bottom,
164
165            // Expand/Collapse with arrow keys
166            KeyCode::Right | KeyCode::Char('l') => Action::Expand,
167            KeyCode::Left | KeyCode::Char('h') => Action::Collapse,
168
169            // Selection - Enter to select, Space to expand
170            KeyCode::Enter => Action::ToggleSelect,
171            KeyCode::Char(' ') => Action::ToggleExpand,
172            KeyCode::Char('a') => Action::SelectAll,
173            KeyCode::Char('A') => Action::DeselectAll,
174            KeyCode::Char('u') if !key.modifiers.contains(KeyModifiers::CONTROL) => Action::DeselectAll,
175
176            // Actions
177            KeyCode::Char('d') if !key.modifiers.contains(KeyModifiers::CONTROL) => Action::Delete,
178            KeyCode::Delete => Action::Delete,
179            KeyCode::Char('y') => Action::Confirm,
180            KeyCode::Char('n') => Action::Cancel,
181            KeyCode::Char('p') => Action::TogglePermanent,
182            KeyCode::Char('?') => Action::Help,
183            KeyCode::Char('s') => Action::Scan,
184            KeyCode::Char('/') => Action::Search,
185            KeyCode::Char('r') | KeyCode::F(5) => Action::Refresh,
186            KeyCode::Char('b') | KeyCode::Backspace => Action::Back,
187
188            // Tabs
189            KeyCode::Tab => Action::NextTab,
190            KeyCode::BackTab => Action::PrevTab,
191            KeyCode::Char('1') => Action::None, // Could map to specific tabs
192            KeyCode::Char('2') => Action::None,
193
194            _ => Action::None,
195        }
196    }
197
198    /// Convert a mouse event to an action
199    pub fn from_mouse(mouse: &crossterm::event::MouseEvent) -> Self {
200        match mouse.kind {
201            MouseEventKind::ScrollUp => Action::ScrollUp,
202            MouseEventKind::ScrollDown => Action::ScrollDown,
203            _ => Action::None,
204        }
205    }
206}