refactor: component architecture, real CPU temp, perf limiter

- Modular architecture: main.qml orchestrator + CompactView, FullView,
  TemperatureDisplay, PerfLimiter components + CpuReader/CpuWriter logic
- Fix temperature: dynamic thermal zone detection (x86_pkg_temp/TCPU)
  instead of hardcoded thermal_zone0 (was showing 27°C instead of real)
- Add CPU % limiter with -5/+5 buttons via intel_pstate max_perf_pct
- Remove profile switching (already in Kubuntu by default)
- Add polkit policy + helper script for passwordless perf writes
- Add 67 unit tests for all backend logic
- Apple-inspired UI: clean, minimal, color-coded temperature

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Andrés Eduardo García Márquez 2026-02-24 11:19:31 -05:00
parent 7afb6f966e
commit 8d7cd3f65f
14 changed files with 651 additions and 147 deletions

View File

@ -1,23 +1,20 @@
# KDE Plasma CPU Control Widget # KDE Plasma CPU Control Widget
Widget de KDE Plasma 6 para monitorear y controlar el rendimiento del CPU Intel desde el panel. Widget de KDE Plasma 6 para monitorear temperatura del CPU y controlar el límite de rendimiento desde el panel.
## Características ## Características
- Temperatura del CPU en tiempo real en el panel - Temperatura real del CPU en tiempo real (detección dinámica de zona térmica)
- Porcentaje de rendimiento y estado del turbo boost - Limitador de CPU con botones -5% / +5% (rango 10%-100%)
- Cambio de perfil con un click: - Colores dinámicos por temperatura: verde, amarillo, naranja, rojo
- **Performance**: Máximo rendimiento (videollamadas, compilación) - Sin duplicar funcionalidad del sistema (no cambia perfiles de energía)
- **Balanced**: Equilibrio rendimiento/consumo (uso general) - Arquitectura modular por componentes
- **Power Saver**: Bajo consumo (batería, temperaturas altas)
- Usa `powerprofilesctl` (estándar del sistema, sin sudo)
- Colores indicadores de temperatura y estado
## Requisitos ## Requisitos
- KDE Plasma 6 - KDE Plasma 6
- CPU Intel con driver `intel_pstate` - CPU Intel con driver `intel_pstate`
- `power-profiles-daemon` (incluido por defecto en Kubuntu) - Node.js (para ejecutar tests)
## Instalación ## Instalación
@ -29,24 +26,45 @@ kquitapp6 plasmashell && kstart plasmashell
Click derecho en el panel > "Add Widgets..." > Buscar "CPU Control" Click derecho en el panel > "Add Widgets..." > Buscar "CPU Control"
## Desinstalación ## Tests
```bash ```bash
rm -rf ~/.local/share/plasma/plasmoids/org.kde.plasma.cpucontrol ./tests/run_tests.sh
kquitapp6 plasmashell && kstart plasmashell
``` ```
## Estructura ## Estructura
``` ```
cpu-control-plasmoid/ cpu-control-plasmoid/
├── contents/ui/
│ ├── main.qml # Orquestador
│ ├── CompactView.qml # Vista del panel
│ ├── FullView.qml # Vista expandida
│ ├── components/
│ │ ├── TemperatureDisplay.qml
│ │ └── PerfLimiter.qml
│ └── logic/
│ ├── CpuReader.js # Lectura de sensores
│ └── CpuWriter.js # Escritura a intel_pstate
├── system/
│ ├── cpu-perf-set # Helper script
│ └── *.policy # Polkit policy
├── tests/
│ ├── test_CpuReader.js
│ ├── test_CpuWriter.js
│ └── run_tests.sh
├── metadata.json ├── metadata.json
├── metadata.desktop ├── metadata.desktop
├── contents/ └── install.sh
│ └── ui/ ```
│ └── main.qml
├── install.sh ## Desinstalación
└── README.md
```bash
rm -rf ~/.local/share/plasma/plasmoids/org.kde.plasma.cpucontrol
sudo rm -f /usr/local/bin/cpu-perf-set
sudo rm -f /usr/share/polkit-1/actions/org.kde.plasma.cpucontrol.policy
kquitapp6 plasmashell && kstart plasmashell
``` ```
## Licencia ## Licencia

