diff --git a/Bar.qml b/Bar.qml index 596d8ad..9bfddee 100644 --- a/Bar.qml +++ b/Bar.qml @@ -48,6 +48,20 @@ Scope { Layout.topMargin: 6 Layout.bottomMargin: 6 } + + AudioWidget { + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.fillHeight: true + Layout.topMargin: 6 + Layout.bottomMargin: 6 + } + + Resources { + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.fillHeight: true + Layout.topMargin: 6 + Layout.bottomMargin: 6 + } } RowLayout { diff --git a/Modules/ActiveWindow.qml b/Modules/ActiveWindow.qml index 50bb037..e942be7 100644 --- a/Modules/ActiveWindow.qml +++ b/Modules/ActiveWindow.qml @@ -5,5 +5,5 @@ import Quickshell.Hyprland Singleton { id: root - property string activeWindow: Hyprland.activeToplevel?.lastIpcObject.class || "" + property string activeWindow: Hyprland.activeToplevel?.title || "" } diff --git a/Modules/AudioWidget.qml b/Modules/AudioWidget.qml new file mode 100644 index 0000000..110a082 --- /dev/null +++ b/Modules/AudioWidget.qml @@ -0,0 +1,112 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Services.Pipewire +import Quickshell.Widgets +import qs.Modules + +Item { + id: root + implicitWidth: 150 + implicitHeight: 100 + + PwObjectTracker { + objects: [ Pipewire.defaultAudioSink ] + } + + PwObjectTracker { + objects: [ Pipewire.defaultAudioSource ] + } + + Rectangle { + anchors.fill: parent + radius: height / 2 + color: "#40000000" + + // Background circle + Rectangle { + anchors.centerIn: parent + width: parent.width + height: parent.height + radius: width / 2 + color: "transparent" + border.color: "#30ffffff" + border.width: 0 + } + + RowLayout { + anchors { + fill: parent + leftMargin: 10 + rightMargin: 15 + } + + Text { + Layout.alignment: Qt.AlignVCenter + font.family: "Material Symbols Rounded" + font.pixelSize: 18 + text: "\ue050" // volume_up icon + color: "#ffffff" + } + + Rectangle { + Layout.fillWidth: true + + implicitHeight: 4 + radius: 20 + color: "#50ffffff" + + Rectangle { + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + + implicitWidth: parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0) + radius: parent.radius + color: "#ffffff" + } + } + + Text { + Layout.alignment: Qt.AlignVCenter + font.family: "Material Symbols Rounded" + font.pixelSize: 18 + text: "\ue029" + color: (Pipewire.defaultAudioSource?.audio.muted ?? false) ? "#ff4444" : "#ffffff" + } + + Rectangle { + Layout.fillWidth: true + + implicitHeight: 4 + radius: 20 + color: "#50ffffff" + + Rectangle { + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + + implicitWidth: parent.width * (Pipewire.defaultAudioSource?.audio.volume ?? 0) + radius: parent.radius + color: (Pipewire.defaultAudioSource?.audio.muted ?? false) ? "#ff4444" : "#ffffff" + } + } + } + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: pavucontrolProc.running = true + } + Process { + id: pavucontrolProc + command: ["pavucontrol"] + running: false + } + } +} diff --git a/Modules/Resource.qml b/Modules/Resource.qml new file mode 100644 index 0000000..5127edf --- /dev/null +++ b/Modules/Resource.qml @@ -0,0 +1,99 @@ +import qs.Modules +import QtQuick +import QtQuick.Layouts +import Quickshell + +Item { + id: root + required property double percentage + property int warningThreshold: 100 + property bool shown: true + clip: true + visible: width > 0 && height > 0 + implicitWidth: resourceRowLayout.x < 0 ? 0 : resourceRowLayout.implicitWidth + implicitHeight: 22 + property bool warning: percentage * 100 >= warningThreshold + + RowLayout { + id: resourceRowLayout + spacing: 2 + x: shown ? 0 : -resourceRowLayout.width + anchors { + verticalCenter: parent.verticalCenter + } + + Item { + Layout.alignment: Qt.AlignVCenter + implicitWidth: 14 + implicitHeight: root.implicitHeight + + // Background circle + Rectangle { + id: backgroundCircle + anchors.centerIn: parent + width: 14 + height: 14 + radius: height / 2 + color: "#40000000" + border.color: "#404040" + border.width: 1 + } + + // Pie progress indicator + Canvas { + id: progressCanvas + anchors.fill: backgroundCircle + + Connections { + target: root + function onPercentageChanged() { + progressCanvas.requestPaint() + } + function onWarningChanged() { + progressCanvas.requestPaint() + } + } + + onPaint: { + var ctx = getContext("2d"); + ctx.reset(); + + var centerX = width / 2; + var centerY = height / 2; + var radius = width / 2; + var startAngle = -Math.PI / 2; // Start at top + var endAngle = startAngle + (2 * Math.PI * percentage); + + ctx.fillStyle = warning ? "#ff6b6b" : "#4080ff"; + ctx.beginPath(); + ctx.moveTo(centerX, centerY); + ctx.arc(centerX, centerY, radius, startAngle, endAngle); + ctx.lineTo(centerX, centerY); + ctx.fill(); + } + } + + // // Percentage text + // Item { + // anchors.centerIn: backgroundCircle + // implicitWidth: fullPercentageTextMetrics.width + // implicitHeight: percentageText.implicitHeight + // + // TextMetrics { + // id: fullPercentageTextMetrics + // text: "100" + // font.pixelSize: 12 + // } + // + // Text { + // id: percentageText + // anchors.centerIn: parent + // color: "#ffffff" + // font.pixelSize: 12 + // font.bold: true + // text: `${Math.round(percentage * 100).toString()}` + // } + // } + } + } +} diff --git a/Modules/ResourceUsage.qml b/Modules/ResourceUsage.qml new file mode 100644 index 0000000..9e02e7d --- /dev/null +++ b/Modules/ResourceUsage.qml @@ -0,0 +1,78 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io + +/** + * Simple polled resource usage service with RAM, Swap, and CPU usage. + */ +Singleton { + property double memoryTotal: 1 + property double memoryFree: 1 + property double memoryUsed: memoryTotal - memoryFree + property double memoryUsedPercentage: memoryUsed / memoryTotal + property double swapTotal: 1 + property double swapFree: 1 + property double swapUsed: swapTotal - swapFree + property double swapUsedPercentage: swapTotal > 0 ? (swapUsed / swapTotal) : 0 + property double cpuUsage: 0 + property var previousCpuStats + property double gpuUsage: 0 + property double gpuMemUsage: 0 + + Timer { + interval: 1 + running: true + repeat: true + onTriggered: { + // Reload files + fileMeminfo.reload() + fileStat.reload() + + // Parse memory and swap usage + const textMeminfo = fileMeminfo.text() + memoryTotal = Number(textMeminfo.match(/MemTotal: *(\d+)/)?.[1] ?? 1) + memoryFree = Number(textMeminfo.match(/MemAvailable: *(\d+)/)?.[1] ?? 0) + swapTotal = Number(textMeminfo.match(/SwapTotal: *(\d+)/)?.[1] ?? 1) + swapFree = Number(textMeminfo.match(/SwapFree: *(\d+)/)?.[1] ?? 0) + + // Parse CPU usage + const textStat = fileStat.text() + const cpuLine = textStat.match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/) + if (cpuLine) { + const stats = cpuLine.slice(1).map(Number) + const total = stats.reduce((a, b) => a + b, 0) + const idle = stats[3] + + if (previousCpuStats) { + const totalDiff = total - previousCpuStats.total + const idleDiff = idle - previousCpuStats.idle + cpuUsage = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0 + } + + previousCpuStats = { total, idle } + } + processGpu.running = true + + interval = 1000 + } + } + + FileView { id: fileMeminfo; path: "/proc/meminfo" } + FileView { id: fileStat; path: "/proc/stat" } + + Process { + id: processGpu + command: ["nvidia-smi", "--query-gpu=utilization.gpu,utilization.memory", "--format=csv,noheader,nounits"] + running: false + stdout: StdioCollector { + onStreamFinished: { + const parts = this.text.trim().split(", ") + gpuUsage = Number(parts[0]) / 100 + gpuMemUsage = Number(parts[1]) / 100 + } + } + } +} diff --git a/Modules/Resources.qml b/Modules/Resources.qml new file mode 100644 index 0000000..5c908ef --- /dev/null +++ b/Modules/Resources.qml @@ -0,0 +1,71 @@ +import QtQuick +import Quickshell +import QtQuick.Layouts +import qs.Modules + +Item { + id: root + implicitWidth: rowLayout.implicitWidth + rowLayout.anchors.leftMargin + rowLayout.anchors.rightMargin + + Rectangle { + anchors.fill: parent + color: "#40000000" + radius: height / 2 + RowLayout { + id: rowLayout + + spacing: 6 + anchors.fill: parent + anchors.leftMargin: 5 + anchors.rightMargin: 5 + + Text { + Layout.alignment: Qt.AlignVCenter + font.family: "Material Symbols Rounded" + font.pixelSize: 18 + text: "\uf7a3" + color: "#ffffff" + } + + Resource { + percentage: ResourceUsage.memoryUsedPercentage + } + + Text { + Layout.alignment: Qt.AlignVCenter + font.family: "Material Symbols Rounded" + font.pixelSize: 18 + text: "\ue322" + color: "#ffffff" + } + + Resource { + percentage: ResourceUsage.cpuUsage + } + + Text { + Layout.alignment: Qt.AlignVCenter + font.family: "Material Symbols Rounded" + font.pixelSize: 18 + text: "\ue30d" + color: "#ffffff" + } + + Resource { + percentage: ResourceUsage.gpuUsage + } + + Text { + Layout.alignment: Qt.AlignVCenter + font.family: "Material Symbols Rounded" + font.pixelSize: 18 + text: "\ue30d" + color: "#ffffff" + } + + Resource { + percentage: ResourceUsage.gpuMemUsage + } + } + } +} diff --git a/Modules/SystemStats.qml b/Modules/SystemStats.qml new file mode 100644 index 0000000..e69de29 diff --git a/Modules/Workspaces.qml b/Modules/Workspaces.qml index 0993dfc..694e154 100644 --- a/Modules/Workspaces.qml +++ b/Modules/Workspaces.qml @@ -7,9 +7,9 @@ import Quickshell.Hyprland Rectangle { id: root - implicitWidth: workspacesRow.implicitWidth + 12 - implicitHeight: workspacesRow.implicitHeight + 8 - + implicitWidth: workspacesRow.implicitWidth + 10 + implicitHeight: workspacesRow.implicitHeight + 7 + color: "#40000000" radius: height / 2 @@ -24,7 +24,7 @@ Rectangle { id: workspacesRow anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 6 + anchors.leftMargin: 5 spacing: 8 Repeater { @@ -33,9 +33,9 @@ Rectangle { Rectangle { required property var modelData - width: 12 - height: 12 - radius: 6 + width: 14 + height: 14 + radius: height / 2 color: modelData.id === Hyprland.focusedWorkspace.id ? "#4080ff" : "#606060"