Entering raw mode temporarily while in the shell for a TUI

I’ve been trying out the Tic-tac-toe demo at Creating a terminal application — stdlib v7.1 and it works pretty well. What I’m wondering is if it is possible to be in the shell (in cooked mode) and temporarily switch to raw mode for a TUI and then switch back when done.

My use case is for Nerves since when you log into a Nerves device, everything is done via the shell prompt. It’s been possible to write TUI-like apps, but they all require you to press enter for every interaction. Being able to run the terminal in raw mode would let me clean up some rough edges.

My other constraint is to stay within the Erlang I/O protocol since the TUI could be started from a shell on a local UART, an ssh session, or some other remote console.

I tried a few things, but I’m feeling like I’m going about this wrong and wondering if anyone had suggestions or could confirm that temporarily switching to raw mode isn’t possible with OTP 28.1.

Thanks!

1 Like

Hi! I’m not sure if this is exactly what you’re looking for, but maybe it can help as a reference.

The Arizona Framework’s rebar3 plugin has an implementation that might be similar to what you’re trying to achieve. It temporarily switches to raw mode for a TUI and then switches back when done.

The relevant file is src/rebar3_arizona_prv.erl, which does something like this:

  1. Switches to raw mode (around line 51):

    ok = shell:start_interactive({noshell, raw}),
    
  2. Uses alternate screen buffer and hides cursor (lines 54-56):

    io:put_chars("\e[?1049h"),  % alternate screen
    io:put_chars("\e[?25l"),     % hide cursor
    
  3. Handles arrow keys and input for menu navigation (lines 115-162)

  4. Restores everything in an after block (lines 61-64):

    after
        io:put_chars("\e[?25h"),    % show cursor
        io:put_chars("\e[?1049l")   % restore screen
    end
    

It even mentions following the tic-tac-toe pattern in a comment! Maybe this approach could work for your Nerves use case? It stays within the Erlang I/O protocol, so it might work across different console types (local UART, ssh, etc.).

Hope this helps!

2 Likes

Thanks for the quick response. That’s really nice what you did with your TUI.

Here’s the sad part, I can’t run shell:start_interactive/1since it’s already been started. Running it a second time returns an already_started error. I started going down a path where I was killing processes to hack around the error, but that had other issues so I stopped.

My setup is more like running erl, interacting with the shell some, and then calling a function to start the TUI. Then, whenever the TUI is done, get the shell back again.

1 Like

I’m not sure again, but check if something like this can help (not tested):

% Use io:setopts/1 instead to control terminal behavior without restarting the shell
start_tui() ->
    % Save original opts and disable echo
    OrigOpts = io:getopts(),
    ok = io:setopts([{echo, false}, {binary, false}, list]),
                                                                     
    % Use alternate screen and hide cursor
    io:put_chars("\e[?1049h"),
    io:put_chars("\e[?25l"),
                                                                     
    try
        run_tui_loop()
    after
        % Restore cursor and screen
        io:put_chars("\e[?25h"),
        io:put_chars("\e[?1049l"),
        % Restore original terminal options
        io:setopts(OrigOpts)
    end.
                                                                     
