File 2532-system-Update-tic-tac-toe-to-use-io_ansi.patch of Package erlang
From 1c835698a94ceb5f07946ab82804d3ff1479c42e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lukas=20Backstr=C3=B6m?= <lukas@erlang.org>
Date: Fri, 13 Jun 2025 11:47:57 +0200
Subject: [PATCH 12/17] system: Update tic-tac-toe to use io_ansi
---
lib/stdlib/doc/assets/tic-tac-toe.es | 85 +++++++++++----------
lib/stdlib/doc/guides/terminal_interface.md | 79 ++++++++++---------
lib/stdlib/src/io_ansi.erl | 4 +-
3 files changed, 89 insertions(+), 79 deletions(-)
diff --git a/lib/stdlib/doc/assets/tic-tac-toe.es b/lib/stdlib/doc/assets/tic-tac-toe.es
index 83fa40f547..c8c5b7cae5 100755
--- a/lib/stdlib/doc/assets/tic-tac-toe.es
+++ b/lib/stdlib/doc/assets/tic-tac-toe.es
@@ -23,16 +23,18 @@
main(_Args) ->
ok = shell:start_interactive({noshell, raw}),
- io:put_chars("\e[?1049h"), %% Enable alternate screen buffer
- io:put_chars("\e[?25l"), %% Hide the cursor
- draw_board(),
- loop({0, "X", list_to_tuple(lists:duplicate(9, ""))}),
- io:put_chars("\e[?25h"), %% Show the cursor
- io:put_chars("\e[?1049l"), %% Disable alternate screen buffer
- ok.
+ try
+ %% Enable alternate screen buffer, hide cursor and enable keypad_transmit_mode
+ io_ansi:fwrite([alternate_screen, cursor_hide, keypad_transmit_mode]),
+ draw_board(),
+ loop({0, "X", list_to_tuple(lists:duplicate(9, ""))}),
+ timer:sleep(5000)
+ after ->
+ io_ansi:fwrite([alternate_screen_off, cursor_show, keypad_transmit_mode_off])
+ end.
draw_board() ->
- io:put_chars("\e[5;0H"), %% Move cursor to top left
+ io_ansi:fwrite([{cursor, 6, 0}]),
io:put_chars(
[" ╔═══════╤═══════╤═══════╗\r\n",
" ║ │ │ ║\r\n",
@@ -50,76 +52,81 @@ draw_board() ->
ok.
loop(State) ->
- io:put_chars(draw_state(State)),
- case handle_input(io:get_chars("", 30), State) of
- stop -> stop;
- NewState ->
- io:put_chars(clear_selection(State)),
- loop(NewState)
+ io_ansi:fwrite(lists:flatten(draw_state(State))),
+ case io:get_chars("", 1024) of
+ eof -> stop;
+ Chars ->
+ case handle_input(io_ansi:scan(Chars), State) of
+ stop -> stop;
+ NewState ->
+ io_ansi:fwrite(clear_selection(State)),
+ loop(NewState)
+ end
end.
%% Clear/draw the selection markers, making sure
%% not to overwrite if a X or O exists.
-%% \b = Move cursor left
-%% \e[C = Move cursor right
-%% \n = Move cursor down
clear_selection({Pos, _, _}) ->
[set_position(Pos),
- " ","\b\b\b\b\b\b\b\n",
- " \e[C\e[C\e[C\e[C\e[C ",
- "\b\b\b\b\b\b\b\n"," "].
+ " ",{cursor_backward, 7}, cursor_down,
+ " ",{cursor_forward,5}," ",
+ {cursor_backward, 7}, cursor_down,
+ " "].
draw_selection({Pos, _, _}) ->
[set_position(Pos),
- "┌─────┐","\b\b\b\b\b\b\b\n",
- "│\e[C\e[C\e[C\e[C\e[C│",
- "\b\b\b\b\b\b\b\n","└─────┘"].
+ "┌─────┐",
+ {cursor_backward, 7}, cursor_down,
+ "│",{cursor_forward,5},"│",
+ {cursor_backward, 7}, cursor_down,
+ "└─────┘"].
%% Set the cursor position to be at the top
%% left of the field of the given position
set_position(Pos) ->
Row = 6 + (Pos div 3) * 4,
Col = 7 + (Pos rem 3) * 8,
- io_lib:format("\e[~p;~pH",[Row, Col]).
+ {cursor, Row + 1, Col - 1}.
%% Update selection and whos turn it is
draw_state({_, Turn, _} = State) ->
[draw_selection(State),
- io_lib:format("\e[7;45H~s",[Turn])].
+ {cursor, 8, 44}, Turn].
%% Draw X or O
draw_marker(Pos, Turn) ->
- [set_position(Pos), "\e[C\e[C\e[C\n", Turn].
+ [set_position(Pos), {cursor_forward, 3}, cursor_down, Turn].
-handle_input(eof, _State) ->
- stop;
-handle_input("\e[A" ++ Rest, {Pos, Turn, State}) ->
+handle_input([kcursor_up | Rest], {Pos, Turn, State}) ->
%% Up key
handle_input(Rest, {max(0, Pos - 3), Turn, State});
-handle_input("\e[B" ++ Rest, {Pos, Turn, State}) ->
+handle_input([kcursor_down | Rest], {Pos, Turn, State}) ->
%% Down key
handle_input(Rest, {min(8, Pos + 3), Turn, State});
-handle_input("\e[C" ++ Rest, {Pos, Turn, State}) ->
+handle_input([kcursor_forward | Rest], {Pos, Turn, State}) ->
%% right key
handle_input(Rest, {min(8, Pos + 1), Turn, State});
-handle_input("\e[D" ++ Rest, {Pos, Turn, State}) ->
+handle_input([kcursor_backward | Rest], {Pos, Turn, State}) ->
%% left key
handle_input(Rest, {max(0, Pos - 1), Turn, State});
-handle_input("\r" ++ Rest, {Pos, Turn, State} = OldState) ->
+handle_input([<<"\r",Rest/binary>> | T], {Pos, Turn, State} = OldState) ->
NewState =
case element(Pos+1, State) of
"" when Turn =:= "X" ->
- io:put_chars(draw_marker(Pos, Turn)),
+ io_ansi:fwrite(draw_marker(Pos, Turn)),
{Pos, "O", setelement(Pos+1, State, Turn)};
"" when Turn =:= "O" ->
- io:put_chars(draw_marker(Pos, Turn)),
+ io_ansi:fwrite(draw_marker(Pos, Turn)),
{Pos, "X", setelement(Pos+1, State, Turn)};
- _ -> io:put_chars("\^G"), OldState
+ _ -> io_ansi:fwrite("\^G"), OldState
end,
- handle_input(Rest, NewState);
-handle_input("q" ++ _, _State) ->
+ handle_input([Rest | T], NewState);
+handle_input([<<"q",_Rest/binary>> | _T], _State) ->
stop;
-handle_input([_ | T], State) ->
+handle_input([<<>> | T], State) ->
+ handle_input(T, State);
+handle_input([C | T], State) ->
+ io_ansi:fwrite([cursor_save, {cursor, 40, 0}, "Unknown character: ~p", cursor_restore],[C]),
handle_input(T, State);
handle_input([], State) ->
State.
\ No newline at end of file
diff --git a/lib/stdlib/doc/guides/terminal_interface.md b/lib/stdlib/doc/guides/terminal_interface.md
index 8d2b914aa1..014c54992a 100644
--- a/lib/stdlib/doc/guides/terminal_interface.md
+++ b/lib/stdlib/doc/guides/terminal_interface.md
@@ -45,18 +45,18 @@ Let us start by drawing the board which will look like this:
```
-We will use the alternate screen buffer for our game so first we need to set that up:
+We will use the alternate screen buffer for our game so first we need to set
+that up using `m:io_ansi`:
```
#!/usr/bin/env escript
main(_Args) ->
- io:put_chars("\e[?1049h"), %% Enable alternate screen buffer
- io:put_chars("\e[?25l"), %% Hide the cursor
+ %% Enable alternate screen buffer and hide cursor
+ io_ansi:fwrite([alternate_screen, cursor_hide]),
draw_board(),
timer:sleep(5000),
- io:put_chars("\e[?25h"), %% Show the cursor
- io:put_chars("\e[?1049l"), %% Disable alternate screen buffer
+ io_ansi:fwrite([alternate_screen_off, cursor_show]),
ok.
```
@@ -64,7 +64,8 @@ We then use the box drawing parts of Unicode to draw our board:
```
draw_board() ->
- io:put_chars("\e[5;0H"), %% Move cursor to top left
+ %% Place cursor at row 6 column 0
+ io_ansi:fwrite([{cursor, 6, 0}])
io:put_chars(
[" ╔═══════╤═══════╤═══════╗\r\n",
" ║ │ │ ║\r\n",
@@ -87,7 +88,7 @@ shell from running in `cooked` to `raw` mode. This is done by calling
[`shell:start_interactive({noshell, raw})`](`shell:start_interactive/1`).
We can then use `io:get_chars/2` to read key strokes from the user. The key
strokes will be returned as [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code),
-so we will have need to handle the codes for up, down, left, right and enter.
+so we will use `io_ansi:scan/1` to interpret them.
It could look something like this:
@@ -95,38 +96,41 @@ It could look something like this:
main(_Args) ->
ok = shell:start_interactive({noshell, raw}),
- io:put_chars("\e[?1049h"), %% Enable alternate screen buffer
- io:put_chars("\e[?25l"), %% Hide the cursor
- draw_board(),
- loop(0),
- io:put_chars("\e[?25h"), %% Show the cursor
- io:put_chars("\e[?1049l"), %% Disable alternate screen buffer
- ok.
+ try
+ %% Enable alternate screen buffer, hide cursor and enable keypad_transmit_mode
+ io_ansi:fwrite([alternate_screen, cursor_hide, keypad_transmit_mode]),
+ draw_board(),
+ loop(0)
+ after ->
+ io_ansi:fwrite([alternate_screen_off, cursor_show, keypad_transmit_mode_off]),
+ end.
loop(Pos) ->
- io:put_chars(draw_selection(Pos)),
- %% Read at most 1024 characters from stdin.
- Chars = io:get_chars("", 1024),
- case handle_input(Chars, Pos) of
- stop -> stop;
- NewPos ->
- io:put_chars(clear_selection(Pos)),
- loop(NewPos)
+ io_ansi:fwrite(lists:flatten(draw_state(Pos))),
+ case io:get_chars("", 1024) of
+ eof -> stop;
+ Chars ->
+ case handle_input(io_ansi:scan(Chars), Pos) of
+ stop -> stop;
+ NewPos ->
+ io_ansi:fwrite(clear_selection(Pos)),
+ loop(NewPos)
+ end
end.
-handle_input("\e[A" ++ Rest, Pos) ->
+handle_input([kcursor_up | Rest], {Pos, Turn, State}) ->
%% Up key
handle_input(Rest, max(0, Pos - 3));
-handle_input("\e[B" ++ Rest, Pos) ->
+handle_input([kcursor_down | Rest], {Pos, Turn, State}) ->
%% Down key
handle_input(Rest, min(8, Pos + 3));
-handle_input("\e[C" ++ Rest, Pos) ->
+handle_input([kcursor_forward | Rest], {Pos, Turn, State}) ->
%% right key
handle_input(Rest, min(8, Pos + 1));
-handle_input("\e[D" ++ Rest, Pos) ->
+handle_input([kcursor_backward | Rest], {Pos, Turn, State}) ->
%% left key
handle_input(Rest, max(0, Pos - 1));
-handle_input("q" ++ _, _State) ->
+handle_input([<<"q",_Rest/binary>> | _T], _State) ->
stop;
handle_input([_ | T], State) ->
handle_input(T, State);
@@ -145,29 +149,28 @@ routines.
```
%% Clear/draw the selection markers, making sure
%% not to overwrite if a X or O exists.
-%% \b = Move cursor left
-%% \e[C = Move cursor right
-%% \n = Move cursor down
clear_selection(Pos) ->
[set_position(Pos),
- " ","\b\b\b\b\b\b\b\n",
- " \e[C\e[C\e[C\e[C\e[C ",
- "\b\b\b\b\b\b\b\n"," "].
+ " ",{cursor_backward, 7}, cursor_down,
+ " ",{cursor_forward,5}," ",
+ {cursor_backward, 7}, cursor_down,
+ " "].
draw_selection(Pos) ->
[set_position(Pos),
- "┌─────┐","\b\b\b\b\b\b\b\n",
- "│\e[C\e[C\e[C\e[C\e[C│",
- "\b\b\b\b\b\b\b\n","└─────┘"].
+ "┌─────┐",
+ {cursor_backward, 7}, cursor_down,
+ "│",{cursor_forward,5},"│",
+ {cursor_backward, 7}, cursor_down,
+ "└─────┘"].
%% Set the cursor position to be at the top
%% left of the field of the given position
set_position(Pos) ->
Row = 6 + (Pos div 3) * 4,
Col = 7 + (Pos rem 3) * 8,
- io_lib:format("\e[~p;~pH",[Row, Col]).
+ {cursor, Row + 1, Col - 1}.
```
-{: #monospace-font }
Now we have a program where we can move the marker around the board.
To complete the game we need to add some state so that we know which
diff --git a/lib/stdlib/src/io_ansi.erl b/lib/stdlib/src/io_ansi.erl
index 673fc228be..739942156a 100644
--- a/lib/stdlib/src/io_ansi.erl
+++ b/lib/stdlib/src/io_ansi.erl
@@ -2178,8 +2178,8 @@ 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.
+application mode by using `keypad_transmit_mode/0`. This will make it easier for
+`m: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`.
--
2.51.0