15 Commits
Author SHA1 Message Date
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
11 changed files with 755 additions and 2 deletions

No files matched your search

Generated
+162
View File
@@ -61,6 +61,22 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "bitflags"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "cc"
version = "1.2.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "clap"
version = "4.5.60"
@@ -107,6 +123,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "env_filter"
version = "1.0.0"
@@ -130,6 +152,22 @@ dependencies = [
"log",
]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "heck"
version = "0.5.0"
@@ -182,8 +220,22 @@ dependencies = [
"regex",
"serde",
"serde_json",
"wayland-client",
"wayland-protocols-wlr",
]
[[package]]
name = "libc"
version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "log"
version = "0.4.29"
@@ -202,6 +254,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "portable-atomic"
version = "1.13.1"
@@ -226,6 +284,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quick-xml"
version = "0.39.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.45"
@@ -264,6 +331,19 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]]
name = "serde"
version = "1.0.228"
@@ -307,6 +387,18 @@ dependencies = [
"zmij",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "strsim"
version = "0.11.1"
@@ -336,6 +428,76 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "wayland-backend"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa75f400b7f719bcd68b3f47cd939ba654cedeef690f486db71331eec4c6a406"
dependencies = [
"cc",
"downcast-rs",
"rustix",
"smallvec",
"wayland-sys",
]
[[package]]
name = "wayland-client"
version = "0.31.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3"
dependencies = [
"bitflags",
"rustix",
"wayland-backend",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols"
version = "0.32.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7"
dependencies = [
"bitflags",
"wayland-backend",
"wayland-client",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-wlr"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78248e4cc0eff8163370ba5c158630dcae1f3497a586b826eca2ef5f348d6235"
dependencies = [
"bitflags",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "wayland-scanner"
version = "0.31.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3"
dependencies = [
"proc-macro2",
"quick-xml",
"quote",
]
[[package]]
name = "wayland-sys"
version = "0.31.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374f6b70e8e0d6bf9461a32988fd553b59ff630964924dad6e4a4eb6bd538d17"
dependencies = [
"pkg-config",
]
[[package]]
name = "windows-link"
version = "0.2.1"
+3 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "lapctl"
version = "0.1.0"
version = "0.2.0"
edition = "2024"
[dependencies]
@@ -10,3 +10,5 @@ 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"] }
+16 -1
View File
@@ -24,6 +24,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.
@@ -46,7 +48,11 @@ cargo install --path .
#### 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.
---
@@ -57,6 +63,7 @@ cargo install --path .
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,6 +78,14 @@ lapctl power limit-tdp 35 # Stay under 35W
lapctl cooling quiet
lapctl cooling performance
# 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
lapctl inhibit --daemon # Run in background
lapctl inhibit -- why "Critical update" ./long-task.sh
+35
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)
@@ -100,6 +110,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 +147,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,
},
}
+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;
}
}
}
}
}
+38
View File
@@ -485,6 +485,43 @@ fn switch_nvidia(
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::Query => {
@@ -551,5 +588,6 @@ pub fn execute(cmd: &GpuCommands) {
*use_nvidia_current,
*wayland,
),
GpuCommands::Run { command } => run_on_dgpu(command),
}
}
+4
View File
@@ -27,6 +27,9 @@ ACTION=="add", SUBSYSTEM=="powercap", RUN+="/bin/sh -c 'chmod a+w /sys/class/pow
# AMD HWMon Power Capping
ACTION=="add", SUBSYSTEM=="hwmon", RUN+="/bin/sh -c 'find /sys/class/hwmon -name \"power1_cap\" -exec chmod a+w {} + 2>/dev/null'"
# Input Device Inhibition (Touchpad)
ACTION=="add", SUBSYSTEM=="input", RUN+="/bin/sh -c 'find /sys/class/input -name \"inhibited\" -exec chmod a+w {} + 2>/dev/null'"
"#;
pub fn execute() {
@@ -58,6 +61,7 @@ pub fn execute() {
"chmod a+w /sys/devices/platform/asus-nb-wmi/throttle_thermal_policy 2>/dev/null",
"chmod a+w /sys/class/powercap/intel-rapl:0/constraint_*_power_limit_uw 2>/dev/null",
"find /sys/class/hwmon -name \"power1_cap\" -exec chmod a+w {} + 2>/dev/null",
"find /sys/class/input -name \"inhibited\" -exec chmod a+w {} + 2>/dev/null",
];
for cmd in commands {
+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;
+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);
}
}
}
+54
View File
@@ -0,0 +1,54 @@
use crate::cli::TouchpadCommands;
use log::{error, info};
use std::fs;
use std::path::Path;
pub fn execute(command: &TouchpadCommands) {
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),
}
}
}
}
}
+2
View File
@@ -26,8 +26,10 @@ 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,