Documentation

Building plugins

Commander keeps a small core and does almost everything else through plugins: the panel you browse files in is one, so is the preview beside it, and so is the editor that takes over the window. They are ordinary Java, built against the platform SDK and shipped as a signed bundle.

Already written one? Everything published is in the plugin catalogue.

The SDK

dev.nuclr:platform-sdk is published to Maven Central and is the whole contract between a plugin and the application: a handful of interfaces, a plugin context, and the model types a panel or a viewer works with.

<dependency>
    <groupId>dev.nuclr</groupId>
    <artifactId>platform-sdk</artifactId>
    <version>5.0.0</version>
    <scope>provided</scope>
</dependency>

The scope matters: the host supplies the SDK at runtime, and a copy bundled inside your plugin would be loaded by a different classloader than the one the application hands you objects from. Keep it provided.

WhatWhich
Java25 or newer
Maven3.9 or newer
LicenceApache-2.0
Source and README github.com/nuclrdev/platform-sdk
Releases central.sonatype.com

The SDK uses Lombok internally. Since JDK 23 javac no longer runs annotation processors that merely sit on the classpath, so a plugin that uses Lombok has to opt back in - either <proc>full</proc> on the compiler plugin, or Lombok declared in <annotationProcessorPaths>.

SDK 5.0.0 changes

SDK 5.0.0 adds a host-owned credential store for secrets such as API tokens, passwords and access keys. It is deliberately separate from the ordinary settings store: credentials are persisted by the operating system, scoped to the plugin's stable ID, and are never copied into a plaintext settings file.

New APIPurpose
NuclrPluginContext.getCredentialStore() Gets the host-owned store for the current plugin. Obtaining it does not unlock or access the OS keyring.
NuclrCredentialStore.get(key) Returns an Optional<String>; empty means the key is absent.
NuclrCredentialStore.set(key, secret) Creates or replaces a nonempty secret. Whitespace in the secret is preserved.
NuclrCredentialStore.delete(key) Removes a secret; deleting a missing key is a successful no-op.
NuclrCredentialException Reports UNAVAILABLE storage separately from an ACCESS_FAILED operation.
import java.util.Optional;

import dev.nuclr.platform.NuclrCredentialException;
import dev.nuclr.platform.NuclrCredentialStore;

NuclrCredentialStore credentials = context.getCredentialStore();

try {
	Optional<String> token = credentials.get("access-token");
	credentials.set("access-token", userEnteredToken);
} catch (NuclrCredentialException e) {
	// Show a safe recovery message; never log token or secret values.
}

Credential operations may block or prompt to unlock the OS store, so call get, set and delete off the Swing event-dispatch thread. Keys must be nonblank and contain neither NUL nor |. Keep returned secrets only as long as needed: do not log them, cache them, or use settings as a plaintext fallback. Use the settings store for non-secret preferences.

Migration from 4.0.0

  1. Change the Maven dependency to 5.0.0 and set platformSdkVersion to 5.0.0 in plugin.json.
  2. Move passwords, tokens and keys out of NuclrSettings and into context.getCredentialStore().
  3. Handle NuclrCredentialException without exposing the secret, and provide a useful recovery path when the OS store is unavailable.
  4. No changes are required to the three plugin interfaces or their lifecycle unless you want to use credential storage.

The three plugin types

BaseNuclrPlugin is a sealed interface with exactly three permitted subtypes. Implement one of them - never the base directly - and declare the matching type in the manifest. Everything a plugin says about itself lives in that manifest; the interface only describes what it does.

QuickViewNuclrPlugin

Renders a preview in the quick-view pane beside the listing.

  • panel() returns the Swing component to show.
  • supports(NuclrResource) decides whether this file is yours - and may be called off the event thread, so keep it thread-safe and free of modal UI.
  • openResource(resource, cancelled) loads or refreshes it, doing the heavy work in the background and watching the cancel flag.
  • When several viewers accept the same file, the manifest's priority breaks the tie - lower wins.

FilePanelNuclrPlugin

Provides a browser pane: local drives, archives, containers, buckets, remote hosts.

  • openResource(...) returns the entries and the column names for a location; a streaming overload paints entries as they are discovered.
  • getPluginMenuItems() supplies the commands on the function-key bar, which is how a panel decides what F5 or Alt F7 mean while it has the focus.
  • getCurrentLocationDisplayText() and getSelectionSummaryText(...) feed the path and status bars.
  • openTerminal(...) optionally hands the embedded console a shell rooted at the current location.

FullscreenNuclrPlugin

Takes over the whole window - an editor or a full-window viewer.

  • panel() returns the UI and openResource(...) opens the file.
  • Whether it is read-only or not is the manifest's role: Viewer or Editor.

