feat(img): add kitty backend

Implement the kitty graphics protocol as a backend, using kitty's chunked image rendering, which should work within tmux and ssh if we keep the chunks small enough.
This commit is contained in:
Chip Senkbeil 2024-12-01 14:23:39 -06:00
parent 021b8f6e5d
commit a56a5f0117
No known key found for this signature in database
GPG Key ID: 35EF1F8EC72A4131
2 changed files with 109 additions and 0 deletions

View File

@ -8,4 +8,5 @@
return vim._defer_require('vim.img._backend', { return vim._defer_require('vim.img._backend', {
iterm2 = ..., --- @module 'vim.img._backend.iterm2' iterm2 = ..., --- @module 'vim.img._backend.iterm2'
kitty = ..., --- @module 'vim.img._backend.kitty'
}) })

View File

@ -0,0 +1,108 @@
---@class vim.img.KittyBackend: vim.img.Backend
local M = {}
---For kitty, we need to write an image in chunks
---
---Graphics codes are in this form:
---
--- <ESC>_G<control data>;<payload><ESC>\
---
---To stream data for a PNG, we specify the format `f=100`.
---
---To simultaneously transmit and display an image, we use `a=T`.
---
---Chunking data (such as from over a network) requires the
---specification of `m=0|1`, where all chunks must have a
---value of `1` except the very last chunk.
---@param data string
local function write_seq(data)
local terminal = require('vim.img._terminal')
terminal.write(terminal.code.ESC .. '_G') -- Begin sequence
terminal.write(data) -- Primary data
terminal.write(terminal.code.ESC .. '\\') -- End sequence
end
---Builds a header table of key value pairs.
---@param opts vim.img.Backend.RenderOpts
---@return table<string, string>
local function make_header(opts)
---@type table<string, string>
local header = {}
header['a'] = 'T'
header['f'] = '100'
local crop = opts.crop
local size = opts.size
if crop then
header['x'] = tostring(crop.x)
header['y'] = tostring(crop.y)
header['w'] = tostring(crop.width)
header['h'] = tostring(crop.height)
end
if size then
header['c'] = tostring(size.width)
header['r'] = tostring(size.height)
end
return header
end
---@param image vim.img.Image
---@param opts vim.img.Backend.RenderOpts
local function write_multipart_image(image, opts)
image:for_each_chunk(function(chunk, pos, has_more)
local data = {}
-- If at the beginning of our image, mark as a PNG to be
-- transmitted and displayed immediately
if pos == 1 then
-- Add an entry in our data to write out to the terminal
-- that is "k=v," for the key-value entries from the header
for key, value in pairs(make_header(opts)) do
table.insert(data, key .. '=' .. value .. ',')
end
end
-- If we are still sending chunks and not at the end
if has_more then
table.insert(data, 'm=1')
else
table.insert(data, 'm=0')
end
-- If we have a chunk available, write it
if string.len(chunk) > 0 then
table.insert(data, ';')
table.insert(data, chunk)
end
write_seq(table.concat(data))
end)
end
---@param image vim.img.Image
---@param opts? vim.img.Backend.RenderOpts
function M.render(image, opts)
local terminal = require('vim.img._terminal')
if not image:is_loaded() then
return
end
opts = opts or {}
if opts.pos then
terminal.cursor.move(opts.pos.col, opts.pos.row, true)
end
write_multipart_image(image, opts)
if opts.pos then
terminal.cursor.restore()
end
end
return M