View File

@ -0,0 +1,30 @@
import QtQuick 2.15
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as PlasmaComponents
import "logic/CpuReader.js" as Reader
Row {
id: root
property int temperature: 0
signal clicked()
spacing: 4
PlasmaCore.IconItem {
source: "cpu"
width: 16; height: 16
anchors.verticalCenter: parent.verticalCenter
}
PlasmaComponents.Label {
text: root.temperature + "°C"
color: Reader.tempColor(root.temperature)
anchors.verticalCenter: parent.verticalCenter
}
MouseArea {
anchors.fill: parent
onClicked: root.clicked()
}
}

37
contents/ui/FullView.qml Normal file
View File

@ -0,0 +1,37 @@
import QtQuick 2.15
import org.kde.plasma.core 2.0 as PlasmaCore
import "components" as Components
Column {
id: root
property int temperature: 0
property int currentPerf: 100
signal perfChangeRequested(int newValue)
width: 200
spacing: 12
Item { width: 1; height: 4 }
Components.TemperatureDisplay {
temperature: root.temperature
anchors.horizontalCenter: parent.horizontalCenter
}
Rectangle {
width: parent.width - 32
height: 1
color: PlasmaCore.Theme.textColor
opacity: 0.1
anchors.horizontalCenter: parent.horizontalCenter
}
Components.PerfLimiter {
currentPerf: root.currentPerf
anchors.horizontalCenter: parent.horizontalCenter
onPerfChangeRequested: root.perfChangeRequested(newValue)
}
Item { width: 1; height: 4 }
}

View File

