Skip to content

Event bus ​

Two-way events: plugin → host/front end uses emit_event; host → plugin uses handle_host_event. Polling host-private state is forbidden.


1. Plugin → host / front end ​

python
self.emit_event(event_name: str, payload: Any = None) -> None
  • Thread-safe, callable from any thread.
  • Bridged to the QML signal pluginEvent(plugin_id, event_name, payload: dict) (listen for it on the QML side).
  • A non-dict payload is wrapped into {"value": payload}; None becomes {}.
  • To have the front end call the plugin, use bridge.invokePluginAction / invokePluginActionAsync → the plugin's action_*.

Common event names (custom ones are fine, but the host has defined behaviour for these):

EventpayloadHost behaviour
device.switching{"device": str, "reason": str}The settings-page device row shows "switching" and disables the dropdown; reason is shown preferentially
device.ready{"device": str}Ends the "switching" state
stream_chunk / stream_donefree-form (assistant typewriter)Consumed by the plugin's own QML

Override the base-class method:

python
def handle_host_event(self, event_name: str, payload: dict) -> None: ...

Two ways to subscribe (either one, or both):

  1. mod.json:
json
"host_events": ["subtitle.updated", "pipeline.state"]
  1. At runtime:
python
host.subscribe_event(event_name: str,
                     callback: Callable[[str, dict], None] | None = None,
                     plugin_id: str | None = None) -> bool
host.unsubscribe_event(event_name: str, plugin_id: str | None = None) -> bool
host.unsubscribe_all_events(plugin_id: str | None = None) -> bool
  • When callback is omitted the plugin's handle_host_event is called back.
  • Events that were not subscribed are not pushed to that plugin; host_events: ["*"] subscribes to all.
  • On plugin shutdown the host calls unsubscribe_all_events automatically.

3. Standard host events ​

payloads are all dicts, and fields are only ever added, never removed:

EventpayloadWhen
subtitle.updated{"source", "translation", "speaker", "is_partial"}Caption changed (including the typewriter intermediate state is_partial=True)
subtitle.finalsame as above with is_partial=FalseFinalised sentence
history.appended{"id", "speaker", "transcript", "translation", "time"}A history entry is added
history.updated{"id", "speaker", "transcript", "translation"}An existing history entry is updated (e.g. the translation / latency is filled in)
pipeline.statesame shape as get_pipeline_state() + "reason"Start / stop / load / scene switch
provider.changed{"plugin_type", "plugin_id", "track"}Engine or theme switch (track looks like "inbound")
locale.changed{"locale"}UI language switch
config.changed{"plugin_id", "key", "value"}This plugin's config was changed by the UI
plugin.enabled / plugin.disabled{"plugin_id"}Any plugin enabled or disabled
hotkey.triggered{"plugin_id", "action", "key"}This plugin's hotkey was pressed (before handle_action)

Scope restriction ​

The config.changed and hotkey.triggered payloads contain this plugin's private data, so the host delivers them only to the plugin matching plugin_id — other plugins do not receive them even if they subscribed to *. All other events are broadcast by subscription.


4. Typical usage ​

python
class MyPlugin(BasePlugin):
    def initialize(self, config=None):
        super().initialize(config)
        self.host.subscribe_event("pipeline.state", self._on_pipeline)   # runtime subscription
        # or declare host_events in mod.json and only implement handle_host_event

    def _on_pipeline(self, event_name: str, payload: dict) -> None:
        self.host.log("info", f"pipeline running={payload.get('running')}")

    def handle_host_event(self, event_name: str, payload: dict) -> None:
        if event_name == "subtitle.final":
            ...

TransBox — empowering borderless cross-language communication for everyone