Files
2026-11-04 14:29:22 -06:00

13 KiB
Raw Permalink Blame History

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.


ILiveWallPlugin

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

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
  • 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 Effects 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

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.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.