diff --git a/ideapad-battery-care@macal/CHANGELOG.md b/ideapad-battery-care@macal/CHANGELOG.md new file mode 100644 index 00000000000..561edd92690 --- /dev/null +++ b/ideapad-battery-care@macal/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog - IdeaPad Battery Care + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.1.0] - 2025-11-28 + +### Changed +- Renamed from "Coidado Batería" to "IdeaPad Battery Care" +- Changed active mode icon from battery to plug (ac-adapter-symbolic) for better visual distinction +- Updated all user-facing strings to English +- Migrated from sudoers to PolicyKit for permission management +- Restructured repository for Cinnamon Spices compatibility + +### Fixed +- Fixed applet constructor timing issue with `_applet_context_menu` +- Fixed Cinnamon 6.4 compatibility in metadata.json + +## [1.0.0] - 2025-11-27 + +### Added +- Initial release +- Battery conservation mode toggle (80% charge limit) +- Cinnamon panel applet with one-click toggle +- CLI tool with status, on, off, toggle, restore commands +- Automatic hardware detection for Lenovo IdeaPad laptops +- Persistence of user preference across reboots +- Real-time battery percentage display +- Context menu with Enable/Disable options +- PolicyKit integration for passwordless operation +- Support for Cinnamon 5.0 through 6.4 diff --git a/ideapad-battery-care@macal/README.md b/ideapad-battery-care@macal/README.md new file mode 100644 index 00000000000..19d3bf6b078 --- /dev/null +++ b/ideapad-battery-care@macal/README.md @@ -0,0 +1,161 @@ +# IdeaPad Battery Care - Cinnamon Applet + +Battery conservation mode control for Lenovo IdeaPad laptops on Linux. + +## Description + +This Cinnamon applet allows you to control the **battery conservation mode** on Lenovo IdeaPad laptops. When enabled, this mode limits battery charging to **80%** to extend battery lifespan, which is ideal if your laptop is frequently connected to AC power. + +## Features + +- **One-click toggle** - Left-click the panel icon to toggle conservation mode +- **Visual indicators** - Plug icon when active (80%), battery icon when inactive (100%) +- **Real-time status** - Tooltip shows current mode and battery percentage +- **Persistence** - Remembers your preference across reboots +- **Auto-detection** - Automatically finds compatible Lenovo hardware +- **Secure** - Uses PolicyKit for safe permission handling + +## Icons + +| Icon | Status | +|------|--------| +| 🔌 Plug (ac-adapter) | Conservation mode **active** - charging limited to 80% | +| 🔋 Battery | Conservation mode **inactive** - full charge to 100% | +| ⚠️ Warning | Incompatible hardware detected | + +## Compatible Hardware + +This applet works with Lenovo laptops that support the `ideapad_acpi` kernel module with conservation mode feature. + +To check if your laptop is compatible: + +```bash +ls /sys/bus/platform/drivers/ideapad_acpi/*/conservation_mode 2>/dev/null && echo "Compatible" || echo "Not compatible" +``` + +### Tested Models + +- Lenovo IdeaPad 16IRL8 +- Other IdeaPad models with ideapad_acpi support + +## Requirements + +- Linux Mint with Cinnamon desktop (tested on LMDE6 with Cinnamon 6.4) +- Lenovo laptop with `ideapad_acpi` kernel module support +- PolicyKit (pkexec) for privilege elevation + +## Installation + +### From Cinnamon Spices + +1. Right-click on the Cinnamon panel +2. Select "Applets" +3. Click the "Download" tab +4. Search for "IdeaPad Battery Care" +5. Click the install button + +### Manual Installation + +1. Download the applet files +2. Extract to `~/.local/share/cinnamon/applets/ideapad-battery-care@cinnamon/` +3. Restart Cinnamon: `Alt+F2` → type `r` → Enter +4. Add the applet to your panel + +### CLI Installation (Recommended) + +For full functionality with the helper script and PolicyKit policy: + +```bash +git clone https://github.com/your-username/ideapad-battery-care.git +cd ideapad-battery-care +sudo ./scripts/install.sh +./scripts/install-applet.sh +``` + +## Usage + +### Panel Applet + +| Action | Result | +|--------|--------| +| **Left click** | Toggle conservation mode on/off | +| **Right click** | Context menu with Enable/Disable options | +| **Hover** | Tooltip shows current status and battery percentage | + +### CLI Commands (if CLI installed) + +```bash +ideapad-battery-care status # Show current status +ideapad-battery-care on # Enable conservation mode (80%) +ideapad-battery-care off # Disable conservation mode (100%) +ideapad-battery-care toggle # Toggle state +``` + +## Configuration + +Settings are automatically saved to: +``` +~/.config/ideapad-battery-care/config.json +``` + +Your preference is restored when the applet loads or when the system restarts. + +## How It Works + +1. **Hardware Detection**: The applet looks for the conservation mode control file at: + ``` + /sys/bus/platform/drivers/ideapad_acpi/VPC2004:00/conservation_mode + ``` + If not found, it searches all subdirectories under `/sys/bus/platform/drivers/ideapad_acpi/`. + +2. **Control**: Writing `1` enables conservation mode (80% charge limit), writing `0` disables it. + +3. **Permissions**: Uses PolicyKit (pkexec) with a helper script for secure privilege elevation without requiring password entry on active local sessions. + +## Troubleshooting + +### Applet shows warning icon + +Your laptop may not support conservation mode. Check compatibility with: +```bash +lsmod | grep ideapad_acpi +``` + +If the module is not loaded, try: +```bash +sudo modprobe ideapad_acpi +``` + +### Permission errors + +Make sure the CLI tools are installed: +```bash +sudo ./scripts/install.sh +``` + +This installs the PolicyKit policy that allows passwordless operation. + +### Applet doesn't update after CLI changes + +The applet polls for status updates every 30 seconds. Changes made via CLI will be reflected on the next poll cycle. + +### Applet doesn't appear + +Restart Cinnamon: `Alt+F2` → type `r` → Enter + +## License + +MIT License - See [LICENSE](../../LICENSE) file for details. + +## Author + +**Macal** + +## Project Links + +- **Main Repository**: https://github.com/your-username/ideapad-battery-care +- **Report Issues**: https://github.com/your-username/ideapad-battery-care/issues + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md) for version history. diff --git a/ideapad-battery-care@macal/files/ideapad-battery-care@cinnamon/applet.js b/ideapad-battery-care@macal/files/ideapad-battery-care@cinnamon/applet.js new file mode 100644 index 00000000000..c6e3d66f381 --- /dev/null +++ b/ideapad-battery-care@macal/files/ideapad-battery-care@cinnamon/applet.js @@ -0,0 +1,442 @@ +/** + * IdeaPad Battery Care - Cinnamon Applet + * + * Battery conservation mode control for Lenovo IdeaPad laptops. + * Limits charging to 80% to extend battery life. + */ + +const Applet = imports.ui.applet; +const PopupMenu = imports.ui.popupMenu; +const St = imports.gi.St; +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; +const Mainloop = imports.mainloop; +const Lang = imports.lang; +const ByteArray = imports.byteArray; + +// Constants +const UUID = "ideapad-battery-care@cinnamon"; +const APPLET_PATH = imports.ui.appletManager.appletMeta[UUID].path; + +// System paths +const DEFAULT_CONSERVATION_PATH = "/sys/bus/platform/drivers/ideapad_acpi/VPC2004:00/conservation_mode"; +const IDEAPAD_ACPI_DIR = "/sys/bus/platform/drivers/ideapad_acpi"; +const BATTERY_PATHS = [ + "/sys/class/power_supply/BAT0/capacity", + "/sys/class/power_supply/BAT1/capacity" +]; + +// Configuration +const CONFIG_DIR = GLib.get_home_dir() + "/.config/ideapad-battery-care"; +const CONFIG_FILE = CONFIG_DIR + "/config.json"; +const CACHE_FILE = CONFIG_DIR + "/.path_cache"; + +// Update interval (30 seconds) +const UPDATE_INTERVAL = 30; + +// Icons - "ac-adapter" for active (plugged/limited), "battery-good" for inactive (full charge) +const ICON_ACTIVE = "ac-adapter-symbolic"; +const ICON_INACTIVE = "battery-good-symbolic"; +const ICON_ERROR = "dialog-warning-symbolic"; + + +class IdeaPadBatteryCareApplet extends Applet.IconApplet { + + constructor(orientation, panel_height, instance_id) { + super(orientation, panel_height, instance_id); + + this._orientation = orientation; + this._instance_id = instance_id; + + // Internal state + this._conservationPath = null; + this._isConservationActive = false; + this._batteryPercent = null; + this._hardwareError = false; + this._pollTimeoutId = null; + + // Detect hardware + this._detectHardware(); + + // Setup context menu (after super()) + this._setupContextMenu(); + + // Cargar configuración y aplicar si existe + this._loadAndApplyConfig(); + + // Actualizar estado inicial + this._updateState(); + + // Iniciar polling + this._startPolling(); + } + + /** + * Detecta el archivo conservation_mode en el sistema + */ + _detectHardware() { + // Primero intentar leer de cache + let cachedPath = this._readCachedPath(); + if (cachedPath && this._fileExists(cachedPath)) { + this._conservationPath = cachedPath; + return; + } + + // Verificar ruta por defecto + if (this._fileExists(DEFAULT_CONSERVATION_PATH)) { + this._conservationPath = DEFAULT_CONSERVATION_PATH; + this._cachePath(DEFAULT_CONSERVATION_PATH); + return; + } + + // Buscar en rutas alternativas + let foundPath = this._findConservationMode(); + if (foundPath) { + this._conservationPath = foundPath; + this._cachePath(foundPath); + return; + } + + // Hardware no compatible + this._hardwareError = true; + global.logError("IdeaPad Battery Care: Hardware no compatible - conservation_mode no encontrado"); + } + + /** + * Busca el archivo conservation_mode recursivamente + */ + _findConservationMode() { + if (!this._fileExists(IDEAPAD_ACPI_DIR)) { + return null; + } + + try { + let [success, stdout, stderr, exit_status] = GLib.spawn_command_line_sync( + `find ${IDEAPAD_ACPI_DIR} -name conservation_mode 2>/dev/null` + ); + + if (success && stdout) { + let output = ByteArray.toString(stdout).trim(); + if (output) { + return output.split('\n')[0]; + } + } + } catch (e) { + global.logError("IdeaPad Battery Care: Error buscando conservation_mode: " + e.message); + } + + return null; + } + + /** + * Lee la ruta cacheada + */ + _readCachedPath() { + try { + if (this._fileExists(CACHE_FILE)) { + let [success, contents] = GLib.file_get_contents(CACHE_FILE); + if (success) { + return ByteArray.toString(contents).trim(); + } + } + } catch (e) { + // Ignorar errores de cache + } + return null; + } + + /** + * Guarda la ruta en cache + */ + _cachePath(path) { + try { + this._ensureConfigDir(); + GLib.file_set_contents(CACHE_FILE, path); + } catch (e) { + // Ignorar errores de cache + } + } + + /** + * Verifica si un archivo existe + */ + _fileExists(path) { + return GLib.file_test(path, GLib.FileTest.EXISTS); + } + + /** + * Asegura que el directorio de configuración existe + */ + _ensureConfigDir() { + if (!this._fileExists(CONFIG_DIR)) { + GLib.mkdir_with_parents(CONFIG_DIR, 0o755); + } + } + + /** + * Configura el menú contextual + */ + _setupContextMenu() { + // Separador + this._applet_context_menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + // Item para activar + this._activateItem = new PopupMenu.PopupMenuItem("Activar modo conservación"); + this._activateItem.connect('activate', Lang.bind(this, function() { + this._setConservationMode(true); + })); + this._applet_context_menu.addMenuItem(this._activateItem); + + // Item para desactivar + this._deactivateItem = new PopupMenu.PopupMenuItem("Desactivar modo conservación"); + this._deactivateItem.connect('activate', Lang.bind(this, function() { + this._setConservationMode(false); + })); + this._applet_context_menu.addMenuItem(this._deactivateItem); + } + + /** + * Lee el estado actual del modo conservación + */ + _readConservationMode() { + if (this._hardwareError || !this._conservationPath) { + return null; + } + + try { + let [success, contents] = GLib.file_get_contents(this._conservationPath); + if (success) { + let value = ByteArray.toString(contents).trim(); + return value === "1"; + } + } catch (e) { + global.logError("IdeaPad Battery Care: Error leyendo conservation_mode: " + e.message); + } + + return null; + } + + /** + * Escribe el nuevo estado del modo conservación + */ + _writeConservationMode(active) { + if (this._hardwareError || !this._conservationPath) { + return false; + } + + let value = active ? "1" : "0"; + + try { + // Usar el helper con pkexec (PolicyKit) + let [success, stdout, stderr, exit_status] = GLib.spawn_command_line_sync( + `pkexec /usr/local/bin/ideapad-battery-care-helper ${value} ${this._conservationPath}` + ); + + if (exit_status === 0) { + this._saveConfig(active); + return true; + } else { + global.logError("IdeaPad Battery Care: Error escribiendo conservation_mode: " + + (stderr ? ByteArray.toString(stderr) : "exit code " + exit_status)); + } + } catch (e) { + global.logError("IdeaPad Battery Care: Error ejecutando helper: " + e.message); + } + + return false; + } + + /** + * Lee el porcentaje de batería + */ + _readBatteryPercent() { + for (let i = 0; i < BATTERY_PATHS.length; i++) { + let path = BATTERY_PATHS[i]; + if (this._fileExists(path)) { + try { + let [success, contents] = GLib.file_get_contents(path); + if (success) { + return parseInt(ByteArray.toString(contents).trim()); + } + } catch (e) { + continue; + } + } + } + return null; + } + + /** + * Guarda la configuración del usuario + */ + _saveConfig(active) { + try { + this._ensureConfigDir(); + let config = JSON.stringify({ conservation_mode: active }); + GLib.file_set_contents(CONFIG_FILE, config); + } catch (e) { + global.logError("IdeaPad Battery Care: Error guardando configuración: " + e.message); + } + } + + /** + * Carga la configuración del usuario + */ + _loadConfig() { + try { + if (this._fileExists(CONFIG_FILE)) { + let [success, contents] = GLib.file_get_contents(CONFIG_FILE); + if (success) { + let config = JSON.parse(ByteArray.toString(contents)); + return config.conservation_mode; + } + } + } catch (e) { + // Ignorar errores de configuración + } + return null; + } + + /** + * Carga y aplica la configuración guardada + */ + _loadAndApplyConfig() { + let savedMode = this._loadConfig(); + if (savedMode !== null && !this._hardwareError) { + let currentMode = this._readConservationMode(); + if (currentMode !== null && currentMode !== savedMode) { + this._writeConservationMode(savedMode); + } + } + } + + /** + * Actualiza el estado del applet + */ + _updateState() { + // Leer estado actual + let mode = this._readConservationMode(); + if (mode !== null) { + this._isConservationActive = mode; + } + + // Leer batería + this._batteryPercent = this._readBatteryPercent(); + + // Actualizar UI + this._updateIcon(); + this._updateTooltip(); + this._updateMenuItems(); + } + + /** + * Actualiza el icono del applet + */ + _updateIcon() { + if (this._hardwareError) { + this.set_applet_icon_symbolic_name(ICON_ERROR); + } else if (this._isConservationActive) { + this.set_applet_icon_symbolic_name(ICON_ACTIVE); + } else { + this.set_applet_icon_symbolic_name(ICON_INACTIVE); + } + } + + /** + * Actualiza el tooltip del applet + */ + _updateTooltip() { + let tooltip = ""; + + if (this._hardwareError) { + tooltip = "Hardware no compatible"; + } else { + let modeText = this._isConservationActive + ? "Modo conservación: Activo (80%)" + : "Modo conservación: Inactivo (100%)"; + + let batteryText = this._batteryPercent !== null + ? "Batería: " + this._batteryPercent + "%" + : ""; + + tooltip = modeText; + if (batteryText) { + tooltip += "\n" + batteryText; + } + } + + this.set_applet_tooltip(tooltip); + } + + /** + * Actualiza los items del menú contextual + */ + _updateMenuItems() { + if (this._hardwareError) { + this._activateItem.setSensitive(false); + this._deactivateItem.setSensitive(false); + } else { + this._activateItem.setSensitive(!this._isConservationActive); + this._deactivateItem.setSensitive(this._isConservationActive); + } + } + + /** + * Cambia el modo de conservación + */ + _setConservationMode(active) { + if (this._writeConservationMode(active)) { + this._isConservationActive = active; + this._updateIcon(); + this._updateTooltip(); + this._updateMenuItems(); + } + } + + /** + * Inicia el polling para actualizar el estado + */ + _startPolling() { + if (this._pollTimeoutId) { + Mainloop.source_remove(this._pollTimeoutId); + } + + this._pollTimeoutId = Mainloop.timeout_add_seconds(UPDATE_INTERVAL, Lang.bind(this, function() { + this._updateState(); + return true; // Continuar el polling + })); + } + + /** + * Detiene el polling + */ + _stopPolling() { + if (this._pollTimeoutId) { + Mainloop.source_remove(this._pollTimeoutId); + this._pollTimeoutId = null; + } + } + + /** + * Manejador del clic en el applet + */ + on_applet_clicked(event) { + if (!this._hardwareError) { + this._setConservationMode(!this._isConservationActive); + } + } + + /** + * Manejador de eliminación del applet + */ + on_applet_removed_from_panel() { + this._stopPolling(); + } +} + +/** + * Función principal - punto de entrada del applet + */ +function main(metadata, orientation, panel_height, instance_id) { + return new IdeaPadBatteryCareApplet(orientation, panel_height, instance_id); +} diff --git a/ideapad-battery-care@macal/files/ideapad-battery-care@cinnamon/icons/battery-conservation-active.svg b/ideapad-battery-care@macal/files/ideapad-battery-care@cinnamon/icons/battery-conservation-active.svg new file mode 100644 index 00000000000..9a7766559c2 --- /dev/null +++ b/ideapad-battery-care@macal/files/ideapad-battery-care@cinnamon/icons/battery-conservation-active.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ideapad-battery-care@macal/files/ideapad-battery-care@cinnamon/icons/battery-conservation-inactive.svg b/ideapad-battery-care@macal/files/ideapad-battery-care@cinnamon/icons/battery-conservation-inactive.svg new file mode 100644 index 00000000000..0d7f3dfe614 --- /dev/null +++ b/ideapad-battery-care@macal/files/ideapad-battery-care@cinnamon/icons/battery-conservation-inactive.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + diff --git a/ideapad-battery-care@macal/files/ideapad-battery-care@cinnamon/icons/battery-error.svg b/ideapad-battery-care@macal/files/ideapad-battery-care@cinnamon/icons/battery-error.svg new file mode 100644 index 00000000000..3f3e3ee3937 --- /dev/null +++ b/ideapad-battery-care@macal/files/ideapad-battery-care@cinnamon/icons/battery-error.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/ideapad-battery-care@macal/files/ideapad-battery-care@cinnamon/metadata.json b/ideapad-battery-care@macal/files/ideapad-battery-care@cinnamon/metadata.json new file mode 100644 index 00000000000..b07050e7747 --- /dev/null +++ b/ideapad-battery-care@macal/files/ideapad-battery-care@cinnamon/metadata.json @@ -0,0 +1,12 @@ +{ + "uuid": "ideapad-battery-care@cinnamon", + "name": "IdeaPad Battery Care", + "description": "Battery conservation mode control for Lenovo IdeaPad laptops. Limits charging to 80% to extend battery life.", + "version": "1.1.0", + "cinnamon-version": ["5.0", "5.2", "5.4", "5.6", "5.8", "6.0", "6.2", "6.4"], + "max-instances": 1, + "author": "Macal", + "website": "https://github.com/Macal2/ideapad-battery-care", + "icon": "battery-good-symbolic", + "dangerous": false +} diff --git a/ideapad-battery-care@macal/info.json b/ideapad-battery-care@macal/info.json new file mode 100644 index 00000000000..f3801d2dcde --- /dev/null +++ b/ideapad-battery-care@macal/info.json @@ -0,0 +1,9 @@ +{ + "author": "Macal", + "description": "Battery conservation mode controller for Lenovo IdeaPad laptops. Limits battery charging to 80% to extend battery lifespan on laptops frequently connected to AC power.", + "name": "IdeaPad Battery Care", + "spices-human-readable-changelog": "## 1.1.0 (2025-11-28)\n- Renamed from 'Coidado Batería' to 'IdeaPad Battery Care'\n- Changed active mode icon to plug (ac-adapter-symbolic)\n- Updated user-facing strings to English\n- Migrated to PolicyKit for better security\n- Restructured for Cinnamon Spices compatibility\n\n## 1.0.0 (2025-11-27)\n- Initial release\n- Battery conservation mode control\n- Cinnamon panel applet integration\n- PolicyKit support for passwordless operation\n- Real-time battery status display\n- Persistent configuration across reboots", + "url": "https://github.com/Macal2/ideapad-battery-care", + "uuid": "ideapad-battery-care@cinnamon", + "version": "1.1.0" +} diff --git a/ideapad-battery-care@macal/screenshot.png b/ideapad-battery-care@macal/screenshot.png new file mode 100644 index 00000000000..044aaa8f6cb Binary files /dev/null and b/ideapad-battery-care@macal/screenshot.png differ