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.