@ -0,0 +1,105 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as PlasmaComponents
import "../logic/CpuWriter.js" as Writer
Column {
id: root
property int currentPerf: 100
signal perfChangeRequested(int newValue)
spacing: 8
PlasmaComponents.Label {
text: "CPU Limit"
font.pointSize: 10
opacity: 0.6
anchors.horizontalCenter: parent.horizontalCenter
}
RowLayout {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 12
Rectangle {
width: 44; height: 44
radius: 22
color: Writer.canDecrease(root.currentPerf)
? Qt.rgba(PlasmaCore.Theme.textColor.r,
PlasmaCore.Theme.textColor.g,
PlasmaCore.Theme.textColor.b, 0.1)
: "transparent"
opacity: Writer.canDecrease(root.currentPerf) ? 1.0 : 0.3
PlasmaComponents.Label {
text: "5"
font.pointSize: 12
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
enabled: Writer.canDecrease(root.currentPerf)
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: root.perfChangeRequested(Writer.decrease(root.currentPerf))
}
}
Column {
spacing: 2
PlasmaComponents.Label {
text: root.currentPerf + "%"
font.pointSize: 22
font.weight: Font.DemiBold
anchors.horizontalCenter: parent.horizontalCenter
}
Rectangle {
width: 80; height: 4
radius: 2
color: Qt.rgba(PlasmaCore.Theme.textColor.r,
PlasmaCore.Theme.textColor.g,
PlasmaCore.Theme.textColor.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
Rectangle {
width: parent.width * root.currentPerf / 100
height: parent.height
radius: 2
color: root.currentPerf > 80 ? "#34c759"
: root.currentPerf > 50 ? "#ffcc00"
: "#ff9500"
}
}
}
Rectangle {
width: 44; height: 44
radius: 22
color: Writer.canIncrease(root.currentPerf)
? Qt.rgba(PlasmaCore.Theme.textColor.r,
PlasmaCore.Theme.textColor.g,
PlasmaCore.Theme.textColor.b, 0.1)
: "transparent"
opacity: Writer.canIncrease(root.currentPerf) ? 1.0 : 0.3
PlasmaComponents.Label {
text: "+5"
font.pointSize: 12
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
enabled: Writer.canIncrease(root.currentPerf)
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: root.perfChangeRequested(Writer.increase(root.currentPerf))
}
}
}
}

View File

@ -0,0 +1,25 @@
import QtQuick 2.15
import org.kde.plasma.components 2.0 as PlasmaComponents
import "../logic/CpuReader.js" as Reader
Column {
id: root
property int temperature: 0
spacing: 2
PlasmaComponents.Label {
text: root.temperature + "°C"
font.pointSize: 28
font.weight: Font.Light
color: Reader.tempColor(root.temperature)
anchors.horizontalCenter: parent.horizontalCenter
}
PlasmaComponents.Label {
text: Reader.tempLabel(root.temperature)
font.pointSize: 10
opacity: 0.6
anchors.horizontalCenter: parent.horizontalCenter
}
}

View File

@ -0,0 +1,55 @@
.pragma library
// Builds a shell command that finds the real CPU thermal zone dynamically
// and reads temp + intel_pstate values in a single call
function buildReadCommand() {
return "bash -c '"
+ "TEMP=0; "
+ "for z in /sys/class/thermal/thermal_zone*/type; do "
+ " t=$(cat \"$z\" 2>/dev/null); "
+ " if [ \"$t\" = \"x86_pkg_temp\" ] || [ \"$t\" = \"TCPU\" ]; then "
+ " TEMP=$(cat \"${z%type}temp\" 2>/dev/null); break; "
+ " fi; "
+ "done; "
+ "[ \"$TEMP\" -eq 0 ] 2>/dev/null && TEMP=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null); "
+ "PERF=$(cat /sys/devices/system/cpu/intel_pstate/max_perf_pct 2>/dev/null); "
+ "echo \"${TEMP}:${PERF}\"'"
}
// Parses the output of the read command into a structured object
// Input: "94000:85" → { tempRaw: 94000, tempC: 94, perf: 85, valid: true }
function parseOutput(stdout) {
var parts = (stdout || "").trim().split(":")
if (parts.length < 2) return { tempC: 0, perf: 100, valid: false }
var tempRaw = parseInt(parts[0]) || 0
var perf = parseInt(parts[1]) || 100
return {
tempRaw: tempRaw,
tempC: Math.round(tempRaw / 1000),
perf: clampPerf(perf),
valid: tempRaw > 0
}
}
// Returns a color hex based on temperature thresholds
function tempColor(tempC) {
if (tempC < 50) return "#34c759" // green - cool
if (tempC < 65) return "#ffcc00" // yellow - warm
if (tempC < 80) return "#ff9500" // orange - hot
return "#ff3b30" // red - critical
}
// Returns a human label for the temperature range
function tempLabel(tempC) {
if (tempC < 50) return "Cool"
if (tempC < 65) return "Normal"
if (tempC < 80) return "Warm"
return "Hot"
}
// Clamps performance value to valid range
function clampPerf(value) {
return Math.max(10, Math.min(100, value))
}

View File

@ -0,0 +1,41 @@
.pragma library
var MIN_PERF = 10
var MAX_PERF = 100
var STEP = 5
// Validates that a performance value is within allowed range
function isValidPerf(value) {
var n = parseInt(value)
return !isNaN(n) && n >= MIN_PERF && n <= MAX_PERF
}
// Builds the pkexec command to set max_perf_pct
// Returns null if value is invalid
function buildSetCommand(value) {
var n = parseInt(value)
if (!isValidPerf(n)) return null
return "pkexec /usr/local/bin/cpu-perf-set " + n
}
// Calculates the new value after increasing by STEP
function increase(current) {
var next = parseInt(current) + STEP
return Math.min(next, MAX_PERF)
}
// Calculates the new value after decreasing by STEP
function decrease(current) {
var next = parseInt(current) - STEP
return Math.max(next, MIN_PERF)
}
// Returns true if the value can be increased
function canIncrease(current) {
return parseInt(current) < MAX_PERF
}
// Returns true if the value can be decreased
function canDecrease(current) {
return parseInt(current) > MIN_PERF
}

View File

@ -1,16 +1,14 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Layouts 1.15
import org.kde.plasma.plasmoid 2.0 import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as PlasmaComponents import "logic/CpuReader.js" as Reader
import "logic/CpuWriter.js" as Writer
Item { Item {
id: root id: root
property string currentTemp: "..." property int temperature: 0
property string currentProfile: "..." property int currentPerf: 100
property string maxPerf: "..."
property string turboState: "..."
Plasmoid.preferredRepresentation: Plasmoid.compactRepresentation Plasmoid.preferredRepresentation: Plasmoid.compactRepresentation
@ -19,134 +17,44 @@ Item {
running: true running: true
repeat: true repeat: true
triggeredOnStart: true triggeredOnStart: true
onTriggered: readStatus.connectSource(readStatus.cmd) onTriggered: statusSource.connectSource(Reader.buildReadCommand())
} }
PlasmaCore.DataSource { PlasmaCore.DataSource {
id: readStatus id: statusSource
engine: "executable" engine: "executable"
property string cmd: "echo $(cat /sys/class/thermal/thermal_zone0/temp):$(powerprofilesctl get):$(cat /sys/devices/system/cpu/intel_pstate/max_perf_pct):$(cat /sys/devices/system/cpu/intel_pstate/no_turbo)"
onNewData: { onNewData: {
var out = data["stdout"].trim() var result = Reader.parseOutput(data["stdout"])
var p = out.split(":") if (result.valid) {
if (p.length >= 4) { root.temperature = result.tempC
root.currentTemp = Math.round(parseInt(p[0]) / 1000) + "°C" root.currentPerf = result.perf
root.currentProfile = p[1]
root.maxPerf = p[2] + "%"
root.turboState = p[3] === "0" ? "ON" : "OFF"
} }
disconnectSource(sourceName) disconnectSource(sourceName)
} }
} }
PlasmaCore.DataSource { PlasmaCore.DataSource {
id: runCmd id: writeSource
engine: "executable" engine: "executable"
onNewData: { onNewData: {
disconnectSource(sourceName) disconnectSource(sourceName)
readStatus.connectSource(readStatus.cmd) statusSource.connectSource(Reader.buildReadCommand())
} }
} }
function setProfile(profile) { function setPerf(value) {
runCmd.connectSource("powerprofilesctl set " + profile) var cmd = Writer.buildSetCommand(value)
if (cmd) writeSource.connectSource(cmd)
} }
Plasmoid.compactRepresentation: Row { Plasmoid.compactRepresentation: CompactView {
spacing: 4 temperature: root.temperature
PlasmaCore.IconItem {
source: "cpu"
width: 16
height: 16
anchors.verticalCenter: parent.verticalCenter
}
PlasmaComponents.Label {
text: root.currentTemp
anchors.verticalCenter: parent.verticalCenter
}
MouseArea {
anchors.fill: parent
onClicked: plasmoid.expanded = !plasmoid.expanded onClicked: plasmoid.expanded = !plasmoid.expanded
} }
}
Plasmoid.fullRepresentation: Column { Plasmoid.fullRepresentation: FullView {
width: 220 temperature: root.temperature
spacing: 8 currentPerf: root.currentPerf
onPerfChangeRequested: root.setPerf(newValue)
PlasmaComponents.Label {
text: "CPU Control"
font.bold: true
font.pointSize: 12
anchors.horizontalCenter: parent.horizontalCenter
}
Grid {
columns: 2
spacing: 6
anchors.horizontalCenter: parent.horizontalCenter
PlasmaComponents.Label { text: "Temp:"; opacity: 0.7 }
PlasmaComponents.Label {
text: root.currentTemp
font.bold: true
color: parseInt(root.currentTemp) < 60 ? "#27ae60" : parseInt(root.currentTemp) < 75 ? "#f39c12" : "#e74c3c"
}
PlasmaComponents.Label { text: "Perf:"; opacity: 0.7 }
PlasmaComponents.Label { text: root.maxPerf; font.bold: true }
PlasmaComponents.Label { text: "Turbo:"; opacity: 0.7 }
PlasmaComponents.Label {
text: root.turboState
font.bold: true
color: root.turboState === "ON" ? "#27ae60" : "#e74c3c"
}
PlasmaComponents.Label { text: "Profile:"; opacity: 0.7 }
PlasmaComponents.Label {
text: root.currentProfile
font.bold: true
color: root.currentProfile === "performance" ? "#27ae60" : root.currentProfile === "power-saver" ? "#3498db" : "#f39c12"
}
}
Rectangle {
width: parent.width - 20
height: 1
color: PlasmaCore.Theme.textColor
opacity: 0.2
anchors.horizontalCenter: parent.horizontalCenter
}
Column {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 6
PlasmaComponents.Button {
text: "Performance"
width: 180
checked: root.currentProfile === "performance"
onClicked: root.setProfile("performance")
}
PlasmaComponents.Button {
text: "Balanced"
width: 180
checked: root.currentProfile === "balanced"
onClicked: root.setProfile("balanced")
}
PlasmaComponents.Button {
text: "Power Saver"
width: 180
checked: root.currentProfile === "power-saver"
onClicked: root.setProfile("power-saver")
}
}
} }
} }

View File

@ -1,24 +1,44 @@
#!/bin/bash #!/bin/bash
# Instalar CPU Control Plasmoid # Install CPU Control Plasma Widget
# Installs: plasmoid files + polkit policy + helper script
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PLASMOID_DIR="$HOME/.local/share/plasma/plasmoids/org.kde.plasma.cpucontrol" PLASMOID_DIR="$HOME/.local/share/plasma/plasmoids/org.kde.plasma.cpucontrol"
HELPER="/usr/local/bin/cpu-perf-set"
POLICY="/usr/share/polkit-1/actions/org.kde.plasma.cpucontrol.policy"
echo "Instalando CPU Control Plasmoid..." echo "=== CPU Control Widget Installer ==="
# Crear directorio si no existe # 1. Install plasmoid files
mkdir -p "$PLASMOID_DIR" echo "[1/3] Installing widget files..."
rm -rf "$PLASMOID_DIR"
mkdir -p "$PLASMOID_DIR/contents/ui/components"
mkdir -p "$PLASMOID_DIR/contents/ui/logic"
# Copiar archivos cp "$SCRIPT_DIR/metadata.json" "$PLASMOID_DIR/"
cp -r "$(dirname "$0")"/* "$PLASMOID_DIR/" cp "$SCRIPT_DIR/metadata.desktop" "$PLASMOID_DIR/"
cp "$SCRIPT_DIR/contents/ui/main.qml" "$PLASMOID_DIR/contents/ui/"
cp "$SCRIPT_DIR/contents/ui/CompactView.qml" "$PLASMOID_DIR/contents/ui/"
cp "$SCRIPT_DIR/contents/ui/FullView.qml" "$PLASMOID_DIR/contents/ui/"
cp "$SCRIPT_DIR/contents/ui/components/"*.qml "$PLASMOID_DIR/contents/ui/components/"
cp "$SCRIPT_DIR/contents/ui/logic/"*.js "$PLASMOID_DIR/contents/ui/logic/"
# Remover el script de instalación del destino echo " Widget → $PLASMOID_DIR"
rm -f "$PLASMOID_DIR/install.sh"
rm -f "$PLASMOID_DIR/README.md" # 2. Install helper script (requires sudo)
echo "[2/3] Installing helper script..."
sudo cp "$SCRIPT_DIR/system/cpu-perf-set" "$HELPER"
sudo chmod 755 "$HELPER"
echo " Helper → $HELPER"
# 3. Install polkit policy (requires sudo)
echo "[3/3] Installing polkit policy..."
sudo cp "$SCRIPT_DIR/system/org.kde.plasma.cpucontrol.policy" "$POLICY"
echo " Policy → $POLICY"
echo "Instalado en: $PLASMOID_DIR"
echo "" echo ""
echo "Para usar:" echo "=== Installation complete ==="
echo "1. Reinicia Plasma: kquitapp6 plasmashell && kstart plasmashell" echo "Restart Plasma: kquitapp6 plasmashell && kstart plasmashell"
echo "2. Click derecho en panel > Add Widgets > Buscar 'CPU Control'" echo "Then add 'CPU Control' widget to your panel."
echo ""
echo "O reinicia sesión para que los cambios tomen efecto."

28
system/cpu-perf-set Executable file
View File

@ -0,0 +1,28 @@
#!/bin/bash
# Helper script for CPU Control Plasma Widget
# Sets intel_pstate max_perf_pct with input validation
# Called via pkexec from the widget
SYSFS="/sys/devices/system/cpu/intel_pstate/max_perf_pct"
if [ -z "$1" ]; then
echo "Usage: cpu-perf-set <10-100>" >&2
exit 1
fi
if ! [[ "$1" =~ ^[0-9]+$ ]]; then
echo "Error: value must be a number" >&2
exit 1
fi
if [ "$1" -lt 10 ] || [ "$1" -gt 100 ]; then
echo "Error: value must be between 10 and 100" >&2
exit 1
fi
if [ ! -f "$SYSFS" ]; then
echo "Error: intel_pstate not available" >&2
exit 1
fi
echo "$1" > "$SYSFS"

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<action id="org.kde.plasma.cpucontrol.set-perf">
<description>Set CPU performance limit</description>
<message>Authentication is required to change CPU performance limit</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>yes</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">/usr/local/bin/cpu-perf-set</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
</action>
</policyconfig>

26
tests/run_tests.sh Executable file
View File

@ -0,0 +1,26 @@
#!/bin/bash
# Run all tests for CPU Control Widget
set -e
DIR="$(cd "$(dirname "$0")" && pwd)"
FAILED=0
echo "=== CPU Control Widget Tests ==="
echo ""
for test in "$DIR"/test_*.js; do
echo "Running $(basename "$test")..."
if node "$test"; then
echo ""
else
FAILED=1
echo ""
fi
done
if [ $FAILED -eq 0 ]; then
echo "=== ALL TESTS PASSED ==="
else
echo "=== SOME TESTS FAILED ==="
exit 1
fi

100
tests/test_CpuReader.js Normal file
View File

@ -0,0 +1,100 @@
// Tests for CpuReader.js logic
// Run with: node test_CpuReader.js
// Load the module (strip .pragma library for Node.js)
const fs = require("fs")
let src = fs.readFileSync(__dirname + "/../contents/ui/logic/CpuReader.js", "utf8")
src = src.replace(".pragma library", "")
src += "\nmodule.exports = { parseOutput, tempColor, tempLabel, clampPerf, buildReadCommand };"
const tmpFile = "/tmp/test_CpuReader_tmp.js"
fs.writeFileSync(tmpFile, src)
const Reader = require(tmpFile)
let passed = 0
let failed = 0
function assert(condition, message) {
if (condition) {
passed++
console.log(" PASS: " + message)
} else {
failed++
console.error(" FAIL: " + message)
}
}
function assertEqual(actual, expected, message) {
assert(actual === expected, message + " (got " + actual + ", expected " + expected + ")")
}
// --- parseOutput tests ---
console.log("\n=== parseOutput ===")
var r1 = Reader.parseOutput("94000:85")
assertEqual(r1.tempC, 94, "Normal: temp 94000 → 94°C")
assertEqual(r1.perf, 85, "Normal: perf 85%")
assertEqual(r1.valid, true, "Normal: valid=true")
var r2 = Reader.parseOutput("45500:100")
assertEqual(r2.tempC, 46, "Rounding: 45500 → 46°C")
assertEqual(r2.perf, 100, "Full perf: 100%")
var r3 = Reader.parseOutput("")
assertEqual(r3.valid, false, "Empty string: valid=false")
assertEqual(r3.perf, 100, "Empty string: default perf=100")
var r4 = Reader.parseOutput("garbage")
assertEqual(r4.valid, false, "Garbage: valid=false")
var r5 = Reader.parseOutput("0:50")
assertEqual(r5.valid, false, "Zero temp: valid=false")
var r6 = Reader.parseOutput("72000:5")
assertEqual(r6.perf, 10, "Perf below min clamped to 10")
var r7 = Reader.parseOutput("72000:150")
assertEqual(r7.perf, 100, "Perf above max clamped to 100")
// --- tempColor tests ---
console.log("\n=== tempColor ===")
assertEqual(Reader.tempColor(30), "#34c759", "30°C → green")
assertEqual(Reader.tempColor(49), "#34c759", "49°C → green")
assertEqual(Reader.tempColor(50), "#ffcc00", "50°C → yellow")
assertEqual(Reader.tempColor(64), "#ffcc00", "64°C → yellow")
assertEqual(Reader.tempColor(65), "#ff9500", "65°C → orange")
assertEqual(Reader.tempColor(79), "#ff9500", "79°C → orange")
assertEqual(Reader.tempColor(80), "#ff3b30", "80°C → red")
assertEqual(Reader.tempColor(95), "#ff3b30", "95°C → red")
// --- tempLabel tests ---
console.log("\n=== tempLabel ===")
assertEqual(Reader.tempLabel(30), "Cool", "30°C → Cool")
assertEqual(Reader.tempLabel(55), "Normal", "55°C → Normal")
assertEqual(Reader.tempLabel(70), "Warm", "70°C → Warm")
assertEqual(Reader.tempLabel(85), "Hot", "85°C → Hot")
// --- clampPerf tests ---
console.log("\n=== clampPerf ===")
assertEqual(Reader.clampPerf(50), 50, "50 → 50 (no change)")
assertEqual(Reader.clampPerf(0), 10, "0 → 10 (clamped min)")
assertEqual(Reader.clampPerf(5), 10, "5 → 10 (clamped min)")
assertEqual(Reader.clampPerf(200), 100, "200 → 100 (clamped max)")
assertEqual(Reader.clampPerf(10), 10, "10 → 10 (exact min)")
assertEqual(Reader.clampPerf(100), 100, "100 → 100 (exact max)")
// --- buildReadCommand tests ---
console.log("\n=== buildReadCommand ===")
var cmd = Reader.buildReadCommand()
assert(cmd.indexOf("thermal_zone") > -1, "Command contains thermal_zone")
assert(cmd.indexOf("x86_pkg_temp") > -1, "Command searches for x86_pkg_temp")
assert(cmd.indexOf("TCPU") > -1, "Command searches for TCPU")
assert(cmd.indexOf("max_perf_pct") > -1, "Command reads max_perf_pct")
// --- Summary ---
console.log("\n=== Results: " + passed + " passed, " + failed + " failed ===")
fs.unlinkSync(tmpFile)
process.exit(failed > 0 ? 1 : 0)

94
tests/test_CpuWriter.js Normal file
View File

@ -0,0 +1,94 @@
// Tests for CpuWriter.js logic
// Run with: node test_CpuWriter.js
const fs = require("fs")
let src = fs.readFileSync(__dirname + "/../contents/ui/logic/CpuWriter.js", "utf8")
src = src.replace(".pragma library", "")
src += "\nmodule.exports = { MIN_PERF, MAX_PERF, STEP, isValidPerf, buildSetCommand, increase, decrease, canIncrease, canDecrease };"
const tmpFile = "/tmp/test_CpuWriter_tmp.js"
fs.writeFileSync(tmpFile, src)
const Writer = require(tmpFile)
let passed = 0
let failed = 0
function assert(condition, message) {
if (condition) {
passed++
console.log(" PASS: " + message)
} else {
failed++
console.error(" FAIL: " + message)
}
}
function assertEqual(actual, expected, message) {
assert(actual === expected, message + " (got " + actual + ", expected " + expected + ")")
}
// --- Constants ---
console.log("\n=== Constants ===")
assertEqual(Writer.MIN_PERF, 10, "MIN_PERF = 10")
assertEqual(Writer.MAX_PERF, 100, "MAX_PERF = 100")
assertEqual(Writer.STEP, 5, "STEP = 5")
// --- isValidPerf tests ---
console.log("\n=== isValidPerf ===")
assertEqual(Writer.isValidPerf(50), true, "50 is valid")
assertEqual(Writer.isValidPerf(10), true, "10 (min) is valid")
assertEqual(Writer.isValidPerf(100), true, "100 (max) is valid")
assertEqual(Writer.isValidPerf(9), false, "9 is invalid (below min)")
assertEqual(Writer.isValidPerf(101), false, "101 is invalid (above max)")
assertEqual(Writer.isValidPerf(0), false, "0 is invalid")
assertEqual(Writer.isValidPerf(-5), false, "Negative is invalid")
assertEqual(Writer.isValidPerf("abc"), false, "String is invalid")
assertEqual(Writer.isValidPerf(null), false, "null is invalid")
assertEqual(Writer.isValidPerf(undefined), false, "undefined is invalid")
// --- buildSetCommand tests ---
console.log("\n=== buildSetCommand ===")
var cmd = Writer.buildSetCommand(85)
assert(cmd !== null, "Valid value returns command")
assert(cmd.indexOf("pkexec") > -1, "Command uses pkexec")
assert(cmd.indexOf("cpu-perf-set") > -1, "Command calls helper script")
assert(cmd.indexOf("85") > -1, "Command contains value 85")
assertEqual(Writer.buildSetCommand(5), null, "Below min returns null")
assertEqual(Writer.buildSetCommand(105), null, "Above max returns null")
assertEqual(Writer.buildSetCommand("abc"), null, "String returns null")
// --- increase/decrease tests ---
console.log("\n=== increase ===")
assertEqual(Writer.increase(50), 55, "50 + 5 = 55")
assertEqual(Writer.increase(95), 100, "95 + 5 = 100")
assertEqual(Writer.increase(100), 100, "100 capped at 100")
assertEqual(Writer.increase(98), 100, "98 + 5 = 100 (capped)")
console.log("\n=== decrease ===")
assertEqual(Writer.decrease(50), 45, "50 - 5 = 45")
assertEqual(Writer.decrease(15), 10, "15 - 5 = 10")
assertEqual(Writer.decrease(10), 10, "10 capped at 10")
assertEqual(Writer.decrease(12), 10, "12 - 5 = 10 (capped)")
// --- canIncrease/canDecrease tests ---
console.log("\n=== canIncrease ===")
assertEqual(Writer.canIncrease(50), true, "50 can increase")
assertEqual(Writer.canIncrease(99), true, "99 can increase")
assertEqual(Writer.canIncrease(100), false, "100 cannot increase")
console.log("\n=== canDecrease ===")
assertEqual(Writer.canDecrease(50), true, "50 can decrease")
assertEqual(Writer.canDecrease(11), true, "11 can decrease")
assertEqual(Writer.canDecrease(10), false, "10 cannot decrease")
// --- Summary ---
console.log("\n=== Results: " + passed + " passed, " + failed + " failed ===")
fs.unlinkSync(tmpFile)
process.exit(failed > 0 ? 1 : 0)