mirror of
https://github.com/neovim/neovim.git
synced 2024-12-19 10:45:16 -07:00
feat(img): add vim.img.protocol()
to detect preferred graphics protocol
Implement `vim.img.protocol()` that can be used to detect the preferred graphics protocol. This is a reverse-engineered copy of how `timg` implements graphics protocol support, and relies on a couple of terminal queries, hence we implement `vim.img._terminal.query()` and `vim.img._terminal.graphics.detect()` to support figuring out if the terminal supports iterm2, kitty, or sixel protocols and mirrors the logic from `timg`.
This commit is contained in:
parent
a56a5f0117
commit
ce818a9a91
@ -1,5 +1,6 @@
|
||||
local img = vim._defer_require('vim.img', {
|
||||
_backend = ..., --- @module 'vim.img._backend'
|
||||
_detect = ..., --- @module 'vim.img._detect'
|
||||
_image = ..., --- @module 'vim.img._image'
|
||||
_terminal = ..., --- @module 'vim.img._terminal'
|
||||
})
|
||||
@ -13,7 +14,31 @@ function img.load(opts)
|
||||
return img._image:new(opts)
|
||||
end
|
||||
|
||||
---@class vim.img.Protocol 'iterm2'|'kitty'|'sixel'
|
||||
img.protocol = (function()
|
||||
---@class vim.img.Protocol 'iterm2'|'kitty'|'sixel'
|
||||
|
||||
---@type vim.img.Protocol|nil
|
||||
local protocol = nil
|
||||
|
||||
local loaded = false
|
||||
|
||||
---Determines the preferred graphics protocol to use by default.
|
||||
---
|
||||
---@return vim.img.Protocol|nil
|
||||
return function()
|
||||
if not loaded then
|
||||
local graphics = img._detect().graphics
|
||||
|
||||
---@diagnostic disable-next-line:cast-type-mismatch
|
||||
---@cast graphics vim.img.Protocol|nil
|
||||
protocol = graphics
|
||||
|
||||
loaded = true
|
||||
end
|
||||
|
||||
return protocol
|
||||
end
|
||||
end)()
|
||||
|
||||
---@class vim.img.Opts: vim.img.Backend.RenderOpts
|
||||
---@field backend? vim.img.Protocol|vim.img.Backend
|
||||
@ -26,6 +51,14 @@ function img.show(image, opts)
|
||||
|
||||
local backend = opts.backend
|
||||
|
||||
-- If no graphics are explicitly defined, attempt to detect the
|
||||
-- preferred graphics. If we still cannot figure out a backend,
|
||||
-- throw an error early versus silently trying a protocol.
|
||||
if not backend then
|
||||
backend = img.protocol()
|
||||
assert(backend, 'no graphics backend available')
|
||||
end
|
||||
|
||||
-- For named protocols, grab the appropriate backend, failing
|
||||
-- if there is not a default backend for the specified protocol.
|
||||
if type(backend) == 'string' then
|
||||
|
88
runtime/lua/vim/img/_detect.lua
Normal file
88
runtime/lua/vim/img/_detect.lua
Normal file
@ -0,0 +1,88 @@
|
||||
local terminal = require('vim.img._terminal')
|
||||
|
||||
local TERM_QUERY = {
|
||||
-- Request device attributes (DA2).
|
||||
--
|
||||
-- It typically returns information about the terminal type and supported features.
|
||||
--
|
||||
-- Response format is typically something like '\033[>...;...;...c'
|
||||
DEVICE_ATTRIBUTES = terminal.code.ESC .. '[>q',
|
||||
|
||||
-- Request device status report (DSR), checking if terminal is okay.
|
||||
--
|
||||
-- Response indicates its current state.
|
||||
DEVICE_STATUS_REPORT = terminal.code.ESC .. '[5n',
|
||||
}
|
||||
|
||||
local TERM_RESPONSE = {
|
||||
-- Indicates that the terminal is functioning normally (no error).
|
||||
--
|
||||
-- 0 means 'OK'; other values indicate different states or errors.
|
||||
OK = terminal.code.ESC .. '[0n',
|
||||
}
|
||||
|
||||
---Detects supported graphics of the terminal.
|
||||
---@return {graphics:'iterm2'|'kitty'|'sixel'|nil, tmux:boolean, broken_sixel_cursor_placement:boolean}
|
||||
return function()
|
||||
local results = { graphics = nil, tmux = false, broken_sixel_cursor_placement = false }
|
||||
|
||||
local term = os.getenv('TERM')
|
||||
if term == 'xterm-kitty' or term == 'xterm-ghostty' or term == 'ghostty' then
|
||||
results.graphics = 'kitty'
|
||||
end
|
||||
|
||||
local term_program = os.getenv('TERM_PROGRAM')
|
||||
if term_program == 'vscode' then
|
||||
results.graphics = 'iterm2'
|
||||
results.broken_sixel_cursor_placement = true
|
||||
end
|
||||
|
||||
local _, err = terminal.query({
|
||||
query = table.concat({
|
||||
TERM_QUERY.DEVICE_ATTRIBUTES,
|
||||
TERM_QUERY.DEVICE_STATUS_REPORT,
|
||||
}),
|
||||
handler = function(buffer)
|
||||
local function has(s)
|
||||
return string.find(buffer, s, 1, true) ~= nil
|
||||
end
|
||||
|
||||
if has('iTerm2') or has('Konsole 2') then
|
||||
results.graphics = 'iterm2'
|
||||
end
|
||||
|
||||
if has('WezTerm') then
|
||||
results.graphics = 'iterm2'
|
||||
results.broken_sixel_cursor_placement = true
|
||||
end
|
||||
|
||||
if has('kitty') or has('ghostty') then
|
||||
results.graphics = 'kitty'
|
||||
end
|
||||
|
||||
if has('mlterm') then
|
||||
results.graphics = 'sixel'
|
||||
end
|
||||
|
||||
if has('XTerm') or has('foot') then
|
||||
results.graphics = 'sixel'
|
||||
results.broken_sixel_cursor_placement = true
|
||||
end
|
||||
|
||||
if has('tmux') then
|
||||
results.tmux = true
|
||||
end
|
||||
|
||||
-- Check if we have received the ok terminal response
|
||||
local start = string.find(buffer, TERM_RESPONSE.OK, 1, true)
|
||||
if start then
|
||||
return string.sub(buffer, start)
|
||||
end
|
||||
end,
|
||||
timeout = 250,
|
||||
})
|
||||
|
||||
assert(not err, err)
|
||||
|
||||
return results
|
||||
end
|
@ -75,4 +75,66 @@ end
|
||||
---Terminal escape codes.
|
||||
M.code = TERM_CODE
|
||||
|
||||
---@param opts {query:string, handler:(fun(buffer:string):string|nil), timeout?:integer}
|
||||
---@return string|nil result, string|nil err
|
||||
function M.query(opts)
|
||||
local uv = vim.uv
|
||||
|
||||
opts = opts or {}
|
||||
local query = opts.query
|
||||
local handler = opts.handler
|
||||
local timeout = opts.timeout or 250
|
||||
|
||||
local tty_fd, err
|
||||
local function cleanup()
|
||||
if tty_fd then
|
||||
uv.fs_close(tty_fd)
|
||||
tty_fd = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Identify the path to the editor's tty
|
||||
-- NOTE: This only works on Unix-like systems!
|
||||
local ok, tty_path = pcall(M.tty_name)
|
||||
if not ok then
|
||||
return nil, tty_path
|
||||
end
|
||||
|
||||
-- Open the tty so we can write our query
|
||||
tty_fd, err = uv.fs_open(tty_path, 'r+', 438)
|
||||
if not tty_fd then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
-- Write query to terminal.
|
||||
local success, write_err = uv.fs_write(tty_fd, query, -1)
|
||||
if not success then
|
||||
cleanup()
|
||||
return nil, write_err
|
||||
end
|
||||
|
||||
-- Read response with timeout.
|
||||
local buffer = ''
|
||||
local start_time = uv.now()
|
||||
|
||||
while uv.now() - start_time < timeout do
|
||||
local data, read_err = uv.fs_read(tty_fd, 512, -1)
|
||||
if data then
|
||||
buffer = buffer .. data
|
||||
local result = handler(buffer)
|
||||
if result then
|
||||
cleanup()
|
||||
return result
|
||||
end
|
||||
elseif read_err ~= 'EAGAIN' then
|
||||
cleanup()
|
||||
return nil, read_err
|
||||
end
|
||||
uv.sleep(1)
|
||||
end
|
||||
|
||||
cleanup()
|
||||
return nil, 'Timeout'
|
||||
end
|
||||
|
||||
return M
|
||||
|
Loading…
Reference in New Issue
Block a user