Generic factory-by-name registry plus a plugin loader for the five UI extension seams the phosphor-shell exposes: bar widgets, control center tiles, launcher providers, OSDs, and desktop widgets.
Phosphor already ships several hand-rolled registries (phosphor-layout-api/ILayoutSourceFactory, phosphor-tiles/AlgorithmRegistry, phosphor-shaders/ShaderRegistry, phosphor-animation/AnimationShaderRegistry). Each one was designed independently for its own domain. phosphor-registry generalises the pattern into a single template that the shell instantiates once per UI seam.
Registry<T> template, five interface specialisations. The shell owns one Registry<IBarWidgetFactory>, one Registry<IControlCenterTileFactory>, and so on. Each is independent. Tests and the demos instantiate their own local registries with fakes.registry.registerFactory(...) for every built-in. No static-init magic, no global pending list. Same pattern for plugins (the PluginLoader calls the same registerFactory).manifest.json bundles under ~/.local/share/phosphor/plugins/<id>/. The PluginLoader scans the directory, validates each manifest's ABI version, loads the .so via QLibrary, resolves a fixed C entry point, and registers the returned factory.WatchedDirectorySet (from phosphor-fsloader) watches the plugin root with a debounced rescan. Added plugin directories load, and removed directories unregister. In-place .so replacement (writing new bytes to the same path) is NOT honoured today because POSIX dlopen refcounts loads by path. Reloading the same path returns the prior mapping. Plugin authors iterating in development rename the plugin directory or restart the process. Removed plugins keep their .so mapping pinned for the loader's lifetime so widgets the old factory created keep working until they destruct naturally. A versioned-path scheme plus refcounted safe-unload is a future addition alongside the sandbox.capabilities() list (also mirrored into the plugin manifest). An enforcement layer is a future addition.| Type | Purpose |
|---|---|
PhosphorRegistry::Registry<T> | Template registry. Stores factories of type T keyed by T::id(). Instance-per-composition-root. Emits factoryRegistered / factoryUnregistered via its RegistryNotifier. |
PhosphorRegistry::RegistryNotifier | QObject signal carrier the template owns. The template itself can't be Q_OBJECT, so signals route through this. Reached via registry.notifier(). |
PhosphorRegistry::IFactoryBase | Common base for the five interface families. Declares id(), displayName(), capabilities(). |
PhosphorRegistry::IBarWidgetFactory | Factory for one top-bar widget (clock, workspaces, tray, etc.). Adds createWidget(QQmlEngine*, QObject*). |
PhosphorRegistry::IControlCenterTileFactory | Factory for one tile inside the Control Center popout. Adds createTile(QQmlEngine*, QObject*). No consumer yet, reserved for a future control-center surface. |
PhosphorRegistry::ILauncherProviderFactory | Factory for a launcher query provider (apps, calculator, etc.). Adds createProvider(QObject*). No consumer yet, reserved for a future launcher surface. |
PhosphorRegistry::IOSDFactory | Factory for an on-screen display (volume / mic / brightness). Adds createOSD(QQmlEngine*, QObject*). No consumer yet, reserved for a future OSD surface. |
PhosphorRegistry::IDesktopWidgetFactory | Factory for a desktop card / widget. Adds createWidget(QQmlEngine*, QObject*). No consumer yet, reserved for a future desktop-widget surface. |
PhosphorRegistry::Manifest | Plain-old-data mirror of manifest.json. Manifest::parse reads the file, and parseObject exists for tests. Rejects ABI mismatch, missing fields, id / directory mismatch. |
PhosphorRegistry::PluginLoader | Scans a plugin root, loads each plugin's .so + manifest, registers in a Registry<IBarWidgetFactory>. Hot-reloads on filesystem change. |
C++ shell composition root: register built-ins explicitly, then arm the plugin loader for any third-party plugins.
#include <PhosphorRegistry/PhosphorRegistry.h>
using namespace PhosphorRegistry;
Registry<IBarWidgetFactory> barRegistry;
// Built-ins: explicit register
barRegistry.registerFactory(std::make_shared<ClockFactory>());
barRegistry.registerFactory(std::make_shared<WorkspacesFactory>());
barRegistry.registerFactory(std::make_shared<MediaFactory>());
// Plugins (loaded asynchronously into the same registry):
PluginLoader loader(&barRegistry); // defaults to ~/.local/share/phosphor/plugins/
loader.scanAndLoad();
QObject::connect(barRegistry.notifier(), &RegistryNotifier::factoryRegistered,
[&](const QString& id) { qDebug() << "registered" << id; });Glue layer: the shell wraps the registry in a QObject controller that exposes the factory ids as a Q_PROPERTY(QStringList) and the widget construction as a Q_INVOKABLE. The library stays QML-free so consumers own their own facade. This controller pattern is what both phosphor-registry demos use. The snippets below name it BarController to match the production-shell shape (a top-bar widget controller). examples/phosphor-registry-demo/Main.qml wires the same pattern under the context-property name demoController to keep the demo's QML self-describing.
// barcontroller.h
class BarController : public QObject
{
Q_OBJECT
Q_PROPERTY(QStringList factoryIds READ factoryIds NOTIFY factoryIdsChanged)
public:
explicit BarController(QObject* parent = nullptr);
void setEngine(QQmlEngine* engine); // call once from main()
Q_INVOKABLE QQuickItem* createWidgetFor(const QString& id, QQuickItem* parent);
[[nodiscard]] QStringList factoryIds() const;
Q_SIGNALS:
void factoryIdsChanged();
private:
QPointer<QQmlEngine> m_engine;
PhosphorRegistry::Registry<PhosphorRegistry::IBarWidgetFactory> m_registry;
};
// barcontroller.cpp: forward the registry's signals as one factoryIdsChanged
QObject::connect(m_registry.notifier(),
&PhosphorRegistry::RegistryNotifier::factoryRegistered,
this, &BarController::factoryIdsChanged);
QObject::connect(m_registry.notifier(),
&PhosphorRegistry::RegistryNotifier::factoryUnregistered,
this, &BarController::factoryIdsChanged);
QStringList BarController::factoryIds() const
{
// Registry::ids() returns registration (insertion) order, which is
// already deterministic. We sort here only to present an alphabetical
// bar layout (a real shell would persist a user-configured order; the
// demos sort alphabetically for simplicity).
QStringList ids = m_registry.ids();
ids.sort();
return ids;
}
QQuickItem* BarController::createWidgetFor(const QString& id, QQuickItem* parent)
{
if (!m_engine) return nullptr;
auto factory = m_registry.factory(id);
if (!factory) return nullptr;
return factory->createWidget(m_engine.data(), parent);
}Bar QML: a Repeater drives factoryIds and parents each widget under its own delegate Item. Repeater destroys old delegates on model change, and QObject's parent-cascade takes the C++-owned widgets with them (no manual child.destroy(), which fails on C++-owned QQuickItems with "indestructible object" errors).
Row {
spacing: 8
Repeater {
model: barController ? barController.factoryIds : []
delegate: Item {
id: slot
required property string modelData
property Item widget: null
implicitWidth: widget ? widget.implicitWidth : 0
implicitHeight: widget ? widget.implicitHeight : 0
Component.onCompleted: {
slot.widget = barController.createWidgetFor(slot.modelData, slot)
}
}
}
}The bar refreshes automatically when plugins load or unload because factoryIdsChanged re-evaluates the Repeater's model binding. See examples/phosphor-registry-demo/ for the in-process variant and examples/phosphor-registry-plugin-demo/ for the same pattern with a third widget loaded from a separate .so.
Plugin author side: a plugin is a .so exporting one extern "C" entry point alongside a manifest.json.
// in my-plugin/myplugin.cpp
#include <PhosphorRegistry/IBarWidgetFactory.h>
class MyPluginFactory : public PhosphorRegistry::IBarWidgetFactory { /* ... */ };
extern "C" Q_DECL_EXPORT PhosphorRegistry::IBarWidgetFactory*
phosphor_registry_create_factory()
{
return new MyPluginFactory();
}
// in my-plugin/manifest.json: same directory as the .so
{
"id": "my-plugin",
"displayName": "My Plugin",
"abi": 1,
"capabilities": ["bar.widget"]
}The plugin directory's basename must equal the manifest's id. The loader enforces this so on-disk layout and registry keys stay aligned.
registerFactory of an id already in the registry is a no-op + qWarning (the first registration wins). The plugin loader hits the same path for any plugin whose id collides with an existing one.id() against the manifest's id field, and a mismatch is a refused load with a clear log message. This prevents a renamed plugin from quietly taking over another's slot..so files are enumerated with QDir::NoSymLinks, so a symlinked entry pointing outside the (user-writable) plugin root cannot smuggle a plugin tree or shared object past the basename-equals-id containment rule.dlopen, the loader refuses any .so (or plugin directory) that is group- or world-writable, the same StrictModes discipline OpenSSH and sudo apply to files they trust. A permissive mode would let another local process overwrite the code the shell is about to run. Signature / origin verification is reserved for a future sandbox.QLibrary mapping for the loader's lifetime. Widgets the now-gone plugin produced keep working until they destruct naturally. A future sandbox revisits this with refcounted safe-unload."abi" other than PhosphorRegistry::PluginAbiVersion (currently 1) are refused with a clear log message. Bumping the ABI version is reserved for changes to the C entry point signature, the factory vtable layout, or the manifest schema.capabilities() list ships in every factory and every manifest, but the loader does not enforce them yet. A future sandbox uses the same metadata slot.QtCore, QtQml. The library does not link QtGui or QtQuick. Consumers that build widgets do.phosphor-fsloader (PUBLIC link): WatchedDirectorySet + IScanStrategy back both the plugin loader's hot-reload path and the header-only MetadataPackLoader<T> template, which exposes fsloader types in its public interface, so the dependency is transitive (PUBLIC) for consumers.examples/phosphor-registry-demo/: toy bar with two built-in widgets registered explicitly. Proves the registry seam.examples/phosphor-registry-plugin-demo/: same toy bar plus a third widget loaded from a separate .so. Proves the plugin ABI and hot-reload.libs/phosphor-layout-api/ILayoutSourceFactory: the pre-existing registry pattern this library generalises.