Add visual bar customizer

Each layout entry is now an object — {id, ...inline settings} — instead
of a (name, separate modules map) pair. Multiple instances of the same
widget are trivially supported (spacer is the main beneficiary). Settings
travel with the entry, so reordering keeps them attached.

shell.qml:
- normalizeLayout converts string entries to {id} and drops invalid ones
- ModuleSlot binds to a full entry; moduleName and moduleSettings derive
  from it via entryId/entrySettings helpers
- centerAnchor lookup walks layoutEntries directly
- builtinBarConfig defaults use the object form
- README + bar-defaults.json updated to the new schema

bar-settings/ (new Quickshell config):
- Top toolbar with Reset / Save and Position / centerAnchor dropdowns
- Three section editors (left/center/right) with current widgets shown
  as cards: name + description + move up/down/settings/remove buttons
- '+ Add widget' menu per section, sorted alphabetically by display name
  (only spacer can repeat; everything else is one per bar)
- Per-widget settings dialog (FloatingWindow) loads an inline schema for
  widgets in the catalog. Initial schemas: spacer (size), calendar
  (formats), brightness (step)
- FileView { atomicWrites } writes to ~/.config/omarchy/bar.json on Save

omarchy-launch-bar-settings + hypr window rules pin the settings window
to a 760px floating square. controlCenter gains a 'Customize bar…' button
that launches it.
This commit is contained in:
Ryan Hughes
2026-05-14 02:21:47 -04:00
parent 9ddf05f230
commit a03f589258
7 changed files with 1102 additions and 110 deletions
+123 -69
View File
@@ -21,22 +21,21 @@ ShellRoot {
fontFamily: "JetBrainsMono Nerd Font",
centerAnchor: "clock",
layout: {
left: ["omarchy", "workspaces"],
center: ["clock", "weather", "update", "voxtype", "screenRecording", "idle", "notifications"],
right: ["tray", "bluetooth", "network", "audio", "cpu", "battery"]
},
modules: {
clock: {
format: "dddd HH:mm",
formatAlt: "dd MMMM 'W'ww yyyy",
verticalFormat: "HH\n—\nmm"
}
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 userBarConfig: ({})
property var layoutConfig: builtinBarConfig.layout
property var moduleConfig: builtinBarConfig.modules
property string centerAnchor: "clock"
property int barConfigSerial: 0
property string position: "top"
@@ -145,14 +144,38 @@ ShellRoot {
}
}
function normalizeLayoutEntry(entry) {
if (typeof entry === "string") return { id: entry }
if (isPlainObject(entry) && entry.id) return entry
return null
}
function normalizeLayoutSection(list) {
if (!Array.isArray(list)) return []
var result = []
for (var i = 0; i < list.length; i++) {
var normalized = normalizeLayoutEntry(list[i])
if (normalized) result.push(normalized)
}
return result
}
function normalizeLayout(layout) {
if (!isPlainObject(layout)) layout = builtinBarConfig.layout
return {
left: normalizeLayoutSection(layout.left),
center: normalizeLayoutSection(layout.center),
right: normalizeLayoutSection(layout.right)
}
}
function applyBarConfig() {
var config = mergeConfig(defaultBarConfig, userBarConfig)
position = normalizePosition(config.position)
fontFamily = String(config.fontFamily || "JetBrainsMono Nerd Font")
centerAnchor = String(config.centerAnchor || "clock")
layoutConfig = isPlainObject(config.layout) ? config.layout : builtinBarConfig.layout
moduleConfig = isPlainObject(config.modules) ? config.modules : {}
layoutConfig = normalizeLayout(config.layout)
barConfigSerial++
}
@@ -166,42 +189,53 @@ ShellRoot {
applyBarConfig()
}
function layoutModules(region) {
function layoutEntries(region) {
var serial = barConfigSerial
var modules = layoutConfig ? layoutConfig[region] : null
return Array.isArray(modules) ? modules : []
var entries = layoutConfig ? layoutConfig[region] : null
return Array.isArray(entries) ? entries : []
}
function moduleSettings(name) {
var serial = barConfigSerial
var settings = moduleConfig ? moduleConfig[String(name)] : null
return isPlainObject(settings) ? settings : {}
function entrySettings(entry) {
if (!isPlainObject(entry)) return {}
var copy = {}
for (var key in entry) {
if (key === "id") continue
copy[key] = entry[key]
}
return copy
}
function moduleString(name, key, fallback) {
var value = moduleSettings(name)[key]
function entryId(entry) {
if (typeof entry === "string") return entry
if (isPlainObject(entry) && entry.id) return String(entry.id)
return ""
}
function moduleString(entry, key, fallback) {
var settings = entrySettings(entry)
var value = settings[key]
return value === undefined || value === null ? fallback : String(value)
}
function moduleIndex(modules, name) {
if (!Array.isArray(modules)) return -1
function entryIndex(entries, name) {
if (!Array.isArray(entries)) return -1
for (var i = 0; i < modules.length; i++) {
if (String(modules[i]) === name)
for (var i = 0; i < entries.length; i++) {
if (entryId(entries[i]) === name)
return i
}
return -1
}
function modulesBefore(modules, name) {
var index = moduleIndex(modules, name)
return index <= 0 ? [] : modules.slice(0, index)
function entriesBefore(entries, name) {
var index = entryIndex(entries, name)
return index <= 0 ? [] : entries.slice(0, index)
}
function modulesAfter(modules, name) {
var index = moduleIndex(modules, name)
return index === -1 ? [] : modules.slice(index + 1)
function entriesAfter(entries, name) {
var index = entryIndex(entries, name)
return index === -1 ? [] : entries.slice(index + 1)
}
function builtinModuleComponent(name) {
@@ -241,8 +275,8 @@ ShellRoot {
return value !== "" && value.indexOf("..") === -1 && value[0] !== "/"
}
function customModuleType(name) {
var settings = moduleSettings(name)
function customModuleType(entry) {
var settings = entrySettings(entry)
var type = String(settings.type || "")
if (type) return type
if (settings.exec) return "command"
@@ -250,8 +284,9 @@ ShellRoot {
return ""
}
function customModuleSource(name) {
var settings = moduleSettings(name)
function customModuleSource(entry) {
var settings = entrySettings(entry)
var name = entryId(entry)
var source = settings.source ? expandPath(settings.source) : ""
if (!source && customModuleSafeName(name))
source = omarchyConfigDir + "/bar/modules/" + String(name) + ".qml"
@@ -601,14 +636,26 @@ ShellRoot {
root.run("hyprctl dispatch " + shellQuote("hl.dsp.focus({ workspace = \"" + id + "\" })"))
}
function clockEntry() {
var serial = barConfigSerial
var sections = ["left", "center", "right"]
for (var i = 0; i < sections.length; i++) {
var entries = layoutEntries(sections[i])
var idx = entryIndex(entries, "clock")
if (idx !== -1) return entries[idx]
}
return null
}
function clockText() {
var entry = clockEntry()
if (clockAlt)
return Qt.formatDateTime(systemClock.date, moduleString("clock", "formatAlt", "dd MMMM 'W'ww yyyy"))
return Qt.formatDateTime(systemClock.date, moduleString(entry, "formatAlt", "dd MMMM 'W'ww yyyy"))
if (vertical)
return Qt.formatDateTime(systemClock.date, moduleString("clock", "verticalFormat", "HH\n—\nmm"))
return Qt.formatDateTime(systemClock.date, moduleString(entry, "verticalFormat", "HH\n—\nmm"))
return Qt.formatDateTime(systemClock.date, moduleString("clock", "format", "dddd HH:mm"))
return Qt.formatDateTime(systemClock.date, moduleString(entry, "format", "dddd HH:mm"))
}
SystemClock {
@@ -947,19 +994,26 @@ ShellRoot {
Component { id: cpuModuleComponent; CpuModule {} }
Component { id: batteryModuleComponent; BatteryModule {} }
function findCenterAnchorEntry() {
var entries = root.layoutEntries("center")
var idx = root.entryIndex(entries, root.centerAnchor)
return idx === -1 ? null : entries[idx]
}
component LeftModules: ModuleList {
modules: root.layoutModules("left")
entries: root.layoutEntries("left")
}
component RightModules: ModuleList {
modules: root.layoutModules("right")
entries: root.layoutEntries("right")
}
component CenterModules: Item {
id: centerRoot
property var modules: root.layoutModules("center")
readonly property bool hasAnchor: root.moduleIndex(modules, root.centerAnchor) !== -1
property var entries: root.layoutEntries("center")
readonly property bool hasAnchor: root.entryIndex(entries, root.centerAnchor) !== -1
readonly property var anchorEntry: root.findCenterAnchorEntry()
Loader {
anchors.fill: parent
@@ -974,13 +1028,13 @@ ShellRoot {
ModuleList {
visible: !centerRoot.hasAnchor
modules: centerRoot.modules
entries: centerRoot.entries
anchors.centerIn: parent
}
ModuleList {
visible: centerRoot.hasAnchor
modules: root.modulesBefore(centerRoot.modules, root.centerAnchor)
entries: root.entriesBefore(centerRoot.entries, root.centerAnchor)
anchors.right: centerAnchorModule.left
anchors.verticalCenter: centerAnchorModule.verticalCenter
}
@@ -988,13 +1042,13 @@ ShellRoot {
ModuleSlot {
id: centerAnchorModule
visible: centerRoot.hasAnchor
moduleName: root.centerAnchor
entry: centerRoot.anchorEntry
anchors.centerIn: parent
}
ModuleList {
visible: centerRoot.hasAnchor
modules: root.modulesAfter(centerRoot.modules, root.centerAnchor)
entries: root.entriesAfter(centerRoot.entries, root.centerAnchor)
anchors.left: centerAnchorModule.right
anchors.verticalCenter: centerAnchorModule.verticalCenter
}
@@ -1009,13 +1063,13 @@ ShellRoot {
ModuleList {
visible: !centerRoot.hasAnchor
modules: centerRoot.modules
entries: centerRoot.entries
anchors.centerIn: parent
}
ModuleList {
visible: centerRoot.hasAnchor
modules: root.modulesBefore(centerRoot.modules, root.centerAnchor)
entries: root.entriesBefore(centerRoot.entries, root.centerAnchor)
anchors.bottom: centerAnchorModule.top
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
}
@@ -1023,13 +1077,13 @@ ShellRoot {
ModuleSlot {
id: centerAnchorModule
visible: centerRoot.hasAnchor
moduleName: root.centerAnchor
entry: centerRoot.anchorEntry
anchors.centerIn: parent
}
ModuleList {
visible: centerRoot.hasAnchor
modules: root.modulesAfter(centerRoot.modules, root.centerAnchor)
entries: root.entriesAfter(centerRoot.entries, root.centerAnchor)
anchors.top: centerAnchorModule.bottom
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
}
@@ -1040,9 +1094,9 @@ ShellRoot {
component ModuleList: Loader {
id: moduleListRoot
property var modules: []
property var entries: []
visible: modules.length > 0
visible: entries.length > 0
sourceComponent: root.vertical ? verticalModuleList : horizontalModuleList
width: item ? item.implicitWidth : 0
height: item ? item.implicitHeight : 0
@@ -1054,11 +1108,11 @@ ShellRoot {
spacing: 0
Repeater {
model: moduleListRoot.modules
model: moduleListRoot.entries
ModuleSlot {
required property var modelData
moduleName: String(modelData)
entry: modelData
}
}
}
@@ -1071,11 +1125,11 @@ ShellRoot {
spacing: 0
Repeater {
model: moduleListRoot.modules
model: moduleListRoot.entries
ModuleSlot {
required property var modelData
moduleName: String(modelData)
entry: modelData
}
}
}
@@ -1085,8 +1139,10 @@ ShellRoot {
component ModuleSlot: Item {
id: slot
required property string moduleName
readonly property string customType: root.customModuleType(moduleName)
required property var entry
readonly property string moduleName: root.entryId(entry)
readonly property var moduleSettings: root.entrySettings(entry)
readonly property string customType: root.customModuleType(entry)
readonly property var builtinComponent: customType ? null : root.builtinModuleComponent(moduleName)
readonly property string firstPartySource: customType || builtinComponent ? "" : root.firstPartyWidgetSource(moduleName)
readonly property bool qmlCustom: customType === "qml"
@@ -1109,35 +1165,33 @@ ShellRoot {
Loader {
id: qmlLoader
active: slot.qmlCustom || slot.firstParty
source: slot.qmlCustom ? root.customModuleSource(slot.moduleName) : (slot.firstParty ? slot.firstPartySource : "")
source: slot.qmlCustom ? root.customModuleSource(slot.entry) : (slot.firstParty ? slot.firstPartySource : "")
anchors.fill: parent
onLoaded: slot.injectProps()
}
onModuleSettingsChanged: injectProps()
function injectProps() {
var target = qmlLoader.item
if (!target) return
if ("bar" in target) target.bar = root
if ("moduleName" in target) target.moduleName = moduleName
if ("settings" in target) target.settings = root.moduleSettings(moduleName)
}
Connections {
target: root
function onBarConfigSerialChanged() { slot.injectProps() }
if ("settings" in target) target.settings = moduleSettings
}
Component {
id: customCommandModuleComponent
CustomCommandModule { moduleName: slot.moduleName }
CustomCommandModule { entry: slot.entry }
}
}
component CustomCommandModule: ModuleButton {
id: customRoot
required property string moduleName
property var settings: root.moduleSettings(moduleName)
required property var entry
readonly property string moduleName: root.entryId(entry)
readonly property var settings: root.entrySettings(entry)
property string outputText: ""
property string outputTooltip: ""
property bool outputActive: false