Previewer plugins generate visual previews of files in Yazi’s preview pane. They can display text, images, videos, archives, and any custom format.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/sxyazi/yazi/llms.txt
Use this file to discover all available pages before exploring further.
What Are Previewers?
Previewers show file contents in the right pane of Yazi. Built-in previewers handle:- Code files with syntax highlighting
- Images (PNG, JPEG, WebP, etc.)
- Videos (thumbnails/frames)
- Archives (file listings)
- PDFs (rendered pages)
- Folders (directory listings)
Previewer Structure
Previewers implement these methods:local M = {}
-- Required: Generate preview
function M:peek(job)
-- Display preview
end
-- Required: Handle scrolling
function M:seek(job)
-- Respond to scroll events
end
-- Optional: Preload/cache content
function M:preload(job)
-- Prepare content in background
return true -- or false to skip preview
end
return M
The Job Object
function M:peek(job)
job.file -- File object (url, name, cha, etc.)
job.area -- Preview area (x, y, w, h)
job.skip -- Scroll offset (lines or pages)
job.args -- Additional arguments
end
Example: Image Previewer
Here’s Yazi’s built-in image previewer:local M = {}
function M:peek(job)
local start, url = os.clock(), ya.file_cache(job)
if not url or not fs.cha(url) then
url = job.file.url
end
-- Wait for image delay
ya.sleep(math.max(0, rt.preview.image_delay / 1000 + start - os.clock()))
-- Show image in preview area
local _, err = ya.image_show(url, job.area)
ya.preview_widget(job, err)
end
function M:seek() end
function M:preload(job)
local cache = ya.file_cache(job)
if not cache or fs.cha(cache) then
return true
end
-- Precache image for faster display
return ya.image_precache(job.file.url, cache)
end
return M
yazi.toml:
[[plugin.previewers]]
mime = "image/*"
run = "image"
Example: Code Previewer
Display code files with syntax highlighting:local M = {}
function M:peek(job)
local err, bound = ya.preview_code(job)
if bound then
-- Scrolled past end, emit new peek with correct bound
ya.emit("peek", {
bound,
only_if = job.file.url,
upper_bound = true
})
elseif err and not err:find("cancelled", 1, true) then
-- Show error message
require("empty").msg(job, err)
end
end
function M:seek(job)
local h = cx.active.current.hovered
if not h or h.url ~= job.file.url then
return
end
-- Calculate scroll step
local step = math.floor(job.units * job.area.h / 10)
step = step == 0 and ya.clamp(-1, job.units, 1) or step
-- Emit new peek with updated offset
ya.emit("peek", {
math.max(0, cx.active.preview.skip + step),
only_if = job.file.url,
})
end
return M
Example: Video Previewer
Extract video frames as previews:local M = {}
function M:peek(job)
local start, cache = os.clock(), ya.file_cache(job)
if not cache then
return
end
local ok, err = self:preload(job)
if not ok or err then
return ya.preview_widget(job, err)
end
ya.sleep(math.max(0, rt.preview.image_delay / 1000 + start - os.clock()))
local _, err = ya.image_show(cache, job.area)
ya.preview_widget(job, err)
end
function M:seek(job)
local h = cx.active.current.hovered
if h and h.url == job.file.url then
ya.emit("peek", {
math.max(0, cx.active.preview.skip + job.units),
only_if = job.file.url,
})
end
end
function M:preload(job)
local cache = ya.file_cache(job)
if not cache then
return true
end
local cha = fs.cha(cache)
if cha and cha.len > 0 then
return true
end
-- Get video duration
local meta, err = self.list_meta(
job.file.path,
"format=duration:stream_disposition=attached_pic"
)
if not meta then
return true, err
elseif not meta.format.duration then
return true, Err("Failed to get video duration")
end
-- Calculate frame position
local percent = 5 + job.skip
if percent > 95 then
ya.emit("peek", { 90, only_if = job.file.url, upper_bound = true })
return false
end
-- Extract frame with ffmpeg
local output, err = Command("ffmpeg")
:arg({
"-v", "warning",
"-hwaccel", "auto",
"-ss", math.floor(meta.format.duration * percent / 100),
"-skip_frame", "nokey",
"-i", tostring(job.file.path),
"-vframes", 1,
"-q:v", 31 - math.floor(rt.preview.image_quality * 0.3),
"-vf", string.format(
"scale='min(%d,iw)':'min(%d,ih)':force_original_aspect_ratio=decrease",
rt.preview.max_width,
rt.preview.max_height
),
"-f", "image2",
"-y", tostring(cache),
})
:stderr(Command.PIPED)
:output()
if not output then
return true, Err("Failed to start ffmpeg: %s", err)
elseif output.status.success then
return true
else
return false, Err("ffmpeg error: %s", output.stderr)
end
end
function M.list_meta(path, entries)
local output, err = Command("ffprobe")
:arg({
"-v", "quiet",
"-show_entries", entries,
"-of", "json=c=1",
tostring(path)
})
:output()
if not output then
return nil, Err("Failed to start ffprobe: %s", err)
end
local t = ya.json_decode(output.stdout)
if not t then
return nil, Err("Failed to decode ffprobe output")
end
t.format = t.format or {}
t.streams = t.streams or {}
return t
end
return M
Example: PDF Previewer
Render PDF pages as images:local M = {}
function M:peek(job)
local start, cache = os.clock(), ya.file_cache(job)
if not cache then
return
end
local ok, err, bound = self:preload(job)
if bound and bound > 0 then
-- User scrolled past last page
return ya.emit("peek", {
bound - 1,
only_if = job.file.url,
upper_bound = true
})
elseif not ok or err then
return ya.preview_widget(job, err)
end
ya.sleep(math.max(0, rt.preview.image_delay / 1000 + start - os.clock()))
local _, err = ya.image_show(cache, job.area)
ya.preview_widget(job, err)
end
function M:seek(job)
local h = cx.active.current.hovered
if h and h.url == job.file.url then
local step = ya.clamp(-1, job.units, 1)
ya.emit("peek", {
math.max(0, cx.active.preview.skip + step),
only_if = job.file.url
})
end
end
function M:preload(job)
local cache = ya.file_cache(job)
if not cache or fs.cha(cache) then
return true
end
-- Convert PDF page to JPEG with pdftoppm
local output, err = Command("pdftoppm")
:arg({
"-f", job.skip + 1, -- First page
"-l", job.skip + 1, -- Last page
"-singlefile",
"-jpeg",
"-jpegopt", "quality=" .. rt.preview.image_quality,
tostring(job.file.path),
tostring(cache),
})
:output()
if not output then
return true, Err("Failed to start pdftoppm: %s", err)
elseif not output.status.success then
-- Extract page count from error
local pages = job.skip > 0 and tonumber(
output.stderr:match("the last page %((%d+)%)")
)
return true, Err("Failed to convert PDF: %s", output.stderr), pages
end
-- pdftoppm adds .jpg extension
return ya.image_precache(Url(cache .. ".jpg"), cache)
end
return M
Example: Archive Previewer
List contents of archive files:local M = {}
function M:peek(job)
local limit = job.area.h
local files, err = self.list_archive(
{ "-p", tostring(job.file.path) },
job.skip,
limit
)
if err then
return ya.preview_widget(job, err)
elseif job.skip > 0 and #files < job.skip + limit then
return ya.emit("peek", {
math.max(0, #files - limit),
only_if = job.file.url,
upper_bound = true
})
end
-- Build file list display
local left, right = {}, {}
for i = job.skip + 1, #files do
local f = files[i]
local icon = File({
url = Url(f.path),
cha = Cha { mode = tonumber(f.is_dir and "40700" or "100644", 8) },
}):icon()
-- Right side: size
if f.size > 0 then
right[#right + 1] = " " .. ya.readable_size(f.size) .. " "
else
right[#right + 1] = " "
end
-- Left side: icon and name
if icon then
left[#left + 1] = ui.Span(" " .. icon.text .. " "):style(icon.style)
else
left[#left + 1] = " "
end
left[#left] = ui.Line {
string.rep(" │", f.depth),
left[#left],
ui.truncate(f.path.name or tostring(f.path), {
rtl = true,
max = math.max(0, job.area.w - (f.depth * 2)
- ui.width(left[#left]) - ui.width(right[#right])),
}),
}
end
ya.preview_widget(job, {
ui.Text(left):area(job.area),
ui.Text(right):area(job.area):align(ui.Align.RIGHT),
})
end
function M:seek(job)
require("code"):seek(job)
end
function M.list_archive(args, skip, limit)
local child = M.spawn_7z({
"l", "-ba", "-slt", "-sccUTF-8",
table.unpack(args)
})
if not child then
return {}, Err("Failed to start 7-zip")
end
local files = {}
-- Parse 7-zip output...
-- (Implementation details omitted)
child:start_kill()
return files, nil
end
function M.spawn_7z(args)
local child = Command("7zz"):arg(args):stdout(Command.PIPED):spawn()
if not child then
child = Command("7z"):arg(args):stdout(Command.PIPED):spawn()
end
return child
end
return M
Preview Widgets
Display Text
local text = ui.Text("content\nline 2\nline 3")
:area(job.area)
:wrap(ui.Wrap.YES)
:align(ui.Align.CENTER)
ya.preview_widget(job, text)
Display Images
local _, err = ya.image_show(url, job.area)
ya.preview_widget(job, err)
Display List
local lines = {
ui.Line("item 1"),
ui.Line("item 2"),
ui.Line("item 3"),
}
ya.preview_widget(job, {
ui.List(lines):area(job.area),
})
Display Error
ya.preview_widget(job, Err("Failed to load: %s", error_msg))
Caching
Useya.file_cache() to get cache paths:
function M:preload(job)
local cache = ya.file_cache(job)
if not cache then
return true -- No cache available
end
-- Check if already cached
if fs.cha(cache) then
return true -- Cache exists
end
-- Generate cache
Command("convert")
:arg({ tostring(job.file.path), tostring(cache) })
:status()
return true
end
Scrolling
Handle scroll withseek():
function M:seek(job)
local h = cx.active.current.hovered
if not h or h.url ~= job.file.url then
return
end
-- job.units: positive = down, negative = up
local step = job.units
-- Emit new peek with updated offset
ya.emit("peek", {
math.max(0, cx.active.preview.skip + step),
only_if = job.file.url,
})
end
Upper Bounds
Tell Yazi the maximum scroll position:if job.skip > max_lines then
ya.emit("peek", {
max_lines, -- Correct upper bound
only_if = job.file.url,
upper_bound = true
})
return
end
Best Practices
Always implement seek()
Always implement seek()
Even if scrolling isn’t supported:
function M:seek() end
Use preload for expensive operations
Use preload for expensive operations
Generate previews in background:
function M:preload(job)
-- Heavy computation here
return true
end
Handle missing tools gracefully
Handle missing tools gracefully
Check if external tools exist:
local output, err = Command("tool"):output()
if not output then
return ya.preview_widget(job,
Err("tool not found: %s", err))
end
Respect size limits
Respect size limits
Check configuration:
if width > rt.preview.max_width or
height > rt.preview.max_height then
-- Resize or reject
end
Next Steps
Fetchers
Create metadata fetchers
UI Plugins
Customize the interface