perf(tui): faster implementation of terminfo

The processing of terminfo can be separated into two steps:

1. The initialization of terminfo, which includes trying to find $TERM
   in a terminfo database file. As a fallback, common terminfo
   definitions are compiled in. After this, we apply a lot of ad-hoc
   patching to cover over limitations of terminfo.

2. While processing updates from nvim, actually using terminfo strings
   and formatting them with runtime values. for this part, terminfo
   essentially is a hyper-enhanced version of snprintf(), including
   a sm0l stack based virtual machine which can manipulate the runtime
   parameters.

This PR completely replaces libuniblium for step 2, with code
vendored from NetBSD's libtermkey which has been adapted to use typesafe
input parameters and to write into an output buffer in place.

The most immedatiate effects is a performance enhancement of
update_attrs() which is a very hot function when profiling the
TUI-process part of screen updates. In a stupid microbenchmark
(essentially calling nvim__screenshot over and over in a loop) this
leads to a speedup of ca 1.5x for redrawing the screen on the TUI-side.
What this means in practise when using nvim as a text editor is probably
no noticible effect at all, and when reabusing nvim as idk a full screen
RGB ASCII art rendrer maybe an increase from 72 to 75 FPS LMAO.

As nice side-effect, reduce the usage of unibilium to initialization only..
which will make it easier to remove, replace or make unibilium optional,
adressing #31989. Specifically, the builtin fallback doesn't use
unibilium at all, so a unibilium-free build is in principle possible
if the builtin definitions are good enough.

As a caveat, this PR doesn't touch libtermkey at all, which still has a
conditional dependency on unibilium. This will be investigated in a
follow-up PR

Note: the check of $TERMCOLOR was moved from tui/tui.c to
_defaults.lua in d7651b27d5 as we want to
skip the logic in _defaults.lua if the env var was set, but there
is no harm in TUI getting the right value when the TUI is trying to
initialize its terminfo shenanigans. Also this check is needed when
a TUI connects to a `--headless` server later, which will observe
a different $TERMCOLOR value than the nvim core process itself.
This commit is contained in:
bfredl
2025-09-29 13:18:59 +02:00
parent 671841673e
commit e656d7be2e
10 changed files with 2133 additions and 3579 deletions

View File

