Add modular interactive widgets to the Quickshell bar

Introduces a widgets/ directory of first-party QML modules and a common/
helpers library. Widgets auto-load by name from
default/quickshell/bar/widgets/<name>.qml — no edits to shell.qml needed
for additions beyond the firstPartyWidgets registry.

New widgets (all orientation-aware, top/bottom/left/right):
- media: MPRIS now-playing with scrolling label, cover art popup,
  play/pause/skip controls
- audioPanel: master volume slider, output device picker, per-app mixer
- networkPanel: Wi-Fi scan + connect, current connection details
- bluetoothPanel: paired device list, connect/disconnect, battery
- calendar: month grid popup keyed to today
- notificationCenter: live notification feed, DND toggle, clear all
- brightness: slider popup with scroll-to-adjust
- powerProfile: power-saver/balanced/performance picker (UPower)
- systemStats: inline CPU + RAM sparklines, expanded popup with btop
- weatherFlyout: weather popup with refresh and full report
- workspacesPro: animated focus indicator that slides between workspaces
- powerMenu: lock/suspend/log out/reboot/shutdown popup
- idleInhibitor: coffee-cup toggle wired to omarchy-toggle-idle
- microphone: mic state + mute toggle + scroll volume

Common helpers (default/quickshell/bar/common/):
- WidgetButton: hover/press scale animation, tooltip integration
- PopupCard: orientation-aware anchor + slide-in opacity transition
- Slider: drag/wheel-able linear slider with knob hover bump
- PillButton: rounded action button with icon + label

Bar root gains requestPopout/releasePopout so only one popup is open at
a time across widgets.

