commit d4902b0b7276bc1324cee3ee90fbbd86cfbeb081 Author: enne2 Date: Sun Oct 25 20:42:52 2026 +0100 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. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0346a7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Artefatti Plasma / Qt +*.plasmoid +*.qmlc +*.jsc +build/ +package/ + +# Editor e sistema +*~ +*.swp +.DS_Store +*.backup-* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..35c73b3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 enne2 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c32fe1 --- /dev/null +++ b/README.md @@ -0,0 +1,163 @@ +# CPU Frequency Control (org.enne2.cpufreq) + +Plasmoide KDE Plasma 6 per pilotare il profilo energetico della CPU: governor, +limiti di frequenza (min/max), `energy_performance_preference` (EPP), boost/turbo, +quattro profili pronti e **task con profilo dedicato** che ripristina lo stato +precedente quando il comando finisce. + +Verificato su **frigate** (Fedora 44, Plasma 6.7.5 Wayland, kernel 7.2, AMD +Ryzen 7 H 255, driver `amd-pstate-epp` in modalità `active`, 16 policy). + +![widget](docs/widget.png) + +## Cosa fa + +| Area | Azione | +|---|---| +| Profili | **Silenzioso** (EPP power, boost off, tetto 35%), **Risparmio** (balance_power, boost off, tetto 60%), **Bilanciato** (balance_performance, boost on, nessun tetto), **Performance** (governor performance, EPP performance, boost on) | +| Governor | pillole con i valori realmente disponibili in `scaling_available_governors` | +| EPP | pillole con i valori di `energy_performance_available_preferences` | +| Limiti | due slider min/max in kHz con validazione e rilettura; il valore applicato è arrotondato a 10 MHz | +| Boost | pillole `on`/`off` (per-policy su kernel recenti, altrimenti globale, altrimenti `intel_pstate/no_turbo`) | +| Task | elenco configurabile: ogni task applica un profilo, esegue il comando **come utente** e ripristina la baseline all'uscita | +| Footer | messaggio dell'ultima azione e pulsante **Reset** (default hardware: boost on, nessun tetto) | + +La vista compatta (pannello) mostra frequenza corrente e un badge `no boost`. +La frequenza è la massima fra le policy (core più attivo), con la media sotto. + +## Architettura e modello di sicurezza + +``` +org.enne2.cpufreq (QML, non privilegiato) + ├─ contents/scripts/cpufreq-state.sh lettura sysfs, /proc, hwmon (nessun privilegio) + ├─ contents/scripts/enne2-cpufreq-task launcher task (utente) + └─ pkexec /usr/local/libexec/enne2-cpufreq/cpufreq-helper (root) + ├─ comandi a elenco chiuso, argomenti validati contro sysfs + ├─ ogni scrittura riletta per verifica + └─ /var/lib/enne2-cpufreq/{baseline,refcount} (root:root 0755/0644) + +polkit + ├─ /usr/share/polkit-1/actions/org.enne2.cpufreq.set.policy azione dedicata + └─ /etc/polkit-1/rules.d/50-org.enne2.cpufreq.rules allow senza prompt +``` + +- Il widget non è mai root: l'unica operazione privilegiata è l'helper, con + sottocomandi fissi (`set-governor`, `set-epp`, `set-limits`, `set-boost`, + `apply`, `reset`, `task-begin`, `task-end`, `save-state`, `restore-state`). +- L'annotazione `org.freedesktop.policykit.exec.path` lega l'azione polkit a quel + solo binario: nessun altro programma eredita il permesso. +- La regola in `/etc/polkit-1/rules.d/` concede `polkit.Result.YES` solo a utenti + **locali**, con sessione **attiva**, nel gruppo **wheel**. Senza quel file la + `.policy` chiede la password a ogni modifica (`auth_admin_keep`). +- I comandi dei task girano come utente (`setsid sh -c`), mai come root. + +## Installazione + +```sh +cd ~/Dev/kde-cpufreq-dashboard +./install.sh # widget (utente) + backend (sudo) +./install.sh --widget # solo il plasmoide +./install.sh --backend # solo helper + polkit (root) +./install.sh --no-polkit-rule # backend senza la regola "nessun prompt" +./install.sh --dry-run # mostra le operazioni +``` + +Il widget si aggiunge da *Aggiungi widget → «CPU Frequency Control»* oppure via +scripting Plasma: + +```js +desktops()[0].addWidget("org.enne2.cpufreq") +``` + +Disinstallazione: `./uninstall.sh` (ripristina la baseline prima di rimuovere +l'helper). I task configurati restano in `~/.config/enne2-cpufreq/tasks.json`. + +## Task con profilo dedicato + +Dall'interfaccia: **Nuovo** → nome, comando, profilo. Da terminale: + +```sh +enne2-cpufreq-task list +enne2-cpufreq-task add "Render Blender" performance "blender -b scena.blend -f 1" +enne2-cpufreq-task run render-blender +enne2-cpufreq-task stop render-blender +enne2-cpufreq-task active +``` + +Sequenza di `run`: + +1. `helper task-begin `: salva la baseline (solo al primo task) e applica il profilo +2. il comando parte staccato (`setsid`) e scrive il proprio pid in + `$XDG_RUNTIME_DIR/enne2-cpufreq/.pid` +3. un watcher attende l'uscita del pid e chiama `helper task-end`, che decrementa + un contatore e ripristina la baseline a zero (con `notify-send` di conferma) + +Più task in parallelo sono contati: il ripristino avviene solo quando l'ultimo +termina. `enne2-cpufreq-task remove` rifiuta di rimuovere un task in esecuzione. + +## Comportamento sui driver + +- **amd-pstate-epp (active)**: `powersave`/`performance` sono policy del driver; + `powersave` *non* blocca la frequenza al minimo, EPP è il vero bias. + Con `boost=0` il kernel abbassa `cpuinfo_max_freq` alla frequenza nominale + (3.80 GHz invece di 4.97 GHz): il tetto reale viene letto da + `amd_pstate_max_freq`, e per questo `apply` imposta **prima** il boost e poi i + limiti. +- **intel_pstate**: boost tramite `intel_pstate/no_turbo` (invertito). +- **Altri driver cpufreq**: governor e limiti restano validi; EPP e boost + compaiono solo se le interfacce esistono. + +## Servizi concorrenti + +Se `tuned`, `power-profiles-daemon` o `tlp` sono attivi il widget lo segnala: il +profilo `balanced` di tuned scrive esattamente `governor`, `energy_performance_preference` +e `boost`, quindi un `tuned-adm profile` o il boot possono sovrascrivere le +impostazioni. Su frigate `tuned` è attivo: le modifiche valgono fino al prossimo +apply di tuned. + +## Diagnosi + +```sh +/usr/local/libexec/enne2-cpufreq/cpufreq-helper status # stato, baseline, refcount +~/.local/share/plasma/plasmoids/org.enne2.cpufreq/contents/scripts/cpufreq-state.sh +pkexec /usr/local/libexec/enne2-cpufreq/cpufreq-helper apply balanced +journalctl --user -u plasma-plasmashell.service -f | grep cpufreq +``` + +| Sintomo | Causa | +|---|---| +| banner rosso «Backend non installato» | manca `/usr/local/libexec/enne2-cpufreq/` → `./install.sh --backend` | +| dialog password a ogni modifica | manca la regola polkit (o utente fuori da `wheel`) | +| «autorizzazione negata da polkit» | regola non caricata: `systemctl try-reload-or-restart polkit` | +| valori che tornano indietro | servizio concorrente (tuned/power-profiles-daemon/tlp) | +| il widget resta 68×68 | `implicitWidth/Height` del plasmoide non validi: verificare `main.qml` | + +## Note di implementazione Plasma 6 + +- `metadata.json` con `X-Plasma-API-Minimum-Version: "6.0"`, senza chiavi Plasma 5. +- Comandi via `org.kde.plasma.plasma5support` (`engine: "executable"`); ogni + azione usa un source univoco (contatore in coda) perché il motore non riesegue + un source già noto. +- `syncControls()` è protetto: `applyState()` gira ogni 2 s e la rappresentazione + piena può non esistere (popup chiuso) → senza guardia il `ReferenceError` su un + id interrompeva la sincronizzazione dei controlli. +- `implicitWidth/implicitHeight` del `Flickable` sono costanti: se dipendono dal + layout il containment riduce il widget alla dimensione minima (68×68). +- Nessun `Switch` per il boost: `toggled` scatta anche quando il valore viene + riallineato dallo stato. Le pillole sono prive di stato interno. + +## File + +``` +metadata.json +contents/ui/main.qml interfaccia (compact + full, Flickable) +contents/ui/ChoicePill.qml pillola selezionabile +contents/scripts/profiles.sh tabella profili condivisa (unica fonte) +contents/scripts/cpufreq-state.sh lettura stato non privilegiata +contents/scripts/enne2-cpufreq-task launcher task (utente) +bin/cpufreq-helper helper privilegiato (root) +polkit/*.policy, polkit/*.rules azione e regola polkit +install.sh, uninstall.sh +``` + +Licenza MIT. diff --git a/bin/cpufreq-helper b/bin/cpufreq-helper new file mode 100755 index 0000000..6e753f4 --- /dev/null +++ b/bin/cpufreq-helper @@ -0,0 +1,410 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT +# enne2-cpufreq — helper privilegiato a interfaccia ristretta. +# +# Installato come /usr/local/libexec/enne2-cpufreq/cpufreq-helper (root:root 0755), +# invocato dal plasmoide org.enne2.cpufreq tramite pkexec e una azione polkit +# dedicata (org.enne2.cpufreq.set). Non accetta comandi arbitrari: solo i +# sottocomandi elencati in usage() e con argomenti validati contro sysfs. +# +# Uscita: righe key=value; ok=1 in caso di successo, ok=0 + error= + exit 2 +# in caso di rifiuto. Ogni scrittura viene riletta per verifica. +set -u + +PATH=/usr/sbin:/usr/bin:/sbin:/bin +export PATH +umask 022 + +CMD=${1:-status} +[ "$#" -gt 0 ] && shift + +SELF_DIR=$(dirname "$0") +if [ -r "$SELF_DIR/profiles.sh" ]; then + # shellcheck source=../../contents/scripts/profiles.sh + . "$SELF_DIR/profiles.sh" +else + printf 'ok=0\nerror=profiles.sh mancante accanto all helper\n' + exit 2 +fi + +CPUFREQ=/sys/devices/system/cpu/cpufreq +INTEL_PSTATE=/sys/devices/system/cpu/intel_pstate +STATE_DIR=/var/lib/enne2-cpufreq +BASELINE=$STATE_DIR/baseline +REFCOUNT=$STATE_DIR/refcount + +usage() { + cat <<'EOF' +uso: cpufreq-helper [argomenti] + status stato corrente (leggibile anche senza root) + set-governor powersave | performance | ... + set-epp default | performance | balance_performance | balance_power | power + set-limits + set-boost <0|1> + apply quiet | powersave | balanced | performance + reset ripristina i default hardware (boost on, nessun tetto) + save-state salva lo stato corrente come baseline + restore-state ripristina la baseline salvata + task-begin salva la baseline (se serve) e applica il profilo + task-end [--all] decrementa il contatore e ripristina la baseline a zero +EOF +} + +fail() { + printf 'ok=0\nerror=%s\n' "$*" + exit 2 +} + +readf() { # readf -> valore o exit 1 + [ -r "$1" ] || return 1 + _val= + IFS= read -r _val < "$1" 2>/dev/null || [ -n "$_val" ] || return 1 + printf '%s\n' "$_val" +} + +policies() { + for _p in "$CPUFREQ"/policy[0-9]*; do + [ -d "$_p" ] && printf '%s\n' "$_p" + done +} + +write_one() { # write_one + [ -w "$1" ] || return 1 + printf '%s\n' "$2" > "$1" 2>/dev/null +} + +# Scrive un attributo su TUTTE le policy. Imposta WROTE e FAILED. +write_all() { + _attr=$1 + _val=$2 + WROTE=0 + FAILED=0 + for _p in $(policies); do + if write_one "$_p/$_attr" "$_val"; then + WROTE=$((WROTE + 1)) + else + FAILED=$((FAILED + 1)) + fi + done + [ "$FAILED" -eq 0 ] && [ "$WROTE" -gt 0 ] +} + +# Valori distinti letti per un attributo, spazio-separati. +attr_values() { + for _p in $(policies); do + readf "$_p/$1" + done | sort -u | tr '\n' ' ' | sed 's/ *$//' +} + +p0=$(policies | head -n 1) +[ -n "$p0" ] || fail "nessuna policy cpufreq in $CPUFREQ" + +driver=$(readf "$p0/scaling_driver" || echo unknown) +hw_min=$(readf "$p0/cpuinfo_min_freq" || echo 0) +hw_max=$(cf_hw_max_khz "$p0") # indipendente dallo stato del boost +floor=$(readf "$p0/amd_pstate_lowest_nonlinear_freq" || echo "") +[ -n "$floor" ] && [ "$floor" -gt "$hw_min" ] || floor=$hw_min + +boost_global=$CPUFREQ/boost +[ -w "$boost_global" ] || [ -r "$boost_global" ] || boost_global="" + +boost_read() { + for _p in $(policies); do + readf "$_p/boost" && return 0 + done + [ -n "$boost_global" ] && readf "$boost_global" && return 0 + readf "$INTEL_PSTATE/no_turbo" >/dev/null 2>&1 || return 1 + _nt=$(readf "$INTEL_PSTATE/no_turbo") || return 1 + [ "$_nt" = "1" ] && echo 0 || echo 1 +} + +print_state() { + printf 'driver=%s\n' "$driver" + printf 'mode=%s\n' "$(readf /sys/devices/system/cpu/amd_pstate/status || echo n/a)" + printf 'governor=%s\n' "$(attr_values scaling_governor)" + printf 'epp=%s\n' "$(attr_values energy_performance_preference)" + printf 'min_khz=%s\n' "$(readf "$p0/scaling_min_freq")" + printf 'max_khz=%s\n' "$(readf "$p0/scaling_max_freq")" + printf 'hw_min_khz=%s\n' "$hw_min" + printf 'hw_max_khz=%s\n' "$hw_max" + printf 'floor_khz=%s\n' "$floor" + printf 'boost=%s\n' "$(boost_read || echo n/a)" + printf 'policies=%s\n' "$(policies | wc -l | tr -d ' ')" + printf 'baseline=%s\n' "$([ -r "$BASELINE" ] && echo present || echo absent)" + printf 'refcount=%s\n' "$([ -r "$REFCOUNT" ] && readf "$REFCOUNT" || echo 0)" +} + +set_governor() { + _g=${1:-} + [ -n "$_g" ] || fail "governor mancante" + case "$_g" in *[!a-z_-]*) fail "governor non valido: $_g" ;; esac + _avail=$(readf "$p0/scaling_available_governors") || fail "governor non leggibili" + case " $_avail " in + *" $_g "*) ;; + *) fail "governor '$_g' non disponibile ($_avail)" ;; + esac + write_all scaling_governor "$_g" || fail "scrittura scaling_governor fallita su $FAILED policy" + _now=$(attr_values scaling_governor) + [ "$_now" = "$_g" ] || fail "verifica governor fallita: letto '$_now'" + printf 'governor=%s\n' "$_g" +} + +set_epp() { + _e=${1:-} + [ -n "$_e" ] || fail "preferenza EPP mancante" + case "$_e" in *[!a-z_]*) fail "preferenza EPP non valida: $_e" ;; esac + _avail=$(readf "$p0/energy_performance_available_preferences") \ + || fail "questo driver non espone energy_performance_preference" + case " $_avail " in + *" $_e "*) ;; + *) fail "EPP '$_e' non disponibile ($_avail)" ;; + esac + if write_all energy_performance_preference "$_e"; then + printf 'epp=%s\n' "$_e" + return 0 + fi + # Con governor "performance" il driver EPP può ignorare la preferenza: non è + # un errore fatale, viene segnalato come warning. + if [ "$(attr_values scaling_governor)" = "performance" ]; then + printf 'warning=EPP non applicato con governor performance\n' + return 0 + fi + fail "scrittura EPP fallita su $FAILED policy" +} + +set_boost() { + _b=${1:-} + case "$_b" in 0|1) ;; *) fail "boost deve essere 0 o 1" ;; esac + _count=0 + for _p in $(policies); do + if [ -w "$_p/boost" ] && write_one "$_p/boost" "$_b"; then + _count=$((_count + 1)) + fi + done + if [ -n "$boost_global" ] && write_one "$boost_global" "$_b"; then + _count=$((_count + 1)) + fi + if [ -w "$INTEL_PSTATE/no_turbo" ] && write_one "$INTEL_PSTATE/no_turbo" $((1 - _b)); then + _count=$((_count + 1)) + fi + [ "$_count" -gt 0 ] || fail "nessun controllo boost scrivibile su questo sistema" + _now=$(boost_read || echo "") + [ "$_now" = "$_b" ] || printf 'warning=lettura boost dopo la scrittura: %s\n' "${_now:-n/d}" + printf 'boost=%s\n' "$_b" +} + +set_limits() { + _min=${1:-} + _max=${2:-} + case "$_min$_max" in *[!0-9]*) fail "limiti non numerici (kHz interi)" ;; esac + [ -n "$_min" ] && [ -n "$_max" ] || fail "servono min_khz e max_khz" + [ "$_min" -ge "$hw_min" ] || fail "minimo $_min sotto il limite hardware $hw_min" + [ "$_max" -le "$hw_max" ] || fail "massimo $_max sopra il limite hardware $hw_max" + [ "$_min" -le "$_max" ] || fail "minimo $_min maggiore del massimo $_max" + _cur_min=$(readf "$p0/scaling_min_freq" || echo "$hw_min") + _cur_max=$(readf "$p0/scaling_max_freq" || echo "$hw_max") + # Ordine di scrittura sicuro: mai uno stato intermedio con min > max. + if [ "$_min" -gt "$_cur_max" ]; then + write_all scaling_max_freq "$_max" || fail "pre-allargamento del tetto fallito" + fi + if [ "$_max" -lt "$_cur_min" ]; then + write_all scaling_min_freq "$_min" || fail "pre-abbassamento del minimo fallito" + fi + write_all scaling_min_freq "$_min" || fail "scrittura scaling_min_freq fallita su $FAILED policy" + write_all scaling_max_freq "$_max" || fail "scrittura scaling_max_freq fallita su $FAILED policy" + printf 'min_khz=%s\n' "$(readf "$p0/scaling_min_freq")" + printf 'max_khz=%s\n' "$(readf "$p0/scaling_max_freq")" +} + +apply_profile() { + _pr=${1:-} + cf_profile_known "$_pr" || fail "profilo '$_pr' sconosciuto ($(cf_profiles | tr '\n' ' '))" + _gov=$(cf_profile_gov "$_pr") + _epp=$(cf_profile_epp "$_pr") + _bst=$(cf_profile_boost "$_pr") + _maxf=$(cf_profile_max_khz "$_pr" "$floor" "$hw_max") + [ "$_maxf" -gt "$hw_max" ] && _maxf=$hw_max + [ "$_maxf" -lt "$floor" ] && _maxf=$floor + printf 'profile=%s\n' "$_pr" + # Ordine: 1) boost, 2) governor, 3) limiti, 4) EPP. + # Il boost viene per primo perché con boost disattivato il kernel riduce il + # tetto nominale e una scrittura di scaling_max_freq oltre quel valore + # verrebbe rifiutata o limitata. + set_boost "$_bst" || return 1 + if readf "$p0/scaling_available_governors" | grep -qw -- "$_gov"; then + set_governor "$_gov" || return 1 + else + printf 'warning=governor %s non disponibile, mantenuto %s\n' \ + "$_gov" "$(attr_values scaling_governor)" + fi + set_limits "$floor" "$_maxf" || return 1 + if readf "$p0/energy_performance_available_preferences" >/dev/null 2>&1; then + set_epp "$_epp" || return 1 + fi + printf 'profile_label=%s\n' "$(cf_profile_label "$_pr")" +} + +ensure_state_dir() { + if [ ! -d "$STATE_DIR" ]; then + mkdir -p "$STATE_DIR" || fail "impossibile creare $STATE_DIR" + chmod 755 "$STATE_DIR" + fi +} + +save_state() { + ensure_state_dir + { + printf 'governor=%s\n' "$(readf "$p0/scaling_governor")" + printf 'epp=%s\n' "$(readf "$p0/energy_performance_preference" || echo "")" + printf 'min_khz=%s\n' "$(readf "$p0/scaling_min_freq")" + printf 'max_khz=%s\n' "$(readf "$p0/scaling_max_freq")" + printf 'boost=%s\n' "$(boost_read || echo "")" + printf 'saved_at=%s\n' "$(date +%s)" + } > "$BASELINE" || fail "scrittura baseline fallita" + chmod 644 "$BASELINE" + printf 'baseline=saved\n' + cat "$BASELINE" +} + +restore_state() { + [ -r "$BASELINE" ] || fail "nessuna baseline salvata" + _gov=$(sed -n 's/^governor=//p' "$BASELINE") + _epp=$(sed -n 's/^epp=//p' "$BASELINE") + _min=$(sed -n 's/^min_khz=//p' "$BASELINE") + _max=$(sed -n 's/^max_khz=//p' "$BASELINE") + _bst=$(sed -n 's/^boost=//p' "$BASELINE") + if [ -n "$_gov" ]; then + if readf "$p0/scaling_available_governors" | grep -qw -- "$_gov"; then + write_all scaling_governor "$_gov" || printf 'warning=governor non ripristinato\n' + fi + fi + if [ -n "$_min" ] && [ -n "$_max" ]; then + case "$_min$_max" in + *[!0-9]*) ;; + *) set_limits "$_min" "$_max" || printf 'warning=limiti non ripristinati\n' ;; + esac + fi + if [ -n "$_epp" ] && readf "$p0/energy_performance_available_preferences" >/dev/null 2>&1; then + write_all energy_performance_preference "$_epp" || printf 'warning=EPP non ripristinato\n' + fi + if [ -n "$_bst" ]; then + case "$_bst" in + 0|1) set_boost "$_bst" >/dev/null || printf 'warning=boost non ripristinato\n' ;; + esac + fi + printf 'restored=1\n' + print_state +} + +reset_cmd() { + _gov=powersave + if ! readf "$p0/scaling_available_governors" | grep -qw -- "$_gov"; then + _gov=$(readf "$p0/scaling_available_governors" | awk '{print $1}') + fi + printf 'profile=reset\n' + [ -n "$_gov" ] && set_governor "$_gov" + set_limits "$floor" "$hw_max" || return 1 + if readf "$p0/energy_performance_available_preferences" >/dev/null 2>&1; then + _epp=balance_performance + if ! readf "$p0/energy_performance_available_preferences" | grep -qw -- "$_epp"; then + _epp=default + fi + set_epp "$_epp" || return 1 + fi + set_boost 1 || return 1 +} + +task_begin() { + _pr=${1:-} + cf_profile_known "$_pr" || fail "profilo '$_pr' sconosciuto" + ensure_state_dir + _rc=0 + [ -r "$REFCOUNT" ] && _rc=$(readf "$REFCOUNT") + case "${_rc:-0}" in *[!0-9]*|"") _rc=0 ;; esac + if [ "$_rc" -eq 0 ] || [ ! -r "$BASELINE" ]; then + save_state >/dev/null || fail "salvataggio dello stato corrente fallito" + fi + apply_profile "$_pr" || fail "applicazione del profilo '$_pr' fallita" + _rc=$((_rc + 1)) + printf '%s\n' "$_rc" > "$REFCOUNT" + chmod 644 "$REFCOUNT" + printf 'refcount=%s\n' "$_rc" +} + +task_end() { + ensure_state_dir + _rc=0 + [ -r "$REFCOUNT" ] && _rc=$(readf "$REFCOUNT") + case "${_rc:-0}" in *[!0-9]*|"") _rc=0 ;; esac + if [ "${1:-}" = "--all" ]; then + _rc=0 + else + _rc=$((_rc - 1)) + [ "$_rc" -lt 0 ] && _rc=0 + fi + printf '%s\n' "$_rc" > "$REFCOUNT" + chmod 644 "$REFCOUNT" + if [ "$_rc" -eq 0 ] && [ -r "$BASELINE" ]; then + restore_state >/dev/null || printf 'warning=ripristino della baseline fallito\n' + rm -f "$BASELINE" + printf 'restored=1\n' + fi + printf 'refcount=%s\n' "$_rc" +} + +case "$CMD" in + status) + print_state + ;; + usage|-h|--help|help) + usage + ;; + set-governor) + [ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec" + set_governor "${1:-}" + ;; + set-epp) + [ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec" + set_epp "${1:-}" + ;; + set-limits) + [ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec" + set_limits "${1:-}" "${2:-}" + ;; + set-boost) + [ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec" + set_boost "${1:-}" + ;; + apply) + [ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec" + apply_profile "${1:-}" + ;; + reset) + [ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec" + reset_cmd + ;; + save-state) + [ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec" + save_state + ;; + restore-state) + [ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec" + restore_state + ;; + task-begin) + [ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec" + task_begin "${1:-}" + ;; + task-end) + [ "$(id -u)" -eq 0 ] || fail "operazione privilegiata: usare pkexec" + task_end "${1:-}" + ;; + *) + printf 'ok=0\nerror=comando sconosciuto: %s\n' "$CMD" + usage + exit 2 + ;; +esac + +printf 'ok=1\n' +exit 0 diff --git a/contents/scripts/cpufreq-state.sh b/contents/scripts/cpufreq-state.sh new file mode 100755 index 0000000..936b43d --- /dev/null +++ b/contents/scripts/cpufreq-state.sh @@ -0,0 +1,175 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT +# enne2-cpufreq — lettore di stato NON privilegiato per il plasmoide. +# Legge solo attributi world-readable di sysfs, /proc e hwmon; non scrive nulla. +# Uscita: righe key=value (formato usato dagli altri widget org.enne2.*). +set -u + +SELF_DIR=$(dirname "$0") +if [ -r "$SELF_DIR/profiles.sh" ]; then + # shellcheck source=profiles.sh + . "$SELF_DIR/profiles.sh" +else + printf 'helper=missing\nerror=profiles.sh mancante\n' + exit 2 +fi + +CPUFREQ=/sys/devices/system/cpu/cpufreq +HELPER=/usr/local/libexec/enne2-cpufreq/cpufreq-helper +POLICY_POLKIT=/usr/share/polkit-1/actions/org.enne2.cpufreq.set.policy +RULE_POLKIT=/etc/polkit-1/rules.d/50-org.enne2.cpufreq.rules +CONF_DIR=${XDG_CONFIG_HOME:-$HOME/.config}/enne2-cpufreq +TASKS=$CONF_DIR/tasks.json +RUN=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/enne2-cpufreq + +readf() { + [ -r "$1" ] || return 1 + _val= + IFS= read -r _val < "$1" 2>/dev/null || [ -n "$_val" ] || return 1 + printf '%s\n' "$_val" +} + +policies() { + for _p in "$CPUFREQ"/policy[0-9]*; do + [ -d "$_p" ] && printf '%s\n' "$_p" + done +} + +attr_values() { + for _p in $(policies); do + readf "$_p/$1" + done | sort -u | tr '\n' ' ' | sed 's/ *$//' +} + +p0=$(policies | head -n 1) +if [ -z "$p0" ]; then + printf 'helper=missing\nerror=nessuna policy cpufreq (driver non cpufreq?)\n' + exit 2 +fi + +driver=$(readf "$p0/scaling_driver" || echo unknown) +hw_min=$(readf "$p0/cpuinfo_min_freq" || echo 0) +hw_max=$(cf_hw_max_khz "$p0") # indipendente dallo stato del boost +floor=$(readf "$p0/amd_pstate_lowest_nonlinear_freq" || echo "") +[ -n "$floor" ] && [ "$floor" -gt "$hw_min" ] || floor=$hw_min +min_khz=$(readf "$p0/scaling_min_freq" || echo 0) +max_khz=$(readf "$p0/scaling_max_freq" || echo 0) +governor=$(attr_values scaling_governor) +governors=$(readf "$p0/scaling_available_governors" || echo "") +epp=$(attr_values energy_performance_preference) +epps=$(readf "$p0/energy_performance_available_preferences" || echo "") +epp_supported=0 +[ -n "$epps" ] && epp_supported=1 + +# boost: per-policy (kernel recenti) → globale → intel_pstate/no_turbo invertito +boost=$(readf "$p0/boost" || readf "$CPUFREQ/boost" || echo "") +boost_supported=0 +[ -n "$boost" ] && boost_supported=1 +if [ "$boost_supported" -eq 0 ] && [ -r /sys/devices/system/cpu/intel_pstate/no_turbo ]; then + _nt=$(readf /sys/devices/system/cpu/intel_pstate/no_turbo) + [ "$_nt" = "1" ] && boost=0 || boost=1 + boost_supported=1 +fi + +# frequenza istantanea: massima fra le policy (core più attivo) e media +sum=0 +cnt=0 +curmax=0 +for _p in $(policies); do + _f=$(readf "$_p/scaling_cur_freq") || continue + case "$_f" in ''|*[!0-9]*) continue ;; esac + sum=$((sum + _f)) + cnt=$((cnt + 1)) + [ "$_f" -gt "$curmax" ] && curmax=$_f +done +[ "$cnt" -gt 0 ] || cnt=1 +cur_avg=$((sum / cnt)) + +temp=$(readf /sys/class/hwmon/hwmon0/temp1_input 2>/dev/null || echo "") +for _h in /sys/class/hwmon/hwmon*; do + [ -r "$_h/name" ] || continue + case "$(readf "$_h/name")" in + k10temp|coretemp|zenpower) + temp=$(readf "$_h/temp1_input" || echo "") + break + ;; + esac +done + +model=$(sed -n 's/^model name[[:space:]]*: //p' /proc/cpuinfo | head -n 1) +load1=$(awk '{print $1}' /proc/loadavg) + +helper_state=missing +[ -x "$HELPER" ] && helper_state=ok +polkit_state=missing +if [ -r "$POLICY_POLKIT" ] && [ -r "$RULE_POLKIT" ]; then + polkit_state=ok +elif [ -r "$POLICY_POLKIT" ]; then + polkit_state=ask +fi + +conflicts="" +for _svc in tuned power-profiles-daemon tlp; do + if systemctl is-active --quiet "$_svc" 2>/dev/null; then + conflicts="$conflicts $_svc" + fi +done +conflicts=${conflicts# } + +profile=$(cf_detect_profile "${governor:-}" "${epp:-}" "${boost:-0}" "$max_khz" "$floor" "$hw_max") +printf 'driver=%s\n' "$driver" +printf 'mode=%s\n' "$(readf /sys/devices/system/cpu/amd_pstate/status || echo n/a)" +printf 'model=%s\n' "$model" +printf 'policies=%s\n' "$cnt" +printf 'governor=%s\n' "$governor" +printf 'governors=%s\n' "$governors" +printf 'epp=%s\n' "$epp" +printf 'epps=%s\n' "$epps" +printf 'epp_supported=%s\n' "$epp_supported" +printf 'boost=%s\n' "$boost" +printf 'boost_supported=%s\n' "$boost_supported" +printf 'hw_min_khz=%s\n' "$hw_min" +printf 'hw_max_khz=%s\n' "$hw_max" +printf 'floor_khz=%s\n' "$floor" +printf 'min_khz=%s\n' "$min_khz" +printf 'max_khz=%s\n' "$max_khz" +printf 'cur_khz=%s\n' "$curmax" +printf 'cur_avg_khz=%s\n' "$cur_avg" +printf 'temp_mdegc=%s\n' "$temp" +printf 'load1=%s\n' "$load1" +printf 'profile=%s\n' "$profile" +printf 'profile_label=%s\n' "$(cf_profile_label "$profile")" +printf 'helper=%s\n' "$helper_state" +printf 'polkit=%s\n' "$polkit_state" +printf 'conflicts=%s\n' "$conflicts" +[ -r /var/lib/enne2-cpufreq/baseline ] && printf 'baseline=present\n' || printf 'baseline=absent\n' + +# ---- task configurati (un file JSON gestito da enne2-cpufreq-task) ---------- +# Formato di ogni riga: task= +# Il JSON (jq -c, una riga) evita ogni ambiguità su spazi, tabulazioni e pipe +# dentro i comandi definiti dall'utente. +if command -v jq >/dev/null 2>&1 && [ -r "$TASKS" ]; then + _count=$(jq -r '.tasks | length' "$TASKS" 2>/dev/null || echo 0) + printf 'task_count=%s\n' "${_count:-0}" + _i=0 + jq -c '.tasks[] | {name: .name, profile: .profile, command: .command, slug: .slug}' \ + "$TASKS" 2>/dev/null | + while IFS= read -r _line; do + [ -n "$_line" ] || continue + _slug=$(printf '%s' "$_line" | jq -r '.slug // ""' 2>/dev/null) + _running=0 + if [ -n "$_slug" ] && [ -r "$RUN/$_slug.pid" ]; then + _pid=$(readf "$RUN/$_slug.pid") + case "$_pid" in + ''|*[!0-9]*) _running=0 ;; + *) kill -0 "$_pid" 2>/dev/null && _running=1 ;; + esac + fi + printf 'task=%s\t%s\t%s\n' "$_i" "$_running" "$_line" + _i=$((_i + 1)) + done +else + printf 'task_count=0\n' +fi + +printf 'ts=%s\n' "$(date +%s)" diff --git a/contents/scripts/enne2-cpufreq-task b/contents/scripts/enne2-cpufreq-task new file mode 100755 index 0000000..65b210f --- /dev/null +++ b/contents/scripts/enne2-cpufreq-task @@ -0,0 +1,274 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT +# enne2-cpufreq-task — launcher di task con profilo CPU dedicato. +# +# Gira SEMPRE come utente: il comando del task non viene mai eseguito come root. +# Solo il cambio di profilo passa dall'helper privilegiato via pkexec. +# +# Flusso di "run": +# 1. helper task-begin → salva la baseline (una sola volta) e applica +# 2. il comando parte staccato (setsid) e scrive il proprio pid nel file di stato +# 3. un watcher staccato attende l'uscita del pid e chiama helper task-end, che +# ripristina la baseline quando non ci sono altri task attivi +# +# Il plasmoide invoca "add-hex" con gli argomenti codificati in esadecimale +# (UTF-8): così nomi, comandi con spazi, virgolette o newline attraversano +# l'exec engine di Plasma senza dipendere dal parsing delle virgolette. +set -u + +PATH=/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin +export PATH + +SELF_DIR=$(dirname "$0") +HELPER=/usr/local/libexec/enne2-cpufreq/cpufreq-helper +CONF_DIR=${XDG_CONFIG_HOME:-$HOME/.config}/enne2-cpufreq +TASKS=$CONF_DIR/tasks.json +RUN=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/enne2-cpufreq + +mkdir -p "$CONF_DIR" "$RUN" 2>/dev/null + +fail() { + printf 'ok=0\nerror=%s\n' "$*" + exit 2 +} + +readf() { + [ -r "$1" ] || return 1 + _val= + IFS= read -r _val < "$1" 2>/dev/null || [ -n "$_val" ] || return 1 + printf '%s\n' "$_val" +} + +# Decodifica esadecimale UTF-8 senza dipendenze (xxd/od non sempre presenti). +hex_decode() { + [ -n "${1:-}" ] || return 0 + case "$1" in *[!0-9a-fA-F]*) fail "payload esadecimale non valido" ;; esac + _esc=$(printf '%s' "$1" | sed 's/\(..\)/\\x\1/g') + printf '%b' "$_esc" +} + +need_jq() { + command -v jq >/dev/null 2>&1 || fail "jq non installato (serve per gestire tasks.json)" +} + +init_tasks() { + [ -r "$TASKS" ] && return 0 + printf '{"version":1,"tasks":[]}\n' > "$TASKS" || fail "impossibile creare $TASKS" +} + +slugify() { + printf '%s' "$1" | tr 'A-Z' 'a-z' | tr -c 'A-Za-z0-9._-' '-' \ + | sed -e 's/--*/-/g' -e 's/^-//' -e 's/-$//' +} + +# Accetta indifferentemente il nome visualizzato o lo slug. +resolve_slug() { + jq -r --arg q "$1" '.tasks[] | select(.name == $q or .slug == $q) | .slug' \ + "$TASKS" 2>/dev/null | head -n 1 +} + +field_of_slug() { # field_of_slug + jq -r --arg s "$1" --arg f "$2" \ + '.tasks[] | select(.slug == $s) | .[$f] // empty' "$TASKS" 2>/dev/null | head -n 1 +} + +is_running() { # is_running + _pid=$(readf "$RUN/$1.pid") || return 1 + case "$_pid" in ''|*[!0-9]*) return 1 ;; esac + kill -0 "$_pid" 2>/dev/null +} + +helper_ready() { + [ -x "$HELPER" ] || fail "helper non installato in $HELPER (eseguire install.sh)" + command -v pkexec >/dev/null 2>&1 || fail "pkexec non disponibile (installare polkit)" +} + +do_add() { # do_add + init_tasks + need_jq + _name=${1:-} + _prof=${2:-} + _cmd=${3:-} + [ -n "$_name" ] || fail "nome del task mancante" + [ -n "$_cmd" ] || fail "comando del task mancante" + case $_prof in + quiet|powersave|balanced|performance) ;; + *) fail "profilo non valido: $_prof (quiet|powersave|balanced|performance)" ;; + esac + _slug=$(slugify "$_name") + [ -n "$_slug" ] || fail "nome non utilizzabile come slug: $_name" + if [ -n "$(resolve_slug "$_name")" ]; then + fail "esiste già un task chiamato '$_name'" + fi + _tmp=$TASKS.tmp.$$ + jq --arg n "$_name" --arg p "$_prof" --arg c "$_cmd" --arg s "$_slug" \ + '.tasks += [{name: $n, profile: $p, command: $c, slug: $s}]' "$TASKS" > "$_tmp" \ + && mv "$_tmp" "$TASKS" || { rm -f "$_tmp"; fail "scrittura tasks.json fallita"; } + printf 'ok=1\nname=%s\nprofile=%s\nslug=%s\n' "$_name" "$_prof" "$_slug" +} + +cmd_add() { # add + _name=${1:-} + _prof=${2:-} + [ "$#" -ge 3 ] || fail "servono nome, profilo e comando" + shift 2 + do_add "$_name" "$_prof" "$*" +} + +cmd_add_hex() { # add-hex + [ "$#" -ge 3 ] || fail "servono tre argomenti esadecimali" + do_add "$(hex_decode "$1")" "$(hex_decode "$2")" "$(hex_decode "$3")" +} + +cmd_list() { + init_tasks + need_jq + printf 'ok=1\n' + [ -x "$HELPER" ] && printf 'helper=ok\n' || printf 'helper=missing\n' + printf 'count=%s\n' "$(jq -r '.tasks | length' "$TASKS" 2>/dev/null || echo 0)" + _i=0 + jq -c '.tasks[] | {name: .name, profile: .profile, command: .command, slug: .slug}' \ + "$TASKS" 2>/dev/null | + while IFS= read -r _line; do + [ -n "$_line" ] || continue + _slug=$(printf '%s' "$_line" | jq -r '.slug') + _run=0 + is_running "$_slug" && _run=1 + printf 'task=%s\t%s\t%s\n' "$_i" "$_run" "$_line" + _i=$((_i + 1)) + done +} + +cmd_remove() { # remove + init_tasks + need_jq + _slug=$(resolve_slug "${1:-}") + [ -n "$_slug" ] || fail "task '${1:-}' inesistente" + is_running "$_slug" && fail "il task è in esecuzione: fermarlo prima di rimuoverlo" + _tmp=$TASKS.tmp.$$ + jq --arg s "$_slug" '.tasks |= map(select(.slug != $s))' "$TASKS" > "$_tmp" \ + && mv "$_tmp" "$TASKS" || { rm -f "$_tmp"; fail "scrittura tasks.json fallita"; } + rm -f "$RUN/$_slug.pid" "$RUN/$_slug.info" + printf 'ok=1\nremoved=%s\n' "$_slug" +} + +cmd_run() { # run + init_tasks + need_jq + helper_ready + _slug=$(resolve_slug "${1:-}") + [ -n "$_slug" ] || fail "task '${1:-}' inesistente" + _name=$(field_of_slug "$_slug" name) + _prof=$(field_of_slug "$_slug" profile) + _cmd=$(field_of_slug "$_slug" command) + [ -n "$_cmd" ] || fail "comando vuoto per '$_name'" + is_running "$_slug" && fail "il task '$_name' è già in esecuzione" + + _out=$(pkexec "$HELPER" task-begin "$_prof" 2>&1) \ + || fail "profilo '$_prof' non applicato: $(printf '%s' "$_out" | tr '\n' ' ')" + case "$_out" in + *"ok=0"*) fail "profilo '$_prof' non applicato: $(printf '%s' "$_out" | tr '\n' ' ')" ;; + esac + + rm -f "$RUN/$_slug.pid" "$RUN/$_slug.info" + # Il figlio scrive il proprio pid e poi exec il comando: il pid resta valido, + # così il watcher può attenderne davvero la fine. + setsid sh -c 'printf "%s\n" "$$" > "$1"; shift; exec sh -c "$1"' \ + sh "$RUN/$_slug.pid" "$_cmd" /dev/null 2>&1 & + _launcher=$! + + _n=0 + while [ ! -s "$RUN/$_slug.pid" ] && [ "$_n" -lt 25 ]; do + sleep 0.1 2>/dev/null || sleep 1 + _n=$((_n + 1)) + kill -0 "$_launcher" 2>/dev/null || break + done + if [ ! -s "$RUN/$_slug.pid" ]; then + pkexec "$HELPER" task-end >/dev/null 2>&1 + fail "avvio del comando fallito: $_cmd" + fi + _pid=$(readf "$RUN/$_slug.pid") + printf 'name=%s\nprofile=%s\nslug=%s\nstarted=%s\n' \ + "$_name" "$_prof" "$_slug" "$(date +%s)" > "$RUN/$_slug.info" + + setsid "$SELF_DIR/$(basename "$0")" watch "$_slug" /dev/null 2>&1 & + + printf 'ok=1\nname=%s\nslug=%s\nprofile=%s\npid=%s\n' "$_name" "$_slug" "$_prof" "$_pid" +} + +cmd_stop() { # stop + init_tasks + need_jq + _slug=$(resolve_slug "${1:-}") + [ -n "$_slug" ] || fail "task '${1:-}' inesistente" + is_running "$_slug" || fail "il task '$_slug' non è in esecuzione" + _pid=$(readf "$RUN/$_slug.pid") + kill -TERM -- "-$_pid" 2>/dev/null || kill -TERM "$_pid" 2>/dev/null + printf 'ok=1\nstopped=%s\npid=%s\n' "$_slug" "$_pid" +} + +cmd_watch() { # watch (interno) + _slug=${1:-} + [ -n "$_slug" ] || exit 2 + _pid=$(readf "$RUN/$_slug.pid" 2>/dev/null) || exit 0 + while kill -0 "$_pid" 2>/dev/null; do + sleep 2 + done + _name=$(sed -n 's/^name=//p' "$RUN/$_slug.info" 2>/dev/null) + _prof=$(sed -n 's/^profile=//p' "$RUN/$_slug.info" 2>/dev/null) + rm -f "$RUN/$_slug.pid" "$RUN/$_slug.info" + if [ -x "$HELPER" ]; then + pkexec "$HELPER" task-end >/dev/null 2>&1 + fi + if command -v notify-send >/dev/null 2>&1; then + notify-send -a "CPUFreq" -i cpu \ + "Task concluso: ${_name:-$_slug}" \ + "Profilo CPU '${_prof:-?}' chiuso, stato precedente ripristinato" + fi +} + +cmd_active() { + printf 'ok=1\n' + _found=0 + for _f in "$RUN"/*.pid; do + [ -r "$_f" ] || continue + _pid=$(readf "$_f") + case "$_pid" in ''|*[!0-9]*) continue ;; esac + kill -0 "$_pid" 2>/dev/null || continue + _slug=$(basename "$_f" .pid) + _name=$(sed -n 's/^name=//p' "$RUN/$_slug.info" 2>/dev/null) + _prof=$(sed -n 's/^profile=//p' "$RUN/$_slug.info" 2>/dev/null) + printf 'act=%s\t%s\t%s\t%s\n' "$_slug" "${_name:-$_slug}" "${_prof:-?}" "$_pid" + _found=$((_found + 1)) + done + printf 'active=%s\n' "$_found" +} + +usage() { + cat <<'EOF' +uso: enne2-cpufreq-task [argomenti] + list elenco dei task configurati + add + add-hex argomenti in esadecimale UTF-8 (usato dal widget) + remove + run salva lo stato, applica il profilo, esegue, ripristina + stop termina il task (il watcher ripristina il profilo) + active task attualmente in esecuzione +EOF +} + +[ "$#" -gt 0 ] || { usage; exit 2; } +_CMD=$1 +shift +case "$_CMD" in + list) cmd_list ;; + add) cmd_add "$@" ;; + add-hex) cmd_add_hex "$@" ;; + remove|rm) cmd_remove "$@" ;; + run|start) cmd_run "$@" ;; + stop) cmd_stop "$@" ;; + watch) cmd_watch "$@" ;; + active) cmd_active ;; + -h|--help|help) usage ;; + *) printf 'ok=0\nerror=comando sconosciuto: %s\n' "$_CMD"; usage; exit 2 ;; +esac diff --git a/contents/scripts/profiles.sh b/contents/scripts/profiles.sh new file mode 100755 index 0000000..afab301 --- /dev/null +++ b/contents/scripts/profiles.sh @@ -0,0 +1,145 @@ +# shellcheck shell=sh +# Profili di frequenza condivisi fra helper privilegiato, lettore di stato e plasmoide. +# Sorgente unica di verità: NON scrive nulla su sysfs, contiene solo la tabella dei +# profili e l'aritmetica dei limiti (kHz). Copiato da install.sh accanto all'helper +# in /usr/local/libexec/enne2-cpufreq/. +# +# Ogni profilo agisce su: governor, energy_performance_preference (EPP), boost e +# tetto di frequenza espresso come percentuale dello span utile +# (floor_khz .. cpuinfo_max_freq). + +# Lettura minimale (il file è già stato validato dal chiamante dove serve). +cf_read() { + _cfout= + [ -r "$1" ] || return 1 + IFS= read -r _cfout < "$1" 2>/dev/null || [ -n "$_cfout" ] || return 1 + printf '%s\n' "$_cfout" +} + +# Tetto hardware reale (kHz), indipendente dallo stato del boost: +# con boost disattivato il kernel riporta in cpuinfo_max_freq la frequenza +# nominale (es. 3.80 GHz invece di 4.97 GHz su amd-pstate). amd_pstate_max_freq +# conserva il massimo effettivo; su altri driver cpuinfo_max_freq è già corretto. +cf_hw_max_khz() { + _cfp0=$1 + _cfmax=$(cf_read "$_cfp0/cpuinfo_max_freq" || echo 0) + case "$_cfmax" in ''|*[!0-9]*) _cfmax=0 ;; esac + _cfamd=$(cf_read "$_cfp0/amd_pstate_max_freq" || echo 0) + case "$_cfamd" in ''|*[!0-9]*) _cfamd=0 ;; esac + [ "$_cfamd" -gt "$_cfmax" ] && _cfmax=$_cfamd + printf '%s\n' "$_cfmax" +} + +cf_profiles() { + printf '%s\n' quiet powersave balanced performance +} + +cf_profile_known() { + case "$1" in + quiet|powersave|balanced|performance) return 0 ;; + *) return 1 ;; + esac +} + +cf_profile_gov() { + case "$1" in + quiet|powersave|balanced) echo powersave ;; + performance) echo performance ;; + *) return 1 ;; + esac +} + +cf_profile_epp() { + case "$1" in + quiet) echo power ;; + powersave) echo balance_power ;; + balanced) echo balance_performance ;; + performance) echo performance ;; + *) return 1 ;; + esac +} + +cf_profile_boost() { + case "$1" in + quiet|powersave) echo 0 ;; + balanced|performance) echo 1 ;; + *) return 1 ;; + esac +} + +cf_profile_max_pct() { + case "$1" in + quiet) echo 35 ;; + powersave) echo 60 ;; + balanced|performance) echo 100 ;; + *) return 1 ;; + esac +} + +cf_profile_label() { + case "$1" in + quiet) echo "Silenzioso" ;; + powersave) echo "Risparmio" ;; + balanced) echo "Bilanciato" ;; + performance) echo "Performance" ;; + custom) echo "Personalizzato" ;; + *) echo "$1" ;; + esac +} + +cf_profile_desc() { + case "$1" in + quiet) echo "EPP power, boost off, tetto 35% dello span" ;; + powersave) echo "EPP balance_power, boost off, tetto 60%" ;; + balanced) echo "EPP balance_performance, boost on, nessun tetto" ;; + performance) echo "governor performance, EPP performance, boost on" ;; + *) echo "" ;; + esac +} + +# cf_profile_max_khz +cf_profile_max_khz() { + _cfp=$(cf_profile_max_pct "$1") || return 1 + _cfl=$2 + _cfh=$3 + [ "$_cfh" -gt "$_cfl" ] || { echo "$_cfh"; return 0; } + # un profilo al 100% non deve avere tetto: nessun arrotondamento + [ "$_cfp" -ge 100 ] && { echo "$_cfh"; return 0; } + cf_round50 $(( _cfl + (_cfh - _cfl) * _cfp / 100 )) +} + +# Arrotonda a 50 MHz: valori leggibili e stabili fra una lettura e l'altra. +cf_round50() { + echo $(( ($1 + 25000) / 50000 * 50000 )) +} + +# cf_detect_profile +# Stampa il nome del profilo se lo stato coincide (tolleranza sul tetto) oppure "custom". +cf_detect_profile() { + _dg=$1 + _de=$2 + _db=$3 + _dmax=$4 + _dfloor=$5 + _dhwmax=$6 + _span=$(( _dhwmax - _dfloor )) + [ "$_span" -gt 0 ] || _span=1 + _tol=$(( _span / 40 )) # 2.5% dello span + [ "$_tol" -lt 80000 ] && _tol=80000 + for _dp in $(cf_profiles); do + [ "$_dg" = "$(cf_profile_gov "$_dp")" ] || continue + [ "$_db" = "$(cf_profile_boost "$_dp")" ] || continue + _dp_max=$(cf_profile_max_khz "$_dp" "$_dfloor" "$_dhwmax") + _diff=$(( _dmax - _dp_max )) + [ "$_diff" -lt 0 ] && _diff=$(( -_diff )) + [ "$_diff" -le "$_tol" ] || continue + # L'EPP può non essere scrivibile con governor "performance": in quel caso + # accettiamo il profilo anche se l'EPP letto non coincide. + [ "$_de" = "$(cf_profile_epp "$_dp")" ] && { echo "$_dp"; return 0; } + if [ "$_dg" = "performance" ] && [ "$_dp" = "performance" ]; then + echo "$_dp" + return 0 + fi + done + echo custom +} diff --git a/contents/ui/ChoicePill.qml b/contents/ui/ChoicePill.qml new file mode 100644 index 0000000..8645fa4 --- /dev/null +++ b/contents/ui/ChoicePill.qml @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2026 enne2 + * SPDX-License-Identifier: MIT + * + * Pillola selezionabile usata per governor ed EPP: alternativa compatta a una + * ComboBox, senza stato interno (attivo/inattivo derivano dalle proprietà). + */ + +import QtQuick +import org.kde.plasma.components 3.0 as PlasmaComponents +import org.kde.kirigami as Kirigami + +Rectangle { + id: pill + + property string text: "" + property bool active: false + property color accent: "#55c5c0" + signal clicked() + + implicitWidth: label.implicitWidth + 16 + implicitHeight: label.implicitHeight + 8 + radius: 4 + color: active + ? Qt.rgba(accent.r, accent.g, accent.b, 0.22) + : (pillMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.10) : Qt.rgba(1, 1, 1, 0.05)) + border.width: 1 + border.color: active + ? Qt.rgba(accent.r, accent.g, accent.b, 0.8) + : Qt.rgba(1, 1, 1, 0.10) + opacity: enabled ? 1.0 : 0.5 + + Behavior on color { + ColorAnimation { duration: 120 } + } + + PlasmaComponents.Label { + id: label + anchors.centerIn: parent + text: pill.text + font.family: "monospace" + font.pointSize: 8 + font.bold: pill.active + color: pill.active ? pill.accent : Kirigami.Theme.textColor + opacity: pill.active ? 1.0 : 0.78 + } + + MouseArea { + id: pillMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: pill.clicked() + } +} diff --git a/contents/ui/main.qml b/contents/ui/main.qml new file mode 100644 index 0000000..ee63f89 --- /dev/null +++ b/contents/ui/main.qml @@ -0,0 +1,1139 @@ +/* + * 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")) + } + } + } + } +} diff --git a/docs/widget.png b/docs/widget.png new file mode 100644 index 0000000..b9d1a7e Binary files /dev/null and b/docs/widget.png differ diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..94b6a71 --- /dev/null +++ b/install.sh @@ -0,0 +1,147 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT +# enne2-cpufreq — installazione. +# +# ./install.sh widget (utente) + backend privilegiato (sudo) +# ./install.sh --widget solo il plasmoide, come utente +# ./install.sh --backend solo helper + polkit, come root +# ./install.sh --no-polkit-rule +# backend senza la regola "nessun prompt": +# ogni modifica chiederà la password +# ./install.sh --dry-run mostra cosa verrebbe fatto +set -u + +PKG_DIR=$(cd "$(dirname "$0")" && pwd) +ID=org.enne2.cpufreq +LIBDIR=/usr/local/libexec/enne2-cpufreq +POLICY_DST=/usr/share/polkit-1/actions/org.enne2.cpufreq.set.policy +RULE_DST=/etc/polkit-1/rules.d/50-org.enne2.cpufreq.rules +STATEDIR=/var/lib/enne2-cpufreq + +MODE=all +WITH_RULE=1 +DRY=0 + +for arg in "$@"; do + case "$arg" in + --widget) MODE=widget ;; + --backend) MODE=backend ;; + --all) MODE=all ;; + --no-polkit-rule) WITH_RULE=0 ;; + --dry-run|-n) DRY=1 ;; + -h|--help) + sed -n 's/^# \{0,1\}//p' "$0" | sed -n '1,20p' + exit 0 + ;; + *) printf 'opzione sconosciuta: %s\n' "$arg" >&2; exit 2 ;; + esac +done + +say() { printf '%s\n' "$*"; } +run() { + if [ "$DRY" -eq 1 ]; then + printf ' [dry-run] %s\n' "$*" + else + "$@" + fi +} + +have_widget() { + kpackagetool6 --type Plasma/Applet --show "$ID" >/dev/null 2>&1 +} + +install_widget() { + command -v kpackagetool6 >/dev/null 2>&1 || { + say "ERRORE: kpackagetool6 non trovato (pacchetto kpackagetool6/kf6-kpackage)." + return 1 + } + if [ "$DRY" -eq 1 ]; then + say "[dry-run] kpackagetool6 --type Plasma/Applet --install|--upgrade $PKG_DIR" + elif have_widget; then + say "Aggiorno il plasmoide $ID…" + kpackagetool6 --type Plasma/Applet --upgrade "$PKG_DIR" || return 1 + else + say "Installo il plasmoide $ID…" + kpackagetool6 --type Plasma/Applet --install "$PKG_DIR" || return 1 + fi + + WIDGET_DIR=$HOME/.local/share/plasma/plasmoids/$ID + if [ "$DRY" -eq 0 ] && [ -d "$WIDGET_DIR/contents/scripts" ]; then + chmod 0755 "$WIDGET_DIR"/contents/scripts/*.sh \ + "$WIDGET_DIR"/contents/scripts/enne2-cpufreq-task 2>/dev/null + fi + + # comodità da terminale: stesso launcher del widget + if [ "$DRY" -eq 0 ] && [ -d "$HOME/.local/bin" ]; then + ln -sfn "$WIDGET_DIR/contents/scripts/enne2-cpufreq-task" \ + "$HOME/.local/bin/enne2-cpufreq-task" + say "Comando utente: ~/.local/bin/enne2-cpufreq-task (list|add|run|stop)" + fi + return 0 +} + +install_backend() { + if [ "$(id -u)" -ne 0 ]; then + say "ERRORE: la parte privilegiata richiede root (usare sudo)." + return 1 + fi + say "Backend privilegiato in $LIBDIR" + run install -d -m 0755 -o root -g root "$LIBDIR" + run install -d -m 0755 -o root -g root "$STATEDIR" + run install -m 0755 -o root -g root "$PKG_DIR/bin/cpufreq-helper" "$LIBDIR/cpufreq-helper" + run install -m 0644 -o root -g root "$PKG_DIR/contents/scripts/profiles.sh" "$LIBDIR/profiles.sh" + + say "Azione polkit: $POLICY_DST" + run install -d -m 0755 /usr/share/polkit-1/actions + run install -m 0644 -o root -g root \ + "$PKG_DIR/polkit/org.enne2.cpufreq.set.policy" "$POLICY_DST" + + if [ "$WITH_RULE" -eq 1 ]; then + say "Regola polkit senza prompt (gruppo wheel, sessione attiva): $RULE_DST" + run install -d -m 0755 /etc/polkit-1/rules.d + run install -m 0644 -o root -g root \ + "$PKG_DIR/polkit/50-org.enne2.cpufreq.rules" "$RULE_DST" + else + say "Regola senza prompt NON installata: verrà chiesta la password a ogni modifica." + run rm -f "$RULE_DST" + fi + + # polkitd sorveglia le directory delle regole, il reload è solo una sicurezza + if [ "$DRY" -eq 0 ]; then + systemctl try-reload-or-restart polkit >/dev/null 2>&1 || true + fi + + say "" + say "Verifica:" + if [ "$DRY" -eq 0 ]; then + "$LIBDIR/cpufreq-helper" status | sed 's/^/ /' + fi + return 0 +} + +case "$MODE" in + widget) + install_widget + ;; + backend) + install_backend + ;; + all) + install_widget || exit 1 + say "" + if [ "$(id -u)" -eq 0 ]; then + install_backend || exit 1 + else + say "Parte privilegiata: sudo $0 --backend $([ "$WITH_RULE" -eq 0 ] && echo --no-polkit-rule)" + if [ "$DRY" -eq 1 ]; then + sudo -n true >/dev/null 2>&1 && sudo "$0" --backend $([ "$WITH_RULE" -eq 0 ] && echo --no-polkit-rule) || say "[dry-run] sudo richiesto" + else + sudo "$0" --backend $([ "$WITH_RULE" -eq 0 ] && echo --no-polkit-rule) || exit 1 + fi + fi + ;; +esac + +say "" +say "Fatto. Aggiungi il widget dal desktop (Aggiungi widget → «CPU Frequency Control»)" +say "oppure via script: desktops()[0].addWidget(\"$ID\")" diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..eecd38f --- /dev/null +++ b/metadata.json @@ -0,0 +1,19 @@ +{ + "KPackageStructure": "Plasma/Applet", + "KPlugin": { + "Id": "org.enne2.cpufreq", + "Name": "CPU Frequency Control", + "Description": "Controlla governor, limiti di frequenza (min/max), EPP e boost della CPU, applica profili di risparmio o performance e lancia task con un profilo dedicato, ripristinando lo stato precedente alla fine. Backend: helper privilegiato via pkexec + azione polkit org.enne2.cpufreq.set.", + "Icon": "cpu", + "Version": "1.0", + "License": "MIT", + "Authors": [ + { + "Name": "enne2" + } + ], + "Category": "System Information", + "Website": "https://git.enne2.net/enne2/kde-cpufreq-dashboard" + }, + "X-Plasma-API-Minimum-Version": "6.0" +} diff --git a/polkit/50-org.enne2.cpufreq.rules b/polkit/50-org.enne2.cpufreq.rules new file mode 100644 index 0000000..883f202 --- /dev/null +++ b/polkit/50-org.enne2.cpufreq.rules @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: MIT + * + * enne2-cpufreq — consenso senza prompt per l'helper del plasmoide. + * + * Il permesso riguarda UNA sola azione (org.enne2.cpufreq.set) e quindi un solo + * programma: /usr/local/libexec/enne2-cpufreq/cpufreq-helper, con gli argomenti + * validati al suo interno. Condizioni: utente locale, sessione attiva, gruppo + * wheel. Senza questa regola la .policy chiede la password a ogni modifica + * (allow_active=auth_admin_keep): basta eliminare questo file per tornare al + * comportamento con prompt. + */ +polkit.addRule(function (action, subject) { + if (action.id !== "org.enne2.cpufreq.set") { + return; + } + if (!subject.local || !subject.active) { + return; + } + if (subject.isInGroup("wheel")) { + return polkit.Result.YES; + } +}); diff --git a/polkit/org.enne2.cpufreq.set.policy b/polkit/org.enne2.cpufreq.set.policy new file mode 100644 index 0000000..58a6c36 --- /dev/null +++ b/polkit/org.enne2.cpufreq.set.policy @@ -0,0 +1,28 @@ + + + + + enne2 + https://git.enne2.net/enne2/kde-cpufreq-dashboard + + + Configurare governor, limiti di frequenza e boost della CPU + Per modificare governor, limiti di frequenza, EPP o boost della CPU è necessaria l'autorizzazione di amministratore. + cpu + + no + no + auth_admin_keep + + /usr/local/libexec/enne2-cpufreq/cpufreq-helper + + diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..daf0453 --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,64 @@ +#!/bin/sh +# SPDX-License-Identifier: MIT +# enne2-cpufreq — disinstallazione. +# ./uninstall.sh rimuove widget (utente) e backend (sudo) +# ./uninstall.sh --widget solo il plasmoide +# ./uninstall.sh --backend solo helper/polkit/stato (root) +# I task configurati (~/.config/enne2-cpufreq) NON vengono cancellati. +set -u + +PKG_DIR=$(cd "$(dirname "$0")" && pwd) +ID=org.enne2.cpufreq +LIBDIR=/usr/local/libexec/enne2-cpufreq +POLICY_DST=/usr/share/polkit-1/actions/org.enne2.cpufreq.set.policy +RULE_DST=/etc/polkit-1/rules.d/50-org.enne2.cpufreq.rules +STATEDIR=/var/lib/enne2-cpufreq + +MODE=all +for arg in "$@"; do + case "$arg" in + --widget) MODE=widget ;; + --backend) MODE=backend ;; + --all) MODE=all ;; + -h|--help) sed -n 's/^# \{0,1\}//p' "$0" | sed -n '1,10p'; exit 0 ;; + *) printf 'opzione sconosciuta: %s\n' "$arg" >&2; exit 2 ;; + esac +done + +remove_widget() { + if kpackagetool6 --type Plasma/Applet --show "$ID" >/dev/null 2>&1; then + printf 'Rimuovo il plasmoide %s…\n' "$ID" + kpackagetool6 --type Plasma/Applet --remove "$ID" || return 1 + else + printf 'Plasmoide %s non installato.\n' "$ID" + fi + [ -L "$HOME/.local/bin/enne2-cpufreq-task" ] && rm -f "$HOME/.local/bin/enne2-cpufreq-task" + return 0 +} + +remove_backend() { + [ "$(id -u)" -eq 0 ] || { printf 'ERRORE: richiede root.\n' >&2; return 1; } + # ripristina lo stato hardware prima di togliere l'helper + if [ -x "$LIBDIR/cpufreq-helper" ]; then + "$LIBDIR/cpufreq-helper" task-end --all >/dev/null 2>&1 + fi + rm -f "$POLICY_DST" "$RULE_DST" + rm -rf "$LIBDIR" + rm -rf "$STATEDIR" + systemctl try-reload-or-restart polkit >/dev/null 2>&1 || true + printf 'Backend rimosso.\n' + return 0 +} + +case "$MODE" in + widget) remove_widget ;; + backend) remove_backend ;; + all) + remove_widget + if [ "$(id -u)" -eq 0 ]; then + remove_backend + else + sudo "$0" --backend + fi + ;; +esac