import Quickshell import Quickshell.Io import Quickshell.Wayland import QtQuick import QtQuick.Layouts import qs.Commons import qs.Ui Item { id: root // The omarchy-shell host injects omarchyPath from OMARCHY_PATH. required property string omarchyPath // 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, layout. The host owns file IO; // the bar just renders whatever it's handed. The bar font follows the // OS-level fontconfig monospace binding — it is not stored in shell.json. required property var barConfig // Injected by the host shell. Used for shell-wide actions such as opening // settings and persisting inline widget state. property var shell: null // Mirrors the on-disk `bar-off` flag so the user can hide the bar without // killing the entire shell. Wired to BarPanel.visible below; updated by the // FileView watcher further down. property bool barHidden: false property string home: Quickshell.env("HOME") property string omarchyConfigDir: home + "/.config/omarchy" property var fallbackBarConfig: ({ position: "top", transparent: false, centerAnchor: "omarchy.clock", layout: { left: [], center: [], right: [] } }) property var layoutConfig: fallbackBarConfig.layout property string centerAnchor: "" property bool transparent: false property int barConfigSerial: 0 property string position: "top" // Resolves through fontconfig at paint time (Style.font.family defaults // to "monospace"), so changing the system font (via `omarchy-font-set`) // updates the bar without a reload. property string fontFamily: Style.font.family // Bound to the central Color singleton so the bar tracks shell.toml's // [bar] section. Property names kept for the rest of this file's bindings. property color foreground: Color.bar.text property color background: Color.bar.background property color urgent: Color.bar.active Behavior on foreground { ColorAnimation { duration: 420; easing.type: Easing.InOutCubic } } Behavior on background { ColorAnimation { duration: 420; easing.type: Easing.InOutCubic } } Behavior on urgent { ColorAnimation { duration: 420; easing.type: Easing.InOutCubic } } property var tooltipTarget: null property var pendingTooltipTarget: null property string tooltipText: "" property string pendingTooltipText: "" property bool tooltipShown: false property int tooltipRequest: 0 property var activePopout: null property var clickTargets: [] function registerClickTarget(target) { if (!target || clickTargets.indexOf(target) !== -1) return var next = clickTargets.slice() next.push(target) clickTargets = next } function unregisterClickTarget(target) { var next = clickTargets.filter(function(item) { return item !== target }) clickTargets = next } function targetWindow(target) { return target && target.QsWindow ? target.QsWindow.window : null } function targetBelongsToWindow(target, window) { return !!target && !!window && targetWindow(target) === window } function targetTooltipHovered(target) { return !!target && target.visible !== false && target.opacity !== 0 && target.tooltipHovered === true } function clearTooltip() { tooltipTimer.stop() pendingTooltipTarget = null pendingTooltipText = "" tooltipTarget = null tooltipText = "" tooltipShown = false } function requestPopout(owner) { if (activePopout === owner) return if (activePopout) { if ("closeForPopoutSwitch" in activePopout) activePopout.closeForPopoutSwitch() else if ("close" in activePopout) activePopout.close() } activePopout = owner } function releasePopout(owner) { if (activePopout === owner) activePopout = null } readonly property bool vertical: position === "left" || position === "right" readonly property int barSize: vertical ? Style.bar.sizeVertical : Style.bar.sizeHorizontal function normalizePosition(value) { var next = String(value || "").trim() return /^(top|bottom|left|right)$/.test(next) ? next : "top" } // Apply tray-pinning on top of the shared layout normalization so the // bar host and the bar settings panel can't drift on entry shape. function normalizeLayout(layout) { var normalized = Util.normalizeLayout(Util.isPlainObject(layout) ? layout : fallbackBarConfig.layout) return { left: pinTrayToInner(normalized.left, "left"), center: pinTrayToInner(normalized.center, "center"), right: pinTrayToInner(normalized.right, "right") } } // The tray drawer reveals inward (away from the bar edge). Place it at the // section's inner edge: start of the right section, end of the left/center // sections. The drawer's reserved space then sits next to the bar center, // not stranded mid-section. function pinTrayToInner(entries, section) { var trayEntry = null var result = [] for (var i = 0; i < entries.length; i++) { if (entryId(entries[i]) === "omarchy.tray") trayEntry = entries[i] else result.push(entries[i]) } if (trayEntry) { if (section === "right") result.unshift(trayEntry) else result.push(trayEntry) } return result } function applyBarConfig() { var config = Util.isPlainObject(barConfig) ? barConfig : fallbackBarConfig position = normalizePosition(config.position) transparent = config.transparent === true centerAnchor = Util.canonicalWidgetId(config.centerAnchor || "") layoutConfig = normalizeLayout(config.layout) barConfigSerial++ } onBarConfigChanged: applyBarConfig() function layoutEntries(region) { var serial = barConfigSerial var entries = layoutConfig ? layoutConfig[region] : null return Array.isArray(entries) ? entries : [] } function entrySettings(entry) { if (!Util.isPlainObject(entry)) return {} var copy = {} for (var key in entry) { if (key === "id") continue copy[key] = entry[key] } return copy } function entryId(entry) { if (typeof entry === "string") return entry if (Util.isPlainObject(entry)) { var id = entry["id"] if (id !== undefined && id !== null && String(id) !== "") return String(id) } return "" } function moduleString(entry, key, fallback) { var settings = entrySettings(entry) var value = settings[key] return value === undefined || value === null ? fallback : String(value) } function entryIndex(entries, name) { if (!Array.isArray(entries)) return -1 for (var i = 0; i < entries.length; i++) { if (entryId(entries[i]) === name) return i } return -1 } function entriesBefore(entries, name) { var index = entryIndex(entries, name) return index <= 0 ? [] : entries.slice(0, index) } function entriesAfter(entries, name) { var index = entryIndex(entries, name) return index === -1 ? [] : entries.slice(index + 1) } function canonicalWidgetId(name) { return Util.canonicalWidgetId(name) } function expandPath(path) { var value = String(path || "") if (value === "") return "" if (value.indexOf("~/") === 0) return home + value.substring(1) if (value.indexOf("$HOME/") === 0) return home + value.substring(5) return value } function customModuleSafeName(name) { var value = String(name || "") return value !== "" && value.indexOf("..") === -1 && value[0] !== "/" } function customModuleType(entry) { var settings = entrySettings(entry) var type = String(settings.type || "") if (type) return type if (settings.exec) return "command" if (settings.source) return "qml" return "" } 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" return source ? Util.fileUrl(source) : "" } Component.onCompleted: applyBarConfig() function run(command) { if (!command) return launcher.command = ["bash", "-lc", command] launcher.startDetached() } function openBarSettings() { if (root.shell && typeof root.shell.summon === "function") { root.shell.summon("omarchy.settings", "{}") } else { root.run("omarchy-launch-bar-settings") } } function toggleTransparency() { var nextTransparent = !(root.transparent === true) if (root.shell && typeof root.shell.mutateShellConfig === "function") { root.shell.mutateShellConfig(function(config) { if (!Util.isPlainObject(config.bar)) config.bar = {} config.bar.transparent = nextTransparent }) } else { root.transparent = nextTransparent } } function runProcess(process) { if (!process.running) process.running = true } function showTooltip(target, text) { clearTooltip() if (!targetTooltipHovered(target) || !text) { tooltipRequest += 1 return } var request = tooltipRequest + 1 tooltipRequest = request pendingTooltipTarget = target pendingTooltipText = text Qt.callLater(function() { if (request !== tooltipRequest) return if (!targetTooltipHovered(pendingTooltipTarget)) { clearTooltip() return } tooltipTarget = pendingTooltipTarget tooltipText = pendingTooltipText pendingTooltipTarget = null pendingTooltipText = "" tooltipTimer.restart() }) } function hideTooltip(target) { if (tooltipTarget !== target && pendingTooltipTarget !== target) return tooltipRequest += 1 clearTooltip() } Process { id: launcher } Timer { id: tooltipTimer interval: 400 onTriggered: { if (root.targetTooltipHovered(root.tooltipTarget)) root.tooltipShown = true else root.clearTooltip() } } Timer { interval: 100 running: root.tooltipShown repeat: true onTriggered: if (!root.targetTooltipHovered(root.tooltipTarget)) root.hideTooltip(root.tooltipTarget) } // Presence of the `bar-off` flag = bar hidden. Watching the parent toggles // directory because FileView can't observe a file that doesn't exist yet, // and the flag is created/removed by `omarchy-toggle-bar`. Process { id: barHiddenProbe running: true command: ["bash", "-lc", "[[ -f $HOME/.local/state/omarchy/toggles/bar-off ]] && echo yes || echo no"] stdout: SplitParser { onRead: function(line) { root.barHidden = String(line).trim() === "yes" } } } FileView { path: root.home + "/.local/state/omarchy/toggles" watchChanges: true printErrors: false onFileChanged: barHiddenProbe.running = true } Variants { model: Quickshell.screens delegate: Component { BarPanel { required property var modelData screen: modelData } } } component BarPanel: PanelWindow { id: barWindow visible: !root.barHidden anchors { top: root.position === "top" || root.vertical bottom: root.position === "bottom" || root.vertical left: root.position === "left" || !root.vertical right: root.position === "right" || !root.vertical } implicitWidth: root.vertical ? root.barSize : 0 implicitHeight: root.vertical ? 0 : root.barSize color: root.transparent ? "transparent" : root.background WlrLayershell.namespace: "omarchy-bar" WlrLayershell.layer: WlrLayer.Top Loader { anchors.fill: parent sourceComponent: root.vertical ? verticalBar : horizontalBar } PopupWindow { id: tooltipWindow visible: root.tooltipShown && root.tooltipTarget !== null && root.tooltipText !== "" && root.targetBelongsToWindow(root.tooltipTarget, barWindow) color: "transparent" implicitWidth: Math.ceil(tooltipBubble.implicitWidth) implicitHeight: Math.ceil(tooltipBubble.implicitHeight) anchor { id: tooltipAnchor window: barWindow adjustment: PopupAdjustment.Slide edges: Edges.Top | Edges.Left gravity: Edges.Bottom | Edges.Right rect.width: 1 rect.height: 1 onAnchoring: { var target = root.tooltipTarget if (!root.targetBelongsToWindow(target, barWindow)) return var popupWidth = tooltipWindow.implicitWidth var popupHeight = tooltipWindow.implicitHeight var localX = target.width / 2 - popupWidth / 2 var localY = target.height + 6 if (root.position === "bottom") { localY = -popupHeight - 6 } else if (root.position === "left") { localX = target.width + 6 localY = target.height / 2 - popupHeight / 2 } else if (root.position === "right") { localX = -popupWidth - 6 localY = target.height / 2 - popupHeight / 2 } var point = barWindow.contentItem.mapFromItem(target, localX, localY) tooltipAnchor.rect.x = Math.round(point.x) tooltipAnchor.rect.y = Math.round(point.y) } } Rectangle { id: tooltipBubble implicitWidth: tooltipLabel.implicitWidth + 20 implicitHeight: tooltipLabel.implicitHeight + 14 color: Color.tooltip.background border.color: Color.tooltip.border border.width: 1 radius: Style.cornerRadius Text { id: tooltipLabel anchors.centerIn: parent text: root.tooltipText color: Color.tooltip.text font.family: root.fontFamily font.pixelSize: Style.font.body horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } } } Component { id: horizontalBar Item { anchors.fill: parent CenterModules { anchors.fill: parent } LeftModules { anchors.left: parent.left anchors.leftMargin: Style.space(8) anchors.verticalCenter: parent.verticalCenter } RightModules { anchors.right: parent.right anchors.rightMargin: Style.space(8) anchors.verticalCenter: parent.verticalCenter } } } Component { id: verticalBar Item { anchors.fill: parent CenterModules { anchors.fill: parent } LeftModules { anchors.top: parent.top anchors.topMargin: Style.space(8) anchors.horizontalCenter: parent.horizontalCenter } RightModules { anchors.bottom: parent.bottom anchors.bottomMargin: Style.space(8) anchors.horizontalCenter: parent.horizontalCenter } } } } Component { id: emptyModuleComponent; Item { implicitWidth: 0; implicitHeight: 0; visible: false } } function findCenterAnchorEntry() { var entries = root.layoutEntries("center") var idx = root.entryIndex(entries, root.centerAnchor) return idx === -1 ? null : entries[idx] } component LeftModules: ModuleList { entries: root.layoutEntries("left") } component RightModules: ModuleList { entries: root.layoutEntries("right") } component CenterModules: Item { id: centerRoot 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 sourceComponent: root.vertical ? verticalCenterModules : horizontalCenterModules } Component { id: horizontalCenterModules Item { anchors.fill: parent CenterGestureArea { anchors.fill: parent } ModuleList { visible: !centerRoot.hasAnchor entries: centerRoot.entries anchors.centerIn: parent } ModuleList { visible: centerRoot.hasAnchor entries: root.entriesBefore(centerRoot.entries, root.centerAnchor) anchors.right: centerAnchorModule.left anchors.verticalCenter: centerAnchorModule.verticalCenter } ModuleSlot { id: centerAnchorModule visible: centerRoot.hasAnchor entry: centerRoot.anchorEntry anchors.centerIn: parent } ModuleList { visible: centerRoot.hasAnchor entries: root.entriesAfter(centerRoot.entries, root.centerAnchor) anchors.left: centerAnchorModule.right anchors.verticalCenter: centerAnchorModule.verticalCenter } } } Component { id: verticalCenterModules Item { anchors.fill: parent CenterGestureArea { anchors.fill: parent } ModuleList { visible: !centerRoot.hasAnchor entries: centerRoot.entries anchors.centerIn: parent } ModuleList { visible: centerRoot.hasAnchor entries: root.entriesBefore(centerRoot.entries, root.centerAnchor) anchors.bottom: centerAnchorModule.top anchors.horizontalCenter: centerAnchorModule.horizontalCenter } ModuleSlot { id: centerAnchorModule visible: centerRoot.hasAnchor entry: centerRoot.anchorEntry anchors.centerIn: parent } ModuleList { visible: centerRoot.hasAnchor entries: root.entriesAfter(centerRoot.entries, root.centerAnchor) anchors.top: centerAnchorModule.bottom anchors.horizontalCenter: centerAnchorModule.horizontalCenter } } } } component CenterGestureArea: MouseArea { acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: function(mouse) { if (mouse.button === Qt.RightButton) { root.openBarSettings() mouse.accepted = true } } onDoubleClicked: function(mouse) { if (mouse.button !== Qt.RightButton) { root.toggleTransparency() mouse.accepted = true } } } component ModuleList: Loader { id: moduleListRoot property var entries: [] visible: entries.length > 0 sourceComponent: root.vertical ? verticalModuleList : horizontalModuleList width: item ? item.implicitWidth : 0 height: item ? item.implicitHeight : 0 Component { id: horizontalModuleList Row { spacing: 0 Repeater { model: moduleListRoot.entries ModuleSlot { required property var modelData entry: modelData } } } } Component { id: verticalModuleList Column { spacing: 0 Repeater { model: moduleListRoot.entries ModuleSlot { required property var modelData entry: modelData } } } } } component ModuleSlot: Item { id: slot 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) // Re-evaluate when the registry mutates (Component reference changes, // plugin enabled/disabled, etc.). Reading the `widgets` property creates // the binding dependency — the wrapped function call alone wouldn't. readonly property var registryComponent: { var w = root.barWidgetRegistry.widgets if (customType) return null var registryName = root.canonicalWidgetId(moduleName) return w[registryName] ? w[registryName].component : null } readonly property bool qmlCustom: customType === "qml" readonly property bool commandCustom: customType === "command" readonly property bool registered: registryComponent !== null readonly property var activeItem: { if (registered) return registryLoader.item if (qmlCustom) return qmlLoader.item return componentLoader.item } implicitWidth: activeItem && activeItem.visible ? (root.vertical ? root.barSize : activeItem.implicitWidth) : 0 implicitHeight: activeItem && activeItem.visible ? activeItem.implicitHeight : 0 width: implicitWidth height: implicitHeight Loader { id: componentLoader active: !slot.qmlCustom && !slot.registered sourceComponent: slot.commandCustom ? customCommandModuleComponent : emptyModuleComponent anchors.fill: parent onLoaded: { slot.injectProps() Qt.callLater(slot.injectProps) } } Loader { id: registryLoader active: slot.registered sourceComponent: slot.registered ? slot.registryComponent : null anchors.fill: parent onLoaded: { slot.injectProps() Qt.callLater(slot.injectProps) } } Loader { id: qmlLoader active: slot.qmlCustom source: slot.qmlCustom ? root.customModuleSource(slot.entry) : "" anchors.fill: parent onLoaded: { slot.injectProps() Qt.callLater(slot.injectProps) } } onActiveItemChanged: Qt.callLater(injectProps) onModuleSettingsChanged: injectProps() function injectProps() { var target = activeItem if (!target) return if ("bar" in target) target.bar = root if ("moduleName" in target) target.moduleName = moduleName if ("settings" in target) target.settings = moduleSettings } Component { id: customCommandModuleComponent CustomCommandModule { entry: slot.entry } } } component CustomCommandModule: WidgetButton { id: customRoot 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 function setting(name, fallback) { var value = settings ? settings[name] : undefined return value === undefined || value === null ? fallback : value } function update(raw) { var data = Util.parseModuleJson(raw) var klass = data.class || data.alt || "" outputText = data.text || String(raw || "").trim() outputTooltip = data.tooltip || String(setting("tooltip", "")) outputActive = klass === "active" || (Array.isArray(klass) && klass.indexOf("active") !== -1) } bar: root text: outputText || String(setting("text", "")) tooltipText: outputTooltip || String(setting("tooltip", "")) active: outputActive keepSpace: setting("keepSpace", false) === true horizontalMargin: Number(setting("horizontalMargin", 7.5)) verticalPadding: Number(setting("verticalPadding", 6)) fontSize: Number(setting("fontSize", 12)) onPressed: function(button) { var command = "" if (button === Qt.RightButton) command = String(setting("onRightClick", "")) else if (button === Qt.MiddleButton) command = String(setting("onMiddleClick", "")) else command = String(setting("onClick", "")) if (command) root.run(command) } Process { id: customProc command: ["bash", "-lc", String(customRoot.setting("exec", ""))] stdout: StdioCollector { waitForEnd: true onStreamFinished: customRoot.update(text) } } Timer { interval: Math.max(1, Number(customRoot.setting("interval", 5))) * 1000 running: String(customRoot.setting("exec", "")) !== "" repeat: true triggeredOnStart: true onTriggered: root.runProcess(customProc) } } }