diff --git a/.github/README.md b/.github/README.md index ce71e03..0db7f76 100644 --- a/.github/README.md +++ b/.github/README.md @@ -90,58 +90,13 @@ This extension should be configured using the `extensions` field inside Telescop However, you could also pass a table into the extension call. ```lua ---- this is optional require("telescope").setup({ - ---Dimensions of the preview ueberzug window. - geometry = { - ---X-offset of the ueberzug window. - x = -2, - ---Y-offset of the ueberzug window. - y = -2, - ---Width of the ueberzug window. - width = 1, - ---Height of the ueberzug window. - height = 1, - }, - find_command = { - "rg", - "--no-config", - "--files", - ".", - }, - backend = "ueberzug", - dynamic_preview_title = true, - on_confirm = canned.open_path, - on_confirm_muliple = canned.bulk_copy, - cache_path = "/tmp/tele.media.cache", - preview = { - title = "Previews", - filesize = 35, - enable_colorizer = true, - treesitter = true, - check_mime_type = true, - window_options = { - wrap = false, - winhl = "Normal:TelescopePreviewNormal", - signcolumn = "no", - foldlevel = 100, - scrollbind = false, - }, - mimeforce = { - "json", - "lua", - "xml", - }, - filetype_detect = true, - fill = { - mime_disable = "", - not_text_mime = "", - permission_denied = "⦂", - caching = "⎪", - stat_nil = "╱", - file_limit = "ˆ", - }, - }, + extensions = { + backend = "viu", -- "ueberzug"|"viu"|"chafa"|"jp2a"|catimg + on_confirm = canned.single.copy_path, + on_confirm_muliple = canned.multiple.bulk_copy, + cache_path = "/tmp/tele.media.cache", + } }) ``` @@ -182,15 +137,16 @@ Some of these are optional. This is getting out of hand. -- [ ] Add documentations, briefs and notes. +- [x] Add documentations, briefs and notes. - [ ] Recalibrate preview size when window is moved. - [x] Add default text preview. - [ ] Render html files using elinks, pandoc, lynx and w3m. - [ ] Render markdown files using glow and pandoc. -- [ ] Add [viu](https://github.com/atanunq/viu) backend. -- [ ] Add [jp2a](https://github.com/cslarsen/jp2a) backend. -- [ ] Add [chafa](https://github.com/hpjansson/chafa/) backend. +- [x] Add [viu](https://github.com/atanunq/viu) backend. +- [x] Add [jp2a](https://github.com/cslarsen/jp2a) backend. +- [x] Add [chafa](https://github.com/hpjansson/chafa/) backend. - [x] Add support for ZIPs. +- [x] Add support for binaries. - [x] Add default image preview. - [x] Add support for ebooks. - [x] Add support for Ai/EPS. diff --git a/lua/telescope/_extensions/media/init.lua b/lua/telescope/_extensions/media/init.lua index 20733c6..2e1f469 100644 --- a/lua/telescope/_extensions/media/init.lua +++ b/lua/telescope/_extensions/media/init.lua @@ -26,19 +26,14 @@ if not present then return end -local utils = require("telescope.utils") local actions = require("telescope.actions") local finders = require("telescope.finders") local pickers = require("telescope.pickers") local config = require("telescope.config") local action_state = require("telescope.actions.state") -local actions_layout = require("telescope.actions.layout") local make_entry = require("telescope.make_entry") -local Job = require("plenary.job") -local Path = require("plenary.path") - local scope = require("telescope._extensions.media.scope") local canned = require("telescope._extensions.media.canned") local media_previewer = require("telescope._extensions.media.preview") @@ -50,79 +45,18 @@ local fn = vim.fn -- The default configuration. {{{ ---This is the default configuration. local _TelescopeMediaConfig = { - ---Dimensions of the preview ueberzug window. - geometry = { - ---X-offset of the ueberzug window. - x = -2, - ---Y-offset of the ueberzug window. - y = -2, - ---Width of the ueberzug window. - width = 1, - ---Height of the ueberzug window. - height = 1, - }, - find_command = { - "rg", - "--no-config", - "--files", - ".", - }, - backend = "ueberzug", + backend = "viu", on_confirm = canned.single.copy_path, on_confirm_muliple = canned.multiple.bulk_copy, cache_path = "/tmp/tele.media.cache", - mappings = { - { "n", "p", actions_layout.toggle_preview }, - { "n", "v", canned.actions.multiple_vsplit }, - { "n", "s", canned.actions.multiple_split }, - { "n", "f", actions_layout.cycle_layout_next }, - { "n", "b", actions_layout.cycle_layout_prev }, - }, + preview_title = "", results_title = "", - prompt_title = "", - previewer = nil, - theme = "ivy", - sorting_strategy = "ascending", - layout_strategy = "horizontal", - layout_config = { - preview_cutoff = 1, - width = function(_, max_columns, _) return math.min(max_columns, 120) end, - height = function(_, _, max_lines) return math.min(max_lines, 20) end, - }, - border = true, - borderchars = { - prompt = { "─", "│", " ", "│", "╭", "╮", "│", "│" }, - results = { "─", "│", "─", "│", "├", "┤", "╯", "╰" }, - preview = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, - }, + prompt_title = "Media", preview = { - timeout = 250, -- TODO - all_binaries = true, -- TODO - title = "", - filesize = 35, - colorizer = true, - treesitter = true, - check_mime_type = true, - window_options = { - wrap = false, - winhl = "Normal:TelescopePreviewNormal", - signcolumn = "no", - foldlevel = 100, - scrollbind = false, - }, - mimeforce = { - "json", - "lua", - "xml", - }, - filetype_detect = true, fill = { - mime_disable = "", - not_text_mime = "", - permission_denied = "╱", + mime = "", + permission = "╱", caching = "⎪", - file_limit = "ˆ", - timeout = "⦂", -- TODO }, }, } @@ -170,10 +104,6 @@ end local function media(options) options = F.if_nil(options, {}) options.attach_mappings = function(buffer, map) - for _, mapping in ipairs(options.mappings) do - map(mapping[1], mapping[2], mapping[3]) - end - actions.select_default:replace(function(prompt_buffer) local current_picker = action_state.get_current_picker(prompt_buffer) local selections = current_picker:get_multi_selection() @@ -192,7 +122,6 @@ local function media(options) -- we need to do this everytime because a new table might be passed -- for example: one might want to run this through the cmdline or whatever options = vim.tbl_deep_extend("keep", options, _TelescopeMediaConfig) - options.find_command = find_command(options) -- Validate find_command {{{ ---@see telescope.previewers.buffer_previewer diff --git a/lua/telescope/_extensions/media/preview.lua b/lua/telescope/_extensions/media/preview.lua index 8415452..d61b47b 100644 --- a/lua/telescope/_extensions/media/preview.lua +++ b/lua/telescope/_extensions/media/preview.lua @@ -1,294 +1,124 @@ ----@tag media.preview - ----@config { ["name"] = "MEDIA PREVIEWER", ["field_heading"] = "Options", ["module"] = "telescope._extensions.media.preview" } - ----@brief [[ ---- Implementation of a custom previewer. ----@brief ]] - --- Imports and local declarations. {{{ local Path = require("plenary.path") -local Job = require("plenary.job") local Ueberzug = require("telescope._extensions.media.ueberzug") +local Job = require("plenary.job") local utils = require("telescope.utils") -local previewers = require("telescope.previewers") -local scope = require("telescope._extensions.media.scope") -local filetype = require("plenary.filetype") +local state = require("telescope.state") +local view = require("telescope.previewers.buffer_previewer") +local view_util = require("telescope.previewers.utils") -local preview_utils = require("telescope.previewers.utils") +local scope = require("telescope._extensions.media.scope") +local present, colorizer = pcall(require, "colorizer") -local api = vim.api +local NULL = vim.NIL +local F = vim.F local fn = vim.fn +local api = vim.api local uv = vim.loop -local F = vim.F - -local present, colorizer = pcall(require, "colorizer") --- }}} --- Helper functions. {{{ ---- Hide the ueberzug window (stops viewing the image). ----@param options table needs to have the backend key. ----@param ueberzug Ueberzug the ueberzug object. ----@private -local function ueberzug_hide(options, ueberzug) - if options.backend == "ueberzug" and ueberzug then - ueberzug:send({ - path = vim.NIL, -- vim.NIL represents null - x = 100, - y = 100, - width = 1, - height = 1, - }) - end -end --- }}} - --- Preview function. {{{ ---- A subsidiary function of `preview_fn`. This will only be called if a text file is opened. Essentially, ---- this will load a text file and will apply syntax highlights to it if said text file turns out to be a ---- code file (like python, Java, etc.). ----@param extension string file extension ----@param buffer integer the preview buffer identity ----@param window integer the preview window identity ----@param options table plugin settings ----@param mimetype string detect mime-type by `file --mime-type --brief ` ----@param preview_filetype string detected filetype ----@param self table reference to the Previewer ----@param code integer exit code ----@param signal integer exit signal ----@private -local function text_highlighter(extension, buffer, window, options, mimetype, preview_filetype, result) - if not options.preview.check_mime_type then - preview_utils.set_preview_message(buffer, window, "MIMETYPE CHECK IS DISABLED", options.preview.fill.mime_disable) - elseif - not vim.tbl_contains(options.preview.mimeforce, extension) -- allow hardcoded filetypes - and mimetype ~= "text" - and mimetype ~= "inode" - then - preview_utils.set_preview_message(buffer, window, "PREVIEW UNAVAILABLE", options.preview.fill.not_text_mime) - else - if not api.nvim_buf_is_valid(buffer) then return end - api.nvim_buf_set_lines(buffer, 0, -1, false, result) - - if extension ~= "text" and extension ~= "txt" then - -- WARN: Slows down telescope when a file has more than ~1500 lines. Please help. - preview_utils.highlighter(buffer, preview_filetype, options) - end - - -- set window options - for option, value in pairs(options.preview.window_options) do - api.nvim_win_set_option(window, option, value) - end - -- enable nvim-colorizer.lua for rendering colors (if installed) - if options.preview.colorizer then colorizer.attach_to_buffer(buffer) end - end -end - -local function handle_executable(buffer, filepath) - local task = Job:new({ - "readelf", - "-WCa", - filepath, +local function backend_proxy(buffer, args) + local terminal = vim.api.nvim_open_term(buffer, {}) + fn.jobstart(args, { + on_stdout = function(_, data, _) + for _, datum in ipairs(data) do api.nvim_chan_send(terminal, datum .. "\r\n") end + end, stdout_buffered = true }) - task:after_success(function(self, _) - local result = self:result() - vim.schedule(function() api.nvim_buf_set_lines(buffer, 0, -1, false, result) end) - end) - if not api.nvim_buf_is_valid(buffer) then return end - task:start() end ---- Another subsidiary of the `preview_fn` function. This one however, handles media files like images, audios, ---- videos, etc. This will look for a handler in |media.scope| and if it finds one then it'll call that handler. ---- The handler may return a path to the cached image. Which will then be previewed. ----@param ueberzug Ueberzug the ueberzug daemon object ----@param window integer the preview window identity ----@param buffer integer the preview buffer identity ----@param filepath string the path to the image/audio/video/font/... file. ----@param cache_path Path path where the images are being cached ----@param handler function media file handler ----@param preview_window table geometry of the preview window ----@param options table plugin settings ----@private -local function handle_backends(ueberzug, window, buffer, filepath, cache_path, handler, preview_window, options) - -- clear the preview buffer - -- TODO: Is there a better way to do this? - api.nvim_buf_set_lines(buffer, 0, -1, false, { "" }) - local path = handler(filepath, cache_path, {}) - - if path == vim.NIL then - ueberzug_hide(options, ueberzug) - preview_utils.set_preview_message(buffer, window, "CACHING PREVIEW IMAGE", options.preview.fill.caching) - return - end - - if options.backend == "ueberzug" and ueberzug then - ueberzug:send({ - path = path, - x = preview_window.col + options.geometry.x, - y = preview_window.line + options.geometry.y, - width = preview_window.width + options.geometry.width, - height = preview_window.height + options.geometry.height, - }) - elseif options.backend == "viu" then - ---@todo - elseif options.backend == "chafa" then - ---@todo - elseif options.backend == "jp2a" then - ---@todo - end -end - --- stylua: ignore start ---- As the name implies, this function is to be called from `Previewer.preview_fn`. It acts a proxy ---- to `preview_fn`. We did this mainly because the code looks cleaner this way. ----@param self table the Previewer ----@param entry table current selected item on the prompt list ----@param status table I don't know what this is called - Previewer metadata perhaps ----@param cache_path Path path where all the cached images are stored ----@param ueberzug Ueberzug the ueberzug daemon object ----@param options table plugin settings ----@private -local function preview_fn_proxy(self, entry, status, cache_path, ueberzug, options) - local buffer = status.preview_bufnr - local window = status.preview_win - - self.state.winid = window - local filepath = fn.fnamemodify(entry.value, ":p") +local function _hook(filepath, buffer, options) local extension = fn.fnamemodify(filepath, ":e"):lower() - - local preview_filetype = "" - if options.preview.filetype_detect then preview_filetype = filetype.detect(extension, { fs_access = true }) end - if preview_filetype == "" then preview_filetype = extension end - + local absolute = fn.fnamemodify(filepath, ":p") local handler = scope.supports[extension] - -- do not load a file i.e. greater than this size - uv.fs_stat(entry.value, vim.schedule_wrap(function(_, stat) - if not stat then - preview_utils.set_preview_message(buffer, window, "INSUFFICIENT PERMISSIONS", options.preview.fill.permission_denied) + + if handler then + local cached_file = handler(absolute, options.cache_path, options) + if cached_file == NULL then + view_util.set_preview_message(buffer, options.preview.winid, "CACHING ITEM", options.preview.fill.caching) return end - if options.preview.filesize then - local megabyte_filesize = math.floor(stat.size / math.pow(1024, 2)) - if megabyte_filesize > options.preview.filesize then - ueberzug_hide(options, ueberzug) - preview_utils.set_preview_message(buffer, window, "FILE EXCEEDS PREVIEW SIZE LIMIT", options.preview.fill.file_limit) - return - end + local window = options.get_preview_window() + if options.backend == "ueberzug" then + options._ueberzug:send({ + path = cached_file, + x = window.col - 2, + y = window.line - 2, + width = window.width, + height = window.height, + }) + elseif options.backend == "viu" then + backend_proxy(buffer, { "viu", "-s", cached_file }) + elseif options.backend == "chafa" then + backend_proxy(buffer, { "chafa", cached_file }) + elseif options.backend == "jp2a" then + backend_proxy(buffer, { "jp2a", "--colors", cached_file }) + elseif options.backend == "catimg" then + local width = api.nvim_win_get_width(options.preview.winid) + backend_proxy(buffer, { "catimg", "-w", math.floor(width * 1.5), cached_file }) end + return + end - -- if a handler exists for the current file (entry) then call that handler - -- else check if it is a text/text-like file - if so then view its contents - else view a message dialog - if handler then - handle_backends(ueberzug, window, buffer, filepath, cache_path, handler, options.get_preview_window(), options) - else - ueberzug_hide(options, ueberzug) - -- TODO: Use read_async instead. - local task = Job:new({ "cat", filepath }) - -- TODO: Is there a better way? - local mime = (utils.get_os_command_output({ "file", "--mime-type", "--brief", filepath }))[1] - local splited = vim.split(mime, "/", { plain = true }) - - task:add_on_exit_callback(function(...) - local args = { ... } - vim.schedule(function() - text_highlighter(extension, buffer, window, options, splited[1], preview_filetype, args[1]:result()) - end) + local mime = vim.split(utils.get_os_command_output({ "file", "--dereference", "--brief", "--mime-type", absolute })[1], "/", { plain = true }) + if vim.tbl_contains({ "x-executable", "x-pie-executable", "x-sharedlib" }, mime[2]) then + Job:new({ + "readelf", + "--wide", + "--demangle=auto", + "--all", + absolute, + on_exit = vim.schedule_wrap(function(self, code, _) + if code == 0 then api.nvim_buf_set_lines(buffer, 0, -1, false, self:result()) end end) - - -- TODO: Improve this. - if vim.tbl_contains({ - "x-executable", - "x-pie-executable", - "x-sharedlib", - }, splited[2]) then - handle_executable(buffer, filepath) - else - task:start() - end - end - end)) -end --- stylua: ignore end --- }}} - --- Setup, teardown and scroll functions. {{{ ---- Delete all residue cache files from archives and setup the previewer. ----@param self table the Previewer ----@param cache_path Path path to the cache images ----@param ueberzug Ueberzug the ueberzug daemon object ----@param options table plugin settings ----@return table ----@private -local function setup_proxy(self, cache_path, ueberzug, options) - local state = {} - scope.cleanup(cache_path) - return state -end - ---- The scrolling function for the previewer. ----@param self table the Previewer itself ----@param direction integer up/down scroll units ----@param cache_path Path path to the cache images ----@param ueberzug Ueberzug the ueberzug daemon ----@param options table plugin settings ----@private -local function scroll_fn_proxy(self, direction, cache_path, ueberzug, options) - if not self.state then return end - local input = direction > 0 and [[]] or [[]] - local count = math.abs(direction) - api.nvim_win_call(self.state.winid, function() api.nvim_command([[normal! ]] .. count .. input) end) -end - ---- The cleanup function. ----@param self table the Previewer itself ----@param cache_path Path path to the cache images ----@param buffer integer the preview buffer identity ----@param ueberzug Ueberzug the ueberzug daemon ----@param options table plugin settings ----@private -local function teardown_proxy(self, cache_path, buffer, ueberzug, options) - if ueberzug then ueberzug:kill() end - if options.preview.colorizer and present and colorizer.is_buffer_attached(buffer) then - colorizer.detach_from_buffer(buffer) + }):start() + else + Job:new({ + "file", + absolute, + on_exit = vim.schedule_wrap(function(self, code, _) + if code == 0 then api.nvim_buf_set_lines(buffer, 0, -1, false, self:result()) end + end), + }):start() end end --- }}} --- MediaPreview previewer {{{ ---- A new previewer definition which handles viewing of both media files and text/text-like files. ----@param options table plugin settings ----@return table ----@private -local function media_previewer(options) - local cache_path = Path:new(options.cache_path) - scope.load_caches(cache_path) - local UEBERZUG +return utils.make_default_callable(function(options) + options.cache_path = Path:new(options.cache_path) + scope.load_caches(options.cache_path) - -- WARN: This is the most problematic part. If your previewer is open and an error occurs, - -- we will not be able to terminate the ueberzug job. - -- NOTE: We could solve this by going back to non-daemon process and running the script everytime - -- preview_fn is called... but the preview won't be fast. if options.backend == "ueberzug" then - UEBERZUG = Ueberzug:new(os.tmpname()) - UEBERZUG:listen() + options._ueberzug = Ueberzug:new(os.tmpname()) + options._ueberzug:listen() end - --@see https://is.gd/HbG2AD this is so much better now. - return previewers.new({ - setup = function(self) return setup_proxy(self, cache_path, UEBERZUG, options) end, - preview_fn = function(self, entry, status) preview_fn_proxy(self, entry, status, cache_path, UEBERZUG, options) end, - teardown = function(self) teardown_proxy(self, cache_path, buffer, UEBERZUG, options) end, - scroll_fn = function(self, direction) scroll_fn_proxy(self, direction, cache_path, UEBERZUG, options) end, - title = options.preview.title, - -- TODO: Why does this not work? LMAO? - dynamic_title = function(self, entry) return fn.fnamemodify(entry.value, ":t") end, + options.preview.mime_hook = _hook + options.preview.filetype_hook = _hook + options.preview.check_mime_type = true + options.preview.msg_bg_fillchar = options.preview.fill.mime + + return view.new_buffer_previewer({ + define_preview = function(self, entry, _) + uv.fs_access(entry.value, "R", vim.schedule_wrap(function(_, permission) + if permission then + options.preview.winid = self.state.winid + view.file_maker(entry.value, self.state.bufnr, options) + return + end + view_util.set_preview_message(self.state.bufnr, self.state.winid, "PERMISSION DENIED", options.preview.fill.permission) + end)) + if options.backend == "ueberzug" then options._ueberzug:hide() end + end, + setup = function(self) + scope.cleanup(options.cache_path) + return F.if_nil(self.state, {}) + end, + teardown = function(self) + if options.backend == "ueberzug" and options._ueberzug then + options._ueberzug:kill() + options._ueberzug = nil + end + end, }) -end - -return utils.make_default_callable(media_previewer, {}) --- }}} +end, options) -- vim:filetype=lua:fileencoding=utf-8 diff --git a/lua/telescope/_extensions/media/ueberzug.lua b/lua/telescope/_extensions/media/ueberzug.lua index 15e08a7..613c9e7 100644 --- a/lua/telescope/_extensions/media/ueberzug.lua +++ b/lua/telescope/_extensions/media/ueberzug.lua @@ -117,6 +117,10 @@ function Ueberzug:send(message) self.fifo:write((vim.json.encode(message):gsub("\\", "")) .. "\n", "a") end +function Ueberzug:hide() + self:send({ path = vim.NIL, x = 1, y = 1, width = 1, height = 1 }) +end + return Ueberzug -- vim:filetype=lua:fileencoding=utf-8