Your first action
This continues the Quickstart project, Acme.LightControl. You add a
Set brightness action with a 0-100 slider, trigger it, and then make the button show whether the
light is on.
1. Add the strings
Section titled “1. Add the strings”Every user-facing string lives in src/Acme.LightControl/Localization/Strings.resx. Add these entries
before </root>:
<data name="Actions.SetBrightness.Name" xml:space="preserve"> <value>Set brightness</value></data><data name="Actions.SetBrightness.Description" xml:space="preserve"> <value>Sets the light to a brightness between 0 and 100.</value></data><data name="Actions.SetBrightness.Brightness.Label" xml:space="preserve"> <value>Brightness</value></data>The build turns each key into a method on the generated Strings class, for example
Strings.Actions.SetBrightness.Name(). See Localization.
2. Add the action
Section titled “2. Add the action”Create src/Acme.LightControl/SetBrightnessAction.cs:
using System.Globalization;using MacroDeck.Localization;using MacroDeck.Sdk.Actions;using Serilog;
namespace Acme.LightControl;
public sealed class SetBrightnessAction(ILogger logger) : IActionDefinition{ private double _brightness;
public string Id => "set-brightness";
public LocalizedText Name => Strings.Actions.SetBrightness.Name();
public LocalizedText Description => Strings.Actions.SetBrightness.Description();
public IReadOnlyList<ActionParameter> Parameters { get; } = [ ActionParameter.Slider("brightness", 0, 100, label: Strings.Actions.SetBrightness.Brightness.Label(), defaultValue: 100), ];
public IActionExecutor CreateExecutor() => new Executor(this, logger);
private sealed class Executor(SetBrightnessAction action, ILogger logger) : IActionExecutor { public Task<ActionResult> ExecuteAsync(ActionExecutionContext context) { var brightness = Convert.ToDouble( context.Parameters.GetValueOrDefault("brightness")?.ToString() ?? "100", CultureInfo.InvariantCulture);
action._brightness = brightness; logger.Information("Brightness set to {Brightness}", brightness); return ActionResult.SucceededTask; } }}Idis stored in users’ profiles. Never rename it once released.- The executor reads the parameter by name from
context.Parameters.
3. Register it
Section titled “3. Register it”In src/Acme.LightControl/PluginIntegration.cs:
Actions = [new LogMessageAction(logger)];Actions = [new LogMessageAction(logger), new SetBrightnessAction(logger)];dotnet build4. Trigger it from a test
Section titled “4. Trigger it from a test”Add to tests/Acme.LightControl.Tests/PluginIntegrationTests.cs, inside PluginIntegrationTests:
[Test]public async Task Set_brightness_logs_the_new_value(){ await using var harness = CreateHarness(); await harness.InitializeIntegrationsAsync();
var outcome = await harness.Actions.ExecuteAsync( "set-brightness", new Dictionary<string, object?> { ["brightness"] = 40 });
Assert.That(outcome.Succeeded, Is.True); Assert.That(harness.Logs.Events.Any(e => e.Message.Contains("Brightness set to 40")), Is.True);}dotnet testPassed! - Failed: 0, Passed: 8, Skipped: 0, Total: 8The harness runs your action through the same capability handler the host calls. See Testing plugins.
5. Trigger it from a button
Section titled “5. Trigger it from a button”Run the plugin against Macro Deck - press F5 in your IDE or run it with the CLI, see Debugging plugins:
macrodeck-plugin run --project src/Acme.LightControlPut Set brightness on a button, pick a value, lock the deck and press the button. The plugin output shows:
[plugin] Brightness set to 40Restart the plugin after changing its Actions so the host receives the new list.
6. Optional: show the state on the button
Section titled “6. Optional: show the state on the button”Implement IStateProviderActionDefinition so a button can follow the light:
public sealed class SetBrightnessAction(ILogger logger) : IActionDefinitionpublic sealed class SetBrightnessAction(ILogger logger) : IActionDefinition, IStateProviderActionDefinitionpublic Task<ActionStateSnapshot?> GetActionStateAsync( IReadOnlyDictionary<string, object?> parameters, CancellationToken cancellationToken){ ActionStateDefinition[] states = [ new("off", MacroDeckStrings.States.Off()), new("on", MacroDeckStrings.States.On()), ]; return Task.FromResult<ActionStateSnapshot?>(new(states, _brightness > 0 ? "on" : "off"));}Check it in the test:
var state = await harness.Actions.GetActionStateAsync("set-brightness");Assert.That(state.Data!.Value.GetProperty("activeStateId").GetString(), Is.EqualTo("on"));And against the conformance suite, which now checks the state snapshots too:
macrodeck-plugin test --project src/Acme.LightControlPassed: 27, Failed: 0, Skipped: 22Conformant: yes...[PASS] MDC0309 Every state-provider action's state operation returns a well-formed snapshot (Required)[PASS] MDC0310 Every state a state-provider action returns has an id that is a valid declared-kind identifier (Required)In Macro Deck, a button running Set brightness can now show “On” or “Off”. See Button states.
Next steps
Section titled “Next steps”- Actions - parameter types, failures, long-running work.
- Button states - default appearances, polling, expected states.
- Features - everything else a plugin can offer.