@@ -1,87 +0,0 @@
#!/usr/bin/env bash
#
# usage: ./scripts/update_terminfo.sh
#
# This script does:
#
# 1. Download Dickey's terminfo.src
# 2. Compile temporary terminfo database from terminfo.src
# 3. Use database to generate src/nvim/tui/terminfo_defs.h
#
set -e
url='https://invisible-island.net/datafiles/current/terminfo.src.gz'
target='src/nvim/tui/terminfo_defs.h'
readonly -A entries=(
[ansi]=ansi_terminfo
[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
)
db="$(mktemp -du)"
print_bold() {
printf "\\e[1m%b\\e[0m" "$*"
}
cd "$(git rev-parse --show-toplevel)"
#
# Get terminfo.src
#
print_bold '[*] Get terminfo.src\n'
curl -O "$url"
gunzip -f terminfo.src.gz
#
# Build terminfo database
#
print_bold '[*] Build terminfo database\n'
cat terminfo.src scripts/windows.ti | tic -x -o "$db" -
rm -f terminfo.src
#
# Write src/nvim/tui/terminfo_defs.h
#
print_bold "[*] Writing $target... "
sorted_terms="$(echo "${!entries[@]}" | tr ' ' '\n' | sort | xargs)"
cat > "$target" <<EOF
// uncrustify:off
// Generated by scripts/update_terminfo.sh and $(tic -V)
#pragma once
#include <stdint.h>
EOF
for term in $sorted_terms; do
path="$(find "$db" -name "$term")"
if [ -z "$path" ]; then
>&2 echo "Not found: $term. Skipping."
continue
fi
printf '\n'
infocmp -L -x -1 -A "$db" "$term" | sed -e '1d' -e 's#^#// #' | tr '\t' ' '
printf 'static const int8_t %s[] = {\n' "${entries[$term]}"
printf ' '
od -v -t d1 < "$path" | cut -c9- | xargs | tr ' ' ','
printf '};\n'
done >> "$target"
print_bold 'done\n'

204
src/gen/gen_terminfo.lua Normal file
View File

@@ -0,0 +1,204 @@
-- usage: nvim -ll src/gen/gen_terminfo.lua
--
-- This script does:
--
-- 1. Download Dickey's terminfo.src
-- 2. Compile temporary terminfo database from terminfo.src
-- 3. Use database to generate src/nvim/tui/terminfo_defs.h
local url = 'https://invisible-island.net/datafiles/current/terminfo.src.gz'
local target_gen = 'src/nvim/tui/terminfo_defs.h'
local target_enum = 'src/nvim/tui/terminfo_enum_defs.h'
local entries = {
{ 'ansi', 'ansi_terminfo' },
{ '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_bold_mode',
'enter_ca_mode',
'enter_italics_mode',
'enter_reverse_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' },
}
local db = '/tmp/nvim_terminfo'
if vim.uv.fs_stat(db) == nil then
local function sys(cmd)
print(cmd)
os.execute(cmd)
end
sys('curl -O ' .. url)
sys('gunzip -f terminfo.src.gz')
sys('cat terminfo.src scripts/windows.ti | tic -x -o "' .. db .. '" -')
sys('rm -f terminfo.src')
else
print('using cached terminfo in ' .. db)
end
local function enumify(str)
return 'kTerm_' .. str
end
local function quote(str)
if str == nil then
return 'NULL'
end
-- remungle the strings to look like C strings
str = string.gsub(str, '\\E', '\\033')
str = string.gsub(str, '%^G', '\\a')
str = string.gsub(str, '%^H', '\\b')
str = string.gsub(str, '%^O', '\\017') -- o dod
-- str = string.gsub(str, "\\", "\\\\")
str = string.gsub(str, '"', '\\"')
return '"' .. str .. '"'
end
local dbg = function() end
-- dbg = print
local f_enum = io.open(target_enum, 'wb')
f_enum:write('// genenerated by src/gen/gen_terminfo.lua\n\n')
f_enum:write('#pragma once\n\n')
f_enum:write('typedef enum {\n')
for _, name in ipairs(wanted_strings) do
f_enum:write(' ' .. enumify(name) .. ',\n')
end
f_enum:write('#define kTermExtOffset ' .. enumify(wanted_strings_ext[1][1]) .. '\n')
for _, item in ipairs(wanted_strings_ext) do
f_enum:write(' ' .. enumify(item[1]) .. ',\n')
end
f_enum:write(' kTermCount, // sentinel\n')
f_enum:write('} TerminfoDef;\n')
f_enum:close()
local f_defs = io.open(target_gen, 'wb')
f_defs:write('// uncrustify:off\n\n')
local version = io.popen('infocmp -V'):read '*a'
f_defs:write('// Generated by src/gen/gen_terminfo.lua and ' .. version .. '\n')
f_defs:write('#pragma once\n\n')
f_defs:write('#include "nvim/tui/terminfo.h"\n')
for _, entry in ipairs(entries) do
local term, target = unpack(entry)
local fil = io.popen('infocmp -L -x -1 -A ' .. db .. ' ' .. term):read '*a'
local lines = vim.split(fil, '\n')
local prepat = '^%s*([%w_]+)'
local boolpat = prepat .. ','
local numpat = prepat .. '#([^,]+),'
local strpat = prepat .. '=([^,]+),'
local bools, nums, strs = {}, {}, {}
for i, line in ipairs(lines) do
local boolmatch = string.match(line, boolpat)
local nummatch, numval = string.match(line, numpat)
local strmatch, strval = string.match(line, strpat)
if boolmatch then
dbg('boolean: ' .. boolmatch)
bools[boolmatch] = true
elseif nummatch then
dbg('number: ' .. nummatch .. ' is ' .. numval)
nums[nummatch] = numval
elseif strmatch then
dbg('string: ' .. strmatch .. ' is ' .. strval)
strs[strmatch] = strval
else
dbg('UNKNOWN:', i, line)
end
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 _, name in ipairs(wanted_numbers) do
f_defs:write(' .' .. name .. ' = ' .. (nums[name] or '-1') .. ',\n')
end
f_defs:write(' .defs = {\n')
for _, name in ipairs(wanted_strings) do
f_defs:write(' [' .. enumify(name) .. '] = ' .. quote(strs[name]) .. ',\n')
end
for _, item in ipairs(wanted_strings_ext) do
f_defs:write(' [' .. enumify(item[1]) .. '] = ' .. quote(strs[item[2]]) .. ',\n')
end
f_defs:write(' },\n')
f_defs:write('};\n')
end
f_defs:write('\n#define XLIST_TERMINFO_BUILTIN \\\n')
for _, name in ipairs(wanted_strings) do
f_defs:write(' X(' .. name .. ') \\\n')
end
f_defs:write('// end of list\n\n')
f_defs:write('#define XLIST_TERMINFO_EXT \\\n')
for _, item in ipairs(wanted_strings_ext) do
f_defs:write(' X(' .. item[1] .. ', ' .. item[2] .. ') \\\n')
end
f_defs:write('// end of list\n')
f_defs:close()

View File

@@ -2198,7 +2198,7 @@ DictAs(eval_statusline_ret) nvim_eval_statusline(String str, Dict(eval_statuslin
grpname = syn_id2name(-sp->userhl);
} else {
snprintf(user_group, sizeof(user_group), "User%d", sp->userhl);
grpname = arena_memdupz(arena, user_group, strlen(user_group));
grpname = arena_strdup(arena, user_group);
}
const char *combine = sp->item == STL_SIGNCOL ? syn_id2name(scl_hl_id)

View File

@@ -813,12 +813,19 @@ char *arena_allocz(Arena *arena, size_t size)
}
char *arena_memdupz(Arena *arena, const char *buf, size_t size)
FUNC_ATTR_NONNULL_ARG(2)
{
char *mem = arena_allocz(arena, size);
memcpy(mem, buf, size);
return mem;
}
char *arena_strdup(Arena *arena, const char *str)
FUNC_ATTR_NONNULL_ARG(2)
{
return arena_memdupz(arena, str, strlen(str));
}
#if defined(EXITFREE)
# include "nvim/autocmd.h"

View File

@@ -17,6 +17,12 @@
# include "nvim/os/os.h"
#endif
typedef struct {
long nums[20];
char *strings[20];
size_t offset;
} TPSTACK;
#include "tui/terminfo.c.generated.h"
bool terminfo_is_term_family(const char *term, const char *family)
@@ -57,167 +63,606 @@ bool terminfo_is_bsd_console(const char *term)
/// We do not attempt to detect xterm pretenders here.
///
/// @param term $TERM value
/// @param[out,allocated] termname decided builtin 'term' name
/// @param[out,static] termname decided builtin 'term' name
/// @return [allocated] terminfo structure
static unibi_term *terminfo_builtin(const char *term, char **termname)
const TerminfoEntry *terminfo_from_builtin(const char *term, char **termname)
{
if (terminfo_is_term_family(term, "xterm")) {
*termname = xstrdup("builtin_xterm");
return unibi_from_mem((const char *)xterm_256colour_terminfo,
sizeof xterm_256colour_terminfo);
*termname = "builtin_xterm";
return &xterm_256colour_terminfo;
} else if (terminfo_is_term_family(term, "screen")) {
*termname = xstrdup("builtin_screen");
return unibi_from_mem((const char *)screen_256colour_terminfo,
sizeof screen_256colour_terminfo);
*termname = "builtin_screen";
return &screen_256colour_terminfo;
} else if (terminfo_is_term_family(term, "tmux")) {
*termname = xstrdup("builtin_tmux");
return unibi_from_mem((const char *)tmux_256colour_terminfo,
sizeof tmux_256colour_terminfo);
*termname = "builtin_tmux";
return &tmux_256colour_terminfo;
} else if (terminfo_is_term_family(term, "rxvt")) {
*termname = xstrdup("builtin_rxvt");
return unibi_from_mem((const char *)rxvt_256colour_terminfo,
sizeof rxvt_256colour_terminfo);
*termname = "builtin_rxvt";
return &rxvt_256colour_terminfo;
} else if (terminfo_is_term_family(term, "putty")) {
*termname = xstrdup("builtin_putty");
return unibi_from_mem((const char *)putty_256colour_terminfo,
sizeof putty_256colour_terminfo);
*termname = "builtin_putty";
return &putty_256colour_terminfo;
} else if (terminfo_is_term_family(term, "linux")) {
*termname = xstrdup("builtin_linux");
return unibi_from_mem((const char *)linux_16colour_terminfo,
sizeof linux_16colour_terminfo);
*termname = "builtin_linux";
return &linux_16colour_terminfo;
} else if (terminfo_is_term_family(term, "interix")) {
*termname = xstrdup("builtin_interix");
return unibi_from_mem((const char *)interix_8colour_terminfo,
sizeof interix_8colour_terminfo);
*termname = "builtin_interix";
return &interix_8colour_terminfo;
} else if (terminfo_is_term_family(term, "iterm")
|| terminfo_is_term_family(term, "iterm2")
|| terminfo_is_term_family(term, "iTerm.app")
|| terminfo_is_term_family(term, "iTerm2.app")) {
*termname = xstrdup("builtin_iterm");
return unibi_from_mem((const char *)iterm_256colour_terminfo,
sizeof iterm_256colour_terminfo);
*termname = "builtin_iterm";
return &iterm_256colour_terminfo;
} else if (terminfo_is_term_family(term, "st")) {
*termname = xstrdup("builtin_st");
return unibi_from_mem((const char *)st_256colour_terminfo,
sizeof st_256colour_terminfo);
*termname = "builtin_st";
return &st_256colour_terminfo;
} else if (terminfo_is_term_family(term, "gnome")
|| terminfo_is_term_family(term, "vte")) {
*termname = xstrdup("builtin_vte");
return unibi_from_mem((const char *)vte_256colour_terminfo,
sizeof vte_256colour_terminfo);
*termname = "builtin_vte";
return &vte_256colour_terminfo;
} else if (terminfo_is_term_family(term, "cygwin")) {
*termname = xstrdup("builtin_cygwin");
return unibi_from_mem((const char *)cygwin_terminfo,
sizeof cygwin_terminfo);
*termname = "builtin_cygwin";
return &cygwin_terminfo;
} else if (terminfo_is_term_family(term, "win32con")) {
*termname = xstrdup("builtin_win32con");
return unibi_from_mem((const char *)win32con_terminfo,
sizeof win32con_terminfo);
*termname = "builtin_win32con";
return &win32con_terminfo;
} else if (terminfo_is_term_family(term, "conemu")) {
*termname = xstrdup("builtin_conemu");
return unibi_from_mem((const char *)conemu_terminfo,
sizeof conemu_terminfo);
*termname = "builtin_conemu";
return &conemu_terminfo;
} else if (terminfo_is_term_family(term, "vtpcon")) {
*termname = xstrdup("builtin_vtpcon");
return unibi_from_mem((const char *)vtpcon_terminfo,
sizeof vtpcon_terminfo);
*termname = "builtin_vtpcon";
return &vtpcon_terminfo;
} else {
*termname = xstrdup("builtin_ansi");
return unibi_from_mem((const char *)ansi_terminfo,
sizeof ansi_terminfo);
*termname = "builtin_ansi";
return &ansi_terminfo;
}
}
/// @param term $TERM value
/// @param[out,allocated] termname decided builtin 'term' name
/// @return [allocated] terminfo structure
unibi_term *terminfo_from_builtin(const char *term, char **termname)
static ssize_t unibi_find_ext_str(unibi_term *ut, const char *name)
{
unibi_term *ut = terminfo_builtin(term, termname);
if (*termname == NULL) {
*termname = xstrdup("builtin_?");
size_t max = unibi_count_ext_str(ut);
for (size_t i = 0; i < max; i++) {
const char *n = unibi_get_ext_str_name(ut, i);
if (n && 0 == strcmp(n, name)) {
return (ssize_t)i;
}
}
return ut;
return -1;
}
bool terminfo_from_unibilium(TerminfoEntry *ti, char *termname, Arena *arena)
{
unibi_term *ut = unibi_from_term(termname);
if (!ut) {
return false;
}
ti->bce = 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->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;
} else if (n && !strcmp(n, "Su")) {
ti->Su = true;
}
}
static const enum unibi_string uni_ids[] = {
#define X(name) unibi_##name,
XLIST_TERMINFO_BUILTIN
#undef X
};
for (size_t i = 0; i < ARRAY_SIZE(uni_ids); i++) {
const char *val = unibi_get_str(ut, uni_ids[i]);
ti->defs[i] = val ? arena_strdup(arena, val) : NULL;
}
static const char *uni_ext[] = {
#define X(informal_name, terminfo_name) #terminfo_name,
XLIST_TERMINFO_EXT
#undef X
};
for (size_t i = 0; i < ARRAY_SIZE(uni_ext); i++) {
ssize_t val = unibi_find_ext_str(ut, uni_ext[i]);
if (val >= 0) {
const char *data = unibi_get_ext_str(ut, (size_t)val);
ti->defs[kTermExtOffset + i] = data ? arena_strdup(arena, data) : NULL;
}
}
unibi_destroy(ut);
return true;
}
static const char *fmt(bool val)
{
return val ? "true" : "false";
}
/// Dumps termcap info to the messages area.
/// Serves a similar purpose as Vim `:set termcap` (removed in Nvim).
///
/// @note adapted from unibilium unibi-dump.c
/// @return allocated string
String terminfo_info_msg(const unibi_term *ut, const char *termname)
String terminfo_info_msg(const TerminfoEntry *ti, const char *termname)
{
StringBuilder data = KV_INITIAL_VALUE;
kv_printf(data, "&term: %s\n", termname);
kv_printf(data, "Description: %s\n", unibi_get_name(ut));
const char **a = unibi_get_aliases(ut);
if (*a) {
kv_printf(data, "Aliases: ");
do {
kv_printf(data, "%s%s\n", *a, a[1] ? " | " : "");
a++;
} while (*a);
}
kv_printf(data, "\n");
kv_printf(data, "Boolean capabilities:\n");
for (enum unibi_boolean i = unibi_boolean_begin_ + 1;
i < unibi_boolean_end_; i++) {
kv_printf(data, " %-25s %-10s = %s\n", unibi_name_bool(i),
unibi_short_name_bool(i),
unibi_get_bool(ut, i) ? "true" : "false");
}
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, " extended underline ('Su'): %s\n", fmt(ti->Su));
kv_printf(data, "\n");
kv_printf(data, "Numeric capabilities:\n");
for (enum unibi_numeric i = unibi_numeric_begin_ + 1;
i < unibi_numeric_end_; i++) {
int n = unibi_get_num(ut, i); // -1 means "empty"
kv_printf(data, " %-25s %-10s = %d\n", unibi_name_num(i),
unibi_short_name_num(i), 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, "\n");
kv_printf(data, "String capabilities:\n");
for (enum unibi_string i = unibi_string_begin_ + 1;
i < unibi_string_end_; i++) {
const char *s = unibi_get_str(ut, i);
static const char *string_names[] = {
#define X(name) #name,
XLIST_TERMINFO_BUILTIN
#undef X
#define X(internal_name, terminfo_name) (#internal_name " (" #terminfo_name ")"),
XLIST_TERMINFO_EXT
#undef X
};
for (size_t i = 0 + 1; i < ARRAY_SIZE(string_names); i++) {
const char *s = ti->defs[i];
if (s) {
kv_printf(data, " %-25s %-10s = ", unibi_name_str(i),
unibi_short_name_str(i));
kv_printf(data, " %-31s = ", string_names[i]);
// Most of these strings will contain escape sequences.
kv_transstr(&data, s, false);
kv_push(data, '\n');
}
}
if (unibi_count_ext_bool(ut)) {
kv_printf(data, "Extended boolean capabilities:\n");
for (size_t i = 0; i < unibi_count_ext_bool(ut); i++) {
kv_printf(data, " %-25s = %s\n",
unibi_get_ext_bool_name(ut, i),
unibi_get_ext_bool(ut, i) ? "true" : "false");
}
}
if (unibi_count_ext_num(ut)) {
kv_printf(data, "Extended numeric capabilities:\n");
for (size_t i = 0; i < unibi_count_ext_num(ut); i++) {
kv_printf(data, " %-25s = %d\n",
unibi_get_ext_num_name(ut, i),
unibi_get_ext_num(ut, i));
}
}
if (unibi_count_ext_str(ut)) {
kv_printf(data, "Extended string capabilities:\n");
for (size_t i = 0; i < unibi_count_ext_str(ut); i++) {
kv_printf(data, " %-25s = ", unibi_get_ext_str_name(ut, i));
// NOTE: unibi_get_ext_str(ut, i) might be NULL, as termcap
// might include junk data on mac os. kv_transstr will handle this.
kv_transstr(&data, unibi_get_ext_str(ut, i), false);
kv_push(data, '\n');
}
}
kv_push(data, NUL);
return cbuf_as_string(data.items, data.size - 1);
}
// The implementation of terminfo_fmt() is based on NetBSD libterminfo,
// with full license reproduced below
// Copyright (c) 2009, 2011, 2013 The NetBSD Foundation, Inc.
//
// This code is derived from software contributed to The NetBSD Foundation
// by Roy Marples.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
// IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
// NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// nvim modifications:
// - use typesafe param args instead of va_args and piss
// - caller provides the output buffer
// - static variables are not preserved
static int push(long num, char *string, TPSTACK *stack)
{
if (stack->offset >= sizeof(stack->nums)) {
return -1;
}
stack->nums[stack->offset] = num;
stack->strings[stack->offset] = string;
stack->offset++;
return 0;
}
static int pop(long *num, char **string, TPSTACK *stack)
{
if (stack->offset == 0) {
if (num) {
*num = 0;
}
if (string) {
*string = NULL;
}
return -1;
}
stack->offset--;
if (num) {
*num = stack->nums[stack->offset];
}
if (string) {
*string = stack->strings[stack->offset];
}
return 0;
}
static bool ochar(char **buf, const char *buf_end, int c)
{
if (c == 0) {
c = 0200;
}
// Check we have space and a terminator
if (buf_end - *buf < 2) {
return 0;
}
*(*buf)++ = (char)c;
return 1;
}
static bool onum(char **buf, const char *buf_end, const char *fmt, int num, size_t len)
{
const size_t LONG_STR_MAX = 21;
len = MAX(len, LONG_STR_MAX);
if (buf_end - *buf < (ssize_t)(len + 2)) {
return 0;
}
int l = snprintf(*buf, len + 2, fmt, num);
if (l == -1) {
return 0;
}
*buf += l;
return true;
}
/// @return number of chars printed or 0 for any error
size_t terminfo_fmt(char *buf_start, char *buf_end, const char *str, TPVAR params[9])
{
char c, fmt[64], *fp, *ostr;
long val, val2;
long dnums[26]; // dynamic variables a-z, not preserved
long snums[26]; // static variables a-z, not preserved EITHER HAHA
memset(dnums, 0, sizeof snums);
memset(snums, 0, sizeof snums);
char *buf = buf_start;
size_t l, width, precision, olen;
TPSTACK stack;
unsigned done, dot, minus;
memset(&stack, 0, sizeof(stack));
while ((c = *str++) != '\0') {
if (c != '%' || (c = *str++) == '%') {
if (c == '\0') {
break;
}
if (!ochar(&buf, buf_end, c)) {
return false;
}
continue;
}
// Handle formatting.
fp = fmt;
*fp++ = '%';
done = dot = minus = 0;
width = precision = 0;
val = 0;
while (done == 0 && (size_t)(fp - fmt) < sizeof(fmt)) {
switch (c) {
case 'c':
case 's':
*fp++ = c;
done = 1;
break;
case 'd':
case 'o':
case 'x':
case 'X':
*fp++ = 'l';
*fp++ = c;
done = 1;
break;
case '#':
case ' ':
*fp++ = c;
break;
case '.':
*fp++ = c;
if (dot == 0) {
dot = 1;
width = (size_t)val;
} else {
done = 2;
}
val = 0;
break;
case ':':
minus = 1;
break;
case '-':
if (minus) {
*fp++ = c;
} else {
done = 1;
}
break;
default:
if (isdigit((unsigned char)c)) {
val = (val * 10) + (c - '0');
if (val > 10000) {
done = 2;
} else {
*fp++ = c;
}
} else {
done = 1;
}
}
if (done == 0) {
c = *str++;
}
}
if (done == 2) {
// Found an error in the format
fp = fmt + 1;
*fp = *str;
olen = 0;
} else {
if (dot == 0) {
width = (size_t)val;
} else {
precision = (size_t)val;
}
olen = MAX(width, precision);
}
*fp++ = '\0';
// Handle commands
switch (c) {
case 'c':
pop(&val, NULL, &stack);
if (!ochar(&buf, buf_end, (unsigned char)val)) {
return false;
}
break;
case 's':
pop(NULL, &ostr, &stack);
if (ostr != NULL) {
int r;
l = strlen(ostr);
if (l < olen) {
l = olen;
}
if ((size_t)(buf_end - buf) < (l + 1)) {
return false;
}
r = snprintf(buf, l + 1,
fmt, ostr);
if (r != -1) {
buf += (size_t)r;
}
}
break;
case 'l':
pop(NULL, &ostr, &stack);
if (ostr == NULL) {
l = 0;
} else {
l = strlen(ostr);
}
push((long)l, NULL, &stack);
break;
case 'd':
case 'o':
case 'x':
case 'X':
pop(&val, NULL, &stack);
if (onum(&buf, buf_end, fmt, (int)val, olen) == 0) {
return 0;
}
break;
case 'p':
if (*str < '1' || *str > '9') {
break;
}
l = (size_t)(*str++ - '1');
if (push(params[l].num, params[l].string, &stack)) {
return 0;
}
break;
case 'P':
pop(&val, NULL, &stack);
if (*str >= 'a' && *str <= 'z') {
dnums[*str - 'a'] = val;
} else if (*str >= 'A' && *str <= 'Z') {
snums[*str - 'A'] = val;
}
break;
case 'g':
if (*str >= 'a' && *str <= 'z') {
if (push(dnums[*str - 'a'], NULL, &stack)) {
return 0;
}
} else if (*str >= 'A' && *str <= 'Z') {
if (push(snums[*str - 'A'], NULL, &stack)) {
return 0;
}
}
break;
case 'i':
params[0].num++;
params[1].num++;
break;
case '\'':
if (push((long)(unsigned char)(*str++), NULL, &stack)) {
return 0;
}
while (*str != '\0' && *str != '\'') {
str++;
}
if (*str == '\'') {
str++;
}
break;
case '{':
val = 0;
for (; isdigit((unsigned char)(*str)); str++) {
val = (val * 10) + (*str - '0');
}
if (push(val, NULL, &stack)) {
return 0;
}
while (*str != '\0' && *str != '}') {
str++;
}
if (*str == '}') {
str++;
}
break;
case '+':
case '-':
case '*':
case '/':
case 'm':
case 'A':
case 'O':
case '&':
case '|':
case '^':
case '=':
case '<':
case '>':
pop(&val, NULL, &stack);
pop(&val2, NULL, &stack);
switch (c) {
case '+':
val = val + val2;
break;
case '-':
val = val2 - val;
break;
case '*':
val = val * val2;
break;
case '/':
val = val ? val2 / val : 0;
break;
case 'm':
val = val ? val2 % val : 0;
break;
case 'A':
val = val && val2;
break;
case 'O':
val = val || val2;
break;
case '&':
val = val & val2;
break;
case '|':
val = val | val2;
break;
case '^':
val = val ^ val2;
break;
case '=':
val = val == val2;
break;
case '<':
val = val2 < val;
break;
case '>':
val = val2 > val;
break;
}
if (push(val, NULL, &stack)) {
return 0;
}
break;
case '!':
case '~':
pop(&val, NULL, &stack);
switch (c) {
case '!':
val = !val;
break;
case '~':
val = ~val;
break;
}
if (push(val, NULL, &stack)) {
return 0;
}
break;
case '?': // if
break;
case 't': // then
pop(&val, NULL, &stack);
if (val == 0) {
l = 0;
for (; *str != '\0'; str++) {
if (*str != '%') {
continue;
}
str++;
if (*str == '?') {
l++;
} else if (*str == ';') {
if (l > 0) {
l--;
} else {
str++;
break;
}
} else if (*str == 'e' && l == 0) {
str++;
break;
}
}
}
break;
case 'e': // else
l = 0;
for (; *str != '\0'; str++) {
if (*str != '%') {
continue;
}
str++;
if (*str == '?') {
l++;
} else if (*str == ';') {
if (l > 0) {
l--;
} else {
str++;
break;
}
}
}
break;
case ';': // fi
break;
}
}
return (size_t)(buf - buf_start);
}

View File

@@ -1,7 +1,23 @@
#pragma once
#include <unibilium.h> // IWYU pragma: keep
#include "nvim/api/private/defs.h" // IWYU pragma: keep
#include "nvim/tui/terminfo_enum_defs.h"
typedef struct {
bool bce;
// these extended booleans indicate likely 24-color support
bool has_Tc_or_RGB;
bool Su;
int max_colors;
int lines;
int columns;
const char *defs[kTermCount];
} TerminfoEntry;
typedef struct {
long num;
char *string;
} TPVAR;
#include "tui/terminfo.h.generated.h"

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,54 @@
// genenerated by src/gen/gen_terminfo.lua
#pragma once
typedef enum {
kTerm_carriage_return,
kTerm_change_scroll_region,
kTerm_clear_screen,
kTerm_clr_eol,
kTerm_clr_eos,
kTerm_cursor_address,
kTerm_cursor_down,
kTerm_cursor_invisible,
kTerm_cursor_left,
kTerm_cursor_home,
kTerm_cursor_normal,
kTerm_cursor_up,
kTerm_cursor_right,
kTerm_delete_line,
kTerm_enter_bold_mode,
kTerm_enter_ca_mode,
kTerm_enter_italics_mode,
kTerm_enter_reverse_mode,
kTerm_enter_standout_mode,
kTerm_enter_underline_mode,
kTerm_erase_chars,
kTerm_exit_attribute_mode,
kTerm_exit_ca_mode,
kTerm_from_status_line,
kTerm_insert_line,
kTerm_keypad_local,
kTerm_keypad_xmit,
kTerm_parm_delete_line,
kTerm_parm_down_cursor,
kTerm_parm_insert_line,
kTerm_parm_left_cursor,
kTerm_parm_right_cursor,
kTerm_parm_up_cursor,
kTerm_set_a_background,
kTerm_set_a_foreground,
kTerm_set_attributes,
kTerm_set_lr_margin,
kTerm_to_status_line,
#define kTermExtOffset kTerm_reset_cursor_style
kTerm_reset_cursor_style,
kTerm_set_cursor_style,
kTerm_enter_strikethrough_mode,
kTerm_set_rgb_foreground,
kTerm_set_rgb_background,
kTerm_set_cursor_color,
kTerm_reset_cursor_color,
kTerm_set_underline_style,
kTermCount, // sentinel
} TerminfoDef;

File diff suppressed because it is too large Load Diff

View File

@@ -4005,7 +4005,9 @@ describe('TUI client', function()
screen_client:expect({ any = 'GUI Running: 0' })
if is_os('mac') then
assert_log('uv_tty_set_mode failed: Unknown system error %-102', testlog)
-- this might either be "Unknown system error %-102" or
-- "inappropriate ioctl for device" depending on the phase of the moon
assert_log('uv_tty_set_mode failed', testlog)
end
end)
@@ -4037,7 +4039,7 @@ describe('TUI client', function()
ffi.C.ui_call_set_title(title)
]=])
screen_client:expect_unchanged()
assert_log('TUI: escape sequence for ext%.set_title too long', testlog)
assert_log('set_title: title string too long!', testlog)
eq(bufname, api.nvim_buf_get_var(0, 'term_title'))
-- Following escape sequences are not affected.