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<
68 (
69 crate::containers::ContainerRuntime,
70 Vec<crate::containers::ContainerInfo>,
71 ),
72 crate::containers::ContainerError,
73 >,
74 },
75 ContainerActionComplete {
76 alias: String,
77 action: crate::containers::ContainerAction,
78 result: Result<(), String>,
79 },
80 VaultSignResult {
81 alias: String,
82 certificate_file: String,
88 success: bool,
89 message: String,
90 },
91 VaultSignProgress {
92 alias: String,
93 done: usize,
94 total: usize,
95 },
96 VaultSignAllDone {
97 signed: u32,
98 failed: u32,
99 skipped: u32,
100 cancelled: bool,
101 aborted_message: Option<String>,
102 first_error: Option<String>,
103 },
104 CertCheckResult {
105 alias: String,
106 status: crate::vault_ssh::CertStatus,
107 },
108 CertCheckError {
109 alias: String,
110 message: String,
111 },
112 PollError,
113}
114
115pub struct EventHandler {
117 tx: mpsc::Sender<AppEvent>,
118 rx: mpsc::Receiver<AppEvent>,
119 paused: Arc<AtomicBool>,
120 _handle: thread::JoinHandle<()>,
122}
123
124impl EventHandler {
125 pub fn new(tick_rate_ms: u64) -> Self {
126 let (tx, rx) = mpsc::channel();
127 let tick_rate = Duration::from_millis(tick_rate_ms);
128 let event_tx = tx.clone();
129 let paused = Arc::new(AtomicBool::new(false));
130 let paused_flag = paused.clone();
131
132 let handle = thread::spawn(move || {
133 let mut last_tick = Instant::now();
134 loop {
135 if paused_flag.load(Ordering::Acquire) {
137 thread::sleep(Duration::from_millis(50));
138 continue;
139 }
140
141 let remaining = tick_rate
143 .checked_sub(last_tick.elapsed())
144 .unwrap_or(Duration::ZERO);
145 let timeout = remaining.min(Duration::from_millis(50));
146
147 match event::poll(timeout) {
148 Ok(true) => {
149 if let Ok(evt) = event::read() {
150 match evt {
151 CrosstermEvent::Key(key)
152 if key.kind == KeyEventKind::Press
153 && event_tx.send(AppEvent::Key(key)).is_err() =>
154 {
155 return;
156 }
157 CrosstermEvent::Resize(..)
159 if event_tx.send(AppEvent::Tick).is_err() =>
160 {
161 return;
162 }
163 _ => {}
164 }
165 }
166 }
167 Ok(false) => {}
168 Err(_) => {
169 let _ = event_tx.send(AppEvent::PollError);
171 return;
172 }
173 }
174
175 if last_tick.elapsed() >= tick_rate {
176 if event_tx.send(AppEvent::Tick).is_err() {
177 return;
178 }
179 last_tick = Instant::now();
180 }
181 }
182 });
183
184 Self {
185 tx,
186 rx,
187 paused,
188 _handle: handle,
189 }
190 }
191
192 pub fn next(&self) -> Result<AppEvent> {
194 Ok(self.rx.recv()?)
195 }
196
197 pub fn next_timeout(&self, timeout: Duration) -> Result<Option<AppEvent>> {
199 match self.rx.recv_timeout(timeout) {
200 Ok(event) => Ok(Some(event)),
201 Err(mpsc::RecvTimeoutError::Timeout) => Ok(None),
202 Err(mpsc::RecvTimeoutError::Disconnected) => {
203 Err(anyhow::anyhow!("event channel disconnected"))
204 }
205 }
206 }
207
208 pub fn sender(&self) -> mpsc::Sender<AppEvent> {
210 self.tx.clone()
211 }
212
213 pub fn pause(&self) {
215 self.paused.store(true, Ordering::Release);
216 }
217
218 pub fn resume(&self) {
220 let mut preserved = Vec::new();
222 while let Ok(event) = self.rx.try_recv() {
223 match event {
224 AppEvent::PingResult { .. }
225 | AppEvent::SyncComplete { .. }
226 | AppEvent::SyncPartial { .. }
227 | AppEvent::SyncError { .. }
228 | AppEvent::SyncProgress { .. }
229 | AppEvent::UpdateAvailable { .. }
230 | AppEvent::FileBrowserListing { .. }
231 | AppEvent::ScpComplete { .. }
232 | AppEvent::SnippetHostDone { .. }
233 | AppEvent::SnippetAllDone { .. }
234 | AppEvent::SnippetProgress { .. }
235 | AppEvent::ContainerListing { .. }
236 | AppEvent::ContainerActionComplete { .. }
237 | AppEvent::VaultSignResult { .. }
238 | AppEvent::VaultSignProgress { .. }
239 | AppEvent::VaultSignAllDone { .. }
240 | AppEvent::CertCheckResult { .. }
241 | AppEvent::CertCheckError { .. } => preserved.push(event),
242 _ => {}
243 }
244 }
245 for event in preserved {
247 let _ = self.tx.send(event);
248 }
249 self.paused.store(false, Ordering::Release);
250 }
251}