Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2733215d9f | ||
|
|
7fb21f5e3d | ||
|
|
f333dabfe5 | ||
|
|
e840bb7ce1 | ||
|
|
5e921d2833 | ||
|
|
707192903e | ||
|
|
f7a2848214 | ||
|
|
9dba194e7d | ||
|
|
a9ea518457 | ||
|
|
c03251b0a8 | ||
|
|
bb4e9248fd | ||
|
|
24060ebe0d | ||
|
|
d8c0cda3d7 | ||
|
|
42b3fb47fd | ||
|
|
1bb54facaf |
No files matched your search
Generated
+162
@@ -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
@@ -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"] }
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user