4 Commits
Author SHA1 Message Date
sankalp 8947bd643a feat(commands):simplify battery limit messaging for Ideapad laptopsThe command now detects Ideapad hardware and provides clearer messages about Conservation Mode, removing outdated warnings. 2026-03-23 18:30:37 +05:45
sankalp 5ac2aed516 chore(root): bump lapctl version to 0.3.2
Updated Cargo.lock and Cargo.toml to version 0.3.2 of the `lapctl` package.

New Features
- None

Enhancements
- Updated dependency version to 0.3.2Bug Fixes
- None
2026-03-23 17:28:36 +05:45
sankalp 4020f65e28 bumped to 0.3.2 2026-03-23 17:10:37 +05:45
sankalp d52b3992e3 feat(src/commands):add Ideapad conservation mode fallback and improve charge limit messaging 2026-03-23 17:08:36 +05:45
3 changed files with 69 additions and 42 deletions

No files matched your search

Generated
+1 -1
View File
@@ -596,7 +596,7 @@ dependencies = [
[[package]] [[package]]
name = "lapctl" name = "lapctl"
version = "0.3.1" version = "0.3.2"
dependencies = [ dependencies = [
"clap", "clap",
"env_logger", "env_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "lapctl" name = "lapctl"
version = "0.3.1" version = "0.3.2"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
+67 -40
View File
@@ -14,6 +14,10 @@ trait Lapctl {
async fn set_battery_limit(&self, percent: u32) -> zbus::Result<()>; async fn set_battery_limit(&self, percent: u32) -> zbus::Result<()>;
} }
fn is_ideapad() -> bool {
Path::new("/sys/bus/platform/drivers/ideapad_acpi").exists()
}
fn try_call_daemon(command: &BatteryCommands) -> bool { fn try_call_daemon(command: &BatteryCommands) -> bool {
let rt = match tokio::runtime::Runtime::new() { let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt, Ok(rt) => rt,
@@ -56,7 +60,16 @@ pub fn execute(command: &BatteryCommands) {
error!("Invalid percentage. Please provide a value between 1 and 100."); error!("Invalid percentage. Please provide a value between 1 and 100.");
std::process::exit(1); std::process::exit(1);
} }
println!("Setting battery charge limit to {}%", percent);
if is_ideapad() {
if *percent < 100 {
println!("Setting battery charge limit (Lenovo Conservation Mode ~60%)...");
} else {
println!("Disabling battery charge limit (charging to 100%)...");
}
} else {
println!("Setting battery charge limit to {}%...", percent);
}
} }
BatteryCommands::Status => {} BatteryCommands::Status => {}
} }
@@ -72,6 +85,27 @@ pub fn execute_local(command: &BatteryCommands) {
match command { match command {
BatteryCommands::Limit { percent } => { BatteryCommands::Limit { percent } => {
let mut success_count = 0; let mut success_count = 0;
// Ideapad fallback - search for conservation_mode in ideapad_acpi driver directories
if let Ok(drivers) = fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi") {
for entry in drivers.flatten() {
let conservation_path = entry.path().join("conservation_mode");
if conservation_path.exists() {
let val = if *percent < 100 { "1" } else { "0" };
match fs::write(&conservation_path, val) {
Ok(_) => {
success_count += 1;
}
Err(e) => {
debug!("Failed to write to {}: {}", conservation_path.display(), e)
}
}
}
}
}
// Only try standard thresholds if we haven't already succeeded with Ideapad fallback
// or if we want to ensure all bases are covered. For now, let's do both but prioritize the message.
let sys_class_power = Path::new("/sys/class/power_supply"); let sys_class_power = Path::new("/sys/class/power_supply");
if let Ok(entries) = fs::read_dir(sys_class_power) { if let Ok(entries) = fs::read_dir(sys_class_power) {
@@ -82,6 +116,10 @@ pub fn execute_local(command: &BatteryCommands) {
if name_str.starts_with("BAT") { if name_str.starts_with("BAT") {
let threshold_path = entry.path().join("charge_control_end_threshold"); let threshold_path = entry.path().join("charge_control_end_threshold");
if threshold_path.exists() { if threshold_path.exists() {
println!(
"Setting battery charge limit for {} to {}%",
name_str, percent
);
match fs::write(&threshold_path, percent.to_string()) { match fs::write(&threshold_path, percent.to_string()) {
Ok(_) => { Ok(_) => {
info!("Successfully set limit for {}", name_str); info!("Successfully set limit for {}", name_str);
@@ -91,36 +129,6 @@ pub fn execute_local(command: &BatteryCommands) {
debug!("Failed to write to {}: {}", threshold_path.display(), e) debug!("Failed to write to {}: {}", threshold_path.display(), e)
} }
} }
} else {
// Ideapad fallback
let conservation_path = Path::new(
"/sys/bus/platform/devices/ideapad_acpi/conservation_mode",
);
if conservation_path.exists() {
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),
}
}
} }
} }
} }
@@ -151,22 +159,41 @@ pub fn execute_local(command: &BatteryCommands) {
println!(" Capacity: {}%", capacity.trim()); println!(" Capacity: {}%", capacity.trim());
println!(" Status: {}", status.trim()); println!(" Status: {}", status.trim());
// Check for Ideapad conservation mode first using robust detection
let mut limit_shown = false;
if let Ok(drivers) = fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi")
{
for entry in drivers.flatten() {
let conservation_path = entry.path().join("conservation_mode");
if conservation_path.exists()
&& let Ok(mode) = fs::read_to_string(&conservation_path)
{
if mode.trim() == "1" {
println!(" Charge Limit: Conservation Mode (~60%)");
limit_shown = true;
} else if mode.trim() == "0" {
// On Ideapads 0 means 100% or off.
}
}
}
}
if !limit_shown {
if let Ok(limit) = if let Ok(limit) =
fs::read_to_string(bat_path.join("charge_control_end_threshold")) fs::read_to_string(bat_path.join("charge_control_end_threshold"))
{ {
println!(" Charge Limit: {}%", limit.trim()); println!(" Charge Limit: {}%", limit.trim());
} else { } else {
// Ideapad fallback display // Re-check if it's an ideapad and we haven't shown a limit (likely 100%)
let conservation_path = Path::new( if let Ok(drivers) =
"/sys/bus/platform/devices/ideapad_acpi/conservation_mode", fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi")
);
if conservation_path.exists()
&& let Ok(mode) = fs::read_to_string(conservation_path)
{ {
if mode.trim() == "1" { for entry in drivers.flatten() {
println!(" Charge Limit: Conservation Mode (~60%)"); if entry.path().join("conservation_mode").exists() {
} else {
println!(" Charge Limit: 100%"); println!(" Charge Limit: 100%");
break;
}
}
} }
} }
} }