updated components #122

Merged
zach merged 64 commits from feat/updated-components into main 2026-06-23 11:33:20 +02:00
37 changed files with 2736 additions and 1122 deletions
Showing only changes of commit 2a50732bc2 - Show all commits
+4 -5
View File
@@ -17,8 +17,8 @@ BusyIndicator {
}
property int animState
property color bgColour: DynamicColors.palette.m3secondaryContainer
property color fgColour: DynamicColors.palette.m3primary
property color bgColor: DynamicColors.palette.m3secondaryContainer
property color fgColor: DynamicColors.palette.m3primary
property real implicitSize: Appearance.font.size.normal * 3
property real internalStrokeWidth: strokeWidth
readonly property alias progress: manager.progress
@@ -31,8 +31,8 @@ BusyIndicator {
contentItem: CircularProgress {
anchors.fill: parent
bgColour: root.bgColour
fgColour: root.fgColour
bgColor: root.bgColor
fgColor: root.fgColor
padding: root.padding
rotation: manager.rotation
startAngle: manager.startFraction * 360
@@ -73,7 +73,6 @@ BusyIndicator {
CircularIndicatorManager {
id: manager
}
NumberAnimation {
+79 -29
View File
@@ -1,66 +1,116 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Shapes
import ZShell.Components
import qs.Config
Shape {
Item {
id: root
readonly property real arcRadius: (size - padding - strokeWidth) / 2
property color bgColour: DynamicColors.palette.m3secondaryContainer
property color fgColour: DynamicColors.palette.m3primary
readonly property real arcRadius: (size - padding - strokeWidth * (1 + waveAmplitude * 2)) / 2
property color bgColor: DynamicColors.palette.m3secondaryContainer
property real clampedVal: Math.max(1 / 360, Math.min(1, isNaN(value) ? 0 : value))
readonly property real dotAngleRad: (startAngle + sweepAngle - gapAngle * (sweepAngle < 360 ? 0 : 1)) * Math.PI / 180
property color fgColor: DynamicColors.palette.m3primary
readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI)
property alias hasEndIndicator: dot.active
property real implicitSize
property int padding: 0
readonly property real size: Math.min(width, height)
property int spacing: Appearance.spacing.small
property int startAngle: -90
property int strokeWidth: Appearance.padding.smaller
readonly property real vValue: value || 1 / 360
property int strokeWidth: Appearance.padding.small
property int sweepAngle: 360
readonly property real thickness: strokeWidth * (1 + waveAmplitude) * 2
property real value
property alias waveAmplitude: wave.amplitudeMultiplier
property alias waveDuration: waveProgAnim.duration
property alias waveFrequency: wave.frequency
property bool wavePaused
property bool wavy: false
implicitHeight: implicitSize
implicitWidth: implicitSize
Shape {
asynchronous: true
opacity: Math.min(1, remainingArc.sweepAngle)
preferredRendererType: Shape.CurveRenderer
ShapePath {
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
capStyle: ShapePath.RoundCap
fillColor: "transparent"
strokeColor: root.bgColour
strokeWidth: root.strokeWidth
strokeColor: root.bgColor
strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth
Behavior on strokeColor {
CAnim {
duration: Appearance.anim.durations.large
}
}
PathAngleArc {
id: remainingArc
centerX: root.size / 2
centerY: root.size / 2
radiusX: root.arcRadius
radiusY: root.arcRadius
startAngle: root.startAngle + 360 * root.vValue + root.gapAngle
sweepAngle: Math.max(-root.gapAngle, 360 * (1 - root.vValue) - root.gapAngle * 2)
startAngle: root.startAngle + root.clampedVal * root.sweepAngle + root.gapAngle
sweepAngle: Math.max(1 / 360, root.sweepAngle * (1 - root.clampedVal) - root.gapAngle * (root.sweepAngle < 360 ? 1 : 2))
}
}
}
ShapePath {
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
fillColor: "transparent"
strokeColor: root.fgColour
strokeWidth: root.strokeWidth
WavyLine {
id: wave
Behavior on strokeColor {
CAnim {
duration: Appearance.anim.durations.large
}
}
PathAngleArc {
centerX: root.size / 2
centerY: root.size / 2
radiusX: root.arcRadius
radiusY: root.arcRadius
amplitudeMultiplier: root.wavy ? 0.5 : 0
anchors.fill: parent
anchors.margins: -lineWidth * amplitudeMultiplier
color: root.fgColor
frequency: 8
fullAngle: root.sweepAngle
lineWidth: root.strokeWidth
pathType: WavyLine.Arc
radius: root.arcRadius
startAngle: root.startAngle
sweepAngle: 360 * root.vValue
value: root.clampedVal
Behavior on amplitudeMultiplier {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on color {
CAnim {
}
}
Anim on waveProgress {
id: waveProgAnim
duration: 2000
easing.type: Easing.Linear
from: 0
loops: Animation.Infinite
paused: root.wavePaused || wave.amplitudeMultiplier === 0
running: true
to: 1
}
}
Loader {
id: dot
x: root.size / 2 + root.arcRadius * Math.cos(root.dotAngleRad) - width / 2
y: root.size / 2 + root.arcRadius * Math.sin(root.dotAngleRad) - height / 2
sourceComponent: CustomRect {
color: root.fgColor
implicitHeight: Math.min(1, remainingArc.sweepAngle) * Math.min(4, root.strokeWidth)
implicitWidth: Math.min(1, remainingArc.sweepAngle) * Math.min(4, root.strokeWidth)
opacity: Math.min(1, remainingArc.sweepAngle)
radius: Appearance.rounding.full
}
}
}
+237
View File
@@ -0,0 +1,237 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Templates
import ZShell
import ZShell.Components
import ZShell.Internal
import qs.Config
ProgressBar {
id: root
enum IndeterminateAnimState {
Running,
Completing,
Stopped
}
property color bgColor: DynamicColors.palette.m3secondaryContainer
property color fgColor: DynamicColors.palette.m3primary
property int indeterminateAnimState: CustomProgressBar.Stopped
property real waveAmplitude: 0.5
property int waveDuration: 1000
property int waveFrequency: 6
property bool wavePaused
property bool wavy
function toBounds(startFrac: real, endFrac: real, gapSize: real): point {
startFrac = ZUtils.clamp(startFrac, 0, 1);
endFrac = ZUtils.clamp(endFrac, 0, 1);
// Ramp down gap size
const GAP_RAMP_DOWN_THRESHOLD = 0.01;
gapSize += height / 2;
const startGapSize = (gapSize * ZUtils.clamp(startFrac, 0, GAP_RAMP_DOWN_THRESHOLD) / GAP_RAMP_DOWN_THRESHOLD);
const endGapSize = (gapSize * (1 - ZUtils.clamp(endFrac, 1 - GAP_RAMP_DOWN_THRESHOLD, 1)) / GAP_RAMP_DOWN_THRESHOLD);
const start = width * startFrac + startGapSize;
const end = width * endFrac - endGapSize;
return start >= end ? Qt.point(0, 0) : Qt.point(start, end);
}
function updateIAnimState(): void {
if (indeterminate) {
manager.completeEndProgress = 0;
indeterminateAnimState = CustomProgressBar.Running;
} else if (indeterminateAnimState === CustomProgressBar.Running) {
indeterminateAnimState = CustomProgressBar.Completing;
}
}
implicitHeight: 4
implicitWidth: 200
contentItem: Loader {
anchors.fill: parent
asynchronous: true
sourceComponent: root.indeterminate || root.indeterminateAnimState !== CustomProgressBar.Stopped ? indeterminateComp : determinateComp
}
Component.onCompleted: updateIAnimState()
onIndeterminateChanged: updateIAnimState()
LinearIndicatorManager {
id: manager
gap: Appearance.spacing.extraSmall
Anim on completeEndProgress {
duration: manager.completeEndDuration
running: root.indeterminateAnimState === CustomProgressBar.Completing
to: 1
onFinished: {
if (root.indeterminateAnimState === CustomProgressBar.Completing)
root.indeterminateAnimState = CustomProgressBar.Stopped;
}
}
Anim on progress {
duration: manager.duration
easing.type: Easing.Linear
from: 0
loops: Animation.Infinite
running: root.indeterminateAnimState !== CustomProgressBar.Stopped
to: 1
}
}
Component {
id: determinateComp
Item {
Line {
id: remaining
anchors.right: parent.right
implicitWidth: parent.width - wave.implicitWidth - Appearance.spacing.extraSmall
}
Line {
property real implicitSize
anchors.right: parent.right
anchors.rightMargin: (parent.height - implicitHeight) / 2
anchors.verticalCenter: parent.verticalCenter
color: root.fgColor
implicitHeight: implicitSize
implicitWidth: implicitSize
radius: Appearance.rounding.full
Behavior on implicitSize {
Anim {
type: Anim.FastSpatial
}
}
Component.onCompleted: implicitSize = Qt.binding(() => parent.width - wave.implicitWidth < parent.height ? parent.height : 4)
}
Wave {
id: wave
anchors.left: parent.left
Behavior on implicitWidth {
Anim {
}
}
Component.onCompleted: implicitWidth = Qt.binding(() => parent.width * root.visualPosition)
}
}
}
Component {
id: indeterminateComp
Item {
id: content
Line {
bounds: {
const i = manager.activeIndicators[0]; // qmllint disable unresolved-type
return i ? root.toBounds(0, i.startFraction, i.gapSize / 2) : Qt.point(0, 0);
}
}
Line {
bounds: {
const i = manager.activeIndicators[manager.activeIndicators.length - 1]; // qmllint disable unresolved-type
return i ? root.toBounds(i.endFraction, 1, i.gapSize / 2) : Qt.point(0, 0);
}
}
Instantiator {
model: Math.max(manager.activeIndicators.length, 1) - 1 // qmllint disable unresolved-type
delegate: Line {
readonly property LinearIndicatorSegment cur: manager.activeIndicators[index] // qmllint disable unresolved-type
required property int index
readonly property LinearIndicatorSegment next: manager.activeIndicators[index + 1 % manager.activeIndicators.length] // qmllint disable unresolved-type
bounds: root.toBounds(cur.endFraction, next.startFraction, cur.gapSize / 2)
}
onObjectAdded: (_, obj) => content.data.push(obj)
onObjectRemoved: (_, obj) => {
const idx = content.data.indexOf(obj);
if (idx !== -1)
content.data.splice(idx, 1);
}
}
Instantiator {
model: manager.activeIndicators // qmllint disable unresolved-type
delegate: Wave {
readonly property point bounds: root.toBounds(modelData.startFraction, modelData.endFraction, modelData.gapSize / 2)
required property LinearIndicatorSegment modelData
color: root.fgColor
implicitWidth: bounds.y - bounds.x
x: bounds.x
}
onObjectAdded: (_, obj) => content.data.push(obj)
onObjectRemoved: (_, obj) => {
const idx = content.data.indexOf(obj);
if (idx !== -1)
content.data.splice(idx, 1);
}
}
}
}
component Line: CustomRect {
property point bounds
anchors.verticalCenter: parent.verticalCenter
color: root.bgColor
implicitHeight: parent.height
implicitWidth: bounds.y - bounds.x
radius: Appearance.rounding.full
x: bounds.x
}
component Wave: WavyLine {
id: wave
amplitudeMultiplier: root.wavy ? root.waveAmplitude : 0
anchors.verticalCenter: parent.verticalCenter
color: root.fgColor
frequency: root.waveFrequency
fullLength: parent.width
implicitHeight: lineWidth * amplitudeMultiplier * 2 + lineWidth
lineWidth: parent.height
startX: x
Behavior on amplitudeMultiplier {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on color {
CAnim {
}
}
Anim on waveProgress {
duration: root.waveDuration
easing.type: Easing.Linear
from: 0
loops: Animation.Infinite
paused: wave.amplitudeMultiplier === 0 || root.wavePaused
running: true
to: 1
}
}
}
+2
View File
@@ -77,9 +77,11 @@ JsonObject {
}
}
component Padding: JsonObject {
property int extraLarge: 28 * scale
property int extraLargeIncreased: 32 * scale
property int extraSmall: 4 * scale
property int large: 16 * scale
property int largeIncreased: 20 * scale
property int larger: 12 * scale
property int normal: 8 * scale
property real scale: 1
+1 -1
View File
@@ -271,7 +271,7 @@ CustomWindow {
implicitHeight: panels.resources.height
implicitWidth: panels.resources.width
panel: panels.resourcesWrapper
radius: Appearance.rounding.normal
radius: Appearance.rounding.large
x: panels.resourcesWrapper.x + panels.resources.x + Config.barConfig.border
y: panels.resourcesWrapper.y + panels.resources.y + bar.implicitHeight
}
-235
View File
@@ -9,33 +9,13 @@ Singleton {
id: root
property string autoGpuType: "NONE"
property string cpuName: ""
property real cpuPerc
property real cpuTemp
// Individual disks: Array of { mount, used, total, free, perc }
property var disks: []
property real gpuMemTotal: 0
property real gpuMemUsed
property string gpuName
property real gpuPerc
property real gpuTemp
readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType
property real lastCpuIdle
property real lastCpuTotal
readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0
property real memTotal
property real memUsed
property int refCount
readonly property real storagePerc: {
let totalUsed = 0;
let totalSize = 0;
for (const disk of disks) {
totalUsed += disk.used;
totalSize += disk.total;
}
return totalSize > 0 ? totalUsed / totalSize : 0;
}
function cleanCpuName(name: string): string {
return name.replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/CPU/gi, "").replace(/\d+th Gen /gi, "").replace(/\d+nd Gen /gi, "").replace(/\d+rd Gen /gi, "").replace(/\d+st Gen /gi, "").replace(/Core /gi, "").replace(/Processor/gi, "").replace(/\s+/g, " ").trim();
@@ -78,8 +58,6 @@ Singleton {
triggeredOnStart: true
onTriggered: {
stat.reload();
meminfo.reload();
if (root.gpuType === "GENERIC")
gpuUsage.running = true;
@@ -88,170 +66,6 @@ Singleton {
}
}
Timer {
interval: 60000 * 120
repeat: true
running: true
triggeredOnStart: true
onTriggered: {
storage.running = true;
}
}
Timer {
interval: Config.dashboard.resourceUpdateInterval * 5
repeat: true
running: root.refCount > 0
triggeredOnStart: true
onTriggered: {
sensors.running = true;
}
}
FileView {
id: cpuinfoInit
path: "/proc/cpuinfo"
onLoaded: {
const nameMatch = text().match(/model name\s*:\s*(.+)/);
if (nameMatch)
root.cpuName = root.cleanCpuName(nameMatch[1]);
}
}
FileView {
id: stat
path: "/proc/stat"
onLoaded: {
const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/);
if (data) {
const stats = data.slice(1).map(n => parseInt(n, 10));
const total = stats.reduce((a, b) => a + b, 0);
const idle = stats[3] + (stats[4] ?? 0);
const totalDiff = total - root.lastCpuTotal;
const idleDiff = idle - root.lastCpuIdle;
const newCpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0;
root.lastCpuTotal = total;
root.lastCpuIdle = idle;
if (Math.abs(newCpuPerc - root.cpuPerc) >= 0.01)
root.cpuPerc = newCpuPerc;
}
}
}
FileView {
id: meminfo
path: "/proc/meminfo"
onLoaded: {
const data = text();
const total = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1;
const used = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0;
if (root.memTotal !== total)
root.memTotal = total;
if (Math.abs(used - root.memUsed) >= 16384)
root.memUsed = used;
}
}
Process {
id: storage
command: ["lsblk", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE", "-P"]
stdout: StdioCollector {
onStreamFinished: {
const diskMap = {}; // Map disk name -> { name, totalSize, used, fsTotal }
const lines = text.trim().split("\n");
for (const line of lines) {
if (line.trim() === "")
continue;
const nameMatch = line.match(/NAME="([^"]+)"/);
const sizeMatch = line.match(/SIZE="([^"]+)"/);
const typeMatch = line.match(/TYPE="([^"]+)"/);
const fsusedMatch = line.match(/FSUSED="([^"]*)"/);
const fssizeMatch = line.match(/FSSIZE="([^"]*)"/);
if (!nameMatch || !typeMatch)
continue;
const name = nameMatch[1];
const type = typeMatch[1];
const size = parseInt(sizeMatch?.[1] || "0", 10);
const fsused = parseInt(fsusedMatch?.[1] || "0", 10);
const fssize = parseInt(fssizeMatch?.[1] || "0", 10);
if (type === "disk") {
// Skip zram (swap) devices
if (name.startsWith("zram"))
continue;
// Initialize disk entry
if (!diskMap[name]) {
diskMap[name] = {
name: name,
totalSize: size,
used: 0,
fsTotal: 0
};
}
} else if (type === "part") {
// Find parent disk (remove trailing numbers/p+numbers)
let parentDisk = name.replace(/p?\d+$/, "");
// For nvme devices like nvme0n1p1, parent is nvme0n1
if (name.match(/nvme\d+n\d+p\d+/))
parentDisk = name.replace(/p\d+$/, "");
// Aggregate partition usage to parent disk
if (diskMap[parentDisk]) {
diskMap[parentDisk].used += fsused;
diskMap[parentDisk].fsTotal += fssize;
}
}
}
const diskList = [];
let totalUsed = 0;
let totalSize = 0;
for (const diskName of Object.keys(diskMap).sort()) {
const disk = diskMap[diskName];
// Use filesystem total if available, otherwise use disk size
const total = disk.fsTotal > 0 ? disk.fsTotal : disk.totalSize;
const used = disk.used;
const perc = total > 0 ? used / total : 0;
// Convert bytes to KiB for consistency with formatKib
diskList.push({
mount: disk.name // Using 'mount' property for compatibility
,
used: used / 1024,
total: total / 1024,
free: (total - used) / 1024,
perc: perc
});
totalUsed += used;
totalSize += total;
}
root.disks = diskList;
}
}
}
Process {
id: gpuNameDetect
@@ -412,53 +226,4 @@ Singleton {
}
}
}
Process {
id: sensors
command: ["sensors"]
environment: ({
LANG: "C.UTF-8",
LC_ALL: "C.UTF-8"
})
stdout: StdioCollector {
onStreamFinished: {
let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/);
if (!cpuTemp)
// If AMD Tdie pattern failed, try fallback on Tctl
cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/);
if (cpuTemp && Math.abs(parseFloat(cpuTemp[1]) - root.cpuTemp) >= 0.5)
root.cpuTemp = parseFloat(cpuTemp[1]);
if (root.gpuType !== "GENERIC")
return;
let eligible = false;
let sum = 0;
let count = 0;
for (const line of text.trim().split("\n")) {
if (line === "Adapter: PCI adapter")
eligible = true;
else if (line === "")
eligible = false;
else if (eligible) {
let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
if (!match)
// Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs)
match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
if (match) {
sum += parseFloat(match[2]);
count++;
}
}
}
root.gpuTemp = count > 0 ? sum / count : 0;
}
}
}
}
+40 -43
View File
@@ -1,92 +1,89 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Services
import qs.Components
import qs.Helpers
import qs.Config
Row {
Item {
id: root
anchors.bottom: parent.bottom
anchors.top: parent.top
padding: Appearance.padding.large
spacing: Appearance.spacing.large
implicitWidth: layout.implicitWidth + layout.anchors.margins * 4
ServiceRef {
service: Storage
}
ServiceRef {
service: Memory
}
ServiceRef {
service: Cpu
}
Ref {
service: SystemUsage
}
ColumnLayout {
id: layout
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.margins: Appearance.padding.large
anchors.top: parent.top
spacing: Appearance.spacing.normal
Resource {
color: DynamicColors.palette.m3primary
fgColor: DynamicColors.palette.m3primary
icon: "memory"
value: SystemUsage.cpuPerc
value: Cpu.percentage
}
Resource {
color: DynamicColors.palette.m3secondary
fgColor: DynamicColors.palette.m3secondary
icon: "memory_alt"
value: SystemUsage.memPerc
value: Memory.percentage
}
Resource {
color: DynamicColors.palette.m3tertiary
fgColor: DynamicColors.palette.m3tertiary
icon: "gamepad"
value: SystemUsage.gpuPerc
}
Resource {
color: DynamicColors.palette.m3primary
fgColor: DynamicColors.palette.m3primary
icon: "host"
value: SystemUsage.gpuMemUsed
}
Resource {
color: DynamicColors.palette.m3secondary
fgColor: DynamicColors.palette.m3secondary
icon: "hard_disk"
value: SystemUsage.storagePerc
value: Storage.percentage
}
}
component Resource: Item {
component Resource: CircularProgress {
id: res
required property color color
required property string icon
required property real value
anchors.bottom: parent.bottom
anchors.margins: Appearance.padding.large
anchors.top: parent.top
implicitWidth: icon.implicitWidth
Layout.fillHeight: true
implicitSize: height
Behavior on value {
Behavior on clampedVal {
Anim {
duration: Appearance.anim.durations.large
}
}
CustomRect {
anchors.bottom: icon.top
anchors.bottomMargin: Appearance.spacing.small
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
implicitWidth: Config.dashboard.sizes.resourceProgessThickness
radius: Appearance.rounding.full
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
color: res.color
implicitHeight: res.value * parent.height
radius: Appearance.rounding.full
}
}
MaterialIcon {
id: icon
anchors.bottom: parent.bottom
color: res.color
anchors.centerIn: parent
color: res.fgColor
text: res.icon
}
}
+6 -6
View File
@@ -23,7 +23,7 @@ GridLayout {
Resource {
Layout.bottomMargin: Appearance.padding.large
Layout.topMargin: Appearance.padding.large
colour: DynamicColors.palette.m3primary
color: DynamicColors.palette.m3primary
icon: "memory"
value: SystemUsage.cpuPerc
}
@@ -31,7 +31,7 @@ GridLayout {
Resource {
Layout.bottomMargin: Appearance.padding.large
Layout.topMargin: Appearance.padding.large
colour: DynamicColors.palette.m3secondary
color: DynamicColors.palette.m3secondary
icon: "memory_alt"
value: SystemUsage.memPerc
}
@@ -39,7 +39,7 @@ GridLayout {
component Resource: CustomRect {
id: res
required property color colour
required property color color
required property string icon
required property real value
@@ -58,8 +58,8 @@ GridLayout {
id: circ
anchors.fill: parent
bgColour: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3)
fgColour: res.colour
bgColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3)
fgColor: res.color
padding: Appearance.padding.large * 3
strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal
value: res.value
@@ -69,7 +69,7 @@ GridLayout {
id: icon
anchors.centerIn: parent
color: res.colour
color: res.color
font.pointSize: (circ.arcRadius * 0.7) || 1
font.weight: 600
text: res.icon
+17 -7
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import QtQuick.Layouts
import ZShell.Services
import qs.Helpers
import qs.Modules
import qs.Config
@@ -16,7 +17,7 @@ CustomRect {
clip: true
color: visibilities.resources ? DynamicColors.palette.m3primary : DynamicColors.tPalette.m3surfaceContainer
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
implicitWidth: rowLayout.implicitWidth + Appearance.padding.normal * 2
implicitWidth: rowLayout.implicitWidth + Appearance.padding.larger * 2
radius: Appearance.rounding.full
StateLayer {
@@ -29,6 +30,7 @@ CustomRect {
id: rowLayout
anchors.centerIn: parent
anchors.horizontalCenterOffset: -2
implicitHeight: root.implicitHeight
spacing: Appearance.spacing.smaller
@@ -36,13 +38,21 @@ CustomRect {
service: SystemUsage
}
ServiceRef {
service: Cpu
}
ServiceRef {
service: Memory
}
Resource {
Layout.alignment: Qt.AlignVCenter
Layout.fillHeight: true
icon: "memory"
iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
mainColor: root.visibilities.resources ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3primary
percentage: SystemUsage.cpuPerc
mainColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3primary
percentage: Cpu.percentage
warningThreshold: 95
}
@@ -50,8 +60,8 @@ CustomRect {
Layout.fillHeight: true
icon: "memory_alt"
iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
mainColor: root.visibilities.resources ? DynamicColors.palette.m3secondaryContainer : DynamicColors.palette.m3secondary
percentage: SystemUsage.memPerc
mainColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3secondary
percentage: Memory.percentage
warningThreshold: 80
}
@@ -59,7 +69,7 @@ CustomRect {
Layout.fillHeight: true
icon: "gamepad"
iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
mainColor: root.visibilities.resources ? DynamicColors.palette.m3tertiaryContainer : DynamicColors.palette.m3tertiary
mainColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3tertiary
percentage: SystemUsage.gpuPerc
}
@@ -67,7 +77,7 @@ CustomRect {
Layout.fillHeight: true
icon: "developer_board"
iconColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
mainColor: root.visibilities.resources ? DynamicColors.palette.m3primaryContainer : DynamicColors.palette.m3primary
mainColor: root.visibilities.resources ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3primary
percentage: SystemUsage.gpuMemUsed
}
}
+134
View File
@@ -0,0 +1,134 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.UPower
import qs.Components
import qs.Config
CustomClippingRect {
id: root
property real animPerc: UPower.displayDevice.percentage
color: DynamicColors.palette.m3secondaryContainer
implicitWidth: 120
radius: Appearance.rounding.large
Behavior on animPerc {
Anim {
}
}
Contents {
id: layout
accentColor: DynamicColors.palette.m3primary
anchors.fill: parent
anchors.margins: Appearance.padding.larger
subTextColor: DynamicColors.palette.m3onSurfaceVariant
textColor: DynamicColors.palette.m3onSurface
}
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
clip: true
color: DynamicColors.palette.m3secondary
implicitHeight: parent.height * root.animPerc
radius: Appearance.rounding.extraSmall
Contents {
accentColor: DynamicColors.palette.m3primaryContainer
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: layout.anchors.margins
anchors.right: parent.right
height: layout.height
subTextColor: DynamicColors.palette.m3secondaryContainer
textColor: DynamicColors.palette.m3onSecondary
}
}
component Contents: ColumnLayout {
id: contents
required property color accentColor
readonly property bool charging: [UPowerDeviceState.Charging, UPowerDeviceState.FullyCharged, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state)
required property color subTextColor
required property color textColor
spacing: 0
MaterialIcon {
Layout.leftMargin: -Appearance.padding.extraSmall
color: contents.accentColor
text: "battery_full"
}
CustomText {
Layout.fillWidth: true
color: contents.textColor
text: qsTr("Battery")
}
Item {
Layout.fillHeight: true
}
CustomText {
Layout.alignment: Qt.AlignRight
animate: true
color: contents.subTextColor
text: {
if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged)
return qsTr("Full");
if (contents.charging)
return qsTr("Charging");
const s = UPower.displayDevice.timeToEmpty;
if (s === 0)
return qsTr("...");
const hr = Math.floor(s / 3600);
const min = Math.floor((s % 3600) / 60);
if (hr > 0)
return `${hr}h ${min}m`;
return `${min}m`;
}
}
RowLayout {
Layout.alignment: Qt.AlignRight
Layout.bottomMargin: -Appearance.padding.small
Layout.rightMargin: -Appearance.padding.extraSmall
Layout.topMargin: -Appearance.padding.extraSmall
spacing: Appearance.spacing.extraSmall
MaterialIcon {
color: contents.accentColor
fill: 1
opacity: contents.charging ? 1 : 0
scale: contents.charging ? 1 : 0
text: "bolt"
Behavior on opacity {
Anim {
type: Anim.FastEffects
}
}
Behavior on scale {
Anim {
type: Anim.FastSpatial
}
}
}
CustomText {
color: contents.accentColor
text: `${Math.round(UPower.displayDevice.percentage * 100)}%`
}
}
}
}
+138
View File
@@ -0,0 +1,138 @@
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Config
CustomClippingRect {
id: root
required property color accent
required property string icon
required property string label
required property string subLabel
required property real temperature
required property real usage
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: Math.max(tempProg.implicitHeight + detailsRow.implicitHeight + Appearance.spacing.large, usageColumn.implicitHeight + usageLabel.implicitHeight) + Appearance.padding.large * 2
implicitWidth: 450
radius: Appearance.rounding.large - Appearance.padding.normal
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
color: Qt.alpha(root.accent, 0.05)
implicitWidth: parent.width * root.usage
Behavior on implicitWidth {
Anim {
}
}
}
CircularProgress {
id: tempProg
anchors.left: parent.left
anchors.margins: Appearance.padding.large
anchors.top: parent.top
fgColor: root.accent
implicitSize: Math.max(icon.implicitWidth, icon.implicitHeight) + Appearance.padding.larger * 2
spacing: Appearance.spacing.extraSmall
strokeWidth: Appearance.padding.extraSmall
value: root.usage
Behavior on clampedVal {
Anim {
}
}
MaterialIcon {
id: icon
anchors.centerIn: parent
color: root.accent
text: root.icon
}
}
ColumnLayout {
anchors.left: tempProg.right
anchors.margins: Appearance.spacing.large
anchors.right: usageColumn.left
anchors.verticalCenter: tempProg.verticalCenter
spacing: Appearance.spacing.extraSmall
CustomText {
color: root.accent
text: root.label
}
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3onSurfaceVariant
elide: Text.ElideRight
text: root.subLabel
}
}
ColumnLayout {
id: detailsRow
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: Appearance.padding.largeIncreased
spacing: Appearance.spacing.extraSmall
RowLayout {
Layout.leftMargin: -Appearance.padding.extraSmall
spacing: Appearance.spacing.extraSmall
MaterialIcon {
Layout.topMargin: Math.round(fontInfo.pointSize * 0.08)
color: root.temperature > 90 ? DynamicColors.palette.m3error : root.accent
fill: 1
text: root.temperature > 90 ? "thermometer_alert" : "thermometer"
}
CustomText {
text: `${Math.ceil(root.temperature)}°${"C"}`
}
}
CustomProgressBar {
fgColor: root.accent
implicitHeight: Appearance.padding.small
indeterminate: isNaN(root.usage) || isNaN(root.temperature)
value: root.temperature / 100
}
}
Column {
id: usageColumn
anchors.margins: Appearance.padding.large
anchors.right: parent.right
anchors.rightMargin: 32
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
id: usageLabel
anchors.right: parent.right
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.normal
text: qsTr("Usage")
}
CustomText {
anchors.right: parent.right
color: root.accent
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
text: isNaN(root.usage) ? "...%" : Math.round(root.usage * 100) + "%"
}
}
}
+85
View File
@@ -0,0 +1,85 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Services
import qs.Components
import qs.Config
CustomRect {
id: root
readonly property color accent: DynamicColors.palette.m3tertiary
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + Appearance.padding.large * 2
implicitWidth: layout.implicitWidth + Appearance.padding.extraLargeIncreased * 2
radius: Appearance.rounding.medium
ServiceRef {
service: Memory
}
ColumnLayout {
id: layout
anchors.centerIn: parent
spacing: Appearance.spacing.extraSmall
RowLayout {
Layout.leftMargin: -Appearance.padding.extraSmall
spacing: Appearance.spacing.small
MaterialIcon {
color: root.accent
fill: 1
text: "memory_alt"
}
CustomText {
text: qsTr("Memory")
}
}
CircularProgress {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Appearance.spacing.large
fgColor: root.accent
implicitSize: usageColumn.implicitHeight + thickness + Appearance.padding.largeIncreased * 2
startAngle: -225
sweepAngle: 270
value: Memory.percentage
Behavior on clampedVal {
Anim {
}
}
ColumnLayout {
id: usageColumn
anchors.centerIn: parent
anchors.verticalCenterOffset: Appearance.padding.extraSmall
spacing: 0
CustomText {
Layout.alignment: Qt.AlignHCenter
color: root.accent
text: Math.round(Memory.percentage * 100) + "%"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
text: qsTr("Used")
}
}
}
CustomText {
Layout.alignment: Qt.AlignHCenter
text: {
const fmt = UsageFmt.formatKib(Memory.used, Memory.total);
return `${fmt.value.toFixed(1)} / ${Math.floor(fmt.total)} ${fmt.unit}`;
}
}
}
}
+183
View File
@@ -0,0 +1,183 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Internal
import qs.Helpers
import qs.Components
import qs.Config
CustomRect {
id: root
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: 220
implicitWidth: 300
radius: Appearance.rounding.large - Appearance.padding.normal
Ref {
service: NetworkUsage
}
ColumnLayout {
id: layout
anchors.bottomMargin: Appearance.padding.larger
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: 0
RowLayout {
spacing: Appearance.spacing.small
MaterialIcon {
color: DynamicColors.palette.m3primary
text: "swap_vert"
}
CustomText {
text: qsTr("Network")
}
}
Item {
Layout.bottomMargin: Appearance.spacing.small
Layout.fillHeight: true
Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.larger
SparklineItem {
id: sparkline
property real smoothMax: targetMax
property real targetMax: 1024
anchors.fill: parent
historyLength: NetworkUsage.historyLength
line1: NetworkUsage.uploadBuffer // qmllint disable missing-type
line1Color: DynamicColors.palette.m3secondary
line1FillAlpha: 0.15
line2: NetworkUsage.downloadBuffer // qmllint disable missing-type
line2Color: DynamicColors.palette.m3tertiary
line2FillAlpha: 0.2
maxValue: smoothMax
Behavior on smoothMax {
Anim {
}
}
Connections {
function onValuesChanged(): void {
sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024);
slideAnim.restart();
}
target: NetworkUsage.downloadBuffer
}
NumberAnimation {
id: slideAnim
duration: Config.dashboard.resourceUpdateInterval
easing.type: Easing.Linear
from: 0
property: "slideProgress"
target: sparkline
to: 1
}
}
// "Collecting data" placeholder
CustomText {
anchors.centerIn: parent
color: DynamicColors.palette.m3outline
text: qsTr("Collecting data...")
visible: NetworkUsage.downloadBuffer.count < 2
}
}
// Download row
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
MaterialIcon {
color: DynamicColors.palette.m3tertiary
text: "download"
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
text: qsTr("Download")
}
Item {
Layout.fillWidth: true
}
CustomText {
color: DynamicColors.palette.m3tertiary
text: {
const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0);
return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s";
}
}
}
// Upload row
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
MaterialIcon {
color: DynamicColors.palette.m3secondary
text: "upload"
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
text: qsTr("Upload")
}
Item {
Layout.fillWidth: true
}
CustomText {
color: DynamicColors.palette.m3secondary
text: {
const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0);
return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s";
}
}
}
// Session totals
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant
text: "history"
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
text: qsTr("Total")
}
Item {
Layout.fillWidth: true
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
text: {
const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0);
const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0);
return (down && up) ? `${down.value.toFixed(1)}${down.unit} ${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B";
}
}
}
}
}
+127
View File
@@ -0,0 +1,127 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Services
import qs.Components
import qs.Config
CustomRect {
id: root
readonly property color accent: DynamicColors.palette.m3secondary
readonly property real percentage: Storage.primaryDisk?.perc ?? 0
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + Appearance.padding.large * 2
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
radius: Appearance.rounding.large - Appearance.padding.normal
ServiceRef {
service: Storage
}
ColumnLayout {
id: layout
anchors.left: parent.left
anchors.margins: Appearance.padding.small
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 0
RowLayout {
id: row
Layout.alignment: Qt.AlignHCenter
spacing: Appearance.spacing.large
CircularProgress {
fgColor: root.accent
implicitSize: usageColumn.implicitHeight + thickness + Appearance.padding.large * 2
startAngle: -225
sweepAngle: 270
value: root.percentage
Behavior on clampedVal {
Anim {
}
}
ColumnLayout {
id: usageColumn
anchors.centerIn: parent
spacing: 0
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
color: root.accent
font.pointSize: Appearance.font.size.medium
text: "hard_drive"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: root.accent
text: Math.round(root.percentage * 100) + "%"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
text: qsTr("Used")
}
}
}
ColumnLayout {
spacing: Appearance.spacing.extraSmall
CustomText {
text: qsTr("Storage")
}
CustomText {
color: root.accent
text: {
if (!Storage.primaryDisk)
return qsTr("No disks detected");
const fmt = UsageFmt.formatKib(Storage.primaryDisk.used, Storage.primaryDisk.total);
return `${fmt.value.toFixed(1)} / ${Math.floor(fmt.total)} ${fmt.unit}`;
}
}
}
}
CustomSplitButton {
Layout.alignment: Qt.AlignHCenter
active: menuItems.find(m => m.modelData === Storage.primaryDisk) ?? menuItems[0] ?? null
enabled: Storage.disks.length
fallbackIcon: "storage"
fallbackText: qsTr("No disks")
menuItems: disks.instances
minLeftWidth: row.implicitWidth * 0.6
type: CustomSplitButton.Tonal
menu.onItemSelected: item => Storage.manualPrimaryDisk = (item as DiskItem).modelData
Variants {
id: disks
model: Storage.disks
DiskItem {
}
}
}
}
component DiskItem: MenuItem {
required property var modelData
activeIcon: "storage"
icon: modelData === Storage.primaryDisk ? "check" : ""
text: modelData.mount
}
}
+80 -752
View File
@@ -1,792 +1,120 @@
import Quickshell
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell.Services.UPower
import ZShell.Internal
import qs.Components
import ZShell.Services
import qs.Modules.Resources.Cards
import qs.Helpers
import qs.Components
import qs.Config
Item {
id: root
readonly property int minWidth: 400 + 400 + Appearance.spacing.normal + 120 + Appearance.padding.large * 2
readonly property real nonAnimHeight: content.implicitHeight + Appearance.padding.normal * 2
readonly property real nonAnimWidth: Math.max(minWidth, content.implicitWidth) + Appearance.padding.normal * 2
required property real padding
required property PersistentProperties visibilities
function displayTemp(temp: real): string {
return `${Math.ceil(temp)}°C`;
}
implicitHeight: nonAnimHeight
implicitWidth: nonAnimWidth
implicitHeight: content.implicitHeight + Appearance.padding.normal * 2
implicitWidth: content.implicitWidth
RowLayout {
id: content
anchors.left: parent.left
anchors.leftMargin: root.padding
anchors.margins: Appearance.padding.normal
anchors.right: parent.right
anchors.rightMargin: root.padding
anchors.verticalCenter: parent.verticalCenter
spacing: Appearance.spacing.normal
Ref {
service: SystemUsage
}
spacing: Appearance.spacing.larger
ColumnLayout {
id: mainColumn
Layout.fillWidth: true
spacing: Appearance.spacing.normal
spacing: Appearance.spacing.larger
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE")
spacing: Appearance.spacing.larger
visible: cpuCard.active || gpuCard.active
HeroCard {
Layout.fillWidth: true
Layout.minimumWidth: 400
Layout.preferredHeight: 150
accentColor: DynamicColors.palette.m3primary
WrappedLoader {
id: cpuCard
active: Config.dashboard.performance.showCpu
sourceComponent: HeroCard {
accent: DynamicColors.palette.m3primary
icon: "memory"
mainLabel: qsTr("Usage")
mainValue: `${Math.round(SystemUsage.cpuPerc * 100)}%`
secondaryLabel: qsTr("Temp")
secondaryValue: root.displayTemp(SystemUsage.cpuTemp)
temperature: SystemUsage.cpuTemp
title: SystemUsage.cpuName ? `CPU - ${SystemUsage.cpuName}` : qsTr("CPU")
usage: SystemUsage.cpuPerc
visible: Config.dashboard.performance.showCpu
label: qsTr("CPU")
subLabel: Cpu.name
temperature: Cpu.temperature
usage: Cpu.percentage
ServiceRef {
service: Cpu
}
}
}
HeroCard {
Layout.fillWidth: true
Layout.minimumWidth: 400
Layout.preferredHeight: 150
accentColor: DynamicColors.palette.m3secondary
WrappedLoader {
id: gpuCard
active: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== ""
sourceComponent: HeroCard {
accent: DynamicColors.palette.m3secondary
icon: "desktop_windows"
mainLabel: qsTr("Usage")
mainValue: `${Math.round(SystemUsage.gpuPerc * 100)}%`
secondaryLabel: qsTr("Temp")
secondaryValue: root.displayTemp(SystemUsage.gpuTemp)
label: qsTr("GPU")
subLabel: SystemUsage.gpuName
temperature: SystemUsage.gpuTemp
title: SystemUsage.gpuName ? `GPU - ${SystemUsage.gpuName}` : qsTr("GPU")
usage: SystemUsage.gpuPerc
visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE"
}
}
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork
GaugeCard {
Layout.fillWidth: !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork
Layout.minimumWidth: 250
Layout.preferredHeight: 220
accentColor: DynamicColors.palette.m3tertiary
icon: "memory_alt"
percentage: SystemUsage.memPerc
subtitle: {
const usedFmt = SystemUsage.formatKib(SystemUsage.memUsed);
const totalFmt = SystemUsage.formatKib(SystemUsage.memTotal);
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
}
title: qsTr("Memory")
visible: Config.dashboard.performance.showMemory
}
StorageGaugeCard {
Layout.fillWidth: !Config.dashboard.performance.showNetwork
Layout.minimumWidth: 250
Layout.preferredHeight: 220
visible: Config.dashboard.performance.showStorage
}
NetworkCard {
Layout.fillWidth: true
Layout.minimumWidth: 200
Layout.preferredHeight: 220
visible: Config.dashboard.performance.showNetwork
}
}
}
BatteryTank {
Layout.preferredHeight: mainColumn.implicitHeight
Layout.preferredWidth: 120
visible: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery
}
}
component BatteryTank: CustomClippingRect {
id: batteryTank
property color accentColor: DynamicColors.palette.m3primary
property real animatedPercentage: 0
property bool isCharging: UPower.displayDevice.state === UPowerDeviceState.Charging
property real percentage: UPower.displayDevice.percentage
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - Appearance.padding.normal
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: animatedPercentage = percentage
onPercentageChanged: animatedPercentage = percentage
// Background Fill
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
color: Qt.alpha(batteryTank.accentColor, 0.15)
height: parent.height * batteryTank.animatedPercentage
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small
// Header Section
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
MaterialIcon {
color: batteryTank.accentColor
font.pointSize: Appearance.font.size.large
text: {
if (!UPower.displayDevice.isLaptopBattery) {
if (PowerProfiles.profile === PowerProfile.PowerSaver)
return "energy_savings_leaf";
if (PowerProfiles.profile === PowerProfile.Performance)
return "rocket_launch";
return "balance";
}
if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged)
return "battery_full";
const perc = UPower.displayDevice.percentage;
const charging = [UPowerDeviceState.Charging, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state);
if (perc >= 0.99)
return "battery_full";
let level = Math.floor(perc * 7);
if (charging && (level === 4 || level === 1))
level--;
return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`;
}
}
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3onSurface
font.pointSize: Appearance.font.size.normal
text: qsTr("Battery")
}
}
Item {
Layout.fillHeight: true
}
// Bottom Info Section
ColumnLayout {
Layout.fillWidth: true
spacing: -4
CustomText {
Layout.alignment: Qt.AlignRight
color: batteryTank.accentColor
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
text: `${Math.round(batteryTank.percentage * 100)}%`
}
CustomText {
Layout.alignment: Qt.AlignRight
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.smaller
text: {
if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged)
return qsTr("Full");
if (batteryTank.isCharging)
return qsTr("Charging");
const s = UPower.displayDevice.timeToEmpty;
if (s === 0)
return qsTr("...");
const hr = Math.floor(s / 3600);
const min = Math.floor((s % 3600) / 60);
if (hr > 0)
return `${hr}h ${min}m`;
return `${min}m`;
}
}
}
}
}
component CardHeader: RowLayout {
property color accentColor: DynamicColors.palette.m3primary
property string icon
property string title
Layout.fillWidth: true
spacing: Appearance.spacing.small
MaterialIcon {
color: parent.accentColor
fill: 1
font.pointSize: Appearance.spacing.large
text: parent.icon
}
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pointSize: Appearance.font.size.normal
text: parent.title
}
}
component GaugeCard: CustomRect {
id: gaugeCard
property color accentColor: DynamicColors.palette.m3primary
property real animatedPercentage: 0
readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI
property string icon
property real percentage: 0
property string subtitle
property string title
clip: true
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - Appearance.padding.normal
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: animatedPercentage = percentage
onPercentageChanged: animatedPercentage = percentage
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.smaller
CardHeader {
accentColor: gaugeCard.accentColor
icon: gaugeCard.icon
title: gaugeCard.title
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
ArcGauge {
accentColor: gaugeCard.accentColor
anchors.centerIn: parent
height: width
percentage: gaugeCard.animatedPercentage
startAngle: gaugeCard.arcStartAngle
sweepAngle: gaugeCard.arcSweep
trackColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
width: Math.min(parent.width, parent.height)
}
CustomText {
anchors.centerIn: parent
color: gaugeCard.accentColor
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
text: `${Math.round(gaugeCard.percentage * 100)}%`
}
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.smaller
text: gaugeCard.subtitle
}
}
}
component HeroCard: CustomClippingRect {
id: heroCard
property color accentColor: DynamicColors.palette.m3primary
property real animatedTemp: 0
property real animatedUsage: 0
property string icon
property string mainLabel
property string mainValue
readonly property real maxTemp: 100
property string secondaryLabel
property string secondaryValue
readonly property real tempProgress: Math.min(1, Math.max(0, temperature / maxTemp))
property real temperature: 0
property string title
property real usage: 0
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - Appearance.padding.normal
Behavior on animatedTemp {
Anim {
duration: Appearance.anim.durations.large
}
}
Behavior on animatedUsage {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: {
animatedUsage = usage;
animatedTemp = tempProgress;
}
onTempProgressChanged: animatedTemp = tempProgress
onUsageChanged: animatedUsage = usage
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
color: Qt.alpha(heroCard.accentColor, 0.15)
implicitWidth: parent.width * heroCard.animatedUsage
}
CardHeader {
accentColor: heroCard.accentColor
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.large
anchors.top: parent.top
anchors.topMargin: Math.round(Appearance.padding.large * 1.2)
icon: heroCard.icon
title: heroCard.title
width: parent.width - anchors.leftMargin - usageColumn.anchors.rightMargin - usageLabel.width - Appearance.spacing.normal
}
Column {
anchors.bottom: parent.bottom
anchors.bottomMargin: Math.round(Appearance.padding.large * 1.3)
anchors.left: parent.left
anchors.margins: Math.round(Appearance.padding.large * 1.2)
anchors.right: parent.right
spacing: Appearance.spacing.small
Row {
spacing: Appearance.spacing.small
CustomText {
font.pointSize: Appearance.font.size.normal
font.weight: Font.Medium
text: heroCard.secondaryValue
}
CustomText {
anchors.baseline: parent.children[0].baseline
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: heroCard.secondaryLabel
}
}
ProgressBar {
bgColor: Qt.alpha(heroCard.accentColor, 0.2)
fgColor: heroCard.accentColor
implicitHeight: 6
implicitWidth: parent.width * 0.5
value: heroCard.tempProgress
}
}
Column {
id: usageColumn
anchors.margins: Appearance.padding.large
anchors.right: parent.right
anchors.rightMargin: 32
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
id: usageLabel
anchors.right: parent.right
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.normal
text: heroCard.mainLabel
}
CustomText {
anchors.right: parent.right
color: heroCard.accentColor
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
text: heroCard.mainValue
}
}
}
component NetworkCard: CustomRect {
id: networkCard
property color accentColor: DynamicColors.palette.m3primary
clip: true
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - Appearance.padding.normal
Ref {
service: NetworkUsage
service: SystemUsage
}
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.small
RowLayout {
spacing: Appearance.spacing.larger
visible: storageCard.active || networkCard.active || memoryCard.active
CardHeader {
accentColor: networkCard.accentColor
icon: "swap_vert"
title: qsTr("Network")
WrappedLoader {
id: storageCard
active: Config.dashboard.performance.showStorage
sourceComponent: StorageCard {
}
}
// Sparkline graph
Item {
WrappedLoader {
id: memoryCard
active: Config.dashboard.performance.showMemory
sourceComponent: MemoryCard {
}
}
WrappedLoader {
id: networkCard
active: Config.dashboard.performance.showNetwork
sourceComponent: NetworkCard {
}
}
}
}
WrappedLoader {
Layout.fillWidth: false
active: Battery.isLaptop && Config.dashboard.performance.showBattery
sourceComponent: BatteryTank {
}
}
}
component WrappedLoader: Loader {
Layout.fillHeight: true
Layout.fillWidth: true
SparklineItem {
id: sparkline
property real smoothMax: targetMax
property real targetMax: 1024
anchors.fill: parent
historyLength: NetworkUsage.historyLength
line1: NetworkUsage.uploadBuffer // qmllint disable missing-type
line1Color: DynamicColors.palette.m3secondary
line1FillAlpha: 0.15
line2: NetworkUsage.downloadBuffer // qmllint disable missing-type
line2Color: DynamicColors.palette.m3tertiary
line2FillAlpha: 0.2
maxValue: smoothMax
Behavior on smoothMax {
Anim {
duration: Appearance.anim.durations.large
}
}
Connections {
function onValuesChanged(): void {
sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024);
slideAnim.restart();
}
target: NetworkUsage.downloadBuffer
}
NumberAnimation {
id: slideAnim
duration: Config.dashboard.resourceUpdateInterval
from: 0
property: "slideProgress"
target: sparkline
to: 1
}
}
// "No data" placeholder
CustomText {
anchors.centerIn: parent
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
opacity: 0.6
text: qsTr("Collecting data...")
visible: NetworkUsage.downloadBuffer.count < 2
}
}
// Download row
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
MaterialIcon {
color: DynamicColors.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
text: "download"
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: qsTr("Download")
}
Item {
Layout.fillWidth: true
}
CustomText {
color: DynamicColors.palette.m3tertiary
font.pointSize: Appearance.font.size.normal
font.weight: Font.Medium
text: {
const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0);
return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s";
}
}
}
// Upload row
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
MaterialIcon {
color: DynamicColors.palette.m3secondary
font.pointSize: Appearance.font.size.normal
text: "upload"
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: qsTr("Upload")
}
Item {
Layout.fillWidth: true
}
CustomText {
color: DynamicColors.palette.m3secondary
font.pointSize: Appearance.font.size.normal
font.weight: Font.Medium
text: {
const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0);
return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s";
}
}
}
// Session totals
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
MaterialIcon {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.normal
text: "history"
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: qsTr("Total")
}
Item {
Layout.fillWidth: true
}
CustomText {
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.small
text: {
const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0);
const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0);
return (down && up) ? `${down.value.toFixed(1)}${down.unit} ${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B";
}
}
}
}
}
component ProgressBar: CustomRect {
id: progressBar
property real animatedValue: 0
property color bgColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
property color fgColor: DynamicColors.palette.m3primary
property real value: 0
color: bgColor
radius: Appearance.rounding.full
Behavior on animatedValue {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: animatedValue = value
onValueChanged: animatedValue = value
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
color: progressBar.fgColor
radius: Appearance.rounding.full
width: parent.width * progressBar.animatedValue
}
}
component StorageGaugeCard: CustomRect {
id: storageGaugeCard
property color accentColor: DynamicColors.palette.m3secondary
property real animatedPercentage: 0
readonly property real arcStartAngle: 0.75 * Math.PI
readonly property real arcSweep: 1.5 * Math.PI
readonly property var currentDisk: SystemUsage.disks.length > 0 ? SystemUsage.disks[currentDiskIndex] : null
property int currentDiskIndex: 0
property int diskCount: 0
clip: true
color: DynamicColors.tPalette.m3surfaceContainer
radius: Appearance.rounding.large - Appearance.padding.normal
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: {
diskCount = SystemUsage.disks.length;
if (currentDisk)
animatedPercentage = currentDisk.perc;
}
onCurrentDiskChanged: {
if (currentDisk)
animatedPercentage = currentDisk.perc;
}
// Update diskCount and animatedPercentage when disks data changes
Connections {
function onDisksChanged() {
if (SystemUsage.disks.length !== storageGaugeCard.diskCount)
storageGaugeCard.diskCount = SystemUsage.disks.length;
// Update animated percentage when disk data refreshes
if (storageGaugeCard.currentDisk)
storageGaugeCard.animatedPercentage = storageGaugeCard.currentDisk.perc;
}
target: SystemUsage
}
MouseArea {
anchors.fill: parent
onWheel: wheel => {
if (wheel.angleDelta.y > 0)
storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex - 1 + storageGaugeCard.diskCount) % storageGaugeCard.diskCount;
else if (wheel.angleDelta.y < 0)
storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex + 1) % storageGaugeCard.diskCount;
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.smaller
CardHeader {
accentColor: storageGaugeCard.accentColor
icon: "hard_disk"
title: {
const base = qsTr("Storage");
if (!storageGaugeCard.currentDisk)
return base;
return `${base} - ${storageGaugeCard.currentDisk.mount}`;
}
// Scroll hint icon
MaterialIcon {
ToolTip.delay: 500
ToolTip.text: qsTr("Scroll to switch disks")
ToolTip.visible: hintHover.hovered
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.normal
opacity: 0.7
text: "unfold_more"
visible: storageGaugeCard.diskCount > 1
HoverHandler {
id: hintHover
}
}
}
Item {
Layout.fillHeight: true
Layout.fillWidth: true
ArcGauge {
accentColor: storageGaugeCard.accentColor
anchors.centerIn: parent
height: width
percentage: storageGaugeCard.animatedPercentage
startAngle: storageGaugeCard.arcStartAngle
sweepAngle: storageGaugeCard.arcSweep
trackColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
width: Math.min(parent.width, parent.height)
}
CustomText {
anchors.centerIn: parent
color: storageGaugeCard.accentColor
font.pointSize: Appearance.font.size.extraLarge
font.weight: Font.Medium
text: storageGaugeCard.currentDisk ? `${Math.round(storageGaugeCard.currentDisk.perc * 100)}%` : "—"
}
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.smaller
text: {
if (!storageGaugeCard.currentDisk)
return "—";
const usedFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.used);
const totalFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.total);
return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`;
}
}
}
visible: active
}
}
-2
View File
@@ -34,8 +34,6 @@ Item {
anchors.centerIn: parent
sourceComponent: Content {
padding: Appearance.padding.normal
visibilities: root.visibilities
}
}
}
+11
View File
@@ -1,14 +1,25 @@
find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick Concurrent Sql Network DBus)
find_package(PkgConfig REQUIRED)
find_library(SENSORS_LIBRARY NAMES sensors REQUIRED)
find_path(SENSORS_INCLUDE_DIR NAMES sensors/sensors.h REQUIRED)
pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED)
pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED)
pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED)
pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET)
pkg_check_modules(GLIB REQUIRED glib-2.0 gobject-2.0 gio-2.0)
if(NOT Cava_FOUND)
pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED)
endif()
if(NOT TARGET Sensors::Sensors)
add_library(Sensors::Sensors UNKNOWN IMPORTED)
set_target_properties(Sensors::Sensors PROPERTIES
IMPORTED_LOCATION "${SENSORS_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${SENSORS_INCLUDE_DIR}"
)
endif()
set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
qt_standard_project_setup(REQUIRES 6.9)
+2
View File
@@ -10,6 +10,8 @@ qml_module(ZShell-internal
arcgauge.hpp arcgauge.cpp
wallpaperimage.hpp wallpaperimage.cpp
lidwatcher.hpp lidwatcher.cpp
visualizerbars.hpp visualizerbars.cpp
linearindicatormanager.hpp linearindicatormanager.cpp
LIBRARIES
Qt::Gui
Qt::Quick
@@ -0,0 +1,118 @@
#include "linearindicatormanager.hpp"
#include <qpoint.h>
namespace {
constexpr int TOTAL_DURATION_IN_MS = 1800;
constexpr std::array DURATION_TO_MOVE_SEGMENT_ENDS = { 533, 567, 850, 750 };
constexpr std::array DELAY_TO_MOVE_SEGMENT_ENDS = { 1267, 1000, 333, 0 };
QEasingCurve curve(const QPointF& c1, const QPointF& c2) {
QEasingCurve curve(QEasingCurve::BezierSpline);
curve.addCubicBezierSegment(c1, c2, { 1.0, 1.0 });
return curve;
}
qreal getFractionInRange(qreal playtime, int start, int duration) {
const auto fraction = static_cast<qreal>(playtime - start) / duration;
return std::clamp(fraction, 0.0, 1.0);
}
} // namespace
namespace ZShell::controls {
LinearIndicatorSegment::LinearIndicatorSegment(int gap, QObject* parent)
: QObject(parent)
, m_startFraction(0)
, m_endFraction(0)
, m_gapSize(gap) {
}
qreal LinearIndicatorSegment::startFraction() const {
return m_startFraction;
}
qreal LinearIndicatorSegment::endFraction() const {
return m_endFraction;
}
int LinearIndicatorSegment::gapSize() const {
return m_gapSize;
}
LinearIndicatorManager::LinearIndicatorManager(QObject* parent)
: QObject(parent)
, m_interpolators({
curve({ 0.2, 0.0 }, { 0.8, 1.0 }),
curve({ 0.4, 0.0 }, { 1.0, 1.0 }),
curve({ 0.0, 0.0 }, { 0.65, 1.0 }),
curve({ 0.1, 0.0 }, { 0.45, 1.0 }),
})
, m_progress(0)
, m_completeEndProgress(0)
, m_gap(4)
, m_activeIndicators({
new LinearIndicatorSegment(m_gap, this),
new LinearIndicatorSegment(m_gap, this),
}) {
for (auto el : m_activeIndicators)
QObject::connect(this, &LinearIndicatorManager::updated, el, &LinearIndicatorSegment::updated);
}
QList<LinearIndicatorSegment*> LinearIndicatorManager::activeIndicators() const {
return { m_activeIndicators.cbegin(), m_activeIndicators.cend() };
}
qreal LinearIndicatorManager::progress() const {
return m_progress;
}
qreal LinearIndicatorManager::completeEndProgress() const {
return m_completeEndProgress;
}
int LinearIndicatorManager::gap() const {
return m_gap;
}
void LinearIndicatorManager::setGap(int gap) {
m_gap = gap;
for (auto el : m_activeIndicators)
el->m_gapSize = m_gap;
update(m_progress);
}
int LinearIndicatorManager::duration() const {
return TOTAL_DURATION_IN_MS;
}
int LinearIndicatorManager::completeEndDuration() const {
return TOTAL_DURATION_IN_MS;
}
void LinearIndicatorManager::update(qreal progress) {
const auto playtime = progress * TOTAL_DURATION_IN_MS;
for (size_t i = 0; i < SEGMENTS; i++) {
const auto di = i * 2;
auto* const indicator = m_activeIndicators[i];
auto fraction = getFractionInRange(playtime, DELAY_TO_MOVE_SEGMENT_ENDS[di], DURATION_TO_MOVE_SEGMENT_ENDS[di]);
indicator->m_startFraction = std::clamp(m_interpolators[di].valueForProgress(fraction), 0.0, 1.0);
fraction =
getFractionInRange(playtime, DELAY_TO_MOVE_SEGMENT_ENDS[di + 1], DURATION_TO_MOVE_SEGMENT_ENDS[di + 1]);
indicator->m_endFraction = std::clamp(m_interpolators[di + 1].valueForProgress(fraction), 0.0, 1.0);
}
m_progress = progress;
emit updated();
}
void LinearIndicatorManager::updateCompleteEndProgress(qreal progress) {
m_completeEndProgress = progress;
update(m_progress);
}
} // namespace ZShell::controls
@@ -0,0 +1,86 @@
#pragma once
#include <qcolor.h>
#include <qeasingcurve.h>
#include <qobject.h>
#include <qqmlengine.h>
#include <qqmlintegration.h>
namespace ZShell::controls {
class LinearIndicatorManager;
class LinearIndicatorSegment : public QObject {
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("LinearIndicatorSegments can only be retrieved from a "
"LinearIndicatorManager.")
Q_PROPERTY(qreal startFraction READ startFraction NOTIFY updated FINAL)
Q_PROPERTY(qreal endFraction READ endFraction NOTIFY updated FINAL)
Q_PROPERTY(int gapSize READ gapSize NOTIFY updated FINAL)
public:
explicit LinearIndicatorSegment(int gap, QObject* parent = nullptr);
qreal startFraction() const;
qreal endFraction() const;
int gapSize() const;
signals:
void updated();
private:
qreal m_startFraction;
qreal m_endFraction;
int m_gapSize;
friend LinearIndicatorManager;
};
class LinearIndicatorManager : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(
QList<ZShell::controls::LinearIndicatorSegment*> activeIndicators READ activeIndicators CONSTANT FINAL)
Q_PROPERTY(qreal progress READ progress WRITE update NOTIFY updated FINAL)
Q_PROPERTY(qreal completeEndProgress READ completeEndProgress WRITE updateCompleteEndProgress NOTIFY updated FINAL)
Q_PROPERTY(int gap READ gap WRITE setGap NOTIFY updated FINAL)
Q_PROPERTY(qreal duration READ duration CONSTANT FINAL)
Q_PROPERTY(qreal completeEndDuration READ completeEndDuration CONSTANT FINAL)
public:
explicit LinearIndicatorManager(QObject* parent = nullptr);
QList<LinearIndicatorSegment*> activeIndicators() const;
qreal progress() const;
qreal completeEndProgress() const;
int gap() const;
void setGap(int gap);
int duration() const;
int completeEndDuration() const;
void update(qreal progress);
void updateCompleteEndProgress(qreal progress);
signals:
void updated();
private:
static constexpr int SEGMENTS = 2;
std::array<QEasingCurve, 4> m_interpolators;
qreal m_progress;
qreal m_completeEndProgress;
int m_gap;
std::array<LinearIndicatorSegment*, SEGMENTS> m_activeIndicators;
};
} // namespace ZShell::controls
+198
View File
@@ -0,0 +1,198 @@
#include "visualizerbars.hpp"
#include <algorithm>
#include <cmath>
#include <qbrush.h>
#include <qpainter.h>
#include <qpainterpath.h>
#include <qpen.h>
namespace ZShell::internal {
VisualizerBars::VisualizerBars(QQuickItem* parent)
: QQuickPaintedItem(parent) {
setAntialiasing(true);
}
void VisualizerBars::advance(qreal dt) {
if (m_displayValues.isEmpty() || m_settled)
return;
// dt is in seconds (from FrameAnimation.frameTime), convert to ms
const qreal dtMs = dt * 1000.0;
const qreal tau = m_animationDuration / 3.0;
const qreal alpha = 1.0 - std::exp(-dtMs / tau);
bool allSettled = true;
for (qsizetype i = 0; i < m_displayValues.size(); ++i) {
const double diff = m_targetValues[i] - m_displayValues[i];
if (std::abs(diff) > 0.001) {
m_displayValues[i] += diff * alpha;
allSettled = false;
} else {
m_displayValues[i] = m_targetValues[i];
}
}
update();
if (allSettled && !m_settled) {
m_settled = true;
emit settledChanged();
}
}
void VisualizerBars::paint(QPainter* painter) {
if (m_displayValues.isEmpty())
return;
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setPen(Qt::NoPen);
const qreal h = height();
const qreal maxBarHeight = h * 0.4;
QLinearGradient gradient(0, h - maxBarHeight, 0, h);
gradient.setColorAt(0, m_primaryColor);
gradient.setColorAt(1, m_secondaryColor);
painter->setBrush(gradient);
drawSide(painter, false);
drawSide(painter, true);
}
void VisualizerBars::drawSide(QPainter* painter, bool rightSide) {
const qreal w = width();
const qreal h = height();
const auto count = m_displayValues.size();
if (count == 0)
return;
const qreal sideWidth = w * 0.4;
const qreal slotWidth = sideWidth / static_cast<qreal>(count);
const qreal barWidth = slotWidth - m_spacing;
if (barWidth <= 0)
return;
const qreal sideOffset = rightSide ? w * 0.6 : 0;
const qreal maxBarHeight = h * 0.4;
for (qsizetype i = 0; i < count; ++i) {
const qsizetype valueIndex = rightSide ? i : (count - i - 1);
const qreal value = std::clamp(m_displayValues[valueIndex], 0.0, 1.0);
const qreal barHeight = value * maxBarHeight;
if (barHeight <= 0)
continue;
const qreal x = static_cast<qreal>(i) * slotWidth + sideOffset;
const qreal y = h - barHeight;
const qreal r = std::min({ m_rounding, barWidth / 2.0, barHeight });
QPainterPath path;
path.moveTo(x, h);
path.lineTo(x, y + r);
if (r > 0) {
path.arcTo(x, y, r * 2, r * 2, 180, -90);
path.lineTo(x + barWidth - r, y);
path.arcTo(x + barWidth - r * 2, y, r * 2, r * 2, 90, -90);
} else {
path.lineTo(x, y);
path.lineTo(x + barWidth, y);
}
path.lineTo(x + barWidth, h);
path.closeSubpath();
painter->drawPath(path);
}
}
QVector<double> VisualizerBars::values() const {
return m_targetValues;
}
void VisualizerBars::setValues(const QVector<double>& values) {
m_targetValues = values;
if (m_displayValues.size() != values.size()) {
m_displayValues.resize(values.size(), 0.0);
}
if (m_settled) {
m_settled = false;
emit settledChanged();
}
emit valuesChanged();
}
bool VisualizerBars::settled() const {
return m_settled;
}
QColor VisualizerBars::primaryColor() const {
return m_primaryColor;
}
void VisualizerBars::setPrimaryColor(const QColor& color) {
if (m_primaryColor == color)
return;
m_primaryColor = color;
emit primaryColorChanged();
update();
}
QColor VisualizerBars::secondaryColor() const {
return m_secondaryColor;
}
void VisualizerBars::setSecondaryColor(const QColor& color) {
if (m_secondaryColor == color)
return;
m_secondaryColor = color;
emit secondaryColorChanged();
update();
}
qreal VisualizerBars::rounding() const {
return m_rounding;
}
void VisualizerBars::setRounding(qreal rounding) {
if (qFuzzyCompare(m_rounding, rounding))
return;
m_rounding = rounding;
emit roundingChanged();
update();
}
qreal VisualizerBars::spacing() const {
return m_spacing;
}
void VisualizerBars::setSpacing(qreal spacing) {
if (qFuzzyCompare(m_spacing, spacing))
return;
m_spacing = spacing;
emit spacingChanged();
update();
}
int VisualizerBars::animationDuration() const {
return m_animationDuration;
}
void VisualizerBars::setAnimationDuration(int duration) {
if (m_animationDuration == duration)
return;
m_animationDuration = duration;
emit animationDurationChanged();
}
} // namespace ZShell::internal
@@ -0,0 +1,72 @@
#pragma once
#include <qcolor.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qquickpainteditem.h>
#include <qvector.h>
namespace ZShell::internal {
class VisualizerBars : public QQuickPaintedItem {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QVector<double> values READ values WRITE setValues NOTIFY valuesChanged)
Q_PROPERTY(QColor primaryColor READ primaryColor WRITE setPrimaryColor NOTIFY primaryColorChanged)
Q_PROPERTY(QColor secondaryColor READ secondaryColor WRITE setSecondaryColor NOTIFY secondaryColorChanged)
Q_PROPERTY(qreal rounding READ rounding WRITE setRounding NOTIFY roundingChanged)
Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
Q_PROPERTY(int animationDuration READ animationDuration WRITE setAnimationDuration NOTIFY animationDurationChanged)
Q_PROPERTY(bool settled READ settled NOTIFY settledChanged)
public:
explicit VisualizerBars(QQuickItem* parent = nullptr);
void paint(QPainter* painter) override;
Q_INVOKABLE void advance(qreal dt);
[[nodiscard]] QVector<double> values() const;
void setValues(const QVector<double>& values);
[[nodiscard]] QColor primaryColor() const;
void setPrimaryColor(const QColor& color);
[[nodiscard]] QColor secondaryColor() const;
void setSecondaryColor(const QColor& color);
[[nodiscard]] qreal rounding() const;
void setRounding(qreal rounding);
[[nodiscard]] qreal spacing() const;
void setSpacing(qreal spacing);
[[nodiscard]] int animationDuration() const;
void setAnimationDuration(int duration);
[[nodiscard]] bool settled() const;
signals:
void valuesChanged();
void primaryColorChanged();
void secondaryColorChanged();
void roundingChanged();
void spacingChanged();
void animationDurationChanged();
void settledChanged();
private:
void drawSide(QPainter* painter, bool rightSide);
QVector<double> m_targetValues;
QVector<double> m_displayValues;
QColor m_primaryColor;
QColor m_secondaryColor;
qreal m_rounding = 0.0;
qreal m_spacing = 0.0;
int m_animationDuration = 200;
bool m_settled = true;
};
} // namespace ZShell::internal
+8
View File
@@ -10,10 +10,18 @@ qml_module(ZShell-services
desktopmodel.hpp desktopmodel.cpp
desktopstatemanager.hpp desktopstatemanager.cpp
hyprsunsetmanager.hpp hyprsunsetmanager.cpp
tickingservice.hpp tickingservice.cpp
sensorslib.hpp sensorslib.cpp
usagefmt.hpp usagefmt.cpp
cpu.hpp cpu.cpp
memory.hpp memory.cpp
diskinfo.hpp diskinfo.cpp
storage.hpp storage.cpp
LIBRARIES
Qt6::Core
Qt6::Qml
PkgConfig::Pipewire
PkgConfig::Aubio
PkgConfig::Cava
Sensors::Sensors
)
+117
View File
@@ -0,0 +1,117 @@
#include "cpu.hpp"
#include "sensorslib.hpp"
#include <cmath>
#include <qfile.h>
#include <qregularexpression.h>
namespace ZShell::services {
Cpu::Cpu(QObject* parent)
: TickingService(parent) {
readNameOnce();
}
QString Cpu::name() const {
return m_name;
}
qreal Cpu::percentage() const {
return m_percentage;
}
qreal Cpu::temperature() const {
return m_temperature;
}
void Cpu::tick() {
if (!m_nameLoaded) {
readNameOnce();
}
refreshPercentage();
refreshTemperature();
}
void Cpu::readNameOnce() {
QFile f(QStringLiteral("/proc/cpuinfo"));
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
return;
}
const QByteArray data = f.readAll();
f.close();
static const QRegularExpression re(QStringLiteral("model name\\s*:\\s*(.+)"));
const auto match = re.match(QString::fromLatin1(data));
if (!match.hasMatch()) {
return;
}
const QString cleaned = cleanName(match.captured(1));
m_nameLoaded = true;
if (cleaned == m_name) {
return;
}
m_name = cleaned;
Q_EMIT nameChanged();
}
void Cpu::refreshPercentage() {
QFile f(QStringLiteral("/proc/stat"));
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
return;
}
const QByteArray data = f.readAll();
f.close();
static const QRegularExpression re(
QStringLiteral("^cpu\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)"));
const auto match = re.match(QString::fromLatin1(data));
if (!match.hasMatch()) {
return;
}
quint64 total = 0;
quint64 idle = 0;
for (int i = 1; i <= 7; ++i) {
const quint64 v = match.captured(i).toULongLong();
total += v;
if (i == 4 || i == 5) {
idle += v;
}
}
const quint64 totalDiff = total > m_lastTotal ? total - m_lastTotal : 0;
const quint64 idleDiff = idle > m_lastIdle ? idle - m_lastIdle : 0;
const qreal newPerc = totalDiff > 0 ? 1.0 - static_cast<qreal>(idleDiff) / static_cast<qreal>(totalDiff) : 0.0;
m_lastTotal = total;
m_lastIdle = idle;
if (std::abs(newPerc - m_percentage) > 0.0001) {
m_percentage = newPerc;
Q_EMIT percentageChanged();
}
}
void Cpu::refreshTemperature() {
const auto t = sensorslib::cpuPackageTemp();
const qreal newTemp = t.value_or(0.0);
if (std::abs(newTemp - m_temperature) > 0.05) {
m_temperature = newTemp;
Q_EMIT temperatureChanged();
}
}
QString Cpu::cleanName(QString s) {
static const QRegularExpression noise(
QStringLiteral("\\(R\\)|\\(TM\\)|CPU|\\d+(?:th|nd|rd|st) Gen |Core |Processor"),
QRegularExpression::CaseInsensitiveOption);
static const QRegularExpression spaces(QStringLiteral("\\s+"));
s.replace(noise, QString());
s.replace(spaces, QStringLiteral(" "));
return s.trimmed();
}
} // namespace ZShell::services
+48
View File
@@ -0,0 +1,48 @@
#pragma once
#include "tickingservice.hpp"
#include <qqmlintegration.h>
namespace ZShell::services {
class Cpu : public TickingService {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged)
Q_PROPERTY(qreal temperature READ temperature NOTIFY temperatureChanged)
public:
explicit Cpu(QObject* parent = nullptr);
[[nodiscard]] QString name() const;
[[nodiscard]] qreal percentage() const;
[[nodiscard]] qreal temperature() const;
signals:
void nameChanged();
void percentageChanged();
void temperatureChanged();
protected:
void tick() override;
private:
void readNameOnce();
void refreshPercentage();
void refreshTemperature();
[[nodiscard]] static QString cleanName(QString s);
QString m_name;
qreal m_percentage = 0.0;
qreal m_temperature = 0.0;
quint64 m_lastIdle = 0;
quint64 m_lastTotal = 0;
bool m_nameLoaded = false;
};
} // namespace ZShell::services
+68
View File
@@ -0,0 +1,68 @@
#include "diskinfo.hpp"
namespace ZShell::services {
namespace {
constexpr qreal kKib = 1024.0;
} // namespace
DiskInfo::DiskInfo(QString mount, quint64 usedBytes, quint64 totalBytes, bool hasRoot, QObject* parent)
: QObject(parent)
, m_mount(std::move(mount))
, m_usedBytes(usedBytes)
, m_totalBytes(totalBytes)
, m_hasRoot(hasRoot) {
}
QString DiskInfo::mount() const {
return m_mount;
}
qreal DiskInfo::used() const {
return static_cast<qreal>(m_usedBytes) / kKib;
}
qreal DiskInfo::total() const {
return static_cast<qreal>(m_totalBytes) / kKib;
}
qreal DiskInfo::free() const {
const quint64 freeBytes = m_totalBytes > m_usedBytes ? m_totalBytes - m_usedBytes : 0;
return static_cast<qreal>(freeBytes) / kKib;
}
qreal DiskInfo::perc() const {
return m_totalBytes > 0 ? static_cast<qreal>(m_usedBytes) / static_cast<qreal>(m_totalBytes) : 0.0;
}
bool DiskInfo::hasRoot() const {
return m_hasRoot;
}
void DiskInfo::update(quint64 usedBytes, quint64 totalBytes, bool hasRoot) {
const bool usedDiff = usedBytes != m_usedBytes;
const bool totalDiff = totalBytes != m_totalBytes;
const bool rootDiff = hasRoot != m_hasRoot;
m_usedBytes = usedBytes;
m_totalBytes = totalBytes;
m_hasRoot = hasRoot;
if (usedDiff) {
Q_EMIT usedChanged();
}
if (totalDiff) {
Q_EMIT totalChanged();
}
if (usedDiff || totalDiff) {
Q_EMIT freeChanged();
Q_EMIT percChanged();
}
if (rootDiff) {
Q_EMIT hasRootChanged();
}
}
} // namespace ZShell::services
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
namespace ZShell::services {
class DiskInfo : public QObject {
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("DiskInfo is created by DiskUsage")
Q_PROPERTY(QString mount READ mount CONSTANT)
Q_PROPERTY(qreal used READ used NOTIFY usedChanged)
Q_PROPERTY(qreal total READ total NOTIFY totalChanged)
Q_PROPERTY(qreal free READ free NOTIFY freeChanged)
Q_PROPERTY(qreal perc READ perc NOTIFY percChanged)
Q_PROPERTY(bool hasRoot READ hasRoot NOTIFY hasRootChanged)
public:
DiskInfo(QString mount, quint64 usedBytes, quint64 totalBytes, bool hasRoot, QObject* parent = nullptr);
[[nodiscard]] QString mount() const;
[[nodiscard]] qreal used() const;
[[nodiscard]] qreal total() const;
[[nodiscard]] qreal free() const;
[[nodiscard]] qreal perc() const;
[[nodiscard]] bool hasRoot() const;
void update(quint64 usedBytes, quint64 totalBytes, bool hasRoot);
signals:
void usedChanged();
void totalChanged();
void freeChanged();
void percChanged();
void hasRootChanged();
private:
QString m_mount;
quint64 m_usedBytes;
quint64 m_totalBytes;
bool m_hasRoot;
};
} // namespace ZShell::services
+59
View File
@@ -0,0 +1,59 @@
#include "memory.hpp"
#include <qfile.h>
#include <qregularexpression.h>
namespace ZShell::services {
Memory::Memory(QObject* parent)
: TickingService(parent) {
}
qreal Memory::used() const {
return m_used;
}
qreal Memory::total() const {
return m_total;
}
qreal Memory::percentage() const {
return m_total > 0.0 ? m_used / m_total : 0.0;
}
void Memory::tick() {
QFile f(QStringLiteral("/proc/meminfo"));
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
return;
}
const QByteArray data = f.readAll();
f.close();
static const QRegularExpression reTotal(QStringLiteral("MemTotal: *(\\d+)"));
static const QRegularExpression reAvail(QStringLiteral("MemAvailable: *(\\d+)"));
const QString text = QString::fromLatin1(data);
const auto totalMatch = reTotal.match(text);
const auto availMatch = reAvail.match(text);
if (!totalMatch.hasMatch() || !availMatch.hasMatch()) {
return;
}
const quint64 totalKib = totalMatch.captured(1).toULongLong();
const quint64 availKib = availMatch.captured(1).toULongLong();
if (totalKib == 0) {
return;
}
const quint64 usedKib = totalKib > availKib ? totalKib - availKib : 0;
if (totalKib == m_lastTotal && usedKib == m_lastUsed) {
return;
}
m_lastTotal = totalKib;
m_lastUsed = usedKib;
m_total = static_cast<qreal>(totalKib);
m_used = static_cast<qreal>(usedKib);
Q_EMIT changed();
}
} // namespace ZShell::services
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include "tickingservice.hpp"
#include <qqmlintegration.h>
#include <qvariant.h>
namespace ZShell::services {
class Memory : public TickingService {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(qreal used READ used NOTIFY changed)
Q_PROPERTY(qreal total READ total NOTIFY changed)
Q_PROPERTY(qreal percentage READ percentage NOTIFY changed)
public:
explicit Memory(QObject* parent = nullptr);
[[nodiscard]] qreal used() const;
[[nodiscard]] qreal total() const;
[[nodiscard]] qreal percentage() const;
signals:
void changed();
protected:
void tick() override;
private:
qreal m_used = 0.0;
qreal m_total = 1.0;
quint64 m_lastUsed = 0;
quint64 m_lastTotal = 0;
};
} // namespace ZShell::services
+166
View File
@@ -0,0 +1,166 @@
#include "sensorslib.hpp"
#include <atomic>
#include <cctype>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <qloggingcategory.h>
#include <sensors/sensors.h>
Q_LOGGING_CATEGORY(lcSensorsLib, "ZShell.services.sensorslib", QtInfoMsg)
namespace ZShell::services::sensorslib {
namespace {
std::atomic<bool> g_initOk{ false };
std::once_flag g_initFlag;
void doInit() {
if (sensors_init(nullptr) != 0) {
qCWarning(lcSensorsLib, "sensors_init failed");
g_initOk.store(false, std::memory_order_release);
return;
}
g_initOk.store(true, std::memory_order_release);
std::atexit([] {
if (g_initOk.load(std::memory_order_acquire)) {
sensors_cleanup();
}
});
}
[[nodiscard]] std::optional<double> readTempInput(const sensors_chip_name* chip, const sensors_feature* feat) {
const sensors_subfeature* sf = sensors_get_subfeature(chip, feat, SENSORS_SUBFEATURE_TEMP_INPUT);
if (!sf) {
return std::nullopt;
}
double value = 0.0;
if (sensors_get_value(chip, sf->number, &value) != 0) {
return std::nullopt;
}
return value;
}
[[nodiscard]] QByteArray featureLabel(const sensors_chip_name* chip, const sensors_feature* feat) {
char* raw = sensors_get_label(chip, feat);
if (!raw) {
return {};
}
QByteArray out(raw);
std::free(raw);
return out;
}
bool labelEquals(const QByteArray& label, const char* literal) {
return label == QByteArrayView(literal);
}
bool labelStartsWith(const QByteArray& label, const char* prefix) {
const auto n = std::strlen(prefix);
return static_cast<size_t>(label.size()) >= n && std::memcmp(label.constData(), prefix, n) == 0;
}
} // namespace
void ensureInit() {
std::call_once(g_initFlag, doInit);
}
std::optional<double> cpuPackageTemp() {
ensureInit();
if (!g_initOk.load(std::memory_order_acquire)) {
return std::nullopt;
}
std::optional<double> primary; // Package id N / Tdie
std::optional<double> fallback; // Tctl
int chipNr = 0;
while (const sensors_chip_name* chip = sensors_get_detected_chips(nullptr, &chipNr)) {
int featNr = 0;
while (const sensors_feature* feat = sensors_get_features(chip, &featNr)) {
if (feat->type != SENSORS_FEATURE_TEMP) {
continue;
}
const QByteArray label = featureLabel(chip, feat);
if (label.isEmpty()) {
continue;
}
if (labelStartsWith(label, "Package id ") || labelEquals(label, "Tdie")) {
if (auto v = readTempInput(chip, feat)) {
primary = v;
}
} else if (labelEquals(label, "Tctl")) {
if (auto v = readTempInput(chip, feat)) {
fallback = v;
}
}
}
}
return primary.has_value() ? primary : fallback;
}
std::optional<double> gpuPciAverageTemp() {
ensureInit();
if (!g_initOk.load(std::memory_order_acquire)) {
return std::nullopt;
}
double sumPrimary = 0.0;
int countPrimary = 0;
double sumFallback = 0.0;
int countFallback = 0;
int chipNr = 0;
while (const sensors_chip_name* chip = sensors_get_detected_chips(nullptr, &chipNr)) {
if (chip->bus.type != SENSORS_BUS_TYPE_PCI) {
continue;
}
int featNr = 0;
while (const sensors_feature* feat = sensors_get_features(chip, &featNr)) {
if (feat->type != SENSORS_FEATURE_TEMP) {
continue;
}
const QByteArray label = featureLabel(chip, feat);
if (label.isEmpty()) {
continue;
}
const bool tempIndexed = labelStartsWith(label, "temp") && label.size() > 4 &&
std::isdigit(static_cast<unsigned char>(label[4]));
const bool isPrimary = tempIndexed || labelEquals(label, "GPU core") || labelEquals(label, "edge");
const bool isFallback = labelEquals(label, "junction") || labelEquals(label, "mem");
if (!isPrimary && !isFallback) {
continue;
}
const auto v = readTempInput(chip, feat);
if (!v) {
continue;
}
if (isPrimary) {
sumPrimary += *v;
++countPrimary;
} else {
sumFallback += *v;
++countFallback;
}
}
}
if (countPrimary > 0) {
return sumPrimary / countPrimary;
}
if (countFallback > 0) {
return sumFallback / countFallback;
}
return std::nullopt;
}
} // namespace ZShell::services::sensorslib
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <optional>
namespace ZShell::services::sensorslib {
void ensureInit();
[[nodiscard]] std::optional<double> cpuPackageTemp();
[[nodiscard]] std::optional<double> gpuPciAverageTemp();
} // namespace ZShell::services::sensorslib
+313
View File
@@ -0,0 +1,313 @@
#include "storage.hpp"
#include <algorithm>
#include <cmath>
#include <qdir.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qhash.h>
#include <qloggingcategory.h>
#include <qstorageinfo.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
Q_LOGGING_CATEGORY(lcStorage, "ZShell.services.storage", QtInfoMsg)
namespace ZShell::services {
namespace {
struct Accum {
quint64 usedBytes = 0;
quint64 totalBytes = 0;
bool hasRoot = false;
};
[[nodiscard]] QString sysfsRealPath(uint major, uint minor) {
const QString link = QStringLiteral("/sys/dev/block/%1:%2").arg(major).arg(minor);
const QString resolved = QFileInfo(link).canonicalFilePath();
return resolved;
}
[[nodiscard]] bool readDevtFromSysfs(const QString& sysfsBlockDir, uint& major, uint& minor) {
QFile f(sysfsBlockDir + QStringLiteral("/dev"));
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
return false;
}
const QByteArray line = f.readLine().trimmed();
f.close();
const qsizetype colon = line.indexOf(':');
if (colon <= 0) {
return false;
}
bool okM = false;
bool okN = false;
major = line.left(colon).toUInt(&okM);
minor = line.mid(colon + 1).toUInt(&okN);
return okM && okN;
}
QStringList resolveByDevt(uint major, uint minor, int depth = 0);
QStringList resolveAtNode(const QString& node, int depth) {
if (node.isEmpty() || depth > 8) {
return {};
}
const QFileInfo nodeInfo(node);
if (!nodeInfo.exists() || !nodeInfo.isDir()) {
return {};
}
if (QFileInfo::exists(node + QStringLiteral("/partition"))) {
const QString diskNode = nodeInfo.path();
return { QFileInfo(diskNode).fileName() };
}
const QDir slavesDir(node + QStringLiteral("/slaves"));
if (slavesDir.exists()) {
const QStringList slaves = slavesDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
if (!slaves.isEmpty()) {
QStringList out;
for (const QString& slave : slaves) {
uint sm = 0;
uint sn = 0;
const QString slaveDir = QStringLiteral("/sys/class/block/") + slave;
if (!readDevtFromSysfs(slaveDir, sm, sn)) {
continue;
}
const auto devs = resolveByDevt(sm, sn, depth + 1);
for (const QString& d : devs) {
if (!out.contains(d)) {
out.append(d);
}
}
}
return out;
}
}
return { nodeInfo.fileName() };
}
QStringList resolveByDevt(uint major, uint minor, int depth) {
return resolveAtNode(sysfsRealPath(major, minor), depth);
}
} // namespace
Storage::Storage(QObject* parent)
: TickingService(parent) {
}
qreal Storage::percentage() const {
qreal totalUsed = 0.0;
qreal totalSize = 0.0;
for (const DiskInfo* d : m_disks) {
totalUsed += d->used();
totalSize += d->total();
}
return totalSize > 0.0 ? totalUsed / totalSize : 0.0;
}
bool Storage::sameOrder(const QList<DiskInfo*>& a, const QList<DiskInfo*>& b) {
if (a.size() != b.size()) {
return false;
}
for (qsizetype i = 0; i < a.size(); ++i) {
if (a.at(i) != b.at(i)) {
return false;
}
}
return true;
}
QQmlListProperty<DiskInfo> Storage::disksProp() {
return QQmlListProperty<DiskInfo>(this, nullptr, &Storage::disksCount, &Storage::disksAt);
}
qsizetype Storage::disksCount(QQmlListProperty<DiskInfo>* prop) {
return static_cast<Storage*>(prop->object)->m_disks.size();
}
DiskInfo* Storage::disksAt(QQmlListProperty<DiskInfo>* prop, qsizetype i) {
return static_cast<Storage*>(prop->object)->m_disks.at(i);
}
DiskInfo* Storage::manualPrimaryDisk() const {
return m_manualPrimaryDisk.data();
}
void Storage::setManualPrimaryDisk(DiskInfo* disk) {
if (m_manualPrimaryDisk.data() == disk) {
return;
}
m_manualPrimaryDisk = disk;
Q_EMIT manualPrimaryDiskChanged();
Q_EMIT primaryDiskChanged();
}
DiskInfo* Storage::primaryDisk() const {
if (auto* m = m_manualPrimaryDisk.data()) {
return m;
}
return m_disks.isEmpty() ? nullptr : m_disks.first();
}
bool Storage::isPseudoFs(QByteArrayView fsType) {
static constexpr const char* kPseudo[] = {
"tmpfs",
"devtmpfs",
"proc",
"sysfs",
"cgroup",
"cgroup2",
"overlay",
"squashfs",
"devpts",
"mqueue",
"ramfs",
"rpc_pipefs",
"autofs",
"configfs",
"debugfs",
"tracefs",
"securityfs",
"pstore",
"bpf",
"binfmt_misc",
"hugetlbfs",
"fusectl",
"efivarfs",
"selinuxfs",
};
for (const char* p : kPseudo) {
if (fsType == QByteArrayView(p)) {
return true;
}
}
return fsType.startsWith(QByteArrayView("fuse."));
}
QStringList Storage::resolveToPhysicalDisks(const QString& devicePath) {
if (devicePath.isEmpty() || !devicePath.startsWith(QLatin1Char('/'))) {
return {};
}
struct stat st {};
if (::stat(devicePath.toLocal8Bit().constData(), &st) != 0) {
return {};
}
if (!S_ISBLK(st.st_mode)) {
return {};
}
return resolveByDevt(major(st.st_rdev), minor(st.st_rdev));
}
void Storage::tick() {
const qreal prevPercentage = percentage();
QHash<QString, Accum> byDisk;
// Multiple mounts can share a single backing filesystem (btrfs subvolumes,
// bind mounts, etc.) and each one reports identical bytesTotal/bytesAvailable.
// Dedupe by source device so the filesystem only contributes once per disk.
struct DeviceEntry {
quint64 totalBytes = 0;
quint64 usedBytes = 0;
bool hasRoot = false;
QByteArray device;
};
QHash<QByteArray, DeviceEntry> byDevice;
const auto mountedVols = QStorageInfo::mountedVolumes();
for (const QStorageInfo& v : mountedVols) {
if (!v.isReady() || !v.isValid() || v.bytesTotal() <= 0) {
continue;
}
if (isPseudoFs(QByteArrayView(v.fileSystemType()))) {
continue;
}
const QByteArray device = v.device();
const auto totalBytes = static_cast<quint64>(v.bytesTotal());
const auto availBytes = static_cast<quint64>(v.bytesAvailable());
const quint64 usedBytes = totalBytes > availBytes ? totalBytes - availBytes : 0;
const bool isRoot = v.rootPath() == QStringLiteral("/");
DeviceEntry& e = byDevice[device];
e.device = device;
e.totalBytes = totalBytes;
e.usedBytes = usedBytes;
e.hasRoot = e.hasRoot || isRoot;
}
for (auto it = byDevice.constBegin(); it != byDevice.constEnd(); ++it) {
const DeviceEntry& e = it.value();
const QStringList disks = resolveToPhysicalDisks(QString::fromLocal8Bit(e.device));
if (disks.isEmpty()) {
continue;
}
for (const QString& d : disks) {
if (d.startsWith(QStringLiteral("zram"))) {
continue;
}
Accum& a = byDisk[d];
a.usedBytes += e.usedBytes;
a.totalBytes += e.totalBytes;
a.hasRoot = a.hasRoot || e.hasRoot;
}
}
QHash<QString, DiskInfo*> existing;
existing.reserve(m_disks.size());
for (DiskInfo* d : std::as_const(m_disks)) {
existing.insert(d->mount(), d);
}
QList<DiskInfo*> next;
next.reserve(byDisk.size());
for (auto it = byDisk.constBegin(); it != byDisk.constEnd(); ++it) {
if (DiskInfo* survivor = existing.take(it.key())) {
survivor->update(it.value().usedBytes, it.value().totalBytes, it.value().hasRoot);
next.append(survivor);
} else {
next.append(new DiskInfo(it.key(), it.value().usedBytes, it.value().totalBytes, it.value().hasRoot, this));
}
}
std::sort(next.begin(), next.end(), [](const DiskInfo* a, const DiskInfo* b) {
if (a->hasRoot() != b->hasRoot()) {
return a->hasRoot();
}
return a->mount() < b->mount();
});
bool manualCleared = false;
if (DiskInfo* m = m_manualPrimaryDisk.data(); m && existing.contains(m->mount())) {
m_manualPrimaryDisk.clear();
manualCleared = true;
}
for (DiskInfo* stale : std::as_const(existing)) {
stale->deleteLater();
}
const bool listChanged = !sameOrder(m_disks, next);
DiskInfo* prevPrimary = primaryDisk();
m_disks = next;
if (listChanged) {
Q_EMIT disksChanged();
}
if (std::abs(percentage() - prevPercentage) > 0.0001) {
Q_EMIT percentageChanged();
}
if (manualCleared) {
Q_EMIT manualPrimaryDiskChanged();
}
if (primaryDisk() != prevPrimary) {
Q_EMIT primaryDiskChanged();
}
}
} // namespace ZShell::services
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include "diskinfo.hpp"
#include "tickingservice.hpp"
#include <qbytearrayview.h>
#include <qpointer.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qvariant.h>
namespace ZShell::services {
class Storage : public TickingService {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged)
Q_PROPERTY(QQmlListProperty<ZShell::services::DiskInfo> disks READ disksProp NOTIFY disksChanged)
Q_PROPERTY(ZShell::services::DiskInfo* manualPrimaryDisk READ manualPrimaryDisk WRITE setManualPrimaryDisk NOTIFY
manualPrimaryDiskChanged)
Q_PROPERTY(ZShell::services::DiskInfo* primaryDisk READ primaryDisk NOTIFY primaryDiskChanged)
public:
explicit Storage(QObject* parent = nullptr);
[[nodiscard]] qreal percentage() const;
[[nodiscard]] QQmlListProperty<DiskInfo> disksProp();
[[nodiscard]] DiskInfo* manualPrimaryDisk() const;
void setManualPrimaryDisk(DiskInfo* disk);
[[nodiscard]] DiskInfo* primaryDisk() const;
signals:
void disksChanged();
void percentageChanged();
void manualPrimaryDiskChanged();
void primaryDiskChanged();
protected:
void tick() override;
private:
[[nodiscard]] static QStringList resolveToPhysicalDisks(const QString& devicePath);
[[nodiscard]] static bool isPseudoFs(QByteArrayView fsType);
[[nodiscard]] static bool sameOrder(const QList<DiskInfo*>& a, const QList<DiskInfo*>& b);
static qsizetype disksCount(QQmlListProperty<DiskInfo>* prop);
static DiskInfo* disksAt(QQmlListProperty<DiskInfo>* prop, qsizetype i);
QList<DiskInfo*> m_disks;
QPointer<DiskInfo> m_manualPrimaryDisk;
};
} // namespace ZShell::services
@@ -0,0 +1,42 @@
#include "tickingservice.hpp"
namespace ZShell::services {
TickingService::TickingService(QObject* parent)
: Service(parent)
, m_timer(new QTimer(this)) {
m_timer->setSingleShot(false);
QObject::connect(m_timer, &QTimer::timeout, this, [this] {
tick();
});
}
int TickingService::updateInterval() const {
return m_interval;
}
void TickingService::start() {
m_running = true;
if (m_interval > 0) {
m_timer->start(m_interval);
}
tick();
}
void TickingService::stop() {
m_running = false;
m_timer->stop();
}
void TickingService::applyInterval(int ms) {
if (ms <= 0 || ms == m_interval) {
return;
}
m_interval = ms;
if (m_running) {
m_timer->start(m_interval);
}
Q_EMIT updateIntervalChanged();
}
} // namespace ZShell::services
@@ -0,0 +1,35 @@
#pragma once
#include "service.hpp"
#include <qtimer.h>
namespace ZShell::services {
class TickingService : public Service {
Q_OBJECT
Q_PROPERTY(int updateInterval READ updateInterval NOTIFY updateIntervalChanged)
public:
explicit TickingService(QObject* parent = nullptr);
[[nodiscard]] int updateInterval() const;
signals:
void updateIntervalChanged();
protected:
void start() final;
void stop() final;
virtual void tick() = 0;
private:
void applyInterval(int ms);
QTimer* m_timer;
int m_interval = 1000;
bool m_running = false;
};
} // namespace ZShell::services
+33
View File
@@ -0,0 +1,33 @@
#include "usagefmt.hpp"
namespace {
constexpr qreal kKib = 1024.0;
constexpr qreal kMib = kKib * 1024.0;
constexpr qreal kGib = kMib * 1024.0;
bool finitePositive(qreal v) {
return std::isfinite(v) && v >= 0.0;
}
} // namespace
namespace ZShell::services::usagefmt {
FormatResult UsageFmt::formatKib(qreal kib, qreal total) const {
if (!finitePositive(kib) || !finitePositive(total)) {
return { 0.0, 0.0, "KiB" };
}
if (total >= kGib) {
return { kib / kGib, total / kGib, "TiB" };
}
if (total >= kMib) {
return { kib / kMib, total / kMib, "GiB" };
}
if (total >= kKib) {
return { kib / kKib, total / kKib, "MiB" };
}
return { kib, total, "KiB" };
}
} // namespace ZShell::services::usagefmt
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <qobject.h>
#include <qobjectdefs.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>
namespace ZShell::services::usagefmt {
struct FormatResult {
Q_GADGET
QML_ANONYMOUS
Q_PROPERTY(qreal value MEMBER value CONSTANT)
Q_PROPERTY(qreal total MEMBER total CONSTANT)
Q_PROPERTY(QString unit MEMBER unit CONSTANT)
public:
qreal value;
qreal total;
QString unit;
};
class UsageFmt : public QObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
Q_INVOKABLE [[nodiscard]] FormatResult formatKib(qreal kib, qreal total) const;
};
} // namespace ZShell::services::usagefmt