From b5ba6efd41f40cc933c6a8ff6c7f185b9b8ddcb9 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Wed, 11 Mar 2026 23:47:50 +0545 Subject: [PATCH] feat(src/commands): implement battery limit command with validation and enhance status output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The changes introduce a robust `BatteryCommands::Limit` implementation that validates user‑provided percentages, writes charge‑control thresholds, and gracefully handles Lenovo Ideapad firmware via the `conservation_mode` fallback. The `lapctl status` command is also updated to display battery‑specific details such as capacity, status, and charge limits, replacing the previous generic placeholder. New Features - Added `BatteryCommands::Limit` handling with range validation (1‑100) and support for multiple `/sys/class/power_supply` entries. - Implemented Ideapad fallback using `conservation_mode` to provide custom charge limits or warnings when unsupported. - Extended `lapctl status` to show charge limit and Conservation Mode for each battery. Enhancements - Added root‑privilege check (`assert_root`) and improved error/debug logging throughout the command. - Restructured directory traversal logic for readability and added `debug!` traces on write failures. - Re‑used status logic to present battery‑specific information instead of a generic placeholder. Bug Fixes - Resolved placeholder status output and proper handling of missing `charge_control_end_threshold` files. - Ensured correct exit codes and clear error messages for invalid percentage inputs. --- src/commands/battery.rs | 109 +++++++++++++++++++++++++++++++++++++++- src/commands/status.rs | 16 +++++- 2 files changed, 122 insertions(+), 3 deletions(-) diff --git a/src/commands/battery.rs b/src/commands/battery.rs index e9485b6..99c5165 100644 --- a/src/commands/battery.rs +++ b/src/commands/battery.rs @@ -1,12 +1,117 @@ use crate::cli::BatteryCommands; +use crate::utils::system::assert_root; +use log::{debug, error, info}; +use std::fs; +use std::path::Path; pub fn execute(command: &BatteryCommands) { match command { BatteryCommands::Limit { percent } => { - println!("Setting battery limit to {}%", percent); + assert_root(); + + if percent < &1 || percent > &100 { + error!("Invalid percentage. Please provide a value between 1 and 100."); + std::process::exit(1); + } + + println!("Setting battery charge limit to {}%", percent); + + let mut success_count = 0; + let sys_class_power = Path::new("/sys/class/power_supply"); + + if let Ok(entries) = fs::read_dir(sys_class_power) { + for entry in entries.flatten() { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + + if name_str.starts_with("BAT") { + let threshold_path = entry.path().join("charge_control_end_threshold"); + if threshold_path.exists() { + match fs::write(&threshold_path, percent.to_string()) { + Ok(_) => { + info!("Successfully set limit for {}", name_str); + success_count += 1; + } + Err(e) => debug!("Failed to write to {}: {}", threshold_path.display(), e), + } + } else { + // Ideapad fallback + let mut ideapad_found = false; + if let Ok(ideapad_entries) = fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi") { + for ideapad_entry in ideapad_entries.flatten() { + let ideapad_path = ideapad_entry.path(); + let conservation_path = ideapad_path.join("conservation_mode"); + if conservation_path.exists() { + ideapad_found = true; + let val = if *percent < 100 { "1" } else { "0" }; + match fs::write(&conservation_path, val) { + Ok(_) => { + if *percent < 100 { + println!("WARNING: Your laptop (Lenovo Ideapad) does NOT support custom charge limits (like {}%).", percent); + println!("Instead, lapctl has enabled Lenovo's built-in 'Conservation Mode' via the Ideapad ACPI."); + println!("This mode is hard-coded into your laptop's firmware to stop charging at ~60%."); + } else { + println!("Disabled Conservation Mode (charging to 100%)."); + } + success_count += 1; + } + Err(e) => debug!("Failed to write to conservation_mode: {}", e), + } + } + } + } + if !ideapad_found { + debug!("limit not supported for {} (missing thresholds)", name_str); + } + } + } + } + } + + if success_count > 0 { + println!("Operation completed successfully."); + } else { + error!("Hardware does not support dynamic charge thresholds via sysfs or no batteries were found."); + } } BatteryCommands::Status => { - println!("Battery status placeholder"); + // Re-use logic from `lapctl status` to show battery-specific details: + let sys_class_power = Path::new("/sys/class/power_supply"); + if let Ok(entries) = fs::read_dir(sys_class_power) { + for entry in entries.flatten() { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if name_str.starts_with("BAT") { + let bat_path = entry.path(); + let capacity = fs::read_to_string(bat_path.join("capacity")).unwrap_or_else(|_| "Unknown".into()); + let status = fs::read_to_string(bat_path.join("status")).unwrap_or_else(|_| "Unknown".into()); + + println!("{}:", name_str); + println!(" Capacity: {}%", capacity.trim()); + println!(" Status: {}", status.trim()); + + if let Ok(limit) = fs::read_to_string(bat_path.join("charge_control_end_threshold")) { + println!(" Charge Limit: {}%", limit.trim()); + } else { + // Ideapad fallback display + if let Ok(ideapad_entries) = fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi") { + for ideapad_entry in ideapad_entries.flatten() { + let conservation_path = ideapad_entry.path().join("conservation_mode"); + if conservation_path.exists() { + if let Ok(mode) = fs::read_to_string(&conservation_path) { + if mode.trim() == "1" { + println!(" Charge Limit: Conservation Mode (~60%)"); + } else { + println!(" Charge Limit: 100%"); + } + } + } + } + } + } + } + } + } } } } diff --git a/src/commands/status.rs b/src/commands/status.rs index 3014111..2ab6f4b 100644 --- a/src/commands/status.rs +++ b/src/commands/status.rs @@ -26,9 +26,23 @@ pub fn execute() { println!(" Capacity: {}%", capacity.trim()); println!(" Status: {}", status.trim()); - // Print charing limit if exists (e.g., charge_control_end_threshold) if let Ok(limit) = fs::read_to_string(bat_path.join("charge_control_end_threshold")) { println!(" Charge Limit: {}%", limit.trim()); + } else { + if let Ok(ideapad_entries) = fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi") { + for ideapad_entry in ideapad_entries.flatten() { + let conservation_path = ideapad_entry.path().join("conservation_mode"); + if conservation_path.exists() { + if let Ok(mode) = fs::read_to_string(&conservation_path) { + if mode.trim() == "1" { + println!(" Charge Limit: Conservation Mode (~60%)"); + } else { + println!(" Charge Limit: 100%"); + } + } + } + } + } } } }