From 24060ebe0d96bca06d0ab435b414bf4cb8c04b05 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Thu, 19 Mar 2026 20:26:43 +0545 Subject: [PATCH 01/11] =?UTF-8?q?feat(root):=20add=20wayland=20display=20r?= =?UTF-8?q?efresh=E2=80=91rate=20control=20and=20update=20dependencies=20?= =?UTF-8?q?=20Add=20support=20for=20querying=20and=20setting=20display=20r?= =?UTF-8?q?efresh=20rates=20and=20extend=20the=20command=E2=80=91line=20in?= =?UTF-8?q?terface=20for=20refresh=E2=80=91rate=20management.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **New Features** - Introduce `lapctl display rates` to list available refresh rates. - Add `lapctl display set-rate ` 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. --- Cargo.lock | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + README.md | 5 ++ 3 files changed, 169 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index aba62a9..8178e4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index ad2e133..7f2a727 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] } diff --git a/README.md b/README.md index f04e11a..142a1b6 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ 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 (Multi-backend natively supports **Hyprland, Sway, KDE Plasma, GNOME, and X11**). - **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. @@ -76,6 +77,10 @@ lapctl cooling performance 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 From bb4e9248fdfe437f7cb487174d13ca2123d42c68 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Thu, 19 Mar 2026 20:26:47 +0545 Subject: [PATCH 02/11] 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 --- src/cli.rs | 16 ++++++++++++++++ src/main.rs | 1 + 2 files changed, 17 insertions(+) diff --git a/src/cli.rs b/src/cli.rs index 1f848b9..7b086e5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -37,6 +37,11 @@ pub enum Commands { #[command(subcommand)] command: CoolingCommands, }, + /// Display management commands + Display { + #[command(subcommand)] + command: DisplayCommands, + }, /// Hardware status Status, /// Install udev rules for rootless operation @@ -144,3 +149,14 @@ pub enum TouchpadCommands { /// 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, + }, +} diff --git a/src/main.rs b/src/main.rs index 29a775b..df61a6d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,7 @@ 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), From c03251b0a811980d9fa1417f2b7c7ccff2e61a0a Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Thu, 19 Mar 2026 20:26:51 +0545 Subject: [PATCH 03/11] feat(src/commands): add display command implementation for refresh rate control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/commands/display.rs | 378 ++++++++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 1 + 2 files changed, 379 insertions(+) create mode 100644 src/commands/display.rs diff --git a/src/commands/display.rs b/src/commands/display.rs new file mode 100644 index 0000000..f259ead --- /dev/null +++ b/src/commands/display.rs @@ -0,0 +1,378 @@ +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, + 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, + pub heads: HashMap, + pub modes: HashMap, + pub done: bool, + pub config_success: Option, +} + +impl Dispatch for AppState { + fn event( + state: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + let wl_registry::Event::Global { + name, + interface, + version, + } = event + else { + return; + }; + if interface == "zwlr_output_manager_v1" { + let manager = registry.bind::(name, version, qh, ()); + state.manager = Some(manager); + } + } +} + +impl Dispatch for AppState { + fn event( + state: &mut Self, + _: &ZwlrOutputManagerV1, + event: zwlr_output_manager_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + 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, + ) -> std::sync::Arc { + match opcode { + 0 => qh.make_data::(()), + _ => panic!( + "Missing event_created_child specialization for opcode {}", + opcode + ), + } + } +} + +impl Dispatch for AppState { + fn event( + state: &mut Self, + head: &ZwlrOutputHeadV1, + event: zwlr_output_head_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + 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, + ) -> std::sync::Arc { + match opcode { + 3 => qh.make_data::(()), + _ => panic!( + "Missing event_created_child specialization for opcode {} on head", + opcode + ), + } + } +} + +impl Dispatch for AppState { + fn event( + state: &mut Self, + mode: &ZwlrOutputModeV1, + event: zwlr_output_mode_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + 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 for AppState { + fn event( + state: &mut Self, + _: &ZwlrOutputConfigurationV1, + event: zwlr_output_configuration_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + 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 for AppState { + fn event( + _: &mut Self, + _: &ZwlrOutputConfigurationHeadV1, + _: wayland_protocols_wlr::output_management::v1::client::zwlr_output_configuration_head_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + +fn fetch_state() -> Option<(Connection, AppState, wayland_client::EventQueue)> { + 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)) +} + +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; + } + } + } + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index dd4aa25..1f11eeb 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,5 +1,6 @@ pub mod battery; pub mod cooling; +pub mod display; pub mod gpu; pub mod inhibit; pub mod install_rules; From a9ea518457eaa71cbbe08685386c43f46d757f44 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Thu, 19 Mar 2026 20:35:57 +0545 Subject: [PATCH 04/11] 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 --- src/commands/display.rs | 24 ++++++++++++++++++++++++ src/commands/status.rs | 11 +++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/commands/display.rs b/src/commands/display.rs index f259ead..f7c2530 100644 --- a/src/commands/display.rs +++ b/src/commands/display.rs @@ -269,6 +269,30 @@ fn fetch_state() -> Option<(Connection, AppState, wayland_client::EventQueue Vec { + 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() { diff --git a/src/commands/status.rs b/src/commands/status.rs index e5f692a..c459501 100644 --- a/src/commands/status.rs +++ b/src/commands/status.rs @@ -1,3 +1,4 @@ +use crate::commands::display; use crate::hardware::gpu; use std::fs; use std::path::Path; @@ -179,4 +180,14 @@ pub fn execute() { } } } + + // 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); + } + } } From 9dba194e7d454b1b2ff0a73babfa988a5fdcfc13 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Thu, 19 Mar 2026 20:36:00 +0545 Subject: [PATCH 05/11] feat(root): add native Wayland display refresh rate control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 142a1b6..00c1e12 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ 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 (Multi-backend natively supports **Hyprland, Sway, KDE Plasma, GNOME, and X11**). +- **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. @@ -49,6 +49,10 @@ cargo install --path . #### Requirements - **systemd**: For sleep inhibitor (`systemd-inhibit`) - **X11/NVIDIA Tools**: `xrandr`, `nvidia-settings` for GPU management +- **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. --- From f7a28482140ca4033044b08e9acbfd51a7d6eff0 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Thu, 19 Mar 2026 20:38:27 +0545 Subject: [PATCH 06/11] 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. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 00c1e12..7d15a23 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ 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 From 707192903eb1941676c77c97fd12815b82baef85 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Thu, 19 Mar 2026 20:55:15 +0545 Subject: [PATCH 07/11] 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 --- src/cli.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/cli.rs b/src/cli.rs index 7b086e5..1107f6d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -110,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, + }, } #[derive(Subcommand, Debug)] From 5e921d283396002cb8b9f71f9a84f947bfc814d9 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Thu, 19 Mar 2026 20:55:18 +0545 Subject: [PATCH 08/11] feat(src/commands): add run_on_dgpu command to launch applications on discrete GPU MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/commands/gpu.rs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/commands/gpu.rs b/src/commands/gpu.rs index cb80a18..4807750 100644 --- a/src/commands/gpu.rs +++ b/src/commands/gpu.rs @@ -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), } } From e840bb7ce14ad95d01d090c33f8a3e1725433676 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Thu, 19 Mar 2026 20:55:22 +0545 Subject: [PATCH 09/11] 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. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7d15a23..908a7a5 100644 --- a/README.md +++ b/README.md @@ -63,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 From f333dabfe592cb0bd81363b501f381cbd844bee5 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Thu, 19 Mar 2026 20:59:48 +0545 Subject: [PATCH 10/11] docs(root): fixformatting of GPU run command example - New Features: - Enhancements: Clarify formatting of `lapctl gpu run steam` example. - Bug Fixes: --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 908a7a5..0c93351 100644 --- a/README.md +++ b/README.md @@ -63,7 +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 +lapctl gpu run steam # Run 'steam' on dGPU directly while in Hybrid mode # Prolong battery life lapctl battery limit 80 From 7fb21f5e3d1ae767eee02c705542ec8ce61d0c7e Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Thu, 19 Mar 2026 21:03:40 +0545 Subject: [PATCH 11/11] chore(root): bump version to 0.2.0 and set edition 2024 New Features - None Enhancements - Updated edition to 2024 Bug Fixes - None --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7f2a727..2427647 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lapctl" -version = "0.1.0" +version = "0.2.0" edition = "2024" [dependencies]