1mod app;
2mod event;
3mod ui;
4
5use crate::Result;
6use crate::daemon_id::DaemonId;
7use crate::daemon_list::DaemonListEntry;
8use crate::ipc::batch::{StartOptions, StartResult, StopResult};
9use crate::ipc::client::IpcClient;
10use crate::settings::settings;
11use crossterm::{
12 event::{DisableMouseCapture, EnableMouseCapture},
13 execute,
14 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
15};
16use log::LevelFilter;
17use miette::IntoDiagnostic;
18use ratatui::prelude::*;
19use std::io;
20use std::sync::Arc;
21
22pub use app::App;
23
24enum TaskResult {
26 Start {
27 id: DaemonId,
28 result: crate::Result<StartResult>,
29 },
30 Stop {
31 id: DaemonId,
32 result: crate::Result<bool>,
33 },
34 Restart {
35 id: DaemonId,
36 result: crate::Result<StartResult>,
37 },
38 Enable {
39 id: DaemonId,
40 result: crate::Result<bool>,
41 },
42 Disable {
43 id: DaemonId,
44 result: crate::Result<bool>,
45 },
46 BatchStart {
47 count: usize,
48 result: crate::Result<StartResult>,
49 },
50 BatchStop {
51 count: usize,
52 result: crate::Result<StopResult>,
53 },
54 BatchRestart {
55 count: usize,
56 result: crate::Result<StartResult>,
57 },
58 BatchEnable {
59 count: usize,
60 },
61 BatchDisable {
62 count: usize,
63 },
64 Refresh {
65 result: crate::Result<Vec<DaemonListEntry>>,
66 clears_in_flight: bool,
73 },
74 RefreshNetwork(Vec<listeners::Listener>),
75}
76
77pub async fn run() -> Result<()> {
78 let prev_log_level = log::max_level();
80 log::set_max_level(LevelFilter::Off);
81
82 enable_raw_mode().into_diagnostic()?;
84 let mut stdout = io::stdout();
85 execute!(stdout, EnterAlternateScreen, EnableMouseCapture).into_diagnostic()?;
86 let backend = CrosstermBackend::new(stdout);
87 let mut terminal = Terminal::new(backend).into_diagnostic()?;
88
89 let result = run_with_cleanup(&mut terminal).await;
91
92 let _ = disable_raw_mode();
94 let _ = execute!(
95 terminal.backend_mut(),
96 LeaveAlternateScreen,
97 DisableMouseCapture
98 );
99 let _ = terminal.show_cursor();
100
101 log::set_max_level(prev_log_level);
103
104 result
105}
106
107async fn run_with_cleanup(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
108 let client = Arc::new(IpcClient::connect(true).await?);
110
111 let mut app = App::new();
113 app.refresh(&client).await?;
114
115 run_app(terminal, &mut app, &client).await
117}
118
119async fn run_app(
120 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
121 app: &mut App,
122 client: &Arc<IpcClient>,
123) -> Result<()> {
124 let s = settings();
125 let tick_rate = s.tui_tick_rate();
126 let refresh_rate = s.tui_refresh_rate();
127 let mut last_refresh = std::time::Instant::now();
128
129 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<TaskResult>();
130 let mut in_flight = false;
134
135 loop {
136 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
138
139 while let Ok(result) = rx.try_recv() {
141 match result {
142 TaskResult::Start { id, result } => {
143 app.stop_loading();
144 in_flight = false;
145 match result {
146 Ok(r) if r.any_failed => {
147 app.set_message(format!("Failed to start {id}"));
148 }
149 Ok(r) if !r.started.is_empty() => {
150 app.set_message(format!("Started {id}"));
151 }
152 Ok(_) => {
153 app.set_message(format!("No daemons were started for {id}"));
154 }
155 Err(e) => {
156 app.set_message(format!("Failed to start {id}: {e}"));
157 }
158 }
159 spawn_refresh(Arc::clone(client), tx.clone(), false);
160 }
161 TaskResult::Stop { id, result } => {
162 app.stop_loading();
163 in_flight = false;
164 match result {
165 Ok(true) => app.set_message(format!("Stopped {id}")),
166 Ok(false) => app.set_message(format!("Daemon {id} was not running")),
167 Err(e) => app.set_message(format!("Failed to stop {id}: {e}")),
168 }
169 spawn_refresh(Arc::clone(client), tx.clone(), false);
170 }
171 TaskResult::Restart { id, result } => {
172 app.stop_loading();
173 in_flight = false;
174 match result {
175 Ok(r) if r.any_failed => {
176 app.set_message(format!("Failed to restart {id}"));
177 }
178 Ok(_) => {
179 app.set_message(format!("Restarted {id}"));
180 }
181 Err(e) => {
182 app.set_message(format!("Failed to restart {id}: {e}"));
183 }
184 }
185 spawn_refresh(Arc::clone(client), tx.clone(), false);
186 }
187 TaskResult::Enable { id, result } => {
188 app.stop_loading();
189 in_flight = false;
190 match result {
191 Ok(_) => app.set_message(format!("Enabled {id}")),
192 Err(e) => app.set_message(format!("Failed to enable {id}: {e}")),
193 }
194 spawn_refresh(Arc::clone(client), tx.clone(), false);
195 }
196 TaskResult::Disable { id, result } => {
197 app.stop_loading();
198 in_flight = false;
199 match result {
200 Ok(_) => app.set_message(format!("Disabled {id}")),
201 Err(e) => app.set_message(format!("Failed to disable {id}: {e}")),
202 }
203 spawn_refresh(Arc::clone(client), tx.clone(), false);
204 }
205 TaskResult::BatchStart { count, result } => {
206 app.stop_loading();
207 in_flight = false;
208 app.clear_selection();
209 match result {
210 Ok(r) => {
211 let started = r.started.len();
212 if r.any_failed {
213 app.set_message(format!(
214 "Started {started}/{count} daemons (some failed)"
215 ));
216 } else {
217 app.set_message(format!("Started {started} daemons"));
218 }
219 }
220 Err(e) => {
221 app.set_message(format!("Failed to start daemons: {e}"));
222 }
223 }
224 spawn_refresh(Arc::clone(client), tx.clone(), false);
225 }
226 TaskResult::BatchStop { count, result } => {
227 app.stop_loading();
228 in_flight = false;
229 app.clear_selection();
230 match result {
231 Ok(r) if r.any_failed => {
232 app.set_message(format!("Stopped {count} daemons (some failed)"));
233 }
234 Ok(_) => {
235 app.set_message(format!("Stopped {count} daemons"));
236 }
237 Err(e) => {
238 app.set_message(format!("Failed to stop daemons: {e}"));
239 }
240 }
241 spawn_refresh(Arc::clone(client), tx.clone(), false);
242 }
243 TaskResult::BatchRestart { count, result } => {
244 app.stop_loading();
245 in_flight = false;
246 app.clear_selection();
247 match result {
248 Ok(r) => {
249 let restarted = r.started.len();
250 if r.any_failed {
251 app.set_message(format!(
252 "Restarted {restarted}/{count} daemons (some failed)"
253 ));
254 } else {
255 app.set_message(format!("Restarted {restarted} daemons"));
256 }
257 }
258 Err(e) => {
259 app.set_message(format!("Failed to restart daemons: {e}"));
260 }
261 }
262 spawn_refresh(Arc::clone(client), tx.clone(), false);
263 }
264 TaskResult::BatchEnable { count } => {
265 app.stop_loading();
266 in_flight = false;
267 app.clear_selection();
268 app.set_message(format!("Enabled {count} daemons"));
269 spawn_refresh(Arc::clone(client), tx.clone(), false);
270 }
271 TaskResult::BatchDisable { count } => {
272 app.stop_loading();
273 in_flight = false;
274 app.clear_selection();
275 app.set_message(format!("Disabled {count} daemons"));
276 spawn_refresh(Arc::clone(client), tx.clone(), false);
277 }
278 TaskResult::Refresh {
279 result,
280 clears_in_flight,
281 } => {
282 if clears_in_flight {
283 app.stop_loading();
284 in_flight = false;
285 }
286 match result {
287 Ok(entries) => app.apply_refresh(entries),
288 Err(e) => app.set_message(format!("Refresh failed: {e}")),
289 }
290 last_refresh = std::time::Instant::now();
291 }
292 TaskResult::RefreshNetwork(listeners) => {
293 app.apply_network_refresh(listeners);
294 }
295 }
296 }
297
298 if crossterm::event::poll(tick_rate).into_diagnostic()?
300 && let Some(action) = event::handle_event(app)?
301 {
302 match action {
303 event::Action::Quit => break,
304 event::Action::Start(id) if !in_flight => {
305 in_flight = true;
306 app.start_loading(format!("Starting {id}..."));
307 let client = Arc::clone(client);
308 let tx = tx.clone();
309 tokio::spawn(async move {
310 let result = client
311 .start_daemons(
312 std::slice::from_ref(&id),
313 StartOptions {
314 quiet: true,
315 ..StartOptions::default()
316 },
317 )
318 .await;
319 let _ = tx.send(TaskResult::Start { id, result });
320 });
321 }
322 event::Action::Enable(id) if !in_flight => {
323 in_flight = true;
324 app.start_loading(format!("Enabling {id}..."));
325 let client = Arc::clone(client);
326 let tx = tx.clone();
327 tokio::spawn(async move {
328 let result = client.enable(id.clone()).await;
329 let _ = tx.send(TaskResult::Enable { id, result });
330 });
331 }
332 event::Action::BatchStart(ids) if !in_flight => {
333 let count = ids.len();
334 in_flight = true;
335 app.start_loading(format!("Starting {count} daemons..."));
336 let client = Arc::clone(client);
337 let tx = tx.clone();
338 tokio::spawn(async move {
339 let result = client
340 .start_daemons(
341 &ids,
342 StartOptions {
343 quiet: true,
344 ..StartOptions::default()
345 },
346 )
347 .await;
348 let _ = tx.send(TaskResult::BatchStart { count, result });
349 });
350 }
351 event::Action::BatchEnable(ids) if !in_flight => {
352 let count = ids.len();
353 in_flight = true;
354 app.start_loading(format!("Enabling {count} daemons..."));
355 let client = Arc::clone(client);
356 let tx = tx.clone();
357 tokio::spawn(async move {
358 for id in &ids {
359 let _ = client.enable(id.clone()).await;
360 }
361 let _ = tx.send(TaskResult::BatchEnable { count });
362 });
363 }
364 event::Action::Refresh if !in_flight => {
365 in_flight = true;
366 app.start_loading("Refreshing...");
367 spawn_refresh(Arc::clone(client), tx.clone(), true);
368 }
369 event::Action::OpenEditorNew => {
370 app.open_file_selector();
371 }
372 event::Action::OpenEditorEdit(id) => {
373 app.open_editor_edit(&id);
374 }
375 event::Action::SaveConfig => {
376 app.start_loading("Saving...");
377 match app.save_editor_config() {
378 Ok(true) => {
379 app.stop_loading();
380 app.close_editor();
381 spawn_refresh(Arc::clone(client), tx.clone(), false);
382 }
383 Ok(false) => {
384 app.stop_loading();
385 }
386 Err(e) => {
387 app.stop_loading();
388 app.set_message(format!("Save failed: {e}"));
389 }
390 }
391 }
392 event::Action::DeleteDaemon { id, config_path } => {
393 app.confirm_action(app::PendingAction::DeleteDaemon { id, config_path });
394 }
395 event::Action::ConfirmPending if !in_flight => {
396 if let Some(pending) = app.take_pending_action() {
397 match pending {
398 app::PendingAction::Stop(id) => {
399 in_flight = true;
400 app.start_loading(format!("Stopping {id}..."));
401 let client = Arc::clone(client);
402 let tx = tx.clone();
403 tokio::spawn(async move {
404 let result = client.stop(id.clone()).await;
405 let _ = tx.send(TaskResult::Stop { id, result });
406 });
407 }
408 app::PendingAction::Restart(id) => {
409 in_flight = true;
410 app.start_loading(format!("Restarting {id}..."));
411 let client = Arc::clone(client);
412 let tx = tx.clone();
413 tokio::spawn(async move {
414 let opts = StartOptions {
415 force: true,
416 quiet: true,
417 ..Default::default()
418 };
419 let result =
420 client.start_daemons(std::slice::from_ref(&id), opts).await;
421 let _ = tx.send(TaskResult::Restart { id, result });
422 });
423 }
424 app::PendingAction::Disable(id) => {
425 in_flight = true;
426 app.start_loading(format!("Disabling {id}..."));
427 let client = Arc::clone(client);
428 let tx = tx.clone();
429 tokio::spawn(async move {
430 let result = client.disable(id.clone()).await;
431 let _ = tx.send(TaskResult::Disable { id, result });
432 });
433 }
434 app::PendingAction::BatchStop(ids) => {
435 let count = ids.len();
436 in_flight = true;
437 app.start_loading(format!("Stopping {count} daemons..."));
438 let client = Arc::clone(client);
439 let tx = tx.clone();
440 tokio::spawn(async move {
441 let result = client.stop_daemons(&ids).await;
442 let _ = tx.send(TaskResult::BatchStop { count, result });
443 });
444 }
445 app::PendingAction::BatchRestart(ids) => {
446 let count = ids.len();
447 in_flight = true;
448 app.start_loading(format!("Restarting {count} daemons..."));
449 let client = Arc::clone(client);
450 let tx = tx.clone();
451 tokio::spawn(async move {
452 let opts = StartOptions {
453 force: true,
454 quiet: true,
455 ..Default::default()
456 };
457 let result = client.start_daemons(&ids, opts).await;
458 let _ = tx.send(TaskResult::BatchRestart { count, result });
459 });
460 }
461 app::PendingAction::BatchDisable(ids) => {
462 let count = ids.len();
463 in_flight = true;
464 app.start_loading(format!("Disabling {count} daemons..."));
465 let client = Arc::clone(client);
466 let tx = tx.clone();
467 tokio::spawn(async move {
468 for id in &ids {
469 let _ = client.disable(id.clone()).await;
470 }
471 let _ = tx.send(TaskResult::BatchDisable { count });
472 });
473 }
474 app::PendingAction::DeleteDaemon { id, config_path } => {
475 app.start_loading(format!("Deleting {id}..."));
476 match app.delete_daemon_from_config(&id, &config_path) {
477 Ok(true) => {
478 app.stop_loading();
479 app.close_editor();
480 app.set_message(format!("Deleted {id}"));
481 }
482 Ok(false) => {
483 app.stop_loading();
484 app.set_message(format!(
485 "Daemon '{id}' not found in config"
486 ));
487 }
488 Err(e) => {
489 app.stop_loading();
490 app.set_message(format!("Delete failed: {e}"));
491 }
492 }
493 spawn_refresh(Arc::clone(client), tx.clone(), false);
494 }
495 app::PendingAction::DiscardEditorChanges => {
496 app.close_editor();
497 }
498 }
499 }
500 }
501 _ => {}
503 }
504 }
505
506 if last_refresh.elapsed() >= refresh_rate && !in_flight {
508 let is_network = app.view == app::View::Network;
509 let client_ref = Arc::clone(client);
510 let tx_ref = tx.clone();
511 tokio::spawn(async move {
512 let entries = App::fetch_daemon_data(&client_ref).await;
513 let _ = tx_ref.send(TaskResult::Refresh {
514 result: entries,
515 clears_in_flight: false,
516 });
517 if is_network {
518 let listeners = tokio::task::spawn_blocking(|| {
519 listeners::get_all()
520 .map(|set| set.into_iter().collect::<Vec<_>>())
521 .unwrap_or_default()
522 })
523 .await
524 .unwrap_or_default();
525 let _ = tx_ref.send(TaskResult::RefreshNetwork(listeners));
526 }
527 });
528 last_refresh = std::time::Instant::now();
530 }
531 }
532
533 Ok(())
534}
535
536fn spawn_refresh(
537 client: Arc<IpcClient>,
538 tx: tokio::sync::mpsc::UnboundedSender<TaskResult>,
539 clears_in_flight: bool,
540) {
541 tokio::spawn(async move {
542 let entries = App::fetch_daemon_data(&client).await;
543 let _ = tx.send(TaskResult::Refresh {
544 result: entries,
545 clears_in_flight,
546 });
547 });
548}