Files
omarchycn/default/quickshell/bar/shell.qml
T
Ryan Hughes af5a71347b Address reviewer findings for bar settings GUI
CRITICAL:
- Re-inject working entry into the settings form every time the dialog
  opens. Previously the Loader's onLoaded only fires when the
  sourceComponent identity changes, so reopening the dialog on the same
  widget id (or on a second instance of the same id, e.g. two spacers)
  showed stale field values.
- 'Add widget' menu and the centerAnchor dropdown both read
  draft.layout[section] without depending on the section array
  identity. Mutating the array in place did not invalidate those
  bindings, so the menu would show a stale availability list and let
  non-spacer widgets be added twice. Layout mutations now replace the
  whole  object so any binding that reads it re-evaluates.

WARNINGS:
- Save now writes only the diff against the merged defaults so future
  bar-defaults changes are not silently shadowed by a stale snapshot.
- FileView onFileChanged handlers simplified to a single reload() —
  the explicit second loader call was duplicating loadConfig and
  bumping barConfigSerial twice per change (visible flicker).
- userFile onLoaded ignores reloads while the user has an unsaved
  working draft so external edits during editing do not clobber the
  in-progress state.
- SpinBox forms switched from onValueChanged to onValueModified so the
  initial-value bind no longer fires fieldChanged.
- Hyprland window-rule sizes aligned with the FloatingWindow
  implicitWidth/Height values (720 main, 380 dialog).
- omarchy-launch-bar-settings falls back to initialTitle when focusing,
  so a dialog that has not yet had its title applied still surfaces
  the existing instance.
2026-05-14 02:21:47 -04:00

1609 lines
46 KiB
QML

