Clicking the CPU pill opens a popup listing the highest-CPU processes, refreshed live while open. services/topproc.sh computes top-style per-process CPU% and mem% from two /proc snapshots and prints one ranked frame per run; SysStats streams it only while procPollEnabled (drawer open), so the bar pays nothing at rest. CLAUDE.md documents the architecture and the runtime constraints discovered here (helper scripts must be git-tracked for `nix run`; single-shot + exit to flush stdout; bash+coreutils only; StdioCollector/Timer don't fire in this Quickshell build; fixed-size popup to avoid stale-buffer on resize under Sway). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
246 lines
9.4 KiB
QML
246 lines
9.4 KiB
QML
import QtQuick
|
|
import QtQuick.Layouts
|
|
import Quickshell
|
|
import "../config"
|
|
import "../services"
|
|
|
|
// CPU usage as a live filled-area graph plus the current percentage.
|
|
// Clicking the pill toggles a drawer listing the top CPU-consuming processes,
|
|
// refreshed live (SysStats only scans /proc while the drawer is open).
|
|
Item {
|
|
id: root
|
|
|
|
implicitWidth: pill.implicitWidth
|
|
implicitHeight: pill.implicitHeight
|
|
|
|
Pill {
|
|
id: pill
|
|
anchors.fill: parent
|
|
spacing: Theme.spacing
|
|
|
|
Text {
|
|
text: Icons.cpu
|
|
font.family: Theme.monoFont
|
|
font.pixelSize: Theme.fontSize + 1
|
|
color: Theme.loadColor(SysStats.cpu)
|
|
Layout.alignment: Qt.AlignVCenter
|
|
}
|
|
|
|
// One vertical bar per core, filled from the bottom by that core's load.
|
|
Row {
|
|
id: cores
|
|
Layout.alignment: Qt.AlignVCenter
|
|
spacing: 1
|
|
readonly property int barH: Theme.barHeight - Theme.gap * 2 - 12
|
|
|
|
Repeater {
|
|
model: SysStats.coreCount
|
|
delegate: Rectangle {
|
|
readonly property real load: SysStats.coreLoads[index] || 0
|
|
width: 3
|
|
height: cores.barH
|
|
radius: 1
|
|
color: Qt.rgba(1, 1, 1, 0.08) // unfilled track
|
|
|
|
Rectangle {
|
|
anchors.bottom: parent.bottom
|
|
width: parent.width
|
|
radius: 1
|
|
height: Math.max(1, parent.height * (parent.load / 100))
|
|
color: Theme.loadColor(parent.load)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
text: SysStats.cpu.toFixed(0) + "%"
|
|
font.family: Theme.font
|
|
font.pixelSize: Theme.fontSize
|
|
color: Theme.text
|
|
Layout.alignment: Qt.AlignVCenter
|
|
Layout.preferredWidth: 42 // fits "100%" at fontSize 15
|
|
horizontalAlignment: Text.AlignRight
|
|
}
|
|
|
|
// Max current core frequency, shown in GHz.
|
|
Text {
|
|
text: (SysStats.cpuFreq / 1000).toFixed(1) + "GHz"
|
|
font.family: Theme.font
|
|
font.pixelSize: Theme.fontSize
|
|
color: Theme.subtext
|
|
Layout.alignment: Qt.AlignVCenter
|
|
Layout.preferredWidth: 56 // fits "0.0GHz" at fontSize 15
|
|
horizontalAlignment: Text.AlignRight
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: drawer.visible = !drawer.visible
|
|
}
|
|
|
|
// ---- top-processes drawer ---------------------------------------------
|
|
|
|
PopupWindow {
|
|
id: drawer
|
|
visible: false
|
|
color: "transparent"
|
|
|
|
implicitWidth: panel.implicitWidth
|
|
implicitHeight: panel.implicitHeight
|
|
|
|
// Hang below the pill, top-right corner aligned to the pill's
|
|
// bottom-right; Quickshell slides it to stay on-screen if it would
|
|
// overflow the right edge. Anchoring to the item (not the window) means
|
|
// the rect is in the pill's own coordinates, so it tracks the pill
|
|
// wherever the RowLayout places it.
|
|
anchor {
|
|
item: root
|
|
rect.x: 0
|
|
rect.y: root.height + Theme.gap
|
|
rect.width: root.width
|
|
rect.height: 1
|
|
edges: Edges.Bottom | Edges.Right
|
|
gravity: Edges.Bottom | Edges.Left
|
|
}
|
|
|
|
// Only pay for the /proc scan while the drawer is actually showing.
|
|
onVisibleChanged: SysStats.procPollEnabled = visible
|
|
|
|
Rectangle {
|
|
id: panel
|
|
readonly property int rowH: 22
|
|
implicitWidth: 320
|
|
// Fixed height (header + procCount rows): the panel must NOT resize
|
|
// when rows replace "sampling…", or the popup's layer surface shows a
|
|
// stale/blank buffer under Sway on first open (same class of bug as
|
|
// Bar.qml's geomKey remap). A constant size sidesteps it entirely.
|
|
implicitHeight: col.implicitHeight + Theme.padding * 2
|
|
radius: Theme.radius
|
|
color: Theme.barColor
|
|
border.color: Theme.surface1
|
|
border.width: 1
|
|
|
|
ColumnLayout {
|
|
id: col
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.margins: Theme.padding
|
|
spacing: 3
|
|
|
|
// header: title + overall CPU
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Theme.spacing
|
|
|
|
Text {
|
|
text: Icons.cpu
|
|
font.family: Theme.monoFont
|
|
font.pixelSize: Theme.fontSize + 1
|
|
color: Theme.loadColor(SysStats.cpu)
|
|
}
|
|
Text {
|
|
text: "Top processes"
|
|
font.family: Theme.font
|
|
font.pixelSize: Theme.fontSize
|
|
color: Theme.text
|
|
Layout.fillWidth: true
|
|
}
|
|
Text {
|
|
text: SysStats.cpu.toFixed(0) + "% total"
|
|
font.family: Theme.font
|
|
font.pixelSize: Theme.fontSize - 2
|
|
color: Theme.subtext
|
|
}
|
|
}
|
|
|
|
Rectangle { // divider
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: 2
|
|
Layout.bottomMargin: 2
|
|
implicitHeight: 1
|
|
color: Theme.surface1
|
|
}
|
|
|
|
// Fixed-height body so the panel stays one constant size whether
|
|
// it shows the placeholder or the rows (see panel.implicitHeight).
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: 2
|
|
implicitHeight: SysStats.procCount * panel.rowH
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
visible: SysStats.topProcs.length === 0
|
|
text: "sampling…"
|
|
font.family: Theme.font
|
|
font.pixelSize: Theme.fontSize - 2
|
|
color: Theme.subtext
|
|
}
|
|
|
|
Column {
|
|
anchors.fill: parent
|
|
|
|
Repeater {
|
|
model: SysStats.topProcs
|
|
|
|
delegate: Item {
|
|
id: rowItem
|
|
required property var modelData
|
|
width: parent.width
|
|
height: panel.rowH
|
|
|
|
// load bar behind the text; full width == one busy core.
|
|
Rectangle {
|
|
readonly property color loadColor: Theme.loadColor(rowItem.modelData.cpu)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.left: parent.left
|
|
height: parent.height - 4
|
|
radius: 3
|
|
width: parent.width * Math.min(1, rowItem.modelData.cpu / 100)
|
|
color: Qt.rgba(loadColor.r, loadColor.g, loadColor.b, 0.18)
|
|
}
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 6
|
|
anchors.rightMargin: 6
|
|
spacing: Theme.spacing
|
|
|
|
Text {
|
|
text: rowItem.modelData.name
|
|
font.family: Theme.font
|
|
font.pixelSize: Theme.fontSize - 1
|
|
color: Theme.text
|
|
elide: Text.ElideRight
|
|
Layout.fillWidth: true
|
|
}
|
|
Text {
|
|
text: rowItem.modelData.mem.toFixed(0) + "% mem"
|
|
font.family: Theme.font
|
|
font.pixelSize: Theme.fontSize - 3
|
|
color: Theme.subtext
|
|
horizontalAlignment: Text.AlignRight
|
|
Layout.preferredWidth: 54
|
|
}
|
|
Text {
|
|
text: rowItem.modelData.cpu.toFixed(0) + "%"
|
|
font.family: Theme.font
|
|
font.pixelSize: Theme.fontSize - 1
|
|
color: Theme.loadColor(rowItem.modelData.cpu)
|
|
horizontalAlignment: Text.AlignRight
|
|
Layout.preferredWidth: 40
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|