1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::{Arc, mpsc};
3use std::thread;
4use std::time::{Duration, Instant};
5
6use anyhow::Result;
7use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, KeyEventKind};
8
9pub enum AppEvent {
11 Key(KeyEvent),
12 Tick,
13 PingResult {
14 alias: String,
15 rtt_ms: Option<u32>,
16 generation: u64,
17 },
18 SyncComplete {
19 provider: String,
20 hosts: Vec<crate::providers::ProviderHost>,
21 },
22 SyncPartial {
23 provider: String,
24 hosts: Vec<crate::providers::ProviderHost>,
25 failures: usize,
26 total: usize,
27 },
28 SyncError {
29 provider: String,
30 message: String,
31 },
32 SyncProgress {
33 provider: String,
34 message: String,
35 },
36 UpdateAvailable {
37 version: String,
38 headline: Option<String>,
39 },
40 FileBrowserListing {
41 alias: String,
42 path: String,
43 entries: Result<Vec<crate::file_browser::FileEntry>, String>,
44 },
45 ScpComplete {
46 alias: String,
47 success: bool,
48 message: String,
49 },
50 SnippetHostDone {
51 run_id: u64,
52 alias: String,
53 stdout: String,
54 stderr: String,
55 exit_code: Option<i32>,
56 },
57 SnippetAllDone {
58 run_id: u64,
59 },
60 SnippetProgress {
61 run_id: u64,
62 completed: usize,
63 total: usize,
64 },
65 ContainerListing {
66 alias: String,
67 result: Result<crate::containers::ContainerListing, crate::containers::ContainerError>,
68 },
69 ContainerActionComplete {
70 alias: String,
71 action: crate::containers::ContainerAction,
72 result: Result<(), String>,
73 },
74 ContainerInspectComplete {
79 alias: String,
80 container_id: String,
81 result: Box<Result<crate::containers::ContainerInspect, String>>,
86 },
87 ContainerLogsComplete {
91 alias: String,
92 container_id: String,
93 container_name: String,
94 result: Result<Vec<String>, String>,
95 },
96 ContainerLogsTailComplete {
101 alias: String,
102 container_id: String,
103 result: Box<Result<Vec<String>, String>>,
104 },
105 VaultSignResult {
106 alias: String,
107 certificate_file: String,
113 success: bool,
114 message: String,
115 },
116 VaultSignProgress {
117 alias: String,
118 done: usize,
119 total: usize,
120 },
121 VaultSignAllDone {
122 signed: u32,
123 failed: u32,
124 skipped: u32,
125 cancelled: bool,
126 aborted_message: Option<String>,
127 first_error: Option<String>,
128 },
129 CertCheckResult {
130 alias: String,
131 status: crate::vault_ssh::CertStatus,
132 },
133 CertCheckError {
134 alias: String,
135 message: String,
136 },
137 PollError,
138}
139
140pub struct EventHandler {
142 tx: mpsc::Sender<AppEvent>,
143 rx: mpsc::Receiver<AppEvent>,
144 paused: Arc<AtomicBool>,
145 _handle: thread::JoinHandle<()>,
147}
148
149impl EventHandler {
150 pub fn new(tick_rate_ms: u64) -> Self {
151 let (tx, rx) = mpsc::channel();
152 let tick_rate = Duration::from_millis(tick_rate_ms);
153 let event_tx = tx.clone();
154 let paused = Arc::new(AtomicBool::new(false));
155 let paused_flag = paused.clone();
156
157 let handle = thread::spawn(move || {
158 let mut last_tick = Instant::now();
159 loop {
160 if paused_flag.load(Ordering::Acquire) {
162 thread::sleep(Duration::from_millis(50));
163 continue;
164 }
165
166 let remaining = tick_rate
168 .checked_sub(last_tick.elapsed())
169 .unwrap_or(Duration::ZERO);
170 let timeout = remaining.min(Duration::from_millis(50));
171
172 match event::poll(timeout) {
173 Ok(true) => {
174 if let Ok(evt) = event::read() {
175 match evt {
176 CrosstermEvent::Key(key)
177 if key.kind == KeyEventKind::Press
178 && event_tx.send(AppEvent::Key(key)).is_err() =>
179 {
180 return;
181 }
182 CrosstermEvent::Resize(..)
184 if event_tx.send(AppEvent::Tick).is_err() =>
185 {
186 return;
187 }
188 _ => {}
189 }
190 }
191 }
192 Ok(false) => {}
193 Err(_) => {
194 let _ = event_tx.send(AppEvent::PollError);
196 return;
197 }
198 }
199
200 if last_tick.elapsed() >= tick_rate {
201 if event_tx.send(AppEvent::Tick).is_err() {
202 return;
203 }
204 last_tick = Instant::now();
205 }
206 }
207 });
208
209 Self {
210 tx,
211 rx,
212 paused,
213 _handle: handle,
214 }
215 }
216
217 pub fn next(&self) -> Result<AppEvent> {
219 Ok(self.rx.recv()?)
220 }
221
222 pub fn next_timeout(&self, timeout: Duration) -> Result<Option<AppEvent>> {
224 match self.rx.recv_timeout(timeout) {
225 Ok(event) => Ok(Some(event)),
226 Err(mpsc::RecvTimeoutError::Timeout) => Ok(None),
227 Err(mpsc::RecvTimeoutError::Disconnected) => {
228 Err(anyhow::anyhow!("event channel disconnected"))
229 }
230 }
231 }
232
233 pub fn sender(&self) -> mpsc::Sender<AppEvent> {
235 self.tx.clone()
236 }
237
238 pub fn pause(&self) {
240 self.paused.store(true, Ordering::Release);
241 }
242
243 pub fn resume(&self) {
245 let mut preserved = Vec::new();
247 while let Ok(event) = self.rx.try_recv() {
248 match event {
249 AppEvent::PingResult { .. }
250 | AppEvent::SyncComplete { .. }
251 | AppEvent::SyncPartial { .. }
252 | AppEvent::SyncError { .. }
253 | AppEvent::SyncProgress { .. }
254 | AppEvent::UpdateAvailable { .. }
255 | AppEvent::FileBrowserListing { .. }
256 | AppEvent::ScpComplete { .. }
257 | AppEvent::SnippetHostDone { .. }
258 | AppEvent::SnippetAllDone { .. }
259 | AppEvent::SnippetProgress { .. }
260 | AppEvent::ContainerListing { .. }
261 | AppEvent::ContainerActionComplete { .. }
262 | AppEvent::ContainerInspectComplete { .. }
263 | AppEvent::ContainerLogsComplete { .. }
264 | AppEvent::ContainerLogsTailComplete { .. }
265 | AppEvent::VaultSignResult { .. }
266 | AppEvent::VaultSignProgress { .. }
267 | AppEvent::VaultSignAllDone { .. }
268 | AppEvent::CertCheckResult { .. }
269 | AppEvent::CertCheckError { .. } => preserved.push(event),
270 _ => {}
271 }
272 }
273 for event in preserved {
275 let _ = self.tx.send(event);
276 }
277 self.paused.store(false, Ordering::Release);
278 }
279}