first commit

This commit is contained in:
SomeElse
2026-04-24 02:14:35 +00:00
commit 5f3b357344
20 changed files with 3205 additions and 0 deletions

91
bar/modules/ArchLogo.qml Normal file
View File

@@ -0,0 +1,91 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import "../../config" as Cfg
Rectangle {
id: root
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
property bool isTop: true
property var targetScreen: null
property int barHeight: Cfg.Config.barHeight
property int barMargin: Cfg.Config.margin
readonly property var t: Cfg.Config.theme
property var launcher: null
readonly property bool launcherActive: !!launcher && launcher.isOpen
readonly property bool isHovered: hoverHandler.hovered
property real spinTarget: 0
onTargetScreenChanged: Qt.callLater(() => {
launcher = (targetScreen && targetScreen.launcher) ? targetScreen.launcher : null
})
onLauncherActiveChanged: {
spinTarget += launcherActive ? 360 : -360
if (launcherActive && !clickAnim.running)
clickAnim.restart()
}
width: Cfg.Config.logoIconSize * 2 + 4
height: parent.height
color: isHovered ? t.archLogoBgHover : t.archLogoBg
radius: panelRadius
border.color: launcherActive ? t.archLogoBorderActive : t.archLogoBorder
border.width: borderWidth
Behavior on color { ColorAnimation { duration: 250 } }
Behavior on border.color { ColorAnimation { duration: 250 } }
HoverHandler { id: hoverHandler }
TapHandler {
onTapped: {
clickAnim.restart()
if (root.targetScreen && root.targetScreen.launcher) {
root.targetScreen.launcher.toggle()
}
}
}
SequentialAnimation {
id: clickAnim
NumberAnimation { target: logoText; property: "scale"; from: 1.0; to: 0.7; duration: 60; easing.type: Easing.OutQuad }
NumberAnimation { target: logoText; property: "scale"; from: 0.7; to: 1.2; duration: 150; easing.type: Easing.OutBack }
NumberAnimation { target: logoText; property: "scale"; to: 1.0; duration: 100 }
}
Text {
id: logoText
anchors.centerIn: parent
text: Cfg.Config.logoIcon
color: launcherActive ? t.archLogoIconActive : t.archLogoIcon
font.pixelSize: Cfg.Config.logoIconSize
Behavior on color { ColorAnimation { duration: 250 } }
scale: clickAnim.running ? logoText.scale : 1.0
Behavior on scale {
enabled: !clickAnim.running
NumberAnimation { duration: 250; easing.type: Easing.OutBack }
}
rotation: root.spinTarget
Behavior on rotation {
NumberAnimation {
duration: 600
easing.type: Easing.InOutBack
}
}
}
}

331
bar/modules/Clock.qml Normal file
View File

