Add top-processes drawer to the CPU widget
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>
This commit is contained in:
@@ -1,66 +1,245 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "../config"
|
||||
import "../services"
|
||||
|
||||
// CPU usage as a live filled-area graph plus the current percentage.
|
||||
Pill {
|
||||
// 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
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: drawer.visible = !drawer.visible
|
||||
}
|
||||
|
||||
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
|
||||
// ---- top-processes drawer ---------------------------------------------
|
||||
|
||||
Rectangle {
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width
|
||||
radius: 1
|
||||
height: Math.max(1, parent.height * (parent.load / 100))
|
||||
color: Theme.loadColor(parent.load)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user