1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, mpsc};
use std::thread;
use std::time::{Duration, Instant};
use anyhow::Result;
use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, KeyEventKind};
/// Application events.
pub enum AppEvent {
Key(KeyEvent),
Tick,
PingResult { alias: String, reachable: bool },
SyncComplete {
provider: String,
hosts: Vec<crate::providers::ProviderHost>,
},
SyncPartial {
provider: String,
hosts: Vec<crate::providers::ProviderHost>,
failures: usize,
total: usize,
},
SyncError {
provider: String,
message: String,
},
SyncProgress { provider: String, message: String },
UpdateAvailable { version: String },
PollError,
}
/// Polls crossterm events in a background thread.
pub struct EventHandler {
tx: mpsc::Sender<AppEvent>,
rx: mpsc::Receiver<AppEvent>,
paused: Arc<AtomicBool>,
// Keep the thread handle alive
_handle: thread::JoinHandle<()>,
}
impl EventHandler {
pub fn new(tick_rate_ms: u64) -> Self {
let (tx, rx) = mpsc::channel();
let tick_rate = Duration::from_millis(tick_rate_ms);
let event_tx = tx.clone();
let paused = Arc::new(AtomicBool::new(false));
let paused_flag = paused.clone();
let handle = thread::spawn(move || {
let mut last_tick = Instant::now();
loop {
// When paused, sleep instead of polling stdin
if paused_flag.load(Ordering::Acquire) {
thread::sleep(Duration::from_millis(50));
continue;
}
// Cap poll timeout at 50ms so we notice pause flag quickly
let remaining = tick_rate
.checked_sub(last_tick.elapsed())
.unwrap_or(Duration::ZERO);
let timeout = remaining.min(Duration::from_millis(50));
match event::poll(timeout) {
Ok(true) => {
if let Ok(evt) = event::read() {
match evt {
CrosstermEvent::Key(key)
if key.kind == KeyEventKind::Press =>
{
if event_tx.send(AppEvent::Key(key)).is_err() {
return;
}
}
CrosstermEvent::Resize(..) => {
// Trigger immediate redraw on terminal resize
if event_tx.send(AppEvent::Tick).is_err() {
return;
}
}
_ => {}
}
}
}
Ok(false) => {}
Err(_) => {
// Poll error (e.g. stdin closed). Notify main loop and exit.
let _ = event_tx.send(AppEvent::PollError);
return;
}
}
if last_tick.elapsed() >= tick_rate {
if event_tx.send(AppEvent::Tick).is_err() {
return;
}
last_tick = Instant::now();
}
}
});
Self {
tx,
rx,
paused,
_handle: handle,
}
}
/// Get the next event (blocks until available).
pub fn next(&self) -> Result<AppEvent> {
Ok(self.rx.recv()?)
}
/// Get a clone of the sender for sending events from other threads.
pub fn sender(&self) -> mpsc::Sender<AppEvent> {
self.tx.clone()
}
/// Pause event polling (call before spawning SSH).
pub fn pause(&self) {
self.paused.store(true, Ordering::Release);
}
/// Resume event polling (call after SSH exits).
pub fn resume(&self) {
// Drain stale events, but keep background result events
let mut preserved = Vec::new();
while let Ok(event) = self.rx.try_recv() {
match event {
AppEvent::PingResult { .. }
| AppEvent::SyncComplete { .. }
| AppEvent::SyncPartial { .. }
| AppEvent::SyncError { .. }
| AppEvent::SyncProgress { .. }
| AppEvent::UpdateAvailable { .. } => preserved.push(event),
_ => {}
}
}
// Re-send preserved events
for event in preserved {
let _ = self.tx.send(event);
}
self.paused.store(false, Ordering::Release);
}
}