1mod app;
2mod event;
3mod ui;
4
5use crate::Result;
6use crate::daemon_id::DaemonId;
7use crate::daemon_list::{DaemonListEntry, NamespaceFilter};
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 Clean {
65 result: crate::Result<u64>,
66 },
67 Refresh {
68 result: crate::Result<Vec<DaemonListEntry>>,
69 clears_in_flight: bool,
76 },
77 RefreshNetwork(Vec<listeners::Listener>),
78}
79
80pub async fn run(namespace_filter: NamespaceFilter) -> Result<()> {
81 let prev_log_level = log::max_level();
83 log::set_max_level(LevelFilter::Off);
84
85 enable_raw_mode().into_diagnostic()?;
87 let mut stdout = io::stdout();
88 execute!(stdout, EnterAlternateScreen, EnableMouseCapture).into_diagnostic()?;
89 let backend = CrosstermBackend::new(stdout);
90 let mut terminal = Terminal::new(backend).into_diagnostic()?;
91
92 let result = run_with_cleanup(&mut terminal, namespace_filter).await;
94
95 let _ = disable_raw_mode();
97 let _ = execute!(
98 terminal.backend_mut(),
99 LeaveAlternateScreen,
100 DisableMouseCapture
101 );
102 let _ = terminal.show_cursor();
103
104 log::set_max_level(prev_log_level);
106
107 result
108}
109
110async fn run_with_cleanup(
111 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
112 namespace_filter: NamespaceFilter,
113) -> Result<()> {
114 let client = Arc::new(IpcClient::connect(true).await?);
116
117 let mut app = App::new(namespace_filter);
119 app.refresh(&client).await?;
120
121 run_app(terminal, &mut app, &client).await
123}
124
125async fn run_app(
126 terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
127 app: &mut App,
128 client: &Arc<IpcClient>,
129) -> Result<()> {
130 let s = settings();
131 let tick_rate = s.tui_tick_rate();
132 let refresh_rate = s.tui_refresh_rate();
133 let mut last_refresh = std::time::Instant::now();
134
135 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<TaskResult>();
136 let mut in_flight = false;
140
141 loop {
142 terminal.draw(|f| ui::draw(f, app)).into_diagnostic()?;
144
145 while let Ok(result) = rx.try_recv() {
147 match result {
148 TaskResult::Start { id, result } => {
149 app.stop_loading();
150 in_flight = false;
151 match result {
152 Ok(r) if r.any_failed => {
153 app.set_message(format!("Failed to start {id}"));
154 }
155 Ok(r) if !r.started.is_empty() => {
156 app.set_message(format!("Started {id}"));
157 }
158 Ok(_) => {
159 app.set_message(format!("No daemons were started for {id}"));
160 }
161 Err(e) => {
162 app.set_message(format!("Failed to start {id}: {e}"));
163 }
164 }
165 spawn_refresh(
166 Arc::clone(client),
167 tx.clone(),
168 false,
169 app.namespace_filter.clone(),
170 );
171 }
172 TaskResult::Stop { id, result } => {
173 app.stop_loading();
174 in_flight = false;
175 match result {
176 Ok(true) => app.set_message(format!("Stopped {id}")),
177 Ok(false) => app.set_message(format!("Daemon {id} was not running")),
178 Err(e) => app.set_message(format!("Failed to stop {id}: {e}")),
179 }
180 spawn_refresh(
181 Arc::clone(client),
182 tx.clone(),
183 false,
184 app.namespace_filter.clone(),
185 );
186 }
187 TaskResult::Restart { id, result } => {
188 app.stop_loading();
189 in_flight = false;
190 match result {
191 Ok(r) if r.any_failed => {
192 app.set_message(format!("Failed to restart {id}"));
193 }
194 Ok(_) => {
195 app.set_message(format!("Restarted {id}"));
196 }
197 Err(e) => {
198 app.set_message(format!("Failed to restart {id}: {e}"));
199 }
200 }
201 spawn_refresh(
202 Arc::clone(client),
203 tx.clone(),
204 false,
205 app.namespace_filter.clone(),
206 );
207 }
208 TaskResult::Enable { id, result } => {
209 app.stop_loading();
210 in_flight = false;
211 match result {
212 Ok(_) => app.set_message(format!("Enabled {id}")),
213 Err(e) => app.set_message(format!("Failed to enable {id}: {e}")),
214 }
215 spawn_refresh(
216 Arc::clone(client),
217 tx.clone(),
218 false,
219 app.namespace_filter.clone(),
220 );
221 }
222 TaskResult::Disable { id, result } => {
223 app.stop_loading();
224 in_flight = false;
225 match result {
226 Ok(_) => app.set_message(format!("Disabled {id}")),
227 Err(e) => app.set_message(format!("Failed to disable {id}: {e}")),
228 }
229 spawn_refresh(
230 Arc::clone(client),
231 tx.clone(),
232 false,
233 app.namespace_filter.clone(),
234 );
235 }
236 TaskResult::BatchStart { count, result } => {
237 app.stop_loading();
238 in_flight = false;
239 app.clear_selection();
240 match result {
241 Ok(r) => {
242 let started = r.started.len();
243 if r.any_failed {
244 app.set_message(format!(
245 "Started {started}/{count} daemons (some failed)"
246 ));
247 } else {
248 app.set_message(format!("Started {started} daemons"));
249 }
250 }
251 Err(e) => {
252 app.set_message(format!("Failed to start daemons: {e}"));
253 }
254 }
255 spawn_refresh(
256 Arc::clone(client),
257 tx.clone(),
258 false,
259 app.namespace_filter.clone(),
260 );
261 }
262 TaskResult::BatchStop { count, result } => {
263 app.stop_loading();
264 in_flight = false;
265 app.clear_selection();
266 match result {
267 Ok(r) if r.any_failed => {
268 app.set_message(format!("Stopped {count} daemons (some failed)"));
269 }
270 Ok(_) => {
271 app.set_message(format!("Stopped {count} daemons"));
272 }
273 Err(e) => {
274 app.set_message(format!("Failed to stop daemons: {e}"));
275 }
276 }
277 spawn_refresh(
278 Arc::clone(client),
279 tx.clone(),
280 false,
281 app.namespace_filter.clone(),
282 );
283 }
284 TaskResult::BatchRestart { count, result } => {
285 app.stop_loading();
286 in_flight = false;
287 app.clear_selection();
288 match result {
289 Ok(r) => {
290 let restarted = r.started.len();
291 if r.any_failed {
292 app.set_message(format!(
293 "Restarted {restarted}/{count} daemons (some failed)"
294 ));
295 } else {
296 app.set_message(format!("Restarted {restarted} daemons"));
297 }
298 }
299 Err(e) => {
300 app.set_message(format!("Failed to restart daemons: {e}"));
301 }
302 }
303 spawn_refresh(
304 Arc::clone(client),
305 tx.clone(),
306 false,
307 app.namespace_filter.clone(),
308 );
309 }
310 TaskResult::BatchEnable { count } => {
311 app.stop_loading();
312 in_flight = false;
313 app.clear_selection();
314 app.set_message(format!("Enabled {count} daemons"));
315 spawn_refresh(
316 Arc::clone(client),
317 tx.clone(),
318 false,
319 app.namespace_filter.clone(),
320 );
321 }
322 TaskResult::BatchDisable { count } => {
323 app.stop_loading();
324 in_flight = false;
325 app.clear_selection();
326 app.set_message(format!("Disabled {count} daemons"));
327 spawn_refresh(
328 Arc::clone(client),
329 tx.clone(),
330 false,
331 app.namespace_filter.clone(),
332 );
333 }
334 TaskResult::Clean { result } => {
335 app.stop_loading();
336 in_flight = false;
337 match result {
338 Ok(count) => app.set_message(format!(
339 "Removed {count} stopped/failed daemon registration(s)"
340 )),
341 Err(e) => app.set_message(format!("Remove failed: {e}")),
342 }
343 app.clear_selection();
344 spawn_refresh(
345 Arc::clone(client),
346 tx.clone(),
347 false,
348 app.namespace_filter.clone(),
349 );
350 }
351 TaskResult::Refresh {
352 result,
353 clears_in_flight,
354 } => {
355 if clears_in_flight {
356 app.stop_loading();
357 in_flight = false;
358 }
359 match result {
360 Ok(entries) => app.apply_refresh(entries),
361 Err(e) => app.set_message(format!("Refresh failed: {e}")),
362 }
363 last_refresh = std::time::Instant::now();
364 }
365 TaskResult::RefreshNetwork(listeners) => {
366 app.apply_network_refresh(listeners);
367 }
368 }
369 }
370
371 if crossterm::event::poll(tick_rate).into_diagnostic()?
373 && let Some(action) = event::handle_event(app)?
374 {
375 match action {
376 event::Action::Quit => break,
377 event::Action::Start(id) if !in_flight => {
378 in_flight = true;
379 app.start_loading(format!("Starting {id}..."));
380 let client = Arc::clone(client);
381 let tx = tx.clone();
382 tokio::spawn(async move {
383 let result = client
384 .start_daemons(
385 std::slice::from_ref(&id),
386 StartOptions {
387 quiet: true,
388 ..StartOptions::default()
389 },
390 )
391 .await;
392 let _ = tx.send(TaskResult::Start { id, result });
393 });
394 }
395 event::Action::Enable(id) if !in_flight => {
396 in_flight = true;
397 app.start_loading(format!("Enabling {id}..."));
398 let client = Arc::clone(client);
399 let tx = tx.clone();
400 tokio::spawn(async move {
401 let result = client.enable(id.clone()).await;
402 let _ = tx.send(TaskResult::Enable { id, result });
403 });
404 }
405 event::Action::BatchStart(ids) if !in_flight => {
406 let count = ids.len();
407 in_flight = true;
408 app.start_loading(format!("Starting {count} daemons..."));
409 let client = Arc::clone(client);
410 let tx = tx.clone();
411 tokio::spawn(async move {
412 let result = client
413 .start_daemons(
414 &ids,
415 StartOptions {
416 quiet: true,
417 ..StartOptions::default()
418 },
419 )
420 .await;
421 let _ = tx.send(TaskResult::BatchStart { count, result });
422 });
423 }
424 event::Action::BatchEnable(ids) if !in_flight => {
425 let count = ids.len();
426 in_flight = true;
427 app.start_loading(format!("Enabling {count} daemons..."));
428 let client = Arc::clone(client);
429 let tx = tx.clone();
430 tokio::spawn(async move {
431 for id in &ids {
432 let _ = client.enable(id.clone()).await;
433 }
434 let _ = tx.send(TaskResult::BatchEnable { count });
435 });
436 }
437 event::Action::Refresh if !in_flight => {
438 in_flight = true;
439 app.start_loading("Refreshing...");
440 spawn_refresh(
441 Arc::clone(client),
442 tx.clone(),
443 true,
444 app.namespace_filter.clone(),
445 );
446 }
447 event::Action::OpenEditorNew => {
448 app.open_file_selector();
449 }
450 event::Action::OpenEditorEdit(id) => {
451 app.open_editor_edit(&id);
452 }
453 event::Action::SaveConfig => {
454 app.start_loading("Saving...");
455 match app.save_editor_config() {
456 Ok(true) => {
457 app.stop_loading();
458 app.close_editor();
459 spawn_refresh(
460 Arc::clone(client),
461 tx.clone(),
462 false,
463 app.namespace_filter.clone(),
464 );
465 }
466 Ok(false) => {
467 app.stop_loading();
468 }
469 Err(e) => {
470 app.stop_loading();
471 app.set_message(format!("Save failed: {e}"));
472 }
473 }
474 }
475 event::Action::DeleteDaemon { id, config_path } => {
476 app.confirm_action(app::PendingAction::DeleteDaemon { id, config_path });
477 }
478 event::Action::ConfirmPending if !in_flight => {
479 if let Some(pending) = app.take_pending_action() {
480 match pending {
481 app::PendingAction::Stop(id) => {
482 in_flight = true;
483 app.start_loading(format!("Stopping {id}..."));
484 let client = Arc::clone(client);
485 let tx = tx.clone();
486 tokio::spawn(async move {
487 let result = client.stop(id.clone()).await;
488 let _ = tx.send(TaskResult::Stop { id, result });
489 });
490 }
491 app::PendingAction::Restart(id) => {
492 in_flight = true;
493 app.start_loading(format!("Restarting {id}..."));
494 let client = Arc::clone(client);
495 let tx = tx.clone();
496 tokio::spawn(async move {
497 let opts = StartOptions {
498 force: true,
499 quiet: true,
500 ..Default::default()
501 };
502 let result =
503 client.start_daemons(std::slice::from_ref(&id), opts).await;
504 let _ = tx.send(TaskResult::Restart { id, result });
505 });
506 }
507 app::PendingAction::Disable(id) => {
508 in_flight = true;
509 app.start_loading(format!("Disabling {id}..."));
510 let client = Arc::clone(client);
511 let tx = tx.clone();
512 tokio::spawn(async move {
513 let result = client.disable(id.clone()).await;
514 let _ = tx.send(TaskResult::Disable { id, result });
515 });
516 }
517 app::PendingAction::BatchStop(ids) => {
518 let count = ids.len();
519 in_flight = true;
520 app.start_loading(format!("Stopping {count} daemons..."));
521 let client = Arc::clone(client);
522 let tx = tx.clone();
523 tokio::spawn(async move {
524 let result = client.stop_daemons(&ids).await;
525 let _ = tx.send(TaskResult::BatchStop { count, result });
526 });
527 }
528 app::PendingAction::BatchRestart(ids) => {
529 let count = ids.len();
530 in_flight = true;
531 app.start_loading(format!("Restarting {count} daemons..."));
532 let client = Arc::clone(client);
533 let tx = tx.clone();
534 tokio::spawn(async move {
535 let opts = StartOptions {
536 force: true,
537 quiet: true,
538 ..Default::default()
539 };
540 let result = client.start_daemons(&ids, opts).await;
541 let _ = tx.send(TaskResult::BatchRestart { count, result });
542 });
543 }
544 app::PendingAction::BatchDisable(ids) => {
545 let count = ids.len();
546 in_flight = true;
547 app.start_loading(format!("Disabling {count} daemons..."));
548 let client = Arc::clone(client);
549 let tx = tx.clone();
550 tokio::spawn(async move {
551 for id in &ids {
552 let _ = client.disable(id.clone()).await;
553 }
554 let _ = tx.send(TaskResult::BatchDisable { count });
555 });
556 }
557 app::PendingAction::Clean(ids) => {
558 in_flight = true;
559 app.start_loading(format!(
560 "Removing {} daemon registration(s)...",
561 ids.len()
562 ));
563 let client = Arc::clone(client);
564 let tx = tx.clone();
565 tokio::spawn(async move {
566 let result = client.clean_filtered(vec![], ids, false).await;
567 let _ = tx.send(TaskResult::Clean { result });
568 });
569 }
570 app::PendingAction::DeleteDaemon { id, config_path } => {
571 app.start_loading(format!("Deleting {id}..."));
572 match app.delete_daemon_from_config(&id, &config_path) {
573 Ok(true) => {
574 app.stop_loading();
575 app.close_editor();
576 app.set_message(format!("Deleted {id}"));
577 }
578 Ok(false) => {
579 app.stop_loading();
580 app.set_message(format!(
581 "Daemon '{id}' not found in config"
582 ));
583 }
584 Err(e) => {
585 app.stop_loading();
586 app.set_message(format!("Delete failed: {e}"));
587 }
588 }
589 spawn_refresh(
590 Arc::clone(client),
591 tx.clone(),
592 false,
593 app.namespace_filter.clone(),
594 );
595 }
596 app::PendingAction::DiscardEditorChanges => {
597 app.close_editor();
598 }
599 }
600 }
601 }
602 _ => {}
604 }
605 }
606
607 if last_refresh.elapsed() >= refresh_rate && !in_flight {
609 let is_network = app.view == app::View::Network;
610 let client_ref = Arc::clone(client);
611 let tx_ref = tx.clone();
612 let ns_filter = app.namespace_filter.clone();
613 tokio::spawn(async move {
614 let entries = App::fetch_daemon_data(&client_ref, &ns_filter).await;
615 let _ = tx_ref.send(TaskResult::Refresh {
616 result: entries,
617 clears_in_flight: false,
618 });
619 if is_network {
620 let listeners = tokio::task::spawn_blocking(|| {
621 listeners::get_all()
622 .map(|set| set.into_iter().collect::<Vec<_>>())
623 .unwrap_or_default()
624 })
625 .await
626 .unwrap_or_default();
627 let _ = tx_ref.send(TaskResult::RefreshNetwork(listeners));
628 }
629 });
630 last_refresh = std::time::Instant::now();
632 }
633 }
634
635 Ok(())
636}
637
638fn spawn_refresh(
639 client: Arc<IpcClient>,
640 tx: tokio::sync::mpsc::UnboundedSender<TaskResult>,
641 clears_in_flight: bool,
642 namespace_filter: NamespaceFilter,
643) {
644 tokio::spawn(async move {
645 let entries = App::fetch_daemon_data(&client, &namespace_filter).await;
646 let _ = tx.send(TaskResult::Refresh {
647 result: entries,
648 clears_in_flight,
649 });
650 });
651}