a better coloring crate
initial commit
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | Cargo.toml | 16 | ||||
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | src/cfstr.rs | 125 | ||||
| -rw-r--r-- | src/lib.rs | 223 | ||||
| -rw-r--r-- | tests/basic.rs | 12 |
6 files changed, 382 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..70aa946 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "comat" +version = "0.1.0" +edition = "2021" +authors = ["bendn <[email protected]>"] +license = "MIT" +description = "a better coloring crate" +repository = "https://github.com/bend-n/comat" + +[lib] +proc_macro = true + +[dependencies] +proc-macro2 = "1.0.67" +quote = "1.0.32" +syn = { version = "2.0.15", features = ["full"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..970a585 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# comat + +Small terminal coloring library, using proc macros. +See [lib.rs](https://docs.rs/comat) for more information. diff --git a/src/cfstr.rs b/src/cfstr.rs new file mode 100644 index 0000000..96699ca --- /dev/null +++ b/src/cfstr.rs @@ -0,0 +1,125 @@ +use proc_macro2::Literal; +use quote::{ToTokens, TokenStreamExt}; +use syn::{parse::Parse, LitStr, Result}; + +fn name2ansi(name: &str) -> Option<&'static str> { + Some(match name { + "black" => "\x1b[0;34;30m", + "red" => "\x1b[0;34;31m", + "green" => "\x1b[0;34;32m", + "yellow" => "\x1b[0;34;33m", + "blue" => "\x1b[0;34;34m", + "magenta" => "\x1b[0;34;35m", + "cyan" => "\x1b[0;34;36m", + "white" => "\x1b[0;34;37m", + "default" => "\x1b[0;34;39m", + + "bold_black" => "\x1b[1;34;30m", + "bold_red" => "\x1b[1;34;31m", + "bold_green" => "\x1b[1;34;32m", + "bold_yellow" => "\x1b[1;34;33m", + "bold_blue" => "\x1b[1;34;34m", + "bold_magenta" => "\x1b[1;34;35m", + "bold_cyan" => "\x1b[1;34;36m", + "bold_white" => "\x1b[1;34;37m", + "bold_default" => "\x1b[1;34;39m", + + "on_black_bold" => "\x1b[1;34;40m", + "on_red_bold" => "\x1b[1;34;41m", + "on_green_bold" => "\x1b[1;34;42m", + "on_yellow_bold" => "\x1b[1;34;43m", + "on_blue_bold" => "\x1b[1;34;44m", + "on_magenta_bold" => "\x1b[1;44;35m", + "on_cyan_bold" => "\x1b[1;34;46m", + "on_white_bold" => "\x1b[1;34;47m", + "on_default_bold" => "\x1b[1;34;49m", + + "on_black" => "\x1b[0;34;40m", + "on_red" => "\x1b[0;34;41m", + "on_green" => "\x1b[0;34;42m", + "on_yellow" => "\x1b[0;34;43m", + "on_blue" => "\x1b[0;34;44m", + "on_magenta" => "\x1b[0;44;35m", + "on_cyan" => "\x1b[0;34;46m", + "on_white" => "\x1b[0;34;47m", + "on_default" => "\x1b[0;34;49m", + + "reset" => "\x1b[0m", + "dim" => "\x1b[2m", + "italic" => "\x1b[3m", + "underline" => "\x1b[24m", + "blinking" => "\x1b[5m", + "hide" => "\x1b[8m", + "strike" => "\x1b[9m", + "bold" => "\x1b[1m", + _ => return None, + }) +} + +pub struct CFStr(String); + +impl Parse for CFStr { + fn parse(stream: syn::parse::ParseStream) -> Result<Self> { + let input = stream.parse::<LitStr>()?.value(); + let mut chars = input.chars().peekable(); + let mut temp = String::new(); + let mut out = String::new(); + while let Some(ch) = chars.next() { + match ch { + '{' => { + match chars.next() { + Some('{') => { + out.push('{'); + continue; + } + Some(ch) => temp.push(ch), + None => return Err(stream.error("unexpected eof")), + } + for ch in chars.by_ref() { + match ch { + '}' => { + if let Some(a) = name2ansi(&temp) { + out.push_str(a); + temp.clear(); + break; + } else if let Some((b, a)) = temp.split_once(':') { + if let Some(a) = name2ansi(a) { + out.push_str(name2ansi("reset").unwrap()); + out.push_str(a); + out.push('{'); + out.push_str(b); + out.push('}'); + out.push_str(name2ansi("reset").unwrap()); + temp.clear(); + break; + } + } + out.push('{'); + out.push_str(&temp); + out.push('}'); + temp.clear(); + break; + } + t => temp.push(t), + } + } + } + '}' => match chars.next() { + Some('}') => { + out.push('}'); + continue; + } + _ => return Err(stream.error("unexpected text")), + }, + c => out.push(c), + } + } + Ok(Self(out)) + } +} + +impl ToTokens for CFStr { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + tokens.append(Literal::string(&self.0)); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..491dea9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,223 @@ +//! smart crate for terminal coloring. +//! +//! uses macros instead of methods. +//! +//! ## usage +//! +//! heres how it works: +//! ```rust +//! # use comat::cprintln; +//! # use std::time::{Duration, Instant}; +//! cprintln!("the traffic light is {bold_red}red.{reset}"); +//! cprintln!("the traffic light will be {green}green{reset} at {:?}.", Instant::now() + Duration::from_secs(40)); +//! ``` +//! +//! ## why you should use comat instead of {yansi, owo_colors, colored, ..} +//! +//! - no method pollution, your intellisense remains fine +//! - compact: shorter than even raw ansi. see: +//! ``` +//! # use comat::cprint; +//! # let thing = 0; +//! cprint!("{thing:red}."); +//! ``` +//! vs +//! ``` +//! print!("\x1b[0;34;31mred\x1b[0m."); +//! ``` +//! vs +//! ```ignore +//! print!("{}.", "red".red()); +//! ``` +//! - intuitive: you dont have to +//! ```ignore +//! println!("{} {} {}", thing1.red().on_blue(), thing2.red().on_blue(), thing3.italic());. +//! ``` +//! instead, simply +//! ``` +//! # use comat::cprintln; +//! # let thing1 = 0; let thing2 = 5; let thing3 = 4; +//! cprintln!("{red}{on_blue}{thing1} {thing2} {thing3:italic}"); +//! ``` +//! +//! ## syntax +//! +//! `{{` gives you a `{`, to get a `{{` use `{{{{`. +//! +//! `{color}` adds that effect/color to the string. it does not reset afterwards. +//! +//! if the color inside a `{}` is not found, it doesnt touch the block, for convenience. +//! +//! `{thing:color}` will reset everything before the block, color it, and reset that color. similar to `thing.color()` with other libs. +#![forbid(unsafe_code)] +#![warn(clippy::pedantic, clippy::dbg_macro, missing_docs)] +use proc_macro::TokenStream; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{parse::Parse, parse_macro_input, punctuated::Punctuated, Expr, Result, Token}; + +mod cfstr; +use cfstr::CFStr; + +#[proc_macro] +/// Macro that simply modifies the format string to have colors. +/// Mostly for testing. Use [`cformat_args!`] instead where possible. +pub fn comat(input: TokenStream) -> TokenStream { + let str = parse_macro_input!(input as CFStr); + str.to_token_stream().into() +} + +struct One { + cfstr: CFStr, + args: Punctuated<Expr, Token![,]>, +} + +impl Parse for One { + fn parse(input: syn::parse::ParseStream) -> Result<Self> { + let cfstr = input.parse::<CFStr>()?; + let _ = input.parse::<Token![,]>(); + Ok(Self { + cfstr, + args: Punctuated::<Expr, Token![,]>::parse_terminated(input)?, + }) + } +} + +impl ToTokens for One { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.cfstr.to_tokens(tokens); + tokens.append(proc_macro2::Punct::new(',', proc_macro2::Spacing::Alone)); + self.args.to_tokens(tokens); + } +} + +// NOTE: many of these can be made as decl macros, but decl macros can't be exported from proc macro crates yet. + +#[proc_macro] +/// Print text, colorfully, to stdout, with a newline. +/// +/// See also [`println`]. +/// ``` +/// # use comat::*; +/// let magic = 4; +/// cprintln!("{red}look its red{reset}! {bold_blue}{magic}{reset} is the magic number!"); +/// ``` +pub fn cprintln(input: TokenStream) -> TokenStream { + let f = parse_macro_input!(input as One); + quote! { println!(#f) }.into() +} + +#[proc_macro] +/// Print text, colorfully, to stdout, without a newline. +/// +/// See also [`print`]. +/// ``` +/// # use comat::*; +/// cprint!("{yellow}i am a warning. {reset}why do you dislike me?"); +/// ``` +pub fn cprint(input: TokenStream) -> TokenStream { + let f = parse_macro_input!(input as One); + quote! { print!(#f) }.into() +} + +#[proc_macro] +/// Format text, colorfully. +/// +/// See also [`format`]. +/// ``` +/// # use comat::*; +/// let favorite_thing = "teddy bears"; +/// let message = cformat!("the {red}bogeymen{reset} will get your {favorite_thing:underline}"); +/// # assert_eq!(message, "the \x1b[0;34;31mbogeymen\x1b[0m will get your \x1b[0m\x1b[24mteddy bears\x1b[0m"); +/// ``` +pub fn cformat(input: TokenStream) -> TokenStream { + let f = parse_macro_input!(input as One); + quote! { format!(#f) }.into() +} + +#[proc_macro] +/// Produce [`fmt::Arguments`](std::fmt::Arguments). Sometimes functions take these. +/// +/// See also [`format_args`]. +/// ``` +/// # use comat::*; +/// let args = cformat_args!("{bold_red}fatal error. {reset}killing {blue}everything{reset}"); +/// // NOTE: do not do this. instead use cprintln. +/// println!("{}", args); +pub fn cformat_args(input: TokenStream) -> TokenStream { + let f = parse_macro_input!(input as One); + quote! { format_args!(#f) }.into() +} +/// Colorfully panic. +/// +/// See also [`panic`]. +/// ```should_panic +/// # use comat::cpanic; +/// cpanic!("why is the bound {red}bad"); +/// ``` +#[proc_macro] +pub fn cpanic(input: TokenStream) -> TokenStream { + let f = parse_macro_input!(input as One); + quote! { panic!(#f) }.into() +} + +struct Two { + a: Expr, + cfstr: CFStr, + args: Punctuated<Expr, Token![,]>, +} + +impl Parse for Two { + fn parse(input: syn::parse::ParseStream) -> Result<Self> { + let a = input.parse::<Expr>()?; + input.parse::<Token![,]>()?; + let cfstr = input.parse::<CFStr>()?; + let _ = input.parse::<Token![,]>(); + Ok(Self { + a, + cfstr, + args: Punctuated::<Expr, Token![,]>::parse_terminated(input)?, + }) + } +} + +impl ToTokens for Two { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.a.to_tokens(tokens); + tokens.append(proc_macro2::Punct::new(',', proc_macro2::Spacing::Alone)); + self.cfstr.to_tokens(tokens); + tokens.append(proc_macro2::Punct::new(',', proc_macro2::Spacing::Alone)); + self.args.to_tokens(tokens); + } +} + +#[proc_macro] +/// Write to a buffer colorfully, with no newline. +/// +/// See also [`write`] +/// ``` +/// # use comat::cwrite; +/// use std::io::Write; +/// let mut buf = vec![]; +/// cwrite!(buf, "{green}omg there's going to be ansi sequences in a {black}Vec<u8>{reset}!"); +/// # assert_eq!(buf, [27, 91, 48, 59, 51, 52, 59, 51, 50, 109, 111, 109, 103, 32, 116, 104, 101, 114, 101, 39, 115, 32, 103, 111, 105, 110, 103, 32, 116, 111, 32, 98, 101, 32, 97, 110, 115, 105, 32, 115, 101, 113, 117, 101, 110, 99, 101, 115, 32, 105, 110, 32, 97, 32, 27, 91, 48, 59, 51, 52, 59, 51, 48, 109, 86, 101, 99, 60, 117, 56, 62, 27, 91, 48, 109, 33]); +/// ``` +pub fn cwrite(input: TokenStream) -> TokenStream { + let f = parse_macro_input!(input as Two); + quote! { write!(#f) }.into() +} + +#[proc_macro] +/// Write to a buffer colorfully, with newline. +/// +/// See also [`writeln`] +/// ``` +/// # use comat::cwriteln; +/// use std::io::Write; +/// let mut buf = vec![]; +/// cwriteln!(buf, "hey look: {strike}strike'd text{reset}!"); +/// # assert_eq!(buf, [104, 101, 121, 32, 108, 111, 111, 107, 58, 32, 27, 91, 57, 109, 115, 116, 114, 105, 107, 101, 39, 100, 32, 116, 101, 120, 116, 27, 91, 48, 109, 33, 10]); +/// ``` +pub fn cwriteln(input: TokenStream) -> TokenStream { + let f = parse_macro_input!(input as Two); + quote! { writeln!(#f) }.into() +} diff --git a/tests/basic.rs b/tests/basic.rs new file mode 100644 index 0000000..594df32 --- /dev/null +++ b/tests/basic.rs @@ -0,0 +1,12 @@ +use comat::comat; +#[test] +fn basic() { + assert_eq!(comat!("{red}yes{reset}"), "\x1b[0;34;31myes\x1b[0m"); + assert_eq!(comat!("{thing:red}"), "\x1b[0m\x1b[0;34;31m{thing}\x1b[0m"); +} + +#[test] +fn escapes() { + assert_eq!(comat!("{{ow}} {{red}}"), "{ow} {red}"); + assert_eq!(comat!("{{{{"), "{{"); +} |