diff --git a/doc/neo-tree.txt b/doc/neo-tree.txt index af41d1118..abe39073d 100644 --- a/doc/neo-tree.txt +++ b/doc/neo-tree.txt @@ -15,6 +15,7 @@ Configuration ............... |neo-tree-configuration| Setup ..................... |neo-tree-setup| Source Selector ........... |neo-tree-source-selector| Filtered Items ............ |neo-tree-filtered-items| + Trash ..................... |neo-tree-trash| Preview Mode .............. |neo-tree-preview-mode| Hijack Netrw Behavior ..... |neo-tree-netrw-hijack| Component Configs ......... |neo-tree-component-configs| @@ -307,7 +308,11 @@ A = add_directory: Create a new directory, in this mode it does not command. Also accepts `config.show_path` options d = delete: Delete the selected file or directory. - Supports visual selection.~ + Supports visual selection. + +T = trash: Trash the selected file or directory. + See |neo-tree-trash| to configure. + Supports visual selection. i = show_file_details Show file details in a popup window, such as size and last modified date. @@ -798,7 +803,7 @@ wish to remove a default mapping without overriding it with your own function, assign it the string "none". This will cause it to be skipped and allow any existing global mappings to work. -NOTE: SOME OPTIONS ARE ONLY DOCUMENTED IN THE DEFAULT CONFIG!~ +NOTE: SOME OPTIONS ARE ONLY DOCUMENTED IN THE DEFAULT CONFIG! ~ Run `:lua require("neo-tree").paste_default_config()` to dump the fully commented default config in your current file. Even if you don't want to use that config as your starting point, you still may want to dump it to a blank @@ -975,8 +980,95 @@ In addition to `"tab"` and `"window"`, you can also set the target to `"global"` for either option, which is the same as using the |cd| command. Setting the target to `"none"` will prevent neo-tree from setting vim's cwd for that position. +TRASH *neo-tree-trash* + +Neo-tree can use existing commands on your system to trash files. By default, +this is bound to `T`. + +The default commands that neo-tree tries to use, in order, are: + +MacOS: `trash`, or `osascript` commands. + +Linux: `gio trash`, `trash`, and `kioclient/kioclient5`. + +Windows: `trash`, or `PowerShell` commands. + +You can configure the command used by specifying `trash.command` in `setup()`: + +>lua + require("neo-tree").setup({ + trash = { + command = nil -- the default - uses built-ins. + } + }) +< + +This option takes three types of commands: + +1. A string array listing an `rm`-like trash command to use. In otherwords, a + CLI tool where the invocation of the command looks like `trash ...FILES`. + +>lua + require("neo-tree").setup({ + trash = { + command = { "your-trash-cli", "--put" } + } + }) +< + +2. Or a function that either returns a list of commands to execute that will + result in the deletion of the items: + +>lua + require("neo-tree").setup({ + trash = { + ---@param paths string[] + ---@return string[][] commands + command = function(paths) + if not require("neo-tree.utils").executable("mv") then + return nil -- defer to built-ins + end + local cmds = {} + for i, p in ipairs(paths) do + cmds[#cmds+1] = { "mv", p, ("%s/trash"):format(vim.env.HOME) } + end + return cmds + end + } + }) +< + +(if any of these commands fail, the rest are aborted). + +3. Or a function that is entirely responsible for deletion, and returns a + (true/false, string) result tuple. Return false to defer to builtin handlers, + return true to succeed, error to stop. + +>lua + require("neo-tree").setup({ + trash = { + ---@type neotree.trash.FunctionGenerator fun(paths: string[]):(fun():boolean,string?)? + command = function(paths) + if not setup then + return nil -- defer to built-ins + end + return function() + for i, p in ipairs(paths) do + -- ... logic to trash the given paths + if something_failed then + return false, err + end + end + + return true + end + end + } + }) +< + -FILTERED ITEMS *neo-tree-filtered-items* +FILTERED ITEMS *neo-tree-filtered-items* The `filesystem` source has a `filtered_items` section in it's config that allows you to specify what files and folders should be hidden. By default, any diff --git a/lua/neo-tree/defaults.lua b/lua/neo-tree/defaults.lua index 711051a3e..922cbba71 100644 --- a/lua/neo-tree/defaults.lua +++ b/lua/neo-tree/defaults.lua @@ -60,6 +60,9 @@ local config = { sort_function = nil , -- uses a custom function for sorting files and directories in the tree use_popups_for_input = true, -- If false, inputs will use vim.ui.input() instead of custom floats. use_default_mappings = true, + trash = { + cmd = nil -- by default: powershell script on windows, `trash` or `osascript` on macOS, and `gio trash` or `trash` (like trash-cli) on other Unixes + }, -- source_selector provides clickable tabs to switch between sources. source_selector = { winbar = false, -- toggle to show selector on winbar @@ -451,6 +454,8 @@ local config = { }, ["A"] = "add_directory", -- also accepts the config.show_path and config.insert_as options. ["d"] = "delete", + -- ["d"] = "trash", + ["T"] = "trash", ["r"] = "rename", ["y"] = "copy_to_clipboard", ["x"] = "cut_to_clipboard", diff --git a/lua/neo-tree/health/init.lua b/lua/neo-tree/health/init.lua index 7ea49c847..d8c8f361d 100644 --- a/lua/neo-tree/health/init.lua +++ b/lua/neo-tree/health/init.lua @@ -1,4 +1,5 @@ local typecheck = require("neo-tree.health.typecheck") +local utils = require("neo-tree.utils") local health = vim.health local M = {} @@ -21,8 +22,24 @@ local check_dependency = function(modname, repo, optional) health.ok(repo .. " is installed") end +---@param path string +---@param desc string? +---@return boolean success +local check_executable = function(path, desc) + if utils.executable(path) then + health.ok(("`%s` is executable"):format(path)) + return true + end + local warning = ("`%s` not found"):format(path) + if desc then + warning = table.concat({ warning, desc }, " ") + end + health.warn(warning) + return false +end + function M.check() - health.start("Dependencies") + health.start("Required dependencies") check_dependency("plenary", "nvim-lua/plenary.nvim") check_dependency("nui.tree", "MunifTanjim/nui.nvim") @@ -42,6 +59,27 @@ function M.check() health.start("Configuration") local config = require("neo-tree").ensure_config() M.check_config(config) + + health.start("Trash executables (prioritized in descending order)") + if utils.is_windows then + check_executable("trash", "(from https://github.com/sindresorhus/trash#cli or similar)") + if not check_executable("pwsh", "(https://github.com/PowerShell/PowerShell)") then + check_executable("powershell", "(builtin Windows PowerShell)") + end + elseif utils.is_macos then + check_executable("trash", "(builtin)") + check_executable("osascript", "(builtin)") + else + if check_executable("gio", "(from glib2)") then + if not utils.execute_command({ "gio", "trash", "--list" }) then + health.warn("`gio trash` --list failed, maybe you need `gvfs` installed?") + end + end + check_executable("trash", "(from https://github.com/andreafrancia/trash-cli or similar)") + if not check_executable("kioclient") then + check_executable("kioclient5") + end + end end local validate = typecheck.validate @@ -59,6 +97,9 @@ function M.check_config(config) function(cfg) ---@class neotree.health.Validator.Generators local v = { + ---@generic T + ---@param validator neotree.health.Validator + ---@return fun(arr: T[]) array = function(validator) ---@generic T ---@param arr T[] @@ -177,6 +218,15 @@ function M.check_config(config) validate("sort_function", cfg.sort_function, "function", true) validate("use_popups_for_input", cfg.use_popups_for_input, "boolean") validate("use_default_mappings", cfg.use_default_mappings, "boolean") + validate("trash", cfg.trash, function(trash) + validate("cmd", trash.cmd, function(cmd) + if type(cmd) == "function" then + return true -- TODO: maybe better validation here + elseif type(cmd) == "table" then + v.array("string")(cmd) + end + end, true) + end) validate("source_selector", cfg.source_selector, function(ss) validate("winbar", ss.winbar, "boolean") validate("statusline", ss.statusline, "boolean") diff --git a/lua/neo-tree/log.lua b/lua/neo-tree/log.lua index 047a6d635..251d9911d 100644 --- a/lua/neo-tree/log.lua +++ b/lua/neo-tree/log.lua @@ -100,6 +100,7 @@ log_maker.new = function(config) or table.concat({ config.plugin_short, prefix }, " ") local title_opts = { title = config.plugin_short } + ---@param message string ---@param level vim.log.levels local notify = vim.schedule_wrap(function(message, level) @@ -205,9 +206,7 @@ log_maker.new = function(config) -- Output to console if config.use_console and can_log_to_console then - vim.schedule(function() - notify(msg, log_level) - end) + notify(msg, log_level) end end end @@ -262,6 +261,9 @@ log_maker.new = function(config) log.error = logfunc(Levels.ERROR, make_string) ---Unused, kept around for compatibility at the moment. Remove in v4.0. log.fatal = logfunc(Levels.FATAL, make_string) + + log.notify = notify + log.levels = Levels -- tree-sitter queries recognize any .format and highlight it w/ string.format highlights ---@type table log.at = { @@ -374,8 +376,11 @@ log_maker.new = function(config) else errmsg = "assertion failed!" end + local old = config.use_console + config.use_console = false log.error(errmsg) - return assert(v, errmsg) + config.use_console = old + error(errmsg, 2) end ---@param context string diff --git a/lua/neo-tree/sources/common/commands.lua b/lua/neo-tree/sources/common/commands.lua index ac6b1d198..ae63012b4 100644 --- a/lua/neo-tree/sources/common/commands.lua +++ b/lua/neo-tree/sources/common/commands.lua @@ -704,7 +704,7 @@ M.delete = function(state, callback) fs_actions.delete_node(node.path, callback) end ----@param callback function +---@param callback fun(path: string) ---@type neotree.TreeCommandVisual M.delete_visual = function(state, selected_nodes, callback) local paths_to_delete = {} @@ -725,6 +725,44 @@ M.delete_visual = function(state, selected_nodes, callback) fs_actions.delete_nodes(paths_to_delete, callback) end +M.trash = function(state) + local node = assert(state.tree:get_node()) + if node.type ~= "file" and node.type ~= "directory" then + log.warn("The `trash` command can only be used on files and directories") + return + end + if node:get_depth() == 1 then + log.error( + "Will not trash root node " + .. node.path + .. ", please back out of the current directory if you want to trash the root node." + ) + return + end + fs_actions.trash_node(node.path) +end + +---@param callback fun(path: string) +---@type neotree.TreeCommandVisual +M.trash_visual = function(state, selected_nodes, callback) + local paths_to_trash = {} + for _, node_to_trash in pairs(selected_nodes) do + if node_to_trash:get_depth() == 1 then + log.error( + "Will not trash root node " + .. node_to_trash.path + .. ", please back out of the current directory if you want to trash the root node." + ) + return + end + + if node_to_trash.type == "file" or node_to_trash.type == "directory" then + table.insert(paths_to_trash, node_to_trash.path) + end + end + fs_actions.trash_nodes(paths_to_trash, callback) +end + M.preview = function(state) Preview.show(state) end diff --git a/lua/neo-tree/sources/filesystem/lib/fs_actions.lua b/lua/neo-tree/sources/filesystem/lib/fs_actions.lua index a97da67e2..af467ca6a 100644 --- a/lua/neo-tree/sources/filesystem/lib/fs_actions.lua +++ b/lua/neo-tree/sources/filesystem/lib/fs_actions.lua @@ -4,11 +4,11 @@ -- https://github.com/mhartington/dotfiles -- and modified to fit neo-tree's api. -- Permalink: https://github.com/mhartington/dotfiles/blob/7560986378753e0c047d940452cb03a3b6439b11/config/nvim/lua/mh/filetree/init.lua -local api = vim.api local uv = vim.uv or vim.loop local scan = require("plenary.scandir") local utils = require("neo-tree.utils") local inputs = require("neo-tree.ui.inputs") +local trash = require("neo-tree.sources.filesystem.lib.trash") local events = require("neo-tree.events") local log = require("neo-tree.log") local Path = require("plenary.path") @@ -327,7 +327,7 @@ M.copy_node = function(source, destination, callback, using_root_directory) local source_stat = log.assert(uv.fs_lstat(source)) if source_stat.type == "link" then - local target = log.assert(uv.fs_readlink(source)) + local target = log.assert(uv.fs_realpath(source)) local symlink_ok, err = uv.fs_symlink(target, destination) log.assert(symlink_ok, "Could not copy symlink ", source, "to", destination, ":", err) else @@ -542,17 +542,28 @@ local function delete_dir(dir_path) end -- Delete Node +---@param path string +---@param callback fun(string)? +---@param noconfirm boolean? M.delete_node = function(path, callback, noconfirm) local _, name = utils.split_path(path) - local msg = string.format("Are you sure you want to delete '%s'?", name) log.trace("Deleting node:", path) local _type = "unknown" - local stat = uv.fs_stat(path) - if stat then + local stat = uv.fs_lstat(path) + local children_count = 0 + if not stat then + log.warn("Could not read file/dir:", path, stat, ", attempting to delete anyway...") + -- Guess the type by whether it appears to have an extension + if path:match("%.(.+)$") then + _type = "file" + else + _type = "directory" + end + else _type = stat.type if _type == "link" then - local link_to = uv.fs_readlink(path) + local link_to = uv.fs_realpath(path) if not link_to then log.error("Could not read link") return @@ -564,25 +575,13 @@ M.delete_node = function(path, callback, noconfirm) _type = uv.fs_stat(link_to).type end if _type == "directory" then - local children = scan.scan_dir(path, { + children_count = #scan.scan_dir(path, { hidden = true, respect_gitignore = false, add_dirs = true, depth = 1, }) - if #children > 0 then - msg = "WARNING: Dir not empty! " .. msg - end - end - else - log.warn("Could not read file/dir:", path, stat, ", attempting to delete anyway...") - -- Guess the type by whether it appears to have an extension - if path:match("%.(.+)$") then - _type = "file" - else - _type = "directory" end - return end local do_delete = function() @@ -599,29 +598,34 @@ M.delete_node = function(path, callback, noconfirm) return end - if _type == "directory" then + if _type ~= "directory" then + local success = uv.fs_unlink(path) + if not success then + return log.error("Could not remove file: " .. path) + end + clear_buffer(path) + else -- first try using native system commands, which are recursive local success = false if utils.is_windows then - local result = - vim.fn.system({ "cmd.exe", "/c", "rmdir", "/s", "/q", vim.fn.shellescape(path) }) - local error = vim.v.shell_error - if error ~= 0 then + local delete_ok, result = + utils.execute_command({ "cmd.exe", "/c", "rmdir", "/s", "/q", vim.fn.shellescape(path) }) + if not delete_ok then log.debug("Could not delete directory '", path, "' with rmdir: ", result) else log.info("Deleted directory ", path) success = true end else - local result = vim.fn.system({ "rm", "-Rf", path }) - local error = vim.v.shell_error - if error ~= 0 then + local delete_ok, result = utils.execute_command({ "rm", "-Rf", path }) + if not delete_ok then log.debug("Could not delete directory '", path, "' with rm: ", result) else log.info("Deleted directory ", path) success = true end end + -- Fallback to using libuv if native commands fail if not success then success = delete_dir(path) @@ -629,12 +633,6 @@ M.delete_node = function(path, callback, noconfirm) return log.error("Could not remove directory: " .. path) end end - else - local success = uv.fs_unlink(path) - if not success then - return log.error("Could not remove file: " .. path) - end - clear_buffer(path) end complete() end @@ -642,6 +640,14 @@ M.delete_node = function(path, callback, noconfirm) if noconfirm then do_delete() else + local msg = string.format("Are you sure you want to delete '%s'?", name) + if children_count > 0 then + msg = ("WARNING: Dir has %s %s! %s"):format( + children_count, + children_count == 1 and "child" or "children", + msg + ) + end inputs.confirm(msg, function(confirmed) if confirmed then do_delete() @@ -650,6 +656,8 @@ M.delete_node = function(path, callback, noconfirm) end end +---@param paths_to_delete string[] +---@param callback fun(path)? M.delete_nodes = function(paths_to_delete, callback) local msg = "Are you sure you want to delete " .. #paths_to_delete .. " items?" inputs.confirm(msg, function(confirmed) @@ -669,6 +677,89 @@ M.delete_nodes = function(paths_to_delete, callback) end) end +-- Trash Node +---@param path string +---@param callback fun(string)? +---@param noconfirm boolean? +M.trash_node = function(path, callback, noconfirm) + local _, name = utils.split_path(path) + + log.trace("Trashing node:", path) + local _type = "unknown" + local stat = uv.fs_lstat(path) + local children_count = 0 + if not stat then + log.error("Could not read", path) + return + end + + _type = stat.type + if _type == "directory" then + children_count = #scan.scan_dir(path, { + hidden = true, + respect_gitignore = false, + add_dirs = true, + depth = 1, + }) + end + + local do_trash = function() + local event_result = events.fire_event(events.BEFORE_FILE_DELETE, path) or {} + if not event_result.handled then + log.assert(trash.trash({ path })) + log.info("Trashed", path) + return + end + + vim.schedule(function() + events.fire_event(events.FILE_DELETED, path) + if callback then + callback(path) + end + end) + end + + if noconfirm then + do_trash() + else + local msg = string.format("Are you sure you want to trash '%s'?", name) + if children_count > 0 then + msg = ("WARNING: Dir has %s %s! %s"):format( + children_count, + children_count == 1 and "child" or "children", + msg + ) + end + inputs.confirm(msg, function(confirmed) + if confirmed then + do_trash() + end + end) + end +end + +---@param paths_to_trash string[] +---@param callback fun(path)? +M.trash_nodes = function(paths_to_trash, callback) + local msg = "Are you sure you want to trash " .. #paths_to_trash .. " items?" + inputs.confirm(msg, function(confirmed) + if not confirmed then + return + end + + for _, path in ipairs(paths_to_trash) do + M.trash_node(path, nil, true) + end + + log.info("Trashed", #paths_to_trash, #paths_to_trash == 1 and "file" or "files") + if callback then + vim.schedule(function() + callback(paths_to_trash[#paths_to_trash]) + end) + end + end) +end + local rename_node = function(msg, name, get_destination, path, callback) inputs.input(msg, name, function(new_name) -- If cancelled diff --git a/lua/neo-tree/sources/filesystem/lib/trash.lua b/lua/neo-tree/sources/filesystem/lib/trash.lua new file mode 100644 index 000000000..d4039d460 --- /dev/null +++ b/lua/neo-tree/sources/filesystem/lib/trash.lua @@ -0,0 +1,181 @@ +local utils = require("neo-tree.utils") +local log = require("neo-tree.log") +local validate = require("neo-tree.health.typecheck").validate +local M = {} + +---@alias neotree.trash.Command neotree.trash.PureCommand|neotree.trash.FunctionGenerator|neotree.trash.CommandGenerator + +---@class neotree.trash.PureCommand +---@field healthcheck? fun(paths: string[]):boolean,string? +---@field list? fun():boolean,string? +---@field [integer] string + +---A function that may return commands to execute, in order. +---@alias neotree.trash.CommandGenerator fun(paths: string[]):neotree.trash.PureCommand[]? + +---A function that may return a function that will do the trashing. +---@alias neotree.trash.FunctionGenerator fun(paths: string[]):((fun():success: boolean, err: string?)?) + +-- Using programs mentioned by +-- https://github.com/folke/snacks.nvim/blob/ed08ef1a630508ebab098aa6e8814b89084f8c03/lua/snacks/explorer/actions.lua +local trash_builtins = { + macos = { + { "trash" }, -- trash-cli, usually https://github.com/andreafrancia/trash-cli + function(p) + local cmds = {} + for i, path in ipairs(p) do + cmds[i] = { + "osascript", + "-e", + ('tell application "Finder" to delete POSIX file "%s"'):format( + path:gsub("\\", "\\\\"):gsub('"', '\\"') + ), + } + end + return cmds + end, + }, + linux = { + { + "gio", + "trash", + healthcheck = function() + return utils.executable("gio") and utils.execute_command({ "gio", "trash", "--list" }) + end, + }, + { "trash" }, -- trash-cli, usually https://github.com/andreafrancia/trash-cli + function(p) + local kioclient = utils.executable("kioclient") or utils.executable("kioclient5") + if not kioclient then + return nil + end + local kioclient_cmds = {} + for _, path in ipairs(p) do + kioclient_cmds[#kioclient_cmds + 1] = { kioclient, "move", path, "trash:/" } + end + return kioclient_cmds + end, + }, + windows = { + { "trash" }, -- trash-cli, usually https://github.com/sindresorhus/trash#cli + function(p) + local powershell = utils.executable("pwsh") or utils.executable("powershell") + if not powershell then + return nil + end + + local cmd = { + powershell, + "-NoProfile", + "-Command", + } + + local pwsh_cmds = { + "$shell = New-Object -ComObject 'Shell.Application';", + "$folder = $shell.NameSpace(0);", + } + for _, path in ipairs(p) do + local escaped = path:gsub("\\", "\\\\"):gsub("'", "''") + pwsh_cmds[#pwsh_cmds + 1] = ([[$path = Get-Item '%s'; $folder.ParseName($path.FullName).InvokeVerb('delete');]]):format( + escaped + ) + end + cmd[#cmd + 1] = table.concat(pwsh_cmds, " ") + return { + cmd, + } + end, + }, +} + +---@param cmds string[][] +---@return boolean success +---@return string[]? err +---@return integer? failed_at +local execute_trash_commands = function(cmds) + for i, cmd in ipairs(cmds) do + local success, err = utils.execute_command(cmd) + if not success then + return false, err, i + end + end + return true +end + +---@param paths string[] +---@return boolean success +---@return string? err +M.trash = function(paths) + log.assert(#paths > 0) + local commands = { + require("neo-tree").ensure_config().trash.cmd, + } + if utils.is_macos then + vim.list_extend(commands, trash_builtins.macos) + elseif utils.is_windows then + vim.list_extend(commands, trash_builtins.windows) + else + vim.list_extend(commands, trash_builtins.linux) + end + + ---@type string[][] + for _, command in ipairs(commands) do + repeat + if type(command) == "function" then + local res, err = command(paths) + log.debug("Trash function result:", res, err) + if not res then + break -- try next command + end + + if type(res) == "table" then + local cmds = res + local trash_ok, output, failed_at = execute_trash_commands(cmds) + if not trash_ok then + return false, + "Trash commands failed at " .. table.concat(cmds[failed_at], " ") .. ":" .. output + end + + log.debug("Trashed", paths, "using", cmds) + elseif type(res) == "function" then + local trash_ok, trashfunc_err = res() + if not trash_ok then + return false, "Trash function failed: " .. (trashfunc_err or "") + end + log.debug("Trashed", paths, "using function") + end + return true + end + + if type(command) == "table" then + if command.healthcheck then + local command_ok, err = command.healthcheck(paths) + if not command_ok then + log.debug("Trash command", command, "failed healthcheck:", err) + break -- try next command + end + elseif not utils.executable(command[1]) then + log.debug("Trash command", command, "not executable") + break -- try next command + end + + local full_command = vim.list_extend({ + unpack(command), + }, paths) + log.debug("Running trash command", full_command) + local trash_ok, output = execute_trash_commands({ full_command }) + if not trash_ok then + return false, "Could not trash with " .. full_command .. ":" .. output + end + + log.debug("Trashed", paths, "using", full_command) + return true + end + + return false, "Invalid trash command:" .. command + until true + end + return false, "No trash commands or functions worked." +end + +return M diff --git a/lua/neo-tree/types/config.lua b/lua/neo-tree/types/config.lua index 309334f34..f9d10990d 100644 --- a/lua/neo-tree/types/config.lua +++ b/lua/neo-tree/types/config.lua @@ -103,6 +103,9 @@ ---@field created neotree.Component.Common.Created? ---@field symlink_target neotree.Component.Common.SymlinkTarget? +---@class neotree.Config.Trash +---@field cmd neotree.trash.Command? + ---@alias neotree.Config.BorderStyle "NC"|"rounded"|"single"|"solid"|"double"|"" ---@class (exact) neotree.Config.Base @@ -141,6 +144,7 @@ ---@field nesting_rules neotree.filenesting.Rule[] ---@field commands table ---@field window neotree.Config.Window +---@field trash neotree.Config.Trash --- ---@field filesystem neotree.Config.Filesystem ---@field buffers neotree.Config.Buffers diff --git a/lua/neo-tree/utils/init.lua b/lua/neo-tree/utils/init.lua index f89b43c7f..fb921255f 100644 --- a/lua/neo-tree/utils/init.lua +++ b/lua/neo-tree/utils/init.lua @@ -191,12 +191,22 @@ M.tbl_equals = function(table1, table2) return true end +---@generic P : string +---@param path P +---@return P? path_if_executable +M.executable = function(path) + return vim.fn.executable(path) == 1 and path or nil +end + +---@param cmd string|string[] +---@return boolean success +---@return string[] result M.execute_command = function(cmd) local result = vim.fn.systemlist(cmd) -- An empty result is ok if vim.v.shell_error ~= 0 or (#result > 0 and vim.startswith(result[1], "fatal:")) then - return false, {} + return false, result or {} else return true, result end