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

New Features
- None

Enhancements
- Updated dependency version to 0.3.2Bug Fixes
- None
2026-03-23 17:28:36 +05:45
sankalp 4020f65e28 bumped to 0.3.2 2026-03-23 17:10:37 +05:45
sankalp d52b3992e3 feat(src/commands):add Ideapad conservation mode fallback and improve charge limit messaging 2026-03-23 17:08:36 +05:45
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
sankalp 2733215d9f Merge pull request #1 from S4NKALP/dev 2026-03-20 08:39:01 +05:45
sankalp 7fb21f5e3d chore(root): bump version to 0.2.0 and set edition 2024
New Features
- None

Enhancements
- Updated edition to 2024

Bug Fixes
- None
2026-03-19 21:03:40 +05:45
sankalp f333dabfe5 docs(root): fixformatting of GPU run command example
- New Features:
- Enhancements: Clarify formatting of `lapctl gpu run steam` example.
- Bug Fixes:
2026-03-19 20:59:48 +05:45
sankalp e840bb7ce1 feat(root): add GPU run steam command to launch Steam on dGPU
The command enables launching Steam directly on the dGPU while in hybrid mode, improving performance for gaming workloads.

New Features
- Introduces `lapctl gpu run steam` to run Steam on dGPU directly in hybrid mode.

Enhancements
- Updated README with documentation for the new GPU command.

Bug Fixes
- None.
2026-03-19 20:55:22 +05:45
sankalp 5e921d2833 feat(src/commands): add run_on_dgpu command to launch applications on discrete GPU
Running applications on the dGPU now requires proper environment variable setup and runtime mode checking. The implementation includes warnings when the system is not in hybrid mode and logs the launched command and its exit status.

- New Feature: Implemented `run_on_dgpu` function to execute commands with NVIDIA offload environment variables.
- Enhancement: Added runtime mode verification and warning for non‑hybrid configurations.
- Enhancement: Added informational logging for launched command and process exit status.
2026-03-19 20:55:18 +05:45
sankalp 707192903e feat(src): add Run subcommand tolaunch applications on discrete GPU
- New Features
  - Introduce `Run` command with trailing var argument support for executing commands on the discrete GPU (requires Hybrid Mode)
- Enhancements
  - Update CLI parsing to accept a vector of command strings
- Bug Fixes
  - None
2026-03-19 20:55:15 +05:45
sankalp f7a2848214 docs(root): clarify GPU Switching (Optional) requirements
- New Features: Add optional GPU switching requirement description for X11 (lapctl gpu) usage.
- Enhancements: Improve clarity of system dependency documentation.
- Bug Fixes: None.
2026-03-19 20:38:27 +05:45
sankalp 9dba194e7d feat(root): add native Wayland display refresh rate control
Add native Wayland refresh rate control using zwlr_output_manager_v1, remove reliance on xrandr, and document limitations for GNOME/KDE. This enables on‑the‑fly Hz changes without external tools.

- New Feature: Native Wayland refresh rate management
- Enhancement: Updated requirements and limitations sections
- Enhancement: Improved documentation for refresh rate control
2026-03-19 20:36:00 +05:45
sankalp a9ea518457 feat(src/commands): add get_active_display_info and status display output
Add function to retrieve and display active monitor information, and expose it via status command.

New Features:
- Implement get_active_display_info returning formatted display strings.
- Extend status command to list active displays using the new function.

Enhancements:
- Improve console output for display information.
- Update dependency usage for cleaner code.

Bug Fixes:
- None
2026-03-19 20:35:57 +05:45
sankalp c03251b0a8 feat(src/commands): add display command implementation for refresh rate control
New Features
- Create `src/commands/display.rs` providing `execute` handling for `Rates` and `SetRate` commands, querying refresh rates and applying configuration via Wayland output manager
- Implement mode enumeration, deduplication, and marker labeling (current/preferred) for displayed modes
- Enable fuzzy matching of target refresh‑rate when setting a new rateEnhancements
- Register the `display` module in `src/commands/mod.rs`
- Extend `AppState` with manager, heads, and modes maps plus configuration state tracking
- Add dedicated error handling for unsupported compositorsBug Fixes
- None
2026-03-19 20:26:51 +05:45
sankalp bb4e9248fd feat(src): adddisplay management commands
Implement display command to query and set refresh rates.

