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', {
|
local img = vim._defer_require('vim.img', {
|
||||||
_backend = ..., --- @module 'vim.img._backend'
|
_backend = ..., --- @module 'vim.img._backend'
|
||||||
|
_detect = ..., --- @module 'vim.img._detect'
|
||||||
_image = ..., --- @module 'vim.img._image'
|
_image = ..., --- @module 'vim.img._image'
|
||||||
_terminal = ..., --- @module 'vim.img._terminal'
|
_terminal = ..., --- @module 'vim.img._terminal'
|
||||||
})
|
})
|
||||||
@ -13,7 +14,31 @@ function img.load(opts)
|
|||||||
return img._image:new(opts)
|
return img._image:new(opts)
|
||||||
end
|
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
|
---@class vim.img.Opts: vim.img.Backend.RenderOpts
|
||||||
---@field backend? vim.img.Protocol|vim.img.Backend
|
---@field backend? vim.img.Protocol|vim.img.Backend
|
||||||
@ -26,6 +51,14 @@ function img.show(image, opts)
|
|||||||
|
|
||||||
local backend = opts.backend
|
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
|
-- For named protocols, grab the appropriate backend, failing
|
||||||
-- if there is not a default backend for the specified protocol.
|
-- if there is not a default backend for the specified protocol.
|
||||||
if type(backend) == 'string' then
|
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.
|
---Terminal escape codes.
|
||||||
M.code = TERM_CODE
|
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
|
return M
|
||||||
|
Loading…
Reference in New Issue
Block a user