toolkit for mdBook [mirror of my GitHub repo] docs.tonywu.dev/mdbookkit/
permalinks rust-analyzer mdbook
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: prevent progress bars from clobbering logs

Tony Wu e7460868 7886ba46

+404 -245
+9 -9
crates/mdbook-rustdoc-links/src/client.rs
··· 31 31 use tower::ServiceBuilder; 32 32 use tracing::{Level, debug, trace}; 33 33 34 - use mdbookkit::{emit_debug, emit_warning, timer, timer_event}; 34 + use mdbookkit::{emit_debug, emit_warning, ticker, ticker_event}; 35 35 36 36 use crate::{ 37 37 env::Environment, ··· 112 112 async fn spawn(env: &Environment) -> Result<Self> { 113 113 struct State { 114 114 sender: mpsc::Sender<Poll<()>>, 115 - timer: Option<tracing::Span>, 115 + ticker: Option<tracing::Span>, 116 116 // this span is never entered, ticker/timing is updated on span close 117 117 percent_indexed: Option<u32>, 118 118 last_update: Option<String>, 119 119 } 120 120 121 121 impl State { 122 - fn timer(&self) -> Option<tracing::span::Id> { 123 - self.timer.as_ref()?.id() 122 + fn ticker(&self) -> Option<tracing::span::Id> { 123 + self.ticker.as_ref()?.id() 124 124 } 125 125 } 126 126 ··· 166 166 state.percent_indexed = Some(0); 167 167 168 168 let msg = begin.message.as_deref().unwrap_or_default(); 169 - timer_event!(state.timer(), Level::INFO, "{msg}"); 169 + ticker_event!(state.ticker(), Level::INFO, "{msg}"); 170 170 171 171 let tx = state.sender.clone(); 172 172 tokio::spawn(async move { tx.send(Poll::Pending).await.ok() }); ··· 179 179 .unwrap_or(true) 180 180 { 181 181 state.last_update = Some(msg.into()); 182 - timer_event!(state.timer(), Level::INFO, "{msg}"); 182 + ticker_event!(state.ticker(), Level::INFO, "{msg}"); 183 183 } 184 184 185 185 let Some(indexed) = state.percent_indexed.as_mut() else { ··· 219 219 return; 220 220 } 221 221 222 - state.timer.take(); 222 + state.ticker.take(); 223 223 224 224 let tx = state.sender.clone(); 225 225 tokio::spawn(async move { tx.send(Poll::Ready(())).await.ok() }); ··· 233 233 234 234 let (sender, receiver) = mpsc::channel(16); 235 235 236 - let timer = timer!(Level::INFO, "rust-analyzer"); 236 + let ticker = ticker!(Level::INFO, "rust-analyzer"); 237 237 238 238 let stabilizer = EventSampling { 239 239 buffer: Duration::from_millis(500), ··· 245 245 let (background, mut server) = MainLoop::new_client(move |_| { 246 246 let state = State { 247 247 sender, 248 - timer: Some(timer), 248 + ticker: Some(ticker), 249 249 percent_indexed: Some(0), 250 250 last_update: None, 251 251 };
+5 -5
crates/mdbook-rustdoc-links/src/resolver.rs
··· 6 6 use tokio::task::JoinSet; 7 7 use tracing::{Instrument, Level, debug}; 8 8 9 - use mdbookkit::{emit_debug, timer, timer_item}; 9 + use mdbookkit::{emit_debug, ticker, ticker_item}; 10 10 11 11 use crate::{UNIQUE_ID, client::Client, item::Item, link::ItemLinks, page::Pages, url::UrlToPath}; 12 12 ··· 76 76 .await? 77 77 .pipe(Arc::new); 78 78 79 - let timer = timer!(Level::INFO, "resolve-items", count = request.len()); 79 + let ticker = ticker!(Level::INFO, "resolve-items", count = request.len()); 80 80 81 81 let tasks: JoinSet<Option<(String, ItemLinks)>> = request 82 82 .into_iter() 83 83 .map(|(key, pos)| { 84 84 let key = key.to_string(); 85 85 let doc = document.clone(); 86 - let timer = timer_item!(&timer, Level::INFO, "resolve", item = ?key); 86 + let ticker = ticker_item!(&ticker, Level::INFO, "resolve", item = ?key); 87 87 async move { 88 88 for p in pos { 89 89 let resolved = doc ··· 99 99 } 100 100 None 101 101 } 102 - .instrument(timer) 102 + .instrument(ticker) 103 103 }) 104 104 .collect(); 105 105 106 106 let resolved = tasks 107 107 .join_all() 108 - .instrument(timer) 108 + .instrument(ticker) 109 109 .await 110 110 .into_iter() 111 111 .flatten()
+4 -4
crates/mdbookkit/src/diagnostics.rs
··· 32 32 33 33 /// Trait for diagnostics classes. This is like a specific error code. 34 34 /// 35 - /// **For implementors:** The [`Display`] implementation, which is the title of each 36 - /// diagnostic message, should use plurals whenever possible, because error reporters 37 - /// may elect to group together multiple labels of the same [`Issue`] 35 + /// **For implementors:** The [`Display`][fmt::Display] implementation, which is the 36 + /// title of each diagnostic message, should use plurals whenever possible, because 37 + /// error reporters may elect to group together multiple labels of the same [`Issue`] 38 38 pub trait Issue: Default + fmt::Debug + fmt::Display + Clone + Send + Sync { 39 39 fn level(&self) -> Level; 40 40 } ··· 83 83 output 84 84 } 85 85 86 - /// Render the diagnostics as a list of log messages suitable for [`log`]. 86 + /// Render the diagnostics as a list of log messages suitable for logging. 87 87 pub fn to_logs(&self) -> String { 88 88 let mut output = String::new(); 89 89 LoggingReportHandler
-10
crates/mdbookkit/src/lib.rs
··· 1 - //! Toolkit for [`mdbook`]. 2 - //! 3 - //! This is the lib documentation. If you are looking for the mdBook [preprocessors] 4 - //! that this crate provides, visit <https://tonywu6.github.io/mdbookkit/> instead. 5 - //! 6 - //! At the moment, the sole purpose of this crate is to facilitate easier testing. Most of the APIs 7 - //! are not designed for library use and are explicitly NOT stable. 8 - //! 9 - //! [preprocessors]: https://rust-lang.github.io/mdBook/format/configuration/preprocessors.html 10 - 11 1 pub mod book; 12 2 pub mod diagnostics; 13 3 #[cfg(feature = "_testing")]
+144 -216
crates/mdbookkit/src/logging.rs
··· 1 + //! Logging facilities. 2 + //! 3 + //! ## How the ticker system works 4 + //! 5 + //! The ticker system integrates with [`tracing`]. 6 + //! 7 + //! Use [`ticker!`][crate::ticker] to create a progress ticker backed by a [`tracing::Span`]. 8 + //! When the span [closes][mod@tracing::span#closing-spans], the ticker is cleared. 9 + //! 10 + //! ``` 11 + //! # use tracing::Level; 12 + //! # use mdbookkit::ticker; 13 + //! # 14 + //! let ticker = ticker!(Level::INFO, "task-name", count = 63); 15 + //! // ⠋ [parent-span] task-name (0/63) ... 16 + //! ``` 17 + //! 18 + //! Use [`ticker_event!`][crate::ticker_event] to flash a message in the specified ticker. 19 + //! The message is backed by a [`tracing::Event`]. 20 + //! 21 + //! ``` 22 + //! # use tracing::Level; 23 + //! # use mdbookkit::{ticker, ticker_event}; 24 + //! # let ticker = ticker!(Level::INFO, "task-name", count = 63); 25 + //! # 26 + //! ticker_event!(&ticker, Level::INFO, "task updated"); 27 + //! // ⠋ [parent-span] task-name (0/63) ... task updated 28 + //! ``` 29 + //! 30 + //! Use [`ticker_item!`][crate::ticker_item] to add a subtask to the specified ticker, 31 + //! backed by a [`tracing::Span`]. The `item` field provides the displayed name. 32 + //! When the span closes, the item count in the ticker is increased by 1. 33 + //! 34 + //! ``` 35 + //! # use tracing::Level; 36 + //! # use mdbookkit::{ticker, ticker_item}; 37 + //! # let ticker = ticker!(Level::INFO, "task-name", count = 63); 38 + //! # 39 + //! let item = ticker_item!(&ticker, Level::INFO, "task", item = "item name"); 40 + //! // ⠋ [parent-span] task-name (0/63) ... item name 41 + //! drop(item); 42 + //! // ⠋ [parent-span] task-name (1/63) ... item name 43 + //! ``` 44 + //! 45 + //! If the application is configured to be in logging mode, these are emitted as regular logs. 46 + //! 47 + //! ```plaintext 48 + //! INFO parent-span:task-name: started 49 + //! INFO parent-span:task-name: task updated 50 + //! INFO parent-span:task-name:task{item="item name"}: started 51 + //! INFO parent-span:task-name:task{item="item name"}: finished 52 + //! INFO parent-span:task-name: finished 53 + //! ``` 54 + //! 55 + //! ### Notes 56 + //! 57 + //! This system uses the parent-child relationship between spans and events to known 58 + //! which progress bars to update. 59 + //! 60 + //! Parent spans must be specified explicitly because ticker and item lifecycles are 61 + //! tracked in [`Layer::on_new_span`] and [`Layer::on_close`], during which a parent span 62 + //! may not have been [entered][mod@tracing::span#entering-a-span], resulting in a 63 + //! `ticker_item!` or a `ticker_event!` without a parent. 64 + //! 65 + //! Spans are tracked at open/close instead of enter/exit because spans may enter/exit 66 + //! multiple times, which does not make sense for progress bars. 67 + 1 68 use std::{ 2 - collections::BTreeSet, 3 - fmt::Debug, 4 - sync::{Arc, LazyLock, mpsc}, 5 - thread, 6 - time::{Duration, Instant}, 69 + fmt::{Debug, Display}, 70 + io::Write, 71 + sync::{Arc, LazyLock}, 7 72 }; 8 73 9 - use console::{StyledObject, Term}; 10 - use indicatif::{HumanDuration, ProgressBar, ProgressDrawTarget, ProgressStyle}; 74 + use console::StyledObject; 11 75 use tap::{Pipe, Tap}; 12 76 use tracing::{ 13 77 Event, Subscriber, 14 78 field::{Field, Visit}, 15 79 span::{Attributes, Id}, 16 - warn, 17 80 }; 18 81 use tracing_subscriber::{ 19 82 EnvFilter, Layer, ··· 25 88 26 89 use crate::env::{MDBOOK_LOG, is_colored, is_logging, set_colored, set_logging}; 27 90 91 + use self::writer::{MultiProgressTicker, MultiProgressWriter}; 92 + 93 + mod writer; 94 + 28 95 #[doc(hidden)] 29 96 #[macro_export] 30 97 macro_rules! branded { ··· 83 150 .without_time() 84 151 .with_target(filter.max_level_hint().unwrap_or(options.level) < LevelFilter::INFO) 85 152 .with_ansi(is_colored()) 86 - .with_writer(|| WRITER.clone()) 87 - .with_filter(if TICKER.is_some() { 153 + .with_writer(|| TICKER.writer()) 154 + .with_filter(if TICKER.is_enabled() { 88 155 Some(filter_fn(|metadata| !is_branded!(metadata))) 89 156 } else { 90 157 None 91 158 }); 92 159 93 - let ticker = ConsoleLayer.with_filter(if TICKER.is_none() { 160 + let ticker = TickerLayer.with_filter(if !TICKER.is_enabled() { 94 161 Some(filter_fn(|metadata| !metadata.is_event())) 95 162 } else { 96 163 None ··· 117 184 } 118 185 119 186 #[macro_export] 120 - macro_rules! timer { 187 + macro_rules! ticker { 121 188 ( $level:expr, $key:literal, $( $span:tt )* ) => { 122 189 ::tracing::span!( 123 190 $level, 124 191 $key, 125 - { $crate::branded!("timer") } = ::tracing::field::Empty, 192 + { $crate::branded!("ticker") } = ::tracing::field::Empty, 126 193 $($span)* 127 194 ) 128 195 }; 129 196 ( $level:expr, $key:literal ) => { 130 - $crate::timer!($level, $key,) 197 + $crate::ticker!($level, $key,) 131 198 } 132 199 } 133 200 134 201 #[macro_export] 135 - macro_rules! timer_event { 202 + macro_rules! ticker_event { 136 203 ( $parent:expr, $level:expr, $( $span:tt )* ) => { 137 204 ::tracing::event!( 138 205 parent: $parent, 139 206 $level, 140 - { $crate::branded!("timer.event") } = ::tracing::field::Empty, 207 + { $crate::branded!("ticker.event") } = ::tracing::field::Empty, 141 208 $($span)* 142 209 ) 143 210 }; 144 211 ( $parent:expr, $level:expr ) => { 145 - $crate::timer_event!($parent, $level,) 212 + $crate::ticker_event!($parent, $level,) 146 213 } 147 214 } 148 215 149 216 #[macro_export] 150 - macro_rules! timer_item { 217 + macro_rules! ticker_item { 151 218 ( $parent:expr, $level:expr, $key:literal, $( $span:tt )* ) => { 152 219 ::tracing::span!( 153 220 parent: $parent, 154 221 $level, 155 222 $key, 156 - { $crate::branded!("timer.item") } = ::tracing::field::Empty, 223 + { $crate::branded!("ticker.item") } = ::tracing::field::Empty, 157 224 $($span)* 158 225 ) 159 226 }; 160 227 ( $parent:expr, $level:expr, $key:literal ) => { 161 - $crate::timer_item!($parent, $level, $key,) 228 + $crate::ticker_item!($parent, $level, $key,) 162 229 } 163 230 } 164 231 165 - struct ConsoleLayer; 232 + struct TickerLayer; 166 233 167 - impl<S> Layer<S> for ConsoleLayer 234 + impl<S> Layer<S> for TickerLayer 168 235 where 169 236 S: Subscriber + for<'a> LookupSpan<'a>, 170 237 { 171 238 fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) { 172 239 let Some(span) = ctx.span(id) else { return }; 173 240 174 - if is_branded!(span, "timer") { 175 - let TimerVisitor { count, .. } = TimerVisitor::from_attrs(attrs); 241 + if is_branded!(span, "ticker") { 242 + let TickerVisitor { count, .. } = TickerVisitor::from_attrs(attrs); 176 243 177 - let timer = Timer { 244 + let ticker = TickerData { 178 245 prefix: SpanPath::to_string(&span), 179 246 key: span.name(), 180 247 count, 181 248 }; 182 249 183 - span.extensions_mut().insert(timer.clone()); 250 + span.extensions_mut().insert(ticker.clone()); 184 251 185 - if let Some(ticker) = &*TICKER { 186 - (ticker.tx).send(ProgressTick::TimerCreate(timer)).ok(); 252 + if let Some(tx) = TICKER.sender() { 253 + tx.send(ProgressTick::TickerCreate(ticker)).ok(); 187 254 } else { 188 255 derive_event!(id, span.metadata(), "message" = "started"); 189 256 } 190 - } else if is_branded!(span, "timer.item") 191 - && let TimerVisitor { 257 + } else if is_branded!(span, "ticker.item") 258 + && let TickerVisitor { 192 259 item: Some(item), .. 193 - } = TimerVisitor::from_attrs(attrs) 260 + } = TickerVisitor::from_attrs(attrs) 194 261 && let Some(parent) = span.parent() 195 - && let Some(Timer { key, .. }) = parent.extensions().get::<Timer>() 262 + && let Some(TickerData { key, .. }) = parent.extensions().get::<TickerData>() 196 263 { 197 - span.extensions_mut().insert(TimerItem(item.clone())); 264 + span.extensions_mut().insert(TickerItem(item.clone())); 198 265 199 - if let Some(ticker) = &*TICKER { 200 - (ticker.tx).send(ProgressTick::ItemOpen { key, item }).ok(); 266 + if let Some(tx) = TICKER.sender() { 267 + tx.send(ProgressTick::ItemOpen { key, item }).ok(); 201 268 } else { 202 269 derive_event!(id, span.metadata(), "message" = "started"); 203 270 } ··· 210 277 // n.b. using extensions_mut involves a RwLock write, which will cause 211 278 // derive_event to deadlock when it tries to read from the same span 212 279 213 - if let Some(Timer { key, .. }) = span.extensions().get::<Timer>() { 214 - if let Some(ticker) = &*TICKER { 215 - ticker.tx.send(ProgressTick::TimerFinish { key }).ok(); 280 + if let Some(TickerData { key, .. }) = span.extensions().get::<TickerData>() { 281 + if let Some(tx) = TICKER.sender() { 282 + tx.send(ProgressTick::TickerFinish { key }).ok(); 216 283 } else { 217 284 derive_event!(id, span.metadata(), "message" = "finished"); 218 285 } 219 286 } else if let Some(parent) = span.parent() 220 - && let Some(Timer { key, .. }) = parent.extensions().get::<Timer>() 221 - && let Some(TimerItem(item)) = span.extensions().get::<TimerItem>() 287 + && let Some(TickerData { key, .. }) = parent.extensions().get::<TickerData>() 288 + && let Some(TickerItem(item)) = span.extensions().get::<TickerItem>() 222 289 { 223 - if let Some(ticker) = &*TICKER { 290 + if let Some(tx) = TICKER.sender() { 224 291 let item = item.clone(); 225 - (ticker.tx).send(ProgressTick::ItemDone { key, item }).ok(); 292 + tx.send(ProgressTick::ItemDone { key, item }).ok(); 226 293 } else { 227 294 derive_event!(id, span.metadata(), "message" = "finished"); 228 295 } ··· 234 301 return; 235 302 } 236 303 237 - if let TimerVisitor { 304 + if let TickerVisitor { 238 305 message: Some(msg), .. 239 - } = TimerVisitor::from_event(event) 306 + } = TickerVisitor::from_event(event) 240 307 && let Some(span) = (event.parent().cloned()) 241 308 .or_else(|| ctx.current_span().id().cloned()) 242 309 .and_then(|id| ctx.span(&id)) 243 - && let Some(Timer { key, .. }) = span.extensions().get::<Timer>() 310 + && let Some(TickerData { key, .. }) = span.extensions().get::<TickerData>() 244 311 { 245 - if let Some(ticker) = &*TICKER { 246 - (ticker.tx) 247 - .send(ProgressTick::TimerUpdate { key, msg }) 248 - .ok(); 312 + if let Some(tx) = TICKER.sender() { 313 + tx.send(ProgressTick::TickerUpdate { key, msg }).ok(); 249 314 } 250 315 } 251 316 } 252 317 } 253 318 254 319 #[derive(Debug, Default)] 255 - struct TimerVisitor { 320 + struct TickerVisitor { 256 321 message: Option<String>, 257 322 item: Option<Arc<str>>, 258 323 count: Option<u64>, 259 324 } 260 325 261 - impl Visit for TimerVisitor { 326 + impl Visit for TickerVisitor { 262 327 fn record_str(&mut self, field: &Field, value: &str) { 263 328 match field.name() { 264 329 "message" => self.message = Some(value.into()), ··· 278 343 } 279 344 } 280 345 281 - impl TimerVisitor { 346 + impl TickerVisitor { 282 347 #[inline] 283 348 fn from_attrs(attrs: &Attributes<'_>) -> Self { 284 349 Self::default().tap_mut(|v| attrs.values().record(v)) ··· 319 384 } 320 385 321 386 #[derive(Debug, Clone)] 322 - struct Timer { 387 + struct TickerData { 323 388 prefix: Option<Arc<str>>, 324 389 key: &'static str, 325 390 count: Option<u64>, 326 391 } 327 392 328 - impl std::fmt::Display for Timer { 393 + impl Display for TickerData { 329 394 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 330 395 if let Some(ref prefix) = self.prefix { 331 396 write!(f, "[{prefix}] {}", self.key) ··· 336 401 } 337 402 338 403 #[derive(Debug, Clone)] 339 - struct TimerItem(Arc<str>); 404 + struct TickerItem(Arc<str>); 340 405 341 406 #[derive(Debug)] 342 407 enum ProgressTick { 343 - TimerCreate(Timer), 344 - TimerUpdate { key: &'static str, msg: String }, 408 + TickerCreate(TickerData), 409 + TickerUpdate { key: &'static str, msg: String }, 345 410 ItemOpen { key: &'static str, item: Arc<str> }, 346 411 ItemDone { key: &'static str, item: Arc<str> }, 347 - TimerFinish { key: &'static str }, 348 - } 349 - 350 - #[derive(Clone)] 351 - struct ProgressTicker { 352 - tx: mpsc::Sender<ProgressTick>, 412 + TickerFinish { key: &'static str }, 353 413 } 354 414 355 - static WRITER: LazyLock<Term> = LazyLock::new(Term::stderr); 356 - 357 - static TICKER: LazyLock<Option<ProgressTicker>> = LazyLock::new(|| { 358 - if is_logging() { 359 - None 360 - } else { 361 - Some(spawn_ticker()) 362 - } 363 - }); 364 - 365 - fn spawn_ticker() -> ProgressTicker { 366 - let (tx, rx) = mpsc::channel(); 367 - 368 - let style = ProgressStyle::with_template("{spinner:.cyan} {prefix} ... {msg}") 369 - .unwrap() 370 - .tick_chars("⠇⠋⠙⠸⠴⠦⠿"); 371 - 372 - thread::spawn(move || { 373 - struct Bar { 374 - timer: Timer, 375 - bar: ProgressBar, 376 - } 377 - 378 - impl Bar { 379 - fn count(&self) { 380 - let Self { timer, bar } = self; 381 - if let Some(length) = bar.length() { 382 - let counter = styled(format!("({}/{length})", bar.position())).dim(); 383 - bar.set_prefix(format!("{timer} {counter}")) 384 - } 385 - } 386 - } 387 - 388 - let mut current: Option<Bar> = None; 389 - 390 - let mut tasks = BTreeSet::new(); 391 - let mut task_idx = 0; 392 - let mut interval = Instant::now(); 393 - 394 - loop { 395 - let current_ref = |key: &str| { 396 - let bar = current.as_ref()?; 397 - if bar.timer.key == key { 398 - Some(bar) 399 - } else { 400 - None 401 - } 402 - }; 403 - 404 - match rx.recv_timeout(Duration::from_millis(100)) { 405 - Err(mpsc::RecvTimeoutError::Timeout) => {} 406 - 407 - Err(mpsc::RecvTimeoutError::Disconnected) => break, 408 - 409 - Ok(ProgressTick::TimerCreate(timer)) => { 410 - if let Some(bar) = current { 411 - bar.bar.abandon() 412 - } 413 - 414 - let bar = ProgressDrawTarget::term(WRITER.clone(), 20) 415 - .pipe(|target| ProgressBar::with_draw_target(timer.count, target)) 416 - .with_prefix(timer.to_string()) 417 - .with_style(style.clone()); 418 - 419 - bar.enable_steady_tick(Duration::from_millis(100)); 420 - 421 - current = Some(Bar { timer, bar }); 422 - } 423 - 424 - Ok(ProgressTick::TimerUpdate { key, msg }) => { 425 - let Some(Bar { bar, .. }) = current_ref(key) else { 426 - continue; 427 - }; 428 - 429 - bar.set_message(msg); 430 - bar.tick(); 431 - } 432 - 433 - Ok(ProgressTick::TimerFinish { key }) => { 434 - let Some(Bar { bar, .. }) = current_ref(key) else { 435 - continue; 436 - }; 437 - 438 - bar.finish_with_message(styled("done").green().to_string()); 439 - current = None; 440 - } 441 - 442 - Ok(ProgressTick::ItemOpen { key, item }) => { 443 - let Some(current) = current_ref(key) else { 444 - continue; 445 - }; 446 - 447 - current.count(); 448 - current.bar.set_message(styled(&item).magenta().to_string()); 449 - current.bar.tick(); 450 - 451 - tasks.insert(item); 452 - interval = Instant::now(); 453 - } 454 - 455 - Ok(ProgressTick::ItemDone { key, item }) => { 456 - let Some(current) = current_ref(key) else { 457 - continue; 458 - }; 459 - 460 - current.bar.inc(1); 461 - 462 - current.count(); 463 - current.bar.set_message(styled(&item).green().to_string()); 464 - current.bar.tick(); 465 - 466 - tasks.remove(&item); 467 - interval = Instant::now(); 468 - } 469 - } 470 - 471 - if let Some(Bar { 472 - timer: Timer { key, .. }, 473 - ref bar, 474 - .. 475 - }) = current 476 - { 477 - let now = Instant::now(); 478 - 479 - if now - interval > Duration::from_secs(10) { 480 - interval = now; 481 - 482 - if task_idx >= tasks.len() { 483 - task_idx = 0 484 - } 485 - 486 - if let Some(task) = tasks.iter().nth(task_idx) { 487 - let elapsed = HumanDuration(bar.elapsed()); 488 - // TODO: how to attach to current span 489 - // TODO: how to clear line for tracing when ticker is running 490 - warn!("task {key} - {task} has been running for more than {elapsed}"); 491 - bar.set_message(styled(task).magenta().to_string()); 492 - task_idx += 1; 493 - } 494 - } 495 - } 496 - } 497 - }); 498 - 499 - ProgressTicker { tx } 500 - } 415 + static TICKER: LazyLock<MultiProgressTicker> = 416 + LazyLock::new(|| MultiProgressWriter::new(!is_logging()).pipe(MultiProgressTicker::new)); 501 417 502 418 #[inline] 503 - pub fn stderr() -> impl std::io::Write { 504 - WRITER.clone() 419 + pub fn stderr() -> impl Write { 420 + TICKER.writer() 505 421 } 506 422 507 - // FIXME: ensure colors do not appear in tracing output 508 - // https://github.com/tokio-rs/tracing/issues/3378 423 + /// Configure styling for a displayable value. 424 + /// 425 + /// ## Note 426 + /// 427 + /// Avoid using this in tracing messages. 428 + /// 429 + /// tracing-subscriber currently escapes all ANSI characters. 430 + /// See <https://github.com/tokio-rs/tracing/issues/3378> 431 + /// 432 + /// This function disables styling if the application is in logging mode, so that 433 + /// messages can have styling in tickers but not in logs. 509 434 #[inline] 510 435 pub fn styled<D>(val: D) -> StyledObject<D> { 511 - WRITER.style().apply_to(val) 436 + let styled = TICKER.style().apply_to(val); 437 + if TICKER.is_enabled() { 438 + styled 439 + } else { 440 + styled.force_styling(false) 441 + } 512 442 } 513 443 514 444 #[macro_export] ··· 546 476 |e| ::tracing::warn!($fmt, e) 547 477 }; 548 478 } 549 - 550 - // TODO: clean up logging messages & make use of spans/instrument
+241
crates/mdbookkit/src/logging/writer.rs
··· 1 + use std::{ 2 + collections::{BTreeSet, HashMap}, 3 + io::Write, 4 + sync::{Arc, mpsc}, 5 + thread, 6 + time::{Duration, Instant}, 7 + }; 8 + 9 + use console::{Style, Term}; 10 + use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; 11 + use tap::{Pipe, Tap}; 12 + 13 + use super::{ProgressTick, TickerData, styled}; 14 + 15 + #[derive(Debug, Clone)] 16 + pub struct MultiProgressTicker { 17 + tx: Option<mpsc::Sender<ProgressTick>>, 18 + wr: MultiProgressWriter, 19 + } 20 + 21 + impl MultiProgressTicker { 22 + pub fn new(wr: MultiProgressWriter) -> Self { 23 + Self { tx: None, wr }.tap_mut(spawn_ticker) 24 + } 25 + 26 + #[inline] 27 + pub fn sender(&self) -> Option<mpsc::Sender<ProgressTick>> { 28 + self.tx.clone() 29 + } 30 + 31 + #[inline] 32 + pub fn writer(&self) -> impl Write { 33 + self.wr.clone() 34 + } 35 + 36 + #[inline] 37 + pub fn style(&self) -> Style { 38 + self.wr.term.style() 39 + } 40 + 41 + pub fn is_enabled(&self) -> bool { 42 + self.wr.bars.is_some() 43 + } 44 + } 45 + 46 + fn spawn_ticker(this: &mut MultiProgressTicker) { 47 + let Some(manager) = this.wr.bars.clone() else { 48 + return; 49 + }; 50 + 51 + let (tx, rx) = mpsc::channel(); 52 + this.tx = Some(tx); 53 + 54 + let target = this.wr.term.clone(); 55 + 56 + thread::spawn(move || { 57 + struct Bar { 58 + bar: ProgressBar, 59 + ticker: TickerData, 60 + items: BTreeSet<Arc<str>>, 61 + item_idx: usize, 62 + interval: Instant, 63 + } 64 + 65 + impl Bar { 66 + fn update_count(&self) { 67 + let Self { ticker, bar, .. } = self; 68 + if let Some(length) = bar.length() { 69 + let counter = styled(format!("({}/{length})", bar.position())).dim(); 70 + bar.set_prefix(format!("{ticker} {counter}")) 71 + } 72 + } 73 + } 74 + 75 + let style = ProgressStyle::with_template("{spinner:.cyan} {prefix} ... {msg}") 76 + .unwrap() 77 + .tick_chars("⠇⠋⠙⠸⠴⠦⠿"); 78 + 79 + let mut current = HashMap::new(); 80 + 81 + loop { 82 + match rx.recv_timeout(Duration::from_millis(100)) { 83 + Err(mpsc::RecvTimeoutError::Timeout) => {} 84 + 85 + Err(mpsc::RecvTimeoutError::Disconnected) => break, 86 + 87 + Ok(ProgressTick::TickerCreate(ticker)) => { 88 + let bar = ProgressDrawTarget::term(target.clone(), 20) 89 + .pipe(|target| ProgressBar::with_draw_target(ticker.count, target)) 90 + .with_prefix(ticker.to_string()) 91 + .with_style(style.clone()); 92 + 93 + bar.enable_steady_tick(Duration::from_millis(100)); 94 + 95 + let key = ticker.key; 96 + let bar = Bar { 97 + bar, 98 + ticker, 99 + items: Default::default(), 100 + item_idx: Default::default(), 101 + interval: Instant::now(), 102 + }; 103 + 104 + manager.add(bar.bar.clone()); 105 + current.insert(key, bar); 106 + } 107 + 108 + Ok(ProgressTick::TickerUpdate { key, msg }) => { 109 + let Some(Bar { bar, .. }) = current.get(key) else { 110 + continue; 111 + }; 112 + 113 + bar.set_message(msg); 114 + } 115 + 116 + Ok(ProgressTick::ItemOpen { key, item }) => { 117 + let Some(current) = current.get_mut(key) else { 118 + continue; 119 + }; 120 + 121 + current.update_count(); 122 + current.bar.set_message(styled(&item).magenta().to_string()); 123 + 124 + current.items.insert(item); 125 + current.interval = Instant::now(); 126 + } 127 + 128 + Ok(ProgressTick::ItemDone { key, item }) => { 129 + let Some(current) = current.get_mut(key) else { 130 + continue; 131 + }; 132 + 133 + current.bar.inc(1); 134 + 135 + current.update_count(); 136 + current.bar.set_message(styled(&item).green().to_string()); 137 + 138 + current.items.remove(&item); 139 + current.interval = Instant::now(); 140 + } 141 + 142 + Ok(ProgressTick::TickerFinish { key }) => { 143 + let Some(Bar { bar, .. }) = current.remove(key) else { 144 + continue; 145 + }; 146 + 147 + bar.finish_and_clear(); 148 + manager.remove(&bar); 149 + } 150 + } 151 + 152 + for Bar { 153 + bar, 154 + items, 155 + item_idx, 156 + interval, 157 + .. 158 + } in current.values_mut() 159 + { 160 + let now = Instant::now(); 161 + 162 + if now - *interval > Duration::from_secs(2) { 163 + *interval = now; 164 + 165 + if let Some(item) = items.iter().nth(*item_idx) { 166 + bar.set_message(styled(item).magenta().to_string()); 167 + } 168 + 169 + *item_idx += 1; 170 + if *item_idx >= items.len() { 171 + *item_idx = 0 172 + } 173 + } 174 + } 175 + } 176 + }); 177 + } 178 + 179 + #[derive(Debug, Clone)] 180 + pub struct MultiProgressWriter { 181 + bars: Option<MultiProgress>, 182 + term: Term, 183 + } 184 + 185 + // Prevent progress bars from clobbering log output. 186 + // See https://github.com/emersonford/tracing-indicatif/blob/main/src/writer.rs 187 + 188 + impl Write for MultiProgressWriter { 189 + #[inline] 190 + fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { 191 + self.suspended(|term| term.write(buf)) 192 + } 193 + 194 + #[inline] 195 + fn flush(&mut self) -> std::io::Result<()> { 196 + self.suspended(|term| term.flush()) 197 + } 198 + 199 + #[inline] 200 + fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> { 201 + self.suspended(|term| term.write_vectored(bufs)) 202 + } 203 + 204 + #[inline] 205 + fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { 206 + self.suspended(|term| term.write_all(buf)) 207 + } 208 + 209 + #[inline] 210 + fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::io::Result<()> { 211 + self.suspended(|term| term.write_fmt(args)) 212 + } 213 + } 214 + 215 + impl MultiProgressWriter { 216 + pub fn new(enabled: bool) -> Self { 217 + let term = Term::stderr(); 218 + 219 + let bars = if enabled { 220 + ProgressDrawTarget::term(term.clone(), 20) 221 + .pipe(MultiProgress::with_draw_target) 222 + .pipe(Some) 223 + } else { 224 + None 225 + }; 226 + 227 + Self { bars, term } 228 + } 229 + 230 + #[inline(always)] 231 + fn suspended<F, T>(&mut self, f: F) -> T 232 + where 233 + F: FnOnce(&mut Term) -> T, 234 + { 235 + if let Some(ref bars) = self.bars { 236 + bars.suspend(|| f(&mut self.term)) 237 + } else { 238 + f(&mut self.term) 239 + } 240 + } 241 + }
+1 -1
crates/mdbookkit/src/markdown.rs
··· 10 10 /// as much of the original Markdown source as possible, especially with regard to whitespace. 11 11 /// 12 12 /// Currently, when using [`pulldown_cmark_to_cmark`] to generate Markdown from a 13 - /// [`pulldown_cmark::Event`] stream, whitespace is NOT preserved. This is problematic 13 + /// [`pulldown_cmark::Event`][Event] stream, whitespace is NOT preserved. This is problematic 14 14 /// for mdBook preprocessors, because preprocessors downstream may need to work on 15 15 /// syntax that is whitespace-sensitive. Normalizing all whitespace could cause such 16 16 /// usage to no longer be recognized.