Files
dotfiles/mpv/scripts/file-browser/docs/addons.md
2025-06-14 20:26:14 +02:00

959 lines
49 KiB
Markdown

# How to Write an Addon - API v1.7.0
Addons provide ways for file-browser to parse non-native directory structures. This document describes how one can create their own custom addon.
If you have an independent script but want to use file-browser's parsing capabilities, perhaps to make use of existing addons, then look [here](https://github.com/CogentRedTester/mpv-file-browser#get-directory-contents).
## Terminology
For the purpose of this document addons refer to the scripts being loaded while parsers are the objects the scripts return.
An addon can return multiple parsers, but when they only returns one the terms are almost synonymous.
Additionally, `method` refers to functions called using the `object:funct()` syntax, and hence have access to the self object, whereas `function` is the standard `object.funct()` syntax.
## API Version
The API version, shown in the title of this document, allows file-browser to ensure that addons are using the correct
version of the API. It follows [semantic versioning](https://semver.org/) conventions of `MAJOR.MINOR.PATCH`.
A parser sets its version string with the `version` field, as seen [below](#overview).
Any change that breaks backwards compatability will cause the major version number to increase.
A parser MUST have the same major version number as the API, otherwise an error message will be printed and the parser will
not be loaded.
A minor version number denotes a change to the API that is backwards compatible. This includes additional API functions,
or extra fields in tables that were previously unused. It may also include additional arguments to existing functions that
add additional behaviour without changing the old behaviour.
If the parser's minor version number is greater than the API_VERSION, then a warning is printed to the console.
Patch numbers denote bug fixes, and are ignored when loading an addon.
For this reason addon authors are allowed to leave the patch number out of their version tag and just use `MAJOR.MINOR`.
## Overview
File-browser automatically loads any lua files from the `~~/script-modules/file-browser-addons` directory as modules.
Each addon must return either a single parser table, or an array of parser tables. Each parser object must contain the following three members:
| key | type | arguments | returns | description |
|-----------|--------|---------------------------|------------------------|--------------------------------------------------------------------------------------------------------------|
| priority | number | - | - | a number to determine what order parsers are tested - see [here](#priority-suggestions) for suggested values |
| api_version| string | - | - | the API version the parser is using - see [API Version](#api-version) |
| can_parse | method | string, parse_state_table | boolean | returns whether or not the given path is compatible with the parser |
| parse | method | string, parse_state_table | list_table, opts_table | returns an array of item_tables, and a table of options to control how file_browser handles the list |
Additionally, each parser can optionally contain:
| key | type | arguments | returns | description |
|--------------|--------|-----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | string | - | - | the name of the parser used for debug messages and to create a unique id - by default uses the filename with `.lua` or `-browser.lua` removed |
| keybind_name | string | - | - | the name to use when setting custom keybind filters - uses the value of name by default but can be set manually so that the same keys work with multiple addons |
| setup | method | - | - | if it exists this method is automatically run after all parsers are imported and API functions are made available |
| keybinds | table | - | - | an array of keybind objects for the browser to set when loading - see [#keybinds] |
All parsers are given a unique string ID based on their name. If there are collisions then numbers are appended to the end of the name until a free name is found.
These IDs are primarily used for debug messages, though they may gain additional functionality in the future.
Here is an extremely simple example of an addon creating a parser table and returning it to file-browser.
```lua
local parser = {
api_version = '1.0.0',
priority = 100,
name = "example" -- this parser will have the id 'example' or 'example_#' if there are duplicates
}
function parser:can_parse(directory)
return directory == "Example/"
end
function parser:parse(directory, state)
local list, opts
------------------------------
--- populate the list here ---
------------------------------
return list, opts
end
return parser
```
## Parsing
When a directory is loaded file-browser will iterate through the list of parsers from lowest to highest priority.
The first parser for which `can_parse` returns true will be selected as the parser for that directory.
The `parse` method will then be called on the selected parser, which is expected to return either a table of list items, or nil.
If an empty table is returned then file-browser will treat the directory as empty, otherwise if the list_table is nil then file-browser will attempt to run `parse` on the next parser for which `can_parse` returns true.
This continues until a parser returns a list_table, or until there are no more parsers.
The entire parse operation is run inside of a coroutine, this allows parsers to pause execution to handle asynchronous operations.
Please read [coroutines](#coroutines) for all the details.
### Parse State Table
The `parse` and `can_parse` functions are passed a state table as its second argument, this contains the following fields.
| key | type | description |
|----------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| source | string | the source of the parse request |
| directory | string | the directory of the parse request - for debugging purposes |
| already_deferred | boolean | whether or not [defer](#advanced-functions) was called during this parse, if so then file-browser will not try to query any more parsers after receiving the result - set automatically, but can be manually disabled |
| yield | method | a wrapper around `coroutine.yield()` - see [coroutines](#coroutines) |
| is_coroutine_current | method | returns if the browser is waiting on the current coroutine to populate the list |
`already_deferred` is an optimisation. If a script uses defer and still returns nil, then that means that none of the remaining parsers will be able to parse the path.
Therefore, it is more efficient to just immediately jump to the root.
It is up to the addon author to manually disable this if their use of `defer` conflicts with this assumption.
Source can have the following values:
| source | description |
|----------------|---------------------------------------------------------------------------------------------------------|
| browser | triggered by the main browser window |
| loadlist | the browser is scanning the directory to append to the playlist |
| script-message | triggered by the `get-directory-contents` script-message |
| addon | caused by an addon calling the `parse_directory` API function - note that addons can set a custom state |
Note that all calls to any `parse` function during a specific parse request will be given the same parse_state table.
This theoretically allows parsers to communicate with parsers of a lower priority (or modify how they see source information),
but no guarantees are made that specific keys will remain unused by the API.
#### Coroutines
Any calls to `parse()` (or `can_parse()`, but you should never be yielding inside there) are done in a [Lua coroutine](https://www.lua.org/manual/5.1/manual.html#2.11).
This means that you can arbitrarily pause the parse operation if you need to wait for some asynchronous operation to complete,
such as waiting for user input, or for a network request to complete.
Making these operations asynchronous has performance
advantages as well, for example recursively opening a network directory tree could cause the browser to freeze
for a long period of time. If the network query were asynchronous then the browser would only freeze during actual operations,
during network operations it would be free for the user interract with. The browser has even been designed so that
a loadfile/loadlist operation saves it's own copy of the current directory, so even if the user hops around like crazy the original
open operation will still occur in the correct order (though there's nothing stopping them starting a new operation which will cause
random ordering.)
However, there is one downside to this behaviour. If the parse operation is requested by the browser, then it is
possible for the user to change directories while the coroutine is yielded. If you were to resume the coroutine
in that situation, then any operations you do are wasted, and unexpected bahaviour could happen.
file-browser will automatically detect when it receives a list from an aborted coroutine, so there is no risk
of the current list being replaced, but any other logic in your script will continue until `parse` returns.
To fix this there are two methods available in the state table, the `yield()` method is a wrapper around `coroutine.yield()` that
detects when the browser has abandoned the parse, and automatically kills the coroutine by throwing an error.
The `is_coroutine_current()` method simply compares if the current coroutine (as returned by `coroutine.running()`) matches the
coroutine that the browser is waiting for. Remember this is only a problem when the browser is the source of the request,
if the request came from a script-message, or from a loadlist command there are no issues.
### The List Array
The list array must be made up of item_tables, which contain details about each item in the directory.
Each item has the following members:
| key | type | required | description |
|-------------|-----------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------|
| name | string | yes | name of the item, and the string to append after the directory when opening a file/folder |
| type | string | yes | determines whether the item is a file ("file") or directory ("dir") |
| label | string | no | an alternative string to print to the screen instead of name |
| ass | string | no | a string to print to the screen without escaping ass styling - overrides label and name |
| path | string | no | opening the item uses this full path instead of appending directory and name |
| redirect | bool | no | whether `path` should redirect the browser when opening a directory - default yes (nil counts as true) |
| mpv_options | string or table | no | a list of options to be sent to mpv when loading the file - can be in the form `opt1=value1,opt2=value2,...` or a table of string keys and values |
File-browser expects that `type` and `name` will be set for each item, so leaving these out will probably crash the script.
File-browser also assumes that all directories end in a `/` when appending name, and that there will be no backslashes.
The API function [`fix_path`](#utility-functions) can be used to ensure that paths conform to file-browser rules.
Here is an example of a static list table being returned by the `parse` method.
This would allow one to specify a custom list of items.
```lua
function parser:parse(directory, state)
local list = {
{ name = "first/", type = "dir" },
{ name = "second/", type = "dir" },
{ name = "third/", type = "dir" },
{ name = "file%01", type = "file", label = "file1" },
{ name = "file2", type = "file", path = "https://youtube.com/video" },
}
return list
end
```
### The Opts Table
The options table allows scripts to better control how they are handled by file-browser.
None of these values are required, and the opts table can even left as nil when returning.
| key | type | description |
|-----------------|---------|------------------------------------------------------------------------------------------------------------------------------|
| filtered | boolean | if true file-browser will not run the standard filter() function on the list |
| sorted | boolean | if true file-browser will not sort the list |
| directory | string | changes the browser directory to this - used for redirecting to other locations |
| directory_label | string | display this label in the header instead of the actual directory - useful to display encoded paths |
| empty_text | string | display this text when the list is empty - can be used for error messages |
| selected_index | number | the index of the item on the list to select by default - a.k.a. the cursor position |
| id | number | id of the parser that successfully returns a list - set automatically, but can be set manually to take ownership (see defer) |
The previous static example, but modified so that file browser does not try to filter or re-order the list:
```lua
function parser:parse(directory, state)
local list = {
{ name = "first/", type = "dir" },
{ name = "second/", type = "dir" },
{ name = "third/", type = "dir" },
{ name = "file%01", type = "file", label = "file1" },
{ name = "file2", type = "file", path = "https://youtube.com/video" },
}
return list, { sorted = true, filtered = true }
end
```
`id` is used to declare ownership of a page. The name of the parser that has ownership is used for custom-keybinds parser filtering.
When using `defer` id will be the id of whichever parser first returned a list.
This is the only situation when a parser may want to set id manually.
## Priority Suggestions
Below is a table of suggested priority ranges:
| Range | Suggested Use | Example parsers |
|---------|------------------------------------------------------------------------------------------------|------------------------------------------------|
| 0-20 | parsers that purely modify the results of other parsers | [m3u-fixer](m3u-browser.lua) |
| 21-40 | virtual filesystems which need to link to the results of other parsers | [favourites](favourites.lua) |
| 41-50 | to support specific sites or systems which can be inferred from the path | |
| 51-80 | limitted support for specific protocols which requires complex parsing to verify compatability | [apache](apache-browser.lua) |
| 81-90 | parsers that only need to modify the results of full parsers | [home-label](home-label.lua) |
| 91-100 | use for parsers which fully support a non-native protocol with absolutely no overlap | [ftp](ftp-browser.lua), [m3u](m3u-browser.lua) |
| 101-109 | replacements for the native file parser or fallbacks for the full parsers | [powershell](powershell.lua) |
| 110 | priority of the native file parser - don't use | |
| 111+ | fallbacks for native parser - potentially alternatives to the default root | |
## Keybinds
Addons have the ability to set custom keybinds using the `keybinds` field in the `parser` table. `keybinds` must be an array of tables, each of which may be in two forms.
Firstly, the keybind_table may be in the form
`{ "key", "name", [function], [flags] }`
where the table is an array whose four values corresond to the four arguments for the [mp.add_key_binding](https://mpv.io/manual/master/#lua-scripting-[,flags]]\)) API function.
```lua
local function main(keybind, state, co)
-- deletes files
end
parser.keybinds = {
{ "Alt+DEL", "delete_files", main, {} },
}
```
Secondly, the keybind_table may use the same formatting as file-browser's [custom-keybinds](../custom-keybinds.md).
Using the array form is equivalent to setting `key`, `name`, `command`, and `flags` of the custom-keybind form, and leaving everything else on the defaults.
```lua
parser.keybinds = {
{
key = "Alt+DEL",
name = "delete_files",
command = {"run", "rm", "%F"},
filter = "files"
}
}
```
These keybinds are evaluated only once shortly after the addon is loaded, they cannot be modified dynamically during runtime.
Keybinds are applied after the default keybinds, but before the custom keybinds. This means that addons can overwrite the
default keybinds, but that users can ovewrite addon keybinds. Among addons, those with higher priority numbers have their keybinds loaded before those
with lower priority numbers.
Remember that a lower priority value is better, they will overwrite already loaded keybinds.
Keybind passthrough works the same way, though there is some custom behaviour when it comes to [raw functions](#keybind-functions).
### Keybind Names
In either form the naming of the function is different from custom keybinds. Instead of using the form `file_browser/dynamic/custom/[name]`
they use the form `file_browser/dynamic/[parser_ID]/[name]`, where `[parser_id]` is a unique string ID for the parser, which can be retrieved using the
`parser:get_id()` method.
### Native Functions vs Command Tables
There are two ways of specifying the behaviour of a keybind.
It can be in command table form, as done when using custom-keybind syntax, and it can be done in
native function form, as done when using the `mp.add_key_binding` syntax.
However, these two ways of specifying commands are independant of how the overall keybind is defined.
What this means is that the command field of the custom-keybinds syntax can be an array, and the
3rd value in the array syntax can be a table of mpv commands.
```lua
local function main(keybind, state, co)
-- deletes files
end
-- this is a valid keybind table
parser.keybinds = {
{ "Alt+DEL", "delete_files", {"run", "rm", "%F"}, {} },
{
key = "Alt+DEL",
name = "delete_files",
command = main
}
}
```
There are some limitations however, not all custom-keybind options are supported when using native functions.
The supported options are: `key`, `name`, `condition`, `flags`, `parser`, `passthrough`. The other options can be replicated manually (see below).
### Keybind Functions
This section details the use of keybind functions.
#### Function Call
If one uses the raw function then the functions are called directly in the form:
`fn(keybind, state, coroutine)`
Where `keybind` is the keybind_table of the key being run, `state` is a table of state values at the time of the key press, and `coroutine` is the coroutine object
that the keybind is being executed inside.
The `keybind` table uses the same fields as defined
in [custom-keybinds.md](../custom-keybinds.md). Any random extra fields placed in the original
`file-browser-keybinds.json` will likely show up as well (this is not guaranteed).
Note that even if the array form is used, the `keybind` table will still use the custom-keybind format.
The entire process of running a keybind is handled with a coroutine, so the addon can safely pause and resume the coroutine at will. The `state` table is provided to
allow addons to keep a record of important state values that may be changed during a paused coroutine.
#### State Table
The state table contains copies of the following values at the time of the key press.
| key | description |
|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
| directory | the current directory |
| directory_label | the current directory_label - can (and often will) be `nil` |
| list | the current list_table |
| selected | index of the currently selected list item |
| selection | table of currently selected items (for multi-select) - in the form { index = true, ... } - always available even if the `multiselect` flag is not set |
| parser | a copy of the parser object that provided the current directory |
The following example shows the implementation of the `delete_files` keybind using the state values:
```lua
local fb = require "file-browser" -- see #api-functions and #utility-functions
local function main(keybind, state, co)
for index, item in state.list do
if state.selection[index] and item.type == "file" then
os.remove( fb.get_full_path(item, state.directory) )
end
end
end
parser.keybinds = {
{ "Alt+DEL", "delete_files", main, {} },
}
```
#### Passthrough
If the `passthrough` field of the keybind_table is set to `true` or `false` then file-browser will
handle everything. However, if the passthrough field is not set (meaning the bahviour should be automatic)
then it is up to the addon to ensure that they are
correctly notifying when the operation failed and a passthrough should occur.
In order to tell the keybind handler to run the next priority command, the keybind function simply needs to return the value `false`,
any other value (including `nil`) will be treated as a successful operation.
The below example only allows removing files from the `/tmp/` directory and allows other
keybinds to run in different directories:
```lua
local fb = require "file-browser" -- see #api-functions and #utility-functions
local function main(keybind, state, co)
if state.directory ~= "/tmp/" then return false end
for index, item in state.list do
if state.selection[index] and item.type == "file" then
os.remove( fb.get_full_path(item, state.directory) )
end
end
end
parser.keybinds = {
{ "Alt+DEL", "delete_files", main, {} },
}
```
## The API
The API is available through a module, which can be loaded with `require "file-browser"`.
The API provides a variety of different values and functions for an addon to use
in order to make them more powerful.
Function definitions are written using Typescript-style type annotations.
```lua
local fb = require "file-browser"
local parser = {
priority = 100,
}
function parser:setup()
fb.register_root_item("Example/")
end
return parser
```
### Parser API
In addition to the standard API there is also an extra parser API that provides
several parser specific methods, listed below using `parser:method` instead of `fb.function`.
This API is added to the parser object after it is loaded by file-browser,
so if a script wants to call them immediately on load they must do so in the `setup` method.
All the standard API functions are also available in the parser API.
```lua
local parser = {
priority = 100,
}
function parser:setup()
-- same operations
self.insert_root_item({ name = "Example/", type = "dir" })
parser.insert_root_item({ name = "Example/", type = "dir" })
end
-- will not work since the API hasn't been added to the parser yet
parser.insert_root_item({ name = "Example/", type = "dir" })
return parser
```
### General Functions
#### `fb.API_VERSION: string`
The current API version in use by file-browser.
#### `fb.add_default_extension(ext: string): void`
Adds the given extension to the default extension filter whitelist. Can only be run inside the `setup()` method.
#### `fb.browse_directory(directory: string, open_browser: bool = true): coroutine`
Clears the cache and opens the given directory in the browser.
If the `open_browser` argument is truthy or `nil` then the browser will be opened
if it is currently closed. If `open_browser` is `false` then the directory will
be opened in the background.
Returns the coroutine of the upcoming parse operation. The parse is queued and run when the script thread next goes idle,
allowing one to store this value and use it to identify the triggered parse operation.
This is the equivalent of calling the `browse-directory` script-message.
#### `fb.insert_root_item(item: item_table, pos?: number): void`
Add an item_table to the root list at the specified position. If `pos` is nil then append to the end of the root.
`item` must be a valid item_table of `type='dir'`.
#### `fb.register_directory_mapping(directory: string | nil, mapping: string, pattern?: bool): void`
Creates a directory mapping for the given directory. A directory mapping is a
one-way mapping from an external directory string, to an internal directory
within file-browser's directory tree. It allows external paths that may not
exist within file-browser's tree to be mapped to a location that is.
Internally, this is used by file-browser to map the `bd://`, `dvd://`, and `cdda://`
protocol paths to their respective device locations in the filesystem.
Note that as this is still an experimental feature, the exact situations when mappings
are resolved is subject to change. Currently, mapping occurs only when
receiving a directory from an external source, such as the mpv `path` property,
or the `browse-directory` script message.
`directory` is a string that represents a location within file-browser's file-system.
`mapping` is a string that will be replaced by the `directory` string if found in a path:
```lua
fb.register_directory_mapping('/dev/dvd', 'dvd://')
fb.resolve_directory_mapping('dvd://1') -- /dev/dvd/1
```
There can only be one `directory` string associated with each unique `mapping` string,
but multiple mappings can point to the same directory.
If `directory` is set to `nil` then the existing mapping for `mapping` will be removed.
If `pattern` is set to true, then `mapping` will be treated as a Lua
pattern. Any part of an input path that matches the pattern will be substituted for
the `directory` string.
```lua
fb.register_directory_mapping('/dev/dvd', '^dvd://.*', true)
fb.resolve_directory_mapping('dvd://1') -- /dev/dvd
```
When `pattern` is falsy, `mapping` is equivalent to `'^'..fb.pattern_escape(mapping)`.
Captures in the pattern may be given extra behaviour in the future.
#### `fb.register_parseable_extension(ext: string): void`
Register a file extension that the browser will attempt to open, like a directory - for addons which can parse files such
as playlist files.
#### `fb.register_root_item(item: string | item_table, priority?: number): boolean`
Registers an item to be added to the root and an optional priority value that determines the position relative to other items (default is 100).
A lower priority number is better, meaning they will be placed earlier in the list.
Only adds the item if it is not already in the root and returns a boolean that specifies whether or not the item was added.
Must be called during or after the `parser:setup()` method is run.
If `item` is a string then a new item_table is created with the values: `{ type = 'dir', name = item }`.
If `item` is an item_table then it must be a valid directory item.
Use [`fb.fix_path(name, true)`](#fbfix_pathpath-string-is_directory-boolean-string) to ensure the name field is correct.
This function should be used over the older `fb.insert_root_item`.
#### `fb.remove_parseable_extension(ext: string): void`
Remove a file extension that the browser will attempt to open like a directory.
#### `fb.parse_directory(directory: string, parse?: parse_state_table): (list_table, opts_table) | nil`
Starts a new scan for the given directory and returns a list_table and opts_table on success and `nil` on failure.
Must be called from inside a [coroutine](#coroutines).
This function allows addons to request the contents of directories from the loaded parsers. There are no protections
against infinite recursion, so be careful about calling this from within another parse.
Do not use the same `parse` table for multiple parses, state values for the two operations may intefere with each other
and cause undefined behaviour. If the `parse.source` field is not set then it will be set to `"addon"`.
Note that this function is for creating new parse operations, if you wish to create virtual directories or modify
the results of other parsers then use [`defer`](#parserdeferdirectory-string-list_table-opts_table--nil).
Also note that every parse operation is expected to have its own unique coroutine. This acts as a unique
ID that can be used internally or by other addons. This means that if multiple `parse_directory` operations
are run within a single coroutine then file-browser will automatically create a new coroutine for the scan,
which hands execution back to the original coroutine upon completion.
#### `parser:register_root_item(item: string | item_table, priority?: number): boolean`
A wrapper around [`fb.register_root_item`](#fbregister_root_itemitem-string--item_table-priority-number-boolean)
which uses the parser's priority value if `priority` is undefined.
#### `fb.remove_all_mappings(directory: string): string[]`
Removes all [directory mappings](#fbregister_directory_mappingdirectory-string--nil-mapping-string-pattern-bool-void)
that resolve to the given `directory`. Returns a list of the `mapping` strings
that were removed.
### Advanced Functions
#### `fb.clear_cache(): void`
Clears the directory cache. Use this if you are modifying the contents of directories other
than the current one to ensure that their contents will be rescanned when next opened.
#### `fb.coroutine.assert(err?: string): coroutine`
Throws an error if it is not called from within a coroutine. Returns the currently running coroutine on success.
The string argument can be used to throw a custom error string.
#### `fb.coroutine.callback(time_limit?: number): function`
Creates and returns a callback function that resumes the current coroutine.
This function is designed to help streamline asynchronous operations. The best way to explain is with an example:
```lua
local function execute(args)
mp.command_native_async({
name = "subprocess",
playback_only = false,
capture_stdout = true,
capture_stderr = true,
args = args
}, fb.coroutine.callback())
local _, cmd = coroutine.yield()
return cmd.status == 0 and cmd.stdout or nil
end
```
This function uses the mpv [subprocess](https://mpv.io/manual/master/#command-interface-subprocess)
command to execute some system operation. To prevent the whole script (file-browser and all addons) from freezing
it uses the [command_native_async](https://mpv.io/manual/master/#lua-scripting-mp-command-native-async(table-[,fn])) command
to execute the operation asynchronously and takes a callback function as its second argument.
`coroutine.callback())` will automatically create a callback function to resume whatever coroutine ran the `execute` function.
Any arguments passed into the callback function (by the async function, not by you) will be passed on to the resume;
in this case `command_native_async` passes three values into the callback, of which only the second is of interest to me.
If `time_limit` is set to a number, then a boolean is passed as the first resume argument to the coroutine.
If the callback is not run within `time_limit` seconds then the coroutine will be resumed, and the first
argument will be `false`. If the callback is run within the time limit then the first argument will be `true`.
```lua
local function execute(args)
local t = mp.command_native_async({
name = "subprocess",
playback_only = false,
capture_stdout = true,
capture_stderr = true,
args = args
}, fb.coroutine.callback(10))
local success, _, cmd = coroutine.yield()
if not success then
mp.abort_async_command(t)
msg.error("command timed out")
return nil
end
return cmd.status == 0 and cmd.stdout or nil
end
```
The expectation is that the programmer will yield execution before that callback returns. In this example I
yield immediately after running the async command.
If you are doing this during a parse operation you could also substitute `coroutine.yield()` with `parse_state:yield()` to abort the parse if the user changed
browser directories during the asynchronous operation.
If you have no idea what I've been talking about read the [Lua manual on coroutines](https://www.lua.org/manual/5.1/manual.html#2.11).
#### `fb.coroutine.resume_catch(co: coroutine, ...): (boolean, ...)`
Runs `coroutine.resume(co, ...)` with the given coroutine, passing through any additional arguments.
If the coroutine throws an error then an error message and stacktrace is printed to the console.
All the return values of `coroutine.resume` are caught and returned.
#### `fb.coroutine.resume_err(co: coroutine, ...): boolean`
Runs `coroutine.resume(co, ...)` with the given coroutine, passing through any additional arguments.
If the coroutine throws an error then an error message and stacktrace is printed to the console.
Returns the success boolean returned by `coroutine.resume`, but drops all other return values.
#### `fb.coroutine.run(fn: function, ...): void`
Runs the given function in a new coroutine, passing through any additional arguments.
#### `fb.coroutine.queue(fn: function, ...): coroutine`
Runs the given function in a coroutine when the script next goes idle, passing through
any additional arguments. The (not yet started) coroutine is returned by the function.
#### `fb.rescan(): coroutine`
Rescans the current directory. Equivalent to Ctrl+r without the cache refresh for higher level directories.
Returns the coroutine of the upcoming parse operation. The parse is queued and run when the script thread next goes idle,
allowing one to store this value and use it to identify the triggered parse operation.
#### `fb.redraw(): void`
Forces a redraw of the browser UI.
#### `parser:defer(directory: string): (list_table, opts_table) | nil`
Forwards the given directory to the next valid parser. For use from within a parse operation.
The `defer` function is very powerful, and can be used by scripts to create virtual directories, or to modify the results of other parsers.
However, due to how much freedom Lua gives coders, it is impossible for file-browser to ensure that parsers are using defer correctly, which can cause unexpected results.
The following are a list of recommendations that will increase the compatability with other parsers:
* Always return the opts table that is returned by defer, this can contain important values for file-browser, as described [above](#the-opts-table).
* If required modify values in the existing opts table, don't create a new one.
* Respect the `sorted` and `filtered` values in the opts table. This may mean calling `sort` or `filter` manually.
* Think about how to handle the `directory_label` field, especially how it might interract with any virtual paths the parser may be maintaining.
* Think about what to do if the `directory` field is set.
* Think if you want your parser to take full ownership of the results of `defer`, if so consider setting `opts.id = self:get_id()`.
* Currently this only affects custom keybind filtering, though it may be changed in the future.
The [home-label](https://github.com/CogentRedTester/mpv-file-browser/blob/master/addons/home-label.lua)
addon provides a good simple example of the safe use of defer. It lets the normal file
parser load the home directory, then modifies the directory label.
```lua
local mp = require "mp"
local fb = require "file-browser"
local home = fb.fix_path(mp.command_native({"expand-path", "~/"}), true)
local home_label = {
api_version = '1.0.0',
priority = 100
}
function home_label:can_parse(directory)
return directory:sub(1, home:len()) == home
end
function home_label:parse(directory, ...)
local list, opts = self:defer(directory, ...)
if (not opts.directory or opts.directory == directory) and not opts.directory_label then
opts.directory_label = "~/"..(directory:sub(home:len()+1) or "")
end
return list, opts
end
return home_label
```
### Utility Functions
#### `fb.ass_escape(str: string, substitute_newline?: true | string): string`
Returns the `str` string with escaped ass styling codes.
The optional 2nd argument allows replacing newlines with the given string, or `'\\n'` if set to `true`.
#### `fb.copy_table(t: table, depth?: number): table`
Returns a copy of table `t`.
The copy is done recursively to the given `depth`, and any cyclical table references are maintained.
Both keys and values are copied. If `depth` is undefined then it defaults to `math.huge` (infinity).
Additionally, the original table is stored in the `__original` field of the copy's metatable.
The copy behaviour of the metatable itself is subject to change, but currently it is not copied.
#### `fb.evaluate_string(str: string, chunkname?: string, env?: table, defaults?: bool = true): unknown`
Loads `str` as a chunk of Lua statement(s) and runs them, returning the result.
Errors are propagated to the caller. `chunkname` is used
for debug output and error messages.
Each chunk has a separate global environment table that inherits
from the main global table. This means new globals can be created safely,
but the default globals can still be accessed. As such, this method
cannot and should not be used for security or sandboxing.
A custom environment table can be provided with the `env` argument.
Inheritance from the global table is disabled if `defaults` is `false`.
Examples:
```lua
fb.evaluate_string('return 5 + 5') -- 10
fb.evaluate_string('x = 20 ; return x * x') -- 400
local code = [[
local arr = {1, 2, 3, 4}
table.insert(arr, x)
return unpack(arr)
]]
fb.evaluate_string(code, 'test3', {x = 5}) -- 1, 2, 3, 4, 5
fb.evaluate_string(code, 'test4', nil, false) -- Lua error: [string "test4"]:2: attempt to index global 'table' (a nil value)
```
In an expression the `mp`, `mp.msg`, and `mp.utils` modules are available as `mp`, `msg`, and `utils` respectively.
Additionally, in mpv v0.38+ the `mp.input` module is available as `input`.
This addon API is available as `fb` and if [mpv-user-input](https://github.com/CogentRedTester/mpv-user-input)
is installed then user-input will be available in `user_input`.
These modules are all unavailable if `defaults` is `false`.
#### `fb.filter(list: list_table): list_table`
Iterates through the given list and removes items that don't pass the user set filters
(dot files/directories and valid file extensions).
Returns the list but does not create a copy; the `list` table is filtered in-place.
#### `fb.fix_path(path: string, is_directory?: boolean): string`
Takes a path and returns a file-browser compatible path string.
The optional second argument is a boolean that tells the function to format the path to be a
directory.
#### `fb.get_extension(filename: string, def?: any): string | def`
Returns the file extension for the string `filename`, or `nil` if there is no extension.
If `def` is defined then that is returned instead of `nil`.
The full stop is not included in the extension, so `test.mkv` will return `mkv`.
#### `fb.get_full_path(item: item_table, directory?: string): string`
Takes an item table and returns the item's full path assuming it is in the given directory.
Takes into account `item.name`/`item.path` fields, etc.
If directory is nil then it uses the currently open directory.
#### `fb.get_protocol(url: string, def?: any): string | def`
Returns the protocol scheme for the string `url`, or `nil` if there is no scheme.
If `def` is defined then that is returned instead of `nil`.
The `://` is not included, so `https://example.com/test.mkv` will return `https`.
#### `fb.iterate_opt(opts: string): iterator`
Takes an options string consisting of a list of items separated by the `root_separators` defined in `file_browser.conf` and
returns an iterator function that can be used to iterate over each item in the list.
```lua
local opt = "a,b,zz z" -- root_separators=,
for item in fb.iterate_opt(opt) do
print(item) -- prints: 'a', 'b', 'zz z'
end
```
#### `fb.join_path(p1: string, p2: string): string`
A wrapper around [`mp.utils.join_path`](https://mpv.io/manual/master/#lua-scripting-utils-join-path(p1,-p2))
which treats paths with network protocols as absolute paths.
#### `fb.pattern_escape(str: string): string`
Returns `str` with Lua special pattern characters escaped.
#### `fb_utils.resolve_directory_mapping(path: string): string`
Takes a `path` string and resolves any
[directory mappings](#fbregister_directory_mappingdirectory-string--nil-mapping-string-pattern-bool-void),
replacing any substrings that match a mapping with the associated directory.
Only the first valid mapping is applied, but this behaviour will likely change in
the future. Changes to this behaviour will not consitute a major version bump so should not
be relied upon.
#### `fb.sort(list: list_table): list_table`
Iterates through the given list and sorts the items using file-browser's sorting algorithm.
Returns the list but does not create a copy; the `list` table is sorted in-place.
#### `fb.valid_file(name: string): boolean`
Tests if the string `name` passes the user set filters for valid files (extensions/dot files/etc).
#### `fb.valid_dir(name: string): boolean`
Tests if the string `name` passes the user set filters for valid directories (dot folders/etc).
### Getters
These functions allow addons to safely get information from file-browser.
All tables returned by these functions are copies sent through the [`fb.copy_table`](#fbcopy_tablet-table-depth-number-table)
function to ensure addons can't accidentally break things.
#### `fb.get_audio_extensions(): table`
Returns a set of extensions like [`fb.get_extensions`](#fbget_extensions-table) but for extensions that are opened
as additional audio tracks.
All of these are included in `fb.get_extensions`.
#### `fb.get_current_file(): table`
A table containing the path of the current open file in the form:
`{directory = "/home/me/", name = "bunny.mkv", path = "/home/me/bunny.mkv"}`.
#### `fb.get_current_parser(): string`
The unique id of the parser that successfully parsed the current directory.
#### `fb.get_current_parser_keyname(): string`
The `keybind_name` of the parser that successfully parsed the current directory.
Used for custom-keybind filtering.
#### `fb.get_directory(): string`
The current directory open in the browser.
#### `fb.get_dvd_device(): string`
The current dvd-device as reported by mpv's `dvd-device` property.
Formatted to work with file-browser.
#### `fb.get_extensions(): table`
Returns the set of valid extensions after applying the user's whitelist/blacklist options.
The table is in the form `{ mkv = true, mp3 = true, ... }`.
Sub extensions, audio extensions, and parseable extensions are all included in this set.
#### `fb.get_list(): list_table`
The list_table currently open in the browser.
#### `fb.get_open_status(): boolean`
Returns true if the browser is currently open and false if not.
#### `fb.get_opt(name: string): string | number | boolean`
Returns the script-opt with the given name.
#### `fb.get_parsers(): table`
Returns a table of all the loaded parsers/addons.
The formatting of this table in undefined, but it should
always contain an array of the parsers in order of priority.
#### `fb.get_parse_state(co?: coroutine): parse_state_table`
Returns the [parse_state table](#parse-state-table) for the given coroutine.
If no coroutine is given then it uses the running coroutine.
Every parse operation is guaranteed to have a unique coroutine.
#### `fb.get_parseable_extensions(): table`
Returns a set of extensions like [`fb.get_extensions`](#fbget_extensions-table) but for extensions that are
treated as parseable by the browser.
All of these are included in `fb.get_extensions`.
#### `fb.get_root(): list_table`
Returns the root table.
#### `fb.get_script_opts(): table`
The table of script opts set by the user. This currently does not get
changed during runtime, but that is not guaranteed for future minor version increments.
#### `fb.get_selected_index(): number`
The current index of the cursor.
Note that it is possible for the cursor to be outside the bounds of the list;
for example when the list is empty this usually returns 1.
#### `fb.get_selected_item(): item_table | nil`
Returns the item_table of the currently selected item.
If no item is selected (for example an empty list) then returns nil.
#### `fb.get_state(): table`
Returns the current state values of the browser.
These are not documented and are subject to change at any time,
adding a proper getter for anything is a valid request.
#### `fb.get_sub_extensions(): table`
Returns a set of extensions like [`fb.get_extensions`](#fbget_extensions-table) but for extensions that are opened
as additional subtitle tracks.
All of these are included in `fb.get_extensions`.
#### `parser:get_id(): string`
The unique id of the parser. Used for log messages and various internal functions.
#### `parser:get_index(): number`
The index of the parser in order of preference (based on the priority value).
`defer` uses this internally.
### Setters
#### `fb.set_selected_index(pos: number): number | false`
Sets the cursor position and returns the new index.
If the input is not a number return false, if the input is out of bounds move it in bounds.
## Examples
For standard addons that add support for non-native filesystems, but otherwise don't do anything fancy, see [ftp-browser](ftp-browser.lua) and [apache-browser](apache-browser.lua).
For more simple addons that make a few small modifications to how other parsers are displayed, see [home-label](home-label.lua).
For more complex addons that maintain their own virtual directory structure, see
[favourites](favourites.lua).