aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/ui/img.lua
blob: d93005ecd05078210e803aa70fcb9ba8812a2df7 (plain) (blame)
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