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