resources popout refresh + new components & reskinned old components
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
asynchronous: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
implicitHeight: implicitSize
|
||||
implicitWidth: implicitSize
|
||||
|
||||
ShapePath {
|
||||
capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
|
||||
fillColor: "transparent"
|
||||
strokeColor: root.bgColour
|
||||
strokeWidth: root.strokeWidth
|
||||
Shape {
|
||||
asynchronous: true
|
||||
opacity: Math.min(1, remainingArc.sweepAngle)
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
Behavior on strokeColor {
|
||||
CAnim {
|
||||
duration: Appearance.anim.durations.large
|
||||
ShapePath {
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillColor: "transparent"
|
||||
strokeColor: root.bgColor
|
||||
strokeWidth: Math.min(1, remainingArc.sweepAngle) * root.strokeWidth
|
||||
|
||||
Behavior on strokeColor {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PathAngleArc {
|
||||
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)
|
||||
PathAngleArc {
|
||||
id: remainingArc
|
||||
|
||||
centerX: root.size / 2
|
||||
centerY: root.size / 2
|
||||
radiusX: root.arcRadius
|
||||
radiusY: root.arcRadius
|
||||
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
|
||||
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
|
||||
value: root.clampedVal
|
||||
|
||||
Behavior on amplitudeMultiplier {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
Anim on waveProgress {
|
||||
id: waveProgAnim
|
||||
|
||||
PathAngleArc {
|
||||
centerX: root.size / 2
|
||||
centerY: root.size / 2
|
||||
radiusX: root.arcRadius
|
||||
radiusY: root.arcRadius
|
||||
startAngle: root.startAngle
|
||||
sweepAngle: 360 * root.vValue
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Resource {
|
||||
color: DynamicColors.palette.m3primary
|
||||
icon: "memory"
|
||||
value: SystemUsage.cpuPerc
|
||||
}
|
||||
|
||||
Resource {
|
||||
color: DynamicColors.palette.m3secondary
|
||||
icon: "memory_alt"
|
||||
value: SystemUsage.memPerc
|
||||
}
|
||||
|
||||
Resource {
|
||||
color: DynamicColors.palette.m3tertiary
|
||||
icon: "gamepad"
|
||||
value: SystemUsage.gpuPerc
|
||||
}
|
||||
|
||||
Resource {
|
||||
color: DynamicColors.palette.m3primary
|
||||
icon: "host"
|
||||
value: SystemUsage.gpuMemUsed
|
||||
}
|
||||
|
||||
Resource {
|
||||
color: DynamicColors.palette.m3secondary
|
||||
icon: "hard_disk"
|
||||
value: SystemUsage.storagePerc
|
||||
}
|
||||
|
||||
component Resource: Item {
|
||||
id: res
|
||||
|
||||
required property color color
|
||||
required property string icon
|
||||
required property real value
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.top: parent.top
|
||||
implicitWidth: icon.implicitWidth
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
Behavior on value {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.large
|
||||
}
|
||||
Resource {
|
||||
fgColor: DynamicColors.palette.m3primary
|
||||
icon: "memory"
|
||||
value: Cpu.percentage
|
||||
}
|
||||
|
||||
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
|
||||
Resource {
|
||||
fgColor: DynamicColors.palette.m3secondary
|
||||
icon: "memory_alt"
|
||||
value: Memory.percentage
|
||||
}
|
||||
|
||||
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
|
||||
Resource {
|
||||
fgColor: DynamicColors.palette.m3tertiary
|
||||
icon: "gamepad"
|
||||
value: SystemUsage.gpuPerc
|
||||
}
|
||||
|
||||
Resource {
|
||||
fgColor: DynamicColors.palette.m3primary
|
||||
icon: "host"
|
||||
value: SystemUsage.gpuMemUsed
|
||||
}
|
||||
|
||||
Resource {
|
||||
fgColor: DynamicColors.palette.m3secondary
|
||||
icon: "hard_disk"
|
||||
value: Storage.percentage
|
||||
}
|
||||
}
|
||||
|
||||
component Resource: CircularProgress {
|
||||
id: res
|
||||
|
||||
required property string icon
|
||||
|
||||
Layout.fillHeight: true
|
||||
implicitSize: height
|
||||
|
||||
Behavior on clampedVal {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
color: res.color
|
||||
anchors.centerIn: parent
|
||||
color: res.fgColor
|
||||
text: res.icon
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}%`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) + "%"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+71
-743
@@ -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
|
||||
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
|
||||
}
|
||||
WrappedLoader {
|
||||
id: cpuCard
|
||||
|
||||
HeroCard {
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: 400
|
||||
Layout.preferredHeight: 150
|
||||
accentColor: DynamicColors.palette.m3secondary
|
||||
icon: "desktop_windows"
|
||||
mainLabel: qsTr("Usage")
|
||||
mainValue: `${Math.round(SystemUsage.gpuPerc * 100)}%`
|
||||
secondaryLabel: qsTr("Temp")
|
||||
secondaryValue: root.displayTemp(SystemUsage.gpuTemp)
|
||||
temperature: SystemUsage.gpuTemp
|
||||
title: SystemUsage.gpuName ? `GPU - ${SystemUsage.gpuName}` : qsTr("GPU")
|
||||
usage: SystemUsage.gpuPerc
|
||||
visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE"
|
||||
}
|
||||
}
|
||||
active: Config.dashboard.performance.showCpu
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork
|
||||
sourceComponent: HeroCard {
|
||||
accent: DynamicColors.palette.m3primary
|
||||
icon: "memory"
|
||||
label: qsTr("CPU")
|
||||
subLabel: Cpu.name
|
||||
temperature: Cpu.temperature
|
||||
usage: Cpu.percentage
|
||||
|
||||
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";
|
||||
ServiceRef {
|
||||
service: Cpu
|
||||
}
|
||||
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")
|
||||
WrappedLoader {
|
||||
id: gpuCard
|
||||
|
||||
active: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== ""
|
||||
|
||||
sourceComponent: HeroCard {
|
||||
accent: DynamicColors.palette.m3secondary
|
||||
icon: "desktop_windows"
|
||||
label: qsTr("GPU")
|
||||
subLabel: SystemUsage.gpuName
|
||||
temperature: SystemUsage.gpuTemp
|
||||
usage: SystemUsage.gpuPerc
|
||||
|
||||
Ref {
|
||||
service: SystemUsage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
RowLayout {
|
||||
spacing: Appearance.spacing.larger
|
||||
visible: storageCard.active || networkCard.active || memoryCard.active
|
||||
|
||||
// Bottom Info Section
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: -4
|
||||
WrappedLoader {
|
||||
id: storageCard
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
color: batteryTank.accentColor
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
font.weight: Font.Medium
|
||||
text: `${Math.round(batteryTank.percentage * 100)}%`
|
||||
active: Config.dashboard.performance.showStorage
|
||||
|
||||
sourceComponent: StorageCard {
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
WrappedLoader {
|
||||
id: memoryCard
|
||||
|
||||
if (batteryTank.isCharging)
|
||||
return qsTr("Charging");
|
||||
active: Config.dashboard.performance.showMemory
|
||||
|
||||
const s = UPower.displayDevice.timeToEmpty;
|
||||
if (s === 0)
|
||||
return qsTr("...");
|
||||
sourceComponent: MemoryCard {
|
||||
}
|
||||
}
|
||||
|
||||
const hr = Math.floor(s / 3600);
|
||||
const min = Math.floor((s % 3600) / 60);
|
||||
if (hr > 0)
|
||||
return `${hr}h ${min}m`;
|
||||
WrappedLoader {
|
||||
id: networkCard
|
||||
|
||||
return `${min}m`;
|
||||
active: Config.dashboard.performance.showNetwork
|
||||
|
||||
sourceComponent: NetworkCard {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
component CardHeader: RowLayout {
|
||||
property color accentColor: DynamicColors.palette.m3primary
|
||||
property string icon
|
||||
property string title
|
||||
|
||||
WrappedLoader {
|
||||
Layout.fillWidth: false
|
||||
active: Battery.isLaptop && Config.dashboard.performance.showBattery
|
||||
|
||||
sourceComponent: BatteryTank {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component WrappedLoader: Loader {
|
||||
Layout.fillHeight: true
|
||||
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
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
CardHeader {
|
||||
accentColor: networkCard.accentColor
|
||||
icon: "swap_vert"
|
||||
title: qsTr("Network")
|
||||
}
|
||||
|
||||
// Sparkline graph
|
||||
Item {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,6 @@ Item {
|
||||
anchors.centerIn: parent
|
||||
|
||||
sourceComponent: Content {
|
||||
padding: Appearance.padding.normal
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
qml_module(ZShell-internal
|
||||
URI ZShell.Internal
|
||||
SOURCES
|
||||
hyprextras.hpp hyprextras.cpp
|
||||
hyprdevices.hpp hyprdevices.cpp
|
||||
cachingimagemanager.hpp cachingimagemanager.cpp
|
||||
URI ZShell.Internal
|
||||
SOURCES
|
||||
hyprextras.hpp hyprextras.cpp
|
||||
hyprdevices.hpp hyprdevices.cpp
|
||||
cachingimagemanager.hpp cachingimagemanager.cpp
|
||||
circularindicatormanager.hpp circularindicatormanager.cpp
|
||||
circularbuffer.hpp circularbuffer.cpp
|
||||
sparklineitem.hpp sparklineitem.cpp
|
||||
arcgauge.hpp arcgauge.cpp
|
||||
wallpaperimage.hpp wallpaperimage.cpp
|
||||
lidwatcher.hpp lidwatcher.cpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
Qt::Concurrent
|
||||
Qt::Core
|
||||
visualizerbars.hpp visualizerbars.cpp
|
||||
linearindicatormanager.hpp linearindicatormanager.cpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
Qt::Concurrent
|
||||
Qt::Core
|
||||
Qt::Network
|
||||
Qt::DBus
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,19 +1,27 @@
|
||||
qml_module(ZShell-services
|
||||
URI ZShell.Services
|
||||
SOURCES
|
||||
service.hpp service.cpp
|
||||
serviceref.hpp serviceref.cpp
|
||||
beattracker.hpp beattracker.cpp
|
||||
audiocollector.hpp audiocollector.cpp
|
||||
audioprovider.hpp audioprovider.cpp
|
||||
cavaprovider.hpp cavaprovider.cpp
|
||||
SOURCES
|
||||
service.hpp service.cpp
|
||||
serviceref.hpp serviceref.cpp
|
||||
beattracker.hpp beattracker.cpp
|
||||
audiocollector.hpp audiocollector.cpp
|
||||
audioprovider.hpp audioprovider.cpp
|
||||
cavaprovider.hpp cavaprovider.cpp
|
||||
desktopmodel.hpp desktopmodel.cpp
|
||||
desktopstatemanager.hpp desktopstatemanager.cpp
|
||||
hyprsunsetmanager.hpp hyprsunsetmanager.cpp
|
||||
LIBRARIES
|
||||
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
|
||||
PkgConfig::Pipewire
|
||||
PkgConfig::Aubio
|
||||
PkgConfig::Cava
|
||||
Sensors::Sensors
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user