Files
omarchycn/shell/plugins/bar/Bar.qml
T
567e24cd90 Hold the indicator peek open while the pointer is on the bar (#6663)
* Hold the indicator peek open while the pointer is on the bar

Revealing the hidden indicators widens their section, and a section that
grows can slide a neighbouring widget under a pointer that never moved.
Collapsing the peek on that un-hover narrowed the section again, moved the
neighbour back out, and re-opened the peek, so a pointer resting in the bar
space beside a grown section stuttered the bar until it moved away.

Hold the peek while the pointer is anywhere on the bar and close it only
once the pointer has left, which keeps the reveal-on-empty-space gesture
and drops the feedback loop.

Fixes #6581

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Assert the whole-bar hover helper does what the peek depends on

The earlier assertions all held against a no-op setBarHovered, which would
leave barHovered false and let the oscillation straight back in. Pin the
assignment and the collapse re-run too, so the helper cannot be emptied
without the suite noticing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Let the delayed peek re-check collapse only, never open

The timer assigned centerSectionRevealHeld outright, so it opened the peek
from bar hover alone. A pointer resting on the left section that dipped off
the bar and returned inside 120ms left the timer pending with barHovered
true again, and the indicators revealed without the pointer ever touching
the center section.

Opening stays the center section's own gesture in setCenterSectionHovered.
The timer now only closes what that opened, and the test asserts the
invariant against the whole file rather than one helper body that never
had the offending assignment in it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Tally bar hover per monitor instead of sharing one flag

Every screen's bar wrote the same barHovered bool, last writer wins. Sliding
along the top edge from one monitor's bar to the next can deliver the enter
before the leave, leaving the flag false under a live pointer; the collapse
then fired on a peek the user was still hovering, and no further hover change
arrived to correct it until the pointer left and came back.

Counting each surface's hover makes the order irrelevant. A bar destroyed
mid-hover — unplugging a monitor — never sends a leave, so it hands its
tally back on destruction rather than holding the peek open for good.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: David Heinemeier Hansson <david@hey.com>
2026-08-10 19:35:46 +02:00

1809 lines
58 KiB
QML

import Quickshell
import Quickshell.Hyprland
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. Hidden panels stay mapped but park off-screen
// without an exclusion zone; 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
// One bar surface exists per monitor and each reports into this count, so a
// pointer crossing from one monitor's bar to another's stays counted however
// the enter and leave interleave. A single shared bool would be left false by
// whichever event landed last.
property int barHoverCount: 0
// True while the pointer is over any bar, widgets included.
readonly property bool barHovered: barHoverCount > 0
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 var barDragTargetGeometry: 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
barDragTargetGeometry = 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)
}
function dropMarkerRect(slot, after) {
if (!slot) return null
try {
var slotPoint = slot.mapToItem(null, 0, 0)
var screenPoint = barDragScreenPoint(slotPoint)
var thickness = Style.spacing.xs
if (vertical) {
return {
x: screenPoint.x,
y: screenPoint.y + (after ? slot.height : 0) - thickness / 2,
width: slot.width,
height: thickness
}
}
return {
x: screenPoint.x + (after ? slot.width : 0) - thickness / 2,
y: screenPoint.y,
width: thickness,
height: slot.height
}
} catch (e) {
return null
}
}
// 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 || "")
// layoutEntries feeds plain JS arrays to the module Repeaters, and QML
// cannot diff those: reassigning layoutConfig rebuilds every widget on
// every monitor. When a shell.json write only changed inline widget
// settings, patch the live layout and running widgets in place instead.
var next = normalizeLayout(config.layout)
var delta = BarModel.inlineSettingsDelta(layoutConfig, next)
if (delta) {
applySettingsDelta(delta)
return
}
layoutConfig = next
barConfigSerial++
}
function applySettingsDelta(delta) {
for (var i = 0; i < delta.length; i++) {
var change = delta[i]
layoutConfig[change.region][change.index] = change.entry
var settings = entrySettings(change.entry)
for (var s = 0; s < moduleSlots.length; s++) {
var slot = moduleSlots[s]
if (!slot || slot.region !== change.region || slot.moduleName !== entryId(change.entry)) continue
var item = slot.activeItem
if (item && "settings" in item) item.settings = settings
}
}
}
onBarConfigChanged: applyBarConfig()
function layoutEntries(region) {
var serial = barConfigSerial
var entries = layoutConfig ? layoutConfig[region] : null
return Array.isArray(entries) ? entries : []
}
// Tab order for the panels in one bar region. Scoped to a single bar surface
// so tabbing walks the bar the open panel belongs to instead of hopping the
// panel to another monitor's copy of the same widget.
function panelNavigationSlots(region, window) {
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
if (window && !sameWindow(slotWindow(slot), window)) 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, slotWindow(currentSlot))
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
}
function slotScreenName(slot) {
var window = slotWindow(slot)
return window && window.screen ? String(window.screen.name || "") : ""
}
// The output Hyprland has focused, which is where a keyboard-summoned panel
// belongs. Empty until Hyprland reports one, which leaves panel routing on
// its per-monitor fallback rather than guessing at an output.
function focusedScreenName() {
var monitor = Hyprland.focusedMonitor
return monitor ? String(monitor.name || "") : ""
}
// 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: slot, screenName: slotScreenName(slot), opened: item.opened === true })
}
// One copy per monitor, plus a zero-size placeholder for anchored center
// modules. See BarModel.pickPanelSlot for which one a hotkey acts on.
var chosen = BarModel.pickPanelSlot(candidates, focusedScreenName())
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()
// Revealing the indicators widens their section, which can slide a neighbour
// under a stationary pointer. Collapsing on that un-hover would move it back
// out and re-open the peek, so hold until the pointer leaves the bar.
function setCenterSectionHovered(hovered) {
centerSectionHovered = hovered
if (hovered) {
centerSectionRevealTimer.stop()
centerSectionRevealHeld = true
} else {
centerSectionRevealTimer.restart()
}
}
function setBarHovered(hovered) {
barHoverCount = Math.max(0, barHoverCount + (hovered ? 1 : -1))
if (barHoverCount === 0) centerSectionRevealTimer.restart()
}
Timer {
id: centerSectionRevealTimer
interval: 120
// Collapse only. Opening the peek is the center section's own gesture, done
// in setCenterSectionHovered, so a timer left pending by a pointer that dipped
// off the bar and came back cannot reveal indicators it never pointed at.
onTriggered: if (!root.centerSectionHovered && !root.barHovered) root.centerSectionRevealHeld = false
}
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
if (sourceWindow && sourceWindow.contentItem) {
var barPoint = sourceWindow.contentItem.mapFromItem(null, scenePoint.x, scenePoint.y)
if (barPoint.x < 0 || barPoint.x > sourceWindow.contentItem.width ||
barPoint.y < 0 || barPoint.y > sourceWindow.contentItem.height)
return null
}
var candidates = []
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) {
}
candidates.push({
slot: slot,
x: slotPoint.x,
y: slotPoint.y,
width: slot.width,
height: slot.height
})
}
return BarModel.nearestDropTarget(candidates, scenePoint, root.vertical)
}
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
// Hiding parks the bar just past its screen edge instead of unmapping it.
// Unmapping frees the layer surface and the whole scene graph, so every
// reveal has to rebuild them — new surface, re-shaped glyphs, re-uploaded
// textures — which measures ~150ms against ~20ms to tear down. Parking
// keeps the surface alive, so showing is only a margin change.
visible: !remapGuard.remapping
exclusionMode: root.barHidden ? ExclusionMode.Ignore : ExclusionMode.Auto
ScreenMoveRemap {
id: remapGuard
window: barWindow
}
margins {
top: root.barHidden && root.position === "top" ? -root.barSize : 0
bottom: root.barHidden && root.position === "bottom" ? -root.barSize : 0
left: root.barHidden && root.position === "left" ? -root.barSize : 0
right: root.barHidden && root.position === "right" ? -root.barSize : 0
}
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
// A child of the loader, not a sibling of the sections: an ancestor stays
// hovered while the pointer is over a widget, where a sibling would lose
// hover to the section the pointer entered.
HoverHandler {
onHoveredChanged: root.setBarHovered(hovered)
// Unplugging a monitor destroys its bar without a leave event, which
// would strand this surface's tally and hold the peek open for good.
Component.onDestruction: if (hovered) root.setBarHovered(false)
}
}
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
}
}
Rectangle {
readonly property var targetRect: root.barDragTargetGeometry
visible: ghostWindow.active && targetRect !== null
x: targetRect ? Math.round(targetRect.x) : 0
y: targetRect ? Math.round(targetRect.y) : 0
width: targetRect ? targetRect.width : 0
height: targetRect ? targetRect.height : 0
color: Color.accent
radius: Math.min(width, height) / 2
}
}
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
// A hidden list must not build its modules. The center section declares
// both an anchored and an unanchored arrangement and shows whichever
// fits, so leaving the other one loaded mounts every center module
// twice — two IPC handlers registered for the same target, two clocks
// ticking, two of every timer and fetch behind them.
active: 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 }
}
}
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
root.barDragTargetGeometry = drop ? root.dropMarkerRect(drop.slot, drop.after) : null
}
}
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)
}
}
}