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(e) => {
194 log::error!("[external] crossterm poll failed: {e}");
196 let _ = event_tx.send(AppEvent::PollError);
197 return;
198 }
199 }
200
201 if last_tick.elapsed() >= tick_rate {
202 if event_tx.send(AppEvent::Tick).is_err() {
203 return;
204 }
205 last_tick = Instant::now();
206 }
207 }
208 });
209
210 Self {
211 tx,
212 rx,
213 paused,
214 _handle: handle,
215 }
216 }
217
218 pub fn next(&self) -> Result<AppEvent> {
220 Ok(self.rx.recv()?)
221 }
222
223 pub fn next_timeout(&self, timeout: Duration) -> Result<Option<AppEvent>> {
225 match self.rx.recv_timeout(timeout) {
226 Ok(event) => Ok(Some(event)),
227 Err(mpsc::RecvTimeoutError::Timeout) => Ok(None),
228 Err(mpsc::RecvTimeoutError::Disconnected) => {
229 Err(anyhow::anyhow!("event channel disconnected"))
230 }
231 }
232 }
233
234 pub fn sender(&self) -> mpsc::Sender<AppEvent> {
236 self.tx.clone()
237 }
238
239 pub fn pause(&self) {
241 self.paused.store(true, Ordering::Release);
242 }
243
244 pub fn resume(&self) {
246 let mut preserved = Vec::new();
248 while let Ok(event) = self.rx.try_recv() {
249 match event {
250 AppEvent::PingResult { .. }
251 | AppEvent::SyncComplete { .. }
252 | AppEvent::SyncPartial { .. }
253 | AppEvent::SyncError { .. }
254 | AppEvent::SyncProgress { .. }
255 | AppEvent::UpdateAvailable { .. }
256 | AppEvent::FileBrowserListing { .. }
257 | AppEvent::ScpComplete { .. }
258 | AppEvent::SnippetHostDone { .. }
259 | AppEvent::SnippetAllDone { .. }
260 | AppEvent::SnippetProgress { .. }
261 | AppEvent::ContainerListing { .. }
262 | AppEvent::ContainerActionComplete { .. }
263 | AppEvent::ContainerInspectComplete { .. }
264 | AppEvent::ContainerLogsComplete { .. }
265 | AppEvent::ContainerLogsTailComplete { .. }
266 | AppEvent::VaultSignResult { .. }
267 | AppEvent::VaultSignProgress { .. }
268 | AppEvent::VaultSignAllDone { .. }
269 | AppEvent::CertCheckResult { .. }
270 | AppEvent::CertCheckError { .. } => preserved.push(event),
271 _ => {}
272 }
273 }
274 for event in preserved {
276 let _ = self.tx.send(event);
277 }
278 self.paused.store(false, Ordering::Release);
279 }
280}