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

10 KiB

Recipes

Copy, adapt, ship. Everything here compiles against LiveWall.Sdk 1.0; the complete working example is samples/ClockOverlayPlugin/.


A minimal plugin that just does something

using LiveWall.Sdk;

public sealed class PingPlugin : ILiveWallPlugin
{
    public void Initialize(IPluginContext context)
    {
        context.Log.Info("ping plugin ready");

        context.Extensions.AddCommand(new PluginCommand("ping.now", "Ping: notify me",
            ctx => ctx.Host.ShowNotification("Ping", "Still here.")));
    }

    public void Shutdown() { }
}
{ "id": "com.example.ping", "name": "Ping", "version": "1.0.0", "apiVersion": "1.0",
  "entry": "Ping.dll", "type": "PingPlugin", "minHostVersion": "1.8.0",
  "permissions": ["Commands"] }

An overlay that shows the wallpaper's name

public sealed class NameOverlay : IWallpaperOverlay
{
    private TextBlock? _text;

    public string Name => "Now playing";

    public FrameworkElement CreateView(OverlayContext context)
    {
        _text = new TextBlock
        {
            Text = "",
            FontSize = 13,
            Foreground = new SolidColorBrush(Color.FromArgb(170, 255, 255, 255)),
            HorizontalAlignment = HorizontalAlignment.Left,
            VerticalAlignment = VerticalAlignment.Bottom,
            Margin = new Thickness(24, 0, 0, 20)
        };
        return _text;
    }

    public void Update(OverlayState state)
    {
        if (_text == null) return;
        _text.Text = string.IsNullOrEmpty(state.Source)
            ? ""
            : $"{state.Kind} · {System.IO.Path.GetFileName(state.Source)}" +
              (state.IsPaused ? "  (paused)" : "");
    }
}

Update is called on every wallpaper/playback change — no polling needed.


An animated overlay (visualiser-ish) with a timer that behaves

public sealed class PulseOverlay : IWallpaperOverlay
{
    private readonly List<Rectangle> _bars = new();
    private DispatcherTimer? _timer;
    private readonly Random _random = new();

    public string Name => "Pulse";

    public FrameworkElement CreateView(OverlayContext context)
    {
        var panel = new StackPanel
        {
            Orientation = Orientation.Horizontal,
            VerticalAlignment = VerticalAlignment.Bottom,
            HorizontalAlignment = HorizontalAlignment.Center,
            Margin = new Thickness(0, 0, 0, 32),
            Height = 120
        };

        for (int i = 0; i < 32; i++)
        {
            var bar = new Rectangle
            {
                Width = 6,
                Height = 10,
                Margin = new Thickness(2, 0, 2, 0),
                VerticalAlignment = VerticalAlignment.Bottom,
                Fill = new SolidColorBrush(Color.FromArgb(140, 0x4C, 0xC2, 0xFF))
            };
            _bars.Add(bar);
            panel.Children.Add(bar);
        }

        _timer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(80) };
        _timer.Tick += (_, _) =>
        {
            foreach (var bar in _bars)
                bar.Height = 8 + _random.Next(105);
        };
        _timer.Start();

        return panel;
    }

    public void Update(OverlayState state) { }

    public void OnVisibilityChanged(bool visible)
    {
        if (visible) _timer?.Start();
        else _timer?.Stop();
    }
}

Stopping the timer when hidden is what keeps a wallpapered machine cool while a game is fullscreen.


A filter that resolves shorthand and env vars

public sealed class ResolveFilter : IWallpaperFilter
{
    public int Priority => 100;   // run before other plugins' filters

    public WallpaperRequest Apply(WallpaperRequest request, IHostActions host)
    {
        var source = request.Source;

        if (source.StartsWith("yt:", StringComparison.OrdinalIgnoreCase))
        {
            var query = source[3..];
            request.Source = "https://www.youtube.com/results?search_query=" + Uri.EscapeDataString(query);
            request.Kind = "YouTube";
        }
        else if (source.StartsWith("home:"))
        {
            request.Source = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), source[5..]);
            request.Kind = "Video";
        }

        return request;
    }
}

A filter that pins the wallpaper

public sealed class PinFilter : IWallpaperFilter
{
    private readonly IHostActions _host;
    public bool Pinned { get; set; }

    public PinFilter(IHostActions host) => _host = host;

    public int Priority => 1000;

    public WallpaperRequest Apply(WallpaperRequest request, IHostActions host)
    {
        if (!Pinned) return request;

        var current = host.Current;
        return new WallpaperRequest { Kind = current.Kind, Source = current.Source, Title = "pinned" };
    }
}

Toggle Pinned from a command or a settings page; the wallpaper stops changing even for playlists.


A custom renderer (clock/simulation style)

public sealed class ShaderRendererPlugin : IWallpaperRendererPlugin
{
    public string Name => "Example shader";
    public int Priority => 0;

    public bool CanRender(WallpaperRequest request)
        => request.Kind.Equals("Custom", StringComparison.OrdinalIgnoreCase)
           && request.Source.StartsWith("example-scene:", StringComparison.OrdinalIgnoreCase);

