snacks.nvim/lua/snacks/picker/source/proc.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