New Features
- Add DisplayCommands with Rates and SetRate subcommands
- Register display command in CLI dispatcher

Enhancements
- Extend command dispatch to handle Display subcommand

Bug Fixes
- None
2026-03-19 20:26:47 +05:45
sankalp 24060ebe0d feat(root): add wayland display refresh‑rate control and update dependencies Add support for querying and setting display refresh rates and extend the command‑line interface for refresh‑rate management.
- **New Features**
  - Introduce `lapctl display rates` to list available refresh rates.
  - Add `lapctl display set-rate <HZ>` to change the active display’s refresh rate.
  - Support runtime detection on Hyprland, Sway, KDE Plasma, GNOME, and X11 back‑ends.

- **Enhancements**
  - Include `wayland-client` and `wayland-protocols-wlr` crates with the `client` feature.
  - Update `Cargo.toml` to declare the new Wayland-related dependencies.
  - Expand README with usage examples for refresh‑rate management.

- **Bug Fixes**
  - No bug fixes introduced in this change.
2026-03-19 20:26:43 +05:45
sankalp d8c0cda3d7 feat(root): add touchpad enable/disable commands and update README
New Features
- Added `lapctl touchpad disable` and `lapctl touchpad enable` commands

Enhancements
- Added Touchpad section to README with usage examples

Bug Fixes
- (none)
2026-03-16 22:26:25 +05:45
sankalp 42b3fb47fd feat(commands): add touchpad inhibition handling and status display
New Features
- Implement touchpad enable/disable commands via /sys/class/input/inhibited.
- Add permission adjustments for touchpad inhibition files.
- Show touchpad status in `lapctl status`.

Enhancements
- Update install rules to manage touchpad inhibition permissions.
- Expose new `touchpad` command module.

Bug Fixes
- Resolve missing permission errors for touchpad devices.
- Handle missing /sys/class/input directory gracefully.
2026-03-16 22:26:22 +05:45
sankalp 1bb54facaf feat(src): add touchpadenable/disable commands
Implemented touchpad control to allow enabling and disabling the touchpad via lapctl.

- New Features
  - Add `touchpad` command with `enable` and `disable` subcommands.
- Enhancements
  - Extend CLI parsing and command routing to support touchpad operations.
- Bug Fixes
  - (none)
2026-03-16 22:26:17 +05:45
22 changed files with 2997 additions and 180 deletions

No files matched your search

