Unify config into shell.json and add Noctalia plugin support

This commit is contained in:
Ryan Hughes
2026-05-14 02:21:48 -04:00
parent 5c0f5669c9
commit bcbe12b075
49 changed files with 2178 additions and 350 deletions
@@ -19,29 +19,26 @@ Item {
// Injected by the host shell. Shared with the bar-settings panel so both
// see the same widget catalogue.
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 omarchyConfigDir: home + "/.config/omarchy"
property var builtinBarConfig: ({
property var fallbackBarConfig: ({
position: "top",
fontFamily: "JetBrainsMono Nerd Font",
centerAnchor: "clock",
layout: {
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" }
]
}
centerAnchor: "calendar",
layout: { left: [], center: [], right: [] }
})
property var defaultBarConfig: builtinBarConfig
property var userBarConfig: ({})
property var layoutConfig: builtinBarConfig.layout
property string centerAnchor: "clock"
property var layoutConfig: fallbackBarConfig.layout
property string centerAnchor: ""
property int barConfigSerial: 0
property string position: "top"
property string fontFamily: "JetBrainsMono Nerd Font"
@@ -80,6 +77,40 @@ Item {
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 int barSize: vertical ? 28 : 26
@@ -104,51 +135,6 @@ Item {
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) {
if (typeof entry === "string") return { id: entry }
if (isPlainObject(entry) && entry.id) return entry
@@ -166,7 +152,7 @@ Item {
}
function normalizeLayout(layout) {
if (!isPlainObject(layout)) layout = builtinBarConfig.layout
if (!isPlainObject(layout)) layout = fallbackBarConfig.layout
return {
left: normalizeLayoutSection(layout.left),
center: normalizeLayoutSection(layout.center),
@@ -175,24 +161,16 @@ Item {
}
function applyBarConfig() {
var config = mergeConfig(defaultBarConfig, userBarConfig)
var config = isPlainObject(barConfig) ? barConfig : fallbackBarConfig
position = normalizePosition(config.position)
fontFamily = String(config.fontFamily || "JetBrainsMono Nerd Font")
centerAnchor = String(config.centerAnchor || "clock")
centerAnchor = String(config.centerAnchor || "")
layoutConfig = normalizeLayout(config.layout)
barConfigSerial++
}
function loadDefaultBarConfig(raw) {
defaultBarConfig = mergeConfig(builtinBarConfig, parseConfig(raw, "bar defaults"))
applyBarConfig()
}
function loadUserBarConfig(raw) {
userBarConfig = parseConfig(raw, "bar config")
applyBarConfig()
}
onBarConfigChanged: applyBarConfig()
function layoutEntries(region) {
var serial = barConfigSerial
@@ -327,7 +305,10 @@ Item {
property var registeredFirstPartyComponents: ({})
Component.onCompleted: registerFirstPartyWidgets()
Component.onCompleted: {
registerFirstPartyWidgets()
applyBarConfig()
}
function registerFirstPartyWidgets() {
var ids = Object.keys(firstPartyWidgetMetadata)
@@ -713,22 +694,8 @@ Item {
onTriggered: root.tooltipShown = true
}
FileView {
path: root.omarchyPath + "/default/quickshell/omarchy-shell/plugins/bar/bar-defaults.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()
}
// The host owns shell.json loading and injects `barConfig`. Bar still keeps
// its own theme FileView since theme colors are independent of shell.json.
FileView {
path: root.home + "/.config/omarchy/current/theme/colors.toml"
watchChanges: true
@@ -1230,6 +1197,20 @@ Item {
if ("bar" in target) target.bar = root
if ("moduleName" in target) target.moduleName = moduleName
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 {
@@ -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.
- `Bar.qml` is Omarchy-owned bar engine code, loaded by the omarchy-shell host. Users should not edit it directly.
- `bar-defaults.json` is the Omarchy-owned default layout and module settings.
- `widgets/` holds first-party widgets — modular, interactive components shipped with Omarchy.
- `common/` holds shared QML helpers (buttons, sliders, popup cards).
- User overrides live in `~/.config/omarchy/bar.json` and are merged over defaults at runtime.
- `omarchy-style-bar-position` updates only the user override file.
- 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 shell.json file.
## 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.
Example `bar.json`:
Example `shell.json` (bar subtree only shown):
```json
{
"position": "top",
"centerAnchor": "calendar",
"layout": {
"left": [
{ "id": "omarchy" },
{ "id": "spacer", "size": 12 },
{ "id": "workspacesPro" }
],
"center": [
{ "id": "media" },
{ "id": "calendar", "format": "HH:mm" }
],
"right": [
{ "id": "systemStats" },
{ "id": "audioPanel" },
{ "id": "battery" },
{ "id": "controlCenter" },
{ "id": "powerMenu" }
]
"version": 1,
"bar": {
"position": "top",
"centerAnchor": "calendar",
"layout": {
"left": [
{ "id": "omarchy" },
{ "id": "spacer", "size": 12 },
{ "id": "workspacesPro" }
],
"center": [
{ "id": "media" },
{ "id": "calendar", "format": "HH:mm" }
],
"right": [
{ "id": "systemStats" },
{ "id": "audioPanel" },
{ "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
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:
```json
{
"layout": {
"right": [
{ "id": "tray" },
{ "id": "vpn", "type": "command", "exec": "~/.config/omarchy/bar/scripts/vpn-status", "interval": 5, "tooltip": "VPN", "onClick": "nm-connection-editor" },
{ "id": "audioPanel" }
]
"version": 1,
"bar": {
"layout": {
"right": [
{ "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
{
"layout": {
"right": [
{ "id": "gpu", "type": "qml" },
{ "id": "audioPanel" }
]
"version": 1,
"bar": {
"layout": {
"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" }
]
}
}