diff options
Diffstat (limited to 'desktop')
| -rw-r--r-- | desktop/assistant/Assistant.qml | 8 | ||||
| -rw-r--r-- | desktop/assistant/Card.qml | 3 | ||||
| -rw-r--r-- | desktop/companion/Companion.qml | 54 | ||||
| -rw-r--r-- | desktop/companion/Figure.qml | 15 | ||||
| -rw-r--r-- | desktop/companion/Mood.js | 9 | ||||
| -rw-r--r-- | desktop/companion/README.md | 7 | ||||
| -rw-r--r-- | desktop/companion/frames.txt | 35 | ||||
| -rw-r--r-- | desktop/companion/test_mood.js | 15 | ||||
| -rw-r--r-- | desktop/modules/perf/Perf.js | 19 | ||||
| -rw-r--r-- | desktop/modules/perf/PerfPanel.qml | 93 | ||||
| -rw-r--r-- | desktop/modules/perf/README.md | 7 | ||||
| -rw-r--r-- | desktop/modules/perf/test_perf.js | 17 |
12 files changed, 256 insertions, 26 deletions
diff --git a/desktop/assistant/Assistant.qml b/desktop/assistant/Assistant.qml index 0b7026d..4144910 100644 --- a/desktop/assistant/Assistant.qml +++ b/desktop/assistant/Assistant.qml @@ -112,6 +112,12 @@ Scope { readonly property var screenObj: Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0] + // The tall card (a chat, or a panel) is fullHeight and centred, so its + // top only moves on entering or leaving tall. The companion sits there. + readonly property bool tall: root.mode === "chat" || root.historyOpen || root.pickerOpen || root.modelsOpen + readonly property int fullHeight: Math.round(root.screenObj.height * 0.7) + readonly property int cardTop: Math.round((root.screenObj.height - root.fullHeight) / 2) + function toggle() { if (root.open) root.close(); else root.show(); } // A conversation idle for 10 minutes is over: opening starts a new one, @@ -552,7 +558,7 @@ Scope { Card { anchors.centerIn: parent width: Math.min(760, parent.width - 48) - fullHeight: Math.round(parent.height * 0.7) + fullHeight: root.fullHeight canvas: root turnModel: turns } diff --git a/desktop/assistant/Card.qml b/desktop/assistant/Card.qml index 810920f..437f0ad 100644 --- a/desktop/assistant/Card.qml +++ b/desktop/assistant/Card.qml @@ -28,8 +28,7 @@ Rectangle { property real fullHeight: 600 // the chat state's height, set by the window readonly property string mode: card.canvas.mode - readonly property bool panel: card.canvas.historyOpen || card.canvas.pickerOpen || card.canvas.modelsOpen - readonly property bool tall: card.mode === "chat" || card.panel + readonly property bool tall: card.canvas.tall // Bottom-up: the input, then the actions (file state), then the chips. height: card.tall ? card.fullHeight diff --git a/desktop/companion/Companion.qml b/desktop/companion/Companion.qml index e2d91a9..dfdc474 100644 --- a/desktop/companion/Companion.qml +++ b/desktop/companion/Companion.qml @@ -63,24 +63,44 @@ PanelWindow { } } - // The monitor she is on trails the focused one: a change plays her exit - // here first, and only then moves the window (settled, below). + // Where she goes: the focused monitor's corner, or seated on the + // assistant's tall card. here and sitting are where the window is now + // and trail the goal: a change plays her exit first, and only then + // moves the window (settled, below). readonly property string focused: Hyprland.focusedMonitor?.name ?? "" + readonly property bool seated: root.asst.open && root.asst.tall + readonly property string goal: root.seated ? root.asst.monitor : root.focused property string here: "" + property bool sitting: false property bool hopping: false // No focused monitor (outputs gone over a suspend) is not a destination. // Already out of view, there is no exit to wait for: settled would never // fire, hopping would stick and hide her for good, so move at once. - onFocusedChanged: { - if (root.focused === "" || root.focused === root.here) return; - if (root.here === "" || root.away) root.here = root.focused; - else root.hopping = true; + // Mid-hop, settled takes the latest goal; moving now would jump the + // window before the exit has played (hopping counts as away). + function move() { + if (root.hopping || root.goal === "" || (root.goal === root.here && root.seated === root.sitting)) return; + if (root.here === "" || root.away) { + root.here = root.goal; + root.sitting = root.seated; + } else { + root.hopping = true; + } } - Component.onCompleted: root.here = root.focused + onGoalChanged: root.move() + onSeatedChanged: root.move() + Component.onCompleted: root.move() screen: Quickshell.screens.find(s => s.name === root.here) ?? Quickshell.screens[0] anchors { right: true; bottom: true } - margins { right: 24; bottom: 16 } + // Seated, her right edge is 90 px right of the screen's centre, which + // puts her in the gap in the card's header, and her feet 35 px below the + // card's top. The right edge rather than her centre, because frames + // differ in width and the window would shift with every frame. + margins { + right: root.sitting ? Math.round(root.asst.screenObj.width / 2 - 90) : 24 + bottom: root.sitting ? root.asst.screenObj.height - root.asst.cardTop - 35 : 16 + } implicitWidth: 760 implicitHeight: 260 color: "transparent" @@ -268,7 +288,7 @@ PanelWindow { interval: 100 onTriggered: { const t = Date.now(); - if (root.away || root.here !== root.focused || !Mood.admireAllowed(root.admireAt, t)) return; + if (root.away || root.sitting || root.here !== root.focused || !Mood.admireAllowed(root.admireAt, t)) return; if ((Hyprland.focusedWorkspace?.toplevels.values.length ?? 1) !== 0) return; if (!Admired.listed(Admired.on(root.here))) return; root.admireAt = t; @@ -291,8 +311,10 @@ PanelWindow { Timer { id: remap; interval: 400; onTriggered: root.remapping = false } // Balloons stack bottom-right of their screen. When that is her corner, - // any balloon on screen shoves her left past the stack. + // any balloon on screen shoves her left past the stack. Seated on the + // assistant's card she is nowhere near them. readonly property bool shoved: { + if (root.sitting) return false; const place = Notify.config.balloons; if (place.screen !== root.here || place.corner !== "bottom-right") return false; return (Notify.queue || []).some(n => NC.shown(Notify.config, n, Notify.dnd, @@ -440,7 +462,7 @@ PanelWindow { // Frames Mood names that are really a loop: the frame names its cycle // and how fast it turns. Everything else shows as named. readonly property var cycles: ({ - dj: { frames: ["dj", "dj-scratch"], ms: 450 }, + dj: { frames: ["dj", "dj-scratch"], ms: 300 }, headbang: { frames: ["headbang-up", "headbang-down"], ms: 220 }, }) property int cycleIndex: 0 @@ -452,8 +474,10 @@ PanelWindow { running: root.cycle !== null && !root.away onTriggered: root.cycleIndex = (root.cycleIndex + 1) % root.cycle.frames.length } - readonly property string frame: root.leaving ? root.waveCycle[root.waveIndex] + readonly property string standing: root.leaving ? root.waveCycle[root.waveIndex] : root.cycle ? root.cycle.frames[root.cycleIndex % root.cycle.frames.length] : root.mood.frame + // Seated, the sitting version of it (Mood.seatFrame). + readonly property string frame: root.sitting ? Mood.seatFrame(root.standing) : root.standing // qs -p <desktop> ipc call companion toggle -> SUPER+\ IpcHandler { @@ -463,10 +487,12 @@ PanelWindow { function hide() { Prefs.setCompanion(false); } } - // The exit has played on the old monitor: move, and the entrance plays. + // The exit has played: move, and the entrance plays. The goal may have + // changed during the exit; the latest one wins. function settled(out) { if (out && root.hopping) { - root.here = root.focused; + root.here = root.goal; + root.sitting = root.seated; root.hopping = false; } } diff --git a/desktop/companion/Figure.qml b/desktop/companion/Figure.qml index df21bf5..285e2cb 100644 --- a/desktop/companion/Figure.qml +++ b/desktop/companion/Figure.qml @@ -44,6 +44,13 @@ Item { onSourceChanged: s.load(s.source) Component.onCompleted: s.load(s.source) + // The incoming frame fades in on top while the outgoing one stays + // opaque under it. Fading both at once left each near half opacity + // mid-swap, and she blinked see-through on every turn of the dj loop. + property real fade: 1 + NumberAnimation { id: fadeIn; target: s; property: "fade"; from: 0; to: 1; duration: 120 } + onFlipChanged: fadeIn.restart() + function ready(img) { if (img.status === Image.Error && String(img.source) !== s.fallback) { img.source = s.fallback; @@ -101,8 +108,8 @@ Item { width: implicitWidth / 2; height: implicitHeight / 2 anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter } smooth: true; mipmap: true; asynchronous: true - opacity: s.flip ? 0 : 1 - Behavior on opacity { NumberAnimation { duration: 120 } } + z: s.front === a ? 1 : 0 + opacity: s.front === a ? s.fade : s.fade < 1 ? 1 : 0 onStatusChanged: s.ready(a) } Image { @@ -110,8 +117,8 @@ Item { width: implicitWidth / 2; height: implicitHeight / 2 anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter } smooth: true; mipmap: true; asynchronous: true - opacity: s.flip ? 1 : 0 - Behavior on opacity { NumberAnimation { duration: 120 } } + z: s.front === b ? 1 : 0 + opacity: s.front === b ? s.fade : s.fade < 1 ? 1 : 0 onStatusChanged: s.ready(b) } } diff --git a/desktop/companion/Mood.js b/desktop/companion/Mood.js index b59c1ab..be514d7 100644 --- a/desktop/companion/Mood.js +++ b/desktop/companion/Mood.js @@ -105,6 +105,15 @@ function admireAllowed(lastAt, now) { return now - lastAt >= T.admireCooldown; } function canSpeak(lastLineAt, now) { return now - lastLineAt >= T.lineCooldown; } +// Seated on the assistant's tall card (Companion.qml, sitting): the frames +// with a sitting version show it, everything else the plain sit. Typing's +// watch becomes the lean over the card, reading. +var SEAT = { + watch: "sit-read", point: "sit-point", smirk: "sit-smirk", think: "sit-think", + listen: "sit-listen", wave: "sit-wave", "wave-l": "sit-wave", "wave-r": "sit-wave", +}; +function seatFrame(frame) { return SEAT[frame] || "sit"; } + // Whether the opening monologue plays: a chance per trigger, and never // twice within the hour. roll is Math.random(), passed in for the test. function monologue(trigger, lastAt, now, roll) { diff --git a/desktop/companion/README.md b/desktop/companion/README.md index 4fbe887..2bd6c4e 100644 --- a/desktop/companion/README.md +++ b/desktop/companion/README.md @@ -60,6 +60,13 @@ frame falls back to its outfit's `base`. stacks a later surface above an earlier one on a layer, and 0.56.2 has no layer rule to order them (no `order` key in the binary; an unknown key raises no config error). +- The assistant's tall card (a chat, or its history, picker or models + panel) seats her on its top edge, right of centre, through the monitor + hop. `Assistant.qml` gives `tall` and `cardTop`; seated she ignores + balloons and shows `Mood.seatFrame` of her frame, the `sit-*` set. The + bar and the file card leave her in the corner: standing frames face + out, not down at the text. Typing and BackSpace still reach her under + the canvas's exclusive keyboard (checked on socket2). - The program in front of her is Hyprland's `activewindow` event, `class,title`. It also fires on every title change, so she reacts only when the class changes, a third of the time, under the 30 s line diff --git a/desktop/companion/frames.txt b/desktop/companion/frames.txt index 087089d..0553eb5 100644 --- a/desktop/companion/frames.txt +++ b/desktop/companion/frames.txt @@ -19,9 +19,11 @@ # after it. ${NAME} expands a NAME = ... line defined above it. Blank lines # and lines starting with # are skipped. # -# dress/base is the one frame not made here: a klein edit of base-sitting.png, -# recorded in recipe.txt. Every other dress frame is made from it, and every -# leotard frame from its own dress frame, so the pose carries over. +# Two frames are not made here, both recorded in recipe.txt: dress/base, a +# klein edit of base-sitting.png, and dress/sit-read, a masked inpaint of +# dress/sit. The other standing dress frames are made from base, the seated +# ones from sit, and every leotard frame from its own dress frame, so the +# pose carries over. KEEP = She keeps her black gloves on both hands, her puffy black juliet sleeves, her black dress with its skirt, her black thighhigh boots, her black blindfold and hairband, her lips and the small mole under her mouth. Same character, chibi proportions, art style and white background. POSE = keep her pose exactly, standing, arms relaxed at her sides. @@ -37,6 +39,11 @@ LEOTARD = Her skirt is gone, revealing a plain white leotard underneath, and her # Singing along to whatever is playing: headphones on, eyes shut. The # blindfold is gone, but BARE does not fit because it says the eyes show. SING = Her black blindfold is gone: nothing over her eyes, no blindfold, no mask; her black hairband stays. Both her eyes are closed, softly shut. Her mouth is open wide as if she is singing loudly. She wears a pair of black over-ear headphones over her head and ears. A couple of small black musical notes float in the air beside her head. +# Seated on the assistant's card: the original sitting rest pose, legs +# dangling over the edge, from base-sitting (the image dress/base came from). +# Naming a ledge drew one, a grey bar across the whole image: she sits on +# nothing, and the card's edge in the shell is the ledge. +SIT = keep her sitting pose exactly: sitting, front view, legs dangling down, as if perched on an invisible edge. Nothing is drawn under or behind her: no ledge, no bar, no bench, no wall, no floor, no shadow; the background is plain white all around her. # --- expressions dress/smile <- dress/base : Change only her expression to a small warm closed-mouth smile; ${POSE} ${EYES} ${KEEP} @@ -87,6 +94,19 @@ dress/headbang-up <- dress/base : She rocks out: her upper body leans far back dress/headbang-down <- dress/base : She headbangs hard: seen from the front, her whole upper body is bent forward at the waist by about 45 degrees, her head thrown down so the top of her head faces the viewer, her white hair flung forward and hanging down over her face, both fists clenched in front of her chest, knees slightly bent. Her face is hidden by her hair. Her black blindfold is gone: nothing over her eyes, no blindfold, no mask, no band; her black hairband stays. ${KEEP_BARE} dress/airguitar <- dress/base : She plays an electric guitar with a glowing neon blue body and a black neck, held across her hips: one hand strumming at the body, the other fretting the neck to the side, knees bent, eyes shut, mouth open in a big grin. Her black blindfold is gone: nothing over her eyes, no blindfold, no mask, no band; her black hairband stays. ${KEEP_BARE} +# --- seated, on the assistant's chat card (Mood.seatFrame) +dress/sit <- base-sitting : ${SIT} Both gloved hands resting palms down beside her hips, a calm closed mouth. ${EYES} ${KEEP} +# dress/sit-read is hand made, like dress/base: no edit would bend her +# forward, so it is a masked inpaint of dress/sit, seed 13, in recipe.txt. +# No ${SIT} on the posed frames: its "keep her sitting pose exactly" kept +# her arms down, and sit-point came out as sit with a smirk. Seed 42 then +# left her old hand on her hip as well; the kept result is --seed 7. +dress/sit-point <- dress/sit : She raises the hand on the left side of the image and points down past her knees with her index finger, at something below her feet, with a teasing smirk; her other hand stays palms down beside her hip. She keeps sitting with her legs dangling; nothing is drawn under her. ${KEEP} +dress/sit-smirk <- dress/sit : Change only her expression to a teasing one-sided smirk. ${SIT} Both eyes fully visible and uncovered; the blindfold is nowhere on her head or face. ${BARE} ${KEEP_BARE} +dress/sit-think <- dress/sit : One gloved hand at her chin, thinking, head tilted slightly; her other hand palms down beside her hip. She keeps sitting with her legs dangling; nothing is drawn under her. ${KEEP} +dress/sit-listen <- dress/sit : She cups the hand on the left side of the image behind her ear, listening attentively; her other hand palms down beside her hip. She keeps sitting with her legs dangling; nothing is drawn under her. ${KEEP} +dress/sit-wave <- dress/sit : Raise only the hand on the left side of the image beside her head, waving, with a small smile; her other hand stays palms down beside her hip. She keeps sitting with her legs dangling; nothing is drawn under her. ${KEEP} + # --- the leotard set: the same frames, after the self-destruct leotard/base <- dress/base : ${LEOTARD} leotard/smile <- dress/smile : ${LEOTARD} @@ -125,3 +145,12 @@ leotard/whistle <- dress/whistle : ${LEOTARD} leotard/headbang-up <- dress/headbang-up : ${LEOTARD} leotard/headbang-down <- dress/headbang-down : Her skirt is gone, revealing a plain white leotard underneath. Change nothing else: her head stays lowered with her white hair hanging down over her whole face, hiding it completely, no eyes showing, no blindfold; her fists stay clenched in front of her chest; she keeps her black gloves, puffy black juliet sleeves, black bodice, black thighhigh boots, black hairband and pose. Same character, chibi proportions, art style and white background. leotard/airguitar <- dress/airguitar : ${LEOTARD} +leotard/sit <- dress/sit : ${LEOTARD} +# ${LEOTARD} uncovers her eyes and they looked at the viewer, losing the +# read; standalone, as leotard/headbang-down, with the gaze named. +leotard/sit-read <- dress/sit-read : Her skirt is gone, revealing a plain white leotard underneath, and her blindfold is gone: both eyes uncovered, pale blue-grey, lowered and looking down at something just below her feet, as if reading it; her eyelids half lowered, not looking at the viewer. Change nothing else: her hunched pose with her head sunk low, black gloves palms down beside her hips, puffy black juliet sleeves, black bodice, black thighhigh boots, black hairband, lips, the small mole under her mouth, chibi proportions, art style and white background. +leotard/sit-point <- dress/sit-point : ${LEOTARD} +leotard/sit-smirk <- dress/sit-smirk : ${LEOTARD} +leotard/sit-think <- dress/sit-think : ${LEOTARD} +leotard/sit-listen <- dress/sit-listen : ${LEOTARD} +leotard/sit-wave <- dress/sit-wave : ${LEOTARD} diff --git a/desktop/companion/test_mood.js b/desktop/companion/test_mood.js index 3439f7c..3dd3dba 100644 --- a/desktop/companion/test_mood.js +++ b/desktop/companion/test_mood.js @@ -19,7 +19,7 @@ const assert = require("assert"); const src = fs.readFileSync(__dirname + "/Mood.js", "utf8") .replace(/^\.pragma library$/m, ""); const M = new Function(src + - "\nreturn { T, click, clickFrame, pick, outfit, mockAllowed, canSpeak, monologue, admireAllowed };")(); + "\nreturn { T, click, clickFrame, pick, outfit, mockAllowed, canSpeak, monologue, admireAllowed, seatFrame };")(); const lsrc = fs.readFileSync(__dirname + "/ListFile.js", "utf8") .replace(/^\.pragma library$/m, ""); const L = new Function(lsrc + "\nreturn { parse, toggle, outputs };")(); @@ -175,4 +175,17 @@ assert.deepStrictEqual(L.outputs("# x\nOUTPUT_H=DP-1\nOUTPUT_V=DP-3\nDEFAULT_H=~ { "DP-1": "H", "DP-3": "V" }); assert.deepStrictEqual(L.outputs(""), {}); +// Seated on the assistant's card: each standing frame to its sitting one, +// the goodbye's sway to the one wave, and anything else to the plain sit. +assert.strictEqual(M.seatFrame("watch"), "sit-read"); +assert.strictEqual(M.seatFrame("point"), "sit-point"); +assert.strictEqual(M.seatFrame("smirk"), "sit-smirk"); +assert.strictEqual(M.seatFrame("think"), "sit-think"); +assert.strictEqual(M.seatFrame("listen"), "sit-listen"); +assert.strictEqual(M.seatFrame("wave"), "sit-wave"); +assert.strictEqual(M.seatFrame("wave-l"), "sit-wave"); +assert.strictEqual(M.seatFrame("wave-r"), "sit-wave"); +assert.strictEqual(M.seatFrame("dj-scratch"), "sit"); +assert.strictEqual(M.seatFrame("base"), "sit"); + console.log("ok"); diff --git a/desktop/modules/perf/Perf.js b/desktop/modules/perf/Perf.js index d84c694..8294e94 100644 --- a/desktop/modules/perf/Perf.js +++ b/desktop/modules/perf/Perf.js @@ -188,6 +188,25 @@ function push(list, v, n) { return out.length > n ? out.slice(out.length - n) : out; } +// The process rows of one `top -b` frame, run under LC_ALL=C: [{pid, name, +// cpu, mem}], cpu in percent of one core as top shows it, mem the resident +// size in bytes. RES is KiB unless top scaled it to a suffixed unit (1.0g), +// and COMMAND is the last column and may hold spaces ("Isolated Web Co"). +function procs(text) { + const out = []; + let rows = false; + for (const line of text.split("\n")) { + if (/^\s*PID\s+USER\b/.test(line)) { rows = true; continue; } + if (!rows) continue; + const f = line.trim().split(/\s+/); + const res = /^([\d.]+)([kmgtp]?)$/.exec(f[5] || ""); + if (f.length < 12 || !res) continue; + const scale = 1024 ** (1 + Math.max(0, "kmgtp".indexOf(res[2] || "k"))); + out.push({ pid: Number(f[0]), name: f.slice(11).join(" "), cpu: Number(f[8]), mem: Number(res[1]) * scale }); + } + return out; +} + function bytes(n) { if (n === null || n === undefined) return "--"; const u = ["B", "K", "M", "G", "T"]; diff --git a/desktop/modules/perf/PerfPanel.qml b/desktop/modules/perf/PerfPanel.qml index aa2e49f..ff9f74b 100644 --- a/desktop/modules/perf/PerfPanel.qml +++ b/desktop/modules/perf/PerfPanel.qml @@ -131,6 +131,39 @@ Column { // --- CPU ---------------------------------------------------------------- + // The five heaviest processes, from one `top -b` streaming a frame every + // 2s for as long as the page is open; it dies with the panel. top marks + // no frame's end but writes each in one burst, so a frame is published + // once its output goes quiet. The first is skipped, since top computes + // it over each process's whole life rather than the last interval, so + // rows load about 2s after the page opens. + property var procs: [] + property string procBy: "cpu" + property string frame: "" + property int frames: 0 + + Process { + running: true + command: ["env", "LC_ALL=C", "top", "-b", "-d2", "-w512"] + stdout: SplitParser { + onRead: line => { + // Every header after the first reaches here with the blank + // line before it still attached, as "\ntop - ...". + if (/^\s*top - /.test(line)) { + panel.frames++; + panel.frame = ""; + } + panel.frame += line + "\n"; + frameEnd.restart(); + } + } + } + Timer { + id: frameEnd + interval: 150 + onTriggered: if (panel.frames >= 2) panel.procs = P.procs(panel.frame) + } + Card { title: "CPU" headline: Perf.load === null ? "--" : Math.round(Perf.load) + "%" @@ -174,6 +207,66 @@ Column { frac: Perf.mem ? Perf.mem.used / Perf.mem.total : 0 fill: panel.tone(panel.pct(Perf.mem ? Perf.mem.used : null, Perf.mem ? Perf.mem.total : 0), 25, 75) } + + // Sorted by CPU or RAM, the active one lit; switching re-sorts the + // frame already held rather than waiting for the next. + Item { + width: parent.width + height: byRow.implicitHeight + Label { text: "Processes"; color: Theme.subtext } + Row { + id: byRow + anchors.right: parent.right + spacing: 10 + Repeater { + model: [{ key: "cpu", text: "CPU" }, { key: "mem", text: "RAM" }] + delegate: Label { + required property var modelData + text: modelData.text + font.bold: panel.procBy === modelData.key + color: panel.procBy === modelData.key ? Theme.text : Theme.overlay + MouseArea { + anchors.fill: parent + anchors.margins: -4 + cursorShape: Qt.PointingHandCursor + onClicked: panel.procBy = modelData.key + } + } + } + } + } + Repeater { + model: panel.procs.slice().sort((a, b) => b[panel.procBy] - a[panel.procBy]).slice(0, 5) + // Two right-aligned columns of fixed width, so the numbers line + // up down the table whatever their length. + delegate: Item { + required property var modelData + width: parent.width + implicitHeight: nameText.implicitHeight + Label { + id: nameText + anchors { left: parent.left; right: cpuText.left; rightMargin: 8 } + text: modelData.name + elide: Text.ElideRight + } + Label { + id: cpuText + anchors.right: memText.left + width: panel.fs * 4 + horizontalAlignment: Text.AlignRight + text: modelData.cpu.toFixed(1) + "%" + font.features: { "tnum": 1 } + } + Label { + id: memText + anchors.right: parent.right + width: panel.fs * 4.5 + horizontalAlignment: Text.AlignRight + text: P.bytes(modelData.mem) + font.features: { "tnum": 1 } + } + } + } } // --- temperatures ------------------------------------------------------- diff --git a/desktop/modules/perf/README.md b/desktop/modules/perf/README.md index 236f5b4..8282a50 100644 --- a/desktop/modules/perf/README.md +++ b/desktop/modules/perf/README.md @@ -31,6 +31,13 @@ VRAM (a scan of every fdinfo, ~14ms every 2s) and the fan, so the page opens on the last minute of network history instead of an empty graph. The mount list, `br0`, the GPU name and the 12 GiB VRAM size are constants in `Perf.qml`. +The CPU card ends with the five heaviest processes, by CPU or by RAM (a +toggle in the card, not saved). They come from one `top -b -d2` that runs only +while the page is open. A frame is published once top's output goes quiet, +and the first is skipped because top computes it over each process's whole +life, so rows appear about 2s after opening. CPU is per core, as top shows it, so a +busy process can exceed 100%. + The cache card runs `du -B1 -d1` once each time the page opens, 28ms over 2 GiB, instead of reading conky's 15-minute `cachesize.txt`. The largest child is yellow, red when it holds over half the total, as conky had it. diff --git a/desktop/modules/perf/test_perf.js b/desktop/modules/perf/test_perf.js index ac5859d..f2caa94 100644 --- a/desktop/modules/perf/test_perf.js +++ b/desktop/modules/perf/test_perf.js @@ -20,7 +20,7 @@ const src = fs.readFileSync(__dirname + "/Perf.js", "utf8") .replace(/^\.pragma library$/m, ""); const P = new Function(src + "\nreturn { cpuTimes, load, meminfo, df, vram, rate, " + "hwmonMap, lanAddr, cpuModel, du, band, worst, isIPv4, parsePubip, formatPubip, " + - "ipCheck, due, push, bytes, speed };")(); + "ipCheck, due, push, bytes, speed, procs };")(); // cpuTimes: aggregate first, then cores; idle is idle + iowait. const stat = [ @@ -156,4 +156,19 @@ assert.strictEqual(cache.total, 20); assert.deepStrictEqual(cache.items, [{ name: "a b", bytes: 9 }, { name: "b", bytes: 5 }]); assert.strictEqual(P.du(""), null); +// procs: rows after the header only, RES suffixes scaled, spaced names kept. +const frame = [ + "top - 09:50:01 up 34 min, 1 user, load average: 1.02, 0.80, 0.60", + "Tasks: 412 total, 1 running", + "", + " PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND", + " 8733 you 20 0 3824328 1.0g 140312 S 26.9 3.2 2:23.27 Isolated Web Co", + " 3772 you 20 0 1311072 191708 126420 S 5.0 0.6 2:07.86 Hyprland", +].join("\n"); +assert.deepStrictEqual(P.procs(frame), [ + { pid: 8733, name: "Isolated Web Co", cpu: 26.9, mem: 1024 ** 3 }, + { pid: 3772, name: "Hyprland", cpu: 5, mem: 191708 * 1024 }, +]); +assert.deepStrictEqual(P.procs(""), []); + console.log("test_perf: ok"); |