+1 -3
View File
@@ -23,6 +23,4 @@ target
# Added by cargo
/target
./lapctl.py
lapctl.py
/target
Generated
+1192 -1
View File
File diff suppressed because it is too large. Load diff
+6 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "lapctl"
version = "0.1.0"
version = "0.3.2"
edition = "2024"
[dependencies]
@@ -10,3 +10,8 @@ serde_json = "1.0"
log = "0.4"
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"
+47 -12
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**.
---
@@ -24,6 +22,8 @@ Built with performance and simplicity in mind, it talks directly to your system'
- **Battery Health**: Modern batteries hate being at 100% all the time. Set custom charge limits (like 80%) to significantly extend your battery's lifespan.
- **Power Tuning**: Switch through performance profiles or set hard CPU power (TDP) limits in Watts to keep things cool or let them loose.
- **Intelligent Cooling**: Force your fans into Performance, Balanced, or Quiet modes (supporting ASUS and Lenovo laptops).
- **Display Refresh Rate**: Easily query available refresh rates and change your active display's Hz on-the-fly (100% native Rust Wayland implementation using `zwlr_output_manager_v1` for wlroots compositors like Sway and Hyprland).
- **Touchpad Toggle**: Quickly enable or disable your touchpad from the terminal when using an external mouse.
- **Sleep Inhibitor**: Running a long compile or a critical download? Use the inhibitor to stop your laptop from falling asleep mid task.
- **Instant Status**: Get a bird's eye view of your hardware state, battery health, and current limits with one simple command.
@@ -33,30 +33,55 @@ 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.
#### Option 1: Using `just` (Recommended)
If you have [just](https://github.com/casey/just) installed:
#### Option 1: Arch Linux (AUR)
```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
cargo install --path .
sudo lapctl install-rules
```
#### Requirements
- **systemd**: For sleep inhibitor (`systemd-inhibit`)
- **X11/NVIDIA Tools**: `xrandr`, `nvidia-settings` for GPU management
- **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!)
#### 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.
---
### Quick Start Guide
```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
lapctl gpu integrated # Max battery
lapctl gpu hybrid # Best of both worlds
lapctl gpu nvidia # High performance
lapctl gpu run steam # Run 'steam' on dGPU directly while in Hybrid mode
# Prolong battery life
lapctl battery limit 80
@@ -71,9 +96,18 @@ lapctl power limit-tdp 35 # Stay under 35W
lapctl cooling quiet
lapctl cooling performance
# Keep it awake
lapctl inhibit --daemon # Run in background
lapctl inhibit -- why "Critical update" ./long-task.sh
# Manage your touchpad
lapctl touchpad disable
lapctl touchpad enable
# Manage your display refresh rate
lapctl display rates
lapctl display set-rate 144
# 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
@@ -91,7 +125,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
│
+51 -1
View File
@@ -37,10 +37,20 @@ pub enum Commands {
#[command(subcommand)]
command: CoolingCommands,
},
/// Display management commands
Display {
#[command(subcommand)]
command: DisplayCommands,
},
/// Hardware status
Status,
/// Install udev rules for rootless operation
InstallRules,
/// Touchpad management
Touchpad {
#[command(subcommand)]
command: TouchpadCommands,
},
/// Inhibit system sleep/suspend
Inhibit {
/// The command to run while inhibiting (optional)
@@ -54,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)]
@@ -62,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)
@@ -71,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 {
@@ -89,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,
@@ -100,6 +125,12 @@ pub enum GpuCommands {
CacheDelete,
/// Show cache created by lapctl
CacheQuery,
/// Run an application on the discrete GPU (Requires Hybrid Mode)
Run {
/// The command and arguments to execute
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
command: Vec<String>,
},
}
#[derive(Subcommand, Debug)]
@@ -131,3 +162,22 @@ pub enum BatteryCommands {
/// Show current battery status
Status,
}
#[derive(Subcommand, Debug)]
pub enum TouchpadCommands {
/// Enable the touchpad
Enable,
/// Disable the touchpad
Disable,
}
#[derive(Subcommand, Debug)]
pub enum DisplayCommands {
/// Show available and active refresh rates
Rates,
/// Set the display refresh rate
SetRate {
/// Target refresh rate in Hz
rate: f32,
},
}
+127 -61
View File
@@ -2,6 +2,56 @@ 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 is_ideapad() -> bool {
Path::new("/sys/bus/platform/drivers/ideapad_acpi").exists()
}
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) {
match command {
@@ -11,9 +61,51 @@ pub fn execute(command: &BatteryCommands) {
std::process::exit(1);
}
println!("Setting battery charge limit to {}%", percent);
if is_ideapad() {
if *percent < 100 {
println!("Setting battery charge limit (Lenovo Conservation Mode ~60%)...");
} else {
println!("Disabling battery charge limit (charging to 100%)...");
}
} else {
println!("Setting battery charge limit to {}%...", percent);
}
}
BatteryCommands::Status => {}
}
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;
// Ideapad fallback - search for conservation_mode in ideapad_acpi driver directories
if let Ok(drivers) = fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi") {
for entry in drivers.flatten() {
let conservation_path = entry.path().join("conservation_mode");
if conservation_path.exists() {
let val = if *percent < 100 { "1" } else { "0" };
match fs::write(&conservation_path, val) {
Ok(_) => {
success_count += 1;
}
Err(e) => {
debug!("Failed to write to {}: {}", conservation_path.display(), e)
}
}
}
}
}
// Only try standard thresholds if we haven't already succeeded with Ideapad fallback
// or if we want to ensure all bases are covered. For now, let's do both but prioritize the message.
let sys_class_power = Path::new("/sys/class/power_supply");
if let Ok(entries) = fs::read_dir(sys_class_power) {
@@ -24,6 +116,10 @@ pub fn execute(command: &BatteryCommands) {
if name_str.starts_with("BAT") {
let threshold_path = entry.path().join("charge_control_end_threshold");
if threshold_path.exists() {
println!(
"Setting battery charge limit for {} to {}%",
name_str, percent
);
match fs::write(&threshold_path, percent.to_string()) {
Ok(_) => {
info!("Successfully set limit for {}", name_str);
@@ -33,49 +129,6 @@ pub fn execute(command: &BatteryCommands) {
debug!("Failed to write to {}: {}", threshold_path.display(), e)
}
}
} else {
// Ideapad fallback
let mut ideapad_found = false;
if let Ok(ideapad_entries) =
fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi")
{
for ideapad_entry in ideapad_entries.flatten() {
let ideapad_path = ideapad_entry.path();
let conservation_path = ideapad_path.join("conservation_mode");
if conservation_path.exists() {
ideapad_found = true;
let val = if *percent < 100 { "1" } else { "0" };
match fs::write(&conservation_path, val) {
Ok(_) => {
if *percent < 100 {
println!(
"WARNING: Your laptop (Lenovo Ideapad) does NOT support custom charge limits (like {}%).",
percent
);
println!(
"Instead, lapctl has enabled Lenovo's built-in 'Conservation Mode' via the Ideapad ACPI."
);
println!(
"This mode is hard-coded into your laptop's firmware to stop charging at ~60%."
);
} else {
println!(
"Disabled Conservation Mode (charging to 100%)."
);
}
success_count += 1;
}
Err(e) => debug!(
"Failed to write to conservation_mode: {}",
e
),
}
}
}
}
if !ideapad_found {
debug!("limit not supported for {} (missing thresholds)", name_str);
}
}
}
}
@@ -90,7 +143,6 @@ pub fn execute(command: &BatteryCommands) {
}
}
BatteryCommands::Status => {
// Re-use logic from `lapctl status` to show battery-specific details:
let sys_class_power = Path::new("/sys/class/power_supply");
if let Ok(entries) = fs::read_dir(sys_class_power) {
for entry in entries.flatten() {
@@ -107,25 +159,39 @@ pub fn execute(command: &BatteryCommands) {
println!(" Capacity: {}%", capacity.trim());
println!(" Status: {}", status.trim());
if let Ok(limit) =
fs::read_to_string(bat_path.join("charge_control_end_threshold"))
// Check for Ideapad conservation mode first using robust detection
let mut limit_shown = false;
if let Ok(drivers) = fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi")
{
println!(" Charge Limit: {}%", limit.trim());
} else {
// Ideapad fallback display
if let Ok(ideapad_entries) =
fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi")
for entry in drivers.flatten() {
let conservation_path = entry.path().join("conservation_mode");
if conservation_path.exists()
&& let Ok(mode) = fs::read_to_string(&conservation_path)
{
if mode.trim() == "1" {
println!(" Charge Limit: Conservation Mode (~60%)");
limit_shown = true;
} else if mode.trim() == "0" {
// On Ideapads 0 means 100% or off.
}
}
}
}
if !limit_shown {
if let Ok(limit) =
fs::read_to_string(bat_path.join("charge_control_end_threshold"))
{
for ideapad_entry in ideapad_entries.flatten() {
let conservation_path =
ideapad_entry.path().join("conservation_mode");
if conservation_path.exists()
&& let Ok(mode) = fs::read_to_string(&conservation_path)
{
if mode.trim() == "1" {
println!(" Charge Limit: Conservation Mode (~60%)");
} else {
println!(" Charge Limit: {}%", limit.trim());
} else {
// Re-check if it's an ideapad and we haven't shown a limit (likely 100%)
if let Ok(drivers) =
fs::read_dir("/sys/bus/platform/drivers/ideapad_acpi")
{
for entry in drivers.flatten() {
if entry.path().join("conservation_mode").exists() {
println!(" Charge Limit: 100%");
break;
}
}
}
+74
View File
@@ -2,6 +2,65 @@ 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 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 {
let mut success = false;
@@ -38,6 +97,21 @@ fn set_asus_throttle_policy(mode: &str) -> bool {
}
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 {
CoolingCommands::Performance => ("1", "1", "Performance"),
CoolingCommands::Balanced => ("0", "0", "Balanced"),
+402
View File
@@ -0,0 +1,402 @@
use crate::cli::DisplayCommands;
use log::error;
use std::collections::HashMap;
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, protocol::wl_registry};
use wayland_protocols_wlr::output_management::v1::client::{
zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1,
zwlr_output_configuration_v1::{self, ZwlrOutputConfigurationV1},
zwlr_output_head_v1::{self, ZwlrOutputHeadV1},
zwlr_output_manager_v1::{self, ZwlrOutputManagerV1},
zwlr_output_mode_v1::{self, ZwlrOutputModeV1},
};
pub fn execute(command: &DisplayCommands) {
match command {
DisplayCommands::Rates => {
show_refresh_rates();
}
DisplayCommands::SetRate { rate } => {
set_refresh_rate(*rate);
}
}
}
// State Structs
#[derive(Debug, Default, Clone)]
pub struct WlMode {
pub width: i32,
pub height: i32,
pub refresh: i32,
pub preferred: bool,
}
#[derive(Debug, Clone)]
pub struct WlHead {
pub name: String,
pub description: String,
pub make: String,
pub model: String,
pub enabled: bool,
pub current_mode: Option<ZwlrOutputModeV1>,
pub x: i32,
pub y: i32,
pub scale: f64,
pub transform: wayland_client::protocol::wl_output::Transform,
}
impl Default for WlHead {
fn default() -> Self {
Self {
name: String::new(),
description: String::new(),
make: String::new(),
model: String::new(),
enabled: false,
current_mode: None,
x: 0,
y: 0,
scale: 1.0,
transform: wayland_client::protocol::wl_output::Transform::Normal,
}
}
}
pub struct AppState {
pub manager: Option<ZwlrOutputManagerV1>,
pub heads: HashMap<ZwlrOutputHeadV1, WlHead>,
pub modes: HashMap<ZwlrOutputModeV1, (ZwlrOutputHeadV1, WlMode)>,
pub done: bool,
pub config_success: Option<bool>,
}
impl Dispatch<wl_registry::WlRegistry, ()> for AppState {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
let wl_registry::Event::Global {
name,
interface,
version,
} = event
else {
return;
};
if interface == "zwlr_output_manager_v1" {
let manager = registry.bind::<ZwlrOutputManagerV1, _, _>(name, version, qh, ());
state.manager = Some(manager);
}
}
}
impl Dispatch<ZwlrOutputManagerV1, ()> for AppState {
fn event(
state: &mut Self,
_: &ZwlrOutputManagerV1,
event: zwlr_output_manager_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
zwlr_output_manager_v1::Event::Head { head } => {
state.heads.insert(head, WlHead::default());
}
zwlr_output_manager_v1::Event::Done { .. } => {
state.done = true;
}
_ => {}
}
}
fn event_created_child(
opcode: u16,
qh: &QueueHandle<Self>,
) -> std::sync::Arc<dyn wayland_client::backend::ObjectData> {
match opcode {
0 => qh.make_data::<ZwlrOutputHeadV1, ()>(()),
_ => panic!(
"Missing event_created_child specialization for opcode {}",
opcode
),
}
}
}
impl Dispatch<ZwlrOutputHeadV1, ()> for AppState {
fn event(
state: &mut Self,
head: &ZwlrOutputHeadV1,
event: zwlr_output_head_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let Some(h) = state.heads.get_mut(head) {
match event {
zwlr_output_head_v1::Event::Name { name } => h.name = name,
zwlr_output_head_v1::Event::Description { description } => {
h.description = description
}
zwlr_output_head_v1::Event::Make { make } => h.make = make,
zwlr_output_head_v1::Event::Model { model } => h.model = model,
// Mode event gives us a new mode object!
zwlr_output_head_v1::Event::Mode { mode } => {
state.modes.insert(mode, (head.clone(), WlMode::default()));
}
zwlr_output_head_v1::Event::Enabled { enabled } => h.enabled = enabled != 0,
zwlr_output_head_v1::Event::CurrentMode { mode } => h.current_mode = Some(mode),
zwlr_output_head_v1::Event::Position { x, y } => {
h.x = x;
h.y = y;
}
zwlr_output_head_v1::Event::Scale { scale } => h.scale = scale,
zwlr_output_head_v1::Event::Transform {
transform: wayland_client::WEnum::Value(t),
} => {
h.transform = t;
}
_ => {}
}
}
}
fn event_created_child(
opcode: u16,
qh: &QueueHandle<Self>,
) -> std::sync::Arc<dyn wayland_client::backend::ObjectData> {
match opcode {
3 => qh.make_data::<ZwlrOutputModeV1, ()>(()),
_ => panic!(
"Missing event_created_child specialization for opcode {} on head",
opcode
),
}
}
}
impl Dispatch<ZwlrOutputModeV1, ()> for AppState {
fn event(
state: &mut Self,
mode: &ZwlrOutputModeV1,
event: zwlr_output_mode_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let Some((head_handle, mut wl_mode)) = state.modes.remove(mode) {
match event {
zwlr_output_mode_v1::Event::Size { width, height } => {
wl_mode.width = width;
wl_mode.height = height;
}
zwlr_output_mode_v1::Event::Refresh { refresh } => {
wl_mode.refresh = refresh;
}
zwlr_output_mode_v1::Event::Preferred => {
wl_mode.preferred = true;
}
_ => {}
}
state
.modes
.insert(mode.clone(), (head_handle.clone(), wl_mode));
}
}
}
impl Dispatch<ZwlrOutputConfigurationV1, ()> for AppState {
fn event(
state: &mut Self,
_: &ZwlrOutputConfigurationV1,
event: zwlr_output_configuration_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
zwlr_output_configuration_v1::Event::Succeeded => state.config_success = Some(true),
zwlr_output_configuration_v1::Event::Failed => state.config_success = Some(false),
zwlr_output_configuration_v1::Event::Cancelled => state.config_success = Some(false),
_ => {}
}
}
}
impl Dispatch<ZwlrOutputConfigurationHeadV1, ()> for AppState {
fn event(
_: &mut Self,
_: &ZwlrOutputConfigurationHeadV1,
_: wayland_protocols_wlr::output_management::v1::client::zwlr_output_configuration_head_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
fn fetch_state() -> Option<(Connection, AppState, wayland_client::EventQueue<AppState>)> {
let conn = Connection::connect_to_env().ok()?;
let mut event_queue = conn.new_event_queue();
let qh = event_queue.handle();
let display = conn.display();
let mut state = AppState {
manager: None,
heads: HashMap::new(),
modes: HashMap::new(),
done: false,
config_success: None,
};
display.get_registry(&qh, ());
event_queue.roundtrip(&mut state).ok()?;
if state.manager.is_none() {
error!("Your compositor does not support zwlr_output_manager_v1.");
return None;
}
// Two roundtrips to fetch heads then modes
event_queue.roundtrip(&mut state).ok()?;
event_queue.roundtrip(&mut state).ok()?;
Some((conn, state, event_queue))
}
pub fn get_active_display_info() -> Vec<String> {
let mut infos = Vec::new();
if let Some((_, state, _)) = fetch_state() {
for (_, head) in state.heads.iter() {
if !head.enabled {
continue;
}
let Some(current_mode_handle) = &head.current_mode else {
continue;
};
let Some((_, mode)) = state.modes.get(current_mode_handle) else {
continue;
};
let hz = (mode.refresh as f64) / 1000.0;
infos.push(format!(
"{} ({}): {}x{} px, {:.3} Hz",
head.name, head.make, mode.width, mode.height, hz
));
}
}
infos
}
fn show_refresh_rates() {
if let Some((_, state, _)) = fetch_state() {
for (head_handle, head) in state.heads.iter() {
println!("{} \"{}\"", head.name, head.description);
println!(" Make: {}", head.make);
println!(" Model: {}", head.model);
println!(" Enabled: {}", if head.enabled { "yes" } else { "no" });
println!(" Modes:");
// Gather modes for this head
let mut head_modes: Vec<(&ZwlrOutputModeV1, &WlMode)> = state
.modes
.iter()
.filter(|(_, (mh, _))| mh == head_handle)
.map(|(mode_handle, (_, mode))| (mode_handle, mode))
.collect();
// Sort by resolution descending, then refresh rate descending
head_modes.sort_by(|a, b| {
b.1.width
.cmp(&a.1.width)
.then(b.1.height.cmp(&a.1.height))
.then(b.1.refresh.cmp(&a.1.refresh))
});
// Deduplicate modes based on properties
let mut seen = std::collections::HashSet::new();
for (mode_handle, mode) in head_modes {
if mode.width != 0 && mode.height != 0 && mode.refresh != 0 {
let key = format!("{}x{}@{}", mode.width, mode.height, mode.refresh);
let is_current = head.current_mode.as_ref() == Some(mode_handle);
let is_preferred = mode.preferred;
// Always show current/preferred, otherwise deduplicate identical ones
if seen.insert(key) || is_current || is_preferred {
let hz = (mode.refresh as f64) / 1000.0;
let mut markers = Vec::new();
if is_preferred {
markers.push("preferred");
}
if is_current {
markers.push("current");
}
let marker_str = if markers.is_empty() {
String::new()
} else {
format!(" ({})", markers.join(", "))
};
println!(
" {}x{} px, {:.6} Hz{}",
mode.width, mode.height, hz, marker_str
);
}
}
}
}
} else {
error!("Native Wayland configuration failed. Are you on a wlroots compositor?");
}
}
fn set_refresh_rate(rate: f32) {
let Some((_conn, mut state, mut eq)) = fetch_state() else {
return;
};
let Some(manager) = state.manager.clone() else {
return;
};
let qh = eq.handle();
let config = manager.create_configuration(manager.version(), &qh, ());
if let Some((head_handle, head_info)) = state.heads.iter().find(|(_, h)| h.enabled) {
let target_mhz = (rate * 1000.0) as i32;
for (mode_handle, (mh, mode_info)) in state.modes.iter() {
if mh == head_handle {
// fuzzy match the mHz
if (mode_info.refresh - target_mhz).abs() < 5000 {
let head_config = config.enable_head(head_handle, &qh, ());
head_config.set_mode(mode_handle);
head_config.set_position(head_info.x, head_info.y);
head_config.set_scale(head_info.scale);
head_config.set_transform(head_info.transform);
config.apply();
state.config_success = None;
while state.config_success.is_none() {
if eq.blocking_dispatch(&mut state).is_err() {
break;
}
}
if state.config_success == Some(true) {
println!("[SUCCESS] Applied Wayland output configuration!");
} else {
error!("[FAILED] Compositor rejected the output configuration request.");
}
return;
}
}
}
}
}
+317 -40
View File
@@ -1,13 +1,113 @@
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 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 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() {
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 +374,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 +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();
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 +482,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 +500,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 +561,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 +577,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 +642,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,12 +668,97 @@ 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]) {
if command.is_empty() {
error!("No command provided to run.");
return;
}
if get_current_mode() != "hybrid" {
log::warn!(
"Running on dGPU is typically only effective in Hybrid mode. \
Your current mode is '{}'. The application may fail to start or \
not use the NVIDIA GPU.",
get_current_mode()
);
}
log::info!("Launching '{}' on the discrete GPU...", command.join(" "));
let mut child = Command::new(&command[0]);
if command.len() > 1 {
child.args(&command[1..]);
}
// Set PRIME offload variables natively
child.env("__NV_PRIME_RENDER_OFFLOAD", "1");
child.env("__GLX_VENDOR_LIBRARY_NAME", "nvidia");
child.env("__VK_LAYER_NV_optimus", "NVIDIA_only");
match child.status() {
Ok(status) => {
log::info!("Application exited with: {}", status);
}
Err(e) => {
error!("Failed to launch application: {}", e);
}
}
}
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 {
GpuCommands::Query => {
let mode = get_current_mode();
@@ -533,23 +806,27 @@ 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),
}
}
+71 -1
View File
@@ -1,7 +1,77 @@
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 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);
if command_args.is_empty() {
+100 -46
View File
@@ -2,70 +2,124 @@ 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.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
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={EXE_PATH} 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'"
[Install]
WantedBy=multi-user.target
"#;
pub fn execute() {
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";
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_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")
.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",
];
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 from any location.");
}
+2
View File
@@ -1,7 +1,9 @@
pub mod battery;
pub mod cooling;
pub mod display;
pub mod gpu;
pub mod inhibit;
pub mod install_rules;
pub mod power;
pub mod status;
pub mod touchpad;
+84 -2
View File
@@ -2,6 +2,73 @@ 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 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) {
let cpu_dir = Path::new("/sys/devices/system/cpu");
@@ -155,6 +222,23 @@ fn set_platform_profile(profile: &str) {
}
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 {
PowerCommands::Performance => {
println!("Setting power profile to Performance");
@@ -166,8 +250,6 @@ pub fn execute(command: &PowerCommands) {
PowerCommands::Balanced => {
println!("Setting power profile to 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_energy_performance_preference("balance_performance");
println!("Operation completed successfully.");
+37
View File
@@ -1,3 +1,4 @@
use crate::commands::display;
use crate::hardware::gpu;
use std::fs;
use std::path::Path;
@@ -153,4 +154,40 @@ pub fn execute() {
if !tdp_found {
println!("CPU TDP Limit: Hardware Managed");
}
// Touchpad Status
let sys_class_input = Path::new("/sys/class/input");
if sys_class_input.exists()
&& let Ok(entries) = fs::read_dir(sys_class_input)
{
for entry in entries.flatten() {
let path = entry.path();
let name_path = path.join("name");
let inhibited_path = path.join("inhibited");
if name_path.exists()
&& inhibited_path.exists()
&& let Ok(name) = fs::read_to_string(&name_path)
&& name.to_lowercase().contains("touchpad")
&& let Ok(val) = fs::read_to_string(&inhibited_path)
{
let status = if val.trim() == "1" {
"Disabled"
} else {
"Enabled"
};
println!("Touchpad: {} ({})", status, name.trim());
}
}
}
// Display Status
let displays = display::get_active_display_info();
if displays.is_empty() {
println!("Display: Unknown / Firmware managed");
} else {
for d in displays {
println!("Display: {}", d);
}
}
}
+119
View File
@@ -0,0 +1,119 @@
use crate::cli::TouchpadCommands;
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_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) {
match command {
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?");
return;
}
if let Ok(entries) = fs::read_dir(dev_input) {
let mut found = false;
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with("input") {
let name_file = entry.path().join("name");
if let Ok(content) = fs::read_to_string(name_file) {
let content_lower = content.to_lowercase();
if content_lower.contains("touchpad") || content_lower.contains("trackpad") {
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) {
Ok(_) => info!("Set inhibit={} for {}", inhibited, name_str),
Err(e) => {
debug!("Failed to write to {}: {}", inhibited_file.display(), e)
}
}
}
}
}
}
}
if found {
println!("Operation completed successfully.");
} else {
error!("No touchpad device found in /sys/class/input.");
}
} 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);
}
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;
+13 -1
View File
@@ -26,13 +26,25 @@ fn main() {
Commands::Battery { command } => commands::battery::execute(command),
Commands::Power { command } => commands::power::execute(command),
Commands::Cooling { command } => commands::cooling::execute(command),
Commands::Display { command } => commands::display::execute(command),
Commands::Status => commands::status::execute(),
Commands::InstallRules => commands::install_rules::execute(),
Commands::Touchpad { command } => commands::touchpad::execute(command),
Commands::Inhibit {
command,
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());