From a7b962c5b5c73bc1b4fbf87f96fc7bf3af0e4c5d Mon Sep 17 00:00:00 2001 From: Carlos Rivera Date: Mon, 26 Jan 2026 21:06:55 -0600 Subject: [PATCH 1/2] Keep TUI open after all processes exit In TUI mode, do not auto-quit on all_exited() for success policies last/all so users can inspect logs and restart manually. Also make q exit immediately when everything is already stopped.\n\nRefs #11. --- src/main.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index 561f297..6a38c8f 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 From b382a25a7f3e31e9fb4f84d3e63706692e644ace Mon Sep 17 00:00:00 2001 From: Carlos Rivera Date: Mon, 26 Jan 2026 21:09:40 -0600 Subject: [PATCH 2/2] Add regression tests for TUI exit policy Cover success policies last/all in UI mode to ensure we do not auto-quit after all processes exit.\n\nRefs #11. --- src/main.rs | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/main.rs b/src/main.rs index 6a38c8f..25edc31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1732,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 {