mirror of
https://github.com/folke/snacks.nvim
synced 2025-08-04 18:58:12 +00:00
feat: added new image
snacks plugin for the kitty graphics protocol
This commit is contained in:
parent
a17788539a
commit
4e4e63048e
18 changed files with 704 additions and 12 deletions
217
lua/snacks/image.lua
Normal file
217
lua/snacks/image.lua
Normal file
|
@ -0,0 +1,217 @@
|
|||
---@class snacks.Image
|
||||
---@field id number
|
||||
---@field buf number
|
||||
---@field wins table<number, snacks.image.Dim>
|
||||
---@field opts snacks.image.Config
|
||||
---@field file string
|
||||
---@field _convert uv.uv_process_t?
|
||||
local M = {}
|
||||
M.__index = M
|
||||
|
||||
M.meta = {
|
||||
desc = "Image viewer using Kitty Graphics Protocol, supported by `kitty`, `weztermn` and `ghostty`",
|
||||
needs_setup = true,
|
||||
}
|
||||
|
||||
---@class snacks.image.Config
|
||||
---@field file? string
|
||||
local defaults = {}
|
||||
local uv = vim.uv or vim.loop
|
||||
|
||||
---@alias snacks.image.Dim {col: number, row: number, width: number, height: number}
|
||||
|
||||
local images = {} ---@type table<number, snacks.Image>
|
||||
local id = 0
|
||||
local exts = { "png", "jpg", "jpeg", "gif", "bmp", "webp" }
|
||||
|
||||
---@param buf number
|
||||
---@param opts? snacks.image.Config
|
||||
function M.new(buf, opts)
|
||||
if images[buf] then
|
||||
return images[buf]
|
||||
end
|
||||
local file = opts and opts.file or vim.api.nvim_buf_get_name(buf)
|
||||
if not M.supports(file) then
|
||||
return
|
||||
end
|
||||
|
||||
local self = setmetatable({}, M)
|
||||
images[buf] = self
|
||||
id = id + 1
|
||||
self.id = id
|
||||
self.file = file
|
||||
self.opts = Snacks.config.get("image", defaults, opts or {})
|
||||
|
||||
Snacks.util.bo(buf, {
|
||||
filetype = "image",
|
||||
buftype = "nofile",
|
||||
-- modifiable = false,
|
||||
modified = false,
|
||||
swapfile = false,
|
||||
})
|
||||
self.buf = buf
|
||||
self.wins = {}
|
||||
|
||||
local group = vim.api.nvim_create_augroup("snacks.image." .. self.id, { clear = true })
|
||||
vim.api.nvim_create_autocmd(
|
||||
{ "VimResized", "BufWinEnter", "WinClosed", "BufWinLeave", "WinNew", "BufEnter", "BufLeave" },
|
||||
{
|
||||
group = group,
|
||||
buffer = self.buf,
|
||||
callback = function()
|
||||
vim.schedule(function()
|
||||
self:update()
|
||||
end)
|
||||
end,
|
||||
}
|
||||
)
|
||||
|
||||
vim.api.nvim_create_autocmd("BufWipeout", {
|
||||
group = group,
|
||||
buffer = self.buf,
|
||||
callback = function()
|
||||
vim.schedule(function()
|
||||
self:hide()
|
||||
end)
|
||||
pcall(vim.api.nvim_del_augroup_by_id, group)
|
||||
end,
|
||||
})
|
||||
|
||||
local update = self.update
|
||||
self:convert()
|
||||
if self:ready() then
|
||||
vim.schedule(function()
|
||||
self:create()
|
||||
self:update()
|
||||
end)
|
||||
end
|
||||
|
||||
self.update = Snacks.util.debounce(function()
|
||||
update(self)
|
||||
end, { ms = 50 })
|
||||
return self
|
||||
end
|
||||
|
||||
---@param win number
|
||||
---@return snacks.image.Dim
|
||||
function M:dim(win)
|
||||
local border = setmetatable({ opts = vim.api.nvim_win_get_config(win) }, { __index = Snacks.win }):border_size()
|
||||
local pos = vim.api.nvim_win_get_position(win)
|
||||
return {
|
||||
row = pos[1] + border.top,
|
||||
col = pos[2] + border.left,
|
||||
width = vim.api.nvim_win_get_width(win),
|
||||
height = vim.api.nvim_win_get_height(win),
|
||||
}
|
||||
end
|
||||
|
||||
---@param win? number
|
||||
function M:hide(win)
|
||||
self:request({ a = "d", i = self.id, p = win })
|
||||
end
|
||||
|
||||
function M:update()
|
||||
if not self:ready() then
|
||||
return
|
||||
end
|
||||
dd("update")
|
||||
-- hide images that are no longer visible
|
||||
for win in pairs(self.wins) do
|
||||
local buf = vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win)
|
||||
if buf ~= self.buf then
|
||||
self:hide(win)
|
||||
self.wins[win] = nil
|
||||
end
|
||||
end
|
||||
|
||||
for _, win in pairs(vim.api.nvim_list_wins()) do
|
||||
if vim.api.nvim_win_get_buf(win) == self.buf then
|
||||
self:render(win)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param win number
|
||||
function M:render(win)
|
||||
if not vim.api.nvim_win_is_valid(win or win) then
|
||||
return
|
||||
end
|
||||
local dim = self:dim(win)
|
||||
self.wins[win] = dim
|
||||
vim.api.nvim_win_call(win, function()
|
||||
io.write("\27[" .. (dim.row + 1) .. ";" .. (dim.col + 1) .. "H")
|
||||
self:request({
|
||||
a = "p",
|
||||
i = self.id,
|
||||
p = win,
|
||||
c = dim.width,
|
||||
r = dim.height,
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
function M:ready()
|
||||
return vim.api.nvim_buf_is_valid(self.buf) and (not self._convert or self._convert:is_closing())
|
||||
end
|
||||
|
||||
function M:create()
|
||||
-- create the image
|
||||
self:request({
|
||||
f = 100,
|
||||
s = 2,
|
||||
t = "f",
|
||||
i = self.id,
|
||||
data = self.file,
|
||||
})
|
||||
end
|
||||
|
||||
function M:convert()
|
||||
local ext = vim.fn.fnamemodify(self.file, ":e")
|
||||
if ext == "png" then
|
||||
return
|
||||
end
|
||||
local fin = ext == "gif" and self.file .. "[0]" or self.file
|
||||
local root = vim.fn.stdpath("cache") .. "/snacks/image"
|
||||
vim.fn.mkdir(root, "p")
|
||||
self.file = root .. "/" .. Snacks.util.file_encode(fin) .. ".png"
|
||||
if vim.fn.filereadable(self.file) == 1 then
|
||||
return
|
||||
end
|
||||
self._convert = uv.spawn("magick", {
|
||||
args = {
|
||||
fin,
|
||||
self.file,
|
||||
},
|
||||
}, function()
|
||||
self._convert:close()
|
||||
vim.schedule(function()
|
||||
self:create()
|
||||
self:update()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param opts table<string, string|number>|{data?: string}
|
||||
function M:request(opts)
|
||||
opts.q = opts.q or 2 -- silence all
|
||||
local msg = {} ---@type string[]
|
||||
for k, v in pairs(opts) do
|
||||
if k ~= "data" then
|
||||
table.insert(msg, string.format("%s=%s", k, v))
|
||||
end
|
||||
end
|
||||
msg = { table.concat(msg, ",") }
|
||||
if opts.data then
|
||||
msg[#msg + 1] = ";"
|
||||
msg[#msg + 1] = vim.base64.encode(opts.data)
|
||||
end
|
||||
local data = "\27_G" .. table.concat(msg) .. "\27\\"
|
||||
io.write(data)
|
||||
end
|
||||
|
||||
---@param file string
|
||||
function M.supports(file)
|
||||
return vim.tbl_contains(exts, vim.fn.fnamemodify(file, ":e"))
|
||||
end
|
||||
|
||||
return M
|
|
@ -157,8 +157,9 @@ function M.setup(opts)
|
|||
load("UIEnter")
|
||||
end
|
||||
|
||||
local group = vim.api.nvim_create_augroup("snacks", { clear = true })
|
||||
vim.api.nvim_create_autocmd(vim.tbl_keys(events), {
|
||||
group = vim.api.nvim_create_augroup("snacks", { clear = true }),
|
||||
group = group,
|
||||
once = true,
|
||||
nested = true,
|
||||
callback = function(ev)
|
||||
|
@ -166,6 +167,16 @@ function M.setup(opts)
|
|||
end,
|
||||
})
|
||||
|
||||
if M.config.image.enabled then
|
||||
vim.api.nvim_create_autocmd("BufReadCmd", {
|
||||
pattern = "*.png,*.jpg,*.jpeg,*.gif,*.bmp",
|
||||
group = group,
|
||||
callback = function(e)
|
||||
require("snacks.image").new(e.buf)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
if M.config.statuscolumn.enabled then
|
||||
vim.o.statuscolumn = [[%!v:lua.require'snacks.statuscolumn'.get()]]
|
||||
end
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
---@field git snacks.git
|
||||
---@field gitbrowse snacks.gitbrowse
|
||||
---@field health snacks.health
|
||||
---@field image snacks.image
|
||||
---@field indent snacks.indent
|
||||
---@field input snacks.input
|
||||
---@field layout snacks.layout
|
||||
|
@ -40,6 +41,7 @@
|
|||
---@field dim? snacks.dim.Config|{}
|
||||
---@field explorer? snacks.explorer.Config|{}
|
||||
---@field gitbrowse? snacks.gitbrowse.Config|{}
|
||||
---@field image? snacks.image.Config|{}
|
||||
---@field indent? snacks.indent.Config|{}
|
||||
---@field input? snacks.input.Config|{}
|
||||
---@field layout? snacks.layout.Config|{}
|
||||
|
|
|
@ -280,6 +280,17 @@ function M.throttle(fn, opts)
|
|||
end
|
||||
end
|
||||
|
||||
---@generic T
|
||||
---@param fn T
|
||||
---@param opts? {ms?:number}
|
||||
---@return T
|
||||
function M.debounce(fn, opts)
|
||||
local timer, ms = assert(uv.new_timer()), opts and opts.ms or 20
|
||||
return function()
|
||||
timer:start(ms, 0, vim.schedule_wrap(fn))
|
||||
end
|
||||
end
|
||||
|
||||
---@param key string
|
||||
function M.normkey(key)
|
||||
if key_cache[key] then
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue