refactor(ui): simplify stdin handling

This commit is contained in:
bfredl 2022-04-22 20:56:31 +02:00
parent 619c8f4b91
commit ad63b94b03
9 changed files with 106 additions and 53 deletions

View File

@ -52,6 +52,11 @@ with these (optional) keys:
`term_name` Sets the name of the terminal 'term'. `term_name` Sets the name of the terminal 'term'.
`term_colors` Sets the number of supported colors 't_Co'. `term_colors` Sets the number of supported colors 't_Co'.
`term_background` Sets the default value of 'background'. `term_background` Sets the default value of 'background'.
`stdin_fd` Read buffer from `fd` as if it was a stdin pipe
This option can only used by |--embed| ui,
see |ui-startup-stdin|.
Specifying an unknown option is an error; UIs can check the |api-metadata| Specifying an unknown option is an error; UIs can check the |api-metadata|
`ui_options` key for supported options. `ui_options` key for supported options.
@ -140,6 +145,19 @@ procedure:
Inside this request handler, the UI can safely do any initialization before Inside this request handler, the UI can safely do any initialization before
entering normal mode, for example reading variables set by init.vim. entering normal mode, for example reading variables set by init.vim.
*ui-startup-stdin*
An UI can support the native read from stdin feature as invoked with
`command | nvim -` for the builtin TUI. |--|
The embedding process can detect that its stdin is open to a file which
not is a terminal, just like nvim does. It then needs to forward this fd
to Nvim. As fd=0 is already is used to send rpc data from the embedder to
Nvim, it needs to use some other file descriptor, like fd=3 or higher.
Then, `stdin_fd` option should be passed to `nvim_ui_attach` and nvim will
implicitly read it as a buffer. This option can only be used when Nvim is
launched with `--embed` option, as described above.
============================================================================== ==============================================================================
Global Events *ui-global* Global Events *ui-global*

View File

@ -283,6 +283,22 @@ static void ui_set_option(UI *ui, bool init, String name, Object value, Error *e
return; return;
} }
if (strequal(name.data, "stdin_fd")) {
if (value.type != kObjectTypeInteger || value.data.integer < 0) {
api_set_error(error, kErrorTypeValidation, "stdin_fd must be a non-negative Integer");
return;
}
if (starting != NO_SCREEN) {
api_set_error(error, kErrorTypeValidation,
"stdin_fd can only be used with first attached ui");
return;
}
stdin_fd = (int)value.data.integer;
return;
}
// LEGACY: Deprecated option, use `ext_cmdline` instead. // LEGACY: Deprecated option, use `ext_cmdline` instead.
bool is_popupmenu = strequal(name.data, "popupmenu_external"); bool is_popupmenu = strequal(name.data, "popupmenu_external");

View File

@ -2661,35 +2661,3 @@ end:
xfree(cmdline); xfree(cmdline);
return result; return result;
} }
/// Invokes the nvim server to read from stdin when it is not a tty
///
/// It enables functionalities like:
/// - echo "1f u c4n r34d th1s u r34lly n33d t0 g37 r357"| nvim -
/// - cat path/to/a/file | nvim -
/// It has to be called before |nvim_ui_attach()| is called in order
/// to ensure proper functioning.
///
/// @param channel_id: The channel id of the GUI-client
/// @param filedesc: The file descriptor of the GUI-client process' stdin
/// @param implicit: Tells if read_stdin call is implicit.
/// i.e for cases like `echo xxx | nvim`
/// @param[out] err Error details, if any
void nvim_read_stdin(uint64_t channel_id, Integer filedesc, Error *err)
FUNC_API_SINCE(6) FUNC_API_REMOTE_ONLY
{
if (starting != NO_SCREEN) {
api_set_error(err, kErrorTypeValidation,
"nvim_read_stdin must be called before nvim_ui_attach");
return;
}
if (filedesc < 0) {
api_set_error(err, kErrorTypeValidation,
"file descriptor must be non-negative");
return;
}
stdin_filedesc = (int)filedesc;
implicit_readstdin = implicit;
return;
}

View File

