> ## 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.

# Functional Plugins

> Create plugins that add new commands and behaviors to Yazi

Functional plugins extend Yazi's capabilities by adding new commands, integrating external tools, and automating workflows.

## What Are Functional Plugins?

Functional plugins are Lua modules with an `entry()` function that executes when called. They can:

* Add interactive commands
* Integrate with external tools
* Automate file operations
* Process selections
* Show custom dialogs

## Basic Structure

```lua theme={null}
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:

```lua theme={null}
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
```

Bind it in `keymap.toml`:

```toml theme={null}
[[manager.prepend_keymap]]
on = [ "z" ]
run = "plugin zoxide"
desc = "Jump to directory with zoxide"
```

## Example: Fuzzy Finding with FZF

Integrate fzf for file selection:

```lua theme={null}
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:

```lua theme={null}
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
```

Call from command line:

```bash theme={null}
ya pub extract /path/to/archive.zip
```

## Plugin Job Object

The `job` parameter contains:

```lua theme={null}
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

```lua theme={null}
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

```lua theme={null}
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:

```lua theme={null}
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

Use `ya.sync()` for persistent state:

```lua theme={null}
-- 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:

```lua theme={null}
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:

```lua theme={null}
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

```lua theme={null}
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

```lua theme={null}
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

```lua theme={null}
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

```lua theme={null}
ya.async(function()
  local output = Command("long-running-cmd")
    :stdout(Command.PIPED)
    :output()
  
  ya.sync(function()
    ya.notify { content = "Done!" }
  end)
end)
```

### Streaming Output

```lua theme={null}
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

<AccordionGroup>
  <Accordion title="Always provide feedback">
    Notify users of success/failure:

    ```lua theme={null}
    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
    ```
  </Accordion>

  <Accordion title="Use async for slow operations">
    Don't block the UI:

    ```lua theme={null}
    ya.async(function()
      -- Long operation
      local result = Command("slow"):output()
      
      -- Update UI in sync
      ya.sync(function()
        ya.notify { content = "Complete" }
      end)
    end)
    ```
  </Accordion>

  <Accordion title="Handle virtual filesystems">
    Check before operating:

    ```lua theme={null}
    local cwd = cx.active.current.cwd
    if cwd.scheme.is_virtual then
      return ya.notify {
        content = "Not supported in virtual FS",
        level = "warn"
      }
    end
    ```
  </Accordion>

  <Accordion title="Clean up resources">
    Always kill child processes:

    ```lua theme={null}
    local child = Command("cmd"):spawn()
    if child then
      -- Use the child
      child:start_kill()  -- Clean up
    end
    ```
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Previewers" icon="eye" href="plugins/previewers">
    Create custom file previewers
  </Card>

  <Card title="Fetchers" icon="download" href="plugins/fetchers">
    Build metadata fetchers
  </Card>
</CardGroup>
