This looks nice, Ryan.
I've got a little nitpick regarding the API. Take this example
iex> my_string = "This is my string."
"This is my string."
iex> my_string |> Palette.bg("#F60045") |> Palette.bright |> Palette.fg("#9967FD")
"\e[38;5;99m\e[1m\e[48;5;197mThis is my string.\e[0m\e[0m\e[0m"
IMO this is not a functional style. It looks like it's composable, but in reality it's rather awkward to customize.
If I want to write a custom function that lets me set either of the three–bg color, fg color, and brightness–I'll have to come up with something like this:
def colorful(str, opts) do
if bg = opts[:bg], do: str = Palette.bg(str, bg)
if fg = opts[:fg], do: str = Palette.fg(str, fg)
if bright = opts[:bright], do: str = Palette.bright(str)
str
end
With this example you should be able to see where my "non-functional" argument comes from.
If Palette had initially been written with data-driven approach in mind, its API would be more like this: