---@diagnostic disable-next-line: deprecated
local uv = vim.uv or vim.loop
local path = require "fzf-lua.path"
local shell = require "fzf-lua.shell"
local utils = require "fzf-lua.utils"
local libuv = require "fzf-lua.libuv"
local Object = require "fzf-lua.class"
local Previewer = {}
---@class fzf-lua.previewer.Fzf: fzf-lua.Object,{}
---@field close function
Previewer.base = Object:extend()
-- Previewer base object
---@param o fzf-lua.config.Previewer
---@param opts fzf-lua.config.Resolved|{}
---@return fzf-lua.previewer.Fzf
function Previewer.base:new(o, opts)
o = o or {}
self.type = "cmd";
self.cmd = o.cmd;
if type(self.cmd) == "function" then
self.cmd = self.cmd()
end
self.args = o.args or "";
self.preview_offset = o.preview_offset
self.opts = opts;
return self
end
---@param opts fzf-lua.config.Resolved
---@return fzf-lua.config.Resolved
function Previewer.base:setup_opts(opts)
-- Set the preview command line
opts.preview = require("fzf-lua.previewer").normalize_spec(self:cmdline(), self.opts)
opts.preview_offset = self:_preview_offset()
opts.fzf_opts["--preview-window"] = self:preview_window()
opts.fzf_opts["--delimiter"] = self:fzf_delimiter()
return opts
end
---@diagnostic disable-next-line: unused
function Previewer.base:close()
end
---@diagnostic disable-next-line: unused
---@return fzf-lua.preview.spec
function Previewer.base:cmdline(_)
return ""
end
---@diagnostic disable-next-line: unused
function Previewer.base:preview_window(_)
return nil
end
function Previewer.base:_preview_offset(lnum)
--[[
#
# Explanation of the fzf preview offset options:
#
# ~3 Top 3 lines as the fixed header
# +{2} Base scroll offset extracted from the second field
# +3 Extra offset to compensate for the 3-line header
# /2 Put in the middle of the preview area
#
'--preview-window '~3:+{2}+3/2''
]]
lnum = lnum or self.opts.line_field_index
if lnum then
return ("+%s-/2"):format(lnum)
end
end
---@return string
function Previewer.base:fzf_delimiter()
-- set delimiter to ':'
-- entry format is 'file:line:col: text'
local delim = self.opts.fzf_opts and self.opts.fzf_opts["--delimiter"]
if not delim then
delim = "[:]"
elseif not delim:match(":") then
if delim:match("%[.*%]") then
delim = delim:gsub("%]", ":]")
else
-- remove surrounding quotes
delim = delim:match("^'?(.*)'$?") or delim
delim = "[" .. utils.rg_escape(delim):gsub("%]", "\\]") .. ":]"
end
end
return delim
end
-- Generic shell command previewer
---@class fzf-lua.previewer.Cmd : fzf-lua.previewer.Fzf,{}
---@field super fzf-lua.previewer.Fzf,{}
Previewer.cmd = Previewer.base:extend()
function Previewer.cmd:new(o, opts)
Previewer.cmd.super.new(self, o, opts)
return self
end
---@diagnostic disable-next-line: unused
function Previewer.cmd:format_cmd(cmd, args, action, extra_args)
return string.format([[%s %s %s "$(%s)"]],
cmd, args or "", extra_args or "", action)
end
function Previewer.cmd:cmdline(o)
o = o or {}
o.action = o.action or self:action(o)
return self:format_cmd(self.cmd, self.args, o.action)
end
function Previewer.cmd:action(o)
o = o or {}
local act = shell.stringify_data(function(items, _, _)
if not items[1] then return utils.shell_nop() end
local entry = path.entry_to_file(items[1], self.opts)
return entry.bufname or entry.path
end, self.opts, self.opts.field_index_expr or "{}")
return act
end
-- Specialized bat previewer
---@class fzf-lua.previewer.Bat : fzf-lua.previewer.Cmd,{}
---@field super fzf-lua.previewer.Cmd,{}
Previewer.bat = Previewer.cmd:extend()
function Previewer.bat:new(o, opts)
Previewer.bat.super.new(self, o, opts)
self.theme = o.theme
return self
end
function Previewer.bat:cmdline(o)
o = o or {}
o.action = o.action or self:action(o)
local extra_args = ""
if self.theme then
extra_args = string.format([[ --theme="%s"]],
type(self.theme) == "function" and tostring(self.theme()) or tostring(self.theme))
end
if self.opts.line_field_index then
extra_args = extra_args .. string.format(" --highlight-line=%s", self.opts.line_field_index)
end
return self:format_cmd(self.cmd, self.args, o.action, extra_args)
end
-- Specialized head previewer
---@class fzf-lua.previewer.Head : fzf-lua.previewer.Cmd,{}
---@field super fzf-lua.previewer.Cmd,{}
Previewer.head = Previewer.cmd:extend()
function Previewer.head:new(o, opts)
Previewer.head.super.new(self, o, opts)
return self
end
function Previewer.head:cmdline(o)
o = o or {}
o.action = o.action or self:action(o)
local lines = "--lines=-0"
-- print all lines instead
-- if self.opts.line_field_index then
-- lines = string.format("--lines=%s", self.opts.line_field_index)
-- end
return self:format_cmd(self.cmd, self.args, o.action, lines)
end
-- new async_action from nvim-fzf
---@class fzf-lua.previewer.CmdAsync : fzf-lua.previewer.Cmd,{}
---@field super fzf-lua.previewer.Cmd,{}
Previewer.cmd_async = Previewer.base:extend()
function Previewer.cmd_async:new(o, opts)
Previewer.cmd_async.super.new(self, o, opts)
return self
end
local grep_tag = function(file, tag)
local line = 1
if not tag or not file then return line end
local pattern = utils.ctag_escape(tag)
-- ctags uses '$' at the end of short patterns
-- 'rg|grep' does not match these properly when
-- 'fileformat' isn't set to 'unix', when set to
-- 'dos' we need to prepend '$' with '\r$' with 'rg'
-- equivalent pattern to `rg --crlf`
-- see discussion in #219
pattern = pattern:gsub("%$$", "\\r??%$")
local grep_cmd = vim.fn.executable("rg") == 1
and { "rg", "--line-number" }
or { "grep", "-n", "-P" }
local cmd = utils.tbl_deep_clone(grep_cmd)
table.insert(cmd, pattern)
table.insert(cmd, file)
local out, rc = utils.io_system(cmd)
if rc == 0 then
line = utils.tointeger(out:match("[^:]+")) or 1
else
utils.warn("Unable to match pattern '%s' in file '%s'", pattern, file)
end
return line
end
function Previewer.cmd_async:parse_entry_and_verify(entrystr)
-- Handle empty or invalid entries (common on Windows with live_grep)
if not entrystr or #entrystr == 0 or entrystr == "." or entrystr == ".." then
local errmsg = entrystr and #entrystr > 0
and string.format("'%s': Invalid entry", entrystr)
or "No entry selected"
return "", {}, "echo " .. libuv.shellescape(errmsg)
end
local entry = path.entry_to_file(entrystr, self.opts)
if not entry.stripped then return end
-- make relative for bat's header display
local filepath = path.relative_to(entry.bufname or entry.path or "", utils.cwd())
if self.opts._ctag then
if not entry.line or tonumber(entry.line) < 1 then
-- default tags are without line numbers
entry.line = grep_tag(filepath, entry.ctag)
end
end
local errcmd = nil
if entry.debug then
errcmd = "echo " .. libuv.shellescape(entry.debug)
else
-- verify the file exists on disk and is accessible
if #filepath == 0 or not uv.fs_stat(filepath) then
errcmd = "echo " .. libuv.shellescape(
string.format("'%s: NO SUCH FILE OR ACCESS DENIED",
filepath and #filepath > 0 and filepath or "<null>"))
end
end
return filepath, entry, errcmd
end
function Previewer.cmd_async:cmdline(o)
o = o or {}
local act = shell.stringify_cmd(function(items)
FzfLua.get_info().query = items[2] or ""
local filepath, _, errcmd = self:parse_entry_and_verify(items[1])
if not filepath then return utils.shell_nop() end
local cmd = errcmd or ("%s %s %s"):format(
self.cmd, self.args, libuv.shellescape(filepath))
return cmd
end, self.opts, "{} {q}")
return act
end
---@class fzf-lua.previewer.BatAsync : fzf-lua.previewer.CmdAsync,{}
---@field super fzf-lua.previewer.CmdAsync,{}
Previewer.bat_async = Previewer.cmd_async:extend()
---@param lnum string?
---@return string?
function Previewer.bat_async:_preview_offset(lnum)
--[[
#
# Explanation of the fzf preview offset options:
#
# ~3 Top 3 lines as the fixed header
# +{2} Base scroll offset extracted from the second field
# +3 Extra offset to compensate for the 3-line header
# /2 Put in the middle of the preview area
#
'--preview-window '~3:+{2}+3/2''
]]
lnum = lnum or self.opts.line_field_index
if not self.args or not self.args:match("%-%-style=default") then
-- we don't need affixed header unless we use bat default style
-- TODO: should also adjust for "--style=header-filename"
if lnum then
return ("+%s-/2"):format(lnum)
end
else
if lnum then
return ("~3:+%s+3/2"):format(lnum)
else
-- no line offset, affix header
return "~3"
end
end
end
function Previewer.bat_async:new(o, opts)
Previewer.bat_async.super.new(self, o, opts)
self.theme = o.theme
return self
end
function Previewer.bat_async:cmdline(o)
o = o or {}
local act = function(items, fzf_lines)
FzfLua.get_info().query = items[2] or ""
if items[1] == "" then return utils.shell_nop() end
local filepath, entry, errcmd = self:parse_entry_and_verify(items[1])
if not filepath or not entry then return utils.shell_nop() end
local line_range = ""
if self.opts._ctag and entry.line then
-- this is a ctag without line numbers, since we can't
-- provide the preview file offset to fzf via the field
-- index expression we use '--line-range' instead
local start_line = math.floor((math.max(1, entry.line - fzf_lines / 2)))
local end_line = start_line + fzf_lines - 1
line_range = ("--line-range=%d:%d"):format(start_line, end_line)
end
local cmd = errcmd or ("%s %s %s %s %s %s"):format(
self.cmd, self.args,
self.theme and string.format([[--theme="%s"]],
type(self.theme) == "function" and tostring(self.theme()) or tostring(self.theme)) or "",
entry.line and entry.line > 0
and string.format("--highlight-line=%d", entry.line) or "",
line_range,
libuv.shellescape(filepath))
return cmd
end
return { fn = act, type = "cmd", field_index = "{} {q}" }
end
---@class fzf-lua.previewer.GitDiff : fzf-lua.previewer.Fzf,{}
---@field super fzf-lua.previewer.Fzf,{}
Previewer.git_diff = Previewer.base:extend()
---@param o fzf-lua.config.GitDiffPreviewer
---@param opts fzf-lua.config.Resolved
---@return fzf-lua.previewer.GitDiff
function Previewer.git_diff:new(o, opts)
Previewer.git_diff.super.new(self, o, opts)
self.cmd_deleted = path.git_cwd(o.cmd_deleted, opts)
self.cmd_modified = path.git_cwd(o.cmd_modified, opts)
self.cmd_untracked = path.git_cwd(o.cmd_untracked, opts)
local pager = opts.preview_pager == nil and o.pager or opts.preview_pager
if type(pager) == "function" then pager = pager(opts) end
---@cast pager string
local cmd = pager and pager:match("[^%s]+") or nil
if cmd and vim.fn.executable(cmd) == 1 then
-- style 2: as we are unable to use %var% within a "cmd /c" without !var! expansion
-- https://superuser.com/questions/223104/setting-and-using-variable-within-same-command-line-in-windows-cmd-ex
self.pager = "| " .. utils._if_win_normalize_vars(pager, 2)
end
do
-- populate the icon mappings
---@diagnostic disable-next-line: assign-type-mismatch
local icons_overrides = o._fn_git_icons and o._fn_git_icons()
self.git_icons = {}
for _, i in ipairs({ "D", "M", "R", "A", "C", "T", "?" }) do
self.git_icons[i] =
icons_overrides and icons_overrides[i] and
utils.lua_regex_escape(icons_overrides[i].icon) or i
end
end
return self
end
function Previewer.git_diff:cmdline(o)
o = o or {}
local act = function(items, fzf_lines, fzf_columns)
if not items or utils.tbl_isempty(items) then
utils.warn("shell error while running preview action.")
return utils.shell_nop()
end
if not items[1] then return utils.shell_nop() end
local is_deleted = items[1]:match(self.git_icons["D"] .. utils.nbsp) ~= nil
local is_modified = items[1]:match("[" ..
self.git_icons["M"] ..
self.git_icons["R"] ..
self.git_icons["A"] ..
self.git_icons["T"] ..
"]" .. utils.nbsp) ~= nil
local is_untracked = items[1]:match("[" ..
self.git_icons["?"] ..
self.git_icons["C"] ..
"]" .. utils.nbsp) ~= nil
local s = items[1]
if s:match("%s%->%s") then
-- for renames, we take only the last part (#864)
s = s:match("%s%->%s(.*)$") or s
end
local entry = path.entry_to_file(s, self.opts)
local cmd = nil
if is_modified then
cmd = self.cmd_modified
elseif is_deleted then
cmd = self.cmd_deleted
elseif is_untracked then
local stat = entry.path and uv.fs_stat(entry.path)
---@diagnostic disable-next-line: undefined-field
if stat and stat.type == "directory" then
cmd = utils._if_win({ "dir" }, { "ls", "-la" })
else
cmd = self.cmd_untracked
end
end
if not cmd then return "" end
if type(cmd) == "table" then return table.concat(cmd, " ") end
-- with default commands we add the filepath at the end.
-- If the user configured a more complex command, e.g.:
-- git_diff = {
-- cmd_modified = "git diff --color HEAD %s | less -SEX"
-- }
-- we use ':format' directly on the user's command, see
-- issue #392 for more info (limiting diff output width)
if not entry.path then return "" end
local fname_escaped = libuv.shellescape(entry.path)
if cmd:match("[<{]file[}>]") then
cmd = cmd:gsub("[<{]file[}>]", fname_escaped)
elseif cmd:match("%%s") then
cmd = cmd:format(fname_escaped)
else
cmd = string.format("%s %s", cmd, fname_escaped)
end
local env = {
["LINES"] = fzf_lines,
["COLUMNS"] = fzf_columns,
["FZF_PREVIEW_LINES"] = fzf_lines,
["FZF_PREVIEW_COLUMNS"] = fzf_columns,
}
-- NOTE: no longer required to send env string as we're setting
-- env vars in `stringify:libuv.spawn`
-- local setenv = utils.shell_setenv_str(env)
-- cmd = string.format("%s %s %s", table.concat(setenv, " "), cmd, pager)
cmd = string.format("%s %s", cmd, self.pager or "")
return { cmd = cmd, env = env }
end
return { fn = act, type = "cmd", field_index = "{} {q}" }
end
---@class fzf-lua.previewer.fzf.ManPages : fzf-lua.previewer.Fzf,{}
---@field super fzf-lua.previewer.Fzf,{}
Previewer.man_pages = Previewer.base:extend()
function Previewer.man_pages:new(o, opts)
Previewer.man_pages.super.new(self, o, opts)
self.cmd = o.cmd or "man -c %s | col -bx"
---@diagnostic disable-next-line: assign-type-mismatch
self.cmd = type(self.cmd) == "function" and self.cmd() or self.cmd
return self
end
function Previewer.man_pages:cmdline(o)
o = o or {}
local act = function(items)
if not items[1] then return utils.shell_nop() end
local manpage = require("fzf-lua.providers.manpages").manpage_sh_arg(items[1])
local cmd = self.cmd:format(manpage)
return cmd
end
return { fn = act, type = "cmd", field_index = "{}" }
end
---@class fzf-lua.previewer.fzf.HelpTags : fzf-lua.previewer.Fzf,{}
---@field super fzf-lua.previewer.fzf.HelpTags,{}
Previewer.help_tags = Previewer.base:extend()
function Previewer.help_tags:fzf_delimiter()
return self.opts.fzf_opts and self.opts.fzf_opts["--delimiter"] or nil
end
function Previewer.help_tags:new(o, opts)
Previewer.help_tags.super.new(self, o, opts)
self.cmd = self.cmd or vim.fn.executable("bat") == 1
and "bat -p -l help --color=always %s"
or "cat %s"
return self
end
function Previewer.help_tags:cmdline(o)
o = o or {}
local act = function(items)
if not items[1] then return utils.shell_nop() end
local vimdoc = items[1]:match(string.format("[^%s]+$", utils.nbsp))
assert(vimdoc, "help_tags previewer: unable to parse help file name")
local tag = items[1]:match("^[^%s]+")
local ext = path.extension(vimdoc)
local cmd = self.cmd:format(libuv.shellescape(vimdoc))
-- If 'bat' is available attempt to get the helptag line
-- and start the display of the help file from the tag
if self.cmd:match("^bat ") then
local line = grep_tag(vimdoc, ext == "md" and tag or string.format("*%s*", tag))
if tonumber(line) > 0 then
-- this is a ctag without line numbers, since we can't
-- provide the preview file offset to fzf via the field
-- index expression we use '--line-range' instead
cmd = cmd .. string.format(" --line-range=%d:", tonumber(line))
end
end
return cmd
end
return { fn = act, type = "cmd", field_index = "{}" }
end
Previewer.nvim_server = Previewer.cmd_async:extend()
-- Block (bounded) until a screenshot dump stops growing: the dump
-- streams in after the screenshot rpc acks, so a mere size>0 can catch a
-- partial file. Requires two consecutive stable non-empty sizes; `vim.wait`
-- pumps the event loop so deferred writes run.
---@param screenshot string
---@param timeout_ms integer? max wait, defaults to 2000
---@return boolean settled
local function wait_screenshot_settled(screenshot, timeout_ms)
local last_size, stable = -1, 0
local settled = vim.wait(timeout_ms or 2000, function()
local size = vim.fn.getfsize(screenshot)
if size > 0 and size == last_size then
stable = stable + 1
return stable >= 2
end
last_size, stable = size, 0
return false
end, 25)
return settled
end
-- generate screenshot
-- spawn a temporary tui for non-tui/headless client
---@param screenshot string
---@param addr string
---@param lines integer
---@param columns integer
---@return boolean|integer? pid
---@return string|any, ...any error
local function make_screenshot(screenshot, addr, lines, columns)
local closing = false
-- timed: a wedged target must fail here, not freeze the picker forever
local ok, uis = utils.rpcexec_timed(addr, "nvim_list_uis") --[[@as boolean, { stdout_tty: boolean }[]?]]
vim.fn.writefile({}, screenshot)
if not ok then -- instance seems died, signal to reload the list
local winobj = FzfLua.utils.fzf_winobj()
if winobj then winobj:SIGWINCH({ "win.unhide" }) end
-- propagate the raw rpc error so the preview shows WHY (uis holds it)
---@diagnostic disable-next-line: missing-return-value
return nil, uis
end
local uis_list = uis or {} ---@type { stdout_tty: boolean }[]
local has_tui = false
for _, info in ipairs(uis_list) do
if info.stdout_tty then has_tui = true break end
end
if has_tui then
-- deliberately untimed below: the target just proved responsive via
-- the timed gatekeeper above, and a slow dump-ack must still complete
-- (a timeout here would abort an otherwise healthy screenshot)
if vim.env.NVIM == addr then -- parent instance
return utils.rpcexec(addr, "nvim__screenshot", screenshot)
end
if utils.__IS_WINDOWS then
-- Screenshot at native size: `vim._with({ o = { lines, columns }})`
-- resizes the TARGET's UI (and every grid on it) which wedges
-- remote/tmux terminals; fzf crops/scrolls oversized dumps itself.
return utils.rpcexec(addr, "nvim_exec_lua",
"return vim.api.nvim__screenshot(...)", { screenshot })
end
return utils.rpcexec(addr, "nvim_exec_lua", [[
local lines, columns, screenshot = ...
return vim._with({ o = { lines = lines, columns = columns } }, function()
vim.api.nvim__screenshot(screenshot)
end)
]], { lines, columns, screenshot })
end
local nvim_bin = os.getenv("FZF_LUA_NVIM_BIN") or vim.v.progpath
local chan = vim.fn.jobstart({ nvim_bin, "--server", addr, "--remote-ui" }, {
-- No pty on Windows: the ConPTY client exits(1) and its escape
-- sequences leak to our stdout; plain pipes attach and dump fine.
pty = not utils.__IS_WINDOWS,
height = lines,
width = columns,
env = {
TERM = "xterm-256color",
VIMRUNTIME = os.getenv("VIMRUNTIME"),
},
on_stdout = function(chan0)
if closing then return end
closing = true
-- deliberately untimed: the timed gatekeeper above already proved
-- the target responsive, and capping this would drop slow-but-healthy
-- dumps (the settle waiter and pager fallback bound the total wait)
vim.defer_fn(function() utils.rpcexec(addr, "nvim__screenshot", screenshot) end, 10)
vim.defer_fn(function()
if utils.__IS_WINDOWS then
-- The dump lands async after the rpc returns; killing the
-- client at a fixed delay aborts it (permanently empty file).
-- Wait for the settled dump first, same condition the pager
-- side uses below.
wait_screenshot_settled(screenshot, 2000)
end
vim.fn.jobstop(chan0)
end, 20)
end,
})
-- `jobpid` throws when the spawn failed (bad binary, no pty, ...);
-- surface it as a preview error instead of a silent action failure.
local ok_job, pid_or_err = pcall(vim.fn.jobpid, chan)
if not ok_job then return nil, pid_or_err end
---@diagnostic disable-next-line: missing-return-value
return pid_or_err
end
-- Server screenshot preview. On Windows the dump file is converted to plain
-- `\n` rows by `utils.screenshot_to_plaintext` (the raw dump is a terminal
-- script that does not render in every preview terminal) and paged with
-- `cat`/`type`; the target's own window size is never touched. Elsewhere
-- the raw dump is streamed with `tail`/`cat` after a `sleep`/`waitpid`
-- settle delay.
function Previewer.nvim_server:cmdline(_)
local parse_entry = utils.serverlist_parse_entry
local act = function(items, lines, columns)
FzfLua.get_info().query = items[2] or ""
local addr = parse_entry(items[1])
if not addr then return utils.shell_nop() end
local screenshot = assert(self.opts._screenshot) ---@type string
local pid, err = make_screenshot(screenshot, addr, lines, columns)
if err then
return "echo " .. FzfLua.libuv.shellescape(string.format(
"make_screenshot failed with error: %s", tostring(err)))
end
-- NOTE: preview runs under `cmd.exe /c` on Windows where `;` is not a
-- command separator (`&` is) and coreutils may be absent, so only emit
-- a wait prefix when the tool is actually executable and prefer pagers
-- accordingly (`cat`, then builtin `type`, `tail` only as last resort
-- since the header it would skip is already gone).
local is_async = tonumber(pid) ~= nil
local sep = utils._if_win("&", ";")
local wait = ""
if not utils.__IS_WINDOWS then
if is_async and vim.fn.executable("waitpid") == 1 then
wait = ("waitpid %s%s "):format(pid, sep)
elseif vim.fn.executable("sleep") == 1 then
wait = ("sleep %s%s "):format(50 / 1000, sep)
end
else
-- No `sleep` under cmd.exe: wait in-process so the pager never reads
-- an empty file. Needed on BOTH paths: `nvim__screenshot` only acks
-- the rpc while the dump itself lands async (also after a cold
-- `--remote-ui` client spawn, ~1s), and the client case additionally
-- defers the shot. Settle (not just non-empty) so conversion never
-- reads a partial dump. Generous cap for cold client spawns on
-- loaded machines (the teardown waiter below runs independently and
-- the next preview refresh retries); dead targets return early
-- through the error path above (no stall).
wait_screenshot_settled(screenshot, 3000)
-- Render-safe rows (see `utils.screenshot_to_plaintext`); best
-- effort, a failed conversion still leaves the raw dump for the pager.
pcall(utils.screenshot_to_plaintext, screenshot)
end
local shot_path = screenshot
if utils.__IS_WINDOWS then
-- nightly neovim returns '/'-separated `tempname()` values on Windows
-- and cmd.exe's `type` builtin rejects them; the pager runs cmd syntax
shot_path = shot_path:gsub("/", "\\")
end
local shot = libuv.shellescape(shot_path)
local pager
if not utils.__IS_WINDOWS and vim.fn.executable("tail") == 1 then
pager = "tail -n+2 %s"
elseif vim.fn.executable("cat") == 1 then
pager = "cat %s"
elseif utils.__IS_WINDOWS then
pager = "type %s"
else
-- No `tail`/`cat`: `tail` would still dump (minus its assumed header
-- line), better than nothing. Unreachable on Windows (`type` above).
pager = "tail -n+2 %s"
end
if utils.__IS_WINDOWS and vim.fn.getfsize(screenshot) <= 0 then
-- Visible failure beats a silently empty preview pane; show the
-- target address byte-escaped so invisible junk (\r, ANSI, ...) can't
-- hide (pipe names never contain such bytes).
return "echo " .. libuv.shellescape("screenshot not ready: "
.. utils.escape_invisible(addr) .. " -> " .. screenshot)
end
return wait .. pager:format(shot)
end
return { fn = act, type = "cmd", field_index = "{} {q}" }
end
return Previewer