Files
neovim/runtime/lua/vim/_defaults.lua
Gregory Anders 2613ba5000 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.
2023-12-06 07:57:09 -08:00

420 lines
14 KiB
Lua

--- Default mappings
do
--- Default maps for * and # in visual mode.
---
--- See |v_star-default| and |v_#-default|
do
local function region_chunks(region)
local chunks = {}
local maxcol = vim.v.maxcol
for line, cols in vim.spairs(region) do
local endcol = cols[2] == maxcol and -1 or cols[2]
local chunk = vim.api.nvim_buf_get_text(0, line, cols[1], line, endcol, {})[1]
table.insert(chunks, chunk)
end
return chunks
end
local function _visual_search(cmd)
assert(cmd == '/' or cmd == '?')
local region = vim.region(
0,
'.',
'v',
vim.api.nvim_get_mode().mode:sub(1, 1),
vim.o.selection == 'inclusive'
)
local chunks = region_chunks(region)
local esc_chunks = vim
.iter(chunks)
:map(function(v)
return vim.fn.escape(v, cmd == '/' and [[/\]] or [[?\]])
end)
:totable()
local esc_pat = table.concat(esc_chunks, [[\n]])
local search_cmd = ([[%s\V%s%s]]):format(cmd, esc_pat, '\n')
return '\27' .. search_cmd
end
vim.keymap.set('x', '*', function()
return _visual_search('/')
end, { desc = ':help v_star-default', expr = true, silent = true })
vim.keymap.set('x', '#', function()
return _visual_search('?')
end, { desc = ':help v_#-default', expr = true, silent = true })
end
--- Map Y to y$. This mimics the behavior of D and C. See |Y-default|
vim.keymap.set('n', 'Y', 'y$', { desc = ':help Y-default' })
--- Use normal! <C-L> to prevent inserting raw <C-L> when using i_<C-O>. #17473
---
--- See |CTRL-L-default|
vim.keymap.set('n', '<C-L>', '<Cmd>nohlsearch<Bar>diffupdate<Bar>normal! <C-L><CR>', {
desc = ':help CTRL-L-default',
})
--- Set undo points when deleting text in insert mode.
---
--- See |i_CTRL-U-default| and |i_CTRL-W-default|
vim.keymap.set('i', '<C-U>', '<C-G>u<C-U>', { desc = ':help i_CTRL-U-default' })
vim.keymap.set('i', '<C-W>', '<C-G>u<C-W>', { desc = ':help i_CTRL-W-default' })
--- Use the same flags as the previous substitution with &.
---
--- Use : instead of <Cmd> so that ranges are supported. #19365
---
--- See |&-default|
vim.keymap.set('n', '&', ':&&<CR>', { desc = ':help &-default' })
--- Map |gx| to call |vim.ui.open| on the identifier under the cursor
do
-- TODO: use vim.region() when it lands... #13896 #16843
local function get_visual_selection()
local save_a = vim.fn.getreginfo('a')
vim.cmd([[norm! "ay]])
local selection = vim.fn.getreg('a', 1)
vim.fn.setreg('a', save_a)
return selection
end
local function do_open(uri)
local _, err = vim.ui.open(uri)
if err then
vim.notify(err, vim.log.levels.ERROR)
end
end
local gx_desc =
'Opens filepath or URI under cursor with the system handler (file explorer, web browser, …)'
vim.keymap.set({ 'n' }, 'gx', function()
do_open(vim.fn.expand('<cfile>'))
end, { desc = gx_desc })
vim.keymap.set({ 'x' }, 'gx', function()
do_open(get_visual_selection())
end, { desc = gx_desc })
end
end
--- Default menus
do
--- Right click popup menu
-- TODO VimScript, no l10n
vim.cmd([[
aunmenu *
vnoremenu PopUp.Cut "+x
vnoremenu PopUp.Copy "+y
anoremenu PopUp.Paste "+gP
vnoremenu PopUp.Paste "+P
vnoremenu PopUp.Delete "_x
nnoremenu PopUp.Select\ All ggVG
vnoremenu PopUp.Select\ All gg0oG$
inoremenu PopUp.Select\ All <C-Home><C-O>VG
anoremenu PopUp.-1- <Nop>
anoremenu PopUp.How-to\ disable\ mouse <Cmd>help disable-mouse<CR>
]])
end
--- Default autocommands. See |default-autocmds|
do
local nvim_terminal_augroup = vim.api.nvim_create_augroup('nvim_terminal', {})
vim.api.nvim_create_autocmd('BufReadCmd', {
pattern = 'term://*',
group = nvim_terminal_augroup,
desc = 'Treat term:// buffers as terminal buffers',
nested = true,
command = "if !exists('b:term_title')|call termopen(matchstr(expand(\"<amatch>\"), '\\c\\mterm://\\%(.\\{-}//\\%(\\d\\+:\\)\\?\\)\\?\\zs.*'), {'cwd': expand(get(matchlist(expand(\"<amatch>\"), '\\c\\mterm://\\(.\\{-}\\)//'), 1, ''))})",
})
vim.api.nvim_create_autocmd({ 'TermClose' }, {
group = nvim_terminal_augroup,
desc = 'Automatically close terminal buffers when started with no arguments and exiting without an error',
callback = function(args)
if vim.v.event.status == 0 then
local info = vim.api.nvim_get_chan_info(vim.bo[args.buf].channel)
local argv = info.argv or {}
if #argv == 1 and argv[1] == vim.o.shell then
vim.cmd({ cmd = 'bdelete', args = { args.buf }, bang = true })
end
end
end,
})
vim.api.nvim_create_autocmd('CmdwinEnter', {
pattern = '[:>]',
desc = 'Limit syntax sync to maxlines=1 in the command window',
group = vim.api.nvim_create_augroup('nvim_cmdwin', {}),
command = 'syntax sync minlines=1 maxlines=1',
})
vim.api.nvim_create_autocmd('SwapExists', {
pattern = '*',
desc = 'Skip the swapfile prompt when the swapfile is owned by a running Nvim process',
group = vim.api.nvim_create_augroup('nvim_swapfile', {}),
callback = function()
local info = vim.fn.swapinfo(vim.v.swapname)
local user = vim.uv.os_get_passwd().username
local iswin = 1 == vim.fn.has('win32')
if info.error or info.pid <= 0 or (not iswin and info.user ~= user) then
vim.v.swapchoice = '' -- Show the prompt.
return
end
vim.v.swapchoice = 'e' -- Choose "(E)dit".
vim.notify(('W325: Ignoring swapfile from Nvim process %d'):format(info.pid))
end,
})
end
-- Only do the following when the TUI is attached
local tty = nil
for _, ui in ipairs(vim.api.nvim_list_uis()) do
if ui.chan == 1 and ui.stdout_tty then
tty = ui
break
end
end
if tty then
--- Guess value of 'background' based on terminal color.
---
--- 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.
---
--- [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
--- Parse an OSC 11 response
---
--- 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
local timer = assert(vim.uv.new_timer())
---@param bg string New value of the 'background' option
local function setbg(bg)
if vim.api.nvim_get_option_info2('background', {}).was_set then
-- Don't do anything if 'background' is already set
return
end
-- Wait until Nvim is finished starting to set 'background' to ensure the
-- OptionSet event fires.
if vim.v.vim_did_enter == 1 then
if vim.o.background ~= bg then
vim.o.background = bg
end
else
vim.api.nvim_create_autocmd('VimEnter', {
once = true,
nested = true,
callback = function()
setbg(bg)
end,
})
end
end
local id = vim.api.nvim_create_autocmd('TermResponse', {
nested = true,
callback = function(args)
local resp = args.data ---@type string
local r, g, b = parseosc11(resp)
if r and g and b then
local rr = parsecolor(r)
local gg = parsecolor(g)
local bb = parsecolor(b)
if rr and gg and bb then
local luminance = (0.299 * rr) + (0.587 * gg) + (0.114 * bb)
local bg = luminance < 0.5 and 'dark' or 'light'
setbg(bg)
end
return true
end
end,
})
io.stdout:write('\027]11;?\007')
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
--- 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