diff --git a/Taskfile.yml b/Taskfile.yml index e905d7e..0a2d384 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -52,7 +52,7 @@ tasks: doc: cmds: # note: require nightly toolchain in CI - - cargo doc --no-deps --features full,nightly + - cargo doc --no-deps --features full,nightly -p pistonite-cu -p pistonite-cu-proc-macros -p pistonite-pm - cmd: > echo "Generating index redirect"; echo "" > target/doc/index.html; diff --git a/packages/copper/Cargo.toml b/packages/copper/Cargo.toml index bcd72bf..1c77a3b 100644 --- a/packages/copper/Cargo.toml +++ b/packages/copper/Cargo.toml @@ -24,6 +24,7 @@ terminal_size = { version = "0.4.3", optional = true } 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 } +ctrlc = { version = "3.5.1", optional = true } # --- Coroutine --- num_cpus = { version = "1.17.0", optional = true } @@ -74,7 +75,7 @@ full = [ ] # --- Command Line Interface --- -print = ["dep:oneshot", "dep:regex", "dep:env_filter", "dep:terminal_size", "dep:unicode-width"] +print = ["dep:oneshot", "dep:regex", "dep:env_filter", "dep:terminal_size", "dep:unicode-width", "dep:ctrlc"] cli = ["dep:clap", "print"] prompt = ["print"] prompt-password = ["prompt"] @@ -82,7 +83,8 @@ prompt-password = ["prompt"] # --- Coroutine --- coroutine = [ "dep:tokio", "dep:num_cpus", - "tokio/sync", "tokio/io-util", "tokio/io-std" + "tokio/sync", "tokio/io-util", "tokio/io-std", + "tokio/time", ] coroutine-heavy = ["coroutine"] # enable heavy coroutine drived by multi-threaded tokio runtime @@ -91,7 +93,6 @@ process = [ # enable spawning child processes "coroutine", "fs", "dep:spin", "tokio/process", - "tokio/time", ] fs = [ # enable file system and path util "dep:which", "dep:pathdiff", "dep:dunce", "dep:filetime", "dep:glob", @@ -134,3 +135,7 @@ required-features = ["fs", "cli"] [[example]] name = "cargo" required-features = ["process", "cli", "json"] + +[[example]] +name = "ctrlc" +required-features = ["cli", "coroutine"] diff --git a/packages/copper/examples/ctrlc.rs b/packages/copper/examples/ctrlc.rs new file mode 100644 index 0000000..ea30986 --- /dev/null +++ b/packages/copper/examples/ctrlc.rs @@ -0,0 +1,100 @@ +use pistonite_cu as cu; +use std::time::Duration; + +#[cu::cli] +fn main(_: cu::cli::Flags) -> cu::Result<()> { + sync_main()?; + cu::co::run(async move { async_auto_check().await })?; + cu::co::run(async move { async_manual_check().await })?; + Ok(()) +} + +fn sync_main() -> cu::Result<()> { + let result = cu::cli::ctrlc_frame().execute(move |ctrlc| { + for _ in 0..30 { + cu::print!("(sync) please press Ctrl-C"); + std::thread::sleep(Duration::from_millis(100)); + ctrlc.check()?; + } + cu::warn!("(sync) about to return!"); + cu::Ok(42) + }); + match result { + Ok(None) => cu::info!("(sync) was aborted!"), + Ok(Some(n)) => cu::info!("(sync) was finished: {n}"), + Err(e) => cu::error!("(sync) error: {e:?}"), + } + Ok(()) +} + +async fn async_manual_check() -> cu::Result<()> { + let result = cu::cli::ctrlc_frame() + .abort_threshold(3) + .on_signal(|ctrlc| { + if !ctrlc.should_abort() { + cu::warn!("Ctrl-C 3 times to abort!") + } + }) + .co_execute(async |ctrlc| { + for _ in 0..10 { + cu::print!("(async) please press Ctrl-C"); + cu::co::sleep(Duration::from_secs(1)).await; + if ctrlc.should_abort() { + cu::info!("just kidding, we never abort"); + } + } + cu::warn!("(async) about to return!"); + cu::Ok(42) + }) + .await; + match result { + Ok(None) => cu::info!("(async) was aborted!"), + Ok(Some(n)) => cu::info!("(async) was finished: {n}"), + Err(e) => cu::error!("(async) error: {e:?}"), + } + Ok(()) +} + +async fn async_auto_check() -> cu::Result<()> { + let (send, mut recv) = tokio::sync::mpsc::unbounded_channel(); + let result = cu::cli::ctrlc_frame() + .on_signal(move |_| { + let _ = send.send(()); + }) + .co_execute(async move |ctrlc| { + let waiter = async move { + loop { + if recv.recv().await.is_none() { + return; + } + if ctrlc.should_abort() { + return; + } + } + }; + tokio::select! { + result = my_long_running_task() => { + result + } + _ = waiter => { + // does not matter what the value is - + // co_execute will ensure None is returned + // when aborted + Ok(0) + } + } + }) + .await?; + match result { + Some(x) => cu::info!("valud is: {x}"), + None => cu::error!("aborted!"), + } + Ok(()) +} + +async fn my_long_running_task() -> cu::Result { + loop { + cu::print!("will run forever if you don't abort"); + cu::co::sleep(Duration::from_secs(1)).await; + } +} diff --git a/packages/copper/src/atomic.rs b/packages/copper/src/atomic.rs index 68f7435..e5b462b 100644 --- a/packages/copper/src/atomic.rs +++ b/packages/copper/src/atomic.rs @@ -1,3 +1,5 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + /// An atomic wrapper with an underlying atomic storage and conversion to /// a type T. /// @@ -78,3 +80,9 @@ impl_atomic_type! { isize => AtomicIsize, new_isize, usize => AtomicUsize, new_usize, } + +#[allow(unused)] +pub(crate) fn next_atomic_usize() -> usize { + static ID: AtomicUsize = AtomicUsize::new(1); + ID.fetch_add(1, Ordering::SeqCst) +} diff --git a/packages/copper/src/cli/ctrlc.rs b/packages/copper/src/cli/ctrlc.rs new file mode 100644 index 0000000..1e85275 --- /dev/null +++ b/packages/copper/src/cli/ctrlc.rs @@ -0,0 +1,339 @@ +use std::sync::atomic::{AtomicU8, Ordering}; +use std::sync::{Arc, LazyLock, Mutex}; + +/// New handlers that will be added by the ctrlc thread +/// once ctrlc is pressed +static CTRLC_HANDLERS_NEW: Mutex>> = Mutex::new(Vec::new()); +/// Stack of CtrlC frames to signal +static CTRLC_SIGNAL_STACK: Mutex> = Mutex::new(Vec::new()); +/// Thread safe init lock +static INIT_ONCE: LazyLock> = LazyLock::new(|| { + let mut handlers = vec![]; + let set_result = ctrlc::try_set_handler(move || { + // populate new handlers + if let Ok(mut new_handlers) = CTRLC_HANDLERS_NEW.lock() { + handlers.extend(new_handlers.drain(..)); + } + // signal the stack + let mut signalled = false; + if let Ok(stack) = CTRLC_SIGNAL_STACK.lock() { + if let Some(frame) = stack.last() { + signalled = true; + frame.signal.signal(); + if let Some(f) = &frame.on_signal { + f(frame.signal.clone()) + } + } + } + + // note we are not holding any lock when invoking user-defined handlers + + // if user did not set any global handler or action frames, then we terminate + if !signalled && handlers.is_empty() { + std::process::exit(1); + } + for handler in handlers.iter_mut().rev() { + handler(); + } + }); + match set_result { + Err(ctrlc::Error::MultipleHandlers) => { + Err("failed to set ctrl-c handler: a handler is already set using the `ctrlc` crate. please set with cu::cli instead (see documentation for more information)".to_string()) + } + Err(other_error) => { + Err(format!("failed to set ctrl-c handler: {other_error}")) + } + Ok(_) => Ok(()) + } +}); + +/// Add a global handler to handle Ctrl-C signals +/// +/// See [Handling Ctrl-C](fn@crate::cli::ctrlc_frame). +/// +/// Note that this is only available with feature `cli` - since +/// you should not be adding a global handler from a library. +#[cfg(feature = "cli")] +pub fn add_global_ctrlc_handler(handler: F) -> cu::Result<()> { + { + let Ok(mut handlers) = CTRLC_HANDLERS_NEW.lock() else { + cu::bail!("global ctrl-c handler vector is poisoned"); + }; + handlers.push(Box::new(handler)); + } + if let Err(e) = &*INIT_ONCE { + cu::bail!("{e}"); + } + Ok(()) +} + +/// # Handling Ctrl-C +/// +/// The [`ctrlc`](https://docs.rs/ctrlc) crate provides a low-level cross-platform +/// way to set up a handler for `Ctrl-C`. `cu` builds wrappers around it so it can +/// be intergrated with other internals, such as prompts. +/// +/// # Action Frames +/// Most of the time, custom Ctrl-C behavior is for executing some long running +/// tasks and give the user the ability to abort it. +/// The [`execute`](CtrlcBuilder::execute) function pushes a new frame +/// to an internal stack, then executes the task synchronously on the same thread. +/// **The task itself is responsible for periodically checking the received +/// signal object if it has been aborted (by calling `ctrlc.check?`). +/// +/// The frame will only be removed from the stack when the task returns. +/// Note that different threads can spawn Ctrl-C frames, and those frame +/// may not finish in order. When user hits `Ctrl-C`, only the most recently +/// added frame (the top-most of the stack) will be notified. +/// It will continue to notify the frame until the task ends and removes +/// the frame from the stask. +/// +/// # Async Behavior +/// The async version of the API, [`co_execute`](CtrlcBuilder::co_execute), +/// is exactly the same, other than it takes an async closure instead. +/// Even though in the async case, we can let the runtime drop the future +/// to abort it without explicit checks, the explicit checks make it clearer +/// and easier to reason about program states when aborting. +/// +/// If you prefer automatically cancellation, consider this pattern: +/// ```rust,ignore +/// # use pistonite_cu as cu; +/// # async fn main_() -> cu::Result<()> { +/// let (send, mut recv) = tokio::sync::mpsc::unbounded_channel(); +/// let result = cu::cli::ctrlc_frame() +/// .on_signal(move |_| { let _ = send.send(()); }) +/// .co_execute(async move |ctrlc| { +/// let waiter = async move { +/// loop { +/// if recv.recv().await.is_none() { +/// return; +/// } +/// if ctrlc.should_abort() { +/// return; +/// } +/// } +/// }; +/// tokio::select! { +/// result = my_long_running_task() => { +/// return result; +/// } +/// _ = waiter => { +/// // does not matter what the value is - +/// // co_execute will ensure None is returned +/// // when aborted +/// return Ok(0); +/// } +/// } +/// }).await?; +/// match result { +/// Some(x) => cu::info!("valud is: {x}"), +/// None => cu::error!("aborted!"), +/// } +/// # Ok(()) } +/// +/// async fn my_long_running_task() -> cu::Result { +/// // your task here can get aborted without checking the signal +/// // which can be dangerous and have unintended effects! +/// Ok(42) +/// } +/// ``` +/// +/// # Return Value +/// The return value of the inner `cu::Result` will be transformed into +/// `cu::Result>`, where: +/// - `Ok(Some(value))`: the action was not aborted, and produced a value. +/// - `Ok(None)`: the action was aborted. +/// - The definition of "aborted" can be customized by setting the [`abort_threshold()`](CtrlcBuilder::abort_threshold) +/// on the builder +/// - `Err(e)`: either the inner task returned error, or some other error happened in the framework +/// (for example, when joining the task thread/future) +/// +/// ```rust,no_run +/// # use pistonite_cu as cu; +/// use std::thread; +/// use std::time::Duration; +/// +/// match cu::cli::ctrlc_frame().execute(|ctrlc| { +/// for _ in 0..10 { +/// cu::print!("please press Ctrl-C"); +/// thread::sleep(Duration::from_secs(2)); +/// ctrlc.check()?; // returns Err if signaled +/// } +/// cu::Ok(42) +/// }) { +/// Ok(None) => cu::info!("was aborted!"), +/// Ok(Some(n)) => cu::info!("was finished: {n}"), +/// Err(e) => cu::error!("error: {e:?}"), +/// } +/// ``` +/// +/// # Fallback +/// If the `Ctrl-C` framework failed to initialize, this may fallback to simply running +/// the task without the ability to receive signals. +/// +/// # Global Handlers +/// Action frames should be used whenever possible. +/// If you do need a global handler for `Ctrl-C`, use +/// [`cu::cli::add_global_ctrlc_handler`]. This is because the `ctrlc` crate +/// only allows one global handler. +/// +/// Global handlers are called after the action frame is notified (if any), +/// and called in reverse order of registration. +/// The underlying handler is lazily set up whenever a global handler +/// or action frame is added. If there are no longer any action frames +/// and there are no global handlers, the underlying handler +/// will call `std::process::exit(1)` to terminate. +#[inline(always)] +pub fn ctrlc_frame() -> CtrlcBuilder { + CtrlcBuilder::default() +} + +/// Builder for a new frame for handling `Ctrl-C` signals. +/// The canonical factory function for this is `cu::cli::ctrlc_frame`. +/// See [Handling Ctrl-C](ctrlc_frame). +pub struct CtrlcBuilder { + abort_threshold: u8, + on_signal: Option, +} +impl Default for CtrlcBuilder { + #[inline(always)] + fn default() -> Self { + Self { + abort_threshold: 1, + on_signal: None, + } + } +} +impl CtrlcBuilder { + /// Set the number of `Ctrl-C` signals required for the task + /// to be considered aborted (where the return will be `Ok(None`). + /// + /// Default is 1 + #[inline(always)] + pub fn abort_threshold(mut self, threshold: u8) -> Self { + self.abort_threshold = threshold; + self + } + + /// Set a function to be called when `Ctrl-C` signal is received. + /// + /// The function will be executed on the `Ctrl-C` signal handling thread, + /// not the thread that runs the task, and is + #[inline(always)] + pub fn on_signal(mut self, f: F) -> Self { + self.on_signal = Some(Box::new(f)); + self + } + + /// Execute the task + pub fn execute(self, f: F) -> cu::Result> + where + F: FnOnce(CtrlcSignal) -> cu::Result, + { + let signal = CtrlcSignal::new(self.abort_threshold); + let Some(ctrlc_frame_scope) = CtrlcFrame::push_scope(signal.clone(), self.on_signal) else { + return f(signal).map(Some); + }; + if let Err(e) = &*INIT_ONCE { + cu::bail!("{e}"); + } + let result = f(signal.clone()); + if signal.should_abort() { + return Ok(None); + } + drop(ctrlc_frame_scope); // suppress unused warning + result.map(Some) + } + + #[cfg(feature = "coroutine")] + pub async fn co_execute(self, f: F) -> cu::Result> + where + TFuture: Future>, + F: FnOnce(CtrlcSignal) -> TFuture, + { + let signal = CtrlcSignal::new(self.abort_threshold); + let Some(ctrlc_frame_scope) = CtrlcFrame::push_scope(signal.clone(), self.on_signal) else { + return f(signal).await.map(Some); + }; + if let Err(e) = &*INIT_ONCE { + cu::bail!("{e}"); + } + let result = f(signal.clone()).await; + if signal.should_abort() { + return Ok(None); + } + drop(ctrlc_frame_scope); // suppress unused warning + result.map(Some) + } +} + +type OnSignalFn = Box; +struct CtrlcFrame { + id: usize, + signal: CtrlcSignal, + on_signal: Option, +} +struct CtrlcScope(usize); +/// Signal passed into a task executing inside a `Ctrl-C` action frame, +/// for it to check if `Ctrl-C` has been signaled +#[derive(Clone)] +pub struct CtrlcSignal { + signaled_times: Arc, + abort_threshold: u8, +} +impl CtrlcFrame { + pub fn push_scope(signal: CtrlcSignal, on_signal: Option) -> Option { + let Ok(mut signal_stack) = CTRLC_SIGNAL_STACK.lock() else { + cu::trace!("failed to register new ctrl-c frame"); + return None; + }; + let id = crate::next_atomic_usize(); + signal_stack.push(Self { + id, + signal, + on_signal, + }); + Some(CtrlcScope(id)) + } +} +impl Drop for CtrlcScope { + fn drop(&mut self) { + if let Ok(mut signal_stack) = CTRLC_SIGNAL_STACK.lock() { + signal_stack.retain(|x| x.id != self.0); + } + } +} + +impl CtrlcSignal { + fn new(abort_threshold: u8) -> Self { + Self { + signaled_times: Arc::new(AtomicU8::new(0)), + abort_threshold, + } + } + /// Return an `Err` if `Ctrl-C` has been signaled + pub fn check(&self) -> cu::Result<()> { + if self.should_abort() { + cu::bail!("interrupted") + } + Ok(()) + } + /// Return `true` if `Ctrl-C` has been signaled at least + /// the same number of times as the abort_threshold + pub fn should_abort(&self) -> bool { + self.signaled_times() >= self.abort_threshold + } + /// Return `true` if `Ctrl-C` has been signaled at least once + pub fn signaled(&self) -> bool { + self.signaled_times() > 0 + } + /// Get the number of times `Ctrl-C` has been signaled + pub fn signaled_times(&self) -> u8 { + self.signaled_times.load(Ordering::Acquire) + } + /// Programmatically trigger the signal. Note that this does not + /// send actual signal or keyboard events + pub fn signal(&self) { + self.signaled_times.fetch_add(1, Ordering::SeqCst); + } +} diff --git a/packages/copper/src/cli/mod.rs b/packages/copper/src/cli/mod.rs index 575cb54..9303d19 100644 --- a/packages/copper/src/cli/mod.rs +++ b/packages/copper/src/cli/mod.rs @@ -159,6 +159,11 @@ mod password; #[cfg(feature = "prompt-password")] pub use password::password_chars_legal; +mod ctrlc; +#[cfg(feature = "cli")] +pub use ctrlc::add_global_ctrlc_handler; +pub use ctrlc::{CtrlcBuilder, CtrlcSignal, ctrlc_frame}; + /// Formatting utils pub(crate) mod fmt; diff --git a/packages/copper/src/cli/progress/builder.rs b/packages/copper/src/cli/progress/builder.rs index 55616a6..8fd1610 100644 --- a/packages/copper/src/cli/progress/builder.rs +++ b/packages/copper/src/cli/progress/builder.rs @@ -1,5 +1,4 @@ use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; use crate::cli::progress::{Estimater, ProgressBar, State, StateImmut}; @@ -193,7 +192,7 @@ impl ProgressBarBuilder { None }; let state_immut = StateImmut { - id: next_id(), + id: crate::next_atomic_usize(), parent: self.parent.as_ref().map(Arc::clone), prefix: self.message, done_message, @@ -209,8 +208,3 @@ impl ProgressBarBuilder { 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/co/handle.rs b/packages/copper/src/co/handle.rs index f920ae6..ae473f5 100644 --- a/packages/copper/src/co/handle.rs +++ b/packages/copper/src/co/handle.rs @@ -16,6 +16,11 @@ impl Handle { self.into() } + /// Test if the task has finished and is ready to be joined + pub fn is_finished(&self) -> bool { + self.0.is_finished() + } + /// Abort the task, trying to `join` or `co_join` an aborted /// task (if it's not already completed) will return an error indicating /// it's already aborted. diff --git a/packages/copper/src/co/mod.rs b/packages/copper/src/co/mod.rs index d1e8841..021ddac 100644 --- a/packages/copper/src/co/mod.rs +++ b/packages/copper/src/co/mod.rs @@ -72,7 +72,7 @@ //! ``` // re-exports -pub use tokio::{join, select, try_join}; +pub use tokio::{join, select, time::sleep, try_join}; mod runtime; #[cfg(not(feature = "coroutine-heavy"))] diff --git a/packages/copper/src/errhand.rs b/packages/copper/src/errhand.rs index 8734bf3..9845681 100644 --- a/packages/copper/src/errhand.rs +++ b/packages/copper/src/errhand.rs @@ -45,9 +45,10 @@ pub use anyhow::{Context, Error, Ok, Result, anyhow as fmterr, bail}; /// - [`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 +/// - [`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. +/// - [`cu::some!`] checks an `Option` and returns `Ok(None)` if the option is `None`. /// /// Here are other `anyhow` re-exports that are less commonly used /// - `anyhow::anyhow` is `cu::fmterr` @@ -220,6 +221,28 @@ macro_rules! ensure { }}; } +/// Check if an expression is `Some` +/// +/// This is a convienence macro to achieve a similar effect +/// of `?` on an option, in a function that returns `Result>` +/// +/// Effectively expands to +/// ```text +/// match { +/// Some(x) => x, +/// None => return Ok(None), +/// } +/// ``` +#[macro_export] +macro_rules! some { + ($result:expr) => { + match $result { + Some(x) => x, + None => return Ok(None), + } + }; +} + /// Invoke a print macro, then panic with the same message /// /// # Example diff --git a/packages/copper/src/lib.rs b/packages/copper/src/lib.rs index 85e7b44..f018892 100644 --- a/packages/copper/src/lib.rs +++ b/packages/copper/src/lib.rs @@ -62,6 +62,7 @@ //! - [Logging](mod@crate::lv) (via [`log`](https://docs.rs/log)) //! - [Printing and Command Line Interface](mod@crate::cli) (CLI arg parsing via //! [`clap`](https://docs.rs/clap)) +//! - [Handling Ctrl-C](fn@crate::cli::ctrlc_frame) //! - [Progress Bars](fn@crate::progress) //! - [Prompting](macro@crate::prompt) //! - [Coroutines (Async)](mod@crate::co) (via [`tokio`](https://docs.rs/tokio)) diff --git a/packages/terminal-tests/examples/print_levels.rs b/packages/terminal-tests/examples/print_levels.rs index e0da13a..8bf8112 100644 --- a/packages/terminal-tests/examples/print_levels.rs +++ b/packages/terminal-tests/examples/print_levels.rs @@ -11,6 +11,7 @@ #[cu::cli] fn main(_: cu::cli::Flags) -> cu::Result<()> { + cu::lv::disable_print_time(); cu::info!( "this is an info messagenmultilineaa 你好 sldkfjals🤖kdjflkasjdflkjasldkfjaklsdjflkjasldkfjlaksjdflkajsdklfjlaksjdfkljasldkfjlasldkjflaskdjflaksjdlfkajsldkfjkasjdlfkjaskldjflajsdlkfjlaskjdfklajsdf" ); diff --git a/packages/terminal-tests/examples/prompt.rs b/packages/terminal-tests/examples/prompt.rs index 933ea78..01074b9 100644 --- a/packages/terminal-tests/examples/prompt.rs +++ b/packages/terminal-tests/examples/prompt.rs @@ -8,6 +8,7 @@ #[cu::cli] fn main(_: cu::cli::Flags) -> cu::Result<()> { + cu::lv::disable_print_time(); cu::hint!("testing prompts"); if !cu::yesno!("continue?")? { cu::warn!("you chose to not continue!"); diff --git a/packages/terminal-tests/output/print_levels-2.txt b/packages/terminal-tests/output/print_levels-2.txt index 10cd55a..5e2cea5 100644 --- a/packages/terminal-tests/output/print_levels-2.txt +++ b/packages/terminal-tests/output/print_levels-2.txt @@ -9,7 +9,6 @@ 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 ^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> diff --git a/packages/terminal-tests/output/print_levels-3.txt b/packages/terminal-tests/output/print_levels-3.txt index 1fb088a..e6de77c 100644 --- a/packages/terminal-tests/output/print_levels-3.txt +++ b/packages/terminal-tests/output/print_levels-3.txt @@ -12,7 +12,6 @@ D] this is debug message^LF | ^LF :: today's weather is good^LF H] today's weather is ok^LF -I] finished in 0.00s^LF ^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> diff --git a/packages/terminal-tests/output/print_levels-4.txt b/packages/terminal-tests/output/print_levels-4.txt index 781fbcb..51bcd6b 100644 --- a/packages/terminal-tests/output/print_levels-4.txt +++ b/packages/terminal-tests/output/print_levels-4.txt @@ -10,12 +10,11 @@ E] this is error message^LF D] this is debug message^LF | 2^LF | ^LF -*] [print_levels print_levels.rs:20] this is trace message^LF +*] [print_levels print_levels.rs:21] 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 ^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> diff --git a/packages/terminal-tests/output/print_levels-7.txt b/packages/terminal-tests/output/print_levels-7.txt index c14f7a7..6f879c5 100644 --- a/packages/terminal-tests/output/print_levels-7.txt +++ b/packages/terminal-tests/output/print_levels-7.txt @@ -9,7 +9,6 @@ STDOUT >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> \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 ^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> diff --git a/packages/terminal-tests/output/print_levels-8.txt b/packages/terminal-tests/output/print_levels-8.txt index 0b440f8..67715bd 100644 --- a/packages/terminal-tests/output/print_levels-8.txt +++ b/packages/terminal-tests/output/print_levels-8.txt @@ -12,7 +12,6 @@ STDOUT >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> \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 ^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> diff --git a/packages/terminal-tests/output/print_levels-9.txt b/packages/terminal-tests/output/print_levels-9.txt index 27414e4..bce5eed 100644 --- a/packages/terminal-tests/output/print_levels-9.txt +++ b/packages/terminal-tests/output/print_levels-9.txt @@ -10,12 +10,11 @@ STDOUT >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> \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[95m*]\x1B[95m [print_levels print_levels.rs:21] 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 ^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> diff --git a/packages/terminal-tests/output/prompt-0.txt b/packages/terminal-tests/output/prompt-0.txt index f9a5714..ee7e0cc 100644 --- a/packages/terminal-tests/output/prompt-0.txt +++ b/packages/terminal-tests/output/prompt-0.txt @@ -2,7 +2,6 @@ $ -y --non-interactive STDOUT >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> H] testing prompts^LF E] fatal: prompt not allowed with --non-interactive^LF -I] finished in 0.00s^LF ^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> diff --git a/packages/terminal-tests/output/prompt-1.txt b/packages/terminal-tests/output/prompt-1.txt index 4789ae8..940e5f2 100644 --- a/packages/terminal-tests/output/prompt-1.txt +++ b/packages/terminal-tests/output/prompt-1.txt @@ -2,7 +2,6 @@ $ --non-interactive STDOUT >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> H] testing prompts^LF E] fatal: prompt not allowed with --non-interactive^LF -I] finished in 0.00s^LF ^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> diff --git a/packages/terminal-tests/output/prompt-2.txt b/packages/terminal-tests/output/prompt-2.txt index 42bc881..130ade6 100644 --- a/packages/terminal-tests/output/prompt-2.txt +++ b/packages/terminal-tests/output/prompt-2.txt @@ -8,7 +8,6 @@ H] testing prompts^LF ^CR \x1B[KI] you answered: rust^LF I] the answer is correct^LF -I] finished in 0.00s^LF ^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> diff --git a/packages/terminal-tests/output/prompt-3.txt b/packages/terminal-tests/output/prompt-3.txt index 212a297..8f71c4b 100644 --- a/packages/terminal-tests/output/prompt-3.txt +++ b/packages/terminal-tests/output/prompt-3.txt @@ -13,7 +13,6 @@ H] testing prompts^LF ^CR \x1B[KI] you answered: rust^LF I] the answer is correct^LF -I] finished in 0.00s^LF ^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> diff --git a/packages/terminal-tests/output/prompt-4.txt b/packages/terminal-tests/output/prompt-4.txt index c2ed302..46d09eb 100644 --- a/packages/terminal-tests/output/prompt-4.txt +++ b/packages/terminal-tests/output/prompt-4.txt @@ -13,7 +13,6 @@ H] testing prompts^LF ^CR \x1B[KI] you answered: json^LF E] fatal: the answer is incorrect^LF -I] finished in 0.00s^LF ^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> diff --git a/packages/terminal-tests/output/prompt-5.txt b/packages/terminal-tests/output/prompt-5.txt index 45463fc..4c4a597 100644 --- a/packages/terminal-tests/output/prompt-5.txt +++ b/packages/terminal-tests/output/prompt-5.txt @@ -7,7 +7,6 @@ H] testing prompts^LF \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 ^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> diff --git a/packages/terminal-tests/output/prompt-6.txt b/packages/terminal-tests/output/prompt-6.txt index 039c6ce..7146820 100644 --- a/packages/terminal-tests/output/prompt-6.txt +++ b/packages/terminal-tests/output/prompt-6.txt @@ -13,7 +13,6 @@ H] testing prompts^LF \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 ^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>