Files
neovim/runtime/lua/vim/ui/img.lua
Justin M. Keyes 9aa4608401 refactor: introduce nvim_on internally #39883
Problem:
`nvim_create_autocmd` is too verbose and its `callback` requires extra
"nesting".

Solution:
Introduce `nvim_on`. Start using it internally. Then we can get a feel
for how it should look before making it public.
2026-05-20 17:33:01 -04:00

152 lines
4.3 KiB
Lua

local nvim_on = require('vim._core.util').nvim_on
local M = {}
---@brief
---
---EXPERIMENTAL: This API may change in the future. Its semantics are not yet finalized.
---
---This provides a functional API for displaying images in Nvim.
---Currently supports PNG images via the Kitty graphics protocol.
---
---To override the image backend, replace `vim.ui.img` with your own
---implementation providing set/get/del.
---
---Examples:
---
---```lua
----- Load image bytes from disk and display at row 5, column 10
---local id = vim.ui.img.set(
--- vim.fn.readblob('/path/to/img.png'),
--- { row = 5, col = 10, width = 40, height = 20, zindex = 50 }
---)
---
----- Update the image position
---vim.ui.img.set(id, { row = 8, col = 12 })
---
----- Retrieve the current image opts
---local opts = vim.ui.img.get(id)
---
----- Remove the image
---vim.ui.img.del(id)
---
----- Remove all images
---vim.ui.img.del(math.huge)
---```
---@class vim.ui.img.Opts
---@inlinedoc
---@field row? integer starting row (1-indexed)
---@field col? integer starting column (1-indexed)
---@field width? integer width in cells
---@field height? integer height in cells
---@field zindex? integer stacking order (higher = on top)
--- Maps user-facing ID to internal tracking info.
---@type table<integer, { img_id: integer, opts: vim.ui.img.Opts }>
local state = {}
---Display an image or update an existing one.
---
---When {data_or_id} is a string, displays the image bytes at the position
---given by {opts}. Returns an integer id for later use.
---
---When {data_or_id} is an integer (a previously returned id), updates
---the image with new {opts}.
---
---@param data_or_id string|integer image bytes (string) or existing id (integer)
---@param opts? vim.ui.img.Opts
---@return integer id
function M.set(data_or_id, opts)
opts = opts or {}
vim.validate('data_or_id', data_or_id, { 'string', 'number' })
vim.validate('opts', opts, 'table')
local kitty = require('vim.ui.img._kitty')
-- If given a string, this should be the bytes of a new image to display
if type(data_or_id) == 'string' then
local img_id, placement_id = kitty.set(data_or_id, opts)
state[placement_id] = { img_id = img_id, opts = vim.deepcopy(opts) }
return placement_id
end
-- Otherwise, we update an existing image that is actively displayed
local id = data_or_id
local entry = state[id]
assert(entry, 'invalid image id: ' .. tostring(id))
-- We always want to have a full set of options when passing to kitty
local merged = vim.tbl_extend('force', entry.opts, opts)
kitty.update(entry.img_id, id, merged)
entry.opts = merged
return id
end
---Get the opts for an image.
---
---@param id integer
---@return vim.ui.img.Opts? opts copy of image opts, or nil if not found
function M.get(id)
vim.validate('id', id, 'number')
-- Grab a copy of the most recent opts used for the image
local entry = state[id]
if not entry then
return nil
end
return vim.deepcopy(entry.opts)
end
---Delete an image, or all images if `math.huge` is given as the id.
---
---@param id integer image id, or `math.huge` to delete all images
---@return boolean found true if any image existed
function M.del(id)
vim.validate('id', id, 'number')
-- Check if we have images to delete, and clear them out of our memory
-- being tracked here and also send a request to delete them all from
-- the terminal
if id == math.huge then
local has_ids = next(state) ~= nil
state = {}
if has_ids then
local kitty = require('vim.ui.img._kitty')
kitty.delete(math.huge)
end
return has_ids
end
-- Skip performing the deletion if we don't have an active image with the id
local entry = state[id]
if not entry then
return false
end
local kitty = require('vim.ui.img._kitty')
kitty.delete(entry.img_id)
state[id] = nil
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
nvim_on('VimLeavePre', nil, function()
M.del(math.huge)
end)
return M