一个字符一个字符地获取控制台用户输入



我在Elixir中有一个控制台应用程序。我需要通过按键来解释用户的输入。例如,我需要将"q"作为结束会话的命令,而不需要用户显式地按,也就是"回车"。

IO.getn/2出人意料地等待被按下,缓冲输入(我几乎可以肯定,这个缓冲是由控制台本身完成的,但man stty没有提供任何帮助/标志来关闭缓冲。)

Mix.Utils使用无限循环来隐藏用户输入(基本上每1ms向控制台发送退格控制序列),IEx代码封装对标准erlang的io的调用,这提供了在Tab(用于自动完成)上设置回调的唯一能力。

我的猜测是我必须使用Port,将其附加到:stdin并生成一个进程来侦听输入。不幸的是,我一直在尝试实现后者,因为我需要连接到当前运行的控制台,而不是创建到其他进程的新端口(正如这里完美描述的那样)。

我是否错过了一些明显的关于我如何将Port附加到当前进程' :stdin(这是btw在Port.list/0中列出的),或者我是否应该构建整个3管道架构来重定向类型为:stdin的内容以及我的程序想要的puts:stdout ?

您的程序没有获得键,因为在Linux上,终端默认处于熟模式,该模式缓冲所有按键,直到按下Return。

您需要将您的终端切换到原始模式,以便在按键发生时立即将其发送给应用程序。没有跨平台来做这个。

对于类unix系统,有ncurses,它有一个elixir绑定,您应该查看:https://github.com/jfreeze/ex_ncurses。它甚至有一个例子来做你想要的

我能想出的最简单的东西是基于这个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不可访问。

最新更新