From 79c6f7c264e8e9ec02b7f474e71ce94bdfa617ac Mon Sep 17 00:00:00 2001 From: l5y <220195275+l5yth@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:41:08 +0100 Subject: [PATCH 1/5] suspend tui for systemd to query available auth agent --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 1 + src/app/tui/debug.rs | 1 + src/app/tui/mod.rs | 349 +++++++++++++++++------------------------ src/app/tui/state.rs | 20 --- src/app/tui/workers.rs | 10 +- src/systemd.rs | 7 +- 8 files changed, 159 insertions(+), 233 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f29b02..f39cb37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -584,7 +584,7 @@ dependencies = [ [[package]] name = "lsu" -version = "0.1.2" +version = "0.1.3" dependencies = [ "anyhow", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index d922c94..5bd63cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "lsu" -version = "0.1.2" +version = "0.1.3" edition = "2024" license = "Apache-2.0" description = "Terminal UI for systemd services and their journal" diff --git a/README.md b/README.md index 1e1ebc2..0813c67 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ [![Codecov](https://codecov.io/gh/l5yth/lsu/graph/badge.svg)](https://codecov.io/gh/l5yth/lsu) [![GitHub Release](https://img.shields.io/github/v/release/l5yth/lsu)](https://github.com/l5yth/lsu/releases) [![Crates.io](https://img.shields.io/crates/v/lsu.svg)](https://crates.io/crates/lsu) +[![AUR Version](https://img.shields.io/aur/version/lsu-bin)](https://aur.archlinux.org/packages/lsu-bin) [![Top Language](https://img.shields.io/github/languages/top/l5yth/lsu)](https://github.com/l5yth/lsu) [![License: Apache-2.0](https://img.shields.io/github/license/l5yth/lsu)](https://github.com/l5yth/lsu/blob/main/LICENSE) diff --git a/src/app/tui/debug.rs b/src/app/tui/debug.rs index 83aa085..8e48290 100644 --- a/src/app/tui/debug.rs +++ b/src/app/tui/debug.rs @@ -385,6 +385,7 @@ pub(super) fn spawn_debug_action_resolution_worker( } /// Spawn a debug worker that simulates a unit action. +#[cfg(test)] pub(super) fn spawn_debug_action_worker(unit: String, action: UnitAction) -> Receiver { let (tx, rx) = mpsc::channel(); thread::spawn(move || { diff --git a/src/app/tui/mod.rs b/src/app/tui/mod.rs index f59394d..df281eb 100644 --- a/src/app/tui/mod.rs +++ b/src/app/tui/mod.rs @@ -52,6 +52,7 @@ use std::{ use crate::{ cli::{parse_args, usage, version_text}, rows::preserve_selection, + systemd::run_unit_action, types::{ ActionResolutionRequest, ConfirmationState, DetailState, LoadPhase, UnitAction, UnitRow, ViewMode, WorkerMsg, @@ -63,13 +64,11 @@ use self::{ input::{UiCommand, map_confirmation_key, map_key}, render::draw_frame, state::{ - MODE_LABEL, action_resolution_status_text, action_status_text, list_status_text, - loading_units_status_text, stale_status_text, - }, - workers::{ - spawn_action_resolution_worker, spawn_detail_worker, spawn_refresh_worker, - spawn_unit_action_worker, + MODE_LABEL, action_error_status_text, action_queued_status_text, + action_resolution_status_text, list_status_text, loading_units_status_text, + stale_status_text, }, + workers::{spawn_action_resolution_worker, spawn_detail_worker, spawn_refresh_worker}, }; #[cfg(not(test))] @@ -89,6 +88,26 @@ fn restore_terminal(mut terminal: Terminal>) -> Res Ok(()) } +/// Suspend the TUI so external processes (e.g. polkit auth agents) can use the terminal. +#[cfg(not(test))] +fn suspend_terminal(terminal: &mut Terminal>) -> Result<()> { + disable_raw_mode().context("disable_raw_mode failed")?; + execute!(terminal.backend_mut(), LeaveAlternateScreen) + .context("LeaveAlternateScreen failed")?; + terminal.show_cursor().context("show_cursor failed")?; + Ok(()) +} + +/// Resume the TUI after a suspension, clearing any output left by external processes. +#[cfg(not(test))] +fn resume_terminal(terminal: &mut Terminal>) -> Result<()> { + execute!(terminal.backend_mut(), EnterAlternateScreen) + .context("EnterAlternateScreen failed")?; + enable_raw_mode().context("enable_raw_mode failed")?; + terminal.clear().context("terminal clear failed")?; + Ok(()) +} + fn set_status_line( status_line: &mut String, status_line_overrides_stale: &mut bool, @@ -214,41 +233,6 @@ fn apply_action_resolution_msg( } } -fn apply_action_worker_msg( - refresh_requested: &mut bool, - status_line: &mut String, - status_line_overrides_stale: &mut bool, - rows_len: usize, - msg: crate::types::WorkerMsg, -) -> bool { - match msg { - crate::types::WorkerMsg::UnitActionQueued { unit, action } => { - *refresh_requested = true; - set_status_line( - status_line, - status_line_overrides_stale, - self::state::action_queued_status_text(rows_len, action, &unit), - true, - ); - true - } - crate::types::WorkerMsg::UnitActionError { - unit, - action, - error, - } => { - set_status_line( - status_line, - status_line_overrides_stale, - self::state::action_error_status_text(rows_len, action, &unit, &error), - true, - ); - true - } - _ => false, - } -} - const UNIT_ACTION_REFRESH_DELAY: Duration = Duration::from_millis(500); fn defer_queued_action_refresh( @@ -296,7 +280,6 @@ pub fn run() -> Result<()> { let mut worker_rx: Option> = None; let mut detail_worker_rx: Option> = None; let mut action_resolution_worker_rx: Option> = None; - let mut action_worker_rx: Option> = None; let mut queued_action_refresh_deadline: Option = None; let mut loaded_once = false; let mut last_load_error = false; @@ -534,41 +517,6 @@ pub fn run() -> Result<()> { } } - if let Some(rx) = action_worker_rx.as_ref() { - let mut clear_action_worker = false; - loop { - match rx.try_recv() { - Ok(msg) => { - let refresh_was_requested = refresh_requested; - clear_action_worker = apply_action_worker_msg( - &mut refresh_requested, - &mut status_line, - &mut status_line_overrides_stale, - rows.len(), - msg, - ); - defer_queued_action_refresh( - &mut refresh_requested, - &mut queued_action_refresh_deadline, - refresh_was_requested, - Instant::now(), - ); - if clear_action_worker { - break; - } - } - Err(TryRecvError::Empty) => break, - Err(TryRecvError::Disconnected) => { - clear_action_worker = true; - break; - } - } - } - if clear_action_worker { - action_worker_rx = None; - } - } - if event::poll(Duration::from_millis(50))? && let Event::Key(k) = event::read()? && k.kind == KeyEventKind::Press @@ -579,62 +527,136 @@ pub fn run() -> Result<()> { { match cmd { UiCommand::Confirm => { - if action_worker_rx.is_none() - && let Some(pending) = confirmation.take() + if let Some(pending) = confirmation.take() && let Some(action) = pending.confirmed_action() { - let status_confirmation = - ConfirmationState::confirm_action(action, pending.unit.clone()); - set_status_line( - &mut status_line, - &mut status_line_overrides_stale, - action_status_text(rows.len(), &status_confirmation), - true, + suspend_terminal(&mut terminal)?; + let result = run_unit_action(config.scope, &pending.unit, action); + resume_terminal(&mut terminal)?; + let refresh_was_requested = refresh_requested; + match result { + Ok(()) => { + refresh_requested = true; + set_status_line( + &mut status_line, + &mut status_line_overrides_stale, + action_queued_status_text( + rows.len(), + action, + &pending.unit, + ), + true, + ); + } + Err(e) => { + set_status_line( + &mut status_line, + &mut status_line_overrides_stale, + action_error_status_text( + rows.len(), + action, + &pending.unit, + &e.to_string(), + ), + true, + ); + } + } + defer_queued_action_refresh( + &mut refresh_requested, + &mut queued_action_refresh_deadline, + refresh_was_requested, + Instant::now(), ); - action_worker_rx = - Some(spawn_unit_action_worker(&config, pending.unit, action)); } } UiCommand::ChooseRestart => { - if action_worker_rx.is_none() - && let Some(pending) = confirmation.take() - { - let status_confirmation = ConfirmationState::confirm_action( + if let Some(pending) = confirmation.take() { + suspend_terminal(&mut terminal)?; + let result = run_unit_action( + config.scope, + &pending.unit, UnitAction::Restart, - pending.unit.clone(), ); - set_status_line( - &mut status_line, - &mut status_line_overrides_stale, - action_status_text(rows.len(), &status_confirmation), - true, + resume_terminal(&mut terminal)?; + let refresh_was_requested = refresh_requested; + match result { + Ok(()) => { + refresh_requested = true; + set_status_line( + &mut status_line, + &mut status_line_overrides_stale, + action_queued_status_text( + rows.len(), + UnitAction::Restart, + &pending.unit, + ), + true, + ); + } + Err(e) => { + set_status_line( + &mut status_line, + &mut status_line_overrides_stale, + action_error_status_text( + rows.len(), + UnitAction::Restart, + &pending.unit, + &e.to_string(), + ), + true, + ); + } + } + defer_queued_action_refresh( + &mut refresh_requested, + &mut queued_action_refresh_deadline, + refresh_was_requested, + Instant::now(), ); - action_worker_rx = Some(spawn_unit_action_worker( - &config, - pending.unit, - UnitAction::Restart, - )); } } UiCommand::ChooseStop => { - if action_worker_rx.is_none() - && let Some(pending) = confirmation.take() - { - let status_confirmation = ConfirmationState::confirm_action( - UnitAction::Stop, - pending.unit.clone(), - ); - set_status_line( - &mut status_line, - &mut status_line_overrides_stale, - action_status_text(rows.len(), &status_confirmation), - true, + if let Some(pending) = confirmation.take() { + suspend_terminal(&mut terminal)?; + let result = + run_unit_action(config.scope, &pending.unit, UnitAction::Stop); + resume_terminal(&mut terminal)?; + let refresh_was_requested = refresh_requested; + match result { + Ok(()) => { + refresh_requested = true; + set_status_line( + &mut status_line, + &mut status_line_overrides_stale, + action_queued_status_text( + rows.len(), + UnitAction::Stop, + &pending.unit, + ), + true, + ); + } + Err(e) => { + set_status_line( + &mut status_line, + &mut status_line_overrides_stale, + action_error_status_text( + rows.len(), + UnitAction::Stop, + &pending.unit, + &e.to_string(), + ), + true, + ); + } + } + defer_queued_action_refresh( + &mut refresh_requested, + &mut queued_action_refresh_deadline, + refresh_was_requested, + Instant::now(), ); - action_worker_rx = Some(spawn_unit_action_worker( - &config, - pending.unit, - UnitAction::Stop, - )); } } UiCommand::Cancel => { @@ -748,8 +770,7 @@ pub fn run() -> Result<()> { } } UiCommand::RequestStartStop => { - if action_worker_rx.is_none() - && action_resolution_worker_rx.is_none() + if action_resolution_worker_rx.is_none() && let Some(row) = rows.get(selected_idx) { set_status_line( @@ -767,8 +788,7 @@ pub fn run() -> Result<()> { } } UiCommand::RequestEnableDisable => { - if action_worker_rx.is_none() - && action_resolution_worker_rx.is_none() + if action_resolution_worker_rx.is_none() && let Some(row) = rows.get(selected_idx) { set_status_line( @@ -812,9 +832,8 @@ mod tests { use super::state::{list_status_text, stale_status_text}; use super::{ ActionResolutionUiState, UNIT_ACTION_REFRESH_DELAY, activate_queued_action_refresh, - apply_action_resolution_msg, apply_action_worker_msg, cancel_pending_action_resolution, - defer_queued_action_refresh, restore_list_status_line, set_list_status_line, - set_status_line, + apply_action_resolution_msg, cancel_pending_action_resolution, defer_queued_action_refresh, + restore_list_status_line, set_list_status_line, set_status_line, }; use crate::rows::preserve_selection; use crate::types::{ @@ -1393,64 +1412,6 @@ mod tests { assert!(!state.status_line_overrides_stale); } - #[test] - fn apply_action_worker_msg_updates_refresh_and_status() { - let mut refresh_requested = false; - let mut status_line = String::new(); - let mut override_stale = false; - - assert!(apply_action_worker_msg( - &mut refresh_requested, - &mut status_line, - &mut override_stale, - 3, - WorkerMsg::UnitActionQueued { - unit: "demo.service".to_string(), - action: UnitAction::Restart, - }, - )); - assert!(refresh_requested); - assert!(status_line.contains("queued restart for demo.service")); - assert!(override_stale); - - refresh_requested = false; - assert!(apply_action_worker_msg( - &mut refresh_requested, - &mut status_line, - &mut override_stale, - 3, - WorkerMsg::UnitActionError { - unit: "demo.service".to_string(), - action: UnitAction::Stop, - error: "nope".to_string(), - }, - )); - assert!(!refresh_requested); - assert!(status_line.contains("failed to stop demo.service: nope")); - assert!(override_stale); - } - - #[test] - fn apply_action_worker_msg_preserves_existing_refresh_request_on_queue() { - let mut refresh_requested = true; - let mut status_line = String::new(); - let mut override_stale = false; - - assert!(apply_action_worker_msg( - &mut refresh_requested, - &mut status_line, - &mut override_stale, - 3, - WorkerMsg::UnitActionQueued { - unit: "demo.service".to_string(), - action: UnitAction::Start, - }, - )); - assert!(refresh_requested); - assert!(status_line.contains("queued start for demo.service")); - assert!(override_stale); - } - #[test] fn defer_queued_action_refresh_schedules_delayed_list_reload() { let mut refresh_requested = true; @@ -1496,22 +1457,4 @@ mod tests { assert!(refresh_requested); assert!(queued_deadline.is_none()); } - - #[test] - fn apply_action_worker_msg_ignores_unrelated_messages() { - let mut refresh_requested = false; - let mut status_line = "unchanged".to_string(); - let mut override_stale = false; - - assert!(!apply_action_worker_msg( - &mut refresh_requested, - &mut status_line, - &mut override_stale, - 3, - WorkerMsg::Finished, - )); - assert!(!refresh_requested); - assert_eq!(status_line, "unchanged"); - assert!(!override_stale); - } } diff --git a/src/app/tui/state.rs b/src/app/tui/state.rs index 2c90a15..2a3f74e 100644 --- a/src/app/tui/state.rs +++ b/src/app/tui/state.rs @@ -60,18 +60,6 @@ pub fn loading_units_status_text() -> String { format!("{MODE_LABEL}: loading units... | {}", list_controls_text()) } -/// Build the footer status text shown while a unit action is running. -/// -/// `confirmation` must have kind `ConfirmAction`; calling this with a -/// `RestartOrStop` confirmation is a programming error and will panic. -pub fn action_status_text(rows: usize, confirmation: &ConfirmationState) -> String { - let verb = confirmation - .confirmed_action() - .expect("action_status_text requires a ConfirmAction confirmation") - .prompt_verb(); - format!("{MODE_LABEL}: {rows} | {} {}...", verb, confirmation.unit) -} - /// Build the footer status text shown while an action prompt is being resolved. pub fn action_resolution_status_text(rows: usize, unit: &str) -> String { format!("{MODE_LABEL}: {rows} | resolving action for {unit}...") @@ -174,14 +162,6 @@ mod tests { assert_eq!(s, "confirm disabling of unit foobar.service (y/n)"); } - #[test] - fn action_status_text_mentions_running_action() { - let confirmation = - ConfirmationState::confirm_action(UnitAction::Start, "demo.service".to_string()); - let s = action_status_text(3, &confirmation); - assert!(s.contains("starting demo.service")); - } - #[test] fn action_resolution_status_text_mentions_target_unit() { let s = action_resolution_status_text(3, "demo.service"); diff --git a/src/app/tui/workers.rs b/src/app/tui/workers.rs index c5657d4..a585086 100644 --- a/src/app/tui/workers.rs +++ b/src/app/tui/workers.rs @@ -21,12 +21,15 @@ use std::{ thread, }; +#[cfg(all(feature = "debug_tui", test))] +use super::debug::spawn_debug_action_worker; #[cfg(feature = "debug_tui")] use super::debug::{ - spawn_debug_action_resolution_worker, spawn_debug_action_worker, spawn_debug_detail_worker, - spawn_debug_refresh_worker, + spawn_debug_action_resolution_worker, spawn_debug_detail_worker, spawn_debug_refresh_worker, }; #[cfg(test)] +use crate::systemd::run_unit_action; +#[cfg(test)] use crate::types::{Scope, SortMode}; use crate::{ cli::Config, @@ -34,7 +37,7 @@ use crate::{ rows::{build_rows, seed_logs_from_previous, sort_rows}, systemd::{ fetch_services, fetch_unit_files, filter_services, merge_unit_file_entries, - run_unit_action, select_enable_disable_action, select_start_stop_action, should_fetch_all, + select_enable_disable_action, select_start_stop_action, should_fetch_all, }, types::{ActionResolutionRequest, ConfirmationState, UnitAction, UnitRow, WorkerMsg}, }; @@ -142,6 +145,7 @@ pub fn spawn_detail_worker(config: &Config, unit: String, request_id: u64) -> Re } /// Spawn a background worker that executes one unit action. +#[cfg(test)] pub fn spawn_unit_action_worker( config: &Config, unit: String, diff --git a/src/systemd.rs b/src/systemd.rs index 896a466..e461f0c 100644 --- a/src/systemd.rs +++ b/src/systemd.rs @@ -157,7 +157,6 @@ fn unit_action_args(scope: Scope, unit: &str, action: UnitAction) -> Vec } args.extend([ "--no-block".to_string(), - "--no-ask-password".to_string(), scope.as_systemd_arg().to_string(), unit.to_string(), ]); @@ -172,7 +171,7 @@ pub fn run_unit_action(scope: Scope, unit: &str, action: UnitAction) -> Result<( for arg in unit_action_args(scope, unit, action) { cmd.arg(arg); } - cmd.stdin(Stdio::null()); + cmd.stdin(Stdio::inherit()); let _ = cmd_stdout(&mut cmd) .with_context(|| format!("systemctl {} failed", action.as_systemctl_arg()))?; Ok(()) @@ -509,14 +508,13 @@ mod tests { } #[test] - fn unit_action_args_use_non_blocking_non_interactive_flags() { + fn unit_action_args_use_non_blocking_flag() { let args = unit_action_args(Scope::System, "demo.service", UnitAction::Restart); assert_eq!( args, vec![ "restart".to_string(), "--no-block".to_string(), - "--no-ask-password".to_string(), "--system".to_string(), "demo.service".to_string(), ] @@ -532,7 +530,6 @@ mod tests { "disable".to_string(), "--runtime".to_string(), "--no-block".to_string(), - "--no-ask-password".to_string(), "--user".to_string(), "demo.service".to_string(), ] From deb881e87ab781a5ecf3be2368a129df8882b686 Mon Sep 17 00:00:00 2001 From: l5y <220195275+l5yth@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:59:15 +0100 Subject: [PATCH 2/5] suspend tui for systemd to query available auth agent --- src/command.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/systemd.rs | 6 +++-- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/command.rs b/src/command.rs index e6b7f79..59a58a4 100644 --- a/src/command.rs +++ b/src/command.rs @@ -117,6 +117,36 @@ pub fn cmd_stdout(cmd: &mut Command) -> std::result::Result std::result::Result<(), CommandExecError> { + let rendered = render_command(cmd); + cmd.stderr(Stdio::piped()); + let mut child = cmd.spawn()?; + let mut stderr = child + .stderr + .take() + .ok_or_else(|| CommandExecError::Io(std::io::Error::other("missing child stderr pipe")))?; + let stderr_handle = thread::spawn(move || { + let mut out = Vec::new(); + let _ = stderr.read_to_end(&mut out); + out + }); + let status = child.wait()?; + let stderr = stderr_handle.join().unwrap_or_default(); + if !status.success() { + return Err(CommandExecError::NonZeroExit { + command: rendered, + status, + stderr: String::from_utf8_lossy(&stderr).to_string(), + }); + } + Ok(()) +} + /// Run a command with an explicit timeout and return UTF-8 decoded stdout on success. pub fn cmd_stdout_with_timeout( cmd: &mut Command, @@ -617,6 +647,37 @@ mod tests { let _ = fs::remove_dir_all(untrusted); } + #[test] + fn cmd_wait_returns_ok_for_success() { + let mut cmd = Command::new("sh"); + cmd.arg("-c").arg("exit 0"); + cmd_wait(&mut cmd).expect("command should succeed"); + } + + #[test] + fn cmd_wait_returns_error_for_non_zero_exit() { + let mut cmd = Command::new("sh"); + cmd.arg("-c").arg("echo fail 1>&2; exit 3"); + let err = cmd_wait(&mut cmd).expect_err("command should fail"); + match err { + CommandExecError::NonZeroExit { status, stderr, .. } => { + assert_eq!(status.code(), Some(3)); + assert!(stderr.contains("fail")); + } + other => panic!("expected non-zero exit error, got {other}"), + } + } + + #[test] + fn cmd_wait_non_zero_with_empty_stderr_omits_separator_suffix() { + let mut cmd = Command::new("sh"); + cmd.arg("-c").arg("exit 5"); + let err = cmd_wait(&mut cmd).expect_err("command should fail"); + let msg = err.to_string(); + assert!(msg.contains("command failed (status=")); + assert!(!msg.contains(" | ")); + } + #[test] fn resolve_trusted_binary_allows_secure_path_entry_outside_defaults() { let secure = unique_temp_dir("secure-outside-default"); diff --git a/src/systemd.rs b/src/systemd.rs index e461f0c..3a84df1 100644 --- a/src/systemd.rs +++ b/src/systemd.rs @@ -23,7 +23,9 @@ use anyhow::{Result, anyhow}; use std::process::{Command, Stdio}; #[cfg(not(test))] -use crate::command::{CommandExecError, cmd_stdout, command_timeout, resolve_trusted_binary}; +use crate::command::{ + CommandExecError, cmd_stdout, cmd_wait, command_timeout, resolve_trusted_binary, +}; use std::collections::HashSet; #[cfg(test)] @@ -172,7 +174,7 @@ pub fn run_unit_action(scope: Scope, unit: &str, action: UnitAction) -> Result<( cmd.arg(arg); } cmd.stdin(Stdio::inherit()); - let _ = cmd_stdout(&mut cmd) + cmd_wait(&mut cmd) .with_context(|| format!("systemctl {} failed", action.as_systemctl_arg()))?; Ok(()) } From fc034b00d83721397e7c1e73b934c9785ac7dd26 Mon Sep 17 00:00:00 2001 From: l5y <220195275+l5yth@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:32:41 +0100 Subject: [PATCH 3/5] address review comments --- src/app/tui/debug.rs | 33 ------- src/app/tui/mod.rs | 195 +++++++++++++++++------------------------ src/app/tui/state.rs | 19 ++++ src/app/tui/workers.rs | 110 ----------------------- src/command.rs | 2 + 5 files changed, 103 insertions(+), 256 deletions(-) diff --git a/src/app/tui/debug.rs b/src/app/tui/debug.rs index 8e48290..8b6ceb8 100644 --- a/src/app/tui/debug.rs +++ b/src/app/tui/debug.rs @@ -384,24 +384,6 @@ pub(super) fn spawn_debug_action_resolution_worker( rx } -/// Spawn a debug worker that simulates a unit action. -#[cfg(test)] -pub(super) fn spawn_debug_action_worker(unit: String, action: UnitAction) -> Receiver { - let (tx, rx) = mpsc::channel(); - thread::spawn(move || { - if template_for_unit(&unit).is_some() { - let _ = tx.send(WorkerMsg::UnitActionQueued { unit, action }); - } else { - let _ = tx.send(WorkerMsg::UnitActionError { - unit, - action, - error: "unknown debug unit".to_string(), - }); - } - }); - rx -} - #[cfg(test)] mod tests { use super::*; @@ -652,19 +634,4 @@ mod tests { } } - #[test] - fn spawn_debug_action_worker_emits_queued_for_known_units() { - let rx = - spawn_debug_action_worker("debug-api-gateway.service".to_string(), UnitAction::Restart); - match rx - .recv_timeout(Duration::from_millis(500)) - .expect("action message") - { - WorkerMsg::UnitActionQueued { unit, action } => { - assert_eq!(unit, "debug-api-gateway.service"); - assert_eq!(action, UnitAction::Restart); - } - other => panic!("expected UnitActionQueued, got {other:?}"), - } - } } diff --git a/src/app/tui/mod.rs b/src/app/tui/mod.rs index df281eb..0e4205c 100644 --- a/src/app/tui/mod.rs +++ b/src/app/tui/mod.rs @@ -64,9 +64,9 @@ use self::{ input::{UiCommand, map_confirmation_key, map_key}, render::draw_frame, state::{ - MODE_LABEL, action_error_status_text, action_queued_status_text, - action_resolution_status_text, list_status_text, loading_units_status_text, - stale_status_text, + MODE_LABEL, action_authenticating_status_text, action_error_status_text, + action_queued_status_text, action_resolution_status_text, list_status_text, + loading_units_status_text, stale_status_text, }, workers::{spawn_action_resolution_worker, spawn_detail_worker, spawn_refresh_worker}, }; @@ -108,6 +108,61 @@ fn resume_terminal(terminal: &mut Terminal>) -> Res Ok(()) } +/// Suspend the terminal, run a unit action with authentication support, resume, and update status. +/// +/// Returns `Err` only if terminal suspension or resumption fails; action errors are reported +/// via `status_line` rather than propagated. +#[cfg(not(test))] +#[allow(clippy::too_many_arguments)] +fn run_confirmed_action( + terminal: &mut Terminal>, + scope: crate::types::Scope, + unit: &str, + action: UnitAction, + rows_len: usize, + status_line: &mut String, + status_line_overrides_stale: &mut bool, + refresh_requested: &mut bool, + queued_action_refresh_deadline: &mut Option, +) -> Result<()> { + set_status_line( + status_line, + status_line_overrides_stale, + action_authenticating_status_text(rows_len, action, unit), + true, + ); + suspend_terminal(terminal)?; + let result = run_unit_action(scope, unit, action); + resume_terminal(terminal)?; + let refresh_was_requested = *refresh_requested; + match result { + Ok(()) => { + *refresh_requested = true; + set_status_line( + status_line, + status_line_overrides_stale, + action_queued_status_text(rows_len, action, unit), + true, + ); + } + Err(e) => { + set_status_line( + status_line, + status_line_overrides_stale, + action_error_status_text(rows_len, action, unit, &e.to_string()), + true, + ); + } + } + defer_queued_action_refresh( + refresh_requested, + queued_action_refresh_deadline, + refresh_was_requested, + Instant::now(), + ); + Ok(()) +} + fn set_status_line( status_line: &mut String, status_line_overrides_stale: &mut bool, @@ -530,133 +585,47 @@ pub fn run() -> Result<()> { if let Some(pending) = confirmation.take() && let Some(action) = pending.confirmed_action() { - suspend_terminal(&mut terminal)?; - let result = run_unit_action(config.scope, &pending.unit, action); - resume_terminal(&mut terminal)?; - let refresh_was_requested = refresh_requested; - match result { - Ok(()) => { - refresh_requested = true; - set_status_line( - &mut status_line, - &mut status_line_overrides_stale, - action_queued_status_text( - rows.len(), - action, - &pending.unit, - ), - true, - ); - } - Err(e) => { - set_status_line( - &mut status_line, - &mut status_line_overrides_stale, - action_error_status_text( - rows.len(), - action, - &pending.unit, - &e.to_string(), - ), - true, - ); - } - } - defer_queued_action_refresh( + run_confirmed_action( + &mut terminal, + config.scope, + &pending.unit, + action, + rows.len(), + &mut status_line, + &mut status_line_overrides_stale, &mut refresh_requested, &mut queued_action_refresh_deadline, - refresh_was_requested, - Instant::now(), - ); + )?; } } UiCommand::ChooseRestart => { if let Some(pending) = confirmation.take() { - suspend_terminal(&mut terminal)?; - let result = run_unit_action( + run_confirmed_action( + &mut terminal, config.scope, &pending.unit, UnitAction::Restart, - ); - resume_terminal(&mut terminal)?; - let refresh_was_requested = refresh_requested; - match result { - Ok(()) => { - refresh_requested = true; - set_status_line( - &mut status_line, - &mut status_line_overrides_stale, - action_queued_status_text( - rows.len(), - UnitAction::Restart, - &pending.unit, - ), - true, - ); - } - Err(e) => { - set_status_line( - &mut status_line, - &mut status_line_overrides_stale, - action_error_status_text( - rows.len(), - UnitAction::Restart, - &pending.unit, - &e.to_string(), - ), - true, - ); - } - } - defer_queued_action_refresh( + rows.len(), + &mut status_line, + &mut status_line_overrides_stale, &mut refresh_requested, &mut queued_action_refresh_deadline, - refresh_was_requested, - Instant::now(), - ); + )?; } } UiCommand::ChooseStop => { if let Some(pending) = confirmation.take() { - suspend_terminal(&mut terminal)?; - let result = - run_unit_action(config.scope, &pending.unit, UnitAction::Stop); - resume_terminal(&mut terminal)?; - let refresh_was_requested = refresh_requested; - match result { - Ok(()) => { - refresh_requested = true; - set_status_line( - &mut status_line, - &mut status_line_overrides_stale, - action_queued_status_text( - rows.len(), - UnitAction::Stop, - &pending.unit, - ), - true, - ); - } - Err(e) => { - set_status_line( - &mut status_line, - &mut status_line_overrides_stale, - action_error_status_text( - rows.len(), - UnitAction::Stop, - &pending.unit, - &e.to_string(), - ), - true, - ); - } - } - defer_queued_action_refresh( + run_confirmed_action( + &mut terminal, + config.scope, + &pending.unit, + UnitAction::Stop, + rows.len(), + &mut status_line, + &mut status_line_overrides_stale, &mut refresh_requested, &mut queued_action_refresh_deadline, - refresh_was_requested, - Instant::now(), - ); + )?; } } UiCommand::Cancel => { diff --git a/src/app/tui/state.rs b/src/app/tui/state.rs index 2a3f74e..2662900 100644 --- a/src/app/tui/state.rs +++ b/src/app/tui/state.rs @@ -65,6 +65,19 @@ pub fn action_resolution_status_text(rows: usize, unit: &str) -> String { format!("{MODE_LABEL}: {rows} | resolving action for {unit}...") } +/// Build the footer status text shown while waiting for authentication before a unit action. +pub fn action_authenticating_status_text( + rows: usize, + action: crate::types::UnitAction, + unit: &str, +) -> String { + format!( + "{MODE_LABEL}: {rows} | authenticating {} {}...", + action.as_systemctl_arg(), + unit, + ) +} + /// Build the footer status text after a unit action request is queued. pub fn action_queued_status_text( rows: usize, @@ -168,6 +181,12 @@ mod tests { assert!(s.contains("resolving action for demo.service")); } + #[test] + fn action_authenticating_status_text_mentions_unit_and_action() { + let s = action_authenticating_status_text(3, UnitAction::Start, "demo.service"); + assert!(s.contains("authenticating start demo.service...")); + } + #[test] fn action_queued_and_error_status_include_controls() { let queued = action_queued_status_text(4, UnitAction::Enable, "demo.service"); diff --git a/src/app/tui/workers.rs b/src/app/tui/workers.rs index a585086..f0195aa 100644 --- a/src/app/tui/workers.rs +++ b/src/app/tui/workers.rs @@ -21,15 +21,11 @@ use std::{ thread, }; -#[cfg(all(feature = "debug_tui", test))] -use super::debug::spawn_debug_action_worker; #[cfg(feature = "debug_tui")] use super::debug::{ spawn_debug_action_resolution_worker, spawn_debug_detail_worker, spawn_debug_refresh_worker, }; #[cfg(test)] -use crate::systemd::run_unit_action; -#[cfg(test)] use crate::types::{Scope, SortMode}; use crate::{ cli::Config, @@ -144,35 +140,6 @@ pub fn spawn_detail_worker(config: &Config, unit: String, request_id: u64) -> Re rx } -/// Spawn a background worker that executes one unit action. -#[cfg(test)] -pub fn spawn_unit_action_worker( - config: &Config, - unit: String, - action: UnitAction, -) -> Receiver { - #[cfg(feature = "debug_tui")] - if config.debug_tui { - return spawn_debug_action_worker(unit, action); - } - - let (tx, rx) = mpsc::channel(); - let scope = config.scope; - thread::spawn(move || match run_unit_action(scope, &unit, action) { - Ok(()) => { - let _ = tx.send(WorkerMsg::UnitActionQueued { unit, action }); - } - Err(e) => { - let _ = tx.send(WorkerMsg::UnitActionError { - unit, - action, - error: e.to_string(), - }); - } - }); - rx -} - fn resolve_action_confirmation( scope: crate::types::Scope, request: ActionResolutionRequest, @@ -412,67 +379,6 @@ mod tests { } } - #[test] - fn unit_action_worker_emits_queued_on_success() { - let rx = spawn_unit_action_worker( - &Config { - load_filter: "loaded".to_string(), - active_filter: "active".to_string(), - sub_filter: "running".to_string(), - show_help: false, - show_version: false, - debug_tui: false, - scope: Scope::System, - sort_mode: SortMode::Name, - }, - "demo.service".to_string(), - UnitAction::Start, - ); - match rx - .recv_timeout(Duration::from_millis(500)) - .expect("action msg") - { - WorkerMsg::UnitActionQueued { unit, action } => { - assert_eq!(unit, "demo.service"); - assert_eq!(action, UnitAction::Start); - } - other => panic!("expected UnitActionQueued, got {other:?}"), - } - } - - #[test] - fn unit_action_worker_emits_error_on_failure() { - let rx = spawn_unit_action_worker( - &Config { - load_filter: "loaded".to_string(), - active_filter: "active".to_string(), - sub_filter: "running".to_string(), - show_help: false, - show_version: false, - debug_tui: false, - scope: Scope::System, - sort_mode: SortMode::Name, - }, - "action-error.service".to_string(), - UnitAction::Stop, - ); - match rx - .recv_timeout(Duration::from_millis(500)) - .expect("action error msg") - { - WorkerMsg::UnitActionError { - unit, - action, - error, - } => { - assert_eq!(unit, "action-error.service"); - assert_eq!(action, UnitAction::Stop); - assert!(error.contains("unit action test error")); - } - other => panic!("expected UnitActionError, got {other:?}"), - } - } - #[test] fn action_resolution_worker_resolves_start_stop_from_active_state() { let rx = spawn_action_resolution_worker( @@ -781,21 +687,5 @@ mod tests { } other => panic!("expected ActionConfirmationReady, got {other:?}"), } - - let action_rx = spawn_unit_action_worker( - &cfg, - "debug-api-gateway.service".to_string(), - UnitAction::Stop, - ); - match action_rx - .recv_timeout(Duration::from_millis(500)) - .expect("action msg") - { - WorkerMsg::UnitActionQueued { unit, action } => { - assert_eq!(unit, "debug-api-gateway.service"); - assert_eq!(action, UnitAction::Stop); - } - other => panic!("expected UnitActionQueued, got {other:?}"), - } } } diff --git a/src/command.rs b/src/command.rs index 59a58a4..55c9e98 100644 --- a/src/command.rs +++ b/src/command.rs @@ -675,6 +675,8 @@ mod tests { let err = cmd_wait(&mut cmd).expect_err("command should fail"); let msg = err.to_string(); assert!(msg.contains("command failed (status=")); + // The Display impl appends " | " only when stderr is non-empty; + // with no output there should be no trailing separator. assert!(!msg.contains(" | ")); } From b5064724abe80cf3c5c25717bd21c1354054b506 Mon Sep 17 00:00:00 2001 From: l5y <220195275+l5yth@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:02:01 +0100 Subject: [PATCH 4/5] address review comments --- src/app/tui/debug.rs | 21 ++++- src/app/tui/mod.rs | 143 +++++++++++++++++++++++++------ src/command.rs | 4 +- tests/tui_runtime_integration.rs | 51 +++++++++++ 4 files changed, 192 insertions(+), 27 deletions(-) diff --git a/src/app/tui/debug.rs b/src/app/tui/debug.rs index 8b6ceb8..ca6d897 100644 --- a/src/app/tui/debug.rs +++ b/src/app/tui/debug.rs @@ -311,6 +311,17 @@ fn debug_enable_disable_action(template: DebugUnitTemplate) -> anyhow::Result anyhow::Result<()> { + Ok(()) +} + /// Spawn a background worker that emits fake rows and fake preview logs. pub(super) fn spawn_debug_refresh_worker(previous_rows: Vec) -> Receiver { let (tx, rx) = mpsc::channel(); @@ -387,9 +398,18 @@ pub(super) fn spawn_debug_action_resolution_worker( #[cfg(test)] mod tests { use super::*; + use crate::types::UnitAction; use ratatui::prelude::{Color, Style}; use std::time::Duration; + #[test] + fn run_debug_unit_action_returns_ok_for_any_input() { + assert!(run_debug_unit_action("debug-foo.service", UnitAction::Restart).is_ok()); + assert!(run_debug_unit_action("debug-foo.service", UnitAction::Stop).is_ok()); + assert!(run_debug_unit_action("debug-foo.service", UnitAction::Enable).is_ok()); + assert!(run_debug_unit_action("debug-foo.service", UnitAction::Disable).is_ok()); + } + #[test] fn build_debug_rows_stays_within_limit_and_covers_color_buckets() { let rows = build_debug_rows(); @@ -633,5 +653,4 @@ mod tests { other => panic!("expected ActionConfirmationReady, got {other:?}"), } } - } diff --git a/src/app/tui/mod.rs b/src/app/tui/mod.rs index 0e4205c..e4ed8da 100644 --- a/src/app/tui/mod.rs +++ b/src/app/tui/mod.rs @@ -64,9 +64,8 @@ use self::{ input::{UiCommand, map_confirmation_key, map_key}, render::draw_frame, state::{ - MODE_LABEL, action_authenticating_status_text, action_error_status_text, - action_queued_status_text, action_resolution_status_text, list_status_text, - loading_units_status_text, stale_status_text, + MODE_LABEL, action_authenticating_status_text, action_resolution_status_text, + list_status_text, loading_units_status_text, stale_status_text, }, workers::{spawn_action_resolution_worker, spawn_detail_worker, spawn_refresh_worker}, }; @@ -108,32 +107,21 @@ fn resume_terminal(terminal: &mut Terminal>) -> Res Ok(()) } -/// Suspend the terminal, run a unit action with authentication support, resume, and update status. +/// Apply the result of a completed unit action: update the status line and schedule a refresh. /// -/// Returns `Err` only if terminal suspension or resumption fails; action errors are reported -/// via `status_line` rather than propagated. -#[cfg(not(test))] +/// This is the pure-logic counterpart to `run_confirmed_action`; it can be unit-tested +/// independently of the terminal I/O. #[allow(clippy::too_many_arguments)] -fn run_confirmed_action( - terminal: &mut Terminal>, - scope: crate::types::Scope, +fn apply_confirmed_action_result( + result: anyhow::Result<()>, unit: &str, - action: UnitAction, + action: crate::types::UnitAction, rows_len: usize, status_line: &mut String, status_line_overrides_stale: &mut bool, refresh_requested: &mut bool, queued_action_refresh_deadline: &mut Option, -) -> Result<()> { - set_status_line( - status_line, - status_line_overrides_stale, - action_authenticating_status_text(rows_len, action, unit), - true, - ); - suspend_terminal(terminal)?; - let result = run_unit_action(scope, unit, action); - resume_terminal(terminal)?; +) { let refresh_was_requested = *refresh_requested; match result { Ok(()) => { @@ -141,7 +129,7 @@ fn run_confirmed_action( set_status_line( status_line, status_line_overrides_stale, - action_queued_status_text(rows_len, action, unit), + self::state::action_queued_status_text(rows_len, action, unit), true, ); } @@ -149,7 +137,7 @@ fn run_confirmed_action( set_status_line( status_line, status_line_overrides_stale, - action_error_status_text(rows_len, action, unit, &e.to_string()), + self::state::action_error_status_text(rows_len, action, unit, &e.to_string()), true, ); } @@ -160,6 +148,58 @@ fn run_confirmed_action( refresh_was_requested, Instant::now(), ); +} + +/// Suspend the terminal, run a unit action with authentication support, resume, and update status. +/// +/// Returns `Err` only if terminal suspension or resumption fails; action errors are reported +/// via `status_line` rather than propagated. +/// +/// When `debug_tui` is `true` (i.e. the `--debug-tui` flag was passed), action execution uses a +/// self-contained stub so that no real systemd socket or polkit agent is required. +#[cfg(not(test))] +#[allow(clippy::too_many_arguments)] +fn run_confirmed_action( + terminal: &mut Terminal>, + scope: crate::types::Scope, + unit: &str, + action: UnitAction, + debug_tui: bool, + rows_len: usize, + status_line: &mut String, + status_line_overrides_stale: &mut bool, + refresh_requested: &mut bool, + queued_action_refresh_deadline: &mut Option, +) -> Result<()> { + set_status_line( + status_line, + status_line_overrides_stale, + action_authenticating_status_text(rows_len, action, unit), + true, + ); + suspend_terminal(terminal)?; + #[cfg(feature = "debug_tui")] + let result = if debug_tui { + self::debug::run_debug_unit_action(unit, action) + } else { + run_unit_action(scope, unit, action) + }; + #[cfg(not(feature = "debug_tui"))] + let result = { + let _ = debug_tui; + run_unit_action(scope, unit, action) + }; + resume_terminal(terminal)?; + apply_confirmed_action_result( + result, + unit, + action, + rows_len, + status_line, + status_line_overrides_stale, + refresh_requested, + queued_action_refresh_deadline, + ); Ok(()) } @@ -590,6 +630,7 @@ pub fn run() -> Result<()> { config.scope, &pending.unit, action, + config.debug_tui, rows.len(), &mut status_line, &mut status_line_overrides_stale, @@ -605,6 +646,7 @@ pub fn run() -> Result<()> { config.scope, &pending.unit, UnitAction::Restart, + config.debug_tui, rows.len(), &mut status_line, &mut status_line_overrides_stale, @@ -620,6 +662,7 @@ pub fn run() -> Result<()> { config.scope, &pending.unit, UnitAction::Stop, + config.debug_tui, rows.len(), &mut status_line, &mut status_line_overrides_stale, @@ -801,8 +844,9 @@ mod tests { use super::state::{list_status_text, stale_status_text}; use super::{ ActionResolutionUiState, UNIT_ACTION_REFRESH_DELAY, activate_queued_action_refresh, - apply_action_resolution_msg, cancel_pending_action_resolution, defer_queued_action_refresh, - restore_list_status_line, set_list_status_line, set_status_line, + apply_action_resolution_msg, apply_confirmed_action_result, + cancel_pending_action_resolution, defer_queued_action_refresh, restore_list_status_line, + set_list_status_line, set_status_line, }; use crate::rows::preserve_selection; use crate::types::{ @@ -1381,6 +1425,55 @@ mod tests { assert!(!state.status_line_overrides_stale); } + #[test] + fn apply_confirmed_action_result_ok_sets_queued_status_and_schedules_refresh() { + let mut status_line = String::new(); + let mut override_stale = false; + let mut refresh = false; + let mut deadline = None; + + apply_confirmed_action_result( + Ok(()), + "demo.service", + UnitAction::Restart, + 3, + &mut status_line, + &mut override_stale, + &mut refresh, + &mut deadline, + ); + + assert!(status_line.contains("queued restart for demo.service")); + assert!(override_stale); + // refresh set to true, then deferred: deadline scheduled and refresh reset to false + assert!(!refresh); + assert!(deadline.is_some()); + } + + #[test] + fn apply_confirmed_action_result_err_sets_error_status_without_refresh() { + let mut status_line = String::new(); + let mut override_stale = false; + let mut refresh = false; + let mut deadline = None; + + apply_confirmed_action_result( + Err(anyhow::anyhow!("polkit denied")), + "demo.service", + UnitAction::Stop, + 3, + &mut status_line, + &mut override_stale, + &mut refresh, + &mut deadline, + ); + + assert!(status_line.contains("failed to stop demo.service: polkit denied")); + assert!(override_stale); + assert!(!refresh); + assert!(deadline.is_none()); + } + #[test] fn defer_queued_action_refresh_schedules_delayed_list_reload() { let mut refresh_requested = true; diff --git a/src/command.rs b/src/command.rs index 55c9e98..6f99779 100644 --- a/src/command.rs +++ b/src/command.rs @@ -129,7 +129,9 @@ pub fn cmd_wait(cmd: &mut Command) -> std::result::Result<(), CommandExecError> let mut stderr = child .stderr .take() - .ok_or_else(|| CommandExecError::Io(std::io::Error::other("missing child stderr pipe")))?; + .ok_or(CommandExecError::Io(std::io::Error::other( + "missing child stderr pipe", + )))?; let stderr_handle = thread::spawn(move || { let mut out = Vec::new(); let _ = stderr.read_to_end(&mut out); diff --git a/tests/tui_runtime_integration.rs b/tests/tui_runtime_integration.rs index 3afd3b4..2c67f17 100644 --- a/tests/tui_runtime_integration.rs +++ b/tests/tui_runtime_integration.rs @@ -39,6 +39,57 @@ fn tui_process_can_start_and_quit_immediately_with_pty() { assert!(output.status.code().is_some()); } +/// Exercise all three action-confirmation paths (ChooseRestart, ChooseStop, Confirm) so that +/// `run_confirmed_action`, `suspend_terminal`, and `resume_terminal` are covered by the binary +/// execution. The sequence: +/// 1. 's' → RequestStartStop → RestartOrStop prompt +/// 2. 'r' → ChooseRestart → run_confirmed_action(Restart) [systemctl may fail — that's OK] +/// 3. 's' → RequestStartStop → RestartOrStop prompt again +/// 4. 's' → ChooseStop → run_confirmed_action(Stop) +/// 5. 'e' → RequestEnableDisable → ConfirmAction prompt +/// 6. 'y' → Confirm → run_confirmed_action(Disable) +/// 7. 'q' → Quit +#[cfg(feature = "debug_tui")] +#[test] +fn debug_tui_action_confirmation_paths_are_exercised_with_pty() { + let bin = env!("CARGO_BIN_EXE_lsu"); + + let has_script = Command::new("sh") + .arg("-c") + .arg("command -v script >/dev/null 2>&1") + .status() + .expect("check script availability") + .success(); + if !has_script { + return; + } + + // Delays: + // 0.3s — TUI loads debug units + // 0.15s — debug action resolution completes (near-instant, but needs one 50ms poll) + // 1.0s — systemctl finishes (may fail, but run_confirmed_action completes) + TUI resumes + let cmd = format!( + concat!( + "(sleep 0.3;", + " printf 's'; sleep 0.15;", // trigger start/stop resolution + " printf 'r'; sleep 1.0;", // ChooseRestart → run_confirmed_action + " printf 's'; sleep 0.15;", // trigger start/stop resolution again + " printf 's'; sleep 1.0;", // ChooseStop → run_confirmed_action + " printf 'e'; sleep 0.15;", // trigger enable/disable resolution + " printf 'y'; sleep 1.0;", // Confirm → run_confirmed_action + " printf 'q')", // Quit + " | script -qefc '{} --debug-tui' /dev/null" + ), + bin + ); + let output = Command::new("sh") + .arg("-c") + .arg(cmd) + .output() + .expect("run debug tui action paths with pty"); + assert!(output.status.code().is_some()); +} + #[cfg(feature = "debug_tui")] #[test] fn debug_tui_process_can_open_and_refresh_detail_with_pty() { From 6cb2954bf49b5399db15fa12fa650d4aa03974e8 Mon Sep 17 00:00:00 2001 From: l5y <220195275+l5yth@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:09:02 +0100 Subject: [PATCH 5/5] address review comments --- src/app/tui/mod.rs | 10 +++------- src/types.rs | 16 ---------------- tests/tui_runtime_integration.rs | 2 +- 3 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/app/tui/mod.rs b/src/app/tui/mod.rs index e4ed8da..c7b3938 100644 --- a/src/app/tui/mod.rs +++ b/src/app/tui/mod.rs @@ -186,7 +186,7 @@ fn run_confirmed_action( }; #[cfg(not(feature = "debug_tui"))] let result = { - let _ = debug_tui; + let _ = debug_tui; // parameter unused without debug_tui feature run_unit_action(scope, unit, action) }; resume_terminal(terminal)?; @@ -524,9 +524,7 @@ pub fn run() -> Result<()> { WorkerMsg::DetailLogsLoaded { .. } | WorkerMsg::DetailLogsError { .. } | WorkerMsg::ActionConfirmationReady { .. } - | WorkerMsg::ActionResolutionError { .. } - | WorkerMsg::UnitActionQueued { .. } - | WorkerMsg::UnitActionError { .. }, + | WorkerMsg::ActionResolutionError { .. }, ) => continue, Err(TryRecvError::Empty) => break, Err(TryRecvError::Disconnected) => { @@ -1037,9 +1035,7 @@ mod tests { WorkerMsg::DetailLogsLoaded { .. } | WorkerMsg::DetailLogsError { .. } | WorkerMsg::ActionConfirmationReady { .. } - | WorkerMsg::ActionResolutionError { .. } - | WorkerMsg::UnitActionQueued { .. } - | WorkerMsg::UnitActionError { .. } => false, + | WorkerMsg::ActionResolutionError { .. } => false, } } diff --git a/src/types.rs b/src/types.rs index 8e2a4df..50bab5c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -353,22 +353,6 @@ pub enum WorkerMsg { /// Error text to show in the UI. error: String, }, - /// A unit action request was accepted and queued successfully. - UnitActionQueued { - /// Unit for which the action was queued. - unit: String, - /// Queued action. - action: UnitAction, - }, - /// A unit action failed. - UnitActionError { - /// Unit for which the action was attempted. - unit: String, - /// Action that failed. - action: UnitAction, - /// Error text to show in the UI. - error: String, - }, /// Refresh worker finished all tasks. Finished, /// Refresh worker failed with a terminal error. diff --git a/tests/tui_runtime_integration.rs b/tests/tui_runtime_integration.rs index 2c67f17..c35a1c2 100644 --- a/tests/tui_runtime_integration.rs +++ b/tests/tui_runtime_integration.rs @@ -67,7 +67,7 @@ fn debug_tui_action_confirmation_paths_are_exercised_with_pty() { // Delays: // 0.3s — TUI loads debug units // 0.15s — debug action resolution completes (near-instant, but needs one 50ms poll) - // 1.0s — systemctl finishes (may fail, but run_confirmed_action completes) + TUI resumes + // 1.0s — debug stub returns instantly; delay ensures TUI polls and redraws before next key let cmd = format!( concat!( "(sleep 0.3;",