---Kitty graphics protocol implementation for vim.ui.img. local M = {} local generate_id = (function() local bit = require('bit') local NVIM_PID_BITS = 10 local nvim_pid = 0 local cnt = 30 ---@return integer return function() if nvim_pid == 0 then local pid = vim.fn.getpid() nvim_pid = bit.band(bit.bxor(pid, bit.rshift(pid, 5), bit.rshift(pid, NVIM_PID_BITS)), 0x3FF) end cnt = cnt + 1 return bit.bor(bit.lshift(nvim_pid, 24 - NVIM_PID_BITS), cnt) end end)() ---Build a Kitty graphics protocol escape sequence. ---@param control table ---@param payload? string ---@return string local function seq(control, payload) local parts = { '\027_G' } local tmp = {} for k, v in pairs(control) do table.insert(tmp, k .. '=' .. v) end if #tmp > 0 then table.insert(parts, table.concat(tmp, ',')) end if payload and payload ~= '' then table.insert(parts, ';') table.insert(parts, payload) end table.insert(parts, '\027\\') return table.concat(parts) end ---Transmit image bytes to kitty in base64 chunks using direct transmission. --- ---Large images may cause the terminal to hang or the escape sequence to get ---interrupted mid-write. A future filepath option (t=f) could let the ---terminal read the file directly, avoiding this issue for local sessions. ---@param id integer kitty image id ---@param data string raw image bytes local function transmit(id, data) local chunk_size = 4096 local base64_data = vim.base64.encode(data) local pos = 1 local len = #base64_data while pos <= len do local end_pos = math.min(pos + chunk_size - 1, len) local chunk = base64_data:sub(pos, end_pos) local is_last = end_pos >= len local control = {} if pos == 1 then control.f = '100' -- PNG format control.a = 't' -- Transmit without displaying control.t = 'd' -- Direct transmission control.i = id control.q = '2' -- Suppress responses end control.m = is_last and '0' or '1' vim.api.nvim_ui_send(seq(control, chunk)) pos = end_pos + 1 end end ---Send a kitty place/display command with cursor management. ---@param img_id integer kitty image id ---@param placement_id integer kitty placement id ---@param opts vim.ui.img.Opts local function place(img_id, placement_id, opts) local cursor_save = '\0277' local cursor_hide = '\027[?25l' local cursor_move = string.format('\027[%d;%dH', opts.row or 1, opts.col or 1) local cursor_restore = '\0278' local cursor_show = '\027[?25h' ---@type table local control = { a = 'p', i = img_id, p = placement_id, C = '1', -- Don't move the cursor at all q = '2', -- Suppress responses } if opts.width then control.c = opts.width end if opts.height then control.r = opts.height end if opts.zindex then control.z = opts.zindex end vim.api.nvim_ui_send( cursor_save .. cursor_hide .. cursor_move .. seq(control) .. cursor_restore .. cursor_show ) end ---Transmit image bytes and place the image. Returns both IDs. ---@param data string raw image bytes ---@param opts vim.ui.img.Opts ---@return integer img_id ---@return integer placement_id function M.set(data, opts) local img_id = generate_id() local placement_id = generate_id() transmit(img_id, data) place(img_id, placement_id, opts) return img_id, placement_id end ---Update an existing placement (flicker-free, reuses same IDs). ---@param img_id integer ---@param placement_id integer ---@param opts vim.ui.img.Opts function M.update(img_id, placement_id, opts) place(img_id, placement_id, opts) end ---Delete an image and all its placements from the terminal. ---@param img_id integer function M.delete(img_id) vim.api.nvim_ui_send(seq({ a = 'd', d = 'i', i = img_id, q = '2', -- Suppress responses })) end return M