# 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 ```csharp 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() { } } ``` ```json { "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 ```csharp 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 ```csharp public sealed class PulseOverlay : IWallpaperOverlay { private readonly List _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 ```csharp 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 ```csharp 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) ```csharp 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? 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 ```csharp 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 ```csharp private readonly List _subs = new(); public void Initialize(IPluginContext context) { _subs.Add(context.Events.Subscribe(e => context.Log.Info($"new wallpaper: {e.Kind} {System.IO.Path.GetFileName(e.Source)}"))); _subs.Add(context.Events.Subscribe(e => { if (e.Locked) context.Host.Pause(); // cheap power saving else context.Host.Play(); })); _subs.Add(context.Events.Subscribe(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 ```csharp // 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(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? ```csharp 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.