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:
45
CLAUDE.md
45
CLAUDE.md
@@ -49,10 +49,36 @@ modules (right). Changing module order/presence is purely editing that RowLayout
|
|||||||
reassigning its `path`, as `_parseNet` does for the active interface), writing
|
reassigning its `path`, as `_parseNet` does for the active interface), writing
|
||||||
a `_parseX()`, and calling `reload()` + `_parseX()` inside `_tickOnce()`.
|
a `_parseX()`, and calling `reload()` + `_parseX()` inside `_tickOnce()`.
|
||||||
- Deltas (CPU%, net rates) keep a `_prev*` field and diff against the last tick.
|
- Deltas (CPU%, net rates) keep a `_prev*` field and diff against the last tick.
|
||||||
- **Two metrics escape the pure-virtual-file rule:** disk usage runs `df` via a
|
- **Three things escape the pure-virtual-file rule:** disk usage runs `df` via a
|
||||||
`Process` only every 30 ticks (`_tick % 30`), and a one-shot `Process` runs
|
`Process` only every 30 ticks (`_tick % 30`); a one-shot `Process` runs
|
||||||
`services/discover.sh` at startup. The bar avoids per-tick subprocess spawns
|
`services/discover.sh` at startup; and the top-processes list (`topProcs`) is
|
||||||
by design — keep new metrics reading virtual files, not shelling out.
|
fed by `services/topproc.sh`, re-run for each frame only while `procPollEnabled`
|
||||||
|
(true while the CPU drawer is open) — at rest the bar still spawns nothing. The
|
||||||
|
bar avoids per-tick subprocess spawns by design — keep new always-on metrics
|
||||||
|
reading virtual files, not shelling out, and gate anything heavier behind an
|
||||||
|
opt-in flag like `procPollEnabled`.
|
||||||
|
- **`topproc.sh` does the work, QML just displays.** Each run takes two `/proc`
|
||||||
|
snapshots `procInterval` apart, computes top-style per-process CPU% and mem% in
|
||||||
|
the shell, prints one ranked frame of `P <cpu> <mem> <name>` lines, and
|
||||||
|
**exits**. `SysStats` accumulates stdout via a `SplitParser`, parses it in
|
||||||
|
`_parseProcFrame` on `onExited`, then re-arms (`Qt.callLater(running=true)`)
|
||||||
|
while the drawer stays open. Hard-won constraints — violate any one and you get
|
||||||
|
silent empty frames / a stuck "sampling…":
|
||||||
|
- **The helper script must be tracked by git.** `nix run` builds from the flake
|
||||||
|
snapshot (`${self}`), which excludes untracked files (`git status` will warn
|
||||||
|
"tree is dirty") — an untracked script simply isn't found at runtime. For
|
||||||
|
iterative work prefer `quickshell --path .`, which reads the working dir
|
||||||
|
(untracked files included) and hot-reloads.
|
||||||
|
- **Single-shot + exit, not a long-running loop.** A looping script's output
|
||||||
|
stuck in bash's block buffer (pipe stdout is block-buffered), so the QML side
|
||||||
|
never saw a frame; exiting per run forces a flush.
|
||||||
|
- **bash builtins + coreutils only** (`nproc`/`sort`/`head`/`sleep`/`cat`). The
|
||||||
|
flake wrapper's `runtimeInputs` is just `bash` + `coreutils`, so `awk` and
|
||||||
|
`getconf` are absent at runtime.
|
||||||
|
- **`StdioCollector.streamFinished` never fired** in this Quickshell build, and
|
||||||
|
a `Timer`-driven re-arm didn't repeat — but `Process.onStarted`/`onExited` and
|
||||||
|
`SplitParser.onRead` all work (`discover.sh` is the reference). Invoke helpers
|
||||||
|
as `["bash", Quickshell.shellPath("services/x.sh")]`.
|
||||||
|
|
||||||
**`discover.sh` handshake.** At startup it prints `TEMP <path>` and `BAT <path>`
|
**`discover.sh` handshake.** At startup it prints `TEMP <path>` and `BAT <path>`
|
||||||
(hwmon temp sensor + main battery, skipping `scope=Device` peripherals) on stdout;
|
(hwmon temp sensor + main battery, skipping `scope=Device` peripherals) on stdout;
|
||||||
@@ -78,3 +104,14 @@ the QML side parses those lines into `tempPath`/`batPath`, which then feed
|
|||||||
to pick up layer-surface fixes ahead of release.
|
to pick up layer-surface fixes ahead of release.
|
||||||
- Native Quickshell services back some modules: `Quickshell.I3` (workspaces),
|
- Native Quickshell services back some modules: `Quickshell.I3` (workspaces),
|
||||||
`Quickshell.Services.SystemTray` (tray), PipeWire (volume).
|
`Quickshell.Services.SystemTray` (tray), PipeWire (volume).
|
||||||
|
- **Making a module interactive (popups/drawers):** wrap the module's `Pill` in
|
||||||
|
an `Item` (forward `implicitWidth`/`implicitHeight` from the pill so the bar's
|
||||||
|
RowLayout still sizes it), add a `MouseArea`, and open a `Quickshell.PopupWindow`;
|
||||||
|
`CpuGraph.qml` is the reference for a full drawer. Anchor it with
|
||||||
|
`anchor.item: <the wrapper Item>` (**not** `anchor.window` — the module is
|
||||||
|
nested in `Bar.qml`'s RowLayout, so its coordinates aren't window coordinates,
|
||||||
|
and a window-relative rect lands in the screen corner). `anchor.item` makes the
|
||||||
|
rect item-relative and derives the window automatically; position the dropdown
|
||||||
|
with `anchor.edges`/`anchor.gravity` (`Edges.*`) and let Quickshell slide it
|
||||||
|
on-screen. Adding a `MouseArea` directly to a `Pill`'s default content would
|
||||||
|
place it in the layout — wrapping in an `Item` avoids that.
|
||||||
|
|||||||
@@ -28,6 +28,15 @@ Singleton {
|
|||||||
property var coreLoads: []
|
property var coreLoads: []
|
||||||
property int coreCount: 0
|
property int coreCount: 0
|
||||||
|
|
||||||
|
// --- top processes (only streamed while a consumer opts in) ---
|
||||||
|
// Each entry: { name, cpu (top-style %, 100 = one full core), mem (% of RAM) }.
|
||||||
|
// Set procPollEnabled true (e.g. when the CPU drawer opens) to start the
|
||||||
|
// streamer; false when nothing is watching, so the bar pays nothing at rest.
|
||||||
|
property var topProcs: []
|
||||||
|
readonly property int procCount: 6 // how many to show, highest CPU first
|
||||||
|
property bool procPollEnabled: false
|
||||||
|
readonly property int procInterval: 2 // seconds between frames
|
||||||
|
|
||||||
readonly property bool hasBattery: battery >= 0
|
readonly property bool hasBattery: battery >= 0
|
||||||
|
|
||||||
// poll interval in seconds
|
// poll interval in seconds
|
||||||
@@ -214,6 +223,25 @@ Singleton {
|
|||||||
root._prevIface = ifc;
|
root._prevIface = ifc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build topProcs from one run of topproc.sh: lines of
|
||||||
|
// "P <cpu%> <mem%> <name>". The script already did the delta math and
|
||||||
|
// ranking, so this just maps the lines to objects.
|
||||||
|
function _parseProcFrame(text) {
|
||||||
|
if (!text) return;
|
||||||
|
const lines = text.split("\n");
|
||||||
|
const out = [];
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const p = lines[i].split(" ");
|
||||||
|
if (p[0] !== "P" || p.length < 4) continue;
|
||||||
|
out.push({
|
||||||
|
cpu: Number(p[1]),
|
||||||
|
mem: Number(p[2]),
|
||||||
|
name: p.slice(3).join(" ")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
root.topProcs = out;
|
||||||
|
}
|
||||||
|
|
||||||
function _tickOnce() {
|
function _tickOnce() {
|
||||||
statView.reload();
|
statView.reload();
|
||||||
cpuInfoView.reload();
|
cpuInfoView.reload();
|
||||||
@@ -280,6 +308,41 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- top processes (opt-in, drawer-driven) ----------------------------
|
||||||
|
// services/topproc.sh prints one ranked frame and exits; we re-run it every
|
||||||
|
// ~procInterval (the Timer re-arms it once it exits, like dfProc) only while
|
||||||
|
// procPollEnabled (a drawer is open), so the bar pays nothing at rest. The
|
||||||
|
// script itself sleeps procInterval between its two /proc snapshots, so each
|
||||||
|
// run takes about that long; the Timer just relaunches promptly after.
|
||||||
|
|
||||||
|
property var _procFrame: [] // stdout lines accumulated for the current run
|
||||||
|
|
||||||
|
// topproc.sh prints one ranked frame and exits; accumulate its lines with a
|
||||||
|
// SplitParser (StdioCollector.streamFinished doesn't fire in this Quickshell
|
||||||
|
// build) and parse them on exit, then re-arm for the next frame while the
|
||||||
|
// drawer stays open. discover.sh is the reference for the SplitParser pattern.
|
||||||
|
Process {
|
||||||
|
id: procScan
|
||||||
|
command: ["bash", Quickshell.shellPath("services/topproc.sh"),
|
||||||
|
String(root.procInterval), String(root.procCount)]
|
||||||
|
onStarted: root._procFrame = [];
|
||||||
|
onExited: {
|
||||||
|
root._parseProcFrame(root._procFrame.join("\n"));
|
||||||
|
if (root.procPollEnabled) // re-arm for the next frame
|
||||||
|
Qt.callLater(function () { procScan.running = true; });
|
||||||
|
}
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: "\n"
|
||||||
|
onRead: line => { if (line) root._procFrame.push(line); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kick the loop when the drawer opens; clear rows when it closes.
|
||||||
|
onProcPollEnabledChanged: {
|
||||||
|
if (procPollEnabled) procScan.running = true;
|
||||||
|
else root.topProcs = [];
|
||||||
|
}
|
||||||
|
|
||||||
// Human-readable byte rate with kB as the smallest unit, e.g. "1.2M", "8K".
|
// Human-readable byte rate with kB as the smallest unit, e.g. "1.2M", "8K".
|
||||||
function fmtRate(bytes) {
|
function fmtRate(bytes) {
|
||||||
const u = ["K", "M", "G"];
|
const u = ["K", "M", "G"];
|
||||||
|
|||||||
79
services/topproc.sh
Executable file
79
services/topproc.sh
Executable file
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Print one frame of the top CPU-consuming processes, then exit.
|
||||||
|
#
|
||||||
|
# Single-shot by design: SysStats re-runs it every procInterval while the CPU
|
||||||
|
# drawer is open (Process.running re-armed by a Timer, like dfProc). Exiting
|
||||||
|
# per frame guarantees stdout is flushed — a long-running loop kept its frame
|
||||||
|
# delimiter stuck in bash's block buffer and the QML side never saw a full
|
||||||
|
# frame. discover.sh is the reference for the bash-script + parse pattern.
|
||||||
|
#
|
||||||
|
# IMPORTANT: the flake wrapper only puts bash + coreutils on PATH, so this uses
|
||||||
|
# ONLY bash builtins and coreutils (nproc, sort, head, sleep, cat) — no awk, no
|
||||||
|
# getconf. Keep it that way or add the tool to runtimeInputs in flake.nix.
|
||||||
|
#
|
||||||
|
# Output: one "P <cpu%> <mem%> <name>" line per process, highest CPU first.
|
||||||
|
# cpu% is top-style: 100 == one fully-used core.
|
||||||
|
|
||||||
|
intv="${1:-2}"
|
||||||
|
n="${2:-6}"
|
||||||
|
ncpu="$(nproc 2>/dev/null || echo 1)"
|
||||||
|
pg_kb=4 # page size is 4096B on all targeted arches (x86_64/aarch64)
|
||||||
|
|
||||||
|
# MemTotal in kB, read without awk.
|
||||||
|
memkb=0
|
||||||
|
while read -r key val _; do
|
||||||
|
if [ "$key" = "MemTotal:" ]; then memkb="$val"; break; fi
|
||||||
|
done < /proc/meminfo
|
||||||
|
|
||||||
|
# Aggregate CPU jiffies: sum of the fields on the leading "cpu" line.
|
||||||
|
cpu_total() {
|
||||||
|
local line f s=0
|
||||||
|
read -r line < /proc/stat
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
set -- $line # collapses the double space; $1 == "cpu"
|
||||||
|
shift
|
||||||
|
for f in "$@"; do s=$(( s + f )); done
|
||||||
|
echo "$s"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Read pid -> (utime+stime) jiffies and rss pages into the given assoc arrays.
|
||||||
|
# /proc/<pid>/stat is "PID (comm) STATE ...": strip through ") " so comm spaces
|
||||||
|
# and parens can't shift field positions, then utime=14, stime=15, rss=24.
|
||||||
|
snapshot() {
|
||||||
|
local -n _jif="$1" _rss="$2"
|
||||||
|
local f l pid rest
|
||||||
|
for f in /proc/[0-9]*/stat; do
|
||||||
|
# 2>/dev/null first so a process vanishing mid-scan is silent.
|
||||||
|
IFS= read -r l 2>/dev/null < "$f" || continue
|
||||||
|
pid="${l%% *}"
|
||||||
|
rest="${l#*) }"
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
set -- $rest
|
||||||
|
_jif["$pid"]=$(( ${12:-0} + ${13:-0} ))
|
||||||
|
_rss["$pid"]="${22:-0}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
declare -A j1 j2 rss
|
||||||
|
t1="$(cpu_total)"
|
||||||
|
snapshot j1 rss
|
||||||
|
sleep "$intv"
|
||||||
|
t2="$(cpu_total)"
|
||||||
|
snapshot j2 rss
|
||||||
|
dt=$(( t2 - t1 ))
|
||||||
|
[ "$dt" -gt 0 ] || exit 0
|
||||||
|
|
||||||
|
# Emit "<cpu> <pid> <rss>" for each process seen in both snapshots, sort by cpu
|
||||||
|
# desc, take the top n, then resolve name + mem% for just those.
|
||||||
|
for pid in "${!j2[@]}"; do
|
||||||
|
[ -n "${j1[$pid]:-}" ] || continue
|
||||||
|
d=$(( ${j2[$pid]} - ${j1[$pid]} ))
|
||||||
|
cpu=$(( 100 * ncpu * d / dt ))
|
||||||
|
printf '%d %s %s\n' "$cpu" "$pid" "${rss[$pid]:-0}"
|
||||||
|
done | sort -rn | head -n "$n" | while read -r cpu pid r; do
|
||||||
|
name="$(cat "/proc/$pid/comm" 2>/dev/null)"
|
||||||
|
[ -n "$name" ] || name="$pid"
|
||||||
|
mem=0
|
||||||
|
[ "$memkb" -gt 0 ] && mem=$(( r * pg_kb * 100 / memkb ))
|
||||||
|
printf 'P %s %s %s\n' "$cpu" "$mem" "$name"
|
||||||
|
done
|
||||||
@@ -1,66 +1,245 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
import "../config"
|
import "../config"
|
||||||
import "../services"
|
import "../services"
|
||||||
|
|
||||||
// CPU usage as a live filled-area graph plus the current percentage.
|
// 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
|
id: root
|
||||||
spacing: Theme.spacing
|
|
||||||
|
|
||||||
Text {
|
implicitWidth: pill.implicitWidth
|
||||||
text: Icons.cpu
|
implicitHeight: pill.implicitHeight
|
||||||
font.family: Theme.monoFont
|
|
||||||
font.pixelSize: Theme.fontSize + 1
|
Pill {
|
||||||
color: Theme.loadColor(SysStats.cpu)
|
id: pill
|
||||||
Layout.alignment: Qt.AlignVCenter
|
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.
|
MouseArea {
|
||||||
Row {
|
anchors.fill: parent
|
||||||
id: cores
|
cursorShape: Qt.PointingHandCursor
|
||||||
Layout.alignment: Qt.AlignVCenter
|
onClicked: drawer.visible = !drawer.visible
|
||||||
spacing: 1
|
}
|
||||||
readonly property int barH: Theme.barHeight - Theme.gap * 2 - 12
|
|
||||||
|
|
||||||
Repeater {
|
// ---- top-processes drawer ---------------------------------------------
|
||||||
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 {
|
PopupWindow {
|
||||||
anchors.bottom: parent.bottom
|
id: drawer
|
||||||
width: parent.width
|
visible: false
|
||||||
radius: 1
|
color: "transparent"
|
||||||
height: Math.max(1, parent.height * (parent.load / 100))
|
|
||||||
color: Theme.loadColor(parent.load)
|
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