Macro actions
Macros are the automation chains users build in the Mixlar Control app — a sequence of steps like “press a keyboard shortcut,” “wait 500ms,” “call a REST API.” Fourteen step types ship with the app itself, but any plugin can add its own step type to the macro editor by implementing a small set of hooks on its MixlarPlugin subclass.
This page covers those hooks, the ergonomic @action decorator that generates them for you, and the built-in step types you’re extending alongside.
The macro hook quartet
Section titled “The macro hook quartet”Four methods on MixlarPlugin drive everything the macro editor and executor need from your plugin:
| method | signature | purpose |
|---|---|---|
get_macro_actions |
(self) -> list[(id, name)] |
actions offered in the macro step picker |
get_macro_action_groups |
(self) -> list[(group, [(id, name), ...])] |
same list, bucketed into tabs |
get_macro_action_icons |
(self) -> dict[id, "fa5s.icon"] |
per-action icon (a qtawesome fa5s.* name) |
get_macro_step_defaults |
(self) -> dict |
default fields for a newly-added step |
execute_macro_step |
(self, step: dict) -> None |
run the action — called off the GUI thread |
describe_macro_step |
(self, step: dict) -> str |
one-line summary shown in the step list |
build_macro_editor |
(self, step, row, editor_lay, upd, upd_refresh, lbl) -> None |
add custom Qt editor widgets under the action selector |
prime_client |
(self, settings) -> None |
pre-init a client (e.g. open a socket) on the main thread, before execution |
You can override any of these directly. Here’s a minimal hand-written example — this is the actual HttpTrigger example plugin:
class HttpTrigger(MixlarPlugin): plugin_id = "http_trigger" plugin_name = "HTTP Trigger" plugin_version = "1.0.0" plugin_author = "Mixlar Community" plugin_description = "Send HTTP requests from macros and sliders." plugin_icon = "fa5s.globe" plugin_icon_color = "#30d158"
def get_macro_actions(self): return [("get", "GET Request"), ("post", "POST Request")]
def get_macro_action_icons(self): return {"get": "fa5s.download", "post": "fa5s.upload"}
def execute_macro_step(self, step): import requests url = step.get("url", "") if not url: return action = step.get("http_trigger_action", "get") if action == "post": requests.post(url, timeout=5) else: requests.get(url, timeout=5)
def build_macro_editor(self, step, row, editor_lay, upd, upd_refresh, lbl): from PyQt5.QtWidgets import QLineEdit editor_lay.addWidget(lbl("URL")) inp = QLineEdit(step.get("url", "")) inp.setPlaceholderText("https://example.com/api/trigger") inp.textChanged.connect(lambda t: upd(row, "url", t)) editor_lay.addWidget(inp)
def describe_macro_step(self, step): url = step.get("url", "no URL") return f"HTTP → {url[:40]}"The <plugin_id>_action key convention
Section titled “The <plugin_id>_action key convention”Notice execute_macro_step reads step.get("http_trigger_action", "get"), not step.get("action"). When the app’s built-in macro editor saves a step for a plugin-provided action, it namespaces the action field as "<plugin_id>_action" so it can’t collide with the app’s own "action" key on other step types. Your plugin_id is "http_trigger", so the field is "http_trigger_action".
The ergonomic way: MacroActions + @action
Section titled “The ergonomic way: MacroActions + @action”Writing all four methods by hand and keeping an if/elif dispatch table in sync with them gets unwieldy once a plugin has more than a couple of actions (the Bambu Studio example plugin does this at scale). mixlar.macros gives you a mixin and a decorator that let you declare each action once, right next to its handler:
from mixlar import MixlarPluginfrom mixlar.macros import MacroActions, action
class MyPlugin(MacroActions, MixlarPlugin): plugin_id = "my_plugin"
@action("mute", "Mute Mic", icon="fa5s.microphone-slash", group="Audio") def _mute(self, step): ...
@action("unmute", "Unmute Mic", icon="fa5s.microphone", group="Audio") def _unmute(self, step): ...action() decorator
Section titled “action() decorator”action(id: str, name: str, icon: str | None = None, group: str | None = None)| parameter | meaning |
|---|---|
id |
the value written into step["<plugin_id>_action"]; must be unique within your plugin |
name |
display name shown in the step picker |
icon |
optional fa5s.* (Font Awesome) icon name |
group |
tab the action is bucketed into in the macro builder; defaults to "General" |
The decorated method is the handler — it’s called as self.<method>(step) when a step’s action id matches.
What MacroActions derives
Section titled “What MacroActions derives”MacroActions scans the class once (result cached on the class) for every method carrying @action metadata, then implements:
get_macro_actions()—[(id, name), ...]in declaration order.get_macro_action_groups()— the same actions bucketed bygroup=.get_macro_action_icons()—{id: icon}for actions that specified one.execute_macro_step(step)— readsstep["<plugin_id>_action"](falling back tostep["action"]), finds the matching@actionid, and calls its handler.
Nothing here is mandatory. Override any of the four methods directly on your subclass — MRO makes your override the last word, so it simply shadows the mixin’s generated version, no special-casing required.
Built-in macro step types
Section titled “Built-in macro step types”Your plugin’s actions appear in the macro editor alongside 14 step types the app ships with:
| step type | description |
|---|---|
keyboard |
Keyboard shortcut |
type_text |
Type a text string |
open_app |
Open an application |
open_url |
Open a URL in the browser |
run_command |
Run a shell command |
api_call |
REST API call |
mouse_click |
Mouse click at a position |
delay |
Wait a number of milliseconds |
media_key |
Media key (play/pause/next/prev/volume) |
ha_service |
Home Assistant service call (toggle/on/off/trigger/scene) |
discord |
Discord voice actions (mute/deafen/noise/echo/AGC/join/leave/screenshare/camera — 13 actions) |
obs |
OBS Studio control (24 actions: scenes/record/stream/sources/replay/filters/etc.) |
spotify |
Spotify control (play/pause/next/prev/save/shuffle/repeat/volume) |
condition |
Conditional logic (16 condition types: app_running, app_focused, master_muted, mic_muted, audio_playing, time_between, day_of_week, device_connected, bluetooth_active, profile_active, spotify_playing, obs_streaming, obs_recording, discord_muted, file_exists, process_count_gt) |
Your plugin’s actions show up as plugin:<plugin_id> step types, grouped in their own tab (or tabs, if you use group=) in the same picker.
Complete example
Section titled “Complete example”Putting it together — a plugin that adds two grouped macro actions using the decorator, and one hand-written step that also needs a custom editor field:
from mixlar import MixlarPluginfrom mixlar.macros import MacroActions, action
class SceneSwitcher(MacroActions, MixlarPlugin): plugin_id = "scene_switcher" plugin_name = "Scene Switcher" plugin_version = "1.0.0" plugin_icon = "fa5s.film" plugin_icon_color = "#ff9f0a"
@action("next_scene", "Next Scene", icon="fa5s.step-forward", group="Scenes") def _next(self, step): self._client().next_scene()
@action("prev_scene", "Previous Scene", icon="fa5s.step-backward", group="Scenes") def _prev(self, step): self._client().prev_scene()
def _client(self): if not hasattr(self, "_scene_client"): self._scene_client = SceneClient() return self._scene_client
# Hand-written extras the mixin doesn't generate: def describe_macro_step(self, step): aid = step.get("scene_switcher_action", "") return "Next scene" if aid == "next_scene" else "Previous scene"Calling get_macro_action_groups() on this class returns [("Scenes", [("next_scene", "Next Scene"), ("prev_scene", "Previous Scene")])], and saved steps carry {"scene_switcher_action": "next_scene", ...} — the exact convention execute_macro_step dispatches on.
Next steps
Section titled “Next steps”- Plugin API reference for the full method list, including slider hooks and settings helpers.
- Slider actions to opt a fader into your plugin’s own mode.
- System events for hooks like
on_device_connectthat pair well with macro-driven plugins.