Skip to content

How the SDK maps to the app

from mixlar import MixlarPlugin looks like an ordinary import, but which class it actually resolves to depends on where the code is running. Off-app — your editor, mixlar-sdk validate, mixlar-sdk emulate, tests, CI — you get a faithful standalone reimplementation with autocomplete and type checking. Inside the running Mixlar Control app, that same import transparently becomes the app’s own live class instead. This page explains that seam, why it’s safe to rely on, and what it means for how you write and ship plugins.

Everything in the SDK is a faithful, importable port of code that already lives inline in the app (PC Software/mixlar_mini.py) — MixlarPlugin, PluginRegistry, the color palette, ed25519 package signing, and the widget/manifest linters. Rather than maintaining two implementations that could silently drift apart, each SDK module checks at import time whether it’s running inside the app and, if so, hands you the app’s own object instead of its own standalone one.

The check lives at the bottom of mixlar/plugin.py:

# Inside the live app, BE the app's class so issubclass() checks pass and
# pushes use the real link. Off-app, use the standalone base above.
_app = sys.modules.get("mixlar_mini")
if _app is not None and hasattr(_app, "MixlarPlugin"):
MixlarPlugin = _app.MixlarPlugin
else:
MixlarPlugin = _StandaloneMixlarPlugin

mixlar/registry.py does the same thing for PluginRegistry:

_app = sys.modules.get("mixlar_mini")
if _app is not None and hasattr(_app, "PluginRegistry"):
PluginRegistry = _app.PluginRegistry

In other words: the detection is a single sys.modules lookup for mixlar_mini. If the app has already imported itself into the process (it always has, by the time plugins load), the SDK mirrors its live classes instead of defining its own.

Because both branches expose the same class name with the same shape, a plugin authored against mixlar.MixlarPlugin needs zero changes to run for real:

  • issubclass() checks the app’s loader performs against your plugin class succeed, because in-app your class really does subclass the app’s own MixlarPlugin — not a lookalike.
  • push_widget_data() and push_widget_image() reach the actual serial link in-app, instead of the standalone class’s stand-in behavior.
  • Macro/slider registration lands in the same PluginRegistry table the app reads, not a shadow copy.

Off-app, the standalone _StandaloneMixlarPlugin class is behaviorally identical in shape (same hooks, same signatures) but has no device to talk to — push_widget_data is a no-op unless something (like the emulator) has set PluginRegistry.instance()._wdata_sender.

You can build a Mixlar plugin two ways, and this seam is what makes them interoperable:

Legacy / in-app style SDK style
How MixlarPlugin resolves Injected into the module namespace by the app before exec-ing the file Explicit from mixlar import MixlarPlugin
Editor support None — MixlarPlugin, ACCENT, _fa etc. are undefined names until the app injects them Full autocomplete and type checking
Runs standalone (tests, CI, mixlar-sdk emulate) Only via the CLI’s compatibility shim Yes, natively
Runs in the app Yes Yes, unmodified

Legacy plugins (see PC Software/Plugin Examples/*.py) rely on the app injecting names — MixlarPlugin, the color constants, _fa, _EDITOR_COMBO_STYLE, _style_combo_view — into their module namespace before executing them. That’s why those examples reference MixlarPlugin and ACCENT with no import line at all: it only resolves inside the app.

Converting one to explicit SDK imports only touches the top of the file — the class body is untouched:

# Before: works only injected, no editor support
class MouseBatteryPlugin(MixlarPlugin):
plugin_id = "mouse_battery"
plugin_icon_color = ACCENT
def build_settings_ui(self, layout, settings):
icon = _fa("fa5s.battery-full", color=ACCENT)
# After: explicit imports, works everywhere
from mixlar import MixlarPlugin
from mixlar.colors import ACCENT
from mixlar.qt import _fa
class MouseBatteryPlugin(MixlarPlugin):
plugin_id = "mouse_battery"
plugin_icon_color = ACCENT
def build_settings_ui(self, layout, settings):
icon = _fa("fa5s.battery-full", color=ACCENT)

Signing and verification use the same “one implementation, two call sites” approach. mixlar.signing.package_digest computes the exact SHA-256 digest that the app’s own _plugin_package_digest computes over a package’s files — byte-identical, not just equivalent. That means:

  • A .mixplugin package signed with mixlar-sdk pack --sign or mixlar-sdk sign verifies correctly when the app checks its signature at install time.
  • There’s no separate “SDK signature” and “app signature” format to keep in sync — it’s one digest algorithm with two call sites.

See Signing packages for the full trust flow, key generation, and the pinned publisher keyset.

  • Write your plugin once, against mixlar.MixlarPlugin. Test it with mixlar-sdk validate and mixlar-sdk emulate with no device attached, then mixlar-sdk link it into the app’s plugins directory and it runs for real with no code changes.
  • Don’t rely on identity checks like type(x) is SomeSpecificClass against SDK classes in your own code — the whole point is that the class object itself differs between the two worlds (standalone vs. the app’s live class), even though the interface is identical.
  • If you’re hacking on the SDK itself rather than a plugin, the deeper, maintainer-facing side of this — the app eventually import mixlar-ing these modules as its own canonical implementation instead of defining them inline — is covered in Convert a plugin and the SDK’s own docs/migration.md.