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:
parent
7afb6f966e
commit
8d7cd3f65f
54
README.md
54
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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
50
install.sh
50
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."
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
Loading…
Reference in New Issue