Creating Output Plugins
Output plugins are post-processing hooks that run after every export. They receive the exported files, device metadata, and the full binding model, so your plugin can deliver diagrams wherever they need to go.
Quick Start
Here's a minimal output plugin:
from joystick_diagrams.plugins.output_plugin_interface import (
ExportResult,
OutputPluginInterface,
)
from joystick_diagrams.plugins.plugin_settings import PluginMeta, PluginSettings
class MySettings(PluginSettings):
pass
class OutputPlugin(OutputPluginInterface):
plugin_meta = PluginMeta(
name="My Plugin",
version="1.0.0",
icon_path="img/icon.ico",
)
plugin_settings_model = MySettings
def process_export(self, results: list[ExportResult]) -> bool:
for result in results:
print(f"{result.profile_name} - {result.device_name}")
return True Plugin Structure
your_plugin/
__init__.py
main.py
img/
icon.ico
The main.py file must define an OutputPlugin class that inherits from OutputPluginInterface.
Installation & Distribution
Users install output plugins from the Settings page. See Installing Plugins for the end-user walkthrough. As an author, your job is to produce a zip that validates against the installer.
Zip Layout
The archive must contain a single plugin folder at its root:
my_plugin.zip
└── my_plugin/
├── __init__.py # required, can be empty
├── main.py # required, defines OutputPlugin class
├── plugin.sig # optional, Ed25519 signature (see below)
└── img/
└── icon.ico
The installer validates that __init__.py and main.py exist, that
main.py defines an OutputPlugin class subclassing
OutputPluginInterface, and that the class instantiates without errors.
Install Sources
The installer accepts three source types:
- Local folder: point at an unzipped plugin on disk.
- Zip file: the usual distribution format.
- URL: a direct link to a zip (e.g. a GitHub release asset).
User-Side Folder Drop
Users can also install by dropping a plugin folder directly into
%APPDATA%\Roaming\Joystick Diagrams\output_plugins\, then restarting the app.
Useful for development: symlink your working tree in and iterate.
Uninstalling
The trash icon on the plugin's Settings card removes the files but preserves the plugin's saved settings, so a reinstall picks up where the user left off.
Plugin Signing
Joystick Diagrams embeds an Ed25519 public key and verifies signed plugins on load. Signed plugins load silently; unsigned plugins prompt the user via a trust dialog on first use.
What Gets Signed
The signature covers a deterministic SHA-256 digest of every file in the plugin, excluding:
plugin.sigitself- Any
__pycache__directories
Re-signing is required whenever any other file changes, including icons, templates, or bundled data.
Getting a Plugin Signed
The Joystick Diagrams developer holds the signing key. If you'd like an official signature
on your plugin (for distribution to users who want the verified badge),
reach out on Discord. The source of
truth for the signing flow is
joystick_diagrams/plugins/plugin_signing.py.
PluginMeta
| Field | Type | Description |
|---|---|---|
name | str | Display name shown in the UI |
version | str | Plugin version string |
icon_path | str | Path to icon, relative to the plugin directory |
Plugin Settings
Same Pydantic model approach as parser plugins. Define a PluginSettings subclass.
Output plugins additionally support str fields with dropdown options:
from pathlib import Path
from pydantic import Field
class MySettings(PluginSettings):
output_path: Path | None = Field(
default=None,
title="Output Folder",
json_schema_extra={"is_folder": True, "default_path": "~/Documents"},
)
enabled_feature: bool = Field(default=True, title="Enable Feature")
mode: str = Field(
default="default",
title="Mode",
json_schema_extra={"options": ["default", "advanced"]},
) Supported Field Types
| Field Type | UI Control |
|---|---|
Path | None | Folder/file browse button |
bool | Checkbox |
str | Text field |
str with options | Dropdown (QComboBox) |
Path Field Options
Configure path fields via json_schema_extra:
| Key | Type | Default | Description |
|---|---|---|---|
is_folder | bool | True | Folder dialog vs file dialog |
default_path | str | none | Starting directory for the browse dialog |
extensions | list[str] | none | Allowed file extensions (e.g. [".xml"]) |
required | bool | True | Plugin not ready until this path is set |
Access settings with self.get_setting("field_name"). Settings persist to JSON automatically.
Ready State
By default, returns True when all required Path fields are set.
Override the ready property for custom logic:
@property
def ready(self) -> bool:
mode = self.get_setting("mode")
if mode == "dcs":
return self.get_setting("dcs_path") is not None
return self.get_setting("output_path") is not None ExportResult
Each exported device produces one ExportResult. This is the core data your plugin receives:
| Field | Type | Description |
|---|---|---|
profile_name | str | Display name of the profile (e.g. "A-10C II") |
device_name | str | Display name of the hardware device |
device_guid | str | Unique device GUID |
source_plugin | str | Name of the parser plugin that produced this profile |
template_name | str | None | SVG template filename used, or None |
export_format | str | "SVG" or "PNG" |
file_path | Path | Absolute path to the exported file |
export_directory | Path | Root export directory for this run |
device | Device_ | Full device model with all inputs and bindings |
Working with the Device Model
Access parsed binding data through result.device:
def process_export(self, results: list[ExportResult]) -> bool:
for result in results:
device = result.device
# Device properties
print(device.guid) # str, device GUID
print(device.name) # str, display name
# All inputs grouped by type
grouped = device.get_inputs()
for button_id, input_obj in grouped["buttons"].items():
print(f"{button_id}: {input_obj.command}")
# All inputs in a flat dict
for input_id, input_obj in device.get_combined_inputs().items():
print(f"{input_id}: {input_obj.command}")
# Check for modifiers
for mod in input_obj.modifiers:
print(f" {mod.modifiers} -> {mod.command}")
return True Lifecycle
- Discovery. Plugin is found in the
output_plugins/directory at startup. - Settings load. Settings are loaded from disk and enabled state is restored from the database.
- UI. Plugin appears in Settings > Output Plugins with an enable toggle and setup panel.
- Export. After every export, all enabled and ready plugins receive
process_export(results).
Full Example: Bindings to JSON
This example exports all binding data to JSON files, one per device per profile:
import json
from pathlib import Path
from pydantic import Field
from joystick_diagrams.plugins.output_plugin_interface import (
ExportResult,
OutputPluginInterface,
)
from joystick_diagrams.plugins.plugin_settings import PluginMeta, PluginSettings
class BindingsExportSettings(PluginSettings):
output_path: Path | None = Field(
default=None,
title="Output Folder",
json_schema_extra={"is_folder": True, "default_path": "~/Documents"},
)
class OutputPlugin(OutputPluginInterface):
plugin_meta = PluginMeta(
name="Bindings JSON Export",
version="1.0.0",
icon_path="img/icon.ico",
)
plugin_settings_model = BindingsExportSettings
def process_export(self, results: list[ExportResult]) -> bool:
output_path = self.get_setting("output_path")
if output_path is None:
return False
for result in results:
bindings = {}
for input_id, input_obj in result.device.get_combined_inputs().items():
entry = {"command": input_obj.command}
if input_obj.modifiers:
entry["modifiers"] = [
{"keys": sorted(m.modifiers), "command": m.command}
for m in input_obj.modifiers
]
bindings[input_id] = entry
data = {
"profile": result.profile_name,
"device": result.device_name,
"source": result.source_plugin,
"bindings": bindings,
}
filename = f"{result.device_guid[:5]}-{result.profile_name}.json"
file_path = Path.joinpath(Path(output_path), filename)
with open(file_path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
return True Constraints
References
- Input Library Reference: full data model documentation
- Creating Parser Plugins: the other plugin type
- GitHub Repository: browse the source and bundled output plugins
- Discord Server: get help and share plugins