mirror of
https://github.com/folke/snacks.nvim
synced 2025-07-07 21:25:11 +00:00
185 lines
4.8 KiB
Lua
185 lines
4.8 KiB
Lua
---@diagnostic disable: await-in-sync
|
|
local Async = require("snacks.picker.util.async")
|
|
|
|
local M = {}
|
|
|
|
local uv = vim.uv or vim.loop
|
|
M.USE_QUEUE = true
|
|
local islist = vim.islist or vim.tbl_islist
|
|
|
|
---@class snacks.picker.proc.Config: snacks.picker.Config
|
|
---@field cmd string
|
|
---@field sep? string
|
|
---@field args? string[]
|
|
---@field env? table<string, string>
|
|
---@field cwd? string
|
|
---@field notify? boolean Notify on failure
|
|
---@field transform? snacks.picker.transform
|
|
|
|
---@param opts snacks.picker.proc.Config|{[1]: snacks.picker.Config, [2]: snacks.picker.proc.Config}
|
|
---@type snacks.picker.finder
|
|
function M.proc(opts, ctx)
|
|
if islist(opts) then
|
|
local transform = opts[2].transform
|
|
opts = Snacks.config.merge(unpack(vim.deepcopy(opts))) --[[@as snacks.picker.proc.Config]]
|
|
opts.transform = transform
|
|
end
|
|
---@cast opts snacks.picker.proc.Config
|
|
assert(opts.cmd, "`opts.cmd` is required")
|
|
---@async
|
|
return function(cb)
|
|
if opts.transform then
|
|
local _cb = cb
|
|
cb = function(item)
|
|
local t = opts.transform(item, ctx)
|
|
item = type(t) == "table" and t or item
|
|
if t ~= false then
|
|
_cb(item)
|
|
end
|
|
end
|
|
end
|
|
|
|
if ctx.picker.opts.debug.proc then
|
|
vim.schedule(function()
|
|
Snacks.debug.cmd(Snacks.config.merge(opts, { group = true }))
|
|
end)
|
|
end
|
|
|
|
local sep = opts.sep or "\n"
|
|
local aborted = false
|
|
local stdout = assert(uv.new_pipe())
|
|
|
|
local self = Async.running()
|
|
|
|
local spawn_opts = {
|
|
args = opts.args,
|
|
stdio = { nil, stdout, nil },
|
|
cwd = opts.cwd and svim.fs.normalize(opts.cwd) or nil,
|
|
env = opts.env,
|
|
hide = true,
|
|
}
|
|
|
|
local handle ---@type uv.uv_process_t
|
|
---@diagnostic disable-next-line: missing-fields
|
|
handle = uv.spawn(opts.cmd, spawn_opts, function(code, _signal)
|
|
if not aborted and code ~= 0 and opts.notify ~= false then
|
|
local full = { opts.cmd or "" }
|
|
vim.list_extend(full, opts.args or {})
|
|
Snacks.notify.error(("Command failed:\n- cmd: `%s`"):format(table.concat(full, " ")))
|
|
end
|
|
handle:close()
|
|
self:resume()
|
|
end)
|
|
if not handle then
|
|
return Snacks.notify.error("Failed to spawn " .. opts.cmd)
|
|
end
|
|
|
|
local prev ---@type string?
|
|
local queue = require("snacks.picker.util.queue").new()
|
|
|
|
self:on("abort", function()
|
|
stdout:read_stop()
|
|
if not stdout:is_closing() then
|
|
stdout:close()
|
|
end
|
|
aborted = true
|
|
queue:clear()
|
|
cb = function() end
|
|
if not handle:is_closing() then
|
|
handle:kill("sigterm")
|
|
vim.defer_fn(function()
|
|
if not handle:is_closing() then
|
|
handle:kill("sigkill")
|
|
end
|
|
end, 200)
|
|
end
|
|
end)
|
|
|
|
---@param data? string
|
|
local function process(data)
|
|
if aborted then
|
|
return
|
|
end
|
|
if not data then
|
|
return prev and cb({ text = prev })
|
|
end
|
|
local from = 1
|
|
while from <= #data do
|
|
local nl = data:find(sep, from, true)
|
|
if nl then
|
|
local cr = data:byte(nl - 1, nl - 1) == 13 -- \r
|
|
local line = data:sub(from, nl - (cr and 2 or 1))
|
|
if prev then
|
|
line, prev = prev .. line, nil
|
|
end
|
|
cb({ text = line })
|
|
from = nl + 1
|
|
elseif prev then
|
|
prev = prev .. data:sub(from)
|
|
break
|
|
else
|
|
prev = data:sub(from)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
stdout:read_start(function(err, data)
|
|
assert(not err, err)
|
|
if aborted or not data then
|
|
stdout:close()
|
|
self:resume()
|
|
return
|
|
end
|
|
if M.USE_QUEUE then
|
|
queue:push(data)
|
|
self:resume()
|
|
else
|
|
process(data)
|
|
end
|
|
end)
|
|
|
|
while not (stdout:is_closing() and queue:empty()) do
|
|
if queue:empty() then
|
|
self:suspend()
|
|
else
|
|
process(queue:pop())
|
|
end
|
|
end
|
|
-- process the last line
|
|
if prev then
|
|
cb({ text = prev })
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param opts {cmd: string, args?: string[], cwd?: string}
|
|
function M.debug(opts)
|
|
vim.schedule(function()
|
|
local lines = { opts.cmd } ---@type string[]
|
|
for _, arg in ipairs(opts.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
|
|
end
|
|
local id = opts.cmd
|
|
for _, a in ipairs(opts.args or {}) do
|
|
if a:find("^-") then
|
|
id = id .. " " .. a
|
|
end
|
|
end
|
|
Snacks.notify.info(
|
|
("- **cwd**: `%s`\n```sh\n%s\n```"):format(
|
|
vim.fn.fnamemodify(svim.fs.normalize(opts.cwd or uv.cwd() or "."), ":~"),
|
|
table.concat(lines, "\n")
|
|
),
|
|
{ id = "snacks.picker.proc." .. id, title = "Snacks Proc" }
|
|
)
|
|
end)
|
|
end
|
|
|
|
return M
|