feat(health): vim.ui.img healthcheck #39449

This commit is contained in:
Chip Senkbeil
2026-04-28 09:12:13 -05:00
committed by GitHub
parent 62db5bebdd
commit 45c5cbb4c5
5 changed files with 118 additions and 40 deletions

View File

@@ -91,4 +91,52 @@ function M.query(caps, cb)
end)
end
--- Send an APC sequence to the terminal and call {cb} for each APC response received.
--- Cleans up after {timeout} milliseconds if no response is received.
---
--- {cb} receives the full APC sequence including the `\027_` prefix.
--- Return `true` from {cb} to stop listening.
---
---@param payload string APC sequence to send (full escape sequence including prefix/suffix)
---@param opts {timeout?:integer} Options table (timeout in milliseconds, default 1000)
---@param cb fun(resp:string):boolean? Callback invoked for each APC TermResponse
---@overload fun(payload:string, cb:fun(resp:string):boolean?)
function M.query_apc(payload, opts, cb)
if type(opts) == 'function' then
cb = opts
opts = {}
end
vim.validate('payload', payload, 'string')
vim.validate('opts', opts, 'table')
vim.validate('cb', cb, 'function')
local timeout = opts and opts.timeout or 1000
local timer = assert(vim.uv.new_timer())
local id = vim.api.nvim_create_autocmd('TermResponse', {
nested = true,
callback = function(ev)
local resp = ev.data.sequence ---@type string
if resp:match('^\027_') then
if not timer:is_closing() then
timer:close()
end
return cb(resp)
end
end,
})
vim.api.nvim_ui_send(payload)
timer:start(timeout, 0, function()
vim.schedule(function()
pcall(vim.api.nvim_del_autocmd, id)
end)
if not timer:is_closing() then
timer:close()
end
end)
end
return M

View File

@@ -113,6 +113,17 @@ function M.del(id)
return true
end
---@private
--- Query whether the host terminal supports displaying images.
--- Blocks until the terminal responds or times out.
---
---@param opts? {timeout?: integer} timeout in milliseconds (default: 1000)
---@return boolean supported true if the terminal supports image display
---@return string? msg error detail if the terminal responded but not with OK
function M._supported(opts)
return require('vim.ui.img._kitty').supported(opts)
end
vim.api.nvim_create_autocmd('VimLeavePre', {
callback = function()
---@type integer[]

View File

@@ -147,4 +147,50 @@ function M.delete(img_id)
}))
end
--- Query whether this terminal supports the kitty graphics protocol.
--- Blocks until the terminal responds or times out.
---
---@param opts? {timeout?: integer} timeout in milliseconds (default: 1000)
---@return boolean supported
---@return string? msg error detail if terminal responded but not with OK
function M.supported(opts)
local timeout = opts and opts.timeout or 1000
-- Do not use APC on terminals that echo unknown sequences
if vim.env.TERM_PROGRAM == 'Apple_Terminal' then
return false
end
local query_id = generate_id()
---@type boolean?
local result
---@type string?
local msg
require('vim.tty').query_apc(
seq({ a = 'q', i = query_id, s = 1, v = 1 }),
{ timeout = timeout },
function(resp)
-- kitty APC response: \027_G[<fields>,]i=<id>[,<fields>];<status>
-- status is "OK" or an error code+message like "ENODATA:Missing image data"
local id = resp:match('^\027_G[^;]*i=(%d+)')
local status = resp:match(';(.-)%s*$')
if id and tonumber(id) == query_id and status then
result = true
msg = status ~= 'OK' and status or nil
return true
end
end
)
-- Wait in a blocking fashion for the response, checking
-- at least every 200ms, or faster if the timeout is small
vim.wait(timeout + 100, function()
return result ~= nil
end, math.max(math.min(math.ceil(timeout / 10), 200), 1))
return result == true, msg
end
return M

View File

@@ -1,51 +1,23 @@
local M = {}
local health = vim.health
local function system(cmd)
local result = vim.system(cmd, { text = true }):wait()
if not result then -- Workaround https://github.com/neovim/neovim/issues/37922
return false, 'command failed'
end
return result.code == 0, vim.trim(('%s\n%s'):format(result.stdout, result.stderr))
end
local function get_tmux_option(option)
local cmd = { 'tmux', 'show-option', '-qvg', option } -- try global scope
local ok, out = system(cmd)
local val = vim.fn.substitute(out, [[\v(\s|\r|\n)]], '', 'g')
if not ok then
health.error(('command failed: %s\n%s'):format(vim.inspect(cmd), out))
return 'error'
elseif val == '' then
cmd = { 'tmux', 'show-option', '-qvgs', option } -- try session scope
ok, out = system(cmd)
val = vim.fn.substitute(out, [[\v(\s|\r|\n)]], '', 'g')
if not ok then
health.error(('command failed: %s\n%s'):format(vim.inspect(cmd), out))
return 'error'
end
end
return val
end
function M.check()
health.start('vim.ui.img')
if not vim.env.TMUX or vim.fn.executable('tmux') == 0 then
health.ok('no terminal multiplexer detected')
return
local supported, msg = require('vim.ui.img')._supported()
if supported then
if msg then
health.ok(('Graphics protocol: supported (%s)'):format(msg))
else
health.ok('Graphics protocol: supported')
end
else
health.error('Graphics protocol: not supported by this terminal.')
end
local passthrough = get_tmux_option('allow-passthrough')
if passthrough ~= 'error' then
if passthrough == 'on' or passthrough == 'all' then
health.ok('allow-passthrough: ' .. passthrough)
else
health.error(
'`allow-passthrough` is not enabled. Images will not be displayed.',
{ 'Add to ~/.tmux.conf:\nset-option -g allow-passthrough on' }
)
end
if vim.env.TMUX then
health.warn('tmux is detected. Images may not display correctly.')
end
end