diff --git a/README.md b/README.md index e0ec95a..e6b3b3f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/contents/ui/CompactView.qml b/contents/ui/CompactView.qml new file mode 100644 index 0000000..6b4f767 --- /dev/null +++ b/contents/ui/CompactView.qml @@ -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() + } +} diff --git a/contents/ui/FullView.qml b/contents/ui/FullView.qml new file mode 100644 index 0000000..8f674b1 --- /dev/null +++ b/contents/ui/FullView.qml @@ -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 } +} diff --git a/contents/ui/components/PerfLimiter.qml b/contents/ui/components/PerfLimiter.qml new file mode 100644 index 0000000..b94dd00 --- /dev/null +++ b/contents/ui/components/PerfLimiter.qml @@ -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)) + } + } + } +} diff --git a/contents/ui/components/TemperatureDisplay.qml b/contents/ui/components/TemperatureDisplay.qml new file mode 100644 index 0000000..df3ddd7 --- /dev/null +++ b/contents/ui/components/TemperatureDisplay.qml @@ -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 + } +} diff --git a/contents/ui/logic/CpuReader.js b/contents/ui/logic/CpuReader.js new file mode 100644 index 0000000..471271f --- /dev/null +++ b/contents/ui/logic/CpuReader.js @@ -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)) +} diff --git a/contents/ui/logic/CpuWriter.js b/contents/ui/logic/CpuWriter.js new file mode 100644 index 0000000..b906d96 --- /dev/null +++ b/contents/ui/logic/CpuWriter.js @@ -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 +} diff --git a/contents/ui/main.qml b/contents/ui/main.qml index 08849b6..1697c74 100644 --- a/contents/ui/main.qml +++ b/contents/ui/main.qml @@ -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 - onClicked: plasmoid.expanded = !plasmoid.expanded - } + 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) } } diff --git a/install.sh b/install.sh index c7452fd..6982f26 100755 --- a/install.sh +++ b/install.sh @@ -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." diff --git a/system/cpu-perf-set b/system/cpu-perf-set new file mode 100755 index 0000000..b5628a1 --- /dev/null +++ b/system/cpu-perf-set @@ -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" diff --git a/system/org.kde.plasma.cpucontrol.policy b/system/org.kde.plasma.cpucontrol.policy new file mode 100644 index 0000000..8e4e953 --- /dev/null +++ b/system/org.kde.plasma.cpucontrol.policy @@ -0,0 +1,17 @@ + + + + + Set CPU performance limit + Authentication is required to change CPU performance limit + + auth_admin + auth_admin + yes + + /usr/local/bin/cpu-perf-set + true + + diff --git a/tests/run_tests.sh b/tests/run_tests.sh new file mode 100755 index 0000000..b2d9711 --- /dev/null +++ b/tests/run_tests.sh @@ -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 diff --git a/tests/test_CpuReader.js b/tests/test_CpuReader.js new file mode 100644 index 0000000..4d91982 --- /dev/null +++ b/tests/test_CpuReader.js @@ -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) diff --git a/tests/test_CpuWriter.js b/tests/test_CpuWriter.js new file mode 100644 index 0000000..8344926 --- /dev/null +++ b/tests/test_CpuWriter.js @@ -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)