mirror of
https://github.com/neovim/neovim
synced 2025-07-16 01:01:49 +00:00
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:
@ -779,7 +779,7 @@ nvim_get_chan_info({chan}) *nvim_get_chan_info()*
|
||||
• {chan} channel_id, or 0 for current channel
|
||||
|
||||
Return: ~
|
||||
Dictionary describing a channel, with these keys:
|
||||
Channel info dict with these keys:
|
||||
• "id" Channel id.
|
||||
• "argv" (optional) Job arguments list.
|
||||
• "stream" Stream underlying the channel.
|
||||
@ -792,11 +792,11 @@ nvim_get_chan_info({chan}) *nvim_get_chan_info()*
|
||||
• "terminal" |terminal| instance interprets ASCII sequences.
|
||||
• "rpc" |RPC| communication on the channel is active.
|
||||
• "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
|
||||
still be present if a pty is used (e.g. for conpty on Windows).
|
||||
• "buffer" (optional) Buffer with connected |terminal| instance.
|
||||
device path like "/dev/pts/1". If unknown, the key will still be
|
||||
present if a pty is used (e.g. for conpty on Windows).
|
||||
• "buffer" (optional) Buffer connected to |terminal| instance.
|
||||
• "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()*
|
||||
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.
|
||||
• {attributes} Arbitrary string:string map of informal client
|
||||
properties. Suggested keys:
|
||||
• "pid": Process id.
|
||||
• "website": Client homepage URL (e.g. GitHub
|
||||
repository)
|
||||
• "license": License description ("Apache 2", "GPLv3",
|
||||
|
@ -6,14 +6,38 @@
|
||||
|
||||
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.
|
||||
|
||||
==============================================================================
|
||||
Starting the GUI *gui-start* *E229* *E233*
|
||||
Starting the GUI *gui-config* *gui-start*
|
||||
|
||||
*ginit.vim* *gui-init* *gvimrc* *$MYGVIMRC*
|
||||
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: >
|
||||
:autocmd UIEnter * let g:gui = filter(nvim_list_uis(),{k,v-> v.chan==v:event.chan})[0].rgb
|
||||
|
@ -189,6 +189,9 @@ TREESITTER
|
||||
|
||||
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
|
||||
now prefixed with "ui" instead of "?".
|
||||
|
||||
|
@ -6,28 +6,45 @@
|
||||
|
||||
Terminal UI *TUI* *tui*
|
||||
|
||||
Nvim uses a list of terminal capabilities to display its user interface
|
||||
(except in |--embed| and |--headless| modes). If that information is wrong,
|
||||
the screen may be messed up or keys may not be recognized.
|
||||
By default when you run `nvim` (without |--embed| or |--headless|) it starts
|
||||
the builtin "terminal UI" (TUI). This default UI is optional: you can run Nvim
|
||||
as a "headless" server, or you can use a |GUI|.
|
||||
|
||||
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
|
||||
|--headless| modes). The |$TERM| environment variable is the primary hint that
|
||||
determines the terminal type.
|
||||
|
||||
*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 system terminfo with one in $HOME/.terminfo/ directory, in part or in
|
||||
whole.
|
||||
|
||||
Building your own terminfo is usually as simple as running this as
|
||||
a non-superuser:
|
||||
The Unibilium library (used to read terminfo) allows you to override the
|
||||
system terminfo with one in the "$HOME/.terminfo/" directory. Building your
|
||||
own terminfo is usually as simple as running this:
|
||||
>
|
||||
curl -LO https://invisible-island.net/datafiles/current/terminfo.src.gz
|
||||
gunzip terminfo.src.gz
|
||||
|
@ -9,7 +9,7 @@ Nvim UI protocol *UI* *ui*
|
||||
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
|
||||
over the RPC API. The default UI model is a terminal-like grid with a single,
|
||||
|
@ -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.
|
||||
/// Suggested keys:
|
||||
/// - "pid": Process id.
|
||||
/// - "website": Client homepage URL (e.g. GitHub repository)
|
||||
/// - "license": License description ("Apache 2", "GPLv3", "MIT", …)
|
||||
/// - "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.
|
||||
///
|
||||
/// @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.
|
||||
/// - "argv" (optional) Job arguments list.
|
||||
/// - "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.
|
||||
/// - "terminal" |terminal| instance interprets ASCII sequences.
|
||||
/// - "rpc" |RPC| communication on the channel is active.
|
||||
/// - "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 still be present if a pty is used (e.g. for
|
||||
/// conpty on Windows).
|
||||
/// - "buffer" (optional) Buffer with connected |terminal| instance.
|
||||
/// - "client" (optional) Info about the peer (client on the other end of
|
||||
/// the RPC channel), if provided by it via
|
||||
/// |nvim_set_client_info()|.
|
||||
/// - "pty" (optional) Name of pseudoterminal. On a POSIX system this is a device path like
|
||||
/// "/dev/pts/1". If unknown, the key will still be present if a pty is used (e.g.
|
||||
/// for conpty on Windows).
|
||||
/// - "buffer" (optional) Buffer connected to |terminal| instance.
|
||||
/// - "client" (optional) Info about the peer (client on the other end of the RPC channel),
|
||||
/// which it provided via |nvim_set_client_info()|.
|
||||
///
|
||||
Dictionary nvim_get_chan_info(uint64_t channel_id, Integer chan, Arena *arena, Error *err)
|
||||
FUNC_API_SINCE(4)
|
||||
|
@ -351,7 +351,6 @@ int main(int argc, char **argv)
|
||||
|
||||
// NORETURN: Start builtin UI client.
|
||||
if (ui_client_channel_id) {
|
||||
time_finish();
|
||||
ui_client_run(remote_ui); // NORETURN
|
||||
}
|
||||
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++) {
|
||||
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 ---");
|
||||
break;
|
||||
}
|
||||
|
@ -464,7 +464,7 @@ static void tinput_timer_cb(uv_timer_t *handle)
|
||||
{
|
||||
TermInput *input = handle->data;
|
||||
// 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);
|
||||
if (size) {
|
||||
size_t consumed = handle_raw_buffer(input, true, input->read_stream.read_pos, size);
|
||||
|
@ -182,9 +182,10 @@ bool ui_override(void)
|
||||
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)
|
||||
@ -197,7 +198,7 @@ void ui_refresh(void)
|
||||
int height = INT_MAX;
|
||||
bool ext_widgets[kUIExtCount];
|
||||
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++) {
|
||||
RemoteUI *ui = uis[i];
|
||||
|
@ -24,6 +24,7 @@
|
||||
#include "nvim/msgpack_rpc/channel_defs.h"
|
||||
#include "nvim/os/os.h"
|
||||
#include "nvim/os/os_defs.h"
|
||||
#include "nvim/profile.h"
|
||||
#include "nvim/tui/tui.h"
|
||||
#include "nvim/tui/tui_defs.h"
|
||||
#include "nvim/ui.h"
|
||||
@ -81,12 +82,15 @@ uint64_t ui_client_start_server(int argc, char **argv)
|
||||
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)
|
||||
{
|
||||
//
|
||||
// nvim_ui_attach
|
||||
//
|
||||
MAXSIZE_TEMP_ARRAY(args, 3);
|
||||
ADD_C(args, INTEGER_OBJ(width));
|
||||
ADD_C(args, INTEGER_OBJ(height));
|
||||
|
||||
MAXSIZE_TEMP_DICT(opts, 9);
|
||||
PUT_C(opts, "rgb", BOOLEAN_OBJ(rgb));
|
||||
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) {
|
||||
PUT_C(opts, "term_name", CSTR_AS_OBJ(term));
|
||||
}
|
||||
|
||||
PUT_C(opts, "term_colors", INTEGER_OBJ(t_colors));
|
||||
if (!ui_client_is_remote) {
|
||||
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);
|
||||
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)
|
||||
@ -132,6 +169,8 @@ void ui_client_run(bool remote_ui)
|
||||
ELOG("test log message");
|
||||
}
|
||||
|
||||
time_finish();
|
||||
|
||||
// os_exit() will be invoked when the client channel detaches
|
||||
while (true) {
|
||||
LOOP_PROCESS_EVENTS(&main_loop, resize_events, -1);
|
||||
|
@ -96,8 +96,7 @@ end
|
||||
|
||||
--- @param method string
|
||||
--- @param ... any
|
||||
--- @return boolean
|
||||
--- @return table
|
||||
--- @return boolean, table
|
||||
function Session:request(method, ...)
|
||||
local args = { ... }
|
||||
local err, result
|
||||
|
@ -40,8 +40,8 @@ if t.skip(is_os('win')) then
|
||||
end
|
||||
|
||||
describe('TUI', function()
|
||||
local screen
|
||||
local child_session
|
||||
local screen --[[@type test.functional.ui.screen]]
|
||||
local child_session --[[@type test.Session]]
|
||||
local child_exec_lua
|
||||
|
||||
before_each(function()
|
||||
@ -1651,12 +1651,13 @@ describe('TUI', function()
|
||||
]])
|
||||
end)
|
||||
|
||||
it('in nvim_list_uis()', function()
|
||||
it('in nvim_list_uis(), sets nvim_set_client_info()', function()
|
||||
-- $TERM in :terminal.
|
||||
local exp_term = is_os('bsd') and 'builtin_xterm' or 'xterm-256color'
|
||||
local ui_chan = 1
|
||||
local expected = {
|
||||
{
|
||||
chan = 1,
|
||||
chan = ui_chan,
|
||||
ext_cmdline = false,
|
||||
ext_hlstate = false,
|
||||
ext_linegrid = true,
|
||||
@ -1679,6 +1680,43 @@ describe('TUI', function()
|
||||
}
|
||||
local _, rv = child_session:request('nvim_list_uis')
|
||||
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)
|
||||
|
||||
it('allows grid to assume wider ambiwidth chars than host terminal', function()
|
||||
|
@ -458,7 +458,7 @@ end
|
||||
--- @param argv string[]
|
||||
--- @param merge boolean?
|
||||
--- @param env string[]?
|
||||
--- @param keep boolean
|
||||
--- @param keep boolean?
|
||||
--- @param io_extra uv.uv_pipe_t? used for stdin_fd, see :help ui-option
|
||||
--- @return test.Session
|
||||
function M.spawn(argv, merge, env, keep, io_extra)
|
||||
|
Reference in New Issue
Block a user