Updates bar-defaults.json to ship the new widgets out of the box while
keeping the legacy modules available.
This commit is contained in:
Ryan Hughes
2026-05-14 02:21:47 -04:00
parent 30dc9fddde
commit 221e0e1fcc
21 changed files with 2829 additions and 26 deletions
+57 -18
View File
@@ -4,36 +4,59 @@ This is the Quickshell implementation of the Omarchy status bar.
- `shell.qml` is Omarchy-owned bar engine code. 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.
- Wi-Fi/LAN state is polled from the active route and NetworkManager so the QML can mirror Waybar's network icons.
- Tooltips use a small Quickshell `PopupWindow`, not Qt Quick Controls' clipped default tooltip.
Example user override:
```json
{
"position": "right",
"position": "top",
"layout": {
"left": ["omarchy", "workspaces"],
"center": ["clock", "weather", "update"],
"right": ["tray", "bluetooth", "network", "audio", "cpu", "battery"]
"left": ["omarchy", "workspacesPro"],
"center": ["media", "calendar", "weatherFlyout"],
"right": ["systemStats", "notificationCenter", "bluetoothPanel", "networkPanel", "audioPanel", "brightness", "powerProfile", "battery", "powerMenu"]
},
"modules": {
"clock": {
"format": "HH:mm",
"formatAlt": "dd MMMM 'W'ww yyyy",
"verticalFormat": "HH\n—\nmm"
}
}
"centerAnchor": "calendar"
}
```
`centerAnchor` defaults to `clock`. When the anchor module is present in the `center` list, it is pinned to the exact center of the bar and modules before/after it flank that anchor. If the anchor is not in the center list, the center list is centered as a group.
`centerAnchor` pins one center module to the exact horizontal/vertical center and flanks others around it.
Available built-in modules: `omarchy`, `workspaces`, `clock`, `weather`, `update`, `voxtype`, `screenRecording`, `idle`, `notifications`, `tray`, `bluetooth`, `network`, `audio`, `cpu`, `battery`.
## Module catalogue
## Custom modules
### 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
Add a module name to a layout list, then define it under `modules` in `~/.config/omarchy/bar.json`.
@@ -42,7 +65,7 @@ For simple text/JSON output, use a command module:
```json
{
"layout": {
"right": ["tray", "vpn", "audio", "cpu"]
"right": ["tray", "vpn", "audioPanel", "cpu"]
},
"modules": {
"vpn": {
@@ -67,7 +90,7 @@ For advanced custom UI, use QML:
```json
{
"layout": {
"right": ["gpu", "audio", "cpu"]
"right": ["gpu", "audioPanel", "cpu"]
},
"modules": {
"gpu": {
@@ -106,3 +129,19 @@ Item {
}
}
```
## 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.
+9 -4
View File
@@ -1,13 +1,18 @@
{
"position": "top",
"fontFamily": "JetBrainsMono Nerd Font",
"centerAnchor": "clock",
"centerAnchor": "calendar",
"layout": {
"left": ["omarchy", "workspaces"],
"center": ["clock", "weather", "update", "voxtype", "screenRecording", "idle", "notifications"],
"right": ["tray", "bluetooth", "network", "audio", "cpu", "battery"]
"left": ["omarchy", "workspacesPro"],
"center": ["media", "calendar", "weatherFlyout", "update", "voxtype", "screenRecording", "idle"],
"right": ["tray", "systemStats", "notificationCenter", "microphone", "bluetoothPanel", "networkPanel", "audioPanel", "brightness", "powerProfile", "battery", "powerMenu"]
},
"modules": {
"calendar": {
"format": "dddd HH:mm",
"formatAlt": "dd MMMM 'W'ww yyyy",
"verticalFormat": "HH\n—\nmm"
},
"clock": {
"format": "dddd HH:mm",
"formatAlt": "dd MMMM 'W'ww yyyy",
@@ -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,93 @@
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 dismissOnOutsideClick: true
property bool open: false
onOpenChanged: {
if (!bar) return
if (open) bar.requestPopout(owner || root)
else if (bar.activePopout === (owner || root)) bar.releasePopout(owner || root)
}
default property alias contentItem: contentHolder.children
visible: open
color: "transparent"
implicitWidth: contentWidth
implicitHeight: contentHeight
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 ? 0.97 : 0
Behavior on opacity {
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
Item {
id: contentHolder
anchors.fill: parent
anchors.margins: root.padding
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
hoverEnabled: false
propagateComposedEvents: true
}
}
}
+112
View File
@@ -0,0 +1,112 @@
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
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, (value - 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.value = next
root.moved(next)
}
onPositionChanged: function(mouse) {
if (!root.dragging) return
var next = valueFromX(mouse.x)
root.value = next
root.moved(next)
}
onReleased: function(mouse) {
root.dragging = false
root.released(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.value + delta))
if (root.integer) next = Math.round(next)
root.value = 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) }
}
}
+39 -4
View File
@@ -64,6 +64,17 @@ ShellRoot {
property var tooltipTarget: null
property string tooltipText: ""
property bool tooltipShown: false
property var activePopout: null
function requestPopout(owner) {
if (activePopout === owner) return
if (activePopout && "closePopout" in activePopout) activePopout.closePopout()
activePopout = owner
}
function releasePopout(owner) {
if (activePopout === owner) activePopout = null
}
readonly property bool vertical: position === "left" || position === "right"
readonly property int barSize: vertical ? 28 : 26
@@ -248,6 +259,28 @@ ShellRoot {
return source ? fileUrl(source) : ""
}
readonly property var firstPartyWidgets: ({
"media": true,
"audioPanel": true,
"networkPanel": true,
"bluetoothPanel": true,
"calendar": true,
"notificationCenter": true,
"brightness": true,
"powerProfile": true,
"systemStats": true,
"weatherFlyout": true,
"workspacesPro": true,
"powerMenu": true,
"idleInhibitor": true,
"microphone": true
})
function firstPartyWidgetSource(name) {
if (!firstPartyWidgets[String(name)]) return ""
return fileUrl(omarchyPath + "/default/quickshell/bar/widgets/" + String(name) + ".qml")
}
function networkCommand() {
return [
"route_device=$(ip route get 1.1.1.1 2>/dev/null | awk '{ for (i = 1; i <= NF; i++) if ($i == \"dev\") { print $(i + 1); exit } }')",
@@ -1048,9 +1081,11 @@ ShellRoot {
required property string moduleName
readonly property string customType: root.customModuleType(moduleName)
readonly property var builtinComponent: customType ? null : root.builtinModuleComponent(moduleName)
readonly property string firstPartySource: customType || builtinComponent ? "" : root.firstPartyWidgetSource(moduleName)
readonly property bool qmlCustom: customType === "qml"
readonly property bool commandCustom: customType === "command"
readonly property var activeItem: qmlCustom ? qmlLoader.item : componentLoader.item
readonly property bool firstParty: firstPartySource !== ""
readonly property var activeItem: qmlCustom || firstParty ? qmlLoader.item : componentLoader.item
implicitWidth: activeItem && activeItem.visible ? activeItem.implicitWidth : 0
implicitHeight: activeItem && activeItem.visible ? activeItem.implicitHeight : 0
@@ -1059,15 +1094,15 @@ ShellRoot {
Loader {
id: componentLoader
active: !slot.qmlCustom
active: !slot.qmlCustom && !slot.firstParty
sourceComponent: slot.builtinComponent || (slot.commandCustom ? customCommandModuleComponent : emptyModuleComponent)
anchors.fill: parent
}
Loader {
id: qmlLoader
active: slot.qmlCustom
source: slot.qmlCustom ? root.customModuleSource(slot.moduleName) : ""
active: slot.qmlCustom || slot.firstParty
source: slot.qmlCustom ? root.customModuleSource(slot.moduleName) : (slot.firstParty ? slot.firstPartySource : "")
anchors.fill: parent
onLoaded: {
if (item && "bar" in item) item.bar = root
@@ -0,0 +1,232 @@
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 audioSinks: {
var list = []
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i]
if (node && node.isSink && !node.isStream && node.audio) list.push(node)
}
return list
}
readonly property var audioStreams: {
var list = []
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i]
if (node && node.isStream && !node.isSink && node.audio) list.push(node)
}
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.audioSinks }
PwObjectTracker { objects: root.audioStreams }
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,147 @@
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
}
function setBrightness(percent) {
var clamped = Math.max(1, Math.min(100, Math.round(percent)))
currentPercent = clamped
writeProc.command = ["bash", "-lc", "brightnessctl set " + clamped + "% >/dev/null"]
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) }
}
}
}
}
+191
View File
@@ -0,0 +1,191 @@
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
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
}
}
}
}
}
}
MouseArea {
anchors.fill: parent
visible: false
enabled: false
}
}
@@ -0,0 +1,66 @@
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
property int holdSeconds: 0
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() }
}
}
+234
View File
@@ -0,0 +1,234 @@
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
x: needsScroll ? scrollAnim.position : 0
NumberAnimation on x {
id: scrollAnim
property real position: 0
running: labelText.needsScroll && !root.popupOpen
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
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,217 @@
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 signal: bar ? bar.networkSignal : -1
readonly property string icon: {
if (kind === "wifi") {
var icons = ["󰤯", "󰤟", "󰤢", "󰤥", "󰤨"]
var index = Math.max(0, Math.min(4, Math.ceil(signal / 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],
signal: 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.signal - a.signal
})
networks = list
scanning = false
}
function wifiIconFor(signal) {
var icons = ["󰤯", "󰤟", "󰤢", "󰤥", "󰤨"]
var index = Math.max(0, Math.min(4, Math.ceil(signal / 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.signal + "%" : "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: {
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.signal)
text: (modelData.ssid || "Hidden") + (modelData.security ? " · " : "") + (modelData.security || "")
foreground: root.bar.foreground
horizontalPadding: 10
verticalPadding: 6
active: modelData.inUse
onClicked: {
if (modelData.inUse) 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,244 @@
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 ""
}
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.profile !== undefined
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,372 @@
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
}
}
implicitWidth: row.implicitWidth + 6
implicitHeight: bar ? bar.barSize : 26
Row {
id: row
anchors.centerIn: parent
spacing: 8
StatPill {
glyph: "󰍛"
percent: root.cpuPercent
history: root.cpuHistory
vertical: root.bar.vertical
barFg: root.bar.foreground
fontFamily: root.bar.fontFamily
}
StatPill {
glyph: ""
percent: root.memPercent
history: root.memHistory
vertical: root.bar.vertical
barFg: root.bar.foreground
fontFamily: root.bar.fontFamily
}
}
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.bar.foreground
fontFamily: root.bar.fontFamily
width: parent.width
}
DetailStat {
title: "Memory"
value: Math.round(root.memPercent) + "%"
history: root.memHistory
barFg: root.bar.foreground
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,142 @@
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 var forecast: ({})
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
}
function parseForecast(text) {
var lines = String(text || "").split("\n")
var data = { location: "", current: "", hourly: [], daily: [] }
var section = ""
for (var i = 0; i < lines.length; i++) {
var line = lines[i]
if (i === 0 && line.indexOf("Weather report:") === 0) {
data.location = line.replace("Weather report:", "").trim()
} else if (line.match(/^[├└]\s/)) {
section = "daily"
}
}
return data
}
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.foreground
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")
}
}
}
}