feat(tui): $NVIM_TERMDEFS can override builtin terminfo #39555

Problem:
- Windows users can't use terminfo to configure their terminal
  capabilities. #37274
- Terminfo definitions sometimes get out of date or are simply
  inaccurate.
- Eventually, we may want to drop terminfo, relying primarily on
  built-in definitions. Users will still need some flexibility.

Solution:
Support $NVIM_TERMDEFS environment variable, which is JSON data that
defines "terminfo" definitions that override our builtin terminfo.
This commit is contained in:
Kyle
2026-05-17 12:02:46 -05:00
committed by GitHub
parent fff9897ce3
commit 84d5c5f4bf
11 changed files with 613 additions and 223 deletions

View File

@@ -249,6 +249,7 @@ TUI
• The TUI will re-query the terminal's background color when resuming from
a suspended state, and Nvim will update 'background' accordingly.
• TUI uses |$NVIM_TERMDEFS| to override terminfo entries.
UI

View File

@@ -50,6 +50,113 @@ own terminfo is usually as simple as running this:
gunzip terminfo.src.gz
tic -x terminfo.src
<
*$NVIM_TERMDEFS*
To override terminfo entries without modifying terminfo files, use
$NVIM_TERMDEFS. This may be useful on Windows, where Unibilium can't read
terminfo files. When this is set, terminfo files are not used, but builtin
terminfo entries are. Any values in this variable will override the builtin
definitions. For example, to disable alternate screen, override the sequence
nvim sends to reset the cursor style, and set the sequences that get
interpreted as <Home> and <S-Home>: >
NVIM_TERMDEFS='{
"enter_ca_mode": "",
"exit_ca_mode": "",
"reset_cursor_style": "\u001b[0 q",
"key_home": ["\u001bOH", "\u001b[1;2H"]
}' nvim
<
Be careful when putting escape sequences in strings, as JSON uses unicode
escapes like `\u001b`. The following fields are supported. For fields of type
`string[]`, the first entry is the unshifted variant of the key, and the
second entry is the shifted variant.
>
Field Type
------------------------------------------------------------
carriage_return string
change_scroll_region string
clear_screen string
clr_eol string
clr_eos string
cursor_address string
cursor_down string
cursor_home string
cursor_invisible string
cursor_left string
cursor_normal string
cursor_right string
cursor_up string
delete_line string
enter_blink_mode string
enter_bold_mode string
enter_ca_mode string
enter_dim_mode string
enter_italics_mode string
enter_reverse_mode string
enter_secure_mode string
enter_standout_mode string
enter_underline_mode string
erase_chars string
exit_attribute_mode string
exit_ca_mode string
from_status_line string
insert_line string
keypad_local string
keypad_xmit string
parm_delete_line string
parm_down_cursor string
parm_insert_line string
parm_left_cursor string
parm_right_cursor string
parm_up_cursor string
set_a_background string
set_a_foreground string
set_attributes string
set_lr_margin string
to_status_line string
enter_strikethrough_mode string
reset_cursor_color string
reset_cursor_style string
set_cursor_color string
set_cursor_style string
set_rgb_background string
set_rgb_foreground string
set_underline_style string
key_backspace string
key_btab string
key_clear string
key_npage string
key_ppage string
key_select string
key_beg string[]
key_dc string[]
key_end string[]
key_find string[]
key_home string[]
key_ic string[]
key_left string[]
key_right string[]
key_suspend string[]
key_undo string[]
key_f1 string
...
key_f63 string
RGB bool
Su bool
Tc bool
back_color_erase bool
columns int
lines int
max_colors int
<
*$TERM*
The $TERM environment variable must match the terminal you are using!
Otherwise Nvim cannot know what sequences your terminal expects, and weird

View File

@@ -158,4 +158,30 @@ function M.query_apc(payload, opts, on_response)
end)
end
--- Displays error even when this code runs in the context of the client
--- @param msg string
local function broadcast_error(msg)
vim.rpcnotify(0, 'nvim_echo', {
{
msg,
},
}, true, { err = true })
end
--- Get user overrides for terminfo entries as a table. See |$NVIM_TERMDEFS|
function M.get_termdefs()
local termdefs_raw = os.getenv('NVIM_TERMDEFS')
if termdefs_raw ~= nil then
local ok, termdefs_or_err = pcall(vim.json.decode, termdefs_raw)
if not ok then
broadcast_error('E557: Failed to parse $NVIM_TERMDEFS: ' .. vim.inspect(termdefs_or_err))
return
elseif type(termdefs_or_err) ~= 'table' or vim.isarray(termdefs_or_err) then
broadcast_error('E557: expected $NVIM_TERMDEFS to be table. :help $NVIM_TERMDEFS')
return
end
return termdefs_or_err
end
end
return M

View File

