Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Extending the editor

Jackdaw has two deliberate extension seams.

Custom standalone editors

jackdaw_editor exposes the same Bevy plugin group used by the official GUI:

use bevy::prelude::*;
use jackdaw_editor::prelude::*;

fn main() -> AppExit {
    App::new()
        .add_plugins(DefaultPlugins.set(editor_window_plugin()))
        .add_plugins(JackdawEditorPlugins::default())
        .run()
}

Use normal PluginGroup controls to disable or replace editor plugins and add your own. This is unrestricted compile-time Rust composition and remains the right choice for deeply customized editor distributions.

Runtime extensions

Marketplace extensions use the focused jackdaw_extension crate:

#![allow(unused)]
fn main() {
use jackdaw_extension::prelude::*;

#[derive(Default)]
pub struct MyTool;

impl JackdawExtension for MyTool {
    fn id(&self) -> String { "example.my-tool".into() }
    fn label(&self) -> String { "My Tool".into() }

    fn register(&self, registrar: &mut ExtensionRegistrar<'_>) {
        // register operators, panels, menus, keymaps, and host-owned state
    }
}
}

Everything installed through ExtensionRegistrar is owned by that extension. Disable, update, and uninstall remove those registrations immediately. Superseded native libraries remain safely mapped but unreachable until the process exits.

While developing, open the extension project in Jackdaw. The editor builds it against the extension SDK, building the SDK first if this install has none, and loads it. Rebuild Project builds and loads it again after a change, and so does jd build in a terminal while the project is open, unless Hot Reload is turned off in the File menu.

Adding Bevy plugins at launch

register runs against a live world, so it cannot add plugin groups, schedules or system sets. An extension that needs them returns a Bevy plugin from app_plugin, and the editor adds it to its App before the app runs:

#![allow(unused)]
fn main() {
use bevy::prelude::*;
use jackdaw_extension::prelude::*;

#[derive(Default)]
pub struct VehicleTools;

impl JackdawExtension for VehicleTools {
    fn id(&self) -> String { "example.vehicle-tools".into() }

    fn app_plugin(&self) -> Option<Box<dyn Plugin>> {
        Some(Box::new(|app: &mut App| {
            app.add_systems(FixedUpdate, simulate_suspension);
        }))
    }

    fn register(&self, registrar: &mut ExtensionRegistrar<'_>) {
        // windows, operators and menus that use what the plugin added
    }
}
}

The plugin is added only for extensions enabled when the editor starts, both built-in ones and installed bundles. A plugin cannot be removed again, so:

  • Installing or enabling such an extension while the editor runs marks it Restart required in the Extensions dialog. It activates on the next launch.
  • Disabling it removes its windows and operators at once; its plugin keeps running until the editor restarts.
  • Loading a new build of it reloads live when its plugin keeps the same Plugin::name. The plugin added at launch stays in place; a renamed or newly added plugin waits for a restart. A closure plugin is named after the function that contains it.
  • An editor build of the open project cannot add a plugin at launch. Install the extension as a bundle once, restart, and later builds that keep the plugin reload live.

Guard plugins another extension may already have added, such as a physics engine, with app.is_plugin_added::<T>(). jd extension pack records requires_restart in the bundle manifest when the extension defines app_plugin, so tools can show the restart without loading the library.

Input types

Bind an operator with bind_operator_host and a PresetInput; the keymap attaches the binding and the user can rebind it. An extension that declares its own actions takes the types from jackdaw_api::input: an entity with InputContext holds actions (ActionOf), each action holds bindings (BindingOf) with a Condition, and a firing action triggers Fire on its entity. Since 0.20 the editor no longer uses bevy_enhanced_input, and jackdaw_api::prelude no longer re-exports it; operators no longer implement InputAction.

Signed bundles

Create one publisher key, once:

jd extension keygen

That writes publisher-key.pk8 into the Jackdaw data directory and refuses to overwrite an existing one, because publishing an update under a new key makes every user repeat the trust decision. Pass a path to keep it elsewhere.

Then, in the extension project, build and pack:

jd build
jd extension pack
jd extension verify my-tool-0.1.0-x86_64-unknown-linux-gnu.jdext

pack reads the bundle’s identity, version, publisher, license, and homepage from the project’s Cargo.toml:

[package]
name = "my-tool"
version = "0.1.0"
authors = ["Example Studio <hello@example.com>"]
license = "MIT OR Apache-2.0"
repository = "https://example.com/my-tool"

[package.metadata.jackdaw]
label = "My Tool"

Any of those can be overridden with --id, --label, --version, --publisher, --license, --homepage, --key, --out, and --library.

Build and pack separately on Linux, Windows, and macOS. A bundle records the target triple and the SDK ABI string it was built against, and installs only into a Jackdaw that matches both, so publish one bundle per target per Jackdaw release.

Users install signed .jdext bundles from Extensions or with:

jd extension install my-tool.jdext
jd extension list
jd extension disable example.my-tool
jd extension enable example.my-tool
jd extension uninstall example.my-tool

The manifest and signature are checked before native code is loaded. Bundles must match the exact Jackdaw SDK ABI and target.

An extension is built as a cdylib that links the editor’s SDK dynamically, so it shares the editor’s Bevy and exports only its entry points, however large its dependencies (a Windows DLL holds at most 65,535 exports). The build embeds the crate hash of the SDK it was compiled against, and the editor opens an extension library only when that hash is its own SDK’s. One built by another Jackdaw, or by a Jackdaw from before the hash was embedded, is refused with a message to rebuild it. Trusting a publisher is an explicit confirmation because native extensions run with the user’s full permissions. Updates are staged by version and activated atomically; a failed activation leaves the previous version available for recovery. Inter-extension dependencies are not supported by this bundle format.

Distribution

Jackdaw does not host a registry. It provides the pieces an external one needs: a signed bundle format, a compatibility key, and installation straight from a URL.

Publish .jdext files wherever you like, and users install them with:

jd extension install https://example.com/my-tool-0.1.0-x86_64-unknown-linux-gnu.jdext

A URL and a local path go through the same gate. The signature, the library checksum, the ABI and target match, and the publisher trust prompt all run on the fetched bytes exactly as they do on a file, so a remote install is never less checked than a local one. Plain http:// is refused.

Serving the right artifact

A bundle only installs into a Jackdaw whose compatibility key matches, so a marketplace has to know the client’s before it offers a download. Ask it:

$ jd extension abi --json
{"sdk_abi":"jackdaw-0.19.0-bevy-0.19-rustc 1.90.0","target":"x86_64-unknown-linux-gnu",
 "jackdaw":"0.19.0","bevy":"0.19"}

sdk_abi covers the Jackdaw version, the Bevy minor, and the exact rustc that built the SDK; target is the platform triple. Key your catalogue on both. In practice that means one bundle per target per Jackdaw release, rebuilt and re-signed when Jackdaw updates. jd extension verify reports what a given bundle claims, without installing it.