From 6214750e30507484de2797bcab3f6776ac208289 Mon Sep 17 00:00:00 2001 From: Felipe Spychalski Date: Thu, 9 Oct 2025 15:29:56 -0300 Subject: [PATCH] feat:turn off screens when idle turns off screens after an idle period, configured by idle_timeout.timeout_duration. --- Cargo.lock | 121 +++++++++++++++++++++++++- Cargo.toml | 3 + regreet.sample.toml | 5 +- src/config.rs | 13 +++ src/gui/component.rs | 13 +++ src/idle.rs | 200 +++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 7 files changed, 353 insertions(+), 3 deletions(-) create mode 100644 src/idle.rs diff --git a/Cargo.lock b/Cargo.lock index b386e60..8899135 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,6 +312,12 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "educe" version = "0.6.0" @@ -350,6 +356,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "field-offset" version = "0.3.6" @@ -916,9 +932,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "lock_api" @@ -1113,6 +1135,15 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.38" @@ -1179,6 +1210,9 @@ dependencies = [ "tracing-appender", "tracing-subscriber", "tracker", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", ] [[package]] @@ -1230,6 +1264,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + [[package]] name = "ryu" version = "1.0.18" @@ -1719,6 +1766,76 @@ version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" +dependencies = [ + "bitflags", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "pkg-config", +] + [[package]] name = "windows-core" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 824feae..35d4d46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,9 @@ tracing = "0.1" tracing-appender = "0.2" tracing-subscriber = { version = "0.3", features = ["local-time"] } tracker = "0.2" +wayland-client = "0.31.11" +wayland-protocols = { version = "0.32.9", features = ["staging", "client"] } +wayland-protocols-wlr = { version = "0.3.9", features = ["client"] } [features] gtk4_8 = ["gtk4/v4_8"] diff --git a/regreet.sample.toml b/regreet.sample.toml index b85f8b0..1163373 100644 --- a/regreet.sample.toml +++ b/regreet.sample.toml @@ -43,12 +43,15 @@ reboot = ["systemctl", "reboot"] poweroff = ["systemctl", "poweroff"] # The command prefix for X11 sessions to start the X server -x11_prefix = [ "startx", "/usr/bin/env" ] +x11_prefix = ["startx", "/usr/bin/env"] [appearance] # The message that initially displays on startup greeting_msg = "Welcome back!" +[idle_timeout] +# Time in seconds before turning off screens +timeout_duration = 30 [widget.clock] # strftime format argument diff --git a/src/config.rs b/src/config.rs index cd05323..531eb17 100644 --- a/src/config.rs +++ b/src/config.rs @@ -104,6 +104,12 @@ fn default_greeting_msg() -> String { GREETING_MSG.to_string() } +#[derive(Default, Deserialize, Serialize)] +pub struct IdleTimeoutSettings { + #[serde(default)] + timeout_duration: Option, +} + /// The configuration struct #[derive(Default, Deserialize)] pub struct Config { @@ -124,6 +130,9 @@ pub struct Config { #[serde(default)] pub(crate) widget: WidgetConfig, + + #[serde(default)] + idle_timeout: IdleTimeoutSettings, } #[derive(Deserialize, Default)] @@ -161,4 +170,8 @@ impl Config { pub fn get_default_message(&self) -> String { self.appearance.greeting_msg.clone() } + + pub fn get_idle_timeout(&self) -> Option { + self.idle_timeout.timeout_duration + } } diff --git a/src/gui/component.rs b/src/gui/component.rs index eb8385e..63df0ab 100644 --- a/src/gui/component.rs +++ b/src/gui/component.rs @@ -336,6 +336,19 @@ impl AsyncComponent for Greeter { let mut model = Self::new(&input.config_path, input.demo).await; let widgets = view_output!(); + // Start idle monitoring in a separate thread if configured + if let Some(timeout_seconds) = model.config.get_idle_timeout() { + if timeout_seconds > 0 { + let timeout_ms = (timeout_seconds * 1000) as u32; + std::thread::spawn(move || { + if let Err(e) = crate::idle::run_idle_loop(timeout_ms) { + tracing::error!("Idle monitoring failed: {}", e); + } + }); + tracing::info!("Started idle monitoring with {}s timeout", timeout_seconds); + } + } + // Make the info bar permanently visible, since it was made invisible during init. The // actual visuals are controlled by `InfoBar::set_revealed`. widgets.ui.error_info.set_visible(true); diff --git a/src/idle.rs b/src/idle.rs new file mode 100644 index 0000000..4431afb --- /dev/null +++ b/src/idle.rs @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: 2025 fspy +// +// SPDX-License-Identifier: GPL-3.0-or-later + +//! Turns off displays when the user is idle + +use std::collections::HashMap; + +use wayland_client::protocol::{wl_output, wl_registry, wl_seat}; +use wayland_client::{delegate_noop, Connection, Dispatch, QueueHandle}; +use wayland_protocols::ext::idle_notify::v1::client::{ + ext_idle_notification_v1, ext_idle_notifier_v1, +}; +use wayland_protocols_wlr::output_power_management::v1::client::{ + zwlr_output_power_manager_v1, + zwlr_output_power_v1::{self, Mode}, +}; + +#[derive(Default)] +struct State { + outputs: HashMap, + seat: Option, + output_power_manager: Option, + idle_notifier: Option, + timeout_ms: u32, + idle_notification: Option, + is_idle: bool, + protocols_ready: bool, +} + +impl Dispatch for State { + fn event( + state: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + match event { + wl_registry::Event::Global { + name, interface, .. + } => { + match &interface[..] { + "wl_output" => { + let output = registry.bind::(name, 2, qh, ()); + state.outputs.insert(name, output); + } + "wl_seat" => { + state.seat = Some(registry.bind::(name, 7, qh, ())); + } + "zwlr_output_power_manager_v1" => { + state.output_power_manager = Some( + registry + .bind::( + name, + 1, + qh, + (), + ), + ); + } + "ext_idle_notifier_v1" => { + state.idle_notifier = Some( + registry.bind::( + name, + 1, + qh, + (), + ), + ); + } + _ => return, + } + state.check_protocols_ready(); + } + wl_registry::Event::GlobalRemove { name } => { + if let Some(output) = state.outputs.remove(&name) { + output.release(); + } + } + _ => {} + } + } +} + +delegate_noop!(State: ignore wl_seat::WlSeat); +delegate_noop!(State: ignore wl_output::WlOutput); +delegate_noop!(State: ext_idle_notifier_v1::ExtIdleNotifierV1); +delegate_noop!(State: zwlr_output_power_manager_v1::ZwlrOutputPowerManagerV1); +delegate_noop!(State: ignore zwlr_output_power_v1::ZwlrOutputPowerV1); + +impl Dispatch for State { + fn event( + state: &mut Self, + notification: &ext_idle_notification_v1::ExtIdleNotificationV1, + event: ext_idle_notification_v1::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + match event { + ext_idle_notification_v1::Event::Idled => { + if !state.is_idle { + tracing::info!("System went idle, turning off displays"); + state.is_idle = true; + state.set_display_power_mode(Mode::Off, qh); + } + } + ext_idle_notification_v1::Event::Resumed => { + if state.is_idle { + tracing::info!("System resumed from idle, turning on displays"); + state.is_idle = false; + state.set_display_power_mode(Mode::On, qh); + } + notification.destroy(); + state.idle_notification = None; + state.create_idle_notification(qh); + } + _ => { + tracing::warn!("Received unexpected idle notification event"); + } + } + } +} + +impl State { + fn set_display_power_mode(&self, mode: Mode, qh: &QueueHandle) { + if let Some(power_manager) = self.output_power_manager.as_ref() { + self.outputs.iter().for_each(|(_, output)| { + let power_control = power_manager.get_output_power(output, qh, ()); + power_control.set_mode(mode); + power_control.destroy(); + }); + } else { + tracing::warn!("No output power manager available to set display power mode"); + } + } + + fn create_idle_notification(&mut self, qh: &QueueHandle) { + if let (Some(idle_notifier), Some(seat)) = (&self.idle_notifier, &self.seat) { + self.idle_notification = + Some(idle_notifier.get_idle_notification(self.timeout_ms, seat, qh, ())); + } else { + tracing::warn!("Cannot create idle notification: missing idle notifier or seat"); + } + } + + fn has_required_protocols(&self) -> bool { + self.output_power_manager.is_some() + && self.idle_notifier.is_some() + && !self.outputs.is_empty() + } + + fn check_protocols_ready(&mut self) { + self.protocols_ready = self.has_required_protocols(); + } +} + +pub fn run_idle_loop(timeout_ms: u32) -> Result<(), Box> { + tracing::info!("Starting idle monitoring with {}ms timeout", timeout_ms); + + let conn = Connection::connect_to_env()?; + let display = conn.display(); + let mut event_queue = conn.new_event_queue(); + let qh = event_queue.handle(); + let _registry = display.get_registry(&qh, ()); + + let mut state = State { + timeout_ms, + ..Default::default() + }; + + // Discover all available Wayland globals + event_queue.roundtrip(&mut state)?; + + if !state.has_required_protocols() { + if state.output_power_manager.is_none() { + tracing::warn!("zwlr_output_power_manager_v1 not supported by the Wayland compositor. Idle functionality disabled."); + } + if state.idle_notifier.is_none() { + tracing::warn!("ext_idle_notifier_v1 not supported by the Wayland compositor. Idle functionality disabled."); + } + if state.outputs.is_empty() { + tracing::warn!("No outputs found. Idle functionality disabled."); + } + return Ok(()); + } + + tracing::info!( + "Found {} output(s), setting up idle monitoring", + state.outputs.len() + ); + + state.create_idle_notification(&qh); + + loop { + event_queue.blocking_dispatch(&mut state)?; + } +} diff --git a/src/main.rs b/src/main.rs index 30028a4..7dcb56a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod client; mod config; mod constants; mod gui; +mod idle; mod sysutil; mod tomlutils;