@@ -18,104 +18,13 @@ local url = 'https://invisible-island.net/datafiles/current/terminfo.src.gz'
local target_gen = 'src/nvim/tui/terminfo_builtin.h'
local target_enum = 'src/nvim/tui/terminfo_enum_defs.h'
local entries = {
{ 'ansi', 'ansi_terminfo' },
{ 'ghostty', 'ghostty_terminfo' }, -- Note: ncurses defs do not exactly match what ghostty ships.
{ 'interix', 'interix_8colour_terminfo' },
{ 'iterm2', 'iterm_256colour_terminfo' },
{ 'linux', 'linux_16colour_terminfo' },
{ 'putty-256color', 'putty_256colour_terminfo' },
{ 'rxvt-256color', 'rxvt_256colour_terminfo' },
{ 'screen-256color', 'screen_256colour_terminfo' },
{ 'st-256color', 'st_256colour_terminfo' },
{ 'tmux-256color', 'tmux_256colour_terminfo' },
{ 'vte-256color', 'vte_256colour_terminfo' },
{ 'xterm-256color', 'xterm_256colour_terminfo' },
{ 'cygwin', 'cygwin_terminfo' },
{ 'win32con', 'win32con_terminfo' },
{ 'conemu', 'conemu_terminfo' },
{ 'vtpcon', 'vtpcon_terminfo' },
}
local wanted_numbers = { 'max_colors', 'lines', 'columns' }
local wanted_strings = {
'carriage_return',
'change_scroll_region',
'clear_screen',
'clr_eol',
'clr_eos',
'cursor_address',
'cursor_down',
'cursor_invisible',
'cursor_left',
'cursor_home',
'cursor_normal',
'cursor_up',
'cursor_right',
'delete_line',
'enter_blink_mode',
'enter_bold_mode',
'enter_ca_mode',
'enter_dim_mode',
'enter_italics_mode',
'enter_reverse_mode',
'enter_secure_mode',
'enter_standout_mode',
'enter_underline_mode',
'erase_chars',
'exit_attribute_mode',
'exit_ca_mode',
'from_status_line',
'insert_line',
'keypad_local',
'keypad_xmit',
'parm_delete_line',
'parm_down_cursor',
'parm_insert_line',
'parm_left_cursor',
'parm_right_cursor',
'parm_up_cursor',
'set_a_background',
'set_a_foreground',
'set_attributes',
'set_lr_margin',
'to_status_line',
}
local wanted_strings_ext = {
-- the following are our custom name for extensions, see "extmap"
{ 'reset_cursor_style', 'Se' },
{ 'set_cursor_style', 'Ss' },
-- terminfo describes strikethrough modes as rmxx/smxx with respect
-- to the ECMA-48 strikeout/crossed-out attributes.
{ 'enter_strikethrough_mode', 'smxx' },
{ 'set_rgb_foreground', 'setrgbf' },
{ 'set_rgb_background', 'setrgbb' },
{ 'set_cursor_color', 'Cs' },
{ 'reset_cursor_color', 'Cr' },
{ 'set_underline_style', 'Smulx' },
}
-- Note: these are only consumed by driver-ti via it's table of "funcs" keys.
-- Second value is whether there is a "shift" variant in terminfo.
local wanted_termkeys = {
{ 'backspace', false },
{ 'beg', true }, -- sometimes known as: "begin"
{ 'btab', false },
{ 'clear', false },
{ 'dc', true },
{ 'end', true },
{ 'find', true },
{ 'home', true },
{ 'ic', true },
{ 'npage', false },
{ 'ppage', false },
{ 'select', false },
{ 'suspend', true },
{ 'undo', true },
{ 'left', true },
{ 'right', true },
}
local terminfo = require('src.gen.terminfo')
local entries = terminfo.builtin_terminals
local wanted_strings = terminfo.fields.strings
local wanted_strings_ext = terminfo.fields.strings_ext
local wanted_bools = terminfo.fields.bools
local wanted_ints = terminfo.fields.ints
local wanted_termkeys = terminfo.fields.termkeys
local db = '/tmp/nvim_terminfo'
if vim.uv.fs_stat(db) == nil then
@@ -173,7 +82,7 @@ f_enum:write([[
// but will then be encoded as chords. We might actually prefer that but it is
// potentially breaking change.
]])
local func_key_max = 63
local func_key_max = terminfo.fields.func_key_max
f_enum:write('#define kTerminfoFuncKeyMax ' .. func_key_max .. '\n')
f_enum:write('typedef enum {\n')
for _, item in ipairs(wanted_termkeys) do
@@ -223,12 +132,11 @@ for _, entry in ipairs(entries) do
end
f_defs:write('\nstatic const TerminfoEntry ' .. target .. ' = {\n')
f_defs:write(' .bce = ' .. tostring(bools.back_color_erase or false) .. ',\n')
local has_Tc_or_RGB = (bools.Tc or bools.RGB) or false
f_defs:write(' .has_Tc_or_RGB = ' .. tostring(has_Tc_or_RGB or false) .. ',\n')
f_defs:write(' .Su = ' .. tostring(bools.Su or false) .. ',\n')
for _, b in ipairs(wanted_bools) do
f_defs:write(' .' .. b .. ' = ' .. tostring(bools[b] or false) .. ',\n')
end
for _, name in ipairs(wanted_numbers) do
for _, name in ipairs(wanted_ints) do
f_defs:write(' .' .. name .. ' = ' .. (nums[name] or '-1') .. ',\n')
end
f_defs:write(' .defs = {\n')
@@ -269,7 +177,17 @@ for _, entry in ipairs(entries) do
f_defs:write('};\n')
end
f_defs:write('\n#define XLIST_TERMINFO_BUILTIN \\\n')
f_defs:write('\n#define XLIST_TERMINFO_BOOLS \\\n')
for _, item in ipairs(wanted_bools) do
f_defs:write(' X(' .. item .. ') \\\n')
end
f_defs:write('// end of list\n\n')
f_defs:write('#define XLIST_TERMINFO_INTS \\\n')
for _, item in ipairs(wanted_ints) do
f_defs:write(' X(' .. item .. ') \\\n')
end
f_defs:write('// end of list\n\n')
f_defs:write('#define XLIST_TERMINFO_BUILTIN \\\n')
for _, name in ipairs(wanted_strings) do
f_defs:write(' X(' .. name .. ') \\\n')
end
@@ -286,7 +204,7 @@ end
f_defs:write('// end of list\n\n')
f_defs:write('#define XLIST_TERMINFO_FKEYS \\\n')
for i = 1, func_key_max do
f_defs:write(' X(f' .. i .. ') \\\n')
f_defs:write(' X(f' .. i .. ', ' .. i - 1 .. ') \\\n')
end
f_defs:write('// end of list\n')
f_defs:close()

