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
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
- Temperatura del CPU en tiempo real en el panel
- Porcentaje de rendimiento y estado del turbo boost
- Cambio de perfil con un click:
- **Performance**: Máximo rendimiento (videollamadas, compilación)
- **Balanced**: Equilibrio rendimiento/consumo (uso general)
- **Power Saver**: Bajo consumo (batería, temperaturas altas)
- Usa `powerprofilesctl` (estándar del sistema, sin sudo)
- Colores indicadores de temperatura y estado
- Temperatura real del CPU en tiempo real (detección dinámica de zona térmica)
- Limitador de CPU con botones -5% / +5% (rango 10%-100%)
- Colores dinámicos por temperatura: verde, amarillo, naranja, rojo
- Sin duplicar funcionalidad del sistema (no cambia perfiles de energía)
- Arquitectura modular por componentes
## Requisitos
- KDE Plasma 6
- CPU Intel con driver `intel_pstate`
- `power-profiles-daemon` (incluido por defecto en Kubuntu)
- Node.js (para ejecutar tests)
## Instalación
@ -29,24 +26,45 @@ kquitapp6 plasmashell && kstart plasmashell
Click derecho en el panel > "Add Widgets..." > Buscar "CPU Control"
## Desinstalación
## Tests
```bash
rm -rf ~/.local/share/plasma/plasmoids/org.kde.plasma.cpucontrol
kquitapp6 plasmashell && kstart plasmashell
./tests/run_tests.sh
```
## Estructura
```
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.desktop
├── contents/
│ └── ui/
│ └── main.qml
├── install.sh
└── README.md
└── install.sh
```
## Desinstalación
```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

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.Layouts 1.15
import org.kde.plasma.plasmoid 2.0
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 {
id: root
property string currentTemp: "..."
property string currentProfile: "..."
property string maxPerf: "..."
property string turboState: "..."
property int temperature: 0
property int currentPerf: 100
Plasmoid.preferredRepresentation: Plasmoid.compactRepresentation
@ -19,134 +17,44 @@ Item {
running: true
repeat: true
triggeredOnStart: true
onTriggered: readStatus.connectSource(readStatus.cmd)
onTriggered: statusSource.connectSource(Reader.buildReadCommand())
}
PlasmaCore.DataSource {
id: readStatus
id: statusSource
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: {
var out = data["stdout"].trim()
var p = out.split(":")
if (p.length >= 4) {
root.currentTemp = Math.round(parseInt(p[0]) / 1000) + "°C"
root.currentProfile = p[1]
root.maxPerf = p[2] + "%"
root.turboState = p[3] === "0" ? "ON" : "OFF"
var result = Reader.parseOutput(data["stdout"])
if (result.valid) {
root.temperature = result.tempC
root.currentPerf = result.perf
}
disconnectSource(sourceName)
}
}
PlasmaCore.DataSource {
id: runCmd
id: writeSource
engine: "executable"
onNewData: {
disconnectSource(sourceName)
readStatus.connectSource(readStatus.cmd)
statusSource.connectSource(Reader.buildReadCommand())
}
}
function setProfile(profile) {
runCmd.connectSource("powerprofilesctl set " + profile)
function setPerf(value) {
var cmd = Writer.buildSetCommand(value)
if (cmd) writeSource.connectSource(cmd)
}
Plasmoid.compactRepresentation: Row {
spacing: 4
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
Plasmoid.compactRepresentation: CompactView {
temperature: root.temperature
onClicked: plasmoid.expanded = !plasmoid.expanded
}
}
Plasmoid.fullRepresentation: Column {
width: 220
spacing: 8
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")
}
}
Plasmoid.fullRepresentation: FullView {
temperature: root.temperature
currentPerf: root.currentPerf
onPerfChangeRequested: root.setPerf(newValue)
}
}

View File

@ -1,24 +1,44 @@
#!/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"
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
mkdir -p "$PLASMOID_DIR"
# 1. Install plasmoid files
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 -r "$(dirname "$0")"/* "$PLASMOID_DIR/"
cp "$SCRIPT_DIR/metadata.json" "$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
rm -f "$PLASMOID_DIR/install.sh"
rm -f "$PLASMOID_DIR/README.md"
echo " Widget → $PLASMOID_DIR"
# 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 "Para usar:"
echo "1. Reinicia Plasma: kquitapp6 plasmashell && kstart plasmashell"
echo "2. Click derecho en panel > Add Widgets > Buscar 'CPU Control'"
echo ""
echo "O reinicia sesión para que los cambios tomen efecto."
echo "=== Installation complete ==="
echo "Restart Plasma: kquitapp6 plasmashell && kstart plasmashell"
echo "Then add 'CPU Control' widget to your panel."

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)