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.

Fetcher plugins retrieve file metadata asynchronously and in batches. They’re perfect for tasks like detecting MIME types, reading Git status, or extracting custom file properties.

What Are Fetchers?

Fetchers run in the background to collect file metadata without blocking the UI. Common uses:
  • MIME type detection - Identify file types
  • Git status - Show version control info
  • Image metadata - Extract dimensions, format, EXIF
  • Audio tags - Read ID3 tags from music files
  • Custom properties - Any file-specific data
Yazi’s built-in MIME fetchers are essential examples.

Fetcher Structure

local M = {}

function M:fetch(job)
  -- job.files: Array of files to process
  -- Returns: State array (true/false for each file)
  
  for i, file in ipairs(job.files) do
    -- Process file
    -- Update metadata with ya.emit("update_mimes", ...)
  end
  
  return state  -- Array of booleans
end

return M

The Job Object

function M:fetch(job)
  job.files     -- Array of File objects
  job.args      -- Additional arguments
  
  -- Each file has:
  -- file.url    -- File URL
  -- file.path   -- File path
  -- file.cha    -- File characteristics (size, mode, etc.)
  -- file.cache  -- Cache path (if applicable)
end

Example: MIME Type Fetcher

Yazi’s local MIME fetcher using the file command:
local M = {}

function M:fetch(job)
  local urls, paths = {}, {}
  
  -- Build path arrays
  for i, file in ipairs(job.files) do
    if file.cache then
      urls[i], paths[i] = tostring(file.url), tostring(file.cache)
    else
      paths[i] = tostring(file.path)
    end
  end
  
  -- Run file command
  local child, err = M.spawn_file1(paths)
  if not child then
    M.placeholder(err, urls, paths)
    return true, err
  end
  
  -- Process output
  local updates, last = {}, ya.time()
  local flush = function(force)
    if not force and ya.time() - last < 0.3 then
      return  -- Throttle updates
    end
    if next(updates) then
      ya.emit("update_mimes", { updates = updates })
      updates, last = {}, ya.time()
    end
  end
  
  local i, state = 1, {}
  repeat
    local line, event = child:read_line_with { timeout = 300 }
    if event == 3 then
      flush(true)
      goto continue
    elseif event ~= 0 then
      break
    end
    
    -- Parse MIME type from output
    local match = M.match_mimetype(line)
    if match then
      updates[urls[i] or paths[i]], state[i], i = match, true, i + 1
      flush(false)
    else
      state[i], i = false, i + 1
    end
    
    ::continue::
  until i > #paths
  
  flush(true)
  return state
end

function M.match_mimetype(line)
  local patterns = {
    "text", "image", "video", "application",
    "audio", "font", "inode"
  }
  
  for _, pat in ipairs(patterns) do
    local typ, sub = line:match(string.format("(%s/)([+-.a-zA-Z0-9]+)%%s+$", pat))
    if sub and line:find(typ .. sub, 1, true) == 1 then
      return typ:gsub("^x%-", "", 1) .. sub:gsub("^x%-", "", 1)
    end
  end
end

function M.spawn_file1(paths)
  local bin = os.getenv("YAZI_FILE_ONE") or "file"
  local windows = ya.target_family() == "windows"
  
  local cmd = Command(bin)
    :arg({ "-bL", "--mime-type" })
    :stdout(Command.PIPED)
  
  if windows then
    cmd:arg({ "-f", "-" }):stdin(Command.PIPED)
  else
    cmd:arg("--"):arg(paths)
  end
  
  local child, err = cmd:spawn()
  if not child then
    return nil, Error.fs {
      kind = err.kind or "Other",
      message = string.format("Failed to start '%s': %s", bin, err),
    }
  elseif windows then
    child:write_all(table.concat(paths, "\n"))
    child:flush()
    ya.drop(child:take_stdin())
  end
  
  return child
end

function M.placeholder(err, urls, paths)
  if err.kind ~= "NotFound" then
    return
  end
  
  -- Set placeholder MIME when tool not found
  local updates = {}
  for i = 1, #paths do
    updates[urls[i] or paths[i]] = "null/file1-not-found"
  end
  
  ya.emit("update_mimes", { updates = updates })
end

return M
Configure in yazi.toml:
[[plugin.fetchers]]
id = "mime"
url = "local://*"
run = "mime.local"
prio = "high"

Example: Directory MIME Fetcher

Mark directories with a special MIME type:
local M = {}

function M:fetch(job)
  local updates = {}
  
  for _, file in ipairs(job.files) do
    if file.cha.is_dir then
      updates[tostring(file.url)] = "inode/directory"
    end
  end
  
  if next(updates) then
    ya.emit("update_mimes", { updates = updates })
  end
  
  return true
end

return M
[[plugin.fetchers]]
id = "mime"
url = "*/"
run = "mime.dir"
prio = "high"

Example: Remote MIME Fetcher

Detect MIME types for remote files:
local M = {}

function M:fetch(job)
  local updates = {}
  
  for _, file in ipairs(job.files) do
    local url = file.url
    
    -- Use HEAD request to get Content-Type
    local output, err = Command("curl")
      :arg({ "-sI", "-L", tostring(url) })
      :stdout(Command.PIPED)
      :output()
    
    if output and output.status.success then
      local mime = output.stdout:match("Content%-Type:%s*([^%s;]+)")
      if mime then
        updates[tostring(url)] = mime
      end
    end
  end
  
  if next(updates) then
    ya.emit("update_mimes", { updates = updates })
  end
  
  return true
end

return M
[[plugin.fetchers]]
id = "mime"
url = "remote://*"
run = "mime.remote"
prio = "normal"

Example: Git Status Fetcher

Show Git status for files:
local M = {}

