feat(src/commands): implement battery limit command with validation and enhance status output

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.
This commit is contained in:
sankalp committed 2026-03-11 23:47:50 +05:45
1 parent 2d03067f0e
commit b5ba6efd41
2 files changed
+122 -3

No files matched your search

+107 -2
View File
@@ -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%");
}
}
}
}
}
}
}
}
}
}
}
}
+15 -1
View File
@@ -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%");
}
}
}
}
}
}
}
}