initial commit

This commit is contained in:
2026-11-04 14:29:22 -06:00
parent 73028cc5ba
commit 3f37abf34a
170 changed files with 22100 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
# Events, threading and lifetime
## The bus
`IPluginEvents` is a type-based publish/subscribe bus owned by LiveWall.
```csharp
IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : class;
void Publish<TEvent>(TEvent payload) where TEvent : class;
```
* `Subscribe` returns a token: **dispose it** in `Shutdown` (or you leak your plugin into the load
context and it will never unload).
* Handlers are matched on the exact runtime type (`Subscribe<WallpaperChangedEvent>` only receives
`WallpaperChangedEvent`), not on interfaces or base classes.
* Handlers run **on the WPF UI thread**, regardless of which thread published. Never block in one and
never touch a plugin whose state you cannot see.
* Host publications are delivered synchronously as part of the host's own operation; a slow handler
therefore slows the wallpaper change. Do the work in `Task.Run` if it is not instant.
* Exceptions thrown by a handler are logged and the handler is unsubscribed from that publication
onward — a broken handler cannot repeatedly break the app, and cannot break the other handlers.
## Catalogue
| Event | When | Useful fields |
|---|---|---|
| `AppStartedEvent` | After settings load, wallpaper attach and plugin load | `HostVersion`, `PluginApiVersion` |
| `AppStoppingEvent` | On exit, before plugins are unloaded | `Reason` |
| `WallpaperChangedEvent` | A new source finished loading (or failed to) | `Kind`, `Source`, `RendererName`, `FromPlugin` |
| `PlaybackChangedEvent` | Play/pause/suspend, fullscreen pause, battery pause | `IsPlaying`, `IsPaused`, `Reason` |
| `InteractionChangedEvent` | Interactive mode toggled | `Interactive` |
| `SettingsChangedEvent` | Any settings write; only for non-plugin-related keys | `Property`, `Snapshot` |
| `DisplayChangedEvent` | Monitor topology change or WorkerW re-attach | `Devices`, `Reattached` |
| `SessionChangedEvent` | Lock screen, battery, RDP | `Locked`, `OnBattery`, `RemoteSession` |
| `HostErrorEvent` | Something in the host failed and the user was told | `Message`, `Source` |
| `PluginMessageEvent` | Plugin-to-plugin convention | `FromPluginId`, `Topic`, `Payload` |
Events are **not** queued: you miss whatever happened before you subscribed. Pull the current state
from `context.Host.Current` / `context.Settings` during `Initialize` if you need a starting point.
## Threading cheat sheet
| Place | Thread |
|---|---|
| `Initialize`, `Shutdown` | UI thread (Shutdown may be called while the app tears down) |
| Overlay `CreateView` / `Update` / `OnVisibilityChanged` | UI thread |
| Settings page `CreateView` | UI thread |
| Command `Run`, hotkey action | UI thread |
| Filter `Apply` | UI thread, inside the wallpaper change |
| Renderer `Create` / `Load` / `Play` / `Pause` / `Unload` / `ApplyPlayback` | UI thread |
| Event handlers | UI thread |
Anything you start yourself is your business: use `Task.Run`, dispatch UI updates with
`Application.Current.Dispatcher`, and pass `context.ShutdownToken` to cancellable work.
## Lifetime in order
```
Rescan / enable
├─ manifest parsed, assembly hashed, permissions checked
├─ assembly loaded into a collectible AssemblyLoadContext
├─ PluginInfo filled in, IPluginContext created (storage opened, sub-bus created)
└─ Initialize(context) ← you register everything here
Disable / rescan / exit
├─ ShutdownToken cancelled
├─ Shutdown()
├─ your overlays, pages, commands, filters, renderers and hotkeys are dropped
├─ renderers disposed, overlays unhooked
└─ AssemblyLoadContext.Unload()
```
There is no "re-initialise": a plugin that needs to restart shows as **Disabled** then **Active** after
a Rescan, with a brand-new context object. Any state you want to survive goes in `IPluginStorage`.