fix(defaults): detect 'background'/'termguicolors' on UI attach #40175

Problem:  Terminal background and truecolor detection runs only at startup,
          gated on a UI being attached. A headless server has no UI then, so
          `'background'` and `'termguicolors'` are never detected and remote
          UIs ignore the terminal's theme.
Solution: Also (re)detect on UIEnter. The most recently attached terminal
          wins; an explicit user value is preserved.
This commit is contained in:
Barrett Ruth
2026-06-20 19:09:34 -05:00
committed by GitHub
parent 6284c1163a
commit 74f163c67d
6 changed files with 429 additions and 242 deletions

View File

@@ -853,7 +853,11 @@ A jump table for the options with a short description can be found at |Q_op|.
global
When set to "dark" or "light", adjusts the default color groups for
that background type. The |TUI| or other UI sets this on startup
if it can detect the background color.
if it can detect the background color, and re-detects it whenever a UI
attaches later, unless 'background' was set explicitly. When multiple
terminal UIs are attached they share one value, taken from whichever
terminal reports its background last (which may not be the most
recently attached one, since it depends on response speed).
This option does NOT change the background color, it tells Nvim what
the "inherited" (terminal/GUI) background looks like.

View File

@@ -781,267 +781,309 @@ do
end
end
if tty then
local group = vim.api.nvim_create_augroup('nvim.tty', {})
-- Shared TTY option detection used by two paths: the startup path (a TTY is
-- present when Nvim starts) and the UIEnter path (a TTY attaches to a
-- previously |--headless| server, e.g. behind |--remote-ui|). The helpers are
-- hoisted here so both paths share one implementation instead of duplicating
-- it.
--
-- Precedence: never override an explicit user setting. For 'background',
-- bg_user_set() treats only our own sets (SID_LUA, -8) as re-applicable; a
-- user set (real script SID, ":set", or API client) is preserved.
-- 'termguicolors' is guarded by `was_set` and is only ever enabled here.
--
-- Limitation: 'background' is global, the OSC 11 query is broadcast to every
-- attached terminal, and a |TermResponse| is not attributable to a UI, so the
-- value reflects whichever terminal answers last (decided by response speed),
-- not necessarily the most recently attached one.
local group = vim.api.nvim_create_augroup('nvim.tty', {})
--- Set an option after startup (so that OptionSet is fired), but only if not
--- already set by the user.
---
--- @param option string Option name
--- @param value any Option value
--- @param force boolean? Always set the value, even if already set
local function setoption(option, value, force)
if not force and vim.api.nvim_get_option_info2(option, {}).was_set then
-- Don't do anything if option is already set
--- Set an option after startup (so that OptionSet is fired), but only if not
--- already set by the user.
---
--- @param option string Option name
--- @param value any Option value
--- @param force boolean? Always set the value, even if already set
local function setoption(option, value, force)
if not force and vim.api.nvim_get_option_info2(option, {}).was_set then
-- Don't do anything if option is already set
return
end
-- Wait until Nvim is finished starting to set the option to ensure the
-- OptionSet event fires.
if vim.v.vim_did_enter == 1 then
--- @diagnostic disable-next-line:no-unknown
vim.o[option] = value
else
nvim_on('VimEnter', group, {
once = true,
nested = true,
}, function()
setoption(option, value, force)
end)
end
end
--- Script ID (SID) used for our own automatic 'background' sets. Lets
--- bg_user_set() distinguish a user set from our detection.
local sid_lua = -8
--- True if 'background' was set by the user, not by our detection (sid_lua).
local function bg_user_set()
local info = vim.api.nvim_get_option_info2('background', {})
return info.was_set and info.last_set_sid ~= sid_lua
end
--- 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 = vim._tointeger(c, 16)
if not val then
return nil
end
local max = vim._assert_integer(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
--- 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
---
--- In slow environments (e.g. SSH with high latency), this will increase
--- startup time and produce a warning, so users may want to disable it.
---
--- The OSC 11 handler is PERSISTENT (timeout=0) so it also reacts to runtime
--- theme changes (a terminal in mode 2031 re-queries and forwards a fresh
--- |TermResponse|). Its augroup is cleared on each call so only the
--- most-recently-attached TUI's handler remains.
---
--- @param sync boolean When true (a TTY is present at startup), also send a
--- DSR probe and synchronously wait so 'background' is set before user config,
--- warning (E1568) if the terminal never answers the DSR.
local function detect_background(sync)
-- Re-create (clear) the handler's augroup on each call so only the
-- most-recently-attached TUI's handler remains.
local bg_group = vim.api.nvim_create_augroup('nvim.tty.background', {})
-- Send OSC 11 query. In the startup (sync) path also send a DSR probe: if
-- the DSR response comes first, the terminal most likely doesn't support the
-- bg color query, and we don't have to keep waiting for a bg response. #32109
local osc11 = '\027]11;?\007'
local dsr = '\027[5n'
-- This handler updates 'background' anytime we receive an OSC 11 response
-- from the terminal emulator. It is persistent (no built-in timeout) so it
-- also reacts to runtime theme changes; the per-response bg_user_set() guard
-- stops it once the user pins 'background'.
local did_dsr_response = false
vim.tty.request(osc11 .. (sync and dsr or ''), { group = bg_group, timeout = 0 }, function(resp)
-- DSR response that should come after the OSC 11 response if the terminal
-- supports it.
if sync and string.match(resp, '^\027%[0n$') then
did_dsr_response = true
-- Don't stop listening: the bg response may come after the DSR response
-- if the terminal handles requests out of sequence. In that case, the bg
-- will simply be set later in the startup sequence.
return
end
-- Wait until Nvim is finished starting to set the option to ensure the
-- OptionSet event fires.
if vim.v.vim_did_enter == 1 then
--- @diagnostic disable-next-line:no-unknown
vim.o[option] = value
else
nvim_on('VimEnter', group, {
once = true,
nested = true,
}, function()
setoption(option, value, force)
end)
-- Never override an explicit user value: stop once the user pins it.
if bg_user_set() then
return true
end
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'
-- Use :noautocmd to suppress OptionSet event; OSC11 response may arrive after VimEnter.
vim.cmd('noautocmd set background=' .. bg)
end
end
end)
if not sync then
return
end
--- 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
---
--- In slow environments (e.g. SSH with high latency), this will increase
--- startup time and produce a warning, so users may want to disable it.
if vim.o.ttyfast then
--- 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
-- Wait until detection of OSC 11 capabilities is complete to ensure
-- background is automatically set before user config.
if
not vim.wait(100, function()
return did_dsr_response
end, 1)
-- Don't show the warning when running tests to avoid flakiness.
and os.getenv('NVIM_TEST') == nil
then
vim.notify(
"E1568: Terminal did not respond to DSR request for 'background' color. Startup may be slower. :help 'ttyfast'",
vim.log.levels.WARN,
{ _truncate = true }
)
end
end
local val = vim._tointeger(c, 16)
if not val then
return nil
end
local max = vim._assert_integer(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
-- Send OSC 11 query along with DSR sequence to determine whether terminal supports the query.
-- If the DSR response comes first, the terminal most likely doesn't support the bg color
-- query, and we don't have to keep waiting for a bg color response. #32109
local osc11 = '\027]11;?\007'
local dsr = '\027[5n'
-- This handler updates the value of 'background' anytime we receive an OSC 11 response from
-- the terminal emulator. If the user has set 'background' explicitly then we will delete the
-- autocommand below, effectively disabling automatic background setting.
local did_dsr_response = false
-- VimEnter handles cleanup; no built-in timeout.
local id = vim.tty.request(osc11 .. dsr, { group = group, timeout = 0 }, function(resp)
-- DSR response that should come after the OSC 11 response if the
-- terminal supports it.
if string.match(resp, '^\027%[0n$') then
did_dsr_response = true
-- Don't stop listening: the bg response may come after the DSR response if the terminal
-- handles requests out of sequence. In that case, the bg will simply be set later in the
-- startup sequence.
--- 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.
---
--- @param ui table<string,any> The attached TTY UI (see |nvim_list_uis()|).
local function detect_termguicolors(ui)
local colorterm = os.getenv('COLORTERM')
if ui.rgb or colorterm == 'truecolor' or colorterm == '24bit' then
-- The TUI was able to determine truecolor support or $COLORTERM explicitly indicates
-- truecolor support
setoption('termguicolors', true)
elseif (colorterm == nil or colorterm == '') and vim.o.ttyfast then
-- Neither the TUI nor $COLORTERM indicate that truecolor is supported, so query the
-- terminal
local caps = {} ---@type table<string, boolean>
vim.tty.query({ 'Tc', 'RGB', 'setrgbf', 'setrgbb' }, function(cap, found)
if not found then
return
end
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'
-- Use :noautocmd to suppress OptionSet event; OSC11 response may arrive after VimEnter.
vim.cmd('noautocmd set background=' .. bg)
end
caps[cap] = true
if caps.Tc or caps.RGB or (caps.setrgbf and caps.setrgbb) then
setoption('termguicolors', true)
end
end)
nvim_on('VimEnter', group, {
nested = true,
once = true,
}, function()
local optinfo = vim.api.nvim_get_option_info2('background', {})
local sid_lua = -8
if
optinfo.was_set
and optinfo.last_set_sid ~= sid_lua
and next(vim.api.nvim_get_autocmds({ id = id })) ~= nil
then
vim.api.nvim_del_autocmd(id)
-- Arbitrary colors to set in the SGR sequence
local r = 1
local g = 2
local b = 3
-- 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.
--
-- Reset attributes first, as other code may have set attributes.
local payload = ('\027[0m\027[48;2;%d;%d;%dm%s'):format(r, g, b, '\027P$qm\027\\')
vim.tty.request(payload, { group = group }, function(resp)
local decrqss = resp:match('^\027P1%$r([%d;:]+)m$')
if not decrqss then
return
end
end)
-- Wait until detection of OSC 11 capabilities is complete to
-- ensure background is automatically set before user config.
if
not vim.wait(100, function()
return did_dsr_response
end, 1)
-- Don't show the warning when running tests to avoid flakiness.
and os.getenv('NVIM_TEST') == nil
then
vim.notify(
"E1568: Terminal did not respond to DSR request for 'background' color. Startup may be slower. :help 'ttyfast'",
vim.log.levels.WARN,
{ _truncate = true }
)
end
end
-- 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
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
local colorterm = os.getenv('COLORTERM')
if tty.rgb or colorterm == 'truecolor' or colorterm == '24bit' then
-- The TUI was able to determine truecolor support or $COLORTERM explicitly indicates
-- truecolor support
setoption('termguicolors', true)
elseif (colorterm == nil or colorterm == '') and vim.o.ttyfast then
-- Neither the TUI nor $COLORTERM indicate that truecolor is supported, so query the
-- terminal
local caps = {} ---@type table<string, boolean>
vim.tty.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
setoption('termguicolors', true)
end
end)
-- Arbitrary colors to set in the SGR sequence
local r = 1
local g = 2
local b = 3
-- 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.
--
-- Reset attributes first, as other code may have set attributes.
local payload = ('\027[0m\027[48;2;%d;%d;%dm%s'):format(r, g, b, '\027P$qm\027\\')
vim.tty.request(payload, { group = group }, function(resp)
local decrqss = resp:match('^\027P1%$r([%d;:]+)m$')
if not decrqss then
return
end
-- 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
end
-- The returned SGR sequence should begin with 48:2
local sgr = assert(attrs[#attrs]):match('^48:2:([%d:]+)$')
if not sgr then
return
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
vim._tointeger(params[#params - 2]) == r
and vim._tointeger(params[#params - 1]) == g
and vim._tointeger(params[#params]) == b
then
setoption('termguicolors', true)
end
-- The returned SGR sequence should begin with 48:2
local sgr = assert(attrs[#attrs]):match('^48:2:([%d:]+)$')
if not sgr then
return
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)
end
end
if
vim._tointeger(params[#params - 2]) == r
and vim._tointeger(params[#params - 1]) == g
and vim._tointeger(params[#params]) == b
then
setoption('termguicolors', true)
end
return true
end)
end
end
if tty then
-- A TTY is present at startup. Detect synchronously so 'background' is set
-- before user config runs, and detect 'termguicolors'. This path owns the
-- startup TTY (including runtime reactivity), so the UIEnter path below is
-- not registered for it (avoids double-detection).
if vim.o.ttyfast then
detect_background(true)
end
detect_termguicolors(tty)
-- Show progress bars in supporting terminals
nvim_on('Progress', vim.api.nvim_create_augroup('nvim.progress', {}), {
desc = 'Display native progress bars',
@@ -1058,6 +1100,43 @@ do
end
end)
end
-- Re-detect terminal options when a UI attaches, not only at startup: a
-- |--headless| server (e.g. behind |--remote-ui|) has no UI when the block
-- above runs, and swapping a TUI between servers should pick up the current
-- terminal. Only handle TTYs that attach AFTER startup: if a TTY was present
-- at startup the block above already owns detection (incl. runtime
-- reactivity), so registering here too would double-detect.
if not tty then
--- The terminal UI that just attached (per |v:event| `chan`), or nil.
local function attaching_tty()
local chan = (vim.v.event or {}).chan
for _, ui in ipairs(vim.api.nvim_list_uis()) do
if ui.chan == chan then
return ui.stdout_tty and ui or nil
end
end
return nil
end
nvim_on('UIEnter', vim.api.nvim_create_augroup('nvim.tty.attach', {}), {}, function()
local ui = attaching_tty()
if not ui then
return
end
-- 'termguicolors': enable when the attaching UI reports truecolor (or the
-- terminal query confirms it), unless the user set it. Never disabled here.
detect_termguicolors(ui)
-- 'background': (re)query OSC 11. The persistent handler also reacts to
-- runtime theme changes (mode 2031 -> TUI re-queries -> |TermResponse|);
-- the per-response bg_user_set() guard preserves an explicit user value.
if vim.o.ttyfast then
detect_background(false)
end
end)
end
end
--- Default options

View File

@@ -226,7 +226,11 @@ vim.go.awa = vim.go.autowriteall
--- When set to "dark" or "light", adjusts the default color groups for
--- that background type. The `TUI` or other UI sets this on startup
--- if it can detect the background color.
--- if it can detect the background color, and re-detects it whenever a UI
--- attaches later, unless 'background' was set explicitly. When multiple
--- terminal UIs are attached they share one value, taken from whichever
--- terminal reports its background last (which may not be the most
--- recently attached one, since it depends on response speed).
---
--- This option does NOT change the background color, it tells Nvim what
--- the "inherited" (terminal/GUI) background looks like.

View File

@@ -368,7 +368,11 @@ local options = {
desc = [=[
When set to "dark" or "light", adjusts the default color groups for
that background type. The |TUI| or other UI sets this on startup
if it can detect the background color.
if it can detect the background color, and re-detects it whenever a UI
attaches later, unless 'background' was set explicitly. When multiple
terminal UIs are attached they share one value, taken from whichever
terminal reports its background last (which may not be the most
recently attached one, since it depends on response speed).
This option does NOT change the background color, it tells Nvim what
the "inherited" (terminal/GUI) background looks like.

View File

@@ -132,7 +132,9 @@ describe('nvim_ui_send', function()
|
]])
eq('Hello world', table.concat(read_data))
-- The TUI client queries OSC 11 on connect, so that precedes the payload.
local bg_request = '\027]11;?\007'
eq(bg_request .. 'Hello world', table.concat(read_data))
end)
it('ignores ui_send event for UIs without stdout_tty', function()

View File

@@ -4400,6 +4400,100 @@ describe('TUI bg color', function()
eq({ true, 'light' }, { child_session:request('nvim_eval', '&background') })
end)
end)
-- Start a headless server (the outer Nvim is its OSC-11-answering terminal),
-- cleaned up when the test ends, and return its address and a client session.
local function start_headless(extra)
local server = new_pipename()
local argv = { nvim_prog, '--clean', '--headless', '--listen', server }
local job = fn.jobstart(vim.list_extend(argv, extra or {}))
finally(function()
pcall(fn.jobstop, job)
end)
local session
retry(nil, nil, function()
session = n.connect(server)
end)
return server, session
end
it('is detected on remote-ui attach to a headless server #30320', function()
command('highlight clear Normal')
command('set background=light') -- outer Nvim acts as a light terminal
local server, session =
start_headless({ '--cmd', 'colorscheme vim', '--cmd', 'set noswapfile' })
local screen = tt.setup_child_nvim({ '--remote-ui', '--server', server })
screen:expect({ any = '%[No Name%]' })
retry(nil, nil, function()
eq({ true, 'light' }, { session:request('nvim_eval', '&background') })
end)
end)
it('enables termguicolors on remote-ui attach to a headless server #30320', function()
command('set background=light')
local server, session =
start_headless({ '--cmd', 'colorscheme vim', '--cmd', 'set noswapfile' })
eq({ true, 0 }, { session:request('nvim_eval', '&termguicolors') })
-- A truecolor terminal so the attaching client reports rgb=true.
local screen = tt.setup_child_nvim(
{ '--remote-ui', '--server', server },
{ env = { COLORTERM = 'truecolor' } }
)
screen:expect({ any = '%[No Name%]' })
retry(nil, nil, function()
eq({ true, 1 }, { session:request('nvim_eval', '&termguicolors') })
end)
end)
it('does not override an explicit user background on remote-ui attach', function()
command('highlight clear Normal')
command('set background=light') -- light terminal
local server, session = start_headless({
'--cmd',
'set background=dark', -- user pins dark
'--cmd',
'colorscheme vim',
'--cmd',
'set noswapfile',
})
-- Count OSC 11 responses so we can wait for the attach round-trip to finish
-- (a deterministic barrier) before asserting the user's value held.
session:request(
'nvim_exec_lua',
[[
_G.osc11 = 0
vim.api.nvim_create_autocmd('TermResponse', { callback = function(ev)
if ev.data.sequence:find(']11;rgb', 1, true) then
_G.osc11 = _G.osc11 + 1
end
end })
]],
{}
)
local screen = tt.setup_child_nvim({ '--remote-ui', '--server', server })
screen:expect({ any = '%[No Name%]' })
retry(nil, nil, function()
eq({ true, true }, { session:request('nvim_exec_lua', 'return _G.osc11 > 0', {}) })
end)
eq({ true, 'dark' }, { session:request('nvim_eval', '&background') })
end)
it('reacts to a runtime theme change over remote-ui', function()
command('highlight clear Normal')
command('set background=light') -- start light
local server, session =
start_headless({ '--cmd', 'colorscheme vim', '--cmd', 'set noswapfile' })
local screen = tt.setup_child_nvim({ '--remote-ui', '--server', server })
screen:expect({ any = '%[No Name%]' })
retry(nil, nil, function()
eq({ true, 'light' }, { session:request('nvim_eval', '&background') })
end)
command('highlight clear Normal')
command('set background=dark') -- flip the outer terminal at runtime
retry(nil, nil, function()
eq({ true, 'dark' }, { session:request('nvim_eval', '&background') })
end)
end)
end)
describe('TUI client', function()