ci / rust-arm64 (pull_request) Successful in 1m44s
ci / web (pull_request) Successful in 1m3s
ci / bun-nix (pull_request) Successful in 29s
ci / docs-site (pull_request) Successful in 1m24s
ci / docs-drift (pull_request) Successful in 23s
ci / rust (pull_request) Successful in 12m33s
The category sacrificed, per page (RFC 'screenshots over prose' / phase-2 handoff item 3):
- client-settings.md (4368→3144 words): the why-rationale narrative around each setting; every
default, number, availability list, env var and link stays.
- virtual-displays.md (3724→3015): status/history notes (the 'What's live today' tracker
paragraph, 'Windows has always lingered 10 s') and design rationale; compositor-specific
operational facts stay.
- plugins.mdx (2835→2418): narrative duplication — restart instructions stated twice, story
asides the linked plugin repos carry.
- input.md (2499→2403) and wake-on-lan.md (2286→2186): rationale asides and historical notes
('the plugin used to fire a packet itself…'); these two are per-platform fact tables end to
end, so only a light pass was honest.
15712→13166 words over the five biggest Guides (−16%). Zero heading changes (anchors stable),
docs-drift + docs-links green, docs-site build + lint green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
358 lines
16 KiB
Plaintext
358 lines
16 KiB
Plaintext
---
|
|
title: Plugins
|
|
description: First-party plugins — bring each of your game launchers into the library, sync a ROM collection or Playnite library, or hand a real USB device on the couch to the host — and how to install them.
|
|
---
|
|
|
|
Plugins extend the host through the **scripting runner** (see [Events & hooks](/docs/automation)). A
|
|
plugin runs alongside the host, reconciles titles into your [game library](/docs/game-library) as a
|
|
provider — so they appear in the grid on every client — and can add its own page to the
|
|
[web console](/docs/web-console).
|
|
|
|
**Your game library is built from plugins.** From v0.28.0 the host scans no launcher on its own;
|
|
install the one for each launcher you use.
|
|
|
|
| Plugin | Host | What it does |
|
|
|---|---|---|
|
|
| **Steam** | Linux, Windows | Your installed Steam titles and non-Steam shortcuts, with Steam's own art, plus a Big Picture tile. |
|
|
| **Lutris** | Linux | Your Lutris games, with art, and a tile that opens Lutris itself. |
|
|
| **Heroic** | Linux | Everything Heroic manages — Epic, GOG and Amazon — plus a tile that opens Heroic. |
|
|
| **Epic Games Store** | Windows | Your installed Epic titles, from the launcher's own manifests. |
|
|
| **GOG** | Windows | Your installed GOG games, launched directly — Galaxy is never involved. |
|
|
| **Playnite** | Windows | Mirrors your [Playnite](https://playnite.link) library — every store and emulator it manages — into the library, launched back through Playnite. |
|
|
| **ROM Manager** | Linux, Windows | Scans your ROM directories, matches each platform to an installed emulator, and syncs them into the library with box art. |
|
|
| **VirtualHere** | Linux, Windows | Hands a real USB device on the couch — wheel, HOTAS, pad — to the host while you play, and gives it back after. Needs [VirtualHere](https://www.virtualhere.com/), sold separately. |
|
|
|
|
## Installing from the console
|
|
|
|
Every plugin runs inside the **plugin runner**, a separate service. Installing a plugin while it's
|
|
off succeeds and starts nothing, so check it first:
|
|
|
|
1. Open the [web console](/docs/web-console) → **Plugins** → **Installed** and look at the **Plugin
|
|
runner** card. *Not installed*: install the runner package first (see
|
|
[Troubleshooting](#troubleshooting)). *Disabled*: press **Enable runner** — once per host.
|
|
*Stopped*: the runner is enabled but not up, and its log says why.
|
|
2. Go to **Browse**, pick a plugin from the catalog and confirm. The host installs it and restarts
|
|
the runner, and the plugin's own page appears in the console's nav.
|
|
|
|
**Sources** is the third tab: where catalogs come from.
|
|
|
|
### What "Verified" means
|
|
|
|
Every catalogued plugin pins **one exact version** and its package hash. **Verified** means somebody
|
|
at unom reviewed *that exact package* — not the project in general; a new release keeps offering the
|
|
reviewed version until the new one is reviewed too. Before anything is downloaded, the host
|
|
re-checks the pinned hash against the registry, so a package quietly republished under the same
|
|
version number is refused.
|
|
|
|
A catalog can also **revoke** a version: the console shows the advisory and won't install it — a
|
|
red-ringed panel on **Browse**, a warning on **Installed** if you already have it. It never removes
|
|
running code for you.
|
|
|
|
The three badges:
|
|
|
|
| Badge | Where it came from |
|
|
|---|---|
|
|
| **Verified** | The built-in catalog. unom reviewed this exact package. |
|
|
| **External source**, *from <source>* | A catalog **you** added. Still pinned and hash-checked, but curated by somebody else — unom has not looked at the code. Amber, not the red **Unverified** below. |
|
|
| **Unverified** | Installed by hand from a package spec. Nobody reviewed it and nothing pins it; the console asks you to type the name to confirm, and the mark stays for as long as it is installed. |
|
|
|
|
### Adding another catalog
|
|
|
|
**Sources** → *Add a catalog source*: a name and the URL of its index. Optionally paste the source's
|
|
`ed25519:…` public key — with a key set, the host refuses any index from that source that isn't
|
|
correctly signed. Adding a source is a one-time trust decision: its plugins become installable,
|
|
always attributed to it, never carrying **Verified**.
|
|
|
|
To publish a plugin to the built-in catalog, open a pull request against
|
|
[`punktfunk-plugin-index`](https://git.unom.io/unom/punktfunk-plugin-index) — its README covers the
|
|
format and what review looks for.
|
|
|
|
## Installing from the CLI
|
|
|
|
<Tabs items={['Linux', 'Windows']}>
|
|
<Tab value="Linux">
|
|
|
|
```sh
|
|
punktfunk-host plugins add playnite # or: rom-manager
|
|
punktfunk-host plugins enable # turn the runner on (once)
|
|
```
|
|
|
|
On **SteamOS** the [host installer](/docs/steamos-host) ships the runner automatically (user-scoped
|
|
under `~/.local`). If the console reports the runner isn't installed on an older setup, re-run
|
|
`scripts/steamdeck/update.sh` once.
|
|
|
|
</Tab>
|
|
<Tab value="Windows">
|
|
|
|
Run these from an **elevated** PowerShell — the plugins directory lives under
|
|
`%ProgramData%\punktfunk`, which is admin-owned. The runner task itself runs as the low-privilege
|
|
`NT AUTHORITY\LocalService` account; `plugins enable` sets that up (including read access to the
|
|
runner's scoped API token).
|
|
|
|
```powershell
|
|
punktfunk-host plugins add playnite # or: rom-manager
|
|
punktfunk-host plugins enable # turn the runner on (once)
|
|
```
|
|
|
|
If `punktfunk-host` isn't found, open a **new** terminal, or use the full path:
|
|
`& "$env:ProgramFiles\punktfunk\punktfunk-host.exe" plugins add playnite`.
|
|
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
The plugin's page then appears in the [web console](/docs/web-console)'s nav automatically.
|
|
|
|
The runner is **on by default** on a new install (a host that predates this stays however you left
|
|
it — `punktfunk-host plugins enable`, once). The runner discovers plugins when it starts, so one
|
|
installed later needs a runner restart — `systemctl --user restart punktfunk-scripting` on Linux,
|
|
`Restart` the `PunktfunkScripting` task on Windows; the console does that restart for you as part
|
|
of installing.
|
|
|
|
Don't want it? `systemctl --user mask punktfunk-scripting` on Linux, or disable the
|
|
`PunktfunkScripting` scheduled task on Windows. The host keeps streaming; you lose plugin-provided
|
|
game sources and automation.
|
|
|
|
A plugin installed from the CLI shows in the console as **Installed via CLI** — the console knows
|
|
what is installed, not who vouched for it. Install the same plugin from **Browse** and it carries
|
|
its catalog badge instead.
|
|
|
|
### The rest of the commands
|
|
|
|
| Command | What it does |
|
|
|---|---|
|
|
| `punktfunk-host plugins add <name…>` | Install one or more plugins. |
|
|
| `punktfunk-host plugins remove <name…>` | Uninstall. |
|
|
| `punktfunk-host plugins list` | List what's installed, with versions. |
|
|
| `punktfunk-host plugins enable` | Enable + start the runner. |
|
|
| `punktfunk-host plugins disable` | Stop + disable the runner. |
|
|
| `punktfunk-host plugins status` | Is the runner enabled and running? |
|
|
|
|
A bare name resolves to the first-party package — `playnite` installs `@punktfunk/plugin-playnite`,
|
|
always from Punktfunk's own package registry. Any other name (`punktfunk-plugin-*`, a foreign
|
|
`@scope/pkg`) would install from the **public npm registry** and is refused unless you add
|
|
`--allow-public-registry` — a guard against typos and look-alike packages.
|
|
|
|
> Plugins are operator-installed code with operator privileges — they can launch games and run
|
|
> commands. Install only plugins you trust, from a registry you control.
|
|
|
|
## Updating and removing a plugin
|
|
|
|
**Update.** The **Installed** tab grows an **Update to <version>** button whenever the catalog
|
|
pins a newer version; it installs through the same confirmation and restarts the runner for you.
|
|
There is no `plugins update` command — from a terminal, re-run `punktfunk-host plugins add <name>`,
|
|
which installs the newest version the *registry* has (not the catalog pin) and does **not** restart
|
|
the runner: restart it yourself so the new code is picked up.
|
|
|
|
**Remove.** The **Uninstall** (bin) button removes the package *and* restarts the runner.
|
|
`punktfunk-host plugins remove <name>` removes the package only — restart the runner yourself to
|
|
stop a plugin still running.
|
|
|
|
Uninstalling removes the package only; a plugin's own config and cache stay —
|
|
`~/.config/punktfunk/plugin-state/<plugin>/` on Linux,
|
|
`%ProgramData%\punktfunk\plugin-state\<plugin>\` on Windows — so re-installing later picks your
|
|
settings back up.
|
|
|
|
To stop *every* plugin without uninstalling anything: **Disable runner** on the Installed tab, or
|
|
`punktfunk-host plugins disable`.
|
|
|
|
## ROM Manager
|
|
|
|
`@punktfunk/plugin-rom-manager` — point it at your ROM directories and it scans them, matches each
|
|
platform to an installed emulator, fetches box art (SteamGridDB, or the keyless libretro
|
|
thumbnails), and reconciles the result into your library as the `rom-manager` provider. ~25
|
|
built-in platforms (NES through Switch, PS1/2/PSP, Dreamcast, and more), per-game overrides, and a
|
|
console page to configure it all.
|
|
|
|
```sh
|
|
punktfunk-host plugins add rom-manager
|
|
```
|
|
|
|
Then add a root or two — from the console's **ROM Manager** page, or in the config file:
|
|
|
|
<Tabs items={['Linux', 'Windows']}>
|
|
<Tab value="Linux">
|
|
|
|
`~/.config/punktfunk/plugin-state/rom-manager/config.json`:
|
|
|
|
```jsonc
|
|
{
|
|
"roots": [
|
|
{ "dir": "/mnt/roms/snes", "platform": "snes" },
|
|
{ "dir": "/mnt/roms/ps1", "platform": "ps1", "excludes": ["*.sav"] }
|
|
],
|
|
"art": { "provider": "auto", "steamGridDbKey": "" }
|
|
}
|
|
```
|
|
|
|
</Tab>
|
|
<Tab value="Windows">
|
|
|
|
`%ProgramData%\punktfunk\plugin-state\rom-manager\config.json`:
|
|
|
|
```jsonc
|
|
{
|
|
"roots": [
|
|
{ "dir": "D:\\roms\\snes", "platform": "snes" },
|
|
{ "dir": "D:\\roms\\ps1", "platform": "ps1", "excludes": ["*.sav"] }
|
|
],
|
|
"art": { "provider": "auto", "steamGridDbKey": "" }
|
|
}
|
|
```
|
|
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
`plugin-state` is where a plugin reads and writes its own files — on Windows it is the one
|
|
directory `plugins enable` grants the low-privilege runner write access to; a config file placed
|
|
anywhere else under `%ProgramData%\punktfunk` is not read.
|
|
|
|
Full options and the platform/emulator list:
|
|
[the plugin's repo](https://git.unom.io/unom/punktfunk-plugin-rom-manager).
|
|
|
|
## Playnite
|
|
|
|
`@punktfunk/plugin-playnite` — mirrors your **[Playnite](https://playnite.link)** library (every
|
|
store and emulator Playnite manages) into your library. Launching a title hands it back to
|
|
Playnite, so there are no per-store launch commands to maintain; covers are served by the host.
|
|
|
|
Playnite keeps its library locked while running, so there are **two parts**, both on the **Windows
|
|
host**:
|
|
|
|
1. **The plugin** — from an elevated PowerShell:
|
|
|
|
```powershell
|
|
punktfunk-host plugins add playnite
|
|
punktfunk-host plugins enable
|
|
```
|
|
|
|
2. **The Punktfunk Sync extension** (in Playnite) — download `punktfunk-sync.pext` from the
|
|
[plugin's builds](https://git.unom.io/unom/punktfunk-plugin-playnite/actions), **double-click**
|
|
to install it like any add-on, restart Playnite once.
|
|
|
|
The console's **Playnite** page shows "Exporter connected", and your games sync within seconds of
|
|
any library change. Filters (installed-only, per-store, hidden) live on that page or in
|
|
`%ProgramData%\punktfunk\plugin-state\playnite\config.json`. Details:
|
|
[the plugin's repo](https://git.unom.io/unom/punktfunk-plugin-playnite).
|
|
|
|
## VirtualHere (USB passthrough)
|
|
|
|
`@punktfunk/plugin-virtualhere` — hands a **physical USB device** on your couch machine to the
|
|
host while you play, and gives it back afterwards. The game sees the real device — the answer for a
|
|
racing wheel, HOTAS, pedals or arcade stick.
|
|
|
|
<Callout type="warn">
|
|
This plugin drives [VirtualHere](https://www.virtualhere.com/), a commercial USB-over-IP product
|
|
**sold separately** by VirtualHere Pty. Ltd. Nothing from VirtualHere is bundled or downloaded by
|
|
Punktfunk — you install and license it yourself. The plugin is not affiliated with or endorsed by
|
|
VirtualHere.
|
|
</Callout>
|
|
|
|
You need both halves of VirtualHere running first:
|
|
|
|
- **The USB Server on the couch**, sharing the device. Free for one device; beyond that, and to run
|
|
the client as a service, VirtualHere requires a purchased licence.
|
|
- **The USB Client on the host**, ideally installed as a service so it survives logging out.
|
|
|
|
Servers exist for Windows, Linux, macOS and Android couches. **There is no VirtualHere server for
|
|
iOS or tvOS.**
|
|
|
|
```sh
|
|
punktfunk-host plugins add virtualhere
|
|
punktfunk-host plugins enable
|
|
```
|
|
|
|
The console's **VirtualHere** page → **Devices** lists whatever the couch is sharing; pick one and
|
|
it writes a rule matching the device *by name*, which survives reboots and port changes. By default
|
|
the device is handed over when video starts and returned when it stops; you can widen that to the
|
|
whole session, or to the entire time a client is connected.
|
|
|
|
If nothing happens, the **Diagnostics** tab walks the two-sided setup and names the part to fix —
|
|
the same checks as `punktfunk-plugin-virtualhere doctor`, the useful thing to paste into a support
|
|
thread. Full configuration:
|
|
[the plugin's repo](https://git.unom.io/unom/punktfunk-plugin-virtualhere).
|
|
|
|
## Troubleshooting
|
|
|
|
**`punktfunk-host: command not found`** — on Windows, open a new terminal so it picks up the
|
|
installer's `PATH` change, or call the exe by full path. On Linux the host package installs it to
|
|
`/usr/bin/punktfunk-host`.
|
|
|
|
**"the plugin runner isn't installed"** — the runner ships as its own package. Ubuntu:
|
|
`sudo apt install punktfunk-scripting`. Fedora: `sudo dnf install punktfunk-scripting` from the
|
|
[same RPM repo as the host](/docs/fedora). Arch: `sudo pacman -Syu punktfunk-scripting` (a full
|
|
`-Syu`). SteamOS: re-run `scripts/steamdeck/install.sh` (or `update.sh`). NixOS: comes with
|
|
`services.punktfunk.scripting.enable` (on by default whenever the host is). Windows: re-run the
|
|
installer and keep the scripting component.
|
|
|
|
If the runner *is* installed and the host still says it isn't, the host could not find the
|
|
`punktfunk-scripting` executable — it looks beside its own binary, then on `PATH`, then in the
|
|
packaged `/usr` and `~/.local` layouts. Point `PUNKTFUNK_SCRIPTING` at it (see
|
|
[Configuration](/docs/configuration)). The console installs plugins from inside the host *service*,
|
|
whose `PATH` is much shorter than your shell's — a runner that `plugins add` finds and the console
|
|
doesn't is that difference, and the env var is the fix.
|
|
|
|
**Where a plugin's log output goes** — the console's **Logs** page, under the **Plugins** filter.
|
|
Plugin lines sit next to the host's, on one timeline, tagged `plugin:<name>` (`plugin:runner` for
|
|
the supervisor). An empty Plugins view almost always means the runner isn't running — check with
|
|
`punktfunk-host plugins status`.
|
|
|
|
<Callout>
|
|
Nothing is lost if the host is down: the runner keeps buffering and sends the backlog when the host
|
|
comes back, and says so in the log if the buffer overflowed.
|
|
</Callout>
|
|
|
|
**Reading the runner's log directly** — the ground truth if the runner can't reach the host at all:
|
|
|
|
<Tabs items={['Linux', 'Windows']}>
|
|
<Tab value="Linux">
|
|
|
|
```sh
|
|
journalctl --user -u punktfunk-scripting -f
|
|
```
|
|
|
|
</Tab>
|
|
<Tab value="Windows">
|
|
|
|
The runner task writes to `%ProgramData%\punktfunk\plugin-state\runner.log` (previous run:
|
|
`runner.log.1`) — everything the runner and its plugins printed lands here even when they can't
|
|
reach the host.
|
|
|
|
```powershell
|
|
Get-Content "$env:ProgramData\punktfunk\plugin-state\runner.log" -Tail 100
|
|
```
|
|
|
|
If the file doesn't exist, the task started before `punktfunk-host plugins enable` ever ran (which
|
|
is what makes `plugin-state` writable for `LocalService`) — run it from an elevated prompt. To
|
|
watch a start live, run the runner in the foreground (<kbd>Ctrl</kbd>+<kbd>C</kbd> stops it):
|
|
|
|
```powershell
|
|
& "$env:ProgramFiles\punktfunk\bun\bun.exe" "$env:ProgramFiles\punktfunk\scripting\runner-cli.js"
|
|
```
|
|
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
**A plugin can't reach a service running on the same box (Linux)** — plugins that drive a local
|
|
daemon usually talk to it over a socket or FIFO in `/tmp`. The runner's unit shipped with
|
|
`PrivateTmp=yes` in earlier releases, hiding the real `/tmp`: the plugin launches the vendor's
|
|
binary and times out reaching the daemon, while the same command works in your shell. On an older
|
|
host, or with a drop-in that reinstates it:
|
|
|
|
```sh
|
|
systemctl --user edit punktfunk-scripting
|
|
```
|
|
```ini
|
|
[Service]
|
|
PrivateTmp=no
|
|
ReadWritePaths=/tmp
|
|
```
|
|
|
|
## Writing your own
|
|
|
|
A plugin is a TypeScript module built on **`@punktfunk/plugin-kit`** (`definePluginKit`), supervised
|
|
by the runner. The kit owns lifecycle, config and state, the library sync engine, and serving the
|
|
plugin's console page; `@punktfunk/host` (`definePlugin`) is the lower-level host client underneath
|
|
it. Start from the
|
|
[plugin-kit README](https://git.unom.io/unom/punktfunk/src/branch/main/plugin-kit) and
|
|
[ROM Manager](https://git.unom.io/unom/punktfunk-plugin-rom-manager), the reference implementation.
|