Получите ввод пользовательского ввода в консоль, char на char

У меня есть консольное приложение в Elixir. Мне нужно интерпретировать ввод пользователей на основе нажатия клавиш. Например, мне нужно рассматривать "q" как команду для завершения сеанса, без необходимости явно нажимать a.k.a. "возврат каретки".

IO.getn/2 неожиданно ждет нажатия , буферизации ввода (я почти уверен, что эта буферизация выполняется самой консолью, но man stty не предоставляет никакой помощи/флага, чтобы отключить буферизацию.)

Mix.Utils использовать бесконечный цикл, чтобы скрыть ввод пользователя (в основном, посылать контрольную последовательность backspace для консоли каждые 1 мс); IEx код обертывает вызовы стандартным erlangs io, который предоставляет единственную возможность установить обратный вызов на Tab (для автозаполнения.)

Мое предположение: я должен использовать Port, прикрепить его к :stdin и вызвать процесс для прослушивания ввода. К сожалению, я застрял в попытке реализовать последнее, так как мне нужно подключиться к текущей запущенной консоли, а не создавать новый порт для какого-либо другого процесса (как это отлично описано здесь.)

Я пропустил что-то очевидное о том, как мне прикрепить Port к текущему процессу :stdin (который указан btw в Port.list/0), или должен ли Ive построить всю 3-трубную архитектуру для перенаправления введенных до :stdin и независимо от того, что моя программа хочет puts до :stdout?

Ответ 1

Ваша программа не получает ключи, потому что в Linux терминал по умолчанию находится в cooked mode, который буферизует все нажатия клавиш до тех пор, пока Return не будет нажат.

Вам нужно переключить терминал в режим raw, который отправляет нажатия клавиш в приложение, как только они появятся. Для этого нет кросс-платформы.

Для unix-подобных систем существуют ncurses, которые имеют привязку эликсира, которую вы должны проверить: https://github.com/jfreeze/ex_ncurses. У этого даже есть пример, чтобы сделать то, что Вы хотите.

Ответ 2

Простейшая вещь, которую я мог бы приготовить, основана на этом github repo. Поэтому вам нужно следующее:

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;
}

скомпилируйте его с помощью gcc -o reader.so -fpic -shared reader.c. Тогда вам понадобится 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.

Скомпилируйте его с помощью erlc reader.erl.

Затем в iex :reader.start(); :reader.read() он выдает предупреждение о том, что stdin был захвачен, и для каждого нажатия клавиши вы получаете ввод. Единственная проблема заключается в том, что при нажатии q сервер завершает работу, но stdin недоступен.