diff --git a/Assets/Translations/de.json b/Assets/Translations/de.json index 9cddd5c7..82bff890 100644 --- a/Assets/Translations/de.json +++ b/Assets/Translations/de.json @@ -1446,7 +1446,14 @@ "charging-rate": "Laderate: {rate} W.", "discharging-rate": "Entladerate: {rate} W.", "charging": "Wird geladen.", - "discharging": "Wird entladen." + "discharging": "Wird entladen.", + "panel": { + "title": "Ladeschwelle", + "full": "Volle Kapazität ({percent}%)", + "balanced": "Ausgeglichen ({percent}%)", + "lifespan": "Verlängerte Lebensdauer ({percent}%)", + "disabled": "Batteriemanager deaktiviert" + } }, "authentication": { "failed": "Authentifizierung fehlgeschlagen", @@ -1601,6 +1608,19 @@ "low": "Niedriger Batteriestand", "low-desc": "Batterie ist bei {percent}%. Bitte schließen Sie das Ladegerät an." }, + "battery-manager": { + "title": "Batterieschwelle", + "set-success-desc": "Batterieschwelle auf {percent}% gesetzt", + "initial-setup": "Ersteinrichtung erforderlich", + "set-failed": "Fehler beim Setzen der Batterieschwelle", + "install-success": "Erfolgreich installiert", + "install-missing": "Erforderliche Dateien fehlen", + "install-unsupported": "System wird nicht unterstützt", + "install-failed": "Installation fehlgeschlagen", + "uninstall-setup": "Deinstallation, Authentifizierung erforderlich", + "uninstall-success": "Erfolgreich deinstalliert", + "uninstall-failed": "Deinstallation fehlgeschlagen" + }, "missing-control-center": { "label": "Letztes Control-Center-Widget entfernt", "description": "Das Control-Center-Widget wurde aus der Leiste entfernt. Um es erneut über die Leiste zu öffnen, fügen Sie das Widget wieder hinzu. Sie können es auch durch Rechtsklick auf die Leiste öffnen." diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 83e9d189..7bf2adb1 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -1574,6 +1574,19 @@ "low": "Low battery", "low-desc": "Battery is at {percent}%. Please connect the charger." }, + "battery-manager": { + "title": "Battery threshold", + "set-success-desc": "Battery threshold set to {percent}%", + "initial-setup": "Initial setup required", + "set-failed": "Failed to set battery threshold", + "install-success": "Installed successfully", + "install-missing": "Required files are missing", + "install-unsupported": "System is not supported", + "install-failed": "Installation failed", + "uninstall-setup": "Uninstalling, authentication required", + "uninstall-success": "Uninstalled successfully", + "uninstall-failed": "Uninstallation failed" + }, "missing-control-center": { "label": "Last Control Center widget removed", "description": "The Control Center widget has been removed from the bar. To access it from the bar again, you will need to re-add the widget. You can open it with right clicking on the bar too." @@ -1605,6 +1618,13 @@ "charging-rate": "Charging rate: {rate} W.", "discharging-rate": "Discharging rate: {rate} W.", "charging": "Charging.", - "discharging": "Discharging." + "discharging": "Discharging.", + "panel": { + "title": "Charge threshold", + "full": "Full capacity ({percent}%)", + "balanced": "Balanced ({percent}%)", + "lifespan": "Extended lifespan ({percent}%)", + "disabled": "Battery manager disabled" + } } } diff --git a/Assets/Translations/es.json b/Assets/Translations/es.json index 4646bdcf..d5e30bc1 100644 --- a/Assets/Translations/es.json +++ b/Assets/Translations/es.json @@ -1573,6 +1573,19 @@ "low": "Batería baja", "low-desc": "La batería está al {percent}%. Por favor, conecta el cargador." }, + "battery-manager": { + "title": "Umbral de batería", + "set-success-desc": "Umbral de batería establecido en {percent}%", + "initial-setup": "Configuración inicial requerida", + "set-failed": "No se pudo establecer el umbral de batería", + "install-success": "Instalado correctamente", + "install-missing": "Faltan archivos requeridos", + "install-unsupported": "El sistema no es compatible", + "install-failed": "Error en la instalación", + "uninstall-setup": "Desinstalando, se requiere autenticación", + "uninstall-success": "Desinstalado correctamente", + "uninstall-failed": "Error en la desinstalación" + }, "missing-control-center": { "label": "Se eliminó el último widget del Centro de control", "description": "El widget del Centro de control se eliminó de la barra. Para acceder a él nuevamente desde la barra, debes volver a añadir el widget. También puedes abrirlo haciendo clic derecho en la barra." @@ -1604,6 +1617,13 @@ "charging-rate": "Tasa de carga: {rate} W.", "discharging-rate": "Tasa de descarga: {rate} W.", "charging": "Cargando.", - "discharging": "Descargando." + "discharging": "Descargando.", + "panel": { + "title": "Umbral de carga", + "full": "Capacidad total ({percent}%)", + "balanced": "Equilibrado ({percent}%)", + "lifespan": "Vida útil prolongada ({percent}%)", + "disabled": "Administrador de batería deshabilitado" + } } } diff --git a/Assets/Translations/fr.json b/Assets/Translations/fr.json index bfd5e07e..9c9d5491 100644 --- a/Assets/Translations/fr.json +++ b/Assets/Translations/fr.json @@ -1573,6 +1573,19 @@ "low": "Batterie faible", "low-desc": "La batterie est à {percent}%. Veuillez brancher le chargeur." }, + "battery-manager": { + "title": "Seuil de batterie", + "set-success-desc": "Seuil de batterie défini à {percent}%", + "initial-setup": "Configuration initiale requise", + "set-failed": "Échec de la définition du seuil de batterie", + "install-success": "Installation réussie", + "install-missing": "Fichiers requis manquants", + "install-unsupported": "Système non pris en charge", + "install-failed": "Échec de l'installation", + "uninstall-setup": "Désinstallation, authentification requise", + "uninstall-success": "Désinstallation réussie", + "uninstall-failed": "Échec de la désinstallation" + }, "missing-control-center": { "label": "Dernier widget du Centre de contrôle supprimé", "description": "Le widget du Centre de contrôle a été retiré de la barre. Pour y accéder à nouveau depuis la barre, veuillez ré‑ajouter le widget. Vous pouvez aussi l'ouvrir en cliquant avec le bouton droit sur la barre." @@ -1604,6 +1617,13 @@ "charging-rate": "Taux de charge : {rate} W.", "discharging-rate": "Taux de décharge : {rate} W.", "charging": "En charge.", - "discharging": "En décharge." + "discharging": "En décharge.", + "panel": { + "title": "Seuil de charge", + "full": "Capacité totale ({percent}%)", + "balanced": "Équilibré ({percent}%)", + "lifespan": "Durée de vie prolongée ({percent}%)", + "disabled": "Gestionnaire de batterie désactivé" + } } } diff --git a/Assets/Translations/pt.json b/Assets/Translations/pt.json index dbc6ab4e..a4d968ae 100644 --- a/Assets/Translations/pt.json +++ b/Assets/Translations/pt.json @@ -1573,6 +1573,19 @@ "low": "Bateria Fraca", "low-desc": "A bateria está em {percent}%. Por favor, conecte o carregador." }, + "battery-manager": { + "title": "Limite da bateria", + "set-success-desc": "Limite da bateria definido para {percent}%", + "initial-setup": "Configuração inicial necessária", + "set-failed": "Falha ao definir o limite da bateria", + "install-success": "Instalado com sucesso", + "install-missing": "Arquivos necessários ausentes", + "install-unsupported": "Sistema não suportado", + "install-failed": "Falha na instalação", + "uninstall-setup": "Desinstalando, autenticação necessária", + "uninstall-success": "Desinstalado com sucesso", + "uninstall-failed": "Falha na desinstalação" + }, "missing-control-center": { "label": "Último widget da Central de Controle removido", "description": "O widget da Central de Controle foi removido da barra. Para acessá-lo novamente pela barra, adicione o widget novamente. Você também pode abri-lo clicando com o botão direito na barra." @@ -1604,6 +1617,19 @@ "charging-rate": "Taxa de carregamento: {rate} W.", "discharging-rate": "Taxa de descarregamento: {rate} W.", "charging": "Carregando.", - "discharging": "Descarregando." + "discharging": "Descarregando.", + "battery-manager": { + "title": "Limite da bateria", + "set-success-desc": "Limite da bateria definido para {percent}%", + "initial-setup": "Configuração inicial necessária", + "set-failed": "Falha ao definir o limite da bateria", + "install-success": "Instalado com sucesso", + "install-missing": "Arquivos necessários ausentes", + "install-unsupported": "Sistema não suportado", + "install-failed": "Falha na instalação", + "uninstall-setup": "Desinstalando, autenticação necessária", + "uninstall-success": "Desinstalado com sucesso", + "uninstall-failed": "Falha na desinstalação" + } } } diff --git a/Assets/Translations/zh-CN.json b/Assets/Translations/zh-CN.json index 251681be..213be9cd 100644 --- a/Assets/Translations/zh-CN.json +++ b/Assets/Translations/zh-CN.json @@ -1573,6 +1573,19 @@ "low": "电量低", "low-desc": "电量为 {percent}%。请连接充电器。" }, + "battery-manager": { + "title": "电池阈值", + "set-success-desc": "电池阈值已设置为 {percent}%", + "initial-setup": "需要初始设置", + "set-failed": "设置电池阈值失败", + "install-success": "安装成功", + "install-missing": "缺少必要文件", + "install-unsupported": "系统不受支持", + "install-failed": "安装失败", + "uninstall-setup": "正在卸载,需要身份验证", + "uninstall-success": "卸载成功", + "uninstall-failed": "卸载失败" + }, "missing-control-center": { "label": "最后一个控制中心小部件已移除", "description": "控制中心小部件已从状态栏中移除。要再次从状态栏访问它,您需要重新添加小部件。您也可以通过右键点击状态栏来打开它。" @@ -1604,6 +1617,13 @@ "charging-rate": "充电速率:{rate} W。", "discharging-rate": "放电速率:{rate} W。", "charging": "正在充电。", - "discharging": "正在放电。" + "discharging": "正在放电。", + "panel": { + "title": "充电阈值", + "full": "完全容量 ({percent}%)", + "balanced": "平衡 ({percent}%)", + "lifespan": "延长寿命 ({percent}%)", + "disabled": "电池管理器已禁用" + } } } diff --git a/Assets/settings-default.json b/Assets/settings-default.json index 77f04499..b8a82259 100644 --- a/Assets/settings-default.json +++ b/Assets/settings-default.json @@ -249,5 +249,8 @@ "enabled": false, "wallpaperChange": "", "darkModeChange": "" + }, + "battery": { + "chargingMode": 0 } } \ No newline at end of file diff --git a/Bin/battery-manager/battery-paths.conf b/Bin/battery-manager/battery-paths.conf new file mode 100644 index 00000000..3a539237 --- /dev/null +++ b/Bin/battery-manager/battery-paths.conf @@ -0,0 +1,5 @@ +# Battery charge control paths +# Add one path per line +/sys/class/power_supply/BAT0/charge_control_end_threshold +/sys/class/power_supply/BAT1/charge_control_end_threshold +/sys/class/power_supply/BAT0/charge_stop_threshold diff --git a/Bin/battery-manager/install-battery-manager.sh b/Bin/battery-manager/install-battery-manager.sh new file mode 100755 index 00000000..36315d15 --- /dev/null +++ b/Bin/battery-manager/install-battery-manager.sh @@ -0,0 +1,178 @@ +#!/usr/bin/env bash +set -e + +SUCCESS=0 +FAILURE=1 +MISSING_FILES=2 +UNSUPPORTED=3 + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +print_error() { + echo -e "$1" >&2 +} + +print_info() { + echo -e "$1" +} + +if [ "$EUID" -ne 0 ]; then + print_error "This script must be run with root privileges" + exit $FAILURE +fi + +print_info "Installing Battery Manager..." +echo + +if [ -n "$PKEXEC_UID" ]; then + ACTUAL_USER=$(getent passwd "$PKEXEC_UID" | cut -d: -f1) +else + ACTUAL_USER="$SUDO_USER" +fi + +if [ -z "$ACTUAL_USER" ]; then + print_error "Could not determine the actual user" + exit $FAILURE +fi + +print_info "Installing for user: $ACTUAL_USER" +echo + +print_info "Checking required files..." + +MISSING_FILES_LIST=() + +if [ ! -f "$SCRIPT_DIR/battery-paths.conf" ]; then + MISSING_FILES_LIST+=("battery-paths.conf") +fi + +if [ ! -f "$SCRIPT_DIR/templates/battery-manager.sh" ]; then + MISSING_FILES_LIST+=("battery-manager.sh") +fi + +if [ ! -f "$SCRIPT_DIR/templates/battery-manager.policy" ]; then + MISSING_FILES_LIST+=("battery-manager.policy") +fi + +if [ ! -f "$SCRIPT_DIR/templates/battery-manager.rules" ]; then + MISSING_FILES_LIST+=("battery-manager.rules") +fi + +if [ ${#MISSING_FILES_LIST[@]} -gt 0 ]; then + print_error "Missing required files in $SCRIPT_DIR:" + for file in "${MISSING_FILES_LIST[@]}"; do + print_error " - $file" + done + exit $MISSING_FILES +fi + +print_info "All required files found" + +print_info "Checking battery paths..." +BATTERY_PATHS=($(grep -v '^#' "$SCRIPT_DIR/battery-paths.conf" | grep -v '^$')) +EXISTING_PATHS=() + +for path in "${BATTERY_PATHS[@]}"; do + if [ -f "$path" ]; then + EXISTING_PATHS+=("$path") + fi +done + +if [ ${#EXISTING_PATHS[@]} -eq 0 ]; then + print_error "None of the battery control files exist. Please check your hardware compatibility." + exit $UNSUPPORTED +fi + +print_info "Found ${#EXISTING_PATHS[@]} compatible battery control file(s)" + +print_info "Installing battery manager script..." +BATTERY_MANAGER_SCRIPT="/usr/bin/battery-manager-$ACTUAL_USER" + +SHEBANG=$(head -n 1 "$SCRIPT_DIR/templates/battery-manager.sh") +echo "$SHEBANG" > "$BATTERY_MANAGER_SCRIPT" +echo "" >> "$BATTERY_MANAGER_SCRIPT" + +echo "BATTERY_PATHS=(" >> "$BATTERY_MANAGER_SCRIPT" +for path in "${EXISTING_PATHS[@]}"; do + echo " \"$path\"" >> "$BATTERY_MANAGER_SCRIPT" +done +echo ")" >> "$BATTERY_MANAGER_SCRIPT" + +echo "" >> "$BATTERY_MANAGER_SCRIPT" + +tail -n +2 "$SCRIPT_DIR/templates/battery-manager.sh" >> "$BATTERY_MANAGER_SCRIPT" + +chmod +x "$BATTERY_MANAGER_SCRIPT" + +print_info "Battery manager script created from $SCRIPT_DIR/templates/battery-manager.sh with compatible paths" +print_info "Script installed at $BATTERY_MANAGER_SCRIPT" + +print_info "Creating log file..." +touch /var/log/battery-manager.log +chmod 644 /var/log/battery-manager.log +print_info "Log file created at /var/log/battery-manager.log" + +print_info "Creating polkit policy..." + +POLICY_FILE="/usr/share/polkit-1/actions/com.local.battery-manager.$ACTUAL_USER.policy" + +sed -e "s/ACTUAL_USER_PLACEHOLDER/$ACTUAL_USER/g" \ + "$SCRIPT_DIR/templates/battery-manager.policy" > "$POLICY_FILE" + +print_info "Polkit policy copied from $SCRIPT_DIR/templates/battery-manager.policy" +print_info "Polkit policy created at $POLICY_FILE" +print_info "Creating polkit rule..." + +RULES_FILE="/etc/polkit-1/rules.d/50-battery-manager-$ACTUAL_USER.rules" + +sed "s/ACTUAL_USER_PLACEHOLDER/$ACTUAL_USER/g" \ + "$SCRIPT_DIR/templates/battery-manager.rules" > "$RULES_FILE" + +print_info "Polkit rule copied from $SCRIPT_DIR/templates/battery-manager.rules" +print_info "Polkit rule created for user: $ACTUAL_USER at $RULES_FILE" + +print_info "Restarting polkit..." +if systemctl restart polkit 2>/dev/null; then + print_info "Polkit restarted" +else + print_info "Could not restart polkit automatically, you may need to reboot" +fi + +print_info "Creating uninstall script..." +UNINSTALL_SCRIPT="$SCRIPT_DIR/uninstall-battery-manager.sh" + + +if [ -f "$SCRIPT_DIR/templates/uninstall-template" ]; then + SHEBANG=$(head -n 1 "$SCRIPT_DIR/templates/uninstall-template") +else + SHEBANG="#!/usr/bin/env bash" +fi +echo "$SHEBANG" > "$UNINSTALL_SCRIPT" +echo "" >> "$UNINSTALL_SCRIPT" + +cat >> "$UNINSTALL_SCRIPT" << EOF +SCRIPT_PATH="$BATTERY_MANAGER_SCRIPT" +POLICY_PATH="$POLICY_FILE" +RULE_PATH="$RULES_FILE" +LOG_PATH="/var/log/battery-manager.log" + +EOF + +if [ -f "$SCRIPT_DIR/templates/uninstall-template" ]; then + tail -n +2 "$SCRIPT_DIR/templates/uninstall-template" >> "$UNINSTALL_SCRIPT" +fi + +chmod 744 "$UNINSTALL_SCRIPT" +chown root:root "$UNINSTALL_SCRIPT" + +print_info "Uninstall script created at $UNINSTALL_SCRIPT" + + +echo +print_info "Installation complete!" +echo +print_info "Log file: /var/log/battery-manager.log" +print_info "User-specific script: $BATTERY_MANAGER_SCRIPT" +print_info "User-specific policy: $POLICY_FILE" +print_info "User-specific rules: $RULES_FILE" +print_info "User-specific uninstall script: $UNINSTALL_SCRIPT" diff --git a/Bin/battery-manager/set-battery-treshold.sh b/Bin/battery-manager/set-battery-treshold.sh new file mode 100755 index 00000000..defcc7f1 --- /dev/null +++ b/Bin/battery-manager/set-battery-treshold.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SUPPRESS_NOTIFICATIONS=false + +print_error() { + echo -e "$1" >&2 +} + +print_info() { + echo -e "$1" +} + +send_notification() { + local urgency="$1" + local title="$2" + local message="$3" + + if [ "$SUPPRESS_NOTIFICATIONS" = false ] && command -v notify-send >/dev/null 2>&1; then + notify-send -u "$urgency" "$title" "$message" + fi +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -q|--quiet) + SUPPRESS_NOTIFICATIONS=true + shift + ;; + -*) + print_error "Unknown option: $1" + echo "Usage: $0 [OPTIONS] " >&2 + echo "Options:" >&2 + echo " -q, --quiet Suppress notifications" >&2 + exit 1 + ;; + *) + BATTERY_LEVEL="$1" + shift + ;; + esac +done + +if [ -z "$BATTERY_LEVEL" ]; then + print_error "Battery level not specified" + echo "Usage: $0 [OPTIONS] " >&2 + echo "Options:" >&2 + echo " -q, --quiet Suppress notifications" >&2 + exit 1 +fi + +if ! [[ "$BATTERY_LEVEL" =~ ^[0-9]+$ ]] || [ "$BATTERY_LEVEL" -gt 100 ] || [ "$BATTERY_LEVEL" -lt 0 ]; then + print_error "Battery level must be a number between 0-100" + echo "Usage: $0 [OPTIONS] " >&2 + echo "Options:" >&2 + echo " -q, --quiet Suppress notifications" >&2 + exit 1 +fi + +CURRENT_USER="$USER" +if [ -z "$CURRENT_USER" ]; then + CURRENT_USER="$(whoami)" +fi + +BATTERY_MANAGER_PATH="/usr/bin/battery-manager-$CURRENT_USER" + +SUCCESS=0 +MISSING_FILES=2 + +if [ ! -f "$BATTERY_MANAGER_PATH" ]; then + print_error "Battery manager components missing for user $CURRENT_USER!" + exit $MISSING_FILES +fi + +print_info "Setting battery charging threshold to $BATTERY_LEVEL% for user $CURRENT_USER..." + +if pkexec "$BATTERY_MANAGER_PATH" "$BATTERY_LEVEL"; then + print_info "Battery charging threshold set to $BATTERY_LEVEL%" + send_notification "normal" "Battery Threshold Updated" \ + "Battery charging threshold has been set to $BATTERY_LEVEL%" +else + print_error "Failed to set battery charging threshold" + send_notification "critical" "Battery Threshold Failed" \ + "Failed to set battery charging threshold to $BATTERY_LEVEL%" + exit 1 +fi diff --git a/Bin/battery-manager/templates/battery-manager.policy b/Bin/battery-manager/templates/battery-manager.policy new file mode 100644 index 00000000..78473a7a --- /dev/null +++ b/Bin/battery-manager/templates/battery-manager.policy @@ -0,0 +1,17 @@ + + + + + Manage battery settings for ACTUAL_USER_PLACEHOLDER + Authentication is required to manage battery settings + + no + no + yes + + /usr/bin/battery-manager-ACTUAL_USER_PLACEHOLDER + true + + diff --git a/Bin/battery-manager/templates/battery-manager.rules b/Bin/battery-manager/templates/battery-manager.rules new file mode 100644 index 00000000..6e927653 --- /dev/null +++ b/Bin/battery-manager/templates/battery-manager.rules @@ -0,0 +1,14 @@ +polkit.addRule(function(action, subject) { + if (action.id == "com.local.battery-manager.ACTUAL_USER_PLACEHOLDER" && + subject.user == "ACTUAL_USER_PLACEHOLDER") { + + // Check if the parent process is quickshell or set-battery-threshold + var pid = subject.pid; + var ppid = polkit.spawn(["ps", "-o", "ppid=", "-p", pid.toString()]).trim(); + var parentCmd = polkit.spawn(["ps", "-o", "comm=", "-p", ppid]).trim(); + + if (parentCmd.indexOf("quickshell") !== -1 || parentCmd.indexOf("set-battery-treshold") !== -1) { + return polkit.Result.YES; + } + } +}); diff --git a/Bin/battery-manager/templates/battery-manager.sh b/Bin/battery-manager/templates/battery-manager.sh new file mode 100644 index 00000000..41e551b0 --- /dev/null +++ b/Bin/battery-manager/templates/battery-manager.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +LOG_FILE="/var/log/battery-manager.log" + +log_message() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE" +} + +if [ -z "$1" ]; then + echo "Error: No battery level provided" >&2 + log_message "ERROR: No battery level provided" + exit 1 +fi + +BATTERY_LEVEL="$1" + +if ! [[ "$BATTERY_LEVEL" =~ ^[0-9]+$ ]] || [ "$BATTERY_LEVEL" -gt 100 ] || [ "$BATTERY_LEVEL" -lt 0 ]; then + echo "Error: Invalid battery level. Must be 0-100" >&2 + log_message "ERROR: Invalid battery level: $BATTERY_LEVEL" + exit 1 +fi + +SUCCESS_COUNT=0 +FAIL_COUNT=0 + +for path in "${BATTERY_PATHS[@]}"; do + [[ -z "$path" || "$path" =~ ^# ]] && continue + + if [ -f "$path" ] && [ -w "$path" ]; then + if echo "$BATTERY_LEVEL" > "$path" 2>/dev/null; then + echo "Updated: $path" + log_message "SUCCESS: Updated $path to $BATTERY_LEVEL" + ((SUCCESS_COUNT++)) + else + echo "Failed to write: $path" >&2 + log_message "ERROR: Failed to write to $path" + ((FAIL_COUNT++)) + fi + else + echo "Skipped (not found/writable): $path" + log_message "INFO: Skipped $path (not found or not writable)" + fi +done + +log_message "SUMMARY: Updated $SUCCESS_COUNT file(s), failed $FAIL_COUNT, battery level: $BATTERY_LEVEL" + +if [ "$SUCCESS_COUNT" -eq 0 ]; then + echo "Error: No battery files were updated" >&2 + exit 1 +fi + +echo "Successfully updated $SUCCESS_COUNT battery file(s)" +exit 0 diff --git a/Bin/battery-manager/templates/uninstall-template b/Bin/battery-manager/templates/uninstall-template new file mode 100644 index 00000000..f2cf83eb --- /dev/null +++ b/Bin/battery-manager/templates/uninstall-template @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +if [ "$(id -u)" -ne 0 ]; then + echo "This script must be run as root" + exit 1 +fi + +echo "Uninstalling battery manager..." + +if [ -f "$SCRIPT_PATH" ]; then + rm -f "$SCRIPT_PATH" + echo "Removed script from $SCRIPT_PATH" +fi + +if [ -f "$POLICY_PATH" ]; then + rm -f "$POLICY_PATH" + echo "Removed policy file from $POLICY_PATH" +fi + +if [ -f "$RULE_PATH" ]; then + rm -f "$RULE_PATH" + echo "Removed udev rule from $RULE_PATH" +fi + +if [ -f "$LOG_PATH" ]; then + rm -f "$LOG_PATH" + echo "Removed log file from $LOG_PATH" +fi + +echo "Uninstallation completed successfully" diff --git a/Commons/Settings.qml b/Commons/Settings.qml index 01d631e4..d0308487 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -389,6 +389,11 @@ Singleton { property string wallpaperChange: "" property string darkModeChange: "" } + + // battery + property JsonObject battery: JsonObject { + property int chargingMode: 0 + } } // ----------------------------------------------------- diff --git a/Modules/Bar/Battery/BatteryPanel.qml b/Modules/Bar/Battery/BatteryPanel.qml new file mode 100644 index 00000000..72b67f4c --- /dev/null +++ b/Modules/Bar/Battery/BatteryPanel.qml @@ -0,0 +1,137 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import Quickshell +import Quickshell.Wayland +import qs.Commons +import qs.Services +import qs.Widgets + +NPanel { + id: root + + preferredWidth: 350 + preferredHeight: 210 + panelKeyboardFocus: true + + property var optionsModel: [] + + function updateOptionsModel() { + let newOptions = [{ + "id": BatteryService.ChargingMode.Full, + "label": "battery.panel.full", + "icon": "battery-4" + }, { + "id": BatteryService.ChargingMode.Balanced, + "label": "battery.panel.balanced", + "icon": "battery-3" + }, { + "id": BatteryService.ChargingMode.Lifespan, + "label": "battery.panel.lifespan", + "icon": "battery-2" + }] + root.optionsModel = newOptions + } + + onOpened: { + updateOptionsModel() + } + + panelContent: Rectangle { + color: Color.transparent + + ColumnLayout { + anchors.fill: parent + anchors.margins: Style.marginL * scaling + spacing: Style.marginM * scaling + + // HEADER + RowLayout { + Layout.fillWidth: true + spacing: Style.marginM * scaling + + NText { + text: I18n.tr("battery.panel.title") + pointSize: Style.fontSizeL * scaling + font.weight: Style.fontWeightBold + color: Color.mOnSurface + Layout.fillWidth: true + } + + NToggle { + id: batteryManagerSwitch + checked: BatteryService.chargingMode !== BatteryService.ChargingMode.Disabled + onToggled: checked => BatteryService.toggleEnabled(checked) + baseSize: Style.baseWidgetSize * 0.65 * scaling + } + + NIconButton { + icon: "close" + tooltipText: I18n.tr("tooltips.close") + baseSize: Style.baseWidgetSize * 0.8 + onClicked: { + root.close() + } + } + } + + NDivider { + Layout.fillWidth: true + } + + ButtonGroup { + id: batteryGroup + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: Color.transparent + + ColumnLayout { + anchors.fill: parent + spacing: Style.marginM * scaling + + Repeater { + model: optionsModel + + NRadioButton { + visible: BatteryService.chargingMode !== BatteryService.ChargingMode.Disabled + ButtonGroup.group: batteryGroup + required property var modelData + text: I18n.tr(modelData.label, { + "percent": BatteryService.getThresholdValue(modelData.id) + }) + checked: BatteryService.chargingMode === modelData.id + onClicked: { + BatteryService.setChargingMode(modelData.id) + } + Layout.fillWidth: true + } + } + } + + ColumnLayout { + visible: BatteryService.chargingMode === BatteryService.ChargingMode.Disabled + anchors.fill: parent + spacing: Style.marginM * scaling + + Item { + Layout.fillHeight: true + } + + NText { + text: I18n.tr("battery.panel.disabled") + pointSize: Style.fontSizeL * scaling + color: Color.mOnSurfaceVariant + Layout.alignment: Qt.AlignHCenter + } + + Item { + Layout.fillHeight: true + } + } + } + } + } +} diff --git a/Modules/Bar/Widgets/Battery.qml b/Modules/Bar/Widgets/Battery.qml index 02032cd9..47676cdb 100644 --- a/Modules/Bar/Widgets/Battery.qml +++ b/Modules/Bar/Widgets/Battery.qml @@ -95,6 +95,7 @@ Item { forceOpen: isReady && (testMode || battery.isLaptopBattery) && displayMode === "alwaysShow" forceClose: displayMode === "alwaysHide" disableOpen: (!isReady || (!testMode && !battery.isLaptopBattery)) + onClicked: PanelService.getPanel("batteryPanel")?.toggle(this) tooltipText: { let lines = [] if (testMode) { diff --git a/Services/BatteryService.qml b/Services/BatteryService.qml index f927c83a..cf64b853 100644 --- a/Services/BatteryService.qml +++ b/Services/BatteryService.qml @@ -1,6 +1,7 @@ pragma Singleton import Quickshell +import Quickshell.Io import Quickshell.Services.UPower import qs.Commons import qs.Services @@ -8,6 +9,21 @@ import qs.Services Singleton { id: root + enum ChargingMode { + Disabled = 0, + Full, + Balanced, + Lifespan + } + + property int chargingMode: Settings.data.battery.chargingMode + readonly property string batterySetterScript: Quickshell.shellDir + '/Bin/battery-manager/set-battery-treshold.sh' + readonly property string batteryInstallerScript: Quickshell.shellDir + '/Bin/battery-manager/install-battery-manager.sh' + readonly property string batteryUninstallerScript: Quickshell.shellDir + '/Bin/battery-manager/uninstall-battery-manager.sh' + + // This is used to omit toast message and writing mode to settings on startup + property bool initialSetter: true + // Choose icon based on charge and charging state function getIcon(percent, charging, isReady) { if (!isReady) { @@ -28,4 +44,204 @@ Singleton { return "battery" } } + + function getThresholdValue(chargingMode) { + switch (chargingMode) { + case BatteryService.ChargingMode.Full: + return "100" + case BatteryService.ChargingMode.Balanced: + return "80" + case BatteryService.ChargingMode.Lifespan: + return "60" + } + } + + function toggleEnabled(enabled) { + if (enabled) { + setChargingMode(BatteryService.ChargingMode.Full) + } else { + BatteryService.chargingMode = BatteryService.ChargingMode.Disabled + BatteryService.initialSetter = true + ToastService.showNotice(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.uninstall-setup")) + PanelService.getPanel("batteryPanel")?.toggle(this) + uninstallerProcess.running = true + } + } + + function setChargingMode(newMode) { + if (newMode !== BatteryService.ChargingMode.Full && newMode !== BatteryService.ChargingMode.Balanced && newMode !== BatteryService.ChargingMode.Lifespan) { + Logger.warn("BatteryService", `Invalid charging mode set ${newMode}`) + return + } + BatteryService.chargingMode = newMode + BatteryService.applyChargingMode() + } + + function cycleModes() { + // Cycles charging modes from full to lifespan while skipping disabled + const nextMode = (chargingMode % 3) + 1 + setChargingMode(nextMode) + } + + function applyChargingMode() { + let command = [batterySetterScript] + + // Currently the script sends notifications by default but quickshell + // uses toast messages so the flag is passed to supress notifs + command.push("-q") + + command.push(BatteryService.getThresholdValue(BatteryService.chargingMode)) + + setterProcess.command = command + setterProcess.running = true + } + + function runInstaller() { + installerProcess.command = ["pkexec", batteryInstallerScript] + installerProcess.running = true + } + + function init() { + if (BatteryService.chargingMode !== BatteryService.ChargingMode.Disabled && BatteryService.chargingMode !== BatteryService.ChargingMode.Full) { + BatteryService.applyChargingMode() + } + } + + Process { + id: setterProcess + workingDirectory: Quickshell.shellDir + running: false + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + Logger.log("BatteryService", "Battery threshold set successfully") + if (BatteryService.initialSetter) { + BatteryService.initialSetter = false + return + } + ToastService.showNotice(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.set-success-desc", { + "percent": BatteryService.getThresholdValue(BatteryService.chargingMode) + })) + Settings.data.battery.chargingMode = BatteryService.chargingMode + } else if (exitCode === 2) { + ToastService.showWarning(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.initial-setup")) + PanelService.getPanel("batteryPanel")?.toggle(this) + BatteryService.runInstaller() + } else { + ToastService.showError(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.set-failed")) + Logger.error("BatteryService", `Setter process failed with exit code: ${exitCode}`) + } + } + stderr: StdioCollector { + onStreamFinished: { + if (this.text) { + Logger.warn("BatteryService", "SetterProcess stderr:", this.text) + } + } + } + stdout: StdioCollector { + onStreamFinished: { + if (this.text) { + Logger.log("BatteryService", "SetterProcess stdout:", this.text) + } + } + } + } + + // Installer process - installs battery manager components + Process { + id: installerProcess + workingDirectory: Quickshell.shellDir + running: false + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + ToastService.showNotice(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.install-success")) + BatteryService.applyChargingMode() + } else if (exitCode === 2) { + ToastService.showError(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.install-missing")) + } else if (exitCode === 3) { + ToastService.showError(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.install-unsupported")) + } else { + ToastService.showError(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.install-failed")) + } + + if (exitCode !== 0) { + BatteryService.chargingMode = BatteryService.ChargingMode.Disabled + } + } + stderr: StdioCollector { + onStreamFinished: { + if (this.text) { + Logger.warn("BatteryService", "InstallerProcess stderr:", this.text) + } + } + } + stdout: StdioCollector { + onStreamFinished: { + if (this.text) { + Logger.log("BatteryService", "InstallerProcess stdout:", this.text) + } + } + } + } + + Process { + id: uninstallerProcess + workingDirectory: Quickshell.shellDir + command: ["pkexec", batteryUninstallerScript] + running: false + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + Logger.log("BatteryService", "Battery Manager uninstalled successfully") + ToastService.showNotice(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.uninstall-success")) + Settings.data.battery.chargingMode = BatteryService.chargingMode + cleanupProcess.running = true + } else { + ToastService.showError(I18n.tr("toast.battery-manager.title"), I18n.tr("toast.battery-manager.uninstall-failed")) + Logger.error("BatteryService", `Uninstaller process failed with exit code: ${exitCode}`) + } + } + stderr: StdioCollector { + onStreamFinished: { + if (this.text) { + Logger.warn("BatteryService", "UninstallerProcess stderr:", this.text) + } + } + } + stdout: StdioCollector { + onStreamFinished: { + if (this.text) { + Logger.log("BatteryService", "UninstallerProcess stdout:", this.text) + } + } + } + } + + // Cleanup process - deletes uninstaller after it sucessfull ; + Process { + id: cleanupProcess + workingDirectory: Quickshell.shellDir + command: ["rm", "-rf", batteryUninstallerScript] + running: false + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + Logger.log("BatteryService", "Battery Manager uninstalled successfully") + } else { + Logger.error("BatteryService", `Cleanup process failed with exit code: ${exitCode}`) + } + } + stderr: StdioCollector { + onStreamFinished: { + if (this.text) { + Logger.warn("BatteryService", "CleanupProcess stderr:", this.text) + } + } + } + stdout: StdioCollector { + onStreamFinished: { + if (this.text) { + Logger.log("BatteryService", "CleanupProcess stdout:", this.text) + } + } + } + } } diff --git a/Services/IPCService.qml b/Services/IPCService.qml index a62e2474..ddae8f78 100644 --- a/Services/IPCService.qml +++ b/Services/IPCService.qml @@ -183,6 +183,28 @@ Item { } } + IpcHandler { + target: "batteryManager" + + function cycle() { + BatteryService.cycleModes() + } + + function set(mode: string) { + switch (mode) { + case "full": + BatteryService.setChargingMode(BatteryService.ChargingMode.Full) + break + case "balanced": + BatteryService.setChargingMode(BatteryService.ChargingMode.Balanced) + break + case "lifespan": + BatteryService.setChargingMode(BatteryService.ChargingMode.Lifespan) + break + } + } + } + IpcHandler { target: "media" function playPause() { diff --git a/shell.qml b/shell.qml index b110ba32..39b76224 100644 --- a/shell.qml +++ b/shell.qml @@ -28,6 +28,7 @@ import qs.Modules.SessionMenu import qs.Modules.Bar import qs.Modules.Bar.Extras import qs.Modules.Bar.Bluetooth +import qs.Modules.Bar.Battery import qs.Modules.Bar.Calendar import qs.Modules.Bar.WiFi @@ -87,6 +88,7 @@ ShellRoot { FontService.init() HooksService.init() BluetoothService.init() + BatteryService.init() } Background {} @@ -160,6 +162,10 @@ ShellRoot { id: wallpaperPanel objectName: "wallpaperPanel" } + BatteryPanel { + id: batteryPanel + objectName: "batteryPanel" + } } } }