diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index c2bcb61da3..1d7dc96b79 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -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 diff --git a/runtime/doc/tui.txt b/runtime/doc/tui.txt index 42d1ad91c3..55d56cd925 100644 --- a/runtime/doc/tui.txt +++ b/runtime/doc/tui.txt @@ -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 and : > + + 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 diff --git a/runtime/lua/vim/tty.lua b/runtime/lua/vim/tty.lua index b70f4f7887..4e2acb7fc5 100644 --- a/runtime/lua/vim/tty.lua +++ b/runtime/lua/vim/tty.lua @@ -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 diff --git a/src/gen/gen_terminfo.lua b/src/gen/gen_terminfo.lua index 4431389a71..9b3ff080cc 100644 --- a/src/gen/gen_terminfo.lua +++ b/src/gen/gen_terminfo.lua @@ -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() diff --git a/src/gen/terminfo.lua b/src/gen/terminfo.lua new file mode 100644 index 0000000000..c604b78f15 --- /dev/null +++ b/src/gen/terminfo.lua @@ -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 diff --git a/src/nvim/tui/termdef_field_defs.h b/src/nvim/tui/termdef_field_defs.h new file mode 100644 index 0000000000..30bbb71310 --- /dev/null +++ b/src/nvim/tui/termdef_field_defs.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#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]); + } +} diff --git a/src/nvim/tui/terminfo.c b/src/nvim/tui/terminfo.c index 493454d75c..a40fc95dff 100644 --- a/src/nvim/tui/terminfo.c +++ b/src/nvim/tui/terminfo.c @@ -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]); diff --git a/src/nvim/tui/terminfo_builtin.h b/src/nvim/tui/terminfo_builtin.h index 40c5e9fc1d..06e1d9feea 100644 --- a/src/nvim/tui/terminfo_builtin.h +++ b/src/nvim/tui/terminfo_builtin.h @@ -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 diff --git a/src/nvim/tui/terminfo_defs.h b/src/nvim/tui/terminfo_defs.h index 546bb2ec4e..77da31d357 100644 --- a/src/nvim/tui/terminfo_defs.h +++ b/src/nvim/tui/terminfo_defs.h @@ -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; diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 6d277bd7e5..c9673caa27 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -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) { diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index e93288d2f1..d3ca47d3ff 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -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()