107
src/gen/terminfo.lua Normal file
View File

@@ -0,0 +1,107 @@
local M = {}
M.fields = {}
M.fields.bools = { 'back_color_erase', 'Tc', 'RGB', 'Su' }
M.fields.ints = { 'max_colors', 'lines', 'columns' }
M.builtin_terminals = {
{ 'ansi', 'ansi_terminfo' },
{ 'ghostty', 'ghostty_terminfo' }, -- Note: ncurses defs do not exactly match what ghostty ships.
{ 'interix', 'interix_8colour_terminfo' },
{ 'iterm2', 'iterm_256colour_terminfo' },
{ 'linux', 'linux_16colour_terminfo' },
{ 'putty-256color', 'putty_256colour_terminfo' },
{ 'rxvt-256color', 'rxvt_256colour_terminfo' },
{ 'screen-256color', 'screen_256colour_terminfo' },
{ 'st-256color', 'st_256colour_terminfo' },
{ 'tmux-256color', 'tmux_256colour_terminfo' },
{ 'vte-256color', 'vte_256colour_terminfo' },
{ 'xterm-256color', 'xterm_256colour_terminfo' },
{ 'cygwin', 'cygwin_terminfo' },
{ 'win32con', 'win32con_terminfo' },
{ 'conemu', 'conemu_terminfo' },
{ 'vtpcon', 'vtpcon_terminfo' },
}
M.fields.strings = {
'carriage_return',
'change_scroll_region',
'clear_screen',
'clr_eol',
'clr_eos',
'cursor_address',
'cursor_down',
'cursor_invisible',
'cursor_left',
'cursor_home',
'cursor_normal',
'cursor_up',
'cursor_right',
'delete_line',
'enter_blink_mode',
'enter_bold_mode',
'enter_ca_mode',
'enter_dim_mode',
'enter_italics_mode',
'enter_reverse_mode',
'enter_secure_mode',
'enter_standout_mode',
'enter_underline_mode',
'erase_chars',
'exit_attribute_mode',
'exit_ca_mode',
'from_status_line',
'insert_line',
'keypad_local',
'keypad_xmit',
'parm_delete_line',
'parm_down_cursor',
'parm_insert_line',
'parm_left_cursor',
'parm_right_cursor',
'parm_up_cursor',
'set_a_background',
'set_a_foreground',
'set_attributes',
'set_lr_margin',
'to_status_line',
}
M.fields.strings_ext = {
-- the following are our custom name for extensions, see "extmap"
{ 'reset_cursor_style', 'Se' },
{ 'set_cursor_style', 'Ss' },
-- terminfo describes strikethrough modes as rmxx/smxx with respect
-- to the ECMA-48 strikeout/crossed-out attributes.
{ 'enter_strikethrough_mode', 'smxx' },
{ 'set_rgb_foreground', 'setrgbf' },
{ 'set_rgb_background', 'setrgbb' },
{ 'set_cursor_color', 'Cs' },
{ 'reset_cursor_color', 'Cr' },
{ 'set_underline_style', 'Smulx' },
}
-- Note: these are only consumed by driver-ti via it's table of "funcs" keys.
-- Second value is whether there is a "shift" variant in terminfo.
M.fields.termkeys = {
{ 'backspace', false },
{ 'beg', true }, -- sometimes known as: "begin"
{ 'btab', false },
{ 'clear', false },
{ 'dc', true },
{ 'end', true },
{ 'find', true },
{ 'home', true },
{ 'ic', true },
{ 'npage', false },
{ 'ppage', false },
{ 'select', false },
{ 'suspend', true },
{ 'undo', true },
{ 'left', true },
{ 'right', true },
}
M.fields.func_key_max = 63
return M

