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(default0) — the lowest host API minor your plugin needs. A host whoseMTE_PLUGIN_API_MINOR_VERSIONis 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, andPluginInfo::apiMinorVersionfrominfo()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, whateverordersays, 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'splugin.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
@rpathso the plugin picks up the copies bundled insideOctoEdit.app. Qt kits whose frameworks carry absolute install names (Homebrew's, for example) needinstall_name_toolon 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¶
- Plugin API overview — lifecycle, tokens, and the full service table
- Commands & menus
- Docks, tabs & status bar
- Settings pages
- Events
- Translations