Add omarchy-shell with a plugin registry

This commit is contained in:
Ryan Hughes
2026-05-14 02:21:48 -04:00
parent 619a0d5737
commit c72a8756b3
44 changed files with 582 additions and 60 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,159 @@
# Omarchy bar
This is the Quickshell implementation of the Omarchy status bar.
- `Bar.qml` is Omarchy-owned bar engine code, loaded by the omarchy-shell host. Users should not edit it directly.
- `bar-defaults.json` is the Omarchy-owned default layout and module settings.
- `widgets/` holds first-party widgets — modular, interactive components shipped with Omarchy.
- `common/` holds shared QML helpers (buttons, sliders, popup cards).
- User overrides live in `~/.config/omarchy/bar.json` and are merged over defaults at runtime.
- `omarchy-style-bar-position` updates only the user override file.
## Customizing
The bar reads `~/.local/share/omarchy/default/quickshell/omarchy-shell/plugins/bar/bar-defaults.json`, then deep-merges `~/.config/omarchy/bar.json` on top of it. Each `layout.{left,center,right}` entry is an object: at minimum `{ "id": "<widget>" }`, plus any inline settings the widget reads.
Launch the visual editor with `omarchy launch bar-settings` (or run `omarchy-launch-bar-settings`) to reorder widgets, add/remove them, and tweak per-widget options without editing JSON by hand.
Example `bar.json`:
```json
{
"position": "top",
"centerAnchor": "calendar",
"layout": {
"left": [
{ "id": "omarchy" },
{ "id": "spacer", "size": 12 },
{ "id": "workspacesPro" }
],
"center": [
{ "id": "media" },
{ "id": "calendar", "format": "HH:mm" }
],
"right": [
{ "id": "systemStats" },
{ "id": "audioPanel" },
{ "id": "battery" },
{ "id": "controlCenter" },
{ "id": "powerMenu" }
]
}
}
```
`centerAnchor` pins one center module to the exact horizontal/vertical center and flanks others around it. Set to an empty string to disable anchoring (the center list is centered as a group).
## Module catalogue
### First-party interactive widgets (in `widgets/`)
| Name | What it does | Interactions |
|---|---|---|
| `media` | MPRIS now-playing — scrolling track + artist, cover-art popup | left = play/pause · middle = next · scroll = prev/next · right = popup |
| `audioPanel` | Volume icon + popup with master slider, output-device picker, per-app mixer | left = popup · right = mute · middle = audio TUI · scroll = volume |
| `networkPanel` | Wi-Fi/Ethernet icon + popup with Wi-Fi scan, signal, connect | left = popup · right = nmtui |
| `bluetoothPanel` | Bluetooth icon + popup with device list, connect/disconnect, battery | left = popup · right = toggle radio · middle = bluetoothctl TUI |
| `calendar` | Clock + popup with month-grid calendar | left = popup · right = tz selector |
| `notificationCenter` | Bell with badge + popup with recent notifications, DND toggle | left = popup · right = toggle DND |
| `brightness` | Brightness slider + scroll | scroll = adjust · left = popup · middle = reset to 80% |
| `powerProfile` | Current power profile + popup picker | left = popup |
| `systemStats` | Inline CPU + memory sparklines, popup with detail | left = popup · right = terminal |
| `weatherFlyout` | Weather icon + popup with forecast | left = popup · right = full notification |
| `workspacesPro` | Animated focus indicator that slides between workspaces | left = focus · right = move window · scroll = cycle |
| `powerMenu` | Power icon → popup with lock/suspend/log out/reboot/shutdown | left = popup |
| `idleInhibitor` | Coffee-cup that toggles `omarchy-toggle-idle` | left = toggle |
| `microphone` | Mic icon + scroll volume | left = mute toggle · middle = audio TUI · scroll = source volume |
### Built-in legacy modules (in `shell.qml`)
`omarchy`, `workspaces`, `clock`, `weather`, `update`, `voxtype`, `screenRecording`, `idle`, `notifications`, `tray`, `bluetooth`, `network`, `audio`, `cpu`, `battery`.
These remain available — set them in `layout` to use them instead of the richer widget versions.
## Orientation
All widgets work in `top`, `bottom`, `left`, and `right` positions. Popups anchor on the side opposite the bar edge, sliding into the workspace. Vertical bars use 28px width; widgets that show text fall back to compact icon-only forms (e.g. `media` hides its scrolling label).
## Custom user modules
The schema accepts arbitrary module ids that you provide. Set `type` to `command` for shell-driven output or `qml` for a custom QML widget.
Command module:
```json
{
"layout": {
"right": [
{ "id": "tray" },
{ "id": "vpn", "type": "command", "exec": "~/.config/omarchy/bar/scripts/vpn-status", "interval": 5, "tooltip": "VPN", "onClick": "nm-connection-editor" },
{ "id": "audioPanel" }
]
}
}
```
The command may print plain text or Waybar-style JSON, for example:
```json
{"text":"󰌆","tooltip":"Work VPN","class":"active"}
```
QML module:
```json
{
"layout": {
"right": [
{ "id": "gpu", "type": "qml" },
{ "id": "audioPanel" }
]
}
}
```
Then create `~/.config/omarchy/bar/modules/gpu.qml`. If you want to store it elsewhere, add a `source` path.
Custom QML modules should be an `Item` with `implicitWidth` and `implicitHeight`. They may optionally define these properties, which the bar fills after loading:
```qml
import QtQuick
Item {
property var bar
property string moduleName
property var settings
implicitWidth: 28
implicitHeight: bar ? bar.barSize : 26
Text {
anchors.centerIn: parent
text: "GPU"
color: bar ? bar.foreground : "white"
font.family: bar ? bar.fontFamily : "monospace"
font.pixelSize: 12
}
MouseArea {
anchors.fill: parent
onClicked: if (bar) bar.run("omarchy-launch-or-focus-tui btop")
}
}
```
## Bar properties available to widgets
Widgets receive `bar` (the shell root), `moduleName` (string), and `settings` (object) injected at load time. The bar exposes:
- `bar.foreground`, `bar.background`, `bar.urgent` — theme colors (live-updated)
- `bar.fontFamily` — current monospace family
- `bar.position``"top" | "bottom" | "left" | "right"`
- `bar.vertical` — boolean shortcut
- `bar.barSize` — 26 horizontal / 28 vertical
- `bar.run(command)` — fire-and-forget bash exec
- `bar.shellQuote(value)` — safe shell-quote a string
- `bar.showTooltip(target, text)` / `bar.hideTooltip(target)` — shared tooltip popup
- `bar.requestPopout(owner)` / `bar.releasePopout(owner)` — one-popup-at-a-time coordinator
Drop new widgets into `widgets/<name>.qml`, add the name to the `firstPartyWidgets` registry in `shell.qml`, and reference it by name in any layout list.
@@ -0,0 +1,36 @@
{
"position": "top",
"fontFamily": "JetBrainsMono Nerd Font",
"centerAnchor": "calendar",
"layout": {
"left": [
{ "id": "omarchy" },
{ "id": "workspacesPro" },
{ "id": "activeWindow" }
],
"center": [
{ "id": "media" },
{ "id": "calendar", "format": "dddd HH:mm", "formatAlt": "dd MMMM 'W'ww yyyy", "verticalFormat": "HH\n—\nmm" },
{ "id": "weatherFlyout" },
{ "id": "update" },
{ "id": "voxtype" },
{ "id": "screenRecording" },
{ "id": "idle" },
{ "id": "notifications" }
],
"right": [
{ "id": "tray" },
{ "id": "systemStats" },
{ "id": "microphone" },
{ "id": "bluetoothPanel" },
{ "id": "networkPanel" },
{ "id": "audioPanel" },
{ "id": "nightLight" },
{ "id": "brightness" },
{ "id": "powerProfile" },
{ "id": "battery" },
{ "id": "controlCenter" },
{ "id": "powerMenu" }
]
}
}
@@ -0,0 +1,66 @@
import QtQuick
Rectangle {
id: root
property string text: ""
property string iconText: ""
property color foreground: "#cacccc"
property color background: "transparent"
property color hoverBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.12)
property color pressedBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.22)
property string fontFamily: "JetBrainsMono Nerd Font"
property real fontSize: 12
property real iconSize: 14
property real horizontalPadding: 10
property real verticalPadding: 6
property bool active: false
property color activeBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.18)
signal clicked()
signal rightClicked()
implicitWidth: row.implicitWidth + horizontalPadding * 2
implicitHeight: row.implicitHeight + verticalPadding * 2
radius: 4
color: mouseArea.pressed ? pressedBackground : (mouseArea.containsMouse ? hoverBackground : (active ? activeBackground : background))
Behavior on color {
ColorAnimation { duration: 120 }
}
Row {
id: row
anchors.centerIn: parent
spacing: 8
Text {
visible: root.iconText !== ""
text: root.iconText
color: root.foreground
font.family: root.fontFamily
font.pixelSize: root.iconSize
anchors.verticalCenter: parent.verticalCenter
}
Text {
visible: root.text !== ""
text: root.text
color: root.foreground
font.family: root.fontFamily
font.pixelSize: root.fontSize
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function(mouse) {
if (mouse.button === Qt.RightButton) root.rightClicked()
else root.clicked()
}
}
}
@@ -0,0 +1,92 @@
import QtQuick
import Quickshell
PopupWindow {
id: root
required property Item anchorItem
required property QtObject bar
property var owner: null
property int margin: 8
property int padding: 14
property int contentWidth: 280
property int contentHeight: 200
property bool open: false
readonly property var coordinatorKey: owner || root
function closePopout() {
if (owner && "closePopout" in owner) owner.closePopout()
else root.open = false
}
default property alias contentItem: contentHolder.children
visible: open
color: "transparent"
implicitWidth: contentWidth
implicitHeight: contentHeight
onOpenChanged: {
if (!bar) return
if (open) bar.requestPopout(coordinatorKey)
else if (bar.activePopout === coordinatorKey) bar.releasePopout(coordinatorKey)
}
anchor {
id: popupAnchor
window: anchorItem ? anchorItem.QsWindow.window : null
adjustment: PopupAdjustment.Slide
edges: Edges.Top | Edges.Left
gravity: Edges.Bottom | Edges.Right
rect.width: 1
rect.height: 1
onAnchoring: {
if (!root.anchorItem || !root.bar) return
var target = root.anchorItem
var popupWidth = root.implicitWidth
var popupHeight = root.implicitHeight
var localX = target.width / 2 - popupWidth / 2
var localY = target.height + root.margin
if (root.bar.position === "bottom") {
localY = -popupHeight - root.margin
} else if (root.bar.position === "left") {
localX = target.width + root.margin
localY = target.height / 2 - popupHeight / 2
} else if (root.bar.position === "right") {
localX = -popupWidth - root.margin
localY = target.height / 2 - popupHeight / 2
}
var window = target.QsWindow.window
if (!window) return
var point = window.contentItem.mapFromItem(target, localX, localY)
popupAnchor.rect.x = Math.round(point.x)
popupAnchor.rect.y = Math.round(point.y)
}
}
Rectangle {
id: card
anchors.fill: parent
color: root.bar ? root.bar.background : "#101315"
border.color: root.bar ? root.bar.foreground : "#cacccc"
border.width: 1
radius: 0
opacity: root.open ? 1.0 : 0
Behavior on opacity {
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
Item {
id: contentHolder
anchors.fill: parent
anchors.margins: root.padding
}
}
}
@@ -0,0 +1,116 @@
import QtQuick
Item {
id: root
property QtObject bar: null
property real value: 0
property real minimum: 0
property real maximum: 1
property real step: 0.05
property bool integer: false
property color trackColor: bar ? Qt.rgba(bar.foreground.r, bar.foreground.g, bar.foreground.b, 0.18) : "#333"
property color fillColor: bar ? bar.foreground : "#cacccc"
property color knobColor: bar ? bar.foreground : "#cacccc"
property bool dragging: false
property real trackHeight: 4
property real liveValue: value
onValueChanged: if (!dragging) liveValue = value
signal moved(real value)
signal released(real value)
implicitWidth: 200
implicitHeight: 22
readonly property real range: Math.max(0.0001, maximum - minimum)
readonly property real progress: Math.max(0, Math.min(1, (liveValue - minimum) / range))
Rectangle {
id: track
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
height: root.trackHeight
radius: height / 2
color: root.trackColor
}
Rectangle {
id: fill
anchors.verticalCenter: track.verticalCenter
anchors.left: track.left
height: track.height
radius: track.radius
color: root.fillColor
width: track.width * root.progress
Behavior on width {
enabled: !root.dragging
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
}
Rectangle {
id: knob
width: 14
height: 14
radius: 7
color: root.knobColor
border.color: root.bar ? root.bar.background : "#101315"
border.width: 2
anchors.verticalCenter: track.verticalCenter
x: Math.max(0, Math.min(track.width - width, track.width * root.progress - width / 2))
scale: mouseArea.containsMouse || root.dragging ? 1.15 : 1.0
Behavior on x {
enabled: !root.dragging
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
Behavior on scale {
NumberAnimation { duration: 110; easing.type: Easing.OutCubic }
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
function valueFromX(x) {
var clamped = Math.max(0, Math.min(track.width, x))
var raw = root.minimum + (clamped / track.width) * root.range
if (root.integer) raw = Math.round(raw)
return Math.max(root.minimum, Math.min(root.maximum, raw))
}
onPressed: function(mouse) {
root.dragging = true
var next = valueFromX(mouse.x)
root.liveValue = next
root.moved(next)
}
onPositionChanged: function(mouse) {
if (!root.dragging) return
var next = valueFromX(mouse.x)
root.liveValue = next
root.moved(next)
}
onReleased: function(mouse) {
root.dragging = false
root.released(root.liveValue)
root.liveValue = root.value
}
onWheel: function(wheel) {
var delta = wheel.angleDelta.y > 0 ? root.step : -root.step
var next = Math.max(root.minimum, Math.min(root.maximum, root.liveValue + delta))
if (root.integer) next = Math.round(next)
root.liveValue = next
root.moved(next)
root.released(next)
}
}
}
@@ -0,0 +1,69 @@
import QtQuick
Item {
id: root
property var bar: null
property string text: ""
property string fontFamily: bar ? bar.fontFamily : "JetBrainsMono Nerd Font"
property real fontSize: 12
property color foreground: bar ? bar.foreground : "#cacccc"
property color activeColor: bar ? bar.urgent : "#a55555"
property bool active: false
property real horizontalMargin: 7.5
property real verticalPadding: 6
property real fixedWidth: -1
property real fixedHeight: -1
property real textRotation: 0
property bool keepSpace: false
property string tooltipText: ""
property real hoverScale: 1.0
property real pressScale: 0.92
signal pressed(int button)
signal wheelMoved(int delta)
readonly property bool vertical: bar ? bar.vertical : false
readonly property int barSize: bar ? bar.barSize : 26
visible: text !== "" || keepSpace
opacity: text === "" ? 0 : 1
implicitWidth: fixedWidth > 0 ? fixedWidth : (vertical ? barSize : Math.max(12, label.implicitWidth + horizontalMargin * 2))
implicitHeight: fixedHeight > 0 ? fixedHeight : (vertical ? Math.max(12, label.implicitHeight + verticalPadding * 2) : barSize)
Behavior on opacity {
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
Text {
id: label
anchors.centerIn: parent
text: root.text
color: root.active ? root.activeColor : root.foreground
font.family: root.fontFamily
font.pixelSize: root.fontSize
rotation: root.textRotation
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
scale: mouseArea.pressed ? root.pressScale : (mouseArea.containsMouse ? root.hoverScale : 1.0)
Behavior on color {
ColorAnimation { duration: 160 }
}
Behavior on scale {
NumberAnimation { duration: 110; easing.type: Easing.OutCubic }
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true
onEntered: if (root.bar) root.bar.showTooltip(root, root.tooltipText)
onExited: if (root.bar) root.bar.hideTooltip(root)
onClicked: function(mouse) { root.pressed(mouse.button) }
onWheel: function(wheel) { root.wheelMoved(wheel.angleDelta.y) }
}
}
@@ -0,0 +1,11 @@
{
"schemaVersion": 1,
"id": "omarchy.bar",
"name": "Bar",
"version": "1.0.0",
"author": "Omarchy",
"description": "Status bar with widgets",
"kinds": ["bar"],
"activation": "persistent",
"entryPoints": { "bar": "Bar.qml" }
}
@@ -0,0 +1,70 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
Item {
id: root
property QtObject bar: null
property string moduleName: "activeWindow"
property var settings: ({})
function setting(name, fallback) {
var value = settings ? settings[name] : undefined
return value === undefined || value === null ? fallback : value
}
readonly property var toplevel: ToplevelManager.activeToplevel
readonly property string title: toplevel ? (toplevel.title || toplevel.appId || "") : ""
readonly property int maxLabelWidth: Number(setting("maxWidth", 280))
readonly property bool vertical: bar ? bar.vertical : false
visible: title !== "" && !vertical
implicitWidth: visible ? Math.min(maxLabelWidth, labelText.implicitWidth) + 16 : 0
implicitHeight: bar ? bar.barSize : 26
Behavior on implicitWidth {
NumberAnimation { duration: 180; easing.type: Easing.OutCubic }
}
Item {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
clip: true
Text {
id: labelText
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
width: parent.width
text: root.title
color: root.bar ? root.bar.foreground : "#cacccc"
font.family: root.bar ? root.bar.fontFamily : "JetBrainsMono Nerd Font"
font.pixelSize: 12
elide: Text.ElideRight
opacity: 0.85
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: function(mouse) {
if (!root.toplevel) return
if (mouse.button === Qt.MiddleButton) {
root.toplevel.close()
} else if (mouse.button === Qt.RightButton) {
root.toplevel.close()
} else {
root.toplevel.activate()
}
}
onEntered: if (root.bar) root.bar.showTooltip(root, root.title)
onExited: if (root.bar) root.bar.hideTooltip(root)
}
}
@@ -0,0 +1,246 @@
import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
import "../common" as Common
Item {
id: root
property QtObject bar: null
property string moduleName: "audioPanel"
property var settings: ({})
property bool popupOpen: false
function closePopout() { popupOpen = false }
readonly property var sink: Pipewire.defaultAudioSink
readonly property var source: Pipewire.defaultAudioSource
readonly property var nodes: Pipewire.nodes ? Pipewire.nodes.values : []
readonly property var candidateSinks: {
var list = []
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i]
if (node && node.isSink && !node.isStream) list.push(node)
}
return list
}
readonly property var candidateStreams: {
var list = []
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i]
if (node && node.isStream && !node.isSink) list.push(node)
}
return list
}
readonly property var audioSinks: {
var list = []
for (var i = 0; i < candidateSinks.length; i++)
if (candidateSinks[i].audio) list.push(candidateSinks[i])
return list
}
readonly property var audioStreams: {
var list = []
for (var i = 0; i < candidateStreams.length; i++)
if (candidateStreams[i].audio) list.push(candidateStreams[i])
return list
}
readonly property real currentVolume: sink && sink.audio ? sink.audio.volume : 0
readonly property bool muted: sink && sink.audio ? sink.audio.muted : false
readonly property string volumeIcon: {
if (!sink || !sink.audio) return ""
if (muted) return "󰸈"
var v = currentVolume
if (v >= 0.67) return "󰕾"
if (v >= 0.34) return "󰖀"
if (v > 0) return "󰕿"
return "󰸈"
}
function setVolume(v) {
if (!sink || !sink.audio) return
sink.audio.volume = Math.max(0, Math.min(1, v))
}
function toggleMute() {
if (sink && sink.audio) sink.audio.muted = !sink.audio.muted
}
function setDefaultSink(node) {
Pipewire.preferredDefaultAudioSink = node
}
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
PwObjectTracker { objects: root.candidateSinks }
PwObjectTracker { objects: root.candidateStreams }
Common.WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.volumeIcon
tooltipText: root.sink ? (root.sink.description || root.sink.nickname || "Audio") + " · " + Math.round(root.currentVolume * 100) + "%" : "No audio"
onPressed: function(b) {
if (b === Qt.RightButton) root.toggleMute()
else if (b === Qt.MiddleButton) root.bar.run("omarchy-launch-audio")
else root.popupOpen = !root.popupOpen
}
onWheelMoved: function(delta) {
var step = 0.05
root.setVolume(root.currentVolume + (delta > 0 ? step : -step))
}
}
Common.PopupCard {
anchorItem: button
owner: root
bar: root.bar
open: root.popupOpen
contentWidth: 340
contentHeight: panelColumn.implicitHeight + 28
Column {
id: panelColumn
anchors.fill: parent
spacing: 12
// Master volume
Row {
width: parent.width
spacing: 10
Text {
text: root.volumeIcon
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 18
anchors.verticalCenter: parent.verticalCenter
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.toggleMute()
}
}
Common.Slider {
bar: root.bar
width: parent.width - 50
anchors.verticalCenter: parent.verticalCenter
minimum: 0
maximum: 1
step: 0.05
value: root.currentVolume
opacity: root.muted ? 0.5 : 1.0
onMoved: function(v) { root.setVolume(v) }
}
}
// Output device picker
Column {
spacing: 4
width: parent.width
visible: root.audioSinks.length > 0
Text {
text: "Output"
color: Qt.darker(root.bar.foreground, 1.5)
font.family: root.bar.fontFamily
font.pixelSize: 11
font.bold: true
}
Repeater {
model: root.audioSinks
Common.PillButton {
required property var modelData
width: parent.width
text: modelData ? (modelData.description || modelData.nickname || modelData.name || "Unknown") : ""
iconText: root.sinkGlyph(modelData)
foreground: root.bar.foreground
horizontalPadding: 10
verticalPadding: 6
active: root.sink && modelData && root.sink.id === modelData.id
onClicked: { root.setDefaultSink(modelData); }
}
}
}
// Per-app streams
Column {
spacing: 4
width: parent.width
visible: root.audioStreams.length > 0
Text {
text: "Playing"
color: Qt.darker(root.bar.foreground, 1.5)
font.family: root.bar.fontFamily
font.pixelSize: 11
font.bold: true
}
Repeater {
model: root.audioStreams
Row {
required property var modelData
width: parent.width
spacing: 8
Text {
text: modelData && modelData.properties ? (modelData.properties["application.name"] || modelData.properties["node.name"] || "Stream") : "Stream"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 11
elide: Text.ElideRight
width: 110
anchors.verticalCenter: parent.verticalCenter
}
Common.Slider {
bar: root.bar
width: parent.width - 124
anchors.verticalCenter: parent.verticalCenter
minimum: 0
maximum: 1.5
step: 0.05
value: modelData && modelData.audio ? modelData.audio.volume : 0
onMoved: function(v) {
if (modelData && modelData.audio) modelData.audio.volume = v
}
}
}
}
}
}
}
function sinkGlyph(node) {
if (!node) return ""
var blob = String([
node.name, node.description, node.nickname,
node.properties ? node.properties["device.icon-name"] : "",
node.properties ? node.properties["device.product.name"] : ""
].join(" ")).toLowerCase()
if (blob.indexOf("headphone") !== -1 || blob.indexOf("headset") !== -1) return "󰋋"
if (blob.indexOf("bluetooth") !== -1) return "󰂯"
if (blob.indexOf("hdmi") !== -1 || blob.indexOf("display") !== -1) return "󰍹"
return "󰓃"
}
}
@@ -0,0 +1,152 @@
import QtQuick
import Quickshell
import Quickshell.Bluetooth
import "../common" as Common
Item {
id: root
property QtObject bar: null
property string moduleName: "bluetoothPanel"
property var settings: ({})
property bool popupOpen: false
function closePopout() { popupOpen = false }
readonly property var adapter: Bluetooth.defaultAdapter
readonly property var devices: Bluetooth.devices ? Bluetooth.devices.values : []
readonly property var connectedDevices: {
var list = []
for (var i = 0; i < devices.length; i++)
if (devices[i] && devices[i].connected) list.push(devices[i])
return list
}
readonly property var knownDevices: {
var list = []
for (var i = 0; i < devices.length; i++) {
var d = devices[i]
if (d && (d.paired || d.connected || d.bonded || d.trusted)) list.push(d)
}
list.sort(function(a, b) {
if (a.connected !== b.connected) return a.connected ? -1 : 1
return (a.name || a.deviceName || "").localeCompare(b.name || b.deviceName || "")
})
return list
}
readonly property string icon: {
if (!adapter) return ""
if (!adapter.enabled) return "󰂲"
if (connectedDevices.length > 0) return "󰂱"
return "󰂯"
}
visible: adapter !== null
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
Common.WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.icon
horizontalMargin: 8.5
tooltipText: root.adapter ? (root.adapter.enabled ? "Bluetooth: " + root.connectedDevices.length + " connected" : "Bluetooth off") : ""
onPressed: function(b) {
if (b === Qt.RightButton && root.adapter) root.adapter.enabled = !root.adapter.enabled
else if (b === Qt.MiddleButton) root.bar.run("omarchy-launch-bluetooth")
else root.popupOpen = !root.popupOpen
}
}
Common.PopupCard {
anchorItem: button
owner: root
bar: root.bar
open: root.popupOpen
contentWidth: 320
contentHeight: column.implicitHeight + 28
Column {
id: column
anchors.fill: parent
spacing: 8
Row {
width: parent.width
spacing: 8
Text {
text: "Bluetooth"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 13
font.bold: true
anchors.verticalCenter: parent.verticalCenter
}
Item { width: parent.width - 200; height: 1 }
Common.PillButton {
iconText: root.adapter && root.adapter.enabled ? "󰂯" : "󰂲"
text: root.adapter && root.adapter.enabled ? "On" : "Off"
foreground: root.bar.foreground
horizontalPadding: 8
verticalPadding: 4
active: root.adapter && root.adapter.enabled
onClicked: if (root.adapter) root.adapter.enabled = !root.adapter.enabled
}
Common.PillButton {
iconText: "󰂳"
foreground: root.bar.foreground
horizontalPadding: 8
verticalPadding: 4
enabled: root.adapter !== null && root.adapter.enabled
opacity: enabled ? 1 : 0.4
active: root.adapter && root.adapter.discovering
onClicked: if (root.adapter) root.adapter.discovering = !root.adapter.discovering
}
}
Repeater {
model: root.knownDevices
Common.PillButton {
required property var modelData
width: parent.width
iconText: modelData && modelData.connected ? "󰂱" : "󰂯"
text: {
var label = modelData ? (modelData.deviceName || modelData.name || modelData.address || "Device") : ""
if (modelData && modelData.batteryAvailable) label += " " + Math.round(modelData.battery * 100) + "%"
return label
}
foreground: root.bar.foreground
horizontalPadding: 10
verticalPadding: 6
active: modelData && modelData.connected
onClicked: {
if (!modelData) return
if (modelData.connected) modelData.disconnect()
else modelData.connect()
}
onRightClicked: if (modelData) modelData.forget()
}
}
Text {
visible: root.knownDevices.length === 0
text: root.adapter && root.adapter.enabled ? "Scanning for devices…" : "Turn Bluetooth on to scan"
color: Qt.darker(root.bar.foreground, 1.5)
font.family: root.bar.fontFamily
font.pixelSize: 11
}
}
}
}
@@ -0,0 +1,165 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "../common" as Common
Item {
id: root
property QtObject bar: null
property string moduleName: "brightness"
property var settings: ({})
function setting(name, fallback) {
var value = settings ? settings[name] : undefined
return value === undefined || value === null ? fallback : value
}
property int currentPercent: -1
property bool popupOpen: false
function closePopout() { popupOpen = false }
readonly property string iconGlyph: {
if (currentPercent < 0) return ""
if (currentPercent > 66) return "󰃠"
if (currentPercent > 33) return "󰃟"
return "󰃞"
}
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
visible: currentPercent >= 0
function refresh() {
if (!readProc.running) readProc.running = true
}
property int pendingPercent: -1
function setBrightness(percent) {
var clamped = Math.max(1, Math.min(100, Math.round(percent)))
currentPercent = clamped
pendingPercent = clamped
writeTimer.restart()
}
Timer {
id: writeTimer
interval: 60
repeat: false
onTriggered: {
if (writeProc.running) {
writeTimer.restart()
return
}
if (pendingPercent < 0) return
writeProc.command = ["bash", "-lc", "brightnessctl set " + pendingPercent + "% >/dev/null"]
pendingPercent = -1
writeProc.running = true
}
}
Component.onCompleted: refresh()
Process {
id: readProc
command: ["bash", "-lc", "if command -v brightnessctl >/dev/null; then echo $(( 100 * $(brightnessctl get) / $(brightnessctl max) )); fi"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
var n = parseInt(String(text || "").trim(), 10)
if (!isNaN(n)) root.currentPercent = n
}
}
}
Process { id: writeProc }
Timer {
interval: 5000
running: true
repeat: true
onTriggered: root.refresh()
}
Common.WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.iconGlyph
horizontalMargin: 6.5
tooltipText: root.currentPercent >= 0 ? "Brightness " + root.currentPercent + "%" : ""
onPressed: function(b) {
if (b === Qt.MiddleButton) {
root.popupOpen = false
root.setBrightness(80)
} else {
root.popupOpen = !root.popupOpen
}
}
onWheelMoved: function(delta) {
var step = Number(root.setting("step", 5))
root.setBrightness(root.currentPercent + (delta > 0 ? step : -step))
}
}
Common.PopupCard {
anchorItem: button
owner: root
bar: root.bar
open: root.popupOpen
contentWidth: 280
contentHeight: 80
Column {
anchors.fill: parent
spacing: 10
Row {
spacing: 10
width: parent.width
Text {
text: root.iconGlyph
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 18
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Brightness"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 12
anchors.verticalCenter: parent.verticalCenter
}
Item { width: 10; height: 1 }
Text {
text: root.currentPercent + "%"
color: Qt.darker(root.bar.foreground, 1.3)
font.family: root.bar.fontFamily
font.pixelSize: 12
anchors.verticalCenter: parent.verticalCenter
}
}
Common.Slider {
bar: root.bar
width: parent.width
minimum: 1
maximum: 100
step: 5
integer: true
value: root.currentPercent
onMoved: function(v) { root.setBrightness(v) }
}
}
}
}
@@ -0,0 +1,187 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import "../common" as Common
Item {
id: root
property QtObject bar: null
property string moduleName: "calendar"
property var settings: ({})
property date now: new Date()
property date viewMonth: new Date()
property bool popupOpen: false
function closePopout() { popupOpen = false }
function setting(name, fallback) {
var value = settings ? settings[name] : undefined
return value === undefined || value === null ? fallback : value
}
function formatLabel() {
if (!bar) return ""
var fmt = bar.vertical
? String(setting("verticalFormat", "HH\n—\nmm"))
: String(setting("format", "dddd HH:mm"))
return Qt.formatDateTime(now, fmt)
}
function shiftMonth(delta) {
var date = new Date(viewMonth)
date.setDate(1)
date.setMonth(date.getMonth() + delta)
viewMonth = date
}
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
SystemClock {
id: clockTimer
precision: SystemClock.Minutes
onDateChanged: root.now = clockTimer.date
}
Common.WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.formatLabel()
horizontalMargin: 8.75
verticalPadding: 8.75
tooltipText: Qt.formatDateTime(root.now, "dddd, MMMM d, yyyy")
onPressed: function(b) {
if (b === Qt.RightButton) {
root.bar.run("omarchy-launch-floating-terminal-with-presentation omarchy-tz-select")
} else {
root.viewMonth = new Date(root.now)
root.popupOpen = !root.popupOpen
}
}
}
Common.PopupCard {
id: popup
anchorItem: button
bar: root.bar
owner: root
open: root.popupOpen
contentWidth: 300
contentHeight: header.implicitHeight + grid.implicitHeight + 36
Column {
anchors.fill: parent
spacing: 8
Item {
id: header
width: parent.width
implicitHeight: 28
Common.PillButton {
id: prevButton
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
iconText: "󰅁"
foreground: root.bar.foreground
horizontalPadding: 8
verticalPadding: 4
onClicked: root.shiftMonth(-1)
}
Text {
anchors.centerIn: parent
text: Qt.formatDate(root.viewMonth, "MMMM yyyy")
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 14
font.bold: true
}
Common.PillButton {
id: nextButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
iconText: "󰅂"
foreground: root.bar.foreground
horizontalPadding: 8
verticalPadding: 4
onClicked: root.shiftMonth(1)
}
}
Grid {
id: grid
columns: 7
rowSpacing: 4
columnSpacing: 4
width: parent.width
Repeater {
model: ["S", "M", "T", "W", "T", "F", "S"]
Item {
required property string modelData
width: (grid.width - grid.columnSpacing * 6) / 7
height: 18
Text {
anchors.centerIn: parent
text: modelData
color: Qt.darker(root.bar.foreground, 1.6)
font.family: root.bar.fontFamily
font.pixelSize: 11
font.bold: true
}
}
}
Repeater {
model: 42
Rectangle {
required property int index
readonly property var startOfMonth: {
var d = new Date(root.viewMonth)
d.setDate(1)
return d
}
readonly property int firstDayOffset: startOfMonth.getDay()
readonly property var dayDate: {
var d = new Date(startOfMonth)
d.setDate(d.getDate() + index - firstDayOffset)
return d
}
readonly property bool inMonth: dayDate.getMonth() === root.viewMonth.getMonth()
readonly property bool isToday: {
var n = root.now
return dayDate.getDate() === n.getDate() && dayDate.getMonth() === n.getMonth() && dayDate.getFullYear() === n.getFullYear()
}
width: (grid.width - grid.columnSpacing * 6) / 7
height: 28
radius: 4
color: isToday ? root.bar.foreground : "transparent"
border.color: isToday ? root.bar.foreground : "transparent"
border.width: isToday ? 1 : 0
Text {
anchors.centerIn: parent
text: dayDate.getDate()
color: isToday ? root.bar.background : (inMonth ? root.bar.foreground : Qt.darker(root.bar.foreground, 2.2))
font.family: root.bar.fontFamily
font.pixelSize: 12
font.bold: isToday
}
}
}
}
}
}
}
@@ -0,0 +1,435 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import Quickshell.Services.UPower
import "../common" as Common
Item {
id: root
property QtObject bar: null
property string moduleName: "controlCenter"
property var settings: ({})
property bool popupOpen: false
function closePopout() { popupOpen = false }
function run(command) {
if (root.bar) root.bar.run(command)
}
function setting(name, fallback) {
var value = settings ? settings[name] : undefined
return value === undefined || value === null ? fallback : value
}
readonly property var sink: Pipewire.defaultAudioSink
readonly property real currentVolume: sink && sink.audio && isFinite(sink.audio.volume) ? sink.audio.volume : 0
readonly property bool sinkMuted: sink && sink.audio ? sink.audio.muted : false
PwObjectTracker { objects: root.sink ? [root.sink] : [] }
property int currentBrightness: -1
property int pendingBrightness: -1
property bool dndActive: false
property bool idleInhibited: false
property bool nightLightActive: false
property bool nightLightAvailable: false
property string themeName: ""
readonly property bool powerProfileAvailable: PowerProfiles.hasPerformanceProfile || PowerProfiles.profile === PowerProfile.PowerSaver || PowerProfiles.profile === PowerProfile.Balanced
readonly property int currentProfile: PowerProfiles.profile
function setVolume(value) {
if (!sink || !sink.audio) return
sink.audio.volume = Math.max(0, Math.min(1, value))
}
function toggleMute() {
if (!sink || !sink.audio) return
sink.audio.muted = !sink.audio.muted
}
function setBrightness(percent) {
var clamped = Math.max(1, Math.min(100, Math.round(percent)))
currentBrightness = clamped
pendingBrightness = clamped
brightnessWriteTimer.restart()
}
function refresh() {
if (!brightnessProc.running) brightnessProc.running = true
if (!dndProc.running) dndProc.running = true
if (!idleProc.running) idleProc.running = true
if (!nightLightProc.running) nightLightProc.running = true
if (!themeProc.running) themeProc.running = true
}
Component.onCompleted: refresh()
Process {
id: brightnessProc
command: ["bash", "-lc", "command -v brightnessctl >/dev/null || { echo missing; exit; }; cur=$(brightnessctl get); max=$(brightnessctl max); [[ $max -gt 0 ]] && echo $((cur*100/max)) || echo 0"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
var v = String(text || "").trim()
root.currentBrightness = v === "missing" ? -1 : (parseInt(v, 10) || 0)
}
}
}
Process {
id: brightnessWriteProc
}
Timer {
id: brightnessWriteTimer
interval: 60
repeat: false
onTriggered: {
if (brightnessWriteProc.running) { brightnessWriteTimer.restart(); return }
if (root.pendingBrightness < 0) return
brightnessWriteProc.command = ["bash", "-lc", "brightnessctl set " + root.pendingBrightness + "% >/dev/null"]
root.pendingBrightness = -1
brightnessWriteProc.running = true
}
}
Process {
id: dndProc
command: ["bash", "-lc", "if command -v makoctl >/dev/null && makoctl mode 2>/dev/null | grep -q '^do-not-disturb$'; then echo on; else echo off; fi"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.dndActive = String(text || "").trim() === "on"
}
}
Process {
id: idleProc
command: ["bash", "-lc", "if pgrep -x hypridle >/dev/null 2>&1; then echo running; else echo inhibited; fi"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.idleInhibited = String(text || "").trim() === "inhibited"
}
}
Process {
id: nightLightProc
command: ["bash", "-lc", "command -v hyprsunset >/dev/null || { echo missing; exit; }; if pgrep -x hyprsunset >/dev/null 2>&1; then hyprctl hyprsunset temperature 2>/dev/null | grep -oE '[0-9]+' | head -1; else echo idle; fi"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
var state = String(text || "").trim()
if (state === "missing") {
root.nightLightAvailable = false
root.nightLightActive = false
return
}
root.nightLightAvailable = true
var temp = parseInt(state, 10)
root.nightLightActive = !isNaN(temp) && temp < 6000
}
}
}
Process {
id: themeProc
command: ["bash", "-lc", "readlink ~/.config/omarchy/current/theme 2>/dev/null | xargs -r basename"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.themeName = String(text || "").trim()
}
}
Timer {
interval: 3000
running: true
repeat: true
triggeredOnStart: true
onTriggered: root.refresh()
}
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
Common.WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: "󰙪"
fontSize: 14
tooltipText: "Quick Settings"
onPressed: function() { root.popupOpen = !root.popupOpen }
}
Common.PopupCard {
id: popup
anchorItem: button
bar: root.bar
owner: root
open: root.popupOpen
contentWidth: 320
contentHeight: layout.implicitHeight + 28
Column {
id: layout
anchors.fill: parent
spacing: 14
Text {
text: "Quick settings"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 12
font.bold: true
}
Column {
width: parent.width
spacing: 10
visible: root.sink !== null
Row {
width: parent.width
spacing: 8
Common.PillButton {
iconText: root.sinkMuted ? "󰝟" : "󰕾"
foreground: root.bar.foreground
horizontalPadding: 8
verticalPadding: 6
iconSize: 16
onClicked: root.toggleMute()
}
Common.Slider {
bar: root.bar
width: parent.width - 90
value: root.currentVolume
minimum: 0
maximum: 1
step: 0.05
anchors.verticalCenter: parent.verticalCenter
onMoved: function(v) { root.setVolume(v) }
onReleased: function(v) { root.setVolume(v) }
}
Text {
text: Math.round(root.currentVolume * 100) + "%"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 11
anchors.verticalCenter: parent.verticalCenter
width: 32
horizontalAlignment: Text.AlignRight
}
}
Row {
width: parent.width
spacing: 8
visible: root.currentBrightness >= 0
Common.PillButton {
iconText: "󰃠"
foreground: root.bar.foreground
horizontalPadding: 8
verticalPadding: 6
iconSize: 16
}
Common.Slider {
bar: root.bar
width: parent.width - 90
value: Math.max(0, root.currentBrightness / 100)
minimum: 0.01
maximum: 1
step: 0.05
anchors.verticalCenter: parent.verticalCenter
onMoved: function(v) { root.setBrightness(v * 100) }
onReleased: function(v) { root.setBrightness(v * 100) }
}
Text {
text: root.currentBrightness + "%"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 11
anchors.verticalCenter: parent.verticalCenter
width: 32
horizontalAlignment: Text.AlignRight
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
}
Grid {
id: tileGrid
width: parent.width
columns: 2
columnSpacing: 8
rowSpacing: 8
Tile {
width: (tileGrid.width - tileGrid.columnSpacing) / 2
glyph: root.dndActive ? "󰂛" : "󰂚"
title: "Do Not Disturb"
subtitle: root.dndActive ? "On" : "Off"
active: root.dndActive
onClicked: { root.run("omarchy-toggle-notification-silencing"); dndProc.running = true }
}
Tile {
width: (tileGrid.width - tileGrid.columnSpacing) / 2
glyph: root.nightLightActive ? "󰖔" : "󰖙"
title: "Night Light"
subtitle: !root.nightLightAvailable ? "—" : (root.nightLightActive ? "On" : "Off")
active: root.nightLightActive
tileEnabled: root.nightLightAvailable
onClicked: { root.run("omarchy-toggle-nightlight"); nightLightProc.running = true }
}
Tile {
width: (tileGrid.width - tileGrid.columnSpacing) / 2
glyph: root.idleInhibited ? "󰅶" : "󰾪"
title: "Keep Awake"
subtitle: root.idleInhibited ? "On" : "Off"
active: root.idleInhibited
onClicked: { root.run("omarchy-toggle-idle"); idleProc.running = true }
}
Tile {
width: (tileGrid.width - tileGrid.columnSpacing) / 2
glyph: "󰔎"
title: "Theme"
subtitle: root.themeName || "—"
onClicked: { root.run("omarchy-menu themes"); root.popupOpen = false }
}
}
Rectangle {
visible: root.powerProfileAvailable
width: parent.width
height: 1
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
}
Common.PillButton {
width: parent.width
iconText: "󰙪"
text: "Customize bar…"
foreground: root.bar.foreground
horizontalPadding: 10
verticalPadding: 8
onClicked: { root.run("omarchy-launch-bar-settings"); root.popupOpen = false }
}
Row {
width: parent.width
spacing: 6
visible: root.powerProfileAvailable
Repeater {
model: [
{ profile: PowerProfile.PowerSaver, label: "Saver", glyph: "󰌪" },
{ profile: PowerProfile.Balanced, label: "Balanced", glyph: "󰗑" },
{ profile: PowerProfile.Performance, label: "Performance", glyph: "󰓅" }
]
Common.PillButton {
required property var modelData
width: (parent.width - 12) / 3
iconText: modelData.glyph
text: modelData.label
foreground: root.bar.foreground
horizontalPadding: 8
verticalPadding: 8
active: root.currentProfile === modelData.profile
enabled: modelData.profile !== PowerProfile.Performance || PowerProfiles.hasPerformanceProfile
opacity: enabled ? 1 : 0.4
onClicked: PowerProfiles.profile = modelData.profile
}
}
}
}
}
component Tile: Rectangle {
id: tile
property string glyph: ""
property string title: ""
property string subtitle: ""
property bool active: false
property bool tileEnabled: true
signal clicked()
implicitHeight: 56
radius: 6
color: tileArea.containsMouse
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.16)
: (active ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.10)
: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.04))
border.color: active ? root.bar.foreground : Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
border.width: 1
opacity: tileEnabled ? 1 : 0.4
Behavior on color { ColorAnimation { duration: 120 } }
Row {
anchors.fill: parent
anchors.margins: 10
spacing: 8
Text {
text: tile.glyph
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 18
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
width: parent.width - parent.children[0].implicitWidth - 8
Text {
text: tile.title
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 11
font.bold: true
elide: Text.ElideRight
width: parent.width
}
Text {
text: tile.subtitle
color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
font.pixelSize: 10
elide: Text.ElideRight
width: parent.width
}
}
}
MouseArea {
id: tileArea
anchors.fill: parent
hoverEnabled: true
cursorShape: tile.tileEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: tile.tileEnabled
onClicked: tile.clicked()
}
}
}
@@ -0,0 +1,65 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "../common" as Common
Item {
id: root
property QtObject bar: null
property string moduleName: "idleInhibitor"
property var settings: ({})
property bool active: false
readonly property string icon: active ? "󰛊" : "󰾪"
function refresh() {
if (!statusProc.running) statusProc.running = true
}
function toggle() {
if (root.bar) root.bar.run("omarchy-toggle-idle")
refreshTimer.restart()
}
Component.onCompleted: refresh()
Process {
id: statusProc
command: ["bash", "-lc", "if pgrep -x hyprlock >/dev/null 2>&1; then echo locked; elif pgrep -f 'systemd-inhibit' >/dev/null 2>&1; then echo inhibited; elif [[ -f /tmp/omarchy-idle-off ]]; then echo off; else echo idle; fi"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
var state = String(text || "").trim()
root.active = state === "inhibited" || state === "off"
}
}
}
Timer {
id: refreshTimer
interval: 1500
onTriggered: root.refresh()
}
Timer {
interval: 5000
running: true
repeat: true
onTriggered: root.refresh()
}
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
Common.WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.icon
active: root.active
tooltipText: root.active ? "Idle inhibited — click to allow sleep" : "System can idle — click to keep awake"
onPressed: function() { root.toggle() }
}
}
@@ -0,0 +1,84 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import "../common" as Common
Item {
id: root
property QtObject bar: null
property string moduleName: "keyboardLayout"
property var settings: ({})
property string layoutLabel: ""
property string layoutFull: ""
function setting(name, fallback) {
var value = settings ? settings[name] : undefined
return value === undefined || value === null ? fallback : value
}
function refresh() {
if (!queryProc.running) queryProc.running = true
}
function cycleLayout() {
Hyprland.dispatch("switchxkblayout current next")
refreshTimer.restart()
}
Component.onCompleted: refresh()
Connections {
target: Hyprland
function onRawEvent(event) {
if (!event || !event.name) return
if (String(event.name).indexOf("activelayout") !== -1) root.refresh()
}
}
Process {
id: queryProc
command: ["bash", "-lc", "hyprctl -j devices 2>/dev/null | sed -n '/keyboards/,$p' | head -200"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
var match = String(text || "").match(/"active_keymap":\s*"([^"]+)"/)
if (!match) return
var full = match[1]
root.layoutFull = full
var token = full.split(/\s+/)[0]
root.layoutLabel = token.substring(0, 3).toUpperCase()
}
}
}
Timer {
id: refreshTimer
interval: 600
onTriggered: root.refresh()
}
Timer {
interval: 10000
running: true
repeat: true
onTriggered: root.refresh()
}
visible: layoutLabel !== ""
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
Common.WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.layoutLabel
fontSize: 10
horizontalMargin: 6
tooltipText: root.layoutFull
onPressed: function() { root.cycleLayout() }
}
}
@@ -0,0 +1,101 @@
import QtQuick
import Quickshell
import Quickshell.Io
Item {
id: root
property QtObject bar: null
property string moduleName: "lockKeys"
property var settings: ({})
property bool capsOn: false
property bool numOn: false
property bool scrollOn: false
property bool hideWhenOff: true
function setting(name, fallback) {
var value = settings ? settings[name] : undefined
return value === undefined || value === null ? fallback : value
}
Component.onCompleted: {
hideWhenOff = setting("hideWhenOff", true) === true
refresh()
}
function refresh() {
if (!stateProc.running) stateProc.running = true
}
property bool ledsAvailable: true
Process {
id: stateProc
command: ["bash", "-lc", "read_led() { for path in /sys/class/leds/input*::$1; do if [[ -r $path/brightness ]]; then cat $path/brightness; return; fi; done; echo missing; }; read_led capslock; read_led numlock; read_led scrolllock"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
var lines = String(text || "").split("\n")
var caps = String(lines[0] || "").trim()
var num = String(lines[1] || "").trim()
var scroll = String(lines[2] || "").trim()
root.capsOn = caps !== "missing" && parseInt(caps, 10) > 0
root.numOn = num !== "missing" && parseInt(num, 10) > 0
root.scrollOn = scroll !== "missing" && parseInt(scroll, 10) > 0
root.ledsAvailable = caps !== "missing" || num !== "missing" || scroll !== "missing"
}
}
}
Timer {
interval: 2000
running: root.ledsAvailable
repeat: true
onTriggered: root.refresh()
}
readonly property bool anyOn: capsOn || numOn || scrollOn
visible: ledsAvailable && (hideWhenOff ? anyOn : true)
readonly property bool vertical: bar ? bar.vertical : false
implicitWidth: vertical ? (bar ? bar.barSize : 28) : (lay.item ? lay.item.implicitWidth + 8 : 0)
implicitHeight: vertical ? (lay.item ? lay.item.implicitHeight + 8 : 0) : (bar ? bar.barSize : 26)
Loader {
id: lay
anchors.centerIn: parent
sourceComponent: root.vertical ? colLayout : rowLayout
}
Component {
id: rowLayout
Row {
spacing: 4
LockGlyph { glyph: "A"; active: root.capsOn; visible: !root.hideWhenOff || root.capsOn }
LockGlyph { glyph: "1"; active: root.numOn; visible: !root.hideWhenOff || root.numOn }
LockGlyph { glyph: "S"; active: root.scrollOn; visible: !root.hideWhenOff || root.scrollOn }
}
}
Component {
id: colLayout
Column {
spacing: 2
LockGlyph { glyph: "A"; active: root.capsOn; visible: !root.hideWhenOff || root.capsOn }
LockGlyph { glyph: "1"; active: root.numOn; visible: !root.hideWhenOff || root.numOn }
LockGlyph { glyph: "S"; active: root.scrollOn; visible: !root.hideWhenOff || root.scrollOn }
}
}
component LockGlyph: Text {
property string glyph: ""
property bool active: false
text: glyph
color: active ? (root.bar ? root.bar.foreground : "#cacccc") : Qt.rgba(0.7, 0.7, 0.7, 0.3)
font.family: root.bar ? root.bar.fontFamily : "JetBrainsMono Nerd Font"
font.pixelSize: 11
}
}
@@ -0,0 +1,233 @@
import QtQuick
import Quickshell
import Quickshell.Services.Mpris
import "../common" as Common
Item {
id: root
property QtObject bar: null
property string moduleName: "media"
property var settings: ({})
function setting(name, fallback) {
var value = settings ? settings[name] : undefined
return value === undefined || value === null ? fallback : value
}
readonly property var players: Mpris.players ? Mpris.players.values : []
readonly property var activePlayer: {
var playing = null
for (var i = 0; i < players.length; i++) {
var p = players[i]
if (!p) continue
if (p.isPlaying) return p
if (!playing && p.trackTitle) playing = p
}
return playing
}
readonly property bool hasMedia: activePlayer !== null && (activePlayer.trackTitle || activePlayer.trackArtist)
readonly property string playIcon: activePlayer && activePlayer.isPlaying ? "󰏤" : "󰐊"
readonly property string title: activePlayer ? (activePlayer.trackTitle || "") : ""
readonly property string artist: activePlayer ? (activePlayer.trackArtist || "") : ""
property bool popupOpen: false
function closePopout() { popupOpen = false }
property real maxLabelWidth: 180
visible: hasMedia
implicitWidth: hasMedia ? row.implicitWidth + 14 : 0
implicitHeight: bar ? bar.barSize : 26
Row {
id: row
anchors.centerIn: parent
spacing: 6
Text {
id: glyph
anchors.verticalCenter: parent.verticalCenter
text: root.playIcon
color: activePlayer && activePlayer.isPlaying ? root.bar.foreground : Qt.darker(root.bar.foreground, 1.5)
font.family: root.bar.fontFamily
font.pixelSize: 12
Behavior on color { ColorAnimation { duration: 160 } }
}
Item {
id: scrollClip
width: Math.min(root.maxLabelWidth, labelText.implicitWidth)
height: glyph.height
clip: true
anchors.verticalCenter: parent.verticalCenter
visible: !root.bar.vertical && root.title !== ""
Text {
id: labelText
text: root.title + (root.artist ? " · " + root.artist : "")
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 12
anchors.verticalCenter: parent.verticalCenter
property bool needsScroll: implicitWidth > scrollClip.width
NumberAnimation on x {
id: scrollAnim
running: labelText.needsScroll && !root.popupOpen && !root.bar.vertical
loops: Animation.Infinite
duration: Math.max(6000, labelText.implicitWidth * 25)
from: scrollClip.width
to: -labelText.implicitWidth
easing.type: Easing.Linear
}
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: function(mouse) {
if (!root.activePlayer) return
if (mouse.button === Qt.MiddleButton) {
if (root.activePlayer.canGoNext) root.activePlayer.next()
} else if (mouse.button === Qt.RightButton) {
root.popupOpen = !root.popupOpen
} else {
if (root.activePlayer.canTogglePlaying) root.activePlayer.togglePlaying()
}
}
onWheel: function(wheel) {
if (!root.activePlayer) return
if (wheel.angleDelta.y > 0 && root.activePlayer.canGoPrevious) root.activePlayer.previous()
else if (wheel.angleDelta.y < 0 && root.activePlayer.canGoNext) root.activePlayer.next()
}
onEntered: if (root.bar) root.bar.showTooltip(root, root.hasMedia ? (root.title + (root.artist ? " — " + root.artist : "")) : "")
onExited: if (root.bar) root.bar.hideTooltip(root)
}
Common.PopupCard {
id: popup
anchorItem: root
bar: root.bar
owner: root
open: root.popupOpen
contentWidth: 320
contentHeight: column.implicitHeight + 28
Column {
id: column
anchors.fill: parent
spacing: 10
Row {
spacing: 10
width: parent.width
Rectangle {
width: 64
height: 64
radius: 4
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.08)
border.color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.2)
border.width: 1
Image {
anchors.fill: parent
anchors.margins: 2
fillMode: Image.PreserveAspectCrop
asynchronous: true
source: root.activePlayer && root.activePlayer.trackArtUrl ? root.activePlayer.trackArtUrl : ""
visible: source !== ""
}
Text {
anchors.centerIn: parent
visible: !root.activePlayer || !root.activePlayer.trackArtUrl
text: "󰝚"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 28
}
}
Column {
spacing: 4
width: parent.width - 74
Text {
text: root.title || "Nothing playing"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 13
font.bold: true
elide: Text.ElideRight
width: parent.width
}
Text {
text: root.artist
color: Qt.darker(root.bar.foreground, 1.3)
font.family: root.bar.fontFamily
font.pixelSize: 11
elide: Text.ElideRight
width: parent.width
visible: text !== ""
}
Text {
text: root.activePlayer && root.activePlayer.trackAlbum ? root.activePlayer.trackAlbum : ""
color: Qt.darker(root.bar.foreground, 1.6)
font.family: root.bar.fontFamily
font.pixelSize: 10
elide: Text.ElideRight
width: parent.width
visible: text !== ""
}
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 6
Common.PillButton {
iconText: "󰒮"
foreground: root.bar.foreground
horizontalPadding: 10
verticalPadding: 6
enabled: root.activePlayer && root.activePlayer.canGoPrevious
opacity: enabled ? 1.0 : 0.4
onClicked: if (root.activePlayer) root.activePlayer.previous()
}
Common.PillButton {
iconText: root.activePlayer && root.activePlayer.isPlaying ? "󰏤" : "󰐊"
foreground: root.bar.foreground
horizontalPadding: 14
verticalPadding: 6
iconSize: 18
enabled: root.activePlayer && root.activePlayer.canTogglePlaying
opacity: enabled ? 1.0 : 0.4
onClicked: if (root.activePlayer) root.activePlayer.togglePlaying()
}
Common.PillButton {
iconText: "󰒭"
foreground: root.bar.foreground
horizontalPadding: 10
verticalPadding: 6
enabled: root.activePlayer && root.activePlayer.canGoNext
opacity: enabled ? 1.0 : 0.4
onClicked: if (root.activePlayer) root.activePlayer.next()
}
}
}
}
}
@@ -0,0 +1,56 @@
import QtQuick
import Quickshell
import Quickshell.Services.Pipewire
import "../common" as Common
Item {
id: root
property QtObject bar: null
property string moduleName: "microphone"
property var settings: ({})
readonly property var source: Pipewire.defaultAudioSource
readonly property bool muted: source && source.audio ? source.audio.muted : true
readonly property real volume: source && source.audio ? source.audio.volume : 0
readonly property var nodes: Pipewire.nodes ? Pipewire.nodes.values : []
readonly property var activeStreams: {
var list = []
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i]
if (node && node.isStream && node.isSink === false && !node.audio?.muted) list.push(node)
}
return list
}
readonly property bool inUse: activeStreams.length > 0 && !muted
visible: source !== null
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
function toggleMute() {
if (source && source.audio) source.audio.muted = !source.audio.muted
}
PwObjectTracker { objects: root.source ? [root.source] : [] }
Common.WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.muted ? "󰍭" : "󰍬"
active: root.inUse
tooltipText: root.muted ? "Microphone muted" : (root.inUse ? "Microphone in use" : "Microphone live")
onPressed: function(b) {
if (b === Qt.MiddleButton) root.bar.run("omarchy-launch-audio")
else root.toggleMute()
}
onWheelMoved: function(delta) {
if (!root.source || !root.source.audio) return
var step = 0.05
root.source.audio.volume = Math.max(0, Math.min(1, root.volume + (delta > 0 ? step : -step)))
}
}
}
@@ -0,0 +1,219 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "../common" as Common
Item {
id: root
property QtObject bar: null
property string moduleName: "networkPanel"
property var settings: ({})
property bool popupOpen: false
function closePopout() { popupOpen = false }
property var networks: []
property bool scanning: false
readonly property string kind: bar ? bar.networkKind : "disconnected"
readonly property string label: bar ? bar.networkLabel : ""
readonly property int signalStrength: bar ? bar.networkSignal : -1
readonly property string icon: {
if (kind === "wifi") {
var icons = ["󰤯", "󰤟", "󰤢", "󰤥", "󰤨"]
var index = Math.max(0, Math.min(4, Math.ceil(signalStrength / 20) - 1))
return icons[index]
}
if (kind === "ethernet") return "󰈀"
return "󰤮"
}
function refresh() {
if (!scanProc.running) {
scanning = true
scanProc.running = true
}
}
function updateScan(raw) {
var list = []
var lines = String(raw || "").split("\n")
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim()
if (!line) continue
var parts = line.split("\t")
if (parts.length < 3) continue
list.push({
inUse: parts[0] === "*",
ssid: parts[1],
signalStrength: parseInt(parts[2], 10) || 0,
security: parts[3] || ""
})
}
list.sort(function(a, b) {
if (a.inUse !== b.inUse) return a.inUse ? -1 : 1
return b.signalStrength - a.signalStrength
})
networks = list
scanning = false
}
function wifiIconFor(signalStrength) {
var icons = ["󰤯", "󰤟", "󰤢", "󰤥", "󰤨"]
var index = Math.max(0, Math.min(4, Math.ceil(signalStrength / 20) - 1))
return icons[index]
}
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
Component.onCompleted: refresh()
Process {
id: scanProc
command: ["bash", "-lc", "command -v nmcli >/dev/null && nmcli -t -f IN-USE,SSID,SIGNAL,SECURITY device wifi list --rescan no 2>/dev/null"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.updateScan(text)
}
}
Process { id: actionProc }
Common.WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.icon
horizontalMargin: 6.5
tooltipText: bar ? bar.networkTooltip() : ""
onPressed: function(b) {
if (b === Qt.RightButton) root.bar.run("OMARCHY_PATH=" + root.bar.shellQuote(root.bar.omarchyPath) + " " + root.bar.omarchyPath + "/bin/omarchy-launch-wifi")
else {
root.popupOpen = !root.popupOpen
if (root.popupOpen) root.refresh()
}
}
}
Common.PopupCard {
anchorItem: button
owner: root
bar: root.bar
open: root.popupOpen
contentWidth: 320
contentHeight: column.implicitHeight + 28
Column {
id: column
anchors.fill: parent
spacing: 8
Row {
width: parent.width
spacing: 8
Text {
text: root.icon
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 16
anchors.verticalCenter: parent.verticalCenter
}
Column {
spacing: 2
width: parent.width - 100
anchors.verticalCenter: parent.verticalCenter
Text {
text: root.kind === "disconnected" ? "Disconnected" : (root.label || root.kind)
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 12
font.bold: true
elide: Text.ElideRight
width: parent.width
}
Text {
visible: root.kind !== "disconnected"
text: root.kind === "wifi" ? "Wi-Fi · " + root.signalStrength + "%" : "Ethernet"
color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
font.pixelSize: 10
}
}
Common.PillButton {
iconText: "󰑐"
foreground: root.bar.foreground
horizontalPadding: 8
verticalPadding: 4
active: root.scanning
onClicked: {
if (scanProc.running) return
scanProc.command = ["bash", "-lc", "command -v nmcli >/dev/null && nmcli device wifi rescan 2>/dev/null; nmcli -t -f IN-USE,SSID,SIGNAL,SECURITY device wifi list --rescan no 2>/dev/null"]
root.refresh()
}
}
}
Rectangle {
width: parent.width
height: 1
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.15)
}
Column {
spacing: 2
width: parent.width
Repeater {
model: root.networks.slice(0, 6)
Common.PillButton {
required property var modelData
width: parent.width
iconText: root.wifiIconFor(modelData.signalStrength)
text: (modelData.ssid || "Hidden") + (modelData.security ? " · " : "") + (modelData.security || "")
foreground: root.bar.foreground
horizontalPadding: 10
verticalPadding: 6
active: modelData.inUse
onClicked: {
if (modelData.inUse) return
if (actionProc.running) return
actionProc.command = ["bash", "-lc", "command -v nmcli >/dev/null && nmcli device wifi connect " + root.bar.shellQuote(modelData.ssid)]
actionProc.running = true
root.popupOpen = false
}
}
}
Text {
visible: root.networks.length === 0
text: root.scanning ? "Scanning…" : "No networks found"
color: Qt.darker(root.bar.foreground, 1.5)
font.family: root.bar.fontFamily
font.pixelSize: 11
}
}
Common.PillButton {
width: parent.width
iconText: "󰖩"
text: "Open Wi-Fi manager"
foreground: root.bar.foreground
horizontalPadding: 10
verticalPadding: 6
onClicked: { root.bar.run(root.bar.omarchyPath + "/bin/omarchy-launch-wifi"); root.popupOpen = false }
}
}
}
}
@@ -0,0 +1,84 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "../common" as Common
Item {
id: root
property QtObject bar: null
property string moduleName: "nightLight"
property var settings: ({})
property bool active: false
property bool toolAvailable: false
property bool toggling: false
readonly property int onTemp: 4000
readonly property int offTemp: 6000
function setting(name, fallback) {
var value = settings ? settings[name] : undefined
return value === undefined || value === null ? fallback : value
}
function refresh() {
if (!statusProc.running) statusProc.running = true
}
function toggle() {
if (toggling) return
toggling = true
if (root.bar) root.bar.run("omarchy-toggle-nightlight")
refreshTimer.restart()
}
Component.onCompleted: refresh()
Process {
id: statusProc
command: ["bash", "-lc", "command -v hyprsunset >/dev/null || { echo missing; exit; }; if pgrep -x hyprsunset >/dev/null 2>&1; then hyprctl hyprsunset temperature 2>/dev/null | grep -oE '[0-9]+' | head -1; else echo idle; fi"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: {
var state = String(text || "").trim()
root.toggling = false
if (state === "missing") {
root.toolAvailable = false
root.active = false
return
}
root.toolAvailable = true
var temp = parseInt(state, 10)
root.active = !isNaN(temp) && temp < root.offTemp
}
}
}
Timer {
id: refreshTimer
interval: 1500
onTriggered: root.refresh()
}
Timer {
interval: 10000
running: true
repeat: true
onTriggered: root.refresh()
}
visible: toolAvailable
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
Common.WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.active ? "󰖔" : "󰖙"
active: root.active
tooltipText: root.active ? "Night light on" : "Night light off"
onPressed: function() { root.toggle() }
}
}
@@ -0,0 +1,255 @@
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
import "../common" as Common
Item {
id: root
property QtObject bar: null
property string moduleName: "notificationCenter"
property var settings: ({})
property bool popupOpen: false
function closePopout() { popupOpen = false }
property var stored: []
property bool dnd: false
readonly property int count: stored.length
readonly property string icon: {
if (dnd) return "󰂛"
if (count > 0) return "󱅫"
return "󰂚"
}
property bool replaceMako: settings && settings.replaceMako === true
Loader {
active: root.replaceMako
sourceComponent: serverComponent
}
Component {
id: serverComponent
NotificationServer {
id: server
keepOnReload: false
bodySupported: true
actionsSupported: true
imageSupported: true
onNotification: function(notification) {
if (root.dnd) {
notification.expire()
return
}
notification.tracked = true
var snapshot = {
id: notification.id,
app: notification.appName,
summary: notification.summary,
body: notification.body,
time: new Date(),
ref: notification
}
var next = root.stored.slice()
next.unshift(snapshot)
if (next.length > 30) next.pop()
root.stored = next
}
}
}
function dismiss(index) {
var item = stored[index]
if (item && item.ref && !item.ref.closed) item.ref.dismiss()
var next = stored.slice()
next.splice(index, 1)
stored = next
}
function clearAll() {
for (var i = 0; i < stored.length; i++) {
if (stored[i].ref && !stored[i].ref.closed) stored[i].ref.dismiss()
}
stored = []
}
function relativeTime(date) {
if (!date) return ""
var diff = (Date.now() - date.getTime()) / 1000
if (diff < 60) return "just now"
if (diff < 3600) return Math.floor(diff / 60) + "m"
if (diff < 86400) return Math.floor(diff / 3600) + "h"
return Math.floor(diff / 86400) + "d"
}
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
Common.WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.icon
active: root.count > 0 && !root.dnd
tooltipText: root.dnd ? "Do Not Disturb" : (root.count > 0 ? root.count + " notifications" : "No notifications")
onPressed: function(b) {
if (b === Qt.RightButton) root.dnd = !root.dnd
else root.popupOpen = !root.popupOpen
}
}
Common.PopupCard {
anchorItem: button
owner: root
bar: root.bar
open: root.popupOpen
contentWidth: 340
contentHeight: Math.min(420, listColumn.implicitHeight + 60)
Column {
id: listColumn
anchors.fill: parent
spacing: 8
Row {
width: parent.width
spacing: 8
Text {
text: "Notifications"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 13
font.bold: true
anchors.verticalCenter: parent.verticalCenter
}
Item { width: parent.width - 220; height: 1 }
Common.PillButton {
iconText: root.dnd ? "󰂛" : ""
text: root.dnd ? "DND" : ""
foreground: root.bar.foreground
horizontalPadding: 8
verticalPadding: 4
active: root.dnd
onClicked: root.dnd = !root.dnd
}
Common.PillButton {
iconText: "󰎟"
foreground: root.bar.foreground
horizontalPadding: 8
verticalPadding: 4
enabled: root.count > 0
opacity: enabled ? 1 : 0.4
onClicked: root.clearAll()
}
}
Flickable {
width: parent.width
height: Math.min(320, contentHeight)
contentHeight: feedColumn.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
Column {
id: feedColumn
width: parent.width
spacing: 4
Repeater {
model: root.stored
Rectangle {
required property var modelData
required property int index
width: feedColumn.width
radius: 4
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.06)
implicitHeight: notifContent.implicitHeight + 16
Column {
id: notifContent
anchors.fill: parent
anchors.margins: 8
spacing: 2
Row {
width: parent.width
Text {
text: modelData ? (modelData.app || "App") : ""
color: Qt.darker(root.bar.foreground, 1.4)
font.family: root.bar.fontFamily
font.pixelSize: 10
font.bold: true
elide: Text.ElideRight
width: parent.width - timeText.implicitWidth - dismissBtn.width - 12
}
Text {
id: timeText
text: modelData ? root.relativeTime(modelData.time) : ""
color: Qt.darker(root.bar.foreground, 1.6)
font.family: root.bar.fontFamily
font.pixelSize: 10
}
Item { width: 6; height: 1 }
Common.PillButton {
id: dismissBtn
iconText: "󰅖"
foreground: root.bar.foreground
horizontalPadding: 4
verticalPadding: 0
iconSize: 10
onClicked: root.dismiss(index)
}
}
Text {
text: modelData ? (modelData.summary || "") : ""
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 11
font.bold: true
wrapMode: Text.WordWrap
width: parent.width
}
Text {
visible: modelData && modelData.body !== ""
text: modelData ? (modelData.body || "") : ""
color: Qt.darker(root.bar.foreground, 1.2)
font.family: root.bar.fontFamily
font.pixelSize: 10
wrapMode: Text.WordWrap
width: parent.width
maximumLineCount: 3
elide: Text.ElideRight
}
}
}
}
}
}
Text {
visible: root.stored.length === 0
text: root.dnd ? "Do Not Disturb is on" : "Nothing new"
color: Qt.darker(root.bar.foreground, 1.5)
font.family: root.bar.fontFamily
font.pixelSize: 11
}
}
}
}
@@ -0,0 +1,106 @@
import QtQuick
import Quickshell
import "../common" as Common
Item {
id: root
property QtObject bar: null
property string moduleName: "powerMenu"
property var settings: ({})
property bool popupOpen: false
function closePopout() { popupOpen = false }
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
function run(command) {
if (root.bar) root.bar.run(command)
popupOpen = false
}
Common.WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: "󰐥"
fontSize: 14
tooltipText: "Power menu"
onPressed: function() { root.popupOpen = !root.popupOpen }
}
Common.PopupCard {
anchorItem: button
owner: root
bar: root.bar
open: root.popupOpen
contentWidth: 220
contentHeight: column.implicitHeight + 28
Column {
id: column
anchors.fill: parent
spacing: 6
Text {
text: "Power"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 12
font.bold: true
}
Common.PillButton {
width: parent.width
iconText: "󰌾"
text: "Lock"
foreground: root.bar.foreground
horizontalPadding: 10
verticalPadding: 8
onClicked: root.run("loginctl lock-session")
}
Common.PillButton {
width: parent.width
iconText: "󰒲"
text: "Suspend"
foreground: root.bar.foreground
horizontalPadding: 10
verticalPadding: 8
onClicked: root.run("systemctl suspend")
}
Common.PillButton {
width: parent.width
iconText: "󰍃"
text: "Log out"
foreground: root.bar.foreground
horizontalPadding: 10
verticalPadding: 8
onClicked: root.run("hyprctl dispatch exit")
}
Common.PillButton {
width: parent.width
iconText: "󰜉"
text: "Reboot"
foreground: root.bar.foreground
horizontalPadding: 10
verticalPadding: 8
onClicked: root.run("systemctl reboot")
}
Common.PillButton {
width: parent.width
iconText: "󰐥"
text: "Shut down"
foreground: root.bar.urgent
horizontalPadding: 10
verticalPadding: 8
onClicked: root.run("systemctl poweroff")
}
}
}
}
@@ -0,0 +1,94 @@
import QtQuick
import Quickshell
import Quickshell.Services.UPower
import "../common" as Common
Item {
id: root
property QtObject bar: null
property string moduleName: "powerProfile"
property var settings: ({})
property bool popupOpen: false
function closePopout() { popupOpen = false }
readonly property var profileGlyphs: ({
[PowerProfile.PowerSaver]: "󰌪",
[PowerProfile.Balanced]: "󰗑",
[PowerProfile.Performance]: "󰓅"
})
readonly property var profileLabels: ({
[PowerProfile.PowerSaver]: "Power Saver",
[PowerProfile.Balanced]: "Balanced",
[PowerProfile.Performance]: "Performance"
})
readonly property bool available: PowerProfiles.hasPerformanceProfile || PowerProfiles.profile === PowerProfile.PowerSaver || PowerProfiles.profile === PowerProfile.Balanced
readonly property int current: PowerProfiles.profile
visible: available
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
function setProfile(profile) {
PowerProfiles.profile = profile
}
Common.WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.profileGlyphs[root.current] || ""
tooltipText: "Power profile: " + (root.profileLabels[root.current] || "Unknown")
onPressed: function() { root.popupOpen = !root.popupOpen }
}
Common.PopupCard {
anchorItem: button
owner: root
bar: root.bar
open: root.popupOpen
contentWidth: 240
contentHeight: column.implicitHeight + 28
Column {
id: column
anchors.fill: parent
spacing: 6
Text {
text: "Power Profile"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 12
font.bold: true
}
Repeater {
model: [
{ profile: PowerProfile.PowerSaver, label: "Power Saver", glyph: "󰌪" },
{ profile: PowerProfile.Balanced, label: "Balanced", glyph: "󰗑" },
{ profile: PowerProfile.Performance, label: "Performance", glyph: "󰓅" }
]
Common.PillButton {
required property var modelData
width: parent.width
iconText: modelData.glyph
text: modelData.label
foreground: root.bar.foreground
horizontalPadding: 10
verticalPadding: 8
active: root.current === modelData.profile
enabled: modelData.profile !== PowerProfile.Performance || PowerProfiles.hasPerformanceProfile
opacity: enabled ? 1 : 0.4
onClicked: { root.setProfile(modelData.profile); root.popupOpen = false }
}
}
}
}
}
@@ -0,0 +1,16 @@
import QtQuick
Item {
id: root
property QtObject bar: null
property string moduleName: "spacer"
property var settings: ({})
readonly property bool vertical: bar ? bar.vertical : false
readonly property int span: settings && settings.size !== undefined ? Number(settings.size) : 12
implicitWidth: vertical ? (bar ? bar.barSize : 28) : span
implicitHeight: vertical ? span : (bar ? bar.barSize : 26)
visible: span > 0
}
@@ -0,0 +1,405 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "../common" as Common
Item {
id: root
property QtObject bar: null
property string moduleName: "systemStats"
property var settings: ({})
property real cpuPercent: 0
property real memPercent: 0
property var cpuHistory: []
property var memHistory: []
property real loadAvg: 0
property var prevCpu: ({ idle: 0, total: 0 })
property bool popupOpen: false
function closePopout() { popupOpen = false }
readonly property int historyLimit: 30
function refresh() {
if (!cpuProc.running) cpuProc.running = true
if (!memProc.running) memProc.running = true
}
function pushHistory(arr, value) {
var next = arr.slice()
next.push(value)
if (next.length > historyLimit) next.shift()
return next
}
function updateCpu(raw) {
var fields = String(raw || "").trim().split(/\s+/)
if (fields.length < 8) return
var user = parseInt(fields[1], 10) || 0
var nice = parseInt(fields[2], 10) || 0
var sys = parseInt(fields[3], 10) || 0
var idle = parseInt(fields[4], 10) || 0
var iowait = parseInt(fields[5], 10) || 0
var irq = parseInt(fields[6], 10) || 0
var softirq = parseInt(fields[7], 10) || 0
var total = user + nice + sys + idle + iowait + irq + softirq
var totalDiff = total - prevCpu.total
var idleDiff = idle - prevCpu.idle
if (prevCpu.total > 0 && totalDiff > 0) {
var usage = (1 - idleDiff / totalDiff) * 100
cpuPercent = Math.max(0, Math.min(100, usage))
cpuHistory = pushHistory(cpuHistory, cpuPercent)
}
prevCpu = { idle: idle, total: total }
}
function updateMem(raw) {
var lines = String(raw || "").split("\n")
var total = 0
var available = 0
for (var i = 0; i < lines.length; i++) {
var line = lines[i]
if (line.indexOf("MemTotal:") === 0) total = parseInt(line.replace(/[^0-9]/g, ""), 10) || 0
else if (line.indexOf("MemAvailable:") === 0) available = parseInt(line.replace(/[^0-9]/g, ""), 10) || 0
}
if (total > 0) {
memPercent = ((total - available) / total) * 100
memHistory = pushHistory(memHistory, memPercent)
}
}
function updateLoad(raw) {
var n = parseFloat(String(raw || "").trim().split(/\s+/)[0])
if (!isNaN(n)) loadAvg = n
}
Component.onCompleted: refresh()
Process {
id: cpuProc
command: ["bash", "-lc", "head -n1 /proc/stat"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.updateCpu(text)
}
}
Process {
id: memProc
command: ["bash", "-lc", "head -n3 /proc/meminfo"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.updateMem(text)
}
}
Process {
id: loadProc
command: ["bash", "-lc", "cat /proc/loadavg"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.updateLoad(text)
}
}
Timer {
interval: 2000
running: true
repeat: true
onTriggered: {
root.refresh()
if (!loadProc.running) loadProc.running = true
}
}
readonly property bool vertical: bar ? bar.vertical : false
implicitWidth: vertical ? (bar ? bar.barSize : 28) : (statLayout.item ? statLayout.item.implicitWidth + 6 : 0)
implicitHeight: vertical ? (statLayout.item ? statLayout.item.implicitHeight + 6 : 0) : (bar ? bar.barSize : 26)
readonly property color statColor: bar ? bar.foreground : "#cacccc"
readonly property string statFont: bar ? bar.fontFamily : "JetBrainsMono Nerd Font"
Loader {
id: statLayout
anchors.centerIn: parent
sourceComponent: root.vertical ? statColumn : statRow
}
Component {
id: statRow
Row {
spacing: 8
StatPill {
glyph: "󰻠"
percent: root.cpuPercent
history: root.cpuHistory
vertical: false
barFg: root.statColor
fontFamily: root.statFont
}
StatPill {
glyph: "󰍛"
percent: root.memPercent
history: root.memHistory
vertical: false
barFg: root.statColor
fontFamily: root.statFont
}
}
}
Component {
id: statColumn
Column {
spacing: 4
StatPill {
glyph: "󰻠"
percent: root.cpuPercent
history: root.cpuHistory
vertical: true
barFg: root.statColor
fontFamily: root.statFont
}
StatPill {
glyph: "󰍛"
percent: root.memPercent
history: root.memHistory
vertical: true
barFg: root.statColor
fontFamily: root.statFont
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function(mouse) {
if (mouse.button === Qt.RightButton) root.bar.run("alacritty")
else root.popupOpen = !root.popupOpen
}
onEntered: if (root.bar) root.bar.showTooltip(root, "CPU " + Math.round(root.cpuPercent) + "% · Mem " + Math.round(root.memPercent) + "% · Load " + root.loadAvg.toFixed(2))
onExited: if (root.bar) root.bar.hideTooltip(root)
}
Common.PopupCard {
anchorItem: root
owner: root
bar: root.bar
open: root.popupOpen
contentWidth: 320
contentHeight: detailColumn.implicitHeight + 28
Column {
id: detailColumn
anchors.fill: parent
spacing: 10
Text {
text: "System"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 12
font.bold: true
}
DetailStat {
title: "CPU"
value: Math.round(root.cpuPercent) + "%"
history: root.cpuHistory
barFg: root.statColor
fontFamily: root.bar.fontFamily
width: parent.width
}
DetailStat {
title: "Memory"
value: Math.round(root.memPercent) + "%"
history: root.memHistory
barFg: root.statColor
fontFamily: root.bar.fontFamily
width: parent.width
}
Row {
width: parent.width
spacing: 6
Text {
text: "Load"
color: Qt.darker(root.bar.foreground, 1.5)
font.family: root.bar.fontFamily
font.pixelSize: 11
}
Text {
text: root.loadAvg.toFixed(2)
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 11
}
}
Common.PillButton {
width: parent.width
iconText: "󰆍"
text: "Open btop"
foreground: root.bar.foreground
horizontalPadding: 10
verticalPadding: 8
onClicked: { root.bar.run("omarchy-launch-or-focus-tui btop"); root.popupOpen = false }
}
}
}
component StatPill: Item {
id: pill
property string glyph: ""
property real percent: 0
property var history: []
property bool vertical: false
property color barFg: "#cacccc"
property string fontFamily: "JetBrainsMono Nerd Font"
implicitWidth: vertical ? 22 : 56
implicitHeight: vertical ? 32 : 22
Row {
visible: !pill.vertical
anchors.fill: parent
spacing: 4
Text {
text: pill.glyph
color: pill.barFg
font.family: pill.fontFamily
font.pixelSize: 12
anchors.verticalCenter: parent.verticalCenter
}
Canvas {
id: spark
width: 36
height: 14
anchors.verticalCenter: parent.verticalCenter
property var history: pill.history
onHistoryChanged: requestPaint()
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
if (!pill.history || pill.history.length === 0) return
ctx.strokeStyle = pill.barFg
ctx.fillStyle = Qt.rgba(pill.barFg.r, pill.barFg.g, pill.barFg.b, 0.2)
ctx.lineWidth = 1
ctx.beginPath()
var step = width / Math.max(1, pill.history.length - 1)
for (var i = 0; i < pill.history.length; i++) {
var x = i * step
var y = height - (pill.history[i] / 100) * height
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
}
ctx.stroke()
ctx.lineTo(width, height)
ctx.lineTo(0, height)
ctx.closePath()
ctx.fill()
}
}
}
Column {
visible: pill.vertical
anchors.fill: parent
spacing: 2
Text {
text: pill.glyph
color: pill.barFg
font.family: pill.fontFamily
font.pixelSize: 10
anchors.horizontalCenter: parent.horizontalCenter
}
Text {
text: Math.round(pill.percent) + ""
color: pill.barFg
font.family: pill.fontFamily
font.pixelSize: 9
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
component DetailStat: Column {
id: detail
property string title: ""
property string value: ""
property var history: []
property color barFg: "#cacccc"
property string fontFamily: "JetBrainsMono Nerd Font"
spacing: 4
Row {
width: parent.width
Text {
text: detail.title
color: Qt.darker(detail.barFg, 1.4)
font.family: detail.fontFamily
font.pixelSize: 11
}
Item { width: detail.width - parent.children[0].implicitWidth - parent.children[2].implicitWidth; height: 1 }
Text {
text: detail.value
color: detail.barFg
font.family: detail.fontFamily
font.pixelSize: 11
}
}
Canvas {
id: detailCanvas
width: parent.width
height: 40
property var history: detail.history
onHistoryChanged: requestPaint()
onPaint: {
var ctx = getContext("2d")
ctx.clearRect(0, 0, width, height)
if (!detail.history || detail.history.length === 0) return
ctx.strokeStyle = detail.barFg
ctx.fillStyle = Qt.rgba(detail.barFg.r, detail.barFg.g, detail.barFg.b, 0.25)
ctx.lineWidth = 1.5
ctx.beginPath()
var step = width / Math.max(1, detail.history.length - 1)
for (var i = 0; i < detail.history.length; i++) {
var x = i * step
var y = height - (detail.history[i] / 100) * (height - 2) - 1
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
}
ctx.stroke()
ctx.lineTo(width, height)
ctx.lineTo(0, height)
ctx.closePath()
ctx.fill()
}
}
}
}
@@ -0,0 +1,126 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "../common" as Common
Item {
id: root
property QtObject bar: null
property string moduleName: "weatherFlyout"
property var settings: ({})
property bool popupOpen: false
function closePopout() { popupOpen = false }
property string fullReport: ""
readonly property string label: bar ? bar.weatherText : ""
readonly property string klass: bar ? bar.weatherClass : ""
visible: label !== ""
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
function refresh() {
if (!forecastProc.running) forecastProc.running = true
}
Process {
id: forecastProc
command: ["bash", "-lc", "curl -fsS --max-time 5 'wttr.in/?T0&format=%l:+%C+%t+%f+wind+%w+%h+humidity' 2>/dev/null"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.fullReport = String(text || "").trim()
}
}
Common.WidgetButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.label
active: root.klass === "active"
horizontalMargin: 7.5
tooltipText: root.fullReport || "Weather"
onPressed: function(b) {
if (b === Qt.RightButton) root.bar.run("omarchy-notification-send \"$(omarchy-weather-status)\"")
else {
root.popupOpen = !root.popupOpen
if (root.popupOpen) root.refresh()
}
}
}
Common.PopupCard {
anchorItem: button
owner: root
bar: root.bar
open: root.popupOpen
contentWidth: 320
contentHeight: column.implicitHeight + 28
Column {
id: column
anchors.fill: parent
spacing: 10
Row {
spacing: 12
width: parent.width
Text {
text: root.label || "—"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 28
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
text: "Weather"
color: root.bar.foreground
font.family: root.bar.fontFamily
font.pixelSize: 12
font.bold: true
}
Text {
text: root.fullReport || "Fetching forecast…"
color: Qt.darker(root.bar.foreground, 1.2)
font.family: root.bar.fontFamily
font.pixelSize: 10
wrapMode: Text.WordWrap
width: 220
}
}
}
Common.PillButton {
width: parent.width
iconText: "󰑐"
text: "Refresh"
foreground: root.bar.foreground
horizontalPadding: 10
verticalPadding: 6
onClicked: root.refresh()
}
Common.PillButton {
width: parent.width
iconText: "󰏌"
text: "Open wttr.in"
foreground: root.bar.foreground
horizontalPadding: 10
verticalPadding: 6
onClicked: { root.bar.run("xdg-open https://wttr.in"); root.popupOpen = false }
}
}
}
}
@@ -0,0 +1,131 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
Item {
id: root
property QtObject bar: null
property string moduleName: "workspacesPro"
property var settings: ({})
function setting(name, fallback) {
var value = settings ? settings[name] : undefined
return value === undefined || value === null ? fallback : value
}
readonly property var ids: bar ? bar.workspaceIds() : [1, 2, 3, 4, 5]
readonly property bool vertical: bar ? bar.vertical : false
readonly property int barSize: bar ? bar.barSize : 26
readonly property int slotSize: vertical ? barSize : 26
readonly property int spacing: vertical ? 4 : 4
readonly property int focusedIndex: {
if (!Hyprland.focusedWorkspace) return -1
var focused = Hyprland.focusedWorkspace.id
for (var i = 0; i < ids.length; i++) if (ids[i] === focused) return i
return -1
}
implicitWidth: vertical ? barSize : ids.length * slotSize + (ids.length - 1) * spacing + 6
implicitHeight: vertical ? ids.length * slotSize + (ids.length - 1) * spacing + 6 : barSize
Rectangle {
id: indicator
width: vertical ? root.slotSize - 8 : root.slotSize - 6
height: vertical ? root.slotSize - 6 : root.slotSize - 8
radius: 4
color: root.bar ? root.bar.foreground : "#cacccc"
opacity: root.focusedIndex >= 0 ? 0.25 : 0
x: vertical ? (root.width - width) / 2 : 3 + root.focusedIndex * (root.slotSize + root.spacing) + (root.slotSize - width) / 2
y: vertical ? 3 + root.focusedIndex * (root.slotSize + root.spacing) + (root.slotSize - height) / 2 : (root.height - height) / 2
Behavior on x { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } }
Behavior on y { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } }
Behavior on opacity { NumberAnimation { duration: 160 } }
}
Loader {
anchors.fill: parent
sourceComponent: root.vertical ? verticalLayout : horizontalLayout
}
Component {
id: horizontalLayout
Row {
anchors.centerIn: parent
spacing: root.spacing
Repeater {
model: root.ids
WorkspaceButton {
required property int modelData
workspaceId: modelData
}
}
}
}
Component {
id: verticalLayout
Column {
anchors.centerIn: parent
spacing: root.spacing
Repeater {
model: root.ids
WorkspaceButton {
required property int modelData
workspaceId: modelData
}
}
}
}
component WorkspaceButton: Item {
id: ws
property int workspaceId: 0
readonly property var workspace: root.bar ? root.bar.workspaceById(workspaceId) : null
readonly property bool occupied: workspace !== null && workspace.toplevels.values.length > 0
readonly property bool focused: Hyprland.focusedWorkspace !== null && Hyprland.focusedWorkspace.id === workspaceId
implicitWidth: root.slotSize
implicitHeight: root.slotSize
Text {
anchors.centerIn: parent
text: ws.focused ? "󱓻" : (ws.workspaceId === 10 ? "0" : String(ws.workspaceId))
color: root.bar ? root.bar.foreground : "#cacccc"
font.family: root.bar ? root.bar.fontFamily : "JetBrainsMono Nerd Font"
font.pixelSize: ws.focused ? 13 : 11
opacity: ws.focused ? 1 : (ws.occupied || ws.workspaceId <= 5 ? 0.8 : 0.4)
Behavior on opacity { NumberAnimation { duration: 160 } }
Behavior on font.pixelSize { NumberAnimation { duration: 160 } }
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.PointingHandCursor
onClicked: function(mouse) {
if (mouse.button === Qt.RightButton) {
Hyprland.dispatch("movetoworkspace " + ws.workspaceId)
} else {
root.bar.focusWorkspace(ws.workspaceId)
}
}
onWheel: function(wheel) {
if (wheel.angleDelta.y > 0) Hyprland.dispatch("workspace e-1")
else Hyprland.dispatch("workspace e+1")
}
}
}
}