import Quickshell import Quickshell.Io import Quickshell.Wayland import QtQuick import QtQuick.Layouts import qs.Commons import qs.Ui import "BarModel.js" as BarModel Item { id: root // The omarchy-shell host injects omarchyPath from OMARCHY_PATH. required property string omarchyPath // Injected by the host shell so bar slots can resolve enabled widgets. 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 // Manifest for the active bar option. Present for custom bars and useful for // diagnostics; the built-in bar does not otherwise need it. property var manifest: 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 requestedTransparent: false property bool useTransparentForeground: false property bool transparent: false property bool centerSectionHovered: false property bool centerSectionRevealHeld: false property bool centerHoverRevealSuppressed: 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 themeForeground: Color.bar.text property color themeContrastForeground: Color.background property color transparentForeground: Color.bar.text property color foreground: themeForeground property color barForeground: useTransparentForeground ? transparentForeground : themeForeground property bool foregroundAnimationEnabled: true property color background: Color.bar.background property color urgent: Color.bar.active Behavior on barForeground { enabled: root.foregroundAnimationEnabled; 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 barDragSource: null property var barDragTarget: null property bool barDragAfter: false property var barDragWindow: null property var barDragScreen: null property url barDragImageUrl: "" property real barDragSceneX: 0 property real barDragSceneY: 0 property real barDragScreenX: 0 property real barDragScreenY: 0 property real barDragOffsetX: 0 property real barDragOffsetY: 0 property var configControls: [] property var clickTargets: [] property var debugModuleSlots: [] 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 registerDebugModuleSlot(slot) { if (!slot || debugModuleSlots.indexOf(slot) !== -1) return var next = debugModuleSlots.slice() next.push(slot) debugModuleSlots = next } function unregisterDebugModuleSlot(slot) { var next = debugModuleSlots.filter(function(item) { return item !== slot }) debugModuleSlots = next } function registerConfigControl(control) { if (!control || configControls.indexOf(control) !== -1) return var next = configControls.slice() next.push(control) configControls = next } function unregisterConfigControl(control) { var next = configControls.filter(function(item) { return item !== control }) configControls = next } function debugBarGeometry() { var out = [] for (var i = 0; i < debugModuleSlots.length; i++) { var slot = debugModuleSlots[i] if (!slot || !slot.activeItem) continue var point = { x: slot.x, y: slot.y } try { point = slot.mapToItem(null, 0, 0) } catch (e) { } out.push({ id: slot.moduleName, section: slot.region, x: Math.round(point.x), y: Math.round(point.y), width: Math.round(slot.width), height: Math.round(slot.height), visible: slot.visible === true && slot.width > 0 && slot.height > 0, itemVisible: slot.activeItem.visible === true, itemWidth: Math.round(slot.activeItem.implicitWidth || 0), itemHeight: Math.round(slot.activeItem.implicitHeight || 0) }) } return out } function targetWindow(target) { return target && target.QsWindow ? target.QsWindow.window : null } function targetBelongsToWindow(target, window) { return !!target && !!window && targetWindow(target) === window } function slotWindow(slot) { if (!slot) return null return targetWindow(slot.activeItem) || targetWindow(slot) } function sameWindow(left, right) { if (!left || !right) return false if (left === right) return true return !!left.screen && !!right.screen && !!left.screen.name && !!right.screen.name && left.screen.name === right.screen.name } 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 clearBarDrag() { barDragSource = null barDragWindow = null barDragScreen = null barDragImageUrl = "" barDragTarget = null barDragAfter = false barDragSceneX = 0 barDragSceneY = 0 barDragScreenX = 0 barDragScreenY = 0 barDragOffsetX = 0 barDragOffsetY = 0 } function barDragScreenPoint(scenePoint) { var x = scenePoint ? scenePoint.x : 0 var y = scenePoint ? scenePoint.y : 0 var window = barDragWindow if (!window || !window.screen) return { x: x, y: y } if (root.position === "bottom") y += Math.max(0, window.screen.height - window.height) else if (root.position === "right") x += Math.max(0, window.screen.width - window.width) return { x: x, y: y } } function captureBarDragGhost(slot) { var item = slot && slot.activeItem ? slot.activeItem : null barDragImageUrl = "" if (!item || typeof item.grabToImage !== "function") return var grabWidth = Math.max(1, Math.ceil(item.width || item.implicitWidth || slot.width || 1)) var grabHeight = Math.max(1, Math.ceil(item.height || item.implicitHeight || slot.height || 1)) item.grabToImage(function(result) { if (root.barDragSource !== slot || !result || !result.url) return root.barDragImageUrl = result.url }, Qt.size(grabWidth, grabHeight)) } 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) { return BarModel.normalizePosition(value) } // Apply tray-pinning on top of the shared layout normalization so the // bar host and scriptable config helpers 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) { return BarModel.pinTrayToInner(entries, section) } function applyBarConfig() { var config = Util.isPlainObject(barConfig) ? barConfig : fallbackBarConfig position = normalizePosition(config.position) setRequestedTransparency(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 panelNavigationSlots(region) { var entries = layoutEntries(region) var slots = [] for (var i = 0; i < entries.length; i++) { var id = entryId(entries[i]) for (var j = 0; j < debugModuleSlots.length; j++) { var slot = debugModuleSlots[j] if (!slot || slot.region !== region || slot.moduleName !== id) continue var item = slot.activeItem if (!item || item.visible !== true || slot.visible !== true || slot.width <= 0 || slot.height <= 0) continue if (typeof item.open !== "function" || typeof item.close !== "function" || item.opened === undefined) continue slots.push(slot) break } } return slots } function switchPanelFrom(owner, direction) { if (!owner) return false var currentSlot = null for (var i = 0; i < debugModuleSlots.length; i++) { var slot = debugModuleSlots[i] if (slot && slot.activeItem === owner) { currentSlot = slot break } } if (!currentSlot) return false var slots = panelNavigationSlots(currentSlot.region) if (slots.length < 2) return false var currentIndex = -1 for (var j = 0; j < slots.length; j++) { if (slots[j] === currentSlot) { currentIndex = j break } } if (currentIndex < 0) return false var step = direction < 0 ? -1 : 1 var nextSlot = slots[(currentIndex + step + slots.length) % slots.length] if (!nextSlot || !nextSlot.activeItem || nextSlot.activeItem === owner) return false nextSlot.activeItem.open() return true } function entrySettings(entry) { return BarModel.entrySettings(entry) } function entryId(entry) { return BarModel.entryId(entry) } function moduleString(entry, key, fallback) { return BarModel.moduleString(entry, key, fallback) } function entryIndex(entries, name) { return BarModel.entryIndex(entries, name) } function entriesBefore(entries, name) { return BarModel.entriesBefore(entries, name) } function entriesAfter(entries, name) { return BarModel.entriesAfter(entries, name) } function canonicalWidgetId(name) { return Util.canonicalWidgetId(name) } function expandPath(path) { return BarModel.expandPath(path, home) } function customModuleSafeName(name) { return BarModel.customModuleSafeName(name) } function customModuleType(entry) { return BarModel.customModuleType(entry) } function customModuleSource(entry) { var source = BarModel.customModulePath(entry, home, omarchyConfigDir) return source ? Util.fileUrl(source) : "" } Component.onCompleted: applyBarConfig() function setCenterSectionHovered(hovered) { centerSectionHovered = hovered if (hovered) { centerSectionRevealTimer.stop() centerSectionRevealHeld = true } else { centerSectionRevealTimer.restart() } } Timer { id: centerSectionRevealTimer interval: 120 onTriggered: root.centerSectionRevealHeld = root.centerSectionHovered } function run(command) { if (!command) return launcher.command = Util.hyprExecCommand(command) launcher.startDetached() } function openConfigPanel() { for (var i = 0; i < configControls.length; i++) { var control = configControls[i] if (!control || control.visible !== true || typeof control.openPanel !== "function") continue control.openPanel() return true } return false } function toggleTransparency() { var nextTransparent = !(root.requestedTransparent === 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.setRequestedTransparency(nextTransparent) } } function rawLayoutSection(config, region) { if (!Util.isPlainObject(config.bar)) config.bar = {} if (!Util.isPlainObject(config.bar.layout)) config.bar.layout = {} if (!Array.isArray(config.bar.layout[region])) config.bar.layout[region] = [] return config.bar.layout[region] } function rawEntryIndex(entries, name) { for (var i = 0; i < entries.length; i++) { if (root.entryId(entries[i]) === name) return i } return -1 } function moveModuleInConfig(config, fromRegion, fromName, toRegion, beforeName) { var fromEntries = rawLayoutSection(config, fromRegion) var toEntries = rawLayoutSection(config, toRegion) var fromIndex = rawEntryIndex(fromEntries, fromName) if (fromIndex < 0) return false var toIndex = beforeName ? rawEntryIndex(toEntries, beforeName) : toEntries.length if (toIndex < 0) toIndex = toEntries.length if (fromRegion === toRegion && fromIndex === toIndex) return false var movedEntry = fromEntries[fromIndex] fromEntries.splice(fromIndex, 1) if (fromRegion === toRegion && fromIndex < toIndex) toIndex -= 1 if (toIndex < 0) toIndex = 0 if (toIndex > toEntries.length) toIndex = toEntries.length if (fromRegion === toRegion && fromIndex === toIndex) { fromEntries.splice(fromIndex, 0, movedEntry) return false } toEntries.splice(toIndex, 0, movedEntry) return true } function dropBarModule(source, toRegion, beforeName) { if (!source || !source.region || !source.moduleName || !toRegion) return false if (source.region === toRegion && source.moduleName === beforeName) return false if (!root.shell || typeof root.shell.mutateShellConfig !== "function") return false var changed = false root.shell.mutateShellConfig(function(config) { changed = moveModuleInConfig(config, source.region, source.moduleName, toRegion, beforeName) }) return changed } function moduleDropAtScene(scenePoint, sourceSlot) { var sourceWindow = root.slotWindow(sourceSlot) || root.barDragWindow for (var i = 0; i < debugModuleSlots.length; i++) { var slot = debugModuleSlots[i] if (!slot || slot === sourceSlot || !slot.visible || slot.width <= 0 || slot.height <= 0) continue if (sourceWindow && !root.sameWindow(root.slotWindow(slot), sourceWindow)) continue var slotPoint = { x: slot.x, y: slot.y } try { slotPoint = slot.mapToItem(null, 0, 0) } catch (e) { } if (scenePoint.x >= slotPoint.x && scenePoint.x <= slotPoint.x + slot.width && scenePoint.y >= slotPoint.y && scenePoint.y <= slotPoint.y + slot.height) { return { slot: slot, after: root.vertical ? scenePoint.y > slotPoint.y + slot.height / 2 : scenePoint.x > slotPoint.x + slot.width / 2 } } } return null } function visibleModuleSlot(region, name, sourceSlot) { var sourceWindow = root.slotWindow(sourceSlot) || root.barDragWindow for (var i = 0; i < debugModuleSlots.length; i++) { var slot = debugModuleSlots[i] if (!slot || slot === sourceSlot || slot.region !== region || slot.moduleName !== name || !slot.visible || slot.width <= 0 || slot.height <= 0) continue if (sourceWindow && !root.sameWindow(root.slotWindow(slot), sourceWindow)) continue return slot } return null } function nextVisibleModuleName(region, afterName, sourceSlot) { var entries = layoutEntries(region) var found = false for (var i = 0; i < entries.length; i++) { var name = entryId(entries[i]) if (!found) { found = name === afterName continue } if (visibleModuleSlot(region, name, sourceSlot)) return name } return "" } function dropBarModuleAtTarget(sourceSlot, targetSlot, afterTarget) { if (!sourceSlot || !targetSlot) return false var beforeName = afterTarget ? nextVisibleModuleName(targetSlot.region, targetSlot.moduleName, sourceSlot) : targetSlot.moduleName return dropBarModule(sourceSlot, targetSlot.region, beforeName) } function moduleTargetClickable(target) { return target && target.visible !== false && target.opacity !== 0 && target.interactive !== false && target.pressable !== false && target.concealed !== true && typeof target.triggerPress === "function" } function moduleClickTargetAt(slot, localX, localY) { for (var i = clickTargets.length - 1; i >= 0; i--) { var target = clickTargets[i] if (!moduleTargetClickable(target)) continue var targetPoint = { x: localX, y: localY } try { targetPoint = slot.mapToItem(target, localX, localY) } catch (e) { continue } if (targetPoint.x >= 0 && targetPoint.x <= target.width && targetPoint.y >= 0 && targetPoint.y <= target.height) { return target } } if (moduleTargetClickable(slot.activeItem)) return slot.activeItem return null } function pressModuleClickTarget(slot, button, localX, localY) { var target = moduleClickTargetAt(slot, localX, localY) if (!target) return false target.triggerPress(button) return true } function colorHex(colorValue) { var c = colorValue if (typeof c === "string") c = Qt.color(c) function hexChannel(value) { var s = Math.round(Util.clamp(value, 0, 1) * 255).toString(16) return s.length < 2 ? "0" + s : s } return "#" + hexChannel(c.r) + hexChannel(c.g) + hexChannel(c.b) } function setRequestedTransparency(value) { var nextTransparent = value === true requestedTransparent = nextTransparent if (!nextTransparent) { foregroundAnimationEnabled = false useTransparentForeground = false transparent = false transparentForeground = themeForeground restoreForegroundAnimation() return } scheduleTransparentForegroundRefresh() } function restoreForegroundAnimation() { Qt.callLater(function() { Qt.callLater(function() { root.foregroundAnimationEnabled = true }) }) } function scheduleTransparentForegroundRefresh() { if (!requestedTransparent) { transparentForeground = themeForeground return } transparentForegroundTimer.restart() } function refreshTransparentForeground() { if (!requestedTransparent || transparentForegroundProc.running) return transparentForegroundProc.command = [ "omarchy-shell-bar-text-color", root.position, String(root.barSize), colorHex(root.themeForeground), colorHex(root.themeContrastForeground) ] transparentForegroundProc.running = true } onRequestedTransparentChanged: scheduleTransparentForegroundRefresh() onPositionChanged: scheduleTransparentForegroundRefresh() onThemeForegroundChanged: scheduleTransparentForegroundRefresh() onThemeContrastForegroundChanged: scheduleTransparentForegroundRefresh() Timer { id: transparentForegroundTimer interval: 120 repeat: false onTriggered: root.refreshTransparentForeground() } Process { id: transparentForegroundProc stdout: SplitParser { onRead: function(line) { var value = String(line || "").trim() if (!/^#[0-9A-Fa-f]{6}$/.test(value)) return root.foregroundAnimationEnabled = false root.transparentForeground = value if (root.requestedTransparent) { root.useTransparentForeground = true root.transparent = true } root.restoreForegroundAnimation() } } } FileView { path: root.home + "/.config/omarchy/current" watchChanges: true printErrors: false onFileChanged: root.scheduleTransparentForegroundRefresh() } 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 } } } Variants { model: Quickshell.screens delegate: Component { DragGhostPanel { required property var modelData screen: modelData ghostScreen: 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) } } BorderSurface { id: tooltipBubble implicitWidth: tooltipLabel.implicitWidth + 20 implicitHeight: tooltipLabel.implicitHeight + 14 color: Color.tooltip.background borderSpec: Border.surfaceSpec("tooltip", "border", Color.tooltip.border, 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 } } component DragGhostPanel: PanelWindow { id: ghostWindow required property var ghostScreen readonly property bool screenMatches: root.barDragScreen === ghostScreen || (root.barDragScreen && ghostScreen && root.barDragScreen.name && ghostScreen.name && root.barDragScreen.name === ghostScreen.name) readonly property bool active: root.barDragSource && root.barDragScreen && screenMatches readonly property var sourceItem: root.barDragSource ? root.barDragSource.activeItem : null readonly property int ghostPadding: Style.space(1) readonly property int ghostWidth: sourceItem ? Math.max(1, Math.ceil(sourceItem.width)) : 1 readonly property int ghostHeight: sourceItem ? Math.max(1, Math.ceil(sourceItem.height)) : 1 visible: active && sourceItem !== null color: "transparent" exclusionMode: ExclusionMode.Ignore WlrLayershell.namespace: "omarchy-bar-drag-ghost" WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: WlrKeyboardFocus.None anchors { top: true bottom: true left: true right: true } // Visual-only drag feedback. Keep the input region empty so the ghost can // sit under the cursor without stealing the MouseArea's active pointer grab. mask: Region {} Item { visible: ghostWindow.visible x: Math.round(root.barDragScreenX - root.barDragOffsetX - ghostWindow.ghostPadding) y: Math.round(root.barDragScreenY - root.barDragOffsetY - ghostWindow.ghostPadding) width: ghostWindow.ghostWidth + ghostWindow.ghostPadding * 2 height: ghostWindow.ghostHeight + ghostWindow.ghostPadding * 2 BorderSurface { anchors.fill: parent color: root.transparent ? "transparent" : root.background borderSpec: Border.flat(root.barForeground, 1) radius: Math.min(Style.cornerRadius, height / 2) opacity: root.transparent ? 0.45 : 0.94 } Image { anchors.fill: parent anchors.margins: ghostWindow.ghostPadding source: root.barDragImageUrl fillMode: Image.Stretch smooth: true opacity: 0.84 } } } 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") region: "left" } component RightModules: ModuleList { entries: root.layoutEntries("right") region: "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 } HoverHandler { onHoveredChanged: root.setCenterSectionHovered(hovered) } ModuleList { visible: !centerRoot.hasAnchor entries: centerRoot.entries region: "center" anchors.centerIn: parent } ModuleList { visible: centerRoot.hasAnchor entries: root.entriesBefore(centerRoot.entries, root.centerAnchor) region: "center" anchors.right: centerConfigControl.visible ? centerConfigControl.left : centerAnchorModule.left anchors.verticalCenter: centerAnchorModule.verticalCenter } ModuleSlot { id: centerAnchorModule visible: centerRoot.hasAnchor entry: centerRoot.anchorEntry region: "center" anchors.centerIn: parent } BarConfigControl { id: centerConfigControl visible: centerRoot.hasAnchor && centerAnchorModule.moduleName === "omarchy.clock" clockHovered: centerAnchorModule.hovered centerHovered: root.centerSectionRevealHeld && !root.centerHoverRevealSuppressed anchors.right: centerAnchorModule.left anchors.verticalCenter: centerAnchorModule.verticalCenter } ModuleList { visible: centerRoot.hasAnchor entries: root.entriesAfter(centerRoot.entries, root.centerAnchor) region: "center" anchors.left: centerAnchorModule.right anchors.verticalCenter: centerAnchorModule.verticalCenter } } } Component { id: verticalCenterModules Item { anchors.fill: parent CenterGestureArea { anchors.fill: parent } HoverHandler { onHoveredChanged: root.setCenterSectionHovered(hovered) } ModuleList { visible: !centerRoot.hasAnchor entries: centerRoot.entries region: "center" anchors.centerIn: parent } ModuleList { visible: centerRoot.hasAnchor entries: root.entriesBefore(centerRoot.entries, root.centerAnchor) region: "center" anchors.bottom: centerConfigControl.visible ? centerConfigControl.top : centerAnchorModule.top anchors.horizontalCenter: centerAnchorModule.horizontalCenter } ModuleSlot { id: centerAnchorModule visible: centerRoot.hasAnchor entry: centerRoot.anchorEntry region: "center" anchors.centerIn: parent } BarConfigControl { id: centerConfigControl visible: centerRoot.hasAnchor && centerAnchorModule.moduleName === "omarchy.clock" clockHovered: centerAnchorModule.hovered centerHovered: root.centerSectionRevealHeld && !root.centerHoverRevealSuppressed anchors.bottom: centerAnchorModule.top anchors.horizontalCenter: centerAnchorModule.horizontalCenter } ModuleList { visible: centerRoot.hasAnchor entries: root.entriesAfter(centerRoot.entries, root.centerAnchor) region: "center" anchors.top: centerAnchorModule.bottom anchors.horizontalCenter: centerAnchorModule.horizontalCenter } } } } component CenterGestureArea: MouseArea { acceptedButtons: Qt.LeftButton onDoubleClicked: function(mouse) { if (mouse.button === Qt.LeftButton) { root.toggleTransparency() mouse.accepted = true } } } component BarConfigControl: Item { id: configControl property bool clockHovered: false property bool centerHovered: false property bool openWhenReady: false readonly property var panelItem: configPanelLoader.item readonly property bool panelOpen: panelItem ? panelItem.opened === true : false readonly property bool revealed: visible && (clockHovered || centerHovered || controlHover.hovered || panelOpen) implicitWidth: button.implicitWidth implicitHeight: button.implicitHeight width: implicitWidth height: implicitHeight z: 500 HoverHandler { id: controlHover } Component.onCompleted: root.registerConfigControl(configControl) Component.onDestruction: root.unregisterConfigControl(configControl) function configurePanel(panel) { if (!panel) return panel.bar = root panel.anchorItem = button } function openPanel() { if (!panelItem) { openWhenReady = true return } panelItem.open() } function togglePanel() { if (!panelItem) { openPanel() return } panelItem.toggle() } WidgetButton { id: button anchors.fill: parent bar: root text: "" keepSpace: true concealed: !configControl.revealed dimmed: configControl.revealed && !controlHover.hovered && !configControl.panelOpen interactive: configControl.revealed horizontalMargin: 6.5 verticalPadding: 6 tooltipText: "Bar config" onPressed: function(b) { if (b === Qt.LeftButton) configControl.togglePanel() } } Loader { id: configPanelLoader active: true source: Qt.resolvedUrl("BarConfigPanel.qml") onLoaded: { configControl.configurePanel(item) if (configControl.openWhenReady) { configControl.openWhenReady = false item.open() } } } } component ModuleList: Loader { id: moduleListRoot property var entries: [] property string region: "" 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 region: moduleListRoot.region } } } } Component { id: verticalModuleList Column { spacing: 0 Repeater { model: moduleListRoot.entries ModuleSlot { required property var modelData entry: modelData region: moduleListRoot.region } } } } } component ModuleSlot: Item { id: slot required property var entry property string region: "" 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 } readonly property bool hovered: moduleHover.hovered readonly property bool dragSource: root.barDragSource === slot readonly property bool panelOpen: root.activePopout === slot.activeItem readonly property real openIndicatorInlineOffset: { var item = slot.activeItem if (!item || !("openIndicatorInlineOffset" in item)) return 0 var offset = Number(item.openIndicatorInlineOffset) return isFinite(offset) ? offset : 0 } implicitWidth: activeItem && activeItem.visible ? (root.vertical ? root.barSize : activeItem.implicitWidth) : 0 implicitHeight: activeItem && activeItem.visible ? activeItem.implicitHeight : 0 width: implicitWidth height: implicitHeight z: modulePointer.dragging ? 100 : 0 Component.onCompleted: root.registerDebugModuleSlot(slot) Component.onDestruction: { if (root.barDragSource === slot) root.clearBarDrag() root.unregisterDebugModuleSlot(slot) } HoverHandler { id: moduleHover } BorderSurface { visible: slot.dragSource anchors.fill: parent anchors.margins: Style.space(1) color: root.transparent ? "transparent" : root.background borderSpec: Border.flat(root.barForeground, 1) radius: Math.min(Style.cornerRadius, height / 2) opacity: root.transparent ? 0.22 : 0.32 } Loader { id: componentLoader active: !slot.qmlCustom && !slot.registered sourceComponent: slot.commandCustom ? customCommandModuleComponent : emptyModuleComponent anchors.fill: parent opacity: slot.dragSource ? 0.22 : 1.0 onLoaded: { slot.injectProps() Qt.callLater(slot.injectProps) } } Loader { id: registryLoader active: slot.registered sourceComponent: slot.registered ? slot.registryComponent : null anchors.fill: parent opacity: slot.dragSource ? 0.22 : 1.0 onLoaded: { slot.injectProps() Qt.callLater(slot.injectProps) } } Loader { id: qmlLoader active: slot.qmlCustom source: slot.qmlCustom ? root.customModuleSource(slot.entry) : "" anchors.fill: parent opacity: slot.dragSource ? 0.22 : 1.0 onLoaded: { slot.injectProps() Qt.callLater(slot.injectProps) } } Rectangle { id: openPanelIndicator readonly property int inset: Style.space(2) visible: opacity > 0 opacity: slot.panelOpen && !slot.dragSource ? 0.9 : 0 color: Color.accent radius: Math.min(width, height) / 2 width: root.vertical ? Style.space(2) : Math.max(Style.space(10), Math.round(parent.width * 0.55)) height: root.vertical ? Math.max(Style.space(10), Math.round(parent.height * 0.55)) : Style.space(2) x: root.vertical ? (root.position === "left" ? parent.width - width - inset : inset) : (slot.openIndicatorInlineOffset === 0 ? Math.round((parent.width - width) / 2) : (parent.width - width) / 2 + slot.openIndicatorInlineOffset) y: root.vertical ? (slot.openIndicatorInlineOffset === 0 ? Math.round((parent.height - height) / 2) : (parent.height - height) / 2 + slot.openIndicatorInlineOffset) : (root.position === "top" ? parent.height - height - inset : inset) z: 50 Behavior on opacity { NumberAnimation { duration: 120; easing.type: Easing.OutCubic } } } Rectangle { visible: !root.vertical && root.barDragTarget === slot && !root.barDragAfter anchors { left: parent.left top: parent.top bottom: parent.bottom } width: 2 color: root.barForeground opacity: 0.9 } Rectangle { visible: !root.vertical && root.barDragTarget === slot && root.barDragAfter anchors { right: parent.right top: parent.top bottom: parent.bottom } width: 2 color: root.barForeground opacity: 0.9 } Rectangle { visible: root.vertical && root.barDragTarget === slot && !root.barDragAfter anchors { left: parent.left right: parent.right top: parent.top } height: 2 color: root.barForeground opacity: 0.9 } Rectangle { visible: root.vertical && root.barDragTarget === slot && root.barDragAfter anchors { left: parent.left right: parent.right bottom: parent.bottom } height: 2 color: root.barForeground opacity: 0.9 } MouseArea { id: modulePointer property bool dragging: false property bool suppressClick: false property real pressedX: 0 property real pressedY: 0 readonly property bool canReorder: root.shell && typeof root.shell.mutateShellConfig === "function" readonly property real dragThreshold: Style.space(4) anchors.fill: parent acceptedButtons: Qt.LeftButton enabled: slot.visible && slot.width > 0 && slot.height > 0 propagateComposedEvents: true cursorShape: root.moduleClickTargetAt(slot, mouseX, mouseY) ? Qt.PointingHandCursor : Qt.ArrowCursor // Do not assign drag.target here: ModuleSlot is owned by Row/Column // positioners, and mutating slot.x/slot.y can leave stale offsets that // make neighboring modules overlap after a small aborted drag. onPressed: function(mouse) { dragging = false suppressClick = false pressedX = mouse.x pressedY = mouse.y root.clearBarDrag() } onPositionChanged: function(mouse) { if (!canReorder || !(mouse.buttons & Qt.LeftButton)) return var distance = Math.abs(mouse.x - pressedX) + Math.abs(mouse.y - pressedY) if (distance >= dragThreshold) { if (!dragging) { root.barDragWindow = root.targetWindow(slot.activeItem) || root.targetWindow(slot) root.barDragScreen = root.barDragWindow ? root.barDragWindow.screen : null root.barDragOffsetX = pressedX root.barDragOffsetY = pressedY root.captureBarDragGhost(slot) root.barDragSource = slot } dragging = true root.hideTooltip(slot.activeItem) } if (dragging) { var scenePoint = slot.mapToItem(null, mouse.x, mouse.y) var screenPoint = root.barDragScreenPoint(scenePoint) root.barDragSceneX = scenePoint.x root.barDragSceneY = scenePoint.y root.barDragScreenX = screenPoint.x root.barDragScreenY = screenPoint.y var drop = root.moduleDropAtScene(scenePoint, slot) root.barDragTarget = drop ? drop.slot : null root.barDragAfter = drop ? drop.after : false } } onReleased: function(mouse) { var wasDragging = dragging var targetSlot = root.barDragTarget var afterTarget = root.barDragAfter if (wasDragging) suppressClick = true dragging = false root.clearBarDrag() if (wasDragging && targetSlot) { root.dropBarModuleAtTarget(slot, targetSlot, afterTarget) mouse.accepted = true } else if (!wasDragging) { mouse.accepted = false } } onCanceled: { dragging = false suppressClick = false root.clearBarDrag() } onClicked: function(mouse) { if (suppressClick) { suppressClick = false mouse.accepted = true return } if (!root.pressModuleClickTarget(slot, mouse.button, mouse.x, mouse.y)) mouse.accepted = false } } 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) } } }