Merge pull request #3 from S4NKALP/dev

This commit was merged in pull request #3.
This commit is contained in:
sankalp authored and GitHub committed 2026-03-22 17:55:25 +05:45
commit 3590f693d3
18 files changed
+2010 -112

No files matched your search

Generated
+1029
View File
File diff suppressed because it is too large. Load diff
+3
View File
@@ -12,3 +12,6 @@ env_logger = "0.11"
regex = "1.10"
wayland-client = "0.31"
wayland-protocols-wlr = { version = "0.3", features = ["client"] }
zbus = { version = "5.5.0", features = ["tokio"] }
tokio = { version = "1.21", features = ["full"] }
futures-util = "0.3"
+12 -8
View File
@@ -12,9 +12,7 @@
### Why lapctl?
Modern Linux laptops often have great hardware that goes underutilized or requires heavy, bloated background services to manage. **lapctl** changes that.
Built with performance and simplicity in mind, it talks directly to your system's hardware interfaces (`sysfs`, `acpi`, `udev`). No background daemons (unless you want them), no heavy RAM usage just pure control from your terminal.
Built with performance and simplicity in mind, it talks directly to your system's hardware interfaces (`sysfs`, `acpi`, `udev`). A high-performance background daemon (**lapctld**) handles all privileged operations via a secure D-Bus interface, allowing you to control your laptop completely **without sudo**.
---
@@ -52,6 +50,8 @@ just install
sudo lapctl install-rules
```
*The `install-rules` command automatically sets up the D-Bus policy and starts the `lapctld` background service.*
#### Option 3: Using `cargo`
```bash
@@ -74,9 +74,11 @@ sudo lapctl install-rules
### Quick Start Guide
```bash
# Install udev rules (Required for rootless control if not using AUR)
# Install D-Bus policy & start daemon (Required for rootless control)
sudo lapctl install-rules
# From now on, NO sudo is required!
# Manage your GPU
lapctl gpu integrated # Max battery
lapctl gpu hybrid # Best of both worlds
@@ -104,9 +106,10 @@ lapctl touchpad enable
lapctl display rates
lapctl display set-rate 144
# Keep it awake
lapctl inhibit --daemon # Run in background
lapctl inhibit -- why "Critical update" ./long-task.sh
# Keep it awake (requires lapctld)
lapctl inhibit --daemon # Stay awake persistently (via daemon)
lapctl inhibit --stop # Allow sleep again
lapctl inhibit --why "Keeping awake" -- sleep 60 # One-shot (local)
# Check everything
lapctl status
@@ -124,7 +127,8 @@ lapctl
├── src/
│ ├── main.rs # The entry point
│ ├── cli.rs # Command definition & parsing
│ ├── commands/ # Feature logic (GPU, Battery, etc.)
│ ├── daemon/ # lapctld (D-Bus interface & logic)
│ ├── commands/ # CLI feature logic & proxies
│ ├── hardware/ # Hardware specific drivers (NVIDIA, etc.)
│ └── utils/ # System helpers
│
+16 -1
View File
@@ -64,7 +64,12 @@ pub enum Commands {
/// Run the inhibitor in the background (daemon mode)
#[arg(long)]
daemon: bool,
/// Stop any active persistent inhibition managed by the daemon
#[arg(long)]
stop: bool,
},
/// Start the lapctl background daemon (requires root)
Daemon,
}
#[derive(Subcommand, Debug)]
@@ -72,7 +77,11 @@ pub enum GpuCommands {
/// Query the current graphics mode
Query,
/// Switch to integrated graphics
Integrated,
Integrated {
/// Switch without rebooting (experimental)
#[arg(long)]
no_reboot: bool,
},
/// Switch to hybrid graphics
Hybrid {
/// Setup PCI-Express Runtime D3 (RTD3) Power Management on Hybrid mode (0, 1, 2, 3)
@@ -81,6 +90,9 @@ pub enum GpuCommands {
/// Use nvidia-current instead of nvidia for kernel modules
#[arg(long)]
use_nvidia_current: bool,
/// Switch without rebooting (experimental)
#[arg(long)]
no_reboot: bool,
},
/// Switch to NVIDIA graphics
Nvidia {
@@ -99,6 +111,9 @@ pub enum GpuCommands {
/// Set up Nvidia mode for Wayland (skips Xorg configuration)
#[arg(long)]
wayland: bool,
/// Switch without rebooting (experimental)
#[arg(long)]
no_reboot: bool,
},
/// Revert changes made by lapctl
Reset,
+41
View File
@@ -2,8 +2,49 @@ use crate::cli::BatteryCommands;
use log::{debug, error, info};
use std::fs;
use std::path::Path;
use zbus::Connection;
use zbus::proxy;
#[proxy(
interface = "org.lapctl1",
default_service = "org.lapctl",
default_path = "/org/lapctl"
)]
trait Lapctl {
async fn set_battery_limit(&self, percent: u32) -> zbus::Result<()>;
}
fn try_call_daemon(command: &BatteryCommands) -> bool {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(_) => return false,
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
match command {
BatteryCommands::Limit { percent } => {
proxy.set_battery_limit(*percent as u32).await.is_ok()
}
_ => false,
}
})
}
pub fn execute(command: &BatteryCommands) {
if matches!(command, BatteryCommands::Limit { .. }) && try_call_daemon(command) {
println!("Request handled by lapctld daemon.");
return;
}
match command {
BatteryCommands::Limit { percent } => {
if !(&1..=&100).contains(&percent) {
+46
View File
@@ -2,6 +2,47 @@ use crate::cli::CoolingCommands;
use log::{debug, info};
use std::fs;
use std::path::Path;
use zbus::Connection;
use zbus::proxy;
#[proxy(
interface = "org.lapctl1",
default_service = "org.lapctl",
default_path = "/org/lapctl"
)]
trait Lapctl {
async fn set_cooling_profile(&self, profile: String) -> zbus::Result<()>;
}
fn try_call_daemon(command: &CoolingCommands) -> bool {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(_) => return false,
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
match command {
CoolingCommands::Performance => proxy
.set_cooling_profile("performance".to_string())
.await
.is_ok(),
CoolingCommands::Balanced => proxy
.set_cooling_profile("balanced".to_string())
.await
.is_ok(),
CoolingCommands::Quiet => proxy.set_cooling_profile("quiet".to_string()).await.is_ok(),
}
})
}
fn set_ideapad_fan_mode(mode: &str) -> bool {
let mut success = false;
@@ -38,6 +79,11 @@ fn set_asus_throttle_policy(mode: &str) -> bool {
}
pub fn execute(command: &CoolingCommands) {
if try_call_daemon(command) {
println!("Request handled by lapctld daemon.");
return;
}
let (ideapad_mode, asus_mode, desc) = match command {
CoolingCommands::Performance => ("1", "1", "Performance"),
CoolingCommands::Balanced => ("0", "0", "Balanced"),
+257 -40
View File
@@ -1,13 +1,101 @@
use crate::cli::GpuCommands;
use crate::hardware::gpu::{
get_amd_igpu_name, get_current_mode, get_igpu_vendor, get_nvidia_gpu_pci_bus,
get_amd_igpu_name, get_current_mode, get_igpu_vendor, get_nvidia_gpu_pci_addr,
get_nvidia_gpu_pci_bus, kill_gpu_processes, remove_gpu, rescan_pci, unbind_gpu,
};
use crate::utils::system::{
assert_root, create_file, get_active_graphical_sessions, get_display_manager,
is_service_active, manage_service, rebuild_initramfs, terminate_session,
};
use crate::utils::system::{assert_root, create_file, get_display_manager, rebuild_initramfs};
use log::{error, info};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use std::process::Command;
use zbus::Connection;
use zbus::proxy;
#[proxy(
interface = "org.lapctl1",
default_service = "org.lapctl",
default_path = "/org/lapctl"
)]
trait Lapctl {
async fn switch_gpu_integrated(&self, no_reboot: bool) -> zbus::Result<()>;
async fn switch_gpu_hybrid(
&self,
rtd3: i32,
use_nvidia_current: bool,
no_reboot: bool,
) -> zbus::Result<()>;
async fn switch_gpu_nvidia(
&self,
dm: String,
force_comp: bool,
coolbits: i32,
use_nvidia_current: bool,
wayland: bool,
no_reboot: bool,
) -> zbus::Result<()>;
}
fn try_call_daemon(cmd: &GpuCommands) -> bool {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(_) => return false,
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
match cmd {
GpuCommands::Integrated { no_reboot } => {
proxy.switch_gpu_integrated(*no_reboot).await.is_ok()
}
GpuCommands::Hybrid {
rtd3,
use_nvidia_current,
no_reboot,
} => {
let rtd3_val = rtd3.map(|v| v as i32).unwrap_or(-1);
proxy
.switch_gpu_hybrid(rtd3_val, *use_nvidia_current, *no_reboot)
.await
.is_ok()
}
GpuCommands::Nvidia {
dm,
force_comp,
coolbits,
use_nvidia_current,
wayland,
no_reboot,
} => {
let dm_val = dm.clone().unwrap_or_default();
let coolbits_val = coolbits.map(|v| v as i32).unwrap_or(-1);
proxy
.switch_gpu_nvidia(
dm_val,
*force_comp,
coolbits_val,
*use_nvidia_current,
*wayland,
*no_reboot,
)
.await
.is_ok()
}
_ => false,
}
})
}
const CACHE_FILE_PATH: &str = "/var/cache/lapctl/cache.json";
const BLACKLIST_PATH: &str = "/etc/modprobe.d/blacklist-nvidia.conf";
@@ -258,10 +346,12 @@ fn create_cache_obj(bus_id: String) -> Cache {
}
fn create_cache_file() {
if get_current_mode() != "hybrid" {
error!("--cache-create requires that the system be in the hybrid Optimus mode");
let pci_addr = get_nvidia_gpu_pci_addr();
if pci_addr.is_none() {
log::debug!("NVIDIA GPU not found, skipping cache creation");
return;
}
let bus_id = get_nvidia_gpu_pci_bus();
let cache = create_cache_obj(bus_id);
@@ -272,7 +362,7 @@ fn create_cache_file() {
if let Ok(json) = serde_json::to_string_pretty(&cache)
&& fs::write(CACHE_FILE_PATH, json).is_ok()
{
log::debug!("Created file {}", CACHE_FILE_PATH);
log::debug!("Created/Updated cache file {}", CACHE_FILE_PATH);
}
}
@@ -290,10 +380,84 @@ fn read_cache_file() -> Result<Cache, String> {
}
}
fn switch_integrated() {
fn prepare_no_reboot(dm_flag: Option<String>) -> Result<Option<String>, String> {
let dm_name = dm_flag.or_else(get_display_manager);
if let Some(ref name) = dm_name
&& is_service_active(name)
{
info!("Stopping display manager: {}", name);
manage_service(name, "stop")?;
return Ok(Some(name.clone()));
}
// If no active DM found, check for graphical sessions
let sessions = get_active_graphical_sessions();
if !sessions.is_empty() {
println!("No active display manager found, but graphical sessions are running.");
println!("Terminating graphical sessions for no-reboot GPU switching...");
for session_id in sessions {
if let Err(e) = terminate_session(&session_id) {
error!("Failed to terminate session {}: {}", session_id, e);
}
}
// Give some time for sessions to terminate
std::thread::sleep(std::time::Duration::from_secs(2));
}
Ok(None)
}
fn finalize_no_reboot(stopped_dm: Option<String>) {
if let Some(name) = stopped_dm {
info!("Starting display manager: {}", name);
let _ = manage_service(&name, "start");
}
}
fn switch_integrated(no_reboot: bool) {
assert_root();
println!("Switching to integrated mode");
if !Path::new(CACHE_FILE_PATH).exists() {
create_cache_file();
}
cleanup();
create_file(BLACKLIST_PATH, BLACKLIST_CONTENT, false);
create_file(UDEV_INTEGRATED_PATH, UDEV_INTEGRATED, false);
let mut stopped_dm = None;
if no_reboot {
match prepare_no_reboot(None) {
Ok(dm) => stopped_dm = dm,
Err(e) => {
error!("{}", e);
return;
}
}
if let Err(e) = kill_gpu_processes() {
error!("Failed to kill GPU processes: {}", e);
}
// Try to unload modules
let modules = ["nvidia_uvm", "nvidia_modeset", "nvidia_drm", "nvidia"];
for module in modules {
let _ = Command::new("modprobe").args(["-r", module]).status();
}
if let Some(pci_addr) = get_nvidia_gpu_pci_addr() {
if let Err(e) = unbind_gpu(&pci_addr) {
error!("Failed to unbind GPU: {}", e);
}
if let Err(e) = remove_gpu(&pci_addr) {
error!("Failed to remove GPU: {}", e);
}
}
}
let is_debug = log::log_enabled!(log::Level::Debug);
let mut dis_cmd = Command::new("systemctl");
dis_cmd.args(["disable", "nvidia-persistenced.service"]);
@@ -306,21 +470,17 @@ fn switch_integrated() {
_ => error!("An error ocurred while disabling service"),
}
if get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() {
create_cache_file();
if no_reboot {
println!("Operation completed successfully");
finalize_no_reboot(stopped_dm);
} else {
rebuild_initramfs();
println!("Operation completed successfully");
println!("Please reboot your computer for changes to take effect!");
}
cleanup();
create_file(BLACKLIST_PATH, BLACKLIST_CONTENT, false);
create_file(UDEV_INTEGRATED_PATH, UDEV_INTEGRATED, false);
rebuild_initramfs();
println!("Operation completed successfully");
println!("Please reboot your computer for changes to take effect!");
}
fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool) {
fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool, no_reboot: bool) {
assert_root();
println!("Switching to hybrid mode");
println!(
@@ -328,12 +488,31 @@ fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool) {
rtd3.is_some()
);
if get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() {
if no_reboot && let Err(e) = rescan_pci() {
error!("Failed to rescan PCI bus: {}", e);
}
if !Path::new(CACHE_FILE_PATH).exists() {
create_cache_file();
}
cleanup();
let mut stopped_dm = None;
if no_reboot {
match prepare_no_reboot(None) {
Ok(dm) => stopped_dm = dm,
Err(e) => {
error!("{}", e);
return;
}
}
if let Err(e) = kill_gpu_processes() {
error!("Failed to kill GPU processes: {}", e);
}
}
// persistenced enable
let is_debug = log::log_enabled!(log::Level::Debug);
let mut enable_cmd = Command::new("systemctl");
@@ -370,9 +549,14 @@ fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool) {
create_file(MODESET_PATH, &modeset_content, false);
}
rebuild_initramfs();
println!("Operation completed successfully");
println!("Please reboot your computer for changes to take effect!");
if no_reboot {
println!("Operation completed successfully");
finalize_no_reboot(stopped_dm);
} else {
rebuild_initramfs();
println!("Operation completed successfully");
println!("Please reboot your computer for changes to take effect!");
}
}
fn switch_nvidia(
@@ -381,26 +565,18 @@ fn switch_nvidia(
coolbits: Option<u32>,
use_nvidia_current: bool,
wayland: bool,
no_reboot: bool,
) {
assert_root();
println!("Switching to nvidia mode");
println!("Enable ForceCompositionPipeline: {}", force_comp);
println!("Enable Coolbits: {}", coolbits.is_some());
// persistenced enable
let is_debug = log::log_enabled!(log::Level::Debug);
let mut enable_cmd = Command::new("systemctl");
enable_cmd.args(["enable", "nvidia-persistenced.service"]);
if !is_debug {
enable_cmd.stdout(std::process::Stdio::null());
enable_cmd.stderr(std::process::Stdio::null());
}
match enable_cmd.status() {
Ok(s) if s.success() => println!("Successfully enabled nvidia-persistenced.service"),
_ => error!("An error ocurred while enabling service"),
if no_reboot && let Err(e) = rescan_pci() {
error!("Failed to rescan PCI bus: {}", e);
}
if get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() {
if !Path::new(CACHE_FILE_PATH).exists() {
create_cache_file();
}
@@ -454,7 +630,7 @@ fn switch_nvidia(
create_file(EXTRA_XORG_PATH, &extra_xorg, false);
}
let display_manager = dm.or_else(get_display_manager);
let display_manager = dm.clone().or_else(get_display_manager);
if let Some(cdm) = display_manager {
if cdm == "sddm" {
@@ -480,9 +656,42 @@ fn switch_nvidia(
}
}
rebuild_initramfs();
println!("Operation completed successfully");
println!("Please reboot your computer for changes to take effect!");
let mut stopped_dm = None;
if no_reboot {
match prepare_no_reboot(dm.clone()) {
Ok(dm_name) => stopped_dm = dm_name,
Err(e) => {
error!("{}", e);
return;
}
}
if let Err(e) = kill_gpu_processes() {
error!("Failed to kill GPU processes: {}", e);
}
}
// persistenced enable
let is_debug = log::log_enabled!(log::Level::Debug);
let mut enable_cmd = Command::new("systemctl");
enable_cmd.args(["enable", "nvidia-persistenced.service"]);
if !is_debug {
enable_cmd.stdout(std::process::Stdio::null());
enable_cmd.stderr(std::process::Stdio::null());
}
match enable_cmd.status() {
Ok(s) if s.success() => println!("Successfully enabled nvidia-persistenced.service"),
_ => error!("An error ocurred while enabling service"),
}
if no_reboot {
println!("Operation completed successfully");
finalize_no_reboot(stopped_dm);
} else {
rebuild_initramfs();
println!("Operation completed successfully");
println!("Please reboot your computer for changes to take effect!");
}
}
fn run_on_dgpu(command: &[String]) {
@@ -523,6 +732,11 @@ fn run_on_dgpu(command: &[String]) {
}
pub fn execute(cmd: &GpuCommands) {
if try_call_daemon(cmd) {
println!("Request handled by lapctld daemon.");
return;
}
match cmd {
GpuCommands::Query => {
let mode = get_current_mode();
@@ -570,23 +784,26 @@ pub fn execute(cmd: &GpuCommands) {
create_file(SDDM_XSETUP_PATH, SDDM_XSETUP_CONTENT, true);
println!("Operation completed successfully");
}
GpuCommands::Integrated => switch_integrated(),
GpuCommands::Integrated { no_reboot } => switch_integrated(*no_reboot),
GpuCommands::Hybrid {
rtd3,
use_nvidia_current,
} => switch_hybrid(*rtd3, *use_nvidia_current),
no_reboot,
} => switch_hybrid(*rtd3, *use_nvidia_current, *no_reboot),
GpuCommands::Nvidia {
dm,
force_comp,
coolbits,
use_nvidia_current,
wayland,
no_reboot,
} => switch_nvidia(
dm.clone(),
*force_comp,
*coolbits,
*use_nvidia_current,
*wayland,
*no_reboot,
),
GpuCommands::Run { command } => run_on_dgpu(command),
}
+57 -1
View File
@@ -1,7 +1,63 @@
use log::{error, info};
use std::process::Command;
use zbus::Connection;
use zbus::proxy;
#[proxy(
interface = "org.lapctl1",
default_service = "org.lapctl",
default_path = "/org/lapctl"
)]
trait Lapctl {
async fn set_system_inhibition(
&self,
active: bool,
why: String,
who: String,
) -> zbus::Result<()>;
}
fn try_call_daemon_inhibit(active: bool, why: String, who: String) -> bool {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(_) => return false,
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
proxy.set_system_inhibition(active, why, who).await.is_ok()
})
}
pub fn execute(command_args: &[String], why: &str, who: &str, daemon: bool, stop: bool) {
if stop {
if try_call_daemon_inhibit(false, why.to_string(), who.to_string()) {
println!("Persistent system inhibition deactivated via lapctld.");
} else {
error!("Failed to deactivate inhibition via daemon. Is lapctld running?");
}
return;
}
if daemon && command_args.is_empty() {
if try_call_daemon_inhibit(true, why.to_string(), who.to_string()) {
println!("Persistent system inhibition activated via lapctld.");
println!("The system will stay awake until you run: lapctl inhibit --stop");
return;
} else {
error!("Failed to activate inhibition via daemon. Is lapctld running?");
// Fallback to local systemd-inhibit if daemon call fails
}
}
pub fn execute(command_args: &[String], why: &str, who: &str, daemon: bool) {
let args = build_args(command_args, why, who);
if command_args.is_empty() {
+94 -50
View File
@@ -2,74 +2,118 @@ use crate::utils::system::{assert_root, create_file};
use log::info;
use std::process::Command;
const LAPCTL_UDEV_RULES: &str = r#"# /etc/udev/rules.d/99-lapctl.rules
# Automatically generated by lapctl
# This file grants read/write permissions to all local users
# for the SysFS endpoints required by lapctl's power, cooling, and battery commands.
// No longer needed: all privileged operations are now handled by the lapctld daemon via D-Bus.
const LAPCTL_UDEV_RULES: &str = "# lapctl udev rules (placeholder)\n";
# ACPI Platform Profiles & CPU Scaling
ACTION=="add", SUBSYSTEM=="firmware", RUN+="/bin/sh -c 'chmod a+w /sys/firmware/acpi/platform_profile 2>/dev/null'"
ACTION=="add", SUBSYSTEM=="cpu", RUN+="/bin/sh -c 'chmod a+w /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null'"
ACTION=="add", SUBSYSTEM=="cpu", RUN+="/bin/sh -c 'chmod a+w /sys/devices/system/cpu/cpu*/cpufreq/energy_performance_preference 2>/dev/null'"
const POLKIT_POLICY: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<vendor>lapctl</vendor>
<vendor_url>https://github.com/S4NKALP/lapctl</vendor_url>
<action id="org.s4nkalp.lapctl.execute">
<description>Elevated execution for lapctl</description>
<message>Authentication is required to perform privileged lapctl operations</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>yes</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/lapctl</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_gui">false</annotate>
</action>
</policyconfig>
"#;
# Battery Charging Limits
ACTION=="add", SUBSYSTEM=="power_supply", RUN+="/bin/sh -c 'chmod a+w /sys/class/power_supply/BAT*/charge_control_end_threshold 2>/dev/null'"
const POLKIT_RULES: &str = r#"/* /etc/polkit-1/rules.d/10-lapctl.rules
* Automatically generated by lapctl
* This rule allows users in the 'wheel' or 'sudo' groups to
* perform privileged lapctl operations via pkexec without a password.
*/
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.policykit.exec" &&
action.lookup("program").indexOf("lapctl") !== -1 &&
(subject.isInGroup("wheel") || subject.isInGroup("sudo"))) {
return polkit.Result.YES;
}
});
"#;
# Lenovo Ideapad Thermal and Conservation Mode
ACTION=="add", SUBSYSTEM=="platform", RUN+="/bin/sh -c 'chmod a+w /sys/bus/platform/drivers/ideapad_acpi/VPC*/fan_mode 2>/dev/null'"
ACTION=="add", SUBSYSTEM=="platform", RUN+="/bin/sh -c 'chmod a+w /sys/bus/platform/drivers/ideapad_acpi/VPC*/conservation_mode 2>/dev/null'"
const DBUS_POLICY: &str = r#"<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.lapctl"/>
<allow send_destination="org.lapctl"/>
<allow receive_sender="org.lapctl"/>
</policy>
<policy group="wheel">
<allow send_destination="org.lapctl"/>
<allow receive_sender="org.lapctl"/>
</policy>
<policy group="sudo">
<allow send_destination="org.lapctl"/>
<allow receive_sender="org.lapctl"/>
</policy>
</busconfig>
"#;
# ASUS WMI Thermal Policy
ACTION=="add", SUBSYSTEM=="platform", RUN+="/bin/sh -c 'chmod a+w /sys/devices/platform/asus-nb-wmi/throttle_thermal_policy 2>/dev/null'"
const SYSTEMD_SERVICE: &str = r#"[Unit]
Description=lapctl hardware management daemon
After=dbus.service
# Intel RAPL TDP Control
ACTION=="add", SUBSYSTEM=="powercap", RUN+="/bin/sh -c 'chmod a+w /sys/class/powercap/intel-rapl:0/constraint_*_power_limit_uw 2>/dev/null'"
[Service]
Type=simple
ExecStart=/usr/bin/lapctl daemon
Restart=on-failure
# AMD HWMon Power Capping
ACTION=="add", SUBSYSTEM=="hwmon", RUN+="/bin/sh -c 'find /sys/class/hwmon -name \"power1_cap\" -exec chmod a+w {} + 2>/dev/null'"
# Input Device Inhibition (Touchpad)
ACTION=="add", SUBSYSTEM=="input", RUN+="/bin/sh -c 'find /sys/class/input -name \"inhibited\" -exec chmod a+w {} + 2>/dev/null'"
[Install]
WantedBy=multi-user.target
"#;
pub fn execute() {
assert_root();
println!("Installing lapctl udev rules for rootless hardware configuration...");
println!("Installing lapctl configuration for daemon communication...");
let dbus_policy_path = "/etc/dbus-1/system.d/org.lapctl.conf";
create_file(dbus_policy_path, DBUS_POLICY, false);
let service_path = "/etc/systemd/system/lapctld.service";
create_file(service_path, SYSTEMD_SERVICE, false);
// Also install the udev rules file (even if empty now) to avoid errors if it was previously there
let rules_path = "/etc/udev/rules.d/99-lapctl.rules";
create_file(rules_path, LAPCTL_UDEV_RULES, false);
println!("Successfully wrote rules to {}", rules_path);
println!("Loading rules and applying immediate session permissions...");
// Polkit fallbacks for rootless execution without the daemon
let polkit_policy_path = "/usr/share/polkit-1/actions/org.s4nkalp.lapctl.policy";
create_file(polkit_policy_path, POLKIT_POLICY, false);
let _ = Command::new("udevadm")
.args(["control", "--reload-rules"])
let polkit_rules_path = "/etc/polkit-1/rules.d/10-lapctl.rules";
create_file(polkit_rules_path, POLKIT_RULES, false);
println!("Successfully installed configuration files.");
println!("Activating background daemon (lapctld)...");
// Reload systemd to recognize the new service
let _ = Command::new("systemctl").arg("daemon-reload").status();
// Enable and start the daemon
let status = Command::new("systemctl")
.args(["enable", "--now", "lapctld"])
.status();
let _ = Command::new("udevadm").args(["trigger"]).status();
// The udevadm trigger often fails to catch already-initialized ACPI nodes without a reboot,
// so we manually apply the permissions for the current session right now.
let commands = vec![
"chmod a+w /sys/firmware/acpi/platform_profile 2>/dev/null",
"chmod a+w /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null",
"chmod a+w /sys/devices/system/cpu/cpu*/cpufreq/energy_performance_preference 2>/dev/null",
"chmod a+w /sys/class/power_supply/BAT*/charge_control_end_threshold 2>/dev/null",
"chmod a+w /sys/bus/platform/drivers/ideapad_acpi/VPC*/fan_mode 2>/dev/null",
"chmod a+w /sys/bus/platform/drivers/ideapad_acpi/VPC*/conservation_mode 2>/dev/null",
"chmod a+w /sys/devices/platform/asus-nb-wmi/throttle_thermal_policy 2>/dev/null",
"chmod a+w /sys/class/powercap/intel-rapl:0/constraint_*_power_limit_uw 2>/dev/null",
"find /sys/class/hwmon -name \"power1_cap\" -exec chmod a+w {} + 2>/dev/null",
"find /sys/class/input -name \"inhibited\" -exec chmod a+w {} + 2>/dev/null",
];
for cmd in commands {
let _ = Command::new("sh").arg("-c").arg(cmd).status();
if let Ok(s) = status {
if s.success() {
info!("lapctld daemon enabled and started.");
} else {
log::error!("Failed to enable/start lapctld daemon.");
}
}
info!("Udev reload and immediate ACL application complete.");
println!(
"Installation successful! You can now use lapctl power, cooling, and battery commands without sudo."
);
println!("Installation successful! The lapctld daemon is now running.");
println!("You can now use all lapctl commands without sudo.");
}
+51
View File
@@ -2,6 +2,52 @@ use crate::cli::PowerCommands;
use log::{error, info};
use std::fs;
use std::path::Path;
use zbus::Connection;
use zbus::proxy;
#[proxy(
interface = "org.lapctl1",
default_service = "org.lapctl",
default_path = "/org/lapctl"
)]
trait Lapctl {
async fn set_power_profile(&self, profile: String) -> zbus::Result<()>;
async fn set_tdp_limit(&self, watts: u32) -> zbus::Result<()>;
}
fn try_call_daemon(command: &PowerCommands) -> bool {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(_) => return false,
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
match command {
PowerCommands::Performance => proxy
.set_power_profile("performance".to_string())
.await
.is_ok(),
PowerCommands::Balanced => proxy
.set_power_profile("balanced".to_string())
.await
.is_ok(),
PowerCommands::BatterySave => proxy
.set_power_profile("battery-save".to_string())
.await
.is_ok(),
PowerCommands::LimitTdp { watts } => proxy.set_tdp_limit(*watts).await.is_ok(),
}
})
}
fn set_cpu_governor(governor: &str) {
let cpu_dir = Path::new("/sys/devices/system/cpu");
@@ -155,6 +201,11 @@ fn set_platform_profile(profile: &str) {
}
pub fn execute(command: &PowerCommands) {
if try_call_daemon(command) {
println!("Request handled by lapctld daemon.");
return;
}
match command {
PowerCommands::Performance => {
println!("Setting power profile to Performance");
+39
View File
@@ -2,8 +2,47 @@ use crate::cli::TouchpadCommands;
use log::{error, info};
use std::fs;
use std::path::Path;
use zbus::Connection;
use zbus::proxy;
#[proxy(
interface = "org.lapctl1",
default_service = "org.lapctl",
default_path = "/org/lapctl"
)]
trait Lapctl {
async fn set_touchpad_inhibition(&self, inhibited: bool) -> zbus::Result<()>;
}
fn try_call_daemon(command: &TouchpadCommands) -> bool {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(_) => return false,
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
match command {
TouchpadCommands::Enable => proxy.set_touchpad_inhibition(false).await.is_ok(),
TouchpadCommands::Disable => proxy.set_touchpad_inhibition(true).await.is_ok(),
}
})
}
pub fn execute(command: &TouchpadCommands) {
if try_call_daemon(command) {
println!("Request handled by lapctld daemon.");
return;
}
let sys_class_input = Path::new("/sys/class/input");
if !sys_class_input.exists() {
error!("Could not find /sys/class/input. Is this a Linux system?");
+164
View File
@@ -0,0 +1,164 @@
use crate::cli::GpuCommands;
use crate::commands::gpu;
use std::sync::Arc;
use tokio::sync::Mutex;
use zbus::interface;
use zbus::zvariant::OwnedFd;
pub struct LapctlInterface {
inhibit_fd: Arc<Mutex<Option<OwnedFd>>>,
}
impl Default for LapctlInterface {
fn default() -> Self {
Self {
inhibit_fd: Arc::new(Mutex::new(None)),
}
}
}
#[interface(name = "org.lapctl1")]
impl LapctlInterface {
pub async fn switch_gpu_integrated(&self, no_reboot: bool) -> zbus::fdo::Result<()> {
gpu::execute(&GpuCommands::Integrated { no_reboot });
Ok(())
}
pub async fn switch_gpu_hybrid(
&self,
rtd3: i32,
use_nvidia_current: bool,
no_reboot: bool,
) -> zbus::fdo::Result<()> {
let rtd3_opt = if rtd3 < 0 { None } else { Some(rtd3 as u8) };
gpu::execute(&GpuCommands::Hybrid {
rtd3: rtd3_opt,
use_nvidia_current,
no_reboot,
});
Ok(())
}
pub async fn switch_gpu_nvidia(
&self,
dm: String,
force_comp: bool,
coolbits: i32,
use_nvidia_current: bool,
wayland: bool,
no_reboot: bool,
) -> zbus::fdo::Result<()> {
let dm_opt = if dm.is_empty() { None } else { Some(dm) };
let coolbits_opt = if coolbits < 0 {
None
} else {
Some(coolbits as u32)
};
gpu::execute(&GpuCommands::Nvidia {
dm: dm_opt,
force_comp,
coolbits: coolbits_opt,
use_nvidia_current,
wayland,
no_reboot,
});
Ok(())
}
pub async fn set_battery_limit(&self, percent: u32) -> zbus::fdo::Result<()> {
crate::commands::battery::execute(&crate::cli::BatteryCommands::Limit {
percent: percent as u8,
});
Ok(())
}
pub async fn set_power_profile(&self, profile: String) -> zbus::fdo::Result<()> {
let cmd = match profile.as_str() {
"performance" => crate::cli::PowerCommands::Performance,
"balanced" => crate::cli::PowerCommands::Balanced,
"battery-save" => crate::cli::PowerCommands::BatterySave,
_ => {
return Err(zbus::fdo::Error::InvalidArgs(format!(
"Invalid power profile: {}",
profile
)));
}
};
crate::commands::power::execute(&cmd);
Ok(())
}
pub async fn set_tdp_limit(&self, watts: u32) -> zbus::fdo::Result<()> {
crate::commands::power::execute(&crate::cli::PowerCommands::LimitTdp { watts });
Ok(())
}
pub async fn set_cooling_profile(&self, profile: String) -> zbus::fdo::Result<()> {
let cmd = match profile.as_str() {
"performance" => crate::cli::CoolingCommands::Performance,
"balanced" => crate::cli::CoolingCommands::Balanced,
"quiet" => crate::cli::CoolingCommands::Quiet,
_ => {
return Err(zbus::fdo::Error::InvalidArgs(format!(
"Invalid cooling profile: {}",
profile
)));
}
};
crate::commands::cooling::execute(&cmd);
Ok(())
}
pub async fn set_touchpad_inhibition(&self, inhibited: bool) -> zbus::fdo::Result<()> {
let cmd = if inhibited {
crate::cli::TouchpadCommands::Disable
} else {
crate::cli::TouchpadCommands::Enable
};
crate::commands::touchpad::execute(&cmd);
Ok(())
}
pub async fn set_system_inhibition(
&self,
active: bool,
why: String,
who: String,
) -> zbus::fdo::Result<()> {
let mut fd_lock = self.inhibit_fd.lock().await;
if active {
if fd_lock.is_some() {
return Ok(()); // Already inhibiting
}
let conn = zbus::Connection::system().await.map_err(|e| {
zbus::fdo::Error::Failed(format!("Failed to connect to system bus: {}", e))
})?;
let msg = conn
.call_method(
Some("org.freedesktop.login1"),
"/org/freedesktop/login1",
Some("org.freedesktop.login1.Manager"),
"Inhibit",
&("sleep:idle", who, why, "block"),
)
.await
.map_err(|e| zbus::fdo::Error::Failed(format!("Failed to inhibit: {}", e)))?;
let fd: OwnedFd = msg
.body()
.deserialize()
.map_err(|e| zbus::fdo::Error::Failed(format!("Failed to parse FD: {}", e)))?;
*fd_lock = Some(fd);
log::info!("System inhibition activated via lapctld.");
} else {
*fd_lock = None;
log::info!("System inhibition deactivated via lapctld.");
}
Ok(())
}
}
+19
View File
@@ -0,0 +1,19 @@
use log::info;
use std::error::Error;
use zbus::connection::Builder;
mod dbus_iface;
pub async fn run() -> Result<(), Box<dyn Error>> {
info!("Starting lapctld daemon...");
let _connection = Builder::system()?
.name("org.lapctl")?
.serve_at("/org/lapctl", dbus_iface::LapctlInterface::default())?
.build()
.await?;
info!("Daemon is running on the system bus.");
loop {
tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
}
}
+65
View File
@@ -54,6 +54,71 @@ pub fn get_nvidia_gpu_pci_bus() -> String {
std::process::exit(1);
}
pub fn get_nvidia_gpu_pci_addr() -> Option<String> {
let output = Command::new("lspci")
.output()
.expect("Failed to execute lspci");
let lspci_output = String::from_utf8_lossy(&output.stdout);
for line in lspci_output.lines() {
if line.contains("NVIDIA")
&& (line.contains("VGA compatible controller") || line.contains("3D controller"))
{
let pci_addr = line.split_whitespace().next().unwrap();
if pci_addr.contains(':') {
if pci_addr.chars().filter(|&c| c == ':').count() == 1 {
return Some(format!("0000:{}", pci_addr));
}
return Some(pci_addr.to_string());
}
}
}
None
}
pub fn unbind_gpu(pci_addr: &str) -> Result<(), String> {
let driver_path = Path::new("/sys/bus/pci/devices")
.join(pci_addr)
.join("driver");
if driver_path.exists() {
let unbind_path = driver_path.join("unbind");
info!("Unbinding GPU at {} via {:?}", pci_addr, unbind_path);
std::fs::write(unbind_path, pci_addr).map_err(|e| e.to_string())?;
}
Ok(())
}
pub fn remove_gpu(pci_addr: &str) -> Result<(), String> {
let remove_path = Path::new("/sys/bus/pci/devices")
.join(pci_addr)
.join("remove");
if remove_path.exists() {
info!("Removing GPU at {} via {:?}", pci_addr, remove_path);
std::fs::write(remove_path, "1").map_err(|e| e.to_string())?;
}
Ok(())
}
pub fn rescan_pci() -> Result<(), String> {
let rescan_path = Path::new("/sys/bus/pci/rescan");
info!("Rescanning PCI bus via {:?}", rescan_path);
std::fs::write(rescan_path, "1").map_err(|e| e.to_string())?;
Ok(())
}
pub fn kill_gpu_processes() -> Result<(), String> {
info!("Killing processes using NVIDIA GPU...");
// Try using fuser first as it is more likely to be available and easy to use
let _ = Command::new("fuser").args(["-k", "/dev/nvidia*"]).output();
// Also kill specific nvidia services if they are running
let _ = Command::new("systemctl")
.args(["stop", "nvidia-persistenced.service"])
.output();
Ok(())
}
pub fn get_igpu_vendor() -> Option<String> {
let output = Command::new("lspci").output().ok()?;
let lspci_output = String::from_utf8_lossy(&output.stdout);
+1
View File
@@ -1,4 +1,5 @@
pub mod cli;
pub mod commands;
pub mod daemon;
pub mod hardware;
pub mod utils;
+11 -1
View File
@@ -35,6 +35,16 @@ fn main() {
why,
who,
daemon,
} => commands::inhibit::execute(command, why, who, *daemon),
stop,
} => commands::inhibit::execute(command, why, who, *daemon, *stop),
Commands::Daemon => {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
if let Err(e) = lapctl::daemon::run().await {
eprintln!("Daemon error: {}", e);
std::process::exit(1);
}
});
}
}
}
+102 -10
View File
@@ -9,8 +9,25 @@ pub fn assert_root() {
if let Ok(output) = output {
let is_root = String::from_utf8_lossy(&output.stdout).trim() == "0";
if !is_root {
error!("This operation requires root privileges");
std::process::exit(1);
// Try to re-exec with pkexec
if let Ok(exe) = std::env::current_exe() {
let args: Vec<String> = std::env::args().skip(1).collect();
info!("Not running as root. Attempting to elevate privileges via pkexec...");
let status = Command::new("pkexec").arg(exe).args(&args).status();
match status {
Ok(s) if s.success() => std::process::exit(0),
_ => {
error!("Privilege escalation failed or was cancelled.");
std::process::exit(1);
}
}
} else {
error!(
"This operation requires root privileges and could not determine executable path."
);
std::process::exit(1);
}
}
}
}
@@ -43,19 +60,74 @@ pub fn create_file(path: &str, content: &str, executable: bool) {
}
pub fn get_display_manager() -> Option<String> {
if let Ok(content) = fs::read_to_string("/etc/systemd/system/display-manager.service") {
let re = regex::Regex::new(r"ExecStart=(.+)\n").unwrap();
if let Some(cap) = re.captures(&content) {
let path = cap.get(1).unwrap().as_str();
let dm = Path::new(path).file_name()?.to_string_lossy().to_string();
info!("Found {} Display Manager", dm);
return Some(dm);
}
let output = Command::new("systemctl")
.args(["show", "-p", "FragmentPath", "display-manager.service"])
.output()
.ok()?;
let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(line) = stdout.lines().next()
&& let Some(path_str) = line.strip_prefix("FragmentPath=")
&& !path_str.is_empty()
&& path_str != "/dev/null"
{
let dm = Path::new(path_str)
.file_name()?
.to_string_lossy()
.to_string();
info!("Found {} Display Manager", dm);
return Some(dm);
}
warn!("Display Manager detection is not available");
None
}
pub fn is_service_active(name: &str) -> bool {
let output = Command::new("systemctl").args(["is-active", name]).output();
if let Ok(output) = output {
return String::from_utf8_lossy(&output.stdout).trim() == "active";
}
false
}
pub fn get_active_graphical_sessions() -> Vec<String> {
let mut sessions = Vec::new();
let output = Command::new("loginctl")
.args(["list-sessions", "--no-legend"])
.output();
if let Ok(output) = output {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if let Some(id) = parts.first() {
let show_output = Command::new("loginctl")
.args(["show-session", id, "-p", "Type", "-p", "State"])
.output();
if let Ok(show_output) = show_output {
let show_stdout = String::from_utf8_lossy(&show_output.stdout);
let mut is_graphical = false;
let mut is_active = false;
for show_line in show_stdout.lines() {
if let Some(t) = show_line.strip_prefix("Type=") {
if t == "x11" || t == "wayland" {
is_graphical = true;
}
} else if let Some(s) = show_line.strip_prefix("State=")
&& (s == "active" || s == "online")
{
is_active = true;
}
}
if is_graphical && is_active {
sessions.push(id.to_string());
}
}
}
}
}
sessions
}
pub fn rebuild_initramfs() {
let mut command: Vec<&str> = Vec::new();
@@ -112,3 +184,23 @@ pub fn rebuild_initramfs() {
}
}
}
pub fn manage_service(name: &str, action: &str) -> Result<(), String> {
info!("Performing {} on service {}", action, name);
let mut cmd = Command::new("systemctl");
cmd.args([action, name]);
match cmd.status() {
Ok(s) if s.success() => Ok(()),
_ => Err(format!("Failed to {} service {}", action, name)),
}
}
pub fn terminate_session(id: &str) -> Result<(), String> {
info!("Terminating session {}", id);
let mut cmd = Command::new("loginctl");
cmd.args(["terminate-session", id]);
match cmd.status() {
Ok(s) if s.success() => Ok(()),
_ => Err(format!("Failed to terminate session {}", id)),
}
}
+3 -1
View File
@@ -11,6 +11,7 @@ fn test_inhibit_cli_parsing() {
why,
who,
daemon,
stop: _,
} = cli.command
{
assert_eq!(command, vec!["sleep".to_string(), "10".to_string()]);
@@ -30,8 +31,9 @@ fn test_inhibit_daemon_parsing() {
if let Commands::Inhibit {
command,
why,
who,
who: _,
daemon,
stop: _,
} = cli.command
{
assert!(command.is_empty());