DEV Community

Cover image for Terminal Colors Using Ruby
Josh Smith
Josh Smith

Posted on • Updated on

Terminal Colors Using Ruby

I'm a web development student who's been learning through The Odin Project for the past year now (wow time flies). Students who join me in the Ruby on Rails course will encounter a number of projects where we build terminal applications using plain ol' Ruby. This is great for developing general programming skills and familiarity with the Ruby language, but since you're in the terminal, it can be a challenge to make your apps aesthetically pleasing. In this post, I'll explore some ways you can add visual flare to your ruby terminal apps through using colors.

Off-the-Shelf Solutions

So you want to add color to your terminal app? Since you're a programmer, the first thing you do is go straight to Google. This generally leads people to two ideas. One is the popular Colorize gem, which is an easy and super convenient way to get color functionality into your app. You just install the gem, require it into whatever file(s) you wish to use it in, and then you'll have several color utility functions added onto the String class. Other things people often run into are Stack Overflow posts like this one, which essentially peel back the curtain on how the Colorize gem works. When we want to add colors to a string, we wrap the string in an ANSI escape code that signals how your terminal emulator should style the text.

Both of these methods of injecting color into your app are great and easy to use. I would say they're ideal for if you're just wanting to color the log output for some server or process you're running. But if you're using them to add color to a terminal app or game, I find these and most other solutions out there underwhelming for one big reason: they offer an extremely limited color palette. The Colorize gem defines methods for 16 colors, and most other sources only show how to work with 8 or 16 colors. But most modern terminal emulators supports 24-bit "true color", meaning we actually have access to the RGB spectrum of 16,777,216 colors. Using this full spectrum can lend a lot more flavor and personality to a terminal app. So let's explore how to do it!

A Note about MacOS Terminal

The default MacOS Terminal app would be one of the modern terminals that does not support true color. If you're a Mac user and want to use the exact technique I show in this post, you'll have to use a different terminal emulator. iTerm2 is a popular choice on MacOS. It provides support for true color and a few other cool features not present in the default Terminal app.

Another option would be to use 256 color mode, which will work on the default Terminal app. For this mode, you can still reference the rest of the article except your ANSI color string will look like this:

# the 5 at the beginning puts this in 256 color mode
"\e[38;5;#{color_code}m#{string}\e[0m"
Enter fullscreen mode Exit fullscreen mode

And you can find what colors map to what color code through this table on wikipedia

24-bit ANSI color code

So above I mentioned that ANSI codes are used to style the text in a terminal. They can also be used for many other things from moving the cursor around to deleting lines of text, but right now I'll just be focusing on the colors. We can see the form of the 24-bit ANSI code here. Translated into a Ruby string, it would look this:

"\e[38;2;#{r};#{g};#{b}m#{string}\e[0m"
Enter fullscreen mode Exit fullscreen mode

Let's break down what's happening here. "\e[" is the "control sequence introducer" and is how we begin using one of these codes. This is followed by a list of semicolon-separated parameter values to communicate what exactly should happen. 38 signals that we're setting the foreground color (we'd use 48 for the background). The 2 signals that we're using the RGB format for the color. Then we list each RGB value for the color we want to use. The 'm' is a "final byte" to signal the end of the parameter list. This is followed by the string we want to apply the color to. The final piece is the "\e[0m" reset code. This returns the colors to their default state.

So let's quickly test this is working using the color green (r: 0, g: 128, b: 0).

green_rgb = "0;128;0"

puts "\e[38;2;#{green_rgb}mhello\e[0m world"
Enter fullscreen mode Exit fullscreen mode

And you should see this text when you run the above code:
Demonstration of the above ANSI code terminal output

This is good. We have our green "hello", and "world", which was placed after the reset code, is back to normal.

So what's a good way to go about building this into our app? You can probably already see a method that can arise from the above code. Perhaps something like:

def fg_color(string, rgb_values)
  "\e[38;2;#{rgb_values}m#{string}\e[0m"
end

puts fg_color("hello world", "0;128;0")
Enter fullscreen mode Exit fullscreen mode

But I think it'd be nice for the caller to not have to know or remember these RGB values, and if we extend the String class like the other solutions above do, we can get really clean, chainable methods. So let's explore that.

Building a Color Module

So I'm going to build a module that gives the String class some color utilities, but first I want to mention the pitfalls of "monkey patching" (or extending the base classes of the language). Ruby makes this very simple to do. We can just reopen any of the base classes anytime we like.

class String
  def shout
    "#{self}!!!"
  end
end

puts "hello world".shout
#-> "hello world!!!"
Enter fullscreen mode Exit fullscreen mode

But despite the ease of doing this, it's often considered a code smell. One of the reasons is that this kind of code will be globally available -- because String itself is of course globally available. You may think that this kind of thing could work

module StringExtensions
  def shout
    "#{self}!!!"
  end
end

class Display
  String.include StringExtensions
  # these methods are only available in this class, right?

  def yell_at_user
    puts "Don't do that".shout
  end
end

"hello world".shout
#-> "hello world!!!"
# the #shout method is available anywhere
Enter fullscreen mode Exit fullscreen mode

We'd rather these methods only be available where they can/should be used. Enter the refine and using methods, which are builtin methods on the Module class. With refine, we can define some extensions on a base class, and then with using, we can be selective with where these refinements are available in our code. Check it out:

module StringExtensions
  refine String do
    def shout
      "#{self}!!!"
    end
  end
end

class Display
  using StringExtensions

  def yell_at_user
    puts "Don't do that".shout
  end
end

Display.new.yell_at_user
#-> "Don't do that!!!"

puts "hello world".shout
#-> NoMethodError
Enter fullscreen mode Exit fullscreen mode

Using these methods, we can better control where the extensions to a base class are available. With this in mind, let's build a small color module. I'll use a few colors from the scheme of my favorite editor theme: dracula

module ColorableString
  RGB_COLOR_MAP = {
    cyan: "139;233;253",
    green: "80;250;123",
    red: "255;85;85"
  }.freeze

  refine String do
    def fg_color(color_name)
      rgb_val = RGB_COLOR_MAP[color_name]
      "\e[38;2;#{rgb_val}m#{self}\e[0m"
    end
  end
end

class Display
  using ColorableString

  def welcome_user
    puts "Welcome to my App".fg_color(:green)
  end

  def user_choice
    puts "Please choose an option".fg_color(:cyan)
  end

  def yell_at_user
    puts "Don't do that".fg_color(:red)
  end
end

display = Display.new

display.welcome_user
display.user_choice
display.yell_at_user
Enter fullscreen mode Exit fullscreen mode

Demonstration of above terminal output from above code

And now we have color! And it's in a format that's easy to reuse and extend. If you need background colors, just create a bg_color method with the 38 replaced by a 48. You can also create the color methods through metaprogramming if you feel so inclined, but I felt it best to keep it simple. Happy colorful coding!

Top comments (1)

Collapse
 
blpeters profile image
Brett

This is great - Thanks! Can't wait to try some of these out on my TOP Mastermind project.