View File

@@ -0,0 +1,52 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include "nvim/api/private/defs.h"
#include "nvim/map_defs.h"
#include "nvim/tui/terminfo_builtin.h"
#include "nvim/tui/terminfo_defs.h"
typedef struct {
char *name;
ObjectType type; // Lua type in decoded $NVIM_TERMDEFS
size_t offset; // offset in TerminfoEntry
} TermdefField;
// uncrustify:off
static const TermdefField termdef_field_entries[] = {
#define FIELD(n, t, off) { .name = n, .type = kObjectType##t, \
.offset = offsetof(TerminfoEntry, off) },
#define X(n) FIELD(#n, Boolean, n)
XLIST_TERMINFO_BOOLS
#undef X
#define X(n) FIELD(#n, Integer, n)
XLIST_TERMINFO_INTS
#undef X
#define X(n) FIELD(#n, String, defs[kTerm_##n])
XLIST_TERMINFO_BUILTIN
#undef X
#define X(n, code) FIELD(#n, String, defs[kTerm_##n])
XLIST_TERMINFO_EXT
#undef X
#define X(n) FIELD("key_" #n, String, keys[kTermKey_##n])
#define Y(n) FIELD("key_" #n, Array, keys[kTermKey_##n])
XYLIST_TERMINFO_KEYS
#undef X
#undef Y
#define X(n, idx) FIELD("key_" #n, String, f_keys[idx])
XLIST_TERMINFO_FKEYS
#undef X
#undef FIELD
};
// uncrustify:on
static PMap(cstr_t) termdef_fields = MAP_INIT;
static inline void init_termdef_fields(void)
{
for (size_t i = 0; i < ARRAY_SIZE(termdef_field_entries); i++) {
pmap_put(cstr_t)(&termdef_fields, termdef_field_entries[i].name,
(TermdefField *)&termdef_field_entries[i]);
}
}

View File

