Unify config into shell.json and add Noctalia plugin support
This commit is contained in:
@@ -3,10 +3,10 @@
|
|||||||
# omarchy:summary=Show current monospace font
|
# omarchy:summary=Show current monospace font
|
||||||
# omarchy:examples=omarchy font current
|
# omarchy:examples=omarchy font current
|
||||||
|
|
||||||
bar_config="$HOME/.config/omarchy/bar.json"
|
shell_config="$HOME/.config/omarchy/shell.json"
|
||||||
|
|
||||||
if [[ -f $bar_config ]]; then
|
if [[ -f $shell_config ]]; then
|
||||||
font_family=$(python3 - "$bar_config" <<'PY'
|
font_family=$(python3 - "$shell_config" <<'PY'
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -16,7 +16,8 @@ try:
|
|||||||
except (FileNotFoundError, json.JSONDecodeError):
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
font = data.get("fontFamily") if isinstance(data, dict) else None
|
bar = data.get("bar") if isinstance(data, dict) else None
|
||||||
|
font = bar.get("fontFamily") if isinstance(bar, dict) else None
|
||||||
if font:
|
if font:
|
||||||
print(font)
|
print(font)
|
||||||
PY
|
PY
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ if [[ -n $font_name ]]; then
|
|||||||
sed -i "s/font_family = .*/font_family = $font_name/g" ~/.config/hypr/hyprlock.conf
|
sed -i "s/font_family = .*/font_family = $font_name/g" ~/.config/hypr/hyprlock.conf
|
||||||
sed -i "s/font-family: .*/font-family: '$font_name';/g" ~/.config/waybar/style.css
|
sed -i "s/font-family: .*/font-family: '$font_name';/g" ~/.config/waybar/style.css
|
||||||
mkdir -p "$HOME/.config/omarchy"
|
mkdir -p "$HOME/.config/omarchy"
|
||||||
python3 - "$HOME/.config/omarchy/bar.json" "$font_name" <<'PY'
|
python3 - "$HOME/.config/omarchy/shell.json" "$font_name" <<'PY'
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -42,7 +42,12 @@ try:
|
|||||||
except (FileNotFoundError, json.JSONDecodeError):
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
data["fontFamily"] = font_name
|
data.setdefault("version", 1)
|
||||||
|
bar = data.get("bar")
|
||||||
|
if not isinstance(bar, dict):
|
||||||
|
bar = {}
|
||||||
|
data["bar"] = bar
|
||||||
|
bar["fontFamily"] = font_name
|
||||||
|
|
||||||
with open(path, "w") as file:
|
with open(path, "w") as file:
|
||||||
json.dump(data, file, indent=2)
|
json.dump(data, file, indent=2)
|
||||||
|
|||||||
+10
-7
@@ -1,17 +1,20 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# omarchy:summary=Reset bar user overrides to Omarchy defaults
|
# omarchy:summary=Reset shell.json to Omarchy defaults
|
||||||
# omarchy:examples=omarchy refresh bar
|
# omarchy:examples=omarchy refresh bar
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
OMARCHY_PATH="${OMARCHY_PATH:-$HOME/.local/share/omarchy}"
|
USER_CONFIG="$HOME/.config/omarchy/shell.json"
|
||||||
DEFAULT_CONFIG="$OMARCHY_PATH/config/omarchy/bar.json"
|
|
||||||
|
|
||||||
if [[ ! -f $DEFAULT_CONFIG ]]; then
|
# Removing the user file lets the shell fall back to shell-defaults.json on
|
||||||
echo "Missing default bar config at $DEFAULT_CONFIG" >&2
|
# its next reload. omarchy-restart-bar (which is now an alias for restarting
|
||||||
exit 1
|
# omarchy-shell) reloads the shell so the change takes effect immediately.
|
||||||
|
if [[ -f $USER_CONFIG ]]; then
|
||||||
|
backup="$USER_CONFIG.bak.$(date +%s)"
|
||||||
|
cp "$USER_CONFIG" "$backup"
|
||||||
|
echo "Backed up user shell config to $backup"
|
||||||
|
rm -f "$USER_CONFIG"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
omarchy-refresh-config omarchy/bar.json
|
|
||||||
omarchy-restart-bar
|
omarchy-restart-bar
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ Examples:
|
|||||||
omarchy-shell-ipc shell summon omarchy.bar-settings "{}"
|
omarchy-shell-ipc shell summon omarchy.bar-settings "{}"
|
||||||
omarchy-shell-ipc shell hide omarchy.image-picker
|
omarchy-shell-ipc shell hide omarchy.image-picker
|
||||||
omarchy-shell-ipc shell listPlugins
|
omarchy-shell-ipc shell listPlugins
|
||||||
|
omarchy-shell-ipc shell listShellConfig
|
||||||
omarchy-shell-ipc shell rescanPlugins
|
omarchy-shell-ipc shell rescanPlugins
|
||||||
omarchy-shell-ipc image-selector ping
|
omarchy-shell-ipc image-selector ping
|
||||||
omarchy-shell-ipc image-selector cancel ""
|
omarchy-shell-ipc image-selector cancel ""
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
CONFIG_FILE="$HOME/.config/omarchy/bar.json"
|
CONFIG_FILE="$HOME/.config/omarchy/shell.json"
|
||||||
position=$1
|
position=$1
|
||||||
|
|
||||||
if [[ ! $position =~ ^(top|bottom|left|right)$ ]]; then
|
if [[ ! $position =~ ^(top|bottom|left|right)$ ]]; then
|
||||||
@@ -17,7 +17,6 @@ fi
|
|||||||
mkdir -p "$(dirname "$CONFIG_FILE")"
|
mkdir -p "$(dirname "$CONFIG_FILE")"
|
||||||
python3 - "$CONFIG_FILE" "$position" <<'PY'
|
python3 - "$CONFIG_FILE" "$position" <<'PY'
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
path, position = sys.argv[1], sys.argv[2]
|
path, position = sys.argv[1], sys.argv[2]
|
||||||
@@ -29,7 +28,12 @@ try:
|
|||||||
except (FileNotFoundError, json.JSONDecodeError):
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
data["position"] = position
|
data.setdefault("version", 1)
|
||||||
|
bar = data.get("bar")
|
||||||
|
if not isinstance(bar, dict):
|
||||||
|
bar = {}
|
||||||
|
data["bar"] = bar
|
||||||
|
bar["position"] = position
|
||||||
|
|
||||||
with open(path, "w") as file:
|
with open(path, "w") as file:
|
||||||
json.dump(data, file, indent=2)
|
json.dump(data, file, indent=2)
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
// Noctalia compat shim. Plugins import `qs.Commons` and reach for these
|
||||||
|
// palette tokens / helpers. Values stream from the Omarchy theme file; the
|
||||||
|
// shell wires this singleton up at startup via setHostBar()/setHostShell().
|
||||||
|
//
|
||||||
|
// We don't try to replicate Noctalia's full Material You resolver — for the
|
||||||
|
// fields plugins actually read, a flat token table is enough.
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Live updated from theme/colors.toml via the FileView below.
|
||||||
|
property color foreground: "#cacccc"
|
||||||
|
property color background: "#101315"
|
||||||
|
property color accent: "#cacccc"
|
||||||
|
property color urgent: "#a55555"
|
||||||
|
|
||||||
|
// Noctalia palette tokens. We map them onto our theme colors.
|
||||||
|
readonly property color mPrimary: accent
|
||||||
|
readonly property color mSecondary: Qt.darker(accent, 1.2)
|
||||||
|
readonly property color mTertiary: Qt.lighter(accent, 1.3)
|
||||||
|
readonly property color mSurface: background
|
||||||
|
readonly property color mSurfaceVariant: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.06)
|
||||||
|
readonly property color mOnSurface: foreground
|
||||||
|
readonly property color mOnSurfaceVariant: Qt.darker(foreground, 1.4)
|
||||||
|
readonly property color mOutline: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.18)
|
||||||
|
readonly property color mHover: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.14)
|
||||||
|
readonly property color mOnHover: foreground
|
||||||
|
readonly property color mError: urgent
|
||||||
|
readonly property color mOnError: background
|
||||||
|
|
||||||
|
// Plugins read this to know whether to skip mid-flight transitions. We
|
||||||
|
// don't ship theme transitions ourselves, so it's always false.
|
||||||
|
readonly property bool isTransitioning: false
|
||||||
|
|
||||||
|
function alpha(c, opacity) {
|
||||||
|
if (!c) return Qt.rgba(0, 0, 0, opacity)
|
||||||
|
return Qt.rgba(c.r, c.g, c.b, opacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Noctalia's smartAlpha picks an alpha based on the host theme's perceived
|
||||||
|
// contrast. A flat 0.6 reads acceptably across our themes; cheap, no math.
|
||||||
|
function smartAlpha(c) {
|
||||||
|
return alpha(c, 0.6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// adaptiveOpacity takes a 0..1 ratio and clamps it. Plugins use it for
|
||||||
|
// fade animations relative to a "full" opacity value.
|
||||||
|
function adaptiveOpacity(value) {
|
||||||
|
if (value === undefined || value === null) return 1.0
|
||||||
|
return Math.max(0, Math.min(1, Number(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugins occasionally pass a color key like "accent" or "primary" via
|
||||||
|
// their own settings. resolveColorKey returns a color; resolveColorKeyOptional
|
||||||
|
// returns null/undefined-ish for "none".
|
||||||
|
function resolveColorKey(key) {
|
||||||
|
var resolved = resolveColorKeyOptional(key)
|
||||||
|
return resolved === null ? foreground : resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveColorKeyOptional(key) {
|
||||||
|
var k = String(key || "").toLowerCase()
|
||||||
|
if (!k || k === "none") return null
|
||||||
|
switch (k) {
|
||||||
|
case "primary": return mPrimary
|
||||||
|
case "secondary": return mSecondary
|
||||||
|
case "tertiary": return mTertiary
|
||||||
|
case "surface": return mSurface
|
||||||
|
case "surfacevariant": return mSurfaceVariant
|
||||||
|
case "onsurface": return mOnSurface
|
||||||
|
case "onsurfacevariant": return mOnSurfaceVariant
|
||||||
|
case "outline": return mOutline
|
||||||
|
case "hover": return mHover
|
||||||
|
case "onhover": return mOnHover
|
||||||
|
case "error": return mError
|
||||||
|
case "onerror": return mOnError
|
||||||
|
case "accent": return accent
|
||||||
|
case "foreground": return foreground
|
||||||
|
case "background": return background
|
||||||
|
case "urgent":
|
||||||
|
case "red": return urgent
|
||||||
|
}
|
||||||
|
// Anything else, treat as a literal CSS color string (the QML color type
|
||||||
|
// does this conversion implicitly when assigned).
|
||||||
|
return Qt.color(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTheme(raw) {
|
||||||
|
var lines = String(raw || "").split("\n")
|
||||||
|
for (var i = 0; i < lines.length; i++) {
|
||||||
|
var match = lines[i].match(/^\s*([A-Za-z0-9_-]+)\s*=\s*["']?(#[0-9A-Fa-f]{6})/)
|
||||||
|
if (!match) continue
|
||||||
|
if (match[1] === "foreground") foreground = match[2]
|
||||||
|
else if (match[1] === "background") background = match[2]
|
||||||
|
else if (match[1] === "color4" || match[1] === "accent") accent = match[2]
|
||||||
|
else if (match[1] === "red") urgent = match[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property FileView themeFile: FileView {
|
||||||
|
path: Quickshell.env("HOME") + "/.config/omarchy/current/theme/colors.toml"
|
||||||
|
watchChanges: true
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: root.loadTheme(text())
|
||||||
|
onFileChanged: reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
// Noctalia compat shim. Their plugins normally route translations through
|
||||||
|
// pluginApi.tr(), not this singleton, so the stub here mostly exists to
|
||||||
|
// satisfy `import qs.Commons` style usage.
|
||||||
|
QtObject {
|
||||||
|
readonly property string langCode: "en"
|
||||||
|
|
||||||
|
function tr(key, interp) { return String(key === undefined ? "" : key) }
|
||||||
|
function trp(key, count, interp) { return String(key === undefined ? "" : key) }
|
||||||
|
function hasTranslation(key) { return false }
|
||||||
|
|
||||||
|
signal translationsLoaded()
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
// Noctalia compat shim. Their plugins resolve Tabler icon names to glyphs via
|
||||||
|
// this singleton. The original ships ~3000 mappings; for v1 we cover the
|
||||||
|
// names most commonly used by the plugins in their repo and fall back to
|
||||||
|
// "?" for unknowns so missing glyphs are visually obvious.
|
||||||
|
QtObject {
|
||||||
|
// Tabler icon names → Nerd Font glyphs. Most entries are mdi-* substitutes
|
||||||
|
// since Omarchy installs JetBrains Mono Nerd Font which includes Material
|
||||||
|
// Design Icons. Glyph codepoints embedded as actual chars (Python helper
|
||||||
|
// when the write tool strips multi-byte codepoints in some positions).
|
||||||
|
readonly property var glyphs: ({
|
||||||
|
"bell": "\udb80\udc9a", // mdi-bell
|
||||||
|
"bell-off": "\udb80\udc9b", // mdi-bell_off
|
||||||
|
"bell-ring": "\udb80\udd6b", // mdi-bell_ring
|
||||||
|
"clock": "\udb80\udc50", // mdi-clock
|
||||||
|
"clock-outline": "\udb80\udc51", // mdi-clock_outline
|
||||||
|
"wifi": "\udb81\udda9", // mdi-wifi
|
||||||
|
"wifi-off": "\udb81\uddaa", // mdi-wifi_off
|
||||||
|
"ethernet": "\udb80\udc02", // mdi-ethernet
|
||||||
|
"bluetooth": "\udb80\udcaf", // mdi-bluetooth
|
||||||
|
"bluetooth-off": "\udb80\udcb2", // mdi-bluetooth_off
|
||||||
|
"volume": "\udb81\udd7e", // mdi-volume_high
|
||||||
|
"volume-up": "\udb81\udd7e",
|
||||||
|
"volume-down": "\udb81\udd7f",
|
||||||
|
"volume-off": "\udb83\udc08", // mdi-volume_variant_off
|
||||||
|
"volume-mute": "\udb83\udc08",
|
||||||
|
"headphones": "\udb80\udecb", // mdi-headphones
|
||||||
|
"microphone": "\udb80\udf6c", // mdi-microphone
|
||||||
|
"microphone-off": "\udb80\udf6d", // mdi-microphone_off
|
||||||
|
"play": "\udb81\udc0a", // mdi-play
|
||||||
|
"pause": "\udb80\udfe4", // mdi-pause
|
||||||
|
"skip-back": "\udb81\udcae", // mdi-skip_previous
|
||||||
|
"skip-forward": "\udb81\udcad", // mdi-skip_next
|
||||||
|
"music": "\udb81\udd5a", // mdi-music
|
||||||
|
"weather": "\udb81\udd99", // mdi-weather_sunny
|
||||||
|
"weather-sun": "\udb81\udd99",
|
||||||
|
"weather-moon": "\udb81\udd94", // mdi-weather_night
|
||||||
|
"weather-cloud": "\udb81\udd90", // mdi-weather_cloudy
|
||||||
|
"weather-rain": "\udb81\udd96", // mdi-weather_pouring
|
||||||
|
"battery": "\udb80\udc83", // mdi-battery
|
||||||
|
"battery-charging": "\udb80\udc84",
|
||||||
|
"cpu": "\udb83\udee0", // mdi-cpu_64_bit
|
||||||
|
"memory": "\udb80\udd5b", // mdi-memory
|
||||||
|
"tools": "\udb80\udd64", // mdi-tools
|
||||||
|
"settings": "\udb80\udc93", // mdi-cog
|
||||||
|
"settings-outline":"\udb80\udcbb",
|
||||||
|
"power": "\udb80\udc25", // mdi-power
|
||||||
|
"lock": "\udb80\udd3e", // mdi-lock
|
||||||
|
"unlock": "\udb80\udd3f",
|
||||||
|
"refresh": "\udb81\udc50", // mdi-refresh
|
||||||
|
"x": "\udb80\udd56", // mdi-close
|
||||||
|
"check": "\udb80\udc12", // mdi-check
|
||||||
|
"chevron-down": "\udb80\udd40",
|
||||||
|
"chevron-up": "\udb80\udd43",
|
||||||
|
"chevron-left": "\udb80\udd41",
|
||||||
|
"chevron-right": "\udb80\udd42",
|
||||||
|
"plus": "\udb80\udc15",
|
||||||
|
"minus": "\udb80\udc16",
|
||||||
|
"leaf": "\udb80\udf2a",
|
||||||
|
"rocket": "\udb81\udc63",
|
||||||
|
"balance": "\udb81\uddd1",
|
||||||
|
"moon": "\udb81\udd94",
|
||||||
|
"sun": "\udb81\udd99",
|
||||||
|
"trash": "\udb81\udcd7", // mdi-delete
|
||||||
|
"edit": "\udb80\udd66",
|
||||||
|
"search": "\udb80\udd6f",
|
||||||
|
"menu": "\udb80\udd6c",
|
||||||
|
"home": "\udb80\udf0c",
|
||||||
|
"folder": "\udb80\udd99",
|
||||||
|
"file": "\udb80\udd97",
|
||||||
|
"calendar": "\udb80\udcf7", // mdi-calendar
|
||||||
|
"circle": "\udb80\udd2f"
|
||||||
|
})
|
||||||
|
|
||||||
|
function get(name) {
|
||||||
|
var key = String(name || "").toLowerCase()
|
||||||
|
if (glyphs[key]) return glyphs[key]
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
function has(name) {
|
||||||
|
return !!glyphs[String(name || "").toLowerCase()]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
// Noctalia compat shim. Plugins call Logger.d/i/w/e routinely.
|
||||||
|
QtObject {
|
||||||
|
function format(args) {
|
||||||
|
var out = []
|
||||||
|
for (var i = 0; i < args.length; i++) {
|
||||||
|
var a = args[i]
|
||||||
|
out.push(a === undefined ? "undefined" : (a === null ? "null" : String(a)))
|
||||||
|
}
|
||||||
|
return out.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
function d() { console.debug(format(arguments)) }
|
||||||
|
function i() { console.info(format(arguments)) }
|
||||||
|
function w() { console.warn(format(arguments)) }
|
||||||
|
function e() { console.error(format(arguments)) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
// Noctalia compat shim. Their plugins read Settings.data.* and call
|
||||||
|
// Settings.getBarPositionForScreen / getBarWidgetsForScreen. We expose a
|
||||||
|
// read-only view of our shell.json so reads succeed; writes are best routed
|
||||||
|
// through pluginApi.saveSettings() instead.
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var data: ({
|
||||||
|
bar: {
|
||||||
|
position: "top",
|
||||||
|
widgets: { left: [], center: [], right: [] }
|
||||||
|
},
|
||||||
|
general: {},
|
||||||
|
colorSchemes: { darkMode: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Plugins use this gate around verbose debug logging. Defaulting to false
|
||||||
|
// keeps their Logger.d calls silent unless we explicitly flip it.
|
||||||
|
property bool isDebug: false
|
||||||
|
|
||||||
|
// shellConfig is wired by shell.qml at startup; reading it keeps Settings.data
|
||||||
|
// in lockstep with the live shell config.
|
||||||
|
property var shellConfig: null
|
||||||
|
onShellConfigChanged: rebuildData()
|
||||||
|
|
||||||
|
function rebuildData() {
|
||||||
|
var sc = shellConfig || {}
|
||||||
|
var bar = (sc && sc.bar) ? sc.bar : {}
|
||||||
|
var layout = (bar && bar.layout) ? bar.layout : {}
|
||||||
|
data = {
|
||||||
|
bar: {
|
||||||
|
position: bar.position || "top",
|
||||||
|
centerAnchor: bar.centerAnchor || "",
|
||||||
|
fontFamily: bar.fontFamily || "JetBrainsMono Nerd Font",
|
||||||
|
widgets: {
|
||||||
|
left: Array.isArray(layout.left) ? layout.left : [],
|
||||||
|
center: Array.isArray(layout.center) ? layout.center : [],
|
||||||
|
right: Array.isArray(layout.right) ? layout.right : []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
general: {},
|
||||||
|
colorSchemes: { darkMode: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBarPositionForScreen(name) {
|
||||||
|
return data && data.bar ? data.bar.position : "top"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBarWidgetsForScreen(name) {
|
||||||
|
return data && data.bar ? data.bar.widgets : { left: [], center: [], right: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScreenOverrideEntry(name) { return null }
|
||||||
|
function hasScreenOverride(name, key) { return false }
|
||||||
|
function setScreenOverride(name, key, value) { /* not supported */ }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
// Noctalia compat shim. Their plugins occasionally introspect global UI
|
||||||
|
// state through this singleton. Returning safe defaults lets reads succeed
|
||||||
|
// without us tracking the state ourselves.
|
||||||
|
QtObject {
|
||||||
|
readonly property bool isLoaded: true
|
||||||
|
property var activeScreen: null
|
||||||
|
property bool launcherOpen: false
|
||||||
|
property bool controlCenterOpen: false
|
||||||
|
property bool settingsOpen: false
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
import "." as Commons
|
||||||
|
|
||||||
|
// Noctalia compat shim. Sizing/spacing/typography tokens that plugins
|
||||||
|
// reference unconditionally. Real values picked to roughly match what
|
||||||
|
// Omarchy renders today — close enough that plugin layouts don't look
|
||||||
|
// jarring next to native widgets.
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Radii.
|
||||||
|
readonly property real radiusXS: 2
|
||||||
|
readonly property real radiusS: 3
|
||||||
|
readonly property real radiusM: 4
|
||||||
|
readonly property real radiusL: 6
|
||||||
|
readonly property real radiusXL: 8
|
||||||
|
readonly property real iRadiusXS: 2
|
||||||
|
readonly property real iRadiusS: 3
|
||||||
|
readonly property real iRadiusM: 4
|
||||||
|
readonly property real iRadiusL: 6
|
||||||
|
|
||||||
|
// Margins / paddings. Noctalia uses two parallel scales for margins (one
|
||||||
|
// tighter, one looser); we map them to the same values.
|
||||||
|
readonly property real marginXS: 2
|
||||||
|
readonly property real marginS: 4
|
||||||
|
readonly property real marginM: 6
|
||||||
|
readonly property real marginL: 10
|
||||||
|
readonly property real marginXL: 14
|
||||||
|
readonly property real margin2XXS: 1
|
||||||
|
readonly property real margin2XS: 2
|
||||||
|
readonly property real margin2S: 3
|
||||||
|
readonly property real margin2M: 5
|
||||||
|
readonly property real margin2L: 8
|
||||||
|
readonly property real margin2XL: 12
|
||||||
|
|
||||||
|
// Border widths.
|
||||||
|
readonly property real borderS: 1
|
||||||
|
readonly property real borderM: 1
|
||||||
|
readonly property real borderL: 2
|
||||||
|
|
||||||
|
// Font sizes.
|
||||||
|
readonly property real fontSizeXXS: 9
|
||||||
|
readonly property real fontSizeXS: 10
|
||||||
|
readonly property real fontSizeS: 11
|
||||||
|
readonly property real fontSizeM: 12
|
||||||
|
readonly property real fontSizeL: 14
|
||||||
|
readonly property real fontSizeXL: 16
|
||||||
|
readonly property real fontSizeXXL: 20
|
||||||
|
readonly property real fontSizeXXXL: 24
|
||||||
|
|
||||||
|
// Font weights.
|
||||||
|
readonly property int fontWeightLight: 300
|
||||||
|
readonly property int fontWeightRegular: 400
|
||||||
|
readonly property int fontWeightMedium: 500
|
||||||
|
readonly property int fontWeightSemiBold: 600
|
||||||
|
readonly property int fontWeightBold: 700
|
||||||
|
|
||||||
|
// Opacities.
|
||||||
|
readonly property real opacityLight: 0.4
|
||||||
|
readonly property real opacityMedium: 0.6
|
||||||
|
readonly property real opacityHeavy: 0.8
|
||||||
|
readonly property real opacityFull: 1.0
|
||||||
|
|
||||||
|
// Animation durations (ms).
|
||||||
|
readonly property int animationFast: 100
|
||||||
|
readonly property int animationNormal: 160
|
||||||
|
readonly property int animationSlow: 250
|
||||||
|
readonly property int animationSlower: 400
|
||||||
|
readonly property int animationSlowest: 600
|
||||||
|
|
||||||
|
// Tooltip delay (ms).
|
||||||
|
readonly property int tooltipDelay: 400
|
||||||
|
|
||||||
|
// Capsule (bar pill) tokens. The Omarchy bar is flat, no capsule background,
|
||||||
|
// so we route these to transparent + the foreground outline for visual hint.
|
||||||
|
readonly property color capsuleColor: Qt.rgba(0, 0, 0, 0)
|
||||||
|
readonly property color capsuleBorderColor: Qt.rgba(0, 0, 0, 0)
|
||||||
|
readonly property real capsuleBorderWidth: 0
|
||||||
|
readonly property real baseWidgetSize: 22
|
||||||
|
|
||||||
|
// UI scale ratio. Noctalia plugins multiply lengths by this when
|
||||||
|
// applyUiScale is true; we leave it at 1.
|
||||||
|
readonly property real uiScaleRatio: 1.0
|
||||||
|
|
||||||
|
// Helpers. Plugins pass screen names but in practice we ignore them and
|
||||||
|
// return our shell-wide values.
|
||||||
|
function getBarHeightForScreen(name) { return 28 }
|
||||||
|
function getCapsuleHeightForScreen(name) { return 22 }
|
||||||
|
function getBarFontSizeForScreen(name) { return fontSizeM }
|
||||||
|
function getBarRadiusForScreen(name) { return radiusM }
|
||||||
|
|
||||||
|
function pixelAlignCenter(parentSize, childSize) {
|
||||||
|
return Math.round((Number(parentSize) - Number(childSize)) / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toOdd(n) {
|
||||||
|
var i = Math.floor(Number(n))
|
||||||
|
return i | 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
// Noctalia compat shim. Their plugins resolve app icons through XDG icon
|
||||||
|
// themes; we don't ship that machinery, so iconForAppId returns empty and
|
||||||
|
// callers fall back to their default glyph.
|
||||||
|
QtObject {
|
||||||
|
function iconFromName(name) { return "" }
|
||||||
|
function iconForAppId(appId) { return "" }
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
// Noctalia compat shim.
|
||||||
|
QtObject {
|
||||||
|
function getFormattedTimestamp() {
|
||||||
|
return new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function nowMs() {
|
||||||
|
return Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(date, fmt) {
|
||||||
|
if (!date) date = new Date()
|
||||||
|
return Qt.formatDateTime(date, fmt || "yyyy-MM-dd HH:mm:ss")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
module qs.Commons
|
||||||
|
singleton Color 1.0 Color.qml
|
||||||
|
singleton Style 1.0 Style.qml
|
||||||
|
singleton Logger 1.0 Logger.qml
|
||||||
|
singleton Settings 1.0 Settings.qml
|
||||||
|
singleton I18n 1.0 I18n.qml
|
||||||
|
singleton Time 1.0 Time.qml
|
||||||
|
singleton Icons 1.0 Icons.qml
|
||||||
|
singleton ThemeIcons 1.0 ThemeIcons.qml
|
||||||
|
singleton ShellState 1.0 ShellState.qml
|
||||||
@@ -19,8 +19,9 @@ The runtime layout in this branch:
|
|||||||
```
|
```
|
||||||
default/quickshell/omarchy-shell/
|
default/quickshell/omarchy-shell/
|
||||||
shell.qml entry point (ShellRoot)
|
shell.qml entry point (ShellRoot)
|
||||||
|
shell-defaults.json canonical out-of-the-box config
|
||||||
services/
|
services/
|
||||||
PluginRegistry.qml discovers, validates, persists plugin state
|
PluginRegistry.qml discovers, validates plugins, looks up enabled state in shell.json
|
||||||
BarWidgetRegistry.qml unified registry for bar widgets (1p + 3p)
|
BarWidgetRegistry.qml unified registry for bar widgets (1p + 3p)
|
||||||
ui/
|
ui/
|
||||||
settings/
|
settings/
|
||||||
@@ -133,12 +134,63 @@ type-stable across QML's `string`-only IPC arguments.
|
|||||||
|
|
||||||
## Persisted state
|
## Persisted state
|
||||||
|
|
||||||
| Path | Owner | Purpose |
|
There is one user config file. Everything that distinguishes your
|
||||||
|-------------------------------------------|----------------|--------------------------------------|
|
customization from the shipped defaults lives in it.
|
||||||
| `~/.config/omarchy/bar.json` | bar plugin | section layout + per-entry settings |
|
|
||||||
| `~/.config/omarchy/plugins.json` | PluginRegistry | enabled/disabled state |
|
| Path | Owner | Purpose |
|
||||||
| `~/.config/omarchy/plugins/<id>/` | user | manifest + entry points + assets |
|
|-----------------------------------|----------------|--------------------------------------------------------|
|
||||||
| `~/.config/omarchy/plugins/<id>/settings.json` | user | optional per-plugin overrides |
|
| `~/.config/omarchy/shell.json` | the shell | full layout + per-entry settings + enabled plugin list |
|
||||||
|
| `~/.config/omarchy/plugins/<id>/` | user | drop-in third-party plugin source files |
|
||||||
|
|
||||||
|
The `shell-defaults.json` bundled with the shell describes the
|
||||||
|
fresh-install state. When the user has no `shell.json`, the shell uses
|
||||||
|
the defaults verbatim. Once the user customizes anything, `shell.json`
|
||||||
|
becomes the authoritative file — we do **not** deep-merge defaults back
|
||||||
|
in. Pressing **Reset to defaults** in `omarchy launch bar-settings`
|
||||||
|
rewrites `shell.json` from the current `shell-defaults.json`.
|
||||||
|
|
||||||
|
### shell.json shape
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"bar": {
|
||||||
|
"position": "top",
|
||||||
|
"centerAnchor": "calendar",
|
||||||
|
"fontFamily": "JetBrainsMono Nerd Font",
|
||||||
|
"layout": {
|
||||||
|
"left": [ { "id": "omarchy" }, { "id": "workspacesPro" } ],
|
||||||
|
"center": [ { "id": "calendar", "format": "HH:mm" } ],
|
||||||
|
"right": [
|
||||||
|
{ "id": "audioPanel" },
|
||||||
|
{ "id": "controlCenter" },
|
||||||
|
{ "id": "powerMenu" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{ "id": "omarchy.bar-settings" },
|
||||||
|
{ "id": "omarchy.image-picker" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Storage rules
|
||||||
|
|
||||||
|
1. **Every plugin instance is one entry.** Either in `bar.layout.<section>`
|
||||||
|
for bar widgets, or in `plugins[]` for panels, overlays, services,
|
||||||
|
menus, and anything else non-bar.
|
||||||
|
2. **Settings are inline on the entry.** No `config:` sub-object, no
|
||||||
|
separate per-plugin settings file, no merge layers. The fields on each
|
||||||
|
entry are the values the plugin sees.
|
||||||
|
3. **Enabled ⇔ present.** A plugin is enabled iff its id appears somewhere
|
||||||
|
in shell.json. To disable, remove it. (The bar settings UI does both.)
|
||||||
|
4. **Multiple instances** are allowed when a manifest sets
|
||||||
|
`allowMultiple: true`. Each instance is independent — e.g. two clocks
|
||||||
|
in different timezones are just two `{"id":"calendar", "timezone": ...}`
|
||||||
|
entries with their own values.
|
||||||
|
5. **`version: 1` is required** at the top level. The shell will fall back
|
||||||
|
to defaults rather than load an unknown version.
|
||||||
|
|
||||||
## Implementation history
|
## Implementation history
|
||||||
|
|
||||||
@@ -151,6 +203,7 @@ Built up in phases on this branch:
|
|||||||
- Phase 5 — `omarchy-shell phase 5: docs, cleanup, and migration crumbs`
|
- Phase 5 — `omarchy-shell phase 5: docs, cleanup, and migration crumbs`
|
||||||
- Phase 6 — `omarchy-shell phase 6: reviewer cleanup (path traversal, collision, races)`
|
- Phase 6 — `omarchy-shell phase 6: reviewer cleanup (path traversal, collision, races)`
|
||||||
- Phase 7 — `omarchy-shell phase 7: replace socket with IpcHandler, rename to image-picker`
|
- Phase 7 — `omarchy-shell phase 7: replace socket with IpcHandler, rename to image-picker`
|
||||||
|
- Phase 8a — `omarchy-shell phase 8a: unified shell.json with inline plugin settings`
|
||||||
|
|
||||||
Shared services and Pipewire/UPower/Hyprland consolidation are explicitly
|
Shared services and Pipewire/UPower/Hyprland consolidation are explicitly
|
||||||
out of scope here and deferred to a follow-up after a review pass.
|
out of scope here and deferred to a follow-up after a review pass.
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
// Noctalia compat shim. The "noctaliaPerformanceMode" flag was a v3 internal
|
||||||
|
// state Noctalia plugins occasionally check. We hardcode false; plugins that
|
||||||
|
// branch on it will fall into the non-performance code path.
|
||||||
|
QtObject {
|
||||||
|
readonly property bool noctaliaPerformanceMode: false
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
module qs.Services.Power
|
||||||
|
singleton PowerProfileService 1.0 PowerProfileService.qml
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
|
||||||
|
// Noctalia compat shim. activate-linux and a few other plugins display
|
||||||
|
// distro/host info. We parse /etc/os-release lazily.
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string osPretty: ""
|
||||||
|
property string osName: ""
|
||||||
|
property string osVersion: ""
|
||||||
|
property string hostname: ""
|
||||||
|
|
||||||
|
property Process osReleaseProc: Process {
|
||||||
|
command: ["bash", "-c",
|
||||||
|
"( . /etc/os-release && printf '%s\\t%s\\t%s\\n' \"$PRETTY_NAME\" \"$NAME\" \"$VERSION_ID\" ); hostname"]
|
||||||
|
onExited: {
|
||||||
|
var text = String(osReleaseStdout.text || "").trim().split("\n")
|
||||||
|
if (text.length >= 1) {
|
||||||
|
var fields = text[0].split("\t")
|
||||||
|
root.osPretty = fields[0] || ""
|
||||||
|
root.osName = fields[1] || ""
|
||||||
|
root.osVersion = fields[2] || ""
|
||||||
|
}
|
||||||
|
if (text.length >= 2) root.hostname = text[1].trim()
|
||||||
|
}
|
||||||
|
stdout: StdioCollector {
|
||||||
|
id: osReleaseStdout
|
||||||
|
waitForEnd: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: osReleaseProc.running = true
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
module qs.Services.System
|
||||||
|
singleton HostService 1.0 HostService.qml
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
// Noctalia compat shim. Plugin bar widgets call into BarService for
|
||||||
|
// registration bookkeeping and to ask the host where their tooltip should
|
||||||
|
// pop. We track the bar reference (set by the host at startup) and expose
|
||||||
|
// the helpers plugins actually use.
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Wired by omarchy-shell's Bar.qml on construction so we can reach back
|
||||||
|
// for things like position and the tooltip popup.
|
||||||
|
property var bar: null
|
||||||
|
|
||||||
|
// Plugins read this when computing their own layout-revision markers.
|
||||||
|
property int widgetsRevision: 0
|
||||||
|
|
||||||
|
// Bookkeeping for plugin widgets. Not consulted by Omarchy itself; some
|
||||||
|
// plugins call lookupWidget() to coordinate across instances.
|
||||||
|
property var registered: ({})
|
||||||
|
|
||||||
|
function registerWidget(screen, section, widgetId, index, item) {
|
||||||
|
var key = (screen && screen.name ? screen.name : "_") + ":" + section + ":" + widgetId + ":" + index
|
||||||
|
var next = {}
|
||||||
|
for (var k in registered) next[k] = registered[k]
|
||||||
|
next[key] = { screen: screen, section: section, widgetId: widgetId, index: index, item: item }
|
||||||
|
registered = next
|
||||||
|
widgetsRevision++
|
||||||
|
}
|
||||||
|
|
||||||
|
function unregisterWidget(screen, section, widgetId, index) {
|
||||||
|
var key = (screen && screen.name ? screen.name : "_") + ":" + section + ":" + widgetId + ":" + index
|
||||||
|
if (!registered[key]) return
|
||||||
|
var next = {}
|
||||||
|
for (var k in registered) if (k !== key) next[k] = registered[k]
|
||||||
|
registered = next
|
||||||
|
widgetsRevision++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugins occasionally search for a sibling widget by id. We do a linear
|
||||||
|
// scan since the registry is small in practice.
|
||||||
|
function lookupWidget(widgetId) {
|
||||||
|
for (var k in registered) {
|
||||||
|
if (registered[k].widgetId === widgetId) return registered[k]
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate the bar's edge to a tooltip direction string. Noctalia expects
|
||||||
|
// strings; the Omarchy bar drives its own tooltip popup off bar.position.
|
||||||
|
function getTooltipDirection(screenName) {
|
||||||
|
var pos = bar ? bar.position : "top"
|
||||||
|
if (pos === "top") return "down"
|
||||||
|
if (pos === "bottom") return "up"
|
||||||
|
if (pos === "left") return "right"
|
||||||
|
if (pos === "right") return "left"
|
||||||
|
return "down"
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPillDirection(item) {
|
||||||
|
var pos = bar ? bar.position : "top"
|
||||||
|
return pos === "left" || pos === "right" ? "horizontal" : "vertical"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward into the omarchy-shell host. Most Noctalia widgets call this
|
||||||
|
// from a right-click handler.
|
||||||
|
function openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings) {
|
||||||
|
if (bar && bar.shell && typeof bar.shell.summon === "function") {
|
||||||
|
bar.shell.summon("omarchy.bar-settings",
|
||||||
|
JSON.stringify({ focusWidgetId: widgetId, section: section, index: sectionWidgetIndex }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPluginSettings(screen, manifest) {
|
||||||
|
if (bar && bar.shell && typeof bar.shell.summon === "function") {
|
||||||
|
bar.shell.summon("omarchy.bar-settings",
|
||||||
|
JSON.stringify({ focusPluginId: manifest ? manifest.id : "" }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
// Noctalia compat shim. Plugins use this for context menus and to reach
|
||||||
|
// the host's panels. We forward into the omarchy-shell host where there's
|
||||||
|
// a matching surface, and log a warning for anything we don't ship in v1.
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var bar: null
|
||||||
|
property var shell: null
|
||||||
|
|
||||||
|
// Plugins call this on right-click with a Menu component. The Noctalia
|
||||||
|
// implementation parents the menu to the screen's overlay; we simply
|
||||||
|
// call `open()` on whatever the plugin passes — most plugin context menus
|
||||||
|
// are plain QtQuick `Menu` items that know how to show themselves.
|
||||||
|
function showContextMenu(menu, item, screen) {
|
||||||
|
if (!menu) return
|
||||||
|
if (typeof menu.popup === "function") menu.popup()
|
||||||
|
else if (typeof menu.open === "function") menu.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeContextMenu(screen) {
|
||||||
|
// No-op; the Menu / PopupWindow closes itself on outside click.
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLauncherWithSearch(screen, prefix) {
|
||||||
|
console.warn("PanelService.openLauncherWithSearch is not supported in the Omarchy compat layer")
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPanel(name, screen) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
pragma Singleton
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
// Noctalia compat shim. Their plugins call TooltipService.show(item, text)
|
||||||
|
// on hover; we route that into the Omarchy bar's shared tooltip popup.
|
||||||
|
QtObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Wired by Bar.qml on construction.
|
||||||
|
property var bar: null
|
||||||
|
|
||||||
|
function show(item, text, direction) {
|
||||||
|
if (!bar || !item) return
|
||||||
|
if (typeof bar.showTooltip === "function") {
|
||||||
|
bar.showTooltip(item, text || "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide(item) {
|
||||||
|
if (!bar) return
|
||||||
|
if (typeof bar.hideTooltip === "function") {
|
||||||
|
bar.hideTooltip(item || null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
module qs.Services.UI
|
||||||
|
singleton BarService 1.0 BarService.qml
|
||||||
|
singleton TooltipService 1.0 TooltipService.qml
|
||||||
|
singleton PanelService 1.0 PanelService.qml
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
color: Color.mSurfaceVariant
|
||||||
|
border.color: Color.mOutline
|
||||||
|
border.width: Style.borderS
|
||||||
|
radius: Style.radiusM
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
Button {
|
||||||
|
id: root
|
||||||
|
property string label: ""
|
||||||
|
property color colorBg: Color.mSurfaceVariant
|
||||||
|
property color colorFg: Color.mOnSurface
|
||||||
|
|
||||||
|
text: label || ""
|
||||||
|
background: Rectangle {
|
||||||
|
color: root.hovered ? Color.mHover : root.colorBg
|
||||||
|
radius: Style.radiusM
|
||||||
|
border.color: Color.mOutline
|
||||||
|
border.width: Style.borderS
|
||||||
|
}
|
||||||
|
contentItem: Text {
|
||||||
|
text: root.text
|
||||||
|
color: root.colorFg
|
||||||
|
font.family: "JetBrainsMono Nerd Font"
|
||||||
|
font.pixelSize: Style.fontSizeM
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: root
|
||||||
|
property string label: ""
|
||||||
|
text: label || ""
|
||||||
|
font.family: "JetBrainsMono Nerd Font"
|
||||||
|
font.pixelSize: Style.fontSizeS
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
ComboBox {
|
||||||
|
id: root
|
||||||
|
property string label: ""
|
||||||
|
|
||||||
|
font.family: "JetBrainsMono Nerd Font"
|
||||||
|
font.pixelSize: Style.fontSizeS
|
||||||
|
background: Rectangle {
|
||||||
|
color: Color.mSurfaceVariant
|
||||||
|
border.color: Color.mOutline
|
||||||
|
border.width: Style.borderS
|
||||||
|
radius: Style.radiusS
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitHeight: 1
|
||||||
|
color: Color.mOutline
|
||||||
|
opacity: 0.4
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
// Noctalia compat shim. Renders an icon by name through the Icons map.
|
||||||
|
Text {
|
||||||
|
id: root
|
||||||
|
property string icon: ""
|
||||||
|
property real pointSize: 14
|
||||||
|
property bool applyUiScale: true
|
||||||
|
|
||||||
|
text: Icons.get(icon)
|
||||||
|
color: Color.mOnSurface
|
||||||
|
font.family: "JetBrainsMono Nerd Font"
|
||||||
|
font.pixelSize: Math.round(pointSize * (applyUiScale ? Style.uiScaleRatio : 1))
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services.UI
|
||||||
|
|
||||||
|
// Noctalia compat shim. The widget plugins lean on this heavily; it's the
|
||||||
|
// primary clickable bar element. We mimic Noctalia's surface area
|
||||||
|
// (icon/text/tooltip/colors/border) but skip the long tail of properties
|
||||||
|
// no observed plugin actually sets.
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string icon: ""
|
||||||
|
property string text: ""
|
||||||
|
property string tooltipText: ""
|
||||||
|
property string tooltipDirection: ""
|
||||||
|
property real baseSize: Style.baseWidgetSize
|
||||||
|
property bool applyUiScale: false
|
||||||
|
property real customRadius: -1
|
||||||
|
property color colorBg: Style.capsuleColor
|
||||||
|
property color colorFg: Color.mOnSurface
|
||||||
|
property color colorBgHover: Color.mHover
|
||||||
|
property color colorFgHover: Color.mOnHover
|
||||||
|
property color colorBorder: Style.capsuleBorderColor
|
||||||
|
property color colorBorderHover: Style.capsuleBorderColor
|
||||||
|
property bool flat: false
|
||||||
|
|
||||||
|
signal clicked()
|
||||||
|
signal rightClicked()
|
||||||
|
signal middleClicked()
|
||||||
|
signal wheel(int delta)
|
||||||
|
|
||||||
|
readonly property real effectiveSize: Math.round(baseSize * (applyUiScale ? Style.uiScaleRatio : 1))
|
||||||
|
|
||||||
|
implicitWidth: effectiveSize
|
||||||
|
implicitHeight: effectiveSize
|
||||||
|
radius: customRadius >= 0 ? customRadius : Style.radiusM
|
||||||
|
color: mouse.containsMouse ? colorBgHover : colorBg
|
||||||
|
border.color: mouse.containsMouse ? colorBorderHover : colorBorder
|
||||||
|
border.width: Style.capsuleBorderWidth
|
||||||
|
|
||||||
|
Behavior on color { ColorAnimation { duration: Style.animationFast } }
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: root.icon ? Icons.get(root.icon) : root.text
|
||||||
|
color: mouse.containsMouse ? root.colorFgHover : root.colorFg
|
||||||
|
font.family: "JetBrainsMono Nerd Font"
|
||||||
|
font.pixelSize: Math.round(Style.fontSizeM * (root.applyUiScale ? Style.uiScaleRatio : 1))
|
||||||
|
Behavior on color { ColorAnimation { duration: Style.animationFast } }
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||||
|
onEntered: if (root.tooltipText) TooltipService.show(root, root.tooltipText, root.tooltipDirection)
|
||||||
|
onExited: TooltipService.hide(root)
|
||||||
|
onClicked: function(m) {
|
||||||
|
if (m.button === Qt.RightButton) root.rightClicked()
|
||||||
|
else if (m.button === Qt.MiddleButton) root.middleClicked()
|
||||||
|
else root.clicked()
|
||||||
|
}
|
||||||
|
onWheel: function(w) { root.wheel(w.angleDelta.y) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
// Noctalia compat shim. Plugins instantiate this with a model of action
|
||||||
|
// objects and expect `triggered(action)` when a row is picked.
|
||||||
|
Menu {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var model: []
|
||||||
|
signal triggered(string action)
|
||||||
|
|
||||||
|
Instantiator {
|
||||||
|
model: root.model
|
||||||
|
delegate: MenuItem {
|
||||||
|
required property var modelData
|
||||||
|
text: modelData ? String(modelData.label || modelData.action || "") : ""
|
||||||
|
onTriggered: root.triggered(modelData ? String(modelData.action || "") : "")
|
||||||
|
}
|
||||||
|
onObjectAdded: function(index, object) { root.insertItem(index, object) }
|
||||||
|
onObjectRemoved: function(index, object) { root.removeItem(object) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
// Noctalia compat shim. Auto-scrolling label.
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string text: ""
|
||||||
|
property real maxWidth: 200
|
||||||
|
property real pointSize: Style.fontSizeM
|
||||||
|
property int scrollMode: NScrollText.ScrollMode.Hover
|
||||||
|
property bool forcedHover: false
|
||||||
|
property real fadeExtent: 0.1
|
||||||
|
property real fadeCornerRadius: 0
|
||||||
|
property bool fadeRoundLeftCorners: false
|
||||||
|
property bool fadeRoundRightCorners: false
|
||||||
|
property color textColor: Color.mOnSurface
|
||||||
|
|
||||||
|
enum ScrollMode { Always, Hover, Never }
|
||||||
|
|
||||||
|
implicitHeight: label.implicitHeight
|
||||||
|
implicitWidth: Math.min(maxWidth, label.implicitWidth)
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
readonly property bool overflowing: label.implicitWidth > width
|
||||||
|
readonly property bool shouldScroll: {
|
||||||
|
if (!overflowing) return false
|
||||||
|
if (scrollMode === NScrollText.ScrollMode.Always) return true
|
||||||
|
if (scrollMode === NScrollText.ScrollMode.Hover) return forcedHover
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: label
|
||||||
|
text: root.text
|
||||||
|
color: root.textColor
|
||||||
|
font.family: "JetBrainsMono Nerd Font"
|
||||||
|
font.pixelSize: root.pointSize
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
height: parent.height
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
NumberAnimation on x {
|
||||||
|
id: scrollAnim
|
||||||
|
running: root.shouldScroll
|
||||||
|
loops: Animation.Infinite
|
||||||
|
duration: Math.max(5000, label.implicitWidth * 22)
|
||||||
|
from: root.width
|
||||||
|
to: -label.implicitWidth
|
||||||
|
easing.type: Easing.Linear
|
||||||
|
}
|
||||||
|
|
||||||
|
onShouldScrollChanged: if (!root.shouldScroll) x = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
Slider {
|
||||||
|
id: root
|
||||||
|
property string label: ""
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
SpinBox {
|
||||||
|
id: root
|
||||||
|
property string label: ""
|
||||||
|
font.family: "JetBrainsMono Nerd Font"
|
||||||
|
font.pixelSize: Style.fontSizeS
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import QtQuick
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
// Noctalia compat shim. The original supports pointSize (interpreted as
|
||||||
|
// pixel size in Noctalia) plus an applyUiScale flag. We translate to
|
||||||
|
// font.pixelSize directly; uiScaleRatio comes from Style.
|
||||||
|
Text {
|
||||||
|
id: root
|
||||||
|
property real pointSize: 12
|
||||||
|
property bool applyUiScale: true
|
||||||
|
property var customFont: null
|
||||||
|
|
||||||
|
color: Color.mOnSurface
|
||||||
|
font.family: customFont || "JetBrainsMono Nerd Font"
|
||||||
|
font.pixelSize: Math.round(pointSize * (applyUiScale ? Style.uiScaleRatio : 1))
|
||||||
|
font.weight: Style.fontWeightRegular
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: root
|
||||||
|
property string label: ""
|
||||||
|
property string description: ""
|
||||||
|
property string defaultValue: ""
|
||||||
|
|
||||||
|
font.family: "JetBrainsMono Nerd Font"
|
||||||
|
font.pixelSize: Style.fontSizeS
|
||||||
|
color: Color.mOnSurface
|
||||||
|
background: Rectangle {
|
||||||
|
color: Color.mSurfaceVariant
|
||||||
|
border.color: root.focus ? Color.mPrimary : Color.mOutline
|
||||||
|
border.width: Style.borderS
|
||||||
|
radius: Style.radiusS
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
Switch {
|
||||||
|
id: root
|
||||||
|
property string label: ""
|
||||||
|
|
||||||
|
indicator: Rectangle {
|
||||||
|
implicitWidth: 32
|
||||||
|
implicitHeight: 18
|
||||||
|
x: root.leftPadding
|
||||||
|
y: root.height / 2 - height / 2
|
||||||
|
radius: height / 2
|
||||||
|
color: root.checked ? Color.mPrimary : Color.mSurfaceVariant
|
||||||
|
border.color: Color.mOutline
|
||||||
|
border.width: Style.borderS
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
x: root.checked ? parent.width - width - 2 : 2
|
||||||
|
y: 2
|
||||||
|
width: parent.height - 4
|
||||||
|
height: parent.height - 4
|
||||||
|
radius: height / 2
|
||||||
|
color: Color.mOnSurface
|
||||||
|
Behavior on x { NumberAnimation { duration: Style.animationFast } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
leftPadding: 38
|
||||||
|
text: root.label || root.text
|
||||||
|
color: Color.mOnSurface
|
||||||
|
font.family: "JetBrainsMono Nerd Font"
|
||||||
|
font.pixelSize: Style.fontSizeS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
module qs.Widgets
|
||||||
|
NText 1.0 NText.qml
|
||||||
|
NIcon 1.0 NIcon.qml
|
||||||
|
NIconButton 1.0 NIconButton.qml
|
||||||
|
NBox 1.0 NBox.qml
|
||||||
|
NButton 1.0 NButton.qml
|
||||||
|
NPopupContextMenu 1.0 NPopupContextMenu.qml
|
||||||
|
NScrollText 1.0 NScrollText.qml
|
||||||
|
NToggle 1.0 NToggle.qml
|
||||||
|
NSpinBox 1.0 NSpinBox.qml
|
||||||
|
NSlider 1.0 NSlider.qml
|
||||||
|
NTextInput 1.0 NTextInput.qml
|
||||||
|
NComboBox 1.0 NComboBox.qml
|
||||||
|
NCheckbox 1.0 NCheckbox.qml
|
||||||
|
NDivider 1.0 NDivider.qml
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
// Builds the `pluginApi` QtObject that Noctalia plugins expect. The host
|
||||||
|
// (omarchy-shell or the bar) calls create() once per plugin and caches the
|
||||||
|
// returned object so the same handle is reused across the plugin's bar
|
||||||
|
// widget, panel, settings form, etc.
|
||||||
|
Item {
|
||||||
|
// create(pluginId, manifest, settingsProvider, hostBridge) -> QtObject
|
||||||
|
//
|
||||||
|
// settingsProvider: function() that returns the plugin's current settings
|
||||||
|
// merged with defaults. Called fresh on every read so the plugin always
|
||||||
|
// sees the live shell.json entry, not a stale snapshot from when create()
|
||||||
|
// was first called.
|
||||||
|
// hostBridge: object with these methods (all optional):
|
||||||
|
// - persistSettings(pluginId, settings): write back to shell.json via
|
||||||
|
// the shell's updateEntryInline helper.
|
||||||
|
// - openPanel(pluginId, screen, buttonItem): summon the plugin's panel
|
||||||
|
// entry point through the shell's plugin host.
|
||||||
|
// - closePanel(pluginId, screen): hide a previously-summoned panel.
|
||||||
|
// - currentScreen(): return the screen the plugin should anchor to.
|
||||||
|
// togglePanel is provided by the factory itself and routes through the
|
||||||
|
// open/closePanel bridge methods. Main.qml service instances are wired
|
||||||
|
// separately by the shell and set on api.mainInstance once instantiated.
|
||||||
|
function create(pluginId, manifest, settingsProvider, hostBridge) {
|
||||||
|
var api = pluginApiFactory.createObject(null, {
|
||||||
|
pluginId: pluginId,
|
||||||
|
pluginDir: manifest && manifest.__sourceDir ? manifest.__sourceDir : "",
|
||||||
|
manifest: manifest || ({}),
|
||||||
|
_settingsProvider: settingsProvider || (function() { return {} }),
|
||||||
|
_hostBridge: hostBridge || ({})
|
||||||
|
})
|
||||||
|
return api
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: pluginApiFactory
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: api
|
||||||
|
|
||||||
|
property string pluginId: ""
|
||||||
|
property string pluginDir: ""
|
||||||
|
property var manifest: ({})
|
||||||
|
property var _settingsProvider
|
||||||
|
property var _hostBridge: ({})
|
||||||
|
property var mainInstance: null
|
||||||
|
|
||||||
|
// Resolved every read so the plugin sees current shell.json state.
|
||||||
|
readonly property var pluginSettings: _settingsProvider ? _settingsProvider() : ({})
|
||||||
|
|
||||||
|
property var panelOpenScreen: null
|
||||||
|
property var ipcHandlers: ({})
|
||||||
|
|
||||||
|
// Noctalia i18n surface — v1 returns the key as-is.
|
||||||
|
readonly property string currentLanguage: "en"
|
||||||
|
readonly property var pluginTranslations: ({})
|
||||||
|
readonly property var pluginFallbackTranslations: ({})
|
||||||
|
readonly property int translationVersion: 0
|
||||||
|
|
||||||
|
function tr(key, interp) { return String(key === undefined ? "" : key) }
|
||||||
|
function trp(key, count, interp) { return String(key === undefined ? "" : key) }
|
||||||
|
function hasTranslation(key) { return false }
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
if (_hostBridge && typeof _hostBridge.persistSettings === "function")
|
||||||
|
_hostBridge.persistSettings(pluginId, pluginSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPanel(screen, buttonItem) {
|
||||||
|
panelOpenScreen = screen
|
||||||
|
if (_hostBridge && typeof _hostBridge.openPanel === "function")
|
||||||
|
_hostBridge.openPanel(pluginId, screen, buttonItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePanel(screen) {
|
||||||
|
panelOpenScreen = null
|
||||||
|
if (_hostBridge && typeof _hostBridge.closePanel === "function")
|
||||||
|
_hostBridge.closePanel(pluginId, screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
function togglePanel(screen, buttonItem) {
|
||||||
|
if (panelOpenScreen) closePanel(screen)
|
||||||
|
else openPanel(screen, buttonItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
function withCurrentScreen(cb) {
|
||||||
|
if (typeof cb !== "function") return
|
||||||
|
var s = (_hostBridge && typeof _hostBridge.currentScreen === "function")
|
||||||
|
? _hostBridge.currentScreen() : null
|
||||||
|
cb(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLauncher(screen) {
|
||||||
|
console.warn("pluginApi.openLauncher is not supported in the Omarchy compat layer (plugin=" + pluginId + ")")
|
||||||
|
}
|
||||||
|
function closeLauncher(screen) { /* no-op */ }
|
||||||
|
function toggleLauncher(screen) { openLauncher(screen) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Noctalia plugin compatibility (omarchy-shell)
|
||||||
|
|
||||||
|
The omarchy-shell can load most bar widget plugins from the
|
||||||
|
[noctalia-dev/noctalia-plugins](https://github.com/noctalia-dev/noctalia-plugins)
|
||||||
|
ecosystem without any modification to the plugin code.
|
||||||
|
|
||||||
|
## Install a Noctalia plugin
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/noctalia-dev/noctalia-plugins /tmp/noctalia-plugins
|
||||||
|
ln -s /tmp/noctalia-plugins/asus-um5606-fan-state \
|
||||||
|
~/.config/omarchy/plugins/asus-um5606-fan-state
|
||||||
|
omarchy-shell-ipc shell rescanPlugins
|
||||||
|
```
|
||||||
|
|
||||||
|
The plugin id you see inside Omarchy is prefixed: `noctalia.asus-um5606-fan-state`.
|
||||||
|
Add it via the bar customizer or by editing `~/.config/omarchy/shell.json` directly.
|
||||||
|
|
||||||
|
## What's supported in v1
|
||||||
|
|
||||||
|
| Noctalia concept | Omarchy support |
|
||||||
|
|---|---|
|
||||||
|
| `entryPoints.barWidget` | yes — registered through `BarWidgetRegistry` |
|
||||||
|
| `entryPoints.panel` | yes — opened via `pluginApi.openPanel()` |
|
||||||
|
| `entryPoints.settings` | yes — embedded in the bar-settings dialog |
|
||||||
|
| `entryPoints.main` | yes — instantiated as a hidden service, exposed via `pluginApi.mainInstance` |
|
||||||
|
| `entryPoints.desktopWidget` | **no** — skipped with a console warning |
|
||||||
|
| `entryPoints.launcherProvider` | **no** — skipped with a console warning |
|
||||||
|
| `entryPoints.controlCenterWidget` | **no** — skipped with a console warning |
|
||||||
|
| Plugin i18n (`i18n/<lang>.json`) | **no** — `tr()` returns the raw key |
|
||||||
|
| Plugin install from git URL | **no** — manual drop into `~/.config/omarchy/plugins/` |
|
||||||
|
| Hot reload on plugin change | **no** — call `omarchy-shell-ipc shell rescanPlugins` |
|
||||||
|
|
||||||
|
## Shim surface
|
||||||
|
|
||||||
|
We ship just enough of Noctalia's QML namespace to render typical bar widgets.
|
||||||
|
|
||||||
|
| Module | Symbols |
|
||||||
|
|---|---|
|
||||||
|
| `qs.Commons` | `Color`, `Style`, `Logger`, `Settings` (read-only), `I18n` (stub), `Time`, `Icons` (~50 entries), `ThemeIcons` (stub), `ShellState` (stub) |
|
||||||
|
| `qs.Widgets` | `NText`, `NIcon`, `NIconButton`, `NBox`, `NButton`, `NPopupContextMenu`, `NScrollText`, `NToggle`, `NSpinBox`, `NSlider`, `NTextInput`, `NComboBox`, `NCheckbox`, `NDivider` |
|
||||||
|
| `qs.Services.UI` | `BarService`, `TooltipService`, `PanelService` |
|
||||||
|
| `qs.Services.System` | `HostService` (reads `/etc/os-release`) |
|
||||||
|
| `qs.Services.Power` | `PowerProfileService` (returns `noctaliaPerformanceMode: false`) |
|
||||||
|
|
||||||
|
Anything outside this surface logs a `console.warn` instead of crashing the
|
||||||
|
shell, but the plugin may not render correctly.
|
||||||
|
|
||||||
|
## `pluginApi` surface
|
||||||
|
|
||||||
|
A Noctalia plugin's `pluginApi` is built per-plugin and injected onto the bar
|
||||||
|
widget / panel / settings entry points. The implementation lives at
|
||||||
|
`compat/noctalia/PluginApiFactory.qml`.
|
||||||
|
|
||||||
|
| Property / method | Behaviour |
|
||||||
|
|---|---|
|
||||||
|
| `pluginId`, `pluginDir`, `manifest` | Provided as expected. |
|
||||||
|
| `pluginSettings` | Live read of the plugin's entry in `~/.config/omarchy/shell.json`, merged with the manifest's `metadata.defaultSettings`. |
|
||||||
|
| `mainInstance` | Live instance of `Main.qml` when the plugin declares one. |
|
||||||
|
| `saveSettings()` | Persists the merged settings into the plugin's entry in `shell.json`. |
|
||||||
|
| `openPanel(screen, btn)` / `closePanel(screen)` / `togglePanel(screen, btn)` | Routes through `omarchy-shell-ipc shell summon/hide` against the plugin's panel entry point. |
|
||||||
|
| `withCurrentScreen(cb)` | Calls `cb` with the bar's currently-rendering screen. |
|
||||||
|
| `tr/trp/hasTranslation` | Returns the key as-is; no i18n in v1. |
|
||||||
|
| `openLauncher(...)` family | Stub — logs a warning. |
|
||||||
|
|
||||||
|
## Known limitations
|
||||||
|
|
||||||
|
- Plugin labels show raw translation keys for non-English locales.
|
||||||
|
- Icon names not present in our compact `Icons` map render as `?`.
|
||||||
|
- Plugins that declare only `desktopWidget` or `launcherProvider` and no
|
||||||
|
bar/panel/main do not appear in the Omarchy catalog.
|
||||||
|
- Some plugins call `Color.mPrimary` for accent colors that don't perfectly
|
||||||
|
match Omarchy's theme palette; results are close but not pixel-identical.
|
||||||
@@ -16,22 +16,25 @@ User-installed plugins live alongside these conceptually but on disk under
|
|||||||
|
|
||||||
## Bar
|
## Bar
|
||||||
|
|
||||||
The status bar. Mounted at startup, lives forever. Layout is configured
|
The status bar. Mounted at startup, lives forever. Layout lives in the
|
||||||
through `~/.config/omarchy/bar.json` (deep-merged over
|
top-level `bar:` subtree of `~/.config/omarchy/shell.json` (with the shell
|
||||||
[`bar/bar-defaults.json`](bar/bar-defaults.json)). Owns the `bar` IPC
|
providing [`shell-defaults.json`](../shell-defaults.json) when the user has
|
||||||
target for refresh hooks fired by indicator scripts. See
|
no file). Owns the `bar` IPC target for refresh hooks fired by indicator
|
||||||
[`bar/README.md`](bar/README.md) for the widget catalogue and customization
|
scripts. See [`bar/README.md`](bar/README.md) for the widget catalogue
|
||||||
schema.
|
and customization schema.
|
||||||
|
|
||||||
## Bar settings
|
## Bar settings
|
||||||
|
|
||||||
Visual editor for the bar layout. Summoned by
|
Visual editor for the entire shell config. Summoned by
|
||||||
`omarchy-shell-ipc shell summon omarchy.bar-settings "{}"` (which is what
|
`omarchy-shell-ipc shell summon omarchy.bar-settings "{}"` (which is what
|
||||||
`omarchy launch bar-settings` ultimately calls). Provides:
|
`omarchy launch bar-settings` ultimately calls). Provides:
|
||||||
|
|
||||||
- per-section add/move/remove/edit of widget entries
|
- per-section add/move/remove/edit of bar widget entries
|
||||||
- a Plugin Manager tab for enabling/disabling third-party plugins
|
- a separate "Other plugins" section for panels, overlays, services,
|
||||||
- a dynamic settings form driven by each widget's manifest schema
|
and menus (entries that live in `plugins[]` rather than the bar layout)
|
||||||
|
- a Plugin Manager tab listing every discovered plugin with its manifest
|
||||||
|
- a dynamic settings form driven by each widget's manifest schema, that
|
||||||
|
writes inline back to the corresponding shell.json entry
|
||||||
|
|
||||||
## Image picker
|
## Image picker
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ Item {
|
|||||||
// so the panel sees the same registry state the bar wrote into.
|
// so the panel sees the same registry state the bar wrote into.
|
||||||
property var barWidgetRegistry: null
|
property var barWidgetRegistry: null
|
||||||
property var pluginRegistry: null
|
property var pluginRegistry: null
|
||||||
|
// The host shell. Used to look up pluginApi for Noctalia plugins so their
|
||||||
|
// Settings.qml can read/write via pluginApi.saveSettings().
|
||||||
|
property var shell: null
|
||||||
|
|
||||||
// Not `required` so the Loader-based instantiation can satisfy it via
|
// Not `required` so the Loader-based instantiation can satisfy it via
|
||||||
// onLoaded; we still gracefully fall back to deriving from shellDir for
|
// onLoaded; we still gracefully fall back to deriving from shellDir for
|
||||||
@@ -32,8 +35,8 @@ Item {
|
|||||||
return Quickshell.env("HOME") + "/.local/share/omarchy"
|
return Quickshell.env("HOME") + "/.local/share/omarchy"
|
||||||
}
|
}
|
||||||
readonly property string home: Quickshell.env("HOME")
|
readonly property string home: Quickshell.env("HOME")
|
||||||
readonly property string userConfigPath: home + "/.config/omarchy/bar.json"
|
readonly property string userConfigPath: home + "/.config/omarchy/shell.json"
|
||||||
readonly property string defaultsPath: omarchyPath + "/default/quickshell/omarchy-shell/plugins/bar/bar-defaults.json"
|
readonly property string defaultsPath: omarchyPath + "/default/quickshell/omarchy-shell/shell-defaults.json"
|
||||||
|
|
||||||
property color foreground: "#cacccc"
|
property color foreground: "#cacccc"
|
||||||
property color background: "#101315"
|
property color background: "#101315"
|
||||||
@@ -43,32 +46,41 @@ Item {
|
|||||||
property string fontFamily: "JetBrainsMono Nerd Font"
|
property string fontFamily: "JetBrainsMono Nerd Font"
|
||||||
property string activeTab: "layout"
|
property string activeTab: "layout"
|
||||||
|
|
||||||
// Bundled fallback so 'Reset to defaults' never produces an empty bar even
|
// Bundled fallback so 'Reset to defaults' never produces an empty config
|
||||||
// if bar-defaults.json fails to load. Keep in rough sync with the layout
|
// even if shell-defaults.json fails to load. Keep in rough sync with the
|
||||||
// shipped in default/quickshell/omarchy-shell/plugins/bar/bar-defaults.json.
|
// file shipped at default/quickshell/omarchy-shell/shell-defaults.json.
|
||||||
readonly property var builtinBarConfig: ({
|
readonly property var builtinShellConfig: ({
|
||||||
position: "top",
|
version: 1,
|
||||||
fontFamily: "JetBrainsMono Nerd Font",
|
bar: {
|
||||||
centerAnchor: "calendar",
|
position: "top",
|
||||||
layout: {
|
fontFamily: "JetBrainsMono Nerd Font",
|
||||||
left: [{ id: "omarchy" }, { id: "workspacesPro" }, { id: "activeWindow" }],
|
centerAnchor: "calendar",
|
||||||
center: [
|
layout: {
|
||||||
{ id: "media" },
|
left: [{ id: "omarchy" }, { id: "workspacesPro" }, { id: "activeWindow" }],
|
||||||
{ id: "calendar", format: "dddd HH:mm", formatAlt: "dd MMMM 'W'ww yyyy", verticalFormat: "HH\n—\nmm" },
|
center: [
|
||||||
{ id: "weatherFlyout" }, { id: "update" }, { id: "voxtype" },
|
{ id: "media" },
|
||||||
{ id: "screenRecording" }, { id: "idle" }, { id: "notifications" }
|
{ id: "calendar", format: "dddd HH:mm", formatAlt: "dd MMMM 'W'ww yyyy", verticalFormat: "HH\n—\nmm" },
|
||||||
],
|
{ id: "weatherFlyout" }, { id: "update" }, { id: "voxtype" },
|
||||||
right: [
|
{ id: "screenRecording" }, { id: "idle" }, { id: "notifications" }
|
||||||
{ id: "tray" }, { id: "systemStats" }, { id: "microphone" },
|
],
|
||||||
{ id: "bluetoothPanel" }, { id: "networkPanel" }, { id: "audioPanel" },
|
right: [
|
||||||
{ id: "nightLight" }, { id: "brightness" }, { id: "powerProfile" },
|
{ id: "tray" }, { id: "systemStats" }, { id: "microphone" },
|
||||||
{ id: "battery" }, { id: "controlCenter" }, { id: "powerMenu" }
|
{ id: "bluetoothPanel" }, { id: "networkPanel" }, { id: "audioPanel" },
|
||||||
]
|
{ id: "nightLight" }, { id: "brightness" }, { id: "powerProfile" },
|
||||||
}
|
{ id: "battery" }, { id: "controlCenter" }, { id: "powerMenu" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
{ id: "omarchy.bar-settings" },
|
||||||
|
{ id: "omarchy.image-picker" }
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
property var defaultConfig: builtinBarConfig
|
property var defaultConfig: builtinShellConfig
|
||||||
property var draft: ({ position: "top", centerAnchor: "calendar", layout: { left: [], center: [], right: [] }, fontFamily: "JetBrainsMono Nerd Font" })
|
// The draft mirrors the full shell.json on-disk shape. Editors operate on
|
||||||
|
// draft.bar.layout.* and draft.plugins; the file we persist is `draft`.
|
||||||
|
property var draft: ({ version: 1, bar: { position: "top", centerAnchor: "calendar", fontFamily: "JetBrainsMono Nerd Font", layout: { left: [], center: [], right: [] } }, plugins: [] })
|
||||||
property var registry: ({})
|
property var registry: ({})
|
||||||
property int draftRevision: 0
|
property int draftRevision: 0
|
||||||
property bool suppressReload: false
|
property bool suppressReload: false
|
||||||
@@ -116,32 +128,53 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
var defaults = builtinBarConfig
|
var defaults = builtinShellConfig
|
||||||
var diskText = defaultsFile.text()
|
var diskText = defaultsFile.text()
|
||||||
if (diskText) {
|
if (diskText) {
|
||||||
try {
|
try {
|
||||||
defaults = JSON.parse(diskText)
|
var parsed = JSON.parse(diskText)
|
||||||
|
if (isPlainObject(parsed) && parsed.version === 1) defaults = parsed
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Bad defaults JSON, falling back to builtin:", e)
|
console.warn("Bad shell-defaults JSON, falling back to builtin:", e)
|
||||||
defaults = builtinBarConfig
|
defaults = builtinShellConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
defaultConfig = defaults
|
defaultConfig = defaults
|
||||||
|
|
||||||
var userText = userFile.text() || "{}"
|
// shell.json is canonical when present and valid; otherwise we fall back
|
||||||
var user = {}
|
// to defaults. We do NOT deep-merge — once the user has a shell.json, the
|
||||||
try { user = JSON.parse(userText) } catch (e) { user = {} }
|
// file's contents are authoritative.
|
||||||
|
var userText = userFile.text() || ""
|
||||||
var merged = mergeConfig(defaultConfig, user)
|
var source = defaults
|
||||||
draft = {
|
if (userText.trim()) {
|
||||||
position: String(merged.position || "top"),
|
try {
|
||||||
centerAnchor: String(merged.centerAnchor || ""),
|
var u = JSON.parse(userText)
|
||||||
fontFamily: String(merged.fontFamily || "JetBrainsMono Nerd Font"),
|
if (isPlainObject(u) && u.version === 1) source = u
|
||||||
layout: normalizeLayout(merged.layout || {})
|
} catch (e) {
|
||||||
|
console.warn("shell.json parse failed in panel:", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
draft = normalizeDraft(source)
|
||||||
draftRevision++
|
draftRevision++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDraft(source) {
|
||||||
|
var bar = isPlainObject(source.bar) ? source.bar : {}
|
||||||
|
var plugins = Array.isArray(source.plugins) ? source.plugins.slice() : []
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
bar: {
|
||||||
|
position: String(bar.position || "top"),
|
||||||
|
centerAnchor: String(bar.centerAnchor || ""),
|
||||||
|
fontFamily: String(bar.fontFamily || "JetBrainsMono Nerd Font"),
|
||||||
|
layout: normalizeLayout(bar.layout || {})
|
||||||
|
},
|
||||||
|
plugins: plugins
|
||||||
|
.map(normalizeLayoutEntry)
|
||||||
|
.filter(function(e) { return !!e })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function persistDraft() {
|
function persistDraft() {
|
||||||
// Suppress the inotify callback that this write triggers so the FileView
|
// Suppress the inotify callback that this write triggers so the FileView
|
||||||
// reload doesn't race with rapid edits and clobber them.
|
// reload doesn't race with rapid edits and clobber them.
|
||||||
@@ -154,19 +187,14 @@ Item {
|
|||||||
// (path resolution failed or defaultsFile hasn't finished loading), so
|
// (path resolution failed or defaultsFile hasn't finished loading), so
|
||||||
// Reset never zeroes the bar out.
|
// Reset never zeroes the bar out.
|
||||||
var source = defaultConfig
|
var source = defaultConfig
|
||||||
if (!isPlainObject(source) || !isPlainObject(source.layout)) {
|
if (!isPlainObject(source) || !isPlainObject(source.bar) || !isPlainObject(source.bar.layout)) {
|
||||||
source = builtinBarConfig
|
source = builtinShellConfig
|
||||||
} else {
|
} else {
|
||||||
var l = source.layout
|
var l = source.bar.layout
|
||||||
var anyEntries = (l.left && l.left.length) || (l.center && l.center.length) || (l.right && l.right.length)
|
var anyEntries = (l.left && l.left.length) || (l.center && l.center.length) || (l.right && l.right.length)
|
||||||
if (!anyEntries) source = builtinBarConfig
|
if (!anyEntries) source = builtinShellConfig
|
||||||
}
|
|
||||||
var payload = {
|
|
||||||
position: String(source.position || "top"),
|
|
||||||
centerAnchor: String(source.centerAnchor || ""),
|
|
||||||
fontFamily: String(source.fontFamily || "JetBrainsMono Nerd Font"),
|
|
||||||
layout: normalizeLayout(source.layout || {})
|
|
||||||
}
|
}
|
||||||
|
var payload = normalizeDraft(source)
|
||||||
// Update the GUI synchronously — the suppressed file-watch callback won't
|
// Update the GUI synchronously — the suppressed file-watch callback won't
|
||||||
// fire loadConfig, so the draft would otherwise stay stale.
|
// fire loadConfig, so the draft would otherwise stay stale.
|
||||||
draft = payload
|
draft = payload
|
||||||
@@ -180,44 +208,45 @@ Item {
|
|||||||
persistDraft()
|
persistDraft()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace the whole `layout` object so any binding that reads `draft.layout`
|
// Section list helpers operate on a virtual section name. "left" / "center"
|
||||||
// is invalidated. Mutating `draft.layout[section]` alone does not notify QML.
|
// / "right" address draft.bar.layout.<section>; "plugins" addresses
|
||||||
function mutateLayout(section, mutator) {
|
// draft.plugins. The mutators replace the whole `draft` so any binding that
|
||||||
var nextLayout = {
|
// reads it re-evaluates.
|
||||||
left: draft.layout.left.slice(),
|
function sectionArray(section) {
|
||||||
center: draft.layout.center.slice(),
|
if (section === "plugins") return draft.plugins || []
|
||||||
right: draft.layout.right.slice()
|
return (draft.bar && draft.bar.layout && draft.bar.layout[section]) || []
|
||||||
}
|
}
|
||||||
mutator(nextLayout[section])
|
|
||||||
var nextDraft = {
|
function mutateSection(section, mutator) {
|
||||||
position: draft.position,
|
var arr = sectionArray(section).slice()
|
||||||
centerAnchor: draft.centerAnchor,
|
mutator(arr)
|
||||||
fontFamily: draft.fontFamily,
|
var nextDraft = cloneJson(draft)
|
||||||
layout: nextLayout
|
if (section === "plugins") nextDraft.plugins = arr
|
||||||
}
|
else nextDraft.bar.layout[section] = arr
|
||||||
draft = nextDraft
|
draft = nextDraft
|
||||||
markDirty()
|
markDirty()
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveEntry(section, fromIndex, toIndex) {
|
function moveEntry(section, fromIndex, toIndex) {
|
||||||
if (toIndex < 0 || toIndex >= draft.layout[section].length) return
|
var arr = sectionArray(section)
|
||||||
mutateLayout(section, function(arr) {
|
if (toIndex < 0 || toIndex >= arr.length) return
|
||||||
var item = arr[fromIndex]
|
mutateSection(section, function(a) {
|
||||||
arr.splice(fromIndex, 1)
|
var item = a[fromIndex]
|
||||||
arr.splice(toIndex, 0, item)
|
a.splice(fromIndex, 1)
|
||||||
|
a.splice(toIndex, 0, item)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeEntry(section, index) {
|
function removeEntry(section, index) {
|
||||||
mutateLayout(section, function(arr) { arr.splice(index, 1) })
|
mutateSection(section, function(a) { a.splice(index, 1) })
|
||||||
}
|
}
|
||||||
|
|
||||||
function addEntry(section, id) {
|
function addEntry(section, id) {
|
||||||
mutateLayout(section, function(arr) { arr.push({ id: id }) })
|
mutateSection(section, function(a) { a.push({ id: id }) })
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateEntry(section, index, newEntry) {
|
function updateEntry(section, index, newEntry) {
|
||||||
mutateLayout(section, function(arr) { arr[index] = cloneJson(newEntry) })
|
mutateSection(section, function(a) { a[index] = cloneJson(newEntry) })
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadTheme(raw) {
|
function loadTheme(raw) {
|
||||||
@@ -297,12 +326,23 @@ Item {
|
|||||||
var rev = catalogRevision
|
var rev = catalogRevision
|
||||||
var meta = widgetMetadata(id)
|
var meta = widgetMetadata(id)
|
||||||
if (meta.settingsForm) return true
|
if (meta.settingsForm) return true
|
||||||
return widgetSchema(id).length > 0
|
if (widgetSchema(id).length > 0) return true
|
||||||
|
// Noctalia plugins ship their own Settings.qml — surface the gear icon
|
||||||
|
// even though we don't render a built-in form for them.
|
||||||
|
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[id] : null
|
||||||
|
if (manifest && manifest.__noctaliaCompat && manifest.entryPoints && manifest.entryPoints.settings)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function widgetIsPlugin(id) {
|
function widgetIsPlugin(id) {
|
||||||
var meta = widgetMetadata(id)
|
var meta = widgetMetadata(id)
|
||||||
return meta.source === "plugin" || String(id).indexOf("plugin:") === 0
|
return meta.source === "plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
function widgetIsNoctaliaPlugin(id) {
|
||||||
|
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[id] : null
|
||||||
|
return !!(manifest && manifest.__noctaliaCompat)
|
||||||
}
|
}
|
||||||
|
|
||||||
function widgetAllowsMultiple(id) {
|
function widgetAllowsMultiple(id) {
|
||||||
@@ -322,18 +362,24 @@ Item {
|
|||||||
return Object.keys(ids)
|
return Object.keys(ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// section is one of "left" / "center" / "right" for bar widgets, or
|
||||||
|
// "plugins" for the non-bar plugin list. The catalogue we offer differs:
|
||||||
|
// bar sections accept anything with kind `bar-widget` (or anything in the
|
||||||
|
// legacy widget metadata); the plugins section accepts panel/overlay/menu/
|
||||||
|
// service kinds.
|
||||||
function availableToAdd(section) {
|
function availableToAdd(section) {
|
||||||
var rev = catalogRevision
|
var rev = catalogRevision
|
||||||
var existingByOther = {}
|
var isBarSection = section === "left" || section === "center" || section === "right"
|
||||||
var sections = ["left", "center", "right"]
|
var barSections = ["left", "center", "right"]
|
||||||
for (var s = 0; s < sections.length; s++) {
|
|
||||||
if (sections[s] === section) continue
|
var existingInBar = {}
|
||||||
var list = draft.layout[sections[s]] || []
|
for (var s = 0; s < barSections.length; s++) {
|
||||||
for (var i = 0; i < list.length; i++) existingByOther[list[i].id] = true
|
var list = sectionArray(barSections[s])
|
||||||
|
for (var i = 0; i < list.length; i++) existingInBar[list[i].id] = true
|
||||||
}
|
}
|
||||||
var existingHere = {}
|
var existingInPlugins = {}
|
||||||
var here = draft.layout[section] || []
|
var pluginList = sectionArray("plugins")
|
||||||
for (var j = 0; j < here.length; j++) existingHere[here[j].id] = true
|
for (var p = 0; p < pluginList.length; p++) existingInPlugins[pluginList[p].id] = true
|
||||||
|
|
||||||
var ids = catalogIds().sort(function(a, b) {
|
var ids = catalogIds().sort(function(a, b) {
|
||||||
return widgetName(a).localeCompare(widgetName(b))
|
return widgetName(a).localeCompare(widgetName(b))
|
||||||
@@ -342,8 +388,35 @@ Item {
|
|||||||
var result = []
|
var result = []
|
||||||
for (var k = 0; k < ids.length; k++) {
|
for (var k = 0; k < ids.length; k++) {
|
||||||
var id = ids[k]
|
var id = ids[k]
|
||||||
if (!widgetAllowsMultiple(id) && existingHere[id]) continue
|
var meta = widgetMetadata(id)
|
||||||
result.push({ id: id, name: widgetName(id), description: widgetDescription(id), elsewhere: !!existingByOther[id] })
|
var isBarWidget = !!(meta && meta.source !== "plugin")
|
||||||
|
|| (meta && meta.kinds && meta.kinds.indexOf && meta.kinds.indexOf("bar-widget") !== -1)
|
||||||
|
if (isBarSection) {
|
||||||
|
if (!isBarWidget && !legacyWidgetMeta[id]) continue
|
||||||
|
var inSection = sectionArray(section)
|
||||||
|
var existsHere = false
|
||||||
|
for (var x = 0; x < inSection.length; x++) if (inSection[x].id === id) { existsHere = true; break }
|
||||||
|
var allowsMultiple = widgetAllowsMultiple(id)
|
||||||
|
// Hard block: if it's already placed in any bar section and the
|
||||||
|
// widget doesn't permit multiple instances, drop it from the menu
|
||||||
|
// entirely rather than offer an "(elsewhere)" entry that would
|
||||||
|
// silently move/duplicate state.
|
||||||
|
if (!allowsMultiple && existingInBar[id]) continue
|
||||||
|
// For widgets that do allow multiple (spacer), the `elsewhere`
|
||||||
|
// hint is informational only.
|
||||||
|
result.push({ id: id, name: widgetName(id), description: widgetDescription(id),
|
||||||
|
elsewhere: allowsMultiple && !!existingInBar[id] && !existsHere,
|
||||||
|
isNoctalia: widgetIsNoctaliaPlugin(id) })
|
||||||
|
} else {
|
||||||
|
// Plugins section: accept anything that has a manifest in the
|
||||||
|
// registry (plugins[] holds non-bar plugins — panel/overlay/menu/
|
||||||
|
// service — keyed by manifest id).
|
||||||
|
var hasManifest = !!(root.pluginRegistry && root.pluginRegistry.installedPlugins[id])
|
||||||
|
if (!hasManifest) continue
|
||||||
|
if (existingInPlugins[id]) continue
|
||||||
|
result.push({ id: id, name: widgetName(id), description: widgetDescription(id), elsewhere: false,
|
||||||
|
isNoctalia: widgetIsNoctaliaPlugin(id) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -432,7 +505,7 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "Auto-saving to ~/.config/omarchy/bar.json"
|
text: "Auto-saving to ~/.config/omarchy/shell.json"
|
||||||
color: Qt.darker(root.foreground, 1.5)
|
color: Qt.darker(root.foreground, 1.5)
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: 11
|
font.pixelSize: 11
|
||||||
@@ -453,25 +526,29 @@ Item {
|
|||||||
|
|
||||||
OptionDropdown {
|
OptionDropdown {
|
||||||
label: "Position"
|
label: "Position"
|
||||||
value: root.draft.position
|
value: root.draft.bar.position
|
||||||
options: ["top", "right", "bottom", "left"]
|
options: ["top", "right", "bottom", "left"]
|
||||||
onChanged: function(v) {
|
onChanged: function(v) {
|
||||||
root.draft.position = v
|
var next = root.cloneJson(root.draft)
|
||||||
|
next.bar.position = v
|
||||||
|
root.draft = next
|
||||||
root.markDirty()
|
root.markDirty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionDropdown {
|
OptionDropdown {
|
||||||
label: "Center anchor"
|
label: "Center anchor"
|
||||||
value: root.draft.centerAnchor
|
value: root.draft.bar.centerAnchor
|
||||||
options: {
|
options: {
|
||||||
var list = ["(none)"]
|
var list = ["(none)"]
|
||||||
var entries = root.draft.layout.center || []
|
var entries = root.draft.bar.layout.center || []
|
||||||
for (var i = 0; i < entries.length; i++) list.push(entries[i].id)
|
for (var i = 0; i < entries.length; i++) list.push(entries[i].id)
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
onChanged: function(v) {
|
onChanged: function(v) {
|
||||||
root.draft.centerAnchor = v === "(none)" ? "" : v
|
var next = root.cloneJson(root.draft)
|
||||||
|
next.bar.centerAnchor = v === "(none)" ? "" : v
|
||||||
|
root.draft = next
|
||||||
root.markDirty()
|
root.markDirty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -525,9 +602,17 @@ Item {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 14
|
spacing: 14
|
||||||
|
|
||||||
SectionEditor { sectionKey: "left"; sectionLabel: "Left" }
|
SectionEditor { sectionKey: "left"; sectionLabel: "Bar · Left" }
|
||||||
SectionEditor { sectionKey: "center"; sectionLabel: "Center" }
|
SectionEditor { sectionKey: "center"; sectionLabel: "Bar · Center" }
|
||||||
SectionEditor { sectionKey: "right"; sectionLabel: "Right" }
|
SectionEditor { sectionKey: "right"; sectionLabel: "Bar · Right" }
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 1
|
||||||
|
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.12)
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionEditor { sectionKey: "plugins"; sectionLabel: "Other plugins" }
|
||||||
}
|
}
|
||||||
|
|
||||||
PluginManager {
|
PluginManager {
|
||||||
@@ -638,13 +723,13 @@ Item {
|
|||||||
|
|
||||||
property string sectionKey: ""
|
property string sectionKey: ""
|
||||||
property string sectionLabel: ""
|
property string sectionLabel: ""
|
||||||
property var entries: (root.draft.layout && root.draft.layout[section.sectionKey]) || []
|
property var entries: root.sectionArray(section.sectionKey)
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root
|
target: root
|
||||||
function onDraftRevisionChanged() { section.entries = (root.draft.layout && root.draft.layout[section.sectionKey]) || [] }
|
function onDraftRevisionChanged() { section.entries = root.sectionArray(section.sectionKey) }
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
@@ -681,7 +766,11 @@ Item {
|
|||||||
model: root.availableToAdd(section.sectionKey)
|
model: root.availableToAdd(section.sectionKey)
|
||||||
delegate: MenuItem {
|
delegate: MenuItem {
|
||||||
required property var modelData
|
required property var modelData
|
||||||
text: modelData.name + (modelData.elsewhere ? " (elsewhere)" : "")
|
// Suffix order matches what users skim left-to-right:
|
||||||
|
// name first, origin badge, then "elsewhere" if shared across sections.
|
||||||
|
text: modelData.name
|
||||||
|
+ (modelData.isNoctalia ? " (Noctalia)" : "")
|
||||||
|
+ (modelData.elsewhere ? " (elsewhere)" : "")
|
||||||
onTriggered: root.addEntry(section.sectionKey, modelData.id)
|
onTriggered: root.addEntry(section.sectionKey, modelData.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -934,6 +1023,11 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolution order:
|
||||||
|
// 1) First-party inline forms (id-keyed switch).
|
||||||
|
// 2) Noctalia plugin's own Settings.qml — we load it inside a Loader
|
||||||
|
// so the plugin renders its native UI and writes via pluginApi.
|
||||||
|
// 3) Generic DynamicSettingsForm built from manifest schema entries.
|
||||||
function formComponent(id) {
|
function formComponent(id) {
|
||||||
var meta = widgetMetadata(id)
|
var meta = widgetMetadata(id)
|
||||||
if (meta && meta.settingsForm) {
|
if (meta && meta.settingsForm) {
|
||||||
@@ -943,6 +1037,9 @@ Item {
|
|||||||
case "brightnessSettings": return brightnessSettingsComponent
|
case "brightnessSettings": return brightnessSettingsComponent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[id] : null
|
||||||
|
if (manifest && manifest.__noctaliaCompat && manifest.entryPoints && manifest.entryPoints.settings)
|
||||||
|
return noctaliaSettingsComponent
|
||||||
if (widgetSchema(id).length > 0) return dynamicSettingsComponent
|
if (widgetSchema(id).length > 0) return dynamicSettingsComponent
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -956,6 +1053,51 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Loader stub for Noctalia plugins that bundle a Settings.qml. We load the
|
||||||
|
// plugin's form and inject pluginApi so the plugin's own "save" button
|
||||||
|
// routes through pluginApi.saveSettings() — which lands in shell.json via
|
||||||
|
// shell.updateEntryInline. The plugin form doesn't emit `fieldChanged`,
|
||||||
|
// so the dialog's Apply/Cancel buttons are mostly decorative for these
|
||||||
|
// (writes already happened by the time you click Apply).
|
||||||
|
Component {
|
||||||
|
id: noctaliaSettingsComponent
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: noctaliaForm
|
||||||
|
property var entry: ({})
|
||||||
|
property string pluginId: entry && entry.id ? String(entry.id) : ""
|
||||||
|
property var manifest: pluginId && root.pluginRegistry
|
||||||
|
? root.pluginRegistry.installedPlugins[pluginId] : null
|
||||||
|
|
||||||
|
implicitHeight: settingsLoader.item ? settingsLoader.item.implicitHeight : 0
|
||||||
|
implicitWidth: settingsLoader.item ? settingsLoader.item.implicitWidth : 0
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: settingsLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
source: {
|
||||||
|
if (!noctaliaForm.manifest) return ""
|
||||||
|
return root.pluginRegistry.entryPointUrl(noctaliaForm.manifest, "settings")
|
||||||
|
}
|
||||||
|
asynchronous: false
|
||||||
|
onLoaded: {
|
||||||
|
if (!item) return
|
||||||
|
var api = (root.shell && typeof root.shell.noctaliaPluginApiFor === "function")
|
||||||
|
? root.shell.noctaliaPluginApiFor(noctaliaForm.pluginId) : null
|
||||||
|
if (api && "pluginApi" in item) item.pluginApi = api
|
||||||
|
if ("screen" in item && root.QsWindow && root.QsWindow.window)
|
||||||
|
item.screen = root.QsWindow.window.screen
|
||||||
|
}
|
||||||
|
onStatusChanged: {
|
||||||
|
if (status === Loader.Error) {
|
||||||
|
console.warn("noctalia Settings.qml failed for " + noctaliaForm.pluginId + ":",
|
||||||
|
sourceComponent ? sourceComponent.errorString() : "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: spacerSettingsComponent
|
id: spacerSettingsComponent
|
||||||
|
|
||||||
|
|||||||
@@ -19,29 +19,26 @@ Item {
|
|||||||
// Injected by the host shell. Shared with the bar-settings panel so both
|
// Injected by the host shell. Shared with the bar-settings panel so both
|
||||||
// see the same widget catalogue.
|
// see the same widget catalogue.
|
||||||
required property var barWidgetRegistry
|
required property var barWidgetRegistry
|
||||||
|
// Injected by the host shell every time shell.json is reloaded. Holds the
|
||||||
|
// `bar:` subtree: position, centerAnchor, fontFamily, layout. The host owns
|
||||||
|
// file IO; the bar just renders whatever it's handed.
|
||||||
|
required property var barConfig
|
||||||
|
// Injected by the host shell so the bar can detect Noctalia-compat plugins
|
||||||
|
// and look up manifests when wiring per-widget Noctalia pluginApi.
|
||||||
|
property var pluginRegistry: null
|
||||||
|
// Injected by the host shell. Used so Noctalia plugins that reach for
|
||||||
|
// shell-wide APIs (openPanel, currentScreen) can do so via pluginApi.
|
||||||
|
property var shell: null
|
||||||
property string home: Quickshell.env("HOME")
|
property string home: Quickshell.env("HOME")
|
||||||
property string omarchyConfigDir: home + "/.config/omarchy"
|
property string omarchyConfigDir: home + "/.config/omarchy"
|
||||||
property var builtinBarConfig: ({
|
property var fallbackBarConfig: ({
|
||||||
position: "top",
|
position: "top",
|
||||||
fontFamily: "JetBrainsMono Nerd Font",
|
fontFamily: "JetBrainsMono Nerd Font",
|
||||||
centerAnchor: "clock",
|
centerAnchor: "calendar",
|
||||||
layout: {
|
layout: { left: [], center: [], right: [] }
|
||||||
left: [{ id: "omarchy" }, { id: "workspaces" }],
|
|
||||||
center: [
|
|
||||||
{ id: "clock", format: "dddd HH:mm", formatAlt: "dd MMMM 'W'ww yyyy", verticalFormat: "HH\n—\nmm" },
|
|
||||||
{ id: "weather" }, { id: "update" }, { id: "voxtype" },
|
|
||||||
{ id: "screenRecording" }, { id: "idle" }, { id: "notifications" }
|
|
||||||
],
|
|
||||||
right: [
|
|
||||||
{ id: "tray" }, { id: "bluetooth" }, { id: "network" },
|
|
||||||
{ id: "audio" }, { id: "cpu" }, { id: "battery" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
property var defaultBarConfig: builtinBarConfig
|
property var layoutConfig: fallbackBarConfig.layout
|
||||||
property var userBarConfig: ({})
|
property string centerAnchor: ""
|
||||||
property var layoutConfig: builtinBarConfig.layout
|
|
||||||
property string centerAnchor: "clock"
|
|
||||||
property int barConfigSerial: 0
|
property int barConfigSerial: 0
|
||||||
property string position: "top"
|
property string position: "top"
|
||||||
property string fontFamily: "JetBrainsMono Nerd Font"
|
property string fontFamily: "JetBrainsMono Nerd Font"
|
||||||
@@ -80,6 +77,40 @@ Item {
|
|||||||
if (activePopout === owner) activePopout = null
|
if (activePopout === owner) activePopout = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------- Noctalia compat helpers
|
||||||
|
//
|
||||||
|
// The shell owns pluginApi creation and Main.qml service instantiation now;
|
||||||
|
// bar widgets just look up their api by moduleName. Keeping section/index
|
||||||
|
// helpers here because they're needed for Noctalia bar-widget injection
|
||||||
|
// (widgetId, section, sectionWidgetIndex, sectionWidgetsCount).
|
||||||
|
|
||||||
|
function sectionOfEntry(entry) {
|
||||||
|
var sections = ["left", "center", "right"]
|
||||||
|
for (var s = 0; s < sections.length; s++) {
|
||||||
|
var list = layoutConfig[sections[s]] || []
|
||||||
|
for (var i = 0; i < list.length; i++) {
|
||||||
|
if (list[i] === entry) return sections[s]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexOfEntry(entry) {
|
||||||
|
var section = sectionOfEntry(entry)
|
||||||
|
var list = section ? (layoutConfig[section] || []) : []
|
||||||
|
for (var i = 0; i < list.length; i++) if (list[i] === entry) return i
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function entriesOfSection(section) {
|
||||||
|
return Array.isArray(layoutConfig[section]) ? layoutConfig[section] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
function noctaliaPluginApiFor(moduleName) {
|
||||||
|
if (!shell || typeof shell.noctaliaPluginApiFor !== "function") return null
|
||||||
|
return shell.noctaliaPluginApiFor(moduleName)
|
||||||
|
}
|
||||||
|
|
||||||
readonly property bool vertical: position === "left" || position === "right"
|
readonly property bool vertical: position === "left" || position === "right"
|
||||||
readonly property int barSize: vertical ? 28 : 26
|
readonly property int barSize: vertical ? 28 : 26
|
||||||
|
|
||||||
@@ -104,51 +135,6 @@ Item {
|
|||||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneConfig(value) {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
var arrayCopy = []
|
|
||||||
for (var i = 0; i < value.length; i++)
|
|
||||||
arrayCopy.push(cloneConfig(value[i]))
|
|
||||||
return arrayCopy
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlainObject(value)) {
|
|
||||||
var objectCopy = {}
|
|
||||||
for (var key in value)
|
|
||||||
objectCopy[key] = cloneConfig(value[key])
|
|
||||||
return objectCopy
|
|
||||||
}
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeConfig(base, override) {
|
|
||||||
var result = cloneConfig(base || {})
|
|
||||||
if (!isPlainObject(override)) return result
|
|
||||||
|
|
||||||
for (var key in override) {
|
|
||||||
if (isPlainObject(result[key]) && isPlainObject(override[key]))
|
|
||||||
result[key] = mergeConfig(result[key], override[key])
|
|
||||||
else
|
|
||||||
result[key] = cloneConfig(override[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseConfig(raw, label) {
|
|
||||||
var text = String(raw || "").trim()
|
|
||||||
if (!text) return {}
|
|
||||||
|
|
||||||
try {
|
|
||||||
var parsed = JSON.parse(text)
|
|
||||||
return isPlainObject(parsed) ? parsed : {}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Failed to parse " + label + ": " + error)
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeLayoutEntry(entry) {
|
function normalizeLayoutEntry(entry) {
|
||||||
if (typeof entry === "string") return { id: entry }
|
if (typeof entry === "string") return { id: entry }
|
||||||
if (isPlainObject(entry) && entry.id) return entry
|
if (isPlainObject(entry) && entry.id) return entry
|
||||||
@@ -166,7 +152,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeLayout(layout) {
|
function normalizeLayout(layout) {
|
||||||
if (!isPlainObject(layout)) layout = builtinBarConfig.layout
|
if (!isPlainObject(layout)) layout = fallbackBarConfig.layout
|
||||||
return {
|
return {
|
||||||
left: normalizeLayoutSection(layout.left),
|
left: normalizeLayoutSection(layout.left),
|
||||||
center: normalizeLayoutSection(layout.center),
|
center: normalizeLayoutSection(layout.center),
|
||||||
@@ -175,24 +161,16 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyBarConfig() {
|
function applyBarConfig() {
|
||||||
var config = mergeConfig(defaultBarConfig, userBarConfig)
|
var config = isPlainObject(barConfig) ? barConfig : fallbackBarConfig
|
||||||
|
|
||||||
position = normalizePosition(config.position)
|
position = normalizePosition(config.position)
|
||||||
fontFamily = String(config.fontFamily || "JetBrainsMono Nerd Font")
|
fontFamily = String(config.fontFamily || "JetBrainsMono Nerd Font")
|
||||||
centerAnchor = String(config.centerAnchor || "clock")
|
centerAnchor = String(config.centerAnchor || "")
|
||||||
layoutConfig = normalizeLayout(config.layout)
|
layoutConfig = normalizeLayout(config.layout)
|
||||||
barConfigSerial++
|
barConfigSerial++
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadDefaultBarConfig(raw) {
|
onBarConfigChanged: applyBarConfig()
|
||||||
defaultBarConfig = mergeConfig(builtinBarConfig, parseConfig(raw, "bar defaults"))
|
|
||||||
applyBarConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadUserBarConfig(raw) {
|
|
||||||
userBarConfig = parseConfig(raw, "bar config")
|
|
||||||
applyBarConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
function layoutEntries(region) {
|
function layoutEntries(region) {
|
||||||
var serial = barConfigSerial
|
var serial = barConfigSerial
|
||||||
@@ -327,7 +305,10 @@ Item {
|
|||||||
|
|
||||||
property var registeredFirstPartyComponents: ({})
|
property var registeredFirstPartyComponents: ({})
|
||||||
|
|
||||||
Component.onCompleted: registerFirstPartyWidgets()
|
Component.onCompleted: {
|
||||||
|
registerFirstPartyWidgets()
|
||||||
|
applyBarConfig()
|
||||||
|
}
|
||||||
|
|
||||||
function registerFirstPartyWidgets() {
|
function registerFirstPartyWidgets() {
|
||||||
var ids = Object.keys(firstPartyWidgetMetadata)
|
var ids = Object.keys(firstPartyWidgetMetadata)
|
||||||
@@ -713,22 +694,8 @@ Item {
|
|||||||
onTriggered: root.tooltipShown = true
|
onTriggered: root.tooltipShown = true
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
// The host owns shell.json loading and injects `barConfig`. Bar still keeps
|
||||||
path: root.omarchyPath + "/default/quickshell/omarchy-shell/plugins/bar/bar-defaults.json"
|
// its own theme FileView since theme colors are independent of shell.json.
|
||||||
watchChanges: true
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: root.loadDefaultBarConfig(text())
|
|
||||||
onFileChanged: reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
|
||||||
path: root.omarchyConfigDir + "/bar.json"
|
|
||||||
watchChanges: true
|
|
||||||
printErrors: false
|
|
||||||
onLoaded: root.loadUserBarConfig(text())
|
|
||||||
onFileChanged: reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
path: root.home + "/.config/omarchy/current/theme/colors.toml"
|
path: root.home + "/.config/omarchy/current/theme/colors.toml"
|
||||||
watchChanges: true
|
watchChanges: true
|
||||||
@@ -1230,6 +1197,20 @@ Item {
|
|||||||
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
|
||||||
if ("settings" in target) target.settings = moduleSettings
|
if ("settings" in target) target.settings = moduleSettings
|
||||||
|
|
||||||
|
// Noctalia compat injection. Only kicks in for plugins whose manifest
|
||||||
|
// was translated from Noctalia shape; for our first-party widgets these
|
||||||
|
// properties don't exist on the target item so nothing happens.
|
||||||
|
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[moduleName] : null
|
||||||
|
if (manifest && manifest.__noctaliaCompat) {
|
||||||
|
if ("pluginApi" in target) target.pluginApi = root.noctaliaPluginApiFor(moduleName)
|
||||||
|
if ("widgetId" in target) target.widgetId = moduleName
|
||||||
|
if ("section" in target) target.section = root.sectionOfEntry(entry)
|
||||||
|
if ("sectionWidgetIndex" in target) target.sectionWidgetIndex = root.indexOfEntry(entry)
|
||||||
|
if ("sectionWidgetsCount" in target) target.sectionWidgetsCount = root.entriesOfSection(root.sectionOfEntry(entry)).length
|
||||||
|
if ("screen" in target && root.QsWindow && root.QsWindow.window)
|
||||||
|
target.screen = root.QsWindow.window.screen
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
|
|||||||
@@ -7,41 +7,43 @@ the shell for its whole session.
|
|||||||
|
|
||||||
- `manifest.json` declares the plugin (`id: omarchy.bar`, `kind: bar`, `activation: persistent`) and points at `Bar.qml` as the entry point.
|
- `manifest.json` declares the plugin (`id: omarchy.bar`, `kind: bar`, `activation: persistent`) and points at `Bar.qml` as the entry point.
|
||||||
- `Bar.qml` is Omarchy-owned bar engine code, loaded by the omarchy-shell host. 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.
|
- `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).
|
||||||
- User overrides live in `~/.config/omarchy/bar.json` and are merged over defaults at runtime.
|
- The bar receives its config from the host shell as a `barConfig` property; the host loads it from `~/.config/omarchy/shell.json` (or `shell-defaults.json` when the user has no file).
|
||||||
- `omarchy-style-bar-position` updates only the user override file.
|
- `omarchy-style-bar-position` updates only the user shell.json file.
|
||||||
|
|
||||||
## Customizing
|
## Customizing
|
||||||
|
|
||||||
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.
|
The bar config lives under the `bar:` key of [`~/.config/omarchy/shell.json`](../../README.md#shelljson-shape). Out of the box the shell uses [`shell-defaults.json`](../../shell-defaults.json). Once you customize anything via `omarchy launch bar-settings` or by editing shell.json directly, your file is canonical — there is no deep-merge.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
Example `bar.json`:
|
Example `shell.json` (bar subtree only shown):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"position": "top",
|
"version": 1,
|
||||||
"centerAnchor": "calendar",
|
"bar": {
|
||||||
"layout": {
|
"position": "top",
|
||||||
"left": [
|
"centerAnchor": "calendar",
|
||||||
{ "id": "omarchy" },
|
"layout": {
|
||||||
{ "id": "spacer", "size": 12 },
|
"left": [
|
||||||
{ "id": "workspacesPro" }
|
{ "id": "omarchy" },
|
||||||
],
|
{ "id": "spacer", "size": 12 },
|
||||||
"center": [
|
{ "id": "workspacesPro" }
|
||||||
{ "id": "media" },
|
],
|
||||||
{ "id": "calendar", "format": "HH:mm" }
|
"center": [
|
||||||
],
|
{ "id": "media" },
|
||||||
"right": [
|
{ "id": "calendar", "format": "HH:mm" }
|
||||||
{ "id": "systemStats" },
|
],
|
||||||
{ "id": "audioPanel" },
|
"right": [
|
||||||
{ "id": "battery" },
|
{ "id": "systemStats" },
|
||||||
{ "id": "controlCenter" },
|
{ "id": "audioPanel" },
|
||||||
{ "id": "powerMenu" }
|
{ "id": "battery" },
|
||||||
]
|
{ "id": "controlCenter" },
|
||||||
|
{ "id": "powerMenu" }
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -81,18 +83,21 @@ All widgets work in `top`, `bottom`, `left`, and `right` positions. Popups ancho
|
|||||||
|
|
||||||
## Custom user modules
|
## Custom user modules
|
||||||
|
|
||||||
The schema accepts arbitrary module ids that you provide. Set `type` to `command` for shell-driven output or `qml` for a custom QML widget.
|
The schema accepts arbitrary module ids that you provide. Set `type` to `command` for shell-driven output or `qml` for a custom QML widget. Both still go under `bar.layout.<section>` in `shell.json`.
|
||||||
|
|
||||||
Command module:
|
Command module:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"layout": {
|
"version": 1,
|
||||||
"right": [
|
"bar": {
|
||||||
{ "id": "tray" },
|
"layout": {
|
||||||
{ "id": "vpn", "type": "command", "exec": "~/.config/omarchy/bar/scripts/vpn-status", "interval": 5, "tooltip": "VPN", "onClick": "nm-connection-editor" },
|
"right": [
|
||||||
{ "id": "audioPanel" }
|
{ "id": "tray" },
|
||||||
]
|
{ "id": "vpn", "type": "command", "exec": "~/.config/omarchy/bar/scripts/vpn-status", "interval": 5, "tooltip": "VPN", "onClick": "nm-connection-editor" },
|
||||||
|
{ "id": "audioPanel" }
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -107,11 +112,14 @@ QML module:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"layout": {
|
"version": 1,
|
||||||
"right": [
|
"bar": {
|
||||||
{ "id": "gpu", "type": "qml" },
|
"layout": {
|
||||||
{ "id": "audioPanel" }
|
"right": [
|
||||||
]
|
{ "id": "gpu", "type": "qml" },
|
||||||
|
{ "id": "audioPanel" }
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"position": "top",
|
|
||||||
"fontFamily": "JetBrainsMono Nerd Font",
|
|
||||||
"centerAnchor": "calendar",
|
|
||||||
"layout": {
|
|
||||||
"left": [
|
|
||||||
{ "id": "omarchy" },
|
|
||||||
{ "id": "workspacesPro" },
|
|
||||||
{ "id": "activeWindow" }
|
|
||||||
],
|
|
||||||
"center": [
|
|
||||||
{ "id": "media" },
|
|
||||||
{ "id": "calendar", "format": "dddd HH:mm", "formatAlt": "dd MMMM 'W'ww yyyy", "verticalFormat": "HH\n—\nmm" },
|
|
||||||
{ "id": "weatherFlyout" },
|
|
||||||
{ "id": "update" },
|
|
||||||
{ "id": "voxtype" },
|
|
||||||
{ "id": "screenRecording" },
|
|
||||||
{ "id": "idle" },
|
|
||||||
{ "id": "notifications" }
|
|
||||||
],
|
|
||||||
"right": [
|
|
||||||
{ "id": "tray" },
|
|
||||||
{ "id": "systemStats" },
|
|
||||||
{ "id": "microphone" },
|
|
||||||
{ "id": "bluetoothPanel" },
|
|
||||||
{ "id": "networkPanel" },
|
|
||||||
{ "id": "audioPanel" },
|
|
||||||
{ "id": "nightLight" },
|
|
||||||
{ "id": "brightness" },
|
|
||||||
{ "id": "powerProfile" },
|
|
||||||
{ "id": "battery" },
|
|
||||||
{ "id": "controlCenter" },
|
|
||||||
{ "id": "powerMenu" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,15 +8,19 @@ QtObject {
|
|||||||
|
|
||||||
property string home: Quickshell.env("HOME")
|
property string home: Quickshell.env("HOME")
|
||||||
property string pluginsDir: home + "/.config/omarchy/plugins"
|
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.
|
// Set by shell.qml at startup so we can also scan bundled first-party plugins.
|
||||||
property string firstPartyDir: ""
|
property string firstPartyDir: ""
|
||||||
|
|
||||||
|
// Wired by shell.qml so the registry can read the canonical shell.json
|
||||||
|
// without owning file IO itself. shellConfigProvider returns the current
|
||||||
|
// effective shell config; shellConfigMutator takes a function that receives
|
||||||
|
// a deep-cloned config it can mutate in place and persists the result.
|
||||||
|
property var shellConfigProvider: null
|
||||||
|
property var shellConfigMutator: null
|
||||||
|
|
||||||
// { pluginId: manifest } — manifests have __sourceDir and __isFirstParty stamped in.
|
// { pluginId: manifest } — manifests have __sourceDir and __isFirstParty stamped in.
|
||||||
property var installedPlugins: ({})
|
property var installedPlugins: ({})
|
||||||
// { pluginId: { enabled: bool } } — persisted to stateFile.
|
|
||||||
property var pluginStates: ({})
|
|
||||||
property int registryRevision: 0
|
property int registryRevision: 0
|
||||||
property bool scanning: false
|
property bool scanning: false
|
||||||
|
|
||||||
@@ -40,11 +44,99 @@ QtObject {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect a Noctalia-shape manifest (https://github.com/noctalia-dev/noctalia-plugins).
|
||||||
|
// Noctalia manifests don't carry our schemaVersion; we recognise them by the
|
||||||
|
// presence of `minNoctaliaVersion` or `metadata.defaultSettings`, or an
|
||||||
|
// `entryPoints` map that names roles Noctalia owns (main/barWidget/panel).
|
||||||
|
function isNoctaliaShape(manifest) {
|
||||||
|
if (!isPlainObject(manifest)) return false
|
||||||
|
if (manifest.schemaVersion !== undefined) return false
|
||||||
|
if (manifest.minNoctaliaVersion !== undefined) return true
|
||||||
|
if (isPlainObject(manifest.metadata) && isPlainObject(manifest.metadata.defaultSettings)) return true
|
||||||
|
if (isPlainObject(manifest.entryPoints)) {
|
||||||
|
var ep = manifest.entryPoints
|
||||||
|
if (ep.barWidget || ep.main || ep.panel || ep.desktopWidget
|
||||||
|
|| ep.launcherProvider || ep.controlCenterWidget) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translate a Noctalia manifest into the shape we validate normally. We do
|
||||||
|
// not modify the on-disk file; the in-memory copy gets a __noctaliaCompat
|
||||||
|
// marker so downstream code can branch where the semantics diverge.
|
||||||
|
function translateNoctaliaManifest(src) {
|
||||||
|
var ep = isPlainObject(src.entryPoints) ? src.entryPoints : {}
|
||||||
|
var kinds = []
|
||||||
|
var unsupported = []
|
||||||
|
if (ep.main) kinds.push("service")
|
||||||
|
if (ep.barWidget) kinds.push("bar-widget")
|
||||||
|
if (ep.panel) kinds.push("panel")
|
||||||
|
// Out of scope for v1 — warn loudly so we don't silently strand the plugin.
|
||||||
|
if (ep.desktopWidget) unsupported.push("desktopWidget")
|
||||||
|
if (ep.launcherProvider) unsupported.push("launcherProvider")
|
||||||
|
if (ep.controlCenterWidget) unsupported.push("controlCenterWidget")
|
||||||
|
|
||||||
|
if (kinds.length === 0) {
|
||||||
|
console.warn("PluginRegistry: noctalia plugin '" + src.id + "' has no supported entryPoints"
|
||||||
|
+ (unsupported.length ? " (unsupported in v1: " + unsupported.join(", ") + ")" : ""))
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (unsupported.length) {
|
||||||
|
console.warn("PluginRegistry: noctalia plugin '" + src.id
|
||||||
|
+ "' uses entryPoints not supported in v1, ignoring: " + unsupported.join(", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
var translated = {
|
||||||
|
schemaVersion: 1,
|
||||||
|
id: "noctalia." + String(src.id),
|
||||||
|
name: src.name || src.id,
|
||||||
|
version: src.version || "0.0.0",
|
||||||
|
author: src.author || "",
|
||||||
|
description: src.description || "",
|
||||||
|
kinds: kinds,
|
||||||
|
activation: kinds.indexOf("service") !== -1 ? "persistent" : "on-demand",
|
||||||
|
entryPoints: {},
|
||||||
|
__noctaliaCompat: true,
|
||||||
|
__noctaliaOriginal: src
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ep.barWidget) translated.entryPoints.barWidget = ep.barWidget
|
||||||
|
if (ep.main) translated.entryPoints.service = ep.main
|
||||||
|
if (ep.panel) translated.entryPoints.panel = ep.panel
|
||||||
|
if (ep.settings) translated.entryPoints.settings = ep.settings
|
||||||
|
|
||||||
|
var defaults = (isPlainObject(src.metadata) && isPlainObject(src.metadata.defaultSettings))
|
||||||
|
? src.metadata.defaultSettings : {}
|
||||||
|
if (translated.entryPoints.barWidget) {
|
||||||
|
translated.barWidget = {
|
||||||
|
displayName: translated.name,
|
||||||
|
description: translated.description,
|
||||||
|
category: "Noctalia",
|
||||||
|
allowMultiple: false,
|
||||||
|
defaults: defaults,
|
||||||
|
schema: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (defaults && Object.keys(defaults).length > 0) translated.defaults = defaults
|
||||||
|
|
||||||
|
return translated
|
||||||
|
}
|
||||||
|
|
||||||
function validateManifest(manifest, sourcePath) {
|
function validateManifest(manifest, sourcePath) {
|
||||||
if (!isPlainObject(manifest)) {
|
if (!isPlainObject(manifest)) {
|
||||||
console.warn("PluginRegistry: manifest is not an object at " + sourcePath)
|
console.warn("PluginRegistry: manifest is not an object at " + sourcePath)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
// Noctalia shape — translate before validation. The translated manifest
|
||||||
|
// goes through the standard schemaVersion=1 path below.
|
||||||
|
if (isNoctaliaShape(manifest)) {
|
||||||
|
var preservedSourceDir = manifest.__sourceDir
|
||||||
|
var preservedFirstParty = manifest.__isFirstParty
|
||||||
|
manifest = translateNoctaliaManifest(manifest)
|
||||||
|
if (!manifest) return null
|
||||||
|
manifest.__sourceDir = preservedSourceDir
|
||||||
|
manifest.__isFirstParty = preservedFirstParty
|
||||||
|
}
|
||||||
if (manifest.schemaVersion !== 1) {
|
if (manifest.schemaVersion !== 1) {
|
||||||
console.warn("PluginRegistry: unsupported schemaVersion at " + sourcePath)
|
console.warn("PluginRegistry: unsupported schemaVersion at " + sourcePath)
|
||||||
return null
|
return null
|
||||||
@@ -99,18 +191,74 @@ QtObject {
|
|||||||
return fileUrl(resolved)
|
return fileUrl(resolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enabled = the plugin id is referenced somewhere in shell.json. That can
|
||||||
|
// be either a layout entry inside `bar.layout.*` (bar widgets) or a top-level
|
||||||
|
// entry in `plugins[]` (panels, overlays, services).
|
||||||
|
//
|
||||||
|
// Special case: plugins with `kinds` containing "bar" are directly mounted
|
||||||
|
// by the shell host rather than loaded through plugins[], so they're
|
||||||
|
// implicitly always enabled.
|
||||||
function isEnabled(id) {
|
function isEnabled(id) {
|
||||||
var state = pluginStates[String(id)]
|
var key = String(id)
|
||||||
return !!(state && state.enabled)
|
var manifest = installedPlugins[key]
|
||||||
|
if (manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar") !== -1) return true
|
||||||
|
var config = shellConfigProvider ? shellConfigProvider() : null
|
||||||
|
return findEntryLocation(config, key).found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findEntryLocation(config, id) {
|
||||||
|
if (!isPlainObject(config)) return { found: false }
|
||||||
|
if (isPlainObject(config.bar) && isPlainObject(config.bar.layout)) {
|
||||||
|
var sections = ["left", "center", "right"]
|
||||||
|
for (var s = 0; s < sections.length; s++) {
|
||||||
|
var arr = config.bar.layout[sections[s]]
|
||||||
|
if (!Array.isArray(arr)) continue
|
||||||
|
for (var i = 0; i < arr.length; i++) {
|
||||||
|
if (arr[i] && arr[i].id === id) return { found: true, kind: "bar", section: sections[s], index: i }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(config.plugins)) {
|
||||||
|
for (var j = 0; j < config.plugins.length; j++) {
|
||||||
|
if (config.plugins[j] && config.plugins[j].id === id) return { found: true, kind: "plugin", index: j }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { found: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding a plugin places it in the right section based on its declared
|
||||||
|
// kinds. Bar widgets default to the right section; panels/overlays/menus/
|
||||||
|
// services go into the plugins[] array.
|
||||||
function setEnabled(id, value) {
|
function setEnabled(id, value) {
|
||||||
var key = String(id)
|
var key = String(id)
|
||||||
var next = {}
|
if (!shellConfigMutator) {
|
||||||
for (var k in pluginStates) next[k] = pluginStates[k]
|
console.warn("PluginRegistry.setEnabled called before shellConfigMutator wired")
|
||||||
next[key] = { enabled: !!value }
|
return
|
||||||
pluginStates = next
|
}
|
||||||
persistStates()
|
var manifest = installedPlugins[key]
|
||||||
|
var isBarWidget = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar-widget") !== -1
|
||||||
|
shellConfigMutator(function(config) {
|
||||||
|
// Ensure shape exists.
|
||||||
|
if (!isPlainObject(config.bar)) config.bar = { layout: { left: [], center: [], right: [] } }
|
||||||
|
if (!isPlainObject(config.bar.layout)) config.bar.layout = { left: [], center: [], right: [] }
|
||||||
|
if (!Array.isArray(config.plugins)) config.plugins = []
|
||||||
|
var location = findEntryLocation(config, key)
|
||||||
|
if (value && !location.found) {
|
||||||
|
var entry = { id: key }
|
||||||
|
if (isBarWidget) {
|
||||||
|
if (!Array.isArray(config.bar.layout.right)) config.bar.layout.right = []
|
||||||
|
config.bar.layout.right.push(entry)
|
||||||
|
} else {
|
||||||
|
config.plugins.push(entry)
|
||||||
|
}
|
||||||
|
} else if (!value && location.found) {
|
||||||
|
if (location.kind === "bar") {
|
||||||
|
config.bar.layout[location.section].splice(location.index, 1)
|
||||||
|
} else {
|
||||||
|
config.plugins.splice(location.index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
registryRevision++
|
registryRevision++
|
||||||
pluginsChanged()
|
pluginsChanged()
|
||||||
}
|
}
|
||||||
@@ -124,36 +272,6 @@ QtObject {
|
|||||||
return result
|
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
|
// ---------------------------------------------------------------- scanning
|
||||||
|
|
||||||
// Output format produced by the rescan script:
|
// Output format produced by the rescan script:
|
||||||
@@ -207,16 +325,6 @@ QtObject {
|
|||||||
}
|
}
|
||||||
flush()
|
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 = {}
|
var merged = {}
|
||||||
for (var fk in firstParty) merged[fk] = firstParty[fk]
|
for (var fk in firstParty) merged[fk] = firstParty[fk]
|
||||||
// Third-party plugins never shadow a first-party one with the same id.
|
// Third-party plugins never shadow a first-party one with the same id.
|
||||||
@@ -229,7 +337,6 @@ QtObject {
|
|||||||
merged[tk] = thirdParty[tk]
|
merged[tk] = thirdParty[tk]
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginStates = nextStates
|
|
||||||
installedPlugins = merged
|
installedPlugins = merged
|
||||||
registryRevision++
|
registryRevision++
|
||||||
scanning = false
|
scanning = false
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"bar": {
|
||||||
|
"position": "top",
|
||||||
|
"fontFamily": "JetBrainsMono Nerd Font",
|
||||||
|
"centerAnchor": "calendar",
|
||||||
|
"layout": {
|
||||||
|
"left": [
|
||||||
|
{ "id": "omarchy" },
|
||||||
|
{ "id": "workspacesPro" },
|
||||||
|
{ "id": "activeWindow" }
|
||||||
|
],
|
||||||
|
"center": [
|
||||||
|
{ "id": "media" },
|
||||||
|
{ "id": "calendar", "format": "dddd HH:mm", "formatAlt": "dd MMMM 'W'ww yyyy", "verticalFormat": "HH\n—\nmm" },
|
||||||
|
{ "id": "weatherFlyout" },
|
||||||
|
{ "id": "update" },
|
||||||
|
{ "id": "voxtype" },
|
||||||
|
{ "id": "screenRecording" },
|
||||||
|
{ "id": "idle" },
|
||||||
|
{ "id": "notifications" }
|
||||||
|
],
|
||||||
|
"right": [
|
||||||
|
{ "id": "tray" },
|
||||||
|
{ "id": "systemStats" },
|
||||||
|
{ "id": "microphone" },
|
||||||
|
{ "id": "bluetoothPanel" },
|
||||||
|
{ "id": "networkPanel" },
|
||||||
|
{ "id": "audioPanel" },
|
||||||
|
{ "id": "nightLight" },
|
||||||
|
{ "id": "brightness" },
|
||||||
|
{ "id": "powerProfile" },
|
||||||
|
{ "id": "battery" },
|
||||||
|
{ "id": "controlCenter" },
|
||||||
|
{ "id": "powerMenu" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{ "id": "omarchy.bar-settings" },
|
||||||
|
{ "id": "omarchy.image-picker" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@ import Quickshell.Io
|
|||||||
|
|
||||||
import "plugins/bar"
|
import "plugins/bar"
|
||||||
import "services"
|
import "services"
|
||||||
|
import "compat/noctalia" as Compat
|
||||||
|
import qs.Services.UI as NoctaliaUI
|
||||||
|
import qs.Commons as NoctaliaCommons
|
||||||
|
|
||||||
ShellRoot {
|
ShellRoot {
|
||||||
id: shell
|
id: shell
|
||||||
@@ -32,23 +35,363 @@ ShellRoot {
|
|||||||
}
|
}
|
||||||
property string omarchyPath: deriveOmarchyPath()
|
property string omarchyPath: deriveOmarchyPath()
|
||||||
readonly property string firstPartyPluginsDir: omarchyPath + "/default/quickshell/omarchy-shell/plugins"
|
readonly property string firstPartyPluginsDir: omarchyPath + "/default/quickshell/omarchy-shell/plugins"
|
||||||
|
readonly property string defaultsPath: omarchyPath + "/default/quickshell/omarchy-shell/shell-defaults.json"
|
||||||
|
readonly property string userConfigPath: home + "/.config/omarchy/shell.json"
|
||||||
|
|
||||||
|
// Bundled fallback so the shell can start even when shell-defaults.json is
|
||||||
|
// missing or unreadable. The bar config here mirrors the on-disk defaults
|
||||||
|
// closely enough to render a usable bar; not authoritative.
|
||||||
|
readonly property var builtinShellConfig: ({
|
||||||
|
version: 1,
|
||||||
|
bar: {
|
||||||
|
position: "top",
|
||||||
|
fontFamily: "JetBrainsMono Nerd Font",
|
||||||
|
centerAnchor: "calendar",
|
||||||
|
layout: {
|
||||||
|
left: [{ id: "omarchy" }, { id: "workspacesPro" }],
|
||||||
|
center: [{ id: "calendar", format: "dddd HH:mm" }],
|
||||||
|
right: [{ id: "audioPanel" }, { id: "controlCenter" }, { id: "powerMenu" }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
{ id: "omarchy.bar-settings" },
|
||||||
|
{ id: "omarchy.image-picker" }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
property var defaultsConfig: builtinShellConfig
|
||||||
|
property var shellConfig: builtinShellConfig
|
||||||
|
property bool suppressUserReload: false
|
||||||
|
|
||||||
|
function isPlainObject(value) {
|
||||||
|
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyShellConfig() {
|
||||||
|
// Decide which source is canonical: a valid user shell.json overrides
|
||||||
|
// defaults entirely; otherwise fall back to defaults. We do not deep-merge.
|
||||||
|
var defaults = isPlainObject(defaultsConfig) ? defaultsConfig : builtinShellConfig
|
||||||
|
var user = null
|
||||||
|
var userText = userConfigFile.text() || ""
|
||||||
|
if (userText.trim()) {
|
||||||
|
try {
|
||||||
|
var parsed = JSON.parse(userText)
|
||||||
|
if (isPlainObject(parsed) && parsed.version === 1) user = parsed
|
||||||
|
else if (isPlainObject(parsed)) console.warn("shell.json missing version: 1, using defaults")
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("shell.json parse failed, using defaults:", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shellConfig = user || defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDefaults(raw) {
|
||||||
|
var text = String(raw || "").trim()
|
||||||
|
if (!text) {
|
||||||
|
defaultsConfig = builtinShellConfig
|
||||||
|
applyShellConfig()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var parsed = JSON.parse(text)
|
||||||
|
if (isPlainObject(parsed) && parsed.version === 1) defaultsConfig = parsed
|
||||||
|
else defaultsConfig = builtinShellConfig
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("shell-defaults.json parse failed, using builtin:", e)
|
||||||
|
defaultsConfig = builtinShellConfig
|
||||||
|
}
|
||||||
|
applyShellConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistShellConfig(nextConfig) {
|
||||||
|
suppressUserReload = true
|
||||||
|
var payload = JSON.parse(JSON.stringify(nextConfig))
|
||||||
|
payload.version = 1
|
||||||
|
shellConfig = payload
|
||||||
|
userConfigFile.setText(JSON.stringify(payload, null, 2) + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var barConfig: shellConfig && isPlainObject(shellConfig.bar) ? shellConfig.bar : builtinShellConfig.bar
|
||||||
|
readonly property var pluginsConfig: shellConfig && Array.isArray(shellConfig.plugins) ? shellConfig.plugins : []
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: defaultsFile
|
||||||
|
path: shell.defaultsPath
|
||||||
|
watchChanges: true
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: shell.loadDefaults(text())
|
||||||
|
onLoadFailed: function(error) {
|
||||||
|
console.warn("shell-defaults load failed: " + error + " path=" + shell.defaultsPath)
|
||||||
|
shell.loadDefaults("")
|
||||||
|
}
|
||||||
|
onFileChanged: reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
FileView {
|
||||||
|
id: userConfigFile
|
||||||
|
path: shell.userConfigPath
|
||||||
|
watchChanges: true
|
||||||
|
atomicWrites: true
|
||||||
|
printErrors: false
|
||||||
|
onLoaded: {
|
||||||
|
if (shell.suppressUserReload) {
|
||||||
|
shell.suppressUserReload = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
shell.applyShellConfig()
|
||||||
|
}
|
||||||
|
onLoadFailed: function(error) { shell.applyShellConfig() }
|
||||||
|
onFileChanged: reload()
|
||||||
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
console.log("omarchy-shell paths",
|
console.log("omarchy-shell paths",
|
||||||
"omarchyPath=" + shell.omarchyPath,
|
"omarchyPath=" + shell.omarchyPath,
|
||||||
"shellDir=" + Quickshell.shellDir,
|
"shellDir=" + Quickshell.shellDir,
|
||||||
"firstPartyPluginsDir=" + shell.firstPartyPluginsDir)
|
"firstPartyPluginsDir=" + shell.firstPartyPluginsDir,
|
||||||
|
"defaultsPath=" + shell.defaultsPath,
|
||||||
|
"userConfigPath=" + shell.userConfigPath)
|
||||||
pluginRegistry.firstPartyDir = shell.firstPartyPluginsDir
|
pluginRegistry.firstPartyDir = shell.firstPartyPluginsDir
|
||||||
|
pluginRegistry.shellConfigProvider = function() { return shell.shellConfig }
|
||||||
|
pluginRegistry.shellConfigMutator = function(mutate) { shell.mutateShellConfig(mutate) }
|
||||||
// PluginRegistry.ensureUserDir() runs in its own Component.onCompleted and
|
// PluginRegistry.ensureUserDir() runs in its own Component.onCompleted and
|
||||||
// chains rescan() once the directory exists. We also kick a scan here in
|
// chains rescan() once the directory exists. We also kick a scan here in
|
||||||
// case the user dir already existed at startup.
|
// case the user dir already existed at startup.
|
||||||
pluginRegistry.rescan()
|
pluginRegistry.rescan()
|
||||||
|
|
||||||
|
// Wire Noctalia compat singletons. Plugins read state from these globals;
|
||||||
|
// we hand them references to the host so they can route into the right
|
||||||
|
// popup, tooltip, etc.
|
||||||
|
NoctaliaUI.BarService.bar = bar
|
||||||
|
NoctaliaUI.TooltipService.bar = bar
|
||||||
|
NoctaliaUI.PanelService.bar = bar
|
||||||
|
NoctaliaUI.PanelService.shell = shell
|
||||||
|
NoctaliaCommons.Settings.shellConfig = shell.shellConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep Noctalia.Settings.data in sync with whatever shell.json currently is.
|
||||||
|
onShellConfigChanged: NoctaliaCommons.Settings.shellConfig = shell.shellConfig
|
||||||
|
|
||||||
|
function mutateShellConfig(mutator) {
|
||||||
|
var copy = JSON.parse(JSON.stringify(shellConfig || builtinShellConfig))
|
||||||
|
mutator(copy)
|
||||||
|
persistShellConfig(copy)
|
||||||
}
|
}
|
||||||
|
|
||||||
Bar {
|
Bar {
|
||||||
id: bar
|
id: bar
|
||||||
omarchyPath: shell.omarchyPath
|
omarchyPath: shell.omarchyPath
|
||||||
barWidgetRegistry: shell.barWidgetRegistry
|
barWidgetRegistry: shell.barWidgetRegistry
|
||||||
|
barConfig: shell.barConfig
|
||||||
|
pluginRegistry: shell.pluginRegistry
|
||||||
|
shell: shell
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------- Noctalia plugin API
|
||||||
|
//
|
||||||
|
// One pluginApi instance per plugin, cached for the life of the shell. The
|
||||||
|
// factory is host-owned (shell, not bar) so non-bar Noctalia plugins
|
||||||
|
// (panels, services) can call into it too. Settings lookups walk the live
|
||||||
|
// shell.json on every read so reorders/edits never leave a stale entry
|
||||||
|
// captured in the closure.
|
||||||
|
Compat.PluginApiFactory { id: noctaliaApiFactory }
|
||||||
|
|
||||||
|
property var _noctaliaApis: ({})
|
||||||
|
property var _noctaliaServices: ({})
|
||||||
|
|
||||||
|
function findShellEntry(pluginId) {
|
||||||
|
var config = shellConfig
|
||||||
|
if (!isPlainObject(config)) return null
|
||||||
|
if (isPlainObject(config.bar) && isPlainObject(config.bar.layout)) {
|
||||||
|
var sections = ["left", "center", "right"]
|
||||||
|
for (var s = 0; s < sections.length; s++) {
|
||||||
|
var arr = config.bar.layout[sections[s]]
|
||||||
|
if (!Array.isArray(arr)) continue
|
||||||
|
for (var i = 0; i < arr.length; i++) {
|
||||||
|
if (arr[i] && arr[i].id === pluginId) return arr[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(config.plugins)) {
|
||||||
|
for (var p = 0; p < config.plugins.length; p++) {
|
||||||
|
if (config.plugins[p] && config.plugins[p].id === pluginId) return config.plugins[p]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function noctaliaPluginApiFor(pluginId) {
|
||||||
|
var key = String(pluginId)
|
||||||
|
if (!key) return null
|
||||||
|
var existing = _noctaliaApis[key]
|
||||||
|
if (existing) return existing
|
||||||
|
var manifest = pluginRegistry && pluginRegistry.installedPlugins
|
||||||
|
? pluginRegistry.installedPlugins[key] : null
|
||||||
|
if (!manifest || !manifest.__noctaliaCompat) return null
|
||||||
|
|
||||||
|
var api = noctaliaApiFactory.create(
|
||||||
|
key,
|
||||||
|
manifest,
|
||||||
|
function() {
|
||||||
|
// Compute effective settings on every read. We can't capture an entry
|
||||||
|
// reference because mutateSection rebuilds the layout objects and the
|
||||||
|
// captured reference would point at a snapshot.
|
||||||
|
var defaults = (manifest.barWidget && manifest.barWidget.defaults) || manifest.defaults || {}
|
||||||
|
var merged = {}
|
||||||
|
for (var k in defaults) merged[k] = defaults[k]
|
||||||
|
var entry = findShellEntry(key)
|
||||||
|
if (entry) {
|
||||||
|
for (var ek in entry) if (ek !== "id") merged[ek] = entry[ek]
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
},
|
||||||
|
{
|
||||||
|
persistSettings: function(_pluginId, settings) {
|
||||||
|
if (typeof shell.updateEntryInline === "function")
|
||||||
|
shell.updateEntryInline(key, settings)
|
||||||
|
},
|
||||||
|
openPanel: function(_pluginId, _screen, _btn) {
|
||||||
|
shell.summon(key, JSON.stringify({ source: "noctalia" }))
|
||||||
|
},
|
||||||
|
closePanel: function(_pluginId, _screen) { shell.hide(key) },
|
||||||
|
currentScreen: function() {
|
||||||
|
var screens = Quickshell.screens
|
||||||
|
return screens && screens.length > 0 ? screens[0] : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var next = ({})
|
||||||
|
for (var existingKey in _noctaliaApis) next[existingKey] = _noctaliaApis[existingKey]
|
||||||
|
next[key] = api
|
||||||
|
_noctaliaApis = next
|
||||||
|
return api
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------- noctalia service main
|
||||||
|
//
|
||||||
|
// Noctalia plugins with `entryPoints.main` (translated to kind="service")
|
||||||
|
// are instantiated once per shell session when enabled. The Main.qml is
|
||||||
|
// typically headless — a data source the plugin's bar widget reads from.
|
||||||
|
// We rely on the shared pluginApi so the service and any bar widgets see
|
||||||
|
// the same handle.
|
||||||
|
function ensureNoctaliaService(pluginId) {
|
||||||
|
var key = String(pluginId)
|
||||||
|
if (_noctaliaServices[key]) return _noctaliaServices[key]
|
||||||
|
var manifest = pluginRegistry && pluginRegistry.installedPlugins
|
||||||
|
? pluginRegistry.installedPlugins[key] : null
|
||||||
|
if (!manifest || !manifest.__noctaliaCompat) return null
|
||||||
|
if (!manifest.entryPoints || !manifest.entryPoints.service) return null
|
||||||
|
var url = pluginRegistry.entryPointUrl(manifest, "service")
|
||||||
|
if (!url) return null
|
||||||
|
var api = noctaliaPluginApiFor(key)
|
||||||
|
if (!api) return null
|
||||||
|
|
||||||
|
var comp = Qt.createComponent(url, Component.PreferSynchronous)
|
||||||
|
function finalize() {
|
||||||
|
if (comp.status !== Component.Ready) {
|
||||||
|
console.warn("noctalia service load failed for " + key + ": " + comp.errorString())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var inst = comp.createObject(shell, { pluginApi: api })
|
||||||
|
if (!inst) {
|
||||||
|
console.warn("noctalia service createObject returned null for", key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var snext = ({})
|
||||||
|
for (var sk in _noctaliaServices) snext[sk] = _noctaliaServices[sk]
|
||||||
|
snext[key] = inst
|
||||||
|
_noctaliaServices = snext
|
||||||
|
api.mainInstance = inst
|
||||||
|
}
|
||||||
|
if (comp.status === Component.Loading) {
|
||||||
|
comp.statusChanged.connect(finalize)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
finalize()
|
||||||
|
return _noctaliaServices[key] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function _syncNoctaliaServices() {
|
||||||
|
if (!pluginRegistry || !pluginRegistry.installedPlugins) return
|
||||||
|
var plugins = pluginRegistry.installedPlugins
|
||||||
|
for (var id in plugins) {
|
||||||
|
var m = plugins[id]
|
||||||
|
if (!m || !m.__noctaliaCompat) continue
|
||||||
|
if (!m.entryPoints || !m.entryPoints.service) continue
|
||||||
|
if (!pluginRegistry.isEnabled(id)) continue
|
||||||
|
if (_noctaliaServices[id]) continue
|
||||||
|
ensureNoctaliaService(id)
|
||||||
|
}
|
||||||
|
// Drop services for plugins that have been disabled or removed.
|
||||||
|
for (var existingId in _noctaliaServices) {
|
||||||
|
var stillThere = plugins[existingId]
|
||||||
|
var stillEnabled = stillThere && pluginRegistry.isEnabled(existingId)
|
||||||
|
if (stillThere && stillEnabled) continue
|
||||||
|
var inst = _noctaliaServices[existingId]
|
||||||
|
if (inst && typeof inst.destroy === "function") inst.destroy()
|
||||||
|
var next = ({})
|
||||||
|
for (var k in _noctaliaServices) if (k !== existingId) next[k] = _noctaliaServices[k]
|
||||||
|
_noctaliaServices = next
|
||||||
|
var apis = ({})
|
||||||
|
for (var ak in _noctaliaApis) apis[ak] = _noctaliaApis[ak]
|
||||||
|
if (apis[existingId]) apis[existingId].mainInstance = null
|
||||||
|
_noctaliaApis = apis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: shell.pluginRegistry
|
||||||
|
function onPluginsChanged() { shell._syncNoctaliaServices() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used by Noctalia compat: pluginApi.saveSettings() ends up writing inline
|
||||||
|
// settings to the plugin's entry in shell.json. moduleName is the entry id
|
||||||
|
// (the bare manifest id, e.g. "noctalia.air-quality"); settings is the
|
||||||
|
// merged plugin state. Returns true if anything actually changed.
|
||||||
|
// Compute the proposed new shellConfig in a local clone, and only persist
|
||||||
|
// if anything actually changed. Lets Noctalia plugins call saveSettings()
|
||||||
|
// repeatedly with identical values (common for reactive QML bindings)
|
||||||
|
// without dirtying shell.json or thrashing the file watcher.
|
||||||
|
function updateEntryInline(moduleName, settings) {
|
||||||
|
var stripped = String(moduleName)
|
||||||
|
var copy = JSON.parse(JSON.stringify(shellConfig || builtinShellConfig))
|
||||||
|
if (!isPlainObject(copy.bar)) copy.bar = { layout: { left: [], center: [], right: [] } }
|
||||||
|
if (!isPlainObject(copy.bar.layout)) copy.bar.layout = { left: [], center: [], right: [] }
|
||||||
|
if (!Array.isArray(copy.plugins)) copy.plugins = []
|
||||||
|
|
||||||
|
var sections = ["left", "center", "right"]
|
||||||
|
var foundInLayout = false
|
||||||
|
var dirty = false
|
||||||
|
for (var s = 0; s < sections.length; s++) {
|
||||||
|
var arr = copy.bar.layout[sections[s]] || []
|
||||||
|
for (var i = 0; i < arr.length; i++) {
|
||||||
|
if (arr[i] && arr[i].id === stripped) {
|
||||||
|
var next = { id: stripped }
|
||||||
|
for (var k in settings) if (k !== "id") next[k] = settings[k]
|
||||||
|
if (JSON.stringify(arr[i]) !== JSON.stringify(next)) {
|
||||||
|
arr[i] = next
|
||||||
|
dirty = true
|
||||||
|
}
|
||||||
|
foundInLayout = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundInLayout) {
|
||||||
|
for (var j = 0; j < copy.plugins.length; j++) {
|
||||||
|
if (copy.plugins[j] && copy.plugins[j].id === stripped) {
|
||||||
|
var pnext = { id: stripped }
|
||||||
|
for (var pk in settings) if (pk !== "id") pnext[pk] = settings[pk]
|
||||||
|
if (JSON.stringify(copy.plugins[j]) !== JSON.stringify(pnext)) {
|
||||||
|
copy.plugins[j] = pnext
|
||||||
|
dirty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!dirty) return false
|
||||||
|
persistShellConfig(copy)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------- on-demand panels
|
// ---------------------------------------------------------- on-demand panels
|
||||||
@@ -206,6 +549,11 @@ ShellRoot {
|
|||||||
if ("manifest" in item) item.manifest = panelEntry.manifest
|
if ("manifest" in item) item.manifest = panelEntry.manifest
|
||||||
if ("barWidgetRegistry" in item) item.barWidgetRegistry = shell.barWidgetRegistry
|
if ("barWidgetRegistry" in item) item.barWidgetRegistry = shell.barWidgetRegistry
|
||||||
if ("pluginRegistry" in item) item.pluginRegistry = shell.pluginRegistry
|
if ("pluginRegistry" in item) item.pluginRegistry = shell.pluginRegistry
|
||||||
|
// Noctalia panel/overlay/menu plugins expect a pluginApi just like
|
||||||
|
// their bar widgets do. First-party Omarchy plugins don't declare
|
||||||
|
// the property so the `in target` check skips them.
|
||||||
|
if (panelEntry.manifest && panelEntry.manifest.__noctaliaCompat && "pluginApi" in item)
|
||||||
|
item.pluginApi = shell.noctaliaPluginApiFor(panelEntry.pluginId)
|
||||||
shell.registerPanelLoader(panelEntry.pluginId, this)
|
shell.registerPanelLoader(panelEntry.pluginId, this)
|
||||||
}
|
}
|
||||||
onStatusChanged: {
|
onStatusChanged: {
|
||||||
@@ -224,7 +572,10 @@ ShellRoot {
|
|||||||
|
|
||||||
// Mirror plugin registry state into BarWidgetRegistry whenever it changes.
|
// Mirror plugin registry state into BarWidgetRegistry whenever it changes.
|
||||||
// Each enabled plugin with kind "bar-widget" gets a Component created from
|
// Each enabled plugin with kind "bar-widget" gets a Component created from
|
||||||
// its manifest entry point and registered under the id "plugin:<id>".
|
// its manifest entry point and registered under its plain manifest id.
|
||||||
|
// First-party widget ids (calendar, weather, etc.) are short and don't
|
||||||
|
// collide with namespaced plugin ids like noctalia.air-quality, so we don't
|
||||||
|
// need a separate "plugin:" namespace anymore.
|
||||||
Connections {
|
Connections {
|
||||||
target: shell.pluginRegistry
|
target: shell.pluginRegistry
|
||||||
function onPluginsChanged() { shell.syncPluginWidgets() }
|
function onPluginsChanged() { shell.syncPluginWidgets() }
|
||||||
@@ -241,7 +592,7 @@ ShellRoot {
|
|||||||
if (!manifest || !manifest.kinds || manifest.kinds.indexOf("bar-widget") === -1) continue
|
if (!manifest || !manifest.kinds || manifest.kinds.indexOf("bar-widget") === -1) continue
|
||||||
if (!shell.pluginRegistry.isEnabled(pluginId)) continue
|
if (!shell.pluginRegistry.isEnabled(pluginId)) continue
|
||||||
|
|
||||||
var registryKey = "plugin:" + manifest.id
|
var registryKey = String(manifest.id)
|
||||||
seen[registryKey] = true
|
seen[registryKey] = true
|
||||||
|
|
||||||
// Already loaded with matching source — leave it alone.
|
// Already loaded with matching source — leave it alone.
|
||||||
@@ -272,7 +623,7 @@ ShellRoot {
|
|||||||
var allIds = shell.barWidgetRegistry.availableIds()
|
var allIds = shell.barWidgetRegistry.availableIds()
|
||||||
for (var i = 0; i < allIds.length; i++) {
|
for (var i = 0; i < allIds.length; i++) {
|
||||||
var id = allIds[i]
|
var id = allIds[i]
|
||||||
if (id.indexOf("plugin:") !== 0) continue
|
if (!pluginWidgetComponents[id]) continue
|
||||||
if (!seen[id]) {
|
if (!seen[id]) {
|
||||||
shell.barWidgetRegistry.unregister(id)
|
shell.barWidgetRegistry.unregister(id)
|
||||||
var next = ({})
|
var next = ({})
|
||||||
@@ -335,6 +686,13 @@ ShellRoot {
|
|||||||
return JSON.stringify(out)
|
return JSON.stringify(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the effective shell.json content as JSON. Useful for debugging
|
||||||
|
// and for CLI tools that want to inspect the merged state without
|
||||||
|
// re-implementing the load logic.
|
||||||
|
function listShellConfig(): string {
|
||||||
|
return JSON.stringify(shell.shellConfig || {})
|
||||||
|
}
|
||||||
|
|
||||||
function summon(id: string, payloadJson: string): string {
|
function summon(id: string, payloadJson: string): string {
|
||||||
return shell.summon(id, payloadJson) ? "ok" : "unknown"
|
return shell.summon(id, payloadJson) ? "ok" : "unknown"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user