Write a plugin
Two kinds: compiled-in Rust plugins with full access to the UI, and mpv user scripts (Lua or JavaScript) that need no rebuild.
- Architecture
- Manifest format
- A Rust plugin step by step
- Hooks and context
- Adding UI
- mpv script plugins
- Package & publish a downloadable plugin
- Listing in the catalog
Architecture
Plugins live in src/plugins/. Each one is a type implementing the Plugin
trait and is listed in plugins::builtin(). The Registry owns them, keeps
their enabled flag and settings in ~/.config/vivi/config.toml under
[plugins.<id>], and calls the enabled ones from the UI thread:
mpv events ─▶ main.rs ─▶ plugins::file_loaded() ─▶ Plugin::file_loaded
timer (100 ms) ────────▶ Plugin::tick
Settings → Extensions ─▶ plugins::setting_changed() ─▶ enable / disable / setting_changed / command
Slint callback plugin-command(id, name, args) ─▶ Plugin::command
The Settings → Extensions page is generated from the manifests: one card with an ON/OFF toggle per plugin, followed by its settings while it is enabled. Nothing else needs to be registered.
Manifest format
| Field | Type | Meaning |
|---|---|---|
id | string | Stable id, lowercase with dashes (skip-intro). Used as the config key. |
name | string | Display name. |
description | string | One sentence, shown on the card. |
version | string | Semver; built-ins use the Vivi version. |
author | string | Name or handle. |
icon | string | Icon key: visualizer, skip, lyrics, subtitles, script; anything else shows the generic plugin icon (see PluginIcons in ui/settings.slint). |
settings | list | Setting specs, below. |
default_enabled | bool | State before the user touches the toggle. |
Setting specs have a key, title, info (tooltip), a kind and a
default. Values are always strings ("true"/"false" for toggles).
| Kind | Constructor | Settings row |
|---|---|---|
| toggle | SettingSpec::toggle(key, title, info, default) | ON/OFF toggle |
| choice | SettingSpec::choice(key, title, info, &[options]) | Dropdown; the first option is the default |
| text | SettingSpec::text(key, title, info, placeholder, default) | Text field |
| secret | SettingSpec::secret(key, title, info) | Hidden text field (API keys) |
| action | SettingSpec::action(key, title, info, label) | Button; runs Plugin::command(cx, key, &[]) |
In config they look like this:
[plugins.subtitle-search]
enabled = true
[plugins.subtitle-search.settings]
api-key = "…"
languages = "en,uk"
A Rust plugin step by step
1. Create src/plugins/hello.rs:
use super::{Ctx, Manifest, Plugin, SettingSpec};
#[derive(Default)]
pub struct Hello;
impl Plugin for Hello {
fn manifest(&self) -> Manifest {
Manifest::builtin("hello", "Hello", "Greets every file you open.", "plugins")
.setting(SettingSpec::text("greeting", "Greeting", "", "Hello", "Hello"))
}
fn file_loaded(&mut self, cx: &Ctx) {
let title = cx.player.get("media-title").unwrap_or_default();
cx.toast(&format!("{} {title}", cx.get("greeting")));
}
}
2. Add pub mod hello; to src/plugins/mod.rs and
Box::new(hello::Hello) to builtin().
3. Add an entry to plugins/index.json (a unit test checks that every built-in is listed with
the same name, icon and setting keys), then run cargo test.
Hooks and context
| Hook | When |
|---|---|
enable(cx) | At startup if enabled, and when switched on (followed by file_loaded). |
disable(cx) | When switched off (and at startup if disabled). Clear your UI here. |
file_loaded(cx) | mpv loaded a new file (tracks, chapters and metadata are available). |
tick(cx) | Every 100 ms while enabled. Keep it cheap; poll channels from worker threads here. |
setting_changed(cx, key) | The user changed one of your settings (already saved). |
command(cx, name, args) | An action button, or a UI callback sent with plugin-command. |
Ctx gives you cx.app (the Slint AppWindow), cx.player
(mpv: get, set, cmd, seek_abs, chapters(),
mpv.get_property), cx.get(key) / cx.flag(key) / cx.set(key, value)
for your settings, cx.toast(msg) and cx.local_path().
Hooks run on the UI thread. Do network and disk work on a std::thread and send the
result back through an mpsc channel that tick polls (see lyrics.rs
and subtitles.rs).
Adding UI
Put components in ui/plugins.slint, add properties to AppWindow in
ui/app.slint, and set them from your hooks. To call back into the plugin, use the shared callback:
clicked => { root.plugin-command("hello", "wave", ""); }
which arrives as command(cx, "wave", &[""]). Join several arguments with U+001F.
mpv script plugins
Any mpv Lua (.lua) or JavaScript (.js) script, or a directory with
main.lua / main.js, placed in ~/.config/vivi/scripts/ becomes an
external plugin with its own card and toggle. Enabled scripts are loaded with mpv's
load-script command at startup (unless “Load add-ons on startup” is off) or as soon as you
switch them on. mpv cannot unload a script, so switching one off takes effect after a restart.
Use Rescan to pick up new files without restarting.
Optional header comments fill in the card:
-- @name Now Playing OSD
-- @description Shows the title on mpv's OSD when a file starts
-- @version 1.0
-- @author Vivi
mp.register_event("file-loaded", function()
mp.osd_message(mp.get_property("media-title", ""), 3)
end)
JavaScript scripts use // @name …. Scripts run inside Vivi's mpv, so they see the same
properties and can bind commands, but mpv's own OSD is drawn under Vivi's UI and Vivi disables mpv's
default key bindings; keys Vivi does not use itself (and all Alt combinations) are forwarded to mpv, so bind them with mp.add_forced_key_binding. Your libmpv must be built with Lua / JavaScript support (most distributions are).
Package & publish a downloadable plugin
Downloadable plugins are mpv Lua (or JavaScript) scripts with a manifest.toml, installed
from Settings → Extensions → Browse. Why this format and how install, update and rollback work:
Plugin distribution.
- Create
plugins/<id>/withmanifest.toml, the entry script<id>.luaand optionally a README and more files. - Write the manifest:
id = "night-mode" name = "Night Mode" version = "1.0.0" # semver; bump it for every change description = "..." author = "You" kind = "mpv-lua" # or mpv-js entry = "night-mode.lua" # file name = mpv script name = id min_vivi = "0.8.0" icon = "night" # Settings icon key lucide = "moon" # docs icon (lucide.dev) permissions = [] # network, filesystem, subprocess [[settings]] key = "strength" title = "Strength" type = "choice" # toggle | choice | text | secret | action options = ["Medium", "Light", "Strong"] default = "Medium" - Read settings in the script. Vivi passes them as
script-optsnamed<id>-<key>and updates them live; toggles arrive asyes/no. Action settings arrive as script messages:
Vivi disables mpv's default bindings and forwards keys it does not use (and everylocal o = { strength = "Medium" } require("mp.options").read_options(o, "night-mode", function(changed) apply() end) mp.register_script_message("clear", clear) -- an action setting with key "clear" mp.add_forced_key_binding("Alt+n", "toggle", toggle)Altcombination) to mpv, so bind keys withmp.add_forced_key_binding; plainadd_key_bindingis ignored. - Declare honest permissions:
networkfor HTTP (e.g. viacurl),filesystemfor files outside the package folder,subprocessfor running programs. Users see them before installing. - Run
scripts/package-plugins.py. It validates the manifest, builds a deterministic zip indist/plugins/and writes the entry (URL, sha256, size) intoplugins/index.json. Test locally by copying the folder to~/.config/vivi/scripts/. - Commit and push (or open a pull request). The
Pluginsworkflow checks Lua syntax and the index, then publishes a releaseplugin-<id>-v<version>with the zip and verifies the uploaded asset's sha256. TheDocs siteworkflow publishes the new catalog; Vivi picks it up on its next daily check or on Check now.
A released version is immutable: change anything, bump version.
Listing in the catalog
The catalog is generated from plugins/index.json by
scripts/build-site.py. Entries use the manifest fields above plus kind
(builtin or script) and, for scripts, source (a path in the repository).
python3 scripts/build-site.py # builds _site/
python3 -m http.server -d _site