Files
omarchycn/shell/plugins/bar/Bar.qml
T
David Heinemeier HanssonandClaude Opus 5 6d4fa7acaf Make the open-panel mark find and fit its module
A center-anchored module is mounted twice: the copy that is drawn, and a
zero-size placeholder holding its place in the flow beside the anchor.
findPanelWidget returned whichever registered first, and that order is
not stable across a live bar reconfiguration, so a panel could open
anchored to the invisible copy -- mispositioned, with the drawn slot
never lighting up and switchPanelFrom unable to find it again.

The mark was also always 55% of the slot, which fits an icon but
underlines only a fraction of a text label, and runs the full height of a
multi-line module on a vertical bar. Modules can now say how long the
mark should be along the bar; anything that does not answer keeps the old
proportion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:10:06 -07:00

1712 lines
52 KiB
QML

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 stateHome: home + "/.local/state"
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 bool barMoveActive: false
property string barMoveCandidate: ""
property var barMoveWindow: null
property var barMoveScreen: null
property var clickTargets: []
property var moduleSlots: []
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 registerModuleSlot(slot) {
if (!slot || moduleSlots.indexOf(slot) !== -1) return
var next = moduleSlots.slice()
next.push(slot)
moduleSlots = next
}
function unregisterModuleSlot(slot) {
var next = moduleSlots.filter(function(item) { return item !== slot })
moduleSlots = next
}
function debugBarGeometry() {
var out = []
for (var i = 0; i < moduleSlots.length; i++) {
var slot = moduleSlots[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 windowScreenPoint(scenePoint, window) {
var x = scenePoint ? scenePoint.x : 0
var y = scenePoint ? scenePoint.y : 0
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 barDragScreenPoint(scenePoint) {
return windowScreenPoint(scenePoint, barDragWindow)
}
// Split the screen along its diagonals (in normalized space, so widescreens
// don't bias toward left/right): whichever triangle holds the cursor names
// the candidate edge.
function nearestScreenEdge(point, screen) {
var nx = screen.width > 0 ? Util.clamp(point.x / screen.width, 0, 1) : 0.5
var ny = screen.height > 0 ? Util.clamp(point.y / screen.height, 0, 1) : 0.5
var edge = "top"
var best = ny
if (1 - ny < best) { edge = "bottom"; best = 1 - ny }
if (nx < best) { edge = "left"; best = nx }
if (1 - nx < best) { edge = "right"; best = 1 - nx }
return edge
}
function beginBarMove(window) {
barMoveWindow = window
barMoveScreen = window ? window.screen : null
barMoveCandidate = position
barMoveActive = true
}
function updateBarMove(screenPoint) {
if (!barMoveActive || !barMoveScreen) return
barMoveCandidate = nearestScreenEdge(screenPoint, barMoveScreen)
}
function clearBarMove() {
barMoveActive = false
barMoveCandidate = ""
barMoveWindow = null
barMoveScreen = null
}
function finishBarMove() {
var edge = barMoveCandidate
if (!barMoveActive || !edge || edge === position) {
clearBarMove()
return
}
clearBarMove()
setBarPosition(edge)
}
function setBarPosition(value) {
var next = normalizePosition(value)
if (root.shell && typeof root.shell.mutateShellConfig === "function") {
root.shell.mutateShellConfig(function(config) {
if (!Util.isPlainObject(config.bar)) config.bar = {}
config.bar.position = next
})
} else {
root.position = next
}
}
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 < moduleSlots.length; j++) {
var slot = moduleSlots[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 < moduleSlots.length; i++) {
var slot = moduleSlots[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
}
// Every live instance of a widget id. A bar surface is built per monitor, so
// a widget that appears once in the layout is still live once per screen.
function moduleWidgets(pluginId) {
var id = String(pluginId || "")
var items = []
if (!id) return items
for (var i = 0; i < moduleSlots.length; i++) {
var slot = moduleSlots[i]
if (!slot || !slot.activeItem || slot.moduleName !== id) continue
items.push(slot.activeItem)
}
return items
}
// Resolve the live bar-widget instance for a plugin id (e.g. "omarchy.bluetooth").
// Only widgets that expose popup open/close methods count; plain indicators
// (clock, workspaces, tray) return null. Used by shell.summon/toggle so
// panel hotkeys route through the bar instead of a per-target IPC handler
// that only reaches whichever per-monitor instance claimed the target.
function findPanelWidget(pluginId) {
var id = String(pluginId || "")
if (!id) return null
var candidates = []
for (var i = 0; i < moduleSlots.length; i++) {
var slot = moduleSlots[i]
if (!slot || !slot.activeItem) continue
if (slot.moduleName !== id) continue
var item = slot.activeItem
if (typeof item.open !== "function" || typeof item.close !== "function" || item.opened === undefined) continue
candidates.push(slot)
}
// Anchored center modules are mounted twice; only the drawn copy can
// anchor a popup or carry the open-panel mark. See BarModel.pickDrawnSlot.
var chosen = BarModel.pickDrawnSlot(candidates)
return chosen ? chosen.activeItem : null
}
function summonBarWidget(pluginId) {
var item = findPanelWidget(pluginId)
if (!item || typeof item.open !== "function") return false
item.open()
return true
}
function hideBarWidget(pluginId) {
var item = findPanelWidget(pluginId)
if (!item || typeof item.close !== "function") return false
item.close()
return true
}
function isBarWidgetOpen(pluginId) {
var item = findPanelWidget(pluginId)
return !!item && item.opened === 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
Util.execDetached(command)
}
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 < moduleSlots.length; i++) {
var slot = moduleSlots[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 < moduleSlots.length; i++) {
var slot = moduleSlots[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-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.stateHome + "/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()
}
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", "-c", "[[ -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
}
}
}
Variants {
model: Quickshell.screens
delegate: Component {
BarMoveGhostPanel {
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
surfaceFormat.opaque: false
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
}
}
}
component BarMoveGhostPanel: PanelWindow {
id: moveGhostWindow
required property var ghostScreen
readonly property bool screenMatches: root.barMoveScreen === ghostScreen ||
(root.barMoveScreen && ghostScreen && root.barMoveScreen.name && ghostScreen.name && root.barMoveScreen.name === ghostScreen.name)
visible: root.barMoveActive && screenMatches
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "omarchy-bar-move-ghost"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
bottom: true
left: true
right: true
}
// Visual-only preview of the candidate edge. Keep the input region empty
// so the overlay never steals the gesture area's active pointer grab.
mask: Region {}
// One fixed-geometry slab per edge, crossfaded on candidate changes.
// Resizing a single slab between edges repaints mid-transition and
// flickers; fading between static ones does not.
Repeater {
model: ["top", "bottom", "left", "right"]
BorderSurface {
id: edgeSlab
required property string modelData
readonly property bool edgeVertical: modelData === "left" || modelData === "right"
readonly property int edgeSize: edgeVertical ? Style.bar.sizeVertical : Style.bar.sizeHorizontal
x: modelData === "right" ? parent.width - edgeSize : 0
y: modelData === "bottom" ? parent.height - edgeSize : 0
width: edgeVertical ? edgeSize : parent.width
height: edgeVertical ? parent.height : edgeSize
color: root.transparent ? "transparent" : root.background
borderSpec: Border.flat(root.barForeground, 1)
visible: opacity > 0
opacity: root.barMoveCandidate === modelData ? (root.transparent ? 0.45 : 0.7) : 0
Behavior on opacity {
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
}
}
}
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: centerAnchorModule.left
anchors.verticalCenter: centerAnchorModule.verticalCenter
}
ModuleSlot {
id: centerAnchorModule
visible: centerRoot.hasAnchor
entry: centerRoot.anchorEntry
region: "center"
anchors.centerIn: parent
}
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: centerAnchorModule.top
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
}
ModuleSlot {
id: centerAnchorModule
visible: centerRoot.hasAnchor
entry: centerRoot.anchorEntry
region: "center"
anchors.centerIn: parent
}
ModuleList {
visible: centerRoot.hasAnchor
entries: root.entriesAfter(centerRoot.entries, root.centerAnchor)
region: "center"
anchors.top: centerAnchorModule.bottom
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
}
}
}
}
component CenterGestureArea: MouseArea {
id: gestureArea
property bool dragging: false
property bool suppressClick: false
property real pressedX: 0
property real pressedY: 0
readonly property real dragThreshold: Style.space(4)
acceptedButtons: Qt.LeftButton
cursorShape: dragging ? Qt.ClosedHandCursor : Qt.ArrowCursor
pressAndHoldInterval: 200
function startDrag(x, y) {
if (dragging) return
dragging = true
root.beginBarMove(root.targetWindow(gestureArea))
var scenePoint = gestureArea.mapToItem(null, x, y)
root.updateBarMove(root.windowScreenPoint(scenePoint, root.barMoveWindow))
}
onPressed: function(mouse) {
dragging = false
suppressClick = false
pressedX = mouse.x
pressedY = mouse.y
}
onPressAndHold: function(mouse) {
startDrag(mouse.x, mouse.y)
}
onPositionChanged: function(mouse) {
if (!(mouse.buttons & Qt.LeftButton)) return
if (!dragging) {
var distance = Math.abs(mouse.x - pressedX) + Math.abs(mouse.y - pressedY)
if (distance < dragThreshold) return
startDrag(mouse.x, mouse.y)
return
}
var scenePoint = gestureArea.mapToItem(null, mouse.x, mouse.y)
root.updateBarMove(root.windowScreenPoint(scenePoint, root.barMoveWindow))
}
onReleased: function(mouse) {
if (!dragging) return
dragging = false
suppressClick = true
root.finishBarMove()
mouse.accepted = true
}
onCanceled: {
dragging = false
suppressClick = false
root.clearBarMove()
}
onClicked: function(mouse) {
if (suppressClick) {
suppressClick = false
mouse.accepted = true
}
}
onDoubleClicked: function(mouse) {
if (suppressClick) {
suppressClick = false
return
}
if (mouse.button === Qt.LeftButton) {
root.toggleTransparency()
mouse.accepted = true
}
}
}
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
// Modules bigger than the mark they want (a text label in a padded slot,
// a multi-line stack on a vertical bar) can say how long the open-panel
// dot should be along the bar, so it tracks what the module paints
// instead of a fraction of whatever slot it happens to fill.
readonly property real panelIndicatorExtent: {
var key = root.vertical ? "openPanelIndicatorHeight" : "openPanelIndicatorWidth"
var hint = activeItem && key in activeItem ? activeItem[key] : undefined
if (hint !== undefined && hint !== null && hint > 0) return Math.round(hint)
return Math.max(Style.space(10), Math.round((root.vertical ? slot.height : slot.width) * 0.55))
}
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.registerModuleSlot(slot)
Component.onDestruction: {
if (root.barDragSource === slot) root.clearBarDrag()
root.unregisterModuleSlot(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) : slot.panelIndicatorExtent
height: root.vertical ? slot.panelIndicatorExtent : Style.space(2)
// The mark sits on the module's inner edge — the one facing the
// desktop — so it underlines a top bar, overlines a bottom one, and
// points inward from a left or right one. It reads as pointing at the
// panel that opens on that side.
x: root.vertical
? (root.position === "left" ? parent.width - width - inset : inset)
: Math.round((parent.width - width) / 2)
y: root.vertical
? Math.round((parent.height - height) / 2)
: (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)
}
}
}