Skip to content

Writing a native plugin

A minimal native plugin is one class, one factory macro, one JSON file, and a short CMakeLists.txt. You can author it entirely out-of-tree against the Plugin SDK — no MTE source checkout required.

Native or Python?

This page covers native C++ plugins (compiled to a .dll/.so/.dylib and loaded by the Qt loader). If you'd rather ship a folder of .py files the editor spawns as a subprocess, see Python plugins — same PluginApi surface, different backend, no compiler required.

Draft

This walkthrough is grounded in PLUGIN_ARCHITECTURE.md; verify the exact SDK package name and mte_add_plugin() arguments against the shipped SDK before relying on them.

1. Implement IPlugin

#include <PluginApi/IPlugin.h>
#include <PluginApi/IPluginContext.h>

class HelloWorldPlugin : public MTE::plugin::IPlugin {
public:
    MTE::plugin::PluginInfo info() const override {
        return { "org.mte.helloworld", "Hello World", "1.0.0",
                 "Example Vendor", "Says hi.", MTE_PLUGIN_API_VERSION };
    }

    bool initialize(MTE::plugin::IPluginContext& ctx) override {
        m_cmd = ctx.commands().registerCommand(
            { "helloworld.sayHi", "Say Hi", "Ctrl+Alt+H", "Hello" },
            [&ctx] { ctx.log().log(MTE::plugin::LogLevel::Info, "Hi!"); });
        m_menu = ctx.menus().addItem("Plugins/Say Hi", "helloworld.sayHi");
        return true;
    }

    void shutdown() noexcept override {}   // tokens tear down automatically

private:
    MTE::plugin::CommandToken m_cmd;
    MTE::plugin::MenuToken    m_menu;
};

2. Export the factory (native Qt backend)

#include <PluginApi/Qt/PluginQtGlue.h>

class HelloWorldFactory : public QObject, public MTE::plugin::IPluginFactory {
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.mte.IPluginFactory/1" FILE "plugin.json")
    Q_INTERFACES(MTE::plugin::IPluginFactory)
public:
    std::unique_ptr<MTE::plugin::IPlugin> create() override {
        return std::make_unique<HelloWorldPlugin>();
    }
};

3. Describe it in plugin.json

{
  "id":         "org.mte.helloworld",
  "name":       "Hello World",
  "version":    "1.0.0",
  "apiVersion": 1,
  "entry":      "native",
  "requires":   [],
  "order":      600,
  "menus": [
    { "title": "Plugins", "barPriority": 570, "itemPriority": 100 }
  ]
}

The apiVersion here and the one returned by info() must both equal MTE_PLUGIN_API_VERSION, or the host refuses to load the plugin.

Two optional keys go with it:

  • apiMinorVersion (default 0) — the lowest host API minor your plugin needs. A host whose MTE_PLUGIN_API_MINOR_VERSION is lower lists the plugin with "Needs a newer version of OctoEdit" and does not load it. The host checks this manifest value before loading anything, and PluginInfo::apiMinorVersion from info() again afterwards — that one defaults to the minor of the headers you compiled against, so leave it alone unless you know you use nothing newer. See Versioning.
  • requires (default []) — the ids of plugins your plugin cannot work without, for example ["org.mte.lsp"]. Ids only: there is no version range and no optional form — a plugin you merely work better with does not go here; talk to it through events and provider registries at runtime. The editor initialises your dependencies before you, whatever order says, and shuts them down after you. When one of them is missing, disabled, needs a licence that is not active, or did not load, your plugin is not loaded at all, and Preferences ▸ Plugins says which plugin it needs and why ("Needs LSP Provider, which is disabled"). A cycle blocks every plugin in it. The same key means the same thing in a Python plugin's plugin.json.

Attributing bundled libraries — thirdParty

If your plugin bundles or links a third-party component, declare it in an optional thirdParty array so the editor can honour its attribution obligations:

"thirdParty": [
  { "name": "Qt WebEngine", "license": "LGPL-3.0",
    "url": "https://doc.qt.io/qt-6/qtwebengine-index.html",
    "file": "QtWebEngine-LGPLv3.txt" }
]

Each entry needs a name (entries without one are dropped); license, url, and file are optional. Your plugin owns the licence texts — ship them next to the plugin under Plugins/Licenses/<your-plugin-id>/, where file is the basename. The About dialog lists every installed plugin's thirdParty components and points users at that folder; the core never carries a plugin's third-party texts for it. (The bundled HtmlPreviewPlugin, which embeds QtWebEngine + Chromium, is the reference example.)

4. Build it

Use the SDK's CMake helper:

find_package(MTEPluginSDK REQUIRED)
mte_add_plugin(NAME HelloWorldPlugin
    SOURCES Src/HelloWorldPlugin.cpp
    # TRANSLATIONS Translations/HelloWorld_en.ts ...
)

Build requirements

A native plugin is loaded into the editor's process, so it has to be built with a toolchain that is binary-compatible with the editor build it targets. These are the requirements for the current release (built with Qt 6.11.2):

Windows macOS
Compiler Visual Studio 2022, MSVC toolset 14.44 or an older 14.x (VS 2015–2022 share one C++ ABI); clang-cl works too. MinGW / GCC builds cannot load. Apple clang from Xcode (Xcode 27, Apple clang 21.0.0); arm64 only
Build configuration Release, dynamic runtime (/MD, CMake's default). A Debug plugin is refused by the loader; a static-runtime (/MT) plugin crashes when memory crosses the plugin boundary Release; deployment target 14.4 (same as the editor)
Qt Qt 6.11, msvc2022_64 kit. A plugin built against an older 6.x minor loads; one built against a newer minor may not Qt 6.11, same architecture
C++ standard C++20 (the API headers use std::span and concepts) C++20
CMake 3.16+ 3.16+
Plugin API the MTE_PLUGIN_API_VERSION of the SDK that shipped with your target editor version same

Two consequences worth knowing on Windows:

  • The Visual C++ Redistributable installed on the user's machine has to be at least as new as the newest MSVC toolset used by the editor or any loaded plugin. Building your plugin with a toolset newer than the editor's raises that requirement for your users; building with the editor's toolset or an older one does not.
  • The Windows SDK version you compile against does not affect compatibility with the editor; it only decides which Windows APIs your plugin can call, and the editor supports Windows 10 1809 and later.

And on macOS:

  • The editor ships as arm64 only; an x86_64-only plugin cannot load (a universal binary is fine). Keep the deployment target at 14.4: a plugin built for a newer minimum macOS is refused by the system loader on older Macs, and the OS's own libc++ is the runtime, so no redistributable question arises.
  • Reference the Qt frameworks through @rpath so the plugin picks up the copies bundled inside OctoEdit.app. Qt kits whose frameworks carry absolute install names (Homebrew's, for example) need install_name_tool on the built plugin before shipping; otherwise the plugin looks for Qt at the build machine's path.

None of this applies to Python plugins: they run in a separate process and share no binary interface with the editor.

5. Install & run

Either zip the built library + plugin.json as <name>.mteplugin and install it via Preferences ▸ Plugins ▸ Install Plugin... (the package is validated — manifest, archive safety, and the binary's embedded metadata — before anything is touched), or drop the files into a discovered plugin directory by hand (see Architecture overview). Restart the editor; enable it in Preferences ▸ Plugins if needed.

Next