All three share the lifecycle on BaseNuclrPlugin - preinit(context), init(), updateTheme(...), unload() - plus focus callbacks, uuid() for instance identity, and act(...) for the actions the host and other plugins send. The context gives you the event bus, the current theme, a namespaced settings store and the locale.

Threading is the one rule worth repeating: supports(...) can run on a background thread because deciding whether you can open something may touch the filesystem or the network, and openResource(...) must do its work asynchronously, poll the supplied cancel flag, and push every UI change back onto the event-dispatch thread.

The plugin.json manifest

Every bundle carries a plugin.json at its root, and one bundle may ship several plugins. It is the only source of a plugin's identity.

{
  "schemaVersion": 1,
  "version": "1.0.0",
  "platformSdkVersion": "5.0.0",
  "plugins": [
    {
      "class": "com.example.MyQuickViewPlugin",
      "type": "QuickView",
      "id": "com.example.myplugin.quickview",
      "name": "My Quick Viewer",
      "version": "1.0.0",
      "description": "Example quick view plugin.",
      "author": "Your Name",
      "license": "Apache-2.0",
      "developer": "Community",
      "singleton": true,
      "priority": 100
    }
  ]
}
FieldMeans
classThe fully-qualified class implementing one of the three interfaces
typeQuickView, FilePanel or Fullscreen - must match the interface the class implements
idA stable unique identifier, reverse-domain style
developerOfficial or Community
singletonWhether at most one instance may exist; a multi-instance plugin must return a distinct uuid() per instance
priorityQuick view only: lower wins when several viewers accept the same file
roleFullscreen only: Viewer or Editor
name, description, author, licenseWhat the plugin manager shows
website, pageUrl, docUrlOptional links

Let Maven fill the version in - declare "version": "${project.version}" and filter only plugin.json. Bundles routinely carry icons and native libraries that must be copied byte-for-byte.

Packaging and signing

A plugin ships as a ZIP whose root holds:

plugin.json          the manifest above
my-plugin.jar        your classes
lib/                 runtime dependencies - never the SDK, which is provided
...                  any assets you need

Commander verifies a bundle's detached signature before it loads anything from it. Plugins installed from this site are signed; a bundle you build yourself is not, so it loads through the route below.

Running your own build

A bundle you built carries no signature the application can check, so three things must hold before it will load:

  1. Turn on Developer mode in Settings (Ctrl P, under Advanced). Turning it off again unloads every local plugin immediately and keeps them from loading at the next start.
  2. Accept the risk warning once - and again if its wording changes.
  3. Approve the bundle itself - open the plugin manager with F11 and use Install from file…. The approval is recorded against the SHA-256 of the exact bytes you saw, so a ZIP that merely appears in the folder is never loaded.

After that it behaves like any other plugin: it can be enabled and disabled, it is still subject to the published blacklist, and the plugin manager marks it unverified throughout. What it may not do is take an id that belongs to a signed plugin.

Building it with an AI agent

What this page teaches a person is also packaged for a coding agent, as an Agent Skill - a folder the agent reads before it writes anything, so that asking for a quick viewer for some format produces a complete, packaged, validated bundle rather than a plausible-looking sketch. It is the open skill format rather than any one vendor's, and it is Apache-2.0, like the SDK.

git clone https://github.com/nuclrdev/skills.git

Copy or symlink skills/nuclr-commander-plugins/ into whichever directory your agent reads skills from; an agent that installs straight from Git can be given the repository URL and that subdirectory instead. From then on it selects the skill by itself when you ask for a plugin, and you can name it explicitly as nuclr-commander-plugins.

Inside the folder:

  • SKILL.md - how to establish the contract, choose one of the three types, and what counts as finished.
  • A self-contained snapshot of the SDK contract, plus one reference per plugin type: the lifecycle and threading rules, the manifest, and the Maven bundle layout.
  • scripts/validate_plugin.py - a standalone checker, no Maven and no Commander install required.
python3 scripts/validate_plugin.py my-plugin --archive target/my-plugin-1.0.0.zip

The validator is worth running whether or not an agent wrote the code: it checks the SDK dependency and its scope, the manifest's schema, required fields and vocabulary, priority and role per type, id and class uniqueness, the flat ZIP layout, that no ${...} placeholder survived into the packaged manifest, that the SDK is not bundled, and that every class the manifest declares is really in the JAR. It needs Python 3.10 or newer, and exits non-zero on any error.

An agent writing the bundle changes nothing about how it loads: it is still unsigned, so it takes the three steps above, and it is still your plugin. Run the tests, install the ZIP, and confirm in the plugin manager that the build you just made is the one that loaded.

Examples on GitHub

Every panel and viewer a fresh install ships with is a plugin built against this same SDK, each published from its own repository.

File panels browser panes

Quick viewers preview panes

Fullscreen whole-window tools

These are the official plugins; the plugin catalogue lists every published one, each with its README, its versions and a link to its source.

Something here that does not match what the SDK actually does? Tell us.