···7788## [Unreleased] - yyyy-mm-dd
991010+### Fixed
1111+1212+- Status bar is cleaned every time after execution is completed.
1313+1014## [v1.2.0] - 2026-03-18
11151216### Added
+10-8
crates/cli/src/apply.rs
···66use miette::{Diagnostic, IntoDiagnostic, Result};
77use std::any::Any;
88use std::collections::HashSet;
99-use std::io::{Read, stderr};
99+use std::io::Read;
1010use std::sync::Arc;
1111use std::sync::atomic::AtomicBool;
1212use thiserror::Error;
···1515use wire_core::hive::node::{Name, Node};
1616use wire_core::hive::plan::{Goal, plan_for_node};
1717use wire_core::hive::{Hive, HiveLocation};
1818-use wire_core::status::STATUS;
1818+use wire_core::status::{UI_SENDER, UiMessage};
1919use wire_core::{SubCommandModifiers, errors::HiveLibError};
20202121use crate::cli::{ApplyTarget, CommonVerbArgs, Partitions};
···132132 );
133133 }
134134135135- STATUS
136136- .lock()
137137- .add_many(&partitioned_names.iter().collect::<Vec<_>>());
135135+ if let Some(tx) = UI_SENDER.get() {
136136+ let _ = tx.send(UiMessage::AddMany(partitioned_names.clone()));
137137+ }
138138139139 let mut set = hive
140140 .nodes
···178178 );
179179 }
180180181181- if !errors.is_empty() {
182182- // clear the status bar if we are about to print error messages
183183- STATUS.lock().clear(&mut stderr());
181181+ // clear the status bar at the end of execution.
182182+ if let Some(tx) = UI_SENDER.get() {
183183+ let _ = tx.send(UiMessage::Clear);
184184+ }
184185186186+ if !errors.is_empty() {
185187 return Err(NodeErrors(
186188 errors
187189 .into_iter()
+19-65
crates/cli/src/tracing_setup.rs
···11// SPDX-License-Identifier: AGPL-3.0-or-later
22// Copyright 2024-2025 wire Contributors
3344-use std::{
55- collections::VecDeque,
66- io::{self, Stderr, Write, stderr},
77- time::Duration,
88-};
44+use std::io::{self, Write};
95106use clap_verbosity_flag::{LogLevel, Verbosity};
117use owo_colors::{OwoColorize, Stream, Style};
88+use tokio::sync::mpsc;
129use tracing::{Level, Subscriber};
1310use tracing_log::AsTrace;
1411use tracing_subscriber::{
···2219 registry::LookupSpan,
2320 util::SubscriberInitExt,
2421};
2525-use wire_core::{STDIN_CLOBBER_LOCK, status::STATUS};
2222+use wire_core::status::{UI_SENDER, UiMessage};
26232727-/// The non-clobbering writer ensures that log lines are held while interactive
2828-/// prompts are shown to the user. If logs where shown, they would "clobber" the
2929-/// sudo / ssh prompt.
3030-///
3131-/// Additionally, the `STDIN_CLOBBER_LOCK` is used to ensure that no two
3232-/// interactive prompts are shown at the same time.
3333-struct NonClobberingWriter {
3434- queue: VecDeque<Vec<u8>>,
3535- stderr: Stderr,
3636-}
2424+/// Forwards log lines to the UI worker over `UI_SENDER`.
2525+struct NonClobberingWriter;
37263827impl NonClobberingWriter {
3939- fn new() -> Self {
4040- NonClobberingWriter {
4141- queue: VecDeque::with_capacity(100),
4242- stderr: stderr(),
4343- }
4444- }
4545-4646- /// expects the caller to write the status line
4747- fn dump_previous(&mut self) -> Result<(), io::Error> {
4848- STATUS.lock().clear(&mut self.stderr);
4949-5050- for buf in self.queue.iter().rev() {
5151- self.stderr.write(buf).map(|_| ())?;
5252- }
5353-5454- Ok(())
2828+ const fn new() -> Self {
2929+ NonClobberingWriter
5530 }
5631}
57325833impl Write for NonClobberingWriter {
5959- fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
6060- if let 1.. = STDIN_CLOBBER_LOCK.available_permits() {
6161- self.dump_previous().map(|()| 0)?;
6262-6363- STATUS.lock().write_above_status(buf, &mut self.stderr)
6464- } else {
6565- self.queue.push_front(buf.to_vec());
6666-6767- Ok(buf.len())
3434+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
3535+ if let Some(tx) = UI_SENDER.get() {
3636+ let _ = tx.send(UiMessage::LogLine(buf.to_vec()));
6837 }
3838+3939+ Ok(buf.len())
6940 }
70417171- fn flush(&mut self) -> std::io::Result<()> {
7272- self.stderr.flush()
4242+ fn flush(&mut self) -> io::Result<()> {
4343+ Ok(())
7344 }
7445}
7546···231202 }
232203}
233204234234-async fn status_tick_worker() {
235235- let mut interval = tokio::time::interval(Duration::from_secs(1));
236236- let mut stderr = stderr();
237237-238238- loop {
239239- interval.tick().await;
240240-241241- if STDIN_CLOBBER_LOCK.available_permits() < 1 {
242242- continue;
243243- }
244244-245245- let mut status = STATUS.lock();
246246-247247- status.clear(&mut stderr);
248248- status.write_status(&mut stderr);
249249- }
250250-}
251251-252205/// Set up logging for the application
253206/// Uses `WireFieldFormat` if -v was never passed
254207pub fn setup_logging<L: LogLevel>(verbosity: &Verbosity<L>, show_progress: bool) {
255208 let filter = verbosity.log_level_filter().as_trace();
256209 let registry = tracing_subscriber::registry();
257210258258- STATUS.lock().show_progress(show_progress);
211211+ let (tx, rx) = mpsc::unbounded_channel();
212212+ UI_SENDER
213213+ .set(tx)
214214+ .expect("expected setup_logging to the first and only .set() of `UI_SENDER`");
259215260216 // spawn worker to tick the status bar
261261- if show_progress {
262262- tokio::spawn(status_tick_worker());
263263- }
217217+ tokio::spawn(wire_core::status::status_tick_worker(rx, show_progress));
264218265219 if verbosity.is_present() {
266220 let layer = tracing_subscriber::fmt::layer()