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
|
# 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
|
||||||
|
|
|
||||||
|
|
@ -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 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
50
install.sh
50
install.sh
|
|
@ -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."
|
|
||||||
|
|
|
||||||
|
|
@ -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