逐字符获取控制台用户输入
Get console user input as typed, char by char
我在 Elixir 中有一个控制台应用程序。我需要在按键的基础上解释用户的输入。例如,我需要将“q”视为结束会话的命令,而不需要用户明确地按下 ⏎ a.k.a。 “马车return。”
IO.getn/2
令人惊讶地等待 ⏎ 被按下,缓冲输入(我几乎可以肯定,这个缓冲是由控制台本身完成的,但是 man stty
不提供任何 help/flag 来关闭缓冲。)
Mix.Utils
use the infinite loop 隐藏用户输入(基本上每 1 毫秒向控制台发送退格控制序列,)IEx
代码包装对标准 erlang 的 io
的调用,提供在 Tab 上设置回调的唯一能力(用于自动完成。)
我猜我必须使用 Port
, attach it to :stdin
and spawn a process to listen to the input. Unfortunately, I am stuck with trying to implement the latter, since I need to attach to the currently running console, not create a new port to some other process (as it is perfectly described here。)
我是否遗漏了一些关于如何将 Port
附加到当前进程的明显信息':stdin
(顺便说一句,在 Port.list/0
中列出)或者我应该构建了整个 3 管道架构,将输入的内容重定向到 :stdin
以及我的程序想要 puts
到 :stdout
?
的任何内容
您的程序无法获取按键,因为在 Linux 上,终端默认处于 cooked mode,它会缓冲所有按键,直到 Return 被按下。
您需要将终端切换到原始模式,这会在按键发生时立即将其发送到应用程序。没有跨平台可以做到这一点。
对于类 unix 系统,有 ncurses,它有一个 elixir 绑定,您应该查看:https://github.com/jfreeze/ex_ncurses。它甚至有一个例子来做你想做的事。
我能做的最简单的事情是基于 this github 回购协议。所以你需要以下内容:
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
已被劫持,并且对于每个按键你都会收到 Input received。唯一的问题是,当您按 q 时,服务器终止,但无法访问 stdin
。
我在 Elixir 中有一个控制台应用程序。我需要在按键的基础上解释用户的输入。例如,我需要将“q”视为结束会话的命令,而不需要用户明确地按下 ⏎ a.k.a。 “马车return。”
IO.getn/2
令人惊讶地等待 ⏎ 被按下,缓冲输入(我几乎可以肯定,这个缓冲是由控制台本身完成的,但是 man stty
不提供任何 help/flag 来关闭缓冲。)
Mix.Utils
use the infinite loop 隐藏用户输入(基本上每 1 毫秒向控制台发送退格控制序列,)IEx
代码包装对标准 erlang 的 io
的调用,提供在 Tab 上设置回调的唯一能力(用于自动完成。)
我猜我必须使用 Port
, attach it to :stdin
and spawn a process to listen to the input. Unfortunately, I am stuck with trying to implement the latter, since I need to attach to the currently running console, not create a new port to some other process (as it is perfectly described here。)
我是否遗漏了一些关于如何将 Port
附加到当前进程的明显信息':stdin
(顺便说一句,在 Port.list/0
中列出)或者我应该构建了整个 3 管道架构,将输入的内容重定向到 :stdin
以及我的程序想要 puts
到 :stdout
?
您的程序无法获取按键,因为在 Linux 上,终端默认处于 cooked mode,它会缓冲所有按键,直到 Return 被按下。
您需要将终端切换到原始模式,这会在按键发生时立即将其发送到应用程序。没有跨平台可以做到这一点。
对于类 unix 系统,有 ncurses,它有一个 elixir 绑定,您应该查看:https://github.com/jfreeze/ex_ncurses。它甚至有一个例子来做你想做的事。
我能做的最简单的事情是基于 this github 回购协议。所以你需要以下内容:
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
已被劫持,并且对于每个按键你都会收到 Input received。唯一的问题是,当您按 q 时,服务器终止,但无法访问 stdin
。