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

# Fetchers

> Create plugins that fetch file metadata asynchronously

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

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

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

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

```toml theme={null}
[[plugin.fetchers]]
id = "mime"
url = "local://*"
run = "mime.local"
prio = "high"
```

## Example: Directory MIME Fetcher

Mark directories with a special MIME type:

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

```toml theme={null}
[[plugin.fetchers]]
id = "mime"
url = "*/"
run = "mime.dir"
prio = "high"
```

## Example: Remote MIME Fetcher

Detect MIME types for remote files:

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

```toml theme={null}
[[plugin.fetchers]]
id = "mime"
url = "remote://*"
run = "mime.remote"
prio = "normal"
```

## Example: Git Status Fetcher

Show Git status for files:

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

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

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

### Update Custom Metadata

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

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

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

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

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

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

<AccordionGroup>
  <Accordion title="Process files in batches">
    Call external tools once for multiple files:

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

  <Accordion title="Throttle UI updates">
    Don't emit events too frequently:

    ```lua theme={null}
    local last_update = ya.time()
    if ya.time() - last_update >= 0.3 then
      ya.emit("update_mimes", { updates = updates })
      last_update = ya.time()
    end
    ```
  </Accordion>

  <Accordion title="Handle missing tools gracefully">
    Set placeholder values when tools are unavailable:

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

  <Accordion title="Use timeouts for reads">
    Don't block indefinitely:

    ```lua theme={null}
    local line, event = child:read_line_with { timeout = 300 }
    if event == 3 then
      -- Timeout, handle gracefully
    end
    ```
  </Accordion>

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

    ```lua theme={null}
    local child = Command("tool"):spawn()
    -- ... use child ...
    child:start_kill()  -- Always clean up
    ```
  </Accordion>
</AccordionGroup>

## Next Steps

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

  <Card title="Functional Plugins" icon="bolt" href="plugins/functional-plugins">
    Add new commands
  </Card>
</CardGroup>
