diff --git a/packages/copper-proc-macros/Cargo.toml b/packages/copper-proc-macros/Cargo.toml index b495093..b2f58d0 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.10" +version = "0.2.11" edition = "2024" description = "Proc-macros for Cu" repository = "https://github.com/Pistonite/cu" @@ -12,7 +12,7 @@ exclude = [ [dependencies.pm] package = "pistonite-pm" -version = "0.3.0" +version = "0.3.1" path = "../promethium" features = ["full"] diff --git a/packages/copper/.gitignore b/packages/copper/.gitignore index ea8c4bf..69b6b2b 100644 --- a/packages/copper/.gitignore +++ b/packages/copper/.gitignore @@ -1 +1,3 @@ /target +/err +/out diff --git a/packages/copper/Cargo.toml b/packages/copper/Cargo.toml index ede792e..92d03e4 100644 --- a/packages/copper/Cargo.toml +++ b/packages/copper/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pistonite-cu" -version = "0.9.3" +version = "0.9.4" edition = "2024" description = "Battery-included common utils to speed up development of rust tools" repository = "https://github.com/Pistonite/cu" @@ -11,7 +11,7 @@ exclude = [ ] [dependencies] -pistonite-cu-proc-macros = { version = "0.2.10", path = "../copper-proc-macros" } +pistonite-cu-proc-macros = { version = "0.2.11", path = "../copper-proc-macros" } # --- Always enabled --- anyhow = "1.0.104" @@ -22,7 +22,7 @@ oneshot = { version = "0.2.1", optional = true, features = ["std"] } env_filter = { version = "2.0.0", optional = true } terminal_size = { version = "0.4.4", optional = true } unicode-width = { version = "0.2.2", features = ["cjk"], optional = true } -clap = { version = "4.6.6", features = ["derive"], optional = true } +clap = { version = "4.6.7", features = ["derive"], optional = true } regex = { version = "1.13.1", optional = true } ctrlc = { version = "3.5.2", optional = true } @@ -44,7 +44,7 @@ spin = {version = "0.12.3", optional = true} # for PIO serde = { version = "1.0.229", features = ["derive"], optional = true } serde_json = { version = "1.0.151", optional = true } serde_yaml_ng = { version = "0.10.0", optional = true } -toml = { version = "1.1.5", optional = true } +toml = { version = "1.1.6", optional = true } # derive derive_more = { version = "2.1.1", features = ["full"], optional = true } @@ -139,3 +139,7 @@ required-features = ["process", "cli", "json"] [[example]] name = "ctrlc" required-features = ["cli", "coroutine"] + +[[example]] +name = "custom_print_macro" +required-features = ["cli"] diff --git a/packages/copper/examples/custom_print_macro.rs b/packages/copper/examples/custom_print_macro.rs new file mode 100644 index 0000000..55aecc0 --- /dev/null +++ b/packages/copper/examples/custom_print_macro.rs @@ -0,0 +1,33 @@ +use pistonite_cu as cu; + +static GREEN: &str = "\x1b[92m"; +static RED: &str = "\x1b[91m"; +static YELLOW: &str = "\x1b[93m"; +static RESET: &str = "\x1b[0m"; + +/// This logs P) in green +macro_rules! pass { + ($($args:tt)*) => { cu::custom_print!(cu::lv::I, GREEN, 'P', "", ')', GREEN, $($args)*) } +} + +/// This logs F) in red +macro_rules! fail { + ($($args:tt)*) => { cu::custom_print!(cu::lv::E, RED, 'F', "", ')', RED, $($args)*) } +} + +#[cu::cli] +fn main(_: cu::cli::Flags) -> cu::Result<()> { + // cu::cli::print_to(cu::cli::Target::Stderr); + let count = 5; + pass!("{count} tests passed"); + fail!("{count} tests failed"); + // the part after rest should be red instead of no color + // note because the escapes are formatted directly into this message, + // they are not controllable by the --color flag + fail!("{YELLOW}some{RESET} tests failed"); + cu::error!( + "\x1b]8;;https://www.github.com\x07Click Me very very very very very very very very very long longl ongl laskdjfl aksjdlf kjasldkfj lasjdfl kasjdj asd lkfahsld khflaskdh f\x1b]8;;\x07" + ); + cu::print!("\x1b]8;;https://www.github.com\x1b\\Click Me\x1b]8;;\x1b\\"); + Ok(()) +} diff --git a/packages/copper/src/cli/fmt/ansi.rs b/packages/copper/src/cli/fmt/ansi.rs index 5e3e7f1..b188402 100644 --- a/packages/copper/src/cli/fmt/ansi.rs +++ b/packages/copper/src/cli/fmt/ansi.rs @@ -38,13 +38,13 @@ pub(crate) const fn colors(use_color: bool) -> Colors { /// Iterator of (char, width) pub(crate) fn with_width(x: std::str::Chars<'_>) -> AnsiWidthIter<'_> { AnsiWidthIter { - is_escaping: false, + state: AnsiEscapeState::None, chars: x, } } pub(crate) struct AnsiWidthIter<'a> { - is_escaping: bool, + state: AnsiEscapeState, chars: std::str::Chars<'a>, } @@ -52,18 +52,57 @@ impl<'a> Iterator for AnsiWidthIter<'a> { type Item = (char, usize); fn next(&mut self) -> Option { + use unicode_width::UnicodeWidthChar; let c = self.chars.next()?; - let width = if self.is_escaping { - if is_esc_end(c) { - self.is_escaping = false; + let width = match self.state { + AnsiEscapeState::None => { + if c == '\x1b' { + self.state = AnsiEscapeState::SawEsc; + 0 + } else { + c.width_cjk().unwrap_or(0) + } + } + AnsiEscapeState::SawEsc => match c { + '[' => { + self.state = AnsiEscapeState::EscapingControl; + 0 + } + ']' => { + self.state = AnsiEscapeState::EscapingOsCommand; + 0 + } + _ => { + self.state = AnsiEscapeState::None; + c.width_cjk().unwrap_or(0) + } + }, + AnsiEscapeState::EscapingControl => { + if is_esc_end(c) { + self.state = AnsiEscapeState::None; + } + 0 + } + AnsiEscapeState::EscapingOsCommand => { + match c { + '\x1b' => { + self.state = AnsiEscapeState::EscapingOsCommandSawEsc; + } + '\x07' => { + self.state = AnsiEscapeState::None; + } + _ => {} + } + 0 + } + AnsiEscapeState::EscapingOsCommandSawEsc => { + if c == '\\' { + self.state = AnsiEscapeState::None; + } else { + self.state = AnsiEscapeState::EscapingOsCommand; + } + 0 } - 0 - } else if c == '\x1b' { - self.is_escaping = true; - 0 - } else { - use unicode_width::UnicodeWidthChar; - c.width_cjk().unwrap_or(0) }; Some((c, width)) @@ -72,5 +111,13 @@ impl<'a> Iterator for AnsiWidthIter<'a> { 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)) + c < u8::MAX as char && b"mAKGJBCDEFHSTfhlin\\\x07".contains(&(c as u8)) +} + +enum AnsiEscapeState { + None, + SawEsc, + EscapingControl, + EscapingOsCommand, + EscapingOsCommandSawEsc, } diff --git a/packages/copper/src/cli/fmt/format_buffer.rs b/packages/copper/src/cli/fmt/format_buffer.rs index 29e9d8c..e9a662f 100644 --- a/packages/copper/src/cli/fmt/format_buffer.rs +++ b/packages/copper/src/cli/fmt/format_buffer.rs @@ -12,6 +12,8 @@ pub(crate) struct FormatBuffer { gray_color: &'static str, /// ANSI code for the current text color text_color: &'static str, + /// Process ansi escape in the formatting + escape_state: FormatBufferEscapeState, } impl FormatBuffer { @@ -22,6 +24,7 @@ impl FormatBuffer { buffer: String::new(), gray_color: "", text_color: "", + escape_state: FormatBufferEscapeState::None, } } /// Get the formatted buffer content @@ -40,6 +43,7 @@ impl FormatBuffer { self.width = fmt::term_width_or_max(); self.gray_color = gray_color; self.text_color = text_color; + self.escape_state = FormatBufferEscapeState::None; } /// Push a newline character (note this is different from [`new_line`](Self::new_line)) @@ -64,7 +68,7 @@ impl FormatBuffer { return; } if self.width < 5 { - // give up + // window width is too small to meaningfully format self.buffer.push(c); return; } @@ -72,6 +76,59 @@ impl FormatBuffer { self.new_line(); } self.buffer.push(c); + match self.escape_state { + FormatBufferEscapeState::None => { + if c == '\x1b' { + self.escape_state = FormatBufferEscapeState::SawEsc; + } + } + FormatBufferEscapeState::SawEsc => { + if c == '[' { + self.escape_state = FormatBufferEscapeState::SawBracket; + } else { + self.escape_state = FormatBufferEscapeState::None; + } + } + FormatBufferEscapeState::SawBracket => { + if c.is_ascii_digit() { + let code = c as u8 - b'0'; + self.escape_state = FormatBufferEscapeState::SawCode(code as u32); + } else if c == ';' { + self.escape_state = FormatBufferEscapeState::SawBracket; + } else { + self.escape_state = FormatBufferEscapeState::None; + } + } + FormatBufferEscapeState::SawCode(code) => { + if c.is_ascii_digit() { + let digit = c as u8 - b'0'; + self.escape_state = FormatBufferEscapeState::SawCode(code * 10 + digit as u32); + } else if c == ';' || c == 'm' { + // if resetting foreground or full, push text color again + let need_reset = code == 0 || code == 39; + if c == ';' { + if need_reset { + // terminate the control sequence first + // we just pushed `c` so pop is safe + self.buffer.pop(); + self.buffer.push('m'); + // start new control sequence + self.buffer.push_str(self.text_color); + // continue control sequence + self.buffer.push_str("\x1b["); + } + self.escape_state = FormatBufferEscapeState::SawBracket; + } else { + if need_reset { + self.buffer.push_str(self.text_color); + } + self.escape_state = FormatBufferEscapeState::None; + } + } else { + self.escape_state = FormatBufferEscapeState::None; + } + } + } self.curr += w; } /// Start formatting a new line @@ -83,3 +140,10 @@ impl FormatBuffer { self.curr = 3; } } + +enum FormatBufferEscapeState { + None, + SawEsc, + SawBracket, + SawCode(u32), +} diff --git a/packages/copper/src/cli/macros.rs b/packages/copper/src/cli/macros.rs index 3bf186c..9c68f8f 100644 --- a/packages/copper/src/cli/macros.rs +++ b/packages/copper/src/cli/macros.rs @@ -1,4 +1,4 @@ -use cu::cli::printer::PRINTER; +use cu::cli::printer::{PRINTER, PrintPayload}; use cu::lv; /// Print something /// @@ -19,6 +19,56 @@ macro_rules! hint { }} } +/// Print message with a custom log format and level. +/// +/// This lets you customize the "level char", the separator char, and +/// the colors for level char, separator char, and the main text color. +/// Note that if colors are disabled from CLI (--color=never), the control +/// sequences are replaced with empty string. +/// +/// ```rust +/// # use pistonite_cu as cu; +/// static GREEN: &str = "\x1b[92m"; +/// static RED: &str = "\x1b[91m"; +/// static YELLOW: &str = "\x1b[93m"; +/// static RESET: &str = "\x1b[0m"; +/// +/// /// This logs P) in green +/// macro_rules! pass { +/// ($($args:tt)*) => { cu::custom_print!(cu::lv::I, GREEN, 'P', "", ')', GREEN, $($args)*) } +/// } +/// +/// /// This logs F) in red +/// macro_rules! fail { +/// ($($args:tt)*) => { cu::custom_print!(cu::lv::E, RED, 'F', "", ')', RED, $($args)*) } +/// } +/// +/// let count = 5; +/// pass!("{count} tests passed"); +/// fail!("{count} tests failed"); +/// // the part after reset remains red +/// fail!("{YELLOW}some{RESET} tests failed"); +/// ``` +#[macro_export] +#[cfg(feature = "print")] +macro_rules! custom_print { + ( + $level:expr, $level_char_control:expr, $level_char:expr, + $sep_char_control:expr, $sep_char:expr, $text_control:expr, + $($fmt_args:tt)* + ) => {{ + $crate::cli::__print_payload_with_level( + $level, + $level_char_control, + $level_char, + $sep_char_control, + $sep_char, + $text_control, + format_args!($($fmt_args)*) + ); + }}; +} + /// Show prompt to the user. See [Prompting](fn@crate::prompt) #[cfg(all(feature = "prompt", not(feature = "coroutine")))] #[macro_export] @@ -74,3 +124,38 @@ pub fn __print_with_level(lv: lv::Lv, message: std::fmt::Arguments<'_>) { } } } + +/// Internal print function for custom print formats +#[doc(hidden)] +pub fn __print_payload_with_level( + lv: lv::Lv, + mut level_char_control: &'static str, + level_char: char, + mut sep_char_control: &'static str, + sep_char: char, + mut text_control: &'static str, + message: std::fmt::Arguments<'_>, +) { + if !lv.can_print(lv::PRINT_LEVEL.get()) { + return; + } + if !lv::color_enabled() { + level_char_control = ""; + sep_char_control = ""; + text_control = ""; + } + let message = format!("{message}"); + let payload = PrintPayload { + level_char_control, + level_char, + sep_char_control, + sep_char, + text_control, + message: &message, + }; + if let Ok(mut printer) = PRINTER.lock() { + if let Some(printer) = printer.as_mut() { + printer.print_payload(payload); + } + } +} diff --git a/packages/copper/src/cli/mod.rs b/packages/copper/src/cli/mod.rs index 6c7cf0b..a23a75f 100644 --- a/packages/copper/src/cli/mod.rs +++ b/packages/copper/src/cli/mod.rs @@ -143,12 +143,13 @@ pub use flags::{__run, Flags, print_help, reset_color, try_parse}; mod print_init; pub use print_init::{DefaultLogConfig, LogConfig, init_options, level}; mod macros; -pub use macros::__print_with_level; +pub use macros::{__print_payload_with_level, __print_with_level}; mod thread_name; use thread_name::THREAD_NAME; pub use thread_name::{reset_thread_name, set_thread_name}; mod printer; +pub use printer::{Target, animate_to, print_to}; mod progress; pub use progress::{ProgressBar, ProgressBarBuilder, ProgressHandle, progress}; diff --git a/packages/copper/src/cli/printer.rs b/packages/copper/src/cli/printer.rs index b43b7bb..0cae229 100644 --- a/packages/copper/src/cli/printer.rs +++ b/packages/copper/src/cli/printer.rs @@ -30,6 +30,8 @@ pub(crate) struct Printer { colors: ansi::Colors, print_task: PrintingThreadHandle, + /// Target for regular print messages + print_target: Target, /// Target for showing animation (bars and prompts) /// If None, bar animation will not be printed, and prompts /// will still be printed to stderr @@ -68,6 +70,7 @@ impl Printer { colors, print_task: Default::default(), + print_target: Target::Stdout, anime_target, bars: Default::default(), #[cfg(feature = "prompt")] @@ -77,6 +80,14 @@ impl Printer { buffered: String::new(), } } + + pub(crate) fn set_print_target(&mut self, target: Target) { + self.print_target = target; + } + pub(crate) fn set_animation_target(&mut self, target: Option) { + self.anime_target = target; + } + #[cfg(feature = "prompt")] pub(crate) fn show_prompt( &mut self, @@ -169,58 +180,18 @@ impl Printer { /// Format and print the message pub(crate) fn print_message(&mut self, lv: lv::Lv, message: &str) { - let mut lines = message.lines(); - let text_color = match lv { - lv::Lv::Off => return, - lv::Lv::Error => self.colors.red, - lv::Lv::Hint => self.colors.yellow, - lv::Lv::Print => self.colors.reset, - lv::Lv::Warn => self.colors.yellow, - lv::Lv::Info => self.colors.reset, - lv::Lv::Debug => self.colors.cyan, - lv::Lv::Trace => self.colors.magenta, + let Some(payload) = PrintPayload::new_level_message(lv, &self.colors, message) else { + return; }; - self.format_buffer.reset(self.colors.gray, text_color); - match lv { - lv::Lv::Off => unreachable!(), - lv::Lv::Error => { - self.format_buffer.push_control(self.colors.red); - self.format_buffer.push('E', 1); - self.format_buffer.push(']', 1); - } - lv::Lv::Hint => { - self.format_buffer.push_control(self.colors.cyan); - self.format_buffer.push('H', 1); - self.format_buffer.push_control(self.colors.gray); - self.format_buffer.push(']', 1); - } - lv::Lv::Print => { - self.format_buffer.push_control(self.colors.gray); - self.format_buffer.push(':', 1); - self.format_buffer.push(':', 1); - } - lv::Lv::Warn => { - self.format_buffer.push_control(self.colors.yellow); - self.format_buffer.push('W', 1); - self.format_buffer.push(']', 1); - } - lv::Lv::Info => { - self.format_buffer.push_control(self.colors.green); - self.format_buffer.push('I', 1); - self.format_buffer.push_control(self.colors.gray); - self.format_buffer.push(']', 1); - } - lv::Lv::Debug => { - self.format_buffer.push_control(self.colors.gray); - self.format_buffer.push('D', 1); - self.format_buffer.push(']', 1); - } - lv::Lv::Trace => { - self.format_buffer.push_control(self.colors.magenta); - self.format_buffer.push('*', 1); - self.format_buffer.push(']', 1); - } - } + self.print_payload(payload); + } + pub(crate) fn print_payload(&mut self, payload: PrintPayload<'_>) { + self.format_buffer + .reset(self.colors.gray, payload.text_control); + self.format_buffer.push_control(payload.level_char_control); + self.format_buffer.push(payload.level_char, 1); + self.format_buffer.push_control(payload.sep_char_control); + self.format_buffer.push(payload.sep_char, 1); THREAD_NAME.with_borrow(|x| { if let Some(x) = x { self.format_buffer.push_control(self.colors.magenta); @@ -229,7 +200,8 @@ impl Printer { self.format_buffer.push(']', 1); } }); - self.format_buffer.push_control(text_color); + self.format_buffer.push_control(payload.text_control); + let mut lines = payload.message.lines(); if let Some(line) = lines.next() { self.format_buffer.push(' ', 1); self.format_buffer.push_str(line); @@ -244,16 +216,43 @@ impl Printer { fn print_format_buffer(&mut self) { if !self.print_task.active() { use std::io::Write; - let _ = write!(self.stdout, "{}", self.format_buffer.as_str()); - let _ = self.stdout.flush(); + match self.print_target { + Target::Stdout => { + let _ = write!( + self.stdout, + "{}{}", + self.format_buffer.as_str(), + self.colors.reset + ); + let _ = self.stdout.flush(); + } + Target::Stderr => { + let _ = write!( + self.stderr, + "{}{}", + self.format_buffer.as_str(), + self.colors.reset + ); + let _ = self.stderr.flush(); + } + } } else { self.buffered.push_str(self.format_buffer.as_str()); + self.buffered.push_str(self.colors.reset); } } - fn flush_buffered_to_stdout(&mut self) { + fn flush_buffered_to_print_target(&mut self) { use std::io::Write as _; - let _ = write!(self.stdout, "{}", self.buffered); - let _ = self.stdout.flush(); + match self.print_target { + Target::Stdout => { + let _ = write!(self.stdout, "{}", self.buffered); + let _ = self.stdout.flush(); + } + Target::Stderr => { + let _ = write!(self.stderr, "{}", self.buffered); + let _ = self.stderr.flush(); + } + } self.buffered.clear(); } @@ -277,6 +276,83 @@ impl Printer { } } +pub(crate) struct PrintPayload<'a> { + pub level_char_control: &'static str, + pub level_char: char, + pub sep_char_control: &'static str, + pub sep_char: char, + pub text_control: &'static str, + pub message: &'a str, +} +impl<'a> PrintPayload<'a> { + pub fn new_level_message( + level: lv::Lv, + colors: &ansi::Colors, + message: &'a str, + ) -> Option { + let payload = match level { + lv::Lv::Off => return None, + lv::Lv::Error => Self { + level_char_control: colors.red, + level_char: 'E', + sep_char_control: "", + sep_char: ']', + text_control: colors.red, + message, + }, + lv::Lv::Hint => Self { + level_char_control: colors.cyan, + level_char: 'H', + sep_char_control: colors.gray, + sep_char: ']', + text_control: colors.yellow, + message, + }, + lv::Lv::Print => Self { + level_char_control: colors.gray, + level_char: ':', + sep_char_control: "", + sep_char: ':', + text_control: colors.reset, + message, + }, + lv::Lv::Warn => Self { + level_char_control: colors.yellow, + level_char: 'W', + sep_char_control: "", + sep_char: ']', + text_control: colors.yellow, + message, + }, + lv::Lv::Info => Self { + level_char_control: colors.green, + level_char: 'I', + sep_char_control: colors.gray, + sep_char: ']', + text_control: colors.reset, + message, + }, + lv::Lv::Debug => Self { + level_char_control: colors.gray, + level_char: 'D', + sep_char_control: "", + sep_char: ']', + text_control: colors.cyan, + message, + }, + lv::Lv::Trace => Self { + level_char_control: colors.magenta, + level_char: '*', + sep_char_control: "", + sep_char: ']', + text_control: colors.magenta, + message, + }, + }; + Some(payload) + } +} + #[cfg(feature = "prompt")] struct PromptTask { send: OnceSend>>, @@ -388,7 +464,7 @@ impl PrintingThread { // with the prompt if printer.anime_target.is_none() { if !printer.buffered.is_empty() { - printer.flush_buffered_to_stdout(); + printer.flush_buffered_to_print_target(); } // still print the prompt to stderr, but don't print control characters let _ = write!(printer.stderr, "{}", task.prompt); @@ -479,7 +555,7 @@ impl PrintingThread { self.format_bars(printer); self.print_buffer_to_anime_target(printer); } else { - printer.flush_buffered_to_stdout(); + printer.flush_buffered_to_print_target(); } let bars_empty = printer.bars.is_empty(); #[cfg(feature = "prompt")] @@ -583,25 +659,23 @@ impl PrintingThread { /// Flush the printer buffered messages fn flush_buffered(&mut self, printer: &mut Printer) { - match printer.anime_target { - // if the animation target is also stdout, - // it's important that we take the buffered messages - // into our own buffer, so we don't immediately - // flush stdout. this prevents flushing partial output, - // which cause the progress animation to flicker - Some(Target::Stdout) => { - self.buffer.push_str(&printer.buffered); - printer.buffered.clear(); - } - _ => { - printer.flush_buffered_to_stdout(); - } + // if the animation target is the same as the print target + // it's important that we take the buffered messages + // into our own buffer, so we don't immediately + // flush stdout. this prevents flushing partial output, + // which cause the progress animation to flicker + if printer.anime_target == Some(printer.print_target) { + self.buffer.push_str(&printer.buffered); + printer.buffered.clear(); + return; } + printer.flush_buffered_to_print_target(); } } +/// Target for printing output #[derive(Clone, Copy, PartialEq, Eq)] -enum Target { +pub enum Target { /// Print to Stdout Stdout, /// Print to Stderr @@ -622,3 +696,47 @@ fn read_prompt(task: &PromptTask) -> cu::Result> { Some(Some(x)) => Ok(Some(x)), } } + +/// Set the target for printing messages +/// +/// This is useful if `stdout` needs to emit structured messages for other tools. +/// Then you can programmatically change everything to emit to `stderr`. +/// Note this will only affect messages printed after the call. +/// +/// By default, all print messages go to `stdout`. +/// +/// See also [`cu::cli::animate_to`] +/// +/// ```rust +/// # use pistonite_cu as cu; +/// cu::cli::print_to(cu::cli::Target::Stderr); +/// ``` +pub fn print_to(target: Target) { + if let Ok(mut printer) = PRINTER.lock() { + if let Some(printer) = printer.as_mut() { + printer.set_print_target(target); + } + } +} + +/// Set the target for animation messages (prompts and bars) +/// +/// By default, animation messages emit to `stdout` if `stdout` is terminal, +/// and `stderr` if `stderr` is terminal. Otherwise, animation messages are +/// not emitted. You can turn off animation messages by passing in `None`. +/// Note this will only affect messages printed after the call. +/// +/// See also [`cu::cli::print_to`] +/// +/// ```rust +/// # use pistonite_cu as cu; +/// // force animation to go to stderr even if stdout is terminal +/// cu::cli::animate_to(Some(cu::cli::Target::Stderr)); +/// ``` +pub fn animate_to(target: Option) { + if let Ok(mut printer) = PRINTER.lock() { + if let Some(printer) = printer.as_mut() { + printer.set_animation_target(target); + } + } +} diff --git a/packages/promethium/Cargo.toml b/packages/promethium/Cargo.toml index 2ef55a0..03687cb 100644 --- a/packages/promethium/Cargo.toml +++ b/packages/promethium/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pistonite-pm" -version = "0.3.0" +version = "0.3.1" edition = "2024" description = "Procedural Macro Common Utils" repository = "https://github.com/Pistonite/cu" @@ -13,7 +13,7 @@ exclude = [ [dependencies] proc-macro2 = { version = "1.0.107", default-features = false } quote = { version = "1.0.47", default-features = false } -syn = { version = "3.0.5", default-features = false } +syn = { version = "3.0.6", default-features = false } [features] default = [ diff --git a/packages/terminal-tests/output/tt_print_levels-6.txt b/packages/terminal-tests/output/tt_print_levels-6.txt index 65e1abc..8770475 100644 --- a/packages/terminal-tests/output/tt_print_levels-6.txt +++ b/packages/terminal-tests/output/tt_print_levels-6.txt @@ -2,9 +2,9 @@ $ '--color=always' -q STDOUT >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> \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[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[0m\x1B[93mW]\x1B[93m this is a warn message^LF +\x1B[0m\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[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[0m\x1B[93mW]\x1B[93m this is a warn message^LF +\x1B[0m\x1B[91mE]\x1B[91m this is error message^LF \x1B[90m | \x1B[91m^LF -\x1B[90mD]\x1B[96m this is debug message^LF +\x1B[0m\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[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[0m\x1B[93mW]\x1B[93m this is a warn message^LF +\x1B[0m\x1B[91mE]\x1B[91m this is error message^LF \x1B[90m | \x1B[91m^LF -\x1B[90mD]\x1B[96m this is debug message^LF +\x1B[0m\x1B[90mD]\x1B[96m this is debug message^LF \x1B[90m | \x1B[96m2^LF \x1B[90m | \x1B[96m^LF -\x1B[95m*]\x1B[95m [tt_print_levels tt_print_levels.rs:21] this is trace mes^LF +\x1B[0m\x1B[95m*]\x1B[95m [tt_print_levels tt_print_levels.rs:21] this is trace mes^LF \x1B[90m | \x1B[95msage^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 -^>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> ^