feat(tui): builtin UI (TUI) sets client info #30397

Problem:
The default builtin UI client does not declare its client info. This
reduces discoverability and makes it difficult for plugins to identify
the UI.

Solution:
- Call nvim_set_client_info after attaching, as recommended by `:help dev-ui`.
- Also set the "pid" field.
- Also change `ui_active()` to return a count. Not directly relevant to
  this commit, but will be useful later.
This commit is contained in:
Justin M. Keyes
2024-09-18 04:14:06 -07:00
committed by GitHub
parent 22553e1f38
commit ff85e54939
13 changed files with 163 additions and 43 deletions

View File

@ -779,7 +779,7 @@ nvim_get_chan_info({chan}) *nvim_get_chan_info()*
• {chan} channel_id, or 0 for current channel • {chan} channel_id, or 0 for current channel
Return: ~ Return: ~
Dictionary describing a channel, with these keys: Channel info dict with these keys:
• "id" Channel id. • "id" Channel id.
• "argv" (optional) Job arguments list. • "argv" (optional) Job arguments list.
• "stream" Stream underlying the channel. • "stream" Stream underlying the channel.
@ -792,11 +792,11 @@ nvim_get_chan_info({chan}) *nvim_get_chan_info()*
• "terminal" |terminal| instance interprets ASCII sequences. • "terminal" |terminal| instance interprets ASCII sequences.
• "rpc" |RPC| communication on the channel is active. • "rpc" |RPC| communication on the channel is active.
• "pty" (optional) Name of pseudoterminal. On a POSIX system this is a • "pty" (optional) Name of pseudoterminal. On a POSIX system this is a
device path like "/dev/pts/1". If the name is unknown, the key will device path like "/dev/pts/1". If unknown, the key will still be
still be present if a pty is used (e.g. for conpty on Windows). present if a pty is used (e.g. for conpty on Windows).
• "buffer" (optional) Buffer with connected |terminal| instance. • "buffer" (optional) Buffer connected to |terminal| instance.
• "client" (optional) Info about the peer (client on the other end of • "client" (optional) Info about the peer (client on the other end of
the RPC channel), if provided by it via |nvim_set_client_info()|. the RPC channel), which it provided via |nvim_set_client_info()|.
nvim_get_color_by_name({name}) *nvim_get_color_by_name()* nvim_get_color_by_name({name}) *nvim_get_color_by_name()*
Returns the 24-bit RGB value of a |nvim_get_color_map()| color name or Returns the 24-bit RGB value of a |nvim_get_color_map()| color name or
@ -1285,6 +1285,7 @@ nvim_set_client_info({name}, {version}, {type}, {methods}, {attributes})
inclusive. inclusive.
• {attributes} Arbitrary string:string map of informal client • {attributes} Arbitrary string:string map of informal client
properties. Suggested keys: properties. Suggested keys:
• "pid": Process id.
• "website": Client homepage URL (e.g. GitHub • "website": Client homepage URL (e.g. GitHub
repository) repository)
• "license": License description ("Apache 2", "GPLv3", • "license": License description ("Apache 2", "GPLv3",

View File

@ -6,14 +6,38 @@
Nvim Graphical User Interface *gui* *GUI* Nvim Graphical User Interface *gui* *GUI*
Any client that supports the Nvim |ui-protocol| can be used as a UI for Nvim.
And multiple UIs can connect to the same Nvim instance! The terms "UI" and
"GUI" are often used interchangeably because all Nvim UI clients have the same
potential capabilities; the "TUI" refers to a UI client that outputs to your
terminal, whereas a "GUI" outputs directly to the OS graphics system.
Except where noted, this document describes UI capabilities available to both
TUI and GUI (assuming the UI supports the given feature). See |TUI| for notes
specific to the terminal UI. Help tags with the "gui-" prefix refer to UI
features, whereas help tags with the "ui-" prefix refer to the |ui-protocol|.
Nvim provides a default, builtin UI (the |TUI|), but there are many other
(third-party) GUIs that you can use instead:
- Firenvim (Nvim in your web browser!) https://github.com/glacambre/firenvim
- vscode-neovim (Nvim in VSCode!) https://github.com/vscode-neovim/vscode-neovim
- Neovide https://neovide.dev/
- Goneovim https://github.com/akiyosi/goneovim
- Nvy https://github.com/RMichelsen/Nvy
- Neovim-Qt (Qt5) https://github.com/equalsraf/neovim-qt
- VimR (macOS) https://github.com/qvacua/vimr
- Others https://github.com/neovim/neovim/wiki/Related-projects#gui
Type |gO| to see the table of contents. Type |gO| to see the table of contents.
============================================================================== ==============================================================================
Starting the GUI *gui-start* *E229* *E233* Starting the GUI *gui-config* *gui-start*
*ginit.vim* *gui-init* *gvimrc* *$MYGVIMRC* *ginit.vim* *gui-init* *gvimrc* *$MYGVIMRC*
For GUI-specific configuration Nvim provides the |UIEnter| event. This For GUI-specific configuration Nvim provides the |UIEnter| event. This
happens after other |initialization|s, like reading your vimrc file. happens after other |initialization|s, or whenever a UI attaches (multiple UIs
can connect to any Nvim instance).
Example: this sets "g:gui" to the value of the UI's "rgb" field: > Example: this sets "g:gui" to the value of the UI's "rgb" field: >
:autocmd UIEnter * let g:gui = filter(nvim_list_uis(),{k,v-> v.chan==v:event.chan})[0].rgb :autocmd UIEnter * let g:gui = filter(nvim_list_uis(),{k,v-> v.chan==v:event.chan})[0].rgb

View File

@ -189,6 +189,9 @@ TREESITTER
TUI TUI
• The builtin UI declares info |nvim_set_client_info()| on its channel. See
|startup-tui|. To see the current UI info, try this: >
:lua =vim.api.nvim_get_chan_info(vim.api.nvim_list_uis()[1].chan)
• |log| messages written by the builtin UI client (TUI, |--remote-ui|) are • |log| messages written by the builtin UI client (TUI, |--remote-ui|) are
now prefixed with "ui" instead of "?". now prefixed with "ui" instead of "?".

View File

@ -6,28 +6,45 @@
Terminal UI *TUI* *tui* Terminal UI *TUI* *tui*
Nvim uses a list of terminal capabilities to display its user interface By default when you run `nvim` (without |--embed| or |--headless|) it starts
(except in |--embed| and |--headless| modes). If that information is wrong, the builtin "terminal UI" (TUI). This default UI is optional: you can run Nvim
the screen may be messed up or keys may not be recognized. as a "headless" server, or you can use a |GUI|.
Type |gO| to see the table of contents. Type |gO| to see the table of contents.
============================================================================== ==============================================================================
Startup *startup-terminal* Startup *startup-tui* *startup-terminal*
Nvim has a client-server architecture: by default when you run `nvim`, this
starts the builtin UI client, which starts a `nvim --embed` server (child)
process that the UI client connects to. After attaching to the server, the UI
client calls |nvim_set_client_info()| (as recommended for all UIs |dev-ui|)
and sets these fields on its channel: >
client = {
attributes = {
license = 'Apache 2',
pid = …,
website = 'https://neovim.io',
},
name = 'nvim-tui',
type = 'ui',
version = { … },
}
Nvim guesses the terminal type when it starts (except in |--embed| and Nvim guesses the terminal type when it starts (except in |--embed| and
|--headless| modes). The |$TERM| environment variable is the primary hint that |--headless| modes). The |$TERM| environment variable is the primary hint that
determines the terminal type. determines the terminal type.
*terminfo* *E557* *E558* *E559* *terminfo* *E557* *E558* *E559*
The terminfo database is used if available. To display its user interface, Nvim reads a list of "terminal capabilities"
from the system terminfo database (or builtin defaults if terminfo is not
found). If that information is wrong, the screen may be messed up or keys may
not be recognized.
The Unibilium library (used by Nvim to read terminfo) allows you to override The Unibilium library (used to read terminfo) allows you to override the
the system terminfo with one in $HOME/.terminfo/ directory, in part or in system terminfo with one in the "$HOME/.terminfo/" directory. Building your
whole. own terminfo is usually as simple as running this:
Building your own terminfo is usually as simple as running this as
a non-superuser:
> >
curl -LO https://invisible-island.net/datafiles/current/terminfo.src.gz curl -LO https://invisible-island.net/datafiles/current/terminfo.src.gz
gunzip terminfo.src.gz gunzip terminfo.src.gz

View File

@ -9,7 +9,7 @@ Nvim UI protocol *UI* *ui*
Type |gO| to see the table of contents. Type |gO| to see the table of contents.
============================================================================== ==============================================================================
UI Events *ui-events* UI Events *ui-protocol* *ui-events*
UIs can be implemented as external client processes communicating with Nvim UIs can be implemented as external client processes communicating with Nvim
over the RPC API. The default UI model is a terminal-like grid with a single, over the RPC API. The default UI model is a terminal-like grid with a single,

View File

@ -1586,6 +1586,7 @@ Array nvim_get_api_info(uint64_t channel_id, Arena *arena)
/// ///
/// @param attributes Arbitrary string:string map of informal client properties. /// @param attributes Arbitrary string:string map of informal client properties.
/// Suggested keys: /// Suggested keys:
/// - "pid": Process id.
/// - "website": Client homepage URL (e.g. GitHub repository) /// - "website": Client homepage URL (e.g. GitHub repository)
/// - "license": License description ("Apache 2", "GPLv3", "MIT", …) /// - "license": License description ("Apache 2", "GPLv3", "MIT", …)
/// - "logo": URI or path to image, preferably small logo or icon. /// - "logo": URI or path to image, preferably small logo or icon.
@ -1627,7 +1628,7 @@ void nvim_set_client_info(uint64_t channel_id, String name, Dictionary version,
/// Gets information about a channel. /// Gets information about a channel.
/// ///
/// @param chan channel_id, or 0 for current channel /// @param chan channel_id, or 0 for current channel
/// @returns Dictionary describing a channel, with these keys: /// @returns Channel info dict with these keys:
/// - "id" Channel id. /// - "id" Channel id.
/// - "argv" (optional) Job arguments list. /// - "argv" (optional) Job arguments list.
/// - "stream" Stream underlying the channel. /// - "stream" Stream underlying the channel.
@ -1639,14 +1640,12 @@ void nvim_set_client_info(uint64_t channel_id, String name, Dictionary version,
/// - "bytes" Send and receive raw bytes. /// - "bytes" Send and receive raw bytes.
/// - "terminal" |terminal| instance interprets ASCII sequences. /// - "terminal" |terminal| instance interprets ASCII sequences.
/// - "rpc" |RPC| communication on the channel is active. /// - "rpc" |RPC| communication on the channel is active.
/// - "pty" (optional) Name of pseudoterminal. On a POSIX system this /// - "pty" (optional) Name of pseudoterminal. On a POSIX system this is a device path like
/// is a device path like "/dev/pts/1". If the name is unknown, /// "/dev/pts/1". If unknown, the key will still be present if a pty is used (e.g.
/// the key will still be present if a pty is used (e.g. for /// for conpty on Windows).
/// conpty on Windows). /// - "buffer" (optional) Buffer connected to |terminal| instance.
/// - "buffer" (optional) Buffer with connected |terminal| instance. /// - "client" (optional) Info about the peer (client on the other end of the RPC channel),
/// - "client" (optional) Info about the peer (client on the other end of /// which it provided via |nvim_set_client_info()|.
/// the RPC channel), if provided by it via
/// |nvim_set_client_info()|.
/// ///
Dictionary nvim_get_chan_info(uint64_t channel_id, Integer chan, Arena *arena, Error *err) Dictionary nvim_get_chan_info(uint64_t channel_id, Integer chan, Arena *arena, Error *err)
FUNC_API_SINCE(4) FUNC_API_SINCE(4)

View File

@ -351,7 +351,6 @@ int main(int argc, char **argv)
// NORETURN: Start builtin UI client. // NORETURN: Start builtin UI client.
if (ui_client_channel_id) { if (ui_client_channel_id) {
time_finish();
ui_client_run(remote_ui); // NORETURN ui_client_run(remote_ui); // NORETURN
} }
assert(!ui_client_channel_id && !use_builtin_ui); assert(!ui_client_channel_id && !use_builtin_ui);
@ -1514,7 +1513,7 @@ static void init_startuptime(mparm_T *paramp)
} }
for (int i = 1; i < paramp->argc - 1; i++) { for (int i = 1; i < paramp->argc - 1; i++) {
if (STRICMP(paramp->argv[i], "--startuptime") == 0) { if (STRICMP(paramp->argv[i], "--startuptime") == 0) {
time_init(paramp->argv[i + 1], is_embed ? "Embedded" : "Primary/TUI"); time_init(paramp->argv[i + 1], is_embed ? "Embedded" : "Primary (or UI client)");
time_start("--- NVIM STARTING ---"); time_start("--- NVIM STARTING ---");
break; break;
} }

View File

@ -464,7 +464,7 @@ static void tinput_timer_cb(uv_timer_t *handle)
{ {
TermInput *input = handle->data; TermInput *input = handle->data;
// If the raw buffer is not empty, process the raw buffer first because it is // If the raw buffer is not empty, process the raw buffer first because it is
// processing an incomplete bracketed paster sequence. // processing an incomplete bracketed paste sequence.
size_t size = rstream_available(&input->read_stream); size_t size = rstream_available(&input->read_stream);
if (size) { if (size) {
size_t consumed = handle_raw_buffer(input, true, input->read_stream.read_pos, size); size_t consumed = handle_raw_buffer(input, true, input->read_stream.read_pos, size);

View File

@ -182,9 +182,10 @@ bool ui_override(void)
return false; return false;
} }
bool ui_active(void) /// Gets the number of UIs connected to this server.
size_t ui_active(void)
{ {
return ui_count > 0; return ui_count;
} }
void ui_refresh(void) void ui_refresh(void)
@ -197,7 +198,7 @@ void ui_refresh(void)
int height = INT_MAX; int height = INT_MAX;
bool ext_widgets[kUIExtCount]; bool ext_widgets[kUIExtCount];
bool inclusive = ui_override(); bool inclusive = ui_override();
memset(ext_widgets, ui_active(), ARRAY_SIZE(ext_widgets)); memset(ext_widgets, !!ui_active(), ARRAY_SIZE(ext_widgets));
for (size_t i = 0; i < ui_count; i++) { for (size_t i = 0; i < ui_count; i++) {
RemoteUI *ui = uis[i]; RemoteUI *ui = uis[i];

View File

@ -24,6 +24,7 @@
#include "nvim/msgpack_rpc/channel_defs.h" #include "nvim/msgpack_rpc/channel_defs.h"
#include "nvim/os/os.h" #include "nvim/os/os.h"
#include "nvim/os/os_defs.h" #include "nvim/os/os_defs.h"
#include "nvim/profile.h"
#include "nvim/tui/tui.h" #include "nvim/tui/tui.h"
#include "nvim/tui/tui_defs.h" #include "nvim/tui/tui_defs.h"
#include "nvim/ui.h" #include "nvim/ui.h"
@ -81,12 +82,15 @@ uint64_t ui_client_start_server(int argc, char **argv)
return channel->id; return channel->id;
} }
/// Attaches this client to the UI channel, and sets its client info.
void ui_client_attach(int width, int height, char *term, bool rgb) void ui_client_attach(int width, int height, char *term, bool rgb)
{ {
//
// nvim_ui_attach
//
MAXSIZE_TEMP_ARRAY(args, 3); MAXSIZE_TEMP_ARRAY(args, 3);
ADD_C(args, INTEGER_OBJ(width)); ADD_C(args, INTEGER_OBJ(width));
ADD_C(args, INTEGER_OBJ(height)); ADD_C(args, INTEGER_OBJ(height));
MAXSIZE_TEMP_DICT(opts, 9); MAXSIZE_TEMP_DICT(opts, 9);
PUT_C(opts, "rgb", BOOLEAN_OBJ(rgb)); PUT_C(opts, "rgb", BOOLEAN_OBJ(rgb));
PUT_C(opts, "ext_linegrid", BOOLEAN_OBJ(true)); PUT_C(opts, "ext_linegrid", BOOLEAN_OBJ(true));
@ -94,7 +98,6 @@ void ui_client_attach(int width, int height, char *term, bool rgb)
if (term) { if (term) {
PUT_C(opts, "term_name", CSTR_AS_OBJ(term)); PUT_C(opts, "term_name", CSTR_AS_OBJ(term));
} }
PUT_C(opts, "term_colors", INTEGER_OBJ(t_colors)); PUT_C(opts, "term_colors", INTEGER_OBJ(t_colors));
if (!ui_client_is_remote) { if (!ui_client_is_remote) {
PUT_C(opts, "stdin_tty", BOOLEAN_OBJ(stdin_isatty)); PUT_C(opts, "stdin_tty", BOOLEAN_OBJ(stdin_isatty));
@ -108,6 +111,40 @@ void ui_client_attach(int width, int height, char *term, bool rgb)
rpc_send_event(ui_client_channel_id, "nvim_ui_attach", args); rpc_send_event(ui_client_channel_id, "nvim_ui_attach", args);
ui_client_attached = true; ui_client_attached = true;
TIME_MSG("nvim_ui_attach");
//
// nvim_set_client_info
//
MAXSIZE_TEMP_ARRAY(args2, 5);
ADD_C(args2, CSTR_AS_OBJ("nvim-tui")); // name
Object m = api_metadata();
Dictionary version = { 0 };
assert(m.data.dictionary.size > 0);
for (size_t i = 0; i < m.data.dictionary.size; i++) {
if (strequal(m.data.dictionary.items[i].key.data, "version")) {
version = m.data.dictionary.items[i].value.data.dictionary;
break;
} else if (i + 1 == m.data.dictionary.size) {
abort();
}
}
ADD_C(args2, DICTIONARY_OBJ(version)); // version
ADD_C(args2, CSTR_AS_OBJ("ui")); // type
// We don't send api_metadata.functions as the "methods" because:
// 1. it consumes memory.
// 2. it is unlikely to be useful, since the peer can just call `nvim_get_api`.
// 3. nvim_set_client_info expects a dict instead of an array.
ADD_C(args2, ARRAY_OBJ((Array)ARRAY_DICT_INIT)); // methods
MAXSIZE_TEMP_DICT(info, 9); // attributes
PUT_C(info, "website", CSTR_AS_OBJ("https://neovim.io"));
PUT_C(info, "license", CSTR_AS_OBJ("Apache 2"));
PUT_C(info, "pid", INTEGER_OBJ(os_get_pid()));
ADD_C(args2, DICTIONARY_OBJ(info)); // attributes
rpc_send_event(ui_client_channel_id, "nvim_set_client_info", args2);
TIME_MSG("nvim_set_client_info");
} }
void ui_client_detach(void) void ui_client_detach(void)
@ -132,6 +169,8 @@ void ui_client_run(bool remote_ui)
ELOG("test log message"); ELOG("test log message");
} }
time_finish();
// os_exit() will be invoked when the client channel detaches // os_exit() will be invoked when the client channel detaches
while (true) { while (true) {
LOOP_PROCESS_EVENTS(&main_loop, resize_events, -1); LOOP_PROCESS_EVENTS(&main_loop, resize_events, -1);

View File

@ -96,8 +96,7 @@ end
--- @param method string --- @param method string
--- @param ... any --- @param ... any
--- @return boolean --- @return boolean, table
--- @return table
function Session:request(method, ...) function Session:request(method, ...)
local args = { ... } local args = { ... }
local err, result local err, result

View File

@ -40,8 +40,8 @@ if t.skip(is_os('win')) then
end end
describe('TUI', function() describe('TUI', function()
local screen local screen --[[@type test.functional.ui.screen]]
local child_session local child_session --[[@type test.Session]]
local child_exec_lua local child_exec_lua
before_each(function() before_each(function()
@ -1651,12 +1651,13 @@ describe('TUI', function()
]]) ]])
end) end)
it('in nvim_list_uis()', function() it('in nvim_list_uis(), sets nvim_set_client_info()', function()
-- $TERM in :terminal. -- $TERM in :terminal.
local exp_term = is_os('bsd') and 'builtin_xterm' or 'xterm-256color' local exp_term = is_os('bsd') and 'builtin_xterm' or 'xterm-256color'
local ui_chan = 1
local expected = { local expected = {
{ {
chan = 1, chan = ui_chan,
ext_cmdline = false, ext_cmdline = false,
ext_hlstate = false, ext_hlstate = false,
ext_linegrid = true, ext_linegrid = true,
@ -1679,6 +1680,43 @@ describe('TUI', function()
} }
local _, rv = child_session:request('nvim_list_uis') local _, rv = child_session:request('nvim_list_uis')
eq(expected, rv) eq(expected, rv)
---@type table
local expected_version = ({
child_session:request('nvim_exec_lua', 'return vim.version()', {}),
})[2]
-- vim.version() returns `prerelease` string. Coerce it to boolean.
expected_version.prerelease = not not expected_version.prerelease
local expected_chan_info = {
client = {
attributes = {
license = 'Apache 2',
-- pid = 5371,
website = 'https://neovim.io',
},
methods = {},
name = 'nvim-tui',
type = 'ui',
version = expected_version,
},
id = ui_chan,
mode = 'rpc',
stream = 'stdio',
}
local status, chan_info = child_session:request('nvim_get_chan_info', ui_chan)
ok(status)
local info = chan_info.client
ok(info.attributes.pid and info.attributes.pid > 0, 'PID', info.attributes.pid or 'nil')
ok(info.version.major >= 0)
ok(info.version.minor >= 0)
ok(info.version.patch >= 0)
-- Delete variable fields so we can deep-compare.
info.attributes.pid = nil
eq(expected_chan_info, chan_info)
end) end)
it('allows grid to assume wider ambiwidth chars than host terminal', function() it('allows grid to assume wider ambiwidth chars than host terminal', function()

View File

@ -458,7 +458,7 @@ end
--- @param argv string[] --- @param argv string[]
--- @param merge boolean? --- @param merge boolean?
--- @param env string[]? --- @param env string[]?
--- @param keep boolean --- @param keep boolean?
--- @param io_extra uv.uv_pipe_t? used for stdin_fd, see :help ui-option --- @param io_extra uv.uv_pipe_t? used for stdin_fd, see :help ui-option
--- @return test.Session --- @return test.Session
function M.spawn(argv, merge, env, keep, io_extra) function M.spawn(argv, merge, env, keep, io_extra)