mirror of
https://github.com/arthur-pbty/arthur-os.git
synced 2026-08-06 12:39:35 +02:00
406 lines
10 KiB
QML
406 lines
10 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import "../common" as Common
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property QtObject bar: null
|
|
property string moduleName: "systemStats"
|
|
property var settings: ({})
|
|
|
|
property real cpuPercent: 0
|
|
property real memPercent: 0
|
|
property var cpuHistory: []
|
|
property var memHistory: []
|
|
property real loadAvg: 0
|
|
|
|
property var prevCpu: ({ idle: 0, total: 0 })
|
|
|
|
property bool popupOpen: false
|
|
|
|
function closePopout() { popupOpen = false }
|
|
|
|
readonly property int historyLimit: 30
|
|
|
|
function refresh() {
|
|
if (!cpuProc.running) cpuProc.running = true
|
|
if (!memProc.running) memProc.running = true
|
|
}
|
|
|
|
function pushHistory(arr, value) {
|
|
var next = arr.slice()
|
|
next.push(value)
|
|
if (next.length > historyLimit) next.shift()
|
|
return next
|
|
}
|
|
|
|
function updateCpu(raw) {
|
|
var fields = String(raw || "").trim().split(/\s+/)
|
|
if (fields.length < 8) return
|
|
var user = parseInt(fields[1], 10) || 0
|
|
var nice = parseInt(fields[2], 10) || 0
|
|
var sys = parseInt(fields[3], 10) || 0
|
|
var idle = parseInt(fields[4], 10) || 0
|
|
var iowait = parseInt(fields[5], 10) || 0
|
|
var irq = parseInt(fields[6], 10) || 0
|
|
var softirq = parseInt(fields[7], 10) || 0
|
|
|
|
var total = user + nice + sys + idle + iowait + irq + softirq
|
|
var totalDiff = total - prevCpu.total
|
|
var idleDiff = idle - prevCpu.idle
|
|
|
|
if (prevCpu.total > 0 && totalDiff > 0) {
|
|
var usage = (1 - idleDiff / totalDiff) * 100
|
|
cpuPercent = Math.max(0, Math.min(100, usage))
|
|
cpuHistory = pushHistory(cpuHistory, cpuPercent)
|
|
}
|
|
|
|
prevCpu = { idle: idle, total: total }
|
|
}
|
|
|
|
function updateMem(raw) {
|
|
var lines = String(raw || "").split("\n")
|
|
var total = 0
|
|
var available = 0
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var line = lines[i]
|
|
if (line.indexOf("MemTotal:") === 0) total = parseInt(line.replace(/[^0-9]/g, ""), 10) || 0
|
|
else if (line.indexOf("MemAvailable:") === 0) available = parseInt(line.replace(/[^0-9]/g, ""), 10) || 0
|
|
}
|
|
if (total > 0) {
|
|
memPercent = ((total - available) / total) * 100
|
|
memHistory = pushHistory(memHistory, memPercent)
|
|
}
|
|
}
|
|
|
|
function updateLoad(raw) {
|
|
var n = parseFloat(String(raw || "").trim().split(/\s+/)[0])
|
|
if (!isNaN(n)) loadAvg = n
|
|
}
|
|
|
|
Component.onCompleted: refresh()
|
|
|
|
Process {
|
|
id: cpuProc
|
|
command: ["bash", "-lc", "head -n1 /proc/stat"]
|
|
stdout: StdioCollector {
|
|
waitForEnd: true
|
|
onStreamFinished: root.updateCpu(text)
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: memProc
|
|
command: ["bash", "-lc", "head -n3 /proc/meminfo"]
|
|
stdout: StdioCollector {
|
|
waitForEnd: true
|
|
onStreamFinished: root.updateMem(text)
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: loadProc
|
|
command: ["bash", "-lc", "cat /proc/loadavg"]
|
|
stdout: StdioCollector {
|
|
waitForEnd: true
|
|
onStreamFinished: root.updateLoad(text)
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
interval: 2000
|
|
running: true
|
|
repeat: true
|
|
onTriggered: {
|
|
root.refresh()
|
|
if (!loadProc.running) loadProc.running = true
|
|
}
|
|
}
|
|
|
|
readonly property bool vertical: bar ? bar.vertical : false
|
|
|
|
implicitWidth: vertical ? (bar ? bar.barSize : 28) : (statLayout.item ? statLayout.item.implicitWidth + 6 : 0)
|
|
implicitHeight: vertical ? (statLayout.item ? statLayout.item.implicitHeight + 6 : 0) : (bar ? bar.barSize : 26)
|
|
|
|
readonly property color statColor: bar ? bar.foreground : "#cacccc"
|
|
readonly property string statFont: bar ? bar.fontFamily : "JetBrainsMono Nerd Font"
|
|
|
|
Loader {
|
|
id: statLayout
|
|
anchors.centerIn: parent
|
|
sourceComponent: root.vertical ? statColumn : statRow
|
|
}
|
|
|
|
Component {
|
|
id: statRow
|
|
Row {
|
|
spacing: 8
|
|
StatPill {
|
|
glyph: ""
|
|
percent: root.cpuPercent
|
|
history: root.cpuHistory
|
|
vertical: false
|
|
barFg: root.statColor
|
|
fontFamily: root.statFont
|
|
}
|
|
StatPill {
|
|
glyph: ""
|
|
percent: root.memPercent
|
|
history: root.memHistory
|
|
vertical: false
|
|
barFg: root.statColor
|
|
fontFamily: root.statFont
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: statColumn
|
|
Column {
|
|
spacing: 4
|
|
StatPill {
|
|
glyph: ""
|
|
percent: root.cpuPercent
|
|
history: root.cpuHistory
|
|
vertical: true
|
|
barFg: root.statColor
|
|
fontFamily: root.statFont
|
|
}
|
|
StatPill {
|
|
glyph: ""
|
|
percent: root.memPercent
|
|
history: root.memHistory
|
|
vertical: true
|
|
barFg: root.statColor
|
|
fontFamily: root.statFont
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
|
|
onClicked: function(mouse) {
|
|
if (mouse.button === Qt.RightButton) root.bar.run("alacritty")
|
|
else root.popupOpen = !root.popupOpen
|
|
}
|
|
onEntered: if (root.bar) root.bar.showTooltip(root, "CPU " + Math.round(root.cpuPercent) + "% · Mem " + Math.round(root.memPercent) + "% · Load " + root.loadAvg.toFixed(2))
|
|
onExited: if (root.bar) root.bar.hideTooltip(root)
|
|
}
|
|
|
|
Common.PopupCard {
|
|
anchorItem: root
|
|
owner: root
|
|
bar: root.bar
|
|
open: root.popupOpen
|
|
contentWidth: 320
|
|
contentHeight: detailColumn.implicitHeight + 28
|
|
|
|
Column {
|
|
id: detailColumn
|
|
anchors.fill: parent
|
|
spacing: 10
|
|
|
|
Text {
|
|
text: "System"
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: 12
|
|
font.bold: true
|
|
}
|
|
|
|
DetailStat {
|
|
title: "CPU"
|
|
value: Math.round(root.cpuPercent) + "%"
|
|
history: root.cpuHistory
|
|
barFg: root.statColor
|
|
fontFamily: root.bar.fontFamily
|
|
width: parent.width
|
|
}
|
|
|
|
DetailStat {
|
|
title: "Memory"
|
|
value: Math.round(root.memPercent) + "%"
|
|
history: root.memHistory
|
|
barFg: root.statColor
|
|
fontFamily: root.bar.fontFamily
|
|
width: parent.width
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: 6
|
|
Text {
|
|
text: "Load"
|
|
color: Qt.darker(root.bar.foreground, 1.5)
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: 11
|
|
}
|
|
Text {
|
|
text: root.loadAvg.toFixed(2)
|
|
color: root.bar.foreground
|
|
font.family: root.bar.fontFamily
|
|
font.pixelSize: 11
|
|
}
|
|
}
|
|
|
|
Common.PillButton {
|
|
width: parent.width
|
|
iconText: ""
|
|
text: "Open btop"
|
|
foreground: root.bar.foreground
|
|
horizontalPadding: 10
|
|
verticalPadding: 8
|
|
onClicked: { root.bar.run("omarchy-launch-or-focus-tui btop"); root.popupOpen = false }
|
|
}
|
|
}
|
|
}
|
|
|
|
component StatPill: Item {
|
|
id: pill
|
|
property string glyph: ""
|
|
property real percent: 0
|
|
property var history: []
|
|
property bool vertical: false
|
|
property color barFg: "#cacccc"
|
|
property string fontFamily: "JetBrainsMono Nerd Font"
|
|
|
|
implicitWidth: vertical ? 22 : 56
|
|
implicitHeight: vertical ? 32 : 22
|
|
|
|
Row {
|
|
visible: !pill.vertical
|
|
anchors.fill: parent
|
|
spacing: 4
|
|
|
|
Text {
|
|
text: pill.glyph
|
|
color: pill.barFg
|
|
font.family: pill.fontFamily
|
|
font.pixelSize: 12
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Canvas {
|
|
id: spark
|
|
width: 36
|
|
height: 14
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
property var history: pill.history
|
|
onHistoryChanged: requestPaint()
|
|
|
|
onPaint: {
|
|
var ctx = getContext("2d")
|
|
ctx.clearRect(0, 0, width, height)
|
|
if (!pill.history || pill.history.length === 0) return
|
|
|
|
ctx.strokeStyle = pill.barFg
|
|
ctx.fillStyle = Qt.rgba(pill.barFg.r, pill.barFg.g, pill.barFg.b, 0.2)
|
|
ctx.lineWidth = 1
|
|
|
|
ctx.beginPath()
|
|
var step = width / Math.max(1, pill.history.length - 1)
|
|
for (var i = 0; i < pill.history.length; i++) {
|
|
var x = i * step
|
|
var y = height - (pill.history[i] / 100) * height
|
|
if (i === 0) ctx.moveTo(x, y)
|
|
else ctx.lineTo(x, y)
|
|
}
|
|
ctx.stroke()
|
|
ctx.lineTo(width, height)
|
|
ctx.lineTo(0, height)
|
|
ctx.closePath()
|
|
ctx.fill()
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
visible: pill.vertical
|
|
anchors.fill: parent
|
|
spacing: 2
|
|
|
|
Text {
|
|
text: pill.glyph
|
|
color: pill.barFg
|
|
font.family: pill.fontFamily
|
|
font.pixelSize: 10
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
}
|
|
|
|
Text {
|
|
text: Math.round(pill.percent) + ""
|
|
color: pill.barFg
|
|
font.family: pill.fontFamily
|
|
font.pixelSize: 9
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
}
|
|
}
|
|
}
|
|
|
|
component DetailStat: Column {
|
|
id: detail
|
|
|
|
property string title: ""
|
|
property string value: ""
|
|
property var history: []
|
|
property color barFg: "#cacccc"
|
|
property string fontFamily: "JetBrainsMono Nerd Font"
|
|
|
|
spacing: 4
|
|
|
|
Row {
|
|
width: parent.width
|
|
Text {
|
|
text: detail.title
|
|
color: Qt.darker(detail.barFg, 1.4)
|
|
font.family: detail.fontFamily
|
|
font.pixelSize: 11
|
|
}
|
|
Item { width: detail.width - parent.children[0].implicitWidth - parent.children[2].implicitWidth; height: 1 }
|
|
Text {
|
|
text: detail.value
|
|
color: detail.barFg
|
|
font.family: detail.fontFamily
|
|
font.pixelSize: 11
|
|
}
|
|
}
|
|
|
|
Canvas {
|
|
id: detailCanvas
|
|
width: parent.width
|
|
height: 40
|
|
property var history: detail.history
|
|
onHistoryChanged: requestPaint()
|
|
|
|
onPaint: {
|
|
var ctx = getContext("2d")
|
|
ctx.clearRect(0, 0, width, height)
|
|
if (!detail.history || detail.history.length === 0) return
|
|
|
|
ctx.strokeStyle = detail.barFg
|
|
ctx.fillStyle = Qt.rgba(detail.barFg.r, detail.barFg.g, detail.barFg.b, 0.25)
|
|
ctx.lineWidth = 1.5
|
|
|
|
ctx.beginPath()
|
|
var step = width / Math.max(1, detail.history.length - 1)
|
|
for (var i = 0; i < detail.history.length; i++) {
|
|
var x = i * step
|
|
var y = height - (detail.history[i] / 100) * (height - 2) - 1
|
|
if (i === 0) ctx.moveTo(x, y)
|
|
else ctx.lineTo(x, y)
|
|
}
|
|
ctx.stroke()
|
|
ctx.lineTo(width, height)
|
|
ctx.lineTo(0, height)
|
|
ctx.closePath()
|
|
ctx.fill()
|
|
}
|
|
}
|
|
}
|
|
}
|