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}