From 0b0ba55a4a8ba0b6d98b4f9c78cfb9fe55a3ed64 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Fri, 9 Jan 2026 01:25:48 -0800 Subject: [PATCH 01/13] working on bar revamp --- packages/copper/Cargo.toml | 4 +- packages/copper/examples/print.rs | 4 + packages/copper/src/lib.rs | 2 + packages/copper/src/print/mod.rs | 6 + packages/copper/src/print/printer.rs | 9 +- packages/copper/src/print/progress/bar.rs | 251 ++++++++++++++++ packages/copper/src/print/progress/eta.rs | 75 +++++ .../print/{progress.rs => progress/mod.rs} | 82 +++-- packages/copper/src/print/progress/state.rs | 284 ++++++++++++++++++ 9 files changed, 680 insertions(+), 37 deletions(-) create mode 100644 packages/copper/src/print/progress/bar.rs create mode 100644 packages/copper/src/print/progress/eta.rs rename packages/copper/src/print/{progress.rs => progress/mod.rs} (89%) create mode 100644 packages/copper/src/print/progress/state.rs diff --git a/packages/copper/Cargo.toml b/packages/copper/Cargo.toml index 61b7d7a..66122ff 100644 --- a/packages/copper/Cargo.toml +++ b/packages/copper/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pistonite-cu" -version = "0.6.10" +version = "0.7.0" edition = "2024" description = "Battery-included common utils to speed up development of rust tools" repository = "https://github.com/Pistonite/cu" @@ -59,7 +59,7 @@ version = "1.49.0" features = [ "macros", "rt-multi-thread", "time" ] [features] -default = [] +default = ["full"] full = [ "coroutine-heavy", "cli", diff --git a/packages/copper/examples/print.rs b/packages/copper/examples/print.rs index e541a2e..c0e48ff 100644 --- a/packages/copper/examples/print.rs +++ b/packages/copper/examples/print.rs @@ -17,6 +17,10 @@ impl Args { /// Run with cargo run --example print --features prompt,cli #[cu::cli(flags = "inner", preprocess = Args::preprocess)] fn main(_: Args) -> cu::Result<()> { + + cu::progress().foo(); + + cu::print!("today's weather is {}", "good"); cu::hint!("today's weather is {}", "ok"); cu::info!( diff --git a/packages/copper/src/lib.rs b/packages/copper/src/lib.rs index af4b0b8..bda9c72 100644 --- a/packages/copper/src/lib.rs +++ b/packages/copper/src/lib.rs @@ -91,6 +91,8 @@ mod print; pub use print::check_password_legality; #[cfg(feature = "print")] pub use print::{ + progress, + ProgressBarBuilder, ProgressBar, ZeroWhenDropString, init_print_options, log_init, progress_bar, progress_bar_lowp, progress_unbounded, progress_unbounded_lowp, set_thread_print_name, term_width, term_width_height, term_width_or_max, diff --git a/packages/copper/src/print/mod.rs b/packages/copper/src/print/mod.rs index bb23836..cb6fd66 100644 --- a/packages/copper/src/print/mod.rs +++ b/packages/copper/src/print/mod.rs @@ -16,3 +16,9 @@ pub use prompt::*; mod prompt_password; #[cfg(feature = "prompt-password")] pub use prompt_password::check_password_legality; + +// 50ms between each cycle +pub(crate) const TICK_INTERVAL: std::time::Duration = std::time::Duration::from_millis(10); +// 2B ticks * 10ms = 251 days. +// overflown tick means ETA will be inaccurate (after 251 days) +pub(crate) type Tick = u32; diff --git a/packages/copper/src/print/printer.rs b/packages/copper/src/print/printer.rs index 8343913..1fb2f6e 100644 --- a/packages/copper/src/print/printer.rs +++ b/packages/copper/src/print/printer.rs @@ -447,7 +447,6 @@ fn print_task(original_width: usize, max_bars: i32) -> JoinHandle<()> { b.push_str("\x1b[1A\x1b[K"); // move up one line and erase it } } - // std::thread::sleep(INTERVAL); clear(buffer, *lines); // scope for locking the printer let Ok(mut printer) = PRINTER.lock() else { @@ -479,19 +478,25 @@ fn print_task(original_width: usize, max_bars: i32) -> JoinHandle<()> { buffer.push_str(printer.colors.yellow); *lines = 0; let anime = CHARS[(tick as usize) % CHARS.len()]; + let mut ii = 0; printer.bars.retain(|bar| { let Some(bar) = bar.upgrade() else { return false; }; if more_bars < 0 { if width >= 2 { + for _ in 0..ii { + buffer.push(' '); + buffer.push(' '); + } buffer.push(anime); buffer.push(']'); - bar.format(width - 2, now, tick, INTERVAL, buffer, temp); + bar.format(width - 2 - ii * 2, now, tick, INTERVAL, buffer, temp); } buffer.push('\n'); *lines += 1; } + ii += 1; more_bars += 1; true diff --git a/packages/copper/src/print/progress/bar.rs b/packages/copper/src/print/progress/bar.rs new file mode 100644 index 0000000..ecff100 --- /dev/null +++ b/packages/copper/src/print/progress/bar.rs @@ -0,0 +1,251 @@ +use std::{ops::Deref, sync::{Arc, Mutex}}; + +use crate::print::progress::state::{State, StateImmut}; + +/// Make a progress bar builder with the following defaults: +/// +/// - Total steps: unbounded +/// - Keep after done: `true` +/// - Show ETA: `true` (only effective if steps is finite) +/// - Finish message: Default +/// - Interrupted message: Default +/// +/// See [`ProgressBarBuilder`] for builder methods +#[inline(always)] +pub fn progress(message: impl Into) -> ProgressBarBuilder { + ProgressBarBuilder::new(message.into()) +} + +/// Builder for a progress bar +pub struct ProgressBarBuilder { + /// The message prefix for the progress bar + message: String, + /// Total steps (None = unbounded, 0 = not known yet) + total: Option, + /// The progress bar is for displaying bytes + total_is_in_bytes: bool, + /// If the bar should be kept after it's done + keep: bool, + /// If ETA should be visible (only effective if total is finite) + show_eta: bool, + /// If percentage should be visible (only effective if total is finite) + show_percentage: bool, + /// Message to display after done, instead of the default + done_message: Option, + /// Message to display if the bar is interrupted + interrupted_message: Option, + /// Optional parent of the bar + parent: Option>, +} +impl ProgressBarBuilder { + /// Start building a progress bar. Note [`cu::progress`](progress) is the canonical shorthand + pub fn new(message: String) -> Self { + Self { + message, + total: None, + total_is_in_bytes: false, + keep: true, + show_eta: true, + show_percentage: true, + done_message: None, + interrupted_message: None, + parent: None, + } + } + /// Set the total steps. `0` means total is unknown, which can be set + /// at a later point. + /// + /// By default, the progress bar is "unbounded", meaning there is no + /// individual steps + /// + /// ```rust + /// # use pistonite_cu as cu; + /// cu::progress("doing something").total(10); + /// ``` + #[inline(always)] + pub fn total(mut self, total: usize) -> Self { + self.total = Some(total as u64); + self + } + + /// Set the total as a `u64` on platforms where `usize` is less than 64 bits + #[cfg(not(target_pointer_width = "64"))] + pub fn total_u64(mut self, total: u64) -> Self { + self.total = Some(total); + self + } + + /// Set the total bytes and set the progress to be displayed using byte units (SI). + /// `0` means total is unknown, which can be set at a later point. + /// + /// ```rust + /// # use pistonite_cu as cu; + /// cu::progress("doing something").total_bytes(1000000); + /// ``` + #[inline(always)] + pub fn total_bytes(mut self, total: u64) -> Self { + self.total = Some(total as u64); + self.total_is_in_bytes = true; + self + } + + /// Set if the progress bar should be kept in the output + /// after it's done. Default is `true` + /// + /// ```rust + /// # use pistonite_cu as cu; + /// cu::progress("doing something").keep(false); + /// ``` + #[inline(always)] + pub fn keep(mut self, keep: bool) -> Self { + self.keep = keep; + self + } + + /// Set if ETA (estimated time) should be displayed. + /// Only effective if total is not zero (i.e. not unbounded). + /// Default is `true` + /// + /// ```rust + /// # use pistonite_cu as cu; + /// cu::progress("doing something").total(10).eta(false); + /// ``` + #[inline(always)] + pub fn eta(mut self, show: bool) -> Self { + self.show_eta = show; + self + } + + /// Set if percentage should be displayed. + /// Only effective if total is not zero (i.e. not unbounded). + /// Default is `true` + /// + /// ```rust + /// # use pistonite_cu as cu; + /// cu::progress("doing something").total(10).percentage(false); + /// ``` + #[inline(always)] + pub fn percentage(mut self, show: bool) -> Self { + self.show_percentage = show; + self + } + + /// Set a message to be displayed when the progress is done. + /// Requires `keep(true)` - which is the default, but + /// `when_done` will not automatically turn it on for you. + /// + /// Default is the message of the progress bar followed by `done`. + /// + /// ```rust + /// # use pistonite_cu as cu; + /// cu::progress("doing something").when_done("something is done!"); + /// ``` + #[inline(always)] + pub fn when_done(mut self, message: impl Into) -> Self { + self.done_message = Some(message.into()); + self + } + + /// Set a message to be displayed when the progress is interrupted. + /// + /// Default is the message of the progress bar followed by `interrupted`. + /// + /// ```rust + /// # use pistonite_cu as cu; + /// cu::progress("doing something").when_interrupt("something is interrupted!"); + /// ``` + #[inline(always)] + pub fn when_interrupt(mut self, message: impl Into) -> Self { + self.interrupted_message = Some(message.into()); + self + } + + /// Build and start displaying the bar in the console + pub fn spawn(self) -> Arc { + todo!() + } +} + +trait IntoProgress { + fn into_progress(self) -> u64; +} +#[rustfmt::skip] +const _: () = { + impl IntoProgress for u64 { #[inline(always)] fn into_progress(self) -> u64 { self } } + impl IntoProgress for u32 { #[inline(always)] fn into_progress(self) -> u64 { self } } + impl IntoProgress for u16 { #[inline(always)] fn into_progress(self) -> u64 { self } } + impl IntoProgress for u8 { #[inline(always)] fn into_progress(self) -> u64 { self } } + impl IntoProgress for usize { #[inline(always)] fn into_progress(self) -> u64 { self as u64 } } +}; + +pub struct ProgressBar { + pub(crate) props: StateImmut, + state: Mutex, +} +impl ProgressBar { + #[doc(hidden)] + #[inline(always)] + pub fn __set(self: &Arc, current: T, message: Option) { + Self::set(self, current.into_progress(), message) + } + fn set(self: &Arc, current: u64, message: Option) { + if let Ok(mut bar) = self.state.lock() { + bar.set_current(current); + if let Some(x) = message { + bar.set_message(&x); + } + } + } + + #[doc(hidden)] + #[inline(always)] + pub fn __inc(self: &Arc, amount: T, message: Option) { + Self::inc(self, amount.into_progress(), message) + } + fn inc(self: &Arc, amount: u64, message: Option) { + if let Ok(mut bar) = self.state.lock() { + bar.inc_current(amount); + if let Some(x) = message { + bar.set_message(&x); + } + } + } + + /// Set the total steps (if the progress is finite) + #[inline(always)] + pub fn set_total(&self, total: T) { + Self::set_total_impl(self, total.into_progress()) + } + fn set_total_impl(&self, total: u64) { + if let Ok(mut bar) = self.state.lock() { + bar.set_total(total); + } + } + + /// Start building a child progress bar + /// + /// Note that the child builder will keep this bar alive (displayed), even + /// if the child is not spawned + #[inline(always)] + pub fn child(self: &Arc, message: impl Into) -> ProgressBarBuilder { + let mut builder = ProgressBarBuilder::new(message.into()); + builder.parent = Some(Arc::clone(self)); + builder + } + + /// Mark the progress bar as done and drop the handle. + /// + /// This needs to be called if the bar is unbounded. Otherwise, + /// the bar will display in the interrupted state when dropped. + /// + /// If the progress is finite, then interrupted state is automatically + /// determined (`current != total`) + pub fn done(self: Arc) { + if self.props.unbounded { + if let Ok(mut bar) = self.state.lock() { + bar.set_current(1); + bar.set_total(1); + } + } + } +} diff --git a/packages/copper/src/print/progress/eta.rs b/packages/copper/src/print/progress/eta.rs new file mode 100644 index 0000000..9c4d4a9 --- /dev/null +++ b/packages/copper/src/print/progress/eta.rs @@ -0,0 +1,75 @@ +use std::time::Instant; + +use crate::print::{Tick, TICK_INTERVAL}; + + +/// Estimate the time for progress bar +pub struct Estimater { + /// Time when the progress started + pub start: Instant, + /// If the ETA is accurate enough to be displayed + is_reasonably_accurate: bool, + /// Step number when we last estimated ETA + last_step: u64, + /// Tick number when we last estimated ETA + last_tick: u32, + /// Last calculation, in seconds + previous_eta: f32 +} + +impl Estimater { + pub fn new() -> Self { + Self { + start: Instant::now(), + is_reasonably_accurate: false, + last_step: 0, + last_tick: 0, + previous_eta: 0.0, + } + } + + pub fn update(&mut self, + now: &mut Option, + current: u64, + total: u64, + tick: Tick, + ) -> Option { + let now = match now { + None => { + let n = Instant::now(); + *now = Some(n); + n } + Some(n) => *n + }; + let elapsed = (now - self.start).as_secs_f32(); + let secs_per_step = elapsed / current as f32; + let mut eta = secs_per_step * (total - current) as f32; + if current == self.last_step { + // subtract time passed since updating to this step + let elapased_since_current = + (TICK_INTERVAL * (tick - self.last_tick)).as_secs_f32(); + if elapased_since_current > eta { + self.last_step = current; + self.last_tick = tick; + } + eta = (eta - elapased_since_current).max(0.0); + // only start showing ETA if it's reasonably accurate + if !self.is_reasonably_accurate + && eta < self.previous_eta - TICK_INTERVAL.as_secs_f32() + { + self.is_reasonably_accurate = true; + } + self.previous_eta = eta; + } else { + self.last_step = current; + self.last_tick = tick; + } + + if !self.is_reasonably_accurate { + None + } else { + Some(eta) + } + + } +} diff --git a/packages/copper/src/print/progress.rs b/packages/copper/src/print/progress/mod.rs similarity index 89% rename from packages/copper/src/print/progress.rs rename to packages/copper/src/print/progress/mod.rs index 67dfada..ca0cc9e 100644 --- a/packages/copper/src/print/progress.rs +++ b/packages/copper/src/print/progress/mod.rs @@ -3,51 +3,60 @@ use std::time::{Duration, Instant}; use super::ansi; +mod eta; +mod state; +mod bar; + /// Update a progress bar /// +/// The macro takes 2 parts separated by comma `,`: +/// - An expression for updating the progress: +/// - Optional format args for updating the message. +/// +/// The progress update expression can be one of: +/// - `bar = i`: set the progress to `i` +/// - `bar += i`: increment the steps by i +/// - `bar`: don't update the progress +/// +/// , where `bar` is an ident +/// +/// The format args can be omitted to update the progress without +/// updating the message +/// /// # Examples /// ```rust,no_run /// # use pistonite_cu as cu; /// let bar = cu::progress_bar(10, "10 steps"); /// // update the current count and message /// let i = 1; -/// cu::progress!(&bar, i, "doing step {i}"); +/// cu::progress!(bar = i, "doing step {i}"); /// // update the current count without changing message -/// cu::progress!(&bar, 2); -/// // update the message without changing count (or the bar is unbounded) -/// cu::progress!(&bar, (), "doing the thing"); +/// cu::progress!(bar += 2); +/// // update the message without changing current step +/// cu::progress!(bar, "doing the thing"); /// ``` #[macro_export] macro_rules! progress { - ($bar:expr, $current:expr) => { - $crate::ProgressBar::set($bar, $current, None); + ($bar:ident, $($fmt_args:tt)*) => { + {$bar}.__inc(0, Some(format!($($fmt_args)*))) + }; + ($bar:ident += $inc:expr) => { + {$bar}.__inc($inc, None) + }; + ($bar:ident += $inc:expr, $($fmt_args:tt)*) => { + {$bar}.__inc($inc, Some(format!($($fmt_args)*))) + }; + ($bar:ident = $x:expr) => { + {$bar}.__set($x, None) + }; + ($bar:ident = $x:expr, $($fmt_args:tt)*) => { + {$bar}.__set($x, Some(format!($($fmt_args)*))) }; - ($bar:expr, (), $($fmt_args:tt)*) => {{ - let message = format!($($fmt_args)*); - $crate::ProgressBar::set_message($bar, message); - }}; - ($bar:expr, $current:expr, $($fmt_args:tt)*) => {{ - let message = format!($($fmt_args)*); - $crate::ProgressBar::set($bar, $current, Some(message)); - }}; } -/// Signify the progress bar is done, with a custom done message -/// -/// # Examples -/// ```rust,no_run -/// # use pistonite_cu as cu; -/// let bar = cu::progress_bar(10, "10 steps"); -/// cu::progress!(&bar, 1, "doing step {}", 1); -/// cu::progress_done!(&bar, "this is {}", "done!"); -/// ``` -#[macro_export] -macro_rules! progress_done { - ($bar:expr, $($fmt_args:tt)*) => {{ - let message = format!($($fmt_args)*); - $crate::ProgressBar::set_done_message($bar, message); - }}; -} + +/// Marker object for testing if a progress bar is interrupted +pub struct ProgressInterruptGuard; /// Create a progress bar pub fn progress_bar(total: usize, message: impl Into) -> Arc { @@ -191,6 +200,16 @@ impl ProgressBar { } } +struct ProgressBarStateImmut { + /// The prefix message (corresponds to message in the builder) + prefix: String, + /// None means don't keep the progress bar printed + /// (the default done message is formatted at spawn time) + done_message: Option, + /// None means use the default + interrupted_message: Option, +} + /// Progress bar state struct ProgressBarState { /// Total count, or 0 for unbounded @@ -211,8 +230,6 @@ struct ProgressBarState { message: String, /// If bounded, used for estimating the ETA started: Instant, - /// If set, print this message when done instead of the default done message - done_message: Option, } impl ProgressBarState { @@ -227,7 +244,6 @@ impl ProgressBarState { started: Instant::now(), prefix, message: String::new(), - done_message: None, } } pub(crate) fn set_total(&mut self, total: usize) { diff --git a/packages/copper/src/print/progress/state.rs b/packages/copper/src/print/progress/state.rs new file mode 100644 index 0000000..1d7e5f7 --- /dev/null +++ b/packages/copper/src/print/progress/state.rs @@ -0,0 +1,284 @@ +use std::sync::Arc; +use std::time::Instant; + +use crate::print::progress::bar::ProgressBar; +use crate::print::progress::eta::Estimater; +use crate::print::{Tick, ansi}; + +/// Immutable part of progress bar +pub struct StateImmut { + /// Parent of this bar + parent: Arc, + /// The prefix message (corresponds to message in the builder) + prefix: String, + /// None means don't keep the progress bar printed + /// (the default done message is formatted at spawn time) + done_message: Option, + /// None means use the default + interrupted_message: Option, + /// If percentage field is shown + show_percentage: bool, + /// If the steps are unbounded + pub unbounded: bool, + /// Display the progress using bytes format + display_bytes: bool, +} + +pub struct State { + unreal_total: u64, + unreal_current: u64, + message: String, + eta: Option +} +impl State { + pub fn new(total: u64, message: String, eta: Option) -> Self { + Self { + unreal_total: total, + unreal_current: 0, + message, + eta, + } + } + #[inline(always)] + fn estimate_remaining( + &mut self, + unbounded: bool, + now: &mut Option, + tick: Tick, + ) -> Option { + if unbounded || self.unreal_total == 0 { + return None; + } + self.eta.as_mut()? + .update(now, self.unreal_current.min(self.unreal_total) + , self.unreal_total, tick + ) + } + #[inline(always)] + fn real_current_total(&self, unbounded: bool) -> (u64, Option) { + if unbounded { + (0, None) + } else if self.unreal_total == 0 { + // total not known + (self.unreal_current, None) + } else { + (self.unreal_current.min(self.unreal_total), Some(self.unreal_total)) + } + } + + pub fn set_current(&mut self, current: u64) { + self.unreal_current = current; + } + + pub fn inc_current(&mut self, current: u64) { + self.unreal_current += current; + } + + pub fn set_total(&mut self, total: u64) { + if total != 0 { + self.unreal_total = total; + } + } + + pub fn set_message(&mut self, message: &str) { + self.message.clear(); + self.message.push_str(message); + } + + + pub fn format( + &mut self, + mut width: usize, + now: &mut Option, + tick: Tick, + out: &mut String, + temp: &mut String, + state: &StateImmut, + ) { + use std::fmt::Write as _; + + // not enough width + match width { + 0 => return, + 1 => { + out.push('.'); + return; + } + 2 => { + out.push_str(".."); + return; + } + 3 => { + out.push_str("..."); + return; + } + 4 => { + out.push_str("[..]"); + return; + } + _ => {} + } + let (current, total) = self.real_current_total(state.unbounded); + // -- + let show_current_total = !state.unbounded; + let show_prefix = !state.prefix.is_empty(); + // -- : + let show_percentage = state.show_percentage && total.is_some(); + let eta = self.estimate_remaining(state.unbounded, now, tick); + let show_eta = eta.is_some(); + let show_message = !self.message.is_empty(); + + struct Spacing { + show_separator: bool, + show_space_before_eta: bool, + show_space_before_message: bool + } + + let spacing = if state.display_bytes { + Spacing { + show_separator: show_prefix && (show_current_total || show_percentage || show_eta || show_message), + show_space_before_eta: show_percentage || show_current_total, + show_space_before_message: show_percentage ||show_current_total ||show_eta, + } + } else { + Spacing { + show_separator: show_prefix && (show_percentage || show_eta || show_message), + show_space_before_eta: show_percentage, + show_space_before_message: show_percentage || show_eta, + } + }; + + if !state.display_bytes && show_current_total { + temp.clear(); + // _: fmt for string does not fail + let _ = match total { + None => write!(temp, "{current}/?"), + Some(total) => write!(temp, "{current}/{total}"), + }; + + // .len() is safe because / and numbers have the same byte size and width + // -2 is safe because width > 4 here + width -= 2; + out.push('['); + if temp.len() > width { + // not enough space + for _ in 0..width { + out.push('.'); + } + out.push(']'); + return; + } + + width -= temp.len(); + out.push_str(temp); + out.push(']'); + + if width > 0 { + out.push(' '); + width -= 1; + } + } + + if show_prefix { + for (c, w) in ansi::with_width(state.prefix.chars()) { + if w > width { + break; + } + width -= w; + out.push(c); + } + } + + if spacing.show_separator && width > 2{ + width -= 2; + out.push_str(": "); + } + + if state.display_bytes && show_current_total { + temp.clear(); + // _: fmt for string does not fail + let _ = match total { + None => write!(temp, "{}", ByteFormat(current)), + Some(total) => write!(temp, "{} / {}", ByteFormat(current), ByteFormat(total)), + }; + + if width >= temp.len() { + width -= temp.len(); + out.push_str(temp); + } + + if width > 0 { + out.push(' '); + width -= 1; + } + } + + if show_percentage { + // unwrap: total is always Some + let total = total.unwrap(); + if current == total { + if width >= 4 { + width -= 4; + out.push_str("100%") + } + } else { + let percentage = current as f32 * 100f32 / total as f32; + temp.clear(); + // _: fmt for string does not fail + let _ = write!(temp, "{percentage:.2}%"); + if width >= temp.len() { + width -= temp.len(); + out.push_str(temp); + } + } + } + + if let Some(eta) = eta { + // ETA SS.SSs + if spacing.show_space_before_eta && width > 0 { + out.push(' '); + width -= 1; + } + temp.clear(); + // _: fmt for string does not fail + let _ = write!(temp, "ETA {eta:.2}s;"); + if width >= temp.len() { + width -= temp.len(); + out.push_str(temp); + } + } + + if show_message { + if spacing.show_space_before_message && width > 0 { + out.push(' '); + width -= 1; + } + for (c, w) in ansi::with_width(self.message.chars()) { + if w > width { + break; + } + width -= w; + out.push(c); + } + } + } +} + +struct ByteFormat(u64); +impl std::fmt::Display for ByteFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (unit_bytes, unit_char) in [ + (1000_000_000_000, 'T'), + (1000_000_000, 'G'), + (1000_000, 'M'), + (1000, 'k'), + ] { + if self.0 >= unit_bytes { + let whole = self.0 / unit_bytes; + let deci = (self.0 % unit_bytes) * 10/unit_bytes; + return write!(f, "{whole}.{deci}{unit_char}"); + } + } + write!(f, "{}B", self.0) + } +} From 116ae535bfb881c96260f473222d254923c44a5d Mon Sep 17 00:00:00 2001 From: Pistonight Date: Fri, 9 Jan 2026 23:49:43 -0800 Subject: [PATCH 02/13] working on fixing cargo preset --- packages/copper-proc-macros/Cargo.toml | 2 +- packages/copper-proc-macros/src/cli.rs | 8 +- packages/copper/Cargo.toml | 2 +- packages/copper/examples/cargo.rs | 11 +- packages/copper/examples/print.rs | 106 ++-- packages/copper/src/lib.rs | 7 +- packages/copper/src/print/mod.rs | 2 + packages/copper/src/print/printer.rs | 452 ++++++++++-------- packages/copper/src/print/progress/bar.rs | 357 ++++++++++++-- packages/copper/src/print/progress/eta.rs | 38 +- packages/copper/src/print/progress/mod.rs | 447 +---------------- packages/copper/src/print/progress/state.rs | 335 +++++++++++-- packages/copper/src/print/prompt.rs | 99 +--- packages/copper/src/print/zero_when_drop.rs | 68 +++ .../copper/src/process/pio/cargo_preset.rs | 98 ++-- packages/copper/src/process/pio/spinner.rs | 100 ++-- 16 files changed, 1139 insertions(+), 993 deletions(-) create mode 100644 packages/copper/src/print/zero_when_drop.rs diff --git a/packages/copper-proc-macros/Cargo.toml b/packages/copper-proc-macros/Cargo.toml index d090807..72f0f77 100644 --- a/packages/copper-proc-macros/Cargo.toml +++ b/packages/copper-proc-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pistonite-cu-proc-macros" -version = "0.2.4" +version = "0.2.5" edition = "2024" description = "Proc-macros for Cu" repository = "https://github.com/Pistonite/cu" diff --git a/packages/copper-proc-macros/src/cli.rs b/packages/copper-proc-macros/src/cli.rs index fa0c93c..c5d7a0a 100644 --- a/packages/copper-proc-macros/src/cli.rs +++ b/packages/copper-proc-macros/src/cli.rs @@ -41,10 +41,14 @@ pub fn expand(attr: TokenStream, input: TokenStream) -> pm::Result } }; - item.sig.ident = generated_main_name; + let old_name = { + let mut new_name = generated_main_name; + std::mem::swap(&mut new_name, &mut item.sig.ident); + new_name + }; let expanded = pm::quote! { - fn main() -> std::process::ExitCode { + fn #old_name() -> std::process::ExitCode { unsafe { #main_impl } } #item diff --git a/packages/copper/Cargo.toml b/packages/copper/Cargo.toml index 66122ff..958b74e 100644 --- a/packages/copper/Cargo.toml +++ b/packages/copper/Cargo.toml @@ -11,7 +11,7 @@ exclude = [ ] [dependencies] -pistonite-cu-proc-macros = { version = "0.2.4", path = "../copper-proc-macros" } +pistonite-cu-proc-macros = { version = "0.2.5", path = "../copper-proc-macros" } anyhow = "1.0.100" log = "0.4.29" diff --git a/packages/copper/examples/cargo.rs b/packages/copper/examples/cargo.rs index b8ac044..1e59bf3 100644 --- a/packages/copper/examples/cargo.rs +++ b/packages/copper/examples/cargo.rs @@ -12,15 +12,14 @@ struct Cli { #[cu::cli(flags = "common")] fn main(args: Cli) -> cu::Result<()> { cu::info!("invoking cargo"); - cu::which("cargo")? + let (child, bar) = cu::which("cargo")? .command() .args(["build"]) - .name("cargo build") .current_dir(args.path) .add(cu::color_flag()) - .preset(cu::pio::cargo()) - .spawn()? - .0 - .wait_nz()?; + .preset(cu::pio::cargo("cargo build")) + .spawn()?; + child.wait_nz()?; + bar.done(); Ok(()) } diff --git a/packages/copper/examples/print.rs b/packages/copper/examples/print.rs index c0e48ff..42103fd 100644 --- a/packages/copper/examples/print.rs +++ b/packages/copper/examples/print.rs @@ -17,10 +17,6 @@ impl Args { /// Run with cargo run --example print --features prompt,cli #[cu::cli(flags = "inner", preprocess = Args::preprocess)] fn main(_: Args) -> cu::Result<()> { - - cu::progress().foo(); - - cu::print!("today's weather is {}", "good"); cu::hint!("today's weather is {}", "ok"); cu::info!( @@ -37,60 +33,76 @@ fn main(_: Args) -> cu::Result<()> { cu::info!("you chose to continue!"); { - let bar2 = cu::progress_bar(20, "This takes 5 seconds"); - let bar = cu::progress_unbounded("This is unbounded"); + let bar2 = cu::progress("This takes 5 seconds").total(20).spawn(); + let bar = bar2.child("This is unbounded").spawn(); + // make some fake hierarchy + let bar3 = bar.child("level 2").total(3).keep(true).spawn(); + let bar4 = bar3.child("level 3").total(7).spawn(); + let bar5 = bar2.child("last").total(9).keep(true).spawn(); for i in 0..10 { - cu::progress!(&bar, (), "step {i}"); - cu::progress!(&bar2, i, "step {i}"); + cu::progress!(bar, "step {i}"); + cu::progress!(bar2 = i, "step {i}"); + cu::progress!(bar3 += 1, "step {i}"); + cu::progress!(bar4 += 1, "step {i}"); + cu::progress!(bar5 += 1, "step {i}"); cu::debug!("this is debug message\n"); std::thread::sleep(Duration::from_millis(250)); + + if i == 5 { + cu::prompt!("what's your favorite fruit?")?; + } } - drop(bar); + drop(bar4); + drop(bar5); + bar.done(); for i in 0..10 { - cu::progress!(&bar2, i + 10, "step {}", i + 10); + cu::progress!(bar2 += 1, "step {}", i + 10); std::thread::sleep(Duration::from_millis(250)); cu::print!("doing stuff"); } + cu::progress!(bar2 += 1, "last step"); } - let thread1 = std::thread::spawn(|| { - cu::set_thread_print_name("t1"); - let answer = cu::prompt!("from thread 1")?; - cu::info!("you entered: {answer}"); - cu::Ok(()) - }); - let thread2 = std::thread::spawn(|| { - cu::set_thread_print_name("t2"); - let answer = cu::prompt!("from thread 2")?; - cu::info!("you entered: {answer}"); - cu::Ok(()) - }); - let thread3 = std::thread::spawn(|| { - cu::set_thread_print_name("t3"); - let answer = cu::prompt!("from thread 3")?; - cu::info!("you entered: {answer}"); - cu::Ok(()) - }); - let r1 = thread1.join().unwrap(); - let r2 = thread2.join().unwrap(); - let r3 = thread3.join().unwrap(); - r1?; - r2?; - r3?; - cu::info!("all threads joined ok"); + cu::print!("bars done"); - let command = cu::prompt!("enter command")?; - // note: in a real-world application, you would use something like - // the `shell_words` crate to split the input - let args: AnotherArgs = cu::check!( - cu::cli::try_parse(command.split_whitespace()), - "error parsing args" - )?; - cu::print!("parsed args: {args:?}"); - // note: in a real-world application, this will probably be some subcommand - if args.help { - cu::cli::print_help::(true); - } + // let thread1 = std::thread::spawn(|| { + // cu::set_thread_print_name("t1"); + // let answer = cu::prompt!("from thread 1")?; + // cu::info!("you entered: {answer}"); + // cu::Ok(()) + // }); + // let thread2 = std::thread::spawn(|| { + // cu::set_thread_print_name("t2"); + // let answer = cu::prompt!("from thread 2")?; + // cu::info!("you entered: {answer}"); + // cu::Ok(()) + // }); + // let thread3 = std::thread::spawn(|| { + // cu::set_thread_print_name("t3"); + // let answer = cu::prompt!("from thread 3")?; + // cu::info!("you entered: {answer}"); + // cu::Ok(()) + // }); + // let r1 = thread1.join().unwrap(); + // let r2 = thread2.join().unwrap(); + // let r3 = thread3.join().unwrap(); + // r1?; + // r2?; + // r3?; + // cu::info!("all threads joined ok"); + // + // let command = cu::prompt!("enter command")?; + // // note: in a real-world application, you would use something like + // // the `shell_words` crate to split the input + // let args: AnotherArgs = cu::check!( + // cu::cli::try_parse(command.split_whitespace()), + // "error parsing args" + // )?; + // cu::print!("parsed args: {args:?}"); + // // note: in a real-world application, this will probably be some subcommand + // if args.help { + // cu::cli::print_help::(true); + // } Ok(()) } diff --git a/packages/copper/src/lib.rs b/packages/copper/src/lib.rs index bda9c72..6ae2577 100644 --- a/packages/copper/src/lib.rs +++ b/packages/copper/src/lib.rs @@ -91,11 +91,8 @@ mod print; pub use print::check_password_legality; #[cfg(feature = "print")] pub use print::{ - progress, - ProgressBarBuilder, - ProgressBar, ZeroWhenDropString, init_print_options, log_init, progress_bar, progress_bar_lowp, - progress_unbounded, progress_unbounded_lowp, set_thread_print_name, term_width, - term_width_height, term_width_or_max, + ProgressBar, ProgressBarBuilder, ZeroWhenDropString, init_print_options, log_init, progress, + set_thread_print_name, term_width, term_width_height, term_width_or_max, }; /// Printing level values diff --git a/packages/copper/src/print/mod.rs b/packages/copper/src/print/mod.rs index cb6fd66..d7df967 100644 --- a/packages/copper/src/print/mod.rs +++ b/packages/copper/src/print/mod.rs @@ -9,6 +9,8 @@ mod format; pub use format::*; mod progress; pub use progress::*; +mod zero_when_drop; +pub use zero_when_drop::*; mod prompt; pub use prompt::*; diff --git a/packages/copper/src/print/printer.rs b/packages/copper/src/print/printer.rs index 1fb2f6e..9986bf1 100644 --- a/packages/copper/src/print/printer.rs +++ b/packages/copper/src/print/printer.rs @@ -1,10 +1,10 @@ use std::collections::VecDeque; +use std::ops::ControlFlow; use std::sync::{Arc, LazyLock, Mutex, Weak}; use std::thread::JoinHandle; -use std::time::Duration; - -use super::{FormatBuffer, ProgressBar, ansi}; +use crate::print::progress::{BarFormatter, BarResult, ProgressBar}; +use crate::print::{FormatBuffer, TICK_INTERVAL, ansi}; use crate::{ZeroWhenDropString, lv}; /// Print something @@ -40,6 +40,7 @@ pub(crate) static PRINTER: LazyLock> = /// Global printer state pub(crate) struct Printer { + is_stdin_terminal: bool, /// Handle to stdout stdout: std::io::Stdout, /// Handle to stderr @@ -50,14 +51,9 @@ pub(crate) struct Printer { controls: ansi::Controls, // printing - /// Handle for the printing task, None means - /// either no printing task is running, or, the printing - /// task is terminating - print_task: PrintThread, + print_task: PrintingThread, bar_target: Option, bars: Vec>, - - prompt_task: PrintThread, pending_prompts: VecDeque, /// Buffer for automatically do certain formatting @@ -78,6 +74,7 @@ impl Default for Printer { use std::io::IsTerminal as _; let stdout = std::io::stdout(); let stderr = std::io::stderr(); + let is_stdin_terminal = std::io::stdin().is_terminal(); let is_terminal = stdout.is_terminal(); let bar_target = if is_terminal { Some(Target::Stdout) @@ -90,15 +87,15 @@ impl Default for Printer { let controls = ansi::controls(is_terminal); Self { + is_stdin_terminal, stdout, stderr, colors, controls, + print_task: Default::default(), bar_target, bars: Default::default(), - - prompt_task: Default::default(), pending_prompts: Default::default(), format_buffer: FormatBuffer::new(), @@ -138,26 +135,7 @@ impl Printer { self.format_buffer.push_control("-: "); } - // show the prompt let (send, recv) = oneshot::channel(); - if !self.prompt_task.active() { - self.prompt_task.join(); - // erase current line, and print new prompt - // this may mess up progress bars - having both prompts - // and progress bar is not a good idea anyway - use std::io::Write; - let _ = write!( - self.stdout, - "{}{}{}", - self.controls.move_to_begin_and_clear, - self.buffered, - self.format_buffer.as_str() - ); - self.buffered.clear(); - let _ = self.stdout.flush(); - self.prompt_task.assign(prompt_task(send, is_password)); - return recv; - } #[cfg(feature = "prompt-password")] { self.pending_prompts.push_back(PromptTask { @@ -173,6 +151,7 @@ impl Printer { prompt: self.format_buffer.take(), }); } + self.start_print_task_if_needed(); recv } @@ -186,18 +165,13 @@ impl Printer { } // start the bar self.bars.push(Arc::downgrade(bar)); + self.start_print_task_if_needed(); + } + + fn start_print_task_if_needed(&mut self) { if !self.print_task.active() { self.print_task.join(); - // don't use bar if we can't measure terminal size - let Some((width, height)) = super::term_width_height() else { - return; - }; - let max_bars = (height / 2).saturating_sub(2); - // don't use bars if the terminal is too short - if max_bars == 0 { - return; - } - self.print_task.assign(print_task(width, max_bars as i32)); + self.print_task.assign(print_task()); } } @@ -276,19 +250,33 @@ impl Printer { self.print_format_buffer(); } - /// Format and print a progress bar done message - pub(crate) fn print_bar_done(&mut self, message: &str, is_progress_complete: bool) { + /// Print a progress bar done message + pub(crate) fn print_bar_done(&mut self, result: &BarResult, is_root: bool) { if lv::PRINT_LEVEL.get() < lv::Print::Normal { return; } - if is_progress_complete { - self.format_buffer - .reset(self.colors.gray, self.colors.green); - self.format_buffer.push_control(self.colors.green); - } else { - self.format_buffer - .reset(self.colors.gray, self.colors.yellow); - self.format_buffer.push_control(self.colors.yellow); + if !is_root && self.bar_target.is_some() { + // if bar is animated, don't print child's done messages + return; + } + let message = match result { + BarResult::DontKeep => return, + BarResult::Done(message) => { + self.format_buffer + .reset(self.colors.gray, self.colors.green); + self.format_buffer.push_control(self.colors.green); + message + } + BarResult::Interrupted(message) => { + self.format_buffer + .reset(self.colors.gray, self.colors.yellow); + self.format_buffer.push_control(self.colors.yellow); + message + } + }; + self.format_buffer.push_control("\u{283f}]"); + if !message.starts_with('[') { + self.format_buffer.push_control(" "); } self.format_buffer.push_str(message); self.format_buffer.end(); @@ -296,7 +284,7 @@ impl Printer { } fn print_format_buffer(&mut self) { - if !self.prompt_task.active() && self.bars.is_empty() { + if !self.print_task.active() { use std::io::Write; let _ = write!(self.stdout, "{}", self.format_buffer.as_str()); let _ = self.stdout.flush(); @@ -337,28 +325,31 @@ impl Printer { if self.print_task.needs_join { return self.print_task.take(); } - // if there are no bars, then eventually the task will end - let strong_count = self.bars.iter().filter(|x| x.upgrade().is_some()).count(); - if strong_count == 0 { + // if there are no bars and no prompts, then eventually the task will end + // we have to check the strong count and not the bars size, because + // we need to force the last bar to join the printing thread before + // the program exits + let bar_strong_count = self.bars.iter().filter(|x| x.upgrade().is_some()).count(); + if bar_strong_count == 0 && self.pending_prompts.is_empty() { self.print_task.take() } else { None } } - pub(crate) fn take_prompt_task_if_should_join(&mut self) -> Option> { - if self.prompt_task.needs_join { - return self.prompt_task.take(); - } - - if self.pending_prompts.is_empty() { - self.prompt_task.take() - } else { - None - } - } + // pub(crate) fn take_prompt_task_if_should_join(&mut self) -> Option> { + // if self.prompt_task.needs_join { + // return self.prompt_task.take(); + // } + // + // if self.pending_prompts.is_empty() { + // self.prompt_task.take() + // } else { + // None + // } + // } } -#[derive(PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] enum Target { /// Print to Stdout Stdout, @@ -366,27 +357,27 @@ enum Target { Stderr, } #[derive(Default)] -struct PrintThread { +struct PrintingThread { needs_join: bool, + /// Handle for the printing task, None means + /// either no printing task is running, or, the printing + /// task is terminating handle: Option>, } -impl PrintThread { +impl PrintingThread { /// Take the handle for joining fn take(&mut self) -> Option> { self.needs_join = false; self.handle.take() } - /// Mark the task as will end, so it can be joined fn mark_join(&mut self) { self.needs_join = true; } - /// If the task is active fn active(&self) -> bool { !self.needs_join && self.handle.is_some() } - /// Blockingly join the task on the current thread fn join(&mut self) { self.needs_join = false; @@ -394,7 +385,6 @@ impl PrintThread { let _: Result<_, _> = handle.join(); } } - /// Assign a new handle fn assign(&mut self, handle: JoinHandle<()>) { self.needs_join = false; @@ -402,11 +392,9 @@ impl PrintThread { } } -fn print_task(original_width: usize, max_bars: i32) -> JoinHandle<()> { - use std::fmt::Write as _; - - // 50ms between each cycle - const INTERVAL: Duration = Duration::from_millis(10); +/// Printing thread that handles progress bar animation and printing during progress bar display +fn print_task() -> JoinHandle<()> { + // progress bar animation chars #[rustfmt::skip] const CHARS: [char; 30] = [ '\u{280b}', '\u{280b}', '\u{280b}', '\u{280b}', '\u{280b}', @@ -422,13 +410,11 @@ fn print_task(original_width: usize, max_bars: i32) -> JoinHandle<()> { // if drop() is working to prevent holding the lock during sleep #[inline(always)] fn print_loop( - original_width: usize, - max_bars: i32, tick: u32, buffer: &mut String, temp: &mut String, lines: &mut i32, - ) -> std::ops::ControlFlow<()> { + ) -> ControlFlow<()> { // This won't cause race condition where // the return value of start_print_task is put // into the handle after the task is ended, @@ -440,97 +426,148 @@ fn print_task(original_width: usize, max_bars: i32) -> JoinHandle<()> { printer.print_task.mark_join(); } #[inline(always)] - fn clear(b: &mut String, lines: i32) { + fn clear(b: &mut String, lines: &mut i32) { b.clear(); - b.push_str("\r\x1b[K"); // erase the last spacing line (... and X more) - for _ in 0..lines { + b.push_str("\r\x1b[K"); // erase the last spacing line + for _ in 0..*lines { b.push_str("\x1b[1A\x1b[K"); // move up one line and erase it } + *lines = 0; + } + // first check if there are any pending prompts + // scope for locking the printer for checking prompts + { + let Ok(mut printer) = PRINTER.lock() else { + return ControlFlow::Break(()); + }; + let task = printer.pending_prompts.pop_front(); + let is_stdin_terminal = printer.is_stdin_terminal; + if let Some(mut task) = task { + use std::io::Write as _; + // print the prompt + let control = printer.controls.move_to_begin_and_clear; + let _ = write!(printer.stdout, "{}{}", control, task.prompt); + let _ = printer.stdout.flush(); + + // drop the lock while we wait for user input + drop(printer); + // if there is a prompt, don't clear the previous progress bar yet, + // since we want to display the prompt after the progress bars + + // we know the prompt string does not end with a new line (because of + // the prompt prefix), so the number of lines to display + // is exactly .lines().count() + let mut l = task.prompt.lines().count() as i32; + // however, if stdin is not terminal, then user won't press enter, + // and we actually have 1 fewer line + if !is_stdin_terminal { + l = l.saturating_sub(1) + } + *lines += l; + // process this prompt + #[cfg(feature = "prompt-password")] + let (result, is_password) = if task.is_password { + (super::prompt_password::read_password(), true) + } else { + (read_plaintext(temp), false) + }; + #[cfg(not(feature = "prompt-password"))] + let (result, is_password) = (read_plaintext(temp), false); + + // clear sensitive information in the memory + super::zero_string(temp); + // now, re-print the prompt text to the buffer without the prompt prefix + if !is_password { + while !task.prompt.ends_with('\n') { + task.prompt.pop(); + } + task.prompt.pop(); // pop the final new line + } + // add the prompt to the print buffer + if let Ok(mut printer) = PRINTER.lock() { + printer.buffered.push_str(&task.prompt); + printer.buffered.push('\n'); + } + // send the result of the prompt + let _ = task.send.send(result); + + // we only process one prompt at a time + } } - clear(buffer, *lines); - // scope for locking the printer + + // clear previous progress bars and prompts + clear(buffer, lines); + // lock the printer again for printing progress bars let Ok(mut printer) = PRINTER.lock() else { - return std::ops::ControlFlow::Break(()); + return ControlFlow::Break(()); }; - if printer.bar_target.is_none() { - on_task_end(&mut printer); - return std::ops::ControlFlow::Break(()); - } - if printer.prompt_task.active() { - // don't do anything when there's a prompt, - // since that will cause cursor to change position - return std::ops::ControlFlow::Continue(()); - } - let now = std::time::Instant::now(); + if let Some(bar_target) = printer.bar_target { + // print the bars, after processing buffered messages - // remeasure terminal width on every cycle - let width = super::term_width().unwrap_or(original_width); + // remeasure terminal width on every cycle + let width = super::term_width_or_max(); - if printer.bar_target == Some(Target::Stdout) { - // add the buffered messages - printer.take_buffered(buffer); - } else { - printer.print_buffered(); - } - // print the bars - let mut more_bars = -max_bars; - buffer.push_str(printer.colors.yellow); - *lines = 0; - let anime = CHARS[(tick as usize) % CHARS.len()]; - let mut ii = 0; - printer.bars.retain(|bar| { - let Some(bar) = bar.upgrade() else { - return false; + if bar_target == Target::Stdout { + // add the buffered messages + printer.take_buffered(buffer); + } else { + printer.print_buffered(); + } + // print the bars + buffer.push_str(printer.colors.yellow); + let anime = CHARS[(tick as usize) % CHARS.len()]; + + let mut formatter = BarFormatter { + colors: printer.colors, + bar_color: printer.colors.yellow, + width, + tick, + now: &mut None, + out: buffer, + temp, }; - if more_bars < 0 { + printer.bars.retain(|bar| { + let Some(bar) = bar.upgrade() else { + // bar is done + return false; + }; if width >= 2 { - for _ in 0..ii { - buffer.push(' '); - buffer.push(' '); - } - buffer.push(anime); - buffer.push(']'); - bar.format(width - 2 - ii * 2, now, tick, INTERVAL, buffer, temp); + formatter.out.push(anime); + formatter.out.push(']'); + *lines += bar.format(&mut formatter); + } else { + formatter.out.push('\n'); + *lines += 1; } - buffer.push('\n'); - *lines += 1; - } - ii += 1; - more_bars += 1; - true - }); - - if more_bars > 0 { - temp.clear(); - if write!(temp, " ... and {more_bars} more").is_err() { - temp.clear(); - } - if width >= temp.len() { - buffer.push_str(temp); - buffer.push_str(printer.colors.reset); - buffer.push('\r'); - } - } else { - buffer.push_str(printer.colors.reset); + true + }); } - + buffer.push_str(printer.colors.reset); printer.print_to_bar_target(buffer); + let bars_empty = printer.bars.is_empty(); + let prompts_empty = printer.pending_prompts.is_empty(); + + if bars_empty { + // erase the bars + clear(buffer, lines); + printer.print_to_bar_target(buffer); + } // check exit - if printer.bars.is_empty() { + if bars_empty && prompts_empty { + // nothing else to do, mark the task done, + // so the printer knows to join this thread (after we drop the lock guard) + // whenever someone calls, instead of posting to this thread on_task_end(&mut printer); - // erase the bars - clear(buffer, *lines); // we know the printer buffer is empty // because we just printed all of it while having - // the lock on the printer - printer.print_to_bar_target(buffer); - std::ops::ControlFlow::Break(()) - } else { - std::ops::ControlFlow::Continue(()) + // the lock on the printer, no need to print again + return ControlFlow::Break(()); } + + ControlFlow::Continue(()) } std::thread::spawn(move || { @@ -542,17 +579,10 @@ fn print_task(original_width: usize, max_bars: i32) -> JoinHandle<()> { // how many bars were printed let mut lines = 0; loop { - match print_loop( - original_width, - max_bars, - tick, - &mut buffer, - &mut temp, - &mut lines, - ) { - std::ops::ControlFlow::Break(_) => break, + match print_loop(tick, &mut buffer, &mut temp, &mut lines) { + ControlFlow::Break(_) => break, _ => { - std::thread::sleep(INTERVAL); + std::thread::sleep(TICK_INTERVAL); tick = tick.wrapping_add(1); } }; @@ -560,53 +590,57 @@ fn print_task(original_width: usize, max_bars: i32) -> JoinHandle<()> { }) } -// note that for interactive io, it's recommended to use blocking io directly -// on a thread instead of tokio -fn prompt_task( - first_send: oneshot::Sender>, - _is_password: bool, -) -> JoinHandle<()> { - use std::io::Write; - let mut stdout = std::io::stdout(); - std::thread::spawn(move || { - let mut send = first_send; - let mut _is_password = _is_password; - let mut buf = String::new(); - loop { - buf.clear(); - #[cfg(feature = "prompt-password")] - let result = if _is_password { - super::prompt_password::read_password() - } else { - std::io::stdin() - .read_line(&mut buf) - .map(|_| buf.trim().to_string().into()) - }; - #[cfg(not(feature = "prompt-password"))] - let result = std::io::stdin() - .read_line(&mut buf) - .map(|_| buf.trim().to_string().into()); - let _ = send.send(result); - let Ok(mut printer) = super::PRINTER.lock() else { - break; - }; - let Some(next) = printer.pending_prompts.pop_front() else { - printer.prompt_task.mark_join(); - break; - }; - let _ = write!( - stdout, - "{}{}{}", - printer.controls.move_to_begin_and_clear, printer.buffered, next.prompt - ); - printer.buffered.clear(); - let _ = stdout.flush(); - send = next.send; - - #[cfg(feature = "prompt-password")] - { - _is_password = next.is_password; - } - } - }) +// // note that for interactive io, it's recommended to use blocking io directly +// // on a thread instead of tokio +// fn prompt_task( +// first_send: oneshot::Sender>, +// _is_password: bool, +// ) -> JoinHandle<()> { +// use std::io::Write as _; +// let mut stdout = std::io::stdout(); +// std::thread::spawn(move || { +// let mut send = first_send; +// let mut _is_password = _is_password; +// let mut buf = String::new(); +// loop { +// +// #[cfg(feature = "prompt-password")] +// let result = if _is_password { +// super::prompt_password::read_password() +// } else { +// read_plaintext(&mut buf) +// }; +// #[cfg(not(feature = "prompt-password"))] +// let result = read_plaintext(&mut buf); +// +// let _ = send.send(result); +// let Ok(mut printer) = super::PRINTER.lock() else { +// break; +// }; +// let Some(next) = printer.pending_prompts.pop_front() else { +// printer.prompt_task.mark_join(); +// break; +// }; +// let _ = write!( +// stdout, +// "{}{}{}", +// printer.controls.move_to_begin_and_clear, printer.buffered, next.prompt +// ); +// printer.buffered.clear(); +// let _ = stdout.flush(); +// send = next.send; +// +// #[cfg(feature = "prompt-password")] +// { +// _is_password = next.is_password; +// } +// } +// }) +// } + +fn read_plaintext(buf: &mut String) -> std::io::Result { + buf.clear(); + std::io::stdin() + .read_line(buf) + .map(|_| buf.trim().to_string().into()) } diff --git a/packages/copper/src/print/progress/bar.rs b/packages/copper/src/print/progress/bar.rs index ecff100..bab2ab0 100644 --- a/packages/copper/src/print/progress/bar.rs +++ b/packages/copper/src/print/progress/bar.rs @@ -1,9 +1,96 @@ -use std::{ops::Deref, sync::{Arc, Mutex}}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; -use crate::print::progress::state::{State, StateImmut}; +use crate::print::progress::eta::Estimater; +use crate::print::progress::state::{BarFormatter, BarResult, State, StateImmut}; -/// Make a progress bar builder with the following defaults: +/// # Progress Bars +/// Progress bars are a feature in the print system. It is aware of the printing/prompting going on +/// in the console and will keep the bars at the bottom of the console without interferring +/// with the other outputs. /// +/// ## Components +/// A bar has the following display components +/// - Step display: Displays the current and total steps. For example, `[42/100]`. Will not display +/// for bars that are unbounded. Bars that are not unbounded but the total is not set +/// will show total as `?`. The step display can also be configured to a style more suitable +/// for displaying bytes (for example downloading or processing file), like `10.0K / 97.3M` +/// - Prefix: A string configured once when launching the progress bar +/// - Percentage: Percentage display for the current and total steps, For example `42.00%`. +/// This can be turned off if not needed +/// - ETA: Estimated remaining time. This can be turned off if not needed +/// - Message: A message that can be set while the progress bar is showing. For example, +/// this can be the name of the current file being processed, etc. +/// +/// With everything displayed, it will look something like this: +/// ```text +/// X][42/100] prefix: 42.00% ETA 32.35s processing the 42th item +/// ``` +/// (`X`) is where the animated spinner is +/// +/// ## Progress Tree +/// You can display progress bars with a hierarchy if desired. The progress bars +/// will be organized as an directed acyclic graph (i.e. a tree). Special characters +/// will be used to draw the tree in the terminal. +/// +/// Each progress bar holds a strong ref to its parent, and weak refs to all of its children. +/// The printer keeps weak refs to all root progress bars (i.e. one without a parent). +/// +/// ## State and Output +/// Each progress bar can have 3 states: `progress`, `done`, and `interrupted`. +/// +/// When in `progress`, the bar will be animated if the output is a terminal. Otherwise, +/// updates will be ignored. +/// +/// The bar will be `done` when all handles are dropped if 1 of the following is true: +/// - The bar has finite total, and current step equals total step +/// - The bar is unbounded, and `.done()` is called on any handle +/// +/// If neither is true when all handles are dropped, the bar becomes `interrupted`. +/// This makes the bar easier to use with control flows. When the bar is in this state, +/// it will print an interrupted message to the regular print stream, like +/// ```text +/// X][42/100] prefix: interrupted +/// ``` +/// This message is customizable when building the progress bar. All of its children +/// that are interrupted will also be printed. All children that are `done` will only be +/// printed if `keep` is true for that children (see below). The interrupted message is printed +/// regardless if the output is terminal or not. +/// +/// When the progress bar is done, it may print a "done message" depending on +/// if it has a parent and the `keep` option: +/// | Has parent (i.e. is child) | Keep | Behavior | +/// |-|-|-| +/// | Yes | Yes | Done message will be displayed under the parent, but the bar will disappear completely when the parent is done | +/// | Yes | No | The bar will disappear after it's done | +/// | No | Yes | The bar will print a done message to the regular print stream when done, no children will be printed | +/// | No | No | The bar will disappear after done, no children will be printed | +/// +/// The done message is also customizable when building the bar. Note (from the table) that it will +/// be effective in some way if the `keep` option is true. Setting a done message +/// does not automatically set `keep` to true. +/// +/// The default done message is something like below, will be displayed in green. +/// ```text +/// X][100/100] prefix: done +/// ``` +/// +/// ## Updating the bar +/// The [`progress`](macro@crate::progress) macro is used to update the progress bar. +/// For example: +/// +/// ```rust +/// # use pistonite_cu as cu; +/// let bar = cu::progress("doing something").total(10).spawn(); +/// for i in 0..10 { +/// cu::progress!(bar = i, "doing {i}th step"); +/// } +/// drop(bar); +/// ``` +/// +/// ## Building the bar +/// This function `cu::progress` will make a [`ProgressBarBuilder`] +/// with these default configs: /// - Total steps: unbounded /// - Keep after done: `true` /// - Show ETA: `true` (only effective if steps is finite) @@ -11,11 +98,99 @@ use crate::print::progress::state::{State, StateImmut}; /// - Interrupted message: Default /// /// See [`ProgressBarBuilder`] for builder methods +/// +/// ## Print Levels +/// The bar final messages are suppressed at `-q` and the bar animations are suppressed at `-qq` +/// +/// ## Other considerations +/// If the progress bar print section exceeds the terminal height, +/// it will probably not render properly. Keep in mind when you +/// are displaying a large number of progress bars. +/// +/// You can use `.max_display_children()` to set the maximum number of children +/// to display at a time. However, there is no limit on the number of root progress bars. #[inline(always)] pub fn progress(message: impl Into) -> ProgressBarBuilder { ProgressBarBuilder::new(message.into()) } +/// Update a [progress bar](fn@crate::progress) +/// +/// The macro takes 2 parts separated by comma `,`: +/// - An expression for updating the progress: +/// - Optional format args for updating the message. +/// +/// The progress update expression can be one of: +/// - `bar = i`: set the progress to `i` +/// - `bar += i`: increment the steps by i +/// - `bar`: don't update the progress +/// +/// , where `bar` is an ident +/// +/// The format args can be omitted to update the progress without +/// updating the message +/// +/// # Examples +/// ```rust,no_run +/// # use pistonite_cu as cu; +/// let bar = cu::progress_bar(10, "10 steps"); +/// // update the current count and message +/// let i = 1; +/// cu::progress!(bar = i, "doing step {i}"); +/// // update the current count without changing message +/// cu::progress!(bar += 2); +/// // update the message without changing current step +/// cu::progress!(bar, "doing the thing"); +/// ``` +#[macro_export] +macro_rules! progress { + ($bar:ident, $($fmt_args:tt)*) => { + $bar.__inc(0u64, Some(format!($($fmt_args)*))) + }; + ($bar:ident += $inc:expr) => { + $bar.__inc({ $inc } as u64, None) + }; + ($bar:ident += $inc:expr, $($fmt_args:tt)*) => { + $bar.__inc({ $inc } as u64, Some(format!($($fmt_args)*))) + }; + ($bar:ident = $x:expr) => { + $bar.__set({ $x } as u64, None) + }; + ($bar:ident = $x:expr, $($fmt_args:tt)*) => { + $bar.__set({ $x } as u64, Some(format!($($fmt_args)*))) + }; +} + +// spawn_iter stuff, keep for reference, not sure if needed yet +// .enumerate seems more readable +/* +/// In the example above, you can also attach it to an iterator directly. +/// The builder will call `size_hint()` once and set the total on the bar, +/// and will automatically mark it as done if `next()` returns `None`. +/// +/// If the default iteration behavior of `spawn_iter` is not desirable, use `spawn` +/// and iterate manually. +/// ```rust +/// # use pistonite_cu as cu; +/// for i in cu::progress("doing something").spawn_iter(0..10) { +/// cu::print!("doing {i}th step"); +/// } +/// ``` +/// +/// Note that in the code above, we didn't have a handle to the bar directly +/// to update the message, we can fix that by getting the bar from the iter +/// +/// ```rust +/// # use pistonite_cu as cu; +/// let mut iter = cu::progress("doing something").spawn_iter(0..10); +/// let bar = iter.bar(); +/// for i in iter { +/// // bar = i is handled by the iterator automatically +/// cu::progress!(bar, "doing {i}th step"); +/// } +/// ``` +*/ + /// Builder for a progress bar pub struct ProgressBarBuilder { /// The message prefix for the progress bar @@ -25,7 +200,7 @@ pub struct ProgressBarBuilder { /// The progress bar is for displaying bytes total_is_in_bytes: bool, /// If the bar should be kept after it's done - keep: bool, + keep: Option, /// If ETA should be visible (only effective if total is finite) show_eta: bool, /// If percentage should be visible (only effective if total is finite) @@ -34,21 +209,24 @@ pub struct ProgressBarBuilder { done_message: Option, /// Message to display if the bar is interrupted interrupted_message: Option, + /// Maximum number of children to display at a time + max_display_children: usize, /// Optional parent of the bar parent: Option>, } impl ProgressBarBuilder { /// Start building a progress bar. Note [`cu::progress`](progress) is the canonical shorthand pub fn new(message: String) -> Self { - Self { + Self { message, total: None, total_is_in_bytes: false, - keep: true, + keep: None, show_eta: true, show_percentage: true, done_message: None, interrupted_message: None, + max_display_children: usize::MAX / 2, parent: None, } } @@ -90,7 +268,10 @@ impl ProgressBarBuilder { } /// Set if the progress bar should be kept in the output - /// after it's done. Default is `true` + /// after it's done. + /// + /// Default is `true` for root progress bars and `false` + /// for child progress bars /// /// ```rust /// # use pistonite_cu as cu; @@ -98,7 +279,7 @@ impl ProgressBarBuilder { /// ``` #[inline(always)] pub fn keep(mut self, keep: bool) -> Self { - self.keep = keep; + self.keep = Some(keep); self } @@ -160,36 +341,92 @@ impl ProgressBarBuilder { self } + /// Set the max number of children to display at a time. + /// Default is unbounded. + /// + /// ```rust + /// # use pistonite_cu as cu; + /// cu::progress("doing something").max_display_children(30); + /// ``` + pub fn max_display_children(mut self, num: usize) -> Self { + self.max_display_children = num; + self + } + + /// Set the parent progress bar. + /// + /// If the parent is known to be `Some`, use `parent.child(...)` instead + pub fn parent(mut self, parent: Option>) -> Self { + self.parent = parent; + self + } + /// Build and start displaying the bar in the console pub fn spawn(self) -> Arc { - todo!() + let keep = self.keep.unwrap_or(self.parent.is_none()); + let done_message = if keep { + match self.done_message { + None => { + if self.message.is_empty() { + Some("done".to_string()) + } else { + Some(format!("{}: done", self.message)) + } + } + Some(x) => Some(x), + } + } else { + None + }; + let state_immut = StateImmut { + id: next_id(), + parent: self.parent.as_ref().map(Arc::clone), + prefix: self.message, + done_message, + interrupted_message: self.interrupted_message, + show_percentage: self.show_percentage, + unbounded: self.total.is_none(), + display_bytes: self.total_is_in_bytes, + max_display_children: self.max_display_children, + }; + let eta = self.show_eta.then(Estimater::new); + let state = State::new(self.total.unwrap_or(0), eta); + + let bar = Arc::new(ProgressBar { + state: state_immut, + state_mut: Mutex::new(state), + }); + match self.parent { + Some(p) => { + if let Ok(mut p) = p.state_mut.lock() { + p.add_child(&bar); + } + } + None => { + if let Ok(mut printer) = super::super::PRINTER.lock() { + printer.add_progress_bar(&bar); + } + } + } + + bar } } -trait IntoProgress { - fn into_progress(self) -> u64; +fn next_id() -> usize { + static ID: AtomicUsize = AtomicUsize::new(1); + ID.fetch_add(1, Ordering::SeqCst) } -#[rustfmt::skip] -const _: () = { - impl IntoProgress for u64 { #[inline(always)] fn into_progress(self) -> u64 { self } } - impl IntoProgress for u32 { #[inline(always)] fn into_progress(self) -> u64 { self } } - impl IntoProgress for u16 { #[inline(always)] fn into_progress(self) -> u64 { self } } - impl IntoProgress for u8 { #[inline(always)] fn into_progress(self) -> u64 { self } } - impl IntoProgress for usize { #[inline(always)] fn into_progress(self) -> u64 { self as u64 } } -}; pub struct ProgressBar { - pub(crate) props: StateImmut, - state: Mutex, + pub(crate) state: StateImmut, + state_mut: Mutex, } impl ProgressBar { #[doc(hidden)] #[inline(always)] - pub fn __set(self: &Arc, current: T, message: Option) { - Self::set(self, current.into_progress(), message) - } - fn set(self: &Arc, current: u64, message: Option) { - if let Ok(mut bar) = self.state.lock() { + pub fn __set(self: &Arc, current: u64, message: Option) { + if let Ok(mut bar) = self.state_mut.lock() { bar.set_current(current); if let Some(x) = message { bar.set_message(&x); @@ -199,11 +436,8 @@ impl ProgressBar { #[doc(hidden)] #[inline(always)] - pub fn __inc(self: &Arc, amount: T, message: Option) { - Self::inc(self, amount.into_progress(), message) - } - fn inc(self: &Arc, amount: u64, message: Option) { - if let Ok(mut bar) = self.state.lock() { + pub fn __inc(self: &Arc, amount: u64, message: Option) { + if let Ok(mut bar) = self.state_mut.lock() { bar.inc_current(amount); if let Some(x) = message { bar.set_message(&x); @@ -212,12 +446,8 @@ impl ProgressBar { } /// Set the total steps (if the progress is finite) - #[inline(always)] - pub fn set_total(&self, total: T) { - Self::set_total_impl(self, total.into_progress()) - } - fn set_total_impl(&self, total: u64) { - if let Ok(mut bar) = self.state.lock() { + pub fn set_total(&self, total: u64) { + if let Ok(mut bar) = self.state_mut.lock() { bar.set_total(total); } } @@ -228,9 +458,7 @@ impl ProgressBar { /// if the child is not spawned #[inline(always)] pub fn child(self: &Arc, message: impl Into) -> ProgressBarBuilder { - let mut builder = ProgressBarBuilder::new(message.into()); - builder.parent = Some(Arc::clone(self)); - builder + ProgressBarBuilder::new(message.into()).parent(Some(Arc::clone(self))) } /// Mark the progress bar as done and drop the handle. @@ -241,11 +469,56 @@ impl ProgressBar { /// If the progress is finite, then interrupted state is automatically /// determined (`current != total`) pub fn done(self: Arc) { - if self.props.unbounded { - if let Ok(mut bar) = self.state.lock() { + if self.state.unbounded { + if let Ok(mut bar) = self.state_mut.lock() { bar.set_current(1); bar.set_total(1); } } } + + /// Format the bar + #[inline(always)] + pub(crate) fn format(&self, fmt: &mut BarFormatter<'_, '_, '_>) -> i32 { + self.format_at_depth(0, &mut String::new(), fmt) + } + + /// Format the bar at depth + pub(crate) fn format_at_depth( + &self, + depth: usize, + hierarchy: &mut String, + fmt: &mut BarFormatter<'_, '_, '_>, + ) -> i32 { + let Ok(mut bar) = self.state_mut.lock() else { + return 0; + }; + bar.format_at_depth(depth, hierarchy, fmt, &self.state) + } +} + +impl Drop for ProgressBar { + fn drop(&mut self) { + let result = match self.state_mut.lock() { + Err(_) => BarResult::DontKeep, + Ok(bar) => bar.check_result(&self.state), + }; + if let Some(parent) = &self.state.parent { + // inform parent our result + if let Ok(mut parent_state) = parent.state_mut.lock() { + parent_state.child_done(self.state.id, result.clone()); + } + } + let handle = { + // scopr for printer lock + let Ok(mut printer) = super::super::PRINTER.lock() else { + return; + }; + printer.print_bar_done(&result, self.state.parent.is_none()); + printer.take_print_task_if_should_join() + }; + if let Some(x) = handle { + let _: Result<(), _> = x.join(); + } + } } diff --git a/packages/copper/src/print/progress/eta.rs b/packages/copper/src/print/progress/eta.rs index 9c4d4a9..ea5d118 100644 --- a/packages/copper/src/print/progress/eta.rs +++ b/packages/copper/src/print/progress/eta.rs @@ -1,12 +1,11 @@ use std::time::Instant; -use crate::print::{Tick, TICK_INTERVAL}; - +use crate::print::{TICK_INTERVAL, Tick}; /// Estimate the time for progress bar pub struct Estimater { /// Time when the progress started - pub start: Instant, + start: Instant, /// If the ETA is accurate enough to be displayed is_reasonably_accurate: bool, /// Step number when we last estimated ETA @@ -14,55 +13,55 @@ pub struct Estimater { /// Tick number when we last estimated ETA last_tick: u32, /// Last calculation, in seconds - previous_eta: f32 + previous_eta: f32, } impl Estimater { pub fn new() -> Self { - Self { - start: Instant::now(), + Self { + start: Instant::now(), is_reasonably_accurate: false, - last_step: 0, + last_step: 0, last_tick: 0, previous_eta: 0.0, } } - pub fn update(&mut self, - now: &mut Option, - current: u64, + pub fn update( + &mut self, + now: &mut Option, + current: u64, total: u64, tick: Tick, ) -> Option { let now = match now { - None => { + None => { let n = Instant::now(); *now = Some(n); - n } - Some(n) => *n + n + } + Some(n) => *n, }; let elapsed = (now - self.start).as_secs_f32(); let secs_per_step = elapsed / current as f32; let mut eta = secs_per_step * (total - current) as f32; if current == self.last_step { // subtract time passed since updating to this step - let elapased_since_current = - (TICK_INTERVAL * (tick - self.last_tick)).as_secs_f32(); + let elapased_since_current = (TICK_INTERVAL * (tick - self.last_tick)).as_secs_f32(); if elapased_since_current > eta { self.last_step = current; self.last_tick = tick; } eta = (eta - elapased_since_current).max(0.0); // only start showing ETA if it's reasonably accurate - if !self.is_reasonably_accurate - && eta < self.previous_eta - TICK_INTERVAL.as_secs_f32() + if !self.is_reasonably_accurate && eta < self.previous_eta - TICK_INTERVAL.as_secs_f32() { self.is_reasonably_accurate = true; } self.previous_eta = eta; } else { - self.last_step = current; - self.last_tick = tick; + self.last_step = current; + self.last_tick = tick; } if !self.is_reasonably_accurate { @@ -70,6 +69,5 @@ impl Estimater { } else { Some(eta) } - } } diff --git a/packages/copper/src/print/progress/mod.rs b/packages/copper/src/print/progress/mod.rs index ca0cc9e..478cfc6 100644 --- a/packages/copper/src/print/progress/mod.rs +++ b/packages/copper/src/print/progress/mod.rs @@ -1,448 +1,5 @@ -use std::sync::{Arc, Mutex}; -use std::time::{Duration, Instant}; - -use super::ansi; - mod eta; mod state; +pub(crate) use state::{BarFormatter, BarResult}; mod bar; - -/// Update a progress bar -/// -/// The macro takes 2 parts separated by comma `,`: -/// - An expression for updating the progress: -/// - Optional format args for updating the message. -/// -/// The progress update expression can be one of: -/// - `bar = i`: set the progress to `i` -/// - `bar += i`: increment the steps by i -/// - `bar`: don't update the progress -/// -/// , where `bar` is an ident -/// -/// The format args can be omitted to update the progress without -/// updating the message -/// -/// # Examples -/// ```rust,no_run -/// # use pistonite_cu as cu; -/// let bar = cu::progress_bar(10, "10 steps"); -/// // update the current count and message -/// let i = 1; -/// cu::progress!(bar = i, "doing step {i}"); -/// // update the current count without changing message -/// cu::progress!(bar += 2); -/// // update the message without changing current step -/// cu::progress!(bar, "doing the thing"); -/// ``` -#[macro_export] -macro_rules! progress { - ($bar:ident, $($fmt_args:tt)*) => { - {$bar}.__inc(0, Some(format!($($fmt_args)*))) - }; - ($bar:ident += $inc:expr) => { - {$bar}.__inc($inc, None) - }; - ($bar:ident += $inc:expr, $($fmt_args:tt)*) => { - {$bar}.__inc($inc, Some(format!($($fmt_args)*))) - }; - ($bar:ident = $x:expr) => { - {$bar}.__set($x, None) - }; - ($bar:ident = $x:expr, $($fmt_args:tt)*) => { - {$bar}.__set($x, Some(format!($($fmt_args)*))) - }; -} - - -/// Marker object for testing if a progress bar is interrupted -pub struct ProgressInterruptGuard; - -/// Create a progress bar -pub fn progress_bar(total: usize, message: impl Into) -> Arc { - let bar = Arc::new(ProgressBar::new(true, total, message.into())); - if let Ok(mut printer) = super::PRINTER.lock() { - printer.add_progress_bar(&bar); - } - bar -} - -/// Create a progress bar that doesn't print a done message -pub fn progress_bar_lowp(total: usize, message: impl Into) -> Arc { - let bar = Arc::new(ProgressBar::new(false, total, message.into())); - if let Ok(mut printer) = super::PRINTER.lock() { - printer.add_progress_bar(&bar); - } - bar -} - -/// Create a progress bar that doesn't display the current/total -/// -/// This is equipvalent to calling `progress_bar` with a total of 0 -pub fn progress_unbounded(message: impl Into) -> Arc { - progress_bar(0, message) -} - -/// Create a progress bar that doesn't display the current/total, and disappears -/// after done -/// -/// This is equipvalent to calling `progress_bar` with a total of 0 -pub fn progress_unbounded_lowp(message: impl Into) -> Arc { - progress_bar_lowp(0, message) -} - -/// Handle for a progress bar. -/// -/// The [`progress`](crate::progress) macro is used to update -/// the bar using a handle -pub struct ProgressBar { - print_done: bool, - inner: Mutex, -} -impl Drop for ProgressBar { - fn drop(&mut self) { - let (current, total, message, done_message) = { - match self.inner.lock() { - Ok(mut bar) => ( - bar.current, - bar.total, - std::mem::take(&mut bar.prefix), - std::mem::take(&mut bar.done_message), - ), - Err(_) => (0, 0, String::new(), None), - } - }; - let handle = if let Ok(mut x) = super::PRINTER.lock() { - if self.print_done { - let is_progress_complete = current >= total; - match done_message { - None => { - x.print_bar_done( - &format_bar_done(current, total, &message), - is_progress_complete, - ); - } - Some(message) => { - x.print_bar_done( - &format_bar_done_custom(current, total, &message), - is_progress_complete, - ); - } - } - } - x.take_print_task_if_should_join() - } else { - None - }; - if let Some(x) = handle { - let _: Result<(), _> = x.join(); - } - } -} -impl ProgressBar { - fn new(print_done: bool, total: usize, prefix: String) -> Self { - Self { - print_done, - inner: Mutex::new(ProgressBarState::new(total, prefix)), - } - } - /// Set the counter and message of the progress bar. - /// - /// Typically, this is done throught the [`cu::progress`](crate::progress) - /// macro instead of calling this directly - pub fn set(self: &Arc, current: usize, message: Option) { - if let Ok(mut bar) = self.inner.lock() { - bar.current = current; - if let Some(x) = message { - bar.message = x; - } - } - } - /// Set the message of the progress bar, without changing counter. - /// - /// Typically, this is done throught the [`cu::progress`](crate::progress) - /// macro instead of calling this directly - pub fn set_message(self: &Arc, message: String) { - if let Ok(mut bar) = self.inner.lock() { - bar.message = message; - } - } - /// Set the total counter. This can be used in cases where the total - /// count isn't known from the beginning. - pub fn set_total(self: &Arc, total: usize) { - if let Ok(mut bar) = self.inner.lock() { - bar.set_total(total); - } - } - - /// Override the message printed when done. - /// - /// Typically, this is done throught the [`cu::progress_done`](crate::progress_done) - /// macro instead of calling this directly - pub fn set_done_message(self: &Arc, message: String) { - if let Ok(mut bar) = self.inner.lock() { - bar.current = bar.total; - bar.done_message = Some(message); - } - } - pub(crate) fn format( - &self, - width: usize, - now: Instant, - tick: u32, - tick_interval: Duration, - out: &mut String, - temp: &mut String, - ) { - if let Ok(mut bar) = self.inner.lock() { - bar.format(width, now, tick, tick_interval, out, temp) - } - } -} - -struct ProgressBarStateImmut { - /// The prefix message (corresponds to message in the builder) - prefix: String, - /// None means don't keep the progress bar printed - /// (the default done message is formatted at spawn time) - done_message: Option, - /// None means use the default - interrupted_message: Option, -} - -/// Progress bar state -struct ProgressBarState { - /// Total count, or 0 for unbounded - total: usize, - /// Current count, has no meaning for unbounded - current: usize, - /// Current when we last estimated ETA - last_eta_current: usize, - /// Tick when we last estimated ETA - last_eta_tick: u32, - /// Last calculation - previous_eta: f64, - /// If ETA should be shown, we only show if it's reasonably accurate - should_show_eta: bool, - /// Prefix to display, usually indicating what the progress bar is for - prefix: String, - /// Message to display, usually indicating what the current action is - message: String, - /// If bounded, used for estimating the ETA - started: Instant, -} - -impl ProgressBarState { - pub(crate) fn new(total: usize, prefix: String) -> Self { - Self { - total, - current: 0, - last_eta_current: 0, - last_eta_tick: 0, - previous_eta: 0f64, - should_show_eta: false, - started: Instant::now(), - prefix, - message: String::new(), - } - } - pub(crate) fn set_total(&mut self, total: usize) { - self.total = total; - self.current = self.current.min(total); - } - pub(crate) fn is_unbounded(&self) -> bool { - self.total == 0 - } - /// Format the progress bar, adding at most `width` bytes to the buffer, - /// not including a newline - pub(crate) fn format( - &mut self, - mut width: usize, - now: Instant, - tick: u32, - tick_interval: Duration, - out: &mut String, - temp: &mut String, - ) { - use std::fmt::Write; - // format: [current/total] prefix: DD.DD% ETA SS.SSs message - match width { - 0 => return, - 1 => { - out.push('.'); - return; - } - 2 => { - out.push_str(".."); - return; - } - 3 => { - out.push_str("..."); - return; - } - 4 => { - out.push_str("[..]"); - return; - } - _ => {} - } - temp.clear(); - if !self.is_unbounded() { - if write!(temp, "{}/{}", self.current, self.total).is_err() { - temp.clear(); - } - // .len() is safe because / and numbers have the same byte size and width - // -2 is safe because width > 4 here - if temp.len() > width - 2 { - out.push('['); - for _ in 0..(width - 2) { - out.push('.'); - } - out.push(']'); - return; - } - - width -= 2; - width -= temp.len(); - out.push('['); - out.push_str(temp); - out.push(']'); - } - if width > 0 { - out.push(' '); - width -= 1; - } - for (c, w) in ansi::with_width(self.prefix.chars()) { - if w > width { - break; - } - width -= w; - out.push(c); - } - if !self.is_unbounded() && self.current > 0 { - let start = self.started; - let elapsed = (now - start).as_secs_f64(); - // show percentage/ETA if the progress takes more than 2s - if elapsed > 2f64 && self.current <= self.total { - // percentage - // : DD.DD% or : 100% - if self.current == self.total { - if self.prefix.is_empty() { - if width >= 4 { - width -= 4; - out.push_str("100%"); - } - } else { - if width >= 6 { - width -= 6; - out.push_str(": 100%"); - } - } - } else { - let percentage = self.current as f32 * 100f32 / self.total as f32; - temp.clear(); - if self.prefix.is_empty() { - if write!(temp, "{percentage:.2}%").is_err() { - temp.clear(); - } - } else { - if write!(temp, ": {percentage:.2}%").is_err() { - temp.clear(); - } - } - if width >= temp.len() { - width -= temp.len(); - out.push_str(temp); - } - } - // ETA SS.SSs - let secs_per_unit = elapsed / self.current as f64; - let mut eta = secs_per_unit * (self.total - self.current) as f64; - if self.current == self.last_eta_current { - // subtract time passed since updating to this step - let elapased_since_current = - (tick_interval * (tick - self.last_eta_tick)).as_secs_f64(); - if elapased_since_current > eta { - self.last_eta_current = self.current; - self.last_eta_tick = tick; - } - eta = (eta - elapased_since_current).max(0f64); - // only start showing ETA if it's reasonably accurate - if !self.should_show_eta - && eta < self.previous_eta - tick_interval.as_secs_f64() - { - self.should_show_eta = true; - } - self.previous_eta = eta; - } else { - self.last_eta_current = self.current; - self.last_eta_tick = tick; - } - if self.should_show_eta { - if width > 0 { - out.push(' '); - width -= 1; - } - temp.clear(); - if write!(temp, "ETA {eta:.2}s;").is_err() { - temp.clear(); - } - if width >= temp.len() { - width -= temp.len(); - out.push_str(temp); - } - } - } else { - if !self.prefix.is_empty() && !self.message.is_empty() && width > 0 { - out.push(':'); - width -= 1; - } - } - if width > 0 { - out.push(' '); - width -= 1; - } - } else { - if !self.prefix.is_empty() && !self.message.is_empty() && width > 1 { - out.push_str(": "); - width -= 2; - } - } - for (c, w) in ansi::with_width(self.message.chars()) { - if w > width { - break; - } - width -= w; - out.push(c); - } - } -} - -fn format_bar_done(current: usize, total: usize, message: &str) -> String { - if total == 0 { - if message.is_empty() { - "\u{283f}] done".to_string() - } else { - format!("\u{283f}] {message}: done") - } - } else { - let done_word = if current >= total { - "done" - } else { - "interrupted" - }; - if message.is_empty() { - format!("\u{283f}][{current}/{total}] {done_word}") - } else { - format!("\u{283f}][{current}/{total}] {message}: {done_word}") - } - } -} - -fn format_bar_done_custom(current: usize, total: usize, message: &str) -> String { - if total == 0 { - format!("\u{283f}] {message}") - } else { - format!("\u{283f}][{current}/{total}] {message}") - } -} +pub use bar::{ProgressBar, ProgressBarBuilder, progress}; diff --git a/packages/copper/src/print/progress/state.rs b/packages/copper/src/print/progress/state.rs index 1d7e5f7..f88b14f 100644 --- a/packages/copper/src/print/progress/state.rs +++ b/packages/copper/src/print/progress/state.rs @@ -1,42 +1,54 @@ -use std::sync::Arc; +use std::sync::{Arc, Weak}; use std::time::Instant; use crate::print::progress::bar::ProgressBar; use crate::print::progress::eta::Estimater; use crate::print::{Tick, ansi}; -/// Immutable part of progress bar +const CHAR_BAR_TICK: char = '\u{251C}'; +const CHAR_BAR: char = '\u{2502}'; +const CHAR_TICK: char = '\u{2514}'; + +/// Internal, immutable state of progress bar pub struct StateImmut { + /// An ID + pub id: usize, /// Parent of this bar - parent: Arc, + pub parent: Option>, /// The prefix message (corresponds to message in the builder) - prefix: String, + pub prefix: String, /// None means don't keep the progress bar printed /// (the default done message is formatted at spawn time) - done_message: Option, + pub done_message: Option, /// None means use the default - interrupted_message: Option, + pub interrupted_message: Option, /// If percentage field is shown - show_percentage: bool, + pub show_percentage: bool, /// If the steps are unbounded pub unbounded: bool, /// Display the progress using bytes format - display_bytes: bool, + pub display_bytes: bool, + /// Max number of children to display, + /// children after the limit will only display one line "... and X more" + pub max_display_children: usize, } +/// Internal mutable state pub struct State { unreal_total: u64, unreal_current: u64, message: String, - eta: Option + eta: Option, + children: Vec, } impl State { - pub fn new(total: u64, message: String, eta: Option) -> Self { + pub fn new(total: u64, eta: Option) -> Self { Self { unreal_total: total, unreal_current: 0, - message, + message: String::new(), eta, + children: vec![], } } #[inline(always)] @@ -49,9 +61,11 @@ impl State { if unbounded || self.unreal_total == 0 { return None; } - self.eta.as_mut()? - .update(now, self.unreal_current.min(self.unreal_total) - , self.unreal_total, tick + self.eta.as_mut()?.update( + now, + self.unreal_current.min(self.unreal_total), + self.unreal_total, + tick, ) } #[inline(always)] @@ -62,7 +76,10 @@ impl State { // total not known (self.unreal_current, None) } else { - (self.unreal_current.min(self.unreal_total), Some(self.unreal_total)) + ( + self.unreal_current.min(self.unreal_total), + Some(self.unreal_total), + ) } } @@ -73,29 +90,190 @@ impl State { pub fn inc_current(&mut self, current: u64) { self.unreal_current += current; } - + pub fn set_total(&mut self, total: u64) { if total != 0 { self.unreal_total = total; } } + pub fn add_child(&mut self, child: &Arc) { + self.children + .push(ChildState::Progress(child.state.id, Arc::downgrade(child))) + } + + pub fn child_done(&mut self, child_id: usize, mut result: BarResult) { + self.children.retain_mut(|child| { + let ChildState::Progress(id, _) = child else { + return true; + }; + if *id != child_id { + return true; + } + match std::mem::take(&mut result) { + BarResult::DontKeep => false, + BarResult::Done(message) => { + *child = ChildState::Done(message); + true + } + BarResult::Interrupted(message) => { + *child = ChildState::Interrupted(message); + true + } + } + }); + } + + pub fn check_result(&self, state: &StateImmut) -> BarResult { + let is_interrupted = (self.unreal_current == 0 && self.unreal_total == 0) + || (self.unreal_current < self.unreal_total); + if !is_interrupted { + match &state.done_message { + None => BarResult::DontKeep, + Some(message) => { + let message = + self.format_finish_message(message, state.unbounded, state.display_bytes); + BarResult::Done(message) + } + } + } else { + match &state.interrupted_message { + None => { + let message = if state.prefix.is_empty() { + self.format_finish_message( + "interrupted", + state.unbounded, + state.display_bytes, + ) + } else { + self.format_finish_message( + &format!("{}: interrupted", state.prefix), + state.unbounded, + state.display_bytes, + ) + }; + BarResult::Interrupted(message) + } + Some(message) => { + let message = + self.format_finish_message(message, state.unbounded, state.display_bytes); + BarResult::Interrupted(message) + } + } + } + } + pub fn set_message(&mut self, message: &str) { self.message.clear(); self.message.push_str(message); } + /// Format the bar into the out buffer at the depth + /// + /// If depth is 0, the animation character is already formatted. + /// Otherwise, a "| " should be formatted into the out buffer + /// at the beginning. The `width` passed in is terminal width minus 2. + /// + /// It should also format a new line character into the buffer + /// + /// Return number of lines formatted. + pub fn format_at_depth( + &mut self, + depth: usize, + hierarchy: &mut String, + fmt: &mut BarFormatter<'_, '_, '_>, + state: &StateImmut, + ) -> i32 { + self.format_self(fmt, fmt.width.saturating_sub((depth + 1) * 2), state); + fmt.out.push('\n'); + let mut lines = 1; + // process childrens + let mut i = 0; + let mut num_displayed = 0; + let children_count = self.children.len(); + self.children.retain_mut(|child| { + let out = &mut *fmt.out; + let Some(child) = child.upgrade() else { + i += 1; + return false; // remove the finished child + }; + if num_displayed >= state.max_display_children { + num_displayed += 1; + return true; + } + // format the multi-line syntax + out.push_str(". "); + out.push_str(fmt.colors.gray); + out.push_str(hierarchy); + if i == children_count - 1 { + out.push(CHAR_TICK); + hierarchy.push_str(" "); + } else { + out.push(CHAR_BAR_TICK); + hierarchy.push(CHAR_BAR); + hierarchy.push(' '); + } + out.push(' '); + let width = fmt.width.saturating_sub((depth + 2) * 2); + match child { + ChildStateStrong::Done(message) => { + out.push_str(fmt.colors.green); + format_message_with_width(out, width, message); + out.push('\n'); + lines += 1; + out.push_str(fmt.bar_color); + } + ChildStateStrong::Interrupted(message) => { + out.push_str(fmt.colors.yellow); + format_message_with_width(out, width, message); + out.push('\n'); + lines += 1; + out.push_str(fmt.bar_color); + } + ChildStateStrong::Progress(child) => { + out.push_str(fmt.bar_color); + lines += child.format_at_depth(depth + 1, hierarchy, fmt); + } + } + hierarchy.pop(); + hierarchy.pop(); + i += 1; + num_displayed += 1; + true + }); + if num_displayed > state.max_display_children { + // display the ... and more line + let out = &mut *fmt.out; + out.push_str("| "); + out.push_str(fmt.colors.gray); + for _ in 0..depth { + out.push(CHAR_BAR); + out.push(' '); + } + out.push(CHAR_TICK); + use std::fmt::Write as _; + let _ = write!( + out, + " ... and {} more", + state.max_display_children - num_displayed + ); + out.push_str(fmt.bar_color); + out.push('\n'); + lines += 1; + } + // return number of lines + lines + } - pub fn format( + fn format_self( &mut self, + fmt: &mut BarFormatter<'_, '_, '_>, mut width: usize, - now: &mut Option, - tick: Tick, - out: &mut String, - temp: &mut String, state: &StateImmut, ) { use std::fmt::Write as _; + let out = &mut *fmt.out; + let temp = &mut *fmt.temp; // not enough width match width { @@ -124,21 +302,22 @@ impl State { let show_prefix = !state.prefix.is_empty(); // -- : let show_percentage = state.show_percentage && total.is_some(); - let eta = self.estimate_remaining(state.unbounded, now, tick); + let eta = self.estimate_remaining(state.unbounded, fmt.now, fmt.tick); let show_eta = eta.is_some(); let show_message = !self.message.is_empty(); struct Spacing { show_separator: bool, show_space_before_eta: bool, - show_space_before_message: bool + show_space_before_message: bool, } let spacing = if state.display_bytes { Spacing { - show_separator: show_prefix && (show_current_total || show_percentage || show_eta || show_message), + show_separator: show_prefix + && (show_current_total || show_percentage || show_eta || show_message), show_space_before_eta: show_percentage || show_current_total, - show_space_before_message: show_percentage ||show_current_total ||show_eta, + show_space_before_message: show_percentage || show_current_total || show_eta, } } else { Spacing { @@ -172,24 +351,18 @@ impl State { width -= temp.len(); out.push_str(temp); out.push(']'); + } - if width > 0 { - out.push(' '); - width -= 1; - } + if width > 0 { + out.push(' '); + width -= 1; } if show_prefix { - for (c, w) in ansi::with_width(state.prefix.chars()) { - if w > width { - break; - } - width -= w; - out.push(c); - } + width = format_message_with_width(out, width, &state.prefix); } - if spacing.show_separator && width > 2{ + if spacing.show_separator && width > 2 { width -= 2; out.push_str(": "); } @@ -253,17 +426,91 @@ impl State { out.push(' '); width -= 1; } - for (c, w) in ansi::with_width(self.message.chars()) { - if w > width { - break; - } - width -= w; - out.push(c); + format_message_with_width(out, width, &self.message); + } + } + + fn format_finish_message(&self, message: &str, unbounded: bool, in_bytes: bool) -> String { + if unbounded { + return message.to_string(); + } + let (current, total) = self.real_current_total(unbounded); + match (total, in_bytes) { + (None, false) => { + format!("[{current}/?] {message}") + } + (None, true) => { + let current = ByteFormat(current); + format!("{message} ({current})") } + (Some(total), false) => { + format!("[{current}/{total}] {message}") + } + (Some(total), true) => { + let current = ByteFormat(current); + let total = ByteFormat(total); + format!("{message} ({current} / {total})") + } + } + } +} + +fn format_message_with_width(out: &mut String, mut width: usize, message: &str) -> usize { + for (c, w) in ansi::with_width(message.chars()) { + if w > width { + break; } + width -= w; + out.push(c); + } + width +} + +enum ChildState { + /// The done message (if `keep` is true) + Done(String), + /// The interrupted message + Interrupted(String), + /// Still running + Progress(usize, Weak), +} +impl ChildState { + fn upgrade(&self) -> Option> { + Some(match self { + ChildState::Done(x) => ChildStateStrong::Done(x), + ChildState::Interrupted(x) => ChildStateStrong::Interrupted(x), + ChildState::Progress(_, weak) => ChildStateStrong::Progress(weak.upgrade()?), + }) } } +enum ChildStateStrong<'a> { + Done(&'a str), + Interrupted(&'a str), + Progress(Arc), +} + +#[derive(Default, Clone)] +pub enum BarResult { + /// Bar is done and don't keep it + #[default] + DontKeep, + /// Bar is done, with a message to keep + Done(String), + /// Bar is interrupted + Interrupted(String), +} + +pub struct BarFormatter<'a, 'b, 'c> { + pub colors: ansi::Colors, + pub bar_color: &'a str, + pub width: usize, + pub tick: Tick, + pub now: &'c mut Option, + pub out: &'b mut String, + pub temp: &'b mut String, +} + struct ByteFormat(u64); impl std::fmt::Display for ByteFormat { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -275,7 +522,7 @@ impl std::fmt::Display for ByteFormat { ] { if self.0 >= unit_bytes { let whole = self.0 / unit_bytes; - let deci = (self.0 % unit_bytes) * 10/unit_bytes; + let deci = (self.0 % unit_bytes) * 10 / unit_bytes; return write!(f, "{whole}.{deci}{unit_char}"); } } diff --git a/packages/copper/src/print/prompt.rs b/packages/copper/src/print/prompt.rs index 8397e83..a730455 100644 --- a/packages/copper/src/print/prompt.rs +++ b/packages/copper/src/print/prompt.rs @@ -97,7 +97,7 @@ pub fn __prompt_yesno(message: std::fmt::Arguments<'_>) -> crate::Result { } let message = format!("{message} [y/n]"); - let _scope = PromptJoinScope; + // let _scope = PromptJoinScope; loop { let recv = { let Ok(mut printer) = super::PRINTER.lock() else { @@ -136,7 +136,7 @@ pub fn __prompt( } let message = format!("{message}"); let result = { - let _scope = PromptJoinScope; + // let _scope = PromptJoinScope; let recv = { let Ok(mut printer) = super::PRINTER.lock() else { crate::bailand!(error!("prompt failed: global print lock poisoned")); @@ -150,82 +150,19 @@ pub fn __prompt( result.with_context(|| format!("io error while showing the prompt: {message}")) } -struct PromptJoinScope; -impl Drop for PromptJoinScope { - fn drop(&mut self) { - let handle = { - let Ok(mut printer) = super::PRINTER.lock() else { - return; - }; - let Some(handle) = printer.take_prompt_task_if_should_join() else { - return; - }; - handle - }; - let _: Result<_, _> = handle.join(); - } -} - -/// A string that will have its inner buffer zeroed when dropped -#[derive(Default, Clone)] -pub struct ZeroWhenDropString(String); -impl ZeroWhenDropString { - pub const fn new() -> Self { - Self(String::new()) - } -} -impl std::fmt::Display for ZeroWhenDropString { - #[inline(always)] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} -impl From for ZeroWhenDropString { - #[inline(always)] - fn from(value: String) -> Self { - Self(value) - } -} -impl AsRef<[u8]> for ZeroWhenDropString { - #[inline(always)] - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} -impl AsRef for ZeroWhenDropString { - #[inline(always)] - fn as_ref(&self) -> &String { - &self.0 - } -} -impl AsRef for ZeroWhenDropString { - #[inline(always)] - fn as_ref(&self) -> &str { - &self.0 - } -} -impl Drop for ZeroWhenDropString { - #[inline(always)] - fn drop(&mut self) { - // SAFETY: we don't use the string again - for c in unsafe { self.0.as_bytes_mut() } { - // SAFETY: c is a valid u8 pointer - unsafe { std::ptr::write_volatile(c, 0) }; - } - std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst); - std::sync::atomic::compiler_fence(std::sync::atomic::Ordering::SeqCst); - } -} -impl std::ops::Deref for ZeroWhenDropString { - type Target = String; - #[inline(always)] - fn deref(&self) -> &String { - &self.0 - } -} -impl std::ops::DerefMut for ZeroWhenDropString { - #[inline(always)] - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} +// struct PromptJoinScope; +// impl Drop for PromptJoinScope { +// fn drop(&mut self) { +// let handle = { +// let Ok(mut printer) = super::PRINTER.lock() else { +// return; +// }; +// let Some(handle) = printer.take_print_task_if_should_join() else { +// return; +// }; +// handle +// }; +// let _: Result<_, _> = handle.join(); +// } +// } +// diff --git a/packages/copper/src/print/zero_when_drop.rs b/packages/copper/src/print/zero_when_drop.rs new file mode 100644 index 0000000..25c2a04 --- /dev/null +++ b/packages/copper/src/print/zero_when_drop.rs @@ -0,0 +1,68 @@ +/// A string that will have its inner buffer zeroed when dropped +#[derive(Default, Clone)] +pub struct ZeroWhenDropString(String); +impl ZeroWhenDropString { + pub const fn new() -> Self { + Self(String::new()) + } +} +impl std::fmt::Display for ZeroWhenDropString { + #[inline(always)] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} +impl From for ZeroWhenDropString { + #[inline(always)] + fn from(value: String) -> Self { + Self(value) + } +} +impl AsRef<[u8]> for ZeroWhenDropString { + #[inline(always)] + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} +impl AsRef for ZeroWhenDropString { + #[inline(always)] + fn as_ref(&self) -> &String { + &self.0 + } +} +impl AsRef for ZeroWhenDropString { + #[inline(always)] + fn as_ref(&self) -> &str { + &self.0 + } +} +impl Drop for ZeroWhenDropString { + #[inline(always)] + fn drop(&mut self) { + zero_string(&mut self.0) + } +} +impl std::ops::Deref for ZeroWhenDropString { + type Target = String; + #[inline(always)] + fn deref(&self) -> &String { + &self.0 + } +} +impl std::ops::DerefMut for ZeroWhenDropString { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub(crate) fn zero_string(s: &mut String) { + let mut s = std::mem::take(s); + // SAFETY: we don't use the string again + for c in unsafe { s.as_bytes_mut() } { + // SAFETY: c is a valid u8 pointer + unsafe { std::ptr::write_volatile(c, 0) }; + } + std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst); + std::sync::atomic::compiler_fence(std::sync::atomic::Ordering::SeqCst); +} diff --git a/packages/copper/src/process/pio/cargo_preset.rs b/packages/copper/src/process/pio/cargo_preset.rs index f4ab326..54a6dbd 100644 --- a/packages/copper/src/process/pio/cargo_preset.rs +++ b/packages/copper/src/process/pio/cargo_preset.rs @@ -8,7 +8,7 @@ use tokio::process::{Child as TokioChild, ChildStderr, ChildStdout, Command as T use crate::lv::Lv; use crate::process::{Command, Preset, pio}; -use crate::{BoxedFuture, ProgressBar}; +use crate::{BoxedFuture, ProgressBar, ProgressBarBuilder}; /// Display progress of cargo task with a progress bar, and emitting /// status messages and diagnostic messages using this crate's printing utilities. @@ -38,6 +38,8 @@ use crate::{BoxedFuture, ProgressBar}; /// crates being built in one line (similar to the build progress bar shown /// by cargo). /// +/// You can customize the spawned progress bar with +/// /// # Message levels /// Errors, warnings and status messages (like `Compiling foobar v0.1.0`) /// can be configured with the [`error`](Cargo::error), [`warning`](Cargo::warning), @@ -77,15 +79,18 @@ use crate::{BoxedFuture, ProgressBar}; /// ``` /// /// # Output -/// The handle to the progress bar is emitted to both the stdout and stderr slot. -/// Be sure to manually drop the handle to mark the progress as done if needed. +/// The handle to the progress bar is emitted to the stdout slot. +/// Be sure to manually call `.done()` on it. See [Progress Bars](fn@crate::progress) +/// for more details /// -pub fn cargo() -> Cargo { +#[inline(always)] +pub fn cargo(progress_message: impl Into) -> Cargo { Cargo { error_lv: Lv::Error, warning_lv: Lv::Warn, other_lv: Lv::Debug, diagnostic_hook: None, + progress_builder: crate::progress(progress_message), } } pub struct Cargo { @@ -93,6 +98,7 @@ pub struct Cargo { warning_lv: Lv, other_lv: Lv, diagnostic_hook: Option, + progress_builder: ProgressBarBuilder, } impl Cargo { @@ -120,24 +126,30 @@ impl Cargo { self.diagnostic_hook = Some(Box::new(f)); self } + + /// Configure the progress bar that will be spawned + #[inline(always)] + pub fn configure_spinner ProgressBarBuilder>( + mut self, + f: F, + ) -> Self { + self.progress_builder = f(self.progress_builder); + self + } } impl Preset for Cargo { - type Output = Command; + type Output = Command; fn configure(self, command: crate::Command) -> Self::Output { command .args(["--message-format=json-diagnostic-rendered-ansi"]) - .stderr(Cargo { - error_lv: self.error_lv, - warning_lv: self.warning_lv, - other_lv: self.other_lv, - diagnostic_hook: None, - }) + .stderr(CargoStubStdErr) .stdout(self) .stdin_null() } } + pub struct CargoTask { error_lv: Lv, warning_lv: Lv, @@ -149,28 +161,17 @@ pub struct CargoTask { } impl pio::ChildOutConfig for Cargo { - type Task = Option; + type Task = CargoTask; type __Null = super::__OCNonNull; fn configure_stdout(&mut self, command: &mut TokioCommand) { command.stdout(Stdio::piped()); } - fn configure_stderr(&mut self, command: &mut TokioCommand) { - command.stderr(Stdio::piped()); - } - fn take( - self, - child: &mut TokioChild, - name: Option<&str>, - is_out: bool, - ) -> crate::Result { - // we need to take both out and err - if !is_out { - return Ok(None); - } + fn configure_stderr(&mut self, _: &mut TokioCommand) {} + fn take(self, child: &mut TokioChild, _: Option<&str>, _: bool) -> crate::Result { let stdout = super::take_child_stdout(child)?; let stderr = super::take_child_stderr(child)?; - let bar = crate::progress_unbounded(name.unwrap_or("cargo")); - Ok(Some(CargoTask { + let bar = self.progress_builder.spawn(); + Ok(CargoTask { error_lv: self.error_lv, warning_lv: self.warning_lv, other_lv: self.other_lv, @@ -178,20 +179,28 @@ impl pio::ChildOutConfig for Cargo { out: stdout, err: stderr, diagnostic_hook: self.diagnostic_hook, - })) + }) } } -impl pio::ChildOutTask for Option { - type Output = Option>; +pub struct CargoStubStdErr; +impl pio::ChildOutConfig for CargoStubStdErr { + type Task = (); + type __Null = super::__OCNull; + fn configure_stdout(&mut self, _: &mut TokioCommand) {} + fn configure_stderr(&mut self, command: &mut TokioCommand) { + command.stderr(Stdio::piped()); + } + fn take(self, _: &mut TokioChild, _: Option<&str>, _: bool) -> crate::Result { + Ok(()) + } +} + +impl pio::ChildOutTask for CargoTask { + type Output = Arc; fn run(self) -> (Option>, Self::Output) { - match self { - None => (None, None), - Some(task) => { - let bar = Arc::clone(&task.bar); - (Some(Box::pin(task.main())), Some(bar)) - } - } + let bar = Arc::clone(&self.bar); + (Some(Box::pin(self.main())), bar) } } @@ -202,13 +211,15 @@ impl CargoTask { let read_err = tokio::io::BufReader::new(self.err); let mut err_lines = Some(read_err.lines()); - crate::progress!(&self.bar, (), "preparing"); + let bar = self.bar; + + crate::progress!(bar, "preparing"); let mut state = PrintState::new( self.error_lv, self.warning_lv, self.other_lv, - self.bar, + bar, self.diagnostic_hook, ); @@ -343,7 +354,8 @@ impl PrintState { } "build-finished" => match payload.success { Some(true) => { - crate::trace!("cargo build successful"); + todo!() // show done message, mark bar done + // crate::trace!("cargo build successful"); } _ => { crate::trace!("cargo build failed"); @@ -405,6 +417,8 @@ impl PrintState { fn update_bar(&mut self) { let count = self.done_count; + let bar = &self.bar; + self.buf.clear(); let mut iter = self.in_progress.iter(); if let Some(x) = iter.next() { @@ -413,9 +427,9 @@ impl PrintState { self.buf.push_str(", "); self.buf.push_str(c); } - crate::progress!(&self.bar, (), "{count} done, compiling: {}", self.buf); + crate::progress!(bar, "{count} done, compiling: {}", self.buf); } else if count != 0 { - crate::progress!(&self.bar, (), "{count} done"); + crate::progress!(bar, "{count} done"); } } } diff --git a/packages/copper/src/process/pio/spinner.rs b/packages/copper/src/process/pio/spinner.rs index d6404a8..435feac 100644 --- a/packages/copper/src/process/pio/spinner.rs +++ b/packages/copper/src/process/pio/spinner.rs @@ -4,7 +4,8 @@ use std::sync::Arc; use spin::mutex::SpinMutex; use tokio::process::{Child as TokioChild, ChildStderr, ChildStdout, Command as TokioCommand}; -use crate::{Atomic, BoxedFuture, ProgressBar, lv::Lv}; +use crate::lv::Lv; +use crate::{Atomic, BoxedFuture}; use super::{ChildOutConfig, ChildOutTask, Driver, DriverOutput}; @@ -66,7 +67,7 @@ pub fn spinner(name: impl Into) -> Spinner { prefix: name.into(), config: Arc::new(SpinnerInner { lv: Atomic::new_u8(Lv::Off as u8), - bar: SpinMutex::new(None), + // bar: SpinMutex::new(None), }), } } @@ -101,12 +102,12 @@ struct SpinnerInner { // the bar spawned when calling take() for the first time, // using a spin lock because it should be VERY rare that // we get contention - bar: SpinMutex>>, + // bar: SpinMutex>>, } pub struct SpinnerTask { lv: Lv, prefix: String, - bar: Arc, + // bar: Arc, out: Option, err: Option, } @@ -136,58 +137,61 @@ impl ChildOutConfig for Spinner { } else { String::new() }; - let bar = { - let mut bar_arc = self.config.bar.lock(); - if let Some(bar) = bar_arc.as_ref() { - Arc::clone(bar) - } else { - let bar = crate::progress_unbounded(self.prefix); - *bar_arc = Some(Arc::clone(&bar)); - bar - } - }; - Ok(SpinnerTask { - lv, - prefix: log_prefix, - bar, - out: if is_out { child.stdout.take() } else { None }, - err: if !is_out { child.stderr.take() } else { None }, - }) + todo!() + // let bar = { + // let mut bar_arc = self.config.bar.lock(); + // if let Some(bar) = bar_arc.as_ref() { + // Arc::clone(bar) + // } else { + // let bar = crate::progress_unbounded(self.prefix); + // *bar_arc = Some(Arc::clone(&bar)); + // bar + // } + // }; + // Ok(SpinnerTask { + // lv, + // prefix: log_prefix, + // bar, + // out: if is_out { child.stdout.take() } else { None }, + // err: if !is_out { child.stderr.take() } else { None }, + // }) } } impl ChildOutTask for SpinnerTask { - type Output = Arc; + type Output = Arc<() /*ProgressBar*/>; fn run(self) -> (Option>, Self::Output) { - let bar = Arc::clone(&self.bar); - (Some(Box::pin(self.main())), bar) + todo!() + // let bar = Arc::clone(&self.bar); + // (Some(Box::pin(self.main())), bar) } } impl SpinnerTask { async fn main(self) { - let bar = self.bar; - let lv = self.lv; - let prefix = self.prefix; - // if we are printing, then let the driver only return the last - // line if more than one line is found - let mut driver = Driver::new(self.out, self.err, lv == Lv::Off); - loop { - match driver.next().await { - DriverOutput::Line(line) => { - if lv != Lv::Off { - crate::__priv::__print_with_level(lv, format_args!("{prefix}{line}")); - // erase the progress line if we decide to print it out - crate::progress!(&bar, (), "") - } else { - crate::progress!(&bar, (), "{line}") - } - } - DriverOutput::Progress(line) => { - crate::progress!(&bar, (), "{line}") - } - DriverOutput::Done => break, - _ => {} - } - } + todo!() + // let bar = self.bar; + // let lv = self.lv; + // let prefix = self.prefix; + // // if we are printing, then let the driver only return the last + // // line if more than one line is found + // let mut driver = Driver::new(self.out, self.err, lv == Lv::Off); + // loop { + // match driver.next().await { + // DriverOutput::Line(line) => { + // if lv != Lv::Off { + // crate::__priv::__print_with_level(lv, format_args!("{prefix}{line}")); + // // erase the progress line if we decide to print it out + // // crate::progress!(&bar, (), "") + // } else { + // // crate::progress!(&bar, (), "{line}") + // } + // } + // DriverOutput::Progress(line) => { + // // crate::progress!(&bar, (), "{line}") + // } + // DriverOutput::Done => break, + // _ => {} + // } + // } } } From 31f0ad63e775d76ec71ee6deaac947b9daf4a461 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Sat, 10 Jan 2026 19:02:16 -0800 Subject: [PATCH 03/13] working on ui tests and revamping documentation --- Cargo.toml | 1 + packages/copper/Cargo.toml | 7 + packages/copper/examples/print.rs | 9 - packages/copper/src/cli.rs | 11 +- packages/copper/src/error_handling.rs | 179 +++++++++ packages/copper/src/lib.rs | 68 +++- packages/copper/src/lv.rs | 17 +- packages/copper/src/misc.rs | 178 --------- packages/copper/src/print/format.rs | 9 +- packages/copper/src/print/init.rs | 4 +- packages/copper/src/print/mod.rs | 4 +- packages/copper/src/print/printer.rs | 17 +- packages/copper/src/print/progress/bar.rs | 13 +- packages/copper/src/print/progress/eta.rs | 1 + packages/copper/src/print/progress/state.rs | 6 +- packages/copper/src/print/prompt.rs | 191 ++++------ packages/copper/src/print/prompt_password.rs | 3 +- .../copper/src/process/pio/cargo_preset.rs | 4 +- packages/copper/src/process/pio/spinner.rs | 106 +++--- packages/terminal-tests/.gitignore | 1 + packages/terminal-tests/Cargo.toml | 26 ++ packages/terminal-tests/Taskfile.yml | 11 + .../terminal-tests/examples/print_levels.rs | 24 ++ packages/terminal-tests/examples/prompt.rs | 23 ++ .../examples/prompt_password.rs | 8 + packages/terminal-tests/input/prompt-n.txt | 2 + packages/terminal-tests/input/prompt-rust.txt | 2 + packages/terminal-tests/input/prompt-xn.txt | 2 + .../terminal-tests/input/prompt-y-json.txt | 3 + .../terminal-tests/input/prompt-y-rust.txt | 3 + .../terminal-tests/output/print_levels-0.txt | 8 + .../terminal-tests/output/print_levels-1.txt | 12 + .../terminal-tests/output/print_levels-2.txt | 18 + .../terminal-tests/output/print_levels-3.txt | 21 ++ .../terminal-tests/output/print_levels-4.txt | 24 ++ .../terminal-tests/output/print_levels-5.txt | 8 + .../terminal-tests/output/print_levels-6.txt | 12 + .../terminal-tests/output/print_levels-7.txt | 18 + .../terminal-tests/output/print_levels-8.txt | 21 ++ .../terminal-tests/output/print_levels-9.txt | 24 ++ packages/terminal-tests/output/prompt-0.txt | 16 + packages/terminal-tests/output/prompt-1.txt | 15 + packages/terminal-tests/output/prompt-2.txt | 17 + packages/terminal-tests/output/prompt-3.txt | 22 ++ packages/terminal-tests/output/prompt-4.txt | 24 ++ packages/terminal-tests/output/prompt-5.txt | 16 + packages/terminal-tests/output/prompt-6.txt | 22 ++ packages/terminal-tests/src/main.rs | 350 ++++++++++++++++++ 48 files changed, 1179 insertions(+), 402 deletions(-) create mode 100644 packages/copper/src/error_handling.rs create mode 100644 packages/terminal-tests/.gitignore create mode 100644 packages/terminal-tests/Cargo.toml create mode 100644 packages/terminal-tests/Taskfile.yml create mode 100644 packages/terminal-tests/examples/print_levels.rs create mode 100644 packages/terminal-tests/examples/prompt.rs create mode 100644 packages/terminal-tests/examples/prompt_password.rs create mode 100644 packages/terminal-tests/input/prompt-n.txt create mode 100644 packages/terminal-tests/input/prompt-rust.txt create mode 100644 packages/terminal-tests/input/prompt-xn.txt create mode 100644 packages/terminal-tests/input/prompt-y-json.txt create mode 100644 packages/terminal-tests/input/prompt-y-rust.txt create mode 100644 packages/terminal-tests/output/print_levels-0.txt create mode 100644 packages/terminal-tests/output/print_levels-1.txt create mode 100644 packages/terminal-tests/output/print_levels-2.txt create mode 100644 packages/terminal-tests/output/print_levels-3.txt create mode 100644 packages/terminal-tests/output/print_levels-4.txt create mode 100644 packages/terminal-tests/output/print_levels-5.txt create mode 100644 packages/terminal-tests/output/print_levels-6.txt create mode 100644 packages/terminal-tests/output/print_levels-7.txt create mode 100644 packages/terminal-tests/output/print_levels-8.txt create mode 100644 packages/terminal-tests/output/print_levels-9.txt create mode 100644 packages/terminal-tests/output/prompt-0.txt create mode 100644 packages/terminal-tests/output/prompt-1.txt create mode 100644 packages/terminal-tests/output/prompt-2.txt create mode 100644 packages/terminal-tests/output/prompt-3.txt create mode 100644 packages/terminal-tests/output/prompt-4.txt create mode 100644 packages/terminal-tests/output/prompt-5.txt create mode 100644 packages/terminal-tests/output/prompt-6.txt create mode 100644 packages/terminal-tests/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 5501f80..72d6dd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,5 @@ members = [ "packages/copper", "packages/copper-proc-macros", "packages/promethium", + "packages/terminal-tests", ] diff --git a/packages/copper/Cargo.toml b/packages/copper/Cargo.toml index 958b74e..c90f372 100644 --- a/packages/copper/Cargo.toml +++ b/packages/copper/Cargo.toml @@ -54,6 +54,10 @@ windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_Syste version = "1.49.0" features = [ "macros", "rt-multi-thread" ] optional = true + + +[dev-dependencies] + [dev-dependencies.tokio] version = "1.49.0" features = [ "macros", "rt-multi-thread", "time" ] @@ -114,6 +118,9 @@ release-nolog= ["log/release_max_level_off"] release-nodebuglog = ["log/release_max_level_info"] derive = ["dep:derive_more"] +# Internally used to enable test features +__test = [] + [[example]] name = "print" required-features = ["prompt", "cli"] diff --git a/packages/copper/examples/print.rs b/packages/copper/examples/print.rs index 42103fd..a806afd 100644 --- a/packages/copper/examples/print.rs +++ b/packages/copper/examples/print.rs @@ -17,15 +17,6 @@ impl Args { /// Run with cargo run --example print --features prompt,cli #[cu::cli(flags = "inner", preprocess = Args::preprocess)] fn main(_: Args) -> cu::Result<()> { - cu::print!("today's weather is {}", "good"); - cu::hint!("today's weather is {}", "ok"); - cu::info!( - "this is an info messagenmultilineaa 你好 sldkfjals🤖kdjflkasjdflkjasldkfjaklsdjflkjasldkfjlaksjdflkajsdklfjlaksjdfkljasldkfjlasldkjflaskdjflaksjdlfkajsldkfjkasjdlfkjaskldjflajsdlkfjlaskjdfklajsdf" - ); - cu::warn!("this is a warn message\n"); - cu::error!("this is error message\n\n"); - cu::debug!("this is debug message\n2\n\n"); - cu::trace!("this is trace message\n\n2\n"); if !cu::yesno!("continue?")? { cu::warn!("you chose to not continue!"); return Ok(()); diff --git a/packages/copper/src/cli.rs b/packages/copper/src/cli.rs index f922150..861b600 100644 --- a/packages/copper/src/cli.rs +++ b/packages/copper/src/cli.rs @@ -261,21 +261,20 @@ impl Flags { match self.non_interactive.min(i8::MAX as u8) as i8 - self.interactive.min(i8::MAX as u8) as i8 { - ..0 => { + ..=0 => { if self.yes { - Some(lv::Prompt::Yes) + Some(lv::Prompt::YesOrInteractive) } else { Some(lv::Prompt::Interactive) } } - 0 => { + _ => { if self.yes { - Some(lv::Prompt::Yes) + Some(lv::Prompt::YesOrBlock) } else { - None + Some(lv::Prompt::Block) } } - _ => Some(lv::Prompt::No), } #[cfg(not(feature = "prompt"))] { diff --git a/packages/copper/src/error_handling.rs b/packages/copper/src/error_handling.rs new file mode 100644 index 0000000..3eaf40d --- /dev/null +++ b/packages/copper/src/error_handling.rs @@ -0,0 +1,179 @@ +pub use anyhow::{Context, Error, Ok, Result, bail, anyhow as fmterr}; + + +/// # Error Handling +/// *Does not require any feature flag. Please make sure to sponsor [David Tolnay](https://github.com/dtolnay) if you depend heavily on his work +/// on the Rust ecosystem.* +/// +/// Most of the error handling stuff is re-exported from [`anyhow`](https://docs.rs/anyhow), +/// which is a crate that makes tracing and formatting error messages SUPER easy. +/// This is the easiest way to quickly write debuggable program without structured error. +/// Structured error types would be more useful if you are making a library though. +/// +/// The traits required for error handling are included in the prelude import +/// ```rust +/// # use pistonite_cu as cu +/// use cu::pre::*; +/// ``` +/// +/// Here are the most commonly used `anyhow` re-exports +/// - `anyhow::Result` is `cu::Result` +/// - `anyhow::bail!` is `cu::bail!` +/// - `anyhow::Ok` is `cu::Ok` +/// +/// Here are custom utilities from `cu` that integrates with `anyhow` +/// - `cu::check!` wraps `.with_context()` +/// ```rust +/// # use pistonite_cu as cu; +/// use cu::pre::*; +/// +/// fn some_fallable_func() -> cu::Result { +/// Ok("foo".to_string()) +/// } +/// fn main() -> cu::Result<()> { +/// // this input is just to show the formatting +/// let input: i32 = 42; +/// +/// let foo = cu::check!(some_fallable_func(), "failed: {input}")?; +/// // with anyhow, this would be: +/// // let foo = some_fallable_func().with_context(|| format!("failed: {input}"))?; +/// // -- much longer! +/// assert_eq!(foo, "foo"); +/// Ok(()) +/// } +/// ``` +/// - [`cu::rethrow!`](macro@crate::rethrow) is similar to `bail!`, but works with an `Error` instance at hand +/// - [`cu::unimplemented!`](macro@crate::unimplemented) +/// and [`cu::unreachable!`](macro@crate::unreachable) +/// that are similar to the std macros, but instead of `panic!`, they will `bail!` +/// - [`cu::ensure`](macro@crate::ensure) is unlike `anyhow::ensure`, that +/// it evaluates to a `Result<()>` instead of generates a return. +/// It also does not automatically generate debug information. +/// +/// Here are other `anyhow` re-exports that are less commonly used +/// - `anyhow::anyhow` is `cu::fmterr` +/// +/// Finally, if you do need to panic, [`cu::panicand`](macro@crate::panicand) +/// allows you to also log the same message so you can debug it easier. +/// +#[macro_export] +macro_rules! check { + ($result:expr, $($args:tt)*) => {{ + { $result }.with_context(|| format!($($args)*)) + }}; +} + +/// Rethrow an `Err`, optionally with additional context +/// +/// This is useful if the error path requires additional handling +/// +/// Prelude import is required to bring in the Context trait. +/// +/// ```rust +/// # use pistonite_cu as cu; +/// use cu::pre::*; +/// +/// fn some_fallable_func() -> cu::Result { +/// Ok("foo".to_string()) +/// } +/// +/// fn main() -> cu::Result<()> { +/// // this input is just to show the formatting +/// let input: i32 = 42; +/// +/// let foo = match some_fallable_func() { +/// Ok(x) => x, +/// Err(e) => { +/// // supposed some additional handling is needed, +/// // like setting some error state... +/// +/// cu::rethrow!(e, "failed: {input}"); +/// } +/// }; +/// +/// assert_eq!(foo, "foo"); +/// +/// Ok(()) +/// } +/// ``` +#[macro_export] +macro_rules! rethrow { + ($result:expr) => { + return Err($result); + }; + ($result:expr, $($args:tt)*) => {{ + return Err($result).context(format!($($args)*)); + }}; +} + +/// Like `unimplemented!` in std library, but log a message +/// and return an error instead of panicking +#[macro_export] +macro_rules! unimplemented { + () => { + $crate::trace!("unexpected: not implemented reached"); + return $crate::Error::msg("not implemented"); + }; + ($($args:tt)*) => {{ + let msg = format!("{}", format_args!($(args)*)); + $crate::trace!("unexpected: not implemented reached: {msg}"); + $crate::bail!("not implemented: {msg}") + }} +} + +/// Like `unreachable!` in std library, but log a message +/// and return an error instead of panicking reached. +/// This might be less performant in release builds +#[macro_export] +macro_rules! unreachable { + () => { + $crate::trace!("unexpected: entered unreachable code"); + return $crate::Error::msg("unreachable"); + }; + ($($args:tt)*) => {{ + let msg = format!("{}", format_args!($(args)*)); + $crate::trace!("unexpected: entered unreachable code: {msg}"); + $crate::bail!("unreachable: {msg}") + }} +} + +/// Check if an expression is `true` +/// +/// Unlike `anyhow::ensure`, if the condition fail, this will generate an `Error` +/// instead of returning an error directly, so you need to add a `?`. +/// It also always include the expression stringified in the debug info. +/// However, it does not automatically parse the input and generate debug +/// info message based on that (unlike `anyhow`) +#[macro_export] +macro_rules! ensure { + ($result:expr) => {{ + if !bool::from($result) { + Err($crate::fmterr!("condition failed: `{}`", stringify!($result))) + } else { + Ok(()) + } + }}; + ($result:expr, $($args:tt)*) => {{ + if !bool::from($result) { + Err($crate::fmterr!("condition failed: `{}`: {}", stringify!($result), format_args!($($args)*))) + } else { + Ok(()) + } + }}; +} + +/// Invoke a print macro, then panic with the same message +/// +/// # Example +/// ```rust,no_run +/// # use pistonite_cu as cu; +/// cu::panicand!(error!("found {} errors", 3)); +/// ``` +#[macro_export] +macro_rules! panicand { + ($mac:ident !( $($fmt_args:tt)* )) => {{ + let s = format!($($fmt_args)*); + $crate::$mac!("{s}"); + panic!("{s}"); + }} +} diff --git a/packages/copper/src/lib.rs b/packages/copper/src/lib.rs index 6ae2577..abbcf23 100644 --- a/packages/copper/src/lib.rs +++ b/packages/copper/src/lib.rs @@ -1,12 +1,11 @@ +//! # Cu = Copper //! Batteries-included common utils //! //! (If you are viewing this on docs.rs, please use the [self-hosted //! version](https://cu.pistonite.dev) instead) //! -//! # Install -//! Since crates.io does not have namespaces, this crate has a prefix. -//! You should manually rename it to `cu`, as that's what the proc-macros -//! expect. +//! # Quick start +//! When installing, rename the crate to `cu` in `Cargo.toml`: //! ```toml //! # Cargo.toml //! # ... @@ -15,24 +14,51 @@ //! version = "..." # check by running `cargo info pistonite-cu` //! features = [ "full" ] # see docs //! -//! # ... //! [dependencies] +//! # ... //! ``` //! -//! # General Principal -//! `cu` tries to be as short as possible with imports. Common and misc -//! utilities are exported directly by the crate and should be used -//! as `cu::xxx` directly. Sub-functionalities are bundled when makes -//! sense, and should be called from submodules directly, like `cu::fs::xxx` -//! or `cu::co::xxx`. The submodules are usually 2-4 characters. -//! -//! The only time to use `use` to import from `cu`, is with the prelude module -//! `pre`: +//! The goal with using `cu` is use only one `use` statement: //! ```rust //! # use pistonite_cu as cu; //! use cu::pre::*; //! ``` -//! This imports traits like [`Context`] and [`PathExtension`] into scope. +//! This brings into scope a few things: +//! - Traits that are expected to be used, such as `anyhow::Context` +//! - Re-exports of modules, such as `json` if the `json` feature is enabled +//! +//! If a function or type is not included with `pre::*`, that means the canonical +//! style for using it is with the full path, for example, you would write: +//! +//! ```rust,no_run +//! # use pistonite_cu as cu; +//! use cu::pre::*; +//! +//! fn read_file() -> cu::Result<()> { +//! cu::fs::read_string("foo/bar.txt")?; +//! cu::info!("successfully read file"); +//! Ok(()) +//! } +//! ``` +//! +//! instead of the below: +//! ```rust,no_run +//! # use pistonite_cu as cu; +//! use cu::pre::*; +//! use cu::{Result, fs, info}; +//! // ^ don't include extra uses! +//! // the biggest disadvantage of this is +//! // it's easy to confuse with types in the standard library +//! +//! fn read_file() -> Result<()> { +//! fs::read_string("foo/bar.txt")?; +//! info!("successfully read file"); +//! Ok(()) +//! } +//! ``` +//! +//! # Quick Reference +//! - [Error Handling](macro@crate::check) (via [`anyhow`](https://docs.rs/anyhow)) //! //! # Feature Reference: //! - `cli`, `print`, `prompt`: @@ -47,8 +73,13 @@ #![cfg_attr(any(docsrs, feature = "nightly"), feature(doc_cfg))] +// for macros extern crate self as cu; +// --- Error Handling (does not require any feature flag) --- +mod error_handling; +pub use error_handling::*; + #[cfg(feature = "process")] mod process; #[cfg(feature = "process")] @@ -87,8 +118,8 @@ pub mod co; /// Low level printing utils and integration with log and clap #[cfg(feature = "print")] mod print; -#[cfg(feature = "prompt-password")] -pub use print::check_password_legality; +// #[cfg(feature = "prompt-password")] +// pub use print::check_password_legality; #[cfg(feature = "print")] pub use print::{ ProgressBar, ProgressBarBuilder, ZeroWhenDropString, init_print_options, log_init, progress, @@ -119,11 +150,10 @@ mod misc; pub use misc::*; // re-exports from libraries -pub use anyhow::{Context, Error, Ok, Result, anyhow as fmterr, bail, ensure}; pub use log::{debug, error, info, trace, warn}; pub use pistonite_cu_proc_macros::error_ctx; #[cfg(feature = "coroutine")] -pub use tokio::{join, try_join}; +pub use tokio::{join, try_join, select}; #[doc(hidden)] pub mod __priv { diff --git a/packages/copper/src/lv.rs b/packages/copper/src/lv.rs index 01f223c..3e176e1 100644 --- a/packages/copper/src/lv.rs +++ b/packages/copper/src/lv.rs @@ -163,18 +163,21 @@ impl From for log::LevelFilter { #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum Prompt { - /// Show prompts interactively + /// Show all prompts interactively Interactive, - /// Automatically answer "Yes" to all yes/no prompts, and `Auto` for regular prompts - Yes, - /// Do not allow prompts (non-interactive). Attempting to show prompt will error - No, + /// Automatically answer "Yes" to all yes/no prompts, and show other prompts interactively + YesOrInteractive, + /// Automatically answer "Yes" to all yes/no prompts, and do not allow other prompts + YesOrBlock, + /// Do not allow any type of prompts (non-interactive). Attempting to show prompt will error + Block, } impl From for Prompt { fn from(value: u8) -> Self { match value { - 1 => Self::Yes, - 2 => Self::No, + 1 => Self::YesOrInteractive, + 2 => Self::YesOrBlock, + 3 => Self::Block, _ => Self::Interactive, } } diff --git a/packages/copper/src/misc.rs b/packages/copper/src/misc.rs index 2a12698..5451fa6 100644 --- a/packages/copper/src/misc.rs +++ b/packages/copper/src/misc.rs @@ -17,19 +17,6 @@ pub fn best_effort_panic_info<'a>(payload: &'a Box) -> } } -/// Like `unimplemented!` in std library, but log a message -/// and return an error instead of panicking -#[macro_export] -macro_rules! noimpl { - () => { - $crate::bailand!(error!("not implemented")) - }; - ($($args:tt)*) => {{ - let msg = format!("not implemented: {}", format_args!($(args)*)); - $crate::bailand!(error!("{msg}")) - }} -} - /// Copy a reader to a writer. /// /// This is wrapper for `std::io::copy` with error context @@ -56,168 +43,3 @@ where .await .context("async stream copy failed") } - -/// Check a `Result`, unwrapping the value or giving it a context -/// and return the error. -/// -/// Prelude import is required to bring in the Context trait. -/// -/// ```rust -/// # use pistonite_cu as cu; -/// use cu::pre::*; -/// -/// fn some_fallable_func() -> cu::Result { -/// Ok("foo".to_string()) -/// } -/// -/// fn main() -> cu::Result<()> { -/// // this input is just to show the formatting -/// let input: i32 = 42; -/// -/// let foo = cu::check!(some_fallable_func(), "failed: {input}")?; -/// assert_eq!(foo, "foo"); -/// // also log the error as we return the Err -/// let foo = cu::check!(some_fallable_func(), error!("failed: {input}"))?; -/// assert_eq!(foo, "foo"); -/// -/// Ok(()) -/// } -/// ``` -#[macro_export] -macro_rules! check { - ($result:expr, $mac:ident !( $($args:tt)* )) => {{ - { $result }.with_context(|| $crate::fmtand!($mac!($($args)*))) - }}; - ($result:expr, $($args:tt)*) => {{ - { $result }.with_context(|| format!($($args)*)) - }}; -} - -/// Rethrow an `Err`, optionally with additional context -/// -/// This is useful if the error path requires additional handling -/// -/// Prelude import is required to bring in the Context trait. -/// -/// ```rust -/// # use pistonite_cu as cu; -/// use cu::pre::*; -/// -/// fn some_fallable_func() -> cu::Result { -/// Ok("foo".to_string()) -/// } -/// -/// fn main() -> cu::Result<()> { -/// // this input is just to show the formatting -/// let input: i32 = 42; -/// -/// let foo = match some_fallable_func() { -/// Ok(x) => x, -/// Err(e) => { -/// // supposed some additional handling is needed, -/// // like setting some error state... -/// -/// cu::rethrow!(e, "failed: {input}"); -/// } -/// }; -/// -/// assert_eq!(foo, "foo"); -/// -/// Ok(()) -/// } -/// ``` -#[macro_export] -macro_rules! rethrow { - ($result:expr) => { - return Err($result); - }; - ($result:expr, $mac:ident !( $($args:tt)* )) => {{ - return Err($result).context($crate::fmtand!($mac!($($args)*))); - }}; - ($result:expr, $($args:tt)*) => {{ - return Err($result).context(format!($($args)*)); - }}; -} - -/// Format and invoke a print macro -/// -/// # Example -/// ```rust -/// # use pistonite_cu as cu; -/// let x = cu::fmtand!(error!("found {} errors", 3)); -/// assert_eq!(x, "found 3 errors"); -/// ``` -#[macro_export] -macro_rules! fmtand { - ($mac:ident !( $($fmt_args:tt)* )) => {{ - let s = format!($($fmt_args)*); - $crate::$mac!("{s}"); - s - }} -} -/// Invoke a print macro, then bail with the same message -/// -/// # Example -/// ```rust -/// # use pistonite_cu as cu; -/// # fn main() { -/// fn fn_1() -> cu::Result<()> { -/// cu::bailand!(error!("found {} errors", 3)); -/// } -/// fn fn_2() -> cu::Result<()> { -/// cu::bailand!(warn!("warning!")); -/// } -/// assert!(fn_1().is_err()); // will also log error "found 3 errors" -/// assert!(fn_2().is_err()); // will also log warning "warning!" -/// # } -/// ``` -#[macro_export] -macro_rules! bailand { - ($mac:ident !( $($fmt_args:tt)* )) => {{ - let s = format!($($fmt_args)*); - $crate::$mac!("{s}"); - $crate::bail!(s); - }} -} - -/// Return an error, expecting the error will eventually -/// be propagated as a fatal error to the user, as an FYI. -/// -/// This means the error is an expected error, due to invalid -/// input, for example. -/// -/// This will hide the "use -vv for trace" hint message. -/// -/// ```rust -/// # use pistonite_cu as cu; -/// fn foo() -> cu::Result<()> { -/// cu::bailfyi!("input is invalid"); -/// } -/// -/// assert!(foo().is_err()); -/// // exiting will not print the "show trace hint" -/// ``` -/// -#[macro_export] -macro_rules! bailfyi { - ($($arg:tt)*) => {{ - $crate::lv::disable_trace_hint(); - $crate::bail!($($arg)*); - }} -} - -/// Invoke a print macro, then panic with the same message -/// -/// # Example -/// ```rust,no_run -/// # use pistonite_cu as cu; -/// cu::panicand!(error!("found {} errors", 3)); -/// ``` -#[macro_export] -macro_rules! panicand { - ($mac:ident !( $($fmt_args:tt)* )) => {{ - let s = format!($($fmt_args)*); - $crate::$mac!("{s}"); - panic!("{s}"); - }} -} diff --git a/packages/copper/src/print/format.rs b/packages/copper/src/print/format.rs index 9dbf480..68d672e 100644 --- a/packages/copper/src/print/format.rs +++ b/packages/copper/src/print/format.rs @@ -12,8 +12,13 @@ pub fn term_width() -> Option { /// Get the terminal height, capped as some internal amount pub fn term_width_height() -> Option<(usize, usize)> { - use terminal_size::*; - terminal_size().map(|(Width(w), Height(h))| ((w as usize).min(400), (h as usize).min(400))) + if cfg!(feature = "__test") { + // fix the size in test + Some((60, 20)) + } else { + use terminal_size::*; + terminal_size().map(|(Width(w), Height(h))| ((w as usize).min(400), (h as usize).min(400))) + } } pub(crate) struct FormatBuffer { diff --git a/packages/copper/src/print/init.rs b/packages/copper/src/print/init.rs index 00b7a99..5f8ed90 100644 --- a/packages/copper/src/print/init.rs +++ b/packages/copper/src/print/init.rs @@ -22,7 +22,7 @@ pub fn log_init(lv: &str) { "vv" => lv::Print::VerboseVerbose, _ => lv::Print::Normal, }; - init_print_options(lv::Color::Auto, level, Some(lv::Prompt::No)); + init_print_options(lv::Color::Auto, level, Some(lv::Prompt::Block)); } /// Set global print options. This is usually called from clap args @@ -59,7 +59,7 @@ pub fn init_print_options(color: lv::Color, level: lv::Print, prompt: Option usize { static ID: AtomicUsize = AtomicUsize::new(1); ID.fetch_add(1, Ordering::SeqCst) } - +#[derive(Debug)] pub struct ProgressBar { pub(crate) state: StateImmut, state_mut: Mutex, @@ -477,6 +478,16 @@ impl ProgressBar { } } + /// Same as [`done`](Self::done), but does not drop the bar. + pub fn done_by_ref(&self) { + if self.state.unbounded { + if let Ok(mut bar) = self.state_mut.lock() { + bar.set_current(1); + bar.set_total(1); + } + } + } + /// Format the bar #[inline(always)] pub(crate) fn format(&self, fmt: &mut BarFormatter<'_, '_, '_>) -> i32 { diff --git a/packages/copper/src/print/progress/eta.rs b/packages/copper/src/print/progress/eta.rs index ea5d118..64f9883 100644 --- a/packages/copper/src/print/progress/eta.rs +++ b/packages/copper/src/print/progress/eta.rs @@ -3,6 +3,7 @@ use std::time::Instant; use crate::print::{TICK_INTERVAL, Tick}; /// Estimate the time for progress bar +#[derive(Debug)] pub struct Estimater { /// Time when the progress started start: Instant, diff --git a/packages/copper/src/print/progress/state.rs b/packages/copper/src/print/progress/state.rs index f88b14f..1e468b9 100644 --- a/packages/copper/src/print/progress/state.rs +++ b/packages/copper/src/print/progress/state.rs @@ -10,6 +10,7 @@ const CHAR_BAR: char = '\u{2502}'; const CHAR_TICK: char = '\u{2514}'; /// Internal, immutable state of progress bar +#[derive(Debug)] pub struct StateImmut { /// An ID pub id: usize, @@ -34,6 +35,7 @@ pub struct StateImmut { } /// Internal mutable state +#[derive(Debug)] pub struct State { unreal_total: u64, unreal_current: u64, @@ -255,7 +257,7 @@ impl State { let _ = write!( out, " ... and {} more", - state.max_display_children - num_displayed + num_displayed - state.max_display_children ); out.push_str(fmt.bar_color); out.push('\n'); @@ -465,7 +467,7 @@ fn format_message_with_width(out: &mut String, mut width: usize, message: &str) } width } - +#[derive(Debug)] enum ChildState { /// The done message (if `keep` is true) Done(String), diff --git a/packages/copper/src/print/prompt.rs b/packages/copper/src/print/prompt.rs index a730455..1c1892a 100644 --- a/packages/copper/src/print/prompt.rs +++ b/packages/copper/src/print/prompt.rs @@ -1,22 +1,6 @@ use crate::{Atomic, Context as _}; use crate::lv; -/// Show a Yes/No prompt -/// -/// Return `true` if the answer is Yes. Return an error if prompt is not allowed -/// ```rust,ignore -/// if cu::yesno!("do you want to continue?")? { -/// cu::info!("user picked yes"); -/// } -/// ``` -#[cfg(feature = "prompt")] -#[macro_export] -macro_rules! yesno { - ($($fmt_args:tt)*) => {{ - $crate::__priv::__prompt_yesno(format_args!($($fmt_args)*)) - }} -} - /// Show a prompt /// /// Use the `prompt-password` feature and [`prompt_password!`](crate::prompt_password) macro @@ -34,135 +18,122 @@ macro_rules! prompt { }} } -/// Show a password prompt -/// -/// The console will have inputs hidden while user types, and the returned -/// value is a [`ZeroWhenDropString`](crate::ZeroWhenDropString) +/// Show a Yes/No prompt /// +/// Return `true` if the answer is Yes. Return an error if prompt is not allowed /// ```rust,ignore -/// let password = cu::prompt_password!("please enter your password")?; -/// cu::info!("user entered: {password}"); +/// if cu::yesno!("do you want to continue?")? { +/// cu::info!("user picked yes"); +/// } /// ``` -#[cfg(feature = "prompt-password")] +#[cfg(feature = "prompt")] #[macro_export] -macro_rules! prompt_password { +macro_rules! yesno { ($($fmt_args:tt)*) => {{ - $crate::__priv::__prompt(format_args!($($fmt_args)*), true) + $crate::__priv::__prompt_yesno(format_args!($($fmt_args)*)) }} } -/// Show a password prompt and loops until a legal password is accepted. -/// -/// Use this when prompting the user to set a password. + +/// Show a password prompt /// /// The console will have inputs hidden while user types, and the returned /// value is a [`ZeroWhenDropString`](crate::ZeroWhenDropString) /// -/// Legal password must be non-empty, and contains only alphanumeric characters, or selected ascii -/// special characters. -/// /// ```rust,ignore -/// let password = cu::prompt_legal_password!("please enter your password")?; +/// let password = cu::prompt_password!("please enter your password")?; /// cu::info!("user entered: {password}"); /// ``` #[cfg(feature = "prompt-password")] #[macro_export] -macro_rules! prompt_legal_password { +macro_rules! prompt_password { ($($fmt_args:tt)*) => {{ - loop { - let p = $crate::__priv::__prompt(format_args!($($fmt_args)*), true)?; - match $crate::check_password_legality(&*p) { - Ok(()) => break $crate::Ok(p), - Err(e) => { - $crate::error!("{e}"); - ::std::mem::drop(p); - } - } - } + $crate::__priv::__prompt(format_args!($($fmt_args)*), true) }} } pub(crate) static PROMPT_LEVEL: Atomic = Atomic::new_u8(lv::Prompt::Interactive as u8); -pub fn __prompt_yesno(message: std::fmt::Arguments<'_>) -> crate::Result { - match PROMPT_LEVEL.get() { - lv::Prompt::Interactive => {} - lv::Prompt::Yes => return Ok(true), - lv::Prompt::No => { - crate::bailand!(error!( - "prompt not allowed in non-interactive mode: {message}" - )); - } - } - - let message = format!("{message} [y/n]"); - // let _scope = PromptJoinScope; - loop { - let recv = { - let Ok(mut printer) = super::PRINTER.lock() else { - crate::bailand!(error!("prompt failed: global print lock poisoned")); - }; - printer.show_prompt(&message, false) - }; - let result = recv - .recv() - .with_context(|| format!("recv error while showing the prompt: {message}"))?; - match result { - Err(e) => { - Err(e).context(format!("io error while showing the prompt: {message}"))?; +pub fn __prompt_yesno( + message: std::fmt::Arguments<'_>, +) -> crate::Result { + match check_prompt_level(true) { + Ok(false) => {}, + other => return other, + }; + let mut answer = false; + prompt_with_validation_impl(format_args!("{message} [y/n]"), false, |x| { + x.make_ascii_lowercase(); + match x.trim() { + "y" | "yes" => { + answer = true; + Ok(true) + } + "n" | "no" => { + answer = false; + Ok(true) } - Ok(mut x) => { - x.make_ascii_lowercase(); - match x.trim() { - "y" | "yes" => return Ok(true), - "n" | "no" => return Ok(false), - _ => {} - } + _ => { + crate::hint!("please enter yes or no"); + Ok(false) } } - crate::error!("please enter yes or no"); - } + })?; + Ok(answer) } pub fn __prompt( message: std::fmt::Arguments<'_>, is_password: bool, ) -> crate::Result { - if let lv::Prompt::No = PROMPT_LEVEL.get() { - crate::bailand!(error!( - "prompt not allowed in non-interactive mode: {message}" - )); - } + check_prompt_level(false)?; + prompt_impl(&format!("{message}"), is_password) +} + +fn prompt_with_validation_impl crate::Result>( + message: std::fmt::Arguments<'_>, + is_password: bool, + mut validator: F +) -> crate::Result { let message = format!("{message}"); - let result = { - // let _scope = PromptJoinScope; - let recv = { - let Ok(mut printer) = super::PRINTER.lock() else { - crate::bailand!(error!("prompt failed: global print lock poisoned")); - }; - printer.show_prompt(&message, is_password) + loop { + let mut result = prompt_impl(&message, is_password)?; + if validator(&mut result)? { + return Ok(result); + } + } +} + +fn prompt_impl( + message: &str, + is_password: bool, +) -> crate::Result { + let recv = { + let Ok(mut printer) = super::PRINTER.lock() else { + crate::bail!("prompt failed: global print lock poisoned"); }; - recv.recv() - .with_context(|| format!("recv error while showing the prompt: {message}"))? + printer.show_prompt(message, is_password) }; - - result.with_context(|| format!("io error while showing the prompt: {message}")) + let result = crate::check!(recv.recv(), "error while showing prompt")?; + crate::check!(result, "io error while showing prompt") } -// struct PromptJoinScope; -// impl Drop for PromptJoinScope { -// fn drop(&mut self) { -// let handle = { -// let Ok(mut printer) = super::PRINTER.lock() else { -// return; -// }; -// let Some(handle) = printer.take_print_task_if_should_join() else { -// return; -// }; -// handle -// }; -// let _: Result<_, _> = handle.join(); -// } -// } -// +// Ok(true) -> answer Yes +// Ok(false) -> prompt +// Err -> bail +fn check_prompt_level(is_yesno: bool) -> crate::Result { + if is_yesno { + match PROMPT_LEVEL.get() { + // do not even show the prompt if --yes + lv::Prompt::YesOrInteractive | lv::Prompt::YesOrBlock => return Ok(true), + lv::Prompt::Interactive => return Ok(false), + lv::Prompt::Block => { } + } + } else { + if !matches!(PROMPT_LEVEL.get(), lv::Prompt::YesOrBlock | lv::Prompt::Block) { + return Ok(false); + } + } + crate::bail!("prompt not allowed with --non-interactive"); +} diff --git a/packages/copper/src/print/prompt_password.rs b/packages/copper/src/print/prompt_password.rs index 277fe39..5411ac5 100644 --- a/packages/copper/src/print/prompt_password.rs +++ b/packages/copper/src/print/prompt_password.rs @@ -189,7 +189,8 @@ macro_rules! special_chars { } } special_chars! { '!' | '#' | '$' | '%' | '&' | '(' | ')' | '*' | '+' | ',' | '-' | '.' | '/' | ':' | ';' | '<' | '=' | '>' | '?' | '@' | '[' | ']' | '^' | '_' | '`' | '{' | '|' | '}' | '~'} -pub fn check_password_legality(s: &str) -> crate::Result<()> { +/// Check if the password contains all "legal" characters (and is non-empty) +pub fn password_chars_legal(s: &str) -> crate::Result<()> { if s.is_empty() { crate::bail!("password cannot be empty"); } diff --git a/packages/copper/src/process/pio/cargo_preset.rs b/packages/copper/src/process/pio/cargo_preset.rs index 54a6dbd..164ddb8 100644 --- a/packages/copper/src/process/pio/cargo_preset.rs +++ b/packages/copper/src/process/pio/cargo_preset.rs @@ -354,8 +354,8 @@ impl PrintState { } "build-finished" => match payload.success { Some(true) => { - todo!() // show done message, mark bar done - // crate::trace!("cargo build successful"); + self.bar.done_by_ref(); + crate::trace!("cargo build successful"); } _ => { crate::trace!("cargo build failed"); diff --git a/packages/copper/src/process/pio/spinner.rs b/packages/copper/src/process/pio/spinner.rs index 435feac..3f1a7db 100644 --- a/packages/copper/src/process/pio/spinner.rs +++ b/packages/copper/src/process/pio/spinner.rs @@ -5,7 +5,7 @@ use spin::mutex::SpinMutex; use tokio::process::{Child as TokioChild, ChildStderr, ChildStdout, Command as TokioCommand}; use crate::lv::Lv; -use crate::{Atomic, BoxedFuture}; +use crate::{Atomic, ProgressBar, ProgressBarBuilder, BoxedFuture}; use super::{ChildOutConfig, ChildOutTask, Driver, DriverOutput}; @@ -64,10 +64,9 @@ use super::{ChildOutConfig, ChildOutTask, Driver, DriverOutput}; #[inline(always)] pub fn spinner(name: impl Into) -> Spinner { Spinner { - prefix: name.into(), config: Arc::new(SpinnerInner { lv: Atomic::new_u8(Lv::Off as u8), - // bar: SpinMutex::new(None), + bar: SpinMutex::new(Err(crate::progress(name))), }), } } @@ -75,9 +74,6 @@ pub fn spinner(name: impl Into) -> Spinner { #[derive(Clone)] #[doc(hidden)] pub struct Spinner { - /// prefix of the bar - prefix: String, - config: Arc, } #[rustfmt::skip] @@ -102,12 +98,12 @@ struct SpinnerInner { // the bar spawned when calling take() for the first time, // using a spin lock because it should be VERY rare that // we get contention - // bar: SpinMutex>>, + bar: SpinMutex, ProgressBarBuilder>>, } pub struct SpinnerTask { lv: Lv, prefix: String, - // bar: Arc, + bar: Arc, out: Option, err: Option, } @@ -137,61 +133,61 @@ impl ChildOutConfig for Spinner { } else { String::new() }; - todo!() - // let bar = { - // let mut bar_arc = self.config.bar.lock(); - // if let Some(bar) = bar_arc.as_ref() { - // Arc::clone(bar) - // } else { - // let bar = crate::progress_unbounded(self.prefix); - // *bar_arc = Some(Arc::clone(&bar)); - // bar - // } - // }; - // Ok(SpinnerTask { - // lv, - // prefix: log_prefix, - // bar, - // out: if is_out { child.stdout.take() } else { None }, - // err: if !is_out { child.stderr.take() } else { None }, - // }) + let bar = { + let mut bar_arc = self.config.bar.lock(); + match bar_arc.as_mut() { + // if already created, then just use the bar (i.e. if created + // by the same spinner configured for multiple outputs + Ok(bar) => Arc::clone(bar), + Err(e) => { + let bar = e.clone().spawn(); + *bar_arc = Ok(Arc::clone(&bar)); + bar + } + } + }; + Ok(SpinnerTask { + lv, + prefix: log_prefix, + bar, + out: if is_out { child.stdout.take() } else { None }, + err: if !is_out { child.stderr.take() } else { None }, + }) } } impl ChildOutTask for SpinnerTask { - type Output = Arc<() /*ProgressBar*/>; + type Output = Arc; fn run(self) -> (Option>, Self::Output) { - todo!() - // let bar = Arc::clone(&self.bar); - // (Some(Box::pin(self.main())), bar) + let bar = Arc::clone(&self.bar); + (Some(Box::pin(self.main())), bar) } } impl SpinnerTask { async fn main(self) { - todo!() - // let bar = self.bar; - // let lv = self.lv; - // let prefix = self.prefix; - // // if we are printing, then let the driver only return the last - // // line if more than one line is found - // let mut driver = Driver::new(self.out, self.err, lv == Lv::Off); - // loop { - // match driver.next().await { - // DriverOutput::Line(line) => { - // if lv != Lv::Off { - // crate::__priv::__print_with_level(lv, format_args!("{prefix}{line}")); - // // erase the progress line if we decide to print it out - // // crate::progress!(&bar, (), "") - // } else { - // // crate::progress!(&bar, (), "{line}") - // } - // } - // DriverOutput::Progress(line) => { - // // crate::progress!(&bar, (), "{line}") - // } - // DriverOutput::Done => break, - // _ => {} - // } - // } + let bar = self.bar; + let lv = self.lv; + let prefix = self.prefix; + // if we are printing, then let the driver only return the last + // line if more than one line is found + let mut driver = Driver::new(self.out, self.err, lv == Lv::Off); + loop { + match driver.next().await { + DriverOutput::Line(line) => { + if lv != Lv::Off { + crate::__priv::__print_with_level(lv, format_args!("{prefix}{line}")); + // erase the progress line if we decide to print it out + crate::progress!(bar, "") + } else { + crate::progress!(bar, "{line}") + } + } + DriverOutput::Progress(line) => { + crate::progress!(bar, "{line}") + } + DriverOutput::Done => break, + _ => {} + } + } } } diff --git a/packages/terminal-tests/.gitignore b/packages/terminal-tests/.gitignore new file mode 100644 index 0000000..e712304 --- /dev/null +++ b/packages/terminal-tests/.gitignore @@ -0,0 +1 @@ +wip diff --git a/packages/terminal-tests/Cargo.toml b/packages/terminal-tests/Cargo.toml new file mode 100644 index 0000000..bb37272 --- /dev/null +++ b/packages/terminal-tests/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "terminal-tests" +version = "0.0.0" +edition = "2024" +publish = false + +[dependencies] +shell-words = "1.1.1" + +[dependencies.cu] +package = "pistonite-cu" +path = "../copper" + +[[bin]] +name = "terminal-tests" +path = "src/main.rs" +required-features = ["bin"] + +[features] +default = ["bin"] +bin = ["cu/cli", "cu/process", "cu/coroutine-heavy"] +common = ["cu/__test", "cu/cli"] + +__test-print_levels = [] +__test-prompt = ["cu/prompt"] +__test-prompt_password = ["cu/prompt-password"] diff --git a/packages/terminal-tests/Taskfile.yml b/packages/terminal-tests/Taskfile.yml new file mode 100644 index 0000000..37f2b28 --- /dev/null +++ b/packages/terminal-tests/Taskfile.yml @@ -0,0 +1,11 @@ +version: '3' + +includes: + cargo: + taskfile: ../mono-dev/task/cargo.yaml + internal: true + optional: true + +tasks: + run: + - cargo run --features bin -- {{.CLI_ARGS}} diff --git a/packages/terminal-tests/examples/print_levels.rs b/packages/terminal-tests/examples/print_levels.rs new file mode 100644 index 0000000..e0da13a --- /dev/null +++ b/packages/terminal-tests/examples/print_levels.rs @@ -0,0 +1,24 @@ +// $ -qq +// $ -q +// $ +// $ -v +// $ -vv +// $ --color=always -qq +// $ --color=always -q +// $ --color=always +// $ --color=always -v +// $ --color=always -vv + +#[cu::cli] +fn main(_: cu::cli::Flags) -> cu::Result<()> { + cu::info!( + "this is an info messagenmultilineaa 你好 sldkfjals🤖kdjflkasjdflkjasldkfjaklsdjflkjasldkfjlaksjdflkajsdklfjlaksjdfkljasldkfjlasldkjflaskdjflaksjdlfkajsldkfjkasjdlfkjaskldjflajsdlkfjlaskjdfklajsdf" + ); + cu::warn!("this is a warn message\n"); + cu::error!("this is error message\n\n"); + cu::debug!("this is debug message\n2\n\n"); + cu::trace!("this is trace message\n\n2\n"); + cu::print!("today's weather is {}", "good"); + cu::hint!("today's weather is {}", "ok"); + Ok(()) +} diff --git a/packages/terminal-tests/examples/prompt.rs b/packages/terminal-tests/examples/prompt.rs new file mode 100644 index 0000000..933ea78 --- /dev/null +++ b/packages/terminal-tests/examples/prompt.rs @@ -0,0 +1,23 @@ +// $ -y --non-interactive +// $ --non-interactive +// $ -y < prompt-rust.txt +// $ < prompt-y-rust.txt +// $ < prompt-y-json.txt +// $ < prompt-n.txt +// $ < prompt-xn.txt + +#[cu::cli] +fn main(_: cu::cli::Flags) -> cu::Result<()> { + cu::hint!("testing prompts"); + if !cu::yesno!("continue?")? { + cu::warn!("you chose to not continue!"); + return Ok(()); + } + let answer = cu::prompt!("what's your favorite programming language?")?; + cu::info!("you answered: {answer}"); + if answer != "rust" { + cu::bail!("the answer is incorrect"); + } + cu::info!("the answer is correct"); + Ok(()) +} diff --git a/packages/terminal-tests/examples/prompt_password.rs b/packages/terminal-tests/examples/prompt_password.rs new file mode 100644 index 0000000..24150d0 --- /dev/null +++ b/packages/terminal-tests/examples/prompt_password.rs @@ -0,0 +1,8 @@ +// $ < prompt-xn.txt +#[cu::cli] +fn main(_: cu::cli::Flags) -> cu::Result<()> { + cu::hint!("testing prompt password"); + let answer = cu::prompt_password!("enter password")?; + cu::info!("you answered: {answer}"); + Ok(()) +} diff --git a/packages/terminal-tests/input/prompt-n.txt b/packages/terminal-tests/input/prompt-n.txt new file mode 100644 index 0000000..0cb74c0 --- /dev/null +++ b/packages/terminal-tests/input/prompt-n.txt @@ -0,0 +1,2 @@ +n + diff --git a/packages/terminal-tests/input/prompt-rust.txt b/packages/terminal-tests/input/prompt-rust.txt new file mode 100644 index 0000000..9583d3b --- /dev/null +++ b/packages/terminal-tests/input/prompt-rust.txt @@ -0,0 +1,2 @@ +rust + diff --git a/packages/terminal-tests/input/prompt-xn.txt b/packages/terminal-tests/input/prompt-xn.txt new file mode 100644 index 0000000..6ec8811 --- /dev/null +++ b/packages/terminal-tests/input/prompt-xn.txt @@ -0,0 +1,2 @@ +alsdkfjalksdjf +n diff --git a/packages/terminal-tests/input/prompt-y-json.txt b/packages/terminal-tests/input/prompt-y-json.txt new file mode 100644 index 0000000..46b197a --- /dev/null +++ b/packages/terminal-tests/input/prompt-y-json.txt @@ -0,0 +1,3 @@ +y +json + diff --git a/packages/terminal-tests/input/prompt-y-rust.txt b/packages/terminal-tests/input/prompt-y-rust.txt new file mode 100644 index 0000000..9af058b --- /dev/null +++ b/packages/terminal-tests/input/prompt-y-rust.txt @@ -0,0 +1,3 @@ +y +rust + diff --git a/packages/terminal-tests/output/print_levels-0.txt b/packages/terminal-tests/output/print_levels-0.txt new file mode 100644 index 0000000..c6c88bf --- /dev/null +++ b/packages/terminal-tests/output/print_levels-0.txt @@ -0,0 +1,8 @@ +$ -qq +STDOUT >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +E] this is error message^LF + | ^LF +:: today's weather is good^LF +H] today's weather is ok^LF +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +I] this is an info messagenmultilineaa \xE4\xBD\xA0\xE5\xA5\xBD sldkfjals\xF0\x9F\xA4\x96kdjfl^LF + | kasjdflkjasldkfjaklsdjflkjasldkfjlaksjdflkajsdklfjlaksjdf^LF + | kljasldkfjlasldkjflaskdjflaksjdlfkajsldkfjkasjdlfkjaskldj^LF + | flajsdlkfjlaskjdfklajsdf^LF +W] this is a warn message^LF +E] this is error message^LF + | ^LF +:: today's weather is good^LF +H] today's weather is ok^LF +I] finished in 0.00s^LF +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +I] this is an info messagenmultilineaa \xE4\xBD\xA0\xE5\xA5\xBD sldkfjals\xF0\x9F\xA4\x96kdjfl^LF + | kasjdflkjasldkfjaklsdjflkjasldkfjlaksjdflkajsdklfjlaksjdf^LF + | kljasldkfjlasldkjflaskdjflaksjdlfkajsldkfjkasjdlfkjaskldj^LF + | flajsdlkfjlaskjdfklajsdf^LF +W] this is a warn message^LF +E] this is error message^LF + | ^LF +D] this is debug message^LF + | 2^LF + | ^LF +:: today's weather is good^LF +H] today's weather is ok^LF +I] finished in 0.00s^LF +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +I] this is an info messagenmultilineaa \xE4\xBD\xA0\xE5\xA5\xBD sldkfjals\xF0\x9F\xA4\x96kdjfl^LF + | kasjdflkjasldkfjaklsdjflkjasldkfjlaksjdflkajsdklfjlaksjdf^LF + | kljasldkfjlasldkjflaskdjflaksjdlfkajsldkfjkasjdlfkjaskldj^LF + | flajsdlkfjlaskjdfklajsdf^LF +W] this is a warn message^LF +E] this is error message^LF + | ^LF +D] this is debug message^LF + | 2^LF + | ^LF +*] [print_levels print_levels.rs:20] this is trace message^LF + | ^LF + | 2^LF +:: today's weather is good^LF +H] today's weather is ok^LF +I] finished in 0.00s^LF +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +\x1B[91mE]\x1B[91m this is error message^LF +\x1B[90m | \x1B[91m^LF +\x1B[90m::\x1B[0m today's weather is good^LF +\x1B[96mH\x1B[90m]\x1B[93m today's weather is ok^LF +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +\x1B[92mI\x1B[90m]\x1B[0m this is an info messagenmultilineaa \xE4\xBD\xA0\xE5\xA5\xBD sldkfjals\xF0\x9F\xA4\x96kdjfl^LF +\x1B[90m | \x1B[0mkasjdflkjasldkfjaklsdjflkjasldkfjlaksjdflkajsdklfjlaksjdf^LF +\x1B[90m | \x1B[0mkljasldkfjlasldkjflaskdjflaksjdlfkajsldkfjkasjdlfkjaskldj^LF +\x1B[90m | \x1B[0mflajsdlkfjlaskjdfklajsdf^LF +\x1B[93mW]\x1B[93m this is a warn message^LF +\x1B[91mE]\x1B[91m this is error message^LF +\x1B[90m | \x1B[91m^LF +\x1B[90m::\x1B[0m today's weather is good^LF +\x1B[96mH\x1B[90m]\x1B[93m today's weather is ok^LF +\x1B[92mI\x1B[90m]\x1B[0m finished in 0.00s^LF +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +\x1B[92mI\x1B[90m]\x1B[0m this is an info messagenmultilineaa \xE4\xBD\xA0\xE5\xA5\xBD sldkfjals\xF0\x9F\xA4\x96kdjfl^LF +\x1B[90m | \x1B[0mkasjdflkjasldkfjaklsdjflkjasldkfjlaksjdflkajsdklfjlaksjdf^LF +\x1B[90m | \x1B[0mkljasldkfjlasldkjflaskdjflaksjdlfkajsldkfjkasjdlfkjaskldj^LF +\x1B[90m | \x1B[0mflajsdlkfjlaskjdfklajsdf^LF +\x1B[93mW]\x1B[93m this is a warn message^LF +\x1B[91mE]\x1B[91m this is error message^LF +\x1B[90m | \x1B[91m^LF +\x1B[90mD]\x1B[96m this is debug message^LF +\x1B[90m | \x1B[96m2^LF +\x1B[90m | \x1B[96m^LF +\x1B[90m::\x1B[0m today's weather is good^LF +\x1B[96mH\x1B[90m]\x1B[93m today's weather is ok^LF +\x1B[92mI\x1B[90m]\x1B[0m finished in 0.00s^LF +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +\x1B[92mI\x1B[90m]\x1B[0m this is an info messagenmultilineaa \xE4\xBD\xA0\xE5\xA5\xBD sldkfjals\xF0\x9F\xA4\x96kdjfl^LF +\x1B[90m | \x1B[0mkasjdflkjasldkfjaklsdjflkjasldkfjlaksjdflkajsdklfjlaksjdf^LF +\x1B[90m | \x1B[0mkljasldkfjlasldkjflaskdjflaksjdlfkajsldkfjkasjdlfkjaskldj^LF +\x1B[90m | \x1B[0mflajsdlkfjlaskjdfklajsdf^LF +\x1B[93mW]\x1B[93m this is a warn message^LF +\x1B[91mE]\x1B[91m this is error message^LF +\x1B[90m | \x1B[91m^LF +\x1B[90mD]\x1B[96m this is debug message^LF +\x1B[90m | \x1B[96m2^LF +\x1B[90m | \x1B[96m^LF +\x1B[95m*]\x1B[95m [print_levels print_levels.rs:20] this is trace message^LF +\x1B[90m | \x1B[95m^LF +\x1B[90m | \x1B[95m2^LF +\x1B[90m::\x1B[0m today's weather is good^LF +\x1B[96mH\x1B[90m]\x1B[93m today's weather is ok^LF +\x1B[92mI\x1B[90m]\x1B[0m finished in 0.00s^LF +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +H] testing prompts^LF +E] prompt not allowed in non-interactive mode: what's your f^LF + | avorite programming language?^LF +E] fatal: prompt not allowed in non-interactive mode: what's^LF + | your favorite programming language?^LF +H] use -vv or set RUST_BACKTRACE=1 to display the error back^LF + | trace.^LF +I] finished in 0.00s^LF +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +H] testing prompts^LF +E] prompt not allowed in non-interactive mode: continue?^LF +E] fatal: prompt not allowed in non-interactive mode: contin^LF + | ue?^LF +H] use -vv or set RUST_BACKTRACE=1 to display the error back^LF + | trace.^LF +I] finished in 0.00s^LF +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +H] testing prompts^LF +^CR +\x1B[K!] what's your favorite programming language?^LF +-: ^CR +\x1B[K\x1B[1A\x1B[K!] what's your favorite programming language?^LF +^CR +\x1B[KI] you answered: rust^LF +I] the answer is correct^LF +I] finished in 0.00s^LF +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +H] testing prompts^LF +^CR +\x1B[K!] continue? [y/n]^LF +-: ^CR +\x1B[K\x1B[1A\x1B[K!] continue? [y/n]^LF +^CR +\x1B[K^CR +\x1B[K!] what's your favorite programming language?^LF +-: ^CR +\x1B[K\x1B[1A\x1B[K!] what's your favorite programming language?^LF +^CR +\x1B[KI] you answered: rust^LF +I] the answer is correct^LF +I] finished in 0.00s^LF +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +H] testing prompts^LF +^CR +\x1B[K!] continue? [y/n]^LF +-: ^CR +\x1B[K\x1B[1A\x1B[K!] continue? [y/n]^LF +^CR +\x1B[K^CR +\x1B[K!] what's your favorite programming language?^LF +-: ^CR +\x1B[K\x1B[1A\x1B[K!] what's your favorite programming language?^LF +^CR +\x1B[KI] you answered: json^LF +E] fatal: the answer is incorrect^LF +H] use -vv or set RUST_BACKTRACE=1 to display the error back^LF + | trace.^LF +I] finished in 0.00s^LF +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +H] testing prompts^LF +^CR +\x1B[K!] continue? [y/n]^LF +-: ^CR +\x1B[K\x1B[1A\x1B[K!] continue? [y/n]^LF +^CR +\x1B[KW] you chose to not continue!^LF +I] finished in 0.00s^LF +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +H] testing prompts^LF +^CR +\x1B[K!] continue? [y/n]^LF +-: ^CR +\x1B[K\x1B[1A\x1B[K!] continue? [y/n]^LF +^CR +\x1B[KE] please enter yes or no^LF +^CR +\x1B[K!] continue? [y/n]^LF +-: ^CR +\x1B[K\x1B[1A\x1B[K!] continue? [y/n]^LF +^CR +\x1B[KW] you chose to not continue!^LF +I] finished in 0.00s^LF +^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^, + /// Update snapshot + #[clap(short, long)] + update: bool, + /// Display the output of the ith-case in the example instead of testing + #[clap(short, long, requires = "test", conflicts_with = "update")] + display: Option, + + /// Prompt instead of using pre-configured stdin + #[clap(short = 'i', long, requires = "test", conflicts_with = "update")] + inherit_stdin: bool, + + #[clap(flatten)] + flags: cu::cli::Flags, +} + +#[cu::cli(flags = "flags")] +async fn main(args: Cli) -> cu::Result<()> { + match args.test { + None => { + let test_targets = cu::check!(find_tests(), "failed to find tests")?; + run_test_targets(test_targets, args.update).await?; + } + Some(example_name) => { + let path = crate_dir() + .join("examples") + .join(format!("{example_name}.rs")); + let test_cases = cu::check!( + parse_test_cases(&path), + "failed to parse test case from '{example_name}'" + )?; + if let Some(case_i) = args.display { + let test_case = cu::check!( + test_cases.get(case_i), + "index out of bound of test cases: {case_i}" + )?; + let childargs = &test_case.args; + let stdin = test_case.stdin.as_ref().cloned().unwrap_or_default(); + let feature = format!("__test-{example_name},common"); + cu::print!("TEST OUTPUT >>>>>>>>>>>>>>>>>>>>>>>>>>"); + let command_builder = + cu::which("cargo")? + .command() + .args([ + "run", + "-q", + "--example", + &example_name, + "--no-default-features", + "--features", + &feature, + "--", + ]) + .args(childargs) + .stdout_inherit() + .stderr_inherit(); + let exit_status = if args.inherit_stdin { + command_builder.stdin_inherit() + .co_wait() + .await? + } else { + command_builder.stdin(cu::pio::write(stdin)) + .co_wait() + .await? + }; + cu::print!("TEST OUTPUT <<<<<<<<<<<<<<<<<<<<<<<<<<"); + cu::print!("STATUS: {exit_status}"); + } else { + let test_target = TestTarget { + example_name, + test_cases, + }; + run_test_targets(vec![test_target], args.update).await?; + } + } + } + Ok(()) +} + +struct TestTarget { + example_name: String, + test_cases: Vec, +} + +struct TestCase { + stdin: Option>, + args: Vec, +} + +fn find_tests() -> cu::Result> { + let path = crate_dir().join("examples"); + + // find example entry points + let mut test_targets = vec![]; + let dir = cu::fs::read_dir(path)?; + for entry in dir { + let entry = entry?; + let name = entry.file_name(); + let name_str = cu::check!(name.to_str(), "not utf8")?; + let Some(example_name) = name_str.strip_suffix(".rs") else { + continue; // ignore non *.rs file + }; + let test_cases = parse_test_cases(&entry.path())?; + let test_target = TestTarget { + example_name: example_name.to_string(), + test_cases, + }; + test_targets.push(test_target); + } + + Ok(test_targets) +} + +fn parse_test_cases(path: &Path) -> cu::Result> { + let file = cu::fs::read_string(path)?; + let mut test_cases = vec![]; + for line in file.lines() { + let Some(line) = line.strip_prefix("// $") else { + break; + }; + let mut args = cu::check!( + shell_words::split(line.trim()), + "failed to parse command line: {line}" + )?; + let mut stdin = None; + if args.len() >= 2 { + if let Some("<") = args.get(args.len() - 2).map(|x| x.as_str()) { + let stdin_path = args.pop().unwrap(); + let stdin_path = crate_dir().join("input").join(stdin_path); + stdin = Some(cu::check!( + cu::fs::read(stdin_path), + "failed to read stdin for test case" + )?); + args.pop(); + } + } + test_cases.push(TestCase { args, stdin }); + } + Ok(test_cases) +} + +async fn run_test_targets(targets: Vec, update: bool) -> cu::Result<()> { + let build_bar = cu::progress("building test targets") + .total(targets.len()) + .eta(false) + .spawn(); + // build one at a time + let build_pool = cu::co::pool(1); + let mut build_handles = Vec::with_capacity(targets.len()); + let mut total_tests = 0; + for target in &targets { + total_tests += target.test_cases.len(); + if target.test_cases.is_empty() { + cu::warn!("no test case found in '{}'", target.example_name); + } + // cargo build --example X --features __test-X + let example_name = target.example_name.clone(); + let build_bar = Arc::clone(&build_bar); + let handle = build_pool.spawn(async move { + let feature = format!("__test-{example_name},common"); + let (child, bar) = cu::which("cargo")? + .command() + .args([ + "build", + "--example", + &example_name, + "--no-default-features", + "--features", + &feature, + ]) + .preset( + cu::pio::cargo(format!("building {example_name}")) + .configure_spinner(|bar| bar.parent(Some(Arc::clone(&build_bar)))), + ) + .co_spawn() + .await?; + child.co_wait_nz().await?; + bar.done(); + cu::progress!(build_bar += 1); + cu::Ok(example_name) + }); + build_handles.push(handle); + } + drop(build_bar); + + let test_bar = cu::progress("running tests") + .total(total_tests) + .max_display_children(10) + .eta(false) + .spawn(); + let test_pool = cu::co::pool(-2); + let mut test_handles = Vec::with_capacity(total_tests); + let mut build_set = cu::co::set(build_handles); + while let Some(result) = build_set.next().await { + let example_name = result??; + let target = cu::check!( + targets.iter().find(|x| x.example_name == example_name), + "unexpected: cannot find test cases" + )?; + + for (index, test_cases) in target.test_cases.iter().enumerate() { + let example_name = example_name.clone(); + let args = test_cases.args.clone(); + let stdin = test_cases.stdin.clone().unwrap_or_default(); + let test_bar = Arc::clone(&test_bar); + + let handle = test_pool.spawn(async move { + let feature = format!("__test-{example_name},common"); + let command = shell_words::join(&args); + let child_bar = test_bar.child(format!("{example_name}: {command}")).spawn(); + let (child, stdout, stderr) = cu::which("cargo")? + .command() + .args([ + "run", + "-q", + "--example", + &example_name, + "--no-default-features", + "--features", + &feature, + "--", + ]) + .args(args) + .stdout(cu::pio::buffer()) + .stderr(cu::pio::buffer()) + .stdin(cu::pio::write(stdin)) + .co_spawn() + .await?; + let status = child.co_wait().await?; + child_bar.done(); + let stdout = stdout.co_join().await??; + let stderr = stderr.co_join().await??; + let output = decode_output_streams(&command, stdout, stderr, status); + + cu::progress!(test_bar += 1); + cu::Ok((example_name, command, index, output)) + }); + test_handles.push(handle); + } + } + + let mut test_set = cu::co::set(test_handles); + let mut failures = vec![]; + while let Some(result) = test_set.next().await { + let (example_name, command, index, output) = result??; + let result = verify_output(&example_name, &command, index, &output, update); + if let Err(error) = result { + failures.push(error.to_string()); + cu::progress!(test_bar, "{} failed", failures.len()); + } + } + drop(test_bar); + + if failures.is_empty() { + cu::info!("all tests passed"); + return Ok(()); + } + + for f in &failures { + cu::warn!("test failed: {f}"); + } + + cu::bail!("{} tests failed", failures.len()); +} + +fn decode_output_streams( + command: &str, + stdout: Vec, + stderr: Vec, + status: ExitStatus, +) -> String { + use std::fmt::Write as _; + let mut out = String::new(); + out.push_str("$ "); + out.push_str(command); + out.push('\n'); + out.push_str("STDOUT >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"); + decode_output_stream(&mut out, &stdout); + out.push_str("^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"); + decode_output_stream(&mut out, &stderr); + out.push_str("^ out.push(byte as char), + b'\r' => out.push_str("^CR\n"), + b'\n' => out.push_str("^LF\n"), + byte => out.push_str(&format!("\\x{byte:02X}")), + } + } +} + +fn verify_output( + example_name: &str, + command: &str, + index: usize, + output: &str, + update: bool, +) -> cu::Result<()> { + let mut output_path = crate_dir().join("output"); + let file_name = format!("{example_name}-{index}.txt"); + output_path.push(&file_name); + + if !output_path.exists() { + cu::info!("new snapshot: {example_name}: {command}"); + cu::fs::write(output_path, output)?; + return Ok(()); + } + + let expected_output = cu::fs::read_string(&output_path)?; + // normalize line ending for windows users + let expected_output = expected_output.lines().collect::>().join("\n"); + if expected_output.trim() == output.trim() { + cu::info!("pass: {example_name}: {command}"); + return Ok(()); + } + + if !update { + cu::error!("fail: {example_name}: {command}"); + let mut wip_path = crate_dir().join("wip"); + wip_path.push(&file_name); + cu::fs::write(wip_path, output)?; + cu::bail!("output mismatch: {file_name}"); + } + + cu::fs::write(output_path, output)?; + cu::info!("updated snapshot: {example_name}: {command}"); + Ok(()) +} + +fn crate_dir() -> &'static Path { + env!("CARGO_MANIFEST_DIR").as_ref() +} From 5a8816e2e4e171f169536f651e34a584cc8cd710 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Sat, 10 Jan 2026 22:13:03 -0800 Subject: [PATCH 04/13] refactored print stuff --- packages/copper/Cargo.toml | 15 +- packages/copper/examples/prompt_password.rs | 21 - packages/copper/src/{cli.rs => cli/flags.rs} | 209 +------ .../copper/src/{print => cli/fmt}/ansi.rs | 4 +- .../format.rs => cli/fmt/format_buffer.rs} | 52 +- packages/copper/src/cli/fmt/mod.rs | 8 + packages/copper/src/cli/fmt/term_size.rs | 21 + .../copper/src/{print => cli/fmt}/utf8.rs | 0 packages/copper/src/cli/macros.rs | 86 +++ packages/copper/src/cli/mod.rs | 202 +++++++ .../prompt_password.rs => cli/password.rs} | 20 +- packages/copper/src/cli/print_init.rs | 143 +++++ packages/copper/src/{print => cli}/printer.rs | 338 +++++------ packages/copper/src/cli/progress/builder.rs | 216 +++++++ .../copper/src/{print => cli}/progress/eta.rs | 2 +- packages/copper/src/cli/progress/macros.rs | 47 ++ packages/copper/src/cli/progress/mod.rs | 151 +++++ .../src/{print => cli}/progress/state.rs | 249 +++++--- packages/copper/src/cli/progress/util.rs | 53 ++ packages/copper/src/{print => cli}/prompt.rs | 70 +-- packages/copper/src/cli/thread_name.rs | 11 + packages/copper/src/lib.rs | 79 ++- packages/copper/src/lv.rs | 68 ++- packages/copper/src/print/init.rs | 145 ----- packages/copper/src/print/mod.rs | 26 - packages/copper/src/print/progress/bar.rs | 535 ------------------ packages/copper/src/print/progress/mod.rs | 5 - packages/copper/src/process/arg.rs | 8 +- packages/copper/src/process/builder.rs | 2 +- packages/copper/src/process/child.rs | 6 +- .../copper/src/process/pio/cargo_preset.rs | 142 ++--- packages/copper/src/process/pio/print.rs | 7 +- .../copper/src/process/pio/print_driver.rs | 5 +- packages/copper/src/process/pio/spinner.rs | 17 +- packages/copper/src/str/byte_format.rs | 23 + packages/copper/src/str/mod.rs | 11 + packages/copper/src/{ => str}/path.rs | 0 .../zero_when_drop.rs => str/zstring.rs} | 25 +- 38 files changed, 1520 insertions(+), 1502 deletions(-) delete mode 100644 packages/copper/examples/prompt_password.rs rename packages/copper/src/{cli.rs => cli/flags.rs} (53%) rename packages/copper/src/{print => cli/fmt}/ansi.rs (95%) rename packages/copper/src/{print/format.rs => cli/fmt/format_buffer.rs} (66%) create mode 100644 packages/copper/src/cli/fmt/mod.rs create mode 100644 packages/copper/src/cli/fmt/term_size.rs rename packages/copper/src/{print => cli/fmt}/utf8.rs (100%) create mode 100644 packages/copper/src/cli/macros.rs create mode 100644 packages/copper/src/cli/mod.rs rename packages/copper/src/{print/prompt_password.rs => cli/password.rs} (92%) create mode 100644 packages/copper/src/cli/print_init.rs rename packages/copper/src/{print => cli}/printer.rs (81%) create mode 100644 packages/copper/src/cli/progress/builder.rs rename packages/copper/src/{print => cli}/progress/eta.rs (98%) create mode 100644 packages/copper/src/cli/progress/macros.rs create mode 100644 packages/copper/src/cli/progress/mod.rs rename packages/copper/src/{print => cli}/progress/state.rs (73%) create mode 100644 packages/copper/src/cli/progress/util.rs rename packages/copper/src/{print => cli}/prompt.rs (56%) create mode 100644 packages/copper/src/cli/thread_name.rs delete mode 100644 packages/copper/src/print/init.rs delete mode 100644 packages/copper/src/print/mod.rs delete mode 100644 packages/copper/src/print/progress/bar.rs delete mode 100644 packages/copper/src/print/progress/mod.rs create mode 100644 packages/copper/src/str/byte_format.rs create mode 100644 packages/copper/src/str/mod.rs rename packages/copper/src/{ => str}/path.rs (100%) rename packages/copper/src/{print/zero_when_drop.rs => str/zstring.rs} (73%) diff --git a/packages/copper/Cargo.toml b/packages/copper/Cargo.toml index c90f372..6a8fb60 100644 --- a/packages/copper/Cargo.toml +++ b/packages/copper/Cargo.toml @@ -13,11 +13,12 @@ exclude = [ [dependencies] pistonite-cu-proc-macros = { version = "0.2.5", path = "../copper-proc-macros" } +# --- Always enabled --- anyhow = "1.0.100" log = "0.4.29" -oneshot = "0.1.11" # printing/cli +oneshot = { version = "0.1.11", optional = true } env_filter = { version = "0.1.4", optional = true } terminal_size = { version = "0.4.3", optional = true } unicode-width = { version = "0.2.2", features = ["cjk"], optional = true } @@ -65,8 +66,8 @@ features = [ "macros", "rt-multi-thread", "time" ] [features] default = ["full"] full = [ - "coroutine-heavy", "cli", + "coroutine-heavy", "prompt-password", "process", "json", @@ -75,12 +76,12 @@ full = [ "derive", ] -# Command Line Interface (enables integration with `clap` and command line entry points) +# Command Line Interface +print = ["dep:oneshot", "dep:regex", "dep:env_filter", "dep:terminal_size", "dep:unicode-width"] cli = ["dep:clap", "print"] -print = ["dep:regex", "dep:env_filter", "dep:terminal_size", "dep:unicode-width"] -# Utils to show prompt for user input in terminal prompt = ["print"] prompt-password = ["prompt"] + # Enable coroutine drivers, which allow interop with async coroutine = [ "dep:tokio", "dep:num_cpus", @@ -136,7 +137,3 @@ required-features = ["fs", "cli"] [[example]] name = "cargo" required-features = ["process", "cli", "json"] - -[[example]] -name = "prompt_password" -required-features = ["cli", "prompt-password"] diff --git a/packages/copper/examples/prompt_password.rs b/packages/copper/examples/prompt_password.rs deleted file mode 100644 index 30c31d2..0000000 --- a/packages/copper/examples/prompt_password.rs +++ /dev/null @@ -1,21 +0,0 @@ -use pistonite_cu as cu; - -use cu::pre::*; - -#[derive(clap::Parser, Clone)] -struct Args { - #[clap(flatten)] - inner: cu::cli::Flags, -} - -#[cu::cli(flags = "inner")] -fn main(_: Args) -> cu::Result<()> { - let name = cu::prompt!("name")?; - cu::info!("name is: {name}"); - // type for pw is ZeroWhenDropString - let pw = cu::prompt_password!("password")?; - cu::info!("password is: {pw}"); - let pw2 = cu::prompt_legal_password!("legal password")?; - cu::info!("password is: {pw2}"); - Ok(()) -} diff --git a/packages/copper/src/cli.rs b/packages/copper/src/cli/flags.rs similarity index 53% rename from packages/copper/src/cli.rs rename to packages/copper/src/cli/flags.rs index 861b600..63dcdfc 100644 --- a/packages/copper/src/cli.rs +++ b/packages/copper/src/cli/flags.rs @@ -1,204 +1,4 @@ -//! CLI entry point and integration with `clap` -//! -//! When `cli` feature is enabled, `clap` is re-exported from the prelude, -//! so you can use `clap` as if it's a dependency, without actually adding -//! it to your `Cargo.toml` -//! ```rust,no_run -//! # use pistonite_cu as cu; -//! use cu::pre::*; -//! use clap::Parser; -//! -//! #[derive(Parser)] -//! struct MyCli { -//! /// Just an example flag -//! #[clap(short, long)] -//! hello: bool, -//! } -//! ``` -//! -//! # Common Command Options -//! The [`Flags`] struct implement `clap::Args` to provide common -//! options that integrates with the rest of the crate: -//! - `--verbose`/`-v` to increase verbose level. -//! - `--quiet`/`-q` to decrease verbose level. -//! - `--color` to set color mode -//! -//! If your program has user interaction, the `prompt` feature enables these options: -//! - `--yes`/`-y` to answer `y` to all yes/no prompts. -//! - `--non-interactive`: Disallow prompts, prompts will fail with an error instead -//! - `--interactive`: This is the default, and cancels the effect of one `--non-interactive` -//! -//! The [`cu::cli`](macro@crate::cli) macro generates a shim -//! to parse the flags and pass it to your main function. -//! It also handles the `Result` returned back -//! ```rust,no_run -//! # use pistonite_cu as cu; -//! use cu::pre::*; -//! // clap will be part of the prelude -//! // when the `cli` feature is enabled -//! -//! // Typically, you want to have a wrapper struct -//! // so you can derive additional options with clap, -//! // and provide a description via doc comments, like below -//! -//! /// My program -//! /// -//! /// This is my program, it is very good. -//! #[derive(clap::Parser, Clone)] -//! struct Args { -//! /// Input of the program -//! #[clap(short, long)] -//! input: String, -//! /// Output of the program -//! #[clap(short, long)] -//! output: Option, -//! #[clap(flatten)] -//! inner: cu::cli::Flags, -//! } -//! // The 'flags' attribute lets the generated code access the common flags -//! // in the cli struct. When omitted, the struct should implement AsRef -//! #[cu::cli(flags = "inner")] -//! fn main(args: Args) -> cu::Result<()> { -//! cu::info!("input is {}", args.input); -//! cu::info!("output is {:?}", args.output); -//! Ok(()) -//! } -//! ``` -//! -//! If your program is simple or you don't need extra -//! description in the --help message, you can also use `cu::cli::Flags` -//! directly in `main`: -//! ```rust,no_run -//! # use pistonite_cu as cu; -//! #[cu::cli] -//! fn main(args: cu::cli::Flags) -> cu::Result<()> { -//! Ok(()) -//! } -//! ``` -//! -//! Optionally, a preprocessor function can be provided to modify the structs -//! (typically the common flags) before applying them -//! ```rust,no_run -//! # use pistonite_cu as cu; -//! use cu::pre::*; -//! -//! #[derive(clap::Parser)] -//! struct Args { -//! #[clap(subcommand)] -//! subcommand: Option, -//! #[clap(flatten)] -//! inner: cu::cli::Flags, -//! } -//! impl Args { -//! fn preprocess(&mut self) { -//! // merge subcommand flags into top level flags -//! if let Some(Command::Foo(c)) = &self.subcommand { -//! self.inner.merge(c); -//! } -//! } -//! } -//! impl AsRef for Args { -//! fn as_ref(&self) -> &cu::cli::Flags { -//! &self.inner -//! } -//! } -//! #[derive(clap::Subcommand)] -//! enum Command { -//! Foo(cu::cli::Flags), -//! } -//! #[cu::cli(preprocess = Args::preprocess)] -//! fn main(args: Args) -> cu::Result<()> { -//! Ok(()) -//! } -//! ``` -//! -//! # Printing and Logging -//! By default, even without the `cli` feature, `cu` re-exports -//! the features from `log` so you can add logging and error handling (through -//! `anyhow`) by depending on `cu` from a library. -//! -//! For crates only used in binary, but is not a binary target (i.e. -//! some shared module used for binary targets), you can enable -//! the `print` feature to get access to the `print` and `hint` macros: -//! - `print`: like `info`, but has a higher importance -//! - `hint`: like `print`, but specifically for hinting actions the user can take -//! (to resolve an error, for example). -//! -//! These 2 levels are not directly controlled by `log`, -//! and can still print when logging is statically disabled. -//! -//! The following table shows what are printed for each level, -//! (other than `print` and `hint`, the rest are re-exports from `log`) -//! | | `-qq` | ` -q` | ` ` | ` -v` | `-vv` | -//! |-|- |- |- |- |- | -//! | [`error!`](crate::error) | ❌ | ✅ | ✅ | ✅ | ✅ | -//! | [`hint!`](crate::hint) | ❌ | ✅ | ✅ | ✅ | ✅ | -//! | [`print!`](macro@crate::print) | ❌ | ✅ | ✅ | ✅ | ✅ | -//! | [`warn!`](crate::warn) | ❌ | ❌ | ✅ | ✅ | ✅ | -//! | [`info!`](crate::info) | ❌ | ❌ | ✅ | ✅ | ✅ | -//! | [`debug!`](crate::debug) | ❌ | ❌ | ❌ | ✅ | ✅ | -//! | [`trace!`](crate::trace) | ❌ | ❌ | ❌ | ❌ | ✅ | -//! -//! The `RUST_LOG` environment variable is also supported in the same -//! way as in [`env_logger`](https://docs.rs/env_logger/latest/env_logger/#enabling-logging). -//! When mixing `RUST_LOG` and verbosity flags, logging messages are filtered -//! by `RUST_LOG`, and the verbosity would only apply to `print` and `hint` -//! -//! When setting up test, you can use [`log_init`](crate::log_init) to quickly inititialize logging -//! without dealing with the details. -//! -//! [`set_thread_print_name`](crate::set_thread_print_name) can be used to add a prefix to all messages printed -//! by the current thread. -//! -//! Messages that are too long and multi-line messages are automatically wrapped. -//! -//! # Progress Bar -//! Animated progress bars are displayed at the bottom of the terminal. -//! While progress bars are visible, printing still works and will be put -//! above the bars. However, prints will be buffered and refreshed -//! and the same frame rate as the bars. -//! -//! [`progress_bar`](crate::progress_bar) and [`progress_bar_lowp`](crate::progress_bar_lowp) are used to create a bar. -//! The only difference is that `lowp` doesn't print a message when the progress -//! is done (as if the bar was never there). The bar takes a message to indicate -//! the current action, and each update call can accept a message to indicate -//! the current step. When `bar` is dropped, it will print a done message. -//! -//! ```rust,no_run -//! # use pistonite_cu as cu; -//! use std::time::Duration; -//! { -//! let bar = cu::progress_bar(10, "This takes 2.5 seconds"); -//! for i in 0..10 { -//! cu::progress!(&bar, i, "step {i}"); -//! cu::debug!("this is debug message"); -//! std::thread::sleep(Duration::from_millis(250)); -//! } -//! } -//! ``` -//! -//! [`progress_unbounded`](crate::progress_unbounded) and [`progress_unbounded_lowp`](crate::progress_unbounded_lowp) are variants -//! that doesn't display the total steps. Use `()` as the step placeholder -//! when updating the bar. -//! -//! # Prompting -//! With the `prompt` feature enabled, you can -//! use [`prompt!`](crate::prompt) and [`yesno!`](crate::yesno) to show prompts. -//! -//! The prompts are thread-safe, meaning -//! You can call them from multiple threads, and they will be queued to prompt the user one after -//! the other. Prompts are always shown regardless of verbosity. But when stdout is redirected, -//! they will not render in terminal. -//! -//! # Async Entry Point -//! For async usage, see the [`coroutine`](crate::co) concept. -//! -//! # Manual Parsing -//! [`cu::cli::try_parse`](crate::cli::try_parse) -//! and [`cu::cli::print_help`](crate::cli::print_help) can be useful -//! when you want to manually invoke a command parser. These -//! respect the `--color` option passed to the program. -//! + use std::ffi::OsString; use std::time::Instant; @@ -281,7 +81,8 @@ impl Flags { None } }; - crate::init_print_options(self.color.unwrap_or_default(), level, prompt); + todo!() + // crate::init_print_options(self.color.unwrap_or_default(), level, prompt); } /// Merge `other` into self. Options in other will be applied on top of self (equivalent @@ -396,7 +197,7 @@ pub fn try_parse(iter: I) -> Option where I::Item: Into + Clone, { - let use_color = crate::color_enabled(); + let use_color = crate::lv::color_enabled(); let result = get_colored_command::(use_color) .try_get_matches_from(iter) .and_then(|mut matches| ::from_arg_matches_mut(&mut matches)); @@ -420,7 +221,7 @@ where /// an application. #[inline(always)] pub fn print_help(long: bool) { - let command = get_colored_command::(crate::color_enabled()); + let command = get_colored_command::(crate::lv::color_enabled()); print_help_impl(command, long) } fn print_help_impl(mut command: Command, long: bool) { diff --git a/packages/copper/src/print/ansi.rs b/packages/copper/src/cli/fmt/ansi.rs similarity index 95% rename from packages/copper/src/print/ansi.rs rename to packages/copper/src/cli/fmt/ansi.rs index c72e153..a8e113f 100644 --- a/packages/copper/src/print/ansi.rs +++ b/packages/copper/src/cli/fmt/ansi.rs @@ -73,7 +73,7 @@ impl<'a> Iterator for AnsiWidthIter<'a> { fn next(&mut self) -> Option { let c = self.chars.next()?; let width = if self.is_escaping { - if is_ansi_end_char(c) { + if is_esc_end(c) { self.is_escaping = false; } 0 @@ -89,7 +89,7 @@ impl<'a> Iterator for AnsiWidthIter<'a> { } } -pub(crate) fn is_ansi_end_char(c: char) -> bool { +pub(crate) fn is_esc_end(c: char) -> bool { // we only do very basic check right now c < u8::MAX as char && b"mAKGJBCDEFHSTfhlin".contains(&(c as u8)) } diff --git a/packages/copper/src/print/format.rs b/packages/copper/src/cli/fmt/format_buffer.rs similarity index 66% rename from packages/copper/src/print/format.rs rename to packages/copper/src/cli/fmt/format_buffer.rs index 68d672e..4f24baa 100644 --- a/packages/copper/src/print/format.rs +++ b/packages/copper/src/cli/fmt/format_buffer.rs @@ -1,69 +1,63 @@ -use super::ansi; -/// Get the terminal width, or the internal max if cannot get -pub fn term_width_or_max() -> usize { - term_width().unwrap_or(400) -} - -/// Get the terminal width, capped as some internal amount -pub fn term_width() -> Option { - term_width_height().map(|x| x.0) -} - -/// Get the terminal height, capped as some internal amount -pub fn term_width_height() -> Option<(usize, usize)> { - if cfg!(feature = "__test") { - // fix the size in test - Some((60, 20)) - } else { - use terminal_size::*; - terminal_size().map(|(Width(w), Height(h))| ((w as usize).min(400), (h as usize).min(400))) - } -} +use crate::cli::fmt::{self, ansi}; +/// Buffer for formatting printing messages pub(crate) struct FormatBuffer { + /// Total width to print width: usize, + /// Current char position in the line curr: usize, + /// Internal buffer buffer: String, + /// ANSI code for gray gray_color: &'static str, + /// ANSI code for the current text color text_color: &'static str, } impl FormatBuffer { pub fn new() -> Self { Self { - width: term_width_or_max(), + width: fmt::term_width_or_max(), curr: 0, buffer: String::new(), gray_color: "", text_color: "", } } + /// Get the formatted buffer content pub fn as_str(&self) -> &str { self.buffer.as_str() } + /// Take the formatted buffer content out, leaving empty string pub fn take(&mut self) -> String { std::mem::take(&mut self.buffer) } + /// Reset pub fn reset(&mut self, gray_color: &'static str, text_color: &'static str) { self.curr = 0; self.buffer.clear(); - self.width = term_width_or_max(); + self.width = fmt::term_width_or_max(); self.gray_color = gray_color; self.text_color = text_color; } - pub fn end(&mut self) { + + /// Push a newline character (note this is different from [`new_line`](Self::new_line)) + pub fn push_lf(&mut self) { self.buffer.push('\n'); } - + /// Push a string as control characters. i.e. the content will be + /// appended to the buffer without formatting. + pub fn push_control(&mut self, x: &str) { + self.buffer.push_str(x) + } + /// Push and format string content pub fn push_str(&mut self, x: &str) { for (c, w) in ansi::with_width(x.chars()) { self.push(c, w); } } - pub fn push_control(&mut self, x: &str) { - self.buffer.push_str(x) - } + /// Push a character with its width pub fn push(&mut self, c: char, w: usize) { if c == '\n' { self.new_line(); @@ -80,7 +74,7 @@ impl FormatBuffer { self.buffer.push(c); self.curr += w; } - + /// Start formatting a new line pub fn new_line(&mut self) { self.buffer.push('\n'); self.buffer.push_str(self.gray_color); diff --git a/packages/copper/src/cli/fmt/mod.rs b/packages/copper/src/cli/fmt/mod.rs new file mode 100644 index 0000000..ac049a4 --- /dev/null +++ b/packages/copper/src/cli/fmt/mod.rs @@ -0,0 +1,8 @@ +pub mod ansi; +pub mod utf8; + +mod term_size; +pub use term_size::*; +mod format_buffer; +pub(crate) use format_buffer::*; + diff --git a/packages/copper/src/cli/fmt/term_size.rs b/packages/copper/src/cli/fmt/term_size.rs new file mode 100644 index 0000000..bcc7b05 --- /dev/null +++ b/packages/copper/src/cli/fmt/term_size.rs @@ -0,0 +1,21 @@ + +/// Get the terminal width, or the internal max if cannot get +pub fn term_width_or_max() -> usize { + term_width().unwrap_or(400) +} + +/// Get the terminal width, capped as some internal amount +pub fn term_width() -> Option { + term_width_height().map(|x| x.0) +} + +/// Get the terminal height, capped as some internal amount +pub fn term_width_height() -> Option<(usize, usize)> { + if cfg!(feature = "__test") { + // fix the size in test + Some((60, 20)) + } else { + use terminal_size::*; + terminal_size().map(|(Width(w), Height(h))| ((w as usize).min(400), (h as usize).min(400))) + } +} diff --git a/packages/copper/src/print/utf8.rs b/packages/copper/src/cli/fmt/utf8.rs similarity index 100% rename from packages/copper/src/print/utf8.rs rename to packages/copper/src/cli/fmt/utf8.rs diff --git a/packages/copper/src/cli/macros.rs b/packages/copper/src/cli/macros.rs new file mode 100644 index 0000000..1019a58 --- /dev/null +++ b/packages/copper/src/cli/macros.rs @@ -0,0 +1,86 @@ + +use cu::lv; +use cu::cli::printer::PRINTER; +/// Print something +/// +/// This is similar to `info`, but unlike info, this message will still log with `-q`. +#[macro_export] +#[cfg(feature = "print")] +macro_rules! print { + ($($fmt_args:tt)*) => {{ + $crate::cli::__print_with_level($crate::lv::P, format_args!($($fmt_args)*)); + }} +} +/// Logs a hint message +#[macro_export] +#[cfg(feature = "print")] +macro_rules! hint { + ($($fmt_args:tt)*) => {{ + $crate::cli::__print_with_level($crate::lv::H, format_args!($($fmt_args)*)); + }} +} + +/// Show a prompt +/// +/// Use the `prompt-password` feature and [`prompt_password!`](crate::prompt_password) macro +/// if prompting for a password, which will hide user's input from the console +/// +/// ```rust,ignore +/// let name = cu::prompt!("please enter your name")?; +/// cu::info!("user entered: {name}"); +/// ``` +#[cfg(feature = "prompt")] +#[macro_export] +macro_rules! prompt { + ($($fmt_args:tt)*) => {{ + $crate::cli::__prompt(format_args!($($fmt_args)*), false).map(|x| x.to_string()) + }} +} + +/// Show a Yes/No prompt +/// +/// Return `true` if the answer is Yes. Return an error if prompt is not allowed +/// ```rust,ignore +/// if cu::yesno!("do you want to continue?")? { +/// cu::info!("user picked yes"); +/// } +/// ``` +#[cfg(feature = "prompt")] +#[macro_export] +macro_rules! yesno { + ($($fmt_args:tt)*) => {{ + $crate::cli::__prompt_yesno(format_args!($($fmt_args)*)) + }} +} + + +/// Show a password prompt +/// +/// The console will have inputs hidden while user types, and the returned +/// value is a [`ZeroWhenDropString`](crate::ZeroWhenDropString) +/// +/// ```rust,ignore +/// let password = cu::prompt_password!("please enter your password")?; +/// cu::info!("user entered: {password}"); +/// ``` +#[cfg(feature = "prompt-password")] +#[macro_export] +macro_rules! prompt_password { + ($($fmt_args:tt)*) => {{ + $crate::cli::__prompt(format_args!($($fmt_args)*), true) + }} +} + +/// Internal print function for macros +#[doc(hidden)] +pub fn __print_with_level(lv: lv::Lv, message: std::fmt::Arguments<'_>) { + if !lv.can_print(lv::PRINT_LEVEL.get()) { + return; + } + let message = format!("{message}"); + if let Ok(mut printer) = PRINTER.lock() { + if let Some(printer) = printer.as_mut() { + printer.print_message(lv, &message); + } + } +} diff --git a/packages/copper/src/cli/mod.rs b/packages/copper/src/cli/mod.rs new file mode 100644 index 0000000..b3590f5 --- /dev/null +++ b/packages/copper/src/cli/mod.rs @@ -0,0 +1,202 @@ +//! # Printting and Command Line Interface +//! +//! There are 4 feature flags related to CLI +//! - `print`: This is the most minimal feature set. Using +//! features from this feature flag means you acknowledge your code +//! is being called from a program that uses `cu::cli` (i.e. the `cli` feature) +//! - `cli`: Use this if your crate is the end binary (i.e. not a library). +//! This integrates and re-exports [`clap`](https://docs.rs/clap). +//! - This turns on `print` automatically +//! - `prompt`: This implies `print` and will also enable the ability to show prompts in the terminal. +//! - `prompt-password`: This implies `prompt` (which implies `print`), and allows prompting for +//! password (which hides the input when user types into the terminal) +//! +//! # Integration with `clap` +//! +//! When the `cli` feature is enabled, `clap` is re-exported from the prelude, +//! so you can use `clap` as if it's a dependency, without actually adding +//! it to your `Cargo.toml` +//! ```rust,no_run +//! # use pistonite_cu as cu; +//! use cu::pre::*; +//! use clap::Parser; +//! +//! #[derive(Parser)] +//! struct MyCli { +//! /// Just an example flag +//! #[clap(short, long)] +//! hello: bool, +//! } +//! ``` +//! +//! # Common Command Options +//! The [`Flags`] struct implement `clap::Args` to provide common +//! options that integrates with the rest of the crate: +//! - `--verbose`/`-v` to increase verbose level. +//! - `--quiet`/`-q` to decrease verbose level. +//! - `--color` to set color mode +//! +//! The `prompt` feature enables these additional options: +//! - `--yes`/`-y` to answer `y` to all yes/no prompts. +//! - `--non-interactive`: Disallow prompts, prompts will fail with an error instead +//! - With `--yes --non-interactive`, yes/no prompts gets answered `yes` and other prompts are +//! blocked +//! - `--interactive`: This is the default, and cancels the effect of one `--non-interactive` +//! +//! The [`cu::cli`](macro@crate::cli) macro generates a shim +//! to parse the flags and pass it to your main function. +//! It also handles the `Result` returned back. See the example +//! below and more usage examples in the documentation for the macro. +//! ```rust,no_run +//! # use pistonite_cu as cu; +//! use cu::pre::*; +//! // clap will be part of the prelude +//! // when the `cli` feature is enabled +//! +//! // Typically, you want to have a wrapper struct +//! // so you can derive additional options with clap, +//! // and provide a description via doc comments, like below +//! +//! // clap will parse the doc comment of the Args struct +//! // as the help text +//! +//! /// My program +//! /// +//! /// This is my program, it is very good. +//! #[derive(clap::Parser, Clone)] +//! struct Args { +//! /// Input of the program +//! #[clap(short, long)] +//! input: String, +//! /// Output of the program +//! #[clap(short, long)] +//! output: Option, +//! #[clap(flatten)] +//! inner: cu::cli::Flags, +//! } +//! // use the flags attribute to refer to the cu::cli::Flags field inside the Args struct +//! #[cu::cli(flags = "inner")] +//! fn main(args: Args) -> cu::Result<()> { +//! cu::info!("input is {}", args.input); +//! cu::info!("output is {:?}", args.output); +//! Ok(()) +//! } +//! ``` +//! +//! # Printing and Logging +//! In addition to the logging macros re-exported from the [`log`](https://docs.rs/log) +//! crate, `cu` provides `print` and `hint` macros: +//! - `print`: like `info`, but has a higher importance +//! - `hint`: like `print`, but specifically for hinting actions the user can take +//! (to resolve an error, for example). +//! +//! These 2 levels are not directly controlled by `log`, +//! and can still print when logging is statically disabled. +//! +//! The following table shows what are printed for each level, +//! | | `-qq` | ` -q` | ` ` | ` -v` | `-vv` | +//! |-|- |- |- |- |- | +//! | [`error!`](crate::error) | ❌ | ✅ | ✅ | ✅ | ✅ | +//! | [`hint!`](crate::hint) | ❌ | ✅ | ✅ | ✅ | ✅ | +//! | [`print!`](macro@crate::print) | ❌ | ✅ | ✅ | ✅ | ✅ | +//! | [`warn!`](crate::warn) | ❌ | ❌ | ✅ | ✅ | ✅ | +//! | [`info!`](crate::info) | ❌ | ❌ | ✅ | ✅ | ✅ | +//! | [`debug!`](crate::debug) | ❌ | ❌ | ❌ | ✅ | ✅ | +//! | [`trace!`](crate::trace) | ❌ | ❌ | ❌ | ❌ | ✅ | +//! +//! The `RUST_LOG` environment variable is also supported in the same +//! way as in [`env_logger`](https://docs.rs/env_logger/latest/env_logger/#enabling-logging). +//! When mixing `RUST_LOG` and verbosity flags, logging messages are filtered +//! by `RUST_LOG`, and the verbosity would only apply to `print` and `hint` +//! +//! When setting up test, you can use [`log_init`](crate::log_init) to quickly inititialize logging +//! without dealing with the details. +//! +//! [`set_thread_print_name`](crate::set_thread_print_name) can be used to add a prefix to all messages printed +//! by the current thread. +//! +//! Messages that are too long and multi-line messages are automatically wrapped. +//! +//! # Progress Bar +//! Animated progress bars are displayed at the bottom of the terminal. +//! While progress bars are visible, printing still works and will be put +//! above the bars. However, prints will be buffered and refreshed +//! and the same frame rate as the bars. +//! +//! [`progress_bar`](crate::progress_bar) and [`progress_bar_lowp`](crate::progress_bar_lowp) are used to create a bar. +//! The only difference is that `lowp` doesn't print a message when the progress +//! is done (as if the bar was never there). The bar takes a message to indicate +//! the current action, and each update call can accept a message to indicate +//! the current step. When `bar` is dropped, it will print a done message. +//! +//! ```rust,no_run +//! # use pistonite_cu as cu; +//! use std::time::Duration; +//! { +//! let bar = cu::progress_bar(10, "This takes 2.5 seconds"); +//! for i in 0..10 { +//! cu::progress!(&bar, i, "step {i}"); +//! cu::debug!("this is debug message"); +//! std::thread::sleep(Duration::from_millis(250)); +//! } +//! } +//! ``` +//! +//! [`progress_unbounded`](crate::progress_unbounded) and [`progress_unbounded_lowp`](crate::progress_unbounded_lowp) are variants +//! that doesn't display the total steps. Use `()` as the step placeholder +//! when updating the bar. +//! +//! # Prompting +//! With the `prompt` feature enabled, you can +//! use [`prompt!`](crate::prompt) and [`yesno!`](crate::yesno) to show prompts. +//! +//! The prompts are thread-safe, meaning +//! You can call them from multiple threads, and they will be queued to prompt the user one after +//! the other. Prompts are always shown regardless of verbosity. But when stdout is redirected, +//! they will not render in terminal. +//! +//! # Async Entry Point +//! For async usage, see the [`coroutine`](crate::co) concept. +//! +//! # Manual Parsing +//! [`cu::cli::try_parse`](crate::cli::try_parse) +//! and [`cu::cli::print_help`](crate::cli::print_help) can be useful +//! when you want to manually invoke a command parser. These +//! respect the `--color` option passed to the program. +#[cfg(feature = "cli")] +mod flags; +#[cfg(feature = "cli")] +pub use flags::{Flags, try_parse, print_help, __run}; +#[cfg(all(feature = "coroutine", feature = "cli"))] +pub use flags::__co_run; + +mod print_init; +pub use print_init::level; +mod macros; +pub use macros::__print_with_level; + +mod thread_name; +pub use thread_name::set_thread_name; +use thread_name::THREAD_NAME; +mod printer; + +mod progress; +pub use progress::{progress, ProgressBar, ProgressBarBuilder}; + +#[cfg(feature = "prompt")] +mod prompt; +#[cfg(feature = "prompt")] +pub use prompt::{__prompt, __prompt_yesno}; +#[cfg(feature = "prompt-password")] +mod password; +#[cfg(feature = "prompt-password")] +pub use password::password_chars_legal; + +/// Formatting utils +pub(crate) mod fmt; + +// 50ms between each cycle +const TICK_INTERVAL: std::time::Duration = std::time::Duration::from_millis(10); +// 2B ticks * 10ms = 251 days. +// overflown tick means ETA will be inaccurate (after 251 days) +type Tick = u32; diff --git a/packages/copper/src/print/prompt_password.rs b/packages/copper/src/cli/password.rs similarity index 92% rename from packages/copper/src/print/prompt_password.rs rename to packages/copper/src/cli/password.rs index 5411ac5..e22460e 100644 --- a/packages/copper/src/print/prompt_password.rs +++ b/packages/copper/src/cli/password.rs @@ -8,12 +8,12 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) https://github.com/conradkleinespel/rpassword -#[cfg(target_family = "unix")] +#[cfg(unix)] pub(crate) use unix::read_password; -#[cfg(target_family = "windows")] +#[cfg(windows)] pub(crate) use windows::read_password; -#[cfg(target_family = "unix")] +#[cfg(unix)] mod unix { use libc::{ECHO, ECHONL, TCSANOW, c_int, tcsetattr, termios}; use std::io::{self, BufRead}; @@ -56,21 +56,21 @@ mod unix { } /// Turns a C function return into an IO Result - fn io_result(ret: c_int) -> std::io::Result<()> { + fn io_result(ret: c_int) -> io::Result<()> { match ret { 0 => Ok(()), - _ => Err(std::io::Error::last_os_error()), + _ => Err(io::Error::last_os_error()), } } - fn safe_tcgetattr(fd: c_int) -> std::io::Result { + fn safe_tcgetattr(fd: c_int) -> io::Result { let mut term = mem::MaybeUninit::::uninit(); io_result(unsafe { ::libc::tcgetattr(fd, term.as_mut_ptr()) })?; Ok(unsafe { term.assume_init() }) } /// Reads a password from the TTY - pub(crate) fn read_password() -> std::io::Result { + pub(crate) fn read_password() -> io::Result { let tty = std::fs::File::open("/dev/tty")?; let fd = tty.as_raw_fd(); let mut reader = io::BufReader::new(tty); @@ -82,8 +82,8 @@ mod unix { fn read_password_from_fd_with_hidden_input( reader: &mut impl BufRead, fd: i32, - ) -> std::io::Result { - let mut password = crate::ZeroWhenDropString::default(); + ) -> io::Result { + let mut password = crate::ZString::default(); { let _hidden_input = HiddenInput::new(fd)?; reader.read_line(&mut password)?; @@ -92,7 +92,7 @@ mod unix { } } -#[cfg(target_family = "windows")] +#[cfg(windows)] mod windows { use std::io::{self, BufRead, BufReader}; use std::os::windows::io::FromRawHandle; diff --git a/packages/copper/src/cli/print_init.rs b/packages/copper/src/cli/print_init.rs new file mode 100644 index 0000000..2e47e54 --- /dev/null +++ b/packages/copper/src/cli/print_init.rs @@ -0,0 +1,143 @@ +use std::cell::RefCell; +use std::sync::OnceLock; +use std::sync::atomic::Ordering; + +use cu::lv; +use cu::cli::printer::{Printer, PRINTER}; +use env_filter::{Filter as LogEnvFilter, Builder as LogEnvBuilder}; + +static LOG_FILTER: OnceLock = OnceLock::new(); +/// Set the global log filter +pub(crate) fn set_log_filter(filter: LogEnvFilter) { + let _ = LOG_FILTER.set(filter); +} + +/// Shorthand to quickly setup logging. Can be useful in tests. +/// +/// `qq`, `q`, `v` and `vv` inputs map to corresponding print levels. Other inputs +/// are mapped to default level +#[doc(alias = "quick_init")] +pub fn level(lv: &str) { + let level = match lv { + "qq" => lv::Print::QuietQuiet, + "q" => lv::Print::Quiet, + "v" => lv::Print::Verbose, + "vv" => lv::Print::VerboseVerbose, + _ => lv::Print::Normal, + }; + init_options(lv::Color::Auto, level, Some(lv::Prompt::Block)); +} + +/// Set global print options. This is usually called from clap args +/// +/// If prompt option is `None`, it will be `Interactive` unless env var `CI` is `true` or `1`, in which case it becomes `No`. +/// Prompt option is ignored unless `prompt` feature is enabled +pub fn init_options(color: lv::Color, level: lv::Print, prompt: Option) { + // not using cu::env_var, since we are before log initialization + let env_rust_log = std::env::var("RUST_LOG"); + let log_level = match env_rust_log { + Ok(value) if !value.is_empty() => { + let mut builder = LogEnvBuilder::new(); + let filter = builder.parse(&value).build(); + let log_level = filter.filter(); + set_log_filter(filter); + log_level.max(level.into()) + } + _ => level.into() + }; + log::set_max_level(log_level); + + let use_color = color.is_colored_for_stdout(); + lv::USE_COLOR.store(use_color, Ordering::Release); + let printer = Printer::new(use_color); + if let Ok(mut g_printer) = PRINTER.lock() { + *g_printer = Some(printer); + } + todo!() + // #[cfg(feature = "prompt")] + // { + // let prompt = match prompt { + // Some(x) => x, + // None => { + // let is_ci = std::env::var("CI") + // .map(|mut x| { + // x.make_ascii_lowercase(); + // matches!(x.trim(), "true" | "1") + // }) + // .unwrap_or_default(); + // if is_ci { + // lv::Prompt::Block + // } else { + // lv::Prompt::Interactive + // } + // } + // }; + // super::PROMPT_LEVEL.set(prompt) + // } + // #[cfg(not(feature = "prompt"))] + // { + // let _ = prompt; + // super::PROMPT_LEVEL.set(lv::Prompt::No); + // } + // + // lv::PRINT_LEVEL.set(level); + // struct LogImpl; + // impl log::Log for LogImpl { + // fn enabled(&self, metadata: &log::Metadata) -> bool { + // match LOG_FILTER.get() { + // Some(filter) => filter.enabled(metadata), + // None => Lv::from(metadata.level()).can_print(lv::PRINT_LEVEL.get()), + // } + // } + // + // fn log(&self, record: &log::Record) { + // if !self.enabled(record.metadata()) { + // return; + // } + // let typ: Lv = record.level().into(); + // let message = if typ == Lv::Trace { + // // enable source location logging in trace messages + // let mut message = String::new(); + // message.push('['); + // if let Some(p) = record.module_path() { + // // aliased crate, use the shorthand + // if let Some(rest) = p.strip_prefix("pistonite_") { + // message.push_str(rest); + // } else { + // message.push_str(p); + // } + // message.push(' '); + // } + // if let Some(f) = record.file() { + // let name = match f.rfind(['/', '\\']) { + // None => f, + // Some(i) => &f[i + 1..], + // }; + // message.push_str(name); + // } + // if let Some(l) = record.line() { + // message.push(':'); + // message.push_str(&format!("{l}")); + // } + // if message.len() > 1 { + // message += "] "; + // } else { + // message.clear(); + // } + // + // use std::fmt::Write; + // let _: Result<_, _> = write!(&mut message, "{}", record.args()); + // message + // } else { + // record.args().to_string() + // }; + // if let Ok(mut printer) = super::PRINTER.lock() { + // printer.print_message(typ, &message); + // } + // } + // + // fn flush(&self) {} + // } + // + // let _ = log::set_logger(&LogImpl); +} diff --git a/packages/copper/src/print/printer.rs b/packages/copper/src/cli/printer.rs similarity index 81% rename from packages/copper/src/print/printer.rs rename to packages/copper/src/cli/printer.rs index 595057f..5862848 100644 --- a/packages/copper/src/print/printer.rs +++ b/packages/copper/src/cli/printer.rs @@ -1,80 +1,45 @@ +use std::io::{self, IsTerminal as _}; use std::collections::VecDeque; use std::ops::ControlFlow; -use std::sync::{Arc, LazyLock, Mutex, Weak}; +use std::sync::{Arc, Mutex, Weak}; use std::thread::JoinHandle; -use crate::print::progress::{BarFormatter, BarResult, ProgressBar}; -use crate::print::{FormatBuffer, TICK_INTERVAL, ansi}; -use crate::{ZeroWhenDropString, lv}; +use oneshot::{Sender as OnceSend, Receiver as OnceRecv}; -/// Print something -/// -/// This is similar to `info`, but unlike info, this message will still log with `-q`. -#[macro_export] -macro_rules! print { - ($($fmt_args:tt)*) => {{ - $crate::__priv::__print_with_level($crate::lv::P, format_args!($($fmt_args)*)); - }} -} -/// Logs a hint message -#[macro_export] -macro_rules! hint { - ($($fmt_args:tt)*) => {{ - $crate::__priv::__print_with_level($crate::lv::H, format_args!($($fmt_args)*)); - }} -} - -/// Internal print function for macros -pub fn __print_with_level(lv: lv::Lv, message: std::fmt::Arguments<'_>) { - if !lv.can_print(lv::PRINT_LEVEL.get()) { - return; - } - let message = format!("{message}"); - if let Ok(mut printer) = PRINTER.lock() { - printer.print_message(lv, &message); - } -} - -pub(crate) static PRINTER: LazyLock> = - LazyLock::new(|| Mutex::new(Printer::default())); +use crate::cli::{THREAD_NAME, Tick, TICK_INTERVAL, password}; +use crate::cli::fmt::{self, ansi, FormatBuffer}; +use crate::cli::progress::{ProgressBar, BarResult, BarFormatter}; +use crate::lv; /// Global printer state +pub(crate) static PRINTER: Mutex> = Mutex::new(None); pub(crate) struct Printer { is_stdin_terminal: bool, /// Handle to stdout - stdout: std::io::Stdout, + stdout: io::Stdout, /// Handle to stderr - stderr: std::io::Stderr, + stderr: io::Stderr, /// Color codes colors: ansi::Colors, /// Control codes controls: ansi::Controls, - - // printing + print_task: PrintingThread, bar_target: Option, bars: Vec>, pending_prompts: VecDeque, - + /// Buffer for automatically do certain formatting format_buffer: FormatBuffer, /// Place to buffer prints while printing is blocked buffered: String, } - -struct PromptTask { - send: oneshot::Sender>, - prompt: String, - #[cfg(feature = "prompt-password")] - is_password: bool, -} - -impl Default for Printer { - fn default() -> Self { - use std::io::IsTerminal as _; - let stdout = std::io::stdout(); - let stderr = std::io::stderr(); - let is_stdin_terminal = std::io::stdin().is_terminal(); +impl Printer { + pub fn new(use_color: bool) -> Self { + let colors = ansi::colors(use_color); + let stdout = io::stdout(); + let stderr = io::stderr(); + let is_stdin_terminal = io::stdin().is_terminal(); let (bar_target, is_terminal) = if cfg!(feature = "__test") { (Some(Target::Stdout), true) } else { @@ -88,36 +53,29 @@ impl Default for Printer { }; (bar_target, is_terminal) }; - let colors = ansi::colors(is_terminal); let controls = ansi::controls(is_terminal); - + Self { is_stdin_terminal, stdout, stderr, colors, controls, - + print_task: Default::default(), bar_target, bars: Default::default(), pending_prompts: Default::default(), - + format_buffer: FormatBuffer::new(), buffered: String::new(), } } -} -impl Printer { - pub(crate) fn set_colors(&mut self, use_color: bool) { - self.colors = ansi::colors(use_color); - } - pub(crate) fn show_prompt( &mut self, prompt: &str, is_password: bool, - ) -> oneshot::Receiver> { + ) -> OnceRecv> { // format the prompt let mut lines = prompt.lines(); self.format_buffer.reset(self.colors.gray, self.colors.cyan); @@ -135,7 +93,7 @@ impl Printer { if cfg!(feature = "prompt-password") && is_password { self.format_buffer.push_str(": "); } else { - self.format_buffer.end(); + self.format_buffer.push_lf(); self.format_buffer.push_control(self.colors.reset); self.format_buffer.push_control("-: "); } @@ -179,6 +137,38 @@ impl Printer { self.print_task.assign(print_task()); } } + /// Print a progress bar done message + pub(crate) fn print_bar_done(&mut self, result: &BarResult, is_root: bool) { + if lv::PRINT_LEVEL.get() < lv::Print::Normal { + return; + } + if !is_root && self.bar_target.is_some() { + // if bar is animated, don't print child's done messages + return; + } + let message = match result { + BarResult::DontKeep => return, + BarResult::Done(message) => { + self.format_buffer + .reset(self.colors.gray, self.colors.green); + self.format_buffer.push_control(self.colors.green); + message + } + BarResult::Interrupted(message) => { + self.format_buffer + .reset(self.colors.gray, self.colors.yellow); + self.format_buffer.push_control(self.colors.yellow); + message + } + }; + self.format_buffer.push_control("\u{283f}]"); + if !message.starts_with('[') { + self.format_buffer.push_control(" "); + } + self.format_buffer.push_str(message); + self.format_buffer.push_lf(); + self.print_format_buffer(); + } /// Format and print the message pub(crate) fn print_message(&mut self, lv: lv::Lv, message: &str) { @@ -234,7 +224,7 @@ impl Printer { self.format_buffer.push(']', 1); } } - super::THREAD_NAME.with_borrow(|x| { + THREAD_NAME.with_borrow(|x| { if let Some(x) = x { self.format_buffer.push_control(self.colors.magenta); self.format_buffer.push('[', 1); @@ -251,43 +241,9 @@ impl Printer { self.format_buffer.new_line(); self.format_buffer.push_str(line); } - self.format_buffer.end(); - self.print_format_buffer(); - } - - /// Print a progress bar done message - pub(crate) fn print_bar_done(&mut self, result: &BarResult, is_root: bool) { - if lv::PRINT_LEVEL.get() < lv::Print::Normal { - return; - } - if !is_root && self.bar_target.is_some() { - // if bar is animated, don't print child's done messages - return; - } - let message = match result { - BarResult::DontKeep => return, - BarResult::Done(message) => { - self.format_buffer - .reset(self.colors.gray, self.colors.green); - self.format_buffer.push_control(self.colors.green); - message - } - BarResult::Interrupted(message) => { - self.format_buffer - .reset(self.colors.gray, self.colors.yellow); - self.format_buffer.push_control(self.colors.yellow); - message - } - }; - self.format_buffer.push_control("\u{283f}]"); - if !message.starts_with('[') { - self.format_buffer.push_control(" "); - } - self.format_buffer.push_str(message); - self.format_buffer.end(); + self.format_buffer.push_lf(); self.print_format_buffer(); } - fn print_format_buffer(&mut self) { if !self.print_task.active() { use std::io::Write; @@ -341,60 +297,6 @@ impl Printer { None } } - // pub(crate) fn take_prompt_task_if_should_join(&mut self) -> Option> { - // if self.prompt_task.needs_join { - // return self.prompt_task.take(); - // } - // - // if self.pending_prompts.is_empty() { - // self.prompt_task.take() - // } else { - // None - // } - // } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum Target { - /// Print to Stdout - Stdout, - /// Print to Stderr - Stderr, -} -#[derive(Default)] -struct PrintingThread { - needs_join: bool, - /// Handle for the printing task, None means - /// either no printing task is running, or, the printing - /// task is terminating - handle: Option>, -} -impl PrintingThread { - /// Take the handle for joining - fn take(&mut self) -> Option> { - self.needs_join = false; - self.handle.take() - } - /// Mark the task as will end, so it can be joined - fn mark_join(&mut self) { - self.needs_join = true; - } - /// If the task is active - fn active(&self) -> bool { - !self.needs_join && self.handle.is_some() - } - /// Blockingly join the task on the current thread - fn join(&mut self) { - self.needs_join = false; - if let Some(handle) = self.handle.take() { - let _: Result<_, _> = handle.join(); - } - } - /// Assign a new handle - fn assign(&mut self, handle: JoinHandle<()>) { - self.needs_join = false; - self.handle = Some(handle); - } } /// Printing thread that handles progress bar animation and printing during progress bar display @@ -415,7 +317,7 @@ fn print_task() -> JoinHandle<()> { // if drop() is working to prevent holding the lock during sleep #[inline(always)] fn print_loop( - tick: u32, + tick: Tick, buffer: &mut String, temp: &mut String, lines: &mut i32, @@ -445,6 +347,9 @@ fn print_task() -> JoinHandle<()> { let Ok(mut printer) = PRINTER.lock() else { return ControlFlow::Break(()); }; + let Some(printer) = printer.as_mut() else { + return ControlFlow::Break(()); + }; let task = printer.pending_prompts.pop_front(); let is_stdin_terminal = printer.is_stdin_terminal; if let Some(mut task) = task { @@ -472,7 +377,7 @@ fn print_task() -> JoinHandle<()> { // process this prompt #[cfg(feature = "prompt-password")] let (result, is_password) = if task.is_password { - (super::prompt_password::read_password(), true) + (password::read_password(), true) } else { (read_plaintext(temp), false) }; @@ -480,7 +385,7 @@ fn print_task() -> JoinHandle<()> { let (result, is_password) = (read_plaintext(temp), false); // clear sensitive information in the memory - super::zero_string(temp); + crate::zero_string(temp); // now, re-print the prompt text to the buffer without the prompt prefix if !is_password { while !task.prompt.ends_with('\n') { @@ -490,8 +395,10 @@ fn print_task() -> JoinHandle<()> { } // add the prompt to the print buffer if let Ok(mut printer) = PRINTER.lock() { - printer.buffered.push_str(&task.prompt); - printer.buffered.push('\n'); + if let Some(printer) = printer.as_mut() { + printer.buffered.push_str(&task.prompt); + printer.buffered.push('\n'); + } } // send the result of the prompt let _ = task.send.send(result); @@ -506,12 +413,15 @@ fn print_task() -> JoinHandle<()> { let Ok(mut printer) = PRINTER.lock() else { return ControlFlow::Break(()); }; + let Some(printer) = printer.as_mut() else { + return ControlFlow::Break(()); + }; if let Some(bar_target) = printer.bar_target { // print the bars, after processing buffered messages // remeasure terminal width on every cycle - let width = super::term_width_or_max(); + let width = fmt::term_width_or_max(); if bar_target == Target::Stdout { // add the buffered messages @@ -565,7 +475,7 @@ fn print_task() -> JoinHandle<()> { // nothing else to do, mark the task done, // so the printer knows to join this thread (after we drop the lock guard) // whenever someone calls, instead of posting to this thread - on_task_end(&mut printer); + on_task_end(printer); // we know the printer buffer is empty // because we just printed all of it while having // the lock on the printer, no need to print again @@ -595,57 +505,61 @@ fn print_task() -> JoinHandle<()> { }) } -// // note that for interactive io, it's recommended to use blocking io directly -// // on a thread instead of tokio -// fn prompt_task( -// first_send: oneshot::Sender>, -// _is_password: bool, -// ) -> JoinHandle<()> { -// use std::io::Write as _; -// let mut stdout = std::io::stdout(); -// std::thread::spawn(move || { -// let mut send = first_send; -// let mut _is_password = _is_password; -// let mut buf = String::new(); -// loop { -// -// #[cfg(feature = "prompt-password")] -// let result = if _is_password { -// super::prompt_password::read_password() -// } else { -// read_plaintext(&mut buf) -// }; -// #[cfg(not(feature = "prompt-password"))] -// let result = read_plaintext(&mut buf); -// -// let _ = send.send(result); -// let Ok(mut printer) = super::PRINTER.lock() else { -// break; -// }; -// let Some(next) = printer.pending_prompts.pop_front() else { -// printer.prompt_task.mark_join(); -// break; -// }; -// let _ = write!( -// stdout, -// "{}{}{}", -// printer.controls.move_to_begin_and_clear, printer.buffered, next.prompt -// ); -// printer.buffered.clear(); -// let _ = stdout.flush(); -// send = next.send; -// -// #[cfg(feature = "prompt-password")] -// { -// _is_password = next.is_password; -// } -// } -// }) -// } - -fn read_plaintext(buf: &mut String) -> std::io::Result { +fn read_plaintext(buf: &mut String) -> io::Result { buf.clear(); - std::io::stdin() + io::stdin() .read_line(buf) .map(|_| buf.trim().to_string().into()) } + +struct PromptTask { + send: OnceSend>, + prompt: String, + #[cfg(feature = "prompt-password")] + is_password: bool, +} + +/// For synchornizing with the printer +#[derive(Default)] +struct PrintingThread { + needs_join: bool, + /// Handle for the printing task, None means + /// either no printing task is running, or, the printing + /// task is terminating + handle: Option>, +} +impl PrintingThread { + /// Take the handle for joining + fn take(&mut self) -> Option> { + self.needs_join = false; + self.handle.take() + } + /// Mark the task as will end, so it can be joined + fn mark_join(&mut self) { + self.needs_join = true; + } + /// If the task is active + fn active(&self) -> bool { + !self.needs_join && self.handle.is_some() + } + /// Blockingly join the task on the current thread + fn join(&mut self) { + self.needs_join = false; + if let Some(handle) = self.handle.take() { + let _: Result<_, _> = handle.join(); + } + } + /// Assign a new handle + fn assign(&mut self, handle: JoinHandle<()>) { + self.needs_join = false; + self.handle = Some(handle); + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Target { + /// Print to Stdout + Stdout, + /// Print to Stderr + Stderr, +} diff --git a/packages/copper/src/cli/progress/builder.rs b/packages/copper/src/cli/progress/builder.rs new file mode 100644 index 0000000..cad2dc3 --- /dev/null +++ b/packages/copper/src/cli/progress/builder.rs @@ -0,0 +1,216 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; + +use crate::cli::progress::{StateImmut, ProgressBar, Estimater, State}; + +/// Builder for a progress bar +#[derive(Debug, Clone)] // Clone sometimes needed to build by ref.. without unsafe +pub struct ProgressBarBuilder { + /// The message prefix for the progress bar + message: String, + /// Total steps (None = unbounded, 0 = not known yet) + total: Option, + /// The progress bar is for displaying bytes + total_is_in_bytes: bool, + /// If the bar should be kept after it's done + keep: Option, + /// If ETA should be visible (only effective if total is finite) + show_eta: bool, + /// If percentage should be visible (only effective if total is finite) + show_percentage: bool, + /// Message to display after done, instead of the default + done_message: Option, + /// Message to display if the bar is interrupted + interrupted_message: Option, + /// Maximum number of children to display at a time + max_display_children: usize, + /// Optional parent of the bar + parent: Option>, +} + +impl ProgressBarBuilder { + /// Start building a progress bar. Note [`cu::progress`](progress) is the canonical shorthand + pub fn new(message: String) -> Self { + Self { + message, + total: None, + total_is_in_bytes: false, + keep: None, + show_eta: true, + show_percentage: true, + done_message: None, + interrupted_message: None, + max_display_children: usize::MAX / 2, + parent: None, + } + } + /// Set the total steps. `0` means total is unknown, which can be set + /// at a later point. + /// + /// By default, the progress bar is "unbounded", meaning there is no + /// individual steps + /// + /// ```rust + /// # use pistonite_cu as cu; + /// cu::progress("doing something").total(10); + /// ``` + #[inline(always)] + pub fn total(mut self, total: usize) -> Self { + self.total = Some(total as u64); + self + } + + /// Set the total as a `u64` on platforms where `usize` is less than 64 bits + #[cfg(not(target_pointer_width = "64"))] + pub fn total_u64(mut self, total: u64) -> Self { + self.total = Some(total); + self + } + + /// Set the total bytes and set the progress to be displayed using byte units (SI). + /// `0` means total is unknown, which can be set at a later point. + /// + /// ```rust + /// # use pistonite_cu as cu; + /// cu::progress("doing something").total_bytes(1000000); + /// ``` + #[inline(always)] + pub fn total_bytes(mut self, total: u64) -> Self { + self.total = Some(total as u64); + self.total_is_in_bytes = true; + self + } + + /// Set if the progress bar should be kept in the output + /// after it's done. + /// + /// Default is `true` for root progress bars and `false` + /// for child progress bars + /// + /// ```rust + /// # use pistonite_cu as cu; + /// cu::progress("doing something").keep(false); + /// ``` + #[inline(always)] + pub fn keep(mut self, keep: bool) -> Self { + self.keep = Some(keep); + self + } + + /// Set if ETA (estimated time) should be displayed. + /// Only effective if total is not zero (i.e. not unbounded). + /// Default is `true` + /// + /// ```rust + /// # use pistonite_cu as cu; + /// cu::progress("doing something").total(10).eta(false); + /// ``` + #[inline(always)] + pub fn eta(mut self, show: bool) -> Self { + self.show_eta = show; + self + } + + /// Set if percentage should be displayed. + /// Only effective if total is not zero (i.e. not unbounded). + /// Default is `true` + /// + /// ```rust + /// # use pistonite_cu as cu; + /// cu::progress("doing something").total(10).percentage(false); + /// ``` + #[inline(always)] + pub fn percentage(mut self, show: bool) -> Self { + self.show_percentage = show; + self + } + + /// Set a message to be displayed when the progress is done. + /// Requires `keep(true)` - which is the default, but + /// `when_done` will not automatically turn it on for you. + /// + /// Default is the message of the progress bar followed by `done`. + /// + /// ```rust + /// # use pistonite_cu as cu; + /// cu::progress("doing something").when_done("something is done!"); + /// ``` + #[inline(always)] + pub fn when_done(mut self, message: impl Into) -> Self { + self.done_message = Some(message.into()); + self + } + + /// Set a message to be displayed when the progress is interrupted. + /// + /// Default is the message of the progress bar followed by `interrupted`. + /// + /// ```rust + /// # use pistonite_cu as cu; + /// cu::progress("doing something").when_interrupt("something is interrupted!"); + /// ``` + #[inline(always)] + pub fn when_interrupt(mut self, message: impl Into) -> Self { + self.interrupted_message = Some(message.into()); + self + } + + /// Set the max number of children to display at a time. + /// Default is unbounded. + /// + /// ```rust + /// # use pistonite_cu as cu; + /// cu::progress("doing something").max_display_children(30); + /// ``` + pub fn max_display_children(mut self, num: usize) -> Self { + self.max_display_children = num; + self + } + + /// Set the parent progress bar. + /// + /// If the parent is known to be `Some`, use `parent.child(...)` instead + pub fn parent(mut self, parent: Option>) -> Self { + self.parent = parent; + self + } + + /// Build and start displaying the bar in the console + pub fn spawn(self) -> Arc { + let keep = self.keep.unwrap_or(self.parent.is_none()); + let done_message = if keep { + match self.done_message { + None => { + if self.message.is_empty() { + Some("done".to_string()) + } else { + Some(format!("{}: done", self.message)) + } + } + Some(x) => Some(x), + } + } else { + None + }; + let state_immut = StateImmut { + id: next_id(), + parent: self.parent.as_ref().map(Arc::clone), + prefix: self.message, + done_message, + interrupted_message: self.interrupted_message, + show_percentage: self.show_percentage, + unbounded: self.total.is_none(), + display_bytes: self.total_is_in_bytes, + max_display_children: self.max_display_children, + }; + let eta = self.show_eta.then(Estimater::new); + let state = State::new(self.total.unwrap_or(0), eta); + + ProgressBar::spawn(state_immut, state, self.parent) + } +} + +fn next_id() -> usize { + static ID: AtomicUsize = AtomicUsize::new(1); + ID.fetch_add(1, Ordering::SeqCst) +} diff --git a/packages/copper/src/print/progress/eta.rs b/packages/copper/src/cli/progress/eta.rs similarity index 98% rename from packages/copper/src/print/progress/eta.rs rename to packages/copper/src/cli/progress/eta.rs index 64f9883..def6b2f 100644 --- a/packages/copper/src/print/progress/eta.rs +++ b/packages/copper/src/cli/progress/eta.rs @@ -1,6 +1,6 @@ use std::time::Instant; -use crate::print::{TICK_INTERVAL, Tick}; +use crate::cli::{TICK_INTERVAL, Tick}; /// Estimate the time for progress bar #[derive(Debug)] diff --git a/packages/copper/src/cli/progress/macros.rs b/packages/copper/src/cli/progress/macros.rs new file mode 100644 index 0000000..19673c2 --- /dev/null +++ b/packages/copper/src/cli/progress/macros.rs @@ -0,0 +1,47 @@ + +/// Update a [progress bar](fn@crate::progress) +/// +/// The macro takes 2 parts separated by comma `,`: +/// - An expression for updating the progress: +/// - Optional format args for updating the message. +/// +/// The progress update expression can be one of: +/// - `bar = i`: set the progress to `i` +/// - `bar += i`: increment the steps by i +/// - `bar`: don't update the progress +/// +/// , where `bar` is an ident +/// +/// The format args can be omitted to update the progress without +/// updating the message +/// +/// # Examples +/// ```rust,no_run +/// # use pistonite_cu as cu; +/// let bar = cu::progress_bar(10, "10 steps"); +/// // update the current count and message +/// let i = 1; +/// cu::progress!(bar = i, "doing step {i}"); +/// // update the current count without changing message +/// cu::progress!(bar += 2); +/// // update the message without changing current step +/// cu::progress!(bar, "doing the thing"); +/// ``` +#[macro_export] +macro_rules! progress { + ($bar:ident, $($fmt_args:tt)*) => { + $bar.__inc(0u64, Some(format!($($fmt_args)*))) + }; + ($bar:ident += $inc:expr) => { + $bar.__inc({ $inc } as u64, None) + }; + ($bar:ident += $inc:expr, $($fmt_args:tt)*) => { + $bar.__inc({ $inc } as u64, Some(format!($($fmt_args)*))) + }; + ($bar:ident = $x:expr) => { + $bar.__set({ $x } as u64, None) + }; + ($bar:ident = $x:expr, $($fmt_args:tt)*) => { + $bar.__set({ $x } as u64, Some(format!($($fmt_args)*))) + }; +} diff --git a/packages/copper/src/cli/progress/mod.rs b/packages/copper/src/cli/progress/mod.rs new file mode 100644 index 0000000..c109fbd --- /dev/null +++ b/packages/copper/src/cli/progress/mod.rs @@ -0,0 +1,151 @@ +/// # Progress Bars +/// Progress bars are a feature in the print system. It is aware of the printing/prompting going on +/// in the console and will keep the bars at the bottom of the console without interferring +/// with the other outputs. +/// +/// ## Components +/// A bar has the following display components +/// - Step display: Displays the current and total steps. For example, `[42/100]`. Will not display +/// for bars that are unbounded. Bars that are not unbounded but the total is not set +/// will show total as `?`. The step display can also be configured to a style more suitable +/// for displaying bytes (for example downloading or processing file), like `10.0K / 97.3M` +/// - Prefix: A string configured once when launching the progress bar +/// - Percentage: Percentage display for the current and total steps, For example `42.00%`. +/// This can be turned off if not needed +/// - ETA: Estimated remaining time. This can be turned off if not needed +/// - Message: A message that can be set while the progress bar is showing. For example, +/// this can be the name of the current file being processed, etc. +/// +/// With everything displayed, it will look something like this: +/// ```text +/// X][42/100] prefix: 42.00% ETA 32.35s processing the 42th item +/// ``` +/// (`X`) is where the animated spinner is +/// +/// ## Progress Tree +/// You can display progress bars with a hierarchy if desired. The progress bars +/// will be organized as an directed acyclic graph (i.e. a tree). Special characters +/// will be used to draw the tree in the terminal. +/// +/// Each progress bar holds a strong ref to its parent, and weak refs to all of its children. +/// The printer keeps weak refs to all root progress bars (i.e. one without a parent). +/// +/// ## State and Output +/// Each progress bar can have 3 states: `progress`, `done`, and `interrupted`. +/// +/// When in `progress`, the bar will be animated if the output is a terminal. Otherwise, +/// updates will be ignored. +/// +/// The bar will be `done` when all handles are dropped if 1 of the following is true: +/// - The bar has finite total, and current step equals total step +/// - The bar is unbounded, and `.done()` is called on any handle +/// +/// If neither is true when all handles are dropped, the bar becomes `interrupted`. +/// This makes the bar easier to use with control flows. When the bar is in this state, +/// it will print an interrupted message to the regular print stream, like +/// ```text +/// X][42/100] prefix: interrupted +/// ``` +/// This message is customizable when building the progress bar. All of its children +/// that are interrupted will also be printed. All children that are `done` will only be +/// printed if `keep` is true for that children (see below). The interrupted message is printed +/// regardless if the output is terminal or not. +/// +/// When the progress bar is done, it may print a "done message" depending on +/// if it has a parent and the `keep` option: +/// | Has parent (i.e. is child) | Keep | Behavior | +/// |-|-|-| +/// | Yes | Yes | Done message will be displayed under the parent, but the bar will disappear completely when the parent is done | +/// | Yes | No | The bar will disappear after it's done | +/// | No | Yes | The bar will print a done message to the regular print stream when done, no children will be printed | +/// | No | No | The bar will disappear after done, no children will be printed | +/// +/// The done message is also customizable when building the bar. Note (from the table) that it will +/// be effective in some way if the `keep` option is true. Setting a done message +/// does not automatically set `keep` to true. +/// +/// The default done message is something like below, will be displayed in green. +/// ```text +/// X][100/100] prefix: done +/// ``` +/// +/// ## Updating the bar +/// The [`progress`](macro@crate::progress) macro is used to update the progress bar. +/// For example: +/// +/// ```rust +/// # use pistonite_cu as cu; +/// let bar = cu::progress("doing something").total(10).spawn(); +/// for i in 0..10 { +/// cu::progress!(bar = i, "doing {i}th step"); +/// } +/// drop(bar); +/// ``` +/// +/// ## Building the bar +/// This function `cu::progress` will make a [`ProgressBarBuilder`] +/// with these default configs: +/// - Total steps: unbounded +/// - Keep after done: `true` +/// - Show ETA: `true` (only effective if steps is finite) +/// - Finish message: Default +/// - Interrupted message: Default +/// +/// See [`ProgressBarBuilder`] for builder methods +/// +/// ## Print Levels +/// The bar final messages are suppressed at `-q` and the bar animations are suppressed at `-qq` +/// +/// ## Other considerations +/// If the progress bar print section exceeds the terminal height, +/// it will probably not render properly. Keep in mind when you +/// are displaying a large number of progress bars. +/// +/// You can use `.max_display_children()` to set the maximum number of children +/// to display at a time. However, there is no limit on the number of root progress bars. +#[inline(always)] +pub fn progress(message: impl Into) -> ProgressBarBuilder { + ProgressBarBuilder::new(message.into()) +} + +mod eta; +pub use eta::Estimater; +mod state; +pub use state::ProgressBar; +use state::{State, StateImmut}; +mod builder; +pub use builder::ProgressBarBuilder; +mod util; +pub use util::{BarResult, BarFormatter}; +use util::{ChildState, ChildStateStrong}; +mod macros; + +// spawn_iter stuff, keep for reference, not sure if needed yet +// .enumerate seems more readable +/* +/// In the example above, you can also attach it to an iterator directly. +/// The builder will call `size_hint()` once and set the total on the bar, +/// and will automatically mark it as done if `next()` returns `None`. +/// +/// If the default iteration behavior of `spawn_iter` is not desirable, use `spawn` +/// and iterate manually. +/// ```rust +/// # use pistonite_cu as cu; +/// for i in cu::progress("doing something").spawn_iter(0..10) { +/// cu::print!("doing {i}th step"); +/// } +/// ``` +/// +/// Note that in the code above, we didn't have a handle to the bar directly +/// to update the message, we can fix that by getting the bar from the iter +/// +/// ```rust +/// # use pistonite_cu as cu; +/// let mut iter = cu::progress("doing something").spawn_iter(0..10); +/// let bar = iter.bar(); +/// for i in iter { +/// // bar = i is handled by the iterator automatically +/// cu::progress!(bar, "doing {i}th step"); +/// } +/// ``` +*/ diff --git a/packages/copper/src/print/progress/state.rs b/packages/copper/src/cli/progress/state.rs similarity index 73% rename from packages/copper/src/print/progress/state.rs rename to packages/copper/src/cli/progress/state.rs index 1e468b9..f4ffb51 100644 --- a/packages/copper/src/print/progress/state.rs +++ b/packages/copper/src/cli/progress/state.rs @@ -1,13 +1,160 @@ -use std::sync::{Arc, Weak}; +use std::sync::{Arc, Mutex}; use std::time::Instant; -use crate::print::progress::bar::ProgressBar; -use crate::print::progress::eta::Estimater; -use crate::print::{Tick, ansi}; +use crate::cli::Tick; +use crate::cli::progress::{ChildState,Estimater,BarFormatter, BarResult, ProgressBarBuilder, ChildStateStrong}; +use crate::cli::fmt::ansi; +use crate::cli::printer::PRINTER; -const CHAR_BAR_TICK: char = '\u{251C}'; -const CHAR_BAR: char = '\u{2502}'; -const CHAR_TICK: char = '\u{2514}'; +const CHAR_BAR_TICK: char = '\u{251C}'; // |> +const CHAR_BAR: char = '\u{2502}'; // | +const CHAR_TICK: char = '\u{2514}'; // > + +/// Handle for a progress bar (This is the internal state, the handle is `Arc`) +/// +/// See [Progress Bars](fn@crate::progress) +#[derive(Debug)] +pub struct ProgressBar { + pub(crate) state: StateImmut, + state_mut: Mutex, +} +impl ProgressBar { + pub(crate) fn spawn(state: StateImmut, state_mut: State, parent: Option>) -> Arc { + let bar = Arc::new(Self { + state, state_mut: Mutex::new(state_mut) + }); + match parent { + Some(p) => { + if let Ok(mut p) = p.state_mut.lock() { + p.add_child(&bar); + } + } + None => { + if let Ok(mut printer) = PRINTER.lock() { + if let Some(printer) = printer.as_mut() { + printer.add_progress_bar(&bar); + } + } + } + } + bar + } + #[doc(hidden)] + #[inline(always)] + pub fn __set(self: &Arc, current: u64, message: Option) { + if let Ok(mut bar) = self.state_mut.lock() { + bar.unreal_current = current; + if let Some(x) = message { + bar.set_message(&x); + } + } + } + + #[doc(hidden)] + #[inline(always)] + pub fn __inc(self: &Arc, amount: u64, message: Option) { + if let Ok(mut bar) = self.state_mut.lock() { + bar.unreal_current.saturating_add(amount); + if let Some(x) = message { + bar.set_message(&x); + } + } + } + + /// Set the total steps (if the progress is finite) + pub fn set_total(&self, total: u64) { + if total == 0 { + // 0 is a special value, so we do not allow setting it + return; + } + if let Ok(mut bar) = self.state_mut.lock() { + bar.unreal_total = total; + } + } + + /// Start building a child progress bar + /// + /// Note that the child builder will keep this bar alive (displayed), even + /// if the child is not spawned + #[inline(always)] + pub fn child(self: &Arc, message: impl Into) -> ProgressBarBuilder { + ProgressBarBuilder::new(message.into()).parent(Some(Arc::clone(self))) + } + + /// Mark the progress bar as done and drop the handle. + /// + /// This needs to be called if the bar is unbounded. Otherwise, + /// the bar will display in the interrupted state when dropped. + /// + /// If the progress is finite, then interrupted state is automatically + /// determined (`current != total`) + pub fn done(self: Arc) { + if self.state.unbounded { + if let Ok(mut bar) = self.state_mut.lock() { + bar.unreal_current = 1; + bar.unreal_total = 1; + } + } + } + + /// Same as [`done`](Self::done), but does not drop the bar. + pub fn done_by_ref(&self) { + if self.state.unbounded { + if let Ok(mut bar) = self.state_mut.lock() { + bar.unreal_current = 1; + bar.unreal_total = 1; + } + } + } + + /// Format the bar + #[inline(always)] + pub(crate) fn format(&self, fmt: &mut BarFormatter<'_, '_, '_>) -> i32 { + self.format_at_depth(0, &mut String::new(), fmt) + } + + /// Format the bar at depth + fn format_at_depth( + &self, + depth: usize, + hierarchy: &mut String, + fmt: &mut BarFormatter<'_, '_, '_>, + ) -> i32 { + let Ok(mut bar) = self.state_mut.lock() else { + return 0; + }; + bar.format_at_depth(depth, hierarchy, fmt, &self.state) + } +} + +impl Drop for ProgressBar { + fn drop(&mut self) { + let result = match self.state_mut.lock() { + Err(_) => BarResult::DontKeep, + Ok(bar) => bar.check_result(&self.state), + }; + if let Some(parent) = &self.state.parent { + // inform parent our result + if let Ok(mut parent_state) = parent.state_mut.lock() { + parent_state.child_done(self.state.id, result.clone()); + } + } + let handle = { + // scope for printer lock + let Ok(mut printer) = PRINTER.lock() else { + return; + }; + let Some(printer) = printer.as_mut() else { + return; + }; + printer.print_bar_done(&result, self.state.parent.is_none()); + printer.take_print_task_if_should_join() + }; + if let Some(x) = handle { + let _: Result<(), _> = x.join(); + } + } +} /// Internal, immutable state of progress bar #[derive(Debug)] @@ -85,20 +232,6 @@ impl State { } } - pub fn set_current(&mut self, current: u64) { - self.unreal_current = current; - } - - pub fn inc_current(&mut self, current: u64) { - self.unreal_current += current; - } - - pub fn set_total(&mut self, total: u64) { - if total != 0 { - self.unreal_total = total; - } - } - pub fn add_child(&mut self, child: &Arc) { self.children .push(ChildState::Progress(child.state.id, Arc::downgrade(child))) @@ -253,6 +386,7 @@ impl State { out.push(' '); } out.push(CHAR_TICK); + out.push_str(fmt.colors.reset); use std::fmt::Write as _; let _ = write!( out, @@ -373,8 +507,8 @@ impl State { temp.clear(); // _: fmt for string does not fail let _ = match total { - None => write!(temp, "{}", ByteFormat(current)), - Some(total) => write!(temp, "{} / {}", ByteFormat(current), ByteFormat(total)), + None => write!(temp, "{}", cu::ByteFormat(current)), + Some(total) => write!(temp, "{} / {}", cu::ByteFormat(current), cu::ByteFormat(total)), }; if width >= temp.len() { @@ -442,15 +576,15 @@ impl State { format!("[{current}/?] {message}") } (None, true) => { - let current = ByteFormat(current); + let current = cu::ByteFormat(current); format!("{message} ({current})") } (Some(total), false) => { format!("[{current}/{total}] {message}") } (Some(total), true) => { - let current = ByteFormat(current); - let total = ByteFormat(total); + let current = cu::ByteFormat(current); + let total = cu::ByteFormat(total); format!("{message} ({current} / {total})") } } @@ -467,67 +601,4 @@ fn format_message_with_width(out: &mut String, mut width: usize, message: &str) } width } -#[derive(Debug)] -enum ChildState { - /// The done message (if `keep` is true) - Done(String), - /// The interrupted message - Interrupted(String), - /// Still running - Progress(usize, Weak), -} -impl ChildState { - fn upgrade(&self) -> Option> { - Some(match self { - ChildState::Done(x) => ChildStateStrong::Done(x), - ChildState::Interrupted(x) => ChildStateStrong::Interrupted(x), - ChildState::Progress(_, weak) => ChildStateStrong::Progress(weak.upgrade()?), - }) - } -} -enum ChildStateStrong<'a> { - Done(&'a str), - Interrupted(&'a str), - Progress(Arc), -} - -#[derive(Default, Clone)] -pub enum BarResult { - /// Bar is done and don't keep it - #[default] - DontKeep, - /// Bar is done, with a message to keep - Done(String), - /// Bar is interrupted - Interrupted(String), -} - -pub struct BarFormatter<'a, 'b, 'c> { - pub colors: ansi::Colors, - pub bar_color: &'a str, - pub width: usize, - pub tick: Tick, - pub now: &'c mut Option, - pub out: &'b mut String, - pub temp: &'b mut String, -} - -struct ByteFormat(u64); -impl std::fmt::Display for ByteFormat { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for (unit_bytes, unit_char) in [ - (1000_000_000_000, 'T'), - (1000_000_000, 'G'), - (1000_000, 'M'), - (1000, 'k'), - ] { - if self.0 >= unit_bytes { - let whole = self.0 / unit_bytes; - let deci = (self.0 % unit_bytes) * 10 / unit_bytes; - return write!(f, "{whole}.{deci}{unit_char}"); - } - } - write!(f, "{}B", self.0) - } -} diff --git a/packages/copper/src/cli/progress/util.rs b/packages/copper/src/cli/progress/util.rs new file mode 100644 index 0000000..faa9c68 --- /dev/null +++ b/packages/copper/src/cli/progress/util.rs @@ -0,0 +1,53 @@ +use std::sync::{Arc, Weak}; +use std::time::Instant; + +use crate::cli::Tick; +use crate::cli::fmt::ansi; +use crate::cli::progress::ProgressBar; + + +#[derive(Debug)] +pub enum ChildState { + /// The done message (if `keep` is true) + Done(String), + /// The interrupted message + Interrupted(String), + /// Still running + Progress(usize, Weak), +} +impl ChildState { + pub fn upgrade(&self) -> Option> { + Some(match self { + ChildState::Done(x) => ChildStateStrong::Done(x), + ChildState::Interrupted(x) => ChildStateStrong::Interrupted(x), + ChildState::Progress(_, weak) => ChildStateStrong::Progress(weak.upgrade()?), + }) + } +} + +pub enum ChildStateStrong<'a> { + Done(&'a str), + Interrupted(&'a str), + Progress(Arc), +} + +#[derive(Default, Clone)] +pub enum BarResult { + /// Bar is done and don't keep it + #[default] + DontKeep, + /// Bar is done, with a message to keep + Done(String), + /// Bar is interrupted + Interrupted(String), +} + +pub struct BarFormatter<'a, 'b, 'c> { + pub colors: ansi::Colors, + pub bar_color: &'a str, + pub width: usize, + pub tick: Tick, + pub now: &'c mut Option, + pub out: &'b mut String, + pub temp: &'b mut String, +} diff --git a/packages/copper/src/print/prompt.rs b/packages/copper/src/cli/prompt.rs similarity index 56% rename from packages/copper/src/print/prompt.rs rename to packages/copper/src/cli/prompt.rs index 1c1892a..58496e0 100644 --- a/packages/copper/src/print/prompt.rs +++ b/packages/copper/src/cli/prompt.rs @@ -1,60 +1,12 @@ -use crate::{Atomic, Context as _}; +use crate::{Atomic, Context as _}; use crate::lv; -/// Show a prompt -/// -/// Use the `prompt-password` feature and [`prompt_password!`](crate::prompt_password) macro -/// if prompting for a password, which will hide user's input from the console -/// -/// ```rust,ignore -/// let name = cu::prompt!("please enter your name")?; -/// cu::info!("user entered: {name}"); -/// ``` -#[cfg(feature = "prompt")] -#[macro_export] -macro_rules! prompt { - ($($fmt_args:tt)*) => {{ - $crate::__priv::__prompt(format_args!($($fmt_args)*), false).map(|x| x.to_string()) - }} -} - -/// Show a Yes/No prompt -/// -/// Return `true` if the answer is Yes. Return an error if prompt is not allowed -/// ```rust,ignore -/// if cu::yesno!("do you want to continue?")? { -/// cu::info!("user picked yes"); -/// } -/// ``` -#[cfg(feature = "prompt")] -#[macro_export] -macro_rules! yesno { - ($($fmt_args:tt)*) => {{ - $crate::__priv::__prompt_yesno(format_args!($($fmt_args)*)) - }} -} - - -/// Show a password prompt -/// -/// The console will have inputs hidden while user types, and the returned -/// value is a [`ZeroWhenDropString`](crate::ZeroWhenDropString) -/// -/// ```rust,ignore -/// let password = cu::prompt_password!("please enter your password")?; -/// cu::info!("user entered: {password}"); -/// ``` -#[cfg(feature = "prompt-password")] -#[macro_export] -macro_rules! prompt_password { - ($($fmt_args:tt)*) => {{ - $crate::__priv::__prompt(format_args!($($fmt_args)*), true) - }} -} +use crate::cli::printer::PRINTER; pub(crate) static PROMPT_LEVEL: Atomic = Atomic::new_u8(lv::Prompt::Interactive as u8); +#[doc(hidden)] pub fn __prompt_yesno( message: std::fmt::Arguments<'_>, ) -> crate::Result { @@ -83,10 +35,11 @@ pub fn __prompt_yesno( Ok(answer) } +#[doc(hidden)] pub fn __prompt( message: std::fmt::Arguments<'_>, is_password: bool, -) -> crate::Result { +) -> cu::Result { check_prompt_level(false)?; prompt_impl(&format!("{message}"), is_password) } @@ -95,7 +48,7 @@ fn prompt_with_validation_impl crate::Result>( message: std::fmt::Arguments<'_>, is_password: bool, mut validator: F -) -> crate::Result { +) -> cu::Result { let message = format!("{message}"); loop { let mut result = prompt_impl(&message, is_password)?; @@ -108,12 +61,13 @@ fn prompt_with_validation_impl crate::Result>( fn prompt_impl( message: &str, is_password: bool, -) -> crate::Result { +) -> cu::Result { let recv = { - let Ok(mut printer) = super::PRINTER.lock() else { - crate::bail!("prompt failed: global print lock poisoned"); - }; - printer.show_prompt(message, is_password) + if let Ok(mut printer) = PRINTER.lock() && let Some(printer) = printer.as_mut() { + printer.show_prompt(message, is_password) + } else { + crate::bail!("prompt failed: failed to lock global printer"); + } }; let result = crate::check!(recv.recv(), "error while showing prompt")?; crate::check!(result, "io error while showing prompt") diff --git a/packages/copper/src/cli/thread_name.rs b/packages/copper/src/cli/thread_name.rs new file mode 100644 index 0000000..d74b6af --- /dev/null +++ b/packages/copper/src/cli/thread_name.rs @@ -0,0 +1,11 @@ +use std::cell::RefCell; + +thread_local! { + pub(crate) static THREAD_NAME: RefCell> = const { RefCell::new(None) }; +} + +/// Set the name to show up in messages printed by the current thread +#[inline(always)] +pub fn set_thread_name(name: impl Into) { + THREAD_NAME.with_borrow_mut(|x| *x = Some(name.into())) +} diff --git a/packages/copper/src/lib.rs b/packages/copper/src/lib.rs index abbcf23..7d9da5e 100644 --- a/packages/copper/src/lib.rs +++ b/packages/copper/src/lib.rs @@ -59,10 +59,12 @@ //! //! # Quick Reference //! - [Error Handling](macro@crate::check) (via [`anyhow`](https://docs.rs/anyhow)) +//! - [Logging](mod@crate::lv) (via [`log`](https://docs.rs/log)) +//! - [Printting and Command Line Interface](mod@crate::cli) (CLI arg parsing via +//! [`clap`](https://docs.rs/clap)) +//! - [Progress Bars](fn@crate::progress) //! //! # Feature Reference: -//! - `cli`, `print`, `prompt`: -//! See [`cli`](module@cli). Note that logging is still available without any feature flag. //! - `coroutine` and `coroutine-heavy`: //! Enables `async` and integration with `tokio`. See [`cu::co`](module@co). //! - `fs`: Enables file system utils. See [`cu::fs`](module@fs) and [`cu::bin`](module@bin). @@ -76,10 +78,36 @@ // for macros extern crate self as cu; -// --- Error Handling (does not require any feature flag) --- +// --- Basic stuff (no feature needed) --- +pub mod str; +pub use str::*; +mod env_var; +pub use env_var::*; +mod atomic; // Atomic helpers +pub use atomic::*; +mod misc; // other stuff that doesn't have a place +pub use misc::*; + +// --- Error Handling (no feature needed) --- mod error_handling; pub use error_handling::*; +// --- Logging (no feature needed) --- +pub mod lv; +pub use lv::{debug, error, info, trace, warn}; + +// --- Command Line Interface (print/cli/prompt/prompt-password feature) --- +#[cfg(feature = "print")] +pub mod cli; +#[cfg(feature = "print")] +pub use cli::{progress, ProgressBar, ProgressBarBuilder}; +#[cfg(feature = "prompt-password")] +pub use cli::password_chars_legal; +#[cfg(feature = "cli")] +pub use pistonite_cu_proc_macros::cli; + + + #[cfg(feature = "process")] mod process; #[cfg(feature = "process")] @@ -97,17 +125,6 @@ pub use bin::which; #[cfg(feature = "fs")] pub mod fs; -/// Path utils -#[cfg(feature = "fs")] -mod path; -#[cfg(feature = "fs")] -pub use path::{PathExtension, PathExtensionOwned}; - -#[cfg(feature = "cli")] -pub mod cli; -#[cfg(feature = "cli")] -pub use pistonite_cu_proc_macros::cli; - #[cfg(feature = "coroutine")] mod async_; /// Alias for a boxed future @@ -115,21 +132,6 @@ pub type BoxedFuture = std::pin::Pin + Send + 'sta #[cfg(feature = "coroutine")] pub mod co; -/// Low level printing utils and integration with log and clap -#[cfg(feature = "print")] -mod print; -// #[cfg(feature = "prompt-password")] -// pub use print::check_password_legality; -#[cfg(feature = "print")] -pub use print::{ - ProgressBar, ProgressBarBuilder, ZeroWhenDropString, init_print_options, log_init, progress, - set_thread_print_name, term_width, term_width_height, term_width_or_max, -}; - -/// Printing level values -pub mod lv; -#[doc(inline)] -pub use lv::{color_enabled, disable_print_time, disable_trace_hint, log_enabled}; /// Parsing utilities #[cfg(feature = "parse")] @@ -138,27 +140,14 @@ mod parse; pub use parse::*; #[cfg(feature = "parse")] pub use pistonite_cu_proc_macros::Parse; -mod env_var; -pub use env_var::*; - -// Atomic helpers -mod atomic; -pub use atomic::*; - -// other stuff that doesn't have a place -mod misc; -pub use misc::*; // re-exports from libraries -pub use log::{debug, error, info, trace, warn}; pub use pistonite_cu_proc_macros::error_ctx; #[cfg(feature = "coroutine")] pub use tokio::{join, try_join, select}; #[doc(hidden)] pub mod __priv { - #[cfg(feature = "print")] - pub use crate::print::{__print_with_level, __prompt, __prompt_yesno}; #[cfg(feature = "process")] pub use crate::process::__ConfigFn; } @@ -174,6 +163,10 @@ pub mod lib { /// Prelude imports pub mod pre { pub use crate::Context as _; + + #[cfg(feature = "cli")] + pub use crate::lib::clap; + #[cfg(feature = "parse")] pub use crate::ParseTo as _; #[cfg(feature = "fs")] @@ -184,8 +177,6 @@ pub mod pre { pub use crate::Spawn as _; #[cfg(feature = "json")] pub use crate::json; - #[cfg(feature = "cli")] - pub use crate::lib::clap; #[cfg(feature = "toml")] pub use crate::toml; #[cfg(feature = "yaml")] diff --git a/packages/copper/src/lv.rs b/packages/copper/src/lv.rs index 3e176e1..52a05d4 100644 --- a/packages/copper/src/lv.rs +++ b/packages/copper/src/lv.rs @@ -1,16 +1,47 @@ -use std::sync::atomic::{AtomicBool, Ordering}; +//! # Logging +//! +//! *Does not require any feature flag +//! +//! The logging macros (`debug`, `info`, `trace`, `warn`, `error`) are +//! re-exported from the [`log`](https://docs.rs/log) crate and are +//! used as `cu::debug!`, `cu::info!`, etc. This means your log statements +//! are integrated into the log infrastructure when you use `cu` in a library. +//! +//! Additionally, the [`cu::fmtand!`](macro@crate::fmtand) and +//! [`cu::panicand!`](macro@crate::panicand) macros allow you +//! to log a message in additional to `format!`/`panic!`. +//! +//! When the `cli` feature is enabled, you also get log integration +//! with CLI flags and other terminal-printing features. +//! See [Command Line Interface](mod@crate::cli) -use crate::{Atomic, lv}; +pub use log::{debug, error, info, trace, warn}; -pub(crate) static PRINT_LEVEL: Atomic = Atomic::new_u8(lv::Print::Normal as u8); -pub(crate) static USE_COLOR: AtomicBool = AtomicBool::new(true); +use std::sync::atomic::{AtomicBool, Ordering}; + +use cu::Atomic; -/// Check if the logging level is enabled -pub fn log_enabled(lv: lv::Lv) -> bool { - lv.can_print(PRINT_LEVEL.get()) +/// Format and invoke a print macro +/// +/// # Example +/// ```rust +/// # use pistonite_cu as cu; +/// let x = cu::fmtand!(error!("found {} errors", 3)); +/// assert_eq!(x, "found 3 errors"); +/// ``` +#[macro_export] +macro_rules! fmtand { + ($mac:ident !( $($fmt_args:tt)* )) => {{ + let s = format!($($fmt_args)*); + $crate::$mac!("{s}"); + s + }} } -/// Get if color printing is enabled +pub(crate) static PRINT_LEVEL: Atomic = Atomic::new_u8(Print::Normal as u8); +pub(crate) static USE_COLOR: AtomicBool = AtomicBool::new(true); + +/// Get if color printing is enabled **Only works when cu::cli is being used**. pub fn color_enabled() -> bool { USE_COLOR.load(Ordering::Acquire) } @@ -22,11 +53,13 @@ static ENABLE_PRINT_TIME: AtomicBool = AtomicBool::new(true); /// /// By default, the hint is displayed if `RUST_BACKTRACE` env var is not set #[inline(always)] +#[cfg(feature = "print")] pub fn disable_trace_hint() { ENABLE_TRACE_HINT.store(false, Ordering::Release); } -/// Check if the "use -vv to display backtrace" will be printed on error +/// Check if the "use -vv to display backtrace" will be printed on error. +/// **Only works when cu::cli is being used** #[inline(always)] pub fn is_trace_hint_enabled() -> bool { ENABLE_TRACE_HINT.load(Ordering::Acquire) @@ -34,11 +67,13 @@ pub fn is_trace_hint_enabled() -> bool { /// Disable printing the time took to run the command #[inline(always)] +#[cfg(feature = "print")] pub fn disable_print_time() { ENABLE_PRINT_TIME.store(false, Ordering::Release); } -/// Check if the "finished in TIME" line will be printed on exit +/// Check if the "finished in TIME" line will be printed on exit. +/// **Only works when cu::cli is being used** #[inline(always)] pub fn is_print_time_enabled() -> bool { ENABLE_PRINT_TIME.load(Ordering::Acquire) @@ -205,7 +240,18 @@ pub enum Lv { Off, } impl Lv { - /// Check if the current print level can print this message level + /// Check if the logging level is currently enabled **Only works when cu::cli is being used**. + /// + /// ```rust + /// # use pistonite_cu as cu; + /// // check that INFO level is enabled + /// assert!(cu::lv::I.enabled()); + /// ``` + #[inline(always)] + pub fn enabled(self) -> bool { + self.can_print(PRINT_LEVEL.get()) + } + /// Check if a print level can print this message level pub fn can_print(self, level: Print) -> bool { match self { Lv::Off => false, diff --git a/packages/copper/src/print/init.rs b/packages/copper/src/print/init.rs deleted file mode 100644 index 5f8ed90..0000000 --- a/packages/copper/src/print/init.rs +++ /dev/null @@ -1,145 +0,0 @@ -use std::cell::RefCell; -use std::sync::OnceLock; -use std::sync::atomic::Ordering; - -use crate::lv::{self, Lv}; - -static LOG_FILTER: OnceLock = OnceLock::new(); -/// Set the global log filter -pub(crate) fn set_log_filter(filter: env_filter::Filter) { - let _ = LOG_FILTER.set(filter); -} - -/// Shorthand to quickly setup logging. Can be useful in tests. -/// -/// "qq", "q", "v" and "vv" inputs map to corresponding print levels. Other inputs -/// are mapped to default level -pub fn log_init(lv: &str) { - let level = match lv { - "qq" => lv::Print::QuietQuiet, - "q" => lv::Print::Quiet, - "v" => lv::Print::Verbose, - "vv" => lv::Print::VerboseVerbose, - _ => lv::Print::Normal, - }; - init_print_options(lv::Color::Auto, level, Some(lv::Prompt::Block)); -} - -/// Set global print options. This is usually called from clap args -/// -/// If prompt option is `None`, it will be `Interactive` unless env var `CI` is `true` or `1`, in which case it becomes `No`. -/// Prompt option is ignored unless `prompt` feature is enabled -pub fn init_print_options(color: lv::Color, level: lv::Print, prompt: Option) { - let log_level = if let Ok(value) = std::env::var("RUST_LOG") - && !value.is_empty() - { - let mut builder = env_filter::Builder::new(); - let filter = builder.parse(&value).build(); - let log_level = filter.filter(); - set_log_filter(filter); - log_level.max(level.into()) - } else { - level.into() - }; - log::set_max_level(log_level); - let use_color = color.is_colored_for_stdout(); - lv::USE_COLOR.store(use_color, Ordering::Release); - if let Ok(mut printer) = super::PRINTER.lock() { - printer.set_colors(use_color); - } - #[cfg(feature = "prompt")] - { - let prompt = match prompt { - Some(x) => x, - None => { - let is_ci = std::env::var("CI") - .map(|mut x| { - x.make_ascii_lowercase(); - matches!(x.trim(), "true" | "1") - }) - .unwrap_or_default(); - if is_ci { - lv::Prompt::Block - } else { - lv::Prompt::Interactive - } - } - }; - super::PROMPT_LEVEL.set(prompt) - } - #[cfg(not(feature = "prompt"))] - { - let _ = prompt; - super::PROMPT_LEVEL.set(lv::Prompt::No); - } - - lv::PRINT_LEVEL.set(level); - struct LogImpl; - impl log::Log for LogImpl { - fn enabled(&self, metadata: &log::Metadata) -> bool { - match LOG_FILTER.get() { - Some(filter) => filter.enabled(metadata), - None => Lv::from(metadata.level()).can_print(lv::PRINT_LEVEL.get()), - } - } - - fn log(&self, record: &log::Record) { - if !self.enabled(record.metadata()) { - return; - } - let typ: Lv = record.level().into(); - let message = if typ == Lv::Trace { - // enable source location logging in trace messages - let mut message = String::new(); - message.push('['); - if let Some(p) = record.module_path() { - // aliased crate, use the shorthand - if let Some(rest) = p.strip_prefix("pistonite_") { - message.push_str(rest); - } else { - message.push_str(p); - } - message.push(' '); - } - if let Some(f) = record.file() { - let name = match f.rfind(['/', '\\']) { - None => f, - Some(i) => &f[i + 1..], - }; - message.push_str(name); - } - if let Some(l) = record.line() { - message.push(':'); - message.push_str(&format!("{l}")); - } - if message.len() > 1 { - message += "] "; - } else { - message.clear(); - } - - use std::fmt::Write; - let _: Result<_, _> = write!(&mut message, "{}", record.args()); - message - } else { - record.args().to_string() - }; - if let Ok(mut printer) = super::PRINTER.lock() { - printer.print_message(typ, &message); - } - } - - fn flush(&self) {} - } - - let _ = log::set_logger(&LogImpl); -} - -thread_local! { - pub(crate) static THREAD_NAME: RefCell> = const { RefCell::new(None) }; -} - -/// Set the name to show up in messages printed by the current thread -pub fn set_thread_print_name(name: &str) { - THREAD_NAME.with_borrow_mut(|x| *x = Some(name.to_string())) -} diff --git a/packages/copper/src/print/mod.rs b/packages/copper/src/print/mod.rs deleted file mode 100644 index 28e611f..0000000 --- a/packages/copper/src/print/mod.rs +++ /dev/null @@ -1,26 +0,0 @@ -mod init; - -pub use init::*; -pub(crate) mod ansi; -mod printer; -pub(crate) mod utf8; -pub use printer::*; -mod format; -pub use format::*; -mod progress; -pub use progress::*; -mod zero_when_drop; -pub use zero_when_drop::*; - -mod prompt; -pub use prompt::*; -#[cfg(feature = "prompt-password")] -mod prompt_password; -// #[cfg(feature = "prompt-password")] -// pub use prompt_password::check_password_legality; - -// 50ms between each cycle -pub(crate) const TICK_INTERVAL: std::time::Duration = std::time::Duration::from_millis(10); -// 2B ticks * 10ms = 251 days. -// overflown tick means ETA will be inaccurate (after 251 days) -pub(crate) type Tick = u32; diff --git a/packages/copper/src/print/progress/bar.rs b/packages/copper/src/print/progress/bar.rs deleted file mode 100644 index 495b4fa..0000000 --- a/packages/copper/src/print/progress/bar.rs +++ /dev/null @@ -1,535 +0,0 @@ -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; - -use crate::print::progress::eta::Estimater; -use crate::print::progress::state::{BarFormatter, BarResult, State, StateImmut}; - -/// # Progress Bars -/// Progress bars are a feature in the print system. It is aware of the printing/prompting going on -/// in the console and will keep the bars at the bottom of the console without interferring -/// with the other outputs. -/// -/// ## Components -/// A bar has the following display components -/// - Step display: Displays the current and total steps. For example, `[42/100]`. Will not display -/// for bars that are unbounded. Bars that are not unbounded but the total is not set -/// will show total as `?`. The step display can also be configured to a style more suitable -/// for displaying bytes (for example downloading or processing file), like `10.0K / 97.3M` -/// - Prefix: A string configured once when launching the progress bar -/// - Percentage: Percentage display for the current and total steps, For example `42.00%`. -/// This can be turned off if not needed -/// - ETA: Estimated remaining time. This can be turned off if not needed -/// - Message: A message that can be set while the progress bar is showing. For example, -/// this can be the name of the current file being processed, etc. -/// -/// With everything displayed, it will look something like this: -/// ```text -/// X][42/100] prefix: 42.00% ETA 32.35s processing the 42th item -/// ``` -/// (`X`) is where the animated spinner is -/// -/// ## Progress Tree -/// You can display progress bars with a hierarchy if desired. The progress bars -/// will be organized as an directed acyclic graph (i.e. a tree). Special characters -/// will be used to draw the tree in the terminal. -/// -/// Each progress bar holds a strong ref to its parent, and weak refs to all of its children. -/// The printer keeps weak refs to all root progress bars (i.e. one without a parent). -/// -/// ## State and Output -/// Each progress bar can have 3 states: `progress`, `done`, and `interrupted`. -/// -/// When in `progress`, the bar will be animated if the output is a terminal. Otherwise, -/// updates will be ignored. -/// -/// The bar will be `done` when all handles are dropped if 1 of the following is true: -/// - The bar has finite total, and current step equals total step -/// - The bar is unbounded, and `.done()` is called on any handle -/// -/// If neither is true when all handles are dropped, the bar becomes `interrupted`. -/// This makes the bar easier to use with control flows. When the bar is in this state, -/// it will print an interrupted message to the regular print stream, like -/// ```text -/// X][42/100] prefix: interrupted -/// ``` -/// This message is customizable when building the progress bar. All of its children -/// that are interrupted will also be printed. All children that are `done` will only be -/// printed if `keep` is true for that children (see below). The interrupted message is printed -/// regardless if the output is terminal or not. -/// -/// When the progress bar is done, it may print a "done message" depending on -/// if it has a parent and the `keep` option: -/// | Has parent (i.e. is child) | Keep | Behavior | -/// |-|-|-| -/// | Yes | Yes | Done message will be displayed under the parent, but the bar will disappear completely when the parent is done | -/// | Yes | No | The bar will disappear after it's done | -/// | No | Yes | The bar will print a done message to the regular print stream when done, no children will be printed | -/// | No | No | The bar will disappear after done, no children will be printed | -/// -/// The done message is also customizable when building the bar. Note (from the table) that it will -/// be effective in some way if the `keep` option is true. Setting a done message -/// does not automatically set `keep` to true. -/// -/// The default done message is something like below, will be displayed in green. -/// ```text -/// X][100/100] prefix: done -/// ``` -/// -/// ## Updating the bar -/// The [`progress`](macro@crate::progress) macro is used to update the progress bar. -/// For example: -/// -/// ```rust -/// # use pistonite_cu as cu; -/// let bar = cu::progress("doing something").total(10).spawn(); -/// for i in 0..10 { -/// cu::progress!(bar = i, "doing {i}th step"); -/// } -/// drop(bar); -/// ``` -/// -/// ## Building the bar -/// This function `cu::progress` will make a [`ProgressBarBuilder`] -/// with these default configs: -/// - Total steps: unbounded -/// - Keep after done: `true` -/// - Show ETA: `true` (only effective if steps is finite) -/// - Finish message: Default -/// - Interrupted message: Default -/// -/// See [`ProgressBarBuilder`] for builder methods -/// -/// ## Print Levels -/// The bar final messages are suppressed at `-q` and the bar animations are suppressed at `-qq` -/// -/// ## Other considerations -/// If the progress bar print section exceeds the terminal height, -/// it will probably not render properly. Keep in mind when you -/// are displaying a large number of progress bars. -/// -/// You can use `.max_display_children()` to set the maximum number of children -/// to display at a time. However, there is no limit on the number of root progress bars. -#[inline(always)] -pub fn progress(message: impl Into) -> ProgressBarBuilder { - ProgressBarBuilder::new(message.into()) -} - -/// Update a [progress bar](fn@crate::progress) -/// -/// The macro takes 2 parts separated by comma `,`: -/// - An expression for updating the progress: -/// - Optional format args for updating the message. -/// -/// The progress update expression can be one of: -/// - `bar = i`: set the progress to `i` -/// - `bar += i`: increment the steps by i -/// - `bar`: don't update the progress -/// -/// , where `bar` is an ident -/// -/// The format args can be omitted to update the progress without -/// updating the message -/// -/// # Examples -/// ```rust,no_run -/// # use pistonite_cu as cu; -/// let bar = cu::progress_bar(10, "10 steps"); -/// // update the current count and message -/// let i = 1; -/// cu::progress!(bar = i, "doing step {i}"); -/// // update the current count without changing message -/// cu::progress!(bar += 2); -/// // update the message without changing current step -/// cu::progress!(bar, "doing the thing"); -/// ``` -#[macro_export] -macro_rules! progress { - ($bar:ident, $($fmt_args:tt)*) => { - $bar.__inc(0u64, Some(format!($($fmt_args)*))) - }; - ($bar:ident += $inc:expr) => { - $bar.__inc({ $inc } as u64, None) - }; - ($bar:ident += $inc:expr, $($fmt_args:tt)*) => { - $bar.__inc({ $inc } as u64, Some(format!($($fmt_args)*))) - }; - ($bar:ident = $x:expr) => { - $bar.__set({ $x } as u64, None) - }; - ($bar:ident = $x:expr, $($fmt_args:tt)*) => { - $bar.__set({ $x } as u64, Some(format!($($fmt_args)*))) - }; -} - -// spawn_iter stuff, keep for reference, not sure if needed yet -// .enumerate seems more readable -/* -/// In the example above, you can also attach it to an iterator directly. -/// The builder will call `size_hint()` once and set the total on the bar, -/// and will automatically mark it as done if `next()` returns `None`. -/// -/// If the default iteration behavior of `spawn_iter` is not desirable, use `spawn` -/// and iterate manually. -/// ```rust -/// # use pistonite_cu as cu; -/// for i in cu::progress("doing something").spawn_iter(0..10) { -/// cu::print!("doing {i}th step"); -/// } -/// ``` -/// -/// Note that in the code above, we didn't have a handle to the bar directly -/// to update the message, we can fix that by getting the bar from the iter -/// -/// ```rust -/// # use pistonite_cu as cu; -/// let mut iter = cu::progress("doing something").spawn_iter(0..10); -/// let bar = iter.bar(); -/// for i in iter { -/// // bar = i is handled by the iterator automatically -/// cu::progress!(bar, "doing {i}th step"); -/// } -/// ``` -*/ - -/// Builder for a progress bar -#[derive(Debug, Clone)] // Clone sometimes needed to build by ref.. without unsafe -pub struct ProgressBarBuilder { - /// The message prefix for the progress bar - message: String, - /// Total steps (None = unbounded, 0 = not known yet) - total: Option, - /// The progress bar is for displaying bytes - total_is_in_bytes: bool, - /// If the bar should be kept after it's done - keep: Option, - /// If ETA should be visible (only effective if total is finite) - show_eta: bool, - /// If percentage should be visible (only effective if total is finite) - show_percentage: bool, - /// Message to display after done, instead of the default - done_message: Option, - /// Message to display if the bar is interrupted - interrupted_message: Option, - /// Maximum number of children to display at a time - max_display_children: usize, - /// Optional parent of the bar - parent: Option>, -} -impl ProgressBarBuilder { - /// Start building a progress bar. Note [`cu::progress`](progress) is the canonical shorthand - pub fn new(message: String) -> Self { - Self { - message, - total: None, - total_is_in_bytes: false, - keep: None, - show_eta: true, - show_percentage: true, - done_message: None, - interrupted_message: None, - max_display_children: usize::MAX / 2, - parent: None, - } - } - /// Set the total steps. `0` means total is unknown, which can be set - /// at a later point. - /// - /// By default, the progress bar is "unbounded", meaning there is no - /// individual steps - /// - /// ```rust - /// # use pistonite_cu as cu; - /// cu::progress("doing something").total(10); - /// ``` - #[inline(always)] - pub fn total(mut self, total: usize) -> Self { - self.total = Some(total as u64); - self - } - - /// Set the total as a `u64` on platforms where `usize` is less than 64 bits - #[cfg(not(target_pointer_width = "64"))] - pub fn total_u64(mut self, total: u64) -> Self { - self.total = Some(total); - self - } - - /// Set the total bytes and set the progress to be displayed using byte units (SI). - /// `0` means total is unknown, which can be set at a later point. - /// - /// ```rust - /// # use pistonite_cu as cu; - /// cu::progress("doing something").total_bytes(1000000); - /// ``` - #[inline(always)] - pub fn total_bytes(mut self, total: u64) -> Self { - self.total = Some(total as u64); - self.total_is_in_bytes = true; - self - } - - /// Set if the progress bar should be kept in the output - /// after it's done. - /// - /// Default is `true` for root progress bars and `false` - /// for child progress bars - /// - /// ```rust - /// # use pistonite_cu as cu; - /// cu::progress("doing something").keep(false); - /// ``` - #[inline(always)] - pub fn keep(mut self, keep: bool) -> Self { - self.keep = Some(keep); - self - } - - /// Set if ETA (estimated time) should be displayed. - /// Only effective if total is not zero (i.e. not unbounded). - /// Default is `true` - /// - /// ```rust - /// # use pistonite_cu as cu; - /// cu::progress("doing something").total(10).eta(false); - /// ``` - #[inline(always)] - pub fn eta(mut self, show: bool) -> Self { - self.show_eta = show; - self - } - - /// Set if percentage should be displayed. - /// Only effective if total is not zero (i.e. not unbounded). - /// Default is `true` - /// - /// ```rust - /// # use pistonite_cu as cu; - /// cu::progress("doing something").total(10).percentage(false); - /// ``` - #[inline(always)] - pub fn percentage(mut self, show: bool) -> Self { - self.show_percentage = show; - self - } - - /// Set a message to be displayed when the progress is done. - /// Requires `keep(true)` - which is the default, but - /// `when_done` will not automatically turn it on for you. - /// - /// Default is the message of the progress bar followed by `done`. - /// - /// ```rust - /// # use pistonite_cu as cu; - /// cu::progress("doing something").when_done("something is done!"); - /// ``` - #[inline(always)] - pub fn when_done(mut self, message: impl Into) -> Self { - self.done_message = Some(message.into()); - self - } - - /// Set a message to be displayed when the progress is interrupted. - /// - /// Default is the message of the progress bar followed by `interrupted`. - /// - /// ```rust - /// # use pistonite_cu as cu; - /// cu::progress("doing something").when_interrupt("something is interrupted!"); - /// ``` - #[inline(always)] - pub fn when_interrupt(mut self, message: impl Into) -> Self { - self.interrupted_message = Some(message.into()); - self - } - - /// Set the max number of children to display at a time. - /// Default is unbounded. - /// - /// ```rust - /// # use pistonite_cu as cu; - /// cu::progress("doing something").max_display_children(30); - /// ``` - pub fn max_display_children(mut self, num: usize) -> Self { - self.max_display_children = num; - self - } - - /// Set the parent progress bar. - /// - /// If the parent is known to be `Some`, use `parent.child(...)` instead - pub fn parent(mut self, parent: Option>) -> Self { - self.parent = parent; - self - } - - /// Build and start displaying the bar in the console - pub fn spawn(self) -> Arc { - let keep = self.keep.unwrap_or(self.parent.is_none()); - let done_message = if keep { - match self.done_message { - None => { - if self.message.is_empty() { - Some("done".to_string()) - } else { - Some(format!("{}: done", self.message)) - } - } - Some(x) => Some(x), - } - } else { - None - }; - let state_immut = StateImmut { - id: next_id(), - parent: self.parent.as_ref().map(Arc::clone), - prefix: self.message, - done_message, - interrupted_message: self.interrupted_message, - show_percentage: self.show_percentage, - unbounded: self.total.is_none(), - display_bytes: self.total_is_in_bytes, - max_display_children: self.max_display_children, - }; - let eta = self.show_eta.then(Estimater::new); - let state = State::new(self.total.unwrap_or(0), eta); - - let bar = Arc::new(ProgressBar { - state: state_immut, - state_mut: Mutex::new(state), - }); - match self.parent { - Some(p) => { - if let Ok(mut p) = p.state_mut.lock() { - p.add_child(&bar); - } - } - None => { - if let Ok(mut printer) = super::super::PRINTER.lock() { - printer.add_progress_bar(&bar); - } - } - } - - bar - } -} - -fn next_id() -> usize { - static ID: AtomicUsize = AtomicUsize::new(1); - ID.fetch_add(1, Ordering::SeqCst) -} -#[derive(Debug)] -pub struct ProgressBar { - pub(crate) state: StateImmut, - state_mut: Mutex, -} -impl ProgressBar { - #[doc(hidden)] - #[inline(always)] - pub fn __set(self: &Arc, current: u64, message: Option) { - if let Ok(mut bar) = self.state_mut.lock() { - bar.set_current(current); - if let Some(x) = message { - bar.set_message(&x); - } - } - } - - #[doc(hidden)] - #[inline(always)] - pub fn __inc(self: &Arc, amount: u64, message: Option) { - if let Ok(mut bar) = self.state_mut.lock() { - bar.inc_current(amount); - if let Some(x) = message { - bar.set_message(&x); - } - } - } - - /// Set the total steps (if the progress is finite) - pub fn set_total(&self, total: u64) { - if let Ok(mut bar) = self.state_mut.lock() { - bar.set_total(total); - } - } - - /// Start building a child progress bar - /// - /// Note that the child builder will keep this bar alive (displayed), even - /// if the child is not spawned - #[inline(always)] - pub fn child(self: &Arc, message: impl Into) -> ProgressBarBuilder { - ProgressBarBuilder::new(message.into()).parent(Some(Arc::clone(self))) - } - - /// Mark the progress bar as done and drop the handle. - /// - /// This needs to be called if the bar is unbounded. Otherwise, - /// the bar will display in the interrupted state when dropped. - /// - /// If the progress is finite, then interrupted state is automatically - /// determined (`current != total`) - pub fn done(self: Arc) { - if self.state.unbounded { - if let Ok(mut bar) = self.state_mut.lock() { - bar.set_current(1); - bar.set_total(1); - } - } - } - - /// Same as [`done`](Self::done), but does not drop the bar. - pub fn done_by_ref(&self) { - if self.state.unbounded { - if let Ok(mut bar) = self.state_mut.lock() { - bar.set_current(1); - bar.set_total(1); - } - } - } - - /// Format the bar - #[inline(always)] - pub(crate) fn format(&self, fmt: &mut BarFormatter<'_, '_, '_>) -> i32 { - self.format_at_depth(0, &mut String::new(), fmt) - } - - /// Format the bar at depth - pub(crate) fn format_at_depth( - &self, - depth: usize, - hierarchy: &mut String, - fmt: &mut BarFormatter<'_, '_, '_>, - ) -> i32 { - let Ok(mut bar) = self.state_mut.lock() else { - return 0; - }; - bar.format_at_depth(depth, hierarchy, fmt, &self.state) - } -} - -impl Drop for ProgressBar { - fn drop(&mut self) { - let result = match self.state_mut.lock() { - Err(_) => BarResult::DontKeep, - Ok(bar) => bar.check_result(&self.state), - }; - if let Some(parent) = &self.state.parent { - // inform parent our result - if let Ok(mut parent_state) = parent.state_mut.lock() { - parent_state.child_done(self.state.id, result.clone()); - } - } - let handle = { - // scopr for printer lock - let Ok(mut printer) = super::super::PRINTER.lock() else { - return; - }; - printer.print_bar_done(&result, self.state.parent.is_none()); - printer.take_print_task_if_should_join() - }; - if let Some(x) = handle { - let _: Result<(), _> = x.join(); - } - } -} diff --git a/packages/copper/src/print/progress/mod.rs b/packages/copper/src/print/progress/mod.rs deleted file mode 100644 index 478cfc6..0000000 --- a/packages/copper/src/print/progress/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod eta; -mod state; -pub(crate) use state::{BarFormatter, BarResult}; -mod bar; -pub use bar::{ProgressBar, ProgressBarBuilder, progress}; diff --git a/packages/copper/src/process/arg.rs b/packages/copper/src/process/arg.rs index db55126..037e63a 100644 --- a/packages/copper/src/process/arg.rs +++ b/packages/copper/src/process/arg.rs @@ -1,5 +1,7 @@ use tokio::process::Command as TokioCommand; +use crate::cli::fmt; + /// Add arguments to the command #[doc(hidden)] pub trait Config { @@ -94,7 +96,7 @@ impl ColorFlag { } impl std::fmt::Display for ColorFlag { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let flag = if crate::color_enabled() { + let flag = if crate::lv::color_enabled() { "always" } else { "never" @@ -108,7 +110,7 @@ impl std::fmt::Display for ColorFlag { } impl Config for ColorFlag { fn configure(self, command: &mut TokioCommand) { - let flag = if crate::color_enabled() { + let flag = if crate::lv::color_enabled() { "always" } else { "never" @@ -179,7 +181,7 @@ impl WidthFlag { #[cfg(feature = "print")] impl Config for WidthFlag { fn configure(self, command: &mut TokioCommand) { - if let Some(w) = crate::term_width() { + if let Some(w) = fmt::term_width() { if self.use_eq_sign() { command.arg(format!("--width={w}")); } else { diff --git a/packages/copper/src/process/builder.rs b/packages/copper/src/process/builder.rs index b81c4ae..97b86ee 100644 --- a/packages/copper/src/process/builder.rs +++ b/packages/copper/src/process/builder.rs @@ -541,7 +541,7 @@ fn pre_spawn= timeout { break; } + tokio::time::sleep(Duration::from_millis(ms)).await; ms *= 4; } Ok(None) @@ -130,7 +130,6 @@ impl Child { /// # Panic /// Will panic if called outside of a tokio runtime context pub async fn co_kill(mut self) -> crate::Result { - self.io.co_join(&self.name).await; let mut ms = 100; for i in 0..5 { crate::trace!("trying to kill child '{}', attempt {}", self.name, i + 1); @@ -148,6 +147,7 @@ impl Child { tokio::time::sleep(Duration::from_millis(ms)).await; ms *= 4; } + self.io.co_join(&self.name).await; crate::bail!("failed to kill child '{}' after many attempts", self.name); } @@ -157,7 +157,6 @@ impl Child { /// This will block the current thread while trying to join the child. /// Use [`co_kill`](Self::co_kill) to avoid blocking if in async context. pub fn kill(mut self) -> crate::Result { - self.io.join(&self.name); let mut ms = 100; for i in 0..5 { crate::trace!("trying to kill child '{}', attempt {}", self.name, i + 1); @@ -175,6 +174,7 @@ impl Child { std::thread::sleep(Duration::from_millis(ms)); ms *= 4; } + self.io.join(&self.name); crate::bail!("failed to kill child '{}' after many attempts", self.name); } } diff --git a/packages/copper/src/process/pio/cargo_preset.rs b/packages/copper/src/process/pio/cargo_preset.rs index 164ddb8..94aec7a 100644 --- a/packages/copper/src/process/pio/cargo_preset.rs +++ b/packages/copper/src/process/pio/cargo_preset.rs @@ -325,32 +325,33 @@ impl PrintState { let Some(rendered) = message.rendered else { return; }; - match message.level { - Some("warning") => match &self.diagnostic_hook { - None => { - crate::__priv::__print_with_level( - self.warning_lv, - format_args!("{rendered}"), - ); - } - Some(hook) => hook(true, &rendered), - }, - Some("error") => match &self.diagnostic_hook { - None => { - crate::__priv::__print_with_level( - self.error_lv, - format_args!("{rendered}"), - ); - } - Some(hook) => hook(false, &rendered), - }, - _ => { - crate::__priv::__print_with_level( - self.other_lv, - format_args!("{rendered}"), - ); - } - } + todo!() + // match message.level { + // Some("warning") => match &self.diagnostic_hook { + // None => { + // // crate::__priv::__print_with_level( + // // self.warning_lv, + // // format_args!("{rendered}"), + // // ); + // } + // Some(hook) => hook(true, &rendered), + // }, + // Some("error") => match &self.diagnostic_hook { + // None => { + // crate::__priv::__print_with_level( + // self.error_lv, + // format_args!("{rendered}"), + // ); + // } + // Some(hook) => hook(false, &rendered), + // }, + // _ => { + // crate::__priv::__print_with_level( + // self.other_lv, + // format_args!("{rendered}"), + // ); + // } + // } } "build-finished" => match payload.success { Some(true) => { @@ -369,50 +370,51 @@ impl PrintState { } fn handle_stderr(&mut self, line: &str) { - static STATUS_REGEX: LazyLock = LazyLock::new(|| { - Regex::new("^((\x1b[^m]*m)|\\s)*(Compiling|Checking)((\x1b[^m]*m)|\\s)*").unwrap() - }); - static ERROR_REGEX: LazyLock = - LazyLock::new(|| Regex::new("^((\x1b[^m]*m)|\\s)*error").unwrap()); - static WARNING_REGEX: LazyLock = - LazyLock::new(|| Regex::new("^((\x1b[^m]*m)|\\s)*warning").unwrap()); - let Some(m) = STATUS_REGEX.find(line) else { - // some error/warning messages aren't emited to stdout, - // so we use a regex to match and print them - if let Some(lv) = self.stderr_printing_message_lv { - // since the message might be multi-line, we - // keep printing until a status message is matched - crate::__priv::__print_with_level(lv, format_args!("{line}")); - return; - } - // check if the message matches error/warning - if ERROR_REGEX.is_match(line) { - crate::__priv::__print_with_level(self.error_lv, format_args!("{line}")); - self.stderr_printing_message_lv = Some(self.error_lv); - return; - } - if WARNING_REGEX.is_match(line) { - crate::__priv::__print_with_level(self.warning_lv, format_args!("{line}")); - self.stderr_printing_message_lv = Some(self.warning_lv); - return; - } - // print as other message - crate::__priv::__print_with_level(self.other_lv, format_args!("{line}")); - return; - }; - // print the status message as other, and clear the error/warning message state - crate::__priv::__print_with_level(self.other_lv, format_args!("{line}")); - self.stderr_printing_message_lv = None; - - // process the status message - let line = &line[m.end()..].trim(); - // crate name can't have space (right?) - let crate_name = match line.find(' ') { - None => line, - Some(i) => &line[..i], - }; - self.in_progress.insert(crate_name.replace('-', "_")); - self.update_bar(); + todo!() + // static STATUS_REGEX: LazyLock = LazyLock::new(|| { + // Regex::new("^((\x1b[^m]*m)|\\s)*(Compiling|Checking)((\x1b[^m]*m)|\\s)*").unwrap() + // }); + // static ERROR_REGEX: LazyLock = + // LazyLock::new(|| Regex::new("^((\x1b[^m]*m)|\\s)*error").unwrap()); + // static WARNING_REGEX: LazyLock = + // LazyLock::new(|| Regex::new("^((\x1b[^m]*m)|\\s)*warning").unwrap()); + // let Some(m) = STATUS_REGEX.find(line) else { + // // some error/warning messages aren't emited to stdout, + // // so we use a regex to match and print them + // if let Some(lv) = self.stderr_printing_message_lv { + // // since the message might be multi-line, we + // // keep printing until a status message is matched + // crate::__priv::__print_with_level(lv, format_args!("{line}")); + // return; + // } + // // check if the message matches error/warning + // if ERROR_REGEX.is_match(line) { + // crate::__priv::__print_with_level(self.error_lv, format_args!("{line}")); + // self.stderr_printing_message_lv = Some(self.error_lv); + // return; + // } + // if WARNING_REGEX.is_match(line) { + // crate::__priv::__print_with_level(self.warning_lv, format_args!("{line}")); + // self.stderr_printing_message_lv = Some(self.warning_lv); + // return; + // } + // // print as other message + // crate::__priv::__print_with_level(self.other_lv, format_args!("{line}")); + // return; + // }; + // // print the status message as other, and clear the error/warning message state + // crate::__priv::__print_with_level(self.other_lv, format_args!("{line}")); + // self.stderr_printing_message_lv = None; + // + // // process the status message + // let line = &line[m.end()..].trim(); + // // crate name can't have space (right?) + // let crate_name = match line.find(' ') { + // None => line, + // Some(i) => &line[..i], + // }; + // self.in_progress.insert(crate_name.replace('-', "_")); + // self.update_bar(); } fn update_bar(&mut self) { diff --git a/packages/copper/src/process/pio/print.rs b/packages/copper/src/process/pio/print.rs index 7b14755..3ebb62a 100644 --- a/packages/copper/src/process/pio/print.rs +++ b/packages/copper/src/process/pio/print.rs @@ -71,9 +71,10 @@ impl PrintTask { loop { match driver.next().await { DriverOutput::Line(line) => { - for l in line.lines() { - crate::__priv::__print_with_level(lv, format_args!("{prefix}{l}")); - } + todo!() + // for l in line.lines() { + // crate::__priv::__print_with_level(lv, format_args!("{prefix}{l}")); + // } } DriverOutput::Done => break, _ => {} diff --git a/packages/copper/src/process/pio/print_driver.rs b/packages/copper/src/process/pio/print_driver.rs index 9f8c416..d14ef76 100644 --- a/packages/copper/src/process/pio/print_driver.rs +++ b/packages/copper/src/process/pio/print_driver.rs @@ -1,6 +1,8 @@ use tokio::io::AsyncReadExt as _; use tokio::process::{ChildStderr, ChildStdout}; +use crate::cli::fmt::{ansi, utf8}; + /// Drive that takes the out stream and err stream, and produces lines pub(crate) struct Driver { out: Option, @@ -159,7 +161,6 @@ impl Driver { line: &mut String, only_last_line: bool, ) -> (usize, bool) { - use crate::print::{ansi, utf8}; let mut i = 0; let mut invalid_while_escaping = false; let mut start_escape_pos: Option = None; @@ -183,7 +184,7 @@ impl Driver { let prev = last; last = c; if let Some(s) = start_escape_pos { - if ansi::is_ansi_end_char(c) { + if ansi::is_esc_end(c) { start_escape_pos = None; // allow color codes if !invalid_while_escaping && c == 'm' { diff --git a/packages/copper/src/process/pio/spinner.rs b/packages/copper/src/process/pio/spinner.rs index 3f1a7db..27ce0a0 100644 --- a/packages/copper/src/process/pio/spinner.rs +++ b/packages/copper/src/process/pio/spinner.rs @@ -123,7 +123,7 @@ impl ChildOutConfig for Spinner { is_out: bool, ) -> crate::Result { let lv = self.config.lv.get(); - let log_prefix = if crate::log_enabled(lv) { + let log_prefix = if lv.enabled() { let name = name.unwrap_or_default(); if name.is_empty() { String::new() @@ -174,13 +174,14 @@ impl SpinnerTask { loop { match driver.next().await { DriverOutput::Line(line) => { - if lv != Lv::Off { - crate::__priv::__print_with_level(lv, format_args!("{prefix}{line}")); - // erase the progress line if we decide to print it out - crate::progress!(bar, "") - } else { - crate::progress!(bar, "{line}") - } + todo!() + // if lv != Lv::Off { + // crate::__priv::__print_with_level(lv, format_args!("{prefix}{line}")); + // // erase the progress line if we decide to print it out + // crate::progress!(bar, "") + // } else { + // crate::progress!(bar, "{line}") + // } } DriverOutput::Progress(line) => { crate::progress!(bar, "{line}") diff --git a/packages/copper/src/str/byte_format.rs b/packages/copper/src/str/byte_format.rs new file mode 100644 index 0000000..086efa5 --- /dev/null +++ b/packages/copper/src/str/byte_format.rs @@ -0,0 +1,23 @@ +/// Format integer in SI bytes. +/// +/// The accuracy is 1 decimal, i.e `999.9T`. +/// +/// Available units are `T`, `G`, `M`, `k`, `B` +pub struct ByteFormat(pub u64); +impl std::fmt::Display for ByteFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (unit_bytes, unit_char) in [ + (1000_000_000_000, 'T'), + (1000_000_000, 'G'), + (1000_000, 'M'), + (1000, 'k'), + ] { + if self.0 >= unit_bytes { + let whole = self.0 / unit_bytes; + let deci = (self.0 % unit_bytes) * 10 / unit_bytes; + return write!(f, "{whole}.{deci}{unit_char}"); + } + } + write!(f, "{}B", self.0) + } +} diff --git a/packages/copper/src/str/mod.rs b/packages/copper/src/str/mod.rs new file mode 100644 index 0000000..901d048 --- /dev/null +++ b/packages/copper/src/str/mod.rs @@ -0,0 +1,11 @@ +mod zstring; +pub use zstring::{zero_string, ZString}; +mod byte_format; +pub use byte_format::ByteFormat; + +// path requires fs since there are utils that checks for existence +// (check_exists, normalize) +#[cfg(feature = "fs")] +mod path; +#[cfg(feature = "fs")] +pub use path::{PathExtension, PathExtensionOwned}; diff --git a/packages/copper/src/path.rs b/packages/copper/src/str/path.rs similarity index 100% rename from packages/copper/src/path.rs rename to packages/copper/src/str/path.rs diff --git a/packages/copper/src/print/zero_when_drop.rs b/packages/copper/src/str/zstring.rs similarity index 73% rename from packages/copper/src/print/zero_when_drop.rs rename to packages/copper/src/str/zstring.rs index 25c2a04..b98fd84 100644 --- a/packages/copper/src/print/zero_when_drop.rs +++ b/packages/copper/src/str/zstring.rs @@ -1,68 +1,71 @@ + /// A string that will have its inner buffer zeroed when dropped #[derive(Default, Clone)] -pub struct ZeroWhenDropString(String); -impl ZeroWhenDropString { +pub struct ZString(String); +impl ZString { pub const fn new() -> Self { Self(String::new()) } } -impl std::fmt::Display for ZeroWhenDropString { +impl std::fmt::Display for ZString { #[inline(always)] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } -impl From for ZeroWhenDropString { +impl From for ZString { #[inline(always)] fn from(value: String) -> Self { Self(value) } } -impl AsRef<[u8]> for ZeroWhenDropString { +impl AsRef<[u8]> for ZString { #[inline(always)] fn as_ref(&self) -> &[u8] { self.0.as_ref() } } -impl AsRef for ZeroWhenDropString { +impl AsRef for ZString { #[inline(always)] fn as_ref(&self) -> &String { &self.0 } } -impl AsRef for ZeroWhenDropString { +impl AsRef for ZString { #[inline(always)] fn as_ref(&self) -> &str { &self.0 } } -impl Drop for ZeroWhenDropString { +impl Drop for ZString { #[inline(always)] fn drop(&mut self) { zero_string(&mut self.0) } } -impl std::ops::Deref for ZeroWhenDropString { +impl std::ops::Deref for ZString { type Target = String; #[inline(always)] fn deref(&self) -> &String { &self.0 } } -impl std::ops::DerefMut for ZeroWhenDropString { +impl std::ops::DerefMut for ZString { #[inline(always)] fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -pub(crate) fn zero_string(s: &mut String) { +/// Write 0's to the internal buffer of the string +pub fn zero_string(s: &mut String) { let mut s = std::mem::take(s); // SAFETY: we don't use the string again for c in unsafe { s.as_bytes_mut() } { // SAFETY: c is a valid u8 pointer unsafe { std::ptr::write_volatile(c, 0) }; } + // ensure the std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst); std::sync::atomic::compiler_fence(std::sync::atomic::Ordering::SeqCst); } From d3d55774f5488c775fe00fe8438610bf0c311de5 Mon Sep 17 00:00:00 2001 From: Michael Zhao <44533763+Pistonight@users.noreply.github.com> Date: Sat, 10 Jan 2026 22:23:12 -0800 Subject: [PATCH 05/13] windows --- packages/copper/src/cli/password.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/copper/src/cli/password.rs b/packages/copper/src/cli/password.rs index e22460e..6eeb2d2 100644 --- a/packages/copper/src/cli/password.rs +++ b/packages/copper/src/cli/password.rs @@ -118,13 +118,13 @@ mod windows { // Get the old mode so we can reset back to it when we are done if unsafe { GetConsoleMode(handle, &mut mode as *mut CONSOLE_MODE) } == 0 { - return Err(std::io::Error::last_os_error()); + return Err(io::Error::last_os_error()); } // We want to be able to read line by line, and we still want backspace to work let new_mode_flags = ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT; if unsafe { SetConsoleMode(handle, new_mode_flags) } == 0 { - return Err(std::io::Error::last_os_error()); + return Err(io::Error::last_os_error()); } Ok(HiddenInput { mode, handle }) @@ -141,7 +141,7 @@ mod windows { } /// Reads a password from the TTY - pub fn read_password() -> std::io::Result { + pub fn read_password() -> io::Result { let handle = unsafe { CreateFileA( c"CONIN$".as_ptr() as PCSTR, @@ -155,7 +155,7 @@ mod windows { }; if handle == INVALID_HANDLE_VALUE { - return Err(std::io::Error::last_os_error()); + return Err(io::Error::last_os_error()); } let mut stream = BufReader::new(unsafe { std::fs::File::from_raw_handle(handle as _) }); @@ -166,8 +166,8 @@ mod windows { fn read_password_from_handle_with_hidden_input( reader: &mut impl BufRead, handle: HANDLE, - ) -> io::Result { - let mut password = crate::ZeroWhenDropString::default(); + ) -> io::Result { + let mut password = crate::ZString::default(); { let _hidden_input = HiddenInput::new(handle)?; reader.read_line(&mut password)?; From cfa72f3e209ad4b4c364fbbb3e667dae43161128 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Sat, 10 Jan 2026 23:07:34 -0800 Subject: [PATCH 06/13] finish refactoring prompts --- packages/copper/Cargo.toml | 4 +- packages/copper/Taskfile.yml | 4 + packages/copper/examples/prompt_password.rs | 10 + packages/copper/examples/walk.rs | 4 +- packages/copper/src/cli/flags.rs | 4 +- packages/copper/src/cli/fmt/format_buffer.rs | 3 +- packages/copper/src/cli/fmt/mod.rs | 1 - packages/copper/src/cli/fmt/term_size.rs | 1 - packages/copper/src/cli/macros.rs | 176 +++++++++++++++-- packages/copper/src/cli/mod.rs | 65 ++----- packages/copper/src/cli/print_init.rs | 183 +++++++++--------- packages/copper/src/cli/printer.rs | 28 +-- packages/copper/src/cli/progress/builder.rs | 8 +- packages/copper/src/cli/progress/macros.rs | 1 - packages/copper/src/cli/progress/mod.rs | 2 +- packages/copper/src/cli/progress/state.rs | 23 ++- packages/copper/src/cli/progress/util.rs | 1 - packages/copper/src/cli/prompt.rs | 45 ++--- packages/copper/src/error_handling.rs | 3 +- packages/copper/src/lib.rs | 10 +- .../copper/src/process/pio/cargo_preset.rs | 139 +++++++------ packages/copper/src/process/pio/print.rs | 7 +- packages/copper/src/process/pio/spinner.rs | 17 +- packages/copper/src/str/byte_format.rs | 8 +- packages/copper/src/str/mod.rs | 2 +- packages/copper/src/str/zstring.rs | 3 +- packages/copper/tests/error_ctx.rs | 4 +- 27 files changed, 441 insertions(+), 315 deletions(-) create mode 100644 packages/copper/examples/prompt_password.rs diff --git a/packages/copper/Cargo.toml b/packages/copper/Cargo.toml index 6a8fb60..92103db 100644 --- a/packages/copper/Cargo.toml +++ b/packages/copper/Cargo.toml @@ -123,8 +123,8 @@ derive = ["dep:derive_more"] __test = [] [[example]] -name = "print" -required-features = ["prompt", "cli"] +name = "prompt_password" +required-features = ["prompt-password", "cli"] [[example]] name = "process" diff --git a/packages/copper/Taskfile.yml b/packages/copper/Taskfile.yml index 3699d53..ddaf378 100644 --- a/packages/copper/Taskfile.yml +++ b/packages/copper/Taskfile.yml @@ -25,6 +25,10 @@ tasks: vars: PACKAGE: pistonite-cu FEATURES: prompt + - task: cargo:clippy-package-feature + vars: + PACKAGE: pistonite-cu + FEATURES: prompt-password - task: cargo:clippy-package-feature vars: PACKAGE: pistonite-cu diff --git a/packages/copper/examples/prompt_password.rs b/packages/copper/examples/prompt_password.rs new file mode 100644 index 0000000..bf19eea --- /dev/null +++ b/packages/copper/examples/prompt_password.rs @@ -0,0 +1,10 @@ +use pistonite_cu as cu; + +// can only manually test this because password not prompted from stdin +#[cu::cli] +fn main(_: cu::cli::Flags) -> cu::Result<()> { + cu::hint!("testing prompt password"); + let answer = cu::prompt_password!("enter password")?; + cu::info!("you answered: {answer}"); + Ok(()) +} diff --git a/packages/copper/examples/walk.rs b/packages/copper/examples/walk.rs index 1161960..e5e301b 100644 --- a/packages/copper/examples/walk.rs +++ b/packages/copper/examples/walk.rs @@ -3,7 +3,7 @@ use pistonite_cu as cu; #[cu::cli] fn main(_: cu::cli::Flags) -> cu::Result<()> { let mut src = cu::fs::walk("src")?; - cu::set_thread_print_name("walk"); + cu::cli::set_thread_name("walk"); while let Some(entry) = src.next() { let entry = entry?; cu::info!( @@ -14,7 +14,7 @@ fn main(_: cu::cli::Flags) -> cu::Result<()> { ) } - cu::set_thread_print_name("glob"); + cu::cli::set_thread_name("glob"); let glob = cu::fs::glob_from("..", "./**/*.rs")?; for entry in glob { let entry = entry?; diff --git a/packages/copper/src/cli/flags.rs b/packages/copper/src/cli/flags.rs index 63dcdfc..8d5edf3 100644 --- a/packages/copper/src/cli/flags.rs +++ b/packages/copper/src/cli/flags.rs @@ -1,4 +1,3 @@ - use std::ffi::OsString; use std::time::Instant; @@ -81,8 +80,7 @@ impl Flags { None } }; - todo!() - // crate::init_print_options(self.color.unwrap_or_default(), level, prompt); + super::print_init::init_options(self.color.unwrap_or_default(), level, prompt); } /// Merge `other` into self. Options in other will be applied on top of self (equivalent diff --git a/packages/copper/src/cli/fmt/format_buffer.rs b/packages/copper/src/cli/fmt/format_buffer.rs index 4f24baa..6f3c486 100644 --- a/packages/copper/src/cli/fmt/format_buffer.rs +++ b/packages/copper/src/cli/fmt/format_buffer.rs @@ -1,4 +1,3 @@ - use crate::cli::fmt::{self, ansi}; /// Buffer for formatting printing messages @@ -33,7 +32,7 @@ impl FormatBuffer { pub fn take(&mut self) -> String { std::mem::take(&mut self.buffer) } - /// Reset + /// Reset pub fn reset(&mut self, gray_color: &'static str, text_color: &'static str) { self.curr = 0; self.buffer.clear(); diff --git a/packages/copper/src/cli/fmt/mod.rs b/packages/copper/src/cli/fmt/mod.rs index ac049a4..44e92c7 100644 --- a/packages/copper/src/cli/fmt/mod.rs +++ b/packages/copper/src/cli/fmt/mod.rs @@ -5,4 +5,3 @@ mod term_size; pub use term_size::*; mod format_buffer; pub(crate) use format_buffer::*; - diff --git a/packages/copper/src/cli/fmt/term_size.rs b/packages/copper/src/cli/fmt/term_size.rs index bcc7b05..5b6998d 100644 --- a/packages/copper/src/cli/fmt/term_size.rs +++ b/packages/copper/src/cli/fmt/term_size.rs @@ -1,4 +1,3 @@ - /// Get the terminal width, or the internal max if cannot get pub fn term_width_or_max() -> usize { term_width().unwrap_or(400) diff --git a/packages/copper/src/cli/macros.rs b/packages/copper/src/cli/macros.rs index 1019a58..2ca3802 100644 --- a/packages/copper/src/cli/macros.rs +++ b/packages/copper/src/cli/macros.rs @@ -1,6 +1,5 @@ - -use cu::lv; use cu::cli::printer::PRINTER; +use cu::lv; /// Print something /// /// This is similar to `info`, but unlike info, this message will still log with `-q`. @@ -20,15 +19,39 @@ macro_rules! hint { }} } +/// # Prompting +/// The `prompt` feature allows displaying prompts in the console to accept +/// user input. The prompts are thread-safe and synchronized with the printer. +/// When a prompt is active, outputs to the console will be buffered inside the printer. +/// Progress bars will also be paused. +/// +/// Prompts are driven by macros where you can format a prompt message +/// ```rust,no_run +/// # use pistonite_cu as cu; +/// let name = cu::prompt!("please enter your name")?; +/// cu::info!("user entered: {name}"); +/// ``` +/// +/// See other macros for advanced usage: +/// - [`cu::yesno!`](macro@crate::yesno): Display a `[y/n]` prompt which loops +/// until user chooses yes or no. +/// - [`cu::prompt_password!`](macro@crate::prompt_password): +/// Display a prompt where the input will be hidden +/// - [`cu::prompt_validate!`](macro@crate::prompt_validate) (and +/// [`prompt_password_validate!`](macro@crate::prompt_password_validate)) to loop the prompt until a validation function passes. +/// +/// With the `prompt` feature enabled, you can +/// use [`prompt!`](crate::prompt) and [`yesno!`](crate::yesno) to show prompts. +/// +/// The prompts are thread-safe, meaning +/// You can call them from multiple threads, and they will be queued to prompt the user one after +/// the other. Prompts are always shown regardless of verbosity. But when stdout is redirected, +/// they will not render in terminal. /// Show a prompt /// /// Use the `prompt-password` feature and [`prompt_password!`](crate::prompt_password) macro /// if prompting for a password, which will hide user's input from the console /// -/// ```rust,ignore -/// let name = cu::prompt!("please enter your name")?; -/// cu::info!("user entered: {name}"); -/// ``` #[cfg(feature = "prompt")] #[macro_export] macro_rules! prompt { @@ -39,8 +62,15 @@ macro_rules! prompt { /// Show a Yes/No prompt /// -/// Return `true` if the answer is Yes. Return an error if prompt is not allowed -/// ```rust,ignore +/// Return `true` if the answer is Yes. Return an error if prompt is not allowed. +/// +/// If `-y` is specified from the command line, then the prompt will not show, +/// and `true` will be returned immediately. +/// +/// If user does not answer `y` or `n`, the prompt will show again, until +/// user makes a decision. +/// ```rust,no_run +/// # use pistonite_cu as cu; /// if cu::yesno!("do you want to continue?")? { /// cu::info!("user picked yes"); /// } @@ -53,13 +83,13 @@ macro_rules! yesno { }} } - /// Show a password prompt /// /// The console will have inputs hidden while user types, and the returned -/// value is a [`ZeroWhenDropString`](crate::ZeroWhenDropString) +/// value is a [`cu::ZString`](struct@crate::ZString) /// -/// ```rust,ignore +/// ```rust,no_run +/// # use pistonite_cu as cu; /// let password = cu::prompt_password!("please enter your password")?; /// cu::info!("user entered: {password}"); /// ``` @@ -71,6 +101,130 @@ macro_rules! prompt_password { }} } +/// Loop a prompt until a validation function passes +/// +/// The validation function takes a `&mut String`, +/// and returns `cu::Result`, where: +/// - `Ok(true)` means the validation passed. +/// - `Ok(false)` means the validation failed. The function can optionally +/// print some kind of error or hint message +/// - `Err` means there is an error, the error will be propagated to the prompt call. +/// +/// ```rust,no_run +/// # use pistonite_cu as cu; +/// // note that extra parenthesis is needed if the format args +/// // are not inlined into the formatting literal +/// let expected = "rust"; +/// let answer = cu::prompt_validate!( +/// ("what's your favorite programming language? please answer {}", expected), +/// |answer| { +/// if answer == expected { +/// return Ok(true); +/// } +/// if answer == "javascript" { +/// cu::bail!("that's not good"); +/// } +/// cu::error!("try again"); +/// Ok(false); +/// } +/// )?; +/// assert!(answer == expected); +/// ``` +/// +/// The validation function can be a `FnMut` closure, which means +/// it can double as a result parsing function if needed +/// +/// ```rust,no_run +/// # use pistonite_cu as cu; +/// let mut index: i32 = 0; +/// cu::prompt_validate!( +/// "select a number between 0 and 5", +/// |answer| { +/// let number = match cu::parse::(answer) { +/// Err(e) => { +/// cu::error!("{e}"); +/// cu::hint!("please ensure you are entering a number"); +/// return Ok(false); +/// } +/// Ok(x) => x +/// }; +/// if number < 0 { +/// cu::error!("the number you entered is too small"); +/// return Ok(false); +/// } +/// if number > 0 { +/// cu::error!("the number you entered is too big"); +/// return Ok(false); +/// } +/// index = number; +/// Ok(true); +/// } +/// )?; +/// cu::info!("index is {index}"); +/// ``` +/// +/// For the password version, see [`prompt_password_validate`](crate::prompt_password_validate) +/// +#[cfg(feature = "prompt")] +#[macro_export] +macro_rules! prompt_validate { + ($l:literal, $validator:expr) => {{ + $crate::cli::__prompt_with_validation(format_args!($l), false, $validator) + .map(|x| x.to_string()) + }}; + (($($fmt_args:tt)*), $validator:expr) => {{ + $crate::cli::__prompt_with_validation(format_args!($($fmt_args)*), false, $validator) + .map(|x| x.to_string()) + }} +} + +/// Loop a password prompt until a validation function passes +/// +/// The validation function takes a `&mut String`, +/// and returns `cu::Result`, where: +/// - `Ok(true)` means the validation passed. +/// - `Ok(false)` means the validation failed. The function can optionally +/// print some kind of error or hint message +/// - `Err` means there is an error, the error will be propagated to the prompt call. +/// +/// ```rust,no_run +/// # use pistonite_cu as cu; +/// // note that extra parenthesis is needed if the format args +/// // are not inlined into the formatting literal +/// let password = cu::prompt_validate!( +/// "please enter a password between 8 and 16 charactres and only contain sensible characters", +/// |answer| { +/// if answer == "123456" { +/// cu::bail!("how can you do that, bye"); +/// } +/// if answer.len() < 8 { +/// cu::error!("password is too short"); +/// return Ok(false); +/// } +/// if answer.len() > 16 { +/// cu::error!("password is too long"); +/// return Ok(false); +/// } +/// if let Err(e) = cu::password_chars_legal(answer) { +/// cu::error!("invalid password: {e}"); +/// return Ok(false); +/// } +/// Ok(true) +/// } +/// )?; +/// cu::print!("{password}"); +/// ``` +#[cfg(feature = "prompt-password")] +#[macro_export] +macro_rules! prompt_password_validate { + ($l:literal, $validator:expr) => {{ + $crate::cli::__prompt_with_validation(format_args!($l), true, $validator) + }}; + (($($fmt_args:tt)*), $validator:expr) => {{ + $crate::cli::__prompt_with_validation(format_args!($($fmt_args)*), true, $validator) + }} +} + /// Internal print function for macros #[doc(hidden)] pub fn __print_with_level(lv: lv::Lv, message: std::fmt::Arguments<'_>) { diff --git a/packages/copper/src/cli/mod.rs b/packages/copper/src/cli/mod.rs index b3590f5..575cb54 100644 --- a/packages/copper/src/cli/mod.rs +++ b/packages/copper/src/cli/mod.rs @@ -109,66 +109,33 @@ //! When mixing `RUST_LOG` and verbosity flags, logging messages are filtered //! by `RUST_LOG`, and the verbosity would only apply to `print` and `hint` //! -//! When setting up test, you can use [`log_init`](crate::log_init) to quickly inititialize logging +//! # Other +//! When setting up test, you can use [`cu::cli::level`] to quickly inititialize logging //! without dealing with the details. //! -//! [`set_thread_print_name`](crate::set_thread_print_name) can be used to add a prefix to all messages printed +//! [`cu::cli::set_thread_name`] can be used to add a prefix to all messages printed //! by the current thread. //! //! Messages that are too long and multi-line messages are automatically wrapped. //! -//! # Progress Bar -//! Animated progress bars are displayed at the bottom of the terminal. -//! While progress bars are visible, printing still works and will be put -//! above the bars. However, prints will be buffered and refreshed -//! and the same frame rate as the bars. -//! -//! [`progress_bar`](crate::progress_bar) and [`progress_bar_lowp`](crate::progress_bar_lowp) are used to create a bar. -//! The only difference is that `lowp` doesn't print a message when the progress -//! is done (as if the bar was never there). The bar takes a message to indicate -//! the current action, and each update call can accept a message to indicate -//! the current step. When `bar` is dropped, it will print a done message. -//! -//! ```rust,no_run -//! # use pistonite_cu as cu; -//! use std::time::Duration; -//! { -//! let bar = cu::progress_bar(10, "This takes 2.5 seconds"); -//! for i in 0..10 { -//! cu::progress!(&bar, i, "step {i}"); -//! cu::debug!("this is debug message"); -//! std::thread::sleep(Duration::from_millis(250)); -//! } -//! } -//! ``` -//! -//! [`progress_unbounded`](crate::progress_unbounded) and [`progress_unbounded_lowp`](crate::progress_unbounded_lowp) are variants -//! that doesn't display the total steps. Use `()` as the step placeholder -//! when updating the bar. -//! -//! # Prompting -//! With the `prompt` feature enabled, you can -//! use [`prompt!`](crate::prompt) and [`yesno!`](crate::yesno) to show prompts. -//! -//! The prompts are thread-safe, meaning -//! You can call them from multiple threads, and they will be queued to prompt the user one after -//! the other. Prompts are always shown regardless of verbosity. But when stdout is redirected, -//! they will not render in terminal. -//! -//! # Async Entry Point -//! For async usage, see the [`coroutine`](crate::co) concept. -//! -//! # Manual Parsing +//! # Manual Parsing CLI args //! [`cu::cli::try_parse`](crate::cli::try_parse) //! and [`cu::cli::print_help`](crate::cli::print_help) can be useful //! when you want to manually invoke a command parser. These //! respect the `--color` option passed to the program. +//! +//! # Progress Bars +//! See [Progress Bars](fn@crate::progress) +//! +//! # Prompting +//! See [Prompting](macro@crate::prompt) +//! #[cfg(feature = "cli")] mod flags; -#[cfg(feature = "cli")] -pub use flags::{Flags, try_parse, print_help, __run}; #[cfg(all(feature = "coroutine", feature = "cli"))] pub use flags::__co_run; +#[cfg(feature = "cli")] +pub use flags::{__run, Flags, print_help, try_parse}; mod print_init; pub use print_init::level; @@ -176,17 +143,17 @@ mod macros; pub use macros::__print_with_level; mod thread_name; -pub use thread_name::set_thread_name; use thread_name::THREAD_NAME; +pub use thread_name::set_thread_name; mod printer; mod progress; -pub use progress::{progress, ProgressBar, ProgressBarBuilder}; +pub use progress::{ProgressBar, ProgressBarBuilder, progress}; #[cfg(feature = "prompt")] mod prompt; #[cfg(feature = "prompt")] -pub use prompt::{__prompt, __prompt_yesno}; +pub use prompt::{__prompt, __prompt_with_validation, __prompt_yesno}; #[cfg(feature = "prompt-password")] mod password; #[cfg(feature = "prompt-password")] diff --git a/packages/copper/src/cli/print_init.rs b/packages/copper/src/cli/print_init.rs index 2e47e54..da4ddac 100644 --- a/packages/copper/src/cli/print_init.rs +++ b/packages/copper/src/cli/print_init.rs @@ -1,10 +1,11 @@ -use std::cell::RefCell; use std::sync::OnceLock; use std::sync::atomic::Ordering; +use cu::cli::printer::{PRINTER, Printer}; +#[cfg(feature = "prompt")] +use cu::cli::prompt::PROMPT_LEVEL; use cu::lv; -use cu::cli::printer::{Printer, PRINTER}; -use env_filter::{Filter as LogEnvFilter, Builder as LogEnvBuilder}; +use env_filter::{Builder as LogEnvBuilder, Filter as LogEnvFilter}; static LOG_FILTER: OnceLock = OnceLock::new(); /// Set the global log filter @@ -43,7 +44,7 @@ pub fn init_options(color: lv::Color, level: lv::Print, prompt: Option level.into() + _ => level.into(), }; log::set_max_level(log_level); @@ -53,91 +54,91 @@ pub fn init_options(color: lv::Color, level: lv::Print, prompt: Option x, - // None => { - // let is_ci = std::env::var("CI") - // .map(|mut x| { - // x.make_ascii_lowercase(); - // matches!(x.trim(), "true" | "1") - // }) - // .unwrap_or_default(); - // if is_ci { - // lv::Prompt::Block - // } else { - // lv::Prompt::Interactive - // } - // } - // }; - // super::PROMPT_LEVEL.set(prompt) - // } - // #[cfg(not(feature = "prompt"))] - // { - // let _ = prompt; - // super::PROMPT_LEVEL.set(lv::Prompt::No); - // } - // - // lv::PRINT_LEVEL.set(level); - // struct LogImpl; - // impl log::Log for LogImpl { - // fn enabled(&self, metadata: &log::Metadata) -> bool { - // match LOG_FILTER.get() { - // Some(filter) => filter.enabled(metadata), - // None => Lv::from(metadata.level()).can_print(lv::PRINT_LEVEL.get()), - // } - // } - // - // fn log(&self, record: &log::Record) { - // if !self.enabled(record.metadata()) { - // return; - // } - // let typ: Lv = record.level().into(); - // let message = if typ == Lv::Trace { - // // enable source location logging in trace messages - // let mut message = String::new(); - // message.push('['); - // if let Some(p) = record.module_path() { - // // aliased crate, use the shorthand - // if let Some(rest) = p.strip_prefix("pistonite_") { - // message.push_str(rest); - // } else { - // message.push_str(p); - // } - // message.push(' '); - // } - // if let Some(f) = record.file() { - // let name = match f.rfind(['/', '\\']) { - // None => f, - // Some(i) => &f[i + 1..], - // }; - // message.push_str(name); - // } - // if let Some(l) = record.line() { - // message.push(':'); - // message.push_str(&format!("{l}")); - // } - // if message.len() > 1 { - // message += "] "; - // } else { - // message.clear(); - // } - // - // use std::fmt::Write; - // let _: Result<_, _> = write!(&mut message, "{}", record.args()); - // message - // } else { - // record.args().to_string() - // }; - // if let Ok(mut printer) = super::PRINTER.lock() { - // printer.print_message(typ, &message); - // } - // } - // - // fn flush(&self) {} - // } - // - // let _ = log::set_logger(&LogImpl); + #[cfg(feature = "prompt")] + { + let prompt = match prompt { + Some(x) => x, + None => { + let is_ci = std::env::var("CI") + .map(|mut x| { + x.make_ascii_lowercase(); + matches!(x.trim(), "true" | "1") + }) + .unwrap_or_default(); + if is_ci { + lv::Prompt::Block + } else { + lv::Prompt::Interactive + } + } + }; + PROMPT_LEVEL.set(prompt) + } + #[cfg(not(feature = "prompt"))] + { + let _ = prompt; + PROMPT_LEVEL.set(lv::Prompt::No); + } + + lv::PRINT_LEVEL.set(level); + let _ = log::set_logger(&LogImpl); +} +struct LogImpl; +impl log::Log for LogImpl { + fn enabled(&self, metadata: &log::Metadata) -> bool { + match LOG_FILTER.get() { + Some(filter) => filter.enabled(metadata), + None => lv::Lv::from(metadata.level()).can_print(lv::PRINT_LEVEL.get()), + } + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + let typ: lv::Lv = record.level().into(); + let message = if typ == lv::T { + // enable source location logging in trace messages + let mut message = String::new(); + message.push('['); + if let Some(p) = record.module_path() { + // aliased crate, use the shorthand + if let Some(rest) = p.strip_prefix("pistonite_") { + message.push_str(rest); + } else { + message.push_str(p); + } + message.push(' '); + } + if let Some(f) = record.file() { + let name = match f.rfind(['/', '\\']) { + None => f, + Some(i) => &f[i + 1..], + }; + message.push_str(name); + } + if let Some(l) = record.line() { + message.push(':'); + message.push_str(&format!("{l}")); + } + if message.len() > 1 { + message += "] "; + } else { + message.clear(); + } + + use std::fmt::Write; + let _: Result<_, _> = write!(&mut message, "{}", record.args()); + message + } else { + record.args().to_string() + }; + if let Ok(mut printer) = PRINTER.lock() { + if let Some(printer) = printer.as_mut() { + printer.print_message(typ, &message); + } + } + } + + fn flush(&self) {} } diff --git a/packages/copper/src/cli/printer.rs b/packages/copper/src/cli/printer.rs index 5862848..d3a5189 100644 --- a/packages/copper/src/cli/printer.rs +++ b/packages/copper/src/cli/printer.rs @@ -1,14 +1,14 @@ -use std::io::{self, IsTerminal as _}; use std::collections::VecDeque; +use std::io::{self, IsTerminal as _}; use std::ops::ControlFlow; use std::sync::{Arc, Mutex, Weak}; use std::thread::JoinHandle; -use oneshot::{Sender as OnceSend, Receiver as OnceRecv}; +use oneshot::{Receiver as OnceRecv, Sender as OnceSend}; -use crate::cli::{THREAD_NAME, Tick, TICK_INTERVAL, password}; -use crate::cli::fmt::{self, ansi, FormatBuffer}; -use crate::cli::progress::{ProgressBar, BarResult, BarFormatter}; +use crate::cli::fmt::{self, FormatBuffer, ansi}; +use crate::cli::progress::{BarFormatter, BarResult, ProgressBar}; +use crate::cli::{THREAD_NAME, TICK_INTERVAL, Tick, password}; use crate::lv; /// Global printer state @@ -23,12 +23,12 @@ pub(crate) struct Printer { colors: ansi::Colors, /// Control codes controls: ansi::Controls, - + print_task: PrintingThread, bar_target: Option, bars: Vec>, pending_prompts: VecDeque, - + /// Buffer for automatically do certain formatting format_buffer: FormatBuffer, /// Place to buffer prints while printing is blocked @@ -54,19 +54,19 @@ impl Printer { (bar_target, is_terminal) }; let controls = ansi::controls(is_terminal); - + Self { is_stdin_terminal, stdout, stderr, colors, controls, - + print_task: Default::default(), bar_target, bars: Default::default(), pending_prompts: Default::default(), - + format_buffer: FormatBuffer::new(), buffered: String::new(), } @@ -344,10 +344,10 @@ fn print_task() -> JoinHandle<()> { // first check if there are any pending prompts // scope for locking the printer for checking prompts { - let Ok(mut printer) = PRINTER.lock() else { + let Ok(mut printer_guard) = PRINTER.lock() else { return ControlFlow::Break(()); }; - let Some(printer) = printer.as_mut() else { + let Some(printer) = printer_guard.as_mut() else { return ControlFlow::Break(()); }; let task = printer.pending_prompts.pop_front(); @@ -360,7 +360,7 @@ fn print_task() -> JoinHandle<()> { let _ = printer.stdout.flush(); // drop the lock while we wait for user input - drop(printer); + drop(printer_guard); // if there is a prompt, don't clear the previous progress bar yet, // since we want to display the prompt after the progress bars @@ -415,7 +415,7 @@ fn print_task() -> JoinHandle<()> { }; let Some(printer) = printer.as_mut() else { return ControlFlow::Break(()); - }; + }; if let Some(bar_target) = printer.bar_target { // print the bars, after processing buffered messages diff --git a/packages/copper/src/cli/progress/builder.rs b/packages/copper/src/cli/progress/builder.rs index cad2dc3..55616a6 100644 --- a/packages/copper/src/cli/progress/builder.rs +++ b/packages/copper/src/cli/progress/builder.rs @@ -1,7 +1,7 @@ -use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; -use crate::cli::progress::{StateImmut, ProgressBar, Estimater, State}; +use crate::cli::progress::{Estimater, ProgressBar, State, StateImmut}; /// Builder for a progress bar #[derive(Debug, Clone)] // Clone sometimes needed to build by ref.. without unsafe @@ -29,7 +29,7 @@ pub struct ProgressBarBuilder { } impl ProgressBarBuilder { - /// Start building a progress bar. Note [`cu::progress`](progress) is the canonical shorthand + /// Start building a progress bar. Note [`cu::progress`](fn@crate::progress) is the canonical shorthand pub fn new(message: String) -> Self { Self { message, @@ -76,7 +76,7 @@ impl ProgressBarBuilder { /// ``` #[inline(always)] pub fn total_bytes(mut self, total: u64) -> Self { - self.total = Some(total as u64); + self.total = Some(total); self.total_is_in_bytes = true; self } diff --git a/packages/copper/src/cli/progress/macros.rs b/packages/copper/src/cli/progress/macros.rs index 19673c2..6e7b101 100644 --- a/packages/copper/src/cli/progress/macros.rs +++ b/packages/copper/src/cli/progress/macros.rs @@ -1,4 +1,3 @@ - /// Update a [progress bar](fn@crate::progress) /// /// The macro takes 2 parts separated by comma `,`: diff --git a/packages/copper/src/cli/progress/mod.rs b/packages/copper/src/cli/progress/mod.rs index c109fbd..4e85c3d 100644 --- a/packages/copper/src/cli/progress/mod.rs +++ b/packages/copper/src/cli/progress/mod.rs @@ -116,7 +116,7 @@ use state::{State, StateImmut}; mod builder; pub use builder::ProgressBarBuilder; mod util; -pub use util::{BarResult, BarFormatter}; +pub use util::{BarFormatter, BarResult}; use util::{ChildState, ChildStateStrong}; mod macros; diff --git a/packages/copper/src/cli/progress/state.rs b/packages/copper/src/cli/progress/state.rs index f4ffb51..8ce8f90 100644 --- a/packages/copper/src/cli/progress/state.rs +++ b/packages/copper/src/cli/progress/state.rs @@ -2,9 +2,11 @@ use std::sync::{Arc, Mutex}; use std::time::Instant; use crate::cli::Tick; -use crate::cli::progress::{ChildState,Estimater,BarFormatter, BarResult, ProgressBarBuilder, ChildStateStrong}; use crate::cli::fmt::ansi; use crate::cli::printer::PRINTER; +use crate::cli::progress::{ + BarFormatter, BarResult, ChildState, ChildStateStrong, Estimater, ProgressBarBuilder, +}; const CHAR_BAR_TICK: char = '\u{251C}'; // |> const CHAR_BAR: char = '\u{2502}'; // | @@ -19,9 +21,14 @@ pub struct ProgressBar { state_mut: Mutex, } impl ProgressBar { - pub(crate) fn spawn(state: StateImmut, state_mut: State, parent: Option>) -> Arc { + pub(crate) fn spawn( + state: StateImmut, + state_mut: State, + parent: Option>, + ) -> Arc { let bar = Arc::new(Self { - state, state_mut: Mutex::new(state_mut) + state, + state_mut: Mutex::new(state_mut), }); match parent { Some(p) => { @@ -54,7 +61,7 @@ impl ProgressBar { #[inline(always)] pub fn __inc(self: &Arc, amount: u64, message: Option) { if let Ok(mut bar) = self.state_mut.lock() { - bar.unreal_current.saturating_add(amount); + bar.unreal_current = bar.unreal_current.saturating_add(amount); if let Some(x) = message { bar.set_message(&x); } @@ -508,7 +515,12 @@ impl State { // _: fmt for string does not fail let _ = match total { None => write!(temp, "{}", cu::ByteFormat(current)), - Some(total) => write!(temp, "{} / {}", cu::ByteFormat(current), cu::ByteFormat(total)), + Some(total) => write!( + temp, + "{} / {}", + cu::ByteFormat(current), + cu::ByteFormat(total) + ), }; if width >= temp.len() { @@ -601,4 +613,3 @@ fn format_message_with_width(out: &mut String, mut width: usize, message: &str) } width } - diff --git a/packages/copper/src/cli/progress/util.rs b/packages/copper/src/cli/progress/util.rs index faa9c68..62d1ccf 100644 --- a/packages/copper/src/cli/progress/util.rs +++ b/packages/copper/src/cli/progress/util.rs @@ -5,7 +5,6 @@ use crate::cli::Tick; use crate::cli::fmt::ansi; use crate::cli::progress::ProgressBar; - #[derive(Debug)] pub enum ChildState { /// The done message (if `keep` is true) diff --git a/packages/copper/src/cli/prompt.rs b/packages/copper/src/cli/prompt.rs index 58496e0..9deb538 100644 --- a/packages/copper/src/cli/prompt.rs +++ b/packages/copper/src/cli/prompt.rs @@ -1,28 +1,25 @@ - -use crate::{Atomic, Context as _}; -use crate::lv; use crate::cli::printer::PRINTER; +use crate::lv; +use crate::{Atomic, Context as _}; pub(crate) static PROMPT_LEVEL: Atomic = Atomic::new_u8(lv::Prompt::Interactive as u8); #[doc(hidden)] -pub fn __prompt_yesno( - message: std::fmt::Arguments<'_>, -) -> crate::Result { +pub fn __prompt_yesno(message: std::fmt::Arguments<'_>) -> crate::Result { match check_prompt_level(true) { - Ok(false) => {}, + Ok(false) => {} other => return other, }; let mut answer = false; - prompt_with_validation_impl(format_args!("{message} [y/n]"), false, |x| { + __prompt_with_validation(format_args!("{message} [y/n]"), false, |x| { x.make_ascii_lowercase(); match x.trim() { "y" | "yes" => { answer = true; Ok(true) - } - "n" | "no" => { + } + "n" | "no" => { answer = false; Ok(true) } @@ -36,18 +33,16 @@ pub fn __prompt_yesno( } #[doc(hidden)] -pub fn __prompt( - message: std::fmt::Arguments<'_>, - is_password: bool, -) -> cu::Result { +pub fn __prompt(message: std::fmt::Arguments<'_>, is_password: bool) -> cu::Result { check_prompt_level(false)?; prompt_impl(&format!("{message}"), is_password) } -fn prompt_with_validation_impl crate::Result>( +#[doc(hidden)] +pub fn __prompt_with_validation crate::Result>( message: std::fmt::Arguments<'_>, is_password: bool, - mut validator: F + mut validator: F, ) -> cu::Result { let message = format!("{message}"); loop { @@ -58,14 +53,13 @@ fn prompt_with_validation_impl crate::Result>( } } -fn prompt_impl( - message: &str, - is_password: bool, -) -> cu::Result { +fn prompt_impl(message: &str, is_password: bool) -> cu::Result { let recv = { - if let Ok(mut printer) = PRINTER.lock() && let Some(printer) = printer.as_mut() { + if let Ok(mut printer) = PRINTER.lock() + && let Some(printer) = printer.as_mut() + { printer.show_prompt(message, is_password) - } else { + } else { crate::bail!("prompt failed: failed to lock global printer"); } }; @@ -82,10 +76,13 @@ fn check_prompt_level(is_yesno: bool) -> crate::Result { // do not even show the prompt if --yes lv::Prompt::YesOrInteractive | lv::Prompt::YesOrBlock => return Ok(true), lv::Prompt::Interactive => return Ok(false), - lv::Prompt::Block => { } + lv::Prompt::Block => {} } } else { - if !matches!(PROMPT_LEVEL.get(), lv::Prompt::YesOrBlock | lv::Prompt::Block) { + if !matches!( + PROMPT_LEVEL.get(), + lv::Prompt::YesOrBlock | lv::Prompt::Block + ) { return Ok(false); } } diff --git a/packages/copper/src/error_handling.rs b/packages/copper/src/error_handling.rs index 3eaf40d..667b25e 100644 --- a/packages/copper/src/error_handling.rs +++ b/packages/copper/src/error_handling.rs @@ -1,5 +1,4 @@ -pub use anyhow::{Context, Error, Ok, Result, bail, anyhow as fmterr}; - +pub use anyhow::{Context, Error, Ok, Result, anyhow as fmterr, bail}; /// # Error Handling /// *Does not require any feature flag. Please make sure to sponsor [David Tolnay](https://github.com/dtolnay) if you depend heavily on his work diff --git a/packages/copper/src/lib.rs b/packages/copper/src/lib.rs index 7d9da5e..e6edafd 100644 --- a/packages/copper/src/lib.rs +++ b/packages/copper/src/lib.rs @@ -63,6 +63,7 @@ //! - [Printting and Command Line Interface](mod@crate::cli) (CLI arg parsing via //! [`clap`](https://docs.rs/clap)) //! - [Progress Bars](fn@crate::progress) +//! - [Prompting](macro@crate::prompt) //! //! # Feature Reference: //! - `coroutine` and `coroutine-heavy`: @@ -99,15 +100,13 @@ pub use lv::{debug, error, info, trace, warn}; // --- Command Line Interface (print/cli/prompt/prompt-password feature) --- #[cfg(feature = "print")] pub mod cli; -#[cfg(feature = "print")] -pub use cli::{progress, ProgressBar, ProgressBarBuilder}; #[cfg(feature = "prompt-password")] pub use cli::password_chars_legal; +#[cfg(feature = "print")] +pub use cli::{ProgressBar, ProgressBarBuilder, progress}; #[cfg(feature = "cli")] pub use pistonite_cu_proc_macros::cli; - - #[cfg(feature = "process")] mod process; #[cfg(feature = "process")] @@ -132,7 +131,6 @@ pub type BoxedFuture = std::pin::Pin + Send + 'sta #[cfg(feature = "coroutine")] pub mod co; - /// Parsing utilities #[cfg(feature = "parse")] mod parse; @@ -144,7 +142,7 @@ pub use pistonite_cu_proc_macros::Parse; // re-exports from libraries pub use pistonite_cu_proc_macros::error_ctx; #[cfg(feature = "coroutine")] -pub use tokio::{join, try_join, select}; +pub use tokio::{join, select, try_join}; #[doc(hidden)] pub mod __priv { diff --git a/packages/copper/src/process/pio/cargo_preset.rs b/packages/copper/src/process/pio/cargo_preset.rs index 94aec7a..45eef55 100644 --- a/packages/copper/src/process/pio/cargo_preset.rs +++ b/packages/copper/src/process/pio/cargo_preset.rs @@ -325,33 +325,29 @@ impl PrintState { let Some(rendered) = message.rendered else { return; }; - todo!() - // match message.level { - // Some("warning") => match &self.diagnostic_hook { - // None => { - // // crate::__priv::__print_with_level( - // // self.warning_lv, - // // format_args!("{rendered}"), - // // ); - // } - // Some(hook) => hook(true, &rendered), - // }, - // Some("error") => match &self.diagnostic_hook { - // None => { - // crate::__priv::__print_with_level( - // self.error_lv, - // format_args!("{rendered}"), - // ); - // } - // Some(hook) => hook(false, &rendered), - // }, - // _ => { - // crate::__priv::__print_with_level( - // self.other_lv, - // format_args!("{rendered}"), - // ); - // } - // } + match message.level { + Some("warning") => match &self.diagnostic_hook { + None => { + crate::cli::__print_with_level( + self.warning_lv, + format_args!("{rendered}"), + ); + } + Some(hook) => hook(true, &rendered), + }, + Some("error") => match &self.diagnostic_hook { + None => { + crate::cli::__print_with_level( + self.error_lv, + format_args!("{rendered}"), + ); + } + Some(hook) => hook(false, &rendered), + }, + _ => { + crate::cli::__print_with_level(self.other_lv, format_args!("{rendered}")); + } + } } "build-finished" => match payload.success { Some(true) => { @@ -370,51 +366,50 @@ impl PrintState { } fn handle_stderr(&mut self, line: &str) { - todo!() - // static STATUS_REGEX: LazyLock = LazyLock::new(|| { - // Regex::new("^((\x1b[^m]*m)|\\s)*(Compiling|Checking)((\x1b[^m]*m)|\\s)*").unwrap() - // }); - // static ERROR_REGEX: LazyLock = - // LazyLock::new(|| Regex::new("^((\x1b[^m]*m)|\\s)*error").unwrap()); - // static WARNING_REGEX: LazyLock = - // LazyLock::new(|| Regex::new("^((\x1b[^m]*m)|\\s)*warning").unwrap()); - // let Some(m) = STATUS_REGEX.find(line) else { - // // some error/warning messages aren't emited to stdout, - // // so we use a regex to match and print them - // if let Some(lv) = self.stderr_printing_message_lv { - // // since the message might be multi-line, we - // // keep printing until a status message is matched - // crate::__priv::__print_with_level(lv, format_args!("{line}")); - // return; - // } - // // check if the message matches error/warning - // if ERROR_REGEX.is_match(line) { - // crate::__priv::__print_with_level(self.error_lv, format_args!("{line}")); - // self.stderr_printing_message_lv = Some(self.error_lv); - // return; - // } - // if WARNING_REGEX.is_match(line) { - // crate::__priv::__print_with_level(self.warning_lv, format_args!("{line}")); - // self.stderr_printing_message_lv = Some(self.warning_lv); - // return; - // } - // // print as other message - // crate::__priv::__print_with_level(self.other_lv, format_args!("{line}")); - // return; - // }; - // // print the status message as other, and clear the error/warning message state - // crate::__priv::__print_with_level(self.other_lv, format_args!("{line}")); - // self.stderr_printing_message_lv = None; - // - // // process the status message - // let line = &line[m.end()..].trim(); - // // crate name can't have space (right?) - // let crate_name = match line.find(' ') { - // None => line, - // Some(i) => &line[..i], - // }; - // self.in_progress.insert(crate_name.replace('-', "_")); - // self.update_bar(); + static STATUS_REGEX: LazyLock = LazyLock::new(|| { + Regex::new("^((\x1b[^m]*m)|\\s)*(Compiling|Checking)((\x1b[^m]*m)|\\s)*").unwrap() + }); + static ERROR_REGEX: LazyLock = + LazyLock::new(|| Regex::new("^((\x1b[^m]*m)|\\s)*error").unwrap()); + static WARNING_REGEX: LazyLock = + LazyLock::new(|| Regex::new("^((\x1b[^m]*m)|\\s)*warning").unwrap()); + let Some(m) = STATUS_REGEX.find(line) else { + // some error/warning messages aren't emited to stdout, + // so we use a regex to match and print them + if let Some(lv) = self.stderr_printing_message_lv { + // since the message might be multi-line, we + // keep printing until a status message is matched + crate::cli::__print_with_level(lv, format_args!("{line}")); + return; + } + // check if the message matches error/warning + if ERROR_REGEX.is_match(line) { + crate::cli::__print_with_level(self.error_lv, format_args!("{line}")); + self.stderr_printing_message_lv = Some(self.error_lv); + return; + } + if WARNING_REGEX.is_match(line) { + crate::cli::__print_with_level(self.warning_lv, format_args!("{line}")); + self.stderr_printing_message_lv = Some(self.warning_lv); + return; + } + // print as other message + crate::cli::__print_with_level(self.other_lv, format_args!("{line}")); + return; + }; + // print the status message as other, and clear the error/warning message state + crate::cli::__print_with_level(self.other_lv, format_args!("{line}")); + self.stderr_printing_message_lv = None; + + // process the status message + let line = &line[m.end()..].trim(); + // crate name can't have space (right?) + let crate_name = match line.find(' ') { + None => line, + Some(i) => &line[..i], + }; + self.in_progress.insert(crate_name.replace('-', "_")); + self.update_bar(); } fn update_bar(&mut self) { diff --git a/packages/copper/src/process/pio/print.rs b/packages/copper/src/process/pio/print.rs index 3ebb62a..7b39eba 100644 --- a/packages/copper/src/process/pio/print.rs +++ b/packages/copper/src/process/pio/print.rs @@ -71,10 +71,9 @@ impl PrintTask { loop { match driver.next().await { DriverOutput::Line(line) => { - todo!() - // for l in line.lines() { - // crate::__priv::__print_with_level(lv, format_args!("{prefix}{l}")); - // } + for l in line.lines() { + crate::cli::__print_with_level(lv, format_args!("{prefix}{l}")); + } } DriverOutput::Done => break, _ => {} diff --git a/packages/copper/src/process/pio/spinner.rs b/packages/copper/src/process/pio/spinner.rs index 27ce0a0..057711d 100644 --- a/packages/copper/src/process/pio/spinner.rs +++ b/packages/copper/src/process/pio/spinner.rs @@ -5,7 +5,7 @@ use spin::mutex::SpinMutex; use tokio::process::{Child as TokioChild, ChildStderr, ChildStdout, Command as TokioCommand}; use crate::lv::Lv; -use crate::{Atomic, ProgressBar, ProgressBarBuilder, BoxedFuture}; +use crate::{Atomic, BoxedFuture, ProgressBar, ProgressBarBuilder}; use super::{ChildOutConfig, ChildOutTask, Driver, DriverOutput}; @@ -174,14 +174,13 @@ impl SpinnerTask { loop { match driver.next().await { DriverOutput::Line(line) => { - todo!() - // if lv != Lv::Off { - // crate::__priv::__print_with_level(lv, format_args!("{prefix}{line}")); - // // erase the progress line if we decide to print it out - // crate::progress!(bar, "") - // } else { - // crate::progress!(bar, "{line}") - // } + if lv != Lv::Off { + crate::cli::__print_with_level(lv, format_args!("{prefix}{line}")); + // erase the progress line if we decide to print it out + crate::progress!(bar, "") + } else { + crate::progress!(bar, "{line}") + } } DriverOutput::Progress(line) => { crate::progress!(bar, "{line}") diff --git a/packages/copper/src/str/byte_format.rs b/packages/copper/src/str/byte_format.rs index 086efa5..afab19a 100644 --- a/packages/copper/src/str/byte_format.rs +++ b/packages/copper/src/str/byte_format.rs @@ -7,10 +7,10 @@ pub struct ByteFormat(pub u64); impl std::fmt::Display for ByteFormat { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for (unit_bytes, unit_char) in [ - (1000_000_000_000, 'T'), - (1000_000_000, 'G'), - (1000_000, 'M'), - (1000, 'k'), + (1_000_000_000_000, 'T'), + (1_000_000_000, 'G'), + (1_000_000, 'M'), + (1_000, 'k'), ] { if self.0 >= unit_bytes { let whole = self.0 / unit_bytes; diff --git a/packages/copper/src/str/mod.rs b/packages/copper/src/str/mod.rs index 901d048..dfc36f6 100644 --- a/packages/copper/src/str/mod.rs +++ b/packages/copper/src/str/mod.rs @@ -1,5 +1,5 @@ mod zstring; -pub use zstring::{zero_string, ZString}; +pub use zstring::{ZString, zero_string}; mod byte_format; pub use byte_format::ByteFormat; diff --git a/packages/copper/src/str/zstring.rs b/packages/copper/src/str/zstring.rs index b98fd84..50de64e 100644 --- a/packages/copper/src/str/zstring.rs +++ b/packages/copper/src/str/zstring.rs @@ -1,4 +1,3 @@ - /// A string that will have its inner buffer zeroed when dropped #[derive(Default, Clone)] pub struct ZString(String); @@ -65,7 +64,7 @@ pub fn zero_string(s: &mut String) { // SAFETY: c is a valid u8 pointer unsafe { std::ptr::write_volatile(c, 0) }; } - // ensure the + // ensure other threads see this change std::sync::atomic::fence(std::sync::atomic::Ordering::SeqCst); std::sync::atomic::compiler_fence(std::sync::atomic::Ordering::SeqCst); } diff --git a/packages/copper/tests/error_ctx.rs b/packages/copper/tests/error_ctx.rs index f715eb9..9fcfa82 100644 --- a/packages/copper/tests/error_ctx.rs +++ b/packages/copper/tests/error_ctx.rs @@ -57,7 +57,7 @@ async fn test_example3_ok() { #[cu::error_ctx("async failed with arg {}", s)] async fn example3(s: u32) -> cu::Result<()> { let value = returns_ok(s)?; - cu::ensure!(value > 4); + cu::ensure!(value > 4)?; Ok(()) } @@ -82,7 +82,7 @@ async fn test_example4_ok() { #[cu::error_ctx(pre, format("async failed with arg {s}"))] async fn example4(s: String) -> cu::Result<()> { let value = returns_ok(s)?; - cu::ensure!(!value.is_empty()); + cu::ensure!(!value.is_empty())?; Ok(()) } From dcf014bd3d0e76efbfbe10a14cd3b1e8430a27dc Mon Sep 17 00:00:00 2001 From: Pistonight Date: Sat, 10 Jan 2026 23:22:11 -0800 Subject: [PATCH 07/13] checking prompt features --- packages/copper/examples/prompt_password.rs | 76 ++++++++++++++++++- packages/copper/src/cli/flags.rs | 9 ++- packages/copper/src/cli/macros.rs | 14 +++- packages/copper/src/cli/progress/macros.rs | 2 +- packages/copper/src/error_handling.rs | 2 +- .../copper/src/process/pio/cargo_preset.rs | 6 +- packages/copper/tests/error_ctx.rs | 4 +- 7 files changed, 98 insertions(+), 15 deletions(-) diff --git a/packages/copper/examples/prompt_password.rs b/packages/copper/examples/prompt_password.rs index bf19eea..cbd3f73 100644 --- a/packages/copper/examples/prompt_password.rs +++ b/packages/copper/examples/prompt_password.rs @@ -3,8 +3,78 @@ use pistonite_cu as cu; // can only manually test this because password not prompted from stdin #[cu::cli] fn main(_: cu::cli::Flags) -> cu::Result<()> { - cu::hint!("testing prompt password"); - let answer = cu::prompt_password!("enter password")?; - cu::info!("you answered: {answer}"); + if !cu::yesno!("do you want to continue?")? { + return Ok(()); + } + cu::info!("user picked yes"); + + let name = cu::prompt!("please enter your name")?; + cu::info!("hi, {name}"); + let password = cu::prompt_password!("please enter your password")?; + cu::info!("user entered: {password}"); + + let expected = "rust"; + let answer = cu::prompt_validate!( + ("what's your favorite programming language? please answer {}", expected), + |answer| { + if answer == expected { + return Ok(true); + } + if answer == "javascript" { + cu::bail!("that's not good"); + } + cu::error!("try again"); + Ok(false) + } + )?; + cu::ensure!(answer == expected)?; +let mut index: i32 = 0; +cu::prompt_validate!( + "select a number between 0 and 5", + |answer| { + let number = match cu::parse::(answer) { + Err(e) => { + cu::error!("{e}"); + cu::hint!("please ensure you are entering a number"); + return Ok(false); + } + Ok(x) => x + }; + if number < 0 { + cu::error!("the number you entered is too small"); + return Ok(false); + } + if number > 5 { + cu::error!("the number you entered is too big"); + return Ok(false); + } + index = number; + Ok(true) + } +)?; +cu::info!("index is {index}"); + +let password = cu::prompt_password_validate!( + "please enter a password between 8 and 16 charactres and only contain sensible characters", + |answer| { + if answer == "123456" { + cu::bail!("how can you do that, bye"); + } + if answer.len() < 8 { + cu::error!("password is too short"); + return Ok(false); + } + if answer.len() > 16 { + cu::error!("password is too long"); + return Ok(false); + } + if let Err(e) = cu::password_chars_legal(answer) { + cu::error!("invalid password: {e}"); + return Ok(false); + } + Ok(true) + } +)?; +cu::print!("{password}"); Ok(()) } diff --git a/packages/copper/src/cli/flags.rs b/packages/copper/src/cli/flags.rs index 8d5edf3..dc2856d 100644 --- a/packages/copper/src/cli/flags.rs +++ b/packages/copper/src/cli/flags.rs @@ -262,7 +262,14 @@ fn handle_result(start: Instant, result: crate::Result<()>) -> std::process::Exi let elapsed = start.elapsed().as_secs_f32(); if let Err(e) = result { crate::error!("fatal: {e:?}"); - if crate::lv::is_trace_hint_enabled() { + // we display the hint for user to use -vv + // if: + // - the user is already tried to get more debug info with -v + // (because otherwise it will be too noisy and it might be a user-error, + // not a bug + // - the trace hint is not explicitly disabled + // - the trace hint is not already displayed + if lv::D.enabled() && crate::lv::is_trace_hint_enabled() { if std::env::var("RUST_BACKTRACE") .unwrap_or_default() .is_empty() diff --git a/packages/copper/src/cli/macros.rs b/packages/copper/src/cli/macros.rs index 2ca3802..4001d71 100644 --- a/packages/copper/src/cli/macros.rs +++ b/packages/copper/src/cli/macros.rs @@ -30,6 +30,7 @@ macro_rules! hint { /// # use pistonite_cu as cu; /// let name = cu::prompt!("please enter your name")?; /// cu::info!("user entered: {name}"); +/// # cu::Ok(()) /// ``` /// /// See other macros for advanced usage: @@ -74,6 +75,7 @@ macro_rules! prompt { /// if cu::yesno!("do you want to continue?")? { /// cu::info!("user picked yes"); /// } +/// # cu::Ok(()) /// ``` #[cfg(feature = "prompt")] #[macro_export] @@ -92,6 +94,7 @@ macro_rules! yesno { /// # use pistonite_cu as cu; /// let password = cu::prompt_password!("please enter your password")?; /// cu::info!("user entered: {password}"); +/// # cu::Ok(()) /// ``` #[cfg(feature = "prompt-password")] #[macro_export] @@ -125,10 +128,11 @@ macro_rules! prompt_password { /// cu::bail!("that's not good"); /// } /// cu::error!("try again"); -/// Ok(false); +/// Ok(false) /// } /// )?; /// assert!(answer == expected); +/// # cu::Ok(()) /// ``` /// /// The validation function can be a `FnMut` closure, which means @@ -152,15 +156,16 @@ macro_rules! prompt_password { /// cu::error!("the number you entered is too small"); /// return Ok(false); /// } -/// if number > 0 { +/// if number > 5 { /// cu::error!("the number you entered is too big"); /// return Ok(false); /// } /// index = number; -/// Ok(true); +/// Ok(true) /// } /// )?; /// cu::info!("index is {index}"); +/// # cu::Ok(()) /// ``` /// /// For the password version, see [`prompt_password_validate`](crate::prompt_password_validate) @@ -191,7 +196,7 @@ macro_rules! prompt_validate { /// # use pistonite_cu as cu; /// // note that extra parenthesis is needed if the format args /// // are not inlined into the formatting literal -/// let password = cu::prompt_validate!( +/// let password = cu::prompt_password_validate!( /// "please enter a password between 8 and 16 charactres and only contain sensible characters", /// |answer| { /// if answer == "123456" { @@ -213,6 +218,7 @@ macro_rules! prompt_validate { /// } /// )?; /// cu::print!("{password}"); +/// # cu::Ok(()) /// ``` #[cfg(feature = "prompt-password")] #[macro_export] diff --git a/packages/copper/src/cli/progress/macros.rs b/packages/copper/src/cli/progress/macros.rs index 6e7b101..dadced8 100644 --- a/packages/copper/src/cli/progress/macros.rs +++ b/packages/copper/src/cli/progress/macros.rs @@ -17,7 +17,7 @@ /// # Examples /// ```rust,no_run /// # use pistonite_cu as cu; -/// let bar = cu::progress_bar(10, "10 steps"); +/// let bar = cu::progress("10 steps").total(10).spawn(); /// // update the current count and message /// let i = 1; /// cu::progress!(bar = i, "doing step {i}"); diff --git a/packages/copper/src/error_handling.rs b/packages/copper/src/error_handling.rs index 667b25e..add5e14 100644 --- a/packages/copper/src/error_handling.rs +++ b/packages/copper/src/error_handling.rs @@ -11,7 +11,7 @@ pub use anyhow::{Context, Error, Ok, Result, anyhow as fmterr, bail}; /// /// The traits required for error handling are included in the prelude import /// ```rust -/// # use pistonite_cu as cu +/// # use pistonite_cu as cu; /// use cu::pre::*; /// ``` /// diff --git a/packages/copper/src/process/pio/cargo_preset.rs b/packages/copper/src/process/pio/cargo_preset.rs index 45eef55..dab7fd2 100644 --- a/packages/copper/src/process/pio/cargo_preset.rs +++ b/packages/copper/src/process/pio/cargo_preset.rs @@ -22,7 +22,7 @@ use crate::{BoxedFuture, ProgressBar, ProgressBarBuilder}; /// # fn main() -> cu::Result<()> { /// cu::which("cargo")?.command() /// .args(["build", "--release"]) -/// .preset(cu::pio::cargo()) +/// .preset(cu::pio::cargo("building my crate")) /// .spawn()?.0 /// .wait_nz()?; /// # Ok(()) } @@ -49,7 +49,7 @@ use crate::{BoxedFuture, ProgressBar, ProgressBarBuilder}; /// # use pistonite_cu as cu; /// use cu::pre::*; /// -/// cu::pio::cargo() +/// cu::pio::cargo("cargo build") /// // configure message levels; levels shown here are the default /// .error(cu::lv::E) /// .warning(cu::lv::W) @@ -66,7 +66,7 @@ use crate::{BoxedFuture, ProgressBar, ProgressBarBuilder}; /// # use pistonite_cu as cu; /// use cu::pre::*; /// -/// cu::pio::cargo() +/// cu::pio::cargo("cargo build") /// // configure message levels; levels shown here are the default /// .on_diagnostic(|is_warning, message| { /// // this implementation will be identical to the default behavior diff --git a/packages/copper/tests/error_ctx.rs b/packages/copper/tests/error_ctx.rs index 9fcfa82..065e7f7 100644 --- a/packages/copper/tests/error_ctx.rs +++ b/packages/copper/tests/error_ctx.rs @@ -44,7 +44,7 @@ async fn test_example3_err() { r"async failed with arg 4 Caused by: - Condition failed: `value > 4` (4 vs 4)" + condition failed: `value > 4`" ) } @@ -69,7 +69,7 @@ async fn test_example4_err() { r"async failed with arg Caused by: - Condition failed: `!value.is_empty()`" + condition failed: `!value.is_empty()`" ) } From 9e8ac093e81e334e1407b70e92bd903b6f6f6da0 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Sun, 11 Jan 2026 01:34:05 -0800 Subject: [PATCH 08/13] refactored co --- .../src/{error_ctx.rs => context.rs} | 0 packages/copper-proc-macros/src/lib.rs | 147 +++++++++++++++++- packages/copper/examples/prompt_password.rs | 60 +++---- packages/copper/src/cli/flags.rs | 3 + packages/copper/src/co.rs | 129 --------------- .../src/{async_/mod.rs => co/co_util.rs} | 7 - packages/copper/src/{async_ => co}/handle.rs | 12 +- packages/copper/src/co/mod.rs | 89 +++++++++++ packages/copper/src/{async_ => co}/pool.rs | 8 +- packages/copper/src/{async_ => co}/runtime.rs | 19 ++- packages/copper/src/error_handling.rs | 59 +++++++ packages/copper/src/fs/dir.rs | 2 +- packages/copper/src/fs/file.rs | 2 +- packages/copper/src/lib.rs | 32 ++-- .../copper/tests/{error_ctx.rs => context.rs} | 10 +- packages/terminal-tests/Cargo.toml | 1 - .../examples/prompt_password.rs | 8 - packages/terminal-tests/output/prompt-0.txt | 7 +- packages/terminal-tests/output/prompt-1.txt | 6 +- packages/terminal-tests/output/prompt-4.txt | 2 - packages/terminal-tests/output/prompt-6.txt | 2 +- packages/terminal-tests/src/main.rs | 57 +++++-- 22 files changed, 410 insertions(+), 252 deletions(-) rename packages/copper-proc-macros/src/{error_ctx.rs => context.rs} (100%) delete mode 100644 packages/copper/src/co.rs rename packages/copper/src/{async_/mod.rs => co/co_util.rs} (83%) rename packages/copper/src/{async_ => co}/handle.rs (97%) create mode 100644 packages/copper/src/co/mod.rs rename packages/copper/src/{async_ => co}/pool.rs (97%) rename packages/copper/src/{async_ => co}/runtime.rs (87%) rename packages/copper/tests/{error_ctx.rs => context.rs} (89%) delete mode 100644 packages/terminal-tests/examples/prompt_password.rs diff --git a/packages/copper-proc-macros/src/error_ctx.rs b/packages/copper-proc-macros/src/context.rs similarity index 100% rename from packages/copper-proc-macros/src/error_ctx.rs rename to packages/copper-proc-macros/src/context.rs diff --git a/packages/copper-proc-macros/src/lib.rs b/packages/copper-proc-macros/src/lib.rs index 9803a4a..eb2eaa9 100644 --- a/packages/copper-proc-macros/src/lib.rs +++ b/packages/copper-proc-macros/src/lib.rs @@ -1,6 +1,143 @@ use pm::pre::*; -/// See documentation for [`cu::cli`](../pistonite-cu/cli/index.html) module +/// **For the Command Line Interface feature set, +/// please refer to the [`cu::cli`](../pistonite-cu/cli/index.html) module.** +/// +/// This is the documentation for the `#[cu::cli]` macro. +/// +/// By annotating the main function, this macro generates +/// a shim that will reference the `cu::cli::Flags` command line +/// arguments and initialize logging, printing, and prompting +/// systems accordingly. +/// +/// The main function can be async or sync. It should +/// return a `cu::Result` +/// ```rust,ignore +/// #[cu::cli] +/// fn main(flags: cu::cli::Flags) -> cu::Result<()> { +/// cu::debug!("flags are {flags:?}"); +/// Ok(()) +/// } +/// ``` +/// +/// To also define your own flags using the [`clap`](https://docs.rs/clap) +/// crate, define a CLI struct that derives `clap::Parser`. +/// Note the prelude import (`cu::pre::*`) automatically +/// brings `clap` into scope. You don't even need to add it +/// to `Cargo.toml`! +/// +/// Make sure to `#[clap(flatten)]` the flags into your struct. +/// +/// ```rust,ignore +/// # use pistonite_cu as cu; +/// use cu::pre::*; +/// /// My program +/// /// +/// /// This is my program, it is very good. +/// #[derive(clap::Parser, Clone)] +/// struct Args { +/// /// Input of the program +/// #[clap(short, long)] +/// input: String, +/// /// Output of the program +/// #[clap(short, long)] +/// output: Option, +/// #[clap(flatten)] +/// inner: cu::cli::Flags, +/// } +/// ``` +/// Now, to tell `cu` where to look for the flags, +/// specify the name of the field with `flags = "field"` +/// ```rust,ignore +/// // use the flags attribute to refer to the cu::cli::Flags field inside the Args struct +/// #[cu::cli(flags = "inner")] +/// fn main(args: Args) -> cu::Result<()> { +/// cu::info!("input is {}", args.input); +/// cu::info!("output is {:?}", args.output); +/// Ok(()) +/// } +/// ``` +/// +/// Alternatively, implement `AsRef` for your struct. +/// +/// ```rust,ignore +/// # use pistonite_cu as cu; +/// use cu::pre::*; +/// #[derive(clap::Parser, Clone)] +/// struct Args { +/// input: String, +/// #[clap(flatten)] +/// inner: cu::cli::Flags, +/// } +/// impl AsRef for Args { +/// fn as_ref(&self) -> cu::cli::Flags { +/// &self.inner +/// } +/// } +/// #[cu::cli] +/// fn main(_: Args) -> cu::Result<()> { +/// Ok(()) +/// } +/// ``` +/// +/// Or enable the `derive` feature and derive `AsRef` (via [`derive_more`](https://docs.rs/derive_more)). +/// ```rust,ignore +/// # use pistonite_cu as cu; +/// use cu::pre::*; +/// #[derive(clap::Parser, Clone, AsRef)] +/// struct Args { +/// input: String, +/// #[clap(flatten)] +/// #[as_ref] +/// inner: cu::cli::Flags, +/// } +/// #[cu::cli] +/// fn main(_: Args) -> cu::Result<()> { +/// Ok(()) +/// } +/// ``` +/// +/// The attribute can also take a `preprocess` function +/// to process flags before initializing the CLI system. +/// This can be useful to merge multiple Flags instance +/// in the CLI. Note that the logging/printing system +/// will not work during the preprocess. +/// +/// ```rust,ignore +/// # use pistonite_cu as cu; +/// use cu::pre::*; +/// +/// #[derive(clap::Parser)] +/// struct Args { +/// #[clap(subcommand)] +/// subcommand: Option, +/// #[clap(flatten)] +/// inner: cu::cli::Flags, +/// } +/// impl Args { +/// fn preprocess(&mut self) { +/// // merge subcommand flags into top level flags +/// // this way, both `-v foo` and `foo -v` will work +/// if let Some(Command::Foo(c)) = &self.subcommand { +/// self.inner.merge(c); +/// } +/// } +/// } +/// impl AsRef for Args { +/// fn as_ref(&self) -> &cu::cli::Flags { +/// &self.inner +/// } +/// } +/// #[derive(clap::Subcommand)] +/// enum Command { +/// Foo(cu::cli::Flags), +/// } +/// #[cu::cli(preprocess = Args::preprocess)] +/// fn main(args: Args) -> cu::Result<()> { +/// Ok(()) +/// } +/// ``` +/// #[proc_macro_attribute] pub fn cli(attr: TokenStream, input: TokenStream) -> TokenStream { pm::flatten(cli::expand(attr, input)) @@ -16,10 +153,10 @@ mod derive_parse; /// Attribute macro for wrapping a function with an error context /// -/// See the [tests](https://github.com/Pistonite/cu/blob/main/packages/copper/tests/error_ctx.rs) +/// See the [tests](https://github.com/Pistonite/cu/blob/main/packages/copper/tests/context.rs) /// for examples #[proc_macro_attribute] -pub fn error_ctx(attr: TokenStream, input: TokenStream) -> TokenStream { - pm::flatten(error_ctx::expand(attr, input)) +pub fn context(attr: TokenStream, input: TokenStream) -> TokenStream { + pm::flatten(context::expand(attr, input)) } -mod error_ctx; +mod context; diff --git a/packages/copper/examples/prompt_password.rs b/packages/copper/examples/prompt_password.rs index cbd3f73..cff4573 100644 --- a/packages/copper/examples/prompt_password.rs +++ b/packages/copper/examples/prompt_password.rs @@ -15,7 +15,10 @@ fn main(_: cu::cli::Flags) -> cu::Result<()> { let expected = "rust"; let answer = cu::prompt_validate!( - ("what's your favorite programming language? please answer {}", expected), + ( + "what's your favorite programming language? please answer {}", + expected + ), |answer| { if answer == expected { return Ok(true); @@ -28,17 +31,15 @@ fn main(_: cu::cli::Flags) -> cu::Result<()> { } )?; cu::ensure!(answer == expected)?; -let mut index: i32 = 0; -cu::prompt_validate!( - "select a number between 0 and 5", - |answer| { + let mut index: i32 = 0; + cu::prompt_validate!("select a number between 0 and 5", |answer| { let number = match cu::parse::(answer) { Err(e) => { cu::error!("{e}"); cu::hint!("please ensure you are entering a number"); return Ok(false); } - Ok(x) => x + Ok(x) => x, }; if number < 0 { cu::error!("the number you entered is too small"); @@ -50,31 +51,30 @@ cu::prompt_validate!( } index = number; Ok(true) - } -)?; -cu::info!("index is {index}"); + })?; + cu::info!("index is {index}"); -let password = cu::prompt_password_validate!( - "please enter a password between 8 and 16 charactres and only contain sensible characters", - |answer| { - if answer == "123456" { - cu::bail!("how can you do that, bye"); - } - if answer.len() < 8 { - cu::error!("password is too short"); - return Ok(false); - } - if answer.len() > 16 { - cu::error!("password is too long"); - return Ok(false); - } - if let Err(e) = cu::password_chars_legal(answer) { - cu::error!("invalid password: {e}"); - return Ok(false); + let password = cu::prompt_password_validate!( + "please enter a password between 8 and 16 charactres and only contain sensible characters", + |answer| { + if answer == "123456" { + cu::bail!("how can you do that, bye"); + } + if answer.len() < 8 { + cu::error!("password is too short"); + return Ok(false); + } + if answer.len() > 16 { + cu::error!("password is too long"); + return Ok(false); + } + if let Err(e) = cu::password_chars_legal(answer) { + cu::error!("invalid password: {e}"); + return Ok(false); + } + Ok(true) } - Ok(true) - } -)?; -cu::print!("{password}"); + )?; + cu::print!("{password}"); Ok(()) } diff --git a/packages/copper/src/cli/flags.rs b/packages/copper/src/cli/flags.rs index dc2856d..f42096b 100644 --- a/packages/copper/src/cli/flags.rs +++ b/packages/copper/src/cli/flags.rs @@ -146,6 +146,9 @@ pub unsafe fn __co_run< ) -> std::process::ExitCode { let start = std::time::Instant::now(); let args = unsafe { parse_args_or_help::(fn_preproc, fn_flag) }; + #[cfg(not(feature = "coroutine-heavy"))] + let result = crate::co::block(async move { fn_execute(args).await }); + #[cfg(feature = "coroutine-heavy")] let result = crate::co::run(async move { fn_execute(args).await }); handle_result(start, result) diff --git a/packages/copper/src/co.rs b/packages/copper/src/co.rs deleted file mode 100644 index f26e004..0000000 --- a/packages/copper/src/co.rs +++ /dev/null @@ -1,129 +0,0 @@ -//! `cu::co::` Coroutine driver -//! -//! This library is designed to have flexible coroutine handling, -//! being able to handle `async` both on the current thread, -//! and on one or more background threads. -//! -//! For example, consider these program styles: -//! - everything being `async` - typically involving both CPU-bound -//! work and IO work interwined. Can take advantage of multiple background threads. -//! - Some IO heavy work that doesn't really involve CPU - for example, -//! spawning compiler processes and wait for them, or spawning network requests. -//! Usually won't have significant performance benefit from having multiple background threads. -//! - Heavy CPU work that only has a little IO. Using `async` usually has very little -//! benefit. (Would probably use something like `rayon` to get parallelism). -//! -//! You pick the style you want. -//! -//! # Async entry point -//! -//! With the [`cli`](module@crate::cli) module, you can use the same macro -//! for an async entry point -//! -//! ```rust -//! use std::time::Duration; -//! # use pistonite_cu as cu; -//! #[cu::cli] -//! async fn main(_: cu::cli::Flags) -> cu::Result<()> { -//! cu::info!("doing some work"); -//! tokio::time::sleep(Duration::from_millis(100)).await; -//! cu::info!("done"); -//! Ok(()) -//! } -//! ``` -//! Note that the entry point is still drived by the main thread despite being `async` -//! (even if `coroutine-heavy` feature is enabled), meaning that the above program -//! is still single-threaded! This makes sense because the (fake) workload doesn't benefit -//! at all from having multiple threads. -//! -//! By default, the number of background threads is 1. -//! Enabling the `coroutine-heavy` feature will change it -//! to the number of processors. -//! -//! # Internal Coroutine -//! Some `cu` functions use coroutines internally behind "synchronous" APIs, -//! allowing seamless integration from a synchronous context. -//! -//! For example, `cu` uses coroutines to drive inputs and outputs from a command: -//! ```rust,no_run -//! # use pistonite_cu as cu; -//! use cu::pre::*; -//! -//! #[cu::cli] -//! fn main(_: cu::cli::Flags) -> cu::Result<()> { -//! let git = cu::which("git")?; -//! let child1 = git.command() -//! .args(["clone", "https://example1.git", "dest1", "--progress"]) -//! .stdin_null() -//! // use a progress bar to display progress, and print other -//! // messages as info -//! .stdoe(cu::pio::spinner("cloning example1").info()) -//! .spawn()?.0; -//! // same configuration -//! let child2 = git.command() -//! .args(["clone", "https://example2.git", "dest2", "--progress"]) -//! .stdin_null() -//! .stdoe(cu::pio::spinner("cloning example2").info()) -//! .spawn()?.0; -//! -//! // Both childs are now running as separate processes in the OS. -//! // Also, IO from both childs are drived by the same background thread. -//! // You can block the main thread to do other work, and it will not -//! // block the child from printing messages -//! -//! // since we don't get benefit from one child finishing early -//! // here, we just wait for them in order -//! child1.wait_nz()?; -//! child2.wait_nz()?; -//! -//! Ok(()) -//! } -//! ``` -//! -//! # `co_*` APIs -//! Many APIs in `cu` has a same version with `co_` prefix. -//! These are designed to be called when you are already in an asynchronous -//! context. For example, we can rewrite the example above using `co_wait_nz`. -//! Note that in this case, there's no benefit of using `co_spawn`/`co_wait_nz`, -//! since we are not doing any extra work. -//! -//! ```rust,no_run -//! # use pistonite_cu as cu; -//! use cu::pre::*; -//! #[cu::cli] -//! async fn main(_: cu::cli::Flags) -> cu::Result<()> { -//! let git = cu::which("git")?; -//! let child1 = git.command() -//! .args(["clone", "https://example1.git", "dest1", "--progress"]) -//! .stdin_null() -//! .stdoe(cu::pio::spinner("cloning example1").info()) -//! // using co_spawn() will do the work needed at spawn time -//! // using the current async context, instead of off-loading -//! // it to a background thread. -//! .co_spawn().await?.0; -//! // however, note that the IO work, once spawned, are still -//! // driven by a background thread regardless of which spawn API -//! // is used -//! // same configuration -//! let child2 = git.command() -//! .args(["clone", "https://example2.git", "dest2", "--progress"]) -//! .stdin_null() -//! .stdoe(cu::pio::spinner("cloning example2").info()) -//! .co_spawn().await?.0; -//! -//! child1.co_wait_nz().await?; -//! child2.co_wait_nz().await?; -//! -//! Ok(()) -//! } -//! ``` - -pub use crate::async_::{ - AbortHandle, Handle, Pool, RobustAbortHandle, RobustHandle, Set, pool, run, set, set_flatten, - spawn, -}; - -#[cfg(not(feature = "coroutine-heavy"))] -pub use crate::async_::block; -#[cfg(feature = "coroutine-heavy")] -pub use crate::async_::spawn_blocking; diff --git a/packages/copper/src/async_/mod.rs b/packages/copper/src/co/co_util.rs similarity index 83% rename from packages/copper/src/async_/mod.rs rename to packages/copper/src/co/co_util.rs index 0bbae9c..753145c 100644 --- a/packages/copper/src/async_/mod.rs +++ b/packages/copper/src/co/co_util.rs @@ -1,10 +1,3 @@ -mod pool; -pub use pool::*; -mod runtime; -pub use runtime::*; -mod handle; -pub use handle::*; - /// return Ok if the error is abort pub(crate) fn handle_join_error(e: tokio::task::JoinError) -> crate::Result<()> { let e = match e.try_into_panic() { diff --git a/packages/copper/src/async_/handle.rs b/packages/copper/src/co/handle.rs similarity index 97% rename from packages/copper/src/async_/handle.rs rename to packages/copper/src/co/handle.rs index 5590bd6..f920ae6 100644 --- a/packages/copper/src/async_/handle.rs +++ b/packages/copper/src/co/handle.rs @@ -3,6 +3,8 @@ use std::sync::atomic::{AtomicU8, Ordering}; use tokio::task::{JoinError, JoinHandle}; +use crate::co::{co_util, runtime}; + /// Join handle for async task /// /// This is a wrapper around `tokio`'s `JoinHandle` type. @@ -43,7 +45,7 @@ impl Handle { /// Use [`co_join().await`](`Self::co_join`) instead. #[inline] pub fn join(self) -> crate::Result { - Self::handle_error(super::foreground().block_on(self.0)) + Self::handle_error(runtime::foreground().block_on(self.0)) } /// Wait for the task asynchronously @@ -67,7 +69,7 @@ impl Handle { /// Use [`co_join_maybe_aborted().await`](`Self::co_join_maybe_aborted`) instead. #[inline] pub fn join_maybe_aborted(self) -> crate::Result> { - Self::handle_error_maybe_aborted(super::foreground().block_on(self.0)) + Self::handle_error_maybe_aborted(runtime::foreground().block_on(self.0)) } /// Like [`co_join`](Self::co_join), but returns `None` if the task was aborted. @@ -90,7 +92,7 @@ impl Handle { Ok(x) => return Ok(Some(x)), Err(e) => e, }; - super::handle_join_error(e)?; + co_util::handle_join_error(e)?; Ok(None) } } @@ -249,7 +251,7 @@ impl RobustHandle { /// Use [`co_join_maybe_aborted_robust().await`](`Self::co_join_maybe_aborted_robust`) instead. pub fn join_maybe_aborted_robust(self) -> crate::Result>> { Self::handle_error_maybe_aborted_robust( - super::foreground().block_on(self.inner.0), + runtime::foreground().block_on(self.inner.0), &self.aborted, ) } @@ -281,7 +283,7 @@ impl RobustHandle { if Self::check_aborted(aborted) { return Ok(Err(None)); } - super::handle_join_error(e)?; + co_util::handle_join_error(e)?; Ok(Err(None)) } diff --git a/packages/copper/src/co/mod.rs b/packages/copper/src/co/mod.rs new file mode 100644 index 0000000..d1e8841 --- /dev/null +++ b/packages/copper/src/co/mod.rs @@ -0,0 +1,89 @@ +//! # Coroutines (Async) +//! +//! `cu` is designed to have flexible coroutine handling. For example, consider these program styles: +//! - everything being `async` - typically involving both CPU-bound +//! work and IO work interwined. Can take advantage of multiple background threads. +//! - Some IO heavy work that doesn't really involve CPU - for example, +//! spawning compiler processes and wait for them, or spawning network requests. +//! Usually won't have significant performance benefit from having multiple background threads. +//! - Heavy CPU work that only has a little IO. Using `async` usually has very little +//! benefit. (Would probably use something like `rayon` to get parallelism). +//! +//! You pick the style you want. +//! +//! The async runtime being used under the hood is [`tokio`](https://docs.rs/tokio). +//! There are 2 feature flags you can choose from: `coroutine` and `coroutine-heavy`. +//! `coroutine` uses one foreground (current-thread) tokio runtime and one background thread to +//! drive IO tasks. `coroutine-heavy` does not have a current-thread runtime - everything +//! is done on the multi-threaded, background runtime. +//! +//! # Async entry point +//! +//! To make your entire program async with [`cli`](module@crate::cli), +//! simply make the `main` function `async. +//! +//! ```rust +//! use std::time::Duration; +//! # use pistonite_cu as cu; +//! #[cu::cli] +//! async fn main(_: cu::cli::Flags) -> cu::Result<()> { +//! cu::info!("doing some work"); +//! tokio::time::sleep(Duration::from_millis(100)).await; +//! cu::info!("done"); +//! Ok(()) +//! } +//! ``` +//! +//! When using `coroutine`, the main future will be spawned onto the current-thread +//! runtime (so the main thread is still driving it). When using `coroutine-heavy`, +//! the main future is spawned onto the background runtime, and the main thread +//! waits until the future is completed. +//! +//! # Coroutines used internally and `co_*` APIs +//! Some `cu` functions use coroutines internally behind "synchronous" APIs, +//! allowing seamless integration from a synchronous context. +//! +//! For example, when spawning child processes, an async task processes +//! IO from the child and streams results to the main thread. This allows +//! for a clean API to for example, read child's output line-by-line. +//! +//! However, there is an important catch - it is crucial that we never block +//! an async runtime. This means to wait for a future: +//! - If we are not in an async runtime, we can enter the async runtime by +//! calling an entry point to the runtime (a.k.a `block`), to block +//! the current thread while letting the runtime run until the future is finished. +//! - If we are already in an async runtime, we must call `.await` instead +//! of block. Otherwise, either the entire runtime will block and may deadlock, +//! or tokio will detect it and panic. +//! +//! This is why the APIs that use `coroutine` under the hood will have +//! another version with a `co_*` prefix. For example, `spawn` and `co_spawn`. +//! You MUST use `.spawn()?` if not in an async runtime, and `.co_spawn().await?` +//! in an async runtime. Note that calling `.co_spawn()` while not in an async runtime +//! is also not allowed, because tokio will assume a runtime is active and panic +//! if not. +//! +//! # Advanced Usage +//! If additional functionality from `tokio` is needed (not already provided by re-exports), +//! then you can add `tokio` to `Cargo.toml`: +//! ```toml +//! [dependencies] +//! tokio = "1" +//! ``` + +// re-exports +pub use tokio::{join, select, try_join}; + +mod runtime; +#[cfg(not(feature = "coroutine-heavy"))] +pub use runtime::block; +#[cfg(feature = "coroutine-heavy")] +pub use runtime::spawn_blocking; +pub use runtime::{run, spawn}; + +mod pool; +pub use pool::{Pool, Set, pool, set, set_flatten}; + +mod handle; +pub use handle::{AbortHandle, Handle, RobustAbortHandle, RobustHandle}; +mod co_util; diff --git a/packages/copper/src/async_/pool.rs b/packages/copper/src/co/pool.rs similarity index 97% rename from packages/copper/src/async_/pool.rs rename to packages/copper/src/co/pool.rs index a790d73..3326335 100644 --- a/packages/copper/src/async_/pool.rs +++ b/packages/copper/src/co/pool.rs @@ -2,7 +2,7 @@ use std::sync::{Arc, Weak}; use tokio::sync::Semaphore; -use super::{AbortHandle, Handle}; +use crate::co::{AbortHandle, Handle, co_util, runtime}; /// Create a new [`Pool`]. /// @@ -227,7 +227,7 @@ impl Set { drop(abort_handle); result }, - super::background().handle(), + runtime::background().handle(), ); } @@ -246,7 +246,7 @@ impl Set { pub async fn next(&mut self) -> Option> { let result = self.join_set.join_next().await?; match result { - Err(join_error) => match super::handle_join_error(join_error) { + Err(join_error) => match co_util::handle_join_error(join_error) { Err(e) => Some(Err(e)), Ok(_) => Some(Err(crate::fmterr!("aborted"))), }, @@ -263,6 +263,6 @@ impl Set { /// Use [`next().await`](`Self::next`) instead. #[inline] pub fn block(&mut self) -> Option> { - super::foreground().block_on(async move { self.next().await }) + runtime::foreground().block_on(async move { self.next().await }) } } diff --git a/packages/copper/src/async_/runtime.rs b/packages/copper/src/co/runtime.rs similarity index 87% rename from packages/copper/src/async_/runtime.rs rename to packages/copper/src/co/runtime.rs index e5b311e..2eda64c 100644 --- a/packages/copper/src/async_/runtime.rs +++ b/packages/copper/src/co/runtime.rs @@ -2,7 +2,7 @@ use std::sync::LazyLock; use tokio::runtime::{Builder, Runtime}; -use super::Handle; +use crate::co::Handle; /// the current-thread runtime #[cfg(not(feature = "coroutine-heavy"))] @@ -34,6 +34,9 @@ static BACKGROUND_RUNTIME: LazyLock = LazyLock::new(|| { /// Get a reference of a runtime that contains the current thread pub(crate) fn foreground() -> &'static Runtime { + // only use the background runtime, because + // the foreground could be a background thread, + // and blocking it would block the background runtime #[cfg(not(feature = "coroutine-heavy"))] { &RUNTIME @@ -50,13 +53,15 @@ pub(crate) fn background() -> &'static Runtime { /// Run an async task using the current thread. /// -/// To prevent misuse, this is only available without the `coroutine-heavy` -/// feature. Consider this entry point to some async procedure, if most of +/// Consider using this as an entry point to some async procedure, if most of /// your program is sync. /// +/// To prevent misuse, this is only available without the `coroutine-heavy` +/// feature. +/// /// Use [`spawn`] or [`run`] to run async tasks using the background thread(s) /// in both light and heavy async use cases. -#[inline] +#[inline(always)] #[cfg(not(feature = "coroutine-heavy"))] pub fn block(future: F) -> F::Output where @@ -66,7 +71,7 @@ where } /// Spawn a task onto the background runtime -#[inline] +#[inline(always)] pub fn spawn(future: F) -> Handle where F: Future + Send + 'static, @@ -79,7 +84,7 @@ where /// /// Since the light context only has one background thread, /// this is only enabled in heavy context to prevent misuse. -#[inline] +#[inline(always)] #[cfg(feature = "coroutine-heavy")] pub fn spawn_blocking(func: F) -> Handle where @@ -90,7 +95,7 @@ where } /// Run an async task using the background runtime -#[inline] +#[inline(always)] pub fn run(future: F) -> F::Output where F: Future, diff --git a/packages/copper/src/error_handling.rs b/packages/copper/src/error_handling.rs index add5e14..46fa347 100644 --- a/packages/copper/src/error_handling.rs +++ b/packages/copper/src/error_handling.rs @@ -55,6 +55,65 @@ pub use anyhow::{Context, Error, Ok, Result, anyhow as fmterr, bail}; /// Finally, if you do need to panic, [`cu::panicand`](macro@crate::panicand) /// allows you to also log the same message so you can debug it easier. /// +/// # Context (To `check` or not to `check`) +/// It is tricky to determine if you should wrap the result +/// with a `check!`, or just propagate it with a `?`. +/// Ultimately, there is no correct answer (you may say it is contextual). +/// Another way of phrasing the same question is if the context +/// should be added by the caller or the callee. +/// +/// The principle I personally follow is the caller should only `check!` +/// if there are additional information in the caller's context +/// that the callee does not already know. The information that both +/// the callee and caller have access to is: +/// - The function parameters +/// - The name of the call +/// - The behavior of the function +/// +/// These make up the API of the function. +/// +/// For example, I will write the following code +/// ```rust +/// # use pistonite_cu as cu; +/// use cu::pre::*; +/// +/// fn process_paths(paths: &[&str]) -> cu::Result<()> { +/// cu::info!("processing paths..."); +/// for (i, path) in paths.iter().enumerate() { +/// // here there might be some candidates for context: +/// // - "failed to save important value to '{path}'" +/// // - the callee knows the current context is saving +/// // "important value" to "path" (from function name and parameter), +/// // therefore this message does not add additional context +/// // - "process paths failed on '{path}'" +/// // - this could work in some cases, but here I know +/// // cu would already log the path if it fails +/// // - here I choose to log {i} which might help me finding the erroreous +/// // path from some kind of data set. +/// cu::check!(save_important_value_to(path), "failed to process {i}th path")?; +/// } +/// Ok(()) +/// } +/// +/// fn save_important_value_to(path: &str) -> cu::Result<()> { +/// // here, the only information I have that cu::fs::write +/// // does not have, is "important value" is "some random thing". +/// // this is not an important context to log to the error, +/// // so I choose to simply ? +/// cu::fs::write(path, "some random thing")?; +/// Ok(()) +/// } +/// ``` +/// +/// With that said, now we can introduce [`cu::context`](macro@crate::context), +/// which wraps a function and append a formatted context to it. +/// This is a double-edge sword. You could end up with unnecessarily bloated +/// error stack if you put too much context. However, +/// it can be extremely useful in a complex function with many possible error paths +/// to add context for what the overall failure is. +/// +/// If you find yourself writing the same `check!` to every invocation of some function. +/// Considering using it. However, I would not use this on any public API of your code. #[macro_export] macro_rules! check { ($result:expr, $($args:tt)*) => {{ diff --git a/packages/copper/src/fs/dir.rs b/packages/copper/src/fs/dir.rs index e0ba814..32b9bd3 100644 --- a/packages/copper/src/fs/dir.rs +++ b/packages/copper/src/fs/dir.rs @@ -314,7 +314,7 @@ async fn co_read_dir_impl(path: &Path) -> crate::Result { pub fn rec_copy_inefficiently(from: impl AsRef, to: impl AsRef) -> crate::Result<()> { rec_copy_inefficiently_impl(from.as_ref(), to.as_ref()) } -#[crate::error_ctx("failed to copy '{}' to '{}'", from.display(), to.display())] +#[crate::context("failed to copy recursively copy '{}' to '{}'", from.display(), to.display())] fn rec_copy_inefficiently_impl(from: &Path, to: &Path) -> crate::Result<()> { crate::trace!( "rec_copy_inefficiently from='{}' to='{}'", diff --git a/packages/copper/src/fs/file.rs b/packages/copper/src/fs/file.rs index c364fdb..6bf1f8f 100644 --- a/packages/copper/src/fs/file.rs +++ b/packages/copper/src/fs/file.rs @@ -207,7 +207,7 @@ async fn co_remove_impl(path: &Path) -> crate::Result<()> { pub fn rename(from: impl AsRef, to: impl AsRef) -> crate::Result<()> { rename_impl(from.as_ref(), to.as_ref()) } -#[crate::error_ctx("failed to rename '{}' to '{}'", from.display(), to.display())] +#[crate::context("failed to rename '{}' to '{}'", from.display(), to.display())] fn rename_impl(from: &Path, to: &Path) -> crate::Result<()> { crate::trace!("rename: '{}' to '{}'", from.display(), to.display()); let Ok(from_meta) = from.metadata() else { diff --git a/packages/copper/src/lib.rs b/packages/copper/src/lib.rs index e6edafd..74f4009 100644 --- a/packages/copper/src/lib.rs +++ b/packages/copper/src/lib.rs @@ -60,14 +60,13 @@ //! # Quick Reference //! - [Error Handling](macro@crate::check) (via [`anyhow`](https://docs.rs/anyhow)) //! - [Logging](mod@crate::lv) (via [`log`](https://docs.rs/log)) -//! - [Printting and Command Line Interface](mod@crate::cli) (CLI arg parsing via +//! - [Printing and Command Line Interface](mod@crate::cli) (CLI arg parsing via //! [`clap`](https://docs.rs/clap)) //! - [Progress Bars](fn@crate::progress) //! - [Prompting](macro@crate::prompt) +//! - [Coroutines (Async)](mod@crate::co) (via [`tokio`](https://docs.rs/tokio)) //! //! # Feature Reference: -//! - `coroutine` and `coroutine-heavy`: -//! Enables `async` and integration with `tokio`. See [`cu::co`](module@co). //! - `fs`: Enables file system utils. See [`cu::fs`](module@fs) and [`cu::bin`](module@bin). //! - `process`: Enables utils spawning child process. See [`Command`]. //! - `parse`, `json`, `yaml`, `toml`: @@ -92,6 +91,7 @@ pub use misc::*; // --- Error Handling (no feature needed) --- mod error_handling; pub use error_handling::*; +pub use pistonite_cu_proc_macros::context; // --- Logging (no feature needed) --- pub mod lv; @@ -107,6 +107,14 @@ pub use cli::{ProgressBar, ProgressBarBuilder, progress}; #[cfg(feature = "cli")] pub use pistonite_cu_proc_macros::cli; +// --- Async (coroutine/coroutine-heavy) --- +/// Alias for a boxed future +pub type BoxedFuture = std::pin::Pin + Send + 'static>>; +#[cfg(feature = "coroutine")] +pub mod co; +#[cfg(feature = "coroutine")] +pub use co::{join, select, try_join}; + #[cfg(feature = "process")] mod process; #[cfg(feature = "process")] @@ -124,13 +132,6 @@ pub use bin::which; #[cfg(feature = "fs")] pub mod fs; -#[cfg(feature = "coroutine")] -mod async_; -/// Alias for a boxed future -pub type BoxedFuture = std::pin::Pin + Send + 'static>>; -#[cfg(feature = "coroutine")] -pub mod co; - /// Parsing utilities #[cfg(feature = "parse")] mod parse; @@ -139,11 +140,6 @@ pub use parse::*; #[cfg(feature = "parse")] pub use pistonite_cu_proc_macros::Parse; -// re-exports from libraries -pub use pistonite_cu_proc_macros::error_ctx; -#[cfg(feature = "coroutine")] -pub use tokio::{join, select, try_join}; - #[doc(hidden)] pub mod __priv { #[cfg(feature = "process")] @@ -165,6 +161,9 @@ pub mod pre { #[cfg(feature = "cli")] pub use crate::lib::clap; + #[cfg(feature = "coroutine")] + pub use tokio::io::{AsyncBufReadExt as _, AsyncReadExt as _, AsyncWriteExt as _}; + #[cfg(feature = "parse")] pub use crate::ParseTo as _; #[cfg(feature = "fs")] @@ -191,7 +190,4 @@ pub mod pre { LowerHex as DisplayLowerHex, Octal as DisplayOctal, Pointer as DisplayPointer, UpperExp as DisplayUpperExp, UpperHex as DisplayUpperHex, }; - - #[cfg(feature = "coroutine")] - pub use tokio::io::{AsyncBufReadExt as _, AsyncReadExt as _}; } diff --git a/packages/copper/tests/error_ctx.rs b/packages/copper/tests/context.rs similarity index 89% rename from packages/copper/tests/error_ctx.rs rename to packages/copper/tests/context.rs index 065e7f7..04be7d3 100644 --- a/packages/copper/tests/error_ctx.rs +++ b/packages/copper/tests/context.rs @@ -12,7 +12,7 @@ Caused by: ) } -#[cu::error_ctx("failed with arg {arg}")] +#[cu::context("failed with arg {arg}")] fn example1(arg: u32) -> cu::Result<()> { cu::bail!("example1") } @@ -31,7 +31,7 @@ Caused by: // 'pre' is needed because s is moved into the function // so the error message needs to be formatted before running the function -#[cu::error_ctx(pre, format("failed with arg {s}"))] +#[cu::context(pre, format("failed with arg {s}"))] fn example2(s: String) -> cu::Result<()> { cu::bail!("example2: {s}") } @@ -54,7 +54,7 @@ async fn test_example3_ok() { } // question mark works as expected (context is added at return time) -#[cu::error_ctx("async failed with arg {}", s)] +#[cu::context("async failed with arg {}", s)] async fn example3(s: u32) -> cu::Result<()> { let value = returns_ok(s)?; cu::ensure!(value > 4)?; @@ -79,7 +79,7 @@ async fn test_example4_ok() { } // question mark works as expected (context is added at return time) -#[cu::error_ctx(pre, format("async failed with arg {s}"))] +#[cu::context(pre, format("async failed with arg {s}"))] async fn example4(s: String) -> cu::Result<()> { let value = returns_ok(s)?; cu::ensure!(!value.is_empty())?; @@ -101,7 +101,7 @@ Caused by: // associated functions also work struct Foo(u32); impl Foo { - #[cu::error_ctx("Foo failed with arg {}", self.0)] + #[cu::context("Foo failed with arg {}", self.0)] fn example5(&self) -> cu::Result<()> { cu::bail!("example5") } diff --git a/packages/terminal-tests/Cargo.toml b/packages/terminal-tests/Cargo.toml index bb37272..21bada8 100644 --- a/packages/terminal-tests/Cargo.toml +++ b/packages/terminal-tests/Cargo.toml @@ -23,4 +23,3 @@ common = ["cu/__test", "cu/cli"] __test-print_levels = [] __test-prompt = ["cu/prompt"] -__test-prompt_password = ["cu/prompt-password"] diff --git a/packages/terminal-tests/examples/prompt_password.rs b/packages/terminal-tests/examples/prompt_password.rs deleted file mode 100644 index 24150d0..0000000 --- a/packages/terminal-tests/examples/prompt_password.rs +++ /dev/null @@ -1,8 +0,0 @@ -// $ < prompt-xn.txt -#[cu::cli] -fn main(_: cu::cli::Flags) -> cu::Result<()> { - cu::hint!("testing prompt password"); - let answer = cu::prompt_password!("enter password")?; - cu::info!("you answered: {answer}"); - Ok(()) -} diff --git a/packages/terminal-tests/output/prompt-0.txt b/packages/terminal-tests/output/prompt-0.txt index a0abf1d..f9a5714 100644 --- a/packages/terminal-tests/output/prompt-0.txt +++ b/packages/terminal-tests/output/prompt-0.txt @@ -1,12 +1,7 @@ $ -y --non-interactive STDOUT >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> H] testing prompts^LF -E] prompt not allowed in non-interactive mode: what's your f^LF - | avorite programming language?^LF -E] fatal: prompt not allowed in non-interactive mode: what's^LF - | your favorite programming language?^LF -H] use -vv or set RUST_BACKTRACE=1 to display the error back^LF - | trace.^LF +E] fatal: prompt not allowed with --non-interactive^LF I] finished in 0.00s^LF ^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> H] testing prompts^LF -E] prompt not allowed in non-interactive mode: continue?^LF -E] fatal: prompt not allowed in non-interactive mode: contin^LF - | ue?^LF -H] use -vv or set RUST_BACKTRACE=1 to display the error back^LF - | trace.^LF +E] fatal: prompt not allowed with --non-interactive^LF I] finished in 0.00s^LF ^ cu::Result<()> { let stdin = test_case.stdin.as_ref().cloned().unwrap_or_default(); let feature = format!("__test-{example_name},common"); cu::print!("TEST OUTPUT >>>>>>>>>>>>>>>>>>>>>>>>>>"); - let command_builder = - cu::which("cargo")? + let command_builder = cu::which("cargo")? .command() + // don't include warnings in the output + .env("RUSTFLAGS", "-Awarnings") .args([ "run", "-q", @@ -65,13 +67,12 @@ async fn main(args: Cli) -> cu::Result<()> { .stdout_inherit() .stderr_inherit(); let exit_status = if args.inherit_stdin { - command_builder.stdin_inherit() - .co_wait() - .await? + command_builder.stdin_inherit().co_wait().await? } else { - command_builder.stdin(cu::pio::write(stdin)) - .co_wait() - .await? + command_builder + .stdin(cu::pio::write(stdin)) + .co_wait() + .await? }; cu::print!("TEST OUTPUT <<<<<<<<<<<<<<<<<<<<<<<<<<"); cu::print!("STATUS: {exit_status}"); @@ -95,6 +96,7 @@ struct TestTarget { struct TestCase { stdin: Option>, args: Vec, + skip: bool, } fn find_tests() -> cu::Result> { @@ -128,6 +130,10 @@ fn parse_test_cases(path: &Path) -> cu::Result> { let Some(line) = line.strip_prefix("// $") else { break; }; + let (line, skip) = match line.strip_prefix("-") { + None => (line, false), + Some(line) => (line, true), + }; let mut args = cu::check!( shell_words::split(line.trim()), "failed to parse command line: {line}" @@ -144,7 +150,7 @@ fn parse_test_cases(path: &Path) -> cu::Result> { args.pop(); } } - test_cases.push(TestCase { args, stdin }); + test_cases.push(TestCase { args, stdin, skip }); } Ok(test_cases) } @@ -208,18 +214,25 @@ async fn run_test_targets(targets: Vec, update: bool) -> cu::Result< "unexpected: cannot find test cases" )?; - for (index, test_cases) in target.test_cases.iter().enumerate() { + for (index, test_case) in target.test_cases.iter().enumerate() { let example_name = example_name.clone(); - let args = test_cases.args.clone(); - let stdin = test_cases.stdin.clone().unwrap_or_default(); + if test_case.skip { + cu::warn!("skipping {example_name}-{index}"); + cu::progress!(test_bar += 1); + continue; + } + let args = test_case.args.clone(); + let stdin = test_case.stdin.clone().unwrap_or_default(); let test_bar = Arc::clone(&test_bar); let handle = test_pool.spawn(async move { let feature = format!("__test-{example_name},common"); let command = shell_words::join(&args); let child_bar = test_bar.child(format!("{example_name}: {command}")).spawn(); - let (child, stdout, stderr) = cu::which("cargo")? + let (mut child, stdout, stderr) = cu::which("cargo")? .command() + // don't include warnings in the output + .env("RUSTFLAGS", "-Awarnings") .args([ "run", "-q", @@ -236,7 +249,10 @@ async fn run_test_targets(targets: Vec, update: bool) -> cu::Result< .stdin(cu::pio::write(stdin)) .co_spawn() .await?; - let status = child.co_wait().await?; + let status = child.co_wait_timeout(Duration::from_secs(10)).await?; + if status.is_none() { + child.co_kill().await?; + } child_bar.done(); let stdout = stdout.co_join().await??; let stderr = stderr.co_join().await??; @@ -277,7 +293,7 @@ fn decode_output_streams( command: &str, stdout: Vec, stderr: Vec, - status: ExitStatus, + status: Option, ) -> String { use std::fmt::Write as _; let mut out = String::new(); @@ -291,7 +307,14 @@ fn decode_output_streams( decode_output_stream(&mut out, &stderr); out.push_str("^ { + let _ = write!(out, "status: {status}\n"); + } + None => { + let _ = write!(out, "timed out\n"); + } + } out } From fa9bb160b888c1324abcac03f709d418b1481528 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Sun, 11 Jan 2026 13:12:45 -0800 Subject: [PATCH 09/13] spinner test --- packages/copper/src/process/child.rs | 8 +- packages/terminal-tests/Cargo.toml | 1 + packages/terminal-tests/examples/spinner.rs | 114 +++++++++++++++++++ packages/terminal-tests/output/spinner-0.txt | 34 ++++++ 4 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 packages/terminal-tests/examples/spinner.rs create mode 100644 packages/terminal-tests/output/spinner-0.txt diff --git a/packages/copper/src/process/child.rs b/packages/copper/src/process/child.rs index 1dc900c..f3d0b24 100644 --- a/packages/copper/src/process/child.rs +++ b/packages/copper/src/process/child.rs @@ -84,7 +84,7 @@ impl Child { &mut self, timeout: Duration, ) -> crate::Result> { - let mut ms = 100; + let ms = 100; let mut total_ms = 0; loop { match self.inner.try_wait() { @@ -94,18 +94,17 @@ impl Child { crate::rethrow!(e, "io error while waiting {}", self.name) } } + tokio::time::sleep(Duration::from_millis(ms)).await; total_ms += ms; if Duration::from_millis(total_ms) >= timeout { break; } - tokio::time::sleep(Duration::from_millis(ms)).await; - ms *= 4; } Ok(None) } pub fn wait_timeout(&mut self, timeout: Duration) -> crate::Result> { - let mut ms = 100; + let ms = 100; let mut total_ms = 0; loop { match self.inner.try_wait() { @@ -120,7 +119,6 @@ impl Child { if Duration::from_millis(total_ms) >= timeout { break; } - ms *= 4; } Ok(None) } diff --git a/packages/terminal-tests/Cargo.toml b/packages/terminal-tests/Cargo.toml index 21bada8..85a018d 100644 --- a/packages/terminal-tests/Cargo.toml +++ b/packages/terminal-tests/Cargo.toml @@ -23,3 +23,4 @@ common = ["cu/__test", "cu/cli"] __test-print_levels = [] __test-prompt = ["cu/prompt"] +__test-spinner = ["cu/derive", "cu/prompt"] diff --git a/packages/terminal-tests/examples/spinner.rs b/packages/terminal-tests/examples/spinner.rs new file mode 100644 index 0000000..0ece8eb --- /dev/null +++ b/packages/terminal-tests/examples/spinner.rs @@ -0,0 +1,114 @@ +// $ 0 -y +// $ 1 -y +// $ 0 +// $ 1 + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::thread; +use std::time::Duration; + +use cu::pre::*; + +static LONG_SLEEP: AtomicBool = AtomicBool::new(false); + +#[derive(clap::Parser, Clone, AsRef)] +struct Args { + case: usize, + #[clap(flatten)] + #[as_ref] + inner: cu::cli::Flags, +} +#[cu::cli] +fn main(args: Args) -> cu::Result<()> { + cu::lv::disable_print_time(); + if !cu::yesno!("short sleep")? { + // relaxd fine, only accessing on this thread + LONG_SLEEP.store(true, Ordering::Relaxed); + } + static CASES: &[fn() -> cu::Result<()>] = &[ + test_case_1, + test_case_2, + ]; + CASES[args.case]() +} + +fn test_case_1() -> cu::Result<()> { + // 3 sequential bars + { + // bar with message + let bar = cu::progress("unbounded").spawn(); + cu::progress!(bar, "message1"); + sleep_tick(); + cu::progress!(bar, "message2"); + sleep_tick(); + cu::progress!(bar, "message3"); + sleep_tick(); + bar.done(); + } + { + // bar with progress + let bar = cu::progress("finite").total(3).spawn(); + cu::progress!(bar+=1, "message1"); + sleep_tick(); + cu::progress!(bar+=1, "message2"); + sleep_tick(); + cu::progress!(bar+=1, "message3"); + sleep_tick(); + } + { + // bar with no keep + let bar = cu::progress("finite, nokeep").keep(false).spawn(); + cu::progress!(bar=1, "message1"); + sleep_tick(); + cu::progress!(bar=2, "message2"); + sleep_tick(); + cu::progress!(bar=3, "message3"); + sleep_tick(); + } + Ok(()) +} + +fn test_case_2() -> cu::Result<()> { + { + let bar2 = cu::progress("This takes 5 seconds").total(20).spawn(); + let bar = bar2.child("This is unbounded").spawn(); + // make some fake hierarchy + let bar3 = bar.child("level 2").total(3).keep(true).spawn(); + let bar4 = bar3.child("level 3").total(7).spawn(); + let bar5 = bar2.child("last").total(9).keep(true).spawn(); + for i in 0..10 { + cu::progress!(bar, "step {i}"); + cu::progress!(bar2 = i, "step {i}"); + cu::progress!(bar3 += 1, "step {i}"); + cu::progress!(bar4 += 1, "step {i}"); + cu::progress!(bar5 += 1, "step {i}"); + cu::debug!("this is debug message\n"); + sleep_tick(); + + if i == 5 { + cu::prompt!("what's your favorite fruit?")?; + } + } + drop(bar4); + drop(bar5); + bar.done(); + for i in 0..10 { + cu::progress!(bar2 += 1, "step {}", i + 10); + sleep_tick(); + cu::print!("doing stuff"); + } + cu::progress!(bar2 += 1, "last step"); + } + + cu::print!("bars done"); + + Ok(()) +} + +fn sleep_tick() { + if LONG_SLEEP.load(Ordering::Relaxed) { + thread::sleep(Duration::from_secs(2)); + } else { + thread::sleep(Duration::from_millis(10)); + } +} diff --git a/packages/terminal-tests/output/spinner-0.txt b/packages/terminal-tests/output/spinner-0.txt new file mode 100644 index 0000000..33e0712 --- /dev/null +++ b/packages/terminal-tests/output/spinner-0.txt @@ -0,0 +1,34 @@ +$ 0 +STDOUT >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^CR +\x1B[K\xE2\xA0\x8B] unbounded: message1^LF +^CR +\x1B[K\x1B[1A\x1B[K\xE2\xA0\x8B] unbounded: message2^LF +^CR +\x1B[K\x1B[1A\x1B[K\xE2\xA0\x8B] unbounded: message3^LF +^CR +\x1B[K\x1B[1A\x1B[K\xE2\xA0\xBF] unbounded: done^LF +^CR +\x1B[K^CR +\x1B[K\xE2\xA0\x8B][1/3] finite: 33.33% message1^LF +^CR +\x1B[K\x1B[1A\x1B[K\xE2\xA0\x8B][2/3] finite: 66.67% message2^LF +^CR +\x1B[K\x1B[1A\x1B[K\xE2\xA0\x8B][3/3] finite: 100% message3^LF +^CR +\x1B[K\x1B[1A\x1B[K\xE2\xA0\xBF][3/3] finite: done^LF +^CR +\x1B[K^CR +\x1B[K\xE2\xA0\x8B] finite, nokeep: message1^LF +^CR +\x1B[K\x1B[1A\x1B[K\xE2\xA0\x8B] finite, nokeep: message2^LF +^CR +\x1B[K\x1B[1A\x1B[K\xE2\xA0\x8B] finite, nokeep: message3^LF +^CR +\x1B[K\x1B[1A\x1B[K^CR +\x1B[K^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +^ Date: Sun, 11 Jan 2026 14:32:06 -0800 Subject: [PATCH 10/13] osstr extension --- packages/copper/examples/print.rs | 125 ------------------------- packages/copper/src/cli/printer.rs | 2 +- packages/copper/src/lib.rs | 8 +- packages/copper/src/process/builder.rs | 2 +- packages/copper/src/str/mod.rs | 6 +- packages/copper/src/str/osstring.rs | 54 +++++++++++ packages/copper/src/str/path.rs | 41 +++++++- packages/copper/src/str/zstring.rs | 12 ++- 8 files changed, 114 insertions(+), 136 deletions(-) delete mode 100644 packages/copper/examples/print.rs create mode 100644 packages/copper/src/str/osstring.rs diff --git a/packages/copper/examples/print.rs b/packages/copper/examples/print.rs deleted file mode 100644 index a806afd..0000000 --- a/packages/copper/examples/print.rs +++ /dev/null @@ -1,125 +0,0 @@ -use pistonite_cu as cu; -use std::time::Duration; - -use cu::pre::*; - -#[derive(clap::Parser, Clone)] -struct Args { - #[clap(flatten)] - inner: cu::cli::Flags, -} -impl Args { - fn preprocess(&mut self) { - self.inner.verbose += 1; - println!("{:#?}", self.inner); - } -} -/// Run with cargo run --example print --features prompt,cli -#[cu::cli(flags = "inner", preprocess = Args::preprocess)] -fn main(_: Args) -> cu::Result<()> { - if !cu::yesno!("continue?")? { - cu::warn!("you chose to not continue!"); - return Ok(()); - } - cu::info!("you chose to continue!"); - - { - let bar2 = cu::progress("This takes 5 seconds").total(20).spawn(); - let bar = bar2.child("This is unbounded").spawn(); - // make some fake hierarchy - let bar3 = bar.child("level 2").total(3).keep(true).spawn(); - let bar4 = bar3.child("level 3").total(7).spawn(); - let bar5 = bar2.child("last").total(9).keep(true).spawn(); - for i in 0..10 { - cu::progress!(bar, "step {i}"); - cu::progress!(bar2 = i, "step {i}"); - cu::progress!(bar3 += 1, "step {i}"); - cu::progress!(bar4 += 1, "step {i}"); - cu::progress!(bar5 += 1, "step {i}"); - cu::debug!("this is debug message\n"); - std::thread::sleep(Duration::from_millis(250)); - - if i == 5 { - cu::prompt!("what's your favorite fruit?")?; - } - } - drop(bar4); - drop(bar5); - bar.done(); - for i in 0..10 { - cu::progress!(bar2 += 1, "step {}", i + 10); - std::thread::sleep(Duration::from_millis(250)); - cu::print!("doing stuff"); - } - cu::progress!(bar2 += 1, "last step"); - } - - cu::print!("bars done"); - - // let thread1 = std::thread::spawn(|| { - // cu::set_thread_print_name("t1"); - // let answer = cu::prompt!("from thread 1")?; - // cu::info!("you entered: {answer}"); - // cu::Ok(()) - // }); - // let thread2 = std::thread::spawn(|| { - // cu::set_thread_print_name("t2"); - // let answer = cu::prompt!("from thread 2")?; - // cu::info!("you entered: {answer}"); - // cu::Ok(()) - // }); - // let thread3 = std::thread::spawn(|| { - // cu::set_thread_print_name("t3"); - // let answer = cu::prompt!("from thread 3")?; - // cu::info!("you entered: {answer}"); - // cu::Ok(()) - // }); - // let r1 = thread1.join().unwrap(); - // let r2 = thread2.join().unwrap(); - // let r3 = thread3.join().unwrap(); - // r1?; - // r2?; - // r3?; - // cu::info!("all threads joined ok"); - // - // let command = cu::prompt!("enter command")?; - // // note: in a real-world application, you would use something like - // // the `shell_words` crate to split the input - // let args: AnotherArgs = cu::check!( - // cu::cli::try_parse(command.split_whitespace()), - // "error parsing args" - // )?; - // cu::print!("parsed args: {args:?}"); - // // note: in a real-world application, this will probably be some subcommand - // if args.help { - // cu::cli::print_help::(true); - // } - - Ok(()) -} - -/// Test Another Arg -/// -/// long text here -#[derive(Debug, clap::Parser)] -#[clap( - name = "", - no_binary_name = true, - disable_help_flag = true, - disable_version_flag = true -)] -struct AnotherArgs { - /// the file - /// - /// long text here - pub file: String, - /// If we should copy - /// - /// long text here - #[clap(short, long)] - pub copy: bool, - - /// HELP ME - #[clap(short, long, conflicts_with = "copy")] - pub help: bool, -} diff --git a/packages/copper/src/cli/printer.rs b/packages/copper/src/cli/printer.rs index d3a5189..ac52cb7 100644 --- a/packages/copper/src/cli/printer.rs +++ b/packages/copper/src/cli/printer.rs @@ -385,7 +385,7 @@ fn print_task() -> JoinHandle<()> { let (result, is_password) = (read_plaintext(temp), false); // clear sensitive information in the memory - crate::zero_string(temp); + crate::str::zero(temp); // now, re-print the prompt text to the buffer without the prompt prefix if !is_password { while !task.prompt.ends_with('\n') { diff --git a/packages/copper/src/lib.rs b/packages/copper/src/lib.rs index 74f4009..fd19d09 100644 --- a/packages/copper/src/lib.rs +++ b/packages/copper/src/lib.rs @@ -65,6 +65,7 @@ //! - [Progress Bars](fn@crate::progress) //! - [Prompting](macro@crate::prompt) //! - [Coroutines (Async)](mod@crate::co) (via [`tokio`](https://docs.rs/tokio)) +//! - [File System Paths and Strings](trait@crate::PathExtension) //! //! # Feature Reference: //! - `fs`: Enables file system utils. See [`cu::fs`](module@fs) and [`cu::bin`](module@bin). @@ -80,7 +81,8 @@ extern crate self as cu; // --- Basic stuff (no feature needed) --- pub mod str; -pub use str::*; +pub use str::{ZString, ByteFormat}; + mod env_var; pub use env_var::*; mod atomic; // Atomic helpers @@ -167,9 +169,9 @@ pub mod pre { #[cfg(feature = "parse")] pub use crate::ParseTo as _; #[cfg(feature = "fs")] - pub use crate::PathExtension as _; + pub use crate::str::PathExtension as _; #[cfg(feature = "fs")] - pub use crate::PathExtensionOwned as _; + pub use crate::str::PathExtensionOwned as _; #[cfg(feature = "process")] pub use crate::Spawn as _; #[cfg(feature = "json")] diff --git a/packages/copper/src/process/builder.rs b/packages/copper/src/process/builder.rs index 97b86ee..898f5a6 100644 --- a/packages/copper/src/process/builder.rs +++ b/packages/copper/src/process/builder.rs @@ -6,7 +6,7 @@ use tokio::process::{Child as TokioChild, Command as TokioCommand}; use super::{Child, ChildIo, Config, Preset}; -use crate::{Context as _, PathExtension as _, co, pio}; +use crate::{Context as _, str::PathExtension as _, co, pio}; /// A [`Command`] to be built pub type CommandBuilder = Command<(), (), ()>; diff --git a/packages/copper/src/str/mod.rs b/packages/copper/src/str/mod.rs index dfc36f6..2144212 100644 --- a/packages/copper/src/str/mod.rs +++ b/packages/copper/src/str/mod.rs @@ -1,8 +1,12 @@ mod zstring; -pub use zstring::{ZString, zero_string}; +pub use zstring::{ZString, zero}; mod byte_format; pub use byte_format::ByteFormat; +mod osstring; +pub use osstring::OsStrExtension; + + // path requires fs since there are utils that checks for existence // (check_exists, normalize) #[cfg(feature = "fs")] diff --git a/packages/copper/src/str/osstring.rs b/packages/copper/src/str/osstring.rs new file mode 100644 index 0000000..62fdb77 --- /dev/null +++ b/packages/copper/src/str/osstring.rs @@ -0,0 +1,54 @@ +use std::ffi::{OsStr, OsString}; +use std::path::{Path, PathBuf}; + +use cu::Context as _; + +/// Convenience trait for working with [`OsStr`](std::ffi::OsStr) +/// +/// See [File System Paths and Strings](trait@crate::str::PathExtension) +pub trait OsStrExtension { + /// Get the path as UTF-8, error if it's not UTF-8 + fn as_utf8(&self) -> cu::Result<&str>; +} +/// Convenience trait for working with [`OsString`](std::ffi::OsString) +/// +/// See [File System Paths and Strings](trait@crate::str::PathExtension) +pub trait OsStrExtensionOwned { + /// Get the path as UTF-8, error if it's not UTF-8 + fn into_utf8(self) -> cu::Result; +} + +impl OsStrExtension for OsStr { + #[inline(always)] + fn as_utf8(&self) -> cu::Result<&str> { + cu::check!(self.to_str(), "not utf-8: {self:?}") + } +} + +impl OsStrExtension for Path { + #[inline(always)] + fn as_utf8(&self) -> crate::Result<&str> { + self.as_os_str().as_utf8() + } +} + +#[cfg(all(test, unix))] +mod test { + use std::os::unix::ffi::OsStrExt as _; + use super::*; + #[test] + fn test_not_utf8() { + let s = OsStr::from_bytes(b"hello\xffworld"); + let result = s.as_utf8().unwrap_err().to_string(); + assert_eq!(result, r#"not utf-8: "hello\xFFworld""#); + let result = Path::new(s).as_utf8().unwrap_err().to_string(); + assert_eq!(result, r#"not utf-8: "hello\xFFworld""#); + } + #[test] + fn test_utf8() { + let s = OsStr::from_bytes(b"hello world"); + assert!(s.as_utf8().is_ok()); + assert!(Path::new(s).as_utf8().is_ok()); + } +} + diff --git a/packages/copper/src/str/path.rs b/packages/copper/src/str/path.rs index e1bb018..d96237b 100644 --- a/packages/copper/src/str/path.rs +++ b/packages/copper/src/str/path.rs @@ -3,10 +3,45 @@ use std::path::{Path, PathBuf}; use crate::Context as _; -/// Extension to paths +/// # File System Paths and Strings +/// Rust works with [`String`](std::string)s, which are UTF-8 encoded bytes. +/// However, not all operating systems work with UTF-8. That's why Rust has +/// [`OsString`](std::ffi::OsString), which has platform-specific implementations. +/// And `PathBuf`s are wrappers for `OsString`. /// -/// Most of these are related to file system, and not purely path processing. -/// Therefore this is tied to the `fs` feature. +/// However, often when writing platform-independent code, we want to stay +/// in the UTF-8 realm, but conversion can be painful because you must handle +/// the error when the `OsString` is not valid UTF-8. +/// +/// ```rust +/// # use pistonite_cu as cu; +/// use std::ffi::{OsString, OsStr}; +/// +/// use cu::pre::*; +/// +/// fn take_os_string(s: &OsStr) -> cu::Result<()> { +/// match s.to_str() { +/// Some(s) => { +/// cu::info!("valid utf-8: {s}"); +/// Ok(()) +/// } +/// None => { +/// cu::bail!("not valid utf-8!"); +/// } +/// } +/// } +/// ``` +/// +/// `cu` provides extension traits that integrates with `cu::Result`, +/// so you can have the error handling by simply propagate with `?`. +/// +/// There are 4 traits, all will be included into scope with `use cu::pre::*;`. +/// The path extensions also have utilities for working with file system specifically, +/// (such as normalizing it), which is why they require the `fs` feature. +/// +/// - [`OsStrExtension`] +/// - [`OsStrExtensionOwned`] +/// - `PathExtension` - requires `fs` feature pub trait PathExtension { /// Get file name. Error if the file name is not UTF-8 or other error occurs fn file_name_str(&self) -> crate::Result<&str>; diff --git a/packages/copper/src/str/zstring.rs b/packages/copper/src/str/zstring.rs index 50de64e..e2b9471 100644 --- a/packages/copper/src/str/zstring.rs +++ b/packages/copper/src/str/zstring.rs @@ -39,7 +39,8 @@ impl AsRef for ZString { impl Drop for ZString { #[inline(always)] fn drop(&mut self) { - zero_string(&mut self.0) + // safety: we are dropped + unsafe { do_zero(&mut self.0) } } } impl std::ops::Deref for ZString { @@ -57,8 +58,15 @@ impl std::ops::DerefMut for ZString { } /// Write 0's to the internal buffer of the string -pub fn zero_string(s: &mut String) { +#[inline(always)] +pub fn zero(s: &mut String) { let mut s = std::mem::take(s); + // safety: s is dropped afterwards when going out of scope + unsafe { do_zero(&mut s) } +} + +// Safety: the string must be dropped afterwards +unsafe fn do_zero(s: &mut String) { // SAFETY: we don't use the string again for c in unsafe { s.as_bytes_mut() } { // SAFETY: c is a valid u8 pointer From 678d170638f54da9776a71fe68daefe54031f4bd Mon Sep 17 00:00:00 2001 From: Michael Zhao <44533763+Pistonight@users.noreply.github.com> Date: Sun, 11 Jan 2026 14:40:44 -0800 Subject: [PATCH 11/13] windows --- packages/copper/src/str/mod.rs | 2 +- packages/copper/src/str/osstring.rs | 45 ++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/packages/copper/src/str/mod.rs b/packages/copper/src/str/mod.rs index 2144212..620156d 100644 --- a/packages/copper/src/str/mod.rs +++ b/packages/copper/src/str/mod.rs @@ -4,7 +4,7 @@ mod byte_format; pub use byte_format::ByteFormat; mod osstring; -pub use osstring::OsStrExtension; +pub use osstring::{OsStrExtension, OsStrExtensionOwned}; // path requires fs since there are utils that checks for existence diff --git a/packages/copper/src/str/osstring.rs b/packages/copper/src/str/osstring.rs index 62fdb77..7ca859a 100644 --- a/packages/copper/src/str/osstring.rs +++ b/packages/copper/src/str/osstring.rs @@ -27,11 +27,26 @@ impl OsStrExtension for OsStr { impl OsStrExtension for Path { #[inline(always)] - fn as_utf8(&self) -> crate::Result<&str> { + fn as_utf8(&self) -> cu::Result<&str> { self.as_os_str().as_utf8() } } +impl OsStrExtensionOwned for OsString { + #[inline(always)] + fn into_utf8(self) -> cu::Result { + match self.into_string() { + Ok(s) => Ok(s), + Err(e) => cu::bail!("not utf-8: {e:?}"), + } + } +} +impl OsStrExtensionOwned for PathBuf { + #[inline(always)] + fn into_utf8(self) -> cu::Result { + self.into_os_string().into_utf8() + } +} #[cfg(all(test, unix))] mod test { use std::os::unix::ffi::OsStrExt as _; @@ -52,3 +67,31 @@ mod test { } } +#[cfg(all(test, windows))] +mod test { + use std::os::windows::ffi::OsStringExt as _; + use super::*; + #[test] + fn test_not_utf8() { + let wide: &[u16] = &[0x0068, 0x0065, 0x006C, 0x006C, 0x006F, 0xD800, 0x0077, 0x006F, 0x0072, 0x006C, 0x0064]; + let s = OsString::from_wide(wide); + let result = s.as_utf8().unwrap_err().to_string(); + assert_eq!(result, r#"not utf-8: "hello\u{d800}world""#); + let result = Path::new(&s).as_utf8().unwrap_err().to_string(); + assert_eq!(result, r#"not utf-8: "hello\u{d800}world""#); + + let result = s.clone().into_utf8().unwrap_err().to_string(); + assert_eq!(result, r#"not utf-8: "hello\u{d800}world""#); + let result = PathBuf::from(s).into_utf8().unwrap_err().to_string(); + assert_eq!(result, r#"not utf-8: "hello\u{d800}world""#); + } + #[test] + fn test_utf8() { + let wide: &[u16] = &[0x0068, 0x0065, 0x006C, 0x006C, 0x006F, 0x0020, 0x0077, 0x006F, 0x0072, 0x006C, 0x0064]; + let s = OsString::from_wide(wide); + assert!(s.as_utf8().is_ok()); + assert!(Path::new(&s).as_utf8().is_ok()); + assert!(s.clone().into_utf8().is_ok()); + assert!(PathBuf::from(s).into_utf8().is_ok()); + } +} From 476b3a2b9219a23ef6d5990d6296c779eef37557 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Sun, 11 Jan 2026 15:39:53 -0800 Subject: [PATCH 12/13] clean up --- Taskfile.yml | 3 + packages/copper-proc-macros/Cargo.toml | 2 +- packages/copper/Cargo.toml | 47 ++--- packages/copper/Taskfile.yml | 125 ++++++----- packages/copper/src/cli/fmt/format_buffer.rs | 1 + packages/copper/src/cli/print_init.rs | 1 - packages/copper/src/cli/printer.rs | 9 +- .../src/{error_handling.rs => errhand.rs} | 2 +- packages/copper/src/{ => fs}/bin.rs | 0 packages/copper/src/fs/mod.rs | 7 + packages/copper/src/lib.rs | 55 ++--- packages/copper/src/process/arg.rs | 1 + packages/copper/src/process/builder.rs | 2 +- packages/copper/src/str/mod.rs | 5 +- packages/copper/src/str/osstring.rs | 26 ++- packages/copper/src/str/path.rs | 196 ++++++++---------- packages/copper/src/str/zstring.rs | 1 + packages/promethium/Cargo.toml | 8 +- packages/promethium/Taskfile.yml | 20 +- packages/terminal-tests/Cargo.toml | 2 +- packages/terminal-tests/Taskfile.yml | 5 + packages/terminal-tests/examples/spinner.rs | 37 ++-- packages/terminal-tests/output/spinner-0.txt | 34 --- packages/terminal-tests/src/main.rs | 4 +- 24 files changed, 274 insertions(+), 319 deletions(-) rename packages/copper/src/{error_handling.rs => errhand.rs} (99%) rename packages/copper/src/{ => fs}/bin.rs (100%) delete mode 100644 packages/terminal-tests/output/spinner-0.txt diff --git a/Taskfile.yml b/Taskfile.yml index c964b13..e905d7e 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -14,6 +14,7 @@ includes: cu: {taskfile: ./packages/copper, dir: ./packages/copper, internal: true} cu-proc-macros: {taskfile: ./packages/copper-proc-macros, dir: ./packages/copper-proc-macros, internal: true} pm: {taskfile: ./packages/promethium, dir: ./packages/promethium, internal: true} + terminal-tests: {taskfile: ./packages/terminal-tests, dir: ./packages/terminal-tests, internal: true} tasks: install-cargo-extra-tools: @@ -33,11 +34,13 @@ tasks: - task: pm:check - task: cu-proc-macros:check - task: cu:check + - task: terminal-tests:check test: cmds: - task: pm:test - task: cu:test + - task: terminal-tests:run dev-doc: cmds: diff --git a/packages/copper-proc-macros/Cargo.toml b/packages/copper-proc-macros/Cargo.toml index 72f0f77..ed2dd20 100644 --- a/packages/copper-proc-macros/Cargo.toml +++ b/packages/copper-proc-macros/Cargo.toml @@ -12,7 +12,7 @@ exclude = [ [dependencies.pm] package = "pistonite-pm" -version = "0.2.3" +version = "0.2.4" path = "../promethium" features = ["full"] diff --git a/packages/copper/Cargo.toml b/packages/copper/Cargo.toml index 92103db..4d87f04 100644 --- a/packages/copper/Cargo.toml +++ b/packages/copper/Cargo.toml @@ -17,7 +17,7 @@ pistonite-cu-proc-macros = { version = "0.2.5", path = "../copper-proc-macros" } anyhow = "1.0.100" log = "0.4.29" -# printing/cli +# --- Command Line Interface --- oneshot = { version = "0.1.11", optional = true } env_filter = { version = "0.1.4", optional = true } terminal_size = { version = "0.4.3", optional = true } @@ -25,38 +25,35 @@ unicode-width = { version = "0.2.2", features = ["cjk"], optional = true } clap = { version = "4.5.54", features = ["derive"], optional = true } regex = { version = "1.12.2", optional = true } -# fs +# --- Coroutine --- +num_cpus = { version = "1.17.0", optional = true } +tokio = { version = "1.49.0", optional = true, features = [ + "macros", "rt-multi-thread" +] } + +# --- File System --- dunce = {version="1.0.5", optional = true} which = {version = "8.0.0", optional = true } pathdiff = {version = "0.2.3", optional=true} -spin = {version = "0.10.0", optional = true} filetime = { version = "0.2.26", optional = true} glob = { version = "0.3.3", optional = true } +spin = {version = "0.10.0", optional = true} # for PIO # serde serde = { version = "1.0.228", features = ["derive"], optional = true } -serde_json = { version = "1.0.148", optional = true } +serde_json = { version = "1.0.149", optional = true } serde_yaml_ng = { version = "0.10.0", optional = true } -toml = { version = "0.9.10", optional = true } +toml = { version = "0.9.11", optional = true } # derive derive_more = { version = "2.1.1", features = ["full"], optional = true } -# coroutine -num_cpus = { version = "1.17.0", optional = true } - [target.'cfg(unix)'.dependencies] -libc = "0.2.179" +libc = "0.2.180" [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61.2", features = ["Win32_Foundation", "Win32_System_Console", "Win32_Storage_FileSystem", "Win32_Security", "Win32_System_SystemServices"] } -[dependencies.tokio] -version = "1.49.0" -features = [ "macros", "rt-multi-thread" ] -optional = true - - [dev-dependencies] [dev-dependencies.tokio] @@ -64,7 +61,7 @@ version = "1.49.0" features = [ "macros", "rt-multi-thread", "time" ] [features] -default = ["full"] +default = [] full = [ "cli", "coroutine-heavy", @@ -76,31 +73,31 @@ full = [ "derive", ] -# Command Line Interface +# --- Command Line Interface --- print = ["dep:oneshot", "dep:regex", "dep:env_filter", "dep:terminal_size", "dep:unicode-width"] cli = ["dep:clap", "print"] prompt = ["print"] prompt-password = ["prompt"] -# Enable coroutine drivers, which allow interop with async +# --- Coroutine --- coroutine = [ "dep:tokio", "dep:num_cpus", "tokio/sync", "tokio/io-util", "tokio/io-std" ] -# Enable heavy coroutine drived by multi-threaded tokio runtime -coroutine-heavy = ["coroutine"] -# Enable spawning child processes -process = [ +coroutine-heavy = ["coroutine"] # enable heavy coroutine drived by multi-threaded tokio runtime + +# --- File System --- +process = [ # enable spawning child processes "coroutine", "fs", "dep:spin", "tokio/process", "tokio/time", ] -# Enable file system and path util -fs = [ +fs = [ # enable file system and path util "dep:which", "dep:pathdiff", "dep:dunce", "dep:filetime", "dep:glob", "tokio?/fs" ] + # Enable parsing utils parse = [] serde = ["dep:serde"] @@ -124,7 +121,7 @@ __test = [] [[example]] name = "prompt_password" -required-features = ["prompt-password", "cli"] +required-features = ["prompt-password", "cli", "parse"] [[example]] name = "process" diff --git a/packages/copper/Taskfile.yml b/packages/copper/Taskfile.yml index ddaf378..1a1faa6 100644 --- a/packages/copper/Taskfile.yml +++ b/packages/copper/Taskfile.yml @@ -8,73 +8,70 @@ includes: tasks: check: - cmds: - - task: cargo:clippy-package-feature - vars: - PACKAGE: pistonite-cu - FEATURES: default - - task: cargo:clippy-package-feature - vars: - PACKAGE: pistonite-cu - FEATURES: print - - task: cargo:clippy-package-feature - vars: - PACKAGE: pistonite-cu - FEATURES: cli - - task: cargo:clippy-package-feature - vars: - PACKAGE: pistonite-cu - FEATURES: prompt - - task: cargo:clippy-package-feature - vars: - PACKAGE: pistonite-cu - FEATURES: prompt-password - - task: cargo:clippy-package-feature - vars: - PACKAGE: pistonite-cu - FEATURES: prompt,cli - - task: cargo:clippy-package-feature - vars: - PACKAGE: pistonite-cu - FEATURES: prompt-password,cli - - task: cargo:clippy-package-feature - vars: - PACKAGE: pistonite-cu - FEATURES: cli,coroutine - - task: cargo:clippy-package-feature - vars: - PACKAGE: pistonite-cu - FEATURES: fs - - task: cargo:clippy-package-feature - vars: - PACKAGE: pistonite-cu - FEATURES: process - - task: cargo:clippy-package-feature - vars: - PACKAGE: pistonite-cu - FEATURES: parse - - task: cargo:clippy-package-feature - vars: - PACKAGE: pistonite-cu - FEATURES: json,yaml,toml - - task: cargo:clippy-package-feature - vars: - PACKAGE: pistonite-cu - FEATURES: full - - task: cargo:fmt-check - - cmd: echo "checking default feature"; (grep "^default = \[\]$" Cargo.toml || rg "^default = \[\]$" cargo.toml) + - task: cargo:clippy-package-feature + vars: + PACKAGE: pistonite-cu + FEATURES: default + - task: cargo:clippy-package-feature + vars: + PACKAGE: pistonite-cu + FEATURES: print + - task: cargo:clippy-package-feature + vars: + PACKAGE: pistonite-cu + FEATURES: cli + - task: cargo:clippy-package-feature + vars: + PACKAGE: pistonite-cu + FEATURES: prompt + - task: cargo:clippy-package-feature + vars: + PACKAGE: pistonite-cu + FEATURES: prompt-password + - task: cargo:clippy-package-feature + vars: + PACKAGE: pistonite-cu + FEATURES: prompt,cli + - task: cargo:clippy-package-feature + vars: + PACKAGE: pistonite-cu + FEATURES: prompt-password,cli + - task: cargo:clippy-package-feature + vars: + PACKAGE: pistonite-cu + FEATURES: cli,coroutine + - task: cargo:clippy-package-feature + vars: + PACKAGE: pistonite-cu + FEATURES: fs + - task: cargo:clippy-package-feature + vars: + PACKAGE: pistonite-cu + FEATURES: process + - task: cargo:clippy-package-feature + vars: + PACKAGE: pistonite-cu + FEATURES: parse + - task: cargo:clippy-package-feature + vars: + PACKAGE: pistonite-cu + FEATURES: json,yaml,toml + - task: cargo:clippy-package-feature + vars: + PACKAGE: pistonite-cu + FEATURES: full + - task: cargo:fmt-check + - cmd: echo "checking default feature"; (grep "^default = \[\]$" Cargo.toml || rg "^default = \[\]$" Cargo.toml) fix: - cmds: - - task: cargo:fmt-fix + - task: cargo:fmt-fix test: - cmds: - - cargo test - - cargo test --features cli - - cargo test --features full + - cargo test + - cargo test --features cli + - cargo test --features full + publish: - cmds: - - cmd: cargo publish - ignore_error: true + - cmd: cargo publish + ignore_error: true diff --git a/packages/copper/src/cli/fmt/format_buffer.rs b/packages/copper/src/cli/fmt/format_buffer.rs index 6f3c486..29e9d8c 100644 --- a/packages/copper/src/cli/fmt/format_buffer.rs +++ b/packages/copper/src/cli/fmt/format_buffer.rs @@ -29,6 +29,7 @@ impl FormatBuffer { self.buffer.as_str() } /// Take the formatted buffer content out, leaving empty string + #[allow(unused)] // prompt uses it pub fn take(&mut self) -> String { std::mem::take(&mut self.buffer) } diff --git a/packages/copper/src/cli/print_init.rs b/packages/copper/src/cli/print_init.rs index da4ddac..cfd87ec 100644 --- a/packages/copper/src/cli/print_init.rs +++ b/packages/copper/src/cli/print_init.rs @@ -77,7 +77,6 @@ pub fn init_options(color: lv::Color, level: lv::Print, prompt: Option; diff --git a/packages/copper/src/str/mod.rs b/packages/copper/src/str/mod.rs index 620156d..692a261 100644 --- a/packages/copper/src/str/mod.rs +++ b/packages/copper/src/str/mod.rs @@ -1,3 +1,5 @@ +//! Working with bytes + mod zstring; pub use zstring::{ZString, zero}; mod byte_format; @@ -6,10 +8,9 @@ pub use byte_format::ByteFormat; mod osstring; pub use osstring::{OsStrExtension, OsStrExtensionOwned}; - // path requires fs since there are utils that checks for existence // (check_exists, normalize) #[cfg(feature = "fs")] mod path; #[cfg(feature = "fs")] -pub use path::{PathExtension, PathExtensionOwned}; +pub use path::PathExtension; diff --git a/packages/copper/src/str/osstring.rs b/packages/copper/src/str/osstring.rs index 7ca859a..1088717 100644 --- a/packages/copper/src/str/osstring.rs +++ b/packages/copper/src/str/osstring.rs @@ -21,6 +21,9 @@ pub trait OsStrExtensionOwned { impl OsStrExtension for OsStr { #[inline(always)] fn as_utf8(&self) -> cu::Result<&str> { + // to_str is ok on all platforms, because Rust internally + // represent OsStrings on Windows as WTF-8 + // see https://doc.rust-lang.org/src/std/sys_common/wtf8.rs.html cu::check!(self.to_str(), "not utf-8: {self:?}") } } @@ -47,10 +50,11 @@ impl OsStrExtensionOwned for PathBuf { self.into_os_string().into_utf8() } } + #[cfg(all(test, unix))] mod test { - use std::os::unix::ffi::OsStrExt as _; use super::*; + use std::os::unix::ffi::OsStrExt as _; #[test] fn test_not_utf8() { let s = OsStr::from_bytes(b"hello\xffworld"); @@ -58,22 +62,34 @@ mod test { assert_eq!(result, r#"not utf-8: "hello\xFFworld""#); let result = Path::new(s).as_utf8().unwrap_err().to_string(); assert_eq!(result, r#"not utf-8: "hello\xFFworld""#); + + let s = s.to_owned(); + let result = s.clone().into_utf8().unwrap_err().to_string(); + assert_eq!(result, r#"not utf-8: "hello\xFFworld""#); + let result = PathBuf::from(s).into_utf8().unwrap_err().to_string(); + assert_eq!(result, r#"not utf-8: "hello\xFFworld""#); } #[test] fn test_utf8() { let s = OsStr::from_bytes(b"hello world"); assert!(s.as_utf8().is_ok()); assert!(Path::new(s).as_utf8().is_ok()); + + let s = s.to_owned(); + assert!(s.clone().into_utf8().is_ok()); + assert!(PathBuf::from(s).into_utf8().is_ok()); } } #[cfg(all(test, windows))] mod test { - use std::os::windows::ffi::OsStringExt as _; use super::*; + use std::os::windows::ffi::OsStringExt as _; #[test] fn test_not_utf8() { - let wide: &[u16] = &[0x0068, 0x0065, 0x006C, 0x006C, 0x006F, 0xD800, 0x0077, 0x006F, 0x0072, 0x006C, 0x0064]; + let wide: &[u16] = &[ + 0x0068, 0x0065, 0x006C, 0x006C, 0x006F, 0xD800, 0x0077, 0x006F, 0x0072, 0x006C, 0x0064, + ]; let s = OsString::from_wide(wide); let result = s.as_utf8().unwrap_err().to_string(); assert_eq!(result, r#"not utf-8: "hello\u{d800}world""#); @@ -87,7 +103,9 @@ mod test { } #[test] fn test_utf8() { - let wide: &[u16] = &[0x0068, 0x0065, 0x006C, 0x006C, 0x006F, 0x0020, 0x0077, 0x006F, 0x0072, 0x006C, 0x0064]; + let wide: &[u16] = &[ + 0x0068, 0x0065, 0x006C, 0x006C, 0x006F, 0x0020, 0x0077, 0x006F, 0x0072, 0x006C, 0x0064, + ]; let s = OsString::from_wide(wide); assert!(s.as_utf8().is_ok()); assert!(Path::new(&s).as_utf8().is_ok()); diff --git a/packages/copper/src/str/path.rs b/packages/copper/src/str/path.rs index d96237b..806429e 100644 --- a/packages/copper/src/str/path.rs +++ b/packages/copper/src/str/path.rs @@ -1,11 +1,11 @@ use std::borrow::Cow; use std::path::{Path, PathBuf}; -use crate::Context as _; +use cu::Context as _; /// # File System Paths and Strings /// Rust works with [`String`](std::string)s, which are UTF-8 encoded bytes. -/// However, not all operating systems work with UTF-8. That's why Rust has +/// However, not all operating systems work with UTF-8. That's why Rust has /// [`OsString`](std::ffi::OsString), which has platform-specific implementations. /// And `PathBuf`s are wrappers for `OsString`. /// @@ -39,20 +39,19 @@ use crate::Context as _; /// The path extensions also have utilities for working with file system specifically, /// (such as normalizing it), which is why they require the `fs` feature. /// -/// - [`OsStrExtension`] -/// - [`OsStrExtensionOwned`] +/// - [`OsStrExtension`](trait@crate::str::OsStrExtension) +/// - [`OsStrExtensionOwned`](trait@crate::str::OsStrExtensionOwned) /// - `PathExtension` - requires `fs` feature pub trait PathExtension { /// Get file name. Error if the file name is not UTF-8 or other error occurs - fn file_name_str(&self) -> crate::Result<&str>; - - /// Get the path as UTF-8, error if it's not UTF-8 - fn as_utf8(&self) -> crate::Result<&str>; + fn file_name_str(&self) -> cu::Result<&str>; /// Check that the path exists, or fail with an error - fn check_exists(&self) -> crate::Result<()>; + fn ensure_exists(&self) -> cu::Result<()>; /// Return the simplified path if the path has a Windows UNC prefix + /// + /// The behavior is the same cross-platform. fn simplified(&self) -> &Path; /// Get absolute path for a path. @@ -67,33 +66,36 @@ pub trait PathExtension { /// /// On Windows only, it returns the most compatible form using the `dunce` crate instead of /// UNC, and the drive letter is also normalized to upper case - fn normalize(&self) -> crate::Result; + fn normalize(&self) -> cu::Result; /// Like `normalize`, but with the additional guarantee that the path exists - fn normalize_exists(&self) -> crate::Result { + fn normalize_exists(&self) -> cu::Result { let x = self.normalize()?; - x.check_exists()?; + x.ensure_exists()?; Ok(x) } /// Like `normalize`, but with the additional guarantee that: - /// - The file name of the output will be the same as the input + /// - The file name of the output will be the same as the input. This is because + /// the executable can be a multicall binary that behaves differently + /// depending on the executable name. /// - The path exists and is not a directory - fn normalize_executable(&self) -> crate::Result; + fn normalize_executable(&self) -> cu::Result; /// Get the parent path as an absolute path /// /// Path navigation is very complex and that's why we are paying a little performance /// cost and always returning `PathBuf`, and always converting the path to absolute. /// - /// For path manipulating (i.e. as a OsStr), instead of navigation, use std `parent()` + /// For path manipulation (i.e. as a OsStr), instead of navigation, use std `parent()` /// instead - fn parent_abs(&self) -> crate::Result { + #[inline(always)] + fn parent_abs(&self) -> cu::Result { self.parent_abs_times(1) } /// Effecitvely chaining `parent_abs` `x` times - fn parent_abs_times(&self, x: usize) -> crate::Result; + fn parent_abs_times(&self, x: usize) -> cu::Result; /// Try converting self to a relative path from current working directory. /// @@ -106,43 +108,26 @@ pub trait PathExtension { fn try_to_rel_from(&self, path: impl AsRef) -> Cow<'_, Path>; /// Start building a child process with the path as the executable + /// + /// See [Spawn Commands](crate::CommandBuilder) #[cfg(feature = "process")] - fn command(&self) -> crate::CommandBuilder; -} - -/// Extension to paths -/// -/// Most of these are related to file system, and not purely path processing. -/// Therefore this is tied to the `fs` feature. -pub trait PathExtensionOwned { - /// Get the path as UTF-8, error if it's not UTF-8 - fn into_utf8(self) -> crate::Result; + fn command(&self) -> cu::CommandBuilder; } impl PathExtension for Path { - fn file_name_str(&self) -> crate::Result<&str> { + fn file_name_str(&self) -> cu::Result<&str> { let file_name = self .file_name() - .with_context(|| format!("cannot get file name for path: {}", self.display()))?; + .with_context(|| format!("cannot get file name for path: '{}'", self.display()))?; // to_str is ok on all platforms, because Rust internally // represent OsStrings on Windows as WTF-8 // see https://doc.rust-lang.org/src/std/sys_common/wtf8.rs.html let Some(file_name) = file_name.to_str() else { - crate::bail!("file name is not valid UTF-8: {}", self.display()); + crate::bail!("file name is not utf-8: '{}'", self.display()); }; Ok(file_name) } - fn as_utf8(&self) -> crate::Result<&str> { - // to_str is ok on all platforms, because Rust internally - // represent OsStrings on Windows as WTF-8 - // see https://doc.rust-lang.org/src/std/sys_common/wtf8.rs.html - let Some(path) = self.to_str() else { - crate::bail!("path is not valid UTF-8: {}", self.display()); - }; - Ok(path) - } - fn simplified(&self) -> &Path { if self.as_os_str().as_encoded_bytes().starts_with(b"\\\\") { dunce::simplified(self) @@ -151,14 +136,14 @@ impl PathExtension for Path { } } - fn check_exists(&self) -> crate::Result<()> { + fn ensure_exists(&self) -> cu::Result<()> { if !self.exists() { crate::bail!("path '{}' does not exist.", self.display()); } Ok(()) } - fn normalize(&self) -> crate::Result { + fn normalize(&self) -> cu::Result { if let Ok(x) = dunce::canonicalize(self) { return Ok(x); }; @@ -167,9 +152,8 @@ impl PathExtension for Path { } let Ok(mut base) = dunce::canonicalize(".") else { - crate::warn!("failed to normalize current directory"); crate::bail!( - "cannot normalize current directory when normalizing relative path: {}", + "failed to normalize current directory when normalizing relative path: '{}'", self.display() ); }; @@ -185,9 +169,8 @@ impl PathExtension for Path { fallback_normalize_absolute(self)? } else { let Ok(mut base) = dunce::canonicalize(".") else { - crate::warn!("failed to normalize current directory"); crate::bail!( - "cannot normalize current directory when normalizing relative path: {}", + "failed to normalize current directory when normalizing relative path: '{}'", self.display() ); }; @@ -222,36 +205,9 @@ impl PathExtension for Path { Ok(out) } + #[inline(always)] fn try_to_rel_from(&self, path: impl AsRef) -> Cow<'_, Path> { - let path = path.as_ref(); - let res = match (self.is_absolute(), path.is_absolute()) { - (true, true) => pathdiff::diff_paths(self, path), - (true, false) => { - let Ok(base) = path.normalize() else { - return Cow::Borrowed(self); - }; - pathdiff::diff_paths(self, base.as_path()) - } - (false, true) => { - let Ok(self_) = self.normalize() else { - return Cow::Borrowed(self); - }; - pathdiff::diff_paths(self_.as_path(), path) - } - (false, false) => { - let Ok(self_) = self.normalize() else { - return Cow::Borrowed(self); - }; - let Ok(base) = path.normalize() else { - return Cow::Borrowed(self); - }; - pathdiff::diff_paths(self_.as_path(), base.as_path()) - } - }; - match res { - None => Cow::Borrowed(self), - Some(x) => Cow::Owned(x), - } + try_to_rel_from(self, path.as_ref()) } #[cfg(feature = "process")] @@ -301,47 +257,61 @@ fn fallback_normalize_absolute(path: &Path) -> crate::Result { } } -macro_rules! impl_for_as_ref_path { - ($type:ty) => { - impl PathExtension for $type { - fn file_name_str(&self) -> crate::Result<&str> { - AsRef::::as_ref(self).file_name_str() - } - fn as_utf8(&self) -> crate::Result<&str> { - AsRef::::as_ref(self).as_utf8() - } - fn simplified(&self) -> &Path { - AsRef::::as_ref(self).simplified() - } - fn normalize(&self) -> crate::Result { - AsRef::::as_ref(self).normalize() - } - fn normalize_executable(&self) -> crate::Result { - AsRef::::as_ref(self).normalize_executable() - } - fn check_exists(&self) -> crate::Result<()> { - AsRef::::as_ref(self).check_exists() - } - fn parent_abs_times(&self, x: usize) -> crate::Result { - AsRef::::as_ref(self).parent_abs_times(x) - } - fn try_to_rel_from(&self, path: impl AsRef) -> Cow<'_, Path> { - AsRef::::as_ref(self).try_to_rel_from(path) - } - #[cfg(feature = "process")] - fn command(&self) -> crate::CommandBuilder { - AsRef::::as_ref(self).command() - } +fn try_to_rel_from<'a>(self_: &'a Path, path: &Path) -> Cow<'a, Path> { + let res = match (self_.is_absolute(), path.is_absolute()) { + (true, true) => pathdiff::diff_paths(self_, path), + (true, false) => { + let Ok(base) = path.normalize() else { + return Cow::Borrowed(self_); + }; + pathdiff::diff_paths(self_, base.as_path()) + } + (false, true) => { + let Ok(self_) = self_.normalize() else { + return Cow::Borrowed(self_); + }; + pathdiff::diff_paths(self_.as_path(), path) + } + (false, false) => { + let Ok(self_abs) = self_.normalize() else { + return Cow::Borrowed(self_); + }; + let Ok(base) = path.normalize() else { + return Cow::Borrowed(self_); + }; + pathdiff::diff_paths(self_abs.as_path(), base.as_path()) } }; + match res { + None => Cow::Borrowed(self_), + Some(x) => Cow::Owned(x), + } } -impl_for_as_ref_path!(PathBuf); - -impl PathExtensionOwned for PathBuf { - fn into_utf8(self) -> crate::Result { - self.into_os_string() - .into_string() - .map_err(|e| crate::fmterr!("path is not valid UTF-8: {}", e.display())) +impl PathExtension for PathBuf { + fn file_name_str(&self) -> crate::Result<&str> { + self.as_path().file_name_str() + } + fn simplified(&self) -> &Path { + self.as_path().simplified() + } + fn normalize(&self) -> crate::Result { + self.as_path().normalize() + } + fn normalize_executable(&self) -> crate::Result { + self.as_path().normalize_executable() + } + fn ensure_exists(&self) -> crate::Result<()> { + self.as_path().ensure_exists() + } + fn parent_abs_times(&self, x: usize) -> crate::Result { + self.as_path().parent_abs_times(x) + } + fn try_to_rel_from(&self, path: impl AsRef) -> Cow<'_, Path> { + self.as_path().try_to_rel_from(path) + } + #[cfg(feature = "process")] + fn command(&self) -> crate::CommandBuilder { + self.as_path().command() } } diff --git a/packages/copper/src/str/zstring.rs b/packages/copper/src/str/zstring.rs index e2b9471..d0e0dac 100644 --- a/packages/copper/src/str/zstring.rs +++ b/packages/copper/src/str/zstring.rs @@ -66,6 +66,7 @@ pub fn zero(s: &mut String) { } // Safety: the string must be dropped afterwards +#[allow(clippy::ptr_arg)] unsafe fn do_zero(s: &mut String) { // SAFETY: we don't use the string again for c in unsafe { s.as_bytes_mut() } { diff --git a/packages/promethium/Cargo.toml b/packages/promethium/Cargo.toml index 1eac471..5d5e0c6 100644 --- a/packages/promethium/Cargo.toml +++ b/packages/promethium/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pistonite-pm" -version = "0.2.3" +version = "0.2.4" edition = "2024" description = "Procedural Macro Common Utils" repository = "https://github.com/Pistonite/cu" @@ -11,9 +11,9 @@ exclude = [ ] [dependencies] -proc-macro2 = { version = "1.0.104", default-features = false } -quote = { version = "1.0.42", default-features = false } -syn = { version = "2.0.113", default-features = false } +proc-macro2 = { version = "1.0.105", default-features = false } +quote = { version = "1.0.43", default-features = false } +syn = { version = "2.0.114", default-features = false } [features] default = [ diff --git a/packages/promethium/Taskfile.yml b/packages/promethium/Taskfile.yml index ed9be82..f5e50b4 100644 --- a/packages/promethium/Taskfile.yml +++ b/packages/promethium/Taskfile.yml @@ -8,18 +8,14 @@ includes: tasks: check: - cmds: - - task: cargo:clippy-all - - task: cargo:fmt-check + - task: cargo:clippy-all + - task: cargo:fmt-check fix: - cmds: - - task: cargo:fmt-fix + - task: cargo:fmt-fix publish: - cmds: - - cmd: cargo publish - ignore_error: true + - cmd: cargo publish + ignore_error: true test: - cmds: - - cargo test - - cargo test --features full - - cargo test --no-default-features --features full + - cargo test + - cargo test --features full + - cargo test --no-default-features --features full diff --git a/packages/terminal-tests/Cargo.toml b/packages/terminal-tests/Cargo.toml index 85a018d..b3b7165 100644 --- a/packages/terminal-tests/Cargo.toml +++ b/packages/terminal-tests/Cargo.toml @@ -18,7 +18,7 @@ required-features = ["bin"] [features] default = ["bin"] -bin = ["cu/cli", "cu/process", "cu/coroutine-heavy"] +bin = ["cu/cli", "cu/process", "cu/coroutine-heavy", "cu/json"] common = ["cu/__test", "cu/cli"] __test-print_levels = [] diff --git a/packages/terminal-tests/Taskfile.yml b/packages/terminal-tests/Taskfile.yml index 37f2b28..0731d23 100644 --- a/packages/terminal-tests/Taskfile.yml +++ b/packages/terminal-tests/Taskfile.yml @@ -9,3 +9,8 @@ includes: tasks: run: - cargo run --features bin -- {{.CLI_ARGS}} + check: + - task: cargo:clippy-all + - task: cargo:fmt-check + fix: + - task: cargo:fmt-fix diff --git a/packages/terminal-tests/examples/spinner.rs b/packages/terminal-tests/examples/spinner.rs index 0ece8eb..4a24869 100644 --- a/packages/terminal-tests/examples/spinner.rs +++ b/packages/terminal-tests/examples/spinner.rs @@ -1,15 +1,13 @@ -// $ 0 -y -// $ 1 -y -// $ 0 -// $ 1 +// $- 0 +// $- 1 -use std::sync::atomic::{AtomicBool, Ordering}; use std::thread; use std::time::Duration; use cu::pre::*; -static LONG_SLEEP: AtomicBool = AtomicBool::new(false); +// spinner tests are skipped since the output can be unstable, +// depending on how the printing thread is scheduled #[derive(clap::Parser, Clone, AsRef)] struct Args { @@ -21,14 +19,7 @@ struct Args { #[cu::cli] fn main(args: Args) -> cu::Result<()> { cu::lv::disable_print_time(); - if !cu::yesno!("short sleep")? { - // relaxd fine, only accessing on this thread - LONG_SLEEP.store(true, Ordering::Relaxed); - } - static CASES: &[fn() -> cu::Result<()>] = &[ - test_case_1, - test_case_2, - ]; + static CASES: &[fn() -> cu::Result<()>] = &[test_case_1, test_case_2]; CASES[args.case]() } @@ -48,21 +39,21 @@ fn test_case_1() -> cu::Result<()> { { // bar with progress let bar = cu::progress("finite").total(3).spawn(); - cu::progress!(bar+=1, "message1"); + cu::progress!(bar += 1, "message1"); sleep_tick(); - cu::progress!(bar+=1, "message2"); + cu::progress!(bar += 1, "message2"); sleep_tick(); - cu::progress!(bar+=1, "message3"); + cu::progress!(bar += 1, "message3"); sleep_tick(); } { // bar with no keep let bar = cu::progress("finite, nokeep").keep(false).spawn(); - cu::progress!(bar=1, "message1"); + cu::progress!(bar = 1, "message1"); sleep_tick(); - cu::progress!(bar=2, "message2"); + cu::progress!(bar = 2, "message2"); sleep_tick(); - cu::progress!(bar=3, "message3"); + cu::progress!(bar = 3, "message3"); sleep_tick(); } Ok(()) @@ -106,9 +97,5 @@ fn test_case_2() -> cu::Result<()> { } fn sleep_tick() { - if LONG_SLEEP.load(Ordering::Relaxed) { - thread::sleep(Duration::from_secs(2)); - } else { - thread::sleep(Duration::from_millis(10)); - } + thread::sleep(Duration::from_secs(1)); } diff --git a/packages/terminal-tests/output/spinner-0.txt b/packages/terminal-tests/output/spinner-0.txt deleted file mode 100644 index 33e0712..0000000 --- a/packages/terminal-tests/output/spinner-0.txt +++ /dev/null @@ -1,34 +0,0 @@ -$ 0 -STDOUT >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -^CR -\x1B[K\xE2\xA0\x8B] unbounded: message1^LF -^CR -\x1B[K\x1B[1A\x1B[K\xE2\xA0\x8B] unbounded: message2^LF -^CR -\x1B[K\x1B[1A\x1B[K\xE2\xA0\x8B] unbounded: message3^LF -^CR -\x1B[K\x1B[1A\x1B[K\xE2\xA0\xBF] unbounded: done^LF -^CR -\x1B[K^CR -\x1B[K\xE2\xA0\x8B][1/3] finite: 33.33% message1^LF -^CR -\x1B[K\x1B[1A\x1B[K\xE2\xA0\x8B][2/3] finite: 66.67% message2^LF -^CR -\x1B[K\x1B[1A\x1B[K\xE2\xA0\x8B][3/3] finite: 100% message3^LF -^CR -\x1B[K\x1B[1A\x1B[K\xE2\xA0\xBF][3/3] finite: done^LF -^CR -\x1B[K^CR -\x1B[K\xE2\xA0\x8B] finite, nokeep: message1^LF -^CR -\x1B[K\x1B[1A\x1B[K\xE2\xA0\x8B] finite, nokeep: message2^LF -^CR -\x1B[K\x1B[1A\x1B[K\xE2\xA0\x8B] finite, nokeep: message3^LF -^CR -\x1B[K\x1B[1A\x1B[K^CR -\x1B[K^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -^ { - let _ = write!(out, "status: {status}\n"); + let _ = writeln!(out, "status: {status}"); } None => { - let _ = write!(out, "timed out\n"); + let _ = writeln!(out, "timed out"); } } From 2c275e2d7beefbe683460771e7bd77cb9f67a073 Mon Sep 17 00:00:00 2001 From: Pistonight Date: Sun, 11 Jan 2026 15:42:21 -0800 Subject: [PATCH 13/13] fix ci --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88f4cfc..368d205 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: with: mono-dev: packages rust: stable - - run: task exec -- check + - run: task check test: runs-on: ubuntu-latest steps: @@ -22,7 +22,7 @@ jobs: with: mono-dev: packages rust: stable - - run: task exec -- test + - run: task test doc: runs-on: ubuntu-latest steps: @@ -30,4 +30,4 @@ jobs: with: mono-dev: packages rust: nightly - - run: task exec -- doc + - run: task doc