Add omarchy-shell with a plugin registry

This commit is contained in:
Ryan Hughes
2026-05-14 02:21:48 -04:00
parent 619a0d5737
commit c72a8756b3
44 changed files with 582 additions and 60 deletions
+1 -1
View File
@@ -252,7 +252,7 @@ stop_screenrecording() {
toggle_screenrecording_indicator() {
pkill -RTMIN+8 waybar
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
quickshell ipc -p "$OMARCHY_PATH/default/quickshell/bar" call bar refreshScreenRecording >/dev/null 2>&1 || true
quickshell ipc -p "$OMARCHY_PATH/default/quickshell/omarchy-shell" call bar refreshScreenRecording >/dev/null 2>&1 || true
}
screenrecording_active() {
+1 -1
View File
@@ -56,7 +56,7 @@ PY
omarchy-restart-waybar
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
if quickshell list -p "$OMARCHY_PATH/default/quickshell/bar" 2>/dev/null | grep -q '^Instance '; then
if quickshell list -p "$OMARCHY_PATH/default/quickshell/omarchy-shell" 2>/dev/null | grep -q '^Instance '; then
omarchy-restart-bar
fi
omarchy-restart-swayosd
+1 -1
View File
@@ -4,7 +4,7 @@
# omarchy:examples=omarchy restart bar
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
CONFIG_DIR="$OMARCHY_PATH/default/quickshell/bar"
CONFIG_DIR="$OMARCHY_PATH/default/quickshell/omarchy-shell"
quickshell kill -p "$CONFIG_DIR" >/dev/null 2>&1 || true
setsid uwsm-app -- env OMARCHY_PATH="$OMARCHY_PATH" quickshell -p "$CONFIG_DIR" >/dev/null 2>&1 &
+1 -1
View File
@@ -54,7 +54,7 @@ if pgrep -x waybar >/dev/null; then
omarchy-restart-waybar
fi
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
if quickshell list -p "$OMARCHY_PATH/default/quickshell/bar" 2>/dev/null | grep -q '^Instance '; then
if quickshell list -p "$OMARCHY_PATH/default/quickshell/omarchy-shell" 2>/dev/null | grep -q '^Instance '; then
omarchy-restart-bar
fi
omarchy-restart-swayosd
+1 -1
View File
@@ -4,7 +4,7 @@
# omarchy:examples=omarchy toggle bar
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
CONFIG_DIR="$OMARCHY_PATH/default/quickshell/bar"
CONFIG_DIR="$OMARCHY_PATH/default/quickshell/omarchy-shell"
omarchy-toggle bar-off
+1 -1
View File
@@ -12,4 +12,4 @@ fi
pkill -RTMIN+9 waybar
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
quickshell ipc -p "$OMARCHY_PATH/default/quickshell/bar" call bar refreshIndicators >/dev/null 2>&1 || true
quickshell ipc -p "$OMARCHY_PATH/default/quickshell/omarchy-shell" call bar refreshIndicators >/dev/null 2>&1 || true
+1 -1
View File
@@ -12,4 +12,4 @@ fi
pkill -RTMIN+10 waybar
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
quickshell ipc -p "$OMARCHY_PATH/default/quickshell/bar" call bar refreshIndicators >/dev/null 2>&1 || true
quickshell ipc -p "$OMARCHY_PATH/default/quickshell/omarchy-shell" call bar refreshIndicators >/dev/null 2>&1 || true
+1 -1
View File
@@ -4,5 +4,5 @@
pkill -RTMIN+7 waybar
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
quickshell ipc -p "$OMARCHY_PATH/default/quickshell/bar" call bar refreshUpdate >/dev/null 2>&1 || true
quickshell ipc -p "$OMARCHY_PATH/default/quickshell/omarchy-shell" call bar refreshUpdate >/dev/null 2>&1 || true
exit 0
+1 -1
View File
@@ -22,7 +22,7 @@ if gum confirm "Install Voxtype + AI model (~150MB) to enable dictation?"; then
omarchy-restart-waybar
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
if quickshell list -p "$OMARCHY_PATH/default/quickshell/bar" 2>/dev/null | grep -q '^Instance '; then
if quickshell list -p "$OMARCHY_PATH/default/quickshell/omarchy-shell" 2>/dev/null | grep -q '^Instance '; then
omarchy-restart-bar
fi
notify-send " Voxtype Dictation Ready" "Hold F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000
+1 -1
View File
@@ -7,6 +7,6 @@ set -e
omarchy-launch-floating-terminal-with-presentation "voxtype setup model"
omarchy-restart-waybar
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
if quickshell list -p "$OMARCHY_PATH/default/quickshell/bar" 2>/dev/null | grep -q '^Instance '; then
if quickshell list -p "$OMARCHY_PATH/default/quickshell/omarchy-shell" 2>/dev/null | grep -q '^Instance '; then
omarchy-restart-bar
fi
+1 -1
View File
@@ -1,7 +1,7 @@
hl.on("hyprland.start", function()
hl.exec_cmd("uwsm-app -- hypridle")
hl.exec_cmd("uwsm-app -- mako")
hl.exec_cmd("! omarchy-toggle-enabled bar-off && uwsm-app -- env OMARCHY_PATH=$HOME/.local/share/omarchy quickshell -p $HOME/.local/share/omarchy/default/quickshell/bar")
hl.exec_cmd("! omarchy-toggle-enabled bar-off && uwsm-app -- env OMARCHY_PATH=$HOME/.local/share/omarchy quickshell -p $HOME/.local/share/omarchy/default/quickshell/omarchy-shell")
hl.exec_cmd("uwsm-app -- fcitx5 --disable notificationitem")
hl.exec_cmd("uwsm-app -- swaybg -i ~/.config/omarchy/current/background -m fill")
hl.exec_cmd("/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1")
+2 -2
View File
@@ -18,7 +18,7 @@ ShellRoot {
}
property string omarchyPath: deriveOmarchyPath()
readonly property string userConfigPath: home + "/.config/omarchy/bar.json"
readonly property string defaultsPath: omarchyPath + "/default/quickshell/bar/bar-defaults.json"
readonly property string defaultsPath: omarchyPath + "/default/quickshell/omarchy-shell/plugins/bar/bar-defaults.json"
property color foreground: "#cacccc"
property color background: "#101315"
@@ -29,7 +29,7 @@ ShellRoot {
// Bundled fallback so 'Reset to defaults' never produces an empty bar even
// if bar-defaults.json fails to load. Keep in rough sync with the layout
// shipped in default/quickshell/bar/bar-defaults.json.
// shipped in default/quickshell/omarchy-shell/plugins/bar/bar-defaults.json.
readonly property var builtinBarConfig: ({
position: "top",
fontFamily: "JetBrainsMono Nerd Font",
@@ -10,22 +10,15 @@ import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
ShellRoot {
import "../../services" as Services
Item {
id: root
// The omarchy-shell host injects omarchyPath when it instantiates this Bar.
// Default fallback keeps the file loadable in isolation (e.g. for QML tooling).
required property string omarchyPath
property string home: Quickshell.env("HOME")
// Derive omarchyPath from the loaded shell.qml's directory so the bar finds
// its own widgets/defaults regardless of where it was launched. shellDir is
// `<omarchyPath>/default/quickshell/bar` so strip three trailing segments.
function deriveOmarchyPath() {
var env = Quickshell.env("OMARCHY_PATH")
if (env) return env
var dir = String(Quickshell.shellDir || "")
if (dir.indexOf("/default/quickshell/bar") !== -1)
return dir.substring(0, dir.indexOf("/default/quickshell/bar"))
return home + "/.local/share/omarchy"
}
property string omarchyPath: deriveOmarchyPath()
property string omarchyConfigDir: home + "/.config/omarchy"
property var builtinBarConfig: ({
position: "top",
@@ -305,33 +298,69 @@ ShellRoot {
return source ? fileUrl(source) : ""
}
readonly property var firstPartyWidgets: ({
"media": true,
"audioPanel": true,
"networkPanel": true,
"bluetoothPanel": true,
"calendar": true,
"notificationCenter": true,
"brightness": true,
"powerProfile": true,
"systemStats": true,
"weatherFlyout": true,
"workspacesPro": true,
"powerMenu": true,
"idleInhibitor": true,
"microphone": true,
"notificationCenter": true,
"activeWindow": true,
"nightLight": true,
"keyboardLayout": true,
"lockKeys": true,
"spacer": true,
"controlCenter": true
// First-party widgets are registered with the BarWidgetRegistry at startup.
// Each entry maps a widget id to its display metadata; the QML source lives
// at widgets/<id>.qml and is loaded asynchronously via Qt.createComponent.
readonly property var firstPartyWidgetMetadata: ({
"media": { displayName: "Media", description: "MPRIS now-playing with playback controls", category: "Media", allowMultiple: false },
"audioPanel": { displayName: "Audio", description: "Volume slider, output picker, per-app mixer", category: "Audio", allowMultiple: false },
"networkPanel": { displayName: "Network", description: "Wi-Fi list and connection state", category: "Network", allowMultiple: false },
"bluetoothPanel": { displayName: "Bluetooth", description: "Bluetooth device list with connect/disconnect", category: "Network", allowMultiple: false },
"calendar": { displayName: "Calendar", description: "Clock with month-grid popup", category: "Time", allowMultiple: false },
"notificationCenter": { displayName: "Notification center", description: "Recent notifications + DND (replaces mako)", category: "Status", allowMultiple: false },
"brightness": { displayName: "Brightness", description: "Screen brightness slider", category: "System", allowMultiple: false },
"powerProfile": { displayName: "Power profile", description: "power-profiles-daemon selector", category: "System", allowMultiple: false },
"systemStats": { displayName: "System stats", description: "Inline CPU + memory sparklines", category: "System", allowMultiple: false },
"weatherFlyout": { displayName: "Weather", description: "Weather pill with detail popup", category: "Info", allowMultiple: false },
"workspacesPro": { displayName: "Workspaces", description: "Animated workspace switcher", category: "Compositor", allowMultiple: false },
"powerMenu": { displayName: "Power menu", description: "Lock / suspend / reboot / shutdown", category: "System", allowMultiple: false },
"idleInhibitor": { displayName: "Keep awake", description: "Toggle idle inhibitor", category: "System", allowMultiple: false },
"microphone": { displayName: "Microphone", description: "Mic input state and mute toggle", category: "Audio", allowMultiple: false },
"activeWindow": { displayName: "Active window", description: "Title of the focused window", category: "Compositor", allowMultiple: false },
"nightLight": { displayName: "Night light", description: "hyprsunset toggle", category: "System", allowMultiple: false },
"keyboardLayout": { displayName: "Keyboard layout", description: "Current xkb layout, click cycles", category: "Compositor", allowMultiple: false },
"lockKeys": { displayName: "Lock keys", description: "Caps / Num / Scroll lock indicators", category: "System", allowMultiple: false },
"spacer": { displayName: "Spacer", description: "Configurable blank space", category: "Layout", allowMultiple: true },
"controlCenter": { displayName: "Quick settings", description: "Volume / brightness / DND in one popup", category: "System", allowMultiple: false }
})
function firstPartyWidgetSource(name) {
if (!firstPartyWidgets[String(name)]) return ""
return Qt.resolvedUrl("widgets/" + String(name) + ".qml")
property var registeredFirstPartyComponents: ({})
Component.onCompleted: registerFirstPartyWidgets()
function registerFirstPartyWidgets() {
var ids = Object.keys(firstPartyWidgetMetadata)
for (var i = 0; i < ids.length; i++) {
var id = ids[i]
if (Services.BarWidgetRegistry.has(id)) continue
registerOneFirstPartyWidget(id)
}
}
function registerOneFirstPartyWidget(id) {
var url = Qt.resolvedUrl("widgets/" + id + ".qml")
var meta = firstPartyWidgetMetadata[id] || {}
var enrichedMeta = {
displayName: meta.displayName || id,
description: meta.description || "",
category: meta.category || "Misc",
allowMultiple: meta.allowMultiple === true,
source: "first-party"
}
var comp = Qt.createComponent(url, Component.Asynchronous)
function finalize() {
if (comp.status === Component.Ready) {
Services.BarWidgetRegistry.register(id, comp, enrichedMeta)
var next = ({})
for (var k in registeredFirstPartyComponents) next[k] = registeredFirstPartyComponents[k]
next[id] = comp
registeredFirstPartyComponents = next
} else if (comp.status === Component.Error) {
console.warn("first-party widget " + id + " failed to load: " + comp.errorString())
}
}
if (comp.status === Component.Loading) comp.statusChanged.connect(finalize)
else finalize()
}
function networkCommand() {
@@ -1146,11 +1175,22 @@ ShellRoot {
readonly property var moduleSettings: root.entrySettings(entry)
readonly property string customType: root.customModuleType(entry)
readonly property var builtinComponent: customType ? null : root.builtinModuleComponent(moduleName)
readonly property string firstPartySource: customType || builtinComponent ? "" : root.firstPartyWidgetSource(moduleName)
// Re-evaluate when the registry mutates (Component reference changes,
// plugin enabled/disabled, etc.). Reading the `widgets` property creates
// the binding dependency the wrapped function call alone wouldn't.
readonly property var registryComponent: {
var w = Services.BarWidgetRegistry.widgets
if (customType || builtinComponent) return null
return w[moduleName] ? w[moduleName].component : null
}
readonly property bool qmlCustom: customType === "qml"
readonly property bool commandCustom: customType === "command"
readonly property bool firstParty: firstPartySource !== ""
readonly property var activeItem: qmlCustom || firstParty ? qmlLoader.item : componentLoader.item
readonly property bool registered: registryComponent !== null
readonly property var activeItem: {
if (registered) return registryLoader.item
if (qmlCustom) return qmlLoader.item
return componentLoader.item
}
implicitWidth: activeItem && activeItem.visible ? activeItem.implicitWidth : 0
implicitHeight: activeItem && activeItem.visible ? activeItem.implicitHeight : 0
@@ -1159,15 +1199,23 @@ ShellRoot {
Loader {
id: componentLoader
active: !slot.qmlCustom && !slot.firstParty
active: !slot.qmlCustom && !slot.registered
sourceComponent: slot.builtinComponent || (slot.commandCustom ? customCommandModuleComponent : emptyModuleComponent)
anchors.fill: parent
}
Loader {
id: registryLoader
active: slot.registered
sourceComponent: slot.registered ? slot.registryComponent : null
anchors.fill: parent
onLoaded: slot.injectProps()
}
Loader {
id: qmlLoader
active: slot.qmlCustom || slot.firstParty
source: slot.qmlCustom ? root.customModuleSource(slot.entry) : (slot.firstParty ? slot.firstPartySource : "")
active: slot.qmlCustom
source: slot.qmlCustom ? root.customModuleSource(slot.entry) : ""
anchors.fill: parent
onLoaded: slot.injectProps()
}
@@ -1175,7 +1223,7 @@ ShellRoot {
onModuleSettingsChanged: injectProps()
function injectProps() {
var target = qmlLoader.item
var target = registryLoader.item || qmlLoader.item
if (!target) return
if ("bar" in target) target.bar = root
if ("moduleName" in target) target.moduleName = moduleName
@@ -2,7 +2,7 @@
This is the Quickshell implementation of the Omarchy status bar.
- `shell.qml` is Omarchy-owned bar engine code. Users should not edit it directly.
- `Bar.qml` is Omarchy-owned bar engine code, loaded by the omarchy-shell host. Users should not edit it directly.
- `bar-defaults.json` is the Omarchy-owned default layout and module settings.
- `widgets/` holds first-party widgets — modular, interactive components shipped with Omarchy.
- `common/` holds shared QML helpers (buttons, sliders, popup cards).
@@ -11,7 +11,7 @@ This is the Quickshell implementation of the Omarchy status bar.
## Customizing
The bar reads `~/.local/share/omarchy/default/quickshell/bar/bar-defaults.json`, then deep-merges `~/.config/omarchy/bar.json` on top of it. Each `layout.{left,center,right}` entry is an object: at minimum `{ "id": "<widget>" }`, plus any inline settings the widget reads.
The bar reads `~/.local/share/omarchy/default/quickshell/omarchy-shell/plugins/bar/bar-defaults.json`, then deep-merges `~/.config/omarchy/bar.json` on top of it. Each `layout.{left,center,right}` entry is an object: at minimum `{ "id": "<widget>" }`, plus any inline settings the widget reads.
Launch the visual editor with `omarchy launch bar-settings` (or run `omarchy-launch-bar-settings`) to reorder widgets, add/remove them, and tweak per-widget options without editing JSON by hand.
@@ -0,0 +1,11 @@
{
"schemaVersion": 1,
"id": "omarchy.bar",
"name": "Bar",
"version": "1.0.0",
"author": "Omarchy",
"description": "Status bar with widgets",
"kinds": ["bar"],
"activation": "persistent",
"entryPoints": { "bar": "Bar.qml" }
}
@@ -0,0 +1,52 @@
pragma Singleton
import QtQuick
QtObject {
id: registry
// { widgetId: { component: Component, metadata: var } }
property var widgets: ({})
property int revision: 0
signal changed()
function register(id, component, metadata) {
var key = String(id)
if (!key) return
var next = {}
for (var k in widgets) next[k] = widgets[k]
next[key] = { component: component, metadata: metadata || {} }
widgets = next
revision++
changed()
}
function unregister(id) {
var key = String(id)
if (!widgets[key]) return
var next = {}
for (var k in widgets) if (k !== key) next[k] = widgets[k]
widgets = next
revision++
changed()
}
function componentFor(id) {
var entry = widgets[String(id)]
return entry ? entry.component : null
}
function metadataFor(id) {
var entry = widgets[String(id)]
return entry ? entry.metadata : null
}
function availableIds() {
return Object.keys(widgets)
}
function has(id) {
return widgets[String(id)] !== undefined
}
}
@@ -0,0 +1,248 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
QtObject {
id: registry
property string home: Quickshell.env("HOME")
property string pluginsDir: home + "/.config/omarchy/plugins"
property string stateFile: home + "/.config/omarchy/plugins.json"
// Set by shell.qml at startup so we can also scan bundled first-party plugins.
property string firstPartyDir: ""
// { pluginId: manifest } — manifests have __sourceDir and __isFirstParty stamped in.
property var installedPlugins: ({})
// { pluginId: { enabled: bool } } — persisted to stateFile.
property var pluginStates: ({})
property int registryRevision: 0
property bool scanning: false
signal pluginsChanged()
signal pluginLoadFailed(string id, string error)
// ---------------------------------------------------------------- helpers
function isPlainObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value)
}
function fileUrl(path) {
return "file://" + String(path).split("/").map(encodeURIComponent).join("/")
}
function validateManifest(manifest, sourcePath) {
if (!isPlainObject(manifest)) {
console.warn("PluginRegistry: manifest is not an object at " + sourcePath)
return null
}
if (manifest.schemaVersion !== 1) {
console.warn("PluginRegistry: unsupported schemaVersion at " + sourcePath)
return null
}
var required = ["id", "name", "version", "kinds", "entryPoints"]
for (var i = 0; i < required.length; i++) {
if (manifest[required[i]] === undefined) {
console.warn("PluginRegistry: missing required field '" + required[i] + "' at " + sourcePath)
return null
}
}
var id = String(manifest.id)
if (!id || id.indexOf("/") !== -1 || id.indexOf("..") !== -1 || id.charAt(0) === "/") {
console.warn("PluginRegistry: invalid plugin id '" + id + "' at " + sourcePath)
return null
}
if (!Array.isArray(manifest.kinds) || manifest.kinds.length === 0) {
console.warn("PluginRegistry: kinds must be a non-empty array at " + sourcePath)
return null
}
if (!isPlainObject(manifest.entryPoints)) {
console.warn("PluginRegistry: entryPoints must be an object at " + sourcePath)
return null
}
return manifest
}
function entryPointUrl(manifest, kind) {
if (!isPlainObject(manifest)) return ""
var ep = manifest.entryPoints ? manifest.entryPoints[kind] : null
if (!ep) return ""
var dir = manifest.__sourceDir || ""
if (!dir) return ""
return fileUrl(dir + "/" + ep)
}
function isEnabled(id) {
var state = pluginStates[String(id)]
return !!(state && state.enabled)
}
function setEnabled(id, value) {
var key = String(id)
var next = {}
for (var k in pluginStates) next[k] = pluginStates[k]
next[key] = { enabled: !!value }
pluginStates = next
persistStates()
registryRevision++
pluginsChanged()
}
function manifestsOfKind(kind) {
var result = []
for (var id in installedPlugins) {
var m = installedPlugins[id]
if (m && Array.isArray(m.kinds) && m.kinds.indexOf(kind) !== -1) result.push(m)
}
return result
}
// ---------------------------------------------------------------- persistence
property bool suppressStateReload: false
function persistStates() {
suppressStateReload = true
stateFileView.setText(JSON.stringify({ version: 1, states: pluginStates }, null, 2) + "\n")
}
property FileView stateFileView: FileView {
path: registry.stateFile
watchChanges: true
atomicWrites: true
printErrors: false
onLoaded: {
if (registry.suppressStateReload) {
registry.suppressStateReload = false
return
}
try {
var data = JSON.parse(text() || "{}")
registry.pluginStates = (data && data.states) || {}
} catch (e) {
console.warn("PluginRegistry: bad plugins.json:", e)
registry.pluginStates = {}
}
}
onFileChanged: reload()
}
// ---------------------------------------------------------------- scanning
// Output format produced by the rescan script:
// ===<kind>::<absolute-source-dir>===
// ... raw manifest.json content ...
// === EOM ===
// (repeating for every manifest found)
function parseScanOutput(text) {
var lines = String(text || "").split("\n")
var firstParty = {}
var thirdParty = {}
var currentSource = null
var currentKind = null
var currentJson = []
function flush() {
if (!currentSource) return
var raw = currentJson.join("\n").trim()
try {
var manifest = JSON.parse(raw)
manifest.__sourceDir = currentSource
manifest.__isFirstParty = (currentKind === "firstparty")
var validated = validateManifest(manifest, currentSource + "/manifest.json")
if (validated) {
if (currentKind === "firstparty") firstParty[validated.id] = validated
else thirdParty[validated.id] = validated
}
} catch (e) {
console.warn("PluginRegistry: bad manifest at " + currentSource + ": " + e)
}
currentSource = null
currentKind = null
currentJson = []
}
for (var i = 0; i < lines.length; i++) {
var line = lines[i]
var startMatch = line.match(/^===([a-z]+)::(.+)===$/)
if (startMatch) {
flush()
currentKind = startMatch[1]
currentSource = startMatch[2].replace(/\/$/, "")
currentJson = []
continue
}
if (line === "=== EOM ===") {
flush()
continue
}
if (currentSource) currentJson.push(line)
}
flush()
// Merge defaults into pluginStates: first-party defaults to enabled, third-party to disabled.
var nextStates = {}
for (var k in pluginStates) nextStates[k] = pluginStates[k]
for (var fpid in firstParty) {
if (!nextStates[fpid]) nextStates[fpid] = { enabled: true }
}
for (var tpid in thirdParty) {
if (!nextStates[tpid]) nextStates[tpid] = { enabled: false }
}
var merged = {}
for (var fk in firstParty) merged[fk] = firstParty[fk]
for (var tk in thirdParty) merged[tk] = thirdParty[tk]
pluginStates = nextStates
installedPlugins = merged
registryRevision++
scanning = false
pluginsChanged()
}
property Process scanProcess: Process {
onExited: function(exitCode) {
var output = scanStdout.text || ""
registry.parseScanOutput(output)
}
stdout: StdioCollector {
id: scanStdout
waitForEnd: true
}
}
property Process initProcess: Process {
onExited: registry.rescan()
}
function rescan() {
if (scanning) return
scanning = true
// $0 = first-party dir, $1 = third-party dir. Some bash versions need the explicit -- separator.
var script = ""
+ "scan() { local dir=\"$1\"; local kind=\"$2\"; "
+ " [[ -d \"$dir\" ]] || return 0; "
+ " for sub in \"$dir\"/*/; do "
+ " [[ -f \"$sub/manifest.json\" ]] || continue; "
+ " printf '===%s::%s===\\n' \"$kind\" \"$sub\"; "
+ " cat \"$sub/manifest.json\"; "
+ " printf '\\n=== EOM ===\\n'; "
+ " done; "
+ "}; "
+ "scan \"$0\" firstparty; "
+ "scan \"$1\" thirdparty"
scanProcess.command = ["bash", "-lc", script, registry.firstPartyDir, registry.pluginsDir]
scanProcess.running = true
}
function ensureUserDir() {
initProcess.command = ["bash", "-lc", "mkdir -p \"$0\"", registry.pluginsDir]
initProcess.running = true
}
Component.onCompleted: ensureUserDir()
}
@@ -0,0 +1,3 @@
module omarchy.services
singleton PluginRegistry 1.0 PluginRegistry.qml
singleton BarWidgetRegistry 1.0 BarWidgetRegistry.qml
+160
View File
@@ -0,0 +1,160 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "plugins/bar"
import "services" as Services
ShellRoot {
id: shell
property string home: Quickshell.env("HOME")
// The omarchy-shell host is the long-running entry point. Plugins live in
// sibling directories under plugins/. Most child code (the bar, future
// plugins) accept omarchyPath as a required property; resolve it once here
// from the shellDir so we don't depend on OMARCHY_PATH being set in the env.
function deriveOmarchyPath() {
var env = Quickshell.env("OMARCHY_PATH")
if (env) return env
var dir = String(Quickshell.shellDir || "")
if (dir.indexOf("/default/quickshell/omarchy-shell") !== -1)
return dir.substring(0, dir.indexOf("/default/quickshell/omarchy-shell"))
return home + "/.local/share/omarchy"
}
property string omarchyPath: deriveOmarchyPath()
readonly property string firstPartyPluginsDir: omarchyPath + "/default/quickshell/omarchy-shell/plugins"
Component.onCompleted: {
console.log("omarchy-shell paths",
"omarchyPath=" + shell.omarchyPath,
"shellDir=" + Quickshell.shellDir,
"firstPartyPluginsDir=" + shell.firstPartyPluginsDir)
Services.PluginRegistry.firstPartyDir = shell.firstPartyPluginsDir
// PluginRegistry.ensureUserDir() runs in its own Component.onCompleted and
// chains rescan() once the directory exists. We also kick a scan here in
// case the user dir already existed at startup.
Services.PluginRegistry.rescan()
}
Bar {
id: bar
omarchyPath: shell.omarchyPath
}
// ---------------------------------------------------------- plugin loader
// Mirror plugin registry state into BarWidgetRegistry whenever it changes.
// Each enabled plugin with kind "bar-widget" gets a Component created from
// its manifest entry point and registered under the id "plugin:<id>".
Connections {
target: Services.PluginRegistry
function onPluginsChanged() { shell.syncPluginWidgets() }
}
property var pluginWidgetComponents: ({})
function syncPluginWidgets() {
var plugins = Services.PluginRegistry.installedPlugins
var seen = ({})
for (var pluginId in plugins) {
var manifest = plugins[pluginId]
if (!manifest || !manifest.kinds || manifest.kinds.indexOf("bar-widget") === -1) continue
if (!Services.PluginRegistry.isEnabled(pluginId)) continue
var registryKey = "plugin:" + manifest.id
seen[registryKey] = true
// Already loaded with matching source — leave it alone.
var existing = pluginWidgetComponents[registryKey]
var url = Services.PluginRegistry.entryPointUrl(manifest, "barWidget")
if (!url) {
console.warn("Plugin " + manifest.id + " has no barWidget entry point")
continue
}
if (existing && existing.url === url && Services.BarWidgetRegistry.has(registryKey)) continue
var meta = manifest.barWidget || {}
meta = {
displayName: meta.displayName || manifest.name,
description: meta.description || manifest.description,
category: meta.category || "Plugin",
allowMultiple: meta.allowMultiple === true,
defaults: meta.defaults || {},
schema: meta.schema || [],
pluginId: manifest.id,
source: "plugin"
}
loadPluginWidget(registryKey, url, meta)
}
// Drop registrations for plugins that are no longer present or enabled.
var allIds = Services.BarWidgetRegistry.availableIds()
for (var i = 0; i < allIds.length; i++) {
var id = allIds[i]
if (id.indexOf("plugin:") !== 0) continue
if (!seen[id]) {
Services.BarWidgetRegistry.unregister(id)
var next = ({})
for (var k in pluginWidgetComponents) if (k !== id) next[k] = pluginWidgetComponents[k]
pluginWidgetComponents = next
}
}
}
function loadPluginWidget(registryKey, url, meta) {
var comp = Qt.createComponent(url, Component.Asynchronous)
function finalize() {
if (comp.status === Component.Ready) {
Services.BarWidgetRegistry.register(registryKey, comp, meta)
var next = ({})
for (var k in pluginWidgetComponents) next[k] = pluginWidgetComponents[k]
next[registryKey] = { url: url, component: comp }
pluginWidgetComponents = next
} else if (comp.status === Component.Error) {
console.warn("Plugin widget " + registryKey + " failed: " + comp.errorString())
Services.PluginRegistry.pluginLoadFailed(registryKey, comp.errorString())
}
}
if (comp.status === Component.Loading) {
comp.statusChanged.connect(finalize)
} else {
finalize()
}
}
// ---------------------------------------------------------- shell IPC
IpcHandler {
target: "shell"
function ping(): string {
return "ok"
}
function rescanPlugins(): void {
Services.PluginRegistry.rescan()
}
function setPluginEnabled(id: string, enabled: string): void {
Services.PluginRegistry.setEnabled(id, enabled === "true")
}
function listPlugins(): string {
var out = []
var plugins = Services.PluginRegistry.installedPlugins
for (var id in plugins) {
out.push({
id: id,
name: plugins[id].name,
kinds: plugins[id].kinds,
enabled: Services.PluginRegistry.isEnabled(id),
firstParty: !!plugins[id].__isFirstParty
})
}
return JSON.stringify(out)
}
}
}