mirror of
https://github.com/folke/snacks.nvim
synced 2025-08-04 10:49:08 +00:00
feat(image): show progress indicator when converting image files
This commit is contained in:
parent
8d073ccc0c
commit
b65178b470
9 changed files with 108 additions and 33 deletions
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue