Files
arthur-os/default/quickshell/omarchy-shell/plugins/bar/Bar.qml
T
Ryan Hughes 4a665af581 Drop bin/omarchy-menu; sweep JSONC actions to bare commands
The shell now owns the menu — both data and dispatch. The 821-line bash
bin was carrying the open path (cold spawn + jq pipeline + tempfile dance
+ IPC + poll) and a grab-bag of helpers that only existed inside it. Both
go away in this commit.

New bins for the few composite helpers that were genuinely worth keeping:

  - omarchy-install-app NAME PKG
  - omarchy-install-and-launch NAME PKG DESKTOP_ID
  - omarchy-install-font LABEL PKG FAMILY
  - omarchy-launch-config-editor PATH
  - omarchy-reminder-set-interactive
  - omarchy-capture-screenrecording-with-webcam

Sweep through default/omarchy/omarchy-menu.jsonc rewrites:

  - present_terminal X   -> omarchy-launch-floating-terminal-with-presentation X
  - install_terminal X   -> omarchy-launch-floating-terminal-with-presentation 'omarchy-install-terminal X'
  - install / install_and_launch / install_font / open_in_editor -> new bins above
  - terminal X           -> xdg-terminal-exec --app-id=org.omarchy.terminal X
  - default_browser_is X -> [[ "$(omarchy-default-browser)" == "X" ]]
  - default_terminal_is X / default_editor_is X / haptic_touchpad_is X same shape
  - $(hypr_config_file X) -> ~/.config/hypr/X.lua
  - show_custom_reminder_input -> omarchy-reminder-set-interactive
  - screenrecord_with_webcam   -> omarchy-capture-screenrecording-with-webcam
  - stop_active_screenrecording -> omarchy-capture-screenrecording --stop-recording

Hyprland bindings switch from `omarchy-menu X` to
`omarchy-shell-ipc menu summon X` (the keybind hot path now skips bash
entirely). The Bar.qml omarchy widget and the battery right-click do the
same. ALT+PRINT becomes a one-liner: stop the recording if one is going,
otherwise summon the screenrecord submenu.

Measured: keybind-to-visible is ~30ms (was ~235ms). The shell's plugin
keepLoaded:true stops being theoretical \u2014 the menu data lives in memory
across opens, and the only work between keypress and paint is the IPC
roundtrip and the layer-shell window mount.
2026-05-14 17:43:12 -04:00

