feat(defaults): enable 'termguicolors' by default when supported by terminal

Enable 'termguicolors' automatically when Nvim can detect that truecolor
is supported by the host terminal.

If $COLORTERM is set to "truecolor" or "24bit", or the terminal's
terminfo entry contains capabilities for Tc, RGB, or setrgbf and
setrgbb, then we assume that the terminal supports truecolor. Otherwise,
the terminal is queried (using both XTGETTCAP and SGR + DECRQSS). If the
terminal's response to these queries (if any) indicates that it supports
truecolor, then 'termguicolors' is enabled.
This commit is contained in:
Gregory Anders 2023-12-05 10:01:32 -08:00
parent 5b40a1c09d
commit 2613ba5000
11 changed files with 298 additions and 95 deletions

View File

@ -98,6 +98,9 @@ The following changes may require adaptations in user config or plugins.
• Default color scheme has been updated to be "Neovim branded" and accessible. • Default color scheme has been updated to be "Neovim branded" and accessible.
Use `:colorscheme vim` to revert to the old legacy color scheme. Use `:colorscheme vim` to revert to the old legacy color scheme.
• 'termguicolors' is enabled by default when Nvim is able to determine that
the host terminal emulator supports 24-bit color.
============================================================================== ==============================================================================
BREAKING CHANGES IN HEAD *news-breaking-dev* BREAKING CHANGES IN HEAD *news-breaking-dev*

View File

@ -6495,6 +6495,10 @@ A jump table for the options with a short description can be found at |Q_op|.
attributes instead of "cterm" attributes. |guifg| attributes instead of "cterm" attributes. |guifg|
Requires an ISO-8613-3 compatible terminal. Requires an ISO-8613-3 compatible terminal.
Nvim will automatically attempt to determine if the host terminal
supports 24-bit color and will enable this option if it does
(unless explicitly disabled by the user).
*'termpastefilter'* *'tpf'* *'termpastefilter'* *'tpf'*
'termpastefilter' 'tpf' string (default "BS,HT,ESC,DEL") 'termpastefilter' 'tpf' string (default "BS,HT,ESC,DEL")
global global

View File

@ -75,6 +75,8 @@ Defaults *nvim-defaults*
- 'switchbuf' defaults to "uselast" - 'switchbuf' defaults to "uselast"
- 'tabpagemax' defaults to 50 - 'tabpagemax' defaults to 50
- 'tags' defaults to "./tags;,tags" - 'tags' defaults to "./tags;,tags"
- 'termguicolors' is enabled by default if Nvim can detect support from the
host terminal
- 'ttimeoutlen' defaults to 50 - 'ttimeoutlen' defaults to 50
- 'ttyfast' is always set - 'ttyfast' is always set
- 'undodir' defaults to ~/.local/state/nvim/undo// (|xdg|), auto-created - 'undodir' defaults to ~/.local/state/nvim/undo// (|xdg|), auto-created

View File

