---@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 ---@class snacks.picker.proc.Config: snacks.picker.Config ---@field cmd string ---@field sep? string ---@field args? string[] ---@field env? table ---@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 svim.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