commit 74ca562efef160fba3f5cf5b2cb34d9f21cb296c Author: enne2 Date: Fri Sep 25 16:13:26 2026 +0200 Caffeine per KDE Plasma 6: widget di pannello (inibizione schermo + sospensione) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..690dad7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.qmlc +*.jsc +*~ +build/ +package/ +*.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..73b1387 --- /dev/null +++ b/README.md @@ -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. + +![tazza piena = attivo, tazza vuota = inattivo](contents/icons/caffeine-on.svg) + +## 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= +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). diff --git a/bin/enne2-caffeine b/bin/enne2-caffeine new file mode 100755 index 0000000..3e29d2d --- /dev/null +++ b/bin/enne2-caffeine @@ -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 diff --git a/contents/config/main.xml b/contents/config/main.xml new file mode 100644 index 0000000..e142f70 --- /dev/null +++ b/contents/config/main.xml @@ -0,0 +1,23 @@ + + + + + + + true + + + + 0 + 0 + 1440 + + + + true + + + diff --git a/contents/icons/caffeine-off.svg b/contents/icons/caffeine-off.svg new file mode 100644 index 0000000..b7f66a8 --- /dev/null +++ b/contents/icons/caffeine-off.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/contents/icons/caffeine-on.svg b/contents/icons/caffeine-on.svg new file mode 100644 index 0000000..be171c9 --- /dev/null +++ b/contents/icons/caffeine-on.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/contents/scripts/caffeine.sh b/contents/scripts/caffeine.sh new file mode 100755 index 0000000..d30ea2f --- /dev/null +++ b/contents/scripts/caffeine.sh @@ -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 [minuti] +# caffeine.sh switch # cambia modalità mantenendo il timer +# caffeine.sh off [notify] +# caffeine.sh toggle [minuti] +# caffeine.sh ensure-units +# +# 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 diff --git a/contents/ui/main.qml b/contents/ui/main.qml new file mode 100644 index 0000000..b361ffc --- /dev/null +++ b/contents/ui/main.qml @@ -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" + } + } + } +} diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..a9e6519 --- /dev/null +++ b/install.sh @@ -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 diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..188ca71 --- /dev/null +++ b/metadata.json @@ -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" +} diff --git a/systemd/enne2-caffeine-screen.service b/systemd/enne2-caffeine-screen.service new file mode 100644 index 0000000..5602451 --- /dev/null +++ b/systemd/enne2-caffeine-screen.service @@ -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 diff --git a/systemd/enne2-caffeine-sleep.service b/systemd/enne2-caffeine-sleep.service new file mode 100644 index 0000000..2a63fbe --- /dev/null +++ b/systemd/enne2-caffeine-sleep.service @@ -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