12 Commits
Author SHA1 Message Date
sankalp 01d2e148a0 refactor(src/daemon): replace execute calls with execute_local for dbus commands
Why: Adopt local execution path to streamline command handling and reduce RPC overhead.

New Features
-

Enhancements
- Replace `gpu::execute` with `gpu::execute_local` in integrated, hybrid, and nvidia command invocations.
- Replace `crate::commands::power`, `battery`, `cooling`, and `touchpad` executions with `execute_local`.
- Remove the unused `LAPCTL_DAEMON_INTERNAL` environment variable setting.

Bug Fixes
- None
2026-03-23 16:16:31 +05:45
sankalp 0dc00ec5c9 refactor(src/commands): Refactor battery and touchpad command execution
Refactored command flow to separate daemon invocation, added status handling, and improved error handling.

- New Features: Added `execute_local` helper and `BatteryCommands::Status` handling.
- Enhancements: Simplified daemon checks, removed redundant condition, improved readability and error messages.
- Bug Fixes: Fixed missed daemon return path for status command and ensured proper touchpad inhibition logic.
2026-03-23 16:16:25 +05:45
sankalp 319ca54300 bumped to 0.3.1 2026-03-23 15:40:19 +05:45
sankalp f12134a9c8 feat(src/daemon): set LAPCTL_DAEMON_INTERNAL env var internally
Set an internal environment variable when the daemon starts to allow components to detect internal mode and prepare for future runtime configuration.

- New Features
  - Introduce `LAPCTL_DAEMON_INTERNAL` set to "1" inside the daemon.
- Enhancements
  - Enable downstream components to detect internal daemon execution.
2026-03-23 15:39:08 +05:45
sankalp 86ac9b556c chore(root): remove PKGBUILD and update .gitignore
Removed legacy packaging script and cleaned .gitignore entries.

New Features
-

Enhancements
-

Bug Fixes
-
2026-03-23 15:39:04 +05:45
sankalp c564259a19 refactor(src/commands): add timeouthandling and internal daemon guard
Adds 2‑second timeouts around D‑Bus interactions and guards against internal daemon self‑calls; refactors command handling to use a consistent async timeout pattern and improves error reporting.

New Features
- Wrap D‑Bus calls with a 2‑second timeout to prevent hangs
- Early‑exit when $LAPCTL_DAEMON_INTERNAL is set to skip self‑calls- Apply consistent timeout usage across all commands

Enhancements
- Unified error handling using the `matches!` macro
- Reduced race conditions in connection establishment

Bug Fixes
- Prevent silent failures when a connection timeout occurs
2026-03-23 15:38:57 +05:45
sankalp 46cbcb5b4b update 2026-03-23 15:28:18 +05:45
sankalp 899d4c831f docs(root): Simplify Arch Linux installation instructions
Remove outdated Arch AUR install recommendation and clean up documentation wording

New Features
-

Enhancements
- Updated `yay -S` command to install `lapctl` instead of `lapctl-git`
- Removed redundant dependency and udev rules note
- Adjusted emphasis formatting in `install-rules` description

Bug Fixes
- none
2026-03-22 18:05:36 +05:45
sankalp 3590f693d3 Merge pull request #3 from S4NKALP/dev 2026-03-22 17:55:25 +05:45
sankalp e98346fad5 fix test 2026-03-22 17:53:36 +05:45
sankalp b0fdc0d58e fuck it forgot to commit all those changes 2026-03-22 15:05:45 +05:45
sankalp c20a5d3a06 docs(root): update readme 2026-03-20 09:51:03 +05:45
19 changed files with 2248 additions and 211 deletions

No files matched your search

