Caffeine per KDE Plasma 6: widget di pannello (inibizione schermo + sospensione)
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
*.qmlc
|
||||||
|
*.jsc
|
||||||
|
*~
|
||||||
|
build/
|
||||||
|
package/
|
||||||
|
*.backup-*
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# enne2-caffeine
|
||||||
|
|
||||||
|
Widget per il pannello di **KDE Plasma 6** che impedisce lo spegnimento dello schermo, il
|
||||||
|
blocco schermo e la sospensione automatica: l'equivalente Plasma di **Caffeine** di GNOME.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Funzioni
|
||||||
|
|
||||||
|
- **Clic sinistro sull'icona** → attiva/disattiva immediatamente (come Caffeine su GNOME).
|
||||||
|
Tazza piena = inibizione attiva, tazza vuota = inibizione assente.
|
||||||
|
- **Clic centrale** → popup con le opzioni rapide.
|
||||||
|
- **Due modalità**:
|
||||||
|
- *Schermo + sospensione* (default, come Caffeine): blocca spegnimento/dim dello schermo,
|
||||||
|
blocco schermo e sospensione automatica;
|
||||||
|
- *Solo sospensione* (come Amphetamine): il computer non si sospende ma lo schermo può
|
||||||
|
spegnersi normalmente.
|
||||||
|
- **Timer di disattivazione automatica** (5/15/30/60/120/240 minuti, oppure mai), con
|
||||||
|
countdown nel tooltip e nell'icona di pannello; notifica opzionale alla scadenza.
|
||||||
|
- Il widget è "stateless": dopo un riavvio Caffeine è spento, come ci si aspetta.
|
||||||
|
|
||||||
|
## Come funziona
|
||||||
|
|
||||||
|
Il backend sono due unità **systemd utente** che lanciano `systemd-inhibit` (logind) e restano
|
||||||
|
vive finché l'inibizione è attiva:
|
||||||
|
|
||||||
|
| Modalità | Unità | systemd-inhibit |
|
||||||
|
|---|---|---|
|
||||||
|
| schermo + sospensione | `enne2-caffeine-screen.service` | `--what=sleep:idle` |
|
||||||
|
| solo sospensione | `enne2-caffeine-sleep.service` | `--what=sleep` |
|
||||||
|
|
||||||
|
PowerDevil/PolicyAgent traduce le inibizioni logind in policy interne:
|
||||||
|
|
||||||
|
- `sleep` → `InterruptSession` (niente sospensione automatica);
|
||||||
|
- `idle` → `ChangeScreenSettings` (niente dim/spegnimento dello schermo, niente blocco schermo).
|
||||||
|
|
||||||
|
Perché non si usa direttamente `org.kde.Solid.PowerManagement.PolicyAgent.AddInhibition`?
|
||||||
|
Perché il cookie di quell'API è legato al chiamante D-Bus: una chiamata da un processo
|
||||||
|
effimero verrebbe rilasciata subito (e l'inibizione ha 5 s di delay prima di essere applicata).
|
||||||
|
Usando unità systemd l'inibizione sopravvive anche al riavvio di plasmashell e si rilascia da
|
||||||
|
sola alla fine della sessione.
|
||||||
|
|
||||||
|
## Installazione
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone ssh://git@git.enne2.net:222/enne2/kde-caffeine.git
|
||||||
|
cd kde-caffeine
|
||||||
|
./install.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Poi: tasto destro sul pannello → *Aggiungi widget* → **Caffeine** (categoria *Utilità*).
|
||||||
|
Per averlo vicino all'orologio trascinalo a sinistra dell'orologio.
|
||||||
|
|
||||||
|
## CLI e scorciatoia da tastiera
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bin/enne2-caffeine status # state=on|off, mode=screen|sleep, deadline=<epoch>
|
||||||
|
bin/enne2-caffeine on 30 # attiva per 30 minuti (modalità schermo + sospensione)
|
||||||
|
bin/enne2-caffeine toggle # inverti
|
||||||
|
bin/enne2-caffeine sleep on 60 # solo sospensione, 60 minuti
|
||||||
|
bin/enne2-caffeine off
|
||||||
|
```
|
||||||
|
|
||||||
|
Per una scorciatoia globale: *Impostazioni di sistema → Tastiera → Scorciatoie → Aggiungi →
|
||||||
|
Comando o script*, comando `enne2-caffeine toggle` (o il percorso completo dello script).
|
||||||
|
|
||||||
|
## Verifica
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemd-inhibit --list | grep enne2-caffeine
|
||||||
|
busctl --user get-property org.kde.Solid.PowerManagement \
|
||||||
|
/org/kde/Solid/PowerManagement/PolicyAgent \
|
||||||
|
org.kde.Solid.PowerManagement.PolicyAgent ActiveInhibitions | grep -i caffeine
|
||||||
|
```
|
||||||
|
|
||||||
|
La seconda mostra che PowerDevil ha recepito l'inibizione logind (`what=idle:sleep`).
|
||||||
|
|
||||||
|
## Configurazione del widget
|
||||||
|
|
||||||
|
Tasto destro sull'icona → *Configura Caffeine…*:
|
||||||
|
|
||||||
|
- **Impedisci anche lo spegnimento e il blocco dello schermo** (default: attivo);
|
||||||
|
- **Disattiva automaticamente dopo N minuti** (0 = mai);
|
||||||
|
- **Notifica quando scade il timer**.
|
||||||
|
|
||||||
|
## Licenza
|
||||||
|
|
||||||
|
MIT — vedi [LICENSE](LICENSE).
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# enne2-caffeine — comando da terminale / scorciatoia globale
|
||||||
|
#
|
||||||
|
# Uso:
|
||||||
|
# enne2-caffeine stato
|
||||||
|
# enne2-caffeine on [min] attiva (modalità schermo + sospensione)
|
||||||
|
# enne2-caffeine off disattiva
|
||||||
|
# enne2-caffeine toggle [min]
|
||||||
|
# enne2-caffeine sleep on attiva in modalità "solo sospensione"
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BACKEND="$HERE/../contents/scripts/caffeine.sh"
|
||||||
|
|
||||||
|
[ -x "$BACKEND" ] || { echo "error=backend-not-found"; exit 1; }
|
||||||
|
|
||||||
|
# garanzia che le unità siano installate
|
||||||
|
"$BACKEND" ensure-units "$HERE/../systemd" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
command="${1:-status}"
|
||||||
|
shift || true
|
||||||
|
|
||||||
|
case "$command" in
|
||||||
|
status|state)
|
||||||
|
"$BACKEND" status
|
||||||
|
;;
|
||||||
|
on)
|
||||||
|
"$BACKEND" on screen "${1:-0}"
|
||||||
|
;;
|
||||||
|
off)
|
||||||
|
"$BACKEND" off
|
||||||
|
;;
|
||||||
|
toggle)
|
||||||
|
"$BACKEND" toggle screen "${1:-0}"
|
||||||
|
;;
|
||||||
|
sleep)
|
||||||
|
[ "${1:-on}" = "off" ] && "$BACKEND" off || "$BACKEND" on sleep "${2:-0}"
|
||||||
|
;;
|
||||||
|
screen)
|
||||||
|
[ "${1:-on}" = "off" ] && "$BACKEND" off || "$BACKEND" on screen "${2:-0}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "uso: enne2-caffeine [status|on [minuti]|off|toggle [minuti]|sleep on [minuti]|screen on [minuti]]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
|
||||||
|
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd">
|
||||||
|
<kcfgfile name=""/>
|
||||||
|
<group name="General">
|
||||||
|
<entry name="keepScreenOn" type="Bool">
|
||||||
|
<label>Impedisci anche lo spegnimento e il blocco dello schermo (come Caffeine di GNOME). Se disattivato blocca solo la sospensione automatica, lasciando spegnere lo schermo (come Amphetamine).</label>
|
||||||
|
<default>true</default>
|
||||||
|
</entry>
|
||||||
|
<entry name="autoOffMinutes" type="Int">
|
||||||
|
<label>Disattiva automaticamente dopo N minuti (0 = mai). Il timer parte quando si attiva Caffeine.</label>
|
||||||
|
<default>0</default>
|
||||||
|
<min>0</min>
|
||||||
|
<max>1440</max>
|
||||||
|
</entry>
|
||||||
|
<entry name="notifyOnTimerEnd" type="Bool">
|
||||||
|
<label>Mostra una notifica quando il timer disattiva Caffeine.</label>
|
||||||
|
<default>true</default>
|
||||||
|
</entry>
|
||||||
|
</group>
|
||||||
|
</kcfg>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<g fill="none" stroke="#9aa1ac" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M5.6 8.6h10.8l-1.1 10.1a2 2 0 0 1-2 1.8H8.7a2 2 0 0 1-2-1.8Z"/>
|
||||||
|
<path d="M16.6 10.7h1.5a2.6 2.6 0 0 1 0 5.2h-1.8"/>
|
||||||
|
<path d="M4 21.9h16"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 398 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path d="M5.6 8.6h10.8l-1.1 10.1a2 2 0 0 1-2 1.8H8.7a2 2 0 0 1-2-1.8Z" fill="#e8a33d"/>
|
||||||
|
<ellipse cx="11" cy="8.6" rx="5.4" ry="0.9" fill="#c8862a"/>
|
||||||
|
<g fill="none" stroke="#e8a33d" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M16.6 10.7h1.5a2.6 2.6 0 0 1 0 5.2h-1.8"/>
|
||||||
|
<path d="M4 21.9h16"/>
|
||||||
|
<path d="M9.9 6.4c-1-1.3 1-2.3 0-3.7"/>
|
||||||
|
<path d="M13.2 6.4c-1-1.3 1-2.3 0-3.7"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 563 B |
Executable
+179
@@ -0,0 +1,179 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# enne2-caffeine — backend del widget Caffeine per KDE Plasma 6
|
||||||
|
#
|
||||||
|
# Blocca lo spegnimento/blocco dello schermo e la sospensione automatica usando
|
||||||
|
# systemd-inhibit (logind). PowerDevil/PolicyAgent onora le inibizioni logind:
|
||||||
|
# what=sleep -> InterruptSession (niente sospensione automatica)
|
||||||
|
# what=idle -> ChangeScreenSettings (niente dim/spegnimento schermo, niente blocco)
|
||||||
|
# L'inibizione vive in unità systemd utente, quindi sopravvive al riavvio di
|
||||||
|
# plasmashell e si rilascia da sola alla fine della sessione.
|
||||||
|
#
|
||||||
|
# Uso:
|
||||||
|
# caffeine.sh status
|
||||||
|
# caffeine.sh on <screen|sleep> [minuti]
|
||||||
|
# caffeine.sh switch <screen|sleep> # cambia modalità mantenendo il timer
|
||||||
|
# caffeine.sh off [notify]
|
||||||
|
# caffeine.sh toggle <screen|sleep> [minuti]
|
||||||
|
# caffeine.sh ensure-units <cartella-con-i-.service>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
UNIT_SCREEN="enne2-caffeine-screen.service"
|
||||||
|
UNIT_SLEEP="enne2-caffeine-sleep.service"
|
||||||
|
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/enne2-caffeine"
|
||||||
|
DEADLINE_FILE="$STATE_DIR/deadline"
|
||||||
|
USER_UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||||
|
|
||||||
|
SYSTEMCTL="$(command -v systemctl 2>/dev/null || true)"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "error=$1"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# systemd utente disponibile e interrogabile?
|
||||||
|
systemd_ok() {
|
||||||
|
[ -n "$SYSTEMCTL" ] || return 1
|
||||||
|
case "$("$SYSTEMCTL" --user is-active "$UNIT_SCREEN" 2>/dev/null)" in
|
||||||
|
active|inactive|failed|activating|deactivating|unknown) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
unit_state() {
|
||||||
|
"$SYSTEMCTL" --user is-active "$1" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
active_unit() {
|
||||||
|
if [ "$(unit_state "$UNIT_SCREEN")" = "active" ]; then
|
||||||
|
printf '%s\n' "$UNIT_SCREEN"
|
||||||
|
elif [ "$(unit_state "$UNIT_SLEEP")" = "active" ]; then
|
||||||
|
printf '%s\n' "$UNIT_SLEEP"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
only_digits() {
|
||||||
|
case "${1:-}" in
|
||||||
|
''|*[!0-9]*) printf '0\n' ;;
|
||||||
|
*) printf '%s\n' "$1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_ensure_units() {
|
||||||
|
local src="${1:-}"
|
||||||
|
[ -n "$src" ] && [ -d "$src" ] || fail "unit-source-missing"
|
||||||
|
systemd_ok || fail "systemd-user-unavailable"
|
||||||
|
mkdir -p "$USER_UNIT_DIR" || fail "cannot-create-unit-dir"
|
||||||
|
local changed=0 f target
|
||||||
|
for f in "$src"/*.service; do
|
||||||
|
[ -e "$f" ] || continue
|
||||||
|
target="$USER_UNIT_DIR/$(basename "$f")"
|
||||||
|
if ! cmp -s "$f" "$target"; then
|
||||||
|
cp -f "$f" "$target" || fail "cannot-copy-unit"
|
||||||
|
changed=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$changed" = 1 ]; then
|
||||||
|
"$SYSTEMCTL" --user daemon-reload >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
echo "ok=1"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() {
|
||||||
|
mkdir -p "$STATE_DIR" 2>/dev/null
|
||||||
|
if ! systemd_ok; then
|
||||||
|
echo "backend=unsupported"
|
||||||
|
echo "state=off"
|
||||||
|
echo "mode=none"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "backend=systemd"
|
||||||
|
|
||||||
|
local unit
|
||||||
|
unit="$(active_unit)"
|
||||||
|
case "$unit" in
|
||||||
|
"$UNIT_SCREEN") echo "state=on"; echo "mode=screen" ;;
|
||||||
|
"$UNIT_SLEEP") echo "state=on"; echo "mode=sleep" ;;
|
||||||
|
*) echo "state=off"; echo "mode=none" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -n "$unit" ] && [ -f "$DEADLINE_FILE" ]; then
|
||||||
|
local d
|
||||||
|
d="$(only_digits "$(cat "$DEADLINE_FILE" 2>/dev/null)")"
|
||||||
|
if [ "$d" -gt 0 ]; then
|
||||||
|
echo "deadline=$d"
|
||||||
|
else
|
||||||
|
rm -f "$DEADLINE_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_mode() {
|
||||||
|
systemd_ok || fail "systemd-user-unavailable"
|
||||||
|
"$SYSTEMCTL" --user stop "$UNIT_SCREEN" "$UNIT_SLEEP" >/dev/null 2>&1
|
||||||
|
if [ "${1:-screen}" = "sleep" ]; then
|
||||||
|
"$SYSTEMCTL" --user start "$UNIT_SLEEP" >/dev/null 2>&1 || fail "start-failed"
|
||||||
|
else
|
||||||
|
"$SYSTEMCTL" --user start "$UNIT_SCREEN" >/dev/null 2>&1 || fail "start-failed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_on() {
|
||||||
|
local mode="${1:-screen}"
|
||||||
|
local minutes
|
||||||
|
minutes="$(only_digits "${2:-0}")"
|
||||||
|
mkdir -p "$STATE_DIR" 2>/dev/null
|
||||||
|
apply_mode "$mode" || return 1
|
||||||
|
if [ "$minutes" -gt 0 ]; then
|
||||||
|
echo "$(( $(date +%s) + minutes * 60 ))" > "$DEADLINE_FILE"
|
||||||
|
else
|
||||||
|
rm -f "$DEADLINE_FILE"
|
||||||
|
fi
|
||||||
|
echo "ok=1"
|
||||||
|
cmd_status
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_switch() {
|
||||||
|
apply_mode "${1:-screen}" || return 1
|
||||||
|
echo "ok=1"
|
||||||
|
cmd_status
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_off() {
|
||||||
|
local notify="${1:-}"
|
||||||
|
if systemd_ok; then
|
||||||
|
"$SYSTEMCTL" --user stop "$UNIT_SCREEN" "$UNIT_SLEEP" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
rm -f "$DEADLINE_FILE"
|
||||||
|
if [ "$notify" = "notify" ] && command -v notify-send >/dev/null 2>&1; then
|
||||||
|
notify-send -a "Caffeine" -i preferences-system-power-management \
|
||||||
|
"Caffeine disattivato" \
|
||||||
|
"Il timer è scaduto: schermo e sospensione tornano alla gestione automatica." || true
|
||||||
|
fi
|
||||||
|
echo "ok=1"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_toggle() {
|
||||||
|
local mode="${1:-screen}"
|
||||||
|
local minutes
|
||||||
|
minutes="$(only_digits "${2:-0}")"
|
||||||
|
if systemd_ok && [ -n "$(active_unit)" ]; then
|
||||||
|
cmd_off
|
||||||
|
else
|
||||||
|
cmd_on "$mode" "$minutes"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-status}" in
|
||||||
|
status) cmd_status ;;
|
||||||
|
on|start) cmd_on "${2:-screen}" "${3:-0}" ;;
|
||||||
|
switch) cmd_switch "${2:-screen}" ;;
|
||||||
|
off|stop) cmd_off "${2:-}" ;;
|
||||||
|
toggle) cmd_toggle "${2:-screen}" "${3:-0}" ;;
|
||||||
|
ensure-units) cmd_ensure_units "${2:-}" ;;
|
||||||
|
version) echo "name=enne2-caffeine"; echo "version=1.0" ;;
|
||||||
|
*) fail "unknown-command" ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 enne2
|
||||||
|
* SPDX-License-Identifier: MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import org.kde.plasma.plasmoid
|
||||||
|
import org.kde.plasma.core as PlasmaCore
|
||||||
|
import org.kde.plasma.components as PlasmaComponents3
|
||||||
|
import org.kde.plasma.extras as PlasmaExtras
|
||||||
|
import org.kde.plasma.plasma5support as P5Support
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
|
PlasmoidItem {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- percorsi
|
||||||
|
readonly property string pkgPath: Qt.resolvedUrl("..").toString().replace("file://", "")
|
||||||
|
readonly property string scriptPath: pkgPath + "/scripts/caffeine.sh"
|
||||||
|
readonly property string unitSrcPath: pkgPath + "/systemd"
|
||||||
|
readonly property string iconOnPath: Qt.resolvedUrl("../icons/caffeine-on.svg").toString()
|
||||||
|
readonly property string iconOffPath: Qt.resolvedUrl("../icons/caffeine-off.svg").toString()
|
||||||
|
readonly property string statusCommand: scriptPath + " status"
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ stato
|
||||||
|
property string backendState: "off" // on | off
|
||||||
|
property string backendMode: "none" // screen | sleep | none
|
||||||
|
property string backend: "unknown" // systemd | unsupported | unknown
|
||||||
|
property int deadlineEpoch: 0
|
||||||
|
property int remainingSec: 0
|
||||||
|
property bool unitsReady: false
|
||||||
|
property string lastError: ""
|
||||||
|
|
||||||
|
readonly property bool active: backendState === "on"
|
||||||
|
readonly property bool timerActive: active && remainingSec > 0
|
||||||
|
readonly property bool keepScreenOn: plasmoid.configuration.keepScreenOn
|
||||||
|
readonly property string startMode: keepScreenOn ? "screen" : "sleep"
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ presentazione
|
||||||
|
Plasmoid.icon: active ? iconOnPath : iconOffPath
|
||||||
|
Plasmoid.status: active ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.PassiveStatus
|
||||||
|
toolTipMainText: active ? "Caffeine attivo" : "Caffeine inattivo"
|
||||||
|
toolTipTextFormat: Text.PlainText
|
||||||
|
toolTipSubText: {
|
||||||
|
let text = modeText()
|
||||||
|
if (timerActive) {
|
||||||
|
text += "\nSi disattiva automaticamente tra " + formatDuration(remainingSec)
|
||||||
|
}
|
||||||
|
text += "\nClic sinistro: attiva/disattiva · clic centrale: opzioni"
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
// un clic sinistro non deve aprire il popup: lo gestiamo noi
|
||||||
|
activationTogglesExpanded: false
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- funzioni
|
||||||
|
function formatDuration(seconds) {
|
||||||
|
const total = Math.max(0, Math.floor(seconds))
|
||||||
|
const hours = Math.floor(total / 3600)
|
||||||
|
const minutes = Math.floor((total % 3600) / 60)
|
||||||
|
const secs = total % 60
|
||||||
|
const pad = value => (value < 10 ? "0" : "") + value
|
||||||
|
if (hours > 0) {
|
||||||
|
return hours + "h " + pad(minutes) + "m"
|
||||||
|
}
|
||||||
|
if (minutes > 0) {
|
||||||
|
return minutes + "m " + pad(secs) + "s"
|
||||||
|
}
|
||||||
|
return secs + "s"
|
||||||
|
}
|
||||||
|
|
||||||
|
function modeText() {
|
||||||
|
if (!active) {
|
||||||
|
return "Nessuna inibizione attiva: schermo e sospensione gestiti automaticamente"
|
||||||
|
}
|
||||||
|
if (backendMode === "sleep") {
|
||||||
|
return "Sospensione automatica bloccata · lo schermo può spegnersi"
|
||||||
|
}
|
||||||
|
return "Schermo, blocco schermo e sospensione automatica bloccati"
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyStatus(output) {
|
||||||
|
const values = {}
|
||||||
|
String(output || "").split("\n").forEach(function(line) {
|
||||||
|
const index = line.indexOf("=")
|
||||||
|
if (index > 0) {
|
||||||
|
values[line.substring(0, index)] = line.substring(index + 1).trim()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (values["backend"] !== undefined) {
|
||||||
|
backend = values["backend"]
|
||||||
|
}
|
||||||
|
if (values["state"] !== undefined) {
|
||||||
|
backendState = values["state"]
|
||||||
|
}
|
||||||
|
if (values["mode"] !== undefined) {
|
||||||
|
backendMode = values["mode"]
|
||||||
|
}
|
||||||
|
const deadline = parseInt(values["deadline"] || "0", 10)
|
||||||
|
deadlineEpoch = isNaN(deadline) ? 0 : deadline
|
||||||
|
if (deadlineEpoch > 0) {
|
||||||
|
const remaining = deadlineEpoch - Math.floor(Date.now() / 1000)
|
||||||
|
remainingSec = remaining > 0 ? remaining : 0
|
||||||
|
if (remaining <= 0) {
|
||||||
|
deactivate(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
remainingSec = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(command) {
|
||||||
|
if (backend === "unsupported") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const source = scriptPath + " " + command
|
||||||
|
actionSource.disconnectSource(source)
|
||||||
|
actionSource.connectSource(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
statusSource.disconnectSource(statusCommand)
|
||||||
|
statusSource.connectSource(statusCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
function activate() {
|
||||||
|
const minutes = plasmoid.configuration.autoOffMinutes
|
||||||
|
run("on " + startMode + " " + minutes)
|
||||||
|
remainingSec = minutes > 0 ? minutes * 60 : 0
|
||||||
|
refreshDelay.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivate(fromTimer) {
|
||||||
|
const notify = (fromTimer === true && plasmoid.configuration.notifyOnTimerEnd) ? " notify" : ""
|
||||||
|
run("off" + notify)
|
||||||
|
remainingSec = 0
|
||||||
|
deadlineEpoch = 0
|
||||||
|
refreshDelay.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (active) {
|
||||||
|
deactivate(false)
|
||||||
|
} else {
|
||||||
|
activate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeMode(keepScreen) {
|
||||||
|
if (!active) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
run("switch " + (keepScreen ? "screen" : "sleep"))
|
||||||
|
refreshDelay.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeTimer(minutes) {
|
||||||
|
if (!active) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (minutes > 0) {
|
||||||
|
remainingSec = minutes * 60
|
||||||
|
run("on " + startMode + " " + minutes)
|
||||||
|
} else {
|
||||||
|
remainingSec = 0
|
||||||
|
run("switch " + startMode)
|
||||||
|
}
|
||||||
|
refreshDelay.restart()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- backend
|
||||||
|
// Stato: polling ogni 2 secondi (systemctl --user is-active).
|
||||||
|
P5Support.DataSource {
|
||||||
|
id: statusSource
|
||||||
|
engine: "executable"
|
||||||
|
interval: 2000
|
||||||
|
connectedSources: [root.statusCommand]
|
||||||
|
onNewData: function(sourceName, data) {
|
||||||
|
root.applyStatus(data ? data["stdout"] : "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comandi una tantum (on/off/switch), un source per comando.
|
||||||
|
P5Support.DataSource {
|
||||||
|
id: actionSource
|
||||||
|
engine: "executable"
|
||||||
|
connectedSources: []
|
||||||
|
onNewData: function(sourceName, data) {
|
||||||
|
const output = data ? String(data["stdout"] || "") : ""
|
||||||
|
root.lastError = output.indexOf("error=") === 0 ? output : ""
|
||||||
|
if (root.lastError !== "") {
|
||||||
|
console.warn("enne2-caffeine:", root.lastError)
|
||||||
|
}
|
||||||
|
root.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Installa/aggiorna le unità systemd utente al primo avvio.
|
||||||
|
P5Support.DataSource {
|
||||||
|
id: unitsSource
|
||||||
|
engine: "executable"
|
||||||
|
connectedSources: [root.scriptPath + " ensure-units " + root.unitSrcPath]
|
||||||
|
onNewData: function(sourceName, data) {
|
||||||
|
root.unitsReady = Boolean(data && Number(data["exit code"]) === 0
|
||||||
|
&& String(data["stdout"] || "").indexOf("ok=1") === 0)
|
||||||
|
root.refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: refreshDelay
|
||||||
|
interval: 300
|
||||||
|
repeat: false
|
||||||
|
onTriggered: root.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Countdown del timer di disattivazione automatica.
|
||||||
|
Timer {
|
||||||
|
interval: 1000
|
||||||
|
repeat: true
|
||||||
|
running: root.timerActive
|
||||||
|
onTriggered: {
|
||||||
|
const remaining = root.deadlineEpoch - Math.floor(Date.now() / 1000)
|
||||||
|
root.remainingSec = remaining > 0 ? remaining : 0
|
||||||
|
if (remaining <= 0) {
|
||||||
|
root.deactivate(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------- rappresentazione compatta
|
||||||
|
compactRepresentation: Item {
|
||||||
|
implicitWidth: compactRow.implicitWidth
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: compactRow
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
source: root.active ? root.iconOnPath : root.iconOffPath
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.smallMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents3.Label {
|
||||||
|
visible: root.timerActive
|
||||||
|
text: Math.ceil(root.remainingSec / 60) + "m"
|
||||||
|
font: Kirigami.Theme.smallFont
|
||||||
|
color: Kirigami.Theme.textColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: function(mouse) {
|
||||||
|
if (mouse.button === Qt.MiddleButton) {
|
||||||
|
root.expanded = !root.expanded
|
||||||
|
} else {
|
||||||
|
root.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------------------------------------------------------- popup opzioni
|
||||||
|
fullRepresentation: PlasmaExtras.Representation {
|
||||||
|
Layout.minimumWidth: Kirigami.Units.gridUnit * 20
|
||||||
|
Layout.minimumHeight: Kirigami.Units.gridUnit * 13
|
||||||
|
Layout.preferredWidth: Kirigami.Units.gridUnit * 20
|
||||||
|
Layout.preferredHeight: popupColumn.implicitHeight + Kirigami.Units.gridUnit * 2
|
||||||
|
collapseMarginsHint: true
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: popupColumn
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Kirigami.Units.largeSpacing
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
Kirigami.Icon {
|
||||||
|
source: root.active ? root.iconOnPath : root.iconOffPath
|
||||||
|
implicitWidth: Kirigami.Units.iconSizes.medium
|
||||||
|
implicitHeight: Kirigami.Units.iconSizes.medium
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Kirigami.Heading {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
level: 3
|
||||||
|
text: root.active ? "Caffeine attivo" : "Caffeine inattivo"
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents3.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: root.modeText()
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
opacity: 0.7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents3.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: root.timerActive
|
||||||
|
text: "Disattivazione automatica tra " + root.formatDuration(root.remainingSec)
|
||||||
|
color: Kirigami.Theme.positiveTextColor
|
||||||
|
}
|
||||||
|
|
||||||
|
Kirigami.Separator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents3.CheckBox {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: "Impedisci anche lo spegnimento dello schermo"
|
||||||
|
checked: root.keepScreenOn
|
||||||
|
onToggled: {
|
||||||
|
plasmoid.configuration.keepScreenOn = checked
|
||||||
|
root.changeMode(checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
|
PlasmaComponents3.Label {
|
||||||
|
text: "Disattiva dopo:"
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents3.ComboBox {
|
||||||
|
id: timerCombo
|
||||||
|
Layout.fillWidth: true
|
||||||
|
model: ["Mai", "5 minuti", "15 minuti", "30 minuti", "1 ora", "2 ore", "4 ore"]
|
||||||
|
readonly property var minutes: [0, 5, 15, 30, 60, 120, 240]
|
||||||
|
currentIndex: Math.max(0, minutes.indexOf(plasmoid.configuration.autoOffMinutes))
|
||||||
|
onActivated: function(index) {
|
||||||
|
const value = minutes[index]
|
||||||
|
plasmoid.configuration.autoOffMinutes = value
|
||||||
|
root.changeTimer(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents3.CheckBox {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: plasmoid.configuration.autoOffMinutes > 0
|
||||||
|
text: "Notifica quando scade il timer"
|
||||||
|
checked: plasmoid.configuration.notifyOnTimerEnd
|
||||||
|
onToggled: plasmoid.configuration.notifyOnTimerEnd = checked
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents3.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
visible: root.backend === "unsupported" || root.lastError !== ""
|
||||||
|
color: Kirigami.Theme.negativeTextColor
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: root.backend === "unsupported"
|
||||||
|
? "Backend non disponibile: serve systemd con gestore utente attivo."
|
||||||
|
: "Errore dal backend: " + root.lastError
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents3.Button {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: root.active ? "Disattiva Caffeine" : "Attiva Caffeine"
|
||||||
|
icon.name: root.active ? "media-playback-stop" : "media-playback-start"
|
||||||
|
onClicked: root.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
PlasmaComponents3.Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
font: Kirigami.Theme.smallFont
|
||||||
|
opacity: 0.6
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
text: "Clic sinistro sull'icona: attiva/disattiva\nClic centrale: queste opzioni"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+29
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Installa/aggiorna il widget Caffeine e le unità systemd utente.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
if kpackagetool6 -t Plasma/Applet --upgrade "$HERE" 2>/dev/null; then
|
||||||
|
echo "Plasmoid aggiornato."
|
||||||
|
else
|
||||||
|
kpackagetool6 -t Plasma/Applet --install "$HERE"
|
||||||
|
echo "Plasmoid installato."
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$HERE/contents/scripts/caffeine.sh" ensure-units "$HERE/systemd"
|
||||||
|
|
||||||
|
cat <<'EOF'
|
||||||
|
|
||||||
|
Fatto. Per aggiungere il widget al pannello:
|
||||||
|
tasto destro sul pannello -> Aggiungi widget -> Caffeine (categoria Utilità)
|
||||||
|
|
||||||
|
Da terminale:
|
||||||
|
./bin/enne2-caffeine on 30 # attiva per 30 minuti
|
||||||
|
./bin/enne2-caffeine off
|
||||||
|
./bin/enne2-caffeine status
|
||||||
|
EOF
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"KPackageStructure": "Plasma/Applet",
|
||||||
|
"KPlugin": {
|
||||||
|
"Id": "org.enne2.caffeine",
|
||||||
|
"Name": "Caffeine",
|
||||||
|
"Description": "Impedisce con un clic lo spegnimento dello schermo, il blocco schermo e la sospensione automatica (equivalente KDE di Caffeine di GNOME). Backend: systemd-inhibit/logind via unità systemd utente. Due modalità: schermo + sospensione oppure solo sospensione (schermo libero di spegnersi). Timer di disattivazione automatica opzionale.",
|
||||||
|
"Icon": "preferences-system-power-management",
|
||||||
|
"Version": "1.0",
|
||||||
|
"License": "MIT",
|
||||||
|
"Authors": [
|
||||||
|
{
|
||||||
|
"Name": "enne2"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Category": "Utilities",
|
||||||
|
"Website": "https://git.enne2.net/enne2/kde-caffeine"
|
||||||
|
},
|
||||||
|
"X-Plasma-API-Minimum-Version": "6.0"
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Caffeine (enne2): blocca schermo, spegnimento schermo e sospensione automatica
|
||||||
|
Documentation=https://git.enne2.net/enne2/kde-caffeine
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/systemd-inhibit --what=sleep:idle --who=enne2-caffeine --why="Caffeine: schermo e sospensione bloccati" --mode=block /usr/bin/sleep infinity
|
||||||
|
KillMode=control-group
|
||||||
|
TimeoutStopSec=5
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Caffeine (enne2): blocca solo la sospensione automatica (schermo libero di spegnersi)
|
||||||
|
Documentation=https://git.enne2.net/enne2/kde-caffeine
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/bin/systemd-inhibit --what=sleep --who=enne2-caffeine --why="Caffeine: sospensione automatica bloccata" --mode=block /usr/bin/sleep infinity
|
||||||
|
KillMode=control-group
|
||||||
|
TimeoutStopSec=5
|
||||||
Reference in New Issue
Block a user