@ -176,7 +176,7 @@ void filemess(buf_T *buf, char_u *name, char_u *s, int attr)
int readfile(char_u *fname, char_u *sfname, linenr_T from, linenr_T lines_to_skip, int readfile(char_u *fname, char_u *sfname, linenr_T from, linenr_T lines_to_skip,
linenr_T lines_to_read, exarg_T *eap, int flags, bool silent) linenr_T lines_to_read, exarg_T *eap, int flags, bool silent)
{ {
int fd = 0; int fd = stdin_fd >= 0 ? stdin_fd : 0;
int newfile = (flags & READ_NEW); int newfile = (flags & READ_NEW);
int check_readonly; int check_readonly;
int filtering = (flags & READ_FILTER); int filtering = (flags & READ_FILTER);
@ -1722,17 +1722,19 @@ failed:
xfree(buffer); xfree(buffer);
if (read_stdin) { if (read_stdin) {
close(0); close(fd);
if (stdin_fd < 0) {
#ifndef WIN32 #ifndef WIN32
// On Unix, use stderr for stdin, makes shell commands work. // On Unix, use stderr for stdin, makes shell commands work.
vim_ignored = dup(2); vim_ignored = dup(2);
#else #else
// On Windows, use the console input handle for stdin. // On Windows, use the console input handle for stdin.
HANDLE conin = CreateFile("CONIN$", GENERIC_READ | GENERIC_WRITE, HANDLE conin = CreateFile("CONIN$", GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ, (LPSECURITY_ATTRIBUTES)NULL, FILE_SHARE_READ, (LPSECURITY_ATTRIBUTES)NULL,
OPEN_EXISTING, 0, (HANDLE)NULL); OPEN_EXISTING, 0, (HANDLE)NULL);
vim_ignored = _open_osfhandle(conin, _O_RDONLY); vim_ignored = _open_osfhandle(conin, _O_RDONLY);
#endif #endif
}
} }
if (tmpname != NULL) { if (tmpname != NULL) {

View File

@ -507,6 +507,9 @@ EXTERN int v_dying INIT(= 0);
EXTERN int stdin_isatty INIT(= true); EXTERN int stdin_isatty INIT(= true);
// is stdout a terminal? // is stdout a terminal?
EXTERN int stdout_isatty INIT(= true); EXTERN int stdout_isatty INIT(= true);
/// filedesc set by embedder for reading first buffer like `cmd | nvim -`
EXTERN int stdin_fd INIT(= -1);
// true when doing full-screen output, otherwise only writing some messages. // true when doing full-screen output, otherwise only writing some messages.
// volatile because it is used in a signal handler. // volatile because it is used in a signal handler.
EXTERN volatile int full_screen INIT(= false); EXTERN volatile int full_screen INIT(= false);
@ -704,7 +707,6 @@ EXTERN int RedrawingDisabled INIT(= 0);
EXTERN int readonlymode INIT(= false); // Set to true for "view" EXTERN int readonlymode INIT(= false); // Set to true for "view"
EXTERN int recoverymode INIT(= false); // Set to true for "-r" option EXTERN int recoverymode INIT(= false); // Set to true for "-r" option
EXTERN int stdin_filedesc INIT(= -1); // stdin filedesc set by embedder
// typeahead buffer // typeahead buffer
EXTERN typebuf_T typebuf INIT(= { NULL, NULL, 0, 0, 0, 0, 0, 0, 0 }); EXTERN typebuf_T typebuf INIT(= { NULL, NULL, 0, 0, 0, 0, 0, 0, 0 });
@ -848,10 +850,6 @@ EXTERN linenr_T printer_page_num;
EXTERN bool typebuf_was_filled INIT(= false); // received text from client EXTERN bool typebuf_was_filled INIT(= false); // received text from client
// or from feedkeys() // or from feedkeys()
EXTERN bool implicit_readstdin INIT(= false); // Used in embed job created
// by TUI process only in
// builtin tui
#ifdef BACKSLASH_IN_FILENAME #ifdef BACKSLASH_IN_FILENAME
EXTERN char psepc INIT(= '\\'); // normal path separator character EXTERN char psepc INIT(= '\\'); // normal path separator character
EXTERN char psepcN INIT(= '/'); // abnormal path separator character EXTERN char psepcN INIT(= '/'); // abnormal path separator character

View File

@ -454,7 +454,7 @@ int main(int argc, char **argv)
// writing end of the pipe doesn't like, e.g., in case stdin and stderr // writing end of the pipe doesn't like, e.g., in case stdin and stderr
// are the same terminal: "cat | vim -". // are the same terminal: "cat | vim -".
// Using autocommands here may cause trouble... // Using autocommands here may cause trouble...
if ((params.edit_type == EDIT_STDIN || implicit_readstdin) && !recoverymode) { if ((params.edit_type == EDIT_STDIN || stdin_fd >= 0) && !recoverymode) {
read_stdin(); read_stdin();
} }

View File

@ -361,14 +361,15 @@ local function remove_args(args, args_rm)
return new_args return new_args
end end
function module.spawn(argv, merge, env, keep) --- @param io_extra used for stdin_fd, see :help ui-option
function module.spawn(argv, merge, env, keep, io_extra)
if session and not keep then if session and not keep then
session:close() session:close()
end end
local child_stream = ChildProcessStream.spawn( local child_stream = ChildProcessStream.spawn(
merge and module.merge_args(prepend_argv, argv) or argv, merge and module.merge_args(prepend_argv, argv) or argv,
env) env, io_extra)
return Session.new(child_stream) return Session.new(child_stream)
end end
@ -415,8 +416,8 @@ end
-- clear('-e') -- clear('-e')
-- clear{args={'-e'}, args_rm={'-i'}, env={TERM=term}} -- clear{args={'-e'}, args_rm={'-i'}, env={TERM=term}}
function module.clear(...) function module.clear(...)
local argv, env = module.new_argv(...) local argv, env, io_extra = module.new_argv(...)
module.set_session(module.spawn(argv, nil, env)) module.set_session(module.spawn(argv, nil, env, nil, io_extra))
end end
-- Builds an argument list for use in clear(). -- Builds an argument list for use in clear().
@ -426,6 +427,7 @@ function module.new_argv(...)
local args = {unpack(module.nvim_argv)} local args = {unpack(module.nvim_argv)}
table.insert(args, '--headless') table.insert(args, '--headless')
local new_args local new_args
local io_extra
local env = nil local env = nil
local opts = select(1, ...) local opts = select(1, ...)
if type(opts) == 'table' then if type(opts) == 'table' then
@ -461,13 +463,14 @@ function module.new_argv(...)
end end
end end
new_args = opts.args or {} new_args = opts.args or {}
io_extra = opts.io_extra
else else
new_args = {...} new_args = {...}
end end
for _, arg in ipairs(new_args) do for _, arg in ipairs(new_args) do
table.insert(args, arg) table.insert(args, arg)
end end
return args, env return args, env, io_extra
end end
function module.insert(...) function module.insert(...)

View File

@ -1,3 +1,5 @@
local uv = require'luv'
local helpers = require('test.functional.helpers')(after_each) local helpers = require('test.functional.helpers')(after_each)
local Screen = require('test.functional.ui.screen') local Screen = require('test.functional.ui.screen')
@ -98,3 +100,49 @@ end
describe('--embed UI on startup (ext_linegrid=true)', function() test_embed(true) end) describe('--embed UI on startup (ext_linegrid=true)', function() test_embed(true) end)
describe('--embed UI on startup (ext_linegrid=false)', function() test_embed(false) end) describe('--embed UI on startup (ext_linegrid=false)', function() test_embed(false) end)
describe('--embed UI', function()
it('can pass stdin', function()
local pipe = assert(uv.pipe())
local writer = assert(uv.new_pipe(false))
writer:open(pipe.write)
clear {args_rm={'--headless'}, io_extra=pipe.read}
-- attach immediately after startup, for early UI
local screen = Screen.new(40, 8)
screen:attach {stdin_fd=3}
screen:set_default_attr_ids {
[1] = {bold = true, foreground = Screen.colors.Blue1};
[2] = {bold = true};
}
writer:write "hello nvim\nfrom external input\n"
writer:shutdown(function() writer:close() end)
screen:expect{grid=[[
^hello nvim |
from external input |
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
|
]]}
-- stdin (rpc input) still works
feed 'o'
screen:expect{grid=[[
hello nvim |
^ |
from external input |
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{2:-- INSERT --} |
]]}
end)
end)

View File

@ -225,7 +225,7 @@ if(USE_BUNDLED_BUSTED)
# nvim-client: https://github.com/neovim/lua-client # nvim-client: https://github.com/neovim/lua-client
add_custom_command(OUTPUT ${ROCKS_DIR}/nvim-client add_custom_command(OUTPUT ${ROCKS_DIR}/nvim-client
COMMAND ${LUAROCKS_BINARY} COMMAND ${LUAROCKS_BINARY}
ARGS build nvim-client 0.2.2-1 ${LUAROCKS_BUILDARGS} ARGS build nvim-client 0.2.3-1 ${LUAROCKS_BUILDARGS}
DEPENDS luv) DEPENDS luv)
add_custom_target(nvim-client DEPENDS ${ROCKS_DIR}/nvim-client) add_custom_target(nvim-client DEPENDS ${ROCKS_DIR}/nvim-client)