-2
View File
@@ -24,5 +24,3 @@ target
# Added by cargo # Added by cargo
/target /target
./lapctl.py
lapctl.py
Generated
+1030 -1
View File
File diff suppressed because it is too large. Load diff
+4 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "lapctl" name = "lapctl"
version = "0.2.0" version = "0.3.1"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
@@ -12,3 +12,6 @@ env_logger = "0.11"
regex = "1.10" regex = "1.10"
wayland-client = "0.31" wayland-client = "0.31"
wayland-protocols-wlr = { version = "0.3", features = ["client"] } 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"
+31 -11
View File
@@ -12,9 +12,7 @@
### Why lapctl? ### 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`). 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**.
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.
--- ---
@@ -35,23 +33,38 @@ Built with performance and simplicity in mind, it talks directly to your system'
**lapctl** is built with Rust. Ensure you have the [Rust toolchain](https://rustup.rs/) installed. **lapctl** is built with Rust. Ensure you have the [Rust toolchain](https://rustup.rs/) installed.
#### Option 1: Using `just` (Recommended) #### Option 1: Arch Linux (AUR)
If you have [just](https://github.com/casey/just) installed:
```bash ```bash
just install yay -S lapctl
``` ```
#### Option 2: Using `cargo` #### Option 2: Using `just` (Recommended for other distros)
If you have [just](https://github.com/casey/just) installed:
```bash
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 ```bash
cargo install --path . cargo install --path .
sudo lapctl install-rules
``` ```
#### Requirements #### Requirements
- **systemd**: For sleep inhibitor (`systemd-inhibit`) - **systemd**: For sleep inhibitor (`systemd-inhibit`)
- **GPU Switching (Optional)**: `xrandr` and `nvidia-settings` are strictly required **ONLY** when using the `lapctl gpu` command on X11 (to route proprietary NVIDIA Optimus drivers). - **GPU Switching (Optional)**: `xrandr` and `nvidia-settings` are strictly required **ONLY** when using the `lapctl gpu` command on X11 (to route proprietary NVIDIA Optimus drivers).
- **Wayland Display**: Built entirely natively using `wayland-client` and `wayland-protocols-wlr` (no `wlr-randr` required!) - **Wayland Display**: Built entirely natively using `wayland-client` and `wayland-protocols-wlr` (no `wlr-randr` required!)
#### Limitations #### Limitations
- **GNOME / KDE Plasma (Wayland)**: The display refresh rate feature relies heavily on the `zwlr_output_manager_v1` protocol. This protocol is exclusive to wlroots-based compositors (like Sway and Hyprland). GNOME and KDE use their own disparate internal display protocols, meaning this feature will **not work** out-of-the-box on those Desktop Environments. - **GNOME / KDE Plasma (Wayland)**: The display refresh rate feature relies heavily on the `zwlr_output_manager_v1` protocol. This protocol is exclusive to wlroots-based compositors (like Sway and Hyprland). GNOME and KDE use their own disparate internal display protocols, meaning this feature will **not work** out-of-the-box on those Desktop Environments.
--- ---
@@ -59,6 +72,11 @@ cargo install --path .
### Quick Start Guide ### Quick Start Guide
```bash ```bash
# Install D-Bus policy & start daemon (Required for rootless control)
sudo lapctl install-rules
# From now on, NO sudo is required!
# Manage your GPU # Manage your GPU
lapctl gpu integrated # Max battery lapctl gpu integrated # Max battery
lapctl gpu hybrid # Best of both worlds lapctl gpu hybrid # Best of both worlds
@@ -86,9 +104,10 @@ lapctl touchpad enable
lapctl display rates lapctl display rates
lapctl display set-rate 144 lapctl display set-rate 144
# Keep it awake # Keep it awake (requires lapctld)
lapctl inhibit --daemon # Run in background lapctl inhibit --daemon # Stay awake persistently (via daemon)
lapctl inhibit -- why "Critical update" ./long-task.sh lapctl inhibit --stop # Allow sleep again
lapctl inhibit --why "Keeping awake" -- sleep 60 # One-shot (local)
# Check everything # Check everything
lapctl status lapctl status
@@ -106,7 +125,8 @@ lapctl
├── src/ ├── src/
│ ├── main.rs # The entry point │ ├── main.rs # The entry point
│ ├── cli.rs # Command definition & parsing │ ├── 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.) │ ├── hardware/ # Hardware specific drivers (NVIDIA, etc.)
│ └── utils/ # System helpers │ └── utils/ # System helpers
│ │
+16 -1
View File
@@ -64,7 +64,12 @@ pub enum Commands {
/// Run the inhibitor in the background (daemon mode) /// Run the inhibitor in the background (daemon mode)
#[arg(long)] #[arg(long)]
daemon: bool, 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)] #[derive(Subcommand, Debug)]
@@ -72,7 +77,11 @@ pub enum GpuCommands {
/// Query the current graphics mode /// Query the current graphics mode
Query, Query,
/// Switch to integrated graphics /// Switch to integrated graphics
Integrated, Integrated {
/// Switch without rebooting (experimental)
#[arg(long)]
no_reboot: bool,
},
/// Switch to hybrid graphics /// Switch to hybrid graphics
Hybrid { Hybrid {
/// Setup PCI-Express Runtime D3 (RTD3) Power Management on Hybrid mode (0, 1, 2, 3) /// 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 /// Use nvidia-current instead of nvidia for kernel modules
#[arg(long)] #[arg(long)]
use_nvidia_current: bool, use_nvidia_current: bool,
/// Switch without rebooting (experimental)
#[arg(long)]
no_reboot: bool,
}, },
/// Switch to NVIDIA graphics /// Switch to NVIDIA graphics
Nvidia { Nvidia {
@@ -99,6 +111,9 @@ pub enum GpuCommands {
/// Set up Nvidia mode for Wayland (skips Xorg configuration) /// Set up Nvidia mode for Wayland (skips Xorg configuration)
#[arg(long)] #[arg(long)]
wayland: bool, wayland: bool,
/// Switch without rebooting (experimental)
#[arg(long)]
no_reboot: bool,
}, },
/// Revert changes made by lapctl /// Revert changes made by lapctl
Reset, Reset,
+92 -53
View File
@@ -2,6 +2,52 @@ use crate::cli::BatteryCommands;
use log::{debug, error, info}; use log::{debug, error, info};
use std::fs; use std::fs;
use std::path::Path; 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 tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system())
.await
{
Ok(Ok(conn)) => conn,
_ => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
let res = match command {
BatteryCommands::Limit { percent } => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_battery_limit(*percent as u32),
)
.await
}
_ => return false,
};
matches!(res, Ok(Ok(_)))
})
}
pub fn execute(command: &BatteryCommands) { pub fn execute(command: &BatteryCommands) {
match command { match command {
@@ -10,9 +56,21 @@ 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); println!("Setting battery charge limit to {}%", percent);
}
BatteryCommands::Status => {}
}
if matches!(command, BatteryCommands::Limit { .. }) && try_call_daemon(command) {
return;
}
execute_local(command);
}
pub fn execute_local(command: &BatteryCommands) {
match command {
BatteryCommands::Limit { percent } => {
let mut success_count = 0; let mut success_count = 0;
let sys_class_power = Path::new("/sys/class/power_supply"); let sys_class_power = Path::new("/sys/class/power_supply");
@@ -35,47 +93,34 @@ pub fn execute(command: &BatteryCommands) {
} }
} else { } else {
// Ideapad fallback // Ideapad fallback
let mut ideapad_found = false; let conservation_path = Path::new(
if let Ok(ideapad_entries) = "/sys/bus/platform/devices/ideapad_acpi/conservation_mode",
fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi") );
{ if conservation_path.exists() {
for ideapad_entry in ideapad_entries.flatten() { let val = if *percent < 100 { "1" } else { "0" };
let ideapad_path = ideapad_entry.path(); match fs::write(conservation_path, val) {
let conservation_path = ideapad_path.join("conservation_mode"); Ok(_) => {
if conservation_path.exists() { if *percent < 100 {
ideapad_found = true; println!(
let val = if *percent < 100 { "1" } else { "0" }; "WARNING: Your laptop (Lenovo Ideapad) does NOT support custom charge limits (like {}%).",
match fs::write(&conservation_path, val) { percent
Ok(_) => { );
if *percent < 100 { println!(
println!( "Instead, lapctl has enabled Lenovo's built-in 'Conservation Mode' via the Ideapad ACPI."
"WARNING: Your laptop (Lenovo Ideapad) does NOT support custom charge limits (like {}%).", );
percent println!(
); "This mode is hard-coded into your laptop's firmware to stop charging at ~60%."
println!( );
"Instead, lapctl has enabled Lenovo's built-in 'Conservation Mode' via the Ideapad ACPI." } else {
); println!(
println!( "Disabled Conservation Mode (charging to 100%)."
"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
),
} }
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);
}
} }
} }
} }
@@ -90,7 +135,6 @@ pub fn execute(command: &BatteryCommands) {
} }
} }
BatteryCommands::Status => { BatteryCommands::Status => {
// Re-use logic from `lapctl status` to show battery-specific details:
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) {
for entry in entries.flatten() { for entry in entries.flatten() {
@@ -113,21 +157,16 @@ pub fn execute(command: &BatteryCommands) {
println!(" Charge Limit: {}%", limit.trim()); println!(" Charge Limit: {}%", limit.trim());
} else { } else {
// Ideapad fallback display // Ideapad fallback display
if let Ok(ideapad_entries) = let conservation_path = Path::new(
fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi") "/sys/bus/platform/devices/ideapad_acpi/conservation_mode",
);
if conservation_path.exists()
&& let Ok(mode) = fs::read_to_string(conservation_path)
{ {
for ideapad_entry in ideapad_entries.flatten() { if mode.trim() == "1" {
let conservation_path = println!(" Charge Limit: Conservation Mode (~60%)");
ideapad_entry.path().join("conservation_mode"); } else {
if conservation_path.exists() println!(" Charge Limit: 100%");
&& let Ok(mode) = fs::read_to_string(&conservation_path)
{
if mode.trim() == "1" {
println!(" Charge Limit: Conservation Mode (~60%)");
} else {
println!(" Charge Limit: 100%");
}
}
} }
} }
} }
+74
View File
@@ -2,6 +2,65 @@ use crate::cli::CoolingCommands;
use log::{debug, info}; use log::{debug, info};
use std::fs; use std::fs;
use std::path::Path; 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 tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system())
.await
{
Ok(Ok(conn)) => conn,
_ => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
let res = match command {
CoolingCommands::Performance => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_cooling_profile("performance".to_string()),
)
.await
}
CoolingCommands::Balanced => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_cooling_profile("balanced".to_string()),
)
.await
}
CoolingCommands::Quiet => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_cooling_profile("quiet".to_string()),
)
.await
}
};
matches!(res, Ok(Ok(_)))
})
}
fn set_ideapad_fan_mode(mode: &str) -> bool { fn set_ideapad_fan_mode(mode: &str) -> bool {
let mut success = false; let mut success = false;
@@ -38,6 +97,21 @@ fn set_asus_throttle_policy(mode: &str) -> bool {
} }
pub fn execute(command: &CoolingCommands) { pub fn execute(command: &CoolingCommands) {
let desc = match command {
CoolingCommands::Performance => "Performance",
CoolingCommands::Balanced => "Balanced",
CoolingCommands::Quiet => "Quiet",
};
println!("Setting thermal/cooling profile to {}", desc);
if try_call_daemon(command) {
return;
}
execute_local(command);
}
pub fn execute_local(command: &CoolingCommands) {
let (ideapad_mode, asus_mode, desc) = match command { let (ideapad_mode, asus_mode, desc) = match command {
CoolingCommands::Performance => ("1", "1", "Performance"), CoolingCommands::Performance => ("1", "1", "Performance"),
CoolingCommands::Balanced => ("0", "0", "Balanced"), CoolingCommands::Balanced => ("0", "0", "Balanced"),
+279 -40
View File
@@ -1,13 +1,113 @@
use crate::cli::GpuCommands; use crate::cli::GpuCommands;
use crate::hardware::gpu::{ 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 log::{error, info};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use std::process::Command; 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 tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system())
.await
{
Ok(Ok(conn)) => conn,
_ => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
let res = match cmd {
GpuCommands::Integrated { no_reboot } => {
tokio::time::timeout(
std::time::Duration::from_secs(5),
proxy.switch_gpu_integrated(*no_reboot),
)
.await
}
GpuCommands::Hybrid {
rtd3,
use_nvidia_current,
no_reboot,
} => {
let rtd3_val = rtd3.map(|v| v as i32).unwrap_or(-1);
tokio::time::timeout(
std::time::Duration::from_secs(5),
proxy.switch_gpu_hybrid(rtd3_val, *use_nvidia_current, *no_reboot),
)
.await
}
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);
tokio::time::timeout(
std::time::Duration::from_secs(5),
proxy.switch_gpu_nvidia(
dm_val,
*force_comp,
coolbits_val,
*use_nvidia_current,
*wayland,
*no_reboot,
),
)
.await
}
_ => return false,
};
matches!(res, Ok(Ok(_)))
})
}
const CACHE_FILE_PATH: &str = "/var/cache/lapctl/cache.json"; const CACHE_FILE_PATH: &str = "/var/cache/lapctl/cache.json";
const BLACKLIST_PATH: &str = "/etc/modprobe.d/blacklist-nvidia.conf"; const BLACKLIST_PATH: &str = "/etc/modprobe.d/blacklist-nvidia.conf";
@@ -258,10 +358,12 @@ fn create_cache_obj(bus_id: String) -> Cache {
} }
fn create_cache_file() { fn create_cache_file() {
if get_current_mode() != "hybrid" { let pci_addr = get_nvidia_gpu_pci_addr();
error!("--cache-create requires that the system be in the hybrid Optimus mode"); if pci_addr.is_none() {
log::debug!("NVIDIA GPU not found, skipping cache creation");
return; return;
} }
let bus_id = get_nvidia_gpu_pci_bus(); let bus_id = get_nvidia_gpu_pci_bus();
let cache = create_cache_obj(bus_id); let cache = create_cache_obj(bus_id);
@@ -272,7 +374,7 @@ fn create_cache_file() {
if let Ok(json) = serde_json::to_string_pretty(&cache) if let Ok(json) = serde_json::to_string_pretty(&cache)
&& fs::write(CACHE_FILE_PATH, json).is_ok() && 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 +392,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(); assert_root();
println!("Switching to integrated mode"); 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 is_debug = log::log_enabled!(log::Level::Debug);
let mut dis_cmd = Command::new("systemctl"); let mut dis_cmd = Command::new("systemctl");
dis_cmd.args(["disable", "nvidia-persistenced.service"]); dis_cmd.args(["disable", "nvidia-persistenced.service"]);
@@ -306,21 +482,17 @@ fn switch_integrated() {
_ => error!("An error ocurred while disabling service"), _ => error!("An error ocurred while disabling service"),
} }
if get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() { if no_reboot {
create_cache_file(); 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(); assert_root();
println!("Switching to hybrid mode"); println!("Switching to hybrid mode");
println!( println!(
@@ -328,12 +500,31 @@ fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool) {
rtd3.is_some() 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(); create_cache_file();
} }
cleanup(); 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 // persistenced enable
let is_debug = log::log_enabled!(log::Level::Debug); let is_debug = log::log_enabled!(log::Level::Debug);
let mut enable_cmd = Command::new("systemctl"); let mut enable_cmd = Command::new("systemctl");
@@ -370,9 +561,14 @@ fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool) {
create_file(MODESET_PATH, &modeset_content, false); create_file(MODESET_PATH, &modeset_content, false);
} }
rebuild_initramfs(); if no_reboot {
println!("Operation completed successfully"); println!("Operation completed successfully");
println!("Please reboot your computer for changes to take effect!"); 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( fn switch_nvidia(
@@ -381,26 +577,18 @@ fn switch_nvidia(
coolbits: Option<u32>, coolbits: Option<u32>,
use_nvidia_current: bool, use_nvidia_current: bool,
wayland: bool, wayland: bool,
no_reboot: bool,
) { ) {
assert_root(); assert_root();
println!("Switching to nvidia mode"); println!("Switching to nvidia mode");
println!("Enable ForceCompositionPipeline: {}", force_comp); println!("Enable ForceCompositionPipeline: {}", force_comp);
println!("Enable Coolbits: {}", coolbits.is_some()); println!("Enable Coolbits: {}", coolbits.is_some());
// persistenced enable if no_reboot && let Err(e) = rescan_pci() {
let is_debug = log::log_enabled!(log::Level::Debug); error!("Failed to rescan PCI bus: {}", e);
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 get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() { if !Path::new(CACHE_FILE_PATH).exists() {
create_cache_file(); create_cache_file();
} }
@@ -454,7 +642,7 @@ fn switch_nvidia(
create_file(EXTRA_XORG_PATH, &extra_xorg, false); 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 let Some(cdm) = display_manager {
if cdm == "sddm" { if cdm == "sddm" {
@@ -480,9 +668,42 @@ fn switch_nvidia(
} }
} }
rebuild_initramfs(); let mut stopped_dm = None;
println!("Operation completed successfully"); if no_reboot {
println!("Please reboot your computer for changes to take effect!"); 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]) { fn run_on_dgpu(command: &[String]) {
@@ -523,6 +744,21 @@ fn run_on_dgpu(command: &[String]) {
} }
pub fn execute(cmd: &GpuCommands) { pub fn execute(cmd: &GpuCommands) {
match cmd {
GpuCommands::Integrated { .. } => println!("Switching to Integrated GPU mode..."),
GpuCommands::Hybrid { .. } => println!("Switching to Hybrid GPU mode..."),
GpuCommands::Nvidia { .. } => println!("Switching to NVIDIA-only GPU mode..."),
_ => {}
}
if try_call_daemon(cmd) {
return;
}
execute_local(cmd);
}
pub fn execute_local(cmd: &GpuCommands) {
match cmd { match cmd {
GpuCommands::Query => { GpuCommands::Query => {
let mode = get_current_mode(); let mode = get_current_mode();
@@ -570,23 +806,26 @@ pub fn execute(cmd: &GpuCommands) {
create_file(SDDM_XSETUP_PATH, SDDM_XSETUP_CONTENT, true); create_file(SDDM_XSETUP_PATH, SDDM_XSETUP_CONTENT, true);
println!("Operation completed successfully"); println!("Operation completed successfully");
} }
GpuCommands::Integrated => switch_integrated(), GpuCommands::Integrated { no_reboot } => switch_integrated(*no_reboot),
GpuCommands::Hybrid { GpuCommands::Hybrid {
rtd3, rtd3,
use_nvidia_current, use_nvidia_current,
} => switch_hybrid(*rtd3, *use_nvidia_current), no_reboot,
} => switch_hybrid(*rtd3, *use_nvidia_current, *no_reboot),
GpuCommands::Nvidia { GpuCommands::Nvidia {
dm, dm,
force_comp, force_comp,
coolbits, coolbits,
use_nvidia_current, use_nvidia_current,
wayland, wayland,
no_reboot,
} => switch_nvidia( } => switch_nvidia(
dm.clone(), dm.clone(),
*force_comp, *force_comp,
*coolbits, *coolbits,
*use_nvidia_current, *use_nvidia_current,
*wayland, *wayland,
*no_reboot,
), ),
GpuCommands::Run { command } => run_on_dgpu(command), GpuCommands::Run { command } => run_on_dgpu(command),
} }
+71 -1
View File
@@ -1,7 +1,77 @@
use log::{error, info}; use log::{error, info};
use std::process::Command; 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 tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system())
.await
{
Ok(Ok(conn)) => conn,
_ => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
let res = tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_system_inhibition(active, why, who),
)
.await;
matches!(res, Ok(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() {
println!(
"Attempting to activate system inhibition via lapctld: '{}' by '{}'",
why, who
);
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); let args = build_args(command_args, why, who);
if command_args.is_empty() { if command_args.is_empty() {
+100 -50
View File
@@ -2,74 +2,124 @@ use crate::utils::system::{assert_root, create_file};
use log::info; use log::info;
use std::process::Command; use std::process::Command;
const LAPCTL_UDEV_RULES: &str = r#"# /etc/udev/rules.d/99-lapctl.rules // No longer needed: all privileged operations are now handled by the lapctld daemon via D-Bus.
# Automatically generated by lapctl const LAPCTL_UDEV_RULES: &str = "# lapctl udev rules (placeholder)\n";
# This file grants read/write permissions to all local users
# for the SysFS endpoints required by lapctl's power, cooling, and battery commands.
# ACPI Platform Profiles & CPU Scaling const POLKIT_POLICY: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
ACTION=="add", SUBSYSTEM=="firmware", RUN+="/bin/sh -c 'chmod a+w /sys/firmware/acpi/platform_profile 2>/dev/null'" <!DOCTYPE policyconfig PUBLIC
ACTION=="add", SUBSYSTEM=="cpu", RUN+="/bin/sh -c 'chmod a+w /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null'" "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
ACTION=="add", SUBSYSTEM=="cpu", RUN+="/bin/sh -c 'chmod a+w /sys/devices/system/cpu/cpu*/cpufreq/energy_performance_preference 2>/dev/null'" "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.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">{EXE_PATH}</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_gui">false</annotate>
</action>
</policyconfig>
"#;
# Battery Charging Limits const POLKIT_RULES: &str = r#"/* /etc/polkit-1/rules.d/10-lapctl.rules
ACTION=="add", SUBSYSTEM=="power_supply", RUN+="/bin/sh -c 'chmod a+w /sys/class/power_supply/BAT*/charge_control_end_threshold 2>/dev/null'" * 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 const DBUS_POLICY: &str = r#"<!DOCTYPE busconfig PUBLIC
ACTION=="add", SUBSYSTEM=="platform", RUN+="/bin/sh -c 'chmod a+w /sys/bus/platform/drivers/ideapad_acpi/VPC*/fan_mode 2>/dev/null'" "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
ACTION=="add", SUBSYSTEM=="platform", RUN+="/bin/sh -c 'chmod a+w /sys/bus/platform/drivers/ideapad_acpi/VPC*/conservation_mode 2>/dev/null'" "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 const SYSTEMD_SERVICE: &str = r#"[Unit]
ACTION=="add", SUBSYSTEM=="platform", RUN+="/bin/sh -c 'chmod a+w /sys/devices/platform/asus-nb-wmi/throttle_thermal_policy 2>/dev/null'" Description=lapctl hardware management daemon
After=dbus.service
# Intel RAPL TDP Control [Service]
ACTION=="add", SUBSYSTEM=="powercap", RUN+="/bin/sh -c 'chmod a+w /sys/class/powercap/intel-rapl:0/constraint_*_power_limit_uw 2>/dev/null'" Type=simple
ExecStart={EXE_PATH} daemon
Restart=on-failure
# AMD HWMon Power Capping [Install]
ACTION=="add", SUBSYSTEM=="hwmon", RUN+="/bin/sh -c 'find /sys/class/hwmon -name \"power1_cap\" -exec chmod a+w {} + 2>/dev/null'" WantedBy=multi-user.target
# 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'"
"#; "#;
pub fn execute() { pub fn execute() {
assert_root(); assert_root();
println!("Installing lapctl udev rules for rootless hardware configuration..."); let current_exe = std::env::current_exe().expect("Failed to get current executable path");
let exe_path = current_exe.to_str().expect("Invalid executable path");
println!("Found lapctl at: {}", exe_path);
println!("Installing lapctl configuration with dynamic paths...");
let dbus_policy_path = "/etc/dbus-1/system.d/org.lapctl.conf";
create_file(dbus_policy_path, DBUS_POLICY, false);
let systemd_content = SYSTEMD_SERVICE.replace("{EXE_PATH}", exe_path);
let service_path = "/etc/systemd/system/lapctld.service";
create_file(service_path, &systemd_content, 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"; let rules_path = "/etc/udev/rules.d/99-lapctl.rules";
create_file(rules_path, LAPCTL_UDEV_RULES, false); create_file(rules_path, LAPCTL_UDEV_RULES, false);
println!("Successfully wrote rules to {}", rules_path); // Polkit fallbacks for rootless execution without the daemon
println!("Loading rules and applying immediate session permissions..."); let polkit_content = POLKIT_POLICY.replace("{EXE_PATH}", exe_path);
let polkit_policy_path = "/usr/share/polkit-1/actions/org.lapctl.policy";
create_file(polkit_policy_path, &polkit_content, false);
let _ = Command::new("udevadm") let polkit_rules_path = "/etc/polkit-1/rules.d/10-lapctl.rules";
.args(["control", "--reload-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(); .status();
let _ = Command::new("udevadm").args(["trigger"]).status(); if let Ok(s) = status {
if s.success() {
// The udevadm trigger often fails to catch already-initialized ACPI nodes without a reboot, info!("lapctld daemon enabled and started.");
// so we manually apply the permissions for the current session right now. } else {
let commands = vec![ log::error!("Failed to enable/start lapctld daemon.");
"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();
} }
info!("Udev reload and immediate ACL application complete."); println!("Installation successful! The lapctld daemon is now running.");
println!( println!("You can now use all lapctl commands from any location.");
"Installation successful! You can now use lapctl power, cooling, and battery commands without sudo."
);
} }
+84 -2
View File
@@ -2,6 +2,73 @@ use crate::cli::PowerCommands;
use log::{error, info}; use log::{error, info};
use std::fs; use std::fs;
use std::path::Path; 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 tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system())
.await
{
Ok(Ok(conn)) => conn,
_ => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
let res = match command {
PowerCommands::Performance => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_power_profile("performance".to_string()),
)
.await
}
PowerCommands::Balanced => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_power_profile("balanced".to_string()),
)
.await
}
PowerCommands::BatterySave => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_power_profile("battery-save".to_string()),
)
.await
}
PowerCommands::LimitTdp { watts } => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_tdp_limit(*watts),
)
.await
}
};
matches!(res, Ok(Ok(_)))
})
}
fn set_cpu_governor(governor: &str) { fn set_cpu_governor(governor: &str) {
let cpu_dir = Path::new("/sys/devices/system/cpu"); let cpu_dir = Path::new("/sys/devices/system/cpu");
@@ -155,6 +222,23 @@ fn set_platform_profile(profile: &str) {
} }
pub fn execute(command: &PowerCommands) { pub fn execute(command: &PowerCommands) {
match command {
PowerCommands::Performance => println!("Setting power profile to Performance"),
PowerCommands::Balanced => println!("Setting power profile to Balanced"),
PowerCommands::BatterySave => println!("Setting power profile to Battery Saver"),
PowerCommands::LimitTdp { watts } => {
println!("Setting CPU Package TDP Limit to {} Watts...", watts)
}
}
if try_call_daemon(command) {
return;
}
execute_local(command);
}
pub fn execute_local(command: &PowerCommands) {
match command { match command {
PowerCommands::Performance => { PowerCommands::Performance => {
println!("Setting power profile to Performance"); println!("Setting power profile to Performance");
@@ -166,8 +250,6 @@ pub fn execute(command: &PowerCommands) {
PowerCommands::Balanced => { PowerCommands::Balanced => {
println!("Setting power profile to Balanced"); println!("Setting power profile to Balanced");
set_platform_profile("balanced"); set_platform_profile("balanced");
// Schedutil is the modern balanced default. Fallback isn't critical since cpufreq
// usually rejects invalid profiles, and standard Intel/AMD systems use EPP now anyway.
set_cpu_governor("schedutil"); set_cpu_governor("schedutil");
set_energy_performance_preference("balance_performance"); set_energy_performance_preference("balance_performance");
println!("Operation completed successfully."); println!("Operation completed successfully.");
+102 -37
View File
@@ -1,54 +1,119 @@
use crate::cli::TouchpadCommands; use crate::cli::TouchpadCommands;
use log::{error, info}; use log::{debug, error, info};
use std::fs; use std::fs;
use std::path::Path; 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 tokio::time::timeout(std::time::Duration::from_secs(2), Connection::system())
.await
{
Ok(Ok(conn)) => conn,
_ => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
let res = match command {
TouchpadCommands::Enable => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_touchpad_inhibition(false),
)
.await
}
TouchpadCommands::Disable => {
tokio::time::timeout(
std::time::Duration::from_secs(2),
proxy.set_touchpad_inhibition(true),
)
.await
}
};
matches!(res, Ok(Ok(_)))
})
}
pub fn execute(command: &TouchpadCommands) { pub fn execute(command: &TouchpadCommands) {
let sys_class_input = Path::new("/sys/class/input"); match command {
if !sys_class_input.exists() { TouchpadCommands::Enable => println!("Enabling touchpad..."),
TouchpadCommands::Disable => println!("Disabling touchpad..."),
}
if try_call_daemon(command) {
return;
}
execute_local(command);
}
pub fn execute_local(command: &TouchpadCommands) {
match command {
TouchpadCommands::Enable => set_touchpad_inhibition(false),
TouchpadCommands::Disable => set_touchpad_inhibition(true),
}
}
fn set_touchpad_inhibition(inhibited: bool) {
let dev_input = Path::new("/sys/class/input");
if !dev_input.exists() {
error!("Could not find /sys/class/input. Is this a Linux system?"); error!("Could not find /sys/class/input. Is this a Linux system?");
return; return;
} }
let mut found_touchpads = Vec::new(); if let Ok(entries) = fs::read_dir(dev_input) {
let mut found = false;
if let Ok(entries) = fs::read_dir(sys_class_input) {
for entry in entries.flatten() { for entry in entries.flatten() {
let path = entry.path(); let name = entry.file_name();
let name_path = path.join("name"); let name_str = name.to_string_lossy();
let inhibited_path = path.join("inhibited");
if name_path.exists() if name_str.starts_with("input") {
&& inhibited_path.exists() let name_file = entry.path().join("name");
&& let Ok(name) = fs::read_to_string(&name_path) if let Ok(content) = fs::read_to_string(name_file) {
&& name.to_lowercase().contains("touchpad") let content_lower = content.to_lowercase();
{ if content_lower.contains("touchpad") || content_lower.contains("trackpad") {
found_touchpads.push((name.trim().to_string(), inhibited_path)); found = true;
} let inhibited_file = entry.path().join("inhibited");
} if inhibited_file.exists() {
} let val = if inhibited { "1" } else { "0" };
match fs::write(&inhibited_file, val) {
if found_touchpads.is_empty() { Ok(_) => info!("Set inhibit={} for {}", inhibited, name_str),
error!("No touchpad devices found with 'inhibited' support in /sys/class/input."); Err(e) => {
return; debug!("Failed to write to {}: {}", inhibited_file.display(), e)
} }
}
match command { }
TouchpadCommands::Enable => { }
for (name, path) in found_touchpads {
match fs::write(&path, "0") {
Ok(_) => info!("Enabled touchpad: {}", name),
Err(e) => error!("Failed to enable touchpad {}: {}", name, e),
} }
} }
} }
TouchpadCommands::Disable => { if found {
for (name, path) in found_touchpads { println!("Operation completed successfully.");
match fs::write(&path, "1") { } else {
Ok(_) => info!("Disabled touchpad: {}", name), error!("No touchpad device found in /sys/class/input.");
Err(e) => error!("Failed to disable touchpad {}: {}", name, e),
}
}
} }
} else {
error!("Failed to read directory /sys/class/input.");
} }
} }
+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_local(&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_local(&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_local(&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_local(&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_local(&cmd);
Ok(())
}
pub async fn set_tdp_limit(&self, watts: u32) -> zbus::fdo::Result<()> {
crate::commands::power::execute_local(&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_local(&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_local(&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); 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> { pub fn get_igpu_vendor() -> Option<String> {
let output = Command::new("lspci").output().ok()?; let output = Command::new("lspci").output().ok()?;
let lspci_output = String::from_utf8_lossy(&output.stdout); let lspci_output = String::from_utf8_lossy(&output.stdout);
+1
View File
@@ -1,4 +1,5 @@
pub mod cli; pub mod cli;
pub mod commands; pub mod commands;
pub mod daemon;
pub mod hardware; pub mod hardware;
pub mod utils; pub mod utils;
+11 -1
View File
@@ -35,6 +35,16 @@ fn main() {
why, why,
who, who,
daemon, 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 { if let Ok(output) = output {
let is_root = String::from_utf8_lossy(&output.stdout).trim() == "0"; let is_root = String::from_utf8_lossy(&output.stdout).trim() == "0";
if !is_root { if !is_root {
error!("This operation requires root privileges"); // Try to re-exec with pkexec
std::process::exit(1); 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> { pub fn get_display_manager() -> Option<String> {
if let Ok(content) = fs::read_to_string("/etc/systemd/system/display-manager.service") { let output = Command::new("systemctl")
let re = regex::Regex::new(r"ExecStart=(.+)\n").unwrap(); .args(["show", "-p", "FragmentPath", "display-manager.service"])
if let Some(cap) = re.captures(&content) { .output()
let path = cap.get(1).unwrap().as_str(); .ok()?;
let dm = Path::new(path).file_name()?.to_string_lossy().to_string(); let stdout = String::from_utf8_lossy(&output.stdout);
info!("Found {} Display Manager", dm); if let Some(line) = stdout.lines().next()
return Some(dm); && 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"); warn!("Display Manager detection is not available");
None 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() { pub fn rebuild_initramfs() {
let mut command: Vec<&str> = Vec::new(); 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, why,
who, who,
daemon, daemon,
stop: _,
} = cli.command } = cli.command
{ {
assert_eq!(command, vec!["sleep".to_string(), "10".to_string()]); assert_eq!(command, vec!["sleep".to_string(), "10".to_string()]);
@@ -30,8 +31,9 @@ fn test_inhibit_daemon_parsing() {
if let Commands::Inhibit { if let Commands::Inhibit {
command, command,
why, why,
who, who: _,
daemon, daemon,
stop: _,
} = cli.command } = cli.command
{ {
assert!(command.is_empty()); assert!(command.is_empty());