zsh as the default shell, now, and it’s an improvement far getting things done in day to day usage, but that doesn’t mean I want to write an application in it.
I’m sorry to start a post about coloring terminal output by bashing shell languages 😉🤦, but I guess I felt the need to explain why I’ve written haskell instead of bash functions. A short explanation of what’s going on, and then the code.
Beyond the usual set of visible characters, terminals have representations for unprintable “control” characters. To get terminal output to show up in a different color, we input an “escape” character followed by a few arguments separated by semicolons. While this makes sense to me in the abstract, nobody looking at the code would call it “intuitive,” and there’s no way I’m going to remember the sequence of characters representing this. They’re very arbitrary. I’m sure there are
sh functions that make this more ergonomic, but… like most things that have lasted a long time, it’s so encumbered by historical baggage and the need for backwards compatibility that there’s no chance of making it actually pleasant to work with. As opposed to the code below which is compatible with the 1998 Haskell language standard. So modern!:
#!/usr/bin/env stack -- stack runghc --resolver lts-18.26 = main putStrLn (rgbForeground 14 100 10 "green foreground on its own line") >> putStrLn "usual-colored text" >> putStrLn 144 30 40 "red background " ( rgbBackground ++ (rgbBackground 203 208 70 "sharing a line") )>> putStrLn 144 30 40 $ ( rgbBackground "red background & " ++ (rgbForeground 3 8 70 "nested foreground") ) -- | each `Int` should be in the range 0-255 rgbForeground :: Int -> Int -> Int -> String -> String = rgbForeground r' g' b' s let r = show r'; g = show g'; b = show b' in concat ["\ESC[38;2;", r, ";", g, ";", b, "m", s, "\ESC[0m"] -- | each `Int` should be in the range 0-255 rgbBackground :: Int -> Int -> Int -> String -> String = rgbBackground r g b s "\ESC[48;2;" ++ show r ++ ";" ++ show g ++ ";" ++ show b ++ "m" ++ s ++ "\ESC[0m"
To summarize, your terminal will interpret semicolon-delimited decimal characters between
0 as the first character resets the output to its default, and setting the first 2 arguments to
2 set an expectation for 3 more arguments representing red, green, and blue channels to represent the foreground color. There is a ton more to know about this, and you can read many pages of docs on the subject… but really, nobody wants that. This post is a wrapper around 2 one-line functions that give an intuitive (obvious?) and decently-typed interface to a pretty outrageously obfuscated spec.