run_tui_loop() ->
    case io:get_chars("", 1) of
        % Up arrow
        [27, $[, $A] ->
            handle_up(),
            run_tui_loop();
        % Down arrow
        [27, $[, $B] ->
            handle_down(),
            run_tui_loop();
        % Quit
        [$q] -> ok;
        _ -> run_tui_loop()
    end.

BTW, @garazdawi is the guy to help you.

1 Like

There is one extremely ugly hack, which I discovered while digging through io system code:

  os:putenv("VISUAL", "some_specially_crafted_execubtable"),
  user_drv ! {self(), {open_editor, ""}},

Explanation: erlang shell has a functionality for editing strings using an external editor. It creates a temporary file and opens it in your visual editor of choice. While editor is open, input switches to raw mode. When the editor closes, input goes back to normal and the shell receives the contents of the temporary file.

Well, you can use it to your advantage by replacing the editor with some special executable. This works on old releases as well, but needless to say this uses an internal undocumented protocol.

1 Like

I think you unblocked me with this. Up and down get split up into 3 returns from io:get_chars/2 so I’ll need to implement a little state machine to deal with characters like these. This seems to work on the starting terminal and over ssh.

Thank you!

1 Like

Nice hack. It worked on the startup terminal and I played around with launching a few things. Getting btop running was wild. It didn’t work over ssh, which isn’t surprising since it went direct to user_drv. I’m going to persue @williamthome‘s suggestion since it’s not a hack, but your post may be a way I can unlock interactive C apps…

1 Like

When implementing the tty things I imagined allowing this to be done, but it was not trivial to do so I left it for later. Now that I know there is a need, maybe I’ll pick that up again.

2 Likes

Fun stuff, played around with the tic-tac game, created this ansi.hrl to make it easier on the eyes.

%%====================================================================
%% ANSI Escape Sequence Macros for Erlang Terminal Applications
%%====================================================================
%% Author: Your Name
%% License: Apache 2.0
%% Description:
%%   A set of convenient macros for controlling terminal output using
%%   ANSI escape sequences.  Includes cursor movement, screen control,
%%   color styling, alternate screen buffer support, and text effects.
%%====================================================================

%%------------------------------
%% Base escape prefix
%%------------------------------
-define(ESC, "\e[").

%%------------------------------
%% Cursor movement
%%------------------------------
-define(MVTO_ROW_COL(R, C), ?ESC ++ integer_to_list(R) ++ ";" ++ integer_to_list(C) ++ "H").
-define(CURSOR_UP(N),       ?ESC ++ integer_to_list(N) ++ "A").
-define(CURSOR_DOWN(N),     ?ESC ++ integer_to_list(N) ++ "B").
-define(CURSOR_RIGHT(N),    ?ESC ++ integer_to_list(N) ++ "C").
-define(CURSOR_LEFT(N),     ?ESC ++ integer_to_list(N) ++ "D").

%%------------------------------
%% Screen control
%%------------------------------
-define(CLEAR_SCREEN,       ?ESC ++ "2J").
-define(CLEAR_LINE,         ?ESC ++ "2K").
-define(SCROLL_UP(N),       ?ESC ++ integer_to_list(N) ++ "S").
-define(SCROLL_DOWN(N),     ?ESC ++ integer_to_list(N) ++ "T").

%%------------------------------
%% Alternate screen buffer
%%------------------------------
-define(ALT_SCREEN_ON,      ?ESC ++ "?1049h").
-define(ALT_SCREEN_OFF,     ?ESC ++ "?1049l").

%%------------------------------
%% Cursor visibility
%%------------------------------
-define(HIDE_CURSOR,        ?ESC ++ "?25l").
-define(SHOW_CURSOR,        ?ESC ++ "?25h").

%%------------------------------
%% Color codes
%%------------------------------
-define(FG_BLACK_CODE,   "30").
-define(FG_RED_CODE,     "31").
-define(FG_GREEN_CODE,   "32").
-define(FG_YELLOW_CODE,  "33").
-define(FG_BLUE_CODE,    "34").
-define(FG_MAGENTA_CODE, "35").
-define(FG_CYAN_CODE,    "36").
-define(FG_WHITE_CODE,   "37").

-define(BG_BLACK_CODE,   "40").
-define(BG_RED_CODE,     "41").
-define(BG_GREEN_CODE,   "42").
-define(BG_YELLOW_CODE,  "43").
-define(BG_BLUE_CODE,    "44").
-define(BG_MAGENTA_CODE, "45").
-define(BG_CYAN_CODE,    "46").
-define(BG_WHITE_CODE,   "47").

-define(FG_RESET_CODE,   "0m").
-define(FG_RESET, ?ESC ++ ?FG_RESET_CODE).

%%------------------------------
%% Foreground-only color wrappers
%%------------------------------
-define(FG_BLACK(S),   ?ESC ++ ?FG_BLACK_CODE   ++ "m" ++ S ++ ?FG_RESET).
-define(FG_RED(S),     ?ESC ++ ?FG_RED_CODE     ++ "m" ++ S ++ ?FG_RESET).
-define(FG_GREEN(S),   ?ESC ++ ?FG_GREEN_CODE   ++ "m" ++ S ++ ?FG_RESET).
-define(FG_YELLOW(S),  ?ESC ++ ?FG_YELLOW_CODE  ++ "m" ++ S ++ ?FG_RESET).
-define(FG_BLUE(S),    ?ESC ++ ?FG_BLUE_CODE    ++ "m" ++ S ++ ?FG_RESET).
-define(FG_MAGENTA(S), ?ESC ++ ?FG_MAGENTA_CODE ++ "m" ++ S ++ ?FG_RESET).
-define(FG_CYAN(S),    ?ESC ++ ?FG_CYAN_CODE    ++ "m" ++ S ++ ?FG_RESET).
-define(FG_WHITE(S),   ?ESC ++ ?FG_WHITE_CODE   ++ "m" ++ S ++ ?FG_RESET).

%%------------------------------
%% Background-only color wrappers
%%------------------------------
-define(BG_BLACK(S),   ?ESC ++ ?BG_BLACK_CODE   ++ "m" ++ S ++ ?FG_RESET).
-define(BG_RED(S),     ?ESC ++ ?BG_RED_CODE     ++ "m" ++ S ++ ?FG_RESET).
-define(BG_GREEN(S),   ?ESC ++ ?BG_GREEN_CODE   ++ "m" ++ S ++ ?FG_RESET).
-define(BG_YELLOW(S),  ?ESC ++ ?BG_YELLOW_CODE  ++ "m" ++ S ++ ?FG_RESET).
-define(BG_BLUE(S),    ?ESC ++ ?BG_BLUE_CODE    ++ "m" ++ S ++ ?FG_RESET).
-define(BG_MAGENTA(S), ?ESC ++ ?BG_MAGENTA_CODE ++ "m" ++ S ++ ?FG_RESET).
-define(BG_CYAN(S),    ?ESC ++ ?BG_CYAN_CODE    ++ "m" ++ S ++ ?FG_RESET).
-define(BG_WHITE(S),   ?ESC ++ ?BG_WHITE_CODE   ++ "m" ++ S ++ ?FG_RESET).

%%------------------------------
%% Foreground + Background combined
%%------------------------------
-define(FG_RED_BG_WHITE(S),     ?ESC ++ ?FG_RED_CODE     ++ ";" ++ ?BG_WHITE_CODE   ++ "m" ++ S ++ ?FG_RESET).
-define(FG_GREEN_BG_WHITE(S),   ?ESC ++ ?FG_GREEN_CODE   ++ ";" ++ ?BG_WHITE_CODE   ++ "m" ++ S ++ ?FG_RESET).
-define(FG_BLUE_BG_WHITE(S),    ?ESC ++ ?FG_BLUE_CODE    ++ ";" ++ ?BG_WHITE_CODE   ++ "m" ++ S ++ ?FG_RESET).
-define(FG_YELLOW_BG_BLACK(S),  ?ESC ++ ?FG_YELLOW_CODE  ++ ";" ++ ?BG_BLACK_CODE   ++ "m" ++ S ++ ?FG_RESET).
-define(FG_BLACK_BG_YELLOW(S),  ?ESC ++ ?FG_BLACK_CODE   ++ ";" ++ ?BG_YELLOW_CODE  ++ "m" ++ S ++ ?FG_RESET).
-define(FG_WHITE_BG_RED(S),     ?ESC ++ ?FG_WHITE_CODE   ++ ";" ++ ?BG_RED_CODE     ++ "m" ++ S ++ ?FG_RESET).
-define(FG_WHITE_BG_BLUE(S),    ?ESC ++ ?FG_WHITE_CODE   ++ ";" ++ ?BG_BLUE_CODE    ++ "m" ++ S ++ ?FG_RESET).
-define(FG_BLACK_BG_GREEN(S),   ?ESC ++ ?FG_BLACK_CODE   ++ ";" ++ ?BG_GREEN_CODE   ++ "m" ++ S ++ ?FG_RESET).
-define(FG_BLACK_BG_CYAN(S),    ?ESC ++ ?FG_BLACK_CODE   ++ ";" ++ ?BG_CYAN_CODE    ++ "m" ++ S ++ ?FG_RESET).

%%------------------------------
%% Text effects
%%------------------------------
-define(BOLD(S),      ?ESC ++ "1m" ++ S ++ ?FG_RESET).
-define(UNDERLINE(S), ?ESC ++ "4m" ++ S ++ ?FG_RESET).
-define(REVERSE(S),   ?ESC ++ "7m" ++ S ++ ?FG_RESET).
2 Likes

We’ll have something built-in soon o/

3 Likes