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.
1140 lines
43 KiB
QML
1140 lines
43 KiB
QML
/*
|
||
* 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"))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|