Functional plugins extend Yazi’s capabilities by adding new commands, integrating external tools, and automating workflows.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 Functional Plugins?
Functional plugins are Lua modules with anentry() function that executes when called. They can:
- Add interactive commands
- Integrate with external tools
- Automate file operations
- Process selections
- Show custom dialogs
Basic Structure
local M = {}
function M:entry(job)
-- job contains:
-- - job.args: Arguments passed to plugin
-- - job.file: Current file (if applicable)
-- Plugin logic here
end
return M
Example: Directory Jumping with Zoxide
Let’s examine Yazi’s zoxide integration:local M = {}
-- State management
local state = ya.sync(function(st)
return {
cwd = tostring(cx.active.current.cwd),
empty = st.empty,
}
end)
local set_state = ya.sync(function(st, empty)
st.empty = empty
end)
-- Setup function (called from init.lua)
function M:setup(opts)
opts = opts or {}
if opts.update_db then
-- Subscribe to directory changes
ps.sub("cd", function()
local cwd = cx.active.current.cwd
ya.async(function()
Command("zoxide")
:arg({ "add", tostring(cwd) })
:status()
end)
end)
end
end
-- Main entry point
function M:entry()
local st = state()
-- Check if zoxide has data
if st.empty == nil then
st.empty = M.is_empty(st.cwd)
set_state(st.empty)
end
if st.empty then
return ya.notify {
title = "Zoxide",
content = "No directory history found",
timeout = 5,
level = "error",
}
end
-- Hide UI while running interactive command
local permit = ui.hide()
local target, err = M.run_with(st.cwd)
permit:drop()
if not target then
ya.notify {
title = "Zoxide",
content = tostring(err),
timeout = 5,
level = "error"
}
elseif target ~= "" then
ya.emit("cd", { target, raw = true })
end
end
-- Helper: Check if zoxide has entries
function M.is_empty(cwd)
local child = Command("zoxide")
:arg({ "query", "-l", "--exclude", cwd })
:stdout(Command.PIPED)
:spawn()
if not child then
return true
end
local first = child:read_line()
child:start_kill()
return not first
end
-- Helper: Run zoxide interactively
function M.run_with(cwd)
local child, err = Command("zoxide")
:arg({ "query", "-i", "--exclude", cwd })
:env("_ZO_FZF_OPTS", "--height=100%")
:stdin(Command.INHERIT)
:stdout(Command.PIPED)
:stderr(Command.PIPED)
:spawn()
if not child then
return nil, Err("Failed to start zoxide: %s", err)
end
local output, err = child:wait_with_output()
if not output then
return nil, Err("Cannot read output: %s", err)
elseif not output.status.success and output.status.code ~= 130 then
return nil, Err("Exited with code %s", output.status.code)
end
return output.stdout:gsub("\n$", ""), nil
end
return M
keymap.toml:
[[manager.prepend_keymap]]
on = [ "z" ]
run = "plugin zoxide"
desc = "Jump to directory with zoxide"
Example: Fuzzy Finding with FZF
Integrate fzf for file selection:local M = {}
local state = ya.sync(function()
local selected = {}
for _, url in pairs(cx.active.selected) do
selected[#selected + 1] = url
end
return cx.active.current.cwd, selected
end)
function M:entry()
-- Clear visual selection
ya.emit("escape", { visual = true })
local cwd, selected = state()
if cwd.scheme.is_virtual then
return ya.notify {
title = "Fzf",
content = "Not supported under virtual filesystems",
timeout = 5,
level = "warn"
}
end
local permit = ui.hide()
local output, err = M.run_with(cwd, selected)
permit:drop()
if not output then
return ya.notify {
title = "Fzf",
content = tostring(err),
timeout = 5,
level = "error"
}
end
local urls = M.split_urls(cwd, output)
if #urls == 1 then
local cha = #selected == 0 and fs.cha(urls[1])
ya.emit(cha and cha.is_dir and "cd" or "reveal", { urls[1], raw = true })
elseif #urls > 1 then
urls.state = #selected > 0 and "off" or "on"
ya.emit("toggle_all", urls)
end
end
function M.run_with(cwd, selected)
local child, err = Command("fzf")
:arg("-m")
:cwd(tostring(cwd))
:stdin(#selected > 0 and Command.PIPED or Command.INHERIT)
:stdout(Command.PIPED)
:spawn()
if not child then
return nil, Err("Failed to start fzf: %s", err)
end
-- Pipe selected files to fzf
for _, u in ipairs(selected) do
child:write_all(string.format("%s\n", u))
end
if #selected > 0 then
child:flush()
end
local output, err = child:wait_with_output()
if not output then
return nil, Err("Cannot read fzf output: %s", err)
elseif not output.status.success and output.status.code ~= 130 then
return nil, Err("fzf exited with code %s", output.status.code)
end
return output.stdout, nil
end
function M.split_urls(cwd, output)
local t = {}
for line in output:gmatch("[^\r\n]+") do
local u = Url(line)
if u.is_absolute then
t[#t + 1] = u
else
t[#t + 1] = cwd:join(u)
end
end
return t
end
return M
Example: Archive Extraction
A plugin that extracts archives:local M = {}
-- Setup: Register remote command
function M:setup()
ps.sub_remote("extract", function(args)
for _, arg in ipairs(args) do
ya.emit("plugin", { self._id, ya.quote(arg, true) })
end
end)
end
-- Entry point
function M:entry(job)
local from = job.args[1] and Url(job.args[1])
if not from then
return ya.err("No URL provided")
end
local pwd = ""
while true do
if not M:try_with(from, pwd) then
break
end
-- Ask for password
local value, event = ya.input {
pos = { "top-center", y = 2, w = 50 },
title = string.format('Password for "%s":', from.name),
obscure = true,
}
if event == 1 then
pwd = value
else
break
end
end
end
-- Try extracting with password
function M:try_with(from, pwd, to)
to = to or from.parent
if not to then
return ya.err("Invalid URL '%s'", from)
end
-- Create temp directory
local tmp = fs.unique("dir", to:join(".tmp_extract"))
if not tmp then
return ya.err("Failed to create temp directory")
end
-- Run 7zip
local archive = require("archive")
local child, err = archive.spawn_7z {
"x", "-aou", "-sccUTF-8",
"-p" .. pwd,
"-o" .. tostring(tmp),
tostring(from)
}
if not child then
return ya.err("Failed to start 7zip: %s", err)
end
local output, err = child:wait_with_output()
-- Check if password was wrong
if output and output.status.code == 2
and archive.is_encrypted(output.stderr) then
fs.remove("dir_all", tmp)
return true -- Retry with new password
end
-- Move extracted files
self:tidy(from, to, tmp)
if not output then
return ya.err("7zip failed: %s", err)
elseif output.status.code ~= 0 then
return ya.err("7zip error: %s", output.stderr)
end
ya.notify {
title = "Extract",
content = string.format("Extracted %s", from.name),
timeout = 3
}
end
function M:tidy(from, to, tmp)
local outs = fs.read_dir(tmp, { limit = 2 })
if not outs or #outs == 0 then
fs.remove("dir", tmp)
return
end
local target
if #outs == 1 then
target = to:join(outs[1].name)
else
target = to:join(from.stem)
end
target = fs.unique(#outs == 1 and "file" or "dir", target)
if target then
if #outs == 1 then
fs.rename(outs[1].url, target)
else
fs.rename(tmp, target)
end
end
fs.remove("dir", tmp)
end
return M
ya pub extract /path/to/archive.zip
Plugin Job Object
Thejob parameter contains:
function M:entry(job)
-- Arguments passed to plugin
local args = job.args -- table
-- Current file (if applicable)
local file = job.file -- File object or nil
-- Plugin name/ID
local name = job.name -- string
end
Interactive UI
User Input
local value, event = ya.input {
title = "Enter name:",
pos = { "top-center", y = 2, w = 50 },
obscure = false, -- Set true for password
}
if event == 1 then
-- User confirmed (Enter)
ya.notify { content = "You entered: " .. value }
else
-- User cancelled (Esc)
end
Confirmation Dialog
local choice, event = ya.confirm {
title = "Confirm Action",
content = "Are you sure?",
pos = { "center", w = 50, h = 10 },
}
if choice then
-- User confirmed
end
Hide UI for External Commands
When running interactive external commands:local permit = ui.hide()
-- Run interactive command
local output = Command("fzf")
:stdin(Command.INHERIT)
:stdout(Command.PIPED)
:spawn()
:wait_with_output()
permit:drop() -- Restore UI
State Management
Useya.sync() for persistent state:
-- Create state accessor
local get_state = ya.sync(function(st)
st.counter = st.counter or 0
return st.counter
end)
-- Create state mutator
local set_state = ya.sync(function(st, value)
st.counter = value
end)
function M:entry()
local count = get_state()
count = count + 1
set_state(count)
ya.notify { content = "Count: " .. count }
end
Event Subscription
Listen to Yazi events:function M:setup(opts)
-- Subscribe to directory changes
ps.sub("cd", function()
local cwd = cx.active.current.cwd
ya.notify { content = "Changed to: " .. tostring(cwd) }
end)
-- Subscribe to selection changes
ps.sub("select", function()
local count = #cx.active.selected
ya.notify { content = count .. " files selected" }
end)
end
Remote Commands
Register commands callable from CLI:function M:setup()
ps.sub_remote("mycommand", function(args)
-- Handle args from: ya pub mycommand arg1 arg2
for _, arg in ipairs(args) do
ya.notify { content = "Got: " .. arg }
end
end)
end
File Operations
Processing Selection
function M:entry()
local selected = {}
for _, url in pairs(cx.active.selected) do
selected[#selected + 1] = url
end
if #selected == 0 then
local h = cx.active.current.hovered
if h then
selected[1] = h.url
end
end
for _, url in ipairs(selected) do
-- Process each file
ya.notify { content = "Processing: " .. tostring(url) }
end
end
Batch Operations
function M:entry()
local files = cx.active.current.files
for _, file in ipairs(files) do
if file.cha.is_dir then
-- Process directories
else
-- Process files
end
end
end
Running Commands
Synchronous
local output, err = Command("ls")
:arg({ "-la" })
:stdout(Command.PIPED)
:output()
if output and output.status.success then
ya.notify { content = output.stdout }
end
Asynchronous
ya.async(function()
local output = Command("long-running-cmd")
:stdout(Command.PIPED)
:output()
ya.sync(function()
ya.notify { content = "Done!" }
end)
end)
Streaming Output
local child = Command("command")
:stdout(Command.PIPED)
:spawn()
if child then
while true do
local line, event = child:read_line()
if event ~= 0 then
break
end
-- Process line
end
child:start_kill()
end
Best Practices
Always provide feedback
Always provide feedback
Notify users of success/failure:
if success then
ya.notify {
title = "Success",
content = "Operation completed",
timeout = 3
}
else
ya.notify {
title = "Error",
content = error_message,
timeout = 5,
level = "error"
}
end
Use async for slow operations
Use async for slow operations
Don’t block the UI:
ya.async(function()
-- Long operation
local result = Command("slow"):output()
-- Update UI in sync
ya.sync(function()
ya.notify { content = "Complete" }
end)
end)
Handle virtual filesystems
Handle virtual filesystems
Check before operating:
local cwd = cx.active.current.cwd
if cwd.scheme.is_virtual then
return ya.notify {
content = "Not supported in virtual FS",
level = "warn"
}
end
Clean up resources
Clean up resources
Always kill child processes:
local child = Command("cmd"):spawn()
if child then
-- Use the child
child:start_kill() -- Clean up
end
Next Steps
Previewers
Create custom file previewers
Fetchers
Build metadata fetchers