diff --git a/src/main.rs b/src/main.rs index 561f297..25edc31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,7 +30,7 @@ use crate::app::{App, AppAction}; use crate::config::ProcessConfig; use crate::events::{Event, ProcessSignal}; use crate::output::StreamKind; -use crate::process::{ProcessSpec, ProcessState}; +use crate::process::{ProcessSpec, ProcessState, ProcessStatus}; use crate::runner::{ProcessManager, ShutdownConfig}; #[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] @@ -1540,7 +1540,9 @@ async fn handle_exit_policy( *result = Err(anyhow!("last process failed")); } } - app.should_quit = true; + if settings.no_ui { + app.should_quit = true; + } } } SuccessPolicy::All => { @@ -1548,7 +1550,9 @@ async fn handle_exit_policy( if output_state.any_failed() { *result = Err(anyhow!("one or more processes failed")); } - app.should_quit = true; + if settings.no_ui { + app.should_quit = true; + } } } } @@ -1563,12 +1567,22 @@ async fn handle_app_action( ) { match action { AppAction::Quit => { - app.should_quit = false; - let _ = event_tx - .send(Event::Shutdown { - signal: ProcessSignal::SigInt, - }) - .await; + let all_stopped = app.processes.iter().all(|process| { + matches!( + process.status, + ProcessStatus::Idle | ProcessStatus::Exited { .. } | ProcessStatus::Failed { .. } + ) + }); + if all_stopped { + app.should_quit = true; + } else { + app.should_quit = false; + let _ = event_tx + .send(Event::Shutdown { + signal: ProcessSignal::SigInt, + }) + .await; + } } AppAction::Kill(id) => { manager @@ -1718,6 +1732,92 @@ mod tests { assert_eq!(backoff_delay(1, &settings), Duration::from_millis(250)); } + fn test_spec(name: &str) -> ProcessSpec { + ProcessSpec { + name: name.to_string(), + cmd: "echo".to_string(), + args: vec!["ok".to_string()], + cwd: None, + color: None, + env: HashMap::new(), + restart_on_fail: false, + follow: true, + pre_cmd: None, + watch_paths: Vec::new(), + watch_ignore: Vec::new(), + watch_ignore_gitignore: false, + watch_debounce_ms: 200, + depends_on: Vec::new(), + ready_check: None, + tags: Vec::new(), + } + } + + fn test_settings(success: SuccessPolicy, no_ui: bool) -> RunSettings { + RunSettings { + max_lines: 100, + use_symbols: false, + no_ui, + raw: true, + prefix: None, + prefix_length: None, + prefix_colors: false, + timestamp: false, + output_mode: OutputMode::Combined, + success, + kill_others: false, + kill_others_on_fail: false, + restart_tries: None, + restart_delay_ms: None, + shutdown_sigint_ms: 0, + shutdown_sigterm_ms: 0, + input_enabled: true, + log_file: None, + } + } + + #[tokio::test] + async fn success_last_keeps_tui_open_after_all_exited() { + let specs = vec![test_spec("a"), test_spec("b")]; + let settings = test_settings(SuccessPolicy::Last, false); + let mut app = App::new(specs.clone(), settings.max_lines, settings.use_symbols, true); + let (event_tx, _event_rx) = mpsc::channel(4); + let (output_tx, _output_rx) = mpsc::channel(4); + let shutdown = ShutdownConfig::new(settings.shutdown_sigint_ms, settings.shutdown_sigterm_ms); + let mut manager = ProcessManager::new(specs, event_tx, output_tx, shutdown, false); + let mut output_state = OutputState::new(&app.processes, &settings); + let mut result = Ok(()); + + handle_exit_policy(0, Some(0), &mut app, &settings, &mut output_state, &mut manager, &mut result) + .await; + assert!(!app.should_quit); + handle_exit_policy(1, Some(0), &mut app, &settings, &mut output_state, &mut manager, &mut result) + .await; + assert!(!app.should_quit); + assert!(result.is_ok()); + } + + #[tokio::test] + async fn success_all_keeps_tui_open_after_all_exited() { + let specs = vec![test_spec("a"), test_spec("b")]; + let settings = test_settings(SuccessPolicy::All, false); + let mut app = App::new(specs.clone(), settings.max_lines, settings.use_symbols, true); + let (event_tx, _event_rx) = mpsc::channel(4); + let (output_tx, _output_rx) = mpsc::channel(4); + let shutdown = ShutdownConfig::new(settings.shutdown_sigint_ms, settings.shutdown_sigterm_ms); + let mut manager = ProcessManager::new(specs, event_tx, output_tx, shutdown, false); + let mut output_state = OutputState::new(&app.processes, &settings); + let mut result = Ok(()); + + handle_exit_policy(0, Some(0), &mut app, &settings, &mut output_state, &mut manager, &mut result) + .await; + assert!(!app.should_quit); + handle_exit_policy(1, Some(0), &mut app, &settings, &mut output_state, &mut manager, &mut result) + .await; + assert!(!app.should_quit); + assert!(result.is_ok()); + } + #[test] fn format_command_joins_args() { let spec = ProcessSpec {