-- Implementation of file preview with support for syntax highlighting, -- directory and archive contents, and images, -- falling back to showing stats of unsupported files. -- Requires bat for syntax highlighting (https://github.com/sharkdp/bat), -- viu for image preview (https://github.com/atanunq/viu), -- and ouch for archive preview (https://github.com/ouch-org/ouch) ---@diagnostic disable local xplr = xplr ---@diagnostic enable local M = {} local function mimetype(n) return xplr.util.shell_execute("file", { "--brief", "--mime-type", n.absolute_path }).stdout:sub(1, -2) end local function filetype(n) local type = "other" if n.is_file then local mime = mimetype(n) if (mime:match("text") or mime:match("json") or mime:match("csv") or mime:match("empty")) then type = "text" elseif (mime:match("zip") or mime:match("tar")) then type = "archive" elseif (mime:match("image")) then type = "image" end elseif n.is_dir then type = "directory" end return type end local function stats(n) return xplr.util.to_yaml(xplr.util.node(n.absolute_path)) end local function endswith(s, suffix) return s:sub(- #suffix) == suffix end local function render_text(n, ctx) local result = xplr.util.shell_execute("bat", { "--color=always", "--style=plain", "--line-range=:" .. ctx.layout_size.height - 2, n.absolute_path }) local out = (result.returncode == 0 and result.stdout) or stats(n) -- Replace tabs with 4 spaces (for some reason tabs don't seem to render properly) return out:gsub("\t", " ") end local function render_image(n, ctx) local result = xplr.util.shell_execute("viu", { "--blocks", "--static", "--width", ctx.layout_size.width, n.absolute_path }) return (result.returncode == 0 and result.stdout) or stats(n) end local function render_directory(n, ctx) local result = xplr.util.shell_execute("sh", { "-c", "tree -aC --noreport " .. xplr.util.shell_escape(n.absolute_path) .. "| head --lines=" .. ctx.layout_size.height - 1 .. "| tail +2" }) return (result.returncode == 0 and result.stdout) or stats(n) end local function render_archive(n, ctx) -- To keep from lagging out xplr, -- we only extract archives that are 10 MiB or less in size -- (mibibytes, not megabytes, since that's what ls -h shows). if n.size > 10485760 then return stats(n) end local result = xplr.util.shell_execute("sh", { "-c", "ouch list " .. xplr.util.shell_escape(n.absolute_path) .. "| head --lines=" .. ctx.layout_size.height - 1 .. "| tail +2" }) local out = (result.returncode == 0 and result.stdout) or stats(n) -- Since ouch doesn't support forcing coloration in non-interactive terminals (a strange omission TBH), -- we need to add the colors ourselves. -- The following is a bit brittle, since it will break if a file for some reason ends in /, but eh. local body = "" for line in string.gmatch(out, "[^\n]+") do -- Remove extra / from end of directories that head adds for whatever reason if endswith(line, "//") then line = line:sub(1, -2) end -- Directories end with a / and should be highlighted blue if endswith(line, "/") then local style = { fg = "Blue", bg = nil, add_modifiers = { "Bold" }, sub_modifiers = {} } body = body .. xplr.util.paint(line, style) .. "\n" else body = body .. line .. "\n" end end return body end xplr.fn.custom.preview_pane = {} xplr.fn.custom.preview_pane.render = function(ctx) local title = nil local body = "" local n = ctx.app.focused_node -- Follow symlinks if n then -- Symlinks to symlinks are a thing, so we must support that while n.canonical do n = n.canonical end end if n then title = { format = n.absolute_path, style = xplr.util.lscolor(n.absolute_path) } local type = filetype(n) if type == "text" then body = render_text(n, ctx) elseif type == "image" then body = render_image(n, ctx) elseif type == "directory" then body = render_directory(n, ctx) elseif type == "archive" then body = render_archive(n, ctx) else body = stats(n) end end return { CustomParagraph = { ui = { title = title }, body = body } } end M.preview_pane = { Dynamic = "custom.preview_pane.render" } return M