Files
kde-cpufreq-dashboard/contents/ui/main.qml
T
enne2 d4902b0b72 Plasmoide CPU Frequency Control: governor, limiti, EPP, boost e profili per task
Widget KDE Plasma 6 (org.enne2.cpufreq) per pilotare il profilo energetico della
CPU: governor, limiti min/max, energy_performance_preference, boost/turbo,
quattro profili pronti (Silenzioso, Risparmio, Bilanciato, Performance) e task
con profilo dedicato che ripristina lo stato precedente alla fine del comando.

Privilegi separati: il widget resta non privilegiato e l'unica operazione root
passa da un helper a interfaccia ristretta in /usr/local/libexec/enne2-cpufreq/
(comandi a elenco chiuso, argomenti validati su sysfs, rilettura di verifica),
invocato via pkexec con l'azione polkit dedicata org.enne2.cpufreq.set. La regola
in /etc/polkit-1/rules.d/ concede il consenso senza prompt a utenti locali in
sessione attiva nel gruppo wheel; senza la regola la policy chiede la password.

Dettagli verificati su frigate (Plasma 6.7.5, kernel 7.2, amd-pstate-epp):
- con boost disattivato cpuinfo_max_freq scende alla frequenza nominale, quindi
  il tetto hardware si legge da amd_pstate_max_freq e i profili applicano il
  boost prima dei limiti;
- scrittura dei limiti in ordine sicuro (mai min > max intermedio);
- baseline e contatore di riferimento per i task paralleli;
- syncControls protetto da uiReady e try/catch: un ReferenceError su un id della
  rappresentazione non deve interrompere l'aggiornamento dello stato;
- implicitWidth/Height costanti, altrimenti il containment riduce il widget a
  68x68.

Include install.sh/uninstall.sh (widget, backend, dry-run), README con modello di
sicurezza e diagnostica, screenshot docs/widget.png. Licenza MIT.
2026-10-25 20:42:52 +01:00

