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.