1951 lines
63 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
import "common" as BarCommon
Item {
id: root
// The omarchy-shell host injects omarchyPath when it instantiates this Bar.
// Default fallback keeps the file loadable in isolation (e.g. for QML tooling).
required property string omarchyPath
// Injected by the host shell. Shared with the settings panel so both
// see the same widget catalogue.
required property var barWidgetRegistry
// Injected by the host shell every time shell.json is reloaded. Holds the
// `bar:` subtree: position, centerAnchor, layout. The host owns file IO;
// the bar just renders whatever it's handed. The bar font follows the
// OS-level fontconfig monospace binding — it is not stored in shell.json.
required property var barConfig
// Injected by the host shell so the bar can detect Noctalia-compat plugins
// and look up manifests when wiring per-widget Noctalia pluginApi.
property var pluginRegistry: null
// Injected by the host shell. Used so Noctalia plugins that reach for
// shell-wide APIs (openPanel, currentScreen) can do so via pluginApi.
property var shell: null
// Mirrors the on-disk `bar-off` flag so the user can hide the bar without
// killing the entire shell. Wired to BarPanel.visible below; updated by the
// FileView watcher further down.
property bool barHidden: false
property string home: Quickshell.env("HOME")
property string omarchyConfigDir: home + "/.config/omarchy"
property var fallbackBarConfig: ({
position: "top",
centerAnchor: "calendar",
layout: { left: [], center: [], right: [] }
})
property var layoutConfig: fallbackBarConfig.layout
property string centerAnchor: ""
property int barConfigSerial: 0
property string position: "top"
// "monospace" resolves through fontconfig at paint time, so changing the
// system font (via `omarchy-font-set`) updates the bar without a reload.
property string fontFamily: "monospace"
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
}
// -------------------------------------------------- Noctalia compat helpers
//
// The shell owns pluginApi creation and Main.qml service instantiation now;
// bar widgets just look up their api by moduleName. Keeping section/index
// helpers here because they're needed for Noctalia bar-widget injection
// (widgetId, section, sectionWidgetIndex, sectionWidgetsCount).
function sectionOfEntry(entry) {
var sections = ["left", "center", "right"]
for (var s = 0; s < sections.length; s++) {
var list = layoutConfig[sections[s]] || []
for (var i = 0; i < list.length; i++) {
if (list[i] === entry) return sections[s]
}
}
return ""
}
function indexOfEntry(entry) {
var section = sectionOfEntry(entry)
var list = section ? (layoutConfig[section] || []) : []
for (var i = 0; i < list.length; i++) if (list[i] === entry) return i
return -1
}
function entriesOfSection(section) {
return Array.isArray(layoutConfig[section]) ? layoutConfig[section] : []
}
function noctaliaPluginApiFor(moduleName) {
if (!shell || typeof shell.noctaliaPluginApiFor !== "function") return null
return shell.noctaliaPluginApiFor(moduleName)
}
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 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 = fallbackBarConfig.layout
return {
left: pinTrayToInner(normalizeLayoutSection(layout.left), "left"),
center: pinTrayToInner(normalizeLayoutSection(layout.center), "center"),
right: pinTrayToInner(normalizeLayoutSection(layout.right), "right")
}
}
// The tray drawer reveals inward (away from the bar edge). Place it at the
// section's inner edge: start of the right section, end of the left/center
// sections. The drawer's reserved space then sits next to the bar center,
// not stranded mid-section.
function pinTrayToInner(entries, section) {
var trayEntry = null
var result = []
for (var i = 0; i < entries.length; i++) {
if (entryId(entries[i]) === "tray") trayEntry = entries[i]
else result.push(entries[i])
}
if (trayEntry) {
if (section === "right") result.unshift(trayEntry)
else result.push(trayEntry)
}
return result
}
function applyBarConfig() {
var config = isPlainObject(barConfig) ? barConfig : fallbackBarConfig
position = normalizePosition(config.position)
centerAnchor = String(config.centerAnchor || "")
layoutConfig = normalizeLayout(config.layout)
barConfigSerial++
}
onBarConfigChanged: applyBarConfig()
function layoutEntries(region) {
var serial = barConfigSerial
var entries = layoutConfig ? layoutConfig[region] : null
return Array.isArray(entries) ? entries : []
}
function entrySettings(entry) {
if (!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) : ""
}
// First-party widgets are registered with the BarWidgetRegistry at startup.
// Each entry maps a widget id to its display metadata; the QML source lives
// at widgets/<id>.qml and is loaded asynchronously via Qt.createComponent.
readonly property var firstPartyWidgetMetadata: ({
"media": { displayName: "Media", description: "MPRIS now-playing with playback controls", category: "Media", allowMultiple: false },
"audioPanel": { displayName: "Audio", description: "Volume slider, output picker, per-app mixer", category: "Audio", allowMultiple: false },
"networkPanel": { displayName: "Network", description: "Wi-Fi list and connection state", category: "Network", allowMultiple: false },
"bluetoothPanel": { displayName: "Bluetooth", description: "Bluetooth device list with connect/disconnect", category: "Network", allowMultiple: false },
"calendar": { displayName: "Calendar", description: "Clock with month-grid popup", category: "Time", allowMultiple: false, settingsForm: "calendarSettings" },
"notificationCenter": { displayName: "Notification center", description: "Recent notifications + DND", category: "Status", allowMultiple: false },
"systemStats": { displayName: "System stats", description: "CPU icon — hover for graphs, click to open btop", category: "System", allowMultiple: false },
"weatherFlyout": { displayName: "Weather", description: "Weather pill with detail popup", category: "Info", allowMultiple: false },
"idleInhibitor": { displayName: "Keep awake", description: "Toggle idle inhibitor", category: "System", allowMultiple: false },
"microphone": { displayName: "Microphone", description: "Mic input state and mute toggle", category: "Audio", allowMultiple: false },
"activeWindow": { displayName: "Active window", description: "Title of the focused window", category: "Compositor", allowMultiple: false },
"keyboardLayout": { displayName: "Keyboard layout", description: "Current xkb layout, click cycles", category: "Compositor", allowMultiple: false },
"lockKeys": { displayName: "Lock keys", description: "Caps / Num / Scroll lock indicators", category: "System", allowMultiple: false },
"spacer": { displayName: "Spacer", description: "Configurable blank space", category: "Layout", allowMultiple: true, settingsForm: "spacerSettings" },
"controlCenter": { displayName: "Quick settings", description: "Volume / brightness / DND in one popup", category: "System", allowMultiple: false }
})
property var registeredFirstPartyComponents: ({})
Component.onCompleted: {
registerFirstPartyWidgets()
applyBarConfig()
}
function registerFirstPartyWidgets() {
var ids = Object.keys(firstPartyWidgetMetadata)
for (var i = 0; i < ids.length; i++) {
var id = ids[i]
if (barWidgetRegistry.has(id)) continue
registerOneFirstPartyWidget(id)
}
}
function registerOneFirstPartyWidget(id) {
var url = Qt.resolvedUrl("widgets/" + id + ".qml")
var meta = firstPartyWidgetMetadata[id] || {}
var enrichedMeta = {
displayName: meta.displayName || id,
description: meta.description || "",
category: meta.category || "Misc",
allowMultiple: meta.allowMultiple === true,
settingsForm: meta.settingsForm || "",
source: "first-party"
}
var comp = Qt.createComponent(url, Component.Asynchronous)
function finalize() {
if (comp.status === Component.Ready) {
barWidgetRegistry.register(id, comp, enrichedMeta)
var next = ({})
for (var k in registeredFirstPartyComponents) next[k] = registeredFirstPartyComponents[k]
next[id] = comp
registeredFirstPartyComponents = next
} else if (comp.status === Component.Error) {
console.warn("first-party widget " + id + " failed to load: " + comp.errorString())
}
}
if (comp.status === Component.Loading) comp.statusChanged.connect(finalize)
else finalize()
}
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 = String(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
}
// The host owns shell.json loading and injects `barConfig`. Bar still keeps
// its own theme FileView since theme colors are independent of shell.json.
// `omarchy-theme-set` recreates the entire theme/ directory via rm+mv, which
// invalidates the inotify watch on colors.toml. Use theme.name (overwritten
// in place) to force a fresh reload after each swap.
FileView {
id: themeColorsFile
path: root.home + "/.config/omarchy/current/theme/colors.toml"
watchChanges: true
printErrors: false
onLoaded: root.loadTheme(text())
onFileChanged: reload()
}
FileView {
path: root.home + "/.config/omarchy/current/theme.name"
watchChanges: true
printErrors: false
onFileChanged: themeColorsFile.reload()
}
// Presence of the `bar-off` flag = bar hidden. Watching the parent toggles
// directory because FileView can't observe a file that doesn't exist yet,
// and the flag is created/removed by `omarchy-toggle-bar`.
Process {
id: barHiddenProbe
running: true
command: ["bash", "-lc", "[[ -f $HOME/.local/state/omarchy/toggles/bar-off ]] && echo yes || echo no"]
stdout: SplitParser { onRead: function(line) { root.barHidden = String(line).trim() === "yes" } }
}
FileView {
path: root.home + "/.local/state/omarchy/toggles"
watchChanges: true
printErrors: false
onFileChanged: barHiddenProbe.running = true
}
Process {
id: weatherProc
command: ["bash", "-lc", root.commandWithOmarchyPath(root.shellQuote(root.omarchyPath + "/default/quickshell/omarchy-shell/scripts/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", root.commandWithOmarchyPath("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/quickshell/omarchy-shell/scripts/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/quickshell/omarchy-shell/scripts/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/quickshell/omarchy-shell/scripts/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
visible: !root.barHidden
anchors {
top: root.position === "top" || root.vertical
bottom: root.position === "bottom" || root.vertical
left: root.position === "left" || !root.vertical
right: root.position === "right" || !root.vertical
}
implicitWidth: root.vertical ? root.barSize : 0
implicitHeight: root.vertical ? 0 : root.barSize
color: root.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)
// 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 || builtinComponent) return null
return w[moduleName] ? w[moduleName].component : null
}
readonly property bool qmlCustom: customType === "qml"
readonly property bool commandCustom: customType === "command"
readonly property bool registered: registryComponent !== null
readonly property var activeItem: {
if (registered) return registryLoader.item
if (qmlCustom) return qmlLoader.item
return componentLoader.item
}
implicitWidth: activeItem && activeItem.visible ? activeItem.implicitWidth : 0
implicitHeight: activeItem && activeItem.visible ? activeItem.implicitHeight : 0
width: implicitWidth
height: implicitHeight
Loader {
id: componentLoader
active: !slot.qmlCustom && !slot.registered
sourceComponent: slot.builtinComponent || (slot.commandCustom ? customCommandModuleComponent : emptyModuleComponent)
anchors.fill: parent
}
Loader {
id: registryLoader
active: slot.registered
sourceComponent: slot.registered ? slot.registryComponent : null
anchors.fill: parent
onLoaded: slot.injectProps()
}
Loader {
id: qmlLoader
active: slot.qmlCustom
source: slot.qmlCustom ? root.customModuleSource(slot.entry) : ""
anchors.fill: parent
onLoaded: slot.injectProps()
}
onModuleSettingsChanged: injectProps()
function injectProps() {
var target = registryLoader.item || 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
// Noctalia compat injection. Only kicks in for plugins whose manifest
// was translated from Noctalia shape; for our first-party widgets these
// properties don't exist on the target item so nothing happens.
var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[moduleName] : null
if (manifest && manifest.__noctaliaCompat) {
if ("pluginApi" in target) target.pluginApi = root.noctaliaPluginApiFor(moduleName)
if ("widgetId" in target) target.widgetId = moduleName
if ("section" in target) target.section = root.sectionOfEntry(entry)
if ("sectionWidgetIndex" in target) target.sectionWidgetIndex = root.indexOfEntry(entry)
if ("sectionWidgetsCount" in target) target.sectionWidgetsCount = root.entriesOfSection(root.sectionOfEntry(entry)).length
if ("screen" in target && root.QsWindow && root.QsWindow.window)
target.screen = root.QsWindow.window.screen
}
}
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("omarchy-shell-ipc menu summon root")
}
}
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
property bool managePopupOpen: false
function closePopout() { managePopupOpen = false }
// Re-resolve the tray's own entry settings whenever the bar layout reloads.
readonly property var trayEntry: {
var serial = root.barConfigSerial
var sections = ["left", "center", "right"]
for (var s = 0; s < sections.length; s++) {
var arr = root.layoutEntries(sections[s])
for (var i = 0; i < arr.length; i++) {
if (root.entryId(arr[i]) === "tray") return arr[i]
}
}
return ({})
}
readonly property var pinnedIds: Array.isArray(trayEntry.pinned) ? trayEntry.pinned : []
readonly property var hiddenIds: Array.isArray(trayEntry.hidden) ? trayEntry.hidden : []
function classifyItem(item) {
var iid = String(item.id || "")
if (hiddenIds.indexOf(iid) !== -1) return "hidden"
if (pinnedIds.indexOf(iid) !== -1) return "pinned"
return "drawer"
}
function bucket(category) {
var values = SystemTray.items.values
var result = []
for (var i = 0; i < values.length; i++) {
var item = values[i]
if (item.status === Status.Passive) continue
if (category === "all") { result.push(item); continue }
if (classifyItem(item) === category) result.push(item)
}
return result
}
readonly property var pinnedItems: bucket("pinned")
readonly property var drawerItems: bucket("drawer")
readonly property var allItems: bucket("all")
readonly property int drawerCount: drawerItems.length
readonly property int drawerExtent: drawerCount > 0 ? drawerCount * 16 + (drawerCount - 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 }
}
function persistTrayState(pinned, hidden) {
if (!root.shell || typeof root.shell.updateEntryInline !== "function") return
root.shell.updateEntryInline("tray", { id: "tray", pinned: pinned, hidden: hidden })
}
function togglePin(iid) {
var p = pinnedIds.slice(), h = hiddenIds.slice()
var idx = p.indexOf(iid)
if (idx !== -1) p.splice(idx, 1)
else {
p.push(iid)
var hi = h.indexOf(iid); if (hi !== -1) h.splice(hi, 1)
}
persistTrayState(p, h)
}
function toggleHide(iid) {
var p = pinnedIds.slice(), h = hiddenIds.slice()
var idx = h.indexOf(iid)
if (idx !== -1) h.splice(idx, 1)
else {
h.push(iid)
var pi = p.indexOf(iid); if (pi !== -1) p.splice(pi, 1)
}
persistTrayState(p, h)
}
visible: pinnedItems.length > 0 || drawerCount > 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
}
Component {
id: horizontalTray
Item {
id: horizontalTrayRoot
readonly property int pinnedWidth: pinnedRow.implicitWidth
readonly property int drawerBlockWidth: trayRoot.allItems.length > 0 ? expandIcon.implicitWidth + trayRoot.drawerExtent : 0
implicitWidth: pinnedWidth + drawerBlockWidth
implicitHeight: root.barSize
// Mask out the empty area the collapsed drawer reserves for its slide-in,
// so hovering it doesn't trigger expand and clicks pass through.
containmentMask: QtObject {
function contains(point: point): bool {
if (point.y < 0 || point.y > horizontalTrayRoot.height) return false
// Drawer reveals leftward; chevron sits at the right end when collapsed
// and slides left as it opens. The visible region starts at the chevron.
var chevronX = trayRoot.drawerExtent - trayRoot.revealExtent
if (point.x >= chevronX && point.x <= horizontalTrayRoot.drawerBlockWidth) return true
// Pinned items, placed to the right of the drawer block.
var pinnedStart = horizontalTrayRoot.drawerBlockWidth
return point.x >= pinnedStart && point.x <= horizontalTrayRoot.implicitWidth
}
}
Item {
id: drawerArea
x: 0
width: horizontalTrayRoot.drawerBlockWidth
height: root.barSize
visible: trayRoot.allItems.length > 0
HoverHandler {
onHoveredChanged: trayRoot.expanded = hovered
}
ModuleButton {
id: expandIcon
width: implicitWidth
height: implicitHeight
x: trayRoot.drawerExtent - trayRoot.revealExtent
text: ""
horizontalMargin: 9
verticalPadding: 6
onPressed: function(button) {
if (button === Qt.RightButton) trayRoot.managePopupOpen = !trayRoot.managePopupOpen
}
}
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: trayRoot.drawerItems
TrayItem {}
}
}
}
}
Row {
id: pinnedRow
x: drawerArea.x + horizontalTrayRoot.drawerBlockWidth
anchors.verticalCenter: parent.verticalCenter
spacing: 17
leftPadding: trayRoot.pinnedItems.length > 0 && trayRoot.allItems.length > 0 ? 6 : 0
Repeater {
model: trayRoot.pinnedItems
TrayItem {}
}
}
}
}
Component {
id: verticalTray
Item {
id: verticalTrayRoot
readonly property int pinnedHeight: pinnedCol.implicitHeight
readonly property int drawerBlockHeight: trayRoot.allItems.length > 0 ? expandIcon.implicitHeight + trayRoot.drawerExtent : 0
implicitWidth: root.barSize
implicitHeight: pinnedHeight + drawerBlockHeight
containmentMask: QtObject {
function contains(point: point): bool {
if (point.x < 0 || point.x > verticalTrayRoot.width) return false
var chevronY = trayRoot.drawerExtent - trayRoot.revealExtent
if (point.y >= chevronY && point.y <= verticalTrayRoot.drawerBlockHeight) return true
var pinnedStart = verticalTrayRoot.drawerBlockHeight
return point.y >= pinnedStart && point.y <= verticalTrayRoot.implicitHeight
}
}
Item {
id: drawerArea
y: 0
width: root.barSize
height: verticalTrayRoot.drawerBlockHeight
visible: trayRoot.allItems.length > 0
HoverHandler {
onHoveredChanged: trayRoot.expanded = hovered
}
ModuleButton {
id: expandIcon
width: implicitWidth
height: implicitHeight
y: trayRoot.drawerExtent - trayRoot.revealExtent
text: ""
textRotation: 90
horizontalMargin: 9
verticalPadding: 6
onPressed: function(button) {
if (button === Qt.RightButton) trayRoot.managePopupOpen = !trayRoot.managePopupOpen
}
}
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: trayRoot.drawerItems
TrayItem {}
}
}
}
}
Column {
id: pinnedCol
y: drawerArea.y + verticalTrayRoot.drawerBlockHeight
anchors.horizontalCenter: parent.horizontalCenter
spacing: 17
topPadding: trayRoot.pinnedItems.length > 0 && trayRoot.allItems.length > 0 ? 6 : 0
Repeater {
model: trayRoot.pinnedItems
TrayItem {}
}
}
}
}
BarCommon.PopupCard {
id: managePopup
anchorItem: trayRoot
owner: trayRoot
bar: root
open: trayRoot.managePopupOpen
contentWidth: 300
contentHeight: manageColumn.implicitHeight + 28
Column {
id: manageColumn
anchors.fill: parent
spacing: 8
Text {
text: "Tray icons"
color: root.foreground
font.family: root.fontFamily
font.pixelSize: 12
font.bold: true
}
Text {
text: "Pinned icons stay visible. Hidden icons never show."
color: Qt.darker(root.foreground, 1.4)
font.family: root.fontFamily
font.pixelSize: 10
wrapMode: Text.WordWrap
width: parent.width
}
Text {
visible: trayRoot.allItems.length === 0
text: "No tray items reporting."
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: 11
font.italic: true
}
Repeater {
model: trayRoot.allItems
delegate: Item {
id: rowRoot
required property var modelData
required property int index
width: manageColumn.width
implicitHeight: 28
readonly property string itemId: String(modelData.id || "")
readonly property string displayName: {
var t = String(modelData.title || "").trim()
if (t) return t
var tt = String(modelData.tooltipTitle || "").trim()
if (tt) return tt
var id = String(modelData.id || "")
var slash = id.lastIndexOf("/")
return slash !== -1 ? id.substring(slash + 1) : (id || "Unknown")
}
readonly property bool isPinned: trayRoot.pinnedIds.indexOf(itemId) !== -1
readonly property bool isHidden: trayRoot.hiddenIds.indexOf(itemId) !== -1
IconImage {
id: rowIcon
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
implicitSize: 16
width: 16
height: 16
source: root.trayIconSource(rowRoot.modelData.icon)
}
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: rowIcon.right
anchors.leftMargin: 10
anchors.right: rowHideBtn.left
anchors.rightMargin: 8
text: rowRoot.displayName
color: root.foreground
font.family: root.fontFamily
font.pixelSize: 11
elide: Text.ElideRight
}
BarCommon.PillButton {
id: rowPinBtn
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
iconText: ""
text: rowRoot.isPinned ? "Unpin" : "Pin"
foreground: root.foreground
horizontalPadding: 8
verticalPadding: 3
iconSize: 11
fontSize: 11
onClicked: trayRoot.togglePin(rowRoot.itemId)
}
BarCommon.PillButton {
id: rowHideBtn
anchors.verticalCenter: parent.verticalCenter
anchors.right: rowPinBtn.left
anchors.rightMargin: 6
iconText: ""
text: rowRoot.isHidden ? "Show" : "Hide"
foreground: root.foreground
horizontalPadding: 8
verticalPadding: 3
iconSize: 11
fontSize: 11
onClicked: trayRoot.toggleHide(rowRoot.itemId)
}
}
}
}
}
}
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-shell-ipc menu summon power")
}
}
}