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..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] @@ -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..0c93351 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 (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. @@ -47,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. --- @@ -58,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 @@ -76,6 +82,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 diff --git a/src/cli.rs b/src/cli.rs index 1f848b9..1107f6d 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 @@ -105,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)] @@ -144,3 +155,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/commands/display.rs b/src/commands/display.rs new file mode 100644 index 0000000..f7c2530 --- /dev/null +++ b/src/commands/display.rs @@ -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, + 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)) +} + +pub fn get_active_display_info() -> 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() { + 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/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), } } 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; 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); + } + } } 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),