    public IWallpaperRenderer Create() => new ShaderRenderer();
}

public sealed class ShaderRenderer : IWallpaperRenderer
{
    private readonly Grid _root = new();
    private DispatcherTimer? _timer;
    private double _t;

    public event EventHandler<string>? Failed;
    public event EventHandler? Ended;

    public FrameworkElement View => _root;

    public void Load(WallpaperRequest request)
    {
        var name = request.Source["example-scene:".Length..];
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentException("no scene name in " + request.Source);

        _root.Background = new SolidColorBrush(Color.FromRgb(8, 10, 16));   // stay opaque
        _root.Children.Clear();
        _root.Children.Add(new TextBlock
        {
            Text = "scene: " + name,
            Foreground = Brushes.White,
            HorizontalAlignment = HorizontalAlignment.Center,
            VerticalAlignment = VerticalAlignment.Center
        });

        _timer = new DispatcherTimer(DispatcherPriority.Render) { Interval = TimeSpan.FromMilliseconds(33) };
        _timer.Tick += (_, _) => _t += 0.033;
        _timer.Start();
    }

    public void Play() => _timer?.Start();
    public void Pause() => _timer?.Stop();
    public void Unload() { _timer?.Stop(); _timer = null; }
    public void ApplyPlayback(bool muted, double volume, double speed) { /* use them if you like */ }
    public void Dispose() => Unload();
}

Declare "permissions": ["Renderer", "ControlPlayback"] and the plugin can be selected by setting Kind = "Custom", Source = "example-scene:rain" via host.SetWallpaper(...).


A settings page with LiveWall styling

public sealed class MyPage : IPluginSettingsPage
{
    private readonly IPluginContext _context;
    public MyPage(IPluginContext context) => _context = context;

    public string Title => "Example";
    public string Description => "Where the example settings live.";

    public FrameworkElement CreateView()
    {
        var res = Application.Current.Resources;

        var panel = new StackPanel();
        panel.Children.Add(new TextBlock
        {
            Text = "Scene",
            Foreground = (Brush)res["TextDim"],
            Margin = new Thickness(0, 0, 0, 4)
        });

        var combo = new ComboBox { Width = 200, HorizontalAlignment = HorizontalAlignment.Left };
        combo.ItemsSource = new[] { "rain", "snow", "stars" };
        combo.SelectedItem = _context.Storage.Get("scene", "rain");
        combo.SelectionChanged += (_, _) =>
        {
            _context.Storage.Set("scene", combo.SelectedItem as string ?? "rain");
            _context.Host.SetStatus("Example: scene = " + combo.SelectedItem);
        };
        panel.Children.Add(combo);

        var button = new Button
        {
            Content = "Apply now",
            Style = (Style)res["AccentButton"],
            HorizontalAlignment = HorizontalAlignment.Left,
            Margin = new Thickness(0, 14, 0, 0)
        };
        button.Click += (_, _) => _context.Host.SetWallpaper(new WallpaperRequest
        {
            Kind = "Custom",
            Source = "example-scene:" + _context.Storage.Get("scene", "rain")
        });
        panel.Children.Add(button);

        return panel;
    }
}

Reacting to events without leaking

private readonly List<IDisposable> _subs = new();

public void Initialize(IPluginContext context)
{
    _subs.Add(context.Events.Subscribe<WallpaperChangedEvent>(e =>
        context.Log.Info($"new wallpaper: {e.Kind} {System.IO.Path.GetFileName(e.Source)}")));

    _subs.Add(context.Events.Subscribe<SessionChangedEvent>(e =>
    {
        if (e.Locked) context.Host.Pause();          // cheap power saving
        else context.Host.Play();
    }));

    _subs.Add(context.Events.Subscribe<SettingsChangedEvent>(e =>
    {
        var s = e.Snapshot;                          // read-only view of the new settings
        context.Log.Debug($"settings changed ({e.Property}): volume {s.Volume}, muted {s.Muted}");
    }));
}

public void Shutdown()
{
    foreach (var sub in _subs) sub.Dispose();
    _subs.Clear();
}

Talking to another plugin

// sender
context.Events.Publish(new PluginMessageEvent
{
    FromPluginId = context.Info.Id,
    Topic = "weather.now",
    Payload = """{"temp":18.5,"sky":"rain"}"""
});

// listener (another plugin)
context.Events.Subscribe<PluginMessageEvent>(m =>
{
    if (m.Topic == "weather.now") context.Log.Info($"weather: {m.Payload}");
});

PluginMessageEvent is just a convention — the bus is type based, so any class you both reference works. Ship the shared class in a small library, or keep to PluginMessageEvent to avoid one.


Prefer to write it in the tray only?

context.Extensions.AddCommand(new PluginCommand("quiet.toggle", "Mute wallpaper",
    ctx => { if (ctx.Host.Current.IsPlaying) ctx.Host.Pause(); else ctx.Host.Play(); })
{
    Category = "Audio",
    CanExecute = () => true
});

With "permissions": ["TrayMenu", "ControlPlayback", "ReadWallpaper"] that is a complete, useful plugin in twelve lines.