r3bl-org/r3bl-open-core

Make lolcat code better

Closed this issue · 3 comments

Rewrite the current lolcat implementation. The implementation of lolcat should be replaced w/ a "color wheel".

See the related issues below for why this needs to be done to remove ANSI text parsing & make this work on macOS native terminal (not iTerm or Kitty where 24 bit color is supported on macOS).

"Color wheel" is a struct that has a method which takes 3 args and returns a Vec<Color>.

  1. start color,
  2. end color,
  3. a number of intermediate colors between the start & end color.
// A way must be provided to set the max color depth:
// be it, 16 colors, 256 colors, or full 24-bit color
struct ColorWheel {..}
impl ColorWheel {
  pub fn generate_gradient(
    start_color: Color, 
    end_color: Color, 
    num_of_iterations: u8
  ) -> CommonResult<Vec<Color>> {..}

  // It may have more methods ...
}

let color_array = ColorWheel::generate_gradient(
  Color::DarkRed, Color::DarkMagenta, 10
)?;

Implement this struct for different operating systems (see related issues for important details):

  1. On macOS, the native terminal app only supports a max num of 256 colors! 🤯 There must be a way to downgrade color quality in the generate_gradient() method. This is why it wraps the Vec<Color>> in a CommonResult.
    1. The thinking here is that if the user of this method supplies a high quality color (24 bit) on macOS, this should produce a CommonError (we can add this error to CommonErrorType; maybe call it "ColorDepthNotSupported").
    2. The caller then has to downgrade their color to a lower quality in order to get the gradient Vec.
    3. There must be a way of detecting what OS we are running on and setting this max color depth somehow using https://doc.rust-lang.org/rust-by-example/attribute/cfg.html. For the future, besides auto detection, perhaps there should be a way to override this value in the user config files that will have to be created for r3bl-cmdr: settings issue
  2. On Linux, 24-bit
  3. On Windows, probably same as macOS (don't have a Windows machine to test on, w/ WSL or terminal application)

Ultimately, this struct will be used as a replacement for lolcat. In order to colorize a string, this color wheel will be used to generate a Vec<StyledText>. Here's an example.

let input = "test string".to_string();
let gradient = ColorWheel::generate_gradient(
  Color::DarkRed, Color::DarkMagenta, input.len()
)?;

let acc: Vec<StyledText> = vec![];
for (index, char) in input.iter().enumerate() {
  let char_color = gradient[index];
  acc.push(StyledText::new(
    text.to_string(), 
    style!{ color_fg: char_color }
  )
}
println!("{acc:?}");

How to implement this with existing crates

  1. More info in this video on how this is done using other crates.
  2. Repo that shows how to use these crates from the video

Related issues

  • #91 - once this bug is fixed, the ColorWheel can be used to replace the current lolcat struct and it will affect all of the render pipeline code, and make it simpler by using StyledTexts everywhere.
  • #79 - the fix for this bug might resolve the macOS terminal missing 24 bit color support; on macOS, we should restrict this struct to only select from 256 colors in total, and not 24-bit color.

More info on ANSI escape sequences and colors

To begin you must have a solid handle on ANSI escape codes:

  1. https://en.wikipedia.org/wiki/ANSI_escape_code
  2. https://talyian.github.io/ansicolors/
  3. https://tforgione.fr/posts/ansi-escape-codes/

More info on how terminal emulator apps (on various OSes) handle color:

ANSI 256 color wheel

This snippet is inspired by this discord discussion:

fn main() {
    println!("\x1b[48;5;0m");

    let spacer = "▇";
    let text_fg_color = 129;

    for index in 0..=255 {
        let index_text = format!("{index:03}");

        print!("{index_text} \x1b[38;5;{index}m{spacer} \x1b[38;5;{text_fg_color}m");

        if index == 15 || index == 231 {
            println!();
        }

        if (index + 21) % 36 == 0 {
            println!();
        }
    }

    println!("\x1b[0m");
}

Explanation of ANSI codes & 256 colors from https://talyian.github.io/ansicolors/:

  • \x1b[38;5;[n]m is foreground (used above)
  • \x1b[48;5;[n]m is background (used above)
  • The first 16 entries are the basic color table
  • The next 216 entries are a 6x6x6 cube
  • The final 24 entries are a grayscale ramp

Running the code above produces:
image

Sample app to prove API is ergonomic

To test out this API build a command line program like lolcat that reads lines from stdin, colorizes them using this API and then writes them out to stdout. This app might be called r3bl-lolcat or something. To use clap to parse the CLI args, check out these videos:

  1. https://youtu.be/ThxvuMman28
  2. https://youtu.be/WwuG2E7LbdE

To learn more about Posix & stdin, stdout, etc check out this wikipedia article.🤯

The ColorWheel struct can be designed in a way that it makes it easy for this type of program to be written in Rust.

This made up CLI app "r3bl-cowsay" takes the string and makes the ascii art and pipes the art to lolcat program to add color to the art. Eg: echo "nadia" | r3bl-cowsay.

  1. Another improvement over cowsay could be to use the following ASCII characters to print / paint the borders: symbols.
  2. Yet another improvement over cowsay, could be to center the output horizontally on the screen you can use this function lookup_size().

For reference here's how things look w/ the cowsay CLI app piped to lolcat CLI app (on Linux).

image

A real example of why we would need to do this is to pave the way for r3bl-cmdr to support shell prompt generation as described here

Another thought is to add support for HTML output to the ColorWheel struct. It might be possible to provide other methods to this struct in addition to generate_gradient() that does something like what is shown below.

E.g. echo nadia | r3bl-cowsay will output colorized "nadia"
image

E.g. echo nadia | r3bl-cowsay --html will output html tags with string nadia

<pre>
  <font color="#7FEE12">n</font>
  <font color="#83EC10">a</font>
  <font color="#88E90E">d</font>
  <font color="#8CE70C">i</font>
  <font color="#90E40A">a</font>
</pre>