Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/copper-proc-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -12,7 +12,7 @@ exclude = [

[dependencies.pm]
package = "pistonite-pm"
version = "0.3.0"
version = "0.3.1"
path = "../promethium"
features = ["full"]

Expand Down
2 changes: 2 additions & 0 deletions packages/copper/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
/target
/err
/out
12 changes: 8 additions & 4 deletions packages/copper/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
Expand All @@ -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 }

Expand All @@ -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 }
Expand Down Expand Up @@ -139,3 +139,7 @@ required-features = ["process", "cli", "json"]
[[example]]
name = "ctrlc"
required-features = ["cli", "coroutine"]

[[example]]
name = "custom_print_macro"
required-features = ["cli"]
33 changes: 33 additions & 0 deletions packages/copper/examples/custom_print_macro.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
73 changes: 60 additions & 13 deletions packages/copper/src/cli/fmt/ansi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,32 +38,71 @@ 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>,
}

impl<'a> Iterator for AnsiWidthIter<'a> {
type Item = (char, usize);

fn next(&mut self) -> Option<Self::Item> {
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))
Expand All @@ -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,
}
66 changes: 65 additions & 1 deletion packages/copper/src/cli/fmt/format_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -22,6 +24,7 @@ impl FormatBuffer {
buffer: String::new(),
gray_color: "",
text_color: "",
escape_state: FormatBufferEscapeState::None,
}
}
/// Get the formatted buffer content
Expand All @@ -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))
Expand All @@ -64,14 +68,67 @@ impl FormatBuffer {
return;
}
if self.width < 5 {
// give up
// window width is too small to meaningfully format
self.buffer.push(c);
return;
}
if w < self.width && self.curr > self.width - w {
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
Expand All @@ -83,3 +140,10 @@ impl FormatBuffer {
self.curr = 3;
}
}

enum FormatBufferEscapeState {
None,
SawEsc,
SawBracket,
SawCode(u32),
}
Loading