Skip to main content

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.

Learn how to create custom plugins to extend Yazi’s functionality using Lua.

Plugin Basics

Yazi plugins are written in Lua and follow a simple structure. Each plugin is a Lua module that returns a table with functions.

Minimal Plugin Example

Here’s the simplest possible plugin:
-- ~/.config/yazi/plugins/hello.lua
local M = {}

function M:entry()
  ya.notify {
    title = "Hello Plugin",
    content = "Hello from Yazi!",
    timeout = 3,
    level = "info"
  }
end

return M
Call it from your keymap:
[[manager.prepend_keymap]]
on = [ "h", "i" ]
run = "plugin hello"
desc = "Say hello"

Plugin Structure

Module Pattern

All plugins follow this pattern:
local M = {}  -- Create module table

-- Add methods to module
function M:entry()
  -- Plugin logic here
end

return M  -- Return module
The M table can contain:
  • Methods (functions)
  • State variables
  • Configuration options

Entry Points

Different plugin types have different entry points:
Plugin TypeEntry PointPurpose
Functionalentry(job)Main execution function
Previewerpeek(job), seek(job)Preview generation
Fetcherfetch(job)Metadata retrieval
Spotterspot(job)Info panel display
UI Componentredraw()UI rendering

Step-by-Step Plugin Creation

Let’s create a plugin that counts files in the current directory.
1

Create the plugin file

Create ~/.config/yazi/plugins/filecount.lua:
local M = {}

function M:entry()
  -- We'll add logic here
end

return M
2

Access current directory

Use the cx (context) global to access Yazi’s state:
function M:entry()
  local cwd = cx.active.current.cwd
  local files = cx.active.current.files
  
  ya.notify {
    title = "File Count",
    content = string.format("%d files in %s", #files, tostring(cwd)),
    timeout = 3
  }
end
3

Add filtering logic

Count different file types:
function M:entry()
  local files = cx.active.current.files
  local dirs, regulars, hidden = 0, 0, 0
  
  for _, file in ipairs(files) do
    if file.cha.is_dir then
      dirs = dirs + 1
    else
      regulars = regulars + 1
    end
    
    if file.name:sub(1, 1) == "." then
      hidden = hidden + 1
    end
  end
  
  ya.notify {
    title = "File Count",
    content = string.format(
      "Total: %d\nDirectories: %d\nFiles: %d\nHidden: %d",
      #files, dirs, regulars, hidden
    ),
    timeout = 5
  }
end
4

Bind to a key

Add to keymap.toml:
[[manager.prepend_keymap]]
on = [ "c", "c" ]
run = "plugin filecount"
desc = "Count files in directory"

Accessing Yazi State

Yazi provides global objects to access its state:

cx - Context

The main state object:
-- Current tab
local current = cx.active.current
local cwd = current.cwd        -- Current directory
local files = current.files    -- Files in current dir
local hovered = current.hovered -- Currently hovered file

-- Selection
local selected = cx.active.selected  -- Selected files

-- Yanked files (copy/cut)
local yanked = cx.yanked
local is_cut = cx.yanked.is_cut

-- Tab info
local mode = cx.active.mode    -- Select/unset/normal mode

rt - Runtime

Configuration and runtime settings:
-- Preview settings
local max_width = rt.preview.max_width
local image_quality = rt.preview.image_quality

-- Manager settings
local show_hidden = rt.mgr.show_hidden
local show_symlink = rt.mgr.show_symlink

th - Theme

Access theme colors and styles:
-- Manager theme
local cwd_style = th.mgr.cwd
local hovered_style = th.mgr.hovered

-- Status bar theme
local status_style = th.status.overall

Using the Lua API

Yazi provides a rich API through the ya global:

Notifications

ya.notify {
  title = "Title",
  content = "Message",
  timeout = 3,  -- Seconds
  level = "info"  -- "info", "warn", "error"
}

User Input

local value, event = ya.input {
  title = "Enter name:",
  pos = { "center", x = 50, y = 50 },
  obscure = false  -- true for password input
}

if event == 1 then
  -- User confirmed
  ya.notify { content = "You entered: " .. value }
end

File Operations

-- Read directory
local entries = fs.read_dir(path, { limit = 100 })

-- Check file attributes
local cha = fs.cha(path)
if cha and cha.is_dir then
  -- It's a directory
end

-- Create/remove
fs.write(path, "content")
fs.remove("file", path)
fs.remove("dir", path)

Running Commands

-- Run command and get output
local output, err = Command("ls")
  :arg({ "-la", "/tmp" })
  :stdout(Command.PIPED)
  :output()

if output then
  ya.notify { content = output.stdout }
end

-- Spawn async process
local child, err = Command("ffmpeg")
  :arg({ "-i", "input.mp4" })
  :stdout(Command.PIPED)
  :stderr(Command.PIPED)
  :spawn()

if child then
  local output = child:wait_with_output()
end

Emitting Events

-- Navigate to directory
ya.emit("cd", { "/path/to/dir", raw = true })

-- Reveal file
ya.emit("reveal", { Url("/path/to/file") })

-- Open file
ya.emit("open", {})

-- Toggle selection
ya.emit("toggle", {})

Plugin Configuration

Setup Function

Plugins can have a setup() function for initialization:
local M = {}

function M:setup(opts)
  opts = opts or {}
  self.auto_save = opts.auto_save or false
  self.interval = opts.interval or 60
  
  if opts.on_init then
    opts.on_init()
  end
end

function M:entry()
  if self.auto_save then
    -- Use configured option
  end
end

return M
Call setup in init.lua:
require("myplugin"):setup {
  auto_save = true,
  interval = 30
}

Async Programming

For long-running operations, use async:
function M:entry()
  ya.async(function()
    -- This runs in background
    local result = Command("slow-command"):output()
    
    -- Update UI from async
    ya.sync(function()
      ya.notify { content = "Done!" }
    end)
  end)
end

Error Handling

function M:entry()
  local ok, result = pcall(function()
    -- Code that might fail
    return Command("risky-command"):output()
  end)
  
  if not ok then
    ya.notify {
      title = "Error",
      content = tostring(result),
      level = "error"
    }
  end
end

Best Practices

Always declare variables with local to avoid polluting the global namespace:
-- Good
local count = 0

-- Bad
count = 0  -- Global variable
Always validate data before using it:
local hovered = cx.active.current.hovered
if not hovered then
  return
end

-- Safe to use hovered
local name = hovered.name
Don’t block the UI thread:
-- Good
ya.async(function()
  local result = Command("slow-cmd"):output()
end)

-- Bad - blocks UI
local result = Command("slow-cmd"):output()
Always notify users of success or failure:
local output, err = Command("tool"):output()
if not output then
  ya.notify {
    title = "Error",
    content = tostring(err),
    level = "error"
  }
end

Debugging

ya.err("Debug value: " .. tostring(value))

Check Yazi logs

Logs are written to:
  • Linux/macOS: ~/.local/state/yazi/yazi.log
  • Windows: %APPDATA%\yazi\state\yazi.log

Next Steps

UI Plugins

Customize Yazi’s interface

Functional Plugins

Add new commands

Previewers

Create file previewers

Fetchers

Build metadata fetchers

Build docs developers (and LLMs) love