Creating Parser Plugins

Parser plugins teach Joystick Diagrams how to read a new game's config files and return structured binding data. A plugin reads game configuration, parses the bindings, and returns a ProfileCollection that the app turns into diagrams.

Quick Start

Here's a minimal parser plugin. This is all you need to get started:

main.py
from pathlib import Path
from pydantic import Field
from joystick_diagrams.input.profile_collection import ProfileCollection
from joystick_diagrams.plugins.plugin_interface import PluginInterface
from joystick_diagrams.plugins.plugin_settings import PluginMeta, PluginSettings


class MySettings(PluginSettings):
    source_dir: Path | None = Field(
        default=None,
        title="Source Folder",
        json_schema_extra={"is_folder": True, "default_path": "~/Saved Games"},
    )


class ParserPlugin(PluginInterface):
    plugin_meta = PluginMeta(name="My Plugin", version="1.0.0", icon_path="img/icon.ico")
    plugin_settings_model = MySettings

    def process(self) -> ProfileCollection:
        source = self.get_setting("source_dir")
        # Parse your game's config files here
        return ProfileCollection()

    def on_settings_loaded(self) -> None:
        # Rebuild internal state after settings are restored from disk
        pass

Plugin Structure

Place your plugin in the plugins/ directory:

plugins/
└── my_plugin/
    ├── __init__.py
    ├── main.py
    └── img/
        └── icon.ico

The main.py file must contain a ParserPlugin class that inherits from PluginInterface.

PluginMeta

Every plugin declares its identity via a PluginMeta instance:

FieldTypeDescription
namestrDisplay name shown in the UI
versionstrPlugin version string
icon_pathstrPath to icon, relative to the plugin directory

Plugin Settings

Define a Pydantic BaseModel subclass of PluginSettings. The framework auto-generates UI controls from your fields, persists values to disk, and computes the plugin's ready state.

Supported Field Types

Field TypeUI Control
boolCheckbox
strText input
PathFolder/file browse button (required)
Path | NoneFolder/file browse button (optional)

Path Field Options

Configure path fields via json_schema_extra:

KeyTypeDefaultDescription
is_folderboolTrueFolder dialog vs file dialog
default_pathstrnoneStarting directory for the browse dialog
extensionslist[str]noneAllowed file extensions (e.g. [".xml"])
requiredboolTruePlugin not ready until this path is set

Ready State

A plugin is automatically considered ready when every required Path field has a value. No manual implementation needed; the framework computes this from your settings model.

Lifecycle

MethodDescription
process()Called by the framework to get results. Must return a ProfileCollection.
on_settings_loaded()Called after settings are restored from disk. Override to rebuild internal state.
save_plugin_state()Handled by the framework. Don't call directly.
load_settings()Handled by the framework. Don't call directly.

Settings Access

self.get_setting("field_name")

Returns the current value of a settings field.

self.update_setting("field_name", value)

Updates a settings field and persists the change to disk.

Plugin Data Directory

self.get_plugin_data_path()

Returns the full path to the plugin's AppData directory.

self.get_plugin_data()

Returns all files and folders in the plugin's AppData directory.

Exception Helpers

Raise these from process() when path validation fails:

self.file_not_valid_exception("msg")

Raises FileNotValidError.

self.directory_not_valid_exception("msg")

Raises DirectoryNotValidError.

self.file_type_invalid("msg")

Raises FileTypeInvalidError.

Working with the Input Library

Your process() method returns a ProfileCollection. Here's how to build one:

main.py
from joystick_diagrams.input.profile_collection import ProfileCollection
from joystick_diagrams.input.button import Button
from joystick_diagrams.input.axis import Axis, AxisDirection
from joystick_diagrams.input.hat import Hat, HatDirection
from joystick_diagrams.input.axis_slider import AxisSlider

# Create a ProfileCollection (what your plugin returns)
collection = ProfileCollection()

# Create a profile (e.g. one per aircraft in DCS)
profile = collection.create_profile("A-10C II")

# Add a device (GUID, display name); returns the device object
device = profile.add_device("150D55D0-D984-11EE-8001-444553540000", "VPC Stick MT-50CM2")

# Add inputs to the device
device.create_input(Button(1), "Weapon Release")
device.create_input(Hat(1, HatDirection.D), "Trim Down")
device.create_input(Axis(AxisDirection.X), "Roll")
device.create_input(AxisSlider(1), "Zoom")

# Add a modifier (the library creates the base input if needed)
device.add_modifier_to_input(Button(1), {"Ctrl"}, "Pickle")

return collection
For the complete data model reference (all control types, device methods, modifier handling, and profile merging) see the Input Library Reference.

Cross-Device Routing

If your plugin parses a tool that re-emits one device's inputs through another (a virtual joystick, a key remapper), you can declare routes so the downstream bindings surface on the physical device the user actually pressed. This is what ties DCS bindings back to a Virpil stick when Joystick Gremlin sits between them.

from joystick_diagrams.input_routing import RouteKey, RouteTarget, InputType

profile.add_route(
    RouteKey(VJOY_GUID, InputType.BUTTON, 108),
    RouteTarget(PHYSICAL_GUID, InputType.BUTTON, 3, qualifier="Short"),
)

See the Input Routing API page for the full reference, qualifier semantics, and how the framework merges routes across profiles.

Distribution

Parser plugins ship as .zip files and install through the same flow as output plugins. The zip must contain a single folder at the root with __init__.py and main.py (which must define a ParserPlugin class subclassing PluginInterface). An optional plugin.sig file enables the signed & verified badge. See the signing notes on the Output Plugins page; the flow is identical for parser plugins.

Constraints

Plugins can only use standard library packages or dependencies already shipped with Joystick Diagrams. If you need something else, open a discussion on GitHub.

References