File 2523-stdlib-Add-io_ansi.patch of Package erlang
From 66d75063ad3779a190ebc2b1da751c29d8211a0e Mon Sep 17 00:00:00 2001
From: Lukas Larsson <lukas@erlang.org>
Date: Fri, 24 Nov 2023 13:09:11 +0100
Subject: [PATCH 03/17] stdlib: Add io_ansi
---
erts/emulator/nifs/common/prim_tty_nif.c | 174 +-
lib/kernel/src/group.erl | 7 +-
lib/kernel/src/prim_tty.erl | 102 +-
lib/kernel/src/standard_error.erl | 3 +-
lib/kernel/src/user_drv.erl | 74 +-
lib/kernel/test/shell_test_lib.erl | 5 +-
lib/stdlib/doc/docs.exs | 1 +
lib/stdlib/src/Makefile | 1 +
lib/stdlib/src/io_ansi.erl | 2099 ++++++++++++++++++++++
lib/stdlib/src/io_lib.erl | 39 +-
lib/stdlib/src/io_lib_format.erl | 30 +-
lib/stdlib/src/stdlib.app.src | 1 +
lib/stdlib/test/Makefile | 7 +-
lib/stdlib/test/io_ansi_SUITE.erl | 155 ++
14 files changed, 2527 insertions(+), 171 deletions(-)
create mode 100644 lib/stdlib/src/io_ansi.erl
create mode 100644 lib/stdlib/test/io_ansi_SUITE.erl
diff --git a/erts/emulator/nifs/common/prim_tty_nif.c b/erts/emulator/nifs/common/prim_tty_nif.c
index 115e48a0d7..a163ed5294 100644
--- a/erts/emulator/nifs/common/prim_tty_nif.c
+++ b/erts/emulator/nifs/common/prim_tty_nif.c
@@ -44,14 +44,10 @@
#include <stdio.h>
#include <signal.h>
#include <locale.h>
-#if defined(HAVE_TERMCAP) && (defined(HAVE_TERMCAP_H) || (defined(HAVE_CURSES_H) && defined(HAVE_TERM_H)))
+#if defined(HAVE_TERMCAP) && defined(HAVE_CURSES_H) && defined(HAVE_TERM_H)
#include <termios.h>
-#ifdef HAVE_TERMCAP_H
-#include <termcap.h>
-#else /* !HAVE_TERMCAP_H */
#include <curses.h>
#include <term.h>
-#endif
#else
/* We detected TERMCAP support, but could not find the correct headers to include */
#undef HAVE_TERMCAP
@@ -142,11 +138,12 @@ static ERL_NIF_TERM wcwidth_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM arg
static ERL_NIF_TERM wcswidth_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
static ERL_NIF_TERM sizeof_wchar_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
static ERL_NIF_TERM tty_window_size_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
-static ERL_NIF_TERM tty_tgetent_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
-static ERL_NIF_TERM tty_tgetnum_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
-static ERL_NIF_TERM tty_tgetflag_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
-static ERL_NIF_TERM tty_tgetstr_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
-static ERL_NIF_TERM tty_tgoto_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
+static ERL_NIF_TERM tty_setupterm_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
+static ERL_NIF_TERM tty_tigetnum_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
+static ERL_NIF_TERM tty_tigetflag_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
+static ERL_NIF_TERM tty_tinfo_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
+static ERL_NIF_TERM tty_tigetstr_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
+static ERL_NIF_TERM tty_tputs_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]);
static ErlNifFunc nif_funcs[] = {
{"isatty", 1, isatty_nif},
@@ -163,13 +160,12 @@ static ErlNifFunc nif_funcs[] = {
{"wcwidth", 1, wcwidth_nif},
{"wcswidth", 1, wcswidth_nif},
{"sizeof_wchar", 0, sizeof_wchar_nif},
- {"tgetent_nif", 1, tty_tgetent_nif},
- {"tgetnum_nif", 1, tty_tgetnum_nif},
- {"tgetflag_nif", 1, tty_tgetflag_nif},
- {"tgetstr_nif", 1, tty_tgetstr_nif},
- {"tgoto_nif", 1, tty_tgoto_nif},
- {"tgoto_nif", 2, tty_tgoto_nif},
- {"tgoto_nif", 3, tty_tgoto_nif}
+ {"setupterm_nif", 0, tty_setupterm_nif},
+ {"tigetnum_nif", 1, tty_tigetnum_nif},
+ {"tigetflag_nif", 1, tty_tigetflag_nif},
+ {"tinfo_nif", 0, tty_tinfo_nif},
+ {"tigetstr_nif", 1, tty_tigetstr_nif},
+ {"tputs_nif", 2, tty_tputs_nif}
};
/* NIF interface declarations */
@@ -771,13 +767,11 @@ static ERL_NIF_TERM setlocale_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM a
#endif
}
-static ERL_NIF_TERM tty_tgetent_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
+static ERL_NIF_TERM tty_setupterm_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
#ifdef HAVE_TERMCAP
- ErlNifBinary TERM;
- if (!enif_inspect_iolist_as_binary(env, argv[0], &TERM))
- return enif_make_badarg(env);
- if (tgetent((char *)NULL /* ignored */, (char *)TERM.data) <= 0) {
- return make_errno_error(env, "tgetent");
+ int errret;
+ if (setupterm(NULL, -1, &errret) < 0) {
+ return make_errno_error(env, "setupterm");
}
return atom_ok;
#else
@@ -785,23 +779,69 @@ static ERL_NIF_TERM tty_tgetent_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM
#endif
}
-static ERL_NIF_TERM tty_tgetnum_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
+static ERL_NIF_TERM tty_tinfo_make_map(ErlNifEnv* env,
+ const char * const* names,
+ const char * const* codes,
+ const char * const* fnames) {
+ ERL_NIF_TERM res = enif_make_list(env, 0);
+ ERL_NIF_TERM ks[3] = {
+ enif_make_atom(env, "name"),
+ enif_make_atom(env, "code"),
+ enif_make_atom(env, "full_name")
+ };
+ for (int i = 0; names[i] && codes[i] && fnames[i]; i++) {
+ ERL_NIF_TERM map;
+ ERL_NIF_TERM vs[3] = {
+ enif_make_string(env, names[i], ERL_NIF_LATIN1),
+ enif_make_string(env, codes[i], ERL_NIF_LATIN1),
+ enif_make_string(env, fnames[i], ERL_NIF_LATIN1)
+ };
+ enif_make_map_from_arrays(env, ks, vs, 3, &map);
+ res = enif_make_list_cell(env, map, res);
+ }
+ return res;
+}
+
+static ERL_NIF_TERM tty_tinfo_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
#ifdef HAVE_TERMCAP
- ErlNifBinary TERM;
- if (!enif_inspect_iolist_as_binary(env, argv[0], &TERM))
+ ERL_NIF_TERM ks[3] = {
+ enif_make_atom(env, "bool"),
+ enif_make_atom(env, "num"),
+ enif_make_atom(env, "str")
+ };
+
+ ERL_NIF_TERM vs[3] = {
+ tty_tinfo_make_map(env, boolnames, boolcodes, boolfnames),
+ tty_tinfo_make_map(env, numnames, numcodes, numfnames),
+ tty_tinfo_make_map(env, strnames, strcodes, strfnames)
+ };
+ ERL_NIF_TERM res;
+
+ enif_make_map_from_arrays(env, ks, vs, 3, &res);
+
+ return res;
+#else
+ return make_enotsup(env);
+#endif
+}
+
+static ERL_NIF_TERM tty_tigetnum_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
+#ifdef HAVE_TERMCAP
+ ErlNifBinary CAP;
+ if (!enif_inspect_iolist_as_binary(env, argv[0], &CAP))
return enif_make_badarg(env);
- return enif_make_int(env, tgetnum((char*)TERM.data));
+ return enif_make_int(env, tgetnum((char*)CAP.data));
#else
return make_enotsup(env);
#endif
}
-static ERL_NIF_TERM tty_tgetflag_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
+static ERL_NIF_TERM tty_tigetflag_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
#ifdef HAVE_TERMCAP
- ErlNifBinary TERM;
- if (!enif_inspect_iolist_as_binary(env, argv[0], &TERM))
+ ErlNifBinary CAP;
+ if (!enif_inspect_iolist_as_binary(env, argv[0], &CAP))
return enif_make_badarg(env);
- if (tgetflag((char*)TERM.data))
+ if (tgetflag((char*)CAP.data))
return atom_true;
return atom_false;
#else
@@ -809,19 +849,15 @@ static ERL_NIF_TERM tty_tgetflag_nif(ErlNifEnv* env, int argc, const ERL_NIF_TER
#endif
}
-static ERL_NIF_TERM tty_tgetstr_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
+static ERL_NIF_TERM tty_tigetstr_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
#ifdef HAVE_TERMCAP
- ErlNifBinary TERM, ret;
- /* tgetstr seems to use a lot of stack buffer space,
- so buff needs to be relatively "small" */
- char *str = NULL;
- char buff_area[BUFSIZ] = {0};
- char *buff = (char*)buff_area;
-
- if (!enif_inspect_iolist_as_binary(env, argv[0], &TERM))
+ ErlNifBinary CAP, ret;
+ char *str;
+ if (!enif_inspect_iolist_as_binary(env, argv[0], &CAP))
return enif_make_badarg(env);
- str = tgetstr((char*)TERM.data, &buff);
- if (!str) return atom_false;
+ str = tigetstr((char*)CAP.data);
+ if (!str) return atom_false; /* Not supported by terminal */
+ if (str == (char*)-1) return enif_make_badarg(env); /* Invalid capability */
enif_alloc_binary(strlen(str), &ret);
memcpy(ret.data, str, strlen(str));
return enif_make_tuple2(
@@ -832,6 +868,7 @@ static ERL_NIF_TERM tty_tgetstr_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM
}
#ifdef HAVE_TERMCAP
+static ErlNifMutex *tputs_mutex;
static int tputs_buffer_index;
static unsigned char tputs_buffer[1024];
@@ -845,21 +882,50 @@ static int tty_puts_putc(int c) {
}
#endif
-static ERL_NIF_TERM tty_tgoto_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
+static ERL_NIF_TERM tty_tputs_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
#ifdef HAVE_TERMCAP
- ErlNifBinary TERM;
+ ErlNifBinary cap;
ERL_NIF_TERM ret;
char *ent;
- int value1 = 0, value2 = 0;
unsigned char *buff;
+ long params[9] = { 0 };
+ int slot = 0;
- if (!enif_inspect_iolist_as_binary(env, argv[0], &TERM) ||
- (argc > 1 && !enif_get_int(env, argv[1], &value1)) ||
- (argc > 2 && !enif_get_int(env, argv[2], &value2))
- )
+ if (!enif_inspect_iolist_as_binary(env, argv[0], &cap))
return enif_make_badarg(env);
- ent = tgoto((char*)TERM.data, value1, value2);
- if (!ent) return make_errno_error(env, "tgoto");
+
+ {
+ ERL_NIF_TERM head, tail = argv[1];
+ while(enif_get_list_cell(env, tail, &head, &tail)) {
+ if (!enif_get_long(env, head, params + slot)) {
+ return enif_make_badarg(env);
+ }
+ slot++;
+ }
+ if (!enif_is_empty_list(env, tail)) {
+ enif_make_badarg(env);
+ }
+ }
+
+ /* Neither tparm nor tputs are thread safe.. */
+ enif_mutex_lock(tputs_mutex);
+
+ /* If the capability has arguments, we call tparm */
+ if (slot) {
+ /* The https://linux.die.net/man/3/tparm specifies that although tparm uses
+ a vararg prototype on Linux, to be portable we should always call it with
+ 9 arguments as some implementation have a static prototype.
+ */
+ ent = tparm((char*)cap.data, params[0], params[1], params[2], params[3],
+ params[4], params[5], params[6], params[7], params[8]);
+
+ if (!ent) {
+ enif_mutex_unlock(tputs_mutex);
+ return make_errno_error(env, "tparm");
+ }
+ } else {
+ ent = (char*)cap.data;
+ }
tputs_buffer_index = 0;
(void)tputs(ent, 1, tty_puts_putc); /* tputs only fails if ent is null,
@@ -867,6 +933,9 @@ static ERL_NIF_TERM tty_tgoto_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM a
buff = enif_make_new_binary(env, tputs_buffer_index, &ret);
memcpy(buff, tputs_buffer, tputs_buffer_index);
+
+ enif_mutex_unlock(tputs_mutex);
+
return enif_make_tuple2(env, atom_ok, ret);
#else
return make_enotsup(env);
@@ -1150,6 +1219,9 @@ static void load_resources(ErlNifEnv* env, ErlNifResourceFlags rt_flags) {
static int load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info)
{
*priv_data = NULL;
+#ifdef HAVE_TERMCAP
+ tputs_mutex = enif_mutex_create("tputs_muex");
+#endif
load_resources(env, ERL_NIF_RT_CREATE);
return 0;
}
diff --git a/lib/kernel/src/group.erl b/lib/kernel/src/group.erl
index 911e2bbf0a..d4cdeb337c 100644
--- a/lib/kernel/src/group.erl
+++ b/lib/kernel/src/group.erl
@@ -49,7 +49,9 @@
-type mfargs() :: {module(), atom(), [term()]}.
-type nmfargs() :: {node(), module(), atom(), [term()]}.
--define(IS_PUTC_REQ(Req), element(1, Req) =:= put_chars orelse element(1, Req) =:= requests).
+-define(IS_PUTC_REQ(Req), element(1, Req) =:= put_chars orelse
+ element(1, Req) =:= requests orelse
+ element(1, Req) =:= put_ansi).
-define(IS_INPUT_REQ(Req),
element(1, Req) =:= get_chars orelse element(1, Req) =:= get_line orelse
element(1, Req) =:= get_until orelse element(1, Req) =:= get_password).
@@ -609,6 +611,9 @@ putc_request(Req, From, ReplyAs, State) ->
%%
%% These put requests have to be synchronous to the driver as otherwise
%% there is no guarantee that the data has actually been printed.
+putc_request({put_ansi, Opts, Ansi}, Drv, From) ->
+ send_drv(Drv, {put_ansi_sync, Opts, Ansi, From}),
+ noreply;
putc_request({put_chars,unicode,Chars}, Drv, From) ->
case catch unicode:characters_to_binary(Chars,utf8) of
Binary when is_binary(Binary) ->
diff --git a/lib/kernel/src/prim_tty.erl b/lib/kernel/src/prim_tty.erl
index 6777ee1ac8..b24b836c7b 100644
--- a/lib/kernel/src/prim_tty.erl
+++ b/lib/kernel/src/prim_tty.erl
@@ -115,17 +115,21 @@
-export([reader_stop/1, disable_reader/1, enable_reader/1, read/1, read/2,
is_reader/2, is_writer/2, output_mode/1]).
+%% Export to io_ansi
+-export([tigetstr/1, tputs/2, tigetflag/1, tigetnum/1, tinfo/0]).
+
-nifs([isatty/1, tty_create/1, tty_init/2, setlocale/1,
tty_select/2, tty_window_size/1,
tty_encoding/1, tty_is_open/2, write_nif/2, read_nif/3, isprint/1,
wcwidth/1, wcswidth/1,
- sizeof_wchar/0, tgetent_nif/1, tgetnum_nif/1, tgetflag_nif/1, tgetstr_nif/1,
- tgoto_nif/1, tgoto_nif/2, tgoto_nif/3]).
+ sizeof_wchar/0, setupterm_nif/0, tigetnum_nif/1, tigetflag_nif/1,
+ tigetstr_nif/1, tinfo_nif/0, tputs_nif/2
+ ]).
-export([reader_loop/2, writer_loop/2]).
%% Exported in order to remove "unused function" warning
--export([sizeof_wchar/0, wcswidth/1, tgoto/1, tgoto/2, tgoto/3, tty_is_open/2]).
+-export([sizeof_wchar/0, wcswidth/1, tty_is_open/2]).
%% proc_lib exports
-export([reader/1, writer/1]).
@@ -151,6 +155,7 @@
buffer_expand_row = 1,
buffer_expand_limit = 0 :: non_neg_integer(),
putc_buffer = <<>>, %% Buffer for putc containing the last row of characters
+ have_termcap = false :: boolean(),
cols = 80,
rows = 24,
xn = false,
@@ -267,7 +272,12 @@ init(UserOptions) when is_map(UserOptions) ->
true -> UnicodeSupported
end,
{ok, ANSI_RE_MP} = re:compile(?ANSI_REGEXP, [unicode]),
- init_term(#state{ tty = TTY, unicode = UnicodeMode, options = Options, ansi_regexp = ANSI_RE_MP }).
+
+ HaveTermCap = setupterm() =:= ok,
+
+ init_term(#state{ tty = TTY, have_termcap = HaveTermCap,
+ unicode = UnicodeMode, options = Options,
+ ansi_regexp = ANSI_RE_MP }).
init_term(State = #state{ tty = TTY, options = Options }) ->
TTYState =
case maps:get(input, Options) of
@@ -358,66 +368,60 @@ init(State, ssh) ->
State#state{ xn = true };
init(State, {unix,_}) ->
- case os:getenv("TERM") of
- false ->
- error(enotsup);
- Term ->
- case tgetent(Term) of
- ok -> ok;
- {error,_} -> error(enotsup)
- end
- end,
+ State#state.have_termcap orelse error(enotsup),
+
%% See https://www.gnu.org/software/termutils/manual/termcap-1.3/html_mono/termcap.html#SEC23
%% for a list of all possible termcap capabilities
- Clear = case tgetstr("clear") of
+ Clear = case tigetstr("clear") of
{ok, C} -> C;
false -> (#state{})#state.clear
end,
- Cols = case tgetnum("co") of
+
+ Cols = case tigetnum("co") of
{ok, Cs} -> Cs;
_ -> (#state{})#state.cols
end,
- Up = case tgetstr("up") of
+ Up = case tigetstr("cuu1") of
{ok, U} -> U;
false -> error(enotsup)
end,
- Down = case tgetstr("do") of
+ Down = case tigetstr("cud1") of
false -> (#state{})#state.down;
{ok, D} -> D
end,
- Left = case {tgetflag("bs"),tgetstr("bc")} of
+ Left = case {tigetflag("OTbs"),tigetstr("OTbc")} of
{true,_} -> (#state{})#state.left;
{_,false} -> (#state{})#state.left;
{_,{ok, L}} -> L
end,
- Right = case tgetstr("nd") of
+ Right = case tigetstr("cuf1") of
{ok, R} -> R;
false -> error(enotsup)
end,
Insert =
- case tgetstr("IC") of
+ case tigetstr("ich") of
{ok, IC} -> IC;
false -> (#state{})#state.insert
end,
- Tab = case tgetstr("ta") of
+ Tab = case tigetstr("ht") of
{ok, TA} -> TA;
false -> (#state{})#state.tab
end,
- Delete = case tgetstr("DC") of
+ Delete = case tigetstr("dch") of
{ok, DC} -> DC;
false -> (#state{})#state.delete
end,
- Position = case tgetstr("u7") of
+ Position = case tigetstr("u7") of
{ok, <<"\e[6n">> = U7} ->
%% User 7 should contain the codes for getting
%% cursor position.
%% User 6 should contain how to parse the reply
- {ok, <<"\e[%i%d;%dR">>} = tgetstr("u6"),
+ {ok, <<"\e[%i%d;%dR">>} = tigetstr("u6"),
<<"\e[6n">> = U7;
false -> (#state{})#state.position
end,
@@ -425,7 +429,7 @@ init(State, {unix,_}) ->
%% According to the manual this should only be issued when the cursor
%% is at position 0, but until we encounter such a console we keep things
%% simple and issue this with the cursor anywhere
- DeleteAfter = case tgetstr("cd") of
+ DeleteAfter = case tigetstr("ed") of
{ok, DA} ->
DA;
false ->
@@ -435,7 +439,7 @@ init(State, {unix,_}) ->
State#state{
cols = Cols,
clear = Clear,
- xn = tgetflag("xn"),
+ xn = tigetflag("xn"),
up = Up,
down = Down,
left = Left,
@@ -1512,36 +1516,28 @@ sizeof_wchar() ->
erlang:nif_error(undef).
wcswidth(_Char) ->
erlang:nif_error(undef).
-tgetent(Char) ->
- tgetent_nif([Char,0]).
-tgetnum(Char) ->
- tgetnum_nif([Char,0]).
-tgetflag(Char) ->
- tgetflag_nif([Char,0]).
-tgetstr(Char) ->
- case tgetstr_nif([Char,0]) of
- {ok, Str} ->
- {ok, re:replace(Str, "\\$<[^>]*>","")};
- Error ->
- Error
- end.
-tgoto(Char) ->
- tgoto_nif([Char,0]).
-tgoto(Char, Arg) ->
- tgoto_nif([Char,0], Arg).
-tgoto(Char, Arg1, Arg2) ->
- tgoto_nif([Char,0], Arg1, Arg2).
-tgetent_nif(_Char) ->
- erlang:nif_error(undef).
-tgetnum_nif(_Char) ->
+setupterm() ->
+ setupterm_nif().
+tigetnum(Char) ->
+ tigetnum_nif([Char,0]).
+tigetflag(Char) ->
+ tigetflag_nif([Char,0]).
+tigetstr(Char) ->
+ tigetstr_nif([Char,0]).
+tinfo() ->
+ tinfo_nif().
+tputs(Char, Args) ->
+ tputs_nif([Char,0], Args).
+
+setupterm_nif() ->
erlang:nif_error(undef).
-tgetflag_nif(_Char) ->
+tigetnum_nif(_Char) ->
erlang:nif_error(undef).
-tgetstr_nif(_Char) ->
+tigetflag_nif(_Char) ->
erlang:nif_error(undef).
-tgoto_nif(_Ent) ->
+tinfo_nif() ->
erlang:nif_error(undef).
-tgoto_nif(_Ent, _Arg) ->
+tigetstr_nif(_Char) ->
erlang:nif_error(undef).
-tgoto_nif(_Ent, _Arg1, _Arg2) ->
+tputs_nif(_Ent, _Args) ->
erlang:nif_error(undef).
diff --git a/lib/kernel/src/standard_error.erl b/lib/kernel/src/standard_error.erl
index 51a296d7d3..d98ae1602f 100644
--- a/lib/kernel/src/standard_error.erl
+++ b/lib/kernel/src/standard_error.erl
@@ -260,7 +260,8 @@ getopts() ->
Uni = {encoding,get(encoding)},
Onlcr = {onlcr, get(onlcr)},
Log = {log, get(log)},
- {reply,[Uni, Onlcr, Log]}.
+ Terminal = {terminal, get(onlcr)},
+ {reply,[Uni, Onlcr, Log, Terminal]}.
wrap_characters_to_binary(Chars,From,To) ->
TrNl = get(onlcr),
diff --git a/lib/kernel/src/user_drv.erl b/lib/kernel/src/user_drv.erl
index 6d76f62de5..dcc62005b9 100644
--- a/lib/kernel/src/user_drv.erl
+++ b/lib/kernel/src/user_drv.erl
@@ -57,6 +57,8 @@
%% Output raw binary, should only be called if output mode is set to raw
%% and encoding set to latin1.
{put_chars_sync, latin1, binary(), {From :: pid(), Reply :: term()}} |
+ %% Print a terminal special character
+ {put_ansi_sync, list(), io_ansi:vts(), Reply :: term()} |
%% Put text in expansion area
{put_expand, unicode, binary(), integer()} |
{move_expand, -32768..32767} |
@@ -358,8 +360,7 @@ init_shell(State, Slogan) ->
{next_state, server, State#state{ current_group = gr_cur_pid(State#state.groups) },
{next_event, info,
{gr_cur_pid(State#state.groups),
- {put_chars, unicode,
- unicode:characters_to_binary(io_lib:format("~ts", [Slogan]))}}}}.
+ {put_chars, unicode, io_lib:bformat("~ts", [Slogan])}}}}.
%% start_user()
%% Start a group leader process and register it as 'user', unless,
@@ -544,7 +545,8 @@ server(info, {'DOWN', MonitorRef, _, _, Reason},
Origin ! {reply, Reply, {error, Reason}},
?LOG_INFO("Failed to write to standard out (~p)", [Reason]),
stop;
-server(info,{Requester, {put_chars_sync, _, _, Reply}}, _State) ->
+server(info,{Requester, {Request, _, _, Reply}}, _State)
+ when Request =:= put_chars_sync; Request =:= put_ansi_sync ->
%% This is a sync request from an unknown or inactive group.
%% We need to ack the Req otherwise originating process will hang forever.
%% We discard the output to non visible shells
@@ -590,15 +592,15 @@ server(info,{'EXIT', Group, Reason}, State) ->
Group when Reason =/= die, Reason =/= terminated -> % current shell exited
Reqs = [if
Reason =/= normal ->
- {put_chars,unicode,<<"*** ERROR: ">>};
+ {put_chars,unicode,~"*** ERROR: "};
true -> % exit not caused by error
- {put_chars,unicode,<<"*** ">>}
+ {put_chars,unicode,~"*** "}
end,
- {put_chars,unicode,<<"Shell process terminated! ">>}],
+ {put_chars,unicode,~"Shell process terminated! "}],
Gr1 = gr_del_pid(State#state.groups, Group),
case GroupInfo of
{Ix,{shell,start,Params}} -> % 3-tuple == local shell
- NewTTyState = io_requests(Reqs ++ [{put_chars,unicode,<<"***\n">>}],
+ NewTTyState = io_requests(Reqs ++ [{put_chars,unicode,~"***\n"}],
State#state.tty),
%% restart group leader and shell, same index
NewGroup = group:start(self(), {shell,start,Params}),
@@ -613,7 +615,7 @@ server(info,{'EXIT', Group, Reason}, State) ->
true ->
NewTTYState = io_requests(Reqs,
State#state.tty),
- _ = io_request({put_chars_sync,unicode,<<"Read EOF ***\n">>, {self(), none}}, NewTTYState),
+ _ = io_request({put_chars_sync,unicode,~"Read EOF ***\n", {self(), none}}, NewTTYState),
WriterRef = State#state.write,
receive
{WriterRef, ok} -> ok
@@ -623,7 +625,7 @@ server(info,{'EXIT', Group, Reason}, State) ->
erlang:halt(0, []);
false ->
NewTTYState = io_requests(
- Reqs ++ [{put_chars,unicode,<<"(^G to start new job) ***\n">>}],
+ Reqs ++ [{put_chars,unicode,~"(^G to start new job) ***\n"}],
State#state.tty),
{keep_state, State#state{ tty = NewTTYState, groups = Gr1 }}
end
@@ -661,13 +663,13 @@ switch_loop(internal, init, State) ->
end
end,
NewGroup = group:start(self(), {shell,start,[]}),
- NewTTYState = io_requests([{insert_chars,unicode,<<"\n">>}], State#state.tty),
+ NewTTYState = io_requests([{insert_chars,unicode,~"\n"}], State#state.tty),
{next_state, server,
State#state{ tty = NewTTYState,
groups = gr_add_cur(Gr1, NewGroup, {shell,start,[]})}};
jcl ->
NewTTYState =
- io_requests([{insert_chars,unicode,<<"\nUser switch command (enter 'h' for help)\n">>}],
+ io_requests([{insert_chars,unicode,~"\nUser switch command (enter 'h' for help)\n"}],
State#state.tty),
%% init edlin used by switch command and have it copy the
%% text buffer from current group process
@@ -688,22 +690,22 @@ switch_loop(internal, {line, Line}, State) ->
Curr ! {self(), activate},
{next_state, server,
State#state{ current_group = Curr, groups = Groups,
- tty = io_requests([{insert_chars, unicode, <<"\n">>},new_prompt], State#state.tty)}};
+ tty = io_requests([{insert_chars, unicode, ~"\n"},new_prompt], State#state.tty)}};
{retry, Requests} ->
- {keep_state, State#state{ tty = io_requests([{insert_chars, unicode, <<"\n">>},new_prompt|Requests], State#state.tty) },
+ {keep_state, State#state{ tty = io_requests([{insert_chars, unicode, ~"\n"},new_prompt|Requests], State#state.tty) },
{next_event, internal, line}};
{retry, Requests, Groups} ->
Curr = gr_cur_pid(Groups),
put(current_group, Curr),
{keep_state, State#state{
- tty = io_requests([{insert_chars, unicode, <<"\n">>},new_prompt|Requests], State#state.tty),
+ tty = io_requests([{insert_chars, unicode, ~"\n"},new_prompt|Requests], State#state.tty),
current_group = Curr,
groups = Groups },
{next_event, internal, line}}
end;
{error, _, _} ->
NewTTYState =
- io_requests([{insert_chars,unicode,<<"Illegal input\n">>}], State#state.tty),
+ io_requests([{insert_chars,unicode,~"Illegal input\n"}], State#state.tty),
{keep_state, State#state{ tty = NewTTYState },
{next_event, internal, line}}
end;
@@ -771,8 +773,7 @@ switch_cmd({i, I}, Gr, _Dumb) ->
exit(Pid, interrupt),
{retry, [{put_chars, unicode,
- unicode:characters_to_binary(
- io_lib:format("Interrupted job ~p, enter 'c' to resume.~n",[I]))}]};
+ io_lib:bformat("Interrupted job ~p, enter 'c' to resume.~n",[I])}]};
undefined ->
unknown_group()
end;
@@ -809,7 +810,7 @@ switch_cmd(r, Gr0, _Dumb) ->
Gr = gr_add_cur(Gr0, Pid, {Node,shell,start,[]}),
{retry, [], Gr};
false ->
- {retry, [{put_chars,unicode,<<"Node is not alive\n">>}]}
+ {retry, [{put_chars,unicode,~"Node is not alive\n"}]}
end;
switch_cmd({r, Node}, Gr, Dumb) when is_atom(Node)->
switch_cmd({r, Node, shell}, Gr, Dumb);
@@ -822,17 +823,17 @@ switch_cmd({r,Node,Shell}, Gr0, Dumb) when is_atom(Node), is_atom(Shell) ->
Gr = gr_add_cur(Gr0, Pid, {Node,Shell,start,[]}),
{retry, [], Gr};
false ->
- Bin = atom_to_binary(Node),
- {retry, [{put_chars,unicode,<<"Could not connect to node ", Bin/binary, "\n">>}]}
+ {retry, [{put_chars,unicode,<<"Could not connect to node ",
+ (atom_to_binary(Node))/binary, "\n">>}]}
end;
false ->
- {retry, [{put_chars,unicode,"Node is not alive\n"}]}
+ {retry, [{put_chars,unicode,~"Node is not alive\n"}]}
end;
switch_cmd(q, _Gr, _Dumb) ->
case erlang:system_info(break_ignored) of
true -> % noop
- {retry, [{put_chars,unicode,<<"Unknown command\n">>}]};
+ {retry, [{put_chars,unicode,~"Unknown command\n"}]};
false ->
halt()
end;
@@ -841,26 +842,26 @@ switch_cmd(h, _Gr, _Dumb) ->
switch_cmd([], _Gr, _Dumb) ->
{retry,[]};
switch_cmd(_Ts, _Gr, _Dumb) ->
- {retry, [{put_chars,unicode,<<"Unknown command\n">>}]}.
+ {retry, [{put_chars,unicode,~"Unknown command\n"}]}.
unknown_group() ->
- {retry,[{put_chars,unicode,<<"Unknown job\n">>}]}.
+ {retry,[{put_chars,unicode,~"Unknown job\n"}]}.
list_commands() ->
QuitReq = case erlang:system_info(break_ignored) of
true ->
[];
false ->
- [{put_chars, unicode,<<" q - quit erlang\n">>}]
+ [{put_chars, unicode,~" q - quit erlang\n"}]
end,
- [{put_chars, unicode,<<" c [nn] - connect to job\n">>},
- {put_chars, unicode,<<" i [nn] - interrupt job\n">>},
- {put_chars, unicode,<<" k [nn] - kill job\n">>},
- {put_chars, unicode,<<" j - list all jobs\n">>},
- {put_chars, unicode,<<" s [shell] - start local shell\n">>},
- {put_chars, unicode,<<" r [node [shell]] - start remote shell\n">>}] ++
+ [{put_chars, unicode,~" c [nn] - connect to job\n"},
+ {put_chars, unicode,~" i [nn] - interrupt job\n"},
+ {put_chars, unicode,~" k [nn] - kill job\n"},
+ {put_chars, unicode,~" j - list all jobs\n"},
+ {put_chars, unicode,~" s [shell] - start local shell\n"},
+ {put_chars, unicode,~" r [node [shell]] - start remote shell\n"}] ++
QuitReq ++
- [{put_chars, unicode,<<" ? | h - this message\n">>}].
+ [{put_chars, unicode,~" ? | h - this message\n"}].
group_opts(Node) ->
VersionString = erpc:call(Node, erlang, system_info, [otp_release]),
@@ -910,6 +911,12 @@ io_request({put_chars_sync, unicode, Chars, Reply}, TTY) ->
{Output, NewTTY} = prim_tty:handle_request(TTY, {putc, unicode:characters_to_binary(Chars)}),
{ok, MonitorRef} = prim_tty:write(NewTTY, Output, self()),
{Reply, MonitorRef, NewTTY};
+io_request({put_ansi_sync, Options, Ansi, Reply}, TTY) ->
+ try io_ansi:format([Ansi], [], [{reset,false}|Options]) of
+ AnsiVTS ->
+ io_request({put_chars_sync, unicode, AnsiVTS, Reply}, TTY)
+ catch _:_ -> {Reply, {error, {put_ansi, unicode, Ansi}}}
+ end;
io_request({put_expand, unicode, Chars, N}, TTY) ->
write(prim_tty:handle_request(TTY, {expand, unicode:characters_to_binary(Chars), N}));
io_request({move_expand, N}, TTY) ->
@@ -1096,6 +1103,5 @@ gr_list(#gr{ current = Current, groups = Groups}) ->
(#group{ index = I, shell = S }) ->
Marker = ["*" || Current =:= I],
[{put_chars, unicode,
- unicode:characters_to_binary(
- io_lib:format("~4w~.1ts ~w\n", [I,Marker,S]))}]
+ io_lib:bformat("~4w~.1ts ~w\n", [I,Marker,S])}]
end, Groups).
diff --git a/lib/kernel/test/shell_test_lib.erl b/lib/kernel/test/shell_test_lib.erl
index 65733aa00e..3a72b70ee5 100644
--- a/lib/kernel/test/shell_test_lib.erl
+++ b/lib/kernel/test/shell_test_lib.erl
@@ -82,6 +82,9 @@ stop_tmux(_Config) ->
ok.
%% Setup a TTY, or a ssh server and client but do not type anything in terminal (except password)
+-spec setup_tty([{peer, peer:config()} | {tc_path, file:name()} |
+ {env, [{Key :: string(), Value :: string()}]} |
+ {args, [string()]}]) -> term().
setup_tty(Config) ->
ClientName = maps:get(name,proplists:get_value(peer, Config, #{}),
peer:random_name(proplists:get_value(tc_path, Config))),
@@ -317,7 +320,7 @@ check_content(Term, Match, Opts) when is_map(Opts) ->
check_content(Term, Match, Opts, 5).
check_content(Term, Match, Opts, Attempt) ->
OrigContent = case Term of
- #tmux{} -> get_content(Term);
+ #tmux{} -> get_content(Term, maps:get(args, Opts, ""));
Fun when is_function(Fun,0) -> Fun()
end,
Content = case maps:find(replace, Opts) of
diff --git a/lib/stdlib/doc/docs.exs b/lib/stdlib/doc/docs.exs
index b61e18e83c..eb1c7648ef 100644
--- a/lib/stdlib/doc/docs.exs
+++ b/lib/stdlib/doc/docs.exs
@@ -39,6 +39,7 @@
:string,
:re,
:io,
+ :io_ansi,
:io_lib,
:filelib,
:filename,
diff --git a/lib/stdlib/src/Makefile b/lib/stdlib/src/Makefile
index 0ac0e8edda..8f97ef2f8f 100644
--- a/lib/stdlib/src/Makefile
+++ b/lib/stdlib/src/Makefile
@@ -97,6 +97,7 @@ MODULES= \
gen_server \
gen_statem \
io \
+ io_ansi \
io_lib \
io_lib_format \
io_lib_fread \
diff --git a/lib/stdlib/src/io_ansi.erl b/lib/stdlib/src/io_ansi.erl
new file mode 100644
index 0000000000..5e68b8ef66
--- /dev/null
+++ b/lib/stdlib/src/io_ansi.erl
@@ -0,0 +1,2099 @@
+%%
+%% %CopyrightBegin%
+%%
+%% SPDX-License-Identifier: Apache-2.0
+%%
+%% Copyright Ericsson AB 2025. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+%%
+%% https://hexdocs.pm/elixir/IO.ANSI.html
+%% https://en.wikipedia.org/wiki/ANSI_escape_code
+%% https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
+%% https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
+%%
+
+-module(io_ansi).
+-moduledoc """
+Controlling the terminal using virtual terminal sequences (aka [ANSI escape codes]).
+
+This module provides an interface to emit and parse virtual terminal sequences (VTS),
+also known as [ANSI escape codes]. VTS can be used to:
+
+- change the style of text or background in the terminal by adding color or emphasis.
+- delete printed characters or lines.
+- move, hide or show the cursor
+
+and more things. As different terminals are interpret VTSs slightly
+differently, `m:io_ansi` uses the local [terminfo] database together with
+predefined sequences to emit the correct sequence for the terminal that is
+currently used. To fetch values directly from the [terminfo] database you can use
+`tput/2`, `tigetnum/1` and `tigetflag/1`.
+
+`m:io_ansi` provides two interfaces to emit sequences. You can either call the
+function representing the sequence you want to emit, for example `io_ansi:blue()`
+and it will return the sequence representing blue.
+
+```erlang
+1> io_ansi:blue().
+<<"\e[34m">>
+```
+
+This will use the [terminfo] database locally where the call is made, so it may
+not be correct if used across nodes.
+
+You can also use the [`io_ansi:format/1,2,3`](`io_ansi:format/3`) functions
+which works just as `io_lib:bformat/3`, except that it also accepts atoms and
+tuples that represent VTSs. For example:
+
+```erlang
+1> io_ansi:format([blue,"~p"], [red]).
+<<"\e[34mred\e(B\e[m">>
+```
+
+`io_ansi:format/3` will automatically reset the terminal to its original state
+and strip any VTSs that are not supported by the terminal. It can also be disabled
+through an option. For example:
+
+```erlang
+1> io_ansi:format([blue,"~p"], [red], [{enabled, false}]).
+<<"red">>
+```
+
+Finally there is [`io_ansi:fwrite/1,2,3,4`](`io_ansi:fwrite/4`) which does not
+return the string to be printed, but instead sends it to the `t:io:device/0`
+that should handle it. `io_ansi:fwrite/4` works across nodes and will use the
+[terminfo] database where the data is outputted to decide what to emit.
+
+[terminfo]: https://man7.org/linux/man-pages/man5/terminfo.5.html
+[ANSI escape codes]: https://en.wikipedia.org/wiki/ANSI_escape_code
+""".
+
+
+-export([tput/1, tput/2, tigetnum/1, tigetflag/1, tinfo/0]).
+-export([format/1, format/2, format/3, fwrite/1, fwrite/2, fwrite/3, fwrite/4,
+ enabled/0, enabled/1, scan/1]).
+
+-export([black/0, blue/0, cyan/0, green/0, magenta/0, red/0, white/0, yellow/0,
+ color/1, color/3, default_color/0]).
+-export([black_background/0, red_background/0, green_background/0, yellow_background/0,
+ blue_background/0, magenta_background/0, cyan_background/0, white_background/0,
+ background/1, background/3, default_background/0]).
+-export([light_black/0, light_red/0, light_green/0, light_yellow/0, light_blue/0,
+ light_magenta/0, light_cyan/0, light_white/0]).
+-export([light_black_background/0, light_red_background/0, light_green_background/0,
+ light_yellow_background/0, light_blue_background/0, light_magenta_background/0,
+ light_cyan_background/0, light_white_background/0]).
+-export([modify_color/4]).
+-export([bold/0, bold_off/0, underline/0, underline_off/0, negative/0, negative_off/0]).
+-export([hyperlink_start/1, hyperlink_start/2, hyperlink_reset/0]).
+-export([clear/0, erase_display/0, insert_character/1, delete_character/0,
+ delete_character/1, erase_character/1, insert_line/0, insert_line/1,
+ delete_line/0, delete_line/1, erase_line/0]).
+-export([alternate_character_set_mode/0, alternate_character_set_mode_off/0]).
+-export([cursor/2, cursor_up/0, cursor_up/1, cursor_down/0, cursor_down/1,
+ cursor_forward/0, cursor_forward/1, cursor_backward/0, cursor_backward/1,
+ cursor_home/0, reverse_index/0, cursor_save/0, cursor_restore/0,
+ cursor_show/0, cursor_hide/0,
+ cursor_next_line/0, cursor_previous_line/0, cursor_horizontal_absolute/1,
+ cursor_vertical_absolute/1, cursor_horizontal_vertical/2, cursor_report_position/0]).
+-export([alternate_screen/0, alternate_screen_off/0,
+ scroll_forward/0, scroll_forward/1, scroll_backward/0, scroll_backward/1,
+ scroll_change_region/2]).
+-export([tab/0, tab_backward/0, tab_set/0, tab_clear/0, tab_clear_all/0]).
+-export([keypad_transmit_mode/0, keypad_transmit_mode_off/0]).
+-export([reset/0, device_report_attributes/0]).
+
+-import(lists, [concat/1]).
+
+-doc "The format string that can be passed to `format/3` and `fwrite/4`".
+-type format() :: [string() | vts()].
+
+-doc "Virtual terminal sequences that control the foreground (aka text) color.".
+-type foreground_color() :: black | blue | cyan | green | magenta | red | white | yellow |
+ light_black | light_blue | light_cyan | light_green |
+ light_magenta | light_red | light_white | light_yellow |
+ {color, 0..255} | {color, R :: 0..255, G :: 0..255, B :: 0..255} |
+ default_color.
+-doc "Virtual terminal sequences that control the background color.".
+-type background_color() :: black_background | blue_background | cyan_background |
+ green_background | magenta_background | red_background |
+ white_background | yellow_background | default_background |
+ light_black_background | light_blue_background |
+ light_cyan_background | light_green_background |
+ light_magenta_background | light_red_background |
+ light_white_background | light_yellow_background |
+ {color_background, 0..255} |
+ {color_background, R :: 0..255, G :: 0..255, B :: 0..255}.
+-doc "Virtual terminal sequences that control color.".
+-type color() :: foreground_color() | background_color() |
+ {modify_color, Index :: 0..255, R :: 0..255, G :: 0..255, B :: 0..255}.
+
+-doc "Virtual terminal sequences that control text style.".
+-type style() :: bold | bold_off | underline | underline_off | negative | negative_off.
+
+-type hyperlink_params() :: [{Key :: unicode:chardata(), Value :: unicode:chardata()}].
+
+-doc "Virtual terminal sequences that control whether emitted text shall be a hyper link or not.".
+-type hyperlink() :: {hyperlink, URL :: uri_string:uri_string(), Text :: unicode:chardata()} |
+ {hyperlink, URL :: uri_string:uri_string(), hyperlink_params(), Text :: unicode:chardata()} |
+ {hyperlink_start, URL :: uri_string:uri_string()} |
+ {hyperlink_start, URL :: uri_string:uri_string(), hyperlink_params()} |
+ hyperlink_reset.
+
+-doc "Virtual terminal sequences that control text formatting.".
+-type text_formatting() :: color() | style() | hyperlink().
+-doc "Virtual terminal sequences that can erase or owerwrite text.".
+-type text_modification() :: clear | erase_display |
+ insert_character | delete_character | erase_character |
+ insert_line | delete_line | erase_line.
+-doc "Virtual terminal sequences that works on text.".
+-type text() :: text_formatting() | text_modification() |
+ alternate_character_set_mode | alternate_character_set_mode_off.
+-doc "Virtual terminal sequences that controls the cursor.".
+-type cursor() ::
+ {cursor, Line :: non_neg_integer(), Column :: non_neg_integer()} |
+ cursor_down | cursor_up | cursor_backward | cursor_forward |
+ {cursor_down | cursor_backward | cursor_forward | cursor_up, N :: non_neg_integer()} |
+ cursor_home | reverse_index | cursor_save | cursor_restore |
+ cursor_show | cursor_hide |
+ cursor_next_line | cursor_previous_line | cursor_horizontal_absolute |
+ cursor_vertical_absolute | cursor_horizontal_vertical | cursor_report_position.
+-doc "Virtual terminal sequences that controls the screen.".
+-type window() :: alternate_screen | alternate_screen_off |
+ scroll_forward | scroll_backward | scroll_change_region.
+-doc "Virtual terminal sequences that works with tabs.".
+-type tab() :: tab | tab_backward | tab_set | tab_clear | tab_clear_all.
+-doc "Virtual terminal sequences for cursor input.".
+-type input() :: keypad_transmit_mode | keypad_transmit_mode_off |
+ kcursor_down | kcursor_up | kcursor_backward | kcursor_forward |
+ kcursor_home | kcursor_end.
+-doc "Virtual terminal sequences.".
+-type vts() :: text() | cursor() | window() | tab() | input() | reset | device_report_attributes.
+
+-type option() :: {reset, boolean()} | { enabled, boolean()} | io_lib:format_options().
+-type options() :: [option()].
+
+-export_type([vts/0]).
+
+-doc #{ equiv => tput(TermInfoCap, []) }.
+-doc #{ group => ~"Functions: terminfo" }.
+-spec tput(TermInfoCap :: string()) -> unicode:unicode_binary().
+tput(TermInfoCap) ->
+ tput(TermInfoCap, []).
+
+-doc """
+Returns the string representing the action taken by the given terminal capability.
+
+The names of the terminal capabilities can be found in the [terminfo](https://man7.org/linux/man-pages/man5/terminfo.5.html)
+documentation, or by calling `tinfo/0`. `tput/2` will use the terminfo definition
+associated with the `TERM` environment variable when the Erlang VM is started.
+It is not possible to change after startup.
+
+If the given capability is not defined in the terminfo database an `enotsup`
+error is generated, if the given capability is invalid a `badarg` error is
+generated.
+
+This function does not work on Windows and will always generate a `badarg`
+exception.
+
+Example:
+
+```erlang
+%% Set the foreground color to 3
+1> io_ansi:tput("setaf",[3]).
+<<"\e[33m">>
+%% Move the cursor up 2 spaces
+2> io_ansi:tput("cuu",[2]).
+<<"\e[2A">>
+%% Move the cursor down 1 space
+3> io_ansi:tput("cud1").
+<<"\n">>
+%% unsupported capability
+4> io_ansi:tput("slm").
+** exception error: {enotsup,"slm"}
+ in function io_ansi:tput/2
+%% unknown capability
+5> io_ansi:tput("foobar").
+** exception error: {einval,"foobar",[]}
+ in function io_ansi:tput/2
+```
+""".
+-doc #{ group => ~"Functions: terminfo" }.
+-spec tput(TermInfoCapName :: string(), Args :: [integer()]) ->
+ unicode:unicode_binary().
+tput(TermInfoCap, Args) ->
+ try prim_tty:tigetstr(TermInfoCap) of
+ {ok, TermInfoStr} ->
+ try prim_tty:tputs(TermInfoStr, Args) of
+ {ok, S} -> S
+ catch error:badarg ->
+ erlang:error({badarg, TermInfoCap, Args})
+ end;
+ false ->
+ erlang:error({enotsup, TermInfoCap})
+ catch error:badarg ->
+ erlang:error({einval, TermInfoCap, Args})
+ end.
+
+-doc """
+Returns the number representing a terminfo capability.
+
+The names of the terminal capabilities can be found in the [terminfo](https://man7.org/linux/man-pages/man5/terminfo.5.html)
+documentation, or by calling `tinfo/0`. `tigetnum/1` will use the terminfo
+definition associated with the `TERM` environment variable when the Erlang VM is
+started. It is not possible to change after startup.
+
+Returns `-1` if the capability is not available.
+
+Example:
+
+```erlang
+1> io_ansi:tigetnum("co").
+80
+2> io_ansi:tigetnum("foobar").
+-1
+```
+""".
+-doc #{ group => ~"Functions: terminfo" }.
+-spec tigetnum(TermInfoCapName :: string()) -> -1 | non_neg_integer().
+tigetnum(TermInfoCap) ->
+ prim_tty:tigetnum(TermInfoCap).
+
+-doc """
+Returns the true if the terminfo capability is available, otherwise false.
+
+The names of the terminal capabilities can be found in the [terminfo](https://man7.org/linux/man-pages/man5/terminfo.5.html)
+documentation, or by calling `tinfo/0`. `tigetflag/1` will use the terminfo
+definition associated with the `TERM` environment variable when the Erlang VM is
+started. It is not possible to change after startup.
+
+Example:
+
+```erlang
+1> io_ansi:tigetflag("xn").
+true
+2> io_ansi:tigetflag("foobar").
+false
+```
+""".
+-doc #{ group => ~"Functions: terminfo" }.
+-spec tigetflag(TermInfoCapName :: string()) -> boolean().
+tigetflag(TermInfoCap) ->
+ prim_tty:tigetflag(TermInfoCap).
+
+-doc """
+Returns information about all available terminfo capabilities. See
+the [terminfo](https://man7.org/linux/man-pages/man5/terminfo.5.html)
+documentation for details on each.
+
+`tinfo/0` will use the terminfo definition associated with the `TERM` environment
+variable when the Erlang VM is started. It is not possible to change after startup.
+
+When calling `tput/2`, `tigetnum/1` and `tigetflag/1` you should provide the `name`
+of the capability you want.
+
+Example:
+
+```erlang
+1> io_ansi:tinfo().
+#{ bool => [#{code => "xr",name => "OTxr",full_name => "return_does_clr_eol"} | ...],
+ str => [#{code => "bx",name => "box1",full_name => "box_chars_1"} | ...],
+ num => [#{code => "kn",name => "OTkn", full_name => "number_of_function_keys"} | ...]
+ }
+```
+""".
+-doc #{ group => ~"Functions: terminfo" }.
+-spec tinfo() -> #{ bool := [#{ code := string(), name := string(), full_name := string()}]}.
+tinfo() ->
+ prim_tty:tinfo().
+
+-define(FUNCTION(NAME),
+ NAME() -> Fun = lookup(NAME, []), unicode:characters_to_binary(Fun())).
+-define(FUNCTION(NAME, ARG1),
+ NAME(ARG1) -> Fun = lookup(NAME, [ARG1]), unicode:characters_to_binary(Fun(ARG1))).
+-define(FUNCTION(NAME, ARG1, ARG2),
+ NAME(ARG1, ARG2) -> Fun = lookup(NAME, [ARG1, ARG2]), unicode:characters_to_binary(Fun(ARG1, ARG2))).
+-define(FUNCTION(NAME, ARG1, ARG2, ARG3),
+ NAME(ARG1, ARG2, ARG3) -> Fun = lookup(NAME, [ARG1, ARG2, ARG3]), unicode:characters_to_binary(Fun(ARG1, ARG2, ARG3))).
+-define(FUNCTION(NAME, ARG1, ARG2, ARG3, ARG4),
+ NAME(ARG1, ARG2, ARG3, ARG4) -> Fun = lookup(NAME, [ARG1, ARG2, ARG3, ARG4]), unicode:characters_to_binary(Fun(ARG1, ARG2, ARG3, ARG4))).
+
+-define(SPEC(NAME),
+ -spec NAME() -> unicode:chardata()).
+-define(SPEC(NAME, ARG1),
+ -spec NAME(ARG1 :: integer()) -> unicode:chardata()).
+-define(SPEC(NAME, ARG1, ARG2),
+ -spec NAME(ARG1 :: integer(), ARG2 :: integer()) -> unicode:chardata()).
+
+-doc """
+Change foreground (aka text) color to black.
+
+Example:
+```erlang
+1> io_ansi:black().
+<<"\e[30m">>
+```
+""".
+?SPEC(black).
+?FUNCTION(black).
+
+-doc """
+Change foreground (aka text) color to red.
+
+Example:
+```erlang
+1> io_ansi:red().
+<<"\e[31m">>
+```
+""".
+?SPEC(red).
+?FUNCTION(red).
+
+-doc """
+Change foreground (aka text) color to green.
+
+Example:
+```erlang
+1> io_ansi:green().
+<<"\e[32m">>
+```
+""".
+?SPEC(green).
+?FUNCTION(green).
+
+-doc """
+Change foreground (aka text) color to yellow.
+
+Example:
+```erlang
+1> io_ansi:yellow().
+<<"\e[33m">>
+```
+""".
+?SPEC(yellow).
+?FUNCTION(yellow).
+
+-doc """
+Change foreground (aka text) color to blue.
+
+Example:
+```erlang
+1> io_ansi:blue().
+<<"\e[34m">>
+```
+""".
+?SPEC(blue).
+?FUNCTION(blue).
+
+-doc """
+Change foreground (aka text) color to magenta.
+
+Example:
+```erlang
+1> io_ansi:magenta().
+<<"\e[35m">>
+```
+""".
+?SPEC(magenta).
+?FUNCTION(magenta).
+
+-doc """
+Change foreground (aka text) color to cyan.
+
+Example:
+```erlang
+1> io_ansi:cyan().
+<<"\e[36m">>
+```
+""".
+?SPEC(cyan).
+?FUNCTION(cyan).
+
+-doc """
+Change foreground (aka text) color to white.
+
+Example:
+```erlang
+1> io_ansi:white().
+<<"\e[37m">>
+```
+""".
+?SPEC(white).
+?FUNCTION(white).
+
+-doc """
+Change foreground (aka text) color to index color. `Index` 0-15 are equivilant to
+the named colors in `t:foreground_color/0` in the order that they are listed.
+
+Example:
+```erlang
+1> io_ansi:color(5).
+<<"\e[35m">>
+2> io_ansi:color(80).
+<<"\e[38;5;80m">>
+```
+""".
+-spec color(Index :: 0..255 | 0..87) -> unicode:chardata().
+?FUNCTION(color, Index).
+
+-doc """
+Change foreground (aka text) color to RGB color.
+
+Example:
+```erlang
+1> io_ansi:color(255, 0, 0).
+<<"\e[38;2;255;0;0m">>
+```
+""".
+-spec color(0..255, 0..255, 0..255) -> unicode:chardata().
+?FUNCTION(color, Red, Green, Blue).
+
+-doc """
+Change foreground (aka text) color to the default color.
+
+Example:
+```erlang
+1> io_ansi:default_color().
+<<"\e[39m">>
+```
+""".
+?SPEC(default_color).
+?FUNCTION(default_color).
+
+-doc """
+Change background color to black.
+
+Example:
+```erlang
+1> io_ansi:black_background().
+<<"\e[40m">>
+```
+""".
+?SPEC(black_background).
+?FUNCTION(black_background).
+
+-doc """
+Change background color to red.
+
+Example:
+```erlang
+1> io_ansi:red_background().
+<<"\e[41m">>
+```
+""".
+?SPEC(red_background).
+?FUNCTION(red_background).
+
+-doc """
+Change background color to green.
+
+Example:
+```erlang
+1> io_ansi:green_background().
+<<"\e[42m">>
+```
+""".
+?SPEC(green_background).
+?FUNCTION(green_background).
+
+-doc """
+Change background color to yellow.
+
+Example:
+```erlang
+1> io_ansi:yellow_background().
+<<"\e[43m">>
+```
+""".
+?SPEC(yellow_background).
+?FUNCTION(yellow_background).
+
+-doc """
+Change background color to blue.
+
+Example:
+```erlang
+1> io_ansi:blue_background().
+<<"\e[44m">>
+```
+""".
+?SPEC(blue_background).
+?FUNCTION(blue_background).
+
+-doc """
+Change background color to magenta.
+
+Example:
+```erlang
+1> io_ansi:magenta_background().
+<<"\e[45m">>
+```
+""".
+?SPEC(magenta_background).
+?FUNCTION(magenta_background).
+
+-doc """
+Change background color to cyan.
+
+Example:
+```erlang
+1> io_ansi:cyan_background().
+<<"\e[46m">>
+```
+""".
+?SPEC(cyan_background).
+?FUNCTION(cyan_background).
+
+-doc """
+Change background color to white.
+
+Example:
+```erlang
+1> io_ansi:white_background().
+<<"\e[47m">>
+```
+""".
+?SPEC(white_background).
+?FUNCTION(white_background).
+
+-doc """
+Change background color to index color. `Index` 0-15 are equivilant to
+the named colors in `t:background_color/0` in the order that they are listed.
+
+Example:
+```erlang
+1> io_ansi:background(2).
+<<"\e[42m">>
+2> io_ansi:background(80).
+<<"\e[48;5;80m">>
+```
+""".
+-spec background(Index :: 0..255 | 0..87) -> unicode:chardata().
+?FUNCTION(background, Index).
+
+-doc """
+Change background color to RGB color.
+
+Example:
+```erlang
+1> io_ansi:background(255, 255, 0).
+<<"\e[48;2;255;255;0m">>
+```
+""".
+-spec background(0..255, 0..255, 0..255) -> unicode:chardata().
+?FUNCTION(background, Red, Green, Blue).
+
+-doc """
+Change background color to the default color.
+
+Example:
+```erlang
+1> io_ansi:default_background().
+<<"\e[49m">>
+```
+""".
+?SPEC(default_background).
+?FUNCTION(default_background).
+
+-doc """
+Change foreground (aka text) color to light black.
+
+Example:
+```erlang
+1> io_ansi:light_black().
+<<"\e[90m">>
+```
+""".
+?SPEC(light_black).
+?FUNCTION(light_black).
+-doc """
+Change foreground (aka text) color to light red.
+
+Example:
+```erlang
+1> io_ansi:light_red().
+<<"\e[91m">>
+```
+""".
+?SPEC(light_red).
+?FUNCTION(light_red).
+-doc """
+Change foreground (aka text) color to light green.
+
+Example:
+```erlang
+1> io_ansi:light_green().
+<<"\e[92m">>
+```
+""".
+?SPEC(light_green).
+?FUNCTION(light_green).
+-doc """
+Change foreground (aka text) color to light yellow.
+
+Example:
+```erlang
+1> io_ansi:light_yellow().
+<<"\e[93m">>
+```
+""".
+?SPEC(light_yellow).
+?FUNCTION(light_yellow).
+-doc """
+Change foreground (aka text) color to light magenta.
+
+Example:
+```erlang
+1> io_ansi:light_magenta().
+<<"\e[95m">>
+```
+""".
+?SPEC(light_magenta).
+?FUNCTION(light_magenta).
+-doc """
+Change foreground (aka text) color to light blue.
+
+Example:
+```erlang
+1> io_ansi:light_blue().
+<<"\e[94m">>
+```
+""".
+?SPEC(light_blue).
+?FUNCTION(light_blue).
+-doc """
+Change foreground (aka text) color to light cyan.
+
+Example:
+```erlang
+1> io_ansi:light_cyan().
+<<"\e[96m">>
+```
+""".
+?SPEC(light_cyan).
+?FUNCTION(light_cyan).
+-doc """
+Change foreground (aka text) color to light white.
+
+Example:
+```erlang
+1> io_ansi:light_white().
+<<"\e[97m">>
+```
+""".
+?SPEC(light_white).
+?FUNCTION(light_white).
+
+-doc """
+Change background color to light black.
+
+Example:
+```erlang
+1> io_ansi:light_black_background().
+<<"\e[100m">>
+```
+""".
+?SPEC(light_black_background).
+?FUNCTION(light_black_background).
+-doc """
+Change background color to light red.
+
+Example:
+```erlang
+1> io_ansi:light_red_background().
+<<"\e[101m">>
+```
+""".
+?SPEC(light_red_background).
+?FUNCTION(light_red_background).
+-doc """
+Change background color to light green.
+
+Example:
+```erlang
+1> io_ansi:light_green_background().
+<<"\e[102m">>
+```
+""".
+?SPEC(light_green_background).
+?FUNCTION(light_green_background).
+-doc """
+Change background color to light yellow.
+
+Example:
+```erlang
+1> io_ansi:light_yellow_background().
+<<"\e[103m">>
+```
+""".
+?SPEC(light_yellow_background).
+?FUNCTION(light_yellow_background).
+-doc """
+Change background color to light magenta.
+
+Example:
+```erlang
+1> io_ansi:light_magenta_background().
+<<"\e[105m">>
+```
+""".
+?SPEC(light_magenta_background).
+?FUNCTION(light_magenta_background).
+-doc """
+Change background color to light blue.
+
+Example:
+```erlang
+1> io_ansi:light_blue_background().
+<<"\e[104m">>
+```
+""".
+?SPEC(light_blue_background).
+?FUNCTION(light_blue_background).
+-doc """
+Change background color to light cyan.
+
+Example:
+```erlang
+1> io_ansi:light_cyan_background().
+<<"\e[106m">>
+```
+""".
+?SPEC(light_cyan_background).
+?FUNCTION(light_cyan_background).
+
+-doc """
+Change background color to light white.
+
+Example:
+```erlang
+1> io_ansi:light_white_background().
+<<"\e[107m">>
+```
+""".
+?SPEC(light_white_background).
+?FUNCTION(light_white_background).
+
+-doc """
+Modify the color referenced by `Index` to be RGB.
+
+Calling this function for `Index` 0-15 will change the color of the named colors
+in `t:foreground_color/0` and `t:background_color/0`.
+
+Example:
+```erlang
+1> io_ansi:modify_color(1, 255, 100, 0).
+<<"\e]4;1;rgb:41/19/00\e\\">>
+```
+""".
+-spec modify_color(Index :: 0..255, R :: 0..255, G :: 0..255, B :: 0..255) -> unicode:chardata().
+?FUNCTION(modify_color, Index, R, G, B).
+
+-doc """
+Turn on bold text style.
+
+Example:
+```erlang
+1> io_ansi:bold().
+<<"\e[1m">>
+```
+""".
+?SPEC(bold).
+?FUNCTION(bold).
+
+-doc """
+Turn off bold text style.
+
+Example:
+```erlang
+1> io_ansi:bold_off().
+<<"\e[22m">>
+```
+""".
+?SPEC(bold_off).
+?FUNCTION(bold_off).
+
+-doc """
+Turn on underline text style.
+
+Example:
+```erlang
+1> io_ansi:underline().
+<<"\e[4m">>
+```
+""".
+?SPEC(underline).
+?FUNCTION(underline).
+
+-doc """
+Turn off underline text style.
+
+Example:
+```erlang
+1> io_ansi:underline_off().
+<<"\e[24m">>
+```
+""".
+?SPEC(underline_off).
+?FUNCTION(underline_off).
+
+-doc """
+Turn on negative text style.
+
+Example:
+```erlang
+1> io_ansi:negative().
+<<"\e[7m">>
+```
+""".
+?SPEC(negative).
+?FUNCTION(negative).
+
+-doc """
+Turn off negative text style.
+
+Example:
+```erlang
+1> io_ansi:negative_off().
+<<"\e[27m">>
+```
+""".
+?SPEC(negative_off).
+?FUNCTION(negative_off).
+
+-doc """
+Clear screen and set cursor to home.
+
+Example:
+```erlang
+1> io_ansi:clear().
+<<"\e[H\e[2J">>
+```
+""".
+?SPEC(clear).
+?FUNCTION(clear).
+-doc """
+Clear screen after cursor.
+
+Example:
+```erlang
+1> io_ansi:erase_display().
+<<"\e[J">>
+```
+""".
+?SPEC(erase_display).
+?FUNCTION(erase_display).
+
+-doc """
+Insert `Chars` at cursor.
+
+Example:
+```erlang
+1> io_ansi:insert_character(3).
+<<"\e[3@">>
+```
+""".
+?SPEC(insert_character, Chars).
+?FUNCTION(insert_character, Chars).
+
+-doc """
+Delete 1 character at cursor.
+
+Example:
+```erlang
+1> io_ansi:delete_character().
+<<"\e[P">>
+```
+""".
+?SPEC(delete_character).
+?FUNCTION(delete_character).
+
+-doc """
+Delete `Chars` characters at cursor by shifting the text `Chars` characters to the left.
+
+Example:
+```erlang
+1> io_ansi:delete_character(2).
+<<"\e[2P">>
+```
+""".
+?SPEC(delete_character, Chars).
+?FUNCTION(delete_character, Chars).
+
+-doc """
+Erase `Chars` characters at cursor by making `Chars` characters before the cursor blank.
+
+Example:
+```erlang
+1> io_ansi:erase_character(4).
+<<"\e[4X">>
+```
+""".
+?SPEC(erase_character, Chars).
+?FUNCTION(erase_character, Chars).
+
+-doc """
+Insert 1 line at cursor.
+
+Example:
+```erlang
+1> io_ansi:insert_line().
+<<"\e[L">>
+```
+""".
+?SPEC(insert_line).
+?FUNCTION(insert_line).
+
+-doc """
+Insert `Lines` lines at cursor.
+
+Example:
+```erlang
+1> io_ansi:insert_line(2).
+<<"\e[2L">>
+```
+""".
+?SPEC(insert_line, Lines).
+?FUNCTION(insert_line, Lines).
+
+-doc """
+Delete 1 line at cursor.
+
+Example:
+```erlang
+1> io_ansi:delete_line().
+<<"\e[M">>
+```
+""".
+?SPEC(delete_line).
+?FUNCTION(delete_line).
+
+-doc """
+Delete `Lines` lines at cursor.
+
+Example:
+```erlang
+1> io_ansi:delete_line(3).
+<<"\e[3M">>
+```
+""".
+?SPEC(delete_line, Lines).
+?FUNCTION(delete_line, Lines).
+
+-doc """
+Erase line at cursor.
+
+Example:
+```erlang
+1> io_ansi:erase_line().
+<<"\e[K">>
+```
+""".
+?SPEC(erase_line).
+?FUNCTION(erase_line).
+
+-doc """
+Enable the alternate characters set mode
+
+Example:
+```erlang
+1> io_ansi:alternate_character_set_mode().
+<<"\e(0">>
+2> io_ansi:fwrite(["%%", alternate_character_set_mode, " tqqu\n"]).
+%% ├──┤
+ok
+```
+""".
+?SPEC(alternate_character_set_mode).
+?FUNCTION(alternate_character_set_mode).
+
+-doc """
+Disable the alternate characters set mode
+
+Example:
+```erlang
+1> io_ansi:alternate_character_set_mode_off().
+<<"\e(B">>
+```
+""".
+?SPEC(alternate_character_set_mode_off).
+?FUNCTION(alternate_character_set_mode_off).
+
+-doc """
+Move the cursor to the given position. Position 0,0 is at the top left of the
+terminal.
+
+Example:
+```erlang
+1> io_ansi:cursor(5, 10).
+<<"\e[6;11H">>
+```
+""".
+?SPEC(cursor, Line, Column).
+?FUNCTION(cursor, Line, Column).
+
+-doc """
+Move the cursor up one line.
+
+Example:
+```erlang
+1> io_ansi:cursor_up().
+<<"\e[A">>
+```
+""".
+?SPEC(cursor_up).
+?FUNCTION(cursor_up).
+
+-doc """
+Move the cursor up `N` lines.
+
+Example:
+```erlang
+1> io_ansi:cursor_up(42).
+<<"\e[42A">>
+```
+""".
+?SPEC(cursor_up, N).
+?FUNCTION(cursor_up, N).
+
+-doc """
+Move the cursor down one line.
+
+Example:
+```erlang
+1> io_ansi:cursor_down().
+<<"\n">>
+```
+""".
+?SPEC(cursor_down).
+?FUNCTION(cursor_down).
+
+-doc """
+Move the cursor down `N` lines.
+
+Example:
+```erlang
+1> io_ansi:cursor_down(42).
+<<"\e[42B">>
+```
+""".
+?SPEC(cursor_down, N).
+?FUNCTION(cursor_down, N).
+
+-doc """
+Move the cursor forward one character.
+
+Example:
+```erlang
+1> io_ansi:cursor_forward().
+<<"\e[C">>
+```
+""".
+?SPEC(cursor_forward).
+?FUNCTION(cursor_forward).
+-doc """
+Move the cursor forward `N` characters.
+
+Example:
+```erlang
+1> io_ansi:cursor_forward().
+<<"\e[C">>
+```
+""".
+?SPEC(cursor_forward, N).
+?FUNCTION(cursor_forward, N).
+
+-doc """
+Move the cursor backward `N` characters.
+
+Example:
+```erlang
+1> io_ansi:cursor_backward().
+<<"\b">>
+```
+""".
+?SPEC(cursor_backward).
+?FUNCTION(cursor_backward).
+
+-doc """
+Move the cursor backward `N` characters.
+
+Example:
+```erlang
+1> io_ansi:cursor_backward(42).
+<<"\e[42D">>
+```
+""".
+?SPEC(cursor_backward, N).
+?FUNCTION(cursor_backward, N).
+
+-doc """
+Move the cursor to the start of the current line.
+
+Example:
+```erlang
+1> io_ansi:cursor_home().
+<<"\e[H">>
+```
+""".
+?SPEC(cursor_home).
+?FUNCTION(cursor_home).
+
+-doc """
+Move the cursor up one line, but keeps the cursor on the same location on the
+screen by scrolling the screen down.
+
+Example:
+```erlang
+1> io_ansi:reverse_index().
+<<"\eM">>
+```
+""".
+?SPEC(reverse_index).
+?FUNCTION(reverse_index).
+
+-doc """
+Save the current cursor position.
+
+Example:
+```erlang
+1> io_ansi:cursor_save().
+<<"\e7">>
+```
+""".
+?SPEC(cursor_save).
+?FUNCTION(cursor_save).
+
+-doc """
+Restore a saved cursor position.
+
+Example:
+```erlang
+1> io_ansi:cursor_restore().
+<<"\e8">>
+```
+""".
+?SPEC(cursor_restore).
+?FUNCTION(cursor_restore).
+
+-doc """
+Show the cursor.
+
+Example:
+```erlang
+1> io_ansi:cursor_show().
+<<"\e[?12;25h">>
+```
+""".
+?SPEC(cursor_show).
+?FUNCTION(cursor_show).
+
+-doc """
+Hide the cursor.
+
+Example:
+```erlang
+1> io_ansi:cursor_hide().
+<<"\e[?25l">>
+```
+""".
+?SPEC(cursor_hide).
+?FUNCTION(cursor_hide).
+
+-doc """
+Move the cursor down one line and then returns it to home.
+
+Example:
+```erlang
+1> io_ansi:cursor_next_line().
+<<"\eE">>
+```
+""".
+?SPEC(cursor_next_line).
+?FUNCTION(cursor_next_line).
+
+-doc """
+Move the cursor up one line and then returns it to home.
+
+Example:
+```erlang
+1> io_ansi:cursor_previous_line().
+<<"\e[F">>
+```
+""".
+?SPEC(cursor_previous_line).
+?FUNCTION(cursor_previous_line).
+
+-doc """
+Move the cursor to column `X`.
+
+Example:
+```erlang
+1> io_ansi:cursor_horizontal_absolute(10).
+<<"\e[11G">>
+```
+""".
+?SPEC(cursor_horizontal_absolute, X).
+?FUNCTION(cursor_horizontal_absolute, X).
+
+-doc """
+Move the cursor to line `X`.
+
+Example:
+```erlang
+1> io_ansi:cursor_vertical_absolute(20).
+<<"\e[21d">>
+```
+""".
+?SPEC(cursor_vertical_absolute, X).
+?FUNCTION(cursor_vertical_absolute, X).
+
+-doc """
+Move the cursor to line `X` and column `Y`.
+
+Example:
+```erlang
+1> io_ansi:cursor_horizontal_vertical(10, 20).
+<<"\e[10;20f">>
+```
+""".
+?SPEC(cursor_horizontal_vertical, X, Y).
+?FUNCTION(cursor_horizontal_vertical, X, Y).
+
+-doc """
+Instruct the terminal to report the current cursor position.
+
+Examples:
+
+```erlang
+1> io_ansi:cursor_report_position().
+~"\e[6n"
+```
+
+```bash
+## Enter noshell-raw mode and request curson location and then print
+## the reply to stdout.
+$ erl -noshell -eval 'shell:start_interactive({noshell,raw}),
+ io_ansi:fwrite([cursor_report_position]),
+ io:format("~p",[io:get_chars("",20)])' -s init stop
+"\e[58;1R"
+```
+""".
+?SPEC(cursor_report_position).
+?FUNCTION(cursor_report_position).
+
+-doc """
+Activate the alternate screen.
+
+Example:
+```erlang
+1> io_ansi:alternate_screen().
+<<"\e[?1049h\e[22;0;0t">>
+```
+""".
+?SPEC(alternate_screen).
+?FUNCTION(alternate_screen).
+
+-doc """
+Deactivate the alternate screen.
+
+Example:
+```erlang
+1> io_ansi:alternate_screen_off().
+<<"\e[?1049l\e[23;0;0t">>
+```
+""".
+?SPEC(alternate_screen_off).
+?FUNCTION(alternate_screen_off).
+
+-doc """
+Scroll the screen forward 1 step.
+
+Example:
+```erlang
+1> io_ansi:scroll_forward().
+<<"\e[1S">>
+```
+""".
+?SPEC(scroll_forward).
+?FUNCTION(scroll_forward).
+
+-doc """
+Scroll the screen forward `N` step.
+
+Example:
+```erlang
+1> io_ansi:scroll_forward(42).
+<<"\e[42S">>
+```
+""".
+?SPEC(scroll_forward, N).
+?FUNCTION(scroll_forward, N).
+
+-doc """
+Scroll the screen backward 1 step.
+
+Example:
+```erlang
+1> io_ansi:scroll_backward().
+<<"\e[1T">>
+```
+""".
+?SPEC(scroll_backward).
+?FUNCTION(scroll_backward).
+
+-doc """
+Scroll the screen backward `N` step.
+
+Example:
+```erlang
+1> io_ansi:scroll_backward(42).
+<<"\e[42T">>
+```
+""".
+?SPEC(scroll_backward, Steps).
+?FUNCTION(scroll_backward, Steps).
+
+-doc """
+Change the scolling region to be from `Line1` to `Line2`.
+
+Example:
+```erlang
+1> io_ansi:scroll_change_region(10, 20).
+<<"\e[11;21r">>
+```
+""".
+?SPEC(scroll_change_region, Line1, Line2).
+?FUNCTION(scroll_change_region, Line1, Line2).
+
+-doc """
+Move cursor one tab forward.
+
+Example:
+```erlang
+1> io_ansi:tab().
+<<"\t">>
+```
+""".
+?SPEC(tab).
+?FUNCTION(tab).
+
+-doc """
+Move cursor one tab backward.
+
+Example:
+```erlang
+1> io_ansi:tab_backward().
+<<"\e[Z">>
+```
+""".
+?SPEC(tab_backward).
+?FUNCTION(tab_backward).
+
+-doc """
+Set a new tab location at the current cursor location.
+
+Example:
+```erlang
+1> io_ansi:tab_set().
+<<"\eH">>
+```
+""".
+?SPEC(tab_set).
+?FUNCTION(tab_set).
+
+-doc """
+Clear any tab location at the current cursor location.
+
+Example:
+```erlang
+1> io_ansi:tab_clear().
+<<"\e[0g">>
+```
+""".
+?SPEC(tab_clear).
+?FUNCTION(tab_clear).
+-doc """
+Clear all tab locations.
+
+Example:
+```erlang
+1> io_ansi:tab_clear_all().
+<<"\e[3g">>
+```
+""".
+?SPEC(tab_clear_all).
+?FUNCTION(tab_clear_all).
+
+-doc """
+Enable keypad transmit mode.
+
+Example:
+```erlang
+1> io_ansi:keypad_transmit_mode().
+<<"\e[?1h\e=">>
+```
+""".
+?SPEC(keypad_transmit_mode).
+?FUNCTION(keypad_transmit_mode).
+
+-doc """
+Disable keypad transmit mode.
+
+Example:
+```erlang
+1> io_ansi:keypad_transmit_mode_off().
+<<"\e[?1l\e>">>
+```
+""".
+?SPEC(keypad_transmit_mode_off).
+?FUNCTION(keypad_transmit_mode_off).
+
+-doc """
+Reset virtual terminal sequences to their original state.
+
+This only resets the things supported by the loaded terminfo database,
+which means that OSCs such as `hyperlink_start/2` are not reset but have
+to be reset by emitting `hyperlink_reset/0`.
+
+Example:
+```erlang
+1> io_ansi:reset().
+<<"\e(B\e[m">>
+```
+""".
+?SPEC(reset).
+?FUNCTION(reset).
+
+-doc """
+Tell the terminal emulator to report its device attributes.
+
+Examples:
+
+```erlang
+1> io_ansi:device_report_attributes().
+<<"\e[0c">>
+```
+
+```sh
+## Enter noshell-raw mode and request device attributes and then print
+## the reply to stdout.
+$ erl -noshell -eval 'shell:start_interactive({noshell,raw}),
+ io_ansi:fwrite([device_report_attributes]),
+ io:format("~p",[io:get_chars("",20)])' -s init stop
+"\e[?65;1;9c"
+```
+""".
+?SPEC(device_report_attributes).
+?FUNCTION(device_report_attributes).
+
+%% See https://gcc.gnu.org/cgit/gcc/commit/?id=458c8d6459c4005fc9886b6e25d168a6535ac415 for
+%% details on how to check whether we can use terminal URLs.
+%% The specification for how to ANSI URLs work is here: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
+
+-doc #{ equiv => hyperlink_start(URL, []) }.
+-spec hyperlink_start(uri_string:uri_string()) -> unicode:chardata().
+?FUNCTION(hyperlink_start, URL).
+
+-doc """
+Start a hyperlink pointing to the given `URL` using `Params`.
+
+The hyperlink can be any type of URL, but typically it would be a file or http
+URL.
+
+Example:
+```erlang
+1> io_ansi:hyperlink_start("https://erlang.org").
+<<"\e]8;https://erlang.org;\e\\">>
+2> io_ansi:format([{hyperlink_start, "file://tmp/debug.log"},"debug log",hyperlink_reset]).
+~"\e]8;file://tmp/debug.log;\e\\debug log\e]8;;\e\\\e(B\e[m"
+```
+
+See [Hyperlinks (a.k.a. HTML-like anchors) in terminal emulators](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda)
+for more details on limitations and usage of terminal hyperlinks.
+""".
+-spec hyperlink_start(uri_string:uri_string(),
+ [{Key :: unicode:chardata(), Value :: unicode:chardata()}]) -> unicode:chardata().
+?FUNCTION(hyperlink_start, URL, Params).
+-doc """
+Stop emitting a hyperlink.
+
+Example:
+```erlang
+1> io_ansi:hyperlink_reset().
+<<"\e]8;;\e\\">>
+```
+""".
+?SPEC(hyperlink_reset).
+?FUNCTION(hyperlink_reset).
+
+-doc """
+Check if `t:io:user/0` can interpret ANSI escape sequences.
+
+Example:
+```erlang
+1> io_ansi:enabled().
+true
+```
+""".
+-spec enabled() -> boolean().
+enabled() ->
+ enabled(user).
+
+-doc """
+Check if `Device` can interpret ANSI escape sequences.
+
+This is done by checking if `Device` represents a terminal and if the `TERM`
+environment variable is set to a terminal type that supports virtual terminal
+sequences.
+
+Example:
+```erlang
+1> io_ansi:enabled(standard_error).
+true
+2> {ok, File} = file:open("tmp",[write]), io_ansi:enabled(File).
+false
+```
+""".
+-spec enabled(io:device()) -> boolean().
+enabled(Device) ->
+ IsTerminal =
+ case whereis(user_drv) =/= self() of
+ true ->
+ case io:getopts(Device) of
+ {error, _} -> false;
+ Opts ->
+ proplists:get_value(terminal, Opts, false)
+ end;
+ false ->
+ %% if called from within the user_drv process, we only check if
+ %% stdin/stdout are TTYs in order to avoid deadlocks
+ prim_tty:isatty(stdin) =:= true andalso prim_tty:isatty(stdout) =:= true
+ end,
+ IsSmartTerminal =
+ case os:type() of
+ {win32, _} -> true;
+ _ ->
+ try
+ prim_tty:tigetstr("sgr0") =/= false
+ catch error:badarg ->
+ false
+ end
+ end,
+ IsTerminal andalso IsSmartTerminal.
+
+-doc """
+Scan the string for virtial terminal sequences.
+
+The recognized VTSs will be converted into the corresponding `t:vts/0`.
+
+If you intend to parse arrow keys it is recommended that you first set the terminal in
+application mode by using `io_ansi:format(standard_out, [keypad_transmit_mode], [], [])`.
+This will make it easier for io_ansi to correctly detect arrow keys.
+
+Any unrecognized [control sequence introducers](https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands),
+will be placed in a tuple tagged with `csi`.
+
+Example:
+
+```erlang
+1> io_ansi:scan("\eOA").
+[kcursor_up]
+2> io_ansi:scan("\eOB").
+[kcursor_down]
+3> io_ansi:scan(io_ansi:format([bold, "text"])).
+[bold, ~"text", reset]
+4> io_ansi:scan(io_ansi:format([{cursor, 0, 0}])).
+[{csi, ~"\e[1;1H"}, reset]
+```
+
+""".
+-spec scan(unicode:chardata()) -> [unicode:unicode_binary() | vts() | {csi, unicode:unicode_binary()}].
+scan(Data) ->
+ scan_binary(unicode:characters_to_binary(Data), <<>>, []).
+
+scan_binary(<<CSI/utf8, R/binary>> = Data, Bin, Acc) when CSI =:= $\e; CSI =:= 155 ->
+ case lookup_vts(Data) of
+ undefined ->
+ case re:run(Data, prim_tty:ansi_regexp(), [unicode]) of
+ {match, [{0, N}]} ->
+ <<Ansi:N/binary, AnsiRest/binary>> = Data,
+ scan_binary(AnsiRest, <<>>, [{csi, Ansi}, Bin | Acc]);
+ nomatch ->
+ scan_binary(R, <<Bin/binary, CSI/utf8>>, Acc)
+ end;
+ {Code, _, Rest} ->
+ scan_binary(Rest, <<>>, [Code, Bin | Acc])
+ end;
+scan_binary(<<C/utf8, R/binary>>, Bin, Acc) ->
+ scan_binary(R, <<Bin/binary, C/utf8>>, Acc);
+scan_binary(<<>>, Bin, Acc) ->
+ [NonEmpty || NonEmpty <- lists:reverse([Bin | Acc]), NonEmpty =/= <<>>].
+
+lookup_vts(Data) ->
+ try
+ [case Data of
+ <<Value:(byte_size(Value))/binary, Rest/binary>> ->
+ throw({Key, Value, Rest});
+ _ ->
+ ok
+ end || Key := Values <- get_vts_mappings(),
+ Value <- Values],
+ undefined
+ catch throw:KeyValueRest ->
+ KeyValueRest
+ end.
+
+-doc #{ equiv => format(Format, []) }.
+-spec format(format()) -> unicode:unicode_binary().
+format(Format) ->
+ format(Format, []).
+
+-doc #{ equiv => format(Format, [], []) }.
+-spec format(format(), Data :: [term()]) -> unicode:unicode_binary().
+format(Format, Data) ->
+ format(Format, Data, []).
+
+-doc """
+Returns a character list that represents `Data` formatted in accordance with
+`Format`.
+
+This function works just as `io_lib:bformat/2`, except that it also allows
+atoms and tuples represeting virtual terminal sequences as part of the
+`Format` string.
+
+Calling `format/3` will always emit a `reset/0` VTS at the end of the returned
+string. To not emit this, set the `reset` option to `false`.
+
+To force enabling or disabling of emitting VTSs set the `enabled` option to
+`true` or `false`.
+
+Example:
+
+```erlang
+1> io_ansi:format([blue, underline, "Hello world"]).
+~"\e[34m\e[4mHello world\e(B\e[m"
+2> io_ansi:format([blue, underline, "Hello ~p"],[world]).
+~"\e[34m\e[4mHello world\e(B\e[m"
+3> io_ansi:format([blue, underline, "Hello ~p"],[world],[{reset,false}]).
+~"\e[34m\e[4mHello world"
+4> io_ansi:format([blue, underline, "Hello ~p"],[world],[{enabled,false}]).
+~"Hello world"
+5> io_ansi:format([invalid_code, "Hello world"]).
+** exception error: {invalid_code,invalid_code}
+ in function io_ansi:format_internal/3
+```
+
+For a detailed description of the available formatting options, see `io:fwrite/3`.
+""".
+-spec format(format(), Data :: [term()], options()) -> unicode:unicode_binary().
+format(Format, Data, Options) ->
+ format_internal(Format, Data, Options).
+
+format_internal(Format, Data, Options) ->
+ UseAnsi = case proplists:get_value(enabled, Options) of
+ undefined -> enabled();
+ Enabled -> Enabled
+ end,
+ %% Only to be used by fwrite
+ FormatOnly = proplists:get_value(format_only, Options, false),
+ AppendReset = [reset || proplists:get_value(reset, Options, true)],
+ Mappings = get_mappings(),
+ try lists:foldl(
+ fun(Ansi, {Acc, Args}) when is_atom(Ansi) orelse is_tuple(Ansi), FormatOnly ->
+ {[Ansi | Acc], Args};
+ (Ansi, {Acc, Args}) when is_atom(Ansi), UseAnsi ->
+ AnsiFun = lookup(Mappings, Ansi, []),
+ {[AnsiFun() | Acc], Args};
+ (Ansi, {Acc, Args}) when is_tuple(Ansi), UseAnsi ->
+ [AnsiCode | AnsiArgs] = tuple_to_list(Ansi),
+ AnsiFun = lookup(Mappings, AnsiCode, AnsiArgs),
+ {[apply(AnsiFun, AnsiArgs) | Acc], Args};
+ (Ansi, {Acc, Args}) when is_atom(Ansi); is_tuple(Ansi) ->
+ {Acc, Args};
+ (Fmt, {Acc, Args}) ->
+ {Scanned, Rest} = io_lib_format:scan(Fmt, Args),
+ {[io_lib_format:build_bin(Scanned) | Acc], Rest}
+ end, {[], Data}, group([Format,AppendReset])) of
+ {Scanned, []} ->
+ if FormatOnly ->
+ lists:flatten(lists:reverse(Scanned));
+ not FormatOnly ->
+ unicode:characters_to_binary(lists:reverse(Scanned))
+ end;
+ _ ->
+ erlang:error(badarg, [Format, Data, Options])
+ catch throw:{invalid_code, Code, []} ->
+ erlang:error({invalid_code, Code});
+ throw:{invalid_code, Code, Args} ->
+ erlang:error({invalid_code, {Code, Args}});
+ E:R:ST ->
+ erlang:raise(E,R,ST)
+ %% erlang:error(badarg, [Format, Data, Options])
+ end.
+
+-doc #{ equiv => fwrite(standard_io, Format, [], []) }.
+-spec fwrite(Format :: format()) -> ok.
+fwrite(Format) ->
+ fwrite(standard_io, Format, [], []).
+
+-doc #{ equiv => fwrite(standard_io, Format, Data, []) }.
+-spec fwrite(Format :: format(), [term()]) -> ok.
+fwrite(Format, Data) ->
+ fwrite(standard_io, Format, Data, []).
+
+-doc #{ equiv => fwrite(standard_io, Format, Data, Options) }.
+-spec fwrite(Format :: format(), [term()], options()) -> ok.
+fwrite(Format, Data, Options) ->
+ fwrite(standard_io, Format, Data, Options).
+
+-doc """
+Writes the items in `Data` on the [`IoDevice`](`t:io:device/0`) in accordance with `Format`.
+
+This function works just as `io:fwrite/2`, except that it also allows atoms and
+tuples representing virtual terminal sequences (VTS) as part of the `Format` string.
+
+See `format/3` for details on how the different `Options` can be used.
+
+Example:
+
+```erlang
+1> io_ansi:fwrite([blue, "%% Hello world\n"]).
+%% Hello world
+ok
+2> io_ansi:fwrite([underline, "%% Hello ~p\n"], [world]).
+%% Hello world
+ok
+3> io_ansi:fwrite([invalid_code, "%% Hello ~p\n"], [world]).
+** exception error: {error,{put_ansi,unicode,invalid_code}}
+ in function io_ansi:fwrite/4
+ called as io_ansi:fwrite(standard_io,[invalid_code,"%% Hello ~p\n"],[world],[])
+```
+
+The decision what each VTS should be converted to is done by the destination I/O
+device. This means that if the I/O device is on a remote node, the terminfo
+database loaded into that remote node will be used.
+
+All VTSs are stripped if the target I/O device does not support handling VTSs,
+either because it is not implemented by the device (for example if the device
+is a `t:file:io_server/0`) or if the device does not support a certain VTS.
+If you want to force usage of VTSs you can pass `{enabled, true}` and that will
+use the local defintions to translate.
+""".
+-spec fwrite(IODevice :: io:device(), Format :: format(), [term()], options()) -> ok.
+fwrite(Device, Format, Data, Options) ->
+ Ref = make_ref(),
+ try
+ lists:foldl(
+ fun F(Chars, ok) when is_binary(Chars) ->
+ io:request(Device, {put_chars, unicode, Chars});
+ F(Ansi, ok) when is_atom(Ansi); is_tuple(Ansi) ->
+ case io:request(Device,{put_ansi, Options, Ansi}) of
+ {error, request} ->
+ %% The IO server did not support printing ansi.
+ case proplists:get_value(enabled, Options, Ref) of
+ true ->
+ %% If ansi is forced by the ansi option,
+ %% we format using the local ansi definition
+ %% and send as characters
+ F(format([Ansi], [], Options), ok);
+ Ref ->
+ %% We drop the ansi codes
+ ok
+ end;
+ Else -> Else
+ end;
+ F(_Data, Error) ->
+ throw({Ref, Error})
+ end, ok, format_internal(Format, Data, [{format_only, true} | Options]))
+ catch {Ref, Error} ->
+ erlang:error(Error, [Device, Format, Data, Options])
+ end.
+
+%% This function takes a t:format and returns a t:format where all characters
+%% are flattened and grouped. That is: group(["aa","bb",red,["cc"], "dd"]) -> group(["aabb",red,"ccdd"])
+group(Fmt) ->
+ group(lists:flatten(Fmt), []).
+group([Ansi | T], []) when is_atom(Ansi); is_tuple(Ansi) ->
+ [Ansi | group(T, [])];
+group([Ansi | T], Acc) when is_atom(Ansi); is_tuple(Ansi) ->
+ [lists:reverse(Acc), Ansi | group(T, [])];
+group([C | T], Acc) ->
+ group(T, [C | Acc]);
+group([], []) ->
+ [];
+group([], Acc) ->
+ [lists:reverse(Acc)].
+
+sgr(Args) ->
+ ["\e[",lists:join($;,Args),"m"].
+
+%% Lookup a fun for a vts
+lookup(Key, Args) ->
+ lookup(get_mappings(), Key, Args).
+lookup(Mappings, Key, Args) ->
+ case maps:get(length(Args), maps:get(Key, Mappings, #{}), undefined) of
+ #{ terminfo := TermInfoFun } -> TermInfoFun;
+ #{ ansi := AnsiFun } -> AnsiFun;
+ undefined -> throw({invalid_code, Key, Args})
+ end.
+
+get_mappings() ->
+ case persistent_term:get(?MODULE, undefined) of
+ undefined ->
+ persistent_term:put(?MODULE, init_mappings()),
+ get_mappings();
+ Value -> Value
+ end.
+
+%% Take the description in default_mappings and munge it so that
+%% we can look them up quickly.
+-spec init_mappings() -> #{ vts() => #{ arity() => function() } }.
+init_mappings() ->
+ maps:map(
+ fun
+ Map(Key, Mapping) when not is_list(Mapping) ->
+ Map(Key, [Mapping]);
+ Map(_Key, Mappings) when is_list(Mappings) ->
+ maps:from_list(
+ lists:map(
+ fun
+ Fun({TermInfoCap, AnsiString}) when is_list(AnsiString) ->
+ Fun({TermInfoCap, unicode:characters_to_binary(AnsiString)});
+ Fun({undefined, AnsiFun}) when is_function(AnsiFun) ->
+ {arity, Arity} = erlang:fun_info(AnsiFun, arity),
+ {Arity, #{ ansi => AnsiFun }};
+ Fun({undefined, AnsiString}) when not is_function(AnsiString) ->
+ Fun({undefined, fun() -> AnsiString end});
+ Fun({TermInfoCap, AnsiFun}) when is_function(AnsiFun) ->
+ try prim_tty:tigetstr(TermInfoCap) of
+ {ok, TermInfoStr} ->
+ {arity, Arity} = erlang:fun_info(AnsiFun, arity),
+ TermInfoFun =
+ case Arity of
+ 0 -> fun() -> {ok, S} = prim_tty:tputs(TermInfoStr, []), S end;
+ 1 -> fun(A) -> {ok, S} = prim_tty:tputs(TermInfoStr, [A]), S end;
+ 2 -> fun(A, B) -> {ok, S} = prim_tty:tputs(TermInfoStr, [A, B]), S end;
+ 3 -> fun(A, B, C) -> {ok, S} = prim_tty:tputs(TermInfoStr, [A, B, C]), S end;
+ 4 -> fun(A, B, C, D) -> {ok, S} = prim_tty:tputs(TermInfoStr, [A, B, C, D]), S end;
+ 5 -> fun(A, B, C, D, E) -> {ok, S} = prim_tty:tputs(TermInfoStr, [A, B , C, D, E]), S end;
+ 6 -> fun(A, B, C, D, E, F) -> {ok, S} = prim_tty:tputs(TermInfoStr, [A, B , C, D, E, F]), S end;
+ 7 -> fun(A, B, C, D, E, F, G) -> {ok, S} = prim_tty:tputs(TermInfoStr, [A, B , C, D, E, F, G]), S end;
+ 8 -> fun(A, B, C, D, E, F, G, H) -> {ok, S} = prim_tty:tputs(TermInfoStr, [A, B , C, D, E, F, G, H]), S end;
+ 9 -> fun(A, B, C, D, E, F, G, H, I) -> {ok, S} = prim_tty:tputs(TermInfoStr, [A, B , C, D, E, F, G, H, I]), S end
+ end,
+ { Arity, #{ terminfo => TermInfoFun, ansi => AnsiFun }};
+ false -> Fun({undefined, AnsiFun})
+ catch error:badarg ->
+ Fun({undefined, AnsiFun})
+ end;
+ Fun({{TermInfoCap, Args}, AnsiString}) when not is_function(AnsiString) ->
+ try prim_tty:tigetstr(TermInfoCap) of
+ {ok, S} ->
+ {ok, TermInfoStr} = prim_tty:tputs(S, Args),
+ {0, #{ terminfo => fun() -> TermInfoStr end,
+ ansi => fun() -> AnsiString end } };
+ false ->
+ Fun({undefined, AnsiString})
+ catch error:badarg ->
+ Fun({undefined, AnsiString})
+ end;
+ Fun({TermInfoCap, AnsiString}) when not is_function(AnsiString) ->
+ Fun({{TermInfoCap, []}, AnsiString})
+ end, Mappings))
+ end, default_mappings()).
+
+get_vts_mappings() ->
+ case persistent_term:get(io_ansi_vts_mappings, undefined) of
+ undefined ->
+ persistent_term:put(io_ansi_vts_mappings, init_vts_mappings()),
+ get_vts_mappings();
+ Value -> Value
+ end.
+
+%% Take the description in default_mappings and so that we can do a reverse lookup quickly
+-spec init_vts_mappings() -> #{ vts() => [unicode:unicode_binary()] }.
+init_vts_mappings() ->
+ maps:map(fun Map(Key, Value) when not is_list(Value) ->
+ Map(Key, [Value]);
+ Map(_Key, Values) when is_list(Values) ->
+ lists:flatmap(fun F({_TermInfoCap, Fun}) when is_function(Fun) ->
+ [];
+ F({undefined, Ansi}) ->
+ [unicode:characters_to_binary(Ansi)];
+ F({{TermInfoCap, Args}, Ansi}) ->
+ try prim_tty:tigetstr(TermInfoCap) of
+ {ok, S} ->
+ {ok, TermInfoStr} = prim_tty:tputs(S, Args),
+ [unicode:characters_to_binary(TermInfoStr),
+ unicode:characters_to_binary(Ansi)];
+ undefined ->
+ F({undefined, Ansi})
+ catch error:badarg ->
+ F({undefined, Ansi})
+ end;
+ F({TermInfoCap, Ansi}) ->
+ F({{TermInfoCap, []}, Ansi})
+ end, Values)
+ end, default_mappings()).
+
+default_mappings() ->
+ #{ cursor =>
+ [{"cup", fun(Line, Column) -> concat(["\e[",Line+1,";",Column+1,"H"]) end}],
+ cursor_home => {"home", "\eH"},
+ cursor_up =>
+ [{"cuu1", "\e[A"},
+ {"cuu", fun(Steps) -> concat(["\e[", Steps ,"A"]) end}],
+
+ cursor_down =>
+ [{"cud1", "\n"},
+ {"cud", fun(Steps) -> concat(["\e[", Steps ,"B"]) end}],
+
+ cursor_backward =>
+ [{"cub1", "\b"},
+ {"cub", fun(Steps) -> concat(["\e[", Steps ,"D"]) end}],
+
+ cursor_forward =>
+ [{"cuf1", "\e[C"},
+ {"cuf", fun(Steps) -> concat(["\e[", Steps ,"C"]) end}],
+
+ reverse_index => { "ri", "\eM" },
+ cursor_save => { "sc", "\e7" },
+ cursor_restore => { "rc", "\e8" },
+ cursor_show => { "cvvis", "\e[?25h"},
+ cursor_hide => { "civis", "\e[?25l"},
+
+ cursor_next_line => { "nel", "\e[E" },
+ cursor_previous_line => { undefined, "\e[F" },
+ cursor_horizontal_absolute => { "hpa", fun(X) -> concat(["\e[", X, "G"]) end},
+ cursor_vertical_absolute => { "vpa", fun(Y) -> ["\e[", Y, "d"] end},
+ cursor_horizontal_vertical => { undefined, fun(X, Y) -> concat(["\e[", X, ";", Y, "f"]) end},
+
+ alternate_screen => { "smcup", "\e[?1049h" },
+ alternate_screen_off => { "rmcup", "\e[?1049l" },
+
+ scroll_forward => [{{"indn", [1]}, "\eS"},
+ { "indn", fun(Steps) -> concat(["\e", Steps, "S"]) end}],
+ scroll_backward => [{{"rin", [1]}, "\eT"},
+ { "rin", fun(Steps) -> concat(["\e", Steps, "T"]) end}],
+ scroll_change_region => {"csr", fun(Line1, Line2) -> concat(["\e[",Line1,";",Line2,"r"]) end},
+
+ insert_character => { "ich", fun(Chars) -> concat(["\e[", Chars, "@"]) end},
+ delete_character => [{"dch1", "\e[P"},
+ { "dch", fun(Chars) -> concat(["\e[", Chars, "P"]) end}],
+ erase_character => { "ech", fun(Chars) -> concat(["\e[", Chars, "X"]) end},
+ insert_line => [{"il1", "\e[L"},
+ { "il", fun(Chars) -> concat(["\e[", Chars, "L"]) end}],
+ delete_line => [{"dl1", "\e[M"},
+ { "dl", fun(Chars) -> concat(["\e[", Chars, "M"]) end}],
+
+ erase_display => { "ed", "\e[J"},
+ erase_line => {"el", "\e[K"},
+ clear => { "clear", "\e[H\e[2J"},
+
+ modify_color => { "initc", fun(Index, R, G, B) ->
+ io_lib:format("\e]4;~.16b;rgb:~.16b/~.16b/~.16b\e\\",[Index, R, G, B])
+ end},
+
+ keypad_transmit_mode => { "smkx", "\e=" },
+ keypad_transmit_mode_off => { "rmkx", "\e>" },
+
+ kcursor_home => {"khome", "\eH"},
+ kcursor_end => {"kend", "\eF"},
+ kcursor_up => {"kcuu1", "\e[A"},
+ kcursor_down => {"kcud1", "\e[B"},
+ kcursor_backward => {"kcub1", "\e[D"},
+ kcursor_forward => {"kcuf1", "\e[C"},
+
+ cursor_report_position => { "u7", "\e[6n" },
+ device_report_attributes => { undefined, "\e[0c"},
+
+ tab_set => { "hts", "\eH" },
+ tab => { "ht", "\e[I" },
+ tab_backward => { "cbt", "\eI" },
+ tab_clear => { undefined, "\e[0g" },
+ tab_clear_all => { "tbc", "\e[3g" },
+
+ reset => { "sgr0", sgr(["0"])},
+
+ black => { {"setaf", [0]}, sgr(["30"]) },
+ red => { {"setaf", [1]}, sgr(["31"]) },
+ green => { {"setaf", [2]}, sgr(["32"]) },
+ yellow => { {"setaf", [3]}, sgr(["33"]) },
+ blue => { {"setaf", [4]}, sgr(["34"]) },
+ magenta => { {"setaf", [5]}, sgr(["35"]) },
+ cyan => { {"setaf", [6]}, sgr(["36"]) },
+ white => { {"setaf", [7]}, sgr(["37"]) },
+ default_color => { undefined, sgr(["39"]) },
+ color => [{"setaf", fun(Index) -> sgr(["38","5",s(Index)]) end},
+ {undefined, fun(R, G, B) -> sgr(["38","2",s(R),s(G),s(B)]) end}],
+
+ black_background => { {"setab", [0]}, sgr(["40"]) },
+ red_background => { {"setab", [1]}, sgr(["41"]) },
+ green_background => { {"setab", [2]}, sgr(["42"]) },
+ yellow_background => { {"setab", [3]}, sgr(["43"]) },
+ blue_background => { {"setab", [4]}, sgr(["44"]) },
+ magenta_background => { {"setab", [5]}, sgr(["45"]) },
+ cyan_background => { {"setab", [6]}, sgr(["46"]) },
+ white_background => { {"setab", [7]}, sgr(["47"]) },
+ default_background => { undefined, sgr(["49"]) },
+ background => [{"setab", fun(Index) -> sgr(["48","5",s(Index)]) end},
+ {undefined, fun(R, G, B) -> sgr(["48","2",s(R),s(G),s(B)]) end}],
+
+ light_black => { {"setaf", [8]}, sgr(["90"]) },
+ light_red => { {"setaf", [9]}, sgr(["91"]) },
+ light_green => { {"setaf", [10]}, sgr(["92"]) },
+ light_yellow => { {"setaf", [11]}, sgr(["93"]) },
+ light_blue => { {"setaf", [12]}, sgr(["94"]) },
+ light_magenta => { {"setaf", [13]}, sgr(["95"]) },
+ light_cyan => { {"setaf", [14]}, sgr(["96"]) },
+ light_white => { {"setaf", [15]}, sgr(["97"]) },
+
+ light_black_background => { {"setab", [8]}, sgr(["100"]) },
+ light_red_background => { {"setab", [9]}, sgr(["101"]) },
+ light_green_background => { {"setab", [10]}, sgr(["102"]) },
+ light_yellow_background => { {"setab", [11]}, sgr(["103"]) },
+ light_blue_background => { {"setab", [12]}, sgr(["104"]) },
+ light_magenta_background => { {"setab", [13]}, sgr(["105"]) },
+ light_cyan_background => { {"setab", [14]}, sgr(["106"]) },
+ light_white_background => { {"setab", [15]}, sgr(["107"]) },
+
+ bold => { "bold", "\e[1m" },
+ bold_off => { undefined, "\e[22m" },
+ underline => { "smul", "\e[4m" },
+ underline_off => { "rmul", "\e[24m" },
+ negative => { "smso", "\e[7m"},
+ negative_off => { "rmso", "\e[27m"},
+
+ alternate_character_set_mode => {"smacs", "\e(0" },
+ alternate_character_set_mode_off => {"rmacs", "\e(B" },
+
+ hyperlink_start =>
+ [{undefined, fun(Url) -> hyperlink(Url, []) end},
+ {undefined, fun(Url, Params) -> hyperlink(Url, Params) end}],
+
+ hyperlink_reset =>
+ [{undefined, fun() -> hyperlink("", []) end}]
+ }.
+
+hyperlink(URL, Params) ->
+ StringParams = lists:join($:, [[K, "=", V] || {K, V} <- Params]),
+ io_lib:format("\e]8;~s;~s\e\\",[URL, StringParams]).
+
+s(Int) when is_integer(Int) ->
+ integer_to_list(Int).
\ No newline at end of file
diff --git a/lib/stdlib/src/io_lib.erl b/lib/stdlib/src/io_lib.erl
index 25cab90f50..8e1cbf3d59 100644
--- a/lib/stdlib/src/io_lib.erl
+++ b/lib/stdlib/src/io_lib.erl
@@ -107,7 +107,8 @@ used for flattening deep lists.
-export([write_bin/5, write_string_bin/3, write_binary_bin/4]).
-export_type([chars/0, latin1_string/0, continuation/0,
- fread_error/0, fread_item/0, format_spec/0, chars_limit/0]).
+ fread_error/0, fread_item/0, format_spec/0,
+ chars_limit/0, format_options/0]).
-dialyzer([{nowarn_function,
[string_bin_escape_unicode/6,
@@ -193,8 +194,20 @@ Unicode data is allowed.
fwrite(Format, Args) ->
format(Format, Args).
+-doc """
+A soft limit on the number of characters returned.
+
+When the number of characters is reached, remaining structures are
+replaced by "`...`". `CharsLimit` defaults to -1, which means no limit on the
+number of characters returned.
+""".
-type chars_limit() :: integer().
+-doc """
+Options that can be passed to `format/3` and `fwrite/3`.
+""".
+-type format_options() :: [{'chars_limit', chars_limit()}].
+
-doc """
Returns a character list that represents `Data` formatted in accordance with
`Format` in the same way as `fwrite/2` and `format/2`, but takes an extra
@@ -211,9 +224,7 @@ Valid option:
-spec fwrite(Format, Data, Options) -> chars() when
Format :: io:format(),
Data :: [term()],
- Options :: [Option],
- Option :: {'chars_limit', CharsLimit},
- CharsLimit :: chars_limit().
+ Options :: format_options().
fwrite(Format, Args, Options) ->
format(Format, Args, Options).
@@ -237,9 +248,7 @@ bfwrite(F, A) ->
-spec bfwrite(Format, Data, Options) -> unicode:unicode_binary() when
Format :: io:format(),
Data :: [term()],
- Options :: [Option],
- Option :: {'chars_limit', CharsLimit},
- CharsLimit :: chars_limit().
+ Options :: format_options().
bfwrite(F, A, Opts) ->
bformat(F, A, Opts).
@@ -342,9 +351,7 @@ format(Format, Args) ->
-spec format(Format, Data, Options) -> chars() when
Format :: io:format(),
Data :: [term()],
- Options :: [Option],
- Option :: {'chars_limit', CharsLimit},
- CharsLimit :: chars_limit().
+ Options :: format_options().
format(Format, Args, Options) ->
try io_lib_format:fwrite(Format, Args, Options)
@@ -374,9 +381,7 @@ bformat(Format, Args) ->
-spec bformat(Format, Data, Options) -> unicode:unicode_binary() when
Format :: io:format(),
Data :: [term()],
- Options :: [Option],
- Option :: {'chars_limit', CharsLimit},
- CharsLimit :: chars_limit().
+ Options :: format_options().
bformat(Format, Args, Options) ->
try io_lib_format:fwrite_bin(Format, Args, Options)
@@ -406,7 +411,9 @@ formatting to text in, for example, a logger.
FormatList :: [char() | format_spec()].
scan_format(Format, Args) ->
- try io_lib_format:scan(Format, Args)
+ try
+ {Scanned, []} = io_lib_format:scan(Format, Args),
+ Scanned
catch
C:R:S ->
test_modules_loaded(C, R, S),
@@ -439,9 +446,7 @@ build_text(FormatList) ->
-doc false.
-spec build_text(FormatList, Options) -> chars() when
FormatList :: [char() | format_spec()],
- Options :: [Option],
- Option :: {'chars_limit', CharsLimit},
- CharsLimit :: chars_limit().
+ Options :: format_options().
build_text(FormatList, Options) ->
try io_lib_format:build(FormatList, Options)
diff --git a/lib/stdlib/src/io_lib_format.erl b/lib/stdlib/src/io_lib_format.erl
index b0470f5aea..8acb2fac14 100644
--- a/lib/stdlib/src/io_lib_format.erl
+++ b/lib/stdlib/src/io_lib_format.erl
@@ -53,7 +53,8 @@
Data :: [term()].
fwrite(Format, Args) ->
- build(scan(Format, Args)).
+ {Scanned, []} = scan(Format, Args),
+ build(Scanned).
-spec fwrite(Format, Data, Options) -> io_lib:chars() when
Format :: io:format(),
@@ -63,7 +64,8 @@ fwrite(Format, Args) ->
CharsLimit :: io_lib:chars_limit().
fwrite(Format, Args, Options) ->
- build(scan(Format, Args), Options).
+ {Scanned, []} = scan(Format, Args),
+ build(Scanned, Options).
%% Binary variants
-spec fwrite_bin(Format, Data) -> unicode:unicode_binary() when
@@ -71,7 +73,8 @@ fwrite(Format, Args, Options) ->
Data :: [term()].
fwrite_bin(Format, Args) ->
- build_bin(scan(Format, Args)).
+ {Scanned, []} = scan(Format, Args),
+ build_bin(Scanned).
-spec fwrite_bin(Format, Data, Options) -> unicode:unicode_binary() when
Format :: io:format(),
@@ -81,7 +84,8 @@ fwrite_bin(Format, Args) ->
CharsLimit :: io_lib:chars_limit().
fwrite_bin(Format, Args, Options) ->
- build_bin(scan(Format, Args), Options).
+ {Scanned, []} = scan(Format, Args),
+ build_bin(Scanned, Options).
%% Build the output text for a pre-parsed format list.
@@ -137,10 +141,11 @@ build_bin(Cs, Options) ->
%% Parse all control sequences in the format string.
--spec scan(Format, Data) -> FormatList when
+-spec scan(Format, Data) -> {FormatList, Rest} when
Format :: io:format(),
Data :: [term()],
- FormatList :: [char() | io_lib:format_spec()].
+ FormatList :: [char() | io_lib:format_spec()],
+ Rest :: [term()].
scan(Format, Args) when is_atom(Format) ->
scan(atom_to_list(Format), Args);
@@ -207,12 +212,15 @@ print_maps_order(ordered) -> "k";
print_maps_order(reversed) -> "K";
print_maps_order(CmpFun) when is_function(CmpFun, 2) -> "K".
-collect([$~|Fmt0], Args0) ->
+collect(Fmt, Args) ->
+ collect(Fmt, Args, []).
+collect([$~|Fmt0], Args0, Acc) ->
{C,Fmt1,Args1} = collect_cseq(Fmt0, Args0),
- [C|collect(Fmt1, Args1)];
-collect([C|Fmt], Args) ->
- [C|collect(Fmt, Args)];
-collect([], []) -> [].
+ collect(Fmt1, Args1, [C | Acc]);
+collect([C|Fmt], Args, Acc) ->
+ collect(Fmt, Args, [C | Acc]);
+collect([], Remain, Acc) ->
+ {lists:reverse(Acc), Remain}.
collect_cseq(Fmt0, Args0) ->
{F,Ad,Fmt1,Args1} = field_width(Fmt0, Args0),
diff --git a/lib/stdlib/src/stdlib.app.src b/lib/stdlib/src/stdlib.app.src
index c722c1ac7b..9451d8866c 100644
--- a/lib/stdlib/src/stdlib.app.src
+++ b/lib/stdlib/src/stdlib.app.src
@@ -77,6 +77,7 @@
gen_statem,
graph,
io,
+ io_ansi,
io_lib,
io_lib_format,
io_lib_fread,
diff --git a/lib/stdlib/test/Makefile b/lib/stdlib/test/Makefile
index 0ec6214840..9b572ea1dc 100644
--- a/lib/stdlib/test/Makefile
+++ b/lib/stdlib/test/Makefile
@@ -74,6 +74,7 @@ MODULES= \
gen_statem_SUITE \
id_transform_SUITE \
io_SUITE \
+ io_ansi_SUITE \
io_proto_SUITE \
json_SUITE \
lists_SUITE \
@@ -138,12 +139,14 @@ MODULES= \
ERTS_MODULES= erts_test_utils
SASL_MODULES= otp_vsns
-KERNEL_MODULES= rtnode
+KERNEL_MODULES= rtnode shell_test_lib
+KERNEL_HRL= shell_test_lib
ERL_FILES= $(MODULES:%=%.erl) \
$(ERTS_MODULES:%=$(ERL_TOP)/erts/emulator/test/%.erl) \
$(SASL_MODULES:%=$(ERL_TOP)/lib/sasl/test/%.erl) \
- $(KERNEL_MODULES:%=$(ERL_TOP)/lib/kernel/test/%.erl)
+ $(KERNEL_MODULES:%=$(ERL_TOP)/lib/kernel/test/%.erl) \
+ $(KERNEL_HRL:%=$(ERL_TOP)/lib/kernel/test/%.hrl)
EXTRA_FILES= $(ERL_TOP)/otp_versions.table
diff --git a/lib/stdlib/test/io_ansi_SUITE.erl b/lib/stdlib/test/io_ansi_SUITE.erl
new file mode 100644
index 0000000000..b2b3a5f9c5
--- /dev/null
+++ b/lib/stdlib/test/io_ansi_SUITE.erl
@@ -0,0 +1,155 @@
+%%
+%% %CopyrightBegin%
+%%
+%% SPDX-License-Identifier: Apache-2.0
+%%
+%% Copyright Ericsson AB 2020-2025. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%
+%% %CopyrightEnd%
+%%
+
+-module(io_ansi_SUITE).
+-moduledoc false.
+
+-behaviour(ct_suite).
+
+-export([all/0, suite/0, groups/0, init_per_suite/1, end_per_suite/1,
+ init_per_group/2, end_per_group/2, init_per_testcase/2, end_per_testcase/2]).
+
+-export([enabled/1, fwrite/1, fwrite_test/0, doctests/1]).
+
+-include_lib("common_test/include/ct.hrl").
+-include_lib("stdlib/include/assert.hrl").
+
+suite() ->
+ [].
+
+all() ->
+ [ doctests, enabled, fwrite ].
+
+
+groups() ->
+ [ ].
+
+init_per_suite(Config) ->
+ shell_test_lib:start_tmux(Config).
+
+end_per_suite(Config) ->
+ shell_test_lib:stop_tmux(Config).
+
+init_per_group(_GroupName, Config) ->
+ Config.
+
+end_per_group(_GroupName, _Config) ->
+ ok.
+
+init_per_testcase(_TestCase, Config) ->
+ Config.
+
+end_per_testcase(_TestCase, _Config) ->
+ ok.
+
+enabled(Config) ->
+
+ %% Test that when stdout is not a tty, enabled returns false
+ Erl = ct:get_progname(),
+ ?assertEqual("false", os:cmd(Erl ++ ~s` -noshell -eval 'io:format("~p",[io_ansi:enabled()])' -s init stop`)),
+
+ %% Test that when stdout is a tty and various term settings
+ enabled_test(true, "xterm-256color", Config),
+ enabled_test(false, "abc", Config),
+ enabled_test(false, "dumb", Config).
+
+enabled_test(Expect, TermType, Config) ->
+ Term = shell_test_lib:setup_tty([{env, [{"TERM",TermType}]}|Config]),
+ try
+ ?assertEqual(Expect,
+ shell_test_lib:rpc(
+ Term,
+ fun() ->
+ group_leader(whereis(user), self()),
+ io_ansi:enabled()
+ end)),
+ ?assertEqual(Expect,
+ shell_test_lib:rpc(
+ Term,
+ fun() ->
+ io_ansi:enabled(user)
+ end)),
+ ?assertEqual(Expect,
+ shell_test_lib:rpc(
+ Term,
+ fun() ->
+ group_leader(whereis(user), self()),
+ {group_leader, GL} = erlang:process_info(shell:whereis(), group_leader),
+ io_ansi:enabled(GL)
+ end))
+ after
+ shell_test_lib:stop_tty(Term)
+ end.
+
+fwrite(Config) ->
+ {ok, Peer, Node} = ?CT_PEER(#{ env => [{"TERM","dumb"}] }),
+ DumbUser = erpc:call(Node, erlang, whereis, [user]),
+ false = erpc:call(Node, io_ansi, enabled, [DumbUser]),
+
+ Term = shell_test_lib:setup_tty([{args,["-noshell","-eval","io_ansi_SUITE:fwrite_test()."]},
+ {env, [{"TERM","xterm-256color"}]} | Config]),
+
+ try
+ try
+ shell_test_lib:send_tty(Term, atom_to_list(Node) ++ "\n"),
+
+ shell_test_lib:check_content(Term, "\n\e\\[34mblue", #{ args => "-e" }),
+ shell_test_lib:check_content(Term, "\n\e\\[31mred", #{ args => "-e" })
+
+ after
+ shell_test_lib:stop_tty(Term)
+ end,
+
+ DumbTerm = shell_test_lib:setup_tty([{args,["-noshell","-eval","io_ansi_SUITE:fwrite_test()."]},
+ {env, [{"TERM","dumb"}]} | Config]),
+
+ try
+ shell_test_lib:send_tty(DumbTerm, atom_to_list(Node) ++ "\n"),
+
+ shell_test_lib:check_content(DumbTerm, "\nblue", #{ args => "-e" }),
+ shell_test_lib:check_content(DumbTerm, "\nred", #{ args => "-e" })
+
+ after
+ shell_test_lib:stop_tty(DumbTerm)
+ end
+ after
+ peer:stop(Peer)
+ end.
+
+fwrite_test() ->
+ NodeName = string:trim(io:get_line("")),
+
+ io_ansi:fwrite([blue, "blue\n"]),
+ erpc:call(list_to_atom(NodeName), fun() -> io_ansi:fwrite([red, "red\n"]) end),
+
+ ok.
+
+doctests(Config) ->
+ Term = shell_test_lib:setup_tty([{env, [{"TERM","xterm-256color"}]}|Config]),
+ try
+ shell_test_lib:rpc(Term, fun() ->
+ group_leader(whereis(user), self()),
+ shell_docs:test(io_ansi, [])
+ end)
+ after
+ shell_test_lib:stop_tty(Term)
+ end.
\ No newline at end of file
--
2.51.0