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:
2 files changed
+122
-3
No files matched your search
+107
-2
@@ -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
@@ -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%");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user