a better coloring crate
initial commit
bendn 2023-09-16
commit 17c3ed0
-rw-r--r--.gitignore2
-rw-r--r--Cargo.toml16
-rw-r--r--README.md4
-rw-r--r--src/cfstr.rs125
-rw-r--r--src/lib.rs223
-rw-r--r--tests/basic.rs12
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!("{{{{"), "{{");
+}