Add io_ansi module
This PR adds a new module called io_ansi that allows the user to emit Virtual Terminal Sequences (aka ansi sequences) to the terminal in order to add colors/styling to text, or create fullyfledged terminal applications.
io_ansi uses the local terminfo database in order to be as cross-platform compatibly as possible.
It also works across nodes so that if functions on a remote node calls io_ansi:fwrite/1 it will use the destination terminals terminfo database to determine which sequences to emit. In practice this means that you can call things in a remote shell session that uses io_ansi and it will properly detects that terminal sequences the target terminal can handle and will print using them correctly.
At the same time all at-hoc ANSI escape sequence usage in Erlang/OTP has been migrated to use io_ansi and the terminal application user's guide has been updated.
CT Test Results
5 files 283 suites 2h 58m 45s ⏱️ 5 291 tests 4 847 ✅ 443 💤 1 ❌ 6 630 runs 6 102 ✅ 527 💤 1 ❌
For more details on these failures, see this check.
Results for commit c1763186.
:recycle: This comment has been updated with latest results.
To speed up review, make sure that you have read Contributing to Erlang/OTP and that all checks pass.
See the TESTING and DEVELOPMENT HowTo guides for details about how to run test locally.
Artifacts
// Erlang/OTP Github Action Bot
Great to finally see ANSI support in Erlang! 🙌
API
In general, I think it would be nice if there was an API that is a bit more programmable. For example, consider the following hypothetical configuration:
#{
background => blue,
position => {3, down}
}
Code that uses this would have to be a bit convoluted:
render_background(#{background := blue}) ->
io_ansi:blue_background();
render_background(#{background := red}) ->
io_ansi:red_background();
render_background(#{background := green}) ->
io_ansi:green_background();
% etc.
place_cursor(#{position := {N, down}}) ->
io_ansi:cursor_down(N);
place_cursor(#{position := {N, up}}) ->
io_ansi:cursor_down(N);
place_cursor(#{position := {N, forward}}) ->
io_ansi:cursor_forward(N);
% etc.
or the unreadable apply(io_ansi, list_to_atom(atom_to_list(Color) ++ "_background", []).
A better API would perhaps be to parameterize as much of the different values as possible? E.g.:
io_ansi:foreground(Color)(or something shorter likefgandbgeven?)io_ansi:background(Color)io_ansi:move_cursor(Steps, Direction)io_ansi:scroll(forward)
One could even go further with the API in certain places to make it more command based:
io_ansi:cursor(show)io_ansi:cursor(hide)io_ansi:cursor({3, up})
Another feature I'd like to see is support for the widely accepted NO_COLOR environment variable, that disables ANSI colors completely if set.
Name
io_ansi is a tiny bit long. What about just ansi instead? ☺️
Additional Support
Reset
There are individual reset codes for many of the formatting options that are sometimes useful.
For example, \e[3m is italic and \e[23m is reset italic. With this, one can open and close individual formatting options that overlap (think overlapping HTML tags).
Formatting
Also, I don't see italic support in the module, that would be nice too 😉. There are a few additional formatting codes that are widely supported:
- dim/faint
\e[2m - italic
\e[3m - hidden/invisible
\e[8m - strikethrough
\[e9m - double underline
\e[21m
Screen State
One thing useful when building terminal UIs is the possibility to save/restore the screen state:
#!/bin/sh
tput smcup #save previous state
echo hello
sleep 3
tput rmcup #restore previous state
These are \e[?47h and \e[?47l respectively.
In addition, there are also the alternate screen buffer that can be activated (\e[?1049h and \e[?1049l).
I would find this feature pretty neat, it would simplify my code:
https://github.com/wmealing/cellium/blob/master/apps/cellium/src/native_terminal.erl
Great to finally see ANSI support in Erlang! 🙌
API
In general, I think it would be nice if there was an API that is a bit more programmable. For example, consider the following hypothetical configuration:
#{ background => blue, position => {3, down} }Code that uses this would have to be a bit convoluted:
render_background(#{background := blue}) -> io_ansi:blue_background(); render_background(#{background := red}) -> io_ansi:red_background(); render_background(#{background := green}) -> io_ansi:green_background(); % etc.
you can now use io_ansi:background(red) also.
and similarly you can use io_ansi:color(...) for foreground
and io_ansi:underline_color(...) for underline color.
place_cursor(#{position := {N, down}}) -> io_ansi:cursor_down(N); place_cursor(#{position := {N, up}}) -> io_ansi:cursor_down(N); place_cursor(#{position := {N, forward}}) -> io_ansi:cursor_forward(N); % etc.
or the unreadable `apply(io_ansi, list_to_atom(atom_to_list(Color) ++ "_background", [])`. A better API would perhaps be to parameterize as much of the different values as possible? E.g.: * `io_ansi:foreground(Color)` (or something shorter like `fg` and `bg` even?) * `io_ansi:background(Color)` * `io_ansi:move_cursor(Steps, Direction)` * `io_ansi:scroll(forward)` One could even go further with the API in certain places to make it more command based: * `io_ansi:cursor(show)` * `io_ansi:cursor(hide)` * `io_ansi:cursor({3, up})`
I added io_ansi:move_cursor(DeltaLine, DeltaColumn), to move cursor relative to current position, negative means up/left, positive means down/right.
Did not rewrite the API, since I am not sure what value it would give compared to the existing one.
Another feature I'd like to see is support for the widely accepted
NO_COLORenvironment variable, that disables ANSI colors completely if set.
I added support for NO_COLOR, when text is being outputted in prim_tty.erl. Do you want it to also be feature of io_ansi to prevent creating the strings all together in case a different backend is used?
Name
io_ansiis a tiny bit long. What about justansiinstead? ☺️Additional Support
Reset
There are individual reset codes for many of the formatting options that are sometimes useful.
For example,
\e[3mis italic and\e[23mis reset italic. With this, one can open and close individual formatting options that overlap (think overlapping HTML tags).Formatting
Also, I don't see italic support in the module, that would be nice too 😉. There are a few additional formatting codes that are widely supported:
- dim/faint
\e[2m- italic
\e[3m- hidden/invisible
\e[8m- strikethrough
\[e9m- double underline
\e[21m
I have added, blink, dim, italic, invisible, strikethrough, overline, underline colors, curly/double/dashed and dotted underline. and respective reset codes.
Screen State
One thing useful when building terminal UIs is the possibility to save/restore the screen state:
#!/bin/sh tput smcup #save previous state echo hello sleep 3 tput rmcup #restore previous stateThese are
\e[?47hand\e[?47lrespectively.In addition, there are also the alternate screen buffer that can be activated (
\e[?1049hand\e[?1049l).
alternate_screen is implemented cursor_save/restore is implemented screen_save/restore is not implemented, according to AI you mostly want alternate screen in favor of this one.
The ansi codes I chose originally are those that work on windows. If we add more codes, those will not work there.
Regarding NO_ANSI, IMO the correct place to do is in prim_tty and the user should use io_ansi:fwrite to output the data. Elixir does it directly in the module, which might a practical solution for us as well as most users will not care about it working remotely properly.
The ansi codes I chose originally are those that work on windows. If we add more codes, those will not work there.
Regarding NO_ANSI, IMO the correct place to do is in prim_tty and the user should use io_ansi:fwrite to output the data. Elixir does it directly in the module, which might a practical solution for us as well as most users will not care about it working remotely properly.
Right, the ones I added work in gnome terminal (>= VTE 0.76). I'll add docs on what's not widely supported.