-
Notifications
You must be signed in to change notification settings - Fork 1
API Commands
A command is the unit a user assigns to a hardware button, simple key, or
rotary encoder. This page covers the four types that describe and execute a
command: IPluginCommand, IDisplayCommand, CommandDescriptor,
CommandContext, and the ButtonTargets enum.
public interface IPluginCommand
{
CommandDescriptor Descriptor { get; }
ButtonTargets SupportedTargets { get; }
Task Execute(CommandContext ctx);
}A single user-assignable action. The host runs commands on a background worker
queue — Execute is invoked on a thread-pool thread, not on the UI thread.
Two consequences:
- Do not touch UI objects directly from
Execute. If you need to interact with anything Avalonia-owned, marshal back through the host (e.g. by raising an event the host listens on, or by callingDispatcher.UIThread.Postin code you own). - Do not
.Wait()or.Resulton the returnedTask. Block the worker and you stall every following command in the queue — return theTaskand let the host await it.
Exceptions thrown by Execute are caught and logged by the host but do not
unload the plugin. Wrap risky calls in try/catch and call
ctx.Host.Logger.Error(msg, ex) so the failure is attributed to your command
rather than appearing as a generic host error.
internal sealed class HelloCommand : IPluginCommand
{
public CommandDescriptor Descriptor { get; } = new()
{
CommandName = "Hello.SayHello",
DisplayName = "Say hello",
Group = "Hello"
};
public ButtonTargets SupportedTargets => ButtonTargets.TouchButton | ButtonTargets.SimpleButton;
public Task Execute(CommandContext ctx)
{
try { ctx.Host.Logger.Info($"Hello from {ctx.Target}!"); }
catch (Exception ex) { ctx.Host.Logger.Error("SayHello failed", ex); }
return Task.CompletedTask;
}
}public interface IDisplayCommand : IPluginCommand
{
TimeSpan UpdateInterval { get; }
string GetText(CommandContext ctx);
}Extends IPluginCommand with text that is polled and re-rendered on the touch
button. Use it for clocks, sensor readings, current scene names, queue depths,
etc.
- Set
SupportedTargetstoButtonTargets.TouchButton— only touch buttons render text. -
UpdateIntervalis the polling cadence. Pick the longest interval that still feels live; 1–5 seconds is usually right. -
GetTextruns on the polling timer and must be fast and synchronous. Cache the latest value in a field that your async data path updates, and just return that field here.
If your data arrives via push (event subscription) and you want an immediate
redraw without waiting for the next poll, call
host.RequestButtonRefresh(Descriptor.CommandName).
internal sealed class HelloDisplayCommand : IDisplayCommand
{
public CommandDescriptor Descriptor { get; } = new()
{
CommandName = "Hello.Display",
DisplayName = "Hello display",
Group = "Hello"
};
public ButtonTargets SupportedTargets => ButtonTargets.TouchButton;
public TimeSpan UpdateInterval => TimeSpan.FromSeconds(1);
public string GetText(CommandContext ctx) => $"Hello {DateTime.Now:HH:mm:ss}";
public Task Execute(CommandContext ctx) => Task.CompletedTask; // tap = no-op
}public interface IAdjustmentCommand : IPluginCommand
{
Task ApplyAdjustment(CommandContext ctx, int ticks);
Task ApplyReset (CommandContext ctx);
string? GetValueText (CommandContext ctx);
}A rotary-encoder value adjustment: a continuous parameter the user dials
up/down (volume, brightness, zoom), with a press to reset or commit. Set
SupportedTargets to ButtonTargets.RotaryEncoder.
| Member | Notes |
|---|---|
ApplyAdjustment(ctx, ticks) |
Applies a relative change. ticks is positive for a right-turn and negative for a left-turn. Multiple physical detents within one host tick are summed, so ticks may be ±2, ±3… — don't assume ±1. |
ApplyReset(ctx) |
Typically invoked by an encoder press. Snap the value back to a sensible default or commit a pending change. |
GetValueText(ctx) |
Current value as short display text (e.g. "75 %"), or null for no overlay. Called by the host when rendering a dial indicator. Must be fast and synchronous. |
Forward-compatible contract. Host-side dispatch for
IAdjustmentCommandis added alongside the first plugin that needs it. Until then, expose three discrete commands (Foo.Up,Foo.Down,Foo.Reset) bound to the rotary's left/right/press slots and branch onctx.TargetinsideExecute. ImplementingIAdjustmentCommandin addition is safe and forward-compatible — once the host wires it up, your commands light up automatically.
internal sealed class VolumeAdjustment : IAdjustmentCommand
{
public CommandDescriptor Descriptor { get; } = new()
{
CommandName = "MyPlugin.Volume",
DisplayName = "Volume",
Group = "MyPlugin"
};
public ButtonTargets SupportedTargets => ButtonTargets.RotaryEncoder;
public Task ApplyAdjustment(CommandContext ctx, int ticks)
{
var step = ticks * 5; // 5 % per detent
_value = Math.Clamp(_value + step, 0, 100);
ctx.Host.RequestButtonRefresh(Descriptor.CommandName);
return Task.CompletedTask;
}
public Task ApplyReset(CommandContext ctx)
{
_value = 50;
ctx.Host.RequestButtonRefresh(Descriptor.CommandName);
return Task.CompletedTask;
}
public string? GetValueText(CommandContext ctx) => $"{_value} %";
// Fallback dispatch until the host wires up IAdjustmentCommand:
public Task Execute(CommandContext ctx) => Task.CompletedTask;
private int _value = 50;
}public interface IDisplayImageCommand : IPluginCommand
{
TimeSpan UpdateInterval { get; }
byte[]? GetImage(CommandContext ctx);
string? GetText (CommandContext ctx);
}Renders a dynamic image (PNG bytes) onto a touch button, optionally with an overlay text. Use it for album art, camera thumbnails, scene previews — any visual that changes at runtime.
- Set
SupportedTargetstoButtonTargets.TouchButton. -
UpdateIntervalis the polling cadence the host uses to callGetImage. -
GetImagemust be fast and synchronous — it runs on a polling timer. Do any decoding/network work asynchronously (kick it off fromLoupixPlugin.Initializeor an event handler) and cache the result; return the cached PNG bytes here, ornullto fall back to the button's default icon. -
GetTextprovides an optional caption drawn beneath the image, ornullfor image-only rendering.
Forward-compatible contract. Host-side wiring for
IDisplayImageCommandis added alongside the first plugin that needs it. Until then plugins may declare the interface, but only the text path ofIDisplayCommandis guaranteed to render. ImplementingIDisplayImageCommandis safe — it simply remains dormant until the host enables it.
internal sealed class NowPlayingArt : IDisplayImageCommand
{
private byte[]? _cachedArt;
private string _caption = "";
public CommandDescriptor Descriptor { get; } = new()
{
CommandName = "MyPlugin.NowPlaying",
DisplayName = "Now playing",
Group = "MyPlugin"
};
public ButtonTargets SupportedTargets => ButtonTargets.TouchButton;
public TimeSpan UpdateInterval => TimeSpan.FromSeconds(2);
public byte[]? GetImage(CommandContext ctx) => _cachedArt;
public string? GetText (CommandContext ctx) => _caption;
public Task Execute(CommandContext ctx) => Task.CompletedTask;
// Called from your async data path:
internal void Update(byte[] png, string caption, IPluginHost host)
{
_cachedArt = png;
_caption = caption;
host.RequestButtonRefresh(Descriptor.CommandName);
}
}public sealed class CommandDescriptor
{
public required string CommandName { get; init; }
public required string DisplayName { get; init; }
public required string Group { get; init; }
public string? ParameterTemplate { get; init; }
public IReadOnlyList<CommandParameter> Parameters { get; init; } = [];
public bool HiddenFromMenu { get; init; }
}
public sealed class CommandParameter(string name, Type parameterType)
{
public string Name { get; }
public Type ParameterType { get; }
}| Member | Notes |
|---|---|
CommandName |
Stable identifier persisted into user button configurations. Treat it as a public API — renaming it later breaks every saved config that referenced it. Convention: <Plugin>.<Action>, PascalCase. |
DisplayName |
Label in the command-selection menu. |
Group |
Submenu/category. Plugins usually use their Metadata.Name. |
ParameterTemplate |
Placeholder rendered in the command builder, e.g. ({SceneName}). Null when the command takes no parameters. |
Parameters |
Positional parameter definitions in declaration order. Used by the command builder for editors/validation. |
HiddenFromMenu |
When true the command is not listed as a plain leaf; it surfaces only through dynamic submenus built by IMenuContributor (e.g. one entry per OBS scene). The command stays fully registered and executable. |
public sealed class CommandContext
{
public required string[] Parameters { get; init; }
public required ButtonTargets Target { get; init; }
public int? SourceIndex { get; init; }
public DeviceInfo? Device { get; init; }
public required IPluginHost Host { get; init; }
}Everything a command needs at execution time.
| Member | Notes |
|---|---|
Parameters |
Positional parameter values exactly as parsed from the persisted command string's parentheses. Never null; empty when the command takes none. Strings — parse to typed values yourself based on the declared CommandParameter types. |
Target |
Which button type triggered the command. Useful when a single command should behave differently on a rotary encoder vs. a touch button. |
SourceIndex |
Identifier of the originating control when Target denotes an indexed source — rotary index for RotaryEncoder, touch slot for TouchButton, simple-button index for SimpleButton. null when the command was invoked from a chained command or from the CLI. Pair with IPluginHost.GetTouchSlotForRotary to address the touch slot next to the rotary that fired. |
Device |
The active device, or null if none. Mirrors IPluginHost.ActiveDevice. |
Host |
The plugin's IPluginHost. Convenient when a command instance is shared across plugins or when you do not want to capture the host in a closure. |
[Flags]
public enum ButtonTargets
{
None = 0,
TouchButton = 1,
SimpleButton = 2,
RotaryEncoder = 4,
All = TouchButton | SimpleButton | RotaryEncoder
}Flags enum. SupportedTargets filters the command-selection menu of each
button type:
- A user editing a touch button only sees commands whose
SupportedTargetsincludeTouchButton. -
IDisplayCommandmakes no sense on a simple key (no display) — useButtonTargets.TouchButton. -
RotaryEncodercovers all three rotation/press events; the host invokesExecutefor each, and the command should branch onctx.Target(or on a parameter) if it needs to distinguish.
Getting started
API reference
Advanced
Operations
Release notes