# 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\\`, created on demand | | `T? Get(string key, T? fallback = default)` | Deserialised with `System.Text.Json` | | `void Set(string key, T value)` | Buffered; flushed automatically (debounced) and on shutdown | | `bool Remove(string key)` | Removes one key | | `IReadOnlyCollection 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` | 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 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? 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(Action handler) where TEvent : class; void Publish(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.