@@ -200,18 +200,68 @@ impl fmt::Debug for Segment {
200200 }
201201}
202202
203- /// A [`MakeWriter`] that sends `tracing_subscriber::fmt`-formatted lines through
204- /// a channel while the TUI is active. When the channel disconnects (TUI exited),
205- /// output falls back to stderr so post-TUI tracing isn't lost.
203+ /// Shared routing state between [`TuiMakeWriter`] and [`TuiTraceGuard`].
204+ ///
205+ /// While `active` is true, formatted trace output is sent to the TUI channel.
206+ /// Once flipped to false (by dropping the guard), output routes to stderr.
207+ pub struct TuiTraceState {
208+ active : AtomicBool ,
209+ }
210+
211+ impl TuiTraceState {
212+ fn new ( ) -> Self {
213+ Self {
214+ active : AtomicBool :: new ( true ) ,
215+ }
216+ }
217+
218+ fn is_active ( & self ) -> bool {
219+ self . active . load ( Ordering :: Acquire )
220+ }
221+ }
222+
223+ /// RAII guard that reroutes tracing from the TUI channel to stderr on drop.
224+ ///
225+ /// When dropped:
226+ /// 1. Flips the routing flag so future traces write to stderr.
227+ /// 2. Drains any buffered `TuiEvent::Log` events from the channel to stderr.
228+ pub struct TuiTraceGuard {
229+ state : Arc < TuiTraceState > ,
230+ rx : mpsc:: Receiver < TuiEvent > ,
231+ }
232+
233+ impl TuiTraceGuard {
234+ #[ must_use]
235+ pub fn new ( state : Arc < TuiTraceState > , rx : mpsc:: Receiver < TuiEvent > ) -> Self {
236+ Self { state, rx }
237+ }
238+ }
239+
240+ impl Drop for TuiTraceGuard {
241+ fn drop ( & mut self ) {
242+ self . state . active . store ( false , Ordering :: Release ) ;
243+
244+ while let Ok ( event) = self . rx . try_recv ( ) {
245+ if let TuiEvent :: Log ( msg) = event {
246+ let _ = io:: stderr ( ) . write_all ( msg. as_bytes ( ) ) ;
247+ let _ = io:: stderr ( ) . write_all ( b"\n " ) ;
248+ }
249+ }
250+ }
251+ }
252+
253+ /// A [`MakeWriter`] that routes formatted trace lines to the TUI channel
254+ /// while the TUI is active, then to stderr after the [`TuiTraceGuard`] drops.
206255#[ derive( Clone ) ]
207256pub struct TuiMakeWriter {
208257 tx : mpsc:: SyncSender < TuiEvent > ,
258+ state : Arc < TuiTraceState > ,
209259}
210260
211261impl TuiMakeWriter {
212262 #[ must_use]
213- pub fn new ( tx : mpsc:: SyncSender < TuiEvent > ) -> Self {
214- Self { tx }
263+ pub fn new ( tx : mpsc:: SyncSender < TuiEvent > , state : Arc < TuiTraceState > ) -> Self {
264+ Self { tx, state }
215265 }
216266}
217267
@@ -221,15 +271,17 @@ impl<'a> MakeWriter<'a> for TuiMakeWriter {
221271 fn make_writer ( & ' a self ) -> Self :: Writer {
222272 TuiWriter {
223273 tx : self . tx . clone ( ) ,
274+ state : Arc :: clone ( & self . state ) ,
224275 buf : Vec :: with_capacity ( 256 ) ,
225276 }
226277 }
227278}
228279
229280/// Per-event writer that buffers a single formatted log line.
230- /// On drop, sends the line through the channel or falls back to stderr .
281+ /// On drop, routes to the TUI channel or stderr based on [`TuiTraceState`] .
231282pub struct TuiWriter {
232283 tx : mpsc:: SyncSender < TuiEvent > ,
284+ state : Arc < TuiTraceState > ,
233285 buf : Vec < u8 > ,
234286}
235287
@@ -254,9 +306,12 @@ impl Drop for TuiWriter {
254306 if trimmed. is_empty ( ) {
255307 return ;
256308 }
257- // Send to TUI for inline display (best-effort).
258- // A separate tracing layer handles stderr output independently.
259- let _ = self . tx . try_send ( TuiEvent :: Log ( trimmed) ) ;
309+ if self . state . is_active ( ) {
310+ let _ = self . tx . try_send ( TuiEvent :: Log ( trimmed) ) ;
311+ } else {
312+ let _ = io:: stderr ( ) . write_all ( trimmed. as_bytes ( ) ) ;
313+ let _ = io:: stderr ( ) . write_all ( b"\n " ) ;
314+ }
260315 }
261316}
262317
@@ -586,11 +641,17 @@ mod tests {
586641 assert ! ( debug. contains( '3' ) ) ;
587642 }
588643
644+ fn make_active_state ( ) -> Arc < TuiTraceState > {
645+ Arc :: new ( TuiTraceState :: new ( ) )
646+ }
647+
589648 #[ test]
590649 fn tui_writer_sends_through_channel ( ) {
591650 let ( tx, rx) = mpsc:: sync_channel :: < TuiEvent > ( 16 ) ;
651+ let state = make_active_state ( ) ;
592652 let mut writer = TuiWriter {
593653 tx,
654+ state,
594655 buf : Vec :: new ( ) ,
595656 } ;
596657 writer. write_all ( b" INFO ferrite: hello world\n " ) . unwrap ( ) ;
@@ -607,8 +668,10 @@ mod tests {
607668 #[ test]
608669 fn tui_writer_empty_buffer_sends_nothing ( ) {
609670 let ( tx, rx) = mpsc:: sync_channel :: < TuiEvent > ( 16 ) ;
671+ let state = make_active_state ( ) ;
610672 let writer = TuiWriter {
611673 tx,
674+ state,
612675 buf : Vec :: new ( ) ,
613676 } ;
614677 drop ( writer) ;
@@ -618,6 +681,37 @@ mod tests {
618681 ) ;
619682 }
620683
684+ #[ test]
685+ fn tui_writer_routes_to_stderr_when_inactive ( ) {
686+ let ( tx, rx) = mpsc:: sync_channel :: < TuiEvent > ( 16 ) ;
687+ let state = make_active_state ( ) ;
688+ state. active . store ( false , Ordering :: Release ) ;
689+ let mut writer = TuiWriter {
690+ tx,
691+ state,
692+ buf : Vec :: new ( ) ,
693+ } ;
694+ writer. write_all ( b"routed to stderr\n " ) . unwrap ( ) ;
695+ drop ( writer) ;
696+ // Nothing sent to the TUI channel
697+ assert ! ( rx. try_recv( ) . is_err( ) ) ;
698+ }
699+
700+ #[ test]
701+ fn tui_trace_guard_drains_and_deactivates ( ) {
702+ let state = make_active_state ( ) ;
703+ let ( tx, rx) = mpsc:: sync_channel :: < TuiEvent > ( 16 ) ;
704+ let _ = tx. try_send ( TuiEvent :: Log ( "buffered msg" . into ( ) ) ) ;
705+ let _ = tx. try_send ( TuiEvent :: Tick ) ; // non-Log events are discarded
706+ let _ = tx. try_send ( TuiEvent :: Log ( "second msg" . into ( ) ) ) ;
707+
708+ let guard = TuiTraceGuard :: new ( Arc :: clone ( & state) , rx) ;
709+ assert ! ( state. is_active( ) ) ;
710+ drop ( guard) ;
711+ assert ! ( !state. is_active( ) ) ;
712+ // rx is consumed by the guard — channel fully disconnected
713+ }
714+
621715 mod event_loop {
622716 use std:: sync:: Arc ;
623717 use std:: sync:: atomic:: Ordering ;
0 commit comments