Add omarchy-shell with a plugin registry
This commit is contained in:
@@ -252,7 +252,7 @@ stop_screenrecording() {
|
|||||||
toggle_screenrecording_indicator() {
|
toggle_screenrecording_indicator() {
|
||||||
pkill -RTMIN+8 waybar
|
pkill -RTMIN+8 waybar
|
||||||
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
|
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() {
|
screenrecording_active() {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ PY
|
|||||||
|
|
||||||
omarchy-restart-waybar
|
omarchy-restart-waybar
|
||||||
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
|
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
|
omarchy-restart-bar
|
||||||
fi
|
fi
|
||||||
omarchy-restart-swayosd
|
omarchy-restart-swayosd
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# omarchy:examples=omarchy restart bar
|
# omarchy:examples=omarchy restart bar
|
||||||
|
|
||||||
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
|
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
|
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 &
|
setsid uwsm-app -- env OMARCHY_PATH="$OMARCHY_PATH" quickshell -p "$CONFIG_DIR" >/dev/null 2>&1 &
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ if pgrep -x waybar >/dev/null; then
|
|||||||
omarchy-restart-waybar
|
omarchy-restart-waybar
|
||||||
fi
|
fi
|
||||||
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
|
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
|
omarchy-restart-bar
|
||||||
fi
|
fi
|
||||||
omarchy-restart-swayosd
|
omarchy-restart-swayosd
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# omarchy:examples=omarchy toggle bar
|
# omarchy:examples=omarchy toggle bar
|
||||||
|
|
||||||
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
|
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
|
omarchy-toggle bar-off
|
||||||
|
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ fi
|
|||||||
|
|
||||||
pkill -RTMIN+9 waybar
|
pkill -RTMIN+9 waybar
|
||||||
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
|
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
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ fi
|
|||||||
|
|
||||||
pkill -RTMIN+10 waybar
|
pkill -RTMIN+10 waybar
|
||||||
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
|
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
|
||||||
|
|||||||
@@ -4,5 +4,5 @@
|
|||||||
|
|
||||||
pkill -RTMIN+7 waybar
|
pkill -RTMIN+7 waybar
|
||||||
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
|
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
|
exit 0
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ if gum confirm "Install Voxtype + AI model (~150MB) to enable dictation?"; then
|
|||||||
|
|
||||||
omarchy-restart-waybar
|
omarchy-restart-waybar
|
||||||
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
|
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
|
omarchy-restart-bar
|
||||||
fi
|
fi
|
||||||
notify-send " Voxtype Dictation Ready" "Hold F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000
|
notify-send " Voxtype Dictation Ready" "Hold F9 to dictate (or toggle with Super + Ctrl + X)." -t 10000
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ set -e
|
|||||||
omarchy-launch-floating-terminal-with-presentation "voxtype setup model"
|
omarchy-launch-floating-terminal-with-presentation "voxtype setup model"
|
||||||
omarchy-restart-waybar
|
omarchy-restart-waybar
|
||||||
OMARCHY_PATH=${OMARCHY_PATH:-$HOME/.local/share/omarchy}
|
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
|
omarchy-restart-bar
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
hl.on("hyprland.start", function()
|
hl.on("hyprland.start", function()
|
||||||
hl.exec_cmd("uwsm-app -- hypridle")
|
hl.exec_cmd("uwsm-app -- hypridle")
|
||||||
hl.exec_cmd("uwsm-app -- mako")
|
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 -- fcitx5 --disable notificationitem")
|
||||||
hl.exec_cmd("uwsm-app -- swaybg -i ~/.config/omarchy/current/background -m fill")
|
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")
|
hl.exec_cmd("/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1")
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ ShellRoot {
|
|||||||
}
|
}
|
||||||
property string omarchyPath: deriveOmarchyPath()
|
property string omarchyPath: deriveOmarchyPath()
|
||||||
readonly property string userConfigPath: home + "/.config/omarchy/bar.json"
|
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 foreground: "#cacccc"
|
||||||
property color background: "#101315"
|
property color background: "#101315"
|
||||||
@@ -29,7 +29,7 @@ ShellRoot {
|
|||||||
|
|
||||||
// Bundled fallback so 'Reset to defaults' never produces an empty bar even
|
// 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
|
// 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: ({
|
readonly property var builtinBarConfig: ({
|
||||||
position: "top",
|
position: "top",
|
||||||
fontFamily: "JetBrainsMono Nerd Font",
|
fontFamily: "JetBrainsMono Nerd Font",
|
||||||
|
|||||||
+93
-45
@@ -10,22 +10,15 @@ import Quickshell.Widgets
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
|
||||||
ShellRoot {
|
import "../../services" as Services
|
||||||
|
|
||||||
|
Item {
|
||||||
id: root
|
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")
|
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 string omarchyConfigDir: home + "/.config/omarchy"
|
||||||
property var builtinBarConfig: ({
|
property var builtinBarConfig: ({
|
||||||
position: "top",
|
position: "top",
|
||||||
@@ -305,33 +298,69 @@ ShellRoot {
|
|||||||
return source ? fileUrl(source) : ""
|
return source ? fileUrl(source) : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property var firstPartyWidgets: ({
|
// First-party widgets are registered with the BarWidgetRegistry at startup.
|
||||||
"media": true,
|
// Each entry maps a widget id to its display metadata; the QML source lives
|
||||||
"audioPanel": true,
|
// at widgets/<id>.qml and is loaded asynchronously via Qt.createComponent.
|
||||||
"networkPanel": true,
|
readonly property var firstPartyWidgetMetadata: ({
|
||||||
"bluetoothPanel": true,
|
"media": { displayName: "Media", description: "MPRIS now-playing with playback controls", category: "Media", allowMultiple: false },
|
||||||
"calendar": true,
|
"audioPanel": { displayName: "Audio", description: "Volume slider, output picker, per-app mixer", category: "Audio", allowMultiple: false },
|
||||||
"notificationCenter": true,
|
"networkPanel": { displayName: "Network", description: "Wi-Fi list and connection state", category: "Network", allowMultiple: false },
|
||||||
"brightness": true,
|
"bluetoothPanel": { displayName: "Bluetooth", description: "Bluetooth device list with connect/disconnect", category: "Network", allowMultiple: false },
|
||||||
"powerProfile": true,
|
"calendar": { displayName: "Calendar", description: "Clock with month-grid popup", category: "Time", allowMultiple: false },
|
||||||
"systemStats": true,
|
"notificationCenter": { displayName: "Notification center", description: "Recent notifications + DND (replaces mako)", category: "Status", allowMultiple: false },
|
||||||
"weatherFlyout": true,
|
"brightness": { displayName: "Brightness", description: "Screen brightness slider", category: "System", allowMultiple: false },
|
||||||
"workspacesPro": true,
|
"powerProfile": { displayName: "Power profile", description: "power-profiles-daemon selector", category: "System", allowMultiple: false },
|
||||||
"powerMenu": true,
|
"systemStats": { displayName: "System stats", description: "Inline CPU + memory sparklines", category: "System", allowMultiple: false },
|
||||||
"idleInhibitor": true,
|
"weatherFlyout": { displayName: "Weather", description: "Weather pill with detail popup", category: "Info", allowMultiple: false },
|
||||||
"microphone": true,
|
"workspacesPro": { displayName: "Workspaces", description: "Animated workspace switcher", category: "Compositor", allowMultiple: false },
|
||||||
"notificationCenter": true,
|
"powerMenu": { displayName: "Power menu", description: "Lock / suspend / reboot / shutdown", category: "System", allowMultiple: false },
|
||||||
"activeWindow": true,
|
"idleInhibitor": { displayName: "Keep awake", description: "Toggle idle inhibitor", category: "System", allowMultiple: false },
|
||||||
"nightLight": true,
|
"microphone": { displayName: "Microphone", description: "Mic input state and mute toggle", category: "Audio", allowMultiple: false },
|
||||||
"keyboardLayout": true,
|
"activeWindow": { displayName: "Active window", description: "Title of the focused window", category: "Compositor", allowMultiple: false },
|
||||||
"lockKeys": true,
|
"nightLight": { displayName: "Night light", description: "hyprsunset toggle", category: "System", allowMultiple: false },
|
||||||
"spacer": true,
|
"keyboardLayout": { displayName: "Keyboard layout", description: "Current xkb layout, click cycles", category: "Compositor", allowMultiple: false },
|
||||||
"controlCenter": true
|
"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) {
|
property var registeredFirstPartyComponents: ({})
|
||||||
if (!firstPartyWidgets[String(name)]) return ""
|
|
||||||
return Qt.resolvedUrl("widgets/" + String(name) + ".qml")
|
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() {
|
function networkCommand() {
|
||||||
@@ -1146,11 +1175,22 @@ ShellRoot {
|
|||||||
readonly property var moduleSettings: root.entrySettings(entry)
|
readonly property var moduleSettings: root.entrySettings(entry)
|
||||||
readonly property string customType: root.customModuleType(entry)
|
readonly property string customType: root.customModuleType(entry)
|
||||||
readonly property var builtinComponent: customType ? null : root.builtinModuleComponent(moduleName)
|
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 qmlCustom: customType === "qml"
|
||||||
readonly property bool commandCustom: customType === "command"
|
readonly property bool commandCustom: customType === "command"
|
||||||
readonly property bool firstParty: firstPartySource !== ""
|
readonly property bool registered: registryComponent !== null
|
||||||
readonly property var activeItem: qmlCustom || firstParty ? qmlLoader.item : componentLoader.item
|
readonly property var activeItem: {
|
||||||
|
if (registered) return registryLoader.item
|
||||||
|
if (qmlCustom) return qmlLoader.item
|
||||||
|
return componentLoader.item
|
||||||
|
}
|
||||||
|
|
||||||
implicitWidth: activeItem && activeItem.visible ? activeItem.implicitWidth : 0
|
implicitWidth: activeItem && activeItem.visible ? activeItem.implicitWidth : 0
|
||||||
implicitHeight: activeItem && activeItem.visible ? activeItem.implicitHeight : 0
|
implicitHeight: activeItem && activeItem.visible ? activeItem.implicitHeight : 0
|
||||||
@@ -1159,15 +1199,23 @@ ShellRoot {
|
|||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: componentLoader
|
id: componentLoader
|
||||||
active: !slot.qmlCustom && !slot.firstParty
|
active: !slot.qmlCustom && !slot.registered
|
||||||
sourceComponent: slot.builtinComponent || (slot.commandCustom ? customCommandModuleComponent : emptyModuleComponent)
|
sourceComponent: slot.builtinComponent || (slot.commandCustom ? customCommandModuleComponent : emptyModuleComponent)
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: registryLoader
|
||||||
|
active: slot.registered
|
||||||
|
sourceComponent: slot.registered ? slot.registryComponent : null
|
||||||
|
anchors.fill: parent
|
||||||
|
onLoaded: slot.injectProps()
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: qmlLoader
|
id: qmlLoader
|
||||||
active: slot.qmlCustom || slot.firstParty
|
active: slot.qmlCustom
|
||||||
source: slot.qmlCustom ? root.customModuleSource(slot.entry) : (slot.firstParty ? slot.firstPartySource : "")
|
source: slot.qmlCustom ? root.customModuleSource(slot.entry) : ""
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onLoaded: slot.injectProps()
|
onLoaded: slot.injectProps()
|
||||||
}
|
}
|
||||||
@@ -1175,7 +1223,7 @@ ShellRoot {
|
|||||||
onModuleSettingsChanged: injectProps()
|
onModuleSettingsChanged: injectProps()
|
||||||
|
|
||||||
function injectProps() {
|
function injectProps() {
|
||||||
var target = qmlLoader.item
|
var target = registryLoader.item || qmlLoader.item
|
||||||
if (!target) return
|
if (!target) return
|
||||||
if ("bar" in target) target.bar = root
|
if ("bar" in target) target.bar = root
|
||||||
if ("moduleName" in target) target.moduleName = moduleName
|
if ("moduleName" in target) target.moduleName = moduleName
|
||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
This is the Quickshell implementation of the Omarchy status bar.
|
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.
|
- `bar-defaults.json` is the Omarchy-owned default layout and module settings.
|
||||||
- `widgets/` holds first-party widgets — modular, interactive components shipped with Omarchy.
|
- `widgets/` holds first-party widgets — modular, interactive components shipped with Omarchy.
|
||||||
- `common/` holds shared QML helpers (buttons, sliders, popup cards).
|
- `common/` holds shared QML helpers (buttons, sliders, popup cards).
|
||||||
@@ -11,7 +11,7 @@ This is the Quickshell implementation of the Omarchy status bar.
|
|||||||
|
|
||||||
## Customizing
|
## 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.
|
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
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user