Files
LiveWall/docs/plugins/api-reference.md
T
2026-11-04 14:29:22 -06:00

327 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# API reference (`LiveWall.Sdk` 1.0)
Everything below lives in the namespace `LiveWall.Sdk`, in the single assembly `LiveWall.Sdk.dll`
(target `net8.0-windows`, WPF). Nullable reference types are enabled.
---
## `PluginApi` (static)
| Member | Value | Meaning |
|---|---|---|
| `Version` | `"1.0"` | Contract version. Must match the `apiVersion` in `plugin.json`. |
| `HostVersion` | e.g. `"1.8.0"` | LiveWall version this SDK shipped with. Informational. |
## `PluginPermission` (`[Flags]`)
| Flag | Dialog text |
|---|---|
| `None` | — |
| `ReadWallpaper` | Read your current wallpaper, playback state and settings |
| `ControlPlayback` | Start, stop, skip and replace your wallpaper |
| `DesktopOverlay` | Draw content on top of your desktop wallpaper |
| `SettingsUi` | Add pages to the LiveWall settings window |
| `Commands` | Add commands to LiveWall |
| `TrayMenu` | Add entries to the LiveWall tray menu |
| `Hotkeys` | Register system-wide keyboard shortcuts |
| `FileSystem` | Read and write files on your computer |
| `Network` | Connect to the internet |
| `Renderer` | Take over wallpaper playback with its own renderer |
| `Filters` | Rewrite or block wallpaper changes |
| `All` | Union of all of the above |
`PluginPermissionInfo.Describe(PluginPermission)` returns the text for one flag;
`PluginPermissionInfo.Expand(PluginPermission)` returns `(Permission, Description)` pairs — this is
what the warning dialog iterates.
> Permissions are **declarations**, not a sandbox. The host enforces them where it can (it will refuse
> to register an overlay without `DesktopOverlay`, or to change the wallpaper without
> `ControlPlayback`), but nothing stops a determined plugin from calling Win32 directly. They exist so
> the user knows what they are agreeing to — see [security.md](security.md).
---
## `ILiveWallPlugin`
```csharp
public interface ILiveWallPlugin
{
void Initialize(IPluginContext context);
void Shutdown();
}
```
* One public parameterless class per assembly, named by `"type"` in `plugin.json`.
* `Initialize` runs on the WPF UI thread. Register extensions and subscribe to events here; do not
block (no long `Thread.Sleep`, no `.Result`), and start expensive work on a background task.
* Any exception escaping `Initialize` marks the plugin **Failed**; it is never retried automatically
and the message is shown in the Plugins tab.
* `Shutdown` may run on any thread during unload/exit. It must not block for long and must not throw.
## `IPluginContext`
| Member | Type | Notes |
|---|---|---|
| `Info` | `PluginInfo` | Identity, paths, hash, verification flag |
| `Log` | `IPluginLog` | Prefixed with your plugin id, written to LiveWall's log |
| `Storage` | `IPluginStorage` | Per-plugin JSON key/value store |
| `Settings` | `ISettingsSnapshot` | Read-only settings snapshot (a fresh one each get) |
| `Host` | `IHostActions` | Actions in the host |
| `Events` | `IPluginEvents` | Subscribe / publish |
| `Extensions` | `IPluginRegistrar` | Register overlays, pages, commands, filters, renderers, hotkeys |
| `ShutdownToken` | `CancellationToken` | Cancelled when the plugin is being unloaded |
## `PluginInfo`
| Member | Type | Notes |
|---|---|---|
| `Id`, `Name`, `Version`, `Author`, `Description`, `Homepage` | `string` | From `plugin.json` |
| `ApiVersion` | `string` | Version the plugin declared (normalised to `"1.0"`) |
| `Permissions` | `PluginPermission` | Parsed from the manifest |
| `Folder` | `string` | Absolute plugin folder |
| `AssemblyPath` | `string` | Absolute path of the entry assembly |
| `Sha256` | `string` | Lower-case hex SHA-256 of the assembly |
| `Verified` | `bool` | `true` when this exact hash was approved on this machine |
## `IPluginLog`
```csharp
void Debug(string message);
void Info(string message);
void Warn(string message);
void Error(string message, Exception? exception = null);
```
Lines land in `%APPDATA%\LiveWall\logs\livewall-YYYYMMDD.log` as
`[com.example.clock] your message`. `Debug` only writes when LiveWall's debug logging is on.
## `IPluginStorage`
| Member | Notes |
|---|---|
| `string DataFolder` | `%APPDATA%\LiveWall\plugin-data\<id>\`, created on demand |
| `T? Get<T>(string key, T? fallback = default)` | Deserialised with `System.Text.Json` |
| `void Set<T>(string key, T value)` | Buffered; flushed automatically (debounced) and on shutdown |
| `bool Remove(string key)` | Removes one key |
| `IReadOnlyCollection<string> Keys` | All keys in the store |
| `string PathFor(string fileName)` | Absolute path inside `DataFolder`; parent dirs created |
| `void Flush()` | Force a write now |
The store is one JSON object per plugin (`storage.json`). Concurrent `Set` calls from different
threads are safe. Keep it small (it is rewritten wholesale).
## `ISettingsSnapshot`
| Member | Type | Notes |
|---|---|---|
| `SnapshotId` | `Guid` | Changes on every snapshot |
| `SourceKind` | `string` | `Video`, `AnimatedGif`, `Image`, `YouTube`, `WebPage` |
| `SourceLocation` | `string` | File path or URL |
| `Muted` | `bool` | |
| `Volume` | `double` | 0–100 |
| `Speed` | `double` | 0.25–4 |
| `Loop` | `bool` | |
| `InteractiveMode` | `bool` | `true` when the wallpaper accepts clicks |
| `Paused` | `bool` | |
| `DimPercent` | `int` | 0–100 |
| `DimColor` | `string` | `#RRGGBB` |
| `DisplayDevices` | `IReadOnlyList<string>` | Device names, primary first |
## `WallpaperState`
`Kind`, `Source`, `Title`, `IsPlaying`, `IsPaused`, `IsInteractive`, `SuppliedByPlugin`,
`RendererName`. This is `IHostActions.Current`.
## `IHostActions`
| Member | Permission | Behaviour |
|---|---|---|
| `WallpaperState Current { get; }` | `ReadWallpaper` | `Kind`/`Source` empty when unknown |
| `void SetWallpaper(WallpaperRequest)` | `ControlPlayback` | Routes through the normal filter chain then reloads |
| `void Play()` | `ControlPlayback` | |
| `void Pause()` | `ControlPlayback` | |
| `void TogglePause()` | `ControlPlayback` | |
| `void NextWallpaper()` | `ControlPlayback` | Next playlist entry |
| `void ReloadWallpaper()` | `ControlPlayback` | Re-applies the current source |
| `void Reattach()` | — | Re-parents the wallpaper window to WorkerW |
| `void OpenSettings()` | — | Shows the settings window |
| `void SetStatus(string)` | — | Status line text |
| `void ShowNotification(string, string)` | — | Balloon, respects the user's notification setting |
| `void SetOverlayVisible(bool)` | `DesktopOverlay` | Shows/hides **this plugin's** overlays |
Calls the plugin lacks permission for are logged and ignored — they never throw.
## `WallpaperRequest`
```csharp
public sealed class WallpaperRequest
{
public string Kind { get; set; } = "Custom"; // Video | AnimatedGif | Image | YouTube | WebPage | Custom
public string Source { get; set; } = ""; // path, URL, or anything your renderer understands
public string Title { get; set; } = "";
public Dictionary<string, string> Metadata { get; } // free-form, survives filters, not persisted
public WallpaperRequest Clone();
}
```
`Metadata` keys are case-insensitive. Use it to hand parameters to your own renderer.
---
## Extension points
### `IPluginRegistrar`
```csharp
void AddOverlay(IWallpaperOverlay overlay); // DesktopOverlay
void RemoveOverlay(IWallpaperOverlay overlay);
void AddSettingsPage(IPluginSettingsPage page); // SettingsUi
void AddCommand(PluginCommand command); // Commands / TrayMenu
void AddFilter(IWallpaperFilter filter); // Filters
void AddRenderer(IWallpaperRendererPlugin renderer); // Renderer
void AddHotkey(PluginHotkey hotkey); // Hotkeys
```
Registering without the matching permission logs a warning and does nothing.
### `IWallpaperOverlay`
```csharp
string Name { get; }
FrameworkElement CreateView(OverlayContext context);
void Update(OverlayState state);
void OnVisibilityChanged(bool visible) { } // default: do nothing
```
* `CreateView` is called once per display, on the UI thread, when the overlay is attached.
* The element is stretched to the display's DIP size and **is not hit-testable by default** — a
click on your overlay falls through to the desktop. If you need clicks, use `InteractiveMode` plus a
renderer instead; the overlay layer is deliberately click-through.
* Keep it cheap: it repaints whenever anything below it repaints. Avoid `Effect`s on large elements,
prefer text and simple shapes, and stop timers in `OnVisibilityChanged(false)`.
`OverlayContext`: `DisplayIndex`, `DeviceName`, `Width`, `Height` (DIP), `DpiScale`, `Primary`.
`OverlayState`: `Kind`, `Source`, `Title`, `IsPlaying`, `IsPaused`, `IsInteractive`, `TimestampUtc`.
### `IPluginSettingsPage`
```csharp
string Title { get; }
string Description { get; }
FrameworkElement CreateView();
```
Pages appear under **Plugins ▸ Installed plugins ▸ Settings**. Several pages = several tabs in one
window. `CreateView` is called when the window opens, so build fresh controls and read your state
there. The element tree is LiveWall's; if it throws, the message is shown in the page instead of
crashing the app.
Style your controls like the rest of the app — `(Brush)Application.Current.Resources["TextBrush"]`,
`["TextDim"]`, `["BgField"]`, `["AccentBrush"]`, `["DangerBrush"]`, and the `Card`, `Hint`,
`SectionTitle`, `AccentButton`, `GhostButton`, `TinyButton` styles. They are always present.
### `PluginCommand`
```csharp
new PluginCommand("id", "Human title", ctx => { /* ... */ })
{
Category = "Clock", // optional grouping label
CanExecute = () => true // optional; evaluated when the menu opens
};
```
`ctx.Plugin` is the plugin's `IPluginContext`, `ctx.Host` is `IHostActions`. Commands appear in
**Plugins ▸ Plugin commands** with a **Run** button; those with the `TrayMenu` permission also appear
in the tray menu under **Plugins**.
### `PluginHotkey`
```csharp
new PluginHotkey("clock.toggle24", "Ctrl+Alt+F9", () => { /* ... */ })
{
Description = "Clock: toggle 12/24 hour" // tooltip text in the hotkey list
};
```
Gestures accept the same syntax as LiveWall's own hotkeys: modifiers `Ctrl`/`Control`, `Alt`,
`Shift`, `Win` plus one key (`F1`–`F24`, letters, digits, `Space`, `Pause`, `PrintScreen`,
`Insert`/`Delete`/`Home`/`End`/`PageUp`/`PageDown`, arrows, numpad keys). Registration failure (the
combination is taken) is logged and reported — LiveWall's own hotkeys always win.
### `IWallpaperFilter`
```csharp
int Priority { get; } // higher runs first
WallpaperRequest Apply(WallpaperRequest request, IHostActions host);
```
Filters run in the UI thread **before** renderer selection, in descending `Priority` order (ties run in
registration order). Return the (possibly new) request; returning the argument unchanged leaves it
alone. Throwing removes your filter from that pass and is logged — other filters still run.
A filter can therefore: rewrite `Source` (resolve `ytsearch:` links, expand `%DATE%`), add
`Metadata`, or veto by returning the current state (`host.Current`).
### `IWallpaperRendererPlugin` / `IWallpaperRenderer`
```csharp
public interface IWallpaperRendererPlugin
{
string Name { get; }
int Priority { get; } // higher wins among plugins
bool CanRender(WallpaperRequest request);
IWallpaperRenderer Create();
}
public interface IWallpaperRenderer : IDisposable
{
FrameworkElement View { get; } // the wallpaper visual, keep it opaque
void Load(WallpaperRequest request); // throw to report a failure
void Play();
void Pause();
void Unload();
void ApplyPlayback(bool muted, double volume, double speed);
event EventHandler<string>? Failed;
event EventHandler? Ended;
}
```
LiveWall asks every renderer, highest `Priority` first, whether it `CanRender`s the request. The first
`true` wins and the built-in players (video/GIF/YouTube/web) are skipped — so a plugin can take over
sources LiveWall would otherwise handle. Then `Create()` is called once, `Load(request)` once per
source, and `View` is placed in the wallpaper window. `Failed`/`Ended` surface in the status line and
the log. `Dispose` is called when the renderer is replaced or the plugin unloads.
Remember: the wallpaper window is behind the desktop icons and **not** interactive unless
`InteractiveMode` is on.
---
## Events (`Events.cs`)
| Event | Payload |
|---|---|
| `AppStartedEvent` | `HostVersion`, `PluginApiVersion` |
| `AppStoppingEvent` | `Reason` |
| `WallpaperChangedEvent` | `Kind`, `Source`, `RendererName`, `FromPlugin` |
| `PlaybackChangedEvent` | `IsPlaying`, `IsPaused`, `Reason` |
| `InteractionChangedEvent` | `Interactive` |
| `SettingsChangedEvent` | `Property`, `Snapshot` |
| `DisplayChangedEvent` | `Devices`, `Reattached` |
| `SessionChangedEvent` | `Locked`, `OnBattery`, `RemoteSession` |
| `HostErrorEvent` | `Message`, `Source` |
| `PluginMessageEvent` | `FromPluginId`, `Topic`, `Payload` (string) |
```csharp
IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class;
void Publish<TEvent>(TEvent payload) where TEvent : class;
```
* Handlers always run on the WPF UI thread. Never block in one — bounce work to `Task.Run` if needed.
* Always keep the returned `IDisposable` and dispose it in `Shutdown`.
* `Publish` anything you like: it is a type-based bus, so you can define your own event classes for
plugin-to-plugin messaging (`PluginMessageEvent` is the conventional envelope).
* Publishing a host event type is allowed but only reaches other plugins — the host does not re-apply
it.