feat(image): show progress indicator when converting image files

This commit is contained in:
Folke Lemaitre 2025-02-17 23:56:55 +01:00
parent 8d073ccc0c
commit b65178b470
No known key found for this signature in database
GPG key ID: 41F8B1FBACAE2040
9 changed files with 108 additions and 33 deletions

View file

@ -345,7 +345,7 @@ function M.metrics()
Snacks.notify.warn(lines, { title = "Metrics" })
end
---@param opts {cmd: string|string[], args?: string[], cwd?: string, group?: boolean}
---@param opts {cmd: string|string[], args?: string[], cwd?: string, group?: boolean, notify?: boolean}
function M.cmd(opts)
local cmd = opts.cmd
local args = vim.deepcopy(opts.args or {})
@ -355,26 +355,25 @@ function M.cmd(opts)
end
args = vim.tbl_map(tostring, args)
---@cast cmd string
vim.schedule(function()
local lines = { cmd } ---@type string[]
for _, arg in ipairs(args or {}) do
arg = arg:find("[%$%s%?]") and vim.fn.shellescape(arg) or arg
if #arg + #lines[#lines] > 40 then
lines[#lines] = lines[#lines] .. " \\"
table.insert(lines, " " .. arg)
else
lines[#lines] = lines[#lines] .. " " .. arg
end
local lines = { cmd } ---@type string[]
for _, arg in ipairs(args or {}) do
arg = arg:find("[%$%s%?]") and vim.fn.shellescape(arg) or arg
if #arg + #lines[#lines] > 40 then
lines[#lines] = lines[#lines] .. " \\"
table.insert(lines, " " .. arg)
else
lines[#lines] = lines[#lines] .. " " .. arg
end
local id = cmd
Snacks.notify.info(
("- **cwd**: `%s`\n```sh\n%s\n```"):format(
vim.fn.fnamemodify(vim.fs.normalize(opts.cwd or uv.cwd() or "."), ":~"),
table.concat(lines, "\n")
),
{ id = opts.group and ("snacks.debug.cmd." .. id) or nil, title = "Cmd Debug" }
)
end)
end
local id = cmd
local msg = ("- **cwd**: `%s`\n```sh\n%s\n```"):format(
vim.fn.fnamemodify(vim.fs.normalize(opts.cwd or uv.cwd() or "."), ":~"),
table.concat(lines, "\n")
)
if opts.notify ~= false then
Snacks.notify.info(msg, { id = opts.group and ("snacks.debug.cmd." .. id) or nil, title = "Cmd Debug" })
end
return msg
end
return M

View file

@ -61,6 +61,13 @@ function M:on_send()
end
end
function M:failed()
if self._proc and self._proc:failed() then
return true
end
return self.file and vim.fn.filereadable(self.file) == 0
end
function M:ready()
if self._proc and self._proc:running() then
return false
@ -73,7 +80,11 @@ function M:convert()
run = false,
on_exit = function(procs, err)
if err then
Snacks.notify.error("Failed to convert image to " .. self.file)
vim.schedule(function()
for _, p in pairs(self.placements) do
p:error()
end
end)
else
vim.schedule(function()
self:on_ready()

View file

@ -112,6 +112,11 @@ Snacks.config.style("snacks_image", {
-- width/height are automatically set by the image size unless specified below
})
Snacks.util.set_hl({
Spinner = "Special",
Loading = "NonText",
}, { prefix = "SnacksImage", default = true })
---@class snacks.image.Opts
---@field pos? snacks.image.Pos (row, col) (1,0)-indexed. defaults to the top-left corner
---@field inline? boolean render the image inline in the buffer

View file

@ -13,6 +13,7 @@ local M = {}
M.__index = M
local terminal = Snacks.image.terminal
local uv = vim.uv or vim.loop
local PLACEHOLDER = vim.fn.nr2char(0x10EEEE)
-- stylua: ignore
local diacritics = vim.split( "0305,030D,030E,0310,0312,033D,033E,033F,0346,034A,034B,034C,0350,0351,0352,0357,035B,0363,0364,0365,0366,0367,0368,0369,036A,036B,036C,036D,036E,036F,0483,0484,0485,0486,0487,0592,0593,0594,0595,0597,0598,0599,059C,059D,059E,059F,05A0,05A1,05A8,05A9,05AB,05AC,05AF,05C4,0610,0611,0612,0613,0614,0615,0616,0617,0657,0658,0659,065A,065B,065D,065E,06D6,06D7,06D8,06D9,06DA,06DB,06DC,06DF,06E0,06E1,06E2,06E4,06E7,06E8,06EB,06EC,0730,0732,0733,0735,0736,073A,073D,073F,0740,0741,0743,0745,0747,0749,074A,07EB,07EC,07ED,07EE,07EF,07F0,07F1,07F3,0816,0817,0818,0819,081B,081C,081D,081E,081F,0820,0821,0822,0823,0825,0826,0827,0829,082A,082B,082C,082D,0951,0953,0954,0F82,0F83,0F86,0F87,135D,135E,135F,17DD,193A,1A17,1A75,1A76,1A77,1A78,1A79,1A7A,1A7B,1A7C,1B6B,1B6D,1B6E,1B6F,1B70,1B71,1B72,1B73,1CD0,1CD1,1CD2,1CDA,1CDB,1CE0,1DC0,1DC1,1DC3,1DC4,1DC5,1DC6,1DC7,1DC8,1DC9,1DCB,1DCC,1DD1,1DD2,1DD3,1DD4,1DD5,1DD6,1DD7,1DD8,1DD9,1DDA,1DDB,1DDC,1DDD,1DDE,1DDF,1DE0,1DE1,1DE2,1DE3,1DE4,1DE5,1DE6,1DFE,20D0,20D1,20D4,20D5,20D6,20D7,20DB,20DC,20E1,20E7,20E9,20F0,2CEF,2CF0,2CF1,2DE0,2DE1,2DE2,2DE3,2DE4,2DE5,2DE6,2DE7,2DE8,2DE9,2DEA,2DEB,2DEC,2DED,2DEE,2DEF,2DF0,2DF1,2DF2,2DF3,2DF4,2DF5,2DF6,2DF7,2DF8,2DF9,2DFA,2DFB,2DFC,2DFD,2DFE,2DFF,A66F,A67C,A67D,A6F0,A6F1,A8E0,A8E1,A8E2,A8E3,A8E4,A8E5,A8E6,A8E7,A8E8,A8E9,A8EA,A8EB,A8EC,A8ED,A8EE,A8EF,A8F0,A8F1,AAB0,AAB2,AAB3,AAB7,AAB8,AABE,AABF,AAC1,FE20,FE21,FE22,FE23,FE24,FE25,FE26,10A0F,10A38,1D185,1D186,1D187,1D188,1D189,1D1AA,1D1AB,1D1AC,1D1AD,1D242,1D243,1D244", ",")
@ -80,6 +81,10 @@ function M.new(buf, src, opts)
vim.schedule(function()
self:update()
end)
elseif self.img:failed() then
self:error()
else
self:progress()
end
local update = self.update
@ -89,6 +94,53 @@ function M.new(buf, src, opts)
return self
end
function M:error()
if self.opts.inline then
return
end
local msg = "# Image Conversion Failed:\n\n"
local proc = self.img._proc
if proc then
msg = msg .. Snacks.debug.cmd({ cmd = proc.opts.cmd, args = proc.opts.args, cwd = proc.opts.cwd, notify = false })
msg = msg .. "\n\n# Output\n" .. proc:out() .. "\n\n# Error\n" .. proc:err()
end
local lines = vim.split(msg, "\n")
vim.bo[self.buf].modifiable = true
vim.api.nvim_buf_set_lines(self.buf, 0, -1, false, lines)
vim.bo[self.buf].modifiable = false
if not vim.treesitter.start(self.buf, "markdown") then
vim.bo[self.buf].syntax = "markdown"
end
end
function M:progress()
if self.opts.inline or self:ready() then
return
end
vim.bo[self.buf].modifiable = true
vim.api.nvim_buf_set_lines(self.buf, 0, -1, false, {})
vim.bo[self.buf].modifiable = false
local timer = assert(uv.new_timer())
timer:start(
0,
80,
vim.schedule_wrap(function()
if self:ready() or self.img:failed() or not vim.api.nvim_buf_is_valid(self.buf) then
timer:stop()
return timer:close()
end
vim.api.nvim_buf_clear_namespace(self.buf, self.ns, 0, -1)
vim.api.nvim_buf_set_extmark(self.buf, self.ns, 0, 0, {
virt_text = {
{ Snacks.util.spinner(), "SnacksImageSpinner" },
{ " " },
{ self.img._proc.opts.cmd .. " loading …", "SnacksImageLoading" },
},
})
end)
)
end
---@return number[]
function M:wins()
---@param win number

View file

@ -6,7 +6,6 @@
local M = {}
M.__index = M
local uv = vim.uv or vim.loop
local ns = vim.api.nvim_create_namespace("snacks.picker.input")
---@param picker snacks.Picker
@ -144,7 +143,7 @@ function M:update()
end
local line = {} ---@type snacks.picker.Highlight[]
if self.picker:is_active() then
line[#line + 1] = { M.spinner(), "SnacksPickerSpinner" }
line[#line + 1] = { Snacks.util.spinner(), "SnacksPickerSpinner" }
line[#line + 1] = { " " }
end
local selected = #self.picker.list.selected
@ -188,9 +187,4 @@ function M:set(pattern, search)
self.picker:update_titles()
end
function M.spinner()
local spinner = { "", "", "", "", "", "", "", "", "", "" }
return spinner[math.floor(uv.hrtime() / (1e6 * 80)) % #spinner + 1]
end
return M

View file

@ -182,7 +182,9 @@ function M.cmd(cmd, ctx, opts)
if ctx.picker.opts.debug.proc then
local args = vim.deepcopy(cmd)
table.remove(args, 1)
Snacks.debug.cmd({ cmd = cmd[1], args = args, cwd = ctx.item.cwd })
vim.schedule(function()
Snacks.debug.cmd({ cmd = cmd[1], args = args, cwd = ctx.item.cwd })
end)
end
---@param text string

View file

@ -40,7 +40,9 @@ function M.proc(opts, ctx)
end
if ctx.picker.opts.debug.proc then
Snacks.debug.cmd(Snacks.config.merge(opts, { group = true }))
vim.schedule(function()
Snacks.debug.cmd(Snacks.config.merge(opts, { group = true }))
end)
end
local sep = opts.sep or "\n"

View file

@ -402,6 +402,11 @@ function M.is_float(win)
return vim.api.nvim_win_get_config(win or 0).relative ~= ""
end
function M.spinner()
local spinner = { "", "", "", "", "", "", "", "", "", "" }
return spinner[math.floor(uv.hrtime() / (1e6 * 80)) % #spinner + 1]
end
M.base64 = vim.base64 and vim.base64.encode
or function(data)
data = tostring(data)

View file

@ -62,6 +62,10 @@ function Proc:kill(signal)
end
end
function Proc:failed()
return self.code ~= 0 or self.signal ~= 0
end
function Proc:run()
assert(not self.handle, "already running")
self.stdout = assert(uv.new_pipe())
@ -82,6 +86,7 @@ function Proc:run()
end)
if not self.handle then
self.code = 1
self.data[self.stderr] = { "Failed to spawn " .. self.opts.cmd }
close(self.stdout)
close(self.stderr)
return self:on_exit()
@ -158,8 +163,7 @@ function M.multi(procs, opts)
local function done()
if opts.on_exit then
local err = procs[current].code ~= 0 or procs[current].signal ~= 0
opts.on_exit(procs, err)
opts.on_exit(procs, procs[current]:failed())
end
end
@ -179,8 +183,9 @@ function M.multi(procs, opts)
proc:run()
end
---@type snacks.spawn.Proc
---@type snacks.spawn.Proc|{procs: snacks.spawn.Proc[]}
local ret = setmetatable({
procs = procs,
run = next,
}, {
__index = function(_, k)