diff --git a/.github/img/logo.svg b/.github/img/logo.svg new file mode 100644 index 0000000..ab108c6 --- /dev/null +++ b/.github/img/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index a5ad631..e074239 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,7 @@ "Taskfile.yaml": true, ".cocorc": true, ".task": true, - ".cargo": true, + // ".cargo": true, // "Cargo.toml": true, // ๐Ÿ“ฆ @@ -12,7 +12,7 @@ "target": true, // ๐Ÿ“ readmes - "**/**/README.md": true, + // "**/**/README.md": true, "LICENSE": true, // ๐Ÿ—‘๏ธ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..08bde58 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,39 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "lool" +version = "0.0.0-alpha.0" +dependencies = [ + "bitflags", + "eyre", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" diff --git a/Cargo.toml b/Cargo.toml index 24099fe..85e07f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,10 +16,15 @@ overflow-checks = false debug = 0 debug-assertions = false -[dependencies] -eyre = { version = "0.6.12", default-features = false, features = ["auto-install"] } -indoc = "2.0.4" +[registry] +default = "gitea" -[[bin]] -name = "lool" -path = "src/main.rs" +[registries.gitea] +index = "sparse+http://lugit.local/api/packages/lucodear/cargo/" + +[lib] +path = "lib/lib.rs" + +[dependencies] +bitflags = "2.5.0" +eyre = { version = "0.6.12", default-features = false } diff --git a/README.md b/README.md new file mode 100644 index 0000000..af25e1d --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +

+ +
+
+
+ +

๐ฅ๐จ๐จ๐ฅ is a tool-box library with common utilities for ๐š•๐šž๐šŒ๐š˜๐š๐šŽ.๐šŠ๐š› projects. +

+ +
+
+
+ +# Features + +- [ ] **logging**: A simple logging utility. +- [ ] **cli**: Command-line interface utilities: + - [ ] **argparse**: A simple wrapper around `argparse`. + - [ ] **color**: `foo.()` like extension methods for strings. +- [ ] **case**: Case conversion utilities. diff --git a/Taskfile.yaml b/Taskfile.yaml index 08eb026..3ce2173 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -20,6 +20,16 @@ tasks: - python check_size.py fmt: - desc: ๐Ÿš€ format lool + desc: ๐ŸŽจ format lool cmds: - - cargo +nightly fmt --all \ No newline at end of file + - cargo +nightly fmt --all + + lint: + desc: ๐Ÿงถ lint lool + cmds: + - cargo clippy --fix + + test: + desc: ๐Ÿงช test lool + cmds: + - cargo nextest run diff --git a/examples/stylizer.rs b/examples/stylizer.rs new file mode 100644 index 0000000..0c032e7 --- /dev/null +++ b/examples/stylizer.rs @@ -0,0 +1,38 @@ +use eyre::{set_hook, DefaultHandler, Result}; +use lool::cli::colors::{stylize, Stylize}; + +fn setup_eyre() { + let _ = set_hook(Box::new(DefaultHandler::default_with)); +} + +fn main() -> Result<()> { + setup_eyre(); + + let red_bold = stylize("[red+bold]", "red+bold"); + let alt_red_bold = stylize(stylize("alt [red+bold]", "red"), "+bold"); + + let red_bold_italic = stylize("[red+bold|italic]", "red+bold|italic"); + let alt_red_bold_italic = stylize(stylize(stylize("alt [red+bold|italic]", "red"), "+bold"), "+italic"); + + let red_on_blue = stylize("[white on blue]", "white on blue"); + let rgb = stylize("[#3a95ef]", "#3a95ef"); + let rgb_on_rgb = stylize("[#3a95ef on #c174dd]", "#3a95ef on #c174dd"); + + println!("pre {} post", red_bold); + println!("pre {} post", alt_red_bold); + println!("pre {} post", red_bold_italic); + println!("pre {} post", alt_red_bold_italic); + + println!("pre {} post", red_on_blue); + println!("pre {} post", rgb); + println!("pre {} post", rgb_on_rgb); + + println!("pre {} post", "[green]".stl("green").stl("+bold")); + println!("pre {} post", "[green+bold]".stl("green+bold")); + + println!("pre {} post", "[.blue()]".blue()); + println!("pre {} post", "[.blue().bold()]".blue().bold()); + println!("pre {} post", "[.blue().on_red().bold()]".blue().on_red().bold()); + + Ok(()) +} \ No newline at end of file diff --git a/lib/cli/colors.rs b/lib/cli/colors.rs new file mode 100644 index 0000000..a230d48 --- /dev/null +++ b/lib/cli/colors.rs @@ -0,0 +1,7 @@ +mod style; +mod stylizer; + +pub use stylizer::{stylize, Stylize}; + +#[cfg(test)] +mod tests; \ No newline at end of file diff --git a/lib/cli/colors/style.rs b/lib/cli/colors/style.rs new file mode 100644 index 0000000..493bf6b --- /dev/null +++ b/lib/cli/colors/style.rs @@ -0,0 +1,148 @@ +use bitflags::bitflags; +use eyre::Context; +use std::borrow::Cow; + +/// The 8 standard colors. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Color { + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White, + BrightBlack, + BrightRed, + BrightGreen, + BrightYellow, + BrightBlue, + BrightMagenta, + BrightCyan, + BrightWhite, + TrueColor { r: u8, g: u8, b: u8 }, +} + +impl Color { + pub fn to_fg_str(&self) -> Cow<'static, str> { + match *self { + Color::Black => "30".into(), + Color::Red => "31".into(), + Color::Green => "32".into(), + Color::Yellow => "33".into(), + Color::Blue => "34".into(), + Color::Magenta => "35".into(), + Color::Cyan => "36".into(), + Color::White => "37".into(), + Color::BrightBlack => "90".into(), + Color::BrightRed => "91".into(), + Color::BrightGreen => "92".into(), + Color::BrightYellow => "93".into(), + Color::BrightBlue => "94".into(), + Color::BrightMagenta => "95".into(), + Color::BrightCyan => "96".into(), + Color::BrightWhite => "97".into(), + Color::TrueColor { r, g, b } => format!("38;2;{};{};{}", r, g, b).into(), + } + } + + pub fn to_bg_str(&self) -> Cow<'static, str> { + match *self { + Color::Black => "40".into(), + Color::Red => "41".into(), + Color::Green => "42".into(), + Color::Yellow => "43".into(), + Color::Blue => "44".into(), + Color::Magenta => "45".into(), + Color::Cyan => "46".into(), + Color::White => "47".into(), + Color::BrightBlack => "100".into(), + Color::BrightRed => "101".into(), + Color::BrightGreen => "102".into(), + Color::BrightYellow => "103".into(), + Color::BrightBlue => "104".into(), + Color::BrightMagenta => "105".into(), + Color::BrightCyan => "106".into(), + Color::BrightWhite => "107".into(), + Color::TrueColor { r, g, b } => format!("48;2;{};{};{}", r, g, b).into(), + } + } + + pub fn from_str(s: &str) -> eyre::Result { + let color = match s { + "" => None, + "black" => Some(Color::Black), + "red" => Some(Color::Red), + "green" => Some(Color::Green), + "yellow" => Some(Color::Yellow), + "blue" => Some(Color::Blue), + "magenta" => Some(Color::Magenta), + "cyan" => Some(Color::Cyan), + "white" => Some(Color::White), + "bright-black" => Some(Color::BrightBlack), + "bright-red" => Some(Color::BrightRed), + "bright-green" => Some(Color::BrightGreen), + "bright-yellow" => Some(Color::BrightYellow), + "bright-blue" => Some(Color::BrightBlue), + "bright-magenta" => Some(Color::BrightMagenta), + "bright-cyan" => Some(Color::BrightCyan), + "bright-white" => Some(Color::BrightWhite), + s if s.starts_with("#") => { + let s = &s[1..]; + let r = u8::from_str_radix(&s[0..2], 16).context("Error parsing RGB color")?; + let g = u8::from_str_radix(&s[2..4], 16).context("Error parsing RGB color")?; + let b = u8::from_str_radix(&s[4..6], 16).context("Error parsing RGB color")?; + Some(Color::TrueColor { r, g, b }) + } + _ => None, + }; + + color.ok_or_else(|| eyre::eyre!("Invalid color: '{}'", s)) + } +} + +bitflags! { + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct StyleAttributes: u8 { + const BOLD = 0b00000001; + const DIMMED = 0b00000010; + const ITALIC = 0b00000100; + const UNDERLINE = 0b00001000; + const BLINK = 0b00010000; + const REVERSED = 0b00100000; + const HIDDEN = 0b01000000; + const STRIKETHROUGH = 0b10000000; + } +} + +impl StyleAttributes { + pub fn to_ansi_codes(&self) -> Vec<&'static str> { + let mut v = Vec::new(); + if self.contains(StyleAttributes::BOLD) { + v.push("1"); + } + if self.contains(StyleAttributes::DIMMED) { + v.push("2"); + } + if self.contains(StyleAttributes::ITALIC) { + v.push("3"); + } + if self.contains(StyleAttributes::UNDERLINE) { + v.push("4"); + } + if self.contains(StyleAttributes::BLINK) { + v.push("5"); + } + if self.contains(StyleAttributes::REVERSED) { + v.push("7"); + } + if self.contains(StyleAttributes::HIDDEN) { + v.push("8"); + } + if self.contains(StyleAttributes::STRIKETHROUGH) { + v.push("9"); + } + v + } +} diff --git a/lib/cli/colors/stylizer.rs b/lib/cli/colors/stylizer.rs new file mode 100644 index 0000000..1652aff --- /dev/null +++ b/lib/cli/colors/stylizer.rs @@ -0,0 +1,187 @@ +use super::style::{Color, StyleAttributes as StyleBitflags}; +use eyre::Result; + +pub mod instructions { + use super::{Result, StyleBitflags, Color}; + use bitflags::parser::{from_str, ParseError}; + + pub struct StyledString { + pub fg: Option, + pub bg: Option, + pub attrs: StyleBitflags, + } + + impl Default for StyledString { + fn default() -> Self { + StyledString { + fg: None, + bg: None, + attrs: StyleBitflags::empty(), + } + } + } + + /// parses the instruction into a StyledString + pub fn parse(instructions: &str) -> Result { + let mut styled_string = StyledString::default(); + + if instructions.starts_with("+") { + // only attributes + styled_string.attrs = attributes_from_str(instructions.trim_start_matches('+')).map_err(|e| eyre::eyre!(e))?; + return Ok(styled_string); + } + + // try to separate the colors from the attributes + let parts = instructions.split('+').collect::>(); + + match parts.len() { + 1 => { + // no attributes + let (fg, bg) = parse_color_instruction(parts[0].trim())?; + styled_string.fg = fg; + styled_string.bg = bg; + } + 2 => { + // attributes + let (fg, bg) = parse_color_instruction(parts[0].trim())?; + styled_string.fg = fg; + styled_string.bg = bg; + styled_string.attrs = attributes_from_str(parts[1]).map_err(|e| eyre::eyre!(e))?; + } + _ => { + return Err(eyre::eyre!("Invalid instruction: {}", instructions)); + } + }; + + Ok(styled_string) + } + + /// Parses the color instruction into a tuple of fg and bg colors. + pub fn parse_color_instruction(instruction: &str) -> Result<(Option, Option)> { + if instruction.starts_with("on ") { + // If instruction starts with "on ", bg color is provided + return Ok((None, Some(Color::from_str(&instruction[3..])?))); + } + + let colors: Vec<&str> = instruction.split(" on ").collect(); + match colors.len() { + 1 => { + // Only fg color is provided + Ok((Some(Color::from_str(colors[0])?), None)) + } + 2 => { + // Both fg and bg colors are provided + Ok((Some(Color::from_str(colors[0])?), Some(Color::from_str(colors[1])?))) + } + _ => Err(eyre::eyre!("Invalid color instruction: {}", instruction)), + } + } + + fn attributes_from_str (s: &str) -> Result { + from_str(s.to_uppercase().as_str()) + } +} + +/// ๐Ÿง‰ ยป Stylize Trait +/// -- +/// +/// `Trait` that extends `String` and `str` with the ability to stylize them +/// with ANSI color and attrs, using methods that return a new string with the given style. +pub trait Stylize { + /// Basic styling method, receives a styling instruction + /// see the `stylize` function for more information + fn stl(&self, instruction: &str) -> String; + fn black(&self) -> String { self.stl("black") } + fn red(&self) -> String { self.stl("red") } + fn green(&self) -> String { self.stl("green") } + fn yellow(&self) -> String { self.stl("yellow") } + fn blue(&self) -> String { self.stl("blue") } + fn magenta(&self) -> String { self.stl("magenta") } + fn cyan(&self) -> String { self.stl("cyan") } + fn white(&self) -> String { self.stl("white") } + fn bright_black(&self) -> String { self.stl("bright-black") } + fn bright_red(&self) -> String { self.stl("bright-red") } + fn bright_green(&self) -> String { self.stl("bright-green") } + fn bright_yellow(&self) -> String { self.stl("bright-yellow") } + fn bright_blue(&self) -> String { self.stl("bright-blue") } + fn bright_magenta(&self) -> String { self.stl("bright-magenta") } + fn bright_cyan(&self) -> String { self.stl("bright-cyan") } + fn bright_white(&self) -> String { self.stl("bright-white") } + fn rgb(&self, r: u8, g: u8, b: u8) -> String { self.stl(&format!("#{:02X}{:02X}{:02X}", r, g, b)) } + fn on_black(&self) -> String { self.stl("on black") } + fn on_red(&self) -> String { self.stl("on red") } + fn on_green(&self) -> String { self.stl("on green") } + fn on_yellow(&self) -> String { self.stl("on yellow") } + fn on_blue(&self) -> String { self.stl("on blue") } + fn on_magenta(&self) -> String { self.stl("on magenta") } + fn on_cyan(&self) -> String { self.stl("on cyan") } + fn on_white(&self) -> String { self.stl("on white") } + fn on_bright_black(&self) -> String { self.stl("on bright-black") } + fn on_bright_red(&self) -> String { self.stl("on bright-red") } + fn on_bright_green(&self) -> String { self.stl("on bright-green") } + fn on_bright_yellow(&self) -> String { self.stl("on bright-yellow") } + fn on_bright_blue(&self) -> String { self.stl("on bright-blue") } + fn on_bright_magenta(&self) -> String { self.stl("on bright-magenta") } + fn on_bright_cyan(&self) -> String { self.stl("on bright-cyan") } + fn on_bright_white(&self) -> String { self.stl("on bright-white") } + fn on_rgb(&self, r: u8, g: u8, b: u8) -> String { self.stl(&format!("on #{:02X}{:02X}{:02X}", r, g, b)) } + fn bold(&self) -> String { self.stl("+bold") } + fn dim(&self) -> String { self.stl("+dim") } + fn italic(&self) -> String { self.stl("+italic") } + fn underline(&self) -> String { self.stl("+underline") } + fn blink(&self) -> String { self.stl("+blink") } + fn reverse(&self) -> String { self.stl("+reverse") } + fn hidden(&self) -> String { self.stl("+hidden") } + fn strikethrough(&self) -> String { self.stl("+strikethrough") } +} + +impl Stylize for str { + fn stl(&self, instruction: &str) -> String { + stylize(self, instruction) + } +} + +impl Stylize for String { + fn stl(&self, instruction: &str) -> String { + stylize(self, instruction) + } +} + +/// ๐Ÿง‰ ยป stylize fn +/// -- +/// +/// Stylizes a string with optional ANSI color and attributes. +/// +pub fn stylize>(s: S, instructions: &str) -> String { + let styled_string = instructions::parse(instructions); + + if styled_string.is_err() { + return s.as_ref().to_string(); + } + + let styled_string = styled_string.unwrap(); + + let mut formatted = String::new(); + + if let Some(fg) = styled_string.fg { + formatted.push_str(&format!("\x1b[{}m", fg.to_fg_str())); + } + + if let Some(bg) = styled_string.bg { + formatted.push_str(&format!("\x1b[{}m", bg.to_bg_str())); + } + + if !styled_string.attrs.is_empty() { + let ansi_codes = styled_string.attrs.to_ansi_codes(); + + if !ansi_codes.is_empty() { + formatted.push_str(&format!("\x1b[{}m", ansi_codes.join(";"))); + } + } + + // Append the original string and clear the style + formatted.push_str(s.as_ref()); + formatted.push_str("\x1b[0m"); + + formatted +} diff --git a/lib/cli/colors/tests/mod.rs b/lib/cli/colors/tests/mod.rs new file mode 100644 index 0000000..0f92e3b --- /dev/null +++ b/lib/cli/colors/tests/mod.rs @@ -0,0 +1 @@ +mod stylizer; \ No newline at end of file diff --git a/lib/cli/colors/tests/stylizer.rs b/lib/cli/colors/tests/stylizer.rs new file mode 100644 index 0000000..1b5912d --- /dev/null +++ b/lib/cli/colors/tests/stylizer.rs @@ -0,0 +1,103 @@ +#[cfg(test)] +mod parse_instruction { + use { + crate::cli::colors::{ + stylizer::instructions::{parse, parse_color_instruction}, style::{Color, StyleAttributes} + }, eyre::{set_hook, DefaultHandler} + }; + + + fn setup_eyre() { + let _ = set_hook(Box::new(DefaultHandler::default_with)); + } + + #[test] + fn test_parse_color_instruction() -> eyre::Result<()> { + setup_eyre(); + + assert_eq!(parse_color_instruction("red")?, (Some(Color::Red), None)); + assert_eq!(parse_color_instruction("#FF0000")?, (Some(Color::TrueColor { r: 255, g: 0, b: 0 }), None)); + assert_eq!(parse_color_instruction("on blue")?, (None, Some(Color::Blue))); + assert_eq!(parse_color_instruction("on #0000FF")?, (None, Some(Color::TrueColor { r: 0, g: 0, b: 255 }))); + assert_eq!(parse_color_instruction("red on blue")?, (Some(Color::Red), Some(Color::Blue))); + assert_eq!(parse_color_instruction("#FF0000 on #0000FF")?, (Some(Color::TrueColor { r: 255, g: 0, b: 0 }), Some(Color::TrueColor { r: 0, g: 0, b: 255 }))); + assert!(parse_color_instruction("red on blue on green").is_err()); + assert!(parse_color_instruction("red on").is_err()); + assert!(parse_color_instruction("on").is_err()); + assert!(parse_color_instruction("on red blue").is_err()); + assert!(parse_color_instruction("red on blue on green").is_err()); + assert!(parse_color_instruction("invalid").is_err()); + assert!(parse_color_instruction("red on invalid").is_err()); + assert!(parse_color_instruction("invalid on blue").is_err()); + Ok(()) + } + + #[test] + fn test_parse_instructions() -> eyre::Result<()> { + + setup_eyre(); + + let styled_string = parse("red on blue")?; + assert_eq!(styled_string.fg, Some(Color::Red)); + assert_eq!(styled_string.bg, Some(Color::Blue)); + assert_eq!(styled_string.attrs, StyleAttributes::empty()); + + let styled_string = parse("on blue")?; + assert_eq!(styled_string.fg, None); + assert_eq!(styled_string.bg, Some(Color::Blue)); + assert_eq!(styled_string.attrs, StyleAttributes::empty()); + + let styled_string = parse("red")?; + assert_eq!(styled_string.fg, Some(Color::Red)); + assert_eq!(styled_string.bg, None); + assert_eq!(styled_string.attrs, StyleAttributes::empty()); + + // ##RRGGBB + let styled_string = parse("#FF0000 on #0000FF")?; + assert_eq!(styled_string.fg, Some(Color::TrueColor { r: 255, g: 0, b: 0 })); + assert_eq!(styled_string.bg, Some(Color::TrueColor { r: 0, g: 0, b: 255 })); + assert_eq!(styled_string.attrs, StyleAttributes::empty()); + + let styled_string = parse("red on blue+bold")?; + assert_eq!(styled_string.fg, Some(Color::Red)); + assert_eq!(styled_string.bg, Some(Color::Blue)); + assert_eq!(styled_string.attrs, StyleAttributes::BOLD); + + // ##RRGGBB + let styled_string = parse("#FF0000 on #0000FF+bold")?; + assert_eq!(styled_string.fg, Some(Color::TrueColor { r: 255, g: 0, b: 0 })); + assert_eq!(styled_string.bg, Some(Color::TrueColor { r: 0, g: 0, b: 255 })); + assert_eq!(styled_string.attrs, StyleAttributes::BOLD); + + let styled_string = parse("red on blue+bold|underline")?; + assert_eq!(styled_string.fg, Some(Color::Red)); + assert_eq!(styled_string.bg, Some(Color::Blue)); + assert_eq!(styled_string.attrs, StyleAttributes::BOLD | StyleAttributes::UNDERLINE); + + let styled_string = parse("red on #0000FF+bold|underline|italic")?; + assert_eq!(styled_string.fg, Some(Color::Red)); + assert_eq!(styled_string.bg, Some(Color::TrueColor { r: 0, g: 0, b: 255 })); + assert_eq!(styled_string.attrs, StyleAttributes::BOLD | StyleAttributes::UNDERLINE | StyleAttributes::ITALIC); + + let styled_string = parse("+bold")?; + assert_eq!(styled_string.fg, None); + assert_eq!(styled_string.bg, None); + assert_eq!(styled_string.attrs, StyleAttributes::BOLD); + + let styled_string = parse("on red+bold")?; + assert_eq!(styled_string.fg, None); + assert_eq!(styled_string.bg, Some(Color::Red)); + assert_eq!(styled_string.attrs, StyleAttributes::BOLD); + + let styled_string = parse("red+bold")?; + assert_eq!(styled_string.fg, Some(Color::Red)); + assert_eq!(styled_string.bg, None); + assert_eq!(styled_string.attrs, StyleAttributes::BOLD); + + let styled_string = parse("red on blue+bold")?; + assert_eq!(styled_string.fg, Some(Color::Red)); + assert_eq!(styled_string.bg, Some(Color::Blue)); + + Ok(()) + } +} \ No newline at end of file diff --git a/lib/cli/mod.rs b/lib/cli/mod.rs new file mode 100644 index 0000000..40fd5c1 --- /dev/null +++ b/lib/cli/mod.rs @@ -0,0 +1 @@ +pub mod colors; diff --git a/lib/lib.rs b/lib/lib.rs new file mode 100644 index 0000000..5d863fb --- /dev/null +++ b/lib/lib.rs @@ -0,0 +1 @@ +pub mod cli; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 465a5e5..0000000 --- a/src/main.rs +++ /dev/null @@ -1,7 +0,0 @@ -use eyre::Result; - -fn main() -> Result<()> { - println!("Hello World"); - - Ok(()) -}