otp icon indicating copy to clipboard operation
otp copied to clipboard

Add io_ansi module

Open garazdawi opened this issue 6 months ago • 6 comments

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.

garazdawi avatar Jun 13 '25 13:06 garazdawi

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

github-actions[bot] avatar Jun 13 '25 13:06 github-actions[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 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})

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).

eproxus avatar Oct 09 '25 06:10 eproxus

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

wmealing avatar Nov 08 '25 11:11 wmealing

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_COLOR environment 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_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

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 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).

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.

frazze-jobb avatar Dec 03 '25 15:12 frazze-jobb

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.

garazdawi avatar Dec 04 '25 20:12 garazdawi

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.

frazze-jobb avatar Dec 05 '25 11:12 frazze-jobb