@@ -133,18 +133,21 @@ bool terminfo_from_database(TerminfoEntry *ti, char *termname, Arena *arena)
return false;
}
ti->bce = unibi_get_bool(ut, unibi_back_color_erase);
ti->back_color_erase = unibi_get_bool(ut, unibi_back_color_erase);
ti->max_colors = unibi_get_num(ut, unibi_max_colors);
ti->lines = unibi_get_num(ut, unibi_lines);
ti->columns = unibi_get_num(ut, unibi_columns);
// Check for Tc or RGB
ti->has_Tc_or_RGB = false;
ti->Tc = false;
ti->RGB = false;
ti->Su = false;
for (size_t i = 0; i < unibi_count_ext_bool(ut); i++) {
const char *n = unibi_get_ext_bool_name(ut, i);
if (n && (!strcmp(n, "Tc") || !strcmp(n, "RGB"))) {
ti->has_Tc_or_RGB = true;
if (n && !strcmp(n, "Tc")) {
ti->Tc = true;
} else if (n && !strcmp(n, "RGB")) {
ti->RGB = true;
} else if (n && !strcmp(n, "Su")) {
ti->Su = true;
}
@@ -200,7 +203,7 @@ bool terminfo_from_database(TerminfoEntry *ti, char *termname, Arena *arena)
}
static const enum unibi_string uni_fkeys[] = {
# define X(name) unibi_key_##name,
# define X(name, idx) unibi_key_##name,
XLIST_TERMINFO_FKEYS
# undef X
};
@@ -239,15 +242,16 @@ String terminfo_info_msg(const TerminfoEntry *ti, const char *termname, bool fro
kv_printf(data, "\n");
kv_printf(data, "Boolean capabilities:\n");
kv_printf(data, " back_color_erase: %s\n", fmt(ti->bce));
kv_printf(data, " truecolor ('Tc' or 'RGB'): %s\n", fmt(ti->has_Tc_or_RGB));
kv_printf(data, " back_color_erase: %s\n", fmt(ti->back_color_erase));
kv_printf(data, " truecolor ('Tc'): %s\n", fmt(ti->Tc));
kv_printf(data, " RGB: %s\n", fmt(ti->RGB));
kv_printf(data, " extended underline ('Su'): %s\n", fmt(ti->Su));
kv_printf(data, "\n");
kv_printf(data, "Numeric capabilities: (-1 for unknown)\n");
kv_printf(data, " lines: %d\n", ti->lines);
kv_printf(data, " columns: %d\n", ti->columns);
kv_printf(data, " max_colors: %d\n", ti->columns);
kv_printf(data, " max_colors: %d\n", ti->max_colors);
kv_printf(data, "\n");
kv_printf(data, "String capabilities:\n");
@@ -279,7 +283,7 @@ String terminfo_info_msg(const TerminfoEntry *ti, const char *termname, bool fro
#undef Y
};
for (size_t i = 0 + 1; i < ARRAY_SIZE(key_names); i++) {
for (size_t i = 0; i < ARRAY_SIZE(key_names); i++) {
const char *s = ti->keys[i][0];
if (s) {
kv_printf(data, " key_%-27s = ", key_names[i]);
@@ -294,12 +298,12 @@ String terminfo_info_msg(const TerminfoEntry *ti, const char *termname, bool fro
}
static const char *fkey_names[] = {
#define X(name) #name,
#define X(name, idx) #name,
XLIST_TERMINFO_FKEYS
#undef X
};
for (size_t i = 0 + 1; i < ARRAY_SIZE(fkey_names); i++) {
for (size_t i = 0; i < ARRAY_SIZE(fkey_names); i++) {
const char *s = ti->f_keys[i];
if (s) {
kv_printf(data, " key_%-27s = ", fkey_names[i]);

View File

@@ -1,14 +1,15 @@
// uncrustify:off
// Generated by src/gen/gen_terminfo.lua and ncurses 6.6.20251231
// Generated by src/gen/gen_terminfo.lua and ncurses 6.6.20251230
#pragma once
#include "nvim/tui/terminfo_defs.h"
static const TerminfoEntry ansi_terminfo = {
.bce = false,
.has_Tc_or_RGB = false,
.back_color_erase = false,
.Tc = false,
.RGB = false,
.Su = false,
.max_colors = 8,
.lines = 24,
@@ -88,8 +89,9 @@ static const TerminfoEntry ansi_terminfo = {
};
static const TerminfoEntry ghostty_terminfo = {
.bce = true,
.has_Tc_or_RGB = false,
.back_color_erase = true,
.Tc = false,
.RGB = false,
.Su = false,
.max_colors = 0x100,
.lines = 24,
@@ -232,8 +234,9 @@ static const TerminfoEntry ghostty_terminfo = {
};
static const TerminfoEntry interix_8colour_terminfo = {
.bce = true,
.has_Tc_or_RGB = false,
.back_color_erase = true,
.Tc = false,
.RGB = false,
.Su = false,
.max_colors = 8,
.lines = 25,
@@ -373,8 +376,9 @@ static const TerminfoEntry interix_8colour_terminfo = {
};
static const TerminfoEntry iterm_256colour_terminfo = {
.bce = true,
.has_Tc_or_RGB = false,
.back_color_erase = true,
.Tc = false,
.RGB = false,
.Su = false,
.max_colors = 0x100,
.lines = 24,
@@ -478,8 +482,9 @@ static const TerminfoEntry iterm_256colour_terminfo = {
};
static const TerminfoEntry linux_16colour_terminfo = {
.bce = true,
.has_Tc_or_RGB = false,
.back_color_erase = true,
.Tc = false,
.RGB = false,
.Su = false,
.max_colors = 8,
.lines = -1,
@@ -579,8 +584,9 @@ static const TerminfoEntry linux_16colour_terminfo = {
};
static const TerminfoEntry putty_256colour_terminfo = {
.bce = true,
.has_Tc_or_RGB = false,
.back_color_erase = true,
.Tc = false,
.RGB = false,
.Su = false,
.max_colors = 0x100,
.lines = -1,
@@ -680,8 +686,9 @@ static const TerminfoEntry putty_256colour_terminfo = {
};
static const TerminfoEntry rxvt_256colour_terminfo = {
.bce = true,
.has_Tc_or_RGB = false,
.back_color_erase = true,
.Tc = false,
.RGB = false,
.Su = false,
.max_colors = 0x100,
.lines = 24,
@@ -805,8 +812,9 @@ static const TerminfoEntry rxvt_256colour_terminfo = {
};
static const TerminfoEntry screen_256colour_terminfo = {
.bce = false,
.has_Tc_or_RGB = false,
.back_color_erase = false,
.Tc = false,
.RGB = false,
.Su = false,
.max_colors = 0x100,
.lines = 24,
@@ -898,8 +906,9 @@ static const TerminfoEntry screen_256colour_terminfo = {
};
static const TerminfoEntry st_256colour_terminfo = {
.bce = true,
.has_Tc_or_RGB = false,
.back_color_erase = true,
.Tc = false,
.RGB = false,
.Su = false,
.max_colors = 0x100,
.lines = 24,
@@ -1042,8 +1051,9 @@ static const TerminfoEntry st_256colour_terminfo = {
};
static const TerminfoEntry tmux_256colour_terminfo = {
.bce = false,
.has_Tc_or_RGB = false,
.back_color_erase = false,
.Tc = false,
.RGB = false,
.Su = false,
.max_colors = 0x100,
.lines = 24,
@@ -1186,8 +1196,9 @@ static const TerminfoEntry tmux_256colour_terminfo = {
};
static const TerminfoEntry vte_256colour_terminfo = {
.bce = true,
.has_Tc_or_RGB = false,
.back_color_erase = true,
.Tc = false,
.RGB = false,
.Su = false,
.max_colors = 0x100,
.lines = 24,
@@ -1330,8 +1341,9 @@ static const TerminfoEntry vte_256colour_terminfo = {
};
static const TerminfoEntry xterm_256colour_terminfo = {
.bce = true,
.has_Tc_or_RGB = false,
.back_color_erase = true,
.Tc = false,
.RGB = false,
.Su = false,
.max_colors = 0x100,
.lines = 24,
@@ -1474,8 +1486,9 @@ static const TerminfoEntry xterm_256colour_terminfo = {
};
static const TerminfoEntry cygwin_terminfo = {
.bce = false,
.has_Tc_or_RGB = false,
.back_color_erase = false,
.Tc = false,
.RGB = false,
.Su = false,
.max_colors = 8,
.lines = -1,
@@ -1575,8 +1588,9 @@ static const TerminfoEntry cygwin_terminfo = {
};
static const TerminfoEntry win32con_terminfo = {
.bce = false,
.has_Tc_or_RGB = false,
.back_color_erase = false,
.Tc = false,
.RGB = false,
.Su = false,
.max_colors = 8,
.lines = -1,
@@ -1700,8 +1714,9 @@ static const TerminfoEntry win32con_terminfo = {
};
static const TerminfoEntry conemu_terminfo = {
.bce = true,
.has_Tc_or_RGB = false,
.back_color_erase = true,
.Tc = false,
.RGB = false,
.Su = false,
.max_colors = 0x100,
.lines = 24,
@@ -1826,8 +1841,9 @@ static const TerminfoEntry conemu_terminfo = {
};
static const TerminfoEntry vtpcon_terminfo = {
.bce = true,
.has_Tc_or_RGB = false,
.back_color_erase = true,
.Tc = false,
.RGB = false,
.Su = false,
.max_colors = 0x100,
.lines = 24,
@@ -1951,6 +1967,19 @@ static const TerminfoEntry vtpcon_terminfo = {
},
};
#define XLIST_TERMINFO_BOOLS \
X(back_color_erase) \
X(Tc) \
X(RGB) \
X(Su) \
// end of list
#define XLIST_TERMINFO_INTS \
X(max_colors) \
X(lines) \
X(columns) \
// end of list
#define XLIST_TERMINFO_BUILTIN \
X(carriage_return) \
X(change_scroll_region) \
@@ -2026,67 +2055,67 @@ static const TerminfoEntry vtpcon_terminfo = {
// end of list
#define XLIST_TERMINFO_FKEYS \
X(f1) \
X(f2) \
X(f3) \
X(f4) \
X(f5) \
X(f6) \
X(f7) \
X(f8) \
X(f9) \
X(f10) \
X(f11) \
X(f12) \
X(f13) \
X(f14) \
X(f15) \
X(f16) \
X(f17) \
X(f18) \
X(f19) \
X(f20) \
X(f21) \
X(f22) \
X(f23) \
X(f24) \
X(f25) \
X(f26) \
X(f27) \
X(f28) \
X(f29) \
X(f30) \
X(f31) \
X(f32) \
X(f33) \
X(f34) \
X(f35) \
X(f36) \
X(f37) \
X(f38) \
X(f39) \
X(f40) \
X(f41) \
X(f42) \
X(f43) \
X(f44) \
X(f45) \
X(f46) \
X(f47) \
X(f48) \
X(f49) \
X(f50) \
X(f51) \
X(f52) \
X(f53) \
X(f54) \
X(f55) \
X(f56) \
X(f57) \
X(f58) \
X(f59) \
X(f60) \
X(f61) \
X(f62) \
X(f63) \
X(f1, 0) \
X(f2, 1) \
X(f3, 2) \
X(f4, 3) \
X(f5, 4) \
X(f6, 5) \
X(f7, 6) \
X(f8, 7) \
X(f9, 8) \
X(f10, 9) \
X(f11, 10) \
X(f12, 11) \
X(f13, 12) \
X(f14, 13) \
X(f15, 14) \
X(f16, 15) \
X(f17, 16) \
X(f18, 17) \
X(f19, 18) \
X(f20, 19) \
X(f21, 20) \
X(f22, 21) \
X(f23, 22) \
X(f24, 23) \
X(f25, 24) \
X(f26, 25) \
X(f27, 26) \
X(f28, 27) \
X(f29, 28) \
X(f30, 29) \
X(f31, 30) \
X(f32, 31) \
X(f33, 32) \
X(f34, 33) \
X(f35, 34) \
X(f36, 35) \
X(f37, 36) \
X(f38, 37) \
X(f39, 38) \
X(f40, 39) \
X(f41, 40) \
X(f42, 41) \
X(f43, 42) \
X(f44, 43) \
X(f45, 44) \
X(f46, 45) \
X(f47, 46) \
X(f48, 47) \
X(f49, 48) \
X(f50, 49) \
X(f51, 50) \
X(f52, 51) \
X(f53, 52) \
X(f54, 53) \
X(f55, 54) \
X(f56, 55) \
X(f57, 56) \
X(f58, 57) \
X(f59, 58) \
X(f60, 59) \
X(f61, 60) \
X(f62, 61) \
X(f63, 62) \
// end of list

View File

@@ -6,9 +6,10 @@
#include "nvim/tui/terminfo_enum_defs.h"
typedef struct {
bool bce;
bool back_color_erase;
// these extended booleans indicate likely 24-color support
bool has_Tc_or_RGB;
bool Tc;
bool RGB;
bool Su;
int max_colors;

View File

@@ -24,6 +24,7 @@
#include "nvim/grid.h"
#include "nvim/highlight_defs.h"
#include "nvim/log.h"
#include "nvim/lua/executor.h"
#include "nvim/macros_defs.h"
#include "nvim/main.h"
#include "nvim/map_defs.h"
@@ -35,6 +36,7 @@
#include "nvim/os/os_defs.h"
#include "nvim/strings.h"
#include "nvim/tui/input.h"
#include "nvim/tui/termdef_field_defs.h"
#include "nvim/tui/terminfo.h"
#include "nvim/tui/tui.h"
#include "nvim/tui/ugrid.h"
@@ -354,6 +356,108 @@ void tui_query_bg_color(TUIData *tui)
flush_buf(tui);
}
/// Use $NVIM_TERMDEFS to apply user overrides to terminfo.
///
/// This is necessary for Windows, where terminfo files are broken in Unibilium. #37274
/// If/when Unibilium is removed, this will also be useful for anyone who wants to override
/// the built-in definitions.
static void apply_termdefs(TUIData *tui)
{
// We allow empty values just to provide the user with a warning
if (!os_env_exists("NVIM_TERMDEFS", false)) {
return;
}
Error lua_err = ERROR_INIT;
Object rv = NLUA_EXEC_STATIC("return require('vim.tty').get_termdefs()",
(Array)ARRAY_DICT_INIT, kRetObject, NULL, &lua_err);
if (rv.type != kObjectTypeDict) {
return;
}
init_termdef_fields();
Arena err_arena = ARENA_EMPTY;
Array chunks = ARRAY_DICT_INIT;
int err_count = 0;
#define PUSH_ERR(fmt, ...) \
do { \
String str = arena_printf(&err_arena, fmt, __VA_ARGS__); \
Array err = arena_array(&err_arena, 2); \
ADD(err, STRING_OBJ(str)); \
ADD(err, STATIC_CSTR_AS_OBJ("ErrorMsg")); \
ADD(chunks, ARRAY_OBJ(err)); \
err_count++; \
} while (0)
for (size_t i = 0; i < rv.data.dict.size; i++) {
KeyValuePair kv = rv.data.dict.items[i];
TermdefField *td_field = pmap_get(cstr_t)(&termdef_fields, kv.key.data);
if (!td_field || kv.value.type != td_field->type) {
PUSH_ERR("skipping invalid field in $NVIM_TERMDEFS: '%s'\n", kv.key.data);
continue;
}
Object val = kv.value;
char *dst = (char *)&tui->ti + td_field->offset;
switch (val.type) {
case kObjectTypeBoolean:
*(bool *)dst = val.data.boolean;
break;
case kObjectTypeInteger:
*(int *)dst = (int)val.data.integer;
break;
case kObjectTypeString:
*(const char **)dst = arena_strdup(&tui->ti_arena,
val.data.string.data);
break;
case kObjectTypeArray:
if (val.data.array.size > 2) {
PUSH_ERR(
"skipping invalid field definition in $NVIM_TERMDEFS for %s: array should have at most two entries for unshifted and shifted key variants.\n",
kv.key.data);
continue;
}
*(const char **)dst = NULL;
*(const char **)(dst + sizeof(char *)) = NULL;
for (size_t key_map_idx = 0; key_map_idx < val.data.array.size;
key_map_idx++) {
Object key_mapping = val.data.array.items[key_map_idx];
if (key_mapping.type != kObjectTypeString) {
PUSH_ERR(
"skipping invalid field definition in $NVIM_TERMDEFS for %s[%zu]: expected type 'string'\n",
kv.key.data, key_map_idx);
continue;
}
size_t offset = key_map_idx * sizeof(char *);
*(const char **)(dst + offset) = arena_strdup(&tui->ti_arena, key_mapping.data.string.data);
}
break;
default:
break;
}
}
if (err_count > 0) {
MAXSIZE_TEMP_ARRAY(args, 3);
ADD_C(args, ARRAY_OBJ(chunks));
ADD_C(args, BOOLEAN_OBJ(true)); // history
MAXSIZE_TEMP_DICT(opts, 1);
PUT_C(opts, "err", BOOLEAN_OBJ(true));
ADD_C(args, DICT_OBJ(opts));
rpc_send_event(ui_client_channel_id, "nvim_echo", args);
}
arena_mem_free(arena_finish(&err_arena));
kv_destroy(chunks);
api_free_object(rv);
api_clear_error(&lua_err);
#undef PUSH_ERR
}
/// Enable the alternate screen and emit other control sequences to start the TUI.
///
/// This is also called when the TUI is resumed after being suspended. We reinitialize all state
@@ -396,7 +500,7 @@ static void terminfo_start(TUIData *tui)
// Set up terminfo.
tui->terminfo_found_in_db = false;
if (term) {
if (term && !os_env_exists("NVIM_TERMDEFS", false)) {
if (terminfo_from_database(&tui->ti, term, &tui->ti_arena)) {
tui->term = arena_strdup(&tui->ti_arena, term);
tui->terminfo_found_in_db = true;
@@ -436,6 +540,7 @@ static void terminfo_start(TUIData *tui)
patch_terminfo_bugs(tui, term, colorterm, vtev, konsolev, iterm_env, nsterm);
augment_terminfo(tui, term, vtev, konsolev, weztermv, iterm_env, nsterm);
apply_termdefs(tui);
#define TI_HAS(name) (tui->ti.defs[name] != NULL)
tui->can_change_scroll_region = TI_HAS(kTerm_change_scroll_region);
@@ -452,7 +557,7 @@ static void terminfo_start(TUIData *tui)
|| terminfo_is_term_family(term, "cygwin")
|| terminfo_is_term_family(term, "win32con")
|| terminfo_is_term_family(term, "interix");
tui->bce = tui->ti.bce;
tui->bce = tui->ti.back_color_erase;
// Set 't_Co' from the result of terminfo & fix_terminfo.
t_colors = tui->ti.max_colors;
// Enter alternate screen, save title, and clear.
@@ -2033,7 +2138,7 @@ static bool term_has_truecolor(TUIData *tui, const char *colorterm)
return true;
}
if (tui->ti.has_Tc_or_RGB) {
if (tui->ti.Tc || tui->ti.RGB) {
// terminfo had one of "Tc" or "RGB" extended boolean capabilities
return true;
}
@@ -2121,7 +2226,7 @@ static void patch_terminfo_bugs(TUIData *tui, const char *term, const char *colo
if (tmux || screen || kitty) {
// Disable BCE in some cases we know it is not working. #8806
tui->ti.bce = false;
tui->ti.back_color_erase = false;
}
if (xterm || hterm) {

View File

@@ -3917,8 +3917,11 @@ describe('TUI', function()
local screen --[[@type test.functional.ui.screen]]
-- Runs (child) `nvim` in a TTY (:terminal), to start the builtin TUI.
local function nvim_tui(extra_args)
local function nvim_tui(extra_args, extra_env)
clear()
extra_env = extra_env or {}
local env = vim.tbl_extend('force', { LANG = 'C' }, extra_env)
screen = tt.setup_child_nvim({
'--clean',
'--cmd',
@@ -3927,9 +3930,7 @@ describe('TUI', function()
nvim_set .. ' notermguicolors',
extra_args,
}, {
env = {
LANG = 'C',
},
env = env,
})
end
@@ -3956,6 +3957,45 @@ describe('TUI', function()
end)
end)
it('uses $NVIM_TERMDEFS to override terminfo', function()
local logfile = 'Xtest_terminfo_override_verbose_log'
finally(function()
os.remove(logfile)
end)
local terminfo = {
enter_ca_mode = 'ENTER_CA_MODE',
reset_cursor_style = '\027[0 q',
key_home = { 'ABC', 'DEF' },
key_npage = 'npage',
key_end = { 'GHI' },
key_f1 = 'JKL',
key_f63 = 'MNO',
Su = true,
max_colors = 999,
}
nvim_tui('-V3' .. logfile, { NVIM_TERMDEFS = vim.json.encode(terminfo) })
assert_log('enter_ca_mode[^\n]*= ' .. terminfo.enter_ca_mode, logfile, 9999)
assert_log(
'reset_cursor_style[^\n]*= ' .. vim.pesc(terminfo.reset_cursor_style:gsub('\027', '^[')),
logfile,
9999
)
assert_log(
string.format('key_home%%s*= %s, key_shome = %s', terminfo.key_home[1], terminfo.key_home[2]),
logfile,
9999
)
assert_log('key_npage%s*= ' .. terminfo.key_npage, logfile, 9999)
assert_log('key_end%s*= ' .. terminfo.key_end[1] .. '\n', logfile, 9999)
assert_log('key_f1%s*= ' .. terminfo.key_f1, logfile, 9999)
assert_log('key_f63%s*= ' .. terminfo.key_f63, logfile, 9999)
assert_log('extended underline[^\n]*: ' .. tostring(terminfo.Su), logfile, 9999)
assert_log('max_colors: ' .. terminfo.max_colors, logfile, 9999)
end)
it('does not crash on large inputs #26099', function()
nvim_tui()