Skip to main content

mockforge_tui/
event.rs

1//! Channel-based async event handler for terminal, SSE, and data poll events.
2
3use anyhow::Result;
4use crossterm::event::{self, Event as TermEvent, KeyEvent, MouseEvent};
5use std::time::Duration;
6use tokio::sync::mpsc;
7
8/// All event types the main loop can receive.
9#[derive(Debug)]
10pub enum Event {
11    /// Terminal key press.
12    Key(KeyEvent),
13    /// Terminal mouse input.
14    Mouse(MouseEvent),
15    /// Terminal resize.
16    Resize(u16, u16),
17    /// Periodic tick for polling / animation.
18    Tick,
19    /// Data fetched from the admin API (screen id + serialised JSON).
20    Data {
21        screen: &'static str,
22        payload: String,
23    },
24    /// An API error to display.
25    ApiError {
26        screen: &'static str,
27        message: String,
28    },
29    /// SSE log line received.
30    LogLine(String),
31}
32
33/// Spawns a background task that reads terminal events and emits ticks.
34pub struct EventHandler {
35    rx: mpsc::UnboundedReceiver<Event>,
36    _tx: mpsc::UnboundedSender<Event>,
37}
38
39impl EventHandler {
40    /// Create the event handler. `tick_rate` controls how often `Tick` events
41    /// are generated.
42    pub fn new(tick_rate: Duration) -> Self {
43        let (tx, rx) = mpsc::unbounded_channel();
44        let event_tx = tx.clone();
45
46        tokio::spawn(async move {
47            loop {
48                let has_event = event::poll(tick_rate).unwrap_or(false);
49                if has_event {
50                    match event::read() {
51                        Ok(TermEvent::Key(key)) => {
52                            if event_tx.send(Event::Key(key)).is_err() {
53                                return;
54                            }
55                        }
56                        Ok(TermEvent::Mouse(mouse)) => {
57                            if event_tx.send(Event::Mouse(mouse)).is_err() {
58                                return;
59                            }
60                        }
61                        Ok(TermEvent::Resize(w, h)) => {
62                            if event_tx.send(Event::Resize(w, h)).is_err() {
63                                return;
64                            }
65                        }
66                        _ => {}
67                    }
68                } else {
69                    // No terminal event within the tick window — emit tick.
70                    if event_tx.send(Event::Tick).is_err() {
71                        return;
72                    }
73                }
74            }
75        });
76
77        Self { rx, _tx: tx }
78    }
79
80    /// Get a clone of the sender so background tasks can push events.
81    pub fn sender(&self) -> mpsc::UnboundedSender<Event> {
82        self._tx.clone()
83    }
84
85    /// Wait for the next event.
86    pub async fn next(&mut self) -> Result<Event> {
87        self.rx.recv().await.ok_or_else(|| anyhow::anyhow!("event channel closed"))
88    }
89}