13 KiB
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 withoutControlPlayback), but nothing stops a determined plugin from calling Win32 directly. They exist so the user knows what they are agreeing to — see security.md.
ILiveWallPlugin
public interface ILiveWallPlugin
{
void Initialize(IPluginContext context);
void Shutdown();
}
- One public parameterless class per assembly, named by
"type"inplugin.json. Initializeruns on the WPF UI thread. Register extensions and subscribe to events here; do not block (no longThread.Sleep, no.Result), and start expensive work on a background task.- Any exception escaping
Initializemarks the plugin Failed; it is never retried automatically and the message is shown in the Plugins tab. Shutdownmay 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
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
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
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
string Name { get; }
FrameworkElement CreateView(OverlayContext context);
void Update(OverlayState state);
void OnVisibilityChanged(bool visible) { } // default: do nothing
CreateViewis 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
InteractiveModeplus a renderer instead; the overlay layer is deliberately click-through. - Keep it cheap: it repaints whenever anything below it repaints. Avoid
Effects on large elements, prefer text and simple shapes, and stop timers inOnVisibilityChanged(false).
OverlayContext: DisplayIndex, DeviceName, Width, Height (DIP), DpiScale, Primary.
OverlayState: Kind, Source, Title, IsPlaying, IsPaused, IsInteractive, TimestampUtc.
IPluginSettingsPage
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
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
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
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
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 CanRenders 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) |
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.Runif needed. - Always keep the returned
IDisposableand dispose it inShutdown. Publishanything you like: it is a type-based bus, so you can define your own event classes for plugin-to-plugin messaging (PluginMessageEventis the conventional envelope).- Publishing a host event type is allowed but only reaches other plugins — the host does not re-apply it.