From 04cf13ae994de8e14e1055aee215e09d834035ea Mon Sep 17 00:00:00 2001 From: Christian Colglazier Date: Fri, 25 Sep 2026 17:41:59 -0400 Subject: [PATCH] refactor(quickshell): split bar components --- home/dot_config/quickshell/bar/AudioBlock.qml | 48 +++++ home/dot_config/quickshell/bar/Bar.qml | 192 ++---------------- .../quickshell/bar/BatteryBlock.qml | 26 +++ home/dot_config/quickshell/bar/ClockBlock.qml | 20 ++ home/dot_config/quickshell/bar/Tray.qml | 66 ++++++ home/dot_config/quickshell/bar/Workspaces.qml | 53 +++++ 6 files changed, 225 insertions(+), 180 deletions(-) create mode 100644 home/dot_config/quickshell/bar/AudioBlock.qml create mode 100644 home/dot_config/quickshell/bar/BatteryBlock.qml create mode 100644 home/dot_config/quickshell/bar/ClockBlock.qml create mode 100644 home/dot_config/quickshell/bar/Tray.qml create mode 100644 home/dot_config/quickshell/bar/Workspaces.qml diff --git a/home/dot_config/quickshell/bar/AudioBlock.qml b/home/dot_config/quickshell/bar/AudioBlock.qml new file mode 100644 index 0000000..94a5e5d --- /dev/null +++ b/home/dot_config/quickshell/bar/AudioBlock.qml @@ -0,0 +1,48 @@ +import QtQuick +import Quickshell + +Block { + id: root + + required property var sink + required property var source + signal detailsRequested(var anchorItem) + + blockColor: sink && sink.audio && sink.audio.muted + ? Theme.foreground : Theme.primary + textColor: sink && sink.audio && sink.audio.muted + ? Theme.foregroundDark : Theme.foreground + interactive: true + text: { + if (!sink || !sink.audio) + return "\uf026"; + return sink.audio.muted + ? "\uf026" : "\uf028 " + Math.round(sink.audio.volume * 100) + "%"; + } + secondaryIcon: source && source.audio + ? (source.audio.muted + ? "file://" + Quickshell.shellPath("icons/microphone-muted.svg") + : "file://" + Quickshell.shellPath("icons/microphone.svg")) + : "" + secondaryText: source && source.audio + ? (source.audio.muted ? "OFF" + : Math.round(source.audio.volume * 100) + "%") + : "" + + onClicked: mouse => { + if (mouse.button === Qt.LeftButton) + detailsRequested(root); + else if (mouse.button === Qt.MiddleButton && sink && sink.audio) + sink.audio.muted = !sink.audio.muted; + else + Quickshell.execDetached(["qjackctl"]); + } + + onWheel: wheel => { + if (!sink || !sink.audio) + return; + const change = wheel.angleDelta.y > 0 ? 0.05 : -0.05; + sink.audio.volume = Math.max(0, Math.min(1.5, + sink.audio.volume + change)); + } +} diff --git a/home/dot_config/quickshell/bar/Bar.qml b/home/dot_config/quickshell/bar/Bar.qml index 38d786c..a94c02f 100644 --- a/home/dot_config/quickshell/bar/Bar.qml +++ b/home/dot_config/quickshell/bar/Bar.qml @@ -2,9 +2,7 @@ import QtQuick import Quickshell import Quickshell.Hyprland import Quickshell.Services.Pipewire -import Quickshell.Services.SystemTray import Quickshell.Services.UPower -import Quickshell.Widgets PanelWindow { id: bar @@ -14,13 +12,6 @@ PanelWindow { property var battery: UPower.displayDevice property var hardware - function formatClock(date, includeSeconds) { - const hour = date.getHours() % 12 || 12; - const minute = date.getMinutes().toString().padStart(2, "0"); - const second = date.getSeconds().toString().padStart(2, "0"); - return hour + ":" + minute + (includeSeconds ? ":" + second : ""); - } - anchors { top: true left: true @@ -56,55 +47,8 @@ PanelWindow { } spacing: Theme.moduleSpacing - Row { + Workspaces { height: parent.height - spacing: 0 - - Repeater { - model: Hyprland.workspaces - - Rectangle { - id: workspaceButton - - required property var modelData - property bool active: modelData.active - - visible: modelData.id > 0 - width: Theme.blockHeight + 2 - height: parent.height - color: modelData.urgent ? Theme.urgent - : active ? Theme.primary : "transparent" - - Rectangle { - anchors { - left: parent.left - right: parent.right - bottom: parent.bottom - } - height: Theme.activeLineWidth - color: workspaceButton.active ? Theme.accent : "transparent" - } - - Text { - anchors.centerIn: parent - text: modelData.id === 2 ? "\uf120" - : modelData.id === 13 ? "\uf001" : modelData.name - color: Theme.foreground - font.family: Theme.iconFontFamily - font.pixelSize: Theme.fontSize - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Quickshell.execDetached([ - "hyprctl", - "dispatch", - "hl.dsp.focus({ workspace = " + workspaceButton.modelData.id + " })" - ]) - } - } - } } } @@ -130,43 +74,10 @@ PanelWindow { } spacing: Theme.moduleSpacing - Block { - id: audioBlock - blockColor: bar.sink && bar.sink.audio && bar.sink.audio.muted - ? Theme.foreground : Theme.primary - textColor: bar.sink && bar.sink.audio && bar.sink.audio.muted - ? Theme.foregroundDark : Theme.foreground - interactive: true - text: { - if (!bar.sink || !bar.sink.audio) - return "\uf026"; - return bar.sink.audio.muted - ? "\uf026" : "\uf028 " + Math.round(bar.sink.audio.volume * 100) + "%"; - } - secondaryIcon: bar.source && bar.source.audio - ? (bar.source.audio.muted - ? "file://" + Quickshell.shellPath("icons/microphone-muted.svg") - : "file://" + Quickshell.shellPath("icons/microphone.svg")) - : "" - secondaryText: bar.source && bar.source.audio - ? (bar.source.audio.muted ? "OFF" - : Math.round(bar.source.audio.volume * 100) + "%") - : "" - onClicked: mouse => { - if (mouse.button === Qt.LeftButton) - detailsPopup.toggle("audio", audioBlock); - else if (mouse.button === Qt.MiddleButton && bar.sink && bar.sink.audio) - bar.sink.audio.muted = !bar.sink.audio.muted; - else - Quickshell.execDetached(["qjackctl"]); - } - onWheel: wheel => { - if (!bar.sink || !bar.sink.audio) - return; - const change = wheel.angleDelta.y > 0 ? 0.05 : -0.05; - bar.sink.audio.volume = Math.max(0, Math.min(1.5, - bar.sink.audio.volume + change)); - } + AudioBlock { + sink: bar.sink + source: bar.source + onDetailsRequested: anchorItem => detailsPopup.toggle("audio", anchorItem) } Network { @@ -175,95 +86,16 @@ PanelWindow { } Backlight { service: bar.hardware } - Block { - id: batteryBlock - property int percentage: Math.round(bar.battery.percentage * 100) - - visible: bar.battery.ready && bar.battery.isLaptopBattery - interactive: true - blockColor: percentage <= 15 && UPower.onBattery - ? Theme.urgent : UPower.onBattery ? Theme.foreground : Theme.secondary - textColor: UPower.onBattery && percentage > 15 - ? Theme.foregroundDark : Theme.foreground - text: { - if (!UPower.onBattery) - return "\uf1e6 " + percentage + "%"; - if (percentage <= 10) return "\uf244 " + percentage + "%"; - if (percentage <= 35) return "\uf243 " + percentage + "%"; - if (percentage <= 60) return "\uf242 " + percentage + "%"; - if (percentage <= 85) return "\uf241 " + percentage + "%"; - return "\uf240 " + percentage + "%"; - } - onClicked: detailsPopup.toggle("battery", batteryBlock) + BatteryBlock { + battery: bar.battery + onDetailsRequested: anchorItem => detailsPopup.toggle("battery", anchorItem) } - Rectangle { - visible: trayRepeater.count > 0 - implicitWidth: trayItems.implicitWidth + Theme.contentPadding * 2 - implicitHeight: Theme.blockHeight - color: Theme.primary + Tray { barWindow: bar } - Row { - id: trayItems - anchors.centerIn: parent - spacing: Theme.contentPadding - - Repeater { - id: trayRepeater - model: SystemTray.items - - Item { - id: trayItem - - required property var modelData - width: 26 - height: 30 - - IconImage { - anchors { - fill: parent - margins: 3 - } - source: trayItem.modelData.icon - opacity: trayMouse.containsMouse ? 1 : 0.82 - - Behavior on opacity { - NumberAnimation { duration: 120 } - } - } - - MouseArea { - id: trayMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton - onClicked: mouse => { - if (mouse.button === Qt.MiddleButton) - trayItem.modelData.secondaryActivate(); - else if (mouse.button === Qt.RightButton || trayItem.modelData.onlyMenu) { - const point = mapToItem(bar.contentItem, 0, height); - trayItem.modelData.display(bar, point.x, point.y); - } else { - trayItem.modelData.activate(); - } - } - onWheel: wheel => trayItem.modelData.scroll( - wheel.angleDelta.y, false) - } - } - } - } - } - - Block { - id: clockBlock - property bool showSeconds: false - - interactive: true - text: bar.formatClock(clock.date, showSeconds) - onClicked: detailsPopup.toggle("clock", clockBlock) - onDoubleClicked: showSeconds = !showSeconds + ClockBlock { + clock: clock + onDetailsRequested: anchorItem => detailsPopup.toggle("clock", anchorItem) } } } diff --git a/home/dot_config/quickshell/bar/BatteryBlock.qml b/home/dot_config/quickshell/bar/BatteryBlock.qml new file mode 100644 index 0000000..9a525cb --- /dev/null +++ b/home/dot_config/quickshell/bar/BatteryBlock.qml @@ -0,0 +1,26 @@ +import Quickshell.Services.UPower + +Block { + id: root + + required property var battery + property int percentage: Math.round(battery.percentage * 100) + signal detailsRequested(var anchorItem) + + visible: battery.ready && battery.isLaptopBattery + interactive: true + blockColor: percentage <= 15 && UPower.onBattery + ? Theme.urgent : UPower.onBattery ? Theme.foreground : Theme.secondary + textColor: UPower.onBattery && percentage > 15 + ? Theme.foregroundDark : Theme.foreground + text: { + if (!UPower.onBattery) + return "\uf1e6 " + percentage + "%"; + if (percentage <= 10) return "\uf244 " + percentage + "%"; + if (percentage <= 35) return "\uf243 " + percentage + "%"; + if (percentage <= 60) return "\uf242 " + percentage + "%"; + if (percentage <= 85) return "\uf241 " + percentage + "%"; + return "\uf240 " + percentage + "%"; + } + onClicked: detailsRequested(root) +} diff --git a/home/dot_config/quickshell/bar/ClockBlock.qml b/home/dot_config/quickshell/bar/ClockBlock.qml new file mode 100644 index 0000000..3843f3b --- /dev/null +++ b/home/dot_config/quickshell/bar/ClockBlock.qml @@ -0,0 +1,20 @@ +Block { + id: root + + required property var clock + property bool showSeconds: false + signal detailsRequested(var anchorItem) + + function formattedTime() { + const date = clock.date; + const hour = date.getHours() % 12 || 12; + const minute = date.getMinutes().toString().padStart(2, "0"); + const second = date.getSeconds().toString().padStart(2, "0"); + return hour + ":" + minute + (showSeconds ? ":" + second : ""); + } + + interactive: true + text: formattedTime() + onClicked: detailsRequested(root) + onDoubleClicked: showSeconds = !showSeconds +} diff --git a/home/dot_config/quickshell/bar/Tray.qml b/home/dot_config/quickshell/bar/Tray.qml new file mode 100644 index 0000000..bcc3f64 --- /dev/null +++ b/home/dot_config/quickshell/bar/Tray.qml @@ -0,0 +1,66 @@ +import QtQuick +import Quickshell.Services.SystemTray +import Quickshell.Widgets + +Rectangle { + id: root + + required property var barWindow + + visible: trayRepeater.count > 0 + implicitWidth: trayItems.implicitWidth + Theme.contentPadding * 2 + implicitHeight: Theme.blockHeight + color: Theme.primary + + Row { + id: trayItems + anchors.centerIn: parent + spacing: Theme.contentPadding + + Repeater { + id: trayRepeater + model: SystemTray.items + + Item { + id: trayItem + + required property var modelData + width: 26 + height: 30 + + IconImage { + anchors { + fill: parent + margins: 3 + } + source: trayItem.modelData.icon + opacity: trayMouse.containsMouse ? 1 : 0.82 + + Behavior on opacity { + NumberAnimation { duration: 120 } + } + } + + MouseArea { + id: trayMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton + onClicked: mouse => { + if (mouse.button === Qt.MiddleButton) { + trayItem.modelData.secondaryActivate(); + } else if (mouse.button === Qt.RightButton || trayItem.modelData.onlyMenu) { + const point = mapToItem(root.barWindow.contentItem, 0, height); + trayItem.modelData.display(root.barWindow, point.x, point.y); + } else { + trayItem.modelData.activate(); + } + } + onWheel: wheel => trayItem.modelData.scroll( + wheel.angleDelta.y, false) + } + } + } + } +} diff --git a/home/dot_config/quickshell/bar/Workspaces.qml b/home/dot_config/quickshell/bar/Workspaces.qml new file mode 100644 index 0000000..26ecac2 --- /dev/null +++ b/home/dot_config/quickshell/bar/Workspaces.qml @@ -0,0 +1,53 @@ +import QtQuick +import Quickshell +import Quickshell.Hyprland + +Row { + spacing: 0 + + Repeater { + model: Hyprland.workspaces + + Rectangle { + id: workspaceButton + + required property var modelData + property bool active: modelData.active + + visible: modelData.id > 0 + width: Theme.blockHeight + 2 + height: parent.height + color: modelData.urgent ? Theme.urgent + : active ? Theme.primary : "transparent" + + Rectangle { + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: Theme.activeLineWidth + color: workspaceButton.active ? Theme.accent : "transparent" + } + + Text { + anchors.centerIn: parent + text: modelData.id === 2 ? "\uf120" + : modelData.id === 13 ? "\uf001" : modelData.name + color: Theme.foreground + font.family: Theme.iconFontFamily + font.pixelSize: Theme.fontSize + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Quickshell.execDetached([ + "hyprctl", + "dispatch", + "hl.dsp.focus({ workspace = " + workspaceButton.modelData.id + " })" + ]) + } + } + } +}