I have a console application in Elixir. I need to interpret user’s input on by keypress basis. For instance, I need to treat “q” as a command to end the session, without use
Your program doesn't get the keys because on Linux, the terminal is by default in cooked mode, which buffers all keypresses until Return is pressed.
You need to switch your terminal to raw mode, which sends the keypresses to the application as soon as they happen. There's no cross-platform to do this.
For unix-like systems there's ncurses, which has an elixir binding that you should check out: https://github.com/jfreeze/ex_ncurses. It even has an example to do what you want.
The simplest thing I could cook up is based on this github repo. So you need the following:
reader.c
#include "erl_driver.h"
#include <stdio.h>
typedef struct {
ErlDrvPort drv_port;
} state;
static ErlDrvData start(ErlDrvPort port, char *command) {
state *st = (state *)driver_alloc(sizeof(state));
st->drv_port = port;
set_port_control_flags(port, PORT_CONTROL_FLAG_BINARY);
driver_select(st->drv_port, (ErlDrvEvent)(size_t)fileno(stdin), DO_READ, 1);
return (ErlDrvData)st;
}
static void stop(ErlDrvData drvstate) {
state *st = (state *)drvstate;
driver_select(st->drv_port, (ErlDrvEvent)(size_t)fileno(stdin), DO_READ, 0);
driver_free(drvstate);
}
static void do_getch(ErlDrvData drvstate, ErlDrvEvent event) {
state *st = (state *)drvstate;
char* buf = malloc(1);
buf[0] = getchar();
driver_output(st->drv_port, buf, 1);
}
ErlDrvEntry driver_entry = {
NULL,
start,
stop,
NULL,
do_getch,
NULL,
"reader",
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
ERL_DRV_EXTENDED_MARKER,
ERL_DRV_EXTENDED_MAJOR_VERSION,
ERL_DRV_EXTENDED_MINOR_VERSION
};
DRIVER_INIT(reader) {
return &driver_entry;
}
compile it with gcc -o reader.so -fpic -shared reader.c
.
Then you'll need in reader.erl
-module(reader).
-behaviour(gen_server).
-export([start/0, init/1, terminate/2, read/0, handle_cast/2, code_change/3, handle_call/3, handle_info/2, getch/0]).
-record(state, {port, caller}).
start() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, no_args, []).
getch() ->
gen_server:call(?MODULE, getch, infinity).
handle_call(getch, From, #state{caller = undefined} = State) ->
{noreply, State#state{caller = From}};
handle_call(getch, _From, State) ->
{reply, -1, State}.
handle_info({_Port, {data, _Binary}}, #state{ caller = undefined } = State) ->
{noreply, State};
handle_info({_Port, {data, Binary}}, State) ->
gen_server:reply(State#state.caller, binary_to_list(Binary)),
{noreply, State#state{ caller = undefined }}.
init(no_args) ->
case erl_ddll:load(".","reader") of
ok ->
Port = erlang:open_port({spawn, "reader"}, [binary]),
{ok, #state{port = Port}};
{error, ErrorCode} ->
exit({driver_error, erl_ddll:format_error(ErrorCode)})
end.
handle_cast(stop, State) ->
{stop, normal, State};
handle_cast(_, State) ->
{noreply, State}.
code_change(_, State, _) ->
{noreply, State}.
terminate(_Reason, State) ->
erlang:port_close(State#state.port),
erl_ddll:unload("reader").
read() ->
C = getch(),
case C of
"q" ->
gen_server:cast(?MODULE, stop);
_ ->
io:fwrite("Input received~n",[]),
read()
end.
Compile it with erlc reader.erl
.
Then in iex :reader.start(); :reader.read()
it issues a warning that stdin
has been hijacked, and for every keypress you get Input received. The only problem is that when you press q the server terminates, but the stdin
is not accessible.