1140 lines
43 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* SPDX-FileCopyrightText: 2026 enne2
* SPDX-License-Identifier: MIT
*
* CPU Frequency Control — plasmoide KDE Plasma 6 per governor, limiti di
* frequenza, EPP, boost e profili per singolo task.
* Backend: helper privilegiato /usr/local/libexec/enne2-cpufreq/cpufreq-helper
* eseguito via pkexec con azione polkit dedicata (org.enne2.cpufreq.set).
*/
import QtQuick
import QtQuick.Layouts
import org.kde.plasma.plasmoid
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.plasma.plasma5support as P5Support
import org.kde.kirigami as Kirigami
PlasmoidItem {
id: root
// ------------------------------------------------------------------ percorsi
readonly property string scriptsDir: Qt.resolvedUrl("../scripts").toString().replace("file://", "")
readonly property string stateScript: scriptsDir + "/cpufreq-state.sh"
readonly property string taskScript: scriptsDir + "/enne2-cpufreq-task"
readonly property string helperPath: "/usr/local/libexec/enne2-cpufreq/cpufreq-helper"
readonly property int updateMs: 2000
// ------------------------------------------------------------------- palette
readonly property color accentBalanced: "#55c5c0"
readonly property color accentQuiet: "#7aa2d6"
readonly property color accentSaving: "#8bbf6a"
readonly property color accentPerf: "#e0a34e"
readonly property color accentCustom: "#b08ccc"
readonly property color dim: "#8293a8"
readonly property color okColor: "#62c98b"
readonly property color warnColor: "#d9a84e"
readonly property color critColor: "#e5645f"
readonly property var presetModel: [
{ key: "quiet", label: "Silenzioso", hint: "boost off · max 35%", color: accentQuiet },
{ key: "powersave", label: "Risparmio", hint: "boost off · max 60%", color: accentSaving },
{ key: "balanced", label: "Bilanciato", hint: "nessun tetto · boost on", color: accentBalanced },
{ key: "performance", label: "Performance", hint: "governor performance", color: accentPerf }
]
// --------------------------------------------------------------------- stato
property string driver: ""
property string cpuMode: ""
property string cpuModel: ""
property int policies: 0
property string governor: ""
property var governors: []
property string epp: ""
property var epps: []
property bool eppSupported: false
property int boostState: -1
property bool boostSupported: false
property int hwMin: 0
property int hwMax: 0
property int floorKhz: 0
property int minKhz: 0
property int maxKhz: 0
property int curKhz: 0
property int curAvgKhz: 0
property real tempC: NaN
property real load1: NaN
property string profile: "custom"
property string profileLabel: ""
property string helperState: "unknown" // ok | missing
property string polkitState: "unknown" // ok | ask | missing
property string conflicts: ""
property bool baselinePresent: false
property var tasks: []
property bool stateReady: false
property bool uiReady: false
// ---------------------------------------------------------------- azioni UI
property bool busy: false
property string message: ""
property bool messageIsError: false
property bool addFormOpen: false
property string newTaskName: ""
property string newTaskCommand: ""
property int newTaskProfile: 2
property string lastActionSource: ""
property int actionCounter: 0
// ------------------------------------------------------------- presentazione
Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground
Plasmoid.icon: "cpu"
preferredRepresentation: fullRepresentation
toolTipMainText: "CPU Frequency Control"
toolTipTextFormat: Text.PlainText
toolTipSubText: {
if (!root.stateReady) {
return "Lettura dello stato in corso…"
}
var lines = []
lines.push(root.ghz(root.curKhz) + " · governor " + (root.governor || "n/d"))
if (root.eppSupported) {
lines.push("EPP " + (root.epp || "n/d") + " · boost " + root.boostText())
}
lines.push("Profilo: " + (root.profileLabel || root.profile))
lines.push("Limiti " + root.ghzShort(root.minKhz) + " – " + root.ghzShort(root.maxKhz) + " GHz")
if (root.tasks.length > 0) {
lines.push("Task configurati: " + root.tasks.length)
}
return lines.join("\n")
}
readonly property color profileColor: {
switch (root.profile) {
case "quiet": return accentQuiet
case "powersave": return accentSaving
case "balanced": return accentBalanced
case "performance": return accentPerf
default: return accentCustom
}
}
// ---------------------------------------------------------------- formattazione
function ghz(khz) {
if (!khz || isNaN(khz) || khz <= 0) {
return "—"
}
return (khz / 1000000).toFixed(2) + " GHz"
}
function ghzShort(khz) {
if (!khz || isNaN(khz) || khz <= 0) {
return "—"
}
return (khz / 1000000).toFixed(2)
}
// Etichette compatte per le pillole EPP (il valore completo resta nel chip in alto)
function eppShort(name) {
switch (name) {
case "balance_performance": return "bal_perf"
case "balance_power": return "bal_power"
default: return name
}
}
function boostText() {
if (!root.boostSupported || root.boostState < 0) {
return "n/d"
}
return root.boostState === 1 ? "on" : "off"
}
function tempText() {
return isNaN(root.tempC) ? "n/d" : Math.round(root.tempC) + " °C"
}
function tempColor() {
if (isNaN(root.tempC)) {
return root.dim
}
if (root.tempC >= 85) {
return root.critColor
}
if (root.tempC >= 70) {
return root.warnColor
}
return root.okColor
}
// Un solo riquadro di avviso: il problema più grave in ordine di priorità.
function setupWarning() {
if (root.helperState !== "ok") {
return "Backend non installato: esegui ./install.sh --backend (helper in "
+ root.helperPath + ")"
}
if (root.polkitState === "missing") {
return "Azione polkit assente: senza org.enne2.cpufreq.set le modifiche verranno rifiutate"
}
if (root.conflicts !== "") {
return "Servizio concorrente attivo (" + root.conflicts
+ ") che gestisce la CPU: può sovrascrivere queste impostazioni"
}
return ""
}
function setupWarningColor() {
if (root.helperState !== "ok" || root.polkitState === "missing") {
return root.critColor
}
return root.warnColor
}
// ------------------------------------------------------------- lettura stato
P5Support.DataSource {
id: stateSource
engine: "executable"
interval: root.updateMs
connectedSources: [root.stateScript]
onNewData: function(sourceName, data) {
root.applyState(data ? String(data["stdout"] || "") : "")
}
}
// Comandi una tantum: ogni invocazione usa un source univoco (contatore)
// perché il motore "executable" non riesegua lo stesso source già noto.
P5Support.DataSource {
id: actionSource
engine: "executable"
connectedSources: []
onNewData: function(sourceName, data) {
root.finishAction(String(data["stdout"] || ""), String(data["stderr"] || ""),
Number(data["exit code"]))
actionSource.disconnectSource(sourceName)
root.refreshState()
}
}
function applyState(output) {
var values = {}
var taskList = []
String(output || "").split("\n").forEach(function(rawLine) {
if (rawLine.length === 0) {
return
}
if (rawLine.indexOf("task=") === 0) {
var parts = rawLine.substring(5).split("\t")
if (parts.length >= 3) {
try {
var task = JSON.parse(parts[2])
task.running = parts[1] === "1"
taskList.push(task)
} catch (error) {
console.warn("enne2-cpufreq: task JSON non valido:", parts[2])
}
}
return
}
var separator = rawLine.indexOf("=")
if (separator > 0) {
values[rawLine.substring(0, separator)] = rawLine.substring(separator + 1).trim()
}
})
if (values["driver"] !== undefined) {
driver = values["driver"]
}
if (values["mode"] !== undefined) {
cpuMode = values["mode"]
}
if (values["model"] !== undefined) {
cpuModel = values["model"]
}
policies = Number(values["policies"] || 0)
governor = values["governor"] || ""
governors = (values["governors"] || "").split(" ").filter(function(item, index, array) {
return item.length > 0 && array.indexOf(item) === index
})
epp = values["epp"] || ""
epps = (values["epps"] || "").split(" ").filter(function(item, index, array) {
return item.length > 0 && array.indexOf(item) === index
})
eppSupported = values["epp_supported"] === "1"
boostSupported = values["boost_supported"] === "1"
boostState = boostSupported ? Number(values["boost"] || -1) : -1
hwMin = Number(values["hw_min_khz"] || 0)
hwMax = Number(values["hw_max_khz"] || 0)
floorKhz = Number(values["floor_khz"] || 0)
minKhz = Number(values["min_khz"] || 0)
maxKhz = Number(values["max_khz"] || 0)
curKhz = Number(values["cur_khz"] || 0)
curAvgKhz = Number(values["cur_avg_khz"] || 0)
var temp = parseFloat(values["temp_mdegc"])
tempC = isNaN(temp) ? NaN : temp / 1000
var load = parseFloat(values["load1"])
load1 = isNaN(load) ? NaN : load
profile = values["profile"] || "custom"
profileLabel = values["profile_label"] || ""
helperState = values["helper"] || "unknown"
polkitState = values["polkit"] || "unknown"
conflicts = values["conflicts"] || ""
baselinePresent = values["baseline"] === "present"
tasks = taskList
stateReady = values["error"] === undefined
if (values["error"] !== undefined && message === "") {
message = values["error"]
messageIsError = true
}
syncControls()
}
function refreshState() {
stateSource.disconnectSource(root.stateScript)
stateSource.connectSource(root.stateScript)
}
// ------------------------------------------------------------------ azioni
function privileged(args) {
return "timeout 90 /usr/bin/pkexec " + root.helperPath + " " + args
}
function runAction(command) {
if (root.busy) {
return
}
root.busy = true
if (root.lastActionSource !== "") {
actionSource.disconnectSource(root.lastActionSource)
}
root.actionCounter += 1
// argomento extra ignorato dagli script: rende univoco il source
var source = command + " " + root.actionCounter
root.lastActionSource = source
actionSource.connectSource(source)
}
function finishAction(output, stderr, exitCode) {
root.busy = false
var errorText = ""
var warningText = ""
output.split("\n").forEach(function(line) {
if (line.indexOf("error=") === 0) {
errorText = line.substring(6)
} else if (line.indexOf("warning=") === 0) {
warningText = line.substring(8)
}
})
if (errorText !== "") {
root.setMessage(errorText, true)
return
}
if (exitCode !== 0 && exitCode !== undefined && exitCode !== 0) {
var detail = stderr.trim()
if (detail === "") {
detail = "exit code " + exitCode
if (exitCode === 124) {
detail = "timeout in attesa di polkit"
} else if (exitCode === 126) {
detail = "autorizzazione negata da polkit"
} else if (exitCode === 127) {
detail = "helper non trovato: eseguire install.sh"
}
}
root.setMessage(detail, true)
return
}
if (warningText !== "") {
root.setMessage(warningText, false)
} else if (output.indexOf("ok=1") !== -1) {
root.setMessage("Applicato.", false)
}
}
function setMessage(text, isError) {
root.message = text
root.messageIsError = isError
messageTimer.restart()
}
// Sincronizza i controlli senza rompere i binding durante l'interazione.
// uiReady evita ReferenceError quando la rappresentazione piena non è ancora
// istanziata (avvio del widget, popup del pannello chiuso): applyState gira
// ogni 2 s e senza guardia interrompeva la sincronizzazione dei controlli.
function syncControls() {
if (!root.uiReady) {
return
}
try {
if (!minSlider.pressed) {
minSlider.value = root.minKhz
}
if (!maxSlider.pressed) {
maxSlider.value = root.maxKhz
}
} catch (error) {
// la rappresentazione piena non è istanziata in questo momento
// (popup chiuso, widget in pannello): ReferenceError atteso
root.uiReady = false
}
}
function roundKhz(value) {
return Math.max(root.hwMin, Math.min(root.hwMax, Math.round(value / 10000) * 10000))
}
// "which" è il cursore rilasciato: serve solo a decidere quale estremo
// spostare se l'utente ha trascinato min oltre max (o viceversa).
function applyLimits(which) {
var lo = roundKhz(minSlider.value)
var hi = roundKhz(maxSlider.value)
if (lo > hi) {
if (which === "min") {
hi = lo
} else {
lo = hi
}
}
runAction(privileged("set-limits " + lo + " " + hi))
}
function hexEncode(text) {
var input = String(text)
var output = ""
function byte(value) {
return (value < 16 ? "0" : "") + value.toString(16)
}
for (var i = 0; i < input.length; i++) {
var code = input.charCodeAt(i)
if (code < 0x80) {
output += byte(code)
} else if (code < 0x800) {
output += byte(0xc0 | (code >> 6)) + byte(0x80 | (code & 0x3f))
} else if (code >= 0xd800 && code <= 0xdbff && i + 1 < input.length) {
var low = input.charCodeAt(i + 1)
var point = 0x10000 + ((code - 0xd800) << 10) + (low - 0xdc00)
i++
output += byte(0xf0 | (point >> 18)) + byte(0x80 | ((point >> 12) & 0x3f))
+ byte(0x80 | ((point >> 6) & 0x3f)) + byte(0x80 | (point & 0x3f))
} else {
output += byte(0xe0 | (code >> 12)) + byte(0x80 | ((code >> 6) & 0x3f))
+ byte(0x80 | (code & 0x3f))
}
}
return output
}
function addTask() {
var name = root.newTaskName.trim()
var command = root.newTaskCommand.trim()
if (name === "") {
root.setMessage("Il nome del task è obbligatorio.", true)
return
}
if (command === "") {
root.setMessage("Il comando del task è obbligatorio.", true)
return
}
var profileKey = root.presetModel[root.newTaskProfile].key
runAction(root.taskScript + " add-hex " + hexEncode(name) + " "
+ hexEncode(profileKey) + " " + hexEncode(command))
root.addFormOpen = false
root.newTaskName = ""
root.newTaskCommand = ""
}
Timer {
id: messageTimer
interval: 6000
repeat: false
onTriggered: root.message = ""
}
// ------------------------------------------------- rappresentazione compatta
compactRepresentation: MouseArea {
id: compact
implicitWidth: compactRow.implicitWidth + Kirigami.Units.smallSpacing * 2
implicitHeight: compactRow.implicitHeight + Kirigami.Units.smallSpacing
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.expanded = !root.expanded
RowLayout {
id: compactRow
anchors.centerIn: parent
spacing: 4
Kirigami.Icon {
source: "cpu"
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: Kirigami.Units.iconSizes.smallMedium
color: root.profileColor
}
PlasmaComponents.Label {
text: root.ghzShort(root.curKhz)
font.family: "monospace"
font.pointSize: 8
color: Kirigami.Theme.textColor
}
Rectangle {
visible: root.boostState === 0 && root.boostSupported
implicitWidth: boostLabel.implicitWidth + 6
implicitHeight: boostLabel.implicitHeight + 2
radius: 3
color: Qt.rgba(root.warnColor.r, root.warnColor.g, root.warnColor.b, 0.22)
PlasmaComponents.Label {
id: boostLabel
anchors.centerIn: parent
text: "no boost"
font.pointSize: 6.5
color: root.warnColor
}
}
}
}
// ------------------------------------------------------- rappresentazione piena
fullRepresentation: Flickable {
id: fullItem
implicitWidth: Kirigami.Units.gridUnit * 23
implicitHeight: Kirigami.Units.gridUnit * 38
Component.onCompleted: {
root.uiReady = true
root.syncControls()
}
Component.onDestruction: root.uiReady = false
contentWidth: width
contentHeight: content.implicitHeight + Kirigami.Units.smallSpacing * 2
clip: true
boundsBehavior: Flickable.StopAtBounds
interactive: contentHeight > height
flickableDirection: Flickable.VerticalFlick
ColumnLayout {
id: content
x: Kirigami.Units.smallSpacing
y: Kirigami.Units.smallSpacing
width: fullItem.width - Kirigami.Units.smallSpacing * 2
spacing: 4
// ---------------------------------------------------------- intestazione
RowLayout {
Layout.fillWidth: true
spacing: 6
Kirigami.Icon {
source: "cpu"
implicitWidth: Kirigami.Units.iconSizes.small
implicitHeight: Kirigami.Units.iconSizes.small
color: root.profileColor
}
PlasmaComponents.Label {
text: "CPU FREQ"
font.family: "monospace"
font.bold: true
font.pointSize: 8
color: root.profileColor
}
PlasmaComponents.Label {
Layout.fillWidth: true
text: root.driver + (root.cpuMode !== "" && root.cpuMode !== "n/a" ? " · " + root.cpuMode : "")
font.family: "monospace"
font.pointSize: 7.5
color: root.dim
elide: Text.ElideRight
horizontalAlignment: Text.AlignRight
}
}
// ------------------------------------------------------- frequenza e temperatura
RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
ColumnLayout {
spacing: 0
PlasmaComponents.Label {
text: root.ghz(root.curKhz)
font.family: "monospace"
font.bold: true
font.pointSize: 16
color: Kirigami.Theme.textColor
}
PlasmaComponents.Label {
text: "media " + root.ghz(root.curAvgKhz) + " · " + root.policies + " policy"
font.family: "monospace"
font.pointSize: 7
color: root.dim
}
}
Item { Layout.fillWidth: true }
ColumnLayout {
spacing: 0
PlasmaComponents.Label {
Layout.alignment: Qt.AlignRight
text: root.tempText()
font.family: "monospace"
font.bold: true
font.pointSize: 13
color: root.tempColor()
}
PlasmaComponents.Label {
Layout.alignment: Qt.AlignRight
text: isNaN(root.load1) ? "load n/d" : "load " + root.load1.toFixed(2)
font.family: "monospace"
font.pointSize: 7.5
color: root.dim
}
}
}
// Barra: intervallo hardware, finestra consentita (min–max), cursore corrente
Item {
id: freqBar
Layout.fillWidth: true
Layout.preferredHeight: 16
readonly property real rangeMin: root.hwMin > 0 ? root.hwMin : 1
readonly property real rangeMax: root.hwMax > rangeMin ? root.hwMax : rangeMin + 1
function positionOf(khz) {
var ratio = (khz - rangeMin) / (rangeMax - rangeMin)
return Math.max(0, Math.min(1, ratio))
}
Rectangle {
anchors.fill: parent
radius: 3
color: Qt.rgba(1, 1, 1, 0.07)
}
Rectangle {
id: allowedWindow
anchors.verticalCenter: parent.verticalCenter
height: parent.height - 6
radius: 2
x: freqBar.width * freqBar.positionOf(root.minKhz)
width: Math.max(2, freqBar.width * (freqBar.positionOf(root.maxKhz)
- freqBar.positionOf(root.minKhz)))
color: Qt.rgba(root.profileColor.r, root.profileColor.g, root.profileColor.b, 0.30)
border.width: 1
border.color: Qt.rgba(root.profileColor.r, root.profileColor.g, root.profileColor.b, 0.55)
}
Rectangle {
width: 3
height: parent.height
radius: 1
color: Kirigami.Theme.textColor
x: Math.min(freqBar.width - width,
Math.max(0, freqBar.width * freqBar.positionOf(root.curKhz) - width / 2))
}
}
// ------------------------------------------------------------ chip di stato
Flow {
Layout.fillWidth: true
Layout.preferredHeight: childrenRect.height
spacing: 4
Rectangle {
implicitWidth: govLabel.implicitWidth + 10
implicitHeight: govLabel.implicitHeight + 4
radius: 4
color: Qt.rgba(0, 0, 0, 0.28)
border.width: 1
border.color: Qt.rgba(1, 1, 1, 0.10)
PlasmaComponents.Label {
id: govLabel
anchors.centerIn: parent
text: "governor " + (root.governor || "n/d")
font.family: "monospace"
font.pointSize: 7.5
color: Kirigami.Theme.textColor
opacity: 0.8
}
}
Rectangle {
visible: root.eppSupported
implicitWidth: eppLabel.implicitWidth + 10
implicitHeight: eppLabel.implicitHeight + 4
radius: 4
color: Qt.rgba(0, 0, 0, 0.28)
border.width: 1
border.color: Qt.rgba(1, 1, 1, 0.10)
PlasmaComponents.Label {
id: eppLabel
anchors.centerIn: parent
text: "epp " + (root.epp || "n/d")
font.family: "monospace"
font.pointSize: 7.5
color: Kirigami.Theme.textColor
opacity: 0.8
}
}
Rectangle {
visible: root.boostSupported
implicitWidth: boostChip.implicitWidth + 10
implicitHeight: boostChip.implicitHeight + 4
radius: 4
color: root.boostState === 1
? Qt.rgba(root.okColor.r, root.okColor.g, root.okColor.b, 0.14)
: Qt.rgba(root.warnColor.r, root.warnColor.g, root.warnColor.b, 0.16)
border.width: 1
border.color: root.boostState === 1
? Qt.rgba(root.okColor.r, root.okColor.g, root.okColor.b, 0.4)
: Qt.rgba(root.warnColor.r, root.warnColor.g, root.warnColor.b, 0.4)
PlasmaComponents.Label {
id: boostChip
anchors.centerIn: parent
text: "boost " + root.boostText()
font.family: "monospace"
font.pointSize: 7.5
color: root.boostState === 1 ? root.okColor : root.warnColor
}
}
Rectangle {
visible: root.baselinePresent
implicitWidth: baseLabel.implicitWidth + 10
implicitHeight: baseLabel.implicitHeight + 4
radius: 4
color: Qt.rgba(root.accentPerf.r, root.accentPerf.g, root.accentPerf.b, 0.14)
border.width: 1
border.color: Qt.rgba(root.accentPerf.r, root.accentPerf.g, root.accentPerf.b, 0.4)
PlasmaComponents.Label {
id: baseLabel
anchors.centerIn: parent
text: "baseline salvata"
font.family: "monospace"
font.pointSize: 7.5
color: root.accentPerf
}
}
}
// ----------------------------------------------- avvisi di configurazione
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: setupLabel.implicitHeight + 6
visible: root.setupWarning() !== ""
radius: 4
color: Qt.rgba(root.setupWarningColor().r, root.setupWarningColor().g,
root.setupWarningColor().b, 0.14)
border.width: 1
border.color: Qt.rgba(root.setupWarningColor().r, root.setupWarningColor().g,
root.setupWarningColor().b, 0.45)
PlasmaComponents.Label {
id: setupLabel
anchors.centerIn: parent
width: parent.width - 10
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
font.pointSize: 7
color: root.setupWarningColor()
text: root.setupWarning()
}
}
// --------------------------------------------------------------- profili
PlasmaComponents.Label {
text: "PROFILO"
font.family: "monospace"
font.pointSize: 7.5
font.bold: true
color: root.dim
}
Grid {
Layout.fillWidth: true
columns: 2
spacing: 4
Repeater {
model: root.presetModel
delegate: Rectangle {
id: presetButton
required property var modelData
readonly property bool active: root.profile === modelData.key
width: (parent.width - 4) / 2
height: 26
radius: 5
color: active
? Qt.rgba(modelData.color.r, modelData.color.g, modelData.color.b, 0.22)
: (presetHover.containsMouse ? Qt.rgba(1, 1, 1, 0.10) : Qt.rgba(1, 1, 1, 0.05))
border.width: 1
border.color: active
? Qt.rgba(modelData.color.r, modelData.color.g, modelData.color.b, 0.8)
: Qt.rgba(1, 1, 1, 0.10)
opacity: root.busy ? 0.6 : 1.0
Behavior on color { ColorAnimation { duration: 120 } }
ColumnLayout {
anchors.centerIn: parent
spacing: -2
PlasmaComponents.Label {
Layout.alignment: Qt.AlignHCenter
text: presetButton.modelData.label
font.family: "monospace"
font.pointSize: 8.5
font.bold: presetButton.active
color: presetButton.active ? presetButton.modelData.color : Kirigami.Theme.textColor
}
PlasmaComponents.Label {
Layout.alignment: Qt.AlignHCenter
text: presetButton.modelData.hint
font.family: "monospace"
font.pointSize: 6.5
color: root.dim
}
}
MouseArea {
id: presetHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.runAction(root.privileged("apply " + presetButton.modelData.key))
}
}
}
}
// -------------------------------------------------------------- governor
PlasmaComponents.Label {
text: "GOVERNOR"
font.family: "monospace"
font.pointSize: 7.5
font.bold: true
color: root.dim
}
Flow {
Layout.fillWidth: true
Layout.preferredHeight: childrenRect.height
spacing: 4
Repeater {
model: root.governors
delegate: ChoicePill {
required property string modelData
text: modelData
active: root.governor === modelData
accent: root.profileColor
enabled: !root.busy
onClicked: root.runAction(root.privileged("set-governor " + modelData))
}
}
}
// ------------------------------------------------------------------- EPP
PlasmaComponents.Label {
text: "EPP"
font.family: "monospace"
font.pointSize: 7
font.bold: true
color: root.dim
}
Flow {
Layout.fillWidth: true
Layout.preferredHeight: childrenRect.height
visible: root.eppSupported
spacing: 4
Repeater {
model: root.epps
delegate: ChoicePill {
required property string modelData
text: root.eppShort(modelData)
active: root.epp === modelData
accent: root.profileColor
enabled: !root.busy
onClicked: root.runAction(root.privileged("set-epp " + modelData))
}
}
}
// ---------------------------------------------------------------- limiti
RowLayout {
Layout.fillWidth: true
spacing: 6
PlasmaComponents.Label {
text: "LIMITI"
font.family: "monospace"
font.pointSize: 7.5
font.bold: true
color: root.dim
}
Item { Layout.fillWidth: true }
PlasmaComponents.Label {
text: root.ghz(root.minKhz) + " – " + root.ghz(root.maxKhz)
font.family: "monospace"
font.pointSize: 7.5
color: root.profileColor
}
PlasmaComponents.Label {
text: "hw " + root.ghzShort(root.hwMin) + "–" + root.ghzShort(root.hwMax)
font.family: "monospace"
font.pointSize: 7
color: root.dim
}
}
RowLayout {
Layout.fillWidth: true
spacing: 6
PlasmaComponents.Label {
text: "min"
font.family: "monospace"
font.pointSize: 7.5
color: root.dim
Layout.preferredWidth: 26
}
PlasmaComponents.Slider {
id: minSlider
Layout.fillWidth: true
from: root.hwMin
to: root.hwMax
enabled: root.hwMax > root.hwMin && !root.busy
onPressedChanged: if (!pressed) root.applyLimits("min")
}
PlasmaComponents.Label {
text: root.ghzShort(minSlider.pressed ? minSlider.value : root.minKhz)
font.family: "monospace"
font.pointSize: 7.5
color: Kirigami.Theme.textColor
horizontalAlignment: Text.AlignRight
Layout.preferredWidth: 32
}
}
RowLayout {
Layout.fillWidth: true
spacing: 6
PlasmaComponents.Label {
text: "max"
font.family: "monospace"
font.pointSize: 7.5
color: root.dim
Layout.preferredWidth: 26
}
PlasmaComponents.Slider {
id: maxSlider
Layout.fillWidth: true
from: root.hwMin
to: root.hwMax
enabled: root.hwMax > root.hwMin && !root.busy
onPressedChanged: if (!pressed) root.applyLimits("max")
}
PlasmaComponents.Label {
text: root.ghzShort(maxSlider.pressed ? maxSlider.value : root.maxKhz)
font.family: "monospace"
font.pointSize: 7.5
color: Kirigami.Theme.textColor
horizontalAlignment: Text.AlignRight
Layout.preferredWidth: 32
}
}
// ----------------------------------------------------------------- boost
RowLayout {
Layout.fillWidth: true
spacing: 4
PlasmaComponents.Label {
Layout.fillWidth: true
text: "BOOST / TURBO"
font.family: "monospace"
font.pointSize: 7.5
font.bold: true
color: root.dim
}
ChoicePill {
text: "on"
active: root.boostState === 1
accent: root.okColor
enabled: root.boostSupported && !root.busy && root.boostState !== 1
onClicked: root.runAction(root.privileged("set-boost 1"))
}
ChoicePill {
text: "off"
active: root.boostState === 0
accent: root.warnColor
enabled: root.boostSupported && !root.busy && root.boostState !== 0
onClicked: root.runAction(root.privileged("set-boost 0"))
}
}
// ------------------------------------------------------------------ task
RowLayout {
Layout.fillWidth: true
spacing: 6
PlasmaComponents.Label {
text: "TASK CON PROFILO DEDICATO"
font.family: "monospace"
font.pointSize: 7.5
font.bold: true
color: root.dim
}
Item { Layout.fillWidth: true }
PlasmaComponents.ToolButton {
icon.name: root.addFormOpen ? "dialog-cancel" : "list-add"
text: root.addFormOpen ? "Annulla" : "Nuovo"
display: PlasmaComponents.ToolButton.TextBesideIcon
onClicked: root.addFormOpen = !root.addFormOpen
}
}
Repeater {
model: root.tasks
delegate: Rectangle {
id: taskRow
required property var modelData
Layout.fillWidth: true
implicitHeight: 26
radius: 5
color: modelData.running
? Qt.rgba(root.accentPerf.r, root.accentPerf.g, root.accentPerf.b, 0.16)
: Qt.rgba(1, 1, 1, 0.05)
RowLayout {
anchors.fill: parent
anchors.leftMargin: 6
anchors.rightMargin: 4
spacing: 4
Rectangle {
Layout.alignment: Qt.AlignVCenter
width: 6
height: 6
radius: 3
color: taskRow.modelData.running ? root.okColor : root.dim
opacity: taskRow.modelData.running ? 1 : 0.5
}
PlasmaComponents.Label {
Layout.fillWidth: true
text: taskRow.modelData.name
elide: Text.ElideRight
font.pointSize: 8
color: Kirigami.Theme.textColor
}
PlasmaComponents.Label {
text: taskRow.modelData.profile
font.family: "monospace"
font.pointSize: 7
color: root.dim
}
PlasmaComponents.ToolButton {
implicitWidth: 22
implicitHeight: 22
icon.name: taskRow.modelData.running ? "media-playback-stop" : "media-playback-start"
enabled: !root.busy
onClicked: {
var verb = taskRow.modelData.running ? " stop " : " run "
root.runAction(root.taskScript + verb + taskRow.modelData.slug)
}
}
PlasmaComponents.ToolButton {
implicitWidth: 22
implicitHeight: 22
icon.name: "edit-delete"
enabled: !root.busy && !taskRow.modelData.running
onClicked: root.runAction(root.taskScript + " remove " + taskRow.modelData.slug)
}
}
}
}
// ------------------------------------------------------- form nuovo task
ColumnLayout {
Layout.fillWidth: true
spacing: 4
visible: root.addFormOpen
PlasmaComponents.TextField {
Layout.fillWidth: true
placeholderText: "Nome del task (es. Render Blender)"
text: root.newTaskName
onTextChanged: root.newTaskName = text
}
PlasmaComponents.TextField {
Layout.fillWidth: true
placeholderText: "Comando (es. blender -b scena.blend -f 1)"
text: root.newTaskCommand
onTextChanged: root.newTaskCommand = text
}
PlasmaComponents.ComboBox {
Layout.fillWidth: true
model: root.presetModel
textRole: "label"
currentIndex: root.newTaskProfile
onActivated: root.newTaskProfile = currentIndex
}
RowLayout {
Layout.fillWidth: true
spacing: 4
Item { Layout.fillWidth: true }
PlasmaComponents.Button {
text: "Aggiungi"
icon.name: "list-add"
enabled: root.newTaskName.trim() !== "" && root.newTaskCommand.trim() !== ""
onClicked: root.addTask()
}
}
}
// ---------------------------------------------------------------- footer
RowLayout {
Layout.fillWidth: true
spacing: 4
PlasmaComponents.Label {
Layout.fillWidth: true
text: root.message !== ""
? root.message
: (root.busy ? "Applico…" : "Pronto")
color: root.messageIsError ? root.critColor : root.dim
font.pointSize: 7.5
elide: Text.ElideRight
}
PlasmaComponents.ToolButton {
icon.name: "view-refresh"
text: "Reset"
display: PlasmaComponents.ToolButton.TextBesideIcon
enabled: !root.busy
onClicked: root.runAction(root.privileged("reset"))
}
}
}
}
}