import Quickshell
import Quickshell.Bluetooth
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell.Services.Pipewire
import Quickshell.Services.SystemTray
import Quickshell.Services.UPower
import Quickshell.Wayland
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
ShellRoot {
id: root
property string home: Quickshell.env("HOME")
property string omarchyPath: Quickshell.env("OMARCHY_PATH") || (home + "/.local/share/omarchy")
property string omarchyConfigDir: home + "/.config/omarchy"
property var builtinBarConfig: ({
position: "top",
fontFamily: "JetBrainsMono Nerd Font",
centerAnchor: "clock",
layout: {
left: [{ id: "omarchy" }, { id: "workspaces" }],
center: [
{ id: "clock", format: "dddd HH:mm", formatAlt: "dd MMMM 'W'ww yyyy", verticalFormat: "HH\n—\nmm" },
{ id: "weather" }, { id: "update" }, { id: "voxtype" },
{ id: "screenRecording" }, { id: "idle" }, { id: "notifications" }
],
right: [
{ id: "tray" }, { id: "bluetooth" }, { id: "network" },
{ id: "audio" }, { id: "cpu" }, { id: "battery" }
]
}
})
property var defaultBarConfig: builtinBarConfig
property var userBarConfig: ({})
property var layoutConfig: builtinBarConfig.layout
property string centerAnchor: "clock"
property int barConfigSerial: 0
property string position: "top"
property string fontFamily: "JetBrainsMono Nerd Font"
property color foreground: "#cacccc"
property color background: "#101315"
property color urgent: "#a55555"
property string weatherText: ""
property string weatherClass: ""
property bool updateAvailable: false
property string voxtypeIcon: ""
property string voxtypeClass: ""
property string screenRecordingText: ""
property string screenRecordingTooltip: ""
property string idleText: ""
property string idleTooltip: ""
property string notificationSilencingText: ""
property string notificationSilencingTooltip: ""
property int audioVolumePercent: -1
property string networkKind: "disconnected"
property string networkLabel: ""
property int networkSignal: -1
property string networkFrequency: ""
property bool clockAlt: false
property var tooltipTarget: null
property string tooltipText: ""
property bool tooltipShown: false
property var activePopout: null
function requestPopout(owner) {
if (activePopout === owner) return
if (activePopout && "closePopout" in activePopout) activePopout.closePopout()
activePopout = owner
}
function releasePopout(owner) {
if (activePopout === owner) activePopout = null
}
readonly property bool vertical: position === "left" || position === "right"
readonly property int barSize: vertical ? 28 : 26
function normalizePosition(value) {
var next = String(value || "").trim()
return /^(top|bottom|left|right)$/.test(next) ? next : "top"
}
function shellQuote(value) {
return "'" + String(value).replace(/'/g, "'\\''") + "'"
}
function fileUrl(path) {
return "file://" + path.split("/").map(encodeURIComponent).join("/")
}
function commandWithOmarchyPath(command) {
return "OMARCHY_PATH=" + shellQuote(omarchyPath) + " " + command
}
function isPlainObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value)
}
function cloneConfig(value) {
if (Array.isArray(value)) {
var arrayCopy = []
for (var i = 0; i < value.length; i++)
arrayCopy.push(cloneConfig(value[i]))
return arrayCopy
}
if (isPlainObject(value)) {
var objectCopy = {}
for (var key in value)
objectCopy[key] = cloneConfig(value[key])
return objectCopy
}
return value
}
function mergeConfig(base, override) {
var result = cloneConfig(base || {})
if (!isPlainObject(override)) return result
for (var key in override) {
if (isPlainObject(result[key]) && isPlainObject(override[key]))
result[key] = mergeConfig(result[key], override[key])
else
result[key] = cloneConfig(override[key])
}
return result
}
function parseConfig(raw, label) {
var text = String(raw || "").trim()
if (!text) return {}
try {
var parsed = JSON.parse(text)
return isPlainObject(parsed) ? parsed : {}
} catch (error) {
console.warn("Failed to parse " + label + ": " + error)
return {}
}
}
function normalizeLayoutEntry(entry) {
if (typeof entry === "string") return { id: entry }
if (isPlainObject(entry) && entry.id) return entry
return null
}
function normalizeLayoutSection(list) {
if (!Array.isArray(list)) return []
var result = []
for (var i = 0; i < list.length; i++) {
var normalized = normalizeLayoutEntry(list[i])
if (normalized) result.push(normalized)
}
return result
}
function normalizeLayout(layout) {
if (!isPlainObject(layout)) layout = builtinBarConfig.layout
return {
left: normalizeLayoutSection(layout.left),
center: normalizeLayoutSection(layout.center),
right: normalizeLayoutSection(layout.right)
}
}
function applyBarConfig() {
var config = mergeConfig(defaultBarConfig, userBarConfig)
position = normalizePosition(config.position)
fontFamily = String(config.fontFamily || "JetBrainsMono Nerd Font")
centerAnchor = String(config.centerAnchor || "clock")
layoutConfig = normalizeLayout(config.layout)
barConfigSerial++
}
function loadDefaultBarConfig(raw) {
defaultBarConfig = mergeConfig(builtinBarConfig, parseConfig(raw, "bar defaults"))
applyBarConfig()
}
function loadUserBarConfig(raw) {
userBarConfig = parseConfig(raw, "bar config")
applyBarConfig()
}
function layoutEntries(region) {
var serial = barConfigSerial
var entries = layoutConfig ? layoutConfig[region] : null
return Array.isArray(entries) ? entries : []
}
function entrySettings(entry) {
if (!isPlainObject(entry)) return {}
var copy = {}
for (var key in entry) {
if (key === "id") continue
copy[key] = entry[key]
}
return copy
}
function entryId(entry) {
if (typeof entry === "string") return entry
if (isPlainObject(entry) && entry.id) return String(entry.id)
return ""
}
function moduleString(entry, key, fallback) {
var settings = entrySettings(entry)
var value = settings[key]
return value === undefined || value === null ? fallback : String(value)
}
function entryIndex(entries, name) {
if (!Array.isArray(entries)) return -1
for (var i = 0; i < entries.length; i++) {
if (entryId(entries[i]) === name)
return i
}
return -1
}
function entriesBefore(entries, name) {
var index = entryIndex(entries, name)
return index <= 0 ? [] : entries.slice(0, index)
}
function entriesAfter(entries, name) {
var index = entryIndex(entries, name)
return index === -1 ? [] : entries.slice(index + 1)
}
function builtinModuleComponent(name) {
switch (String(name)) {
case "omarchy": return omarchyModuleComponent
case "workspaces": return workspacesModuleComponent
case "clock": return clockModuleComponent
case "weather": return weatherModuleComponent
case "update": return updateModuleComponent
case "voxtype": return voxtypeModuleComponent
case "screenRecording":
case "screenrecording": return screenRecordingModuleComponent
case "idle": return idleModuleComponent
case "notifications":
case "notificationSilencing": return notificationsModuleComponent
case "tray": return trayModuleComponent
case "bluetooth": return bluetoothModuleComponent
case "network": return networkModuleComponent
case "audio":
case "pulseaudio": return audioModuleComponent
case "cpu": return cpuModuleComponent
case "battery": return batteryModuleComponent
default: return null
}
}
function expandPath(path) {
var value = String(path || "")
if (value === "") return ""
if (value.indexOf("~/") === 0) return home + value.substring(1)
if (value.indexOf("$HOME/") === 0) return home + value.substring(5)
return value
}
function customModuleSafeName(name) {
var value = String(name || "")
return value !== "" && value.indexOf("..") === -1 && value[0] !== "/"
}
function customModuleType(entry) {
var settings = entrySettings(entry)
var type = String(settings.type || "")
if (type) return type
if (settings.exec) return "command"
if (settings.source) return "qml"
return ""
}
function customModuleSource(entry) {
var settings = entrySettings(entry)
var name = entryId(entry)
var source = settings.source ? expandPath(settings.source) : ""
if (!source && customModuleSafeName(name))
source = omarchyConfigDir + "/bar/modules/" + String(name) + ".qml"
return source ? fileUrl(source) : ""
}
readonly property var firstPartyWidgets: ({
"media": true,
"audioPanel": true,
"networkPanel": true,
"bluetoothPanel": true,
"calendar": true,
"notificationCenter": true,
"brightness": true,
"powerProfile": true,
"systemStats": true,
"weatherFlyout": true,
"workspacesPro": true,
"powerMenu": true,
"idleInhibitor": true,
"microphone": true,
"notificationCenter": true,
"activeWindow": true,
"nightLight": true,
"keyboardLayout": true,
"lockKeys": true,
"spacer": true,
"controlCenter": true
})
function firstPartyWidgetSource(name) {
if (!firstPartyWidgets[String(name)]) return ""
return fileUrl(omarchyPath + "/default/quickshell/bar/widgets/" + String(name) + ".qml")
}
function networkCommand() {
return [
"route_device=$(ip route get 1.1.1.1 2>/dev/null | awk '{ for (i = 1; i <= NF; i++) if ($i == \"dev\") { print $(i + 1); exit } }')",
"connection_name() { command -v nmcli >/dev/null 2>&1 && nmcli -t -f GENERAL.CONNECTION device show \"$1\" 2>/dev/null | awk -F: '$1 == \"GENERAL.CONNECTION\" { print $2; exit }'; }",
"wifi_details() { command -v nmcli >/dev/null 2>&1 && nmcli -t -f IN-USE,SIGNAL,FREQ device wifi list --rescan no 2>/dev/null | awk -F: '$1 == \"*\" { print $2 \"\\t\" $3; exit }'; }",
"if [[ -n $route_device && ! -d /sys/class/net/$route_device/wireless ]]; then",
" label=$(connection_name \"$route_device\"); label=${label:-$route_device}",
" printf 'ethernet\\t%s\\t\\t\\n' \"$label\"",
"elif [[ -n $route_device ]]; then",
" label=$(connection_name \"$route_device\"); label=${label:-$route_device}",
" details=$(wifi_details)",
" signal=${details%%$'\\t'*}",
" frequency=${details#*$'\\t'}",
" [[ $frequency == \"$details\" ]] && frequency=\"\"",
" printf 'wifi\\t%s\\t%s\\t%s\\n' \"$label\" \"$signal\" \"$frequency\"",
"else",
" printf 'disconnected\\t\\t\\t\\n'",
"fi"
].join("\n")
}
function run(command) {
if (!command) return
launcher.command = ["bash", "-lc", command]
launcher.startDetached()
}
function runProcess(process) {
if (!process.running)
process.running = true
}
function showTooltip(target, text) {
if (!text) return
tooltipTarget = target
tooltipText = text
tooltipShown = false
tooltipTimer.restart()
}
function hideTooltip(target) {
if (tooltipTarget !== target) return
tooltipTimer.stop()
tooltipTarget = null
tooltipText = ""
tooltipShown = false
}
function parseModuleJson(raw) {
var text = String(raw || "").trim()
if (!text) return {}
var lines = text.split("\n")
try {
return JSON.parse(lines[lines.length - 1])
} catch (error) {
return { text: text }
}
}
function loadTheme(raw) {
var lines = String(raw || "").split("\n")
for (var i = 0; i < lines.length; i++) {
var match = lines[i].match(/^\s*([A-Za-z0-9_-]+)\s*=\s*["']?(#[0-9A-Fa-f]{6})/)
if (!match) continue
if (match[1] === "foreground") foreground = match[2]
else if (match[1] === "background") background = match[2]
else if (match[1] === "red") urgent = match[2]
}
}
function updateWeather(raw) {
var data = parseModuleJson(raw)
weatherText = data.text || ""
weatherClass = data.class || ""
}
function updateIndicator(name, raw) {
var data = parseModuleJson(raw)
var text = data.text || ""
var tooltip = data.tooltip || ""
if (name === "screenRecording") {
screenRecordingText = text
screenRecordingTooltip = tooltip
} else if (name === "idle") {
idleText = text
idleTooltip = tooltip
} else if (name === "notifications") {
notificationSilencingText = text
notificationSilencingTooltip = tooltip
}
}
function updateVoxtype(raw) {
var data = parseModuleJson(raw)
var state = data.alt || data.class || "idle"
voxtypeClass = state
if (state === "recording") voxtypeIcon = "󰍬"
else if (state === "transcribing") voxtypeIcon = "󰔟"
else voxtypeIcon = ""
}
function updateNetwork(raw) {
var parts = String(raw || "disconnected\t\t\t").replace(/\r?\n+$/, "").split("\t")
networkKind = parts[0] || "disconnected"
networkLabel = parts[1] || ""
networkSignal = parts[2] ? parseInt(parts[2], 10) : -1
networkFrequency = parts[3] || ""
}
function refreshWeather() {
runProcess(weatherProc)
}
function refreshUpdate() {
runProcess(updateProc)
}
function refreshScreenRecording() {
runProcess(screenRecordingProc)
}
function refreshIndicators() {
refreshScreenRecording()
runProcess(idleProc)
runProcess(notificationSilencingProc)
}
function refreshNetwork() {
runProcess(networkProc)
}
function updateAudioVolume(raw) {
var volume = parseInt(String(raw || "").trim(), 10)
audioVolumePercent = isNaN(volume) ? -1 : Math.max(0, volume)
}
function refreshAudio() {
runProcess(audioProc)
}
function workspaceById(id) {
var values = Hyprland.workspaces.values
for (var i = 0; i < values.length; i++) {
if (values[i].id === id)
return values[i]
}
return null
}
function workspaceIds() {
var ids = [1, 2, 3, 4, 5]
var values = Hyprland.workspaces.values
for (var i = 0; i < values.length; i++) {
var id = values[i].id
if (id > 0 && id <= 10 && ids.indexOf(id) === -1)
ids.push(id)
}
ids.sort(function(left, right) { return left - right })
return ids
}
function activeTrayItemCount() {
var count = 0
var values = SystemTray.items.values
for (var i = 0; i < values.length; i++) {
if (values[i].status !== Status.Passive)
count++
}
return count
}
function batteryIcon() {
var device = UPower.displayDevice
if (!device || !device.isPresent) return ""
var chargingIcons = ["󰢜", "󰂆", "󰂇", "󰂈", "󰢝", "󰂉", "󰢞", "󰂊", "󰂋", "󰂅"]
var defaultIcons = ["󰁺", "󰁻", "󰁼", "󰁽", "󰁾", "󰁿", "󰂀", "󰂁", "󰂂", "󰁹"]
var index = Math.max(0, Math.min(9, Math.floor(device.percentage / 10)))
if (device.state === UPowerDeviceState.FullyCharged) return "󰂅"
if (!UPower.onBattery && device.state !== UPowerDeviceState.Charging) return ""
if (device.state === UPowerDeviceState.Charging) return chargingIcons[index]
return defaultIcons[index]
}
function audioIcon() {
var sink = Pipewire.defaultAudioSink
if (!sink || !sink.audio) return ""
if (sink.audio.muted) return ""
var props = sink.properties || {}
var sinkText = String([
sink.name,
sink.description,
sink.nickname,
props["device.icon-name"],
props["device.product.name"],
props["node.name"]
].join(" ")).toLowerCase()
if (sinkText.indexOf("headphone") !== -1 || sinkText.indexOf("headset") !== -1)
return ""
var volume = sink.audio.volume
if (!isFinite(volume) && audioVolumePercent >= 0)
volume = audioVolumePercent / 100
if (!isFinite(volume)) return ""
if (volume < 0.34) return ""
if (volume < 0.67) return ""
return ""
}
function bluetoothIcon() {
var adapter = Bluetooth.defaultAdapter
if (!adapter) return ""
if (!adapter.enabled) return "󰂲"
var devices = Bluetooth.devices.values
for (var i = 0; i < devices.length; i++) {
if (devices[i].connected)
return "󰂱"
}
return ""
}
function networkIcon() {
if (networkKind === "wifi") {
var icons = ["󰤯", "󰤟", "󰤢", "󰤥", "󰤨"]
var index = Math.max(0, Math.min(4, Math.ceil(networkSignal / 20) - 1))
return icons[index]
}
if (networkKind === "ethernet") return "󰀂"
return "󰤮"
}
function networkTooltip() {
if (networkKind === "wifi") {
var frequency = parseFloat(networkFrequency)
var frequencyText = frequency > 0 ? " (" + (frequency / 1000).toFixed(1) + " GHz)" : ""
return (networkLabel || "Wi-Fi") + frequencyText
}
if (networkKind === "ethernet") return "Connected"
return "Disconnected"
}
function audioTooltip() {
var sink = Pipewire.defaultAudioSink
if (sink && sink.audio) {
var volume = sink.audio.volume
if (isFinite(volume))
return "Playing at " + Math.round(volume * 100) + "%"
}
if (audioVolumePercent >= 0)
return "Playing at " + audioVolumePercent + "%"
return "Audio"
}
function bluetoothTooltip() {
var count = 0
var devices = Bluetooth.devices.values
for (var i = 0; i < devices.length; i++) {
if (devices[i].connected)
count++
}
return "Devices connected: " + count
}
function batteryTooltip() {
var device = UPower.displayDevice
if (!device || !device.isPresent) return ""
var direction = UPower.onBattery ? "↓" : "↑"
return Math.round(device.changeRate) + "W" + direction + " " + Math.round(device.percentage) + "%"
}
function trayIconSource(icon) {
var value = String(icon || "")
var marker = "?path="
var markerIndex = value.indexOf(marker)
if (markerIndex === -1) return value
var name = value.substring(0, markerIndex).split("/").pop()
var iconPath = value.substring(markerIndex + marker.length).split("&")[0]
return fileUrl(iconPath + "/hicolor/16x16/status/" + name + ".png")
}
function trayTooltip(item) {
return item.tooltipTitle || item.title || item.id || ""
}
function focusWorkspace(id) {
root.run("hyprctl dispatch " + shellQuote("hl.dsp.focus({ workspace = \"" + id + "\" })"))
}
function clockEntry() {
var serial = barConfigSerial
var sections = ["left", "center", "right"]
for (var i = 0; i < sections.length; i++) {
var entries = layoutEntries(sections[i])
var idx = entryIndex(entries, "clock")
if (idx !== -1) return entries[idx]
}
return null
}
function clockText() {
var entry = clockEntry()
if (clockAlt)
return Qt.formatDateTime(systemClock.date, moduleString(entry, "formatAlt", "dd MMMM 'W'ww yyyy"))
if (vertical)
return Qt.formatDateTime(systemClock.date, moduleString(entry, "verticalFormat", "HH\n—\nmm"))
return Qt.formatDateTime(systemClock.date, moduleString(entry, "format", "dddd HH:mm"))
}
SystemClock {
id: systemClock
precision: SystemClock.Minutes
}
Process { id: launcher }
Timer {
id: tooltipTimer
interval: 400
onTriggered: root.tooltipShown = true
}
FileView {
path: root.omarchyPath + "/default/quickshell/bar/bar-defaults.json"
watchChanges: true
printErrors: false
onLoaded: root.loadDefaultBarConfig(text())
onFileChanged: reload()
}
FileView {
path: root.omarchyConfigDir + "/bar.json"
watchChanges: true
printErrors: false
onLoaded: root.loadUserBarConfig(text())
onFileChanged: reload()
}
FileView {
path: root.home + "/.config/omarchy/current/theme/colors.toml"
watchChanges: true
printErrors: false
onLoaded: root.loadTheme(text())
onFileChanged: reload()
}
Process {
id: weatherProc
command: ["bash", "-lc", root.commandWithOmarchyPath(root.shellQuote(root.omarchyPath + "/default/waybar/weather.sh"))]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.updateWeather(text)
}
}
Timer {
interval: 60000
running: true
repeat: true
triggeredOnStart: true
onTriggered: root.refreshWeather()
}
Process {
id: updateProc
command: ["bash", "-lc", root.commandWithOmarchyPath("omarchy-update-available")]
stdout: StdioCollector { waitForEnd: true }
onExited: function(exitCode) {
root.updateAvailable = exitCode === 0
}
}
Timer {
interval: 21600000
running: true
repeat: true
triggeredOnStart: true
onTriggered: root.refreshUpdate()
}
Process {
id: voxtypeProc
command: ["bash", "-lc", "omarchy-voxtype-status"]
running: true
stdout: SplitParser {
onRead: function(data) {
root.updateVoxtype(data)
}
}
}
Process {
id: screenRecordingProc
command: ["bash", "-lc", root.commandWithOmarchyPath(root.shellQuote(root.omarchyPath + "/default/waybar/indicators/screen-recording.sh"))]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.updateIndicator("screenRecording", text)
}
}
Process {
id: idleProc
command: ["bash", "-lc", root.commandWithOmarchyPath(root.shellQuote(root.omarchyPath + "/default/waybar/indicators/idle.sh"))]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.updateIndicator("idle", text)
}
}
Process {
id: notificationSilencingProc
command: ["bash", "-lc", root.commandWithOmarchyPath(root.shellQuote(root.omarchyPath + "/default/waybar/indicators/notification-silencing.sh"))]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.updateIndicator("notifications", text)
}
}
Timer {
interval: 2000
running: true
repeat: true
triggeredOnStart: true
onTriggered: root.refreshIndicators()
}
Process {
id: networkProc
command: ["bash", "-lc", root.commandWithOmarchyPath(root.networkCommand())]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.updateNetwork(text)
}
}
Timer {
interval: 3000
running: true
repeat: true
triggeredOnStart: true
onTriggered: root.refreshNetwork()
}
Process {
id: audioProc
command: ["bash", "-lc", "if command -v pamixer >/dev/null; then pamixer --get-volume; elif command -v wpctl >/dev/null; then wpctl get-volume @DEFAULT_AUDIO_SINK@ | awk '{ print int($2 * 100) }'; fi"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.updateAudioVolume(text)
}
}
Timer {
interval: 2000
running: true
repeat: true
triggeredOnStart: true
onTriggered: root.refreshAudio()
}
IpcHandler {
target: "bar"
function refreshUpdate(): void {
root.refreshUpdate()
}
function refreshScreenRecording(): void {
root.refreshScreenRecording()
}
function refreshIndicators(): void {
root.refreshIndicators()
}
}
Variants {
model: Quickshell.screens
delegate: Component {
BarPanel {
required property var modelData
screen: modelData
}
}
}
component BarPanel: PanelWindow {
id: barWindow
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.background
WlrLayershell.namespace: "omarchy-bar"
WlrLayershell.layer: WlrLayer.Top
Loader {
anchors.fill: parent
sourceComponent: root.vertical ? verticalBar : horizontalBar
}
PopupWindow {
id: tooltipWindow
visible: root.tooltipShown && root.tooltipTarget !== null && root.tooltipText !== ""
color: "transparent"
implicitWidth: tooltipBubble.implicitWidth
implicitHeight: 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 (!target) return
var popupWidth = tooltipWindow.implicitWidth
var popupHeight = tooltipWindow.implicitHeight
var localX = target.width / 2 - popupWidth / 2
var localY = target.height + 6
if (root.position === "bottom") {
localY = -popupHeight - 6
} else if (root.position === "left") {
localX = target.width + 6
localY = target.height / 2 - popupHeight / 2
} else if (root.position === "right") {
localX = -popupWidth - 6
localY = target.height / 2 - popupHeight / 2
}
var point = barWindow.contentItem.mapFromItem(target, localX, localY)
tooltipAnchor.rect.x = Math.round(point.x)
tooltipAnchor.rect.y = Math.round(point.y)
}
}
Rectangle {
id: tooltipBubble
implicitWidth: tooltipLabel.implicitWidth + 20
implicitHeight: tooltipLabel.implicitHeight + 14
color: root.background
border.color: root.foreground
border.width: 1
radius: 0
opacity: 0.97
Text {
id: tooltipLabel
anchors.centerIn: parent
text: root.tooltipText
color: root.foreground
font.family: root.fontFamily
font.pixelSize: 12
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
Component {
id: horizontalBar
Item {
anchors.fill: parent
CenterModules { anchors.fill: parent }
LeftModules {
anchors.left: parent.left
anchors.leftMargin: 8
anchors.verticalCenter: parent.verticalCenter
}
RightModules {
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
}
}
}
Component {
id: verticalBar
Item {
anchors.fill: parent
CenterModules { anchors.fill: parent }
LeftModules {
anchors.top: parent.top
anchors.topMargin: 8
anchors.horizontalCenter: parent.horizontalCenter
}
RightModules {
anchors.bottom: parent.bottom
anchors.bottomMargin: 8
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
Component { id: emptyModuleComponent; Item { implicitWidth: 0; implicitHeight: 0; visible: false } }
Component { id: omarchyModuleComponent; OmarchyModule {} }
Component { id: workspacesModuleComponent; WorkspacesModule {} }
Component { id: clockModuleComponent; ClockModule {} }
Component { id: weatherModuleComponent; WeatherModule {} }
Component { id: updateModuleComponent; UpdateModule {} }
Component { id: voxtypeModuleComponent; VoxtypeModule {} }
Component { id: screenRecordingModuleComponent; ScreenRecordingModule {} }
Component { id: idleModuleComponent; IdleModule {} }
Component { id: notificationsModuleComponent; NotificationsModule {} }
Component { id: trayModuleComponent; TrayModule {} }
Component { id: bluetoothModuleComponent; BluetoothModule {} }
Component { id: networkModuleComponent; NetworkModule {} }
Component { id: audioModuleComponent; AudioModule {} }
Component { id: cpuModuleComponent; CpuModule {} }
Component { id: batteryModuleComponent; BatteryModule {} }
function findCenterAnchorEntry() {
var entries = root.layoutEntries("center")
var idx = root.entryIndex(entries, root.centerAnchor)
return idx === -1 ? null : entries[idx]
}
component LeftModules: ModuleList {
entries: root.layoutEntries("left")
}
component RightModules: ModuleList {
entries: root.layoutEntries("right")
}
component CenterModules: Item {
id: centerRoot
property var entries: root.layoutEntries("center")
readonly property bool hasAnchor: root.entryIndex(entries, root.centerAnchor) !== -1
readonly property var anchorEntry: root.findCenterAnchorEntry()
Loader {
anchors.fill: parent
sourceComponent: root.vertical ? verticalCenterModules : horizontalCenterModules
}
Component {
id: horizontalCenterModules
Item {
anchors.fill: parent
ModuleList {
visible: !centerRoot.hasAnchor
entries: centerRoot.entries
anchors.centerIn: parent
}
ModuleList {
visible: centerRoot.hasAnchor
entries: root.entriesBefore(centerRoot.entries, root.centerAnchor)
anchors.right: centerAnchorModule.left
anchors.verticalCenter: centerAnchorModule.verticalCenter
}
ModuleSlot {
id: centerAnchorModule
visible: centerRoot.hasAnchor
entry: centerRoot.anchorEntry
anchors.centerIn: parent
}
ModuleList {
visible: centerRoot.hasAnchor
entries: root.entriesAfter(centerRoot.entries, root.centerAnchor)
anchors.left: centerAnchorModule.right
anchors.verticalCenter: centerAnchorModule.verticalCenter
}
}
}
Component {
id: verticalCenterModules
Item {
anchors.fill: parent
ModuleList {
visible: !centerRoot.hasAnchor
entries: centerRoot.entries
anchors.centerIn: parent
}
ModuleList {
visible: centerRoot.hasAnchor
entries: root.entriesBefore(centerRoot.entries, root.centerAnchor)
anchors.bottom: centerAnchorModule.top
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
}
ModuleSlot {
id: centerAnchorModule
visible: centerRoot.hasAnchor
entry: centerRoot.anchorEntry
anchors.centerIn: parent
}
ModuleList {
visible: centerRoot.hasAnchor
entries: root.entriesAfter(centerRoot.entries, root.centerAnchor)
anchors.top: centerAnchorModule.bottom
anchors.horizontalCenter: centerAnchorModule.horizontalCenter
}
}
}
}
component ModuleList: Loader {
id: moduleListRoot
property var entries: []
visible: entries.length > 0
sourceComponent: root.vertical ? verticalModuleList : horizontalModuleList
width: item ? item.implicitWidth : 0
height: item ? item.implicitHeight : 0
Component {
id: horizontalModuleList
Row {
spacing: 0
Repeater {
model: moduleListRoot.entries
ModuleSlot {
required property var modelData
entry: modelData
}
}
}
}
Component {
id: verticalModuleList
Column {
spacing: 0
Repeater {
model: moduleListRoot.entries
ModuleSlot {
required property var modelData
entry: modelData
}
}
}
}
}
component ModuleSlot: Item {
id: slot
required property var entry
readonly property string moduleName: root.entryId(entry)
readonly property var moduleSettings: root.entrySettings(entry)
readonly property string customType: root.customModuleType(entry)
readonly property var builtinComponent: customType ? null : root.builtinModuleComponent(moduleName)
readonly property string firstPartySource: customType || builtinComponent ? "" : root.firstPartyWidgetSource(moduleName)
readonly property bool qmlCustom: customType === "qml"
readonly property bool commandCustom: customType === "command"
readonly property bool firstParty: firstPartySource !== ""
readonly property var activeItem: qmlCustom || firstParty ? qmlLoader.item : componentLoader.item
implicitWidth: activeItem && activeItem.visible ? activeItem.implicitWidth : 0
implicitHeight: activeItem && activeItem.visible ? activeItem.implicitHeight : 0
width: implicitWidth
height: implicitHeight
Loader {
id: componentLoader
active: !slot.qmlCustom && !slot.firstParty
sourceComponent: slot.builtinComponent || (slot.commandCustom ? customCommandModuleComponent : emptyModuleComponent)
anchors.fill: parent
}
Loader {
id: qmlLoader
active: slot.qmlCustom || slot.firstParty
source: slot.qmlCustom ? root.customModuleSource(slot.entry) : (slot.firstParty ? slot.firstPartySource : "")
anchors.fill: parent
onLoaded: slot.injectProps()
}
onModuleSettingsChanged: injectProps()
function injectProps() {
var target = qmlLoader.item
if (!target) return
if ("bar" in target) target.bar = root
if ("moduleName" in target) target.moduleName = moduleName
if ("settings" in target) target.settings = moduleSettings
}
Component {
id: customCommandModuleComponent
CustomCommandModule { entry: slot.entry }
}
}
component CustomCommandModule: ModuleButton {
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 = root.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)
}
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", root.commandWithOmarchyPath(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)
}
}
component ModuleButton: Item {
id: buttonRoot
property string text: ""
property bool active: false
property bool keepSpace: false
property string fontFamily: root.fontFamily
property real fontSize: 12
property real horizontalMargin: 7.5
property real verticalPadding: 6
property real textRotation: 0
property real fixedWidth: -1
property real fixedHeight: -1
property string tooltipText: ""
signal pressed(int button)
signal wheelMoved(int delta)
visible: text !== "" || keepSpace
opacity: text === "" ? 0 : 1
implicitWidth: fixedWidth > 0 ? fixedWidth : (root.vertical ? root.barSize : Math.max(12, label.implicitWidth + horizontalMargin * 2))
implicitHeight: fixedHeight > 0 ? fixedHeight : (root.vertical ? Math.max(12, label.implicitHeight + verticalPadding * 2) : root.barSize)
Text {
id: label
anchors.centerIn: parent
text: buttonRoot.text
color: buttonRoot.active ? root.urgent : root.foreground
font.family: buttonRoot.fontFamily
font.pixelSize: buttonRoot.fontSize
rotation: buttonRoot.textRotation
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true
onEntered: root.showTooltip(buttonRoot, buttonRoot.tooltipText)
onExited: root.hideTooltip(buttonRoot)
onClicked: function(mouse) { buttonRoot.pressed(mouse.button) }
onWheel: function(wheel) { buttonRoot.wheelMoved(wheel.angleDelta.y) }
}
}
component IndicatorModule: ModuleButton {
fontSize: 10
horizontalMargin: 5
verticalPadding: 5
}
component OmarchyModule: ModuleButton {
text: "\ue900"
fontFamily: "omarchy"
horizontalMargin: 7.5
tooltipText: "Omarchy Menu\n\nSuper + Alt + Space"
onPressed: function(button) {
if (button === Qt.RightButton) root.run("xdg-terminal-exec")
else root.run("env -u OMARCHY_PATH " + root.shellQuote(root.home + "/.local/share/omarchy/bin/omarchy-menu"))
}
}
component WorkspacesModule: GridLayout {
columns: root.vertical ? 1 : root.workspaceIds().length
columnSpacing: root.vertical ? 0 : 3
rowSpacing: root.vertical ? 3 : 0
Repeater {
model: root.workspaceIds()
ModuleButton {
required property int modelData
property var workspace: root.workspaceById(modelData)
property bool occupied: workspace !== null && workspace.toplevels.values.length > 0
property bool focused: Hyprland.focusedWorkspace !== null && Hyprland.focusedWorkspace.id === modelData
text: focused ? "󱓻" : (modelData === 10 ? "0" : String(modelData))
opacity: occupied || modelData <= 5 || focused ? 1 : 0.5
horizontalMargin: 6
verticalPadding: 6
fixedWidth: root.vertical ? root.barSize : 24
fixedHeight: root.barSize
onPressed: function() { root.focusWorkspace(modelData) }
}
}
}
component ClockModule: ModuleButton {
text: root.clockText()
horizontalMargin: 8.75
verticalPadding: 8.75
onPressed: function(button) {
if (button === Qt.RightButton) root.run("omarchy-launch-floating-terminal-with-presentation omarchy-tz-select")
else root.clockAlt = !root.clockAlt
}
}
component WeatherModule: ModuleButton {
text: root.weatherText
active: root.weatherClass === "active"
keepSpace: false
horizontalMargin: 7.5
onPressed: function() { root.run("omarchy-notification-send \"$(omarchy-weather-status)\"") }
}
component UpdateModule: ModuleButton {
text: root.updateAvailable ? "" : ""
fontSize: 10
tooltipText: root.updateAvailable ? "Omarchy update available" : ""
onPressed: function() { root.run("omarchy-launch-floating-terminal-with-presentation omarchy-update") }
}
component VoxtypeModule: ModuleButton {
text: root.voxtypeIcon
active: root.voxtypeClass === "recording"
tooltipText: root.voxtypeClass
onPressed: function(button) {
if (button === Qt.RightButton) root.run("omarchy-voxtype-config")
else root.run("omarchy-voxtype-model")
}
}
component ScreenRecordingModule: IndicatorModule {
text: root.screenRecordingText
active: root.screenRecordingText !== ""
tooltipText: root.screenRecordingTooltip
onPressed: function() { root.run("omarchy-capture-screenrecording") }
}
component IdleModule: IndicatorModule {
text: root.idleText
active: root.idleText !== ""
tooltipText: root.idleTooltip
onPressed: function() { root.run("omarchy-toggle-idle") }
}
component NotificationsModule: IndicatorModule {
text: root.notificationSilencingText
active: root.notificationSilencingText !== ""
tooltipText: root.notificationSilencingTooltip
onPressed: function() { root.run("omarchy-toggle-notification-silencing") }
}
component TrayModule: Item {
id: trayRoot
property bool expanded: false
readonly property int activeItems: root.activeTrayItemCount()
readonly property int drawerExtent: activeItems > 0 ? activeItems * 16 + (activeItems - 1) * 17 : 0
// Match Waybar's group/tray-expander drawer transition-duration.
readonly property int animationDuration: 600
property real revealProgress: expanded ? 1 : 0
readonly property real revealExtent: drawerExtent * revealProgress
Behavior on revealProgress {
NumberAnimation { duration: trayRoot.animationDuration; easing.type: Easing.OutCubic }
}
containmentMask: QtObject {
function contains(point: point): bool {
if (root.vertical)
return point.x >= 0 && point.x <= trayRoot.width && point.y >= trayRoot.drawerExtent - trayRoot.revealExtent && point.y <= trayRoot.height
return point.x >= trayRoot.drawerExtent - trayRoot.revealExtent && point.x <= trayRoot.width && point.y >= 0 && point.y <= trayRoot.height
}
}
visible: activeItems > 0
clip: false
implicitWidth: root.vertical ? root.barSize : trayContent.implicitWidth
implicitHeight: root.vertical ? trayContent.implicitHeight : root.barSize
Loader {
id: trayContent
anchors.fill: parent
sourceComponent: root.vertical ? verticalTray : horizontalTray
}
HoverHandler {
onHoveredChanged: trayRoot.expanded = hovered
}
Component {
id: horizontalTray
Item {
id: horizontalTrayRoot
implicitWidth: expandIcon.implicitWidth + trayRoot.drawerExtent
implicitHeight: root.barSize
ModuleButton {
id: expandIcon
width: implicitWidth
height: implicitHeight
x: trayRoot.drawerExtent - trayRoot.revealExtent
text: ""
horizontalMargin: 9
verticalPadding: 6
}
Item {
id: trayClip
x: expandIcon.width
anchors.verticalCenter: parent.verticalCenter
width: trayRoot.drawerExtent
height: root.barSize
clip: true
Row {
id: trayIcons
x: trayRoot.drawerExtent - trayRoot.revealExtent
anchors.verticalCenter: parent.verticalCenter
spacing: 17
layer.enabled: true
Repeater {
model: SystemTray.items
TrayItem {}
}
}
}
}
}
Component {
id: verticalTray
Item {
id: verticalTrayRoot
implicitWidth: root.barSize
implicitHeight: expandIcon.implicitHeight + trayRoot.drawerExtent
ModuleButton {
id: expandIcon
width: implicitWidth
height: implicitHeight
y: trayRoot.drawerExtent - trayRoot.revealExtent
text: ""
textRotation: 90
horizontalMargin: 9
verticalPadding: 6
}
Item {
id: trayClip
y: expandIcon.height
anchors.horizontalCenter: parent.horizontalCenter
width: root.barSize
height: trayRoot.drawerExtent
clip: true
Column {
id: trayIcons
y: trayRoot.drawerExtent - trayRoot.revealExtent
anchors.horizontalCenter: parent.horizontalCenter
spacing: 17
layer.enabled: true
Repeater {
model: SystemTray.items
TrayItem {}
}
}
}
}
}
}
component TrayItem: Item {
id: trayItemRoot
required property var modelData
visible: modelData.status !== Status.Passive
implicitWidth: visible ? 16 : 0
implicitHeight: visible ? 16 : 0
IconImage {
anchors.centerIn: parent
implicitSize: 12
width: 12
height: 12
source: root.trayIconSource(trayItemRoot.modelData.icon)
}
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true
onEntered: root.showTooltip(trayItemRoot, root.trayTooltip(modelData))
onExited: root.hideTooltip(trayItemRoot)
onClicked: function(mouse) {
if (mouse.button === Qt.RightButton && trayItemRoot.modelData.hasMenu) {
var point = trayItemRoot.QsWindow.contentItem.mapFromItem(trayItemRoot, mouse.x, mouse.y)
trayItemRoot.modelData.display(trayItemRoot.QsWindow.window, point.x, point.y)
} else if (mouse.button === Qt.MiddleButton) {
trayItemRoot.modelData.secondaryActivate()
} else {
trayItemRoot.modelData.activate()
}
}
onWheel: function(wheel) {
trayItemRoot.modelData.scroll(wheel.angleDelta.y, false)
}
}
}
component BluetoothModule: ModuleButton {
text: root.bluetoothIcon()
horizontalMargin: 8.5
visible: Bluetooth.defaultAdapter !== null
tooltipText: root.bluetoothTooltip()
onPressed: function() { root.run("omarchy-launch-bluetooth") }
}
component NetworkModule: ModuleButton {
text: root.networkIcon()
horizontalMargin: 6.5
tooltipText: root.networkTooltip()
onPressed: function() { root.run("OMARCHY_PATH=" + root.shellQuote(root.omarchyPath) + " PATH=" + root.shellQuote(root.omarchyPath + "/bin:" + (Quickshell.env("PATH") || "")) + " " + root.shellQuote(root.omarchyPath + "/bin/omarchy-launch-wifi")) }
}
component AudioModule: ModuleButton {
text: root.audioIcon()
tooltipText: root.audioTooltip()
onPressed: function(button) {
if (button === Qt.RightButton) root.run("pamixer -t")
else root.run("omarchy-launch-audio")
}
onWheelMoved: function(delta) {
if (delta > 0) root.run("pamixer -i 5")
else if (delta < 0) root.run("pamixer -d 5")
root.refreshAudio()
}
}
component CpuModule: ModuleButton {
text: "󰍛"
onPressed: function(button) {
if (button === Qt.RightButton) root.run("alacritty")
else root.run("omarchy-launch-or-focus-tui btop")
}
}
component BatteryModule: ModuleButton {
property var device: UPower.displayDevice
text: root.batteryIcon()
visible: device !== null && device.isPresent && device.percentage > 0
active: device !== null && device.percentage <= 20 && UPower.onBattery
tooltipText: root.batteryTooltip()
onPressed: function(button) {
if (button === Qt.RightButton) root.run("omarchy-notification-send \"$(omarchy-battery-status)\"")
else root.run("omarchy-menu power")
}
}
}