1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
|
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 --[[@as integer]])
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, chan?: 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 --[[@as integer]])
end)
return M
|