function M:fetch(job)
  -- Get repository root
  local output, err = Command("git")
    :arg({ "rev-parse", "--show-toplevel" })
    :cwd(tostring(job.files[1].url.parent))
    :stdout(Command.PIPED)
    :output()
  
  if not output or not output.status.success then
    return true  -- Not a git repo
  end
  
  local root = output.stdout:gsub("\n$", "")
  
  -- Get status for all files
  output, err = Command("git")
    :arg({ "status", "--porcelain", "-z" })
    :cwd(root)
    :stdout(Command.PIPED)
    :output()
  
  if not output or not output.status.success then
    return true
  end
  
  -- Parse status output
  local statuses = {}
  for status, path in output.stdout:gmatch("(%S+) ([^%z]+)%z") do
    statuses[path] = status
  end
  
  -- Update file metadata
  local updates = {}
  for _, file in ipairs(job.files) do
    local rel = tostring(file.path):sub(#root + 2)
    if statuses[rel] then
      updates[tostring(file.url)] = {
        git_status = statuses[rel]
      }
    end
  end
  
  if next(updates) then
    ya.emit("update_metadata", { updates = updates })
  end
  
  return true
end

return M

Updating Metadata

Update MIME Types

local updates = {
  ["file:///path/to/file1"] = "image/png",
  ["file:///path/to/file2"] = "text/plain",
}

ya.emit("update_mimes", { updates = updates })

Update Custom Metadata

local updates = {
  ["file:///path/to/image.jpg"] = {
    dimensions = "1920x1080",
    format = "JPEG",
    size_kb = 512,
  },
}

ya.emit("update_metadata", { updates = updates })

Batch Processing

Process files efficiently in batches:
function M:fetch(job)
  local batch_size = 50
  local state = {}
  
  for i = 1, #job.files, batch_size do
    local batch = {}
    for j = i, math.min(i + batch_size - 1, #job.files) do
      batch[#batch + 1] = job.files[j]
    end
    
    -- Process batch
    local updates = self:process_batch(batch)
    
    if next(updates) then
      ya.emit("update_mimes", { updates = updates })
    end
    
    for j = i, math.min(i + batch_size - 1, #job.files) do
      state[j] = true
    end
  end
  
  return state
end

Throttling Updates

Avoid overwhelming the UI:
function M:fetch(job)
  local updates, last = {}, ya.time()
  
  local flush = function(force)
    local now = ya.time()
    if not force and now - last < 0.3 then
      return  -- Wait at least 300ms between updates
    end
    
    if next(updates) then
      ya.emit("update_mimes", { updates = updates })
      updates, last = {}, now
    end
  end
  
  for _, file in ipairs(job.files) do
    -- Process file
    updates[tostring(file.url)] = mime
    flush(false)  -- Throttled
  end
  
  flush(true)  -- Force final update
  return true
end

Streaming Output

Read command output line by line:
function M:fetch(job)
  local child = Command("tool")
    :arg(paths)
    :stdout(Command.PIPED)
    :spawn()
  
  if not child then
    return true
  end
  
  local i = 1
  while true do
    local line, event = child:read_line_with { timeout = 300 }
    
    if event == 3 then
      -- Timeout, continue waiting
      goto continue
    elseif event ~= 0 then
      -- EOF or error
      break
    end
    
    -- Process line
    -- Update metadata for job.files[i]
    i = i + 1
    
    ::continue::
  end
  
  child:start_kill()
  return true
end

Error Handling

function M:fetch(job)
  local output, err = Command("tool"):output()
  
  if not output then
    ya.err("Fetcher failed: " .. tostring(err))
    return true  -- Don't retry
  end
  
  if not output.status.success then
    ya.err("Tool exited with code: " .. output.status.code)
    
    -- Set error state
    local updates = {}
    for _, file in ipairs(job.files) do
      updates[tostring(file.url)] = "null/error"
    end
    ya.emit("update_mimes", { updates = updates })
  end
  
  return true
end

Configuration

Configure fetchers in yazi.toml:
[plugin]
fetchers = [
  # High priority for essential MIME detection
  { id = "mime", url = "*/", run = "mime.dir", prio = "high" },
  { id = "mime", url = "local://*", run = "mime.local", prio = "high" },
  { id = "mime", url = "remote://*", run = "mime.remote", prio = "normal" },
  
  # Custom fetchers
  { id = "git", url = "local://*", run = "git-status", prio = "low" },
  { id = "exif", url = "*.{jpg,jpeg,png}", run = "exif-reader", prio = "low" },
]

Priority Levels

  • high - Essential, run first
  • normal - Standard priority
  • low - Nice-to-have, run last

Best Practices

Call external tools once for multiple files:
-- Good: Single call for all files
Command("file"):arg(all_paths):spawn()

-- Bad: One call per file
for _, file in ipairs(job.files) do
  Command("file"):arg(tostring(file.path)):spawn()
end
Don’t emit events too frequently:
local last_update = ya.time()
if ya.time() - last_update >= 0.3 then
  ya.emit("update_mimes", { updates = updates })
  last_update = ya.time()
end
Set placeholder values when tools are unavailable:
if not child then
  local updates = {}
  for _, file in ipairs(job.files) do
    updates[tostring(file.url)] = "null/tool-not-found"
  end
  ya.emit("update_mimes", { updates = updates })
  return true
end
Don’t block indefinitely:
local line, event = child:read_line_with { timeout = 300 }
if event == 3 then
  -- Timeout, handle gracefully
end
Always kill spawned processes:
local child = Command("tool"):spawn()
-- ... use child ...
child:start_kill()  -- Always clean up

Next Steps

Previewers

Create custom file previewers

Functional Plugins

Add new commands

Build docs developers (and LLMs) love