@ -165,91 +165,92 @@ do
}) })
end end
--- Guess value of 'background' based on terminal color. -- Only do the following when the TUI is attached
--- local tty = nil
--- We write Operating System Command (OSC) 11 to the terminal to request the for _, ui in ipairs(vim.api.nvim_list_uis()) do
--- terminal's background color. We then wait for a response. If the response if ui.chan == 1 and ui.stdout_tty then
--- matches `rgba:RRRR/GGGG/BBBB/AAAA` where R, G, B, and A are hex digits, then tty = ui
--- compute the luminance[1] of the RGB color and classify it as light/dark break
--- accordingly. Note that the color components may have anywhere from one to
--- four hex digits, and require scaling accordingly as values out of 4, 8, 12,
--- or 16 bits. Also note the A(lpha) component is optional, and is parsed but
--- ignored in the calculations.
---
--- [1] https://en.wikipedia.org/wiki/Luma_%28video%29
do
--- Parse a string of hex characters as a color.
---
--- The string can contain 1 to 4 hex characters. The returned value is
--- between 0.0 and 1.0 (inclusive) representing the intensity of the color.
---
--- For instance, if only a single hex char "a" is used, then this function
--- returns 0.625 (10 / 16), while a value of "aa" would return 0.664 (170 /
--- 256).
---
--- @param c string Color as a string of hex chars
--- @return number? Intensity of the color
local function parsecolor(c)
if #c == 0 or #c > 4 then
return nil
end
local val = tonumber(c, 16)
if not val then
return nil
end
local max = tonumber(string.rep('f', #c), 16)
return val / max
end end
end
--- Parse an OSC 11 response if tty then
--- Guess value of 'background' based on terminal color.
--- ---
--- Either of the two formats below are accepted: --- We write Operating System Command (OSC) 11 to the terminal to request the
--- terminal's background color. We then wait for a response. If the response
--- matches `rgba:RRRR/GGGG/BBBB/AAAA` where R, G, B, and A are hex digits, then
--- compute the luminance[1] of the RGB color and classify it as light/dark
--- accordingly. Note that the color components may have anywhere from one to
--- four hex digits, and require scaling accordingly as values out of 4, 8, 12,
--- or 16 bits. Also note the A(lpha) component is optional, and is parsed but
--- ignored in the calculations.
--- ---
--- OSC 11 ; rgb:<red>/<green>/<blue> --- [1] https://en.wikipedia.org/wiki/Luma_%28video%29
--- do
--- or --- Parse a string of hex characters as a color.
--- ---
--- OSC 11 ; rgba:<red>/<green>/<blue>/<alpha> --- The string can contain 1 to 4 hex characters. The returned value is
--- --- between 0.0 and 1.0 (inclusive) representing the intensity of the color.
--- where ---
--- --- For instance, if only a single hex char "a" is used, then this function
--- <red>, <green>, <blue>, <alpha> := h | hh | hhh | hhhh --- returns 0.625 (10 / 16), while a value of "aa" would return 0.664 (170 /
--- --- 256).
--- The alpha component is ignored, if present. ---
--- --- @param c string Color as a string of hex chars
--- @param resp string OSC 11 response --- @return number? Intensity of the color
--- @return string? Red component local function parsecolor(c)
--- @return string? Green component if #c == 0 or #c > 4 then
--- @return string? Blue component return nil
local function parseosc11(resp)
local r, g, b
r, g, b = resp:match('^\027%]11;rgb:(%x+)/(%x+)/(%x+)$')
if not r and not g and not b then
local a
r, g, b, a = resp:match('^\027%]11;rgba:(%x+)/(%x+)/(%x+)/(%x+)$')
if not a or #a > 4 then
return nil, nil, nil
end end
local val = tonumber(c, 16)
if not val then
return nil
end
local max = tonumber(string.rep('f', #c), 16)
return val / max
end end
if r and g and b and #r <= 4 and #g <= 4 and #b <= 4 then --- Parse an OSC 11 response
return r, g, b ---
--- Either of the two formats below are accepted:
---
--- OSC 11 ; rgb:<red>/<green>/<blue>
---
--- or
---
--- OSC 11 ; rgba:<red>/<green>/<blue>/<alpha>
---
--- where
---
--- <red>, <green>, <blue>, <alpha> := h | hh | hhh | hhhh
---
--- The alpha component is ignored, if present.
---
--- @param resp string OSC 11 response
--- @return string? Red component
--- @return string? Green component
--- @return string? Blue component
local function parseosc11(resp)
local r, g, b
r, g, b = resp:match('^\027%]11;rgb:(%x+)/(%x+)/(%x+)$')
if not r and not g and not b then
local a
r, g, b, a = resp:match('^\027%]11;rgba:(%x+)/(%x+)/(%x+)/(%x+)$')
if not a or #a > 4 then
return nil, nil, nil
end
end
if r and g and b and #r <= 4 and #g <= 4 and #b <= 4 then
return r, g, b
end
return nil, nil, nil
end end
return nil, nil, nil
end
local tty = false
for _, ui in ipairs(vim.api.nvim_list_uis()) do
if ui.chan == 1 and ui.stdout_tty then
tty = true
break
end
end
if tty then
local timer = assert(vim.uv.new_timer()) local timer = assert(vim.uv.new_timer())
---@param bg string New value of the 'background' option ---@param bg string New value of the 'background' option
@ -300,7 +301,7 @@ do
io.stdout:write('\027]11;?\007') io.stdout:write('\027]11;?\007')
timer:start(1000, 0, function() timer:start(1000, 0, function()
-- No response received. Delete the autocommand -- Delete the autocommand if no response was received
vim.schedule(function() vim.schedule(function()
-- Suppress error if autocommand has already been deleted -- Suppress error if autocommand has already been deleted
pcall(vim.api.nvim_del_autocmd, id) pcall(vim.api.nvim_del_autocmd, id)
@ -311,4 +312,108 @@ do
end end
end) end)
end end
--- If the TUI (term_has_truecolor) was able to determine that the host
--- terminal supports truecolor, enable 'termguicolors'. Otherwise, query the
--- terminal (using both XTGETTCAP and SGR + DECRQSS). If the terminal's
--- response indicates that it does support truecolor enable 'termguicolors',
--- but only if the user has not already disabled it.
do
if tty.rgb then
-- The TUI was able to determine truecolor support
vim.o.termguicolors = true
else
--- Enable 'termguicolors', but only if it was not already set by the user.
local function settgc()
if not vim.api.nvim_get_option_info2('termguicolors', {}).was_set then
vim.o.termguicolors = true
end
end
local caps = {} ---@type table<string, boolean>
require('vim.termcap').query({ 'Tc', 'RGB', 'setrgbf', 'setrgbb' }, function(cap, found)
if not found then
return
end
caps[cap] = true
if caps.Tc or caps.RGB or (caps.setrgbf and caps.setrgbb) then
settgc()
end
end)
local timer = assert(vim.uv.new_timer())
-- Arbitrary colors to set in the SGR sequence
local r = 1
local g = 2
local b = 3
local id = vim.api.nvim_create_autocmd('TermResponse', {
nested = true,
callback = function(args)
local resp = args.data ---@type string
local decrqss = resp:match('^\027P1%$r([%d;:]+)m$')
if decrqss then
-- The DECRQSS SGR response first contains attributes separated by
-- semicolons, followed by the SGR itself with parameters separated
-- by colons. Some terminals include "0" in the attribute list
-- unconditionally; others do not. Our SGR sequence did not set any
-- attributes, so there should be no attributes in the list.
local attrs = vim.split(decrqss, ';')
if #attrs ~= 1 and (#attrs ~= 2 or attrs[1] ~= '0') then
return true
end
-- The returned SGR sequence should begin with 48:2
local sgr = attrs[#attrs]:match('^48:2:([%d:]+)$')
if not sgr then
return true
end
-- The remaining elements of the SGR sequence should be the 3 colors
-- we set. Some terminals also include an additional parameter
-- (which can even be empty!), so handle those cases as well
local params = vim.split(sgr, ':')
if #params ~= 3 and (#params ~= 4 or (params[1] ~= '' and params[1] ~= '1')) then
return true
end
if
tonumber(params[#params - 2]) == r
and tonumber(params[#params - 1]) == g
and tonumber(params[#params]) == b
then
settgc()
end
return true
end
end,
})
-- Write SGR followed by DECRQSS. This sets the background color then
-- immediately asks the terminal what the background color is. If the
-- terminal responds to the DECRQSS with the same SGR sequence that we
-- sent then the terminal supports truecolor.
local decrqss = '\027P$qm\027\\'
if os.getenv('TMUX') then
decrqss = string.format('\027Ptmux;%s\027\\', decrqss:gsub('\027', '\027\027'))
end
io.stdout:write(string.format('\027[48;2;%d;%d;%dm%s', r, g, b, decrqss))
timer:start(1000, 0, function()
-- Delete the autocommand if no response was received
vim.schedule(function()
-- Suppress error if autocommand has already been deleted
pcall(vim.api.nvim_del_autocmd, id)
end)
if not timer:is_closing() then
timer:close()
end
end)
end
end
end end

View File

@ -20,6 +20,7 @@ vim.lsp = require('vim.lsp')
vim.re = require('vim.re') vim.re = require('vim.re')
vim.secure = require('vim.secure') vim.secure = require('vim.secure')
vim.snippet = require('vim.snippet') vim.snippet = require('vim.snippet')
vim.text = require('vim.text')
vim.treesitter = require('vim.treesitter') vim.treesitter = require('vim.treesitter')
vim.ui = require('vim.ui') vim.ui = require('vim.ui')
vim.version = require('vim.version') vim.version = require('vim.version')

View File

@ -6941,6 +6941,10 @@ vim.go.tbidi = vim.go.termbidi
--- attributes instead of "cterm" attributes. `guifg` --- attributes instead of "cterm" attributes. `guifg`
--- Requires an ISO-8613-3 compatible terminal. --- Requires an ISO-8613-3 compatible terminal.
--- ---
--- Nvim will automatically attempt to determine if the host terminal
--- supports 24-bit color and will enable this option if it does
--- (unless explicitly disabled by the user).
---
--- @type boolean --- @type boolean
vim.o.termguicolors = false vim.o.termguicolors = false
vim.o.tgc = vim.o.termguicolors vim.o.tgc = vim.o.termguicolors

View File

@ -12,7 +12,10 @@ local M = {}
--- emulator supports the XTGETTCAP sequence. --- emulator supports the XTGETTCAP sequence.
--- ---
--- @param caps string|table A terminal capability or list of capabilities to query --- @param caps string|table A terminal capability or list of capabilities to query
--- @param cb function(cap:string, seq:string) Function to call when a response is received --- @param cb function(cap:string, found:bool, seq:string?) Callback function which is called for
--- each capability in {caps}. {found} is set to true if the capability was found or false
--- otherwise. {seq} is the control sequence for the capability if found, or nil for
--- boolean capabilities.
function M.query(caps, cb) function M.query(caps, cb)
vim.validate({ vim.validate({
caps = { caps, { 'string', 'table' } }, caps = { caps, { 'string', 'table' } },
@ -23,21 +26,33 @@ function M.query(caps, cb)
caps = { caps } caps = { caps }
end end
local count = #caps local pending = {} ---@type table<string, boolean>
for _, v in ipairs(caps) do
pending[v] = true
end
vim.api.nvim_create_autocmd('TermResponse', { local timer = assert(vim.uv.new_timer())
local id = vim.api.nvim_create_autocmd('TermResponse', {
callback = function(args) callback = function(args)
local resp = args.data ---@type string local resp = args.data ---@type string
local k, v = resp:match('^\027P1%+r(%x+)=(%x+)$') local k, rest = resp:match('^\027P1%+r(%x+)(.*)$')
if k and v then if k and rest then
local cap = vim.text.hexdecode(k) local cap = vim.text.hexdecode(k)
local seq = local seq ---@type string?
vim.text.hexdecode(v):gsub('\\E', '\027'):gsub('%%p%d', ''):gsub('\\(%d+)', string.char) if rest:match('^=%x+$') then
seq = vim.text
.hexdecode(rest:sub(2))
:gsub('\\E', '\027')
:gsub('%%p%d', '')
:gsub('\\(%d+)', string.char)
end
cb(cap, seq) cb(cap, true, seq)
count = count - 1 pending[cap] = nil
if count == 0 then
if next(pending) == nil then
return true return true
end end
end end
@ -57,6 +72,23 @@ function M.query(caps, cb)
end end
io.stdout:write(query) io.stdout:write(query)
timer:start(1000, 0, function()
-- Delete the autocommand if no response was received
vim.schedule(function()
-- Suppress error if autocommand has already been deleted
pcall(vim.api.nvim_del_autocmd, id)
-- Call the callback for all capabilities that were not found
for k in pairs(pending) do
cb(k, false, nil)
end
end)
if not timer:is_closing() then
timer:close()
end
end)
end end
return M return M

View File

@ -6,7 +6,11 @@ if not tty or vim.g.clipboard ~= nil or vim.o.clipboard ~= '' or not os.getenv('
return return
end end
require('vim.termcap').query('Ms', function(cap, seq) require('vim.termcap').query('Ms', function(cap, found, seq)
if not found then
return
end
assert(cap == 'Ms') assert(cap == 'Ms')
-- Check 'clipboard' and g:clipboard again to avoid a race condition -- Check 'clipboard' and g:clipboard again to avoid a race condition
@ -16,7 +20,7 @@ require('vim.termcap').query('Ms', function(cap, seq)
-- If the terminal reports a sequence other than OSC 52 for the Ms capability -- If the terminal reports a sequence other than OSC 52 for the Ms capability
-- then ignore it. We only support OSC 52 (for now) -- then ignore it. We only support OSC 52 (for now)
if not seq:match('^\027%]52') then if not seq or not seq:match('^\027%]52') then
return return
end end

View File

@ -8764,6 +8764,10 @@ return {
Enables 24-bit RGB color in the |TUI|. Uses "gui" |:highlight| Enables 24-bit RGB color in the |TUI|. Uses "gui" |:highlight|
attributes instead of "cterm" attributes. |guifg| attributes instead of "cterm" attributes. |guifg|
Requires an ISO-8613-3 compatible terminal. Requires an ISO-8613-3 compatible terminal.
Nvim will automatically attempt to determine if the host terminal
supports 24-bit color and will enable this option if it does
(unless explicitly disabled by the user).
]=], ]=],
full_name = 'termguicolors', full_name = 'termguicolors',
redraw = { 'ui_option' }, redraw = { 'ui_option' },

View File

@ -148,7 +148,8 @@ static bool cursor_style_enabled = false;
# include "tui/tui.c.generated.h" # include "tui/tui.c.generated.h"
#endif #endif
void tui_start(TUIData **tui_p, int *width, int *height, char **term) void tui_start(TUIData **tui_p, int *width, int *height, char **term, bool *rgb)
FUNC_ATTR_NONNULL_ALL
{ {
TUIData *tui = xcalloc(1, sizeof(TUIData)); TUIData *tui = xcalloc(1, sizeof(TUIData));
tui->is_starting = true; tui->is_starting = true;
@ -177,6 +178,7 @@ void tui_start(TUIData **tui_p, int *width, int *height, char **term)
*width = tui->width; *width = tui->width;
*height = tui->height; *height = tui->height;
*term = tui->term; *term = tui->term;
*rgb = tui->rgb;
} }
void tui_set_key_encoding(TUIData *tui) void tui_set_key_encoding(TUIData *tui)
@ -334,6 +336,9 @@ static void terminfo_start(TUIData *tui)
int konsolev = konsolev_env ? (int)strtol(konsolev_env, NULL, 10) int konsolev = konsolev_env ? (int)strtol(konsolev_env, NULL, 10)
: (konsole ? 1 : 0); : (konsole ? 1 : 0);
// truecolor support must be checked before patching/augmenting terminfo
tui->rgb = term_has_truecolor(tui, colorterm);
patch_terminfo_bugs(tui, term, colorterm, vtev, konsolev, iterm_env, nsterm); patch_terminfo_bugs(tui, term, colorterm, vtev, konsolev, iterm_env, nsterm);
augment_terminfo(tui, term, vtev, konsolev, iterm_env, nsterm); augment_terminfo(tui, term, vtev, konsolev, iterm_env, nsterm);
tui->can_change_scroll_region = tui->can_change_scroll_region =
@ -1439,7 +1444,7 @@ void tui_suspend(TUIData *tui)
tui_mouse_on(tui); tui_mouse_on(tui);
} }
stream_set_blocking(tui->input.in_fd, false); // libuv expects this stream_set_blocking(tui->input.in_fd, false); // libuv expects this
ui_client_attach(tui->width, tui->height, tui->term); ui_client_attach(tui->width, tui->height, tui->term, tui->rgb);
#endif #endif
} }
@ -1752,6 +1757,44 @@ static int unibi_find_ext_bool(unibi_term *ut, const char *name)
return -1; return -1;
} }
/// Determine if the terminal supports truecolor or not:
///
/// 1. If $COLORTERM is "24bit" or "truecolor", return true
/// 2. Else, check terminfo for Tc, RGB, setrgbf, or setrgbb capabilities. If
/// found, return true
/// 3. Else, return false
static bool term_has_truecolor(TUIData *tui, const char *colorterm)
{
// Check $COLORTERM
if (strequal(colorterm, "truecolor") || strequal(colorterm, "24bit")) {
return true;
}
// Check for Tc and RGB
for (size_t i = 0; i < unibi_count_ext_bool(tui->ut); i++) {
const char *n = unibi_get_ext_bool_name(tui->ut, i);
if (n && (!strcmp(n, "Tc") || !strcmp(n, "RGB"))) {
return true;
}
}
// Check for setrgbf and setrgbb
bool setrgbf = false;
bool setrgbb = false;
for (size_t i = 0; i < unibi_count_ext_str(tui->ut) && (!setrgbf || !setrgbb); i++) {
const char *n = unibi_get_ext_str_name(tui->ut, i);
if (n) {
if (!setrgbf && !strcmp(n, "setrgbf")) {
setrgbf = true;
} else if (!setrgbb && !strcmp(n, "setrgbb")) {
setrgbb = true;
}
}
}
return setrgbf && setrgbb;
}
/// Patches the terminfo records after loading from system or built-in db. /// Patches the terminfo records after loading from system or built-in db.
/// Several entries in terminfo are known to be deficient or outright wrong; /// Several entries in terminfo are known to be deficient or outright wrong;
/// and several terminal emulators falsely announce incorrect terminal types. /// and several terminal emulators falsely announce incorrect terminal types.

View File

@ -70,14 +70,14 @@ uint64_t ui_client_start_server(int argc, char **argv)
return channel->id; return channel->id;
} }
void ui_client_attach(int width, int height, char *term) void ui_client_attach(int width, int height, char *term, bool rgb)
{ {
MAXSIZE_TEMP_ARRAY(args, 3); MAXSIZE_TEMP_ARRAY(args, 3);
ADD_C(args, INTEGER_OBJ(width)); ADD_C(args, INTEGER_OBJ(width));
ADD_C(args, INTEGER_OBJ(height)); ADD_C(args, INTEGER_OBJ(height));
MAXSIZE_TEMP_DICT(opts, 9); MAXSIZE_TEMP_DICT(opts, 9);
PUT_C(opts, "rgb", BOOLEAN_OBJ(true)); PUT_C(opts, "rgb", BOOLEAN_OBJ(rgb));
PUT_C(opts, "ext_linegrid", BOOLEAN_OBJ(true)); PUT_C(opts, "ext_linegrid", BOOLEAN_OBJ(true));
PUT_C(opts, "ext_termcolors", BOOLEAN_OBJ(true)); PUT_C(opts, "ext_termcolors", BOOLEAN_OBJ(true));
if (term) { if (term) {
@ -111,9 +111,10 @@ void ui_client_run(bool remote_ui)
ui_client_is_remote = remote_ui; ui_client_is_remote = remote_ui;
int width, height; int width, height;
char *term; char *term;
tui_start(&tui, &width, &height, &term); bool rgb;
tui_start(&tui, &width, &height, &term, &rgb);
ui_client_attach(width, height, term); ui_client_attach(width, height, term, rgb);
// os_exit() will be invoked when the client channel detaches // os_exit() will be invoked when the client channel detaches
while (true) { while (true) {