@@ -0,0 +1,331 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import "../../config" as Cfg
Rectangle {
id: root
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
property bool isTop: true
property var targetScreen: null
property int barHeight: Cfg.Config.barHeight
property int barMargin: Cfg.Config.margin
readonly property var t: Cfg.Config.theme
readonly property int tzOffsetHours: -timeManager.now.getTimezoneOffset() / 60
width: clockLayout.implicitWidth + 24
height: Cfg.Config.moduleHeight
radius: panelRadius
color: hoverHandler.hovered ? t.clockBgHover : t.clockBg
border.color: hoverHandler.hovered ? t.clockBorderHover : t.clockBorder
border.width: borderWidth
anchors.right: parent.right
Behavior on width { NumberAnimation { duration: 250; easing.type: Easing.OutCubic } }
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
HoverHandler {
id: hoverHandler
cursorShape: Qt.PointingHandCursor
}
TapHandler {
onTapped: calendarPopup.toggle()
}
Item {
id: timeManager
property date now: new Date()
Timer {
interval: 1000; running: true; repeat: true
onTriggered: timeManager.now = new Date()
}
}
RowLayout {
id: clockLayout
anchors.centerIn: parent
spacing: 0
Text {
text: "󱑂"
color: root.t.clockIcon
font.pixelSize: 14
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: 8
}
Text {
text: timeManager.now.toLocaleTimeString(Qt.locale(), "HH:mm")
color: root.t.clockTime
font.pixelSize: 13
font.bold: true
verticalAlignment: Text.AlignVCenter
}
Text {
id: secondsText
text: ":" + timeManager.now.toLocaleTimeString(Qt.locale(), "ss")
color: root.t.clockSeconds
font.pixelSize: 13
font.bold: true
verticalAlignment: Text.AlignVCenter
opacity: (hoverHandler.hovered || calendarPopup.popupOpen) ? 1.0 : 0.0
visible: opacity > 0
Layout.preferredWidth: (hoverHandler.hovered || calendarPopup.popupOpen) ? implicitWidth : 0
clip: true
Behavior on opacity { NumberAnimation { duration: 200 } }
Behavior on Layout.preferredWidth { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
}
}
PanelWindow {
id: calendarPopup
screen: root.targetScreen
visible: false
color: "transparent"
property bool popupOpen: false
function open() { hideTimer.stop(); visible = true; popupOpen = true }
function close() { popupOpen = false; hideTimer.restart() }
function toggle() { popupOpen ? close() : open() }
Timer {
id: hideTimer
interval: Cfg.Config.popupAnimDuration + 70
onTriggered: calendarPopup.visible = false
}
anchors { top: true; bottom: true; left: true; right: true }
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "main-shell-calendar"
WlrLayershell.exclusiveZone: -1
mask: popupOpen ? null : _noInput
Region { id: _noInput }
TapHandler { onTapped: calendarPopup.close() }
Item {
id: calClipContainer
readonly property int cardW: 284
readonly property int screenPad: root.barMargin + 10
x: {
let centerX = root.mapToItem(null, 0, 0).x + root.width / 2
let desiredX = centerX - cardW / 2
return Math.max(screenPad, Math.min(desiredX, parent.width - cardW - screenPad))
}
width: cardW
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
height: parent.height - root.barHeight - 10
clip: true
Rectangle {
id: card
width: parent.width
height: cardCol.implicitHeight + 24
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
transform: Translate {
y: calendarPopup.popupOpen ? 0 : (root.isTop ? -card.height : card.height)
Behavior on y {
NumberAnimation {
duration: Cfg.Config.popupAnimDuration;
easing.type: Easing.OutExpo
}
enabled: calendarPopup.visible
}
}
opacity: calendarPopup.popupOpen ? 1.0 : 0.0
Behavior on opacity { NumberAnimation { duration: 200 } }
color: root.t.calCardBg
radius: Cfg.Config.popupRadius
border.color: root.t.calCardBorder
border.width: Cfg.Config.popupBorderWidth
layer.enabled: true
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
ColumnLayout {
id: cardCol
anchors { left: parent.left; right: parent.right; top: parent.top }
anchors.margins: 16
anchors.topMargin: 18
spacing: 14
ColumnLayout {
spacing: 2
Layout.fillWidth: true
Text {
text: timeManager.now.toLocaleDateString(Qt.locale(), "dddd")
color: root.t.clockPopupDim
font.pixelSize: 12
font.capitalization: Font.AllUppercase
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
}
Text {
text: timeManager.now.toLocaleDateString(Qt.locale(), "d MMMM yyyy")
color: root.t.clockPopupHeader
font.pixelSize: 22
font.bold: true
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.calSeparator }
RowLayout {
Layout.fillWidth: true
Text {
text: ""; color: root.t.clockPopupDim; font.pixelSize: 18; font.bold: true
MouseArea {
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
onClicked: {
cal.displayMonth -= 1
if (cal.displayMonth < 0) { cal.displayMonth = 11; cal.displayYear -= 1 }
}
}
}
Text {
text: new Date(cal.displayYear, cal.displayMonth, 1).toLocaleDateString(Qt.locale(), "MMMM yyyy")
color: root.t.clockPopupText
font.pixelSize: 13; font.bold: true
Layout.fillWidth: true; horizontalAlignment: Text.AlignHCenter
}
Text {
text: ""; color: root.t.clockPopupDim; font.pixelSize: 18; font.bold: true
MouseArea {
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
onClicked: {
cal.displayMonth += 1
if (cal.displayMonth > 11) { cal.displayMonth = 0; cal.displayYear += 1 }
}
}
}
}
Item {
id: cal
Layout.fillWidth: true
implicitHeight: gridCol.implicitHeight
property int displayYear: timeManager.now.getFullYear()
property int displayMonth: timeManager.now.getMonth()
Connections {
target: calendarPopup
function onPopupOpenChanged() {
if (calendarPopup.popupOpen) {
cal.displayYear = timeManager.now.getFullYear()
cal.displayMonth = timeManager.now.getMonth()
}
}
}
ColumnLayout {
id: gridCol
anchors.fill: parent
spacing: 2
Row {
spacing: 0
Repeater {
model: ["Mo","Tu","We","Th","Fr","Sa","Su"]
Text {
width: (card.width - 32) / 7
text: modelData; color: root.t.clockPopupDim
font.pixelSize: 11; horizontalAlignment: Text.AlignHCenter
}
}
}
Repeater {
model: 6
Row {
id: weekRow
property int weekIndex: index
Repeater {
model: 7
delegate: Item {
width: (card.width - 32) / 7; height: 26
readonly property int dayNum: (weekRow.weekIndex * 7 + index) - ((new Date(cal.displayYear, cal.displayMonth, 1).getDay() + 6) % 7) + 1
readonly property bool inMonth: dayNum >= 1 && dayNum <= new Date(cal.displayYear, cal.displayMonth + 1, 0).getDate()
readonly property bool isToday: inMonth && dayNum === timeManager.now.getDate() && cal.displayYear === timeManager.now.getFullYear() && cal.displayMonth === timeManager.now.getMonth()
Rectangle {
anchors.centerIn: parent; width: 22; height: 22; radius: 11
color: isToday ? root.t.accent : "transparent"
visible: inMonth
}
Text {
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: inMonth ? dayNum : ""
color: isToday ? root.t.todayText : (index >= 5 ? root.t.clockPopupDim : root.t.clockPopupText)
font.pixelSize: 12; font.bold: isToday
}
}
}
}
}
}
}
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.calSeparator }
RowLayout {
Layout.fillWidth: true; spacing: 12
Text { text: "󰣐"; color: root.t.clockIcon; font.pixelSize: 16 }
ColumnLayout {
spacing: 0; Layout.fillWidth: true
Text {
text: timeManager.now.toLocaleTimeString(Qt.locale(), "t") || "UTC"
color: root.t.clockPopupText; font.pixelSize: 12; font.bold: true
}
Text {
text: "UTC " + (root.tzOffsetHours >= 0 ? "+" : "") + root.tzOffsetHours
color: root.t.clockPopupDim; font.pixelSize: 10
}
}
Text {
text: timeManager.now.toLocaleTimeString(Qt.locale(), "HH:mm:ss")
color: root.t.clockTime; font.pixelSize: 20; font.bold: true; font.family: "monospace"
}
}
Item { height: 4 }
}
}
}
}
}

View File

@@ -0,0 +1,357 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Notifications
import "../../config" as Cfg
import "../../notifications" as Notif
Rectangle {
id: root
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
property bool isTop: true
property var targetScreen: null
property int barHeight: Cfg.Config.barHeight
property int barMargin: Cfg.Config.margin
readonly property var t: Cfg.Config.theme
property int unreadCount: 0
property bool historyOpen: historyPopup.popupOpen
width: 35
height: 40
radius: panelRadius
color: hoverHandler.hovered ? t.notifBgHover : t.notifBg
border.color: (hoverHandler.hovered || historyOpen) ? t.notifBorderActive : t.notifBorder
border.width: borderWidth
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
HoverHandler { id: hoverHandler; cursorShape: Qt.PointingHandCursor }
TapHandler {
onTapped: {
if (historyPopup.popupOpen) {
historyPopup.close()
} else {
root.unreadCount = 0
historyPopup.open()
}
}
}
Item {
anchors.centerIn: parent
width: Cfg.Config.notifIconSize + 10
height: Cfg.Config.notifIconSize + 10
Text {
id: bellIcon
anchors.centerIn: parent
text: "󰂚"
color: root.unreadCount > 0 ? t.notifIconActive : t.notifIcon
font.pixelSize: Cfg.Config.notifIconSize
Behavior on color { ColorAnimation { duration: 150 } }
}
Rectangle {
visible: root.unreadCount > 0
anchors.top: parent.top
anchors.right: parent.right
width: Math.max(14, badgeLabel.implicitWidth + 6)
height: 14
radius: 7
color: t.notifBadgeBg
Behavior on width { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
Text {
id: badgeLabel
anchors.centerIn: parent
text: root.unreadCount > 99 ? "99+" : root.unreadCount.toString()
color: t.notifBadgeText
font.pixelSize: 8
font.bold: true
}
}
}
ListModel { id: notifHistory }
ListModel { id: activeToasts }
function removeToast(toastId) {
for (var i = 0; i < activeToasts.count; i++) {
if (activeToasts.get(i).toastId === toastId) {
activeToasts.remove(i)
return
}
}
}
function urgencyColor(urgency) {
if (urgency === NotificationUrgency.Critical) return t.notifUrgencyCritical
if (urgency === NotificationUrgency.Normal) return t.notifUrgencyNormal
return t.notifUrgencyLow
}
function urgencyIcon(urgency) {
if (urgency === NotificationUrgency.Critical) return "󰀪"
if (urgency === NotificationUrgency.Normal) return "󰋼"
return "󰵙"
}
NotificationServer {
id: notifServer
keepOnReload: false
actionIconsSupported: true
bodyMarkupSupported: true
bodySupported: true
persistenceSupported: true
imageSupported: true
onNotification: function(notif) {
notif.tracked = true
var timeStr = new Date().toLocaleTimeString(Qt.locale(), "HH:mm")
var toastId = notif.id + "_" + Date.now()
notifHistory.insert(0, {
nid: notif.id,
appName: notif.appName || "Unknown",
summary: notif.summary || "",
body: notif.body || "",
urgency: notif.urgency,
timeStr: timeStr
})
while (notifHistory.count > Cfg.Config.notificationHistorySize)
notifHistory.remove(notifHistory.count - 1)
var cfgTimeout = notif.urgency === NotificationUrgency.Critical
? Cfg.Config.notifTimeoutCritical
: (notif.urgency === NotificationUrgency.Low
? Cfg.Config.notifTimeoutLow
: Cfg.Config.notifTimeoutNormal)
var finalTimeout = (notif.expireTimeout > 0) ? notif.expireTimeout : cfgTimeout
var toastEntry = {
toastId: toastId,
nid: notif.id,
appName: notif.appName || "Unknown",
summary: notif.summary || "",
body: notif.body || "",
urgency: notif.urgency,
timeStr: timeStr,
timeout: finalTimeout
}
if (toastLayer.atBottom) activeToasts.append(toastEntry)
else activeToasts.insert(0, toastEntry)
if (!root.historyOpen) root.unreadCount++
}
}
PanelWindow {
id: toastLayer
screen: root.targetScreen
visible: activeToasts.count > 0
color: "transparent"
readonly property string pos: Cfg.Config.notificationPosition
readonly property bool atRight: pos === "topright" || pos === "bottomright"
readonly property bool atBottom: pos === "bottomleft" || pos === "bottomright"
anchors { top: true; bottom: true; right: atRight; left: !atRight }
implicitWidth: 350 + root.barMargin + 4
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.namespace: "main-shell-notifications-toasts"
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
mask: Region { item: toastCol }
readonly property bool shareEdge: (root.isTop && !atBottom) || (!root.isTop && atBottom)
readonly property int edgeGap: shareEdge ? root.barMargin + root.barHeight + 10 : root.barMargin + 10
Column {
id: toastCol
x: !toastLayer.atRight ? 4 : 4
y: !toastLayer.atBottom ? toastLayer.edgeGap : parent.height - toastLayer.edgeGap - toastCol.height
spacing: 8
width: 350
Repeater {
model: activeToasts
Notif.NotificationCard {
width: toastCol.width
toastId: model.toastId
appName: model.appName
summary: model.summary
body: model.body
urgency: model.urgency
timeStr: model.timeStr
timeout: model.timeout
t: root.t
urgencyColor: root.urgencyColor
urgencyIcon: root.urgencyIcon
onDismissed: (id) => root.removeToast(id)
}
}
}
}
PanelWindow {
id: historyPopup
screen: root.targetScreen
visible: false
color: "transparent"
property bool popupOpen: false
function open() { hideTimer.stop(); visible = true; popupOpen = true }
function close() { popupOpen = false; hideTimer.restart() }
Timer {
id: hideTimer
interval: Cfg.Config.popupAnimDuration + 70
onTriggered: historyPopup.visible = false
}
anchors { top: true; bottom: true; left: true; right: true }
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "main-shell-notifications-history"
WlrLayershell.exclusiveZone: -1
mask: popupOpen ? null : _noInput
Region { id: _noInput }
MouseArea {
anchors.fill: parent
onClicked: historyPopup.close()
}
Item {
id: histClipContainer
readonly property int cardW: 340
readonly property int screenPad: root.barMargin + 10
x: {
var cx = root.mapToItem(null, 0, 0).x + root.width / 2
return Math.max(screenPad, Math.min(cx - cardW / 2, parent.width - cardW - screenPad))
}
width: cardW
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
height: parent.height - root.barHeight - 10
clip: true
Rectangle {
id: historyCard
width: parent.width
height: Math.min(histHeader.implicitHeight + 56 + (notifHistory.count > 0 ? notifHistory.count * 76 : 90), 520)
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
transform: Translate {
y: historyPopup.popupOpen ? 0 : (root.isTop ? -historyCard.height : historyCard.height)
Behavior on y {
NumberAnimation {
duration: Cfg.Config.popupAnimDuration;
easing.type: Easing.OutExpo
}
enabled: historyPopup.visible
}
}
opacity: historyPopup.popupOpen ? 1.0 : 0.0
Behavior on opacity { NumberAnimation { duration: 200 } }
color: t.notifHistoryBg
radius: Cfg.Config.popupRadius
border.color: t.notifHistoryBorder
border.width: Cfg.Config.popupBorderWidth
layer.enabled: true
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
RowLayout {
id: histHeader
anchors { left: parent.left; right: parent.right; top: parent.top }
anchors.topMargin: 20; anchors.leftMargin: 20; anchors.rightMargin: 20
spacing: 10
Text { text: "󰂚"; color: t.notifToastAppName; font.pixelSize: 18; Layout.alignment: Qt.AlignVCenter }
Text {
text: "Notifications"; color: t.notifToastAppName; font.pixelSize: 15; font.bold: true
Layout.fillWidth: true; Layout.alignment: Qt.AlignVCenter
}
Rectangle {
visible: notifHistory.count > 0
Layout.alignment: Qt.AlignVCenter
width: clearText.implicitWidth + 16
height: 24
radius: 12
color: clearHover.hovered ? t.notifHistoryHover : "transparent"
Behavior on color { ColorAnimation { duration: 150 } }
Text { id: clearText; anchors.centerIn: parent; text: "Clear all"; color: t.notifToastDim; font.pixelSize: 11 }
HoverHandler { id: clearHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: { notifHistory.clear(); root.unreadCount = 0 } }
}
}
Rectangle {
id: histDivider
anchors { left: parent.left; right: parent.right; top: histHeader.bottom }
anchors.topMargin: 12; height: 1; color: t.notifSeparator
}
Flickable {
id: histFlick
anchors { left: parent.left; right: parent.right; top: histDivider.bottom; bottom: parent.bottom }
anchors.leftMargin: 8; anchors.rightMargin: 8
anchors.topMargin: 6; anchors.bottomMargin: 8; clip: true
contentHeight: histCol.implicitHeight
flickableDirection: Flickable.VerticalFlick
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
Column {
id: histCol
width: histFlick.width; spacing: 2; topPadding: 4; bottomPadding: 4; leftPadding: 8; rightPadding: 8
Item {
width: histCol.width - 16; height: 90; visible: notifHistory.count === 0
ColumnLayout {
anchors.centerIn: parent; spacing: 8
Text { text: "󰂚"; color: t.notifHistoryEmpty; font.pixelSize: 30; Layout.alignment: Qt.AlignHCenter }
Text { text: "No notifications"; color: t.notifHistoryEmpty; font.pixelSize: 12; Layout.alignment: Qt.AlignHCenter }
}
}
Repeater {
model: notifHistory
Notif.HistoryCard {
width: histCol.width - 16; x: 8; itemIndex: index
appName: model.appName; summary: model.summary; body: model.body; urgency: model.urgency; timeStr: model.timeStr
t: root.t; urgencyColor: root.urgencyColor; urgencyIcon: root.urgencyIcon
onDismissed: (idx) => notifHistory.remove(idx)
}
}
}
}
}
}
}
}

181
bar/modules/Stats.qml Normal file
View File

@@ -0,0 +1,181 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import "../../config" as Cfg
Rectangle {
id: root
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
property int barHeight: Cfg.Config.barHeight
property int barMargin: Cfg.Config.margin
readonly property var t: Cfg.Config.theme
width: statsLayout.implicitWidth + 20
height: Cfg.Config.moduleHeight
color: t.statsBg
radius: panelRadius
border.color: t.statsBorder
border.width: borderWidth
Behavior on width {
NumberAnimation { duration: 250; easing.type: Easing.OutCubic }
}
HoverHandler { cursorShape: Qt.PointingHandCursor }
property real cpuVal: 0
property real memVal: 0
property real diskVal: 0
Timer {
interval: 2000
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
cpuProc.running = true
memProc.running = true
diskProc.running = true
}
}
Process {
id: cpuProc
command: ["sh", "-c", "top -bn1 | awk '/^%Cpu/ {print 100-$8}'"]
stdout: SplitParser {
onRead: data => {
let val = parseFloat(data.trim())
root.cpuVal = isNaN(val) ? 0 : val
}
}
}
Process {
id: memProc
command: ["sh", "-c", "free | awk '/Mem:/ {print ($2-$7)/$2 * 100}'"]
stdout: SplitParser {
onRead: data => {
let val = parseFloat(data.trim())
root.memVal = isNaN(val) ? 0 : val
}
}
}
Process {
id: diskProc
command: ["sh", "-c", "df / --output=pcent | tail -1 | tr -dc '0-9'"]
stdout: SplitParser {
onRead: data => {
let val = parseFloat(data.trim())
root.diskVal = isNaN(val) ? 0 : val
}
}
}
component StatRing: Item {
id: ring
property real value: 0.0
property string label: ""
property color ringColor: Cfg.Config.theme.accent
property color trackColor: Cfg.Config.theme.statsTrackColor
property color labelColor: Cfg.Config.theme.textMain
Behavior on value {
NumberAnimation {
duration: 800
easing.type: Easing.OutCubic
}
}
readonly property real fraction: Math.max(0, Math.min(value, 100)) / 100
width: Cfg.Config.statsRingSize
height: Cfg.Config.statsRingSize
onFractionChanged: arc.requestPaint()
onRingColorChanged: arc.requestPaint()
Canvas {
id: arc
anchors.fill: parent
antialiasing: true
onPaint: {
var ctx = getContext("2d")
ctx.reset()
ctx.clearRect(0, 0, width, height)
var cx = width / 2
var cy = height / 2
var lw = 2.8
var r = Math.min(cx, cy) - lw / 2 - 0.5
ctx.beginPath()
ctx.arc(cx, cy, r, 0, Math.PI * 2)
ctx.strokeStyle = ring.trackColor
ctx.lineWidth = lw
ctx.stroke()
if (ring.fraction > 0) {
var start = -Math.PI / 2
var end = start + (ring.fraction * Math.PI * 2)
ctx.beginPath()
ctx.arc(cx, cy, r, start, end)
ctx.strokeStyle = ring.ringColor
ctx.lineWidth = lw
ctx.lineCap = "round"
ctx.stroke()
}
}
}
HoverHandler { id: hov }
Text {
anchors.centerIn: parent
text: Cfg.Config.statsShowPercentByDefault
? (hov.hovered ? ring.label : Math.round(ring.value) + "%")
: (hov.hovered ? Math.round(ring.value) + "%" : ring.label)
color: ring.labelColor
font.pixelSize: Cfg.Config.statsRingFontSize
font.bold: true
font.family: "monospace"
}
}
RowLayout {
id: statsLayout
anchors.centerIn: parent
spacing: 10
StatRing {
value: root.cpuVal
label: "cpu"
ringColor: root.t.statsCpuColor
trackColor: root.t.statsTrackColor
labelColor: root.t.statsText
}
StatRing {
value: root.memVal
label: "ram"
ringColor: root.t.statsMemColor
trackColor: root.t.statsTrackColor
labelColor: root.t.statsText
}
StatRing {
value: root.diskVal
label: "disk"
ringColor: root.t.statsDiskColor
trackColor: root.t.statsTrackColor
labelColor: root.t.statsText
}
}
}

161
bar/modules/Tray.qml Normal file
View File

@@ -0,0 +1,161 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.SystemTray
import Quickshell.Widgets
import "../../config" as Cfg
Item {
id: root
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
property bool isTop: true
property var targetScreen: null
property string barSide: "right"
readonly property var t: Cfg.Config.theme
property bool expanded: false
property bool manuallyExpanded: false
property var menuWindow: null
implicitWidth: bgRect.width
implicitHeight: bgRect.height
Rectangle {
id: bgRect
anchors.verticalCenter: parent.verticalCenter
height: Cfg.Config.trayHeight
width: mainRow.width
radius: root.panelRadius
color: bgHover.hovered ? root.t.trayBgHover : root.t.trayBg
border.color: bgHover.hovered ? root.t.trayBorderHover : root.t.trayBorder
border.width: root.borderWidth
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
HoverHandler {
id: bgHover
onHoveredChanged: {
if (!root.manuallyExpanded) {
root.expanded = hovered;
}
}
}
Row {
id: mainRow
anchors.verticalCenter: parent.verticalCenter
leftPadding: root.expanded && root.barSide === "right" ? 8 : 0
rightPadding: root.expanded && root.barSide === "left" ? 8 : 0
Behavior on leftPadding { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
Behavior on rightPadding { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
layoutDirection: root.barSide === "left" ? Qt.LeftToRight : Qt.RightToLeft
spacing: root.expanded ? 4 : 0
Behavior on spacing { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } }
Item {
id: toggleArea
width: Cfg.Config.trayHeight
height: Cfg.Config.trayHeight
z: 10
TapHandler {
onTapped: {
root.manuallyExpanded = !root.manuallyExpanded;
root.expanded = root.manuallyExpanded;
}
}
Text {
anchors.centerIn: parent
text: root.expanded
? (root.barSide === "left" ? "󰅁" : "󰅂")
: (root.barSide === "left" ? "󰅂" : "󰅁")
color: root.t.trayIcon
font.pixelSize: Cfg.Config.trayHeight * 0.55
}
}
Row {
id: iconsRow
spacing: 6
clip: true
anchors.verticalCenter: parent.verticalCenter
width: root.expanded ? implicitWidth : 0
opacity: root.expanded ? 1 : 0
Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
Behavior on opacity { NumberAnimation { duration: 150 } }
Repeater {
model: SystemTray.items
delegate: MouseArea {
id: iconDelegate
required property SystemTrayItem modelData
property alias item: iconDelegate.modelData
implicitWidth: Cfg.Config.trayIconSize
implicitHeight: Cfg.Config.trayIconSize
width: Cfg.Config.trayIconSize
height: Cfg.Config.trayIconSize
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true
onClicked: function(mouse) {
if (mouse.button === Qt.LeftButton) {
item.activate()
} else if (mouse.button === Qt.MiddleButton) {
item.secondaryActivate()
} else if (mouse.button === Qt.RightButton) {
menuAnchor.open()
}
}
onWheel: function(wheel) {
wheel.accepted = true
item.scroll(wheel.angleDelta.y / 120, false)
}
IconImage {
anchors.fill: parent
source: item.icon
}
QsMenuAnchor {
id: menuAnchor
menu: item.menu
anchor.window: root.menuWindow
anchor.adjustment: PopupAdjustment.Flip
anchor.onAnchoring: {
if (!root.menuWindow) return
const global = iconDelegate.mapToGlobal(0, iconDelegate.height)
const local = root.menuWindow.contentItem.mapFromGlobal(
global.x, global.y
)
menuAnchor.anchor.rect = Qt.rect(
local.x, local.y,
iconDelegate.width, 0
)
}
}
}
}
}
}
}
}

530
bar/modules/Volume.qml Normal file
View File

@@ -0,0 +1,530 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Services.Pipewire
import "../../config" as Cfg
Rectangle {
id: root
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
property bool isTop: true
property var targetScreen: null
property int barHeight: Cfg.Config.barHeight
property int barMargin: Cfg.Config.margin
readonly property var t: Cfg.Config.theme
property int outputMaxVolume: Cfg.Config.volumeOutputMax
property int inputMaxVolume: Cfg.Config.volumeInputMax
readonly property var sink: Pipewire.defaultAudioSink
readonly property var source: Pipewire.defaultAudioSource
PwObjectTracker {
objects: root.sinkNodes.concat(root.sourceNodes)
}
readonly property var sinkNodes: {
if (!Pipewire.nodes) return []
return Pipewire.nodes.values.filter(n => !n.isStream && n.isSink)
}
readonly property var sourceNodes: {
if (!Pipewire.nodes) return []
return Pipewire.nodes.values.filter(n => !n.isStream && !n.isSink)
}
property int _outVol: 0
property int _inVol: 0
readonly property int outputVolume: {
if (sink && sink.ready) { _outVol = Math.round(sink.audio.volume * 100); return _outVol }
return _outVol
}
readonly property int inputVolume: {
if (source && source.ready) { _inVol = Math.round(source.audio.volume * 100); return _inVol }
return _inVol
}
readonly property bool outputMuted: sink && sink.ready ? sink.audio.muted : false
readonly property bool inputMuted: source && source.ready ? source.audio.muted : false
readonly property string outputDeviceName: sink ? (sink.description || sink.nickName || sink.name) : "No device"
readonly property string inputDeviceName: source ? (source.description || source.nickName || source.name) : "No device"
function setOutputVol(pct) { if (sink && sink.ready) sink.audio.volume = pct / 100 }
function setInputVol(pct) { if (source && source.ready) source.audio.volume = pct / 100 }
function toggleOutputMute() { if (sink && sink.ready) sink.audio.muted = !sink.audio.muted }
function toggleInputMute() { if (source && source.ready) source.audio.muted = !source.audio.muted }
Process {
id: defaultSetter
running: false
property string nodeId: "0"
command: ["wpctl", "set-default", nodeId]
}
function setDefaultDevice(node) {
defaultSetter.nodeId = node.id.toString()
defaultSetter.running = true
}
readonly property string iconSpeakerHigh: "󰕾"
readonly property string iconSpeakerMed: "󰖀"
readonly property string iconSpeakerLow: "󰕿"
readonly property string iconSpeakerMuted: "󰖁"
readonly property string iconMic: "󰍬"
readonly property string iconMicMuted: "󰍭"
function speakerGlyph(muted, vol) {
if (muted || vol === 0) return iconSpeakerMuted
if (vol > 60) return iconSpeakerHigh
if (vol > 25) return iconSpeakerMed
return iconSpeakerLow
}
width: chipLayout.implicitWidth + 15
height: Cfg.Config.moduleHeight
radius: panelRadius
color: hoverHandler.hovered ? t.volBgHover : t.volBg
border.color: (hoverHandler.hovered || volumePopup.popupOpen) ? t.volBorderHover : t.volBorder
border.width: borderWidth
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
HoverHandler { id: hoverHandler; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: volumePopup.toggle() }
RowLayout {
id: chipLayout
anchors.centerIn: parent
spacing: 5
Text {
text: root.speakerGlyph(root.outputMuted, root.outputVolume)
color: root.outputMuted ? root.t.volIconMuted : root.t.volIcon
font.pixelSize: Cfg.Config.volumeIconSize
Layout.alignment: Qt.AlignVCenter
}
}
PanelWindow {
id: volumePopup
screen: root.targetScreen
visible: false
color: "transparent"
property bool popupOpen: false
function open() { hideTimer.stop(); visible = true; popupOpen = true }
function close() { popupOpen = false; hideTimer.restart() }
function toggle() { popupOpen ? close() : open() }
Timer {
id: hideTimer
interval: Cfg.Config.popupAnimDuration + 70
onTriggered: volumePopup.visible = false
}
anchors { top: true; bottom: true; left: true; right: true }
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.namespace: "main-shell-volume"
WlrLayershell.exclusiveZone: -1
mask: popupOpen ? null : _noInput
Region { id: _noInput }
MouseArea {
anchors.fill: parent
onClicked: volumePopup.close()
}
Item {
id: volClipContainer
readonly property int cardW: Cfg.Config.volumePopupWidth
readonly property int screenPad: root.barMargin + 10
x: {
let cx = root.mapToItem(null, 0, 0).x + root.width / 2
return Math.max(screenPad, Math.min(cx - cardW / 2, parent.width - cardW - screenPad))
}
width: cardW
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
height: parent.height - root.barHeight - 10
clip: true
Rectangle {
id: volCard
width: parent.width
height: popupCol.implicitHeight + 32
anchors.top: root.isTop ? parent.top : undefined
anchors.bottom: root.isTop ? undefined : parent.bottom
transform: Translate {
y: volumePopup.popupOpen ? 0 : (root.isTop ? -volCard.height : volCard.height)
Behavior on y {
NumberAnimation {
duration: Cfg.Config.popupAnimDuration;
easing.type: Easing.OutExpo
}
enabled: volumePopup.visible
}
}
opacity: volumePopup.popupOpen ? 1.0 : 0.0
Behavior on opacity { NumberAnimation { duration: 200 } }
radius: Cfg.Config.popupRadius
color: root.t.volPopupBg
border.color: root.t.volPopupBorder
border.width: Cfg.Config.popupBorderWidth
layer.enabled: true
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
ColumnLayout {
id: popupCol
anchors { left: parent.left; right: parent.right; top: parent.top }
anchors.margins: 16
anchors.topMargin: 18
spacing: 14
VolumeSection {
id: outputSection
Layout.fillWidth: true
label: "Output"
icon: root.speakerGlyph(root.outputMuted, root.outputVolume)
iconColor: root.t.accent
volume: root.outputVolume
maxVolume: root.outputMaxVolume
muted: root.outputMuted
accentColor: root.t.volHandle
dimColor: root.t.textDim
textColor: root.t.textMain
trackColor: root.t.volTrack
deviceName: root.outputDeviceName
nodes: root.sinkNodes
currentNode: root.sink
onSliderMoved: vol => root.setOutputVol(vol)
onMuteToggled: root.toggleOutputMute()
onDeviceSelected: node => root.setDefaultDevice(node)
}
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.separator }
VolumeSection {
id: inputSection
Layout.fillWidth: true
label: "Input"
icon: root.inputMuted ? root.iconMicMuted : root.iconMic
iconColor: root.t.statusInfo
volume: root.inputVolume
maxVolume: root.inputMaxVolume
muted: root.inputMuted
accentColor: root.t.statusInfo
dimColor: root.t.textDim
textColor: root.t.textMain
trackColor: root.t.volTrack
deviceName: root.inputDeviceName
nodes: root.sourceNodes
currentNode: root.source
onSliderMoved: vol => root.setInputVol(vol)
onMuteToggled: root.toggleInputMute()
onDeviceSelected: node => root.setDefaultDevice(node)
}
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.separator }
ColumnLayout {
Layout.fillWidth: true
spacing: 10
Text {
text: "Volume Limits"
color: root.t.textDim
font.pixelSize: 10
font.capitalization: Font.AllUppercase
Layout.fillWidth: true
}
LimitRow {
Layout.fillWidth: true
icon: root.iconSpeakerHigh
iconColor: root.t.accent
labelText: "Output max"
value: root.outputMaxVolume
minValue: 10
maxValue: 300
textColor: root.t.textMain
dimColor: root.t.textDim
borderColor: root.t.volPopupBorder
onDecrement: root.outputMaxVolume = Math.max(10, root.outputMaxVolume - 10)
onIncrement: root.outputMaxVolume = Math.min(300, root.outputMaxVolume + 10)
}
LimitRow {
Layout.fillWidth: true
icon: root.iconMic
iconColor: root.t.statusInfo
labelText: "Input max"
value: root.inputMaxVolume
minValue: 10
maxValue: 300
textColor: root.t.textMain
dimColor: root.t.textDim
borderColor: root.t.volPopupBorder
onDecrement: root.inputMaxVolume = Math.max(10, root.inputMaxVolume - 10)
onIncrement: root.inputMaxVolume = Math.min(300, root.inputMaxVolume + 10)
}
}
Item { height: 2 }
}
}
}
}
component VolumeSection: ColumnLayout {
id: vs
spacing: 8
required property string label
required property string icon
required property color iconColor
required property int volume
required property int maxVolume
required property bool muted
required property color accentColor
required property color dimColor
required property color textColor
required property color trackColor
required property string deviceName
required property var nodes
required property var currentNode
signal sliderMoved(int vol)
signal muteToggled()
signal deviceSelected(var node)
property bool pickerOpen: false
RowLayout {
Layout.fillWidth: true
spacing: 6
Text {
text: vs.icon
color: vs.muted ? vs.dimColor : vs.iconColor
font.pixelSize: Cfg.Config.volumeIconSize
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: Cfg.Config.volumeIconSize + 4
horizontalAlignment: Text.AlignHCenter
}
Text {
text: vs.label
color: vs.textColor
font.pixelSize: 13
font.bold: true
Layout.fillWidth: true
}
Text {
text: vs.muted ? "muted" : vs.volume + "%"
color: vs.muted ? vs.dimColor : vs.textColor
font.pixelSize: 12
font.bold: !vs.muted
horizontalAlignment: Text.AlignHCenter
Layout.preferredWidth: 44
}
Rectangle {
width: 22; height: 22; radius: 6
color: muteHover.hovered ? (vs.muted ? "#33ffffff" : "#22ffffff") : (vs.muted ? root.t.volMuteBg : "transparent")
border.color: vs.muted ? vs.accentColor : vs.dimColor
border.width: 1
Text {
anchors.centerIn: parent
text: vs.muted ? "󰖁" : "󰕾"
color: vs.muted ? vs.accentColor : vs.dimColor
font.pixelSize: 11
}
HoverHandler { id: muteHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: vs.muteToggled() }
}
}
Item {
id: sliderItem
Layout.fillWidth: true
height: 22
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width; height: Cfg.Config.volumeSliderHeight; radius: 2
color: vs.trackColor
Rectangle {
width: Math.min(parent.width, parent.width * (vs.volume / Math.max(1, vs.maxVolume)))
height: parent.height; radius: parent.radius
color: vs.muted ? vs.dimColor : vs.accentColor
}
}
Rectangle {
readonly property real ratio: vs.volume / Math.max(1, vs.maxVolume)
x: Math.min(sliderItem.width - width, Math.max(0, sliderItem.width * ratio - width / 2))
anchors.verticalCenter: parent.verticalCenter
width: Cfg.Config.volumeHandleSize; height: Cfg.Config.volumeHandleSize; radius: width / 2
color: vs.muted ? vs.dimColor : vs.accentColor
border.color: root.t.volHandleBorder
border.width: 1
}
MouseArea {
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
onPressed: pos => sliderItem.seek(pos.x)
onPositionChanged: pos => { if (pressed) sliderItem.seek(pos.x) }
}
function seek(x) {
let ratio = Math.max(0.0, Math.min(1.0, x / sliderItem.width))
vs.sliderMoved(Math.round(ratio * vs.maxVolume))
}
}
RowLayout {
Layout.fillWidth: true
spacing: 6
Text { text: "󰒓"; color: vs.dimColor; font.pixelSize: 11 }
Text {
text: vs.deviceName
color: vs.textColor
font.pixelSize: 11
elide: Text.ElideRight
Layout.fillWidth: true
}
Rectangle {
width: 20; height: 20; radius: 5
color: chevHover.hovered ? "#22ffffff" : "transparent"
border.color: vs.pickerOpen ? vs.accentColor : root.t.volPopupBorder
border.width: 1
Text {
anchors.centerIn: parent
text: vs.pickerOpen ? "󰅃" : "󰅀"
color: vs.pickerOpen ? vs.accentColor : vs.dimColor
font.pixelSize: 10
}
HoverHandler { id: chevHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: vs.pickerOpen = !vs.pickerOpen }
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: vs.pickerOpen ? Math.min(deviceFlickable.contentHeight, 120) : 0
opacity: vs.pickerOpen ? 1.0 : 0.0
clip: true
Behavior on Layout.preferredHeight { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
Behavior on opacity { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
Flickable {
id: deviceFlickable
anchors.fill: parent
contentWidth: width
contentHeight: deviceList.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
ColumnLayout {
id: deviceList
width: deviceFlickable.width
spacing: 2
Repeater {
model: vs.nodes
delegate: Rectangle {
required property var modelData
readonly property bool isActive: vs.currentNode !== null && modelData.id === vs.currentNode.id
Layout.fillWidth: true; height: 28; radius: 7
color: isActive ? Qt.alpha(vs.accentColor, 0.18) : (rowHover.hovered ? root.t.volPickerHover : "transparent")
border.color: isActive ? Qt.alpha(vs.accentColor, 0.45) : "transparent"
border.width: 1
RowLayout {
anchors { fill: parent; leftMargin: 8; rightMargin: 8 }
spacing: 8
Rectangle {
width: 6; height: 6; radius: 3
color: isActive ? vs.accentColor : "transparent"
border.color: isActive ? "transparent" : vs.dimColor
border.width: 1
}
Text {
text: modelData.description || modelData.nickName || modelData.name
color: isActive ? vs.accentColor : vs.textColor
font.pixelSize: 11; font.bold: isActive
elide: Text.ElideRight; Layout.fillWidth: true
}
}
HoverHandler { id: rowHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: { vs.deviceSelected(modelData); vs.pickerOpen = false } }
}
}
}
}
}
}
component LimitRow: RowLayout {
id: lr
spacing: 8
required property string icon
required property color iconColor
required property string labelText
required property int value
required property int minValue
required property int maxValue
required property color textColor
required property color dimColor
required property color borderColor
signal decrement()
signal increment()
Text { text: lr.icon; color: lr.iconColor; font.pixelSize: 13 }
Text {
text: lr.labelText
color: lr.textColor
font.pixelSize: 11
Layout.fillWidth: true
}
Rectangle {
width: 22; height: 22; radius: 5
color: decHover.hovered ? "#22ffffff" : "transparent"
border.color: lr.borderColor; border.width: 1
Text {
anchors.centerIn: parent; text: ""
color: lr.value <= lr.minValue ? lr.dimColor : lr.textColor
font.pixelSize: 14
}
HoverHandler { id: decHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: { if (lr.value > lr.minValue) lr.decrement() } }
}
Text {
text: lr.value + "%"
color: lr.textColor
font.pixelSize: 12; font.bold: true
horizontalAlignment: Text.AlignHCenter
Layout.preferredWidth: 44
}
Rectangle {
width: 22; height: 22; radius: 5
color: incHover.hovered ? "#22ffffff" : "transparent"
border.color: lr.borderColor; border.width: 1
Text {
anchors.centerIn: parent; text: "+"
color: lr.value >= lr.maxValue ? lr.dimColor : lr.textColor
font.pixelSize: 14
}
HoverHandler { id: incHover; cursorShape: Qt.PointingHandCursor }
TapHandler { onTapped: { if (lr.value < lr.maxValue) lr.increment() } }
}
}
}

62
bar/modules/Workspace.qml Normal file
View File

@@ -0,0 +1,62 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
import "../../config" as Cfg
Rectangle {
id: root
property real panelRadius: Cfg.Config.radius / 2
property int borderWidth: Cfg.Config.panelBorderWidth
readonly property var t: Cfg.Config.theme
width: workspaceLayout.implicitWidth + 24
height: Cfg.Config.moduleHeight
color: t.wsPanelBg
radius: panelRadius
border.color: t.wsPanelBorder
border.width: borderWidth
RowLayout {
id: workspaceLayout
anchors.centerIn: parent
spacing: 6
Repeater {
model: Hyprland.workspaces
Rectangle {
Layout.preferredWidth: modelData.active ? 34 : 28
Layout.preferredHeight: 22
radius: 11
color: modelData.active
? root.t.wsActiveBg
: (wsHover.containsMouse ? root.t.wsHoverBg : root.t.wsInactiveBg)
Behavior on color { ColorAnimation { duration: 200 } }
Behavior on Layout.preferredWidth { NumberAnimation { duration: 200 } }
Text {
anchors.centerIn: parent
text: modelData.name
font.pixelSize: 11
font.bold: true
color: modelData.active
? root.t.wsActiveText
: (wsHover.containsMouse ? root.t.wsHoverText : root.t.wsInactiveText)
}
MouseArea {
id: wsHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: modelData.activate()
}
}
}
}
}