From 4d50adcfb29e7ee3fb66769e82f5beaf65db2532 Mon Sep 17 00:00:00 2001 From: Vika Date: Fri, 3 Jan 2025 14:52:47 +0300 Subject: Allow idle time detection on all Wayland desktops Other desktops require C dependencies I don't want to bring in, so let's leave it at that. Pretty much all of the code was kanged from the new Iced rewrite. --- src/gtk/preferences_dialog.ui | 2 +- src/helpers/idle.rs | 120 +++++++++++++++ src/helpers/wayland_idle.rs | 348 ++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 6 + src/ui/window.rs | 12 +- 5 files changed, 476 insertions(+), 12 deletions(-) create mode 100644 src/helpers/idle.rs create mode 100644 src/helpers/wayland_idle.rs (limited to 'src') diff --git a/src/gtk/preferences_dialog.ui b/src/gtk/preferences_dialog.ui index 2971d88..8def8cb 100644 --- a/src/gtk/preferences_dialog.ui +++ b/src/gtk/preferences_dialog.ui @@ -24,7 +24,7 @@ _Notify of Idle - (GNOME Only) + (Wayland only) True True diff --git a/src/helpers/idle.rs b/src/helpers/idle.rs new file mode 100644 index 0000000..074eba9 --- /dev/null +++ b/src/helpers/idle.rs @@ -0,0 +1,120 @@ +// Furtherance - Track your time without being tracked +// Copyright (C) 2024 Ricky Kresslein +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::env; +use std::time::Duration; + +#[cfg(target_os = "linux")] +use { + std::path::Path, std::sync::Arc +}; + +pub fn get_mac_windows_x11_idle_seconds() -> u64 { + 0 // unimplemented +} + +pub fn get_idle_time() -> Result> { + match env::consts::OS { + "windows" => Ok(get_mac_windows_x11_idle_seconds()), + "macos" => Ok(get_mac_windows_x11_idle_seconds()), + #[cfg(target_os = "linux")] + "linux" => { + if is_wayland() { + if is_gnome() { + get_gnome_idle_sync() + } else { + get_wayland_idle_sync() + } + } else if is_x11() { + Ok(get_mac_windows_x11_idle_seconds()) + } else { + Ok(0) + } + } + _ => Ok(0), + } +} + +#[cfg(target_os = "linux")] +fn is_wayland() -> bool { + if let Ok(_) = env::var("XDG_SESSION_TYPE").map(|v| v == "wayland") { + return true; + } else if let Ok(display) = env::var("WAYLAND_DISPLAY") { + if display.chars().next() == Some('/') { + return Path::new(&display).exists(); + } + if let Ok(runtime_dir) = env::var("XDG_RUNTIME_DIR") { + return Path::new( + &format!("{}/{}", runtime_dir, display) + ).exists(); + } + } + false +} + +#[cfg(target_os = "linux")] +fn is_x11() -> bool { + x11rb::connect(None).is_ok() +} + +#[cfg(target_os = "linux")] +fn get_wayland_idle_sync() -> Result> { + use crate::helpers::wayland_idle; + + wayland_idle::initialize_wayland().unwrap(); + + Ok(wayland_idle::get_idle_time()) +} + +#[cfg(target_os = "linux")] +fn get_gnome_idle_sync() -> Result> { + use dbus::blocking::stdintf::org_freedesktop_dbus::Properties; + let c = dbus::blocking::Connection::new_session()?; + + let p = c.with_proxy( + "org.gnome.Mutter.IdleMonitor", + "/org/gnome/Mutter/IdleMonitor/Core", + Duration::from_millis(5000), + ); + let (idle_time,): (u64,) = + p.method_call("org.gnome.Mutter.IdleMonitor", "GetIdletime", ())?; + Ok(idle_time / 1000) +} + +#[cfg(target_os = "linux")] +pub fn is_kde() -> bool { + if let Ok(desktop) = std::env::var("XDG_CURRENT_DESKTOP") { + return desktop.to_uppercase().contains("KDE"); + } + false +} + +#[cfg(target_os = "linux")] +fn is_gnome() -> bool { + if let Ok(xdg_current_desktop) = env::var("XDG_CURRENT_DESKTOP") { + if xdg_current_desktop.to_lowercase().contains("gnome") { + return true; + } + } + + if let Ok(gdm_session) = env::var("GDMSESSION") { + if gdm_session.to_lowercase().contains("gnome") { + return true; + } + } + + false +} diff --git a/src/helpers/wayland_idle.rs b/src/helpers/wayland_idle.rs new file mode 100644 index 0000000..ae2bce7 --- /dev/null +++ b/src/helpers/wayland_idle.rs @@ -0,0 +1,348 @@ +// Furtherance - Track your time without being tracked +// Copyright (C) 2024 Ricky Kresslein +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{ + mpsc::{channel, Receiver, Sender}, + Arc, Mutex, +}; +use std::thread; +use wayland_client::protocol::wl_registry::{self, WlRegistry}; +use wayland_client::protocol::wl_seat::WlSeat; +use wayland_client::{Connection, Dispatch, QueueHandle}; +use wayland_protocols::ext::idle_notify::v1::client::ext_idle_notification_v1::{ + self, ExtIdleNotificationV1, +}; +use wayland_protocols::ext::idle_notify::v1::client::ext_idle_notifier_v1::ExtIdleNotifierV1; +use wayland_protocols_plasma::idle::client::org_kde_kwin_idle::OrgKdeKwinIdle; +use wayland_protocols_plasma::idle::client::org_kde_kwin_idle_timeout::{ + self, OrgKdeKwinIdleTimeout, +}; + +struct IdleState { + idle_since: Option, +} + +impl IdleState { + fn new() -> Self { + Self { idle_since: None } + } +} + +lazy_static::lazy_static! { + static ref IDLE_STATE: Arc> = Arc::new(Mutex::new(IdleState::new())); + static ref WAYLAND_INITIALIZED: Arc> = Arc::new(Mutex::new(false)); + static ref MONITOR_RUNNING: Arc = Arc::new(AtomicBool::new(false)); + static ref STOP_SIGNAL: Arc>>> = Arc::new(Mutex::new(None)); +} + +enum IdleManager { + Kde(OrgKdeKwinIdle), + Standard(ExtIdleNotifierV1), +} + +struct WaylandState { + idle_state: Arc>, + seats: HashMap, + idle_manager: Option, +} + +impl WaylandState { + fn new(idle_state: Arc>) -> Self { + Self { + idle_state, + seats: HashMap::new(), + idle_manager: None, + } + } + + fn handle_global( + &mut self, + registry: &WlRegistry, + name: u32, + interface: String, + version: u32, + qh: &QueueHandle, + ) { + match &interface[..] { + "wl_seat" => { + let seat = registry.bind::(name, version, qh, ()); + if let Some(idle_manager) = &self.idle_manager { + let timeout_ms = 1000; // 1 second + match idle_manager { + IdleManager::Kde(manager) => { + let _timeout = manager.get_idle_timeout(&seat, timeout_ms, qh, ()); + } + IdleManager::Standard(manager) => { + let _notification = + manager.get_idle_notification(timeout_ms, &seat, qh, name); + } + } + } + self.seats.insert(name, seat); + } + "org_kde_kwin_idle" => { + let idle_manager: OrgKdeKwinIdle = registry.bind(name, version, qh, ()); + // Set up idle timeouts for existing seats + for (_, seat) in &self.seats { + let _timeout = idle_manager.get_idle_timeout(seat, 1000, qh, ()); + } + self.idle_manager = Some(IdleManager::Kde(idle_manager)); + } + "ext_idle_notifier_v1" => { + let idle_manager: ExtIdleNotifierV1 = registry.bind(name, version, qh, ()); + // Set up idle notifications for existing seats + for (name, seat) in &self.seats { + let _notification = idle_manager.get_idle_notification(1000, seat, qh, *name); + } + self.idle_manager = Some(IdleManager::Standard(idle_manager)); + } + _ => {} + } + } +} + +impl Dispatch for WaylandState { + fn event( + state: &mut Self, + registry: &WlRegistry, + event: wl_registry::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + match event { + wl_registry::Event::Global { + name, + interface, + version, + } => state.handle_global(registry, name, interface, version, qh), + _ => {} + } + } +} + +impl Dispatch for WaylandState { + fn event( + state: &mut Self, + _proxy: &OrgKdeKwinIdleTimeout, + event: org_kde_kwin_idle_timeout::Event, + _data: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + match event { + org_kde_kwin_idle_timeout::Event::Idle => { + if let Ok(mut state) = state.idle_state.lock() { + state.idle_since = Some(std::time::Instant::now()); + } + } + org_kde_kwin_idle_timeout::Event::Resumed => { + if let Ok(mut state) = state.idle_state.lock() { + state.idle_since = None; + } + } + _ => {} + } + } +} + +impl Dispatch for WaylandState { + fn event( + state: &mut Self, + _proxy: &ExtIdleNotificationV1, + event: ext_idle_notification_v1::Event, + _data: &u32, + _conn: &Connection, + _qh: &QueueHandle, + ) { + match event { + ext_idle_notification_v1::Event::Idled => { + if let Ok(mut state) = state.idle_state.lock() { + state.idle_since = Some(std::time::Instant::now()); + } + } + ext_idle_notification_v1::Event::Resumed => { + if let Ok(mut state) = state.idle_state.lock() { + state.idle_since = None; + } + } + _ => {} + } + } +} + +impl Dispatch for WaylandState { + fn event( + _state: &mut Self, + _proxy: &WlSeat, + _event: ::Event, + _data: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + } +} + +impl Dispatch for WaylandState { + fn event( + _state: &mut Self, + _proxy: &OrgKdeKwinIdle, + _event: ::Event, + _data: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + } +} + +impl Dispatch for WaylandState { + fn event( + _state: &mut Self, + _proxy: &ExtIdleNotifierV1, + _event: ::Event, + _data: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + } +} + +fn run_wayland_monitor(rx: Receiver<()>) -> Result<(), Box> { + let conn = Connection::connect_to_env()?; + let mut event_queue = conn.new_event_queue(); + let qh = event_queue.handle(); + + let display = conn.display(); + display.get_registry(&qh, ()); + + let state = WaylandState::new(IDLE_STATE.clone()); + let mut state = state; + + loop { + if !MONITOR_RUNNING.load(Ordering::SeqCst) { + break; + } + + // Check if we received a stop signal + if rx.try_recv().is_ok() { + break; + } + + event_queue.blocking_dispatch(&mut state)?; + thread::sleep(std::time::Duration::from_millis(100)); + } + Ok(()) +} + +pub fn initialize_wayland() -> Result<(), Box> { + if let Ok(mut initialized) = WAYLAND_INITIALIZED.lock() { + if *initialized { + return Ok(()); + } + + MONITOR_RUNNING.store(true, Ordering::SeqCst); + + // Create a channel for stop signaling + let (tx, rx) = channel(); + if let Ok(mut stop_signal) = STOP_SIGNAL.lock() { + *stop_signal = Some(tx); + } + + thread::spawn(move || { + if let Err(e) = run_wayland_monitor(rx) { + eprintln!("Wayland monitor error: {}", e); + } + }); + + *initialized = true; + } + Ok(()) +} + +pub fn get_idle_time() -> u64 { + if !MONITOR_RUNNING.load(Ordering::SeqCst) { + return 0; + } + + if let Ok(state) = IDLE_STATE.lock() { + if let Some(idle_since) = state.idle_since { + idle_since.elapsed().as_secs() + } else { + 0 + } + } else { + 0 + } +} + +pub fn start_idle_monitor() -> Result<(), Box> { + // Stop any existing monitor and wait for confirmation + stop_idle_monitor(); + + // Reset idle state + if let Ok(mut state) = IDLE_STATE.lock() { + state.idle_since = None; + } + + if let Ok(mut initialized) = WAYLAND_INITIALIZED.lock() { + if !*initialized { + MONITOR_RUNNING.store(true, Ordering::SeqCst); + + // Create a channel for stop signaling + let (tx, rx) = channel(); + if let Ok(mut stop_signal) = STOP_SIGNAL.lock() { + *stop_signal = Some(tx); + } + + thread::spawn(move || { + if let Err(e) = run_wayland_monitor(rx) { + eprintln!("Wayland monitor error: {}", e); + } + }); + + *initialized = true; + } + } + Ok(()) +} + +pub fn stop_idle_monitor() { + MONITOR_RUNNING.store(false, Ordering::SeqCst); + + // Signal the monitor thread to stop + if let Ok(stop_signal) = STOP_SIGNAL.lock() { + if let Some(tx) = stop_signal.as_ref() { + let _ = tx.send(()); + } + } + + // Reset idle state + if let Ok(mut state) = IDLE_STATE.lock() { + state.idle_since = None; + } + + // Reset initialized state + if let Ok(mut initialized) = WAYLAND_INITIALIZED.lock() { + *initialized = false; + } + + // Clear the stop signal + if let Ok(mut stop_signal) = STOP_SIGNAL.lock() { + *stop_signal = None; + } +} diff --git a/src/main.rs b/src/main.rs index abedf64..ac51761 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,12 @@ mod config; mod database; mod settings_manager; mod ui; +mod helpers { + mod idle; + mod wayland_idle; + + pub use idle::get_idle_time; +} use self::application::FurtheranceApplication; diff --git a/src/ui/window.rs b/src/ui/window.rs index bf23729..7fd4450 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -583,17 +583,7 @@ impl FurtheranceWindow { } fn get_idle_time(&self) -> Result> { - let c = Connection::new_session()?; - - let p = c.with_proxy( - "org.gnome.Mutter.IdleMonitor", - "/org/gnome/Mutter/IdleMonitor/Core", - Duration::from_millis(5000), - ); - let (idle_time,): (u64,) = - p.method_call("org.gnome.Mutter.IdleMonitor", "GetIdletime", ())?; - - Ok(idle_time / 1000) + crate::helpers::get_idle_time() } fn check_user_idle(&self) { -- cgit 1.4.1