The letter A styled as Alchemists logo. lchemists

Putin's War on Ukraine - Watch President Zelenskyy's speech and help Ukraine fight against the senseless cruelty of a dictator!

Published April 1, 2023 Updated May 10, 2023
Tone Icon

Tone

0.2.0

Provides a customizable ANSI text colorizer for your terminal. This allows you to encode plain text that renders as colorized output. This is great for enhancing Command Line Interfaces (CLIs) and terminal output in general.

Features

  • Provides default styles with multiple options for unique combinations.

  • Provides a single object for encoding and decoding colorized text.

  • Provides aliasing of color configurations.

  • Provides quick access to default and aliased styles.

Screenshots

A screenshot of colors

Requirements

  1. Ruby.

Setup

To install with security, run:

# 💡 Skip this line if you already have the public certificate installed.
gem cert --add <(curl --compressed --location https://alchemists.io/gems.pem)
gem install tone --trust-policy HighSecurity

To install without security, run:

gem install tone

You can also add the gem directly to your project:

bundle add tone

Once the gem is installed, you only need to require it:

require "tone"

Usage

Basic usage is as follows:

tone = Tone.new
tone["Success!", :green]  # "\e[32mSuccess!\e[0m"

There is a lot more you can do with this gem so the following sections will delve into the specifics.

Encode

As you saw earlier, you can encode plain text as colorized text using #[]. Use of the #[] method is an alias to the longer #encode method. This allows you to use minimal syntax to create colorized text. Here’s a few more examples:

tone = Tone.new

# With symbols.
tone["Success", :black, :on_green]    # "\e[30;42mSuccess\e[0m"

# With strings.
tone["Success", "black", "on_green"]  # "\e[30;42mSuccess\e[0m"

# With no styles.
tone["Success"]                       # "Success"

# With any object that responds to `#to_str` or `#to_s`.
tone[Object.new, :green]              # "\e[32m#<Object:0x000000010f095668>\e[0m"

# With nil.
tone[nil]                             # ""

# With interspersed nils (nils are ignored).
tone["Success", nil, :green, nil]     # "\e[32mSuccess\e[0m"

The first argument is the text you want to encode/colorize. This can be a word, phrase, paragraph, or entire document. All arguments that follow after the first argument are style arguments which allow you to style the color of your text as you see fit. In this case, the "Success" text will use a black foreground on a green background. The styles available for you to use will be explained shortly, though. For now, know that #[] is shorthand for #encode so any of the above examples could be replaced with #encode messages. Example:

tone = Tone.new
tone.encode "Success", :black, :on_green  # "\e[30;42mSuccess\e[0m"

Both methods are available to use depending on your preference.

Decode

Once your text has been encoded with colors, it can be nice to decode the colorized text back to plain text along with additional metadata. This is helpful — as an example — for testing purposes since you might not always want to deal with the hard to read escape characters. If we build upon the examples from the Encode section, we can decode our colorized text into plain text with extra metadata:

tone = Tone.new

tone.decode "\e[30;42mSuccess\e[0m"  # [["Success", :black, :on_green]]
tone.decode "\e[37;41mFailure\e[0m"  # [["Failure", :white, :on_red]]

Notice we get an array of sub arrays which mimic the original arguments passed to #encode. This allows you to encode and decode with minimal effort. Here’s a more complex example where a sentence is used and formatted with the Amazing Print gem:

tone = Tone.new
ap tone.decode("We turned a \e[37;41mfailure\e[0m into a \e[30;42msuccess\e[0m!")

# [
#   [
#     "We turned a "
#   ],
#   [
#     "failure",
#     :white,
#     :on_red
#   ],
#   [
#     " into a "
#   ],
#   [
#     "success",
#     :black,
#     :on_green
#   ],
#   [
#     "!"
#   ]
# ]

For plain text, you get a single element array but for colorized text, it will be broken down into an array of arguments. This allows you to easily iterate over this structure for parsing, transformation, or pattern matching purposes.

Here’s another example where a paragraph is used:

tone = Tone.new

paragraph = <<~CONTENT.strip
  Yesterday \e[30;42mwent well\e[0m
  but tomorrow will be \e[37;41mmore challenging\e[0m.
CONTENT

ap tone.decode(paragraph)

# [
#   [
#     "Yesterday "
#   ],
#   [
#     "went well",
#     :black,
#     :on_green
#   ],
#   [
#     "\nbut tomorrow will be "
#   ],
#   [
#     "more challenging",
#     :white,
#     :on_red
#   ],
#   [
#     "."
#   ]
# ]

Defaults

To display defaults, use:

tone = Tone.new
tone.defaults

The above will output something similar to what you see below (minus the categorization) of key and value which will allow you to pick and choose the style or combination of styles you desire.

  • Styles

    • clear

    • bold

    • dim

    • italic

    • underline

    • inverse

    • hidden

    • strikethrough

  • Foregrounds

    • black

    • red

    • green

    • yellow

    • blue

    • purple

    • cyan

    • white

    • bright_black

    • bright_red

    • bright_green

    • bright_yellow

    • bright_blue

    • bright_purple

    • bright_cyan

    • bright_white

  • Backgrounds

    • on_black

    • on_red

    • on_green

    • on_yellow

    • on_blue

    • on_purple

    • on_cyan

    • on_white

    • on_bright_black

    • on_bright_red

    • on_bright_green

    • on_bright_yellow

    • on_bright_blue

    • on_bright_purple

    • on_bright_cyan

    • on_bright_white

These are the defaults for which you can mix-n-match as desired to produce colorful output. For example, if you want black text on a green background with an underline, you could use:

tone = Tone.new
puts tone["Success!", :black, :on_green, :strikethrough]

Codes

For situations where you’d like to find a code (or codes) for a symbol you can use the following:

tone = Tone.new

tone.find_code :green                # 32
tone.find_code :bogus                # nil
tone.find_codes :green               # [32]
tone.find_codes :red, :green, :blue  # [31, 32, 34]
tone.find_codes :bogus, :invalid     # [nil, nil]

Symbols

Much like with the codes, mentioned above, you can find a symbol (or symbols) for a code too:

tone = Tone.new

tone.find_symbol 32           # :green
tone.find_symbol 666          # nil
tone.find_symbols 32          # [:green]
tone.find_symbols 31, 32, 34  # [:red, :green, :blue]
tone.find_symbols 666, 999    # [nil, nil]

Aliases

You can alias combinations of default styles with a descriptive name for shorthand reuse. This allows you to reduce duplicated effort and speed up your workflow. Here are a few examples:

tone = Tone.new
tone.add_alias :success, :black, :on_green
tone.add_alias :failure, :white, :on_red

tone["Success!", :success]  # "\e[30;42mSuccess!\e[0m"
tone["Failure!", :failure]  # "\e[37;41mFailure!\e[0m"

Notice that the first argument is your alias and all arguments after the first argument is the list of styles. Once added, both the :success and :failure aliases can immediately be used. You can also add multiple aliases, at once, by chaining your messages:

tone = Tone.new
           .add_alias(:success, :black, :on_green)
           .add_alias :failure, :white, :on_red

tone["Success!", :success]  # "\e[30;42mSuccess!\e[0m"
tone["Failure!", :failure]  # "\e[37;41mFailure!\e[0m"

Aliases — and associated styles — can be symbols or strings. The following, despite using strings, is identical to the above:

tone = Tone.new
           .add_alias("success", "black", "on_green")
           .add_alias "failure", "white", "on_red"

tone["Success!", :success]  # "\e[30;42mSuccess!\e[0m"
tone["Failure!", :failure]  # "\e[37;41mFailure!\e[0m"

To see the list of all aliases added, use:

tone = Tone.new.add_alias(:success, :black, :on_green).add_alias :failure, :white, :on_red
ap tone.aliases

# {
#   :success => [
#     :black,
#     :on_green
#   ],
#   :failure => [
#     :white,
#     :on_red
#   ]
# }

To get a specific alias, use:

tone = Tone.new.add_alias :success, :black, :on_green
tone.get_alias :success

# [:black, :on_green]

In the case of a default, you’ll only get back the given key:

Tone.new.get_alias :green  # :green

Errors

There are several checks performed which might result in a Tone::Error if not properly used. Here’s a few examples of what you might see.

tone = Tone.new

tone.add_alias :bogus
# Alias must have styles: :bogus. (Tone::Error)

tone.add_alias :bogus, nil
# Alias must have styles: :bogus. (Tone::Error)

tone.add_alias :red, :red
# Alias mustn't duplicate (override) default: :red. (Tone::Error)

tone.add_alias :bogus, :invalid
# Invalid style (:invalid) for key (:bogus). (Tone::Error)

tone.add_alias :success, :black, :on_green
tone.add_alias :success, :black, :on_green
# Duplicate alias detected (already exists): :success. (Tone::Error)

tone.get_alias nil
# Invalid alias or default: nil. (Tone::Error)

tone.get_alias :bogus
# Invalid alias or default: :bogus. (Tone::Error)

Testing

When using this gem in your project, you might find it convenient to use the have_color RSpec matcher. This matcher is optional and must be manually required for use in your spec helper:

# spec_helper.rb
require "tone/rspec/matchers/have_color"

Once required, you can leverage the matcher in any spec as follows:

RSpec.describe DemoPresenter do
  subject(:presenter) { DemoPresenter.new color: }

  let(:color) { Tone.new }

  describe "#to_s" do
    it "renders colored text" do
      expect(presenter.to_s).to have_color(color, ["Test 0.0.0: A test.", :bold])
    end
  end
end

The first argument must be an instanced of Tone because you might have custom aliases which must be known in order to validate your spec. All subsequent arguments (one to many) that follow after the first argument can be a list of decoded tuples as would normally be answered by Tone#decode.

In situations where the spec fails, you’ll get a formatted error so you can quickly fix as necessary:

expected "\e[37mtest\e[0m\n" to have color decoded as:
["text", :blue],
["\n"]

but actually is:
["test", :white],
["\n"]

Guidelines

The following are worth considering, when using this gem, to help keep your implementation consistent.

Order your arguments by style, foreground, and background when encoding:

# No
tone["test, :underline, :on_black, :white]
tone["test, :white, :underline, :on_black]
tone["test, :on_black, :white, :underline]

# Yes
tone["test, :underline, :white, :on_black]

Order your arguments by style, foreground, and background when adding aliases:

# No
tone.add_alias :demo, :underline, :on_black, :white
tone.add_alias :demo, :white, :underline, :on_black
tone.add_alias :demo, :on_black, :white, :underline

# Yes
tone.add_alias :demo, :underline, :white, :on_black

These are not hard requirements but these little touches will help improve readability. 🎉

Development

To contribute, run:

git clone https://github.com/bkuhlmann/tone
cd tone
bin/setup

You can use the IRB console for direct access to all objects:

bin/console

Lastly, there is a bin/show script which displays the default styles for quick visual reference. This is the same script used to generate the screenshots shown at the top of this document.

bin/show

Tests

To test, run:

bin/rake

Credits