19 Commits
Author SHA1 Message Date
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
21 changed files with 2783 additions and 114 deletions

No files matched your search

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.2.0"
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"
+49 -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,57 @@ 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)
If you are on Arch Linux, you can install **lapctl-git** from the AUR. This is the recommended method for Arch users as it automatically handles dependencies and udev rules.
```bash
just install
yay -S lapctl-git
```
#### 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 +98,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 +127,8 @@ lapctl
├── src/
│ ├── main.rs # The entry point
│ ├── cli.rs # Command definition & parsing
│ ├── commands/ # Feature logic (GPU, Battery, etc.)
│ ├── daemon/ # lapctld (D-Bus interface & logic)
│ ├── commands/ # CLI feature logic & proxies
│ ├── hardware/ # Hardware specific drivers (NVIDIA, etc.)
│ └── utils/ # System helpers
│
+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,
},
}
+41
View File
@@ -2,8 +2,49 @@ use crate::cli::BatteryCommands;
use log::{debug, error, info};
use std::fs;
use std::path::Path;
use zbus::Connection;
use zbus::proxy;
#[proxy(
interface = "org.lapctl1",
default_service = "org.lapctl",
default_path = "/org/lapctl"
)]
trait Lapctl {
async fn set_battery_limit(&self, percent: u32) -> zbus::Result<()>;
}
fn try_call_daemon(command: &BatteryCommands) -> bool {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(_) => return false,
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
match command {
BatteryCommands::Limit { percent } => {
proxy.set_battery_limit(*percent as u32).await.is_ok()
}
_ => false,
}
})
}
pub fn execute(command: &BatteryCommands) {
if matches!(command, BatteryCommands::Limit { .. }) && try_call_daemon(command) {
println!("Request handled by lapctld daemon.");
return;
}
match command {
BatteryCommands::Limit { percent } => {
if !(&1..=&100).contains(&percent) {
+46
View File
@@ -2,6 +2,47 @@ use crate::cli::CoolingCommands;
use log::{debug, info};
use std::fs;
use std::path::Path;
use zbus::Connection;
use zbus::proxy;
#[proxy(
interface = "org.lapctl1",
default_service = "org.lapctl",
default_path = "/org/lapctl"
)]
trait Lapctl {
async fn set_cooling_profile(&self, profile: String) -> zbus::Result<()>;
}
fn try_call_daemon(command: &CoolingCommands) -> bool {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(_) => return false,
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
match command {
CoolingCommands::Performance => proxy
.set_cooling_profile("performance".to_string())
.await
.is_ok(),
CoolingCommands::Balanced => proxy
.set_cooling_profile("balanced".to_string())
.await
.is_ok(),
CoolingCommands::Quiet => proxy.set_cooling_profile("quiet".to_string()).await.is_ok(),
}
})
}
fn set_ideapad_fan_mode(mode: &str) -> bool {
let mut success = false;
@@ -38,6 +79,11 @@ fn set_asus_throttle_policy(mode: &str) -> bool {
}
pub fn execute(command: &CoolingCommands) {
if try_call_daemon(command) {
println!("Request handled by lapctld daemon.");
return;
}
let (ideapad_mode, asus_mode, desc) = match command {
CoolingCommands::Performance => ("1", "1", "Performance"),
CoolingCommands::Balanced => ("0", "0", "Balanced"),
+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;
}
}
}
}
}
+295 -40
View File
@@ -1,13 +1,101 @@
use crate::cli::GpuCommands;
use crate::hardware::gpu::{
get_amd_igpu_name, get_current_mode, get_igpu_vendor, get_nvidia_gpu_pci_bus,
get_amd_igpu_name, get_current_mode, get_igpu_vendor, get_nvidia_gpu_pci_addr,
get_nvidia_gpu_pci_bus, kill_gpu_processes, remove_gpu, rescan_pci, unbind_gpu,
};
use crate::utils::system::{
assert_root, create_file, get_active_graphical_sessions, get_display_manager,
is_service_active, manage_service, rebuild_initramfs, terminate_session,
};
use crate::utils::system::{assert_root, create_file, get_display_manager, rebuild_initramfs};
use log::{error, info};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use std::process::Command;
use zbus::Connection;
use zbus::proxy;
#[proxy(
interface = "org.lapctl1",
default_service = "org.lapctl",
default_path = "/org/lapctl"
)]
trait Lapctl {
async fn switch_gpu_integrated(&self, no_reboot: bool) -> zbus::Result<()>;
async fn switch_gpu_hybrid(
&self,
rtd3: i32,
use_nvidia_current: bool,
no_reboot: bool,
) -> zbus::Result<()>;
async fn switch_gpu_nvidia(
&self,
dm: String,
force_comp: bool,
coolbits: i32,
use_nvidia_current: bool,
wayland: bool,
no_reboot: bool,
) -> zbus::Result<()>;
}
fn try_call_daemon(cmd: &GpuCommands) -> bool {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(_) => return false,
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
match cmd {
GpuCommands::Integrated { no_reboot } => {
proxy.switch_gpu_integrated(*no_reboot).await.is_ok()
}
GpuCommands::Hybrid {
rtd3,
use_nvidia_current,
no_reboot,
} => {
let rtd3_val = rtd3.map(|v| v as i32).unwrap_or(-1);
proxy
.switch_gpu_hybrid(rtd3_val, *use_nvidia_current, *no_reboot)
.await
.is_ok()
}
GpuCommands::Nvidia {
dm,
force_comp,
coolbits,
use_nvidia_current,
wayland,
no_reboot,
} => {
let dm_val = dm.clone().unwrap_or_default();
let coolbits_val = coolbits.map(|v| v as i32).unwrap_or(-1);
proxy
.switch_gpu_nvidia(
dm_val,
*force_comp,
coolbits_val,
*use_nvidia_current,
*wayland,
*no_reboot,
)
.await
.is_ok()
}
_ => false,
}
})
}
const CACHE_FILE_PATH: &str = "/var/cache/lapctl/cache.json";
const BLACKLIST_PATH: &str = "/etc/modprobe.d/blacklist-nvidia.conf";
@@ -258,10 +346,12 @@ fn create_cache_obj(bus_id: String) -> Cache {
}
fn create_cache_file() {
if get_current_mode() != "hybrid" {
error!("--cache-create requires that the system be in the hybrid Optimus mode");
let pci_addr = get_nvidia_gpu_pci_addr();
if pci_addr.is_none() {
log::debug!("NVIDIA GPU not found, skipping cache creation");
return;
}
let bus_id = get_nvidia_gpu_pci_bus();
let cache = create_cache_obj(bus_id);
@@ -272,7 +362,7 @@ fn create_cache_file() {
if let Ok(json) = serde_json::to_string_pretty(&cache)
&& fs::write(CACHE_FILE_PATH, json).is_ok()
{
log::debug!("Created file {}", CACHE_FILE_PATH);
log::debug!("Created/Updated cache file {}", CACHE_FILE_PATH);
}
}
@@ -290,10 +380,84 @@ fn read_cache_file() -> Result<Cache, String> {
}
}
fn switch_integrated() {
fn prepare_no_reboot(dm_flag: Option<String>) -> Result<Option<String>, String> {
let dm_name = dm_flag.or_else(get_display_manager);
if let Some(ref name) = dm_name
&& is_service_active(name)
{
info!("Stopping display manager: {}", name);
manage_service(name, "stop")?;
return Ok(Some(name.clone()));
}
// If no active DM found, check for graphical sessions
let sessions = get_active_graphical_sessions();
if !sessions.is_empty() {
println!("No active display manager found, but graphical sessions are running.");
println!("Terminating graphical sessions for no-reboot GPU switching...");
for session_id in sessions {
if let Err(e) = terminate_session(&session_id) {
error!("Failed to terminate session {}: {}", session_id, e);
}
}
// Give some time for sessions to terminate
std::thread::sleep(std::time::Duration::from_secs(2));
}
Ok(None)
}
fn finalize_no_reboot(stopped_dm: Option<String>) {
if let Some(name) = stopped_dm {
info!("Starting display manager: {}", name);
let _ = manage_service(&name, "start");
}
}
fn switch_integrated(no_reboot: bool) {
assert_root();
println!("Switching to integrated mode");
if !Path::new(CACHE_FILE_PATH).exists() {
create_cache_file();
}
cleanup();
create_file(BLACKLIST_PATH, BLACKLIST_CONTENT, false);
create_file(UDEV_INTEGRATED_PATH, UDEV_INTEGRATED, false);
let mut stopped_dm = None;
if no_reboot {
match prepare_no_reboot(None) {
Ok(dm) => stopped_dm = dm,
Err(e) => {
error!("{}", e);
return;
}
}
if let Err(e) = kill_gpu_processes() {
error!("Failed to kill GPU processes: {}", e);
}
// Try to unload modules
let modules = ["nvidia_uvm", "nvidia_modeset", "nvidia_drm", "nvidia"];
for module in modules {
let _ = Command::new("modprobe").args(["-r", module]).status();
}
if let Some(pci_addr) = get_nvidia_gpu_pci_addr() {
if let Err(e) = unbind_gpu(&pci_addr) {
error!("Failed to unbind GPU: {}", e);
}
if let Err(e) = remove_gpu(&pci_addr) {
error!("Failed to remove GPU: {}", e);
}
}
}
let is_debug = log::log_enabled!(log::Level::Debug);
let mut dis_cmd = Command::new("systemctl");
dis_cmd.args(["disable", "nvidia-persistenced.service"]);
@@ -306,21 +470,17 @@ fn switch_integrated() {
_ => error!("An error ocurred while disabling service"),
}
if get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() {
create_cache_file();
if no_reboot {
println!("Operation completed successfully");
finalize_no_reboot(stopped_dm);
} else {
rebuild_initramfs();
println!("Operation completed successfully");
println!("Please reboot your computer for changes to take effect!");
}
cleanup();
create_file(BLACKLIST_PATH, BLACKLIST_CONTENT, false);
create_file(UDEV_INTEGRATED_PATH, UDEV_INTEGRATED, false);
rebuild_initramfs();
println!("Operation completed successfully");
println!("Please reboot your computer for changes to take effect!");
}
fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool) {
fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool, no_reboot: bool) {
assert_root();
println!("Switching to hybrid mode");
println!(
@@ -328,12 +488,31 @@ fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool) {
rtd3.is_some()
);
if get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() {
if no_reboot && let Err(e) = rescan_pci() {
error!("Failed to rescan PCI bus: {}", e);
}
if !Path::new(CACHE_FILE_PATH).exists() {
create_cache_file();
}
cleanup();
let mut stopped_dm = None;
if no_reboot {
match prepare_no_reboot(None) {
Ok(dm) => stopped_dm = dm,
Err(e) => {
error!("{}", e);
return;
}
}
if let Err(e) = kill_gpu_processes() {
error!("Failed to kill GPU processes: {}", e);
}
}
// persistenced enable
let is_debug = log::log_enabled!(log::Level::Debug);
let mut enable_cmd = Command::new("systemctl");
@@ -370,9 +549,14 @@ fn switch_hybrid(rtd3: Option<u8>, use_nvidia_current: bool) {
create_file(MODESET_PATH, &modeset_content, false);
}
rebuild_initramfs();
println!("Operation completed successfully");
println!("Please reboot your computer for changes to take effect!");
if no_reboot {
println!("Operation completed successfully");
finalize_no_reboot(stopped_dm);
} else {
rebuild_initramfs();
println!("Operation completed successfully");
println!("Please reboot your computer for changes to take effect!");
}
}
fn switch_nvidia(
@@ -381,26 +565,18 @@ fn switch_nvidia(
coolbits: Option<u32>,
use_nvidia_current: bool,
wayland: bool,
no_reboot: bool,
) {
assert_root();
println!("Switching to nvidia mode");
println!("Enable ForceCompositionPipeline: {}", force_comp);
println!("Enable Coolbits: {}", coolbits.is_some());
// persistenced enable
let is_debug = log::log_enabled!(log::Level::Debug);
let mut enable_cmd = Command::new("systemctl");
enable_cmd.args(["enable", "nvidia-persistenced.service"]);
if !is_debug {
enable_cmd.stdout(std::process::Stdio::null());
enable_cmd.stderr(std::process::Stdio::null());
}
match enable_cmd.status() {
Ok(s) if s.success() => println!("Successfully enabled nvidia-persistenced.service"),
_ => error!("An error ocurred while enabling service"),
if no_reboot && let Err(e) = rescan_pci() {
error!("Failed to rescan PCI bus: {}", e);
}
if get_current_mode() == "hybrid" && !Path::new(CACHE_FILE_PATH).exists() {
if !Path::new(CACHE_FILE_PATH).exists() {
create_cache_file();
}
@@ -454,7 +630,7 @@ fn switch_nvidia(
create_file(EXTRA_XORG_PATH, &extra_xorg, false);
}
let display_manager = dm.or_else(get_display_manager);
let display_manager = dm.clone().or_else(get_display_manager);
if let Some(cdm) = display_manager {
if cdm == "sddm" {
@@ -480,12 +656,87 @@ 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) {
if try_call_daemon(cmd) {
println!("Request handled by lapctld daemon.");
return;
}
match cmd {
GpuCommands::Query => {
let mode = get_current_mode();
@@ -533,23 +784,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),
}
}
+57 -1
View File
@@ -1,7 +1,63 @@
use log::{error, info};
use std::process::Command;
use zbus::Connection;
use zbus::proxy;
#[proxy(
interface = "org.lapctl1",
default_service = "org.lapctl",
default_path = "/org/lapctl"
)]
trait Lapctl {
async fn set_system_inhibition(
&self,
active: bool,
why: String,
who: String,
) -> zbus::Result<()>;
}
fn try_call_daemon_inhibit(active: bool, why: String, who: String) -> bool {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(_) => return false,
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
proxy.set_system_inhibition(active, why, who).await.is_ok()
})
}
pub fn execute(command_args: &[String], why: &str, who: &str, daemon: bool, stop: bool) {
if stop {
if try_call_daemon_inhibit(false, why.to_string(), who.to_string()) {
println!("Persistent system inhibition deactivated via lapctld.");
} else {
error!("Failed to deactivate inhibition via daemon. Is lapctld running?");
}
return;
}
if daemon && command_args.is_empty() {
if try_call_daemon_inhibit(true, why.to_string(), who.to_string()) {
println!("Persistent system inhibition activated via lapctld.");
println!("The system will stay awake until you run: lapctl inhibit --stop");
return;
} else {
error!("Failed to activate inhibition via daemon. Is lapctld running?");
// Fallback to local systemd-inhibit if daemon call fails
}
}
pub fn execute(command_args: &[String], why: &str, who: &str, daemon: bool) {
let args = build_args(command_args, why, who);
if command_args.is_empty() {
+94 -46
View File
@@ -2,70 +2,118 @@ use crate::utils::system::{assert_root, create_file};
use log::info;
use std::process::Command;
const LAPCTL_UDEV_RULES: &str = r#"# /etc/udev/rules.d/99-lapctl.rules
# Automatically generated by lapctl
# This file grants read/write permissions to all local users
# for the SysFS endpoints required by lapctl's power, cooling, and battery commands.
// No longer needed: all privileged operations are now handled by the lapctld daemon via D-Bus.
const LAPCTL_UDEV_RULES: &str = "# lapctl udev rules (placeholder)\n";
# ACPI Platform Profiles & CPU Scaling
ACTION=="add", SUBSYSTEM=="firmware", RUN+="/bin/sh -c 'chmod a+w /sys/firmware/acpi/platform_profile 2>/dev/null'"
ACTION=="add", SUBSYSTEM=="cpu", RUN+="/bin/sh -c 'chmod a+w /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null'"
ACTION=="add", SUBSYSTEM=="cpu", RUN+="/bin/sh -c 'chmod a+w /sys/devices/system/cpu/cpu*/cpufreq/energy_performance_preference 2>/dev/null'"
const POLKIT_POLICY: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<vendor>lapctl</vendor>
<vendor_url>https://github.com/S4NKALP/lapctl</vendor_url>
<action id="org.s4nkalp.lapctl.execute">
<description>Elevated execution for lapctl</description>
<message>Authentication is required to perform privileged lapctl operations</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>yes</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">/usr/bin/lapctl</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_gui">false</annotate>
</action>
</policyconfig>
"#;
# Battery Charging Limits
ACTION=="add", SUBSYSTEM=="power_supply", RUN+="/bin/sh -c 'chmod a+w /sys/class/power_supply/BAT*/charge_control_end_threshold 2>/dev/null'"
const POLKIT_RULES: &str = r#"/* /etc/polkit-1/rules.d/10-lapctl.rules
* Automatically generated by lapctl
* This rule allows users in the 'wheel' or 'sudo' groups to
* perform privileged lapctl operations via pkexec without a password.
*/
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.policykit.exec" &&
action.lookup("program").indexOf("lapctl") !== -1 &&
(subject.isInGroup("wheel") || subject.isInGroup("sudo"))) {
return polkit.Result.YES;
}
});
"#;
# Lenovo Ideapad Thermal and Conservation Mode
ACTION=="add", SUBSYSTEM=="platform", RUN+="/bin/sh -c 'chmod a+w /sys/bus/platform/drivers/ideapad_acpi/VPC*/fan_mode 2>/dev/null'"
ACTION=="add", SUBSYSTEM=="platform", RUN+="/bin/sh -c 'chmod a+w /sys/bus/platform/drivers/ideapad_acpi/VPC*/conservation_mode 2>/dev/null'"
const DBUS_POLICY: &str = r#"<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.lapctl"/>
<allow send_destination="org.lapctl"/>
<allow receive_sender="org.lapctl"/>
</policy>
<policy group="wheel">
<allow send_destination="org.lapctl"/>
<allow receive_sender="org.lapctl"/>
</policy>
<policy group="sudo">
<allow send_destination="org.lapctl"/>
<allow receive_sender="org.lapctl"/>
</policy>
</busconfig>
"#;
# ASUS WMI Thermal Policy
ACTION=="add", SUBSYSTEM=="platform", RUN+="/bin/sh -c 'chmod a+w /sys/devices/platform/asus-nb-wmi/throttle_thermal_policy 2>/dev/null'"
const SYSTEMD_SERVICE: &str = r#"[Unit]
Description=lapctl hardware management daemon
After=dbus.service
# Intel RAPL TDP Control
ACTION=="add", SUBSYSTEM=="powercap", RUN+="/bin/sh -c 'chmod a+w /sys/class/powercap/intel-rapl:0/constraint_*_power_limit_uw 2>/dev/null'"
[Service]
Type=simple
ExecStart=/usr/bin/lapctl daemon
Restart=on-failure
# AMD HWMon Power Capping
ACTION=="add", SUBSYSTEM=="hwmon", RUN+="/bin/sh -c 'find /sys/class/hwmon -name \"power1_cap\" -exec chmod a+w {} + 2>/dev/null'"
[Install]
WantedBy=multi-user.target
"#;
pub fn execute() {
assert_root();
println!("Installing lapctl udev rules for rootless hardware configuration...");
println!("Installing lapctl configuration for daemon communication...");
let dbus_policy_path = "/etc/dbus-1/system.d/org.lapctl.conf";
create_file(dbus_policy_path, DBUS_POLICY, false);
let service_path = "/etc/systemd/system/lapctld.service";
create_file(service_path, SYSTEMD_SERVICE, false);
// Also install the udev rules file (even if empty now) to avoid errors if it was previously there
let rules_path = "/etc/udev/rules.d/99-lapctl.rules";
create_file(rules_path, LAPCTL_UDEV_RULES, false);
println!("Successfully wrote rules to {}", rules_path);
println!("Loading rules and applying immediate session permissions...");
// Polkit fallbacks for rootless execution without the daemon
let polkit_policy_path = "/usr/share/polkit-1/actions/org.s4nkalp.lapctl.policy";
create_file(polkit_policy_path, POLKIT_POLICY, false);
let _ = Command::new("udevadm")
.args(["control", "--reload-rules"])
let polkit_rules_path = "/etc/polkit-1/rules.d/10-lapctl.rules";
create_file(polkit_rules_path, POLKIT_RULES, false);
println!("Successfully installed configuration files.");
println!("Activating background daemon (lapctld)...");
// Reload systemd to recognize the new service
let _ = Command::new("systemctl").arg("daemon-reload").status();
// Enable and start the daemon
let status = Command::new("systemctl")
.args(["enable", "--now", "lapctld"])
.status();
let _ = Command::new("udevadm").args(["trigger"]).status();
// The udevadm trigger often fails to catch already-initialized ACPI nodes without a reboot,
// so we manually apply the permissions for the current session right now.
let commands = vec![
"chmod a+w /sys/firmware/acpi/platform_profile 2>/dev/null",
"chmod a+w /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null",
"chmod a+w /sys/devices/system/cpu/cpu*/cpufreq/energy_performance_preference 2>/dev/null",
"chmod a+w /sys/class/power_supply/BAT*/charge_control_end_threshold 2>/dev/null",
"chmod a+w /sys/bus/platform/drivers/ideapad_acpi/VPC*/fan_mode 2>/dev/null",
"chmod a+w /sys/bus/platform/drivers/ideapad_acpi/VPC*/conservation_mode 2>/dev/null",
"chmod a+w /sys/devices/platform/asus-nb-wmi/throttle_thermal_policy 2>/dev/null",
"chmod a+w /sys/class/powercap/intel-rapl:0/constraint_*_power_limit_uw 2>/dev/null",
"find /sys/class/hwmon -name \"power1_cap\" -exec chmod a+w {} + 2>/dev/null",
];
for cmd in commands {
let _ = Command::new("sh").arg("-c").arg(cmd).status();
if let Ok(s) = status {
if s.success() {
info!("lapctld daemon enabled and started.");
} else {
log::error!("Failed to enable/start lapctld daemon.");
}
}
info!("Udev reload and immediate ACL application complete.");
println!(
"Installation successful! You can now use lapctl power, cooling, and battery commands without sudo."
);
println!("Installation successful! The lapctld daemon is now running.");
println!("You can now use all lapctl commands without sudo.");
}
+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;
+51
View File
@@ -2,6 +2,52 @@ use crate::cli::PowerCommands;
use log::{error, info};
use std::fs;
use std::path::Path;
use zbus::Connection;
use zbus::proxy;
#[proxy(
interface = "org.lapctl1",
default_service = "org.lapctl",
default_path = "/org/lapctl"
)]
trait Lapctl {
async fn set_power_profile(&self, profile: String) -> zbus::Result<()>;
async fn set_tdp_limit(&self, watts: u32) -> zbus::Result<()>;
}
fn try_call_daemon(command: &PowerCommands) -> bool {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(_) => return false,
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
match command {
PowerCommands::Performance => proxy
.set_power_profile("performance".to_string())
.await
.is_ok(),
PowerCommands::Balanced => proxy
.set_power_profile("balanced".to_string())
.await
.is_ok(),
PowerCommands::BatterySave => proxy
.set_power_profile("battery-save".to_string())
.await
.is_ok(),
PowerCommands::LimitTdp { watts } => proxy.set_tdp_limit(*watts).await.is_ok(),
}
})
}
fn set_cpu_governor(governor: &str) {
let cpu_dir = Path::new("/sys/devices/system/cpu");
@@ -155,6 +201,11 @@ fn set_platform_profile(profile: &str) {
}
pub fn execute(command: &PowerCommands) {
if try_call_daemon(command) {
println!("Request handled by lapctld daemon.");
return;
}
match command {
PowerCommands::Performance => {
println!("Setting power profile to Performance");
+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);
}
}
}
+93
View File
@@ -0,0 +1,93 @@
use crate::cli::TouchpadCommands;
use log::{error, info};
use std::fs;
use std::path::Path;
use zbus::Connection;
use zbus::proxy;
#[proxy(
interface = "org.lapctl1",
default_service = "org.lapctl",
default_path = "/org/lapctl"
)]
trait Lapctl {
async fn set_touchpad_inhibition(&self, inhibited: bool) -> zbus::Result<()>;
}
fn try_call_daemon(command: &TouchpadCommands) -> bool {
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(_) => return false,
};
rt.block_on(async {
let connection = match Connection::system().await {
Ok(conn) => conn,
Err(_) => return false,
};
let proxy = match LapctlProxy::new(&connection).await {
Ok(p) => p,
Err(_) => return false,
};
match command {
TouchpadCommands::Enable => proxy.set_touchpad_inhibition(false).await.is_ok(),
TouchpadCommands::Disable => proxy.set_touchpad_inhibition(true).await.is_ok(),
}
})
}
pub fn execute(command: &TouchpadCommands) {
if try_call_daemon(command) {
println!("Request handled by lapctld daemon.");
return;
}
let sys_class_input = Path::new("/sys/class/input");
if !sys_class_input.exists() {
error!("Could not find /sys/class/input. Is this a Linux system?");
return;
}
let mut found_touchpads = Vec::new();
if 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")
{
found_touchpads.push((name.trim().to_string(), inhibited_path));
}
}
}
if found_touchpads.is_empty() {
error!("No touchpad devices found with 'inhibited' support in /sys/class/input.");
return;
}
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 => {
for (name, path) in found_touchpads {
match fs::write(&path, "1") {
Ok(_) => info!("Disabled touchpad: {}", name),
Err(e) => error!("Failed to disable touchpad {}: {}", name, e),
}
}
}
}
}
+164
View File
@@ -0,0 +1,164 @@
use crate::cli::GpuCommands;
use crate::commands::gpu;
use std::sync::Arc;
use tokio::sync::Mutex;
use zbus::interface;
use zbus::zvariant::OwnedFd;
pub struct LapctlInterface {
inhibit_fd: Arc<Mutex<Option<OwnedFd>>>,
}
impl Default for LapctlInterface {
fn default() -> Self {
Self {
inhibit_fd: Arc::new(Mutex::new(None)),
}
}
}
#[interface(name = "org.lapctl1")]
impl LapctlInterface {
pub async fn switch_gpu_integrated(&self, no_reboot: bool) -> zbus::fdo::Result<()> {
gpu::execute(&GpuCommands::Integrated { no_reboot });
Ok(())
}
pub async fn switch_gpu_hybrid(
&self,
rtd3: i32,
use_nvidia_current: bool,
no_reboot: bool,
) -> zbus::fdo::Result<()> {
let rtd3_opt = if rtd3 < 0 { None } else { Some(rtd3 as u8) };
gpu::execute(&GpuCommands::Hybrid {
rtd3: rtd3_opt,
use_nvidia_current,
no_reboot,
});
Ok(())
}
pub async fn switch_gpu_nvidia(
&self,
dm: String,
force_comp: bool,
coolbits: i32,
use_nvidia_current: bool,
wayland: bool,
no_reboot: bool,
) -> zbus::fdo::Result<()> {
let dm_opt = if dm.is_empty() { None } else { Some(dm) };
let coolbits_opt = if coolbits < 0 {
None
} else {
Some(coolbits as u32)
};
gpu::execute(&GpuCommands::Nvidia {
dm: dm_opt,
force_comp,
coolbits: coolbits_opt,
use_nvidia_current,
wayland,
no_reboot,
});
Ok(())
}
pub async fn set_battery_limit(&self, percent: u32) -> zbus::fdo::Result<()> {
crate::commands::battery::execute(&crate::cli::BatteryCommands::Limit {
percent: percent as u8,
});
Ok(())
}
pub async fn set_power_profile(&self, profile: String) -> zbus::fdo::Result<()> {
let cmd = match profile.as_str() {
"performance" => crate::cli::PowerCommands::Performance,
"balanced" => crate::cli::PowerCommands::Balanced,
"battery-save" => crate::cli::PowerCommands::BatterySave,
_ => {
return Err(zbus::fdo::Error::InvalidArgs(format!(
"Invalid power profile: {}",
profile
)));
}
};
crate::commands::power::execute(&cmd);
Ok(())
}
pub async fn set_tdp_limit(&self, watts: u32) -> zbus::fdo::Result<()> {
crate::commands::power::execute(&crate::cli::PowerCommands::LimitTdp { watts });
Ok(())
}
pub async fn set_cooling_profile(&self, profile: String) -> zbus::fdo::Result<()> {
let cmd = match profile.as_str() {
"performance" => crate::cli::CoolingCommands::Performance,
"balanced" => crate::cli::CoolingCommands::Balanced,
"quiet" => crate::cli::CoolingCommands::Quiet,
_ => {
return Err(zbus::fdo::Error::InvalidArgs(format!(
"Invalid cooling profile: {}",
profile
)));
}
};
crate::commands::cooling::execute(&cmd);
Ok(())
}
pub async fn set_touchpad_inhibition(&self, inhibited: bool) -> zbus::fdo::Result<()> {
let cmd = if inhibited {
crate::cli::TouchpadCommands::Disable
} else {
crate::cli::TouchpadCommands::Enable
};
crate::commands::touchpad::execute(&cmd);
Ok(())
}
pub async fn set_system_inhibition(
&self,
active: bool,
why: String,
who: String,
) -> zbus::fdo::Result<()> {
let mut fd_lock = self.inhibit_fd.lock().await;
if active {
if fd_lock.is_some() {
return Ok(()); // Already inhibiting
}
let conn = zbus::Connection::system().await.map_err(|e| {
zbus::fdo::Error::Failed(format!("Failed to connect to system bus: {}", e))
})?;
let msg = conn
.call_method(
Some("org.freedesktop.login1"),
"/org/freedesktop/login1",
Some("org.freedesktop.login1.Manager"),
"Inhibit",
&("sleep:idle", who, why, "block"),
)
.await
.map_err(|e| zbus::fdo::Error::Failed(format!("Failed to inhibit: {}", e)))?;
let fd: OwnedFd = msg
.body()
.deserialize()
.map_err(|e| zbus::fdo::Error::Failed(format!("Failed to parse FD: {}", e)))?;
*fd_lock = Some(fd);
log::info!("System inhibition activated via lapctld.");
} else {
*fd_lock = None;
log::info!("System inhibition deactivated via lapctld.");
}
Ok(())
}
}
+19
View File
@@ -0,0 +1,19 @@
use log::info;
use std::error::Error;
use zbus::connection::Builder;
mod dbus_iface;
pub async fn run() -> Result<(), Box<dyn Error>> {
info!("Starting lapctld daemon...");
let _connection = Builder::system()?
.name("org.lapctl")?
.serve_at("/org/lapctl", dbus_iface::LapctlInterface::default())?
.build()
.await?;
info!("Daemon is running on the system bus.");
loop {
tokio::time::sleep(std::time::Duration::from_secs(3600)).await;
}
}
+65
View File
@@ -54,6 +54,71 @@ pub fn get_nvidia_gpu_pci_bus() -> String {
std::process::exit(1);
}
pub fn get_nvidia_gpu_pci_addr() -> Option<String> {
let output = Command::new("lspci")
.output()
.expect("Failed to execute lspci");
let lspci_output = String::from_utf8_lossy(&output.stdout);
for line in lspci_output.lines() {
if line.contains("NVIDIA")
&& (line.contains("VGA compatible controller") || line.contains("3D controller"))
{
let pci_addr = line.split_whitespace().next().unwrap();
if pci_addr.contains(':') {
if pci_addr.chars().filter(|&c| c == ':').count() == 1 {
return Some(format!("0000:{}", pci_addr));
}
return Some(pci_addr.to_string());
}
}
}
None
}
pub fn unbind_gpu(pci_addr: &str) -> Result<(), String> {
let driver_path = Path::new("/sys/bus/pci/devices")
.join(pci_addr)
.join("driver");
if driver_path.exists() {
let unbind_path = driver_path.join("unbind");
info!("Unbinding GPU at {} via {:?}", pci_addr, unbind_path);
std::fs::write(unbind_path, pci_addr).map_err(|e| e.to_string())?;
}
Ok(())
}
pub fn remove_gpu(pci_addr: &str) -> Result<(), String> {
let remove_path = Path::new("/sys/bus/pci/devices")
.join(pci_addr)
.join("remove");
if remove_path.exists() {
info!("Removing GPU at {} via {:?}", pci_addr, remove_path);
std::fs::write(remove_path, "1").map_err(|e| e.to_string())?;
}
Ok(())
}
pub fn rescan_pci() -> Result<(), String> {
let rescan_path = Path::new("/sys/bus/pci/rescan");
info!("Rescanning PCI bus via {:?}", rescan_path);
std::fs::write(rescan_path, "1").map_err(|e| e.to_string())?;
Ok(())
}
pub fn kill_gpu_processes() -> Result<(), String> {
info!("Killing processes using NVIDIA GPU...");
// Try using fuser first as it is more likely to be available and easy to use
let _ = Command::new("fuser").args(["-k", "/dev/nvidia*"]).output();
// Also kill specific nvidia services if they are running
let _ = Command::new("systemctl")
.args(["stop", "nvidia-persistenced.service"])
.output();
Ok(())
}
pub fn get_igpu_vendor() -> Option<String> {
let output = Command::new("lspci").output().ok()?;
let lspci_output = String::from_utf8_lossy(&output.stdout);
+1
View File
@@ -1,4 +1,5 @@
pub mod cli;
pub mod commands;
pub mod daemon;
pub mod hardware;
pub mod utils;
+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());