From 2ecd9f6c37943cea7d0f0fea2fbcba58f0c347a6 Mon Sep 17 00:00:00 2001 From: Jacob Ross <16659674+jacobaross@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:49:53 -0400 Subject: [PATCH] Fix 64-bit Hyprland window addresses --- src/cycle_state.rs | 18 ++--- src/preview_windows/mod.rs | 10 +-- src/wayland_backends.rs | 131 +++++++++++++++++++++---------------- src/window_manager.rs | 16 +++-- src/windows_manager.rs | 16 ++--- src/x11_manager.rs | 27 ++++---- 6 files changed, 125 insertions(+), 93 deletions(-) diff --git a/src/cycle_state.rs b/src/cycle_state.rs index 62e07ae..1601bad 100644 --- a/src/cycle_state.rs +++ b/src/cycle_state.rs @@ -1,4 +1,4 @@ -use crate::window_manager::{EveWindow, WindowManager}; +use crate::window_manager::{EveWindow, WindowId, WindowManager}; use anyhow::Result; use std::time::{Duration, Instant}; @@ -247,7 +247,7 @@ impl CycleState { .is_some_and(|t| t.elapsed() < ACTIVATION_GRACE) } - pub fn sync_with_active(&mut self, active_window: u32) { + pub fn sync_with_active(&mut self, active_window: WindowId) { // Within the grace window after our own activation, the compositor's // reported active window may still be the *previous* one — its focus // commit is asynchronous. Trust `current_index` rather than rewinding @@ -347,7 +347,7 @@ impl CycleState { mod tests { use super::*; - fn create_test_window(id: u32, title: &str) -> EveWindow { + fn create_test_window(id: WindowId, title: &str) -> EveWindow { EveWindow { id, title: title.to_string(), @@ -505,7 +505,7 @@ mod tests { // Mock WindowManager for testing switch_to struct MockWindowManager { - activated_windows: std::sync::Mutex>, + activated_windows: std::sync::Mutex>, } impl MockWindowManager { @@ -515,7 +515,7 @@ mod tests { } } - fn get_activated(&self) -> Vec { + fn get_activated(&self) -> Vec { self.activated_windows.lock().unwrap().clone() } } @@ -525,7 +525,7 @@ mod tests { Ok(vec![]) } - fn activate_window(&self, window_id: u32) -> anyhow::Result<()> { + fn activate_window(&self, window_id: WindowId) -> anyhow::Result<()> { self.activated_windows.lock().unwrap().push(window_id); Ok(()) } @@ -538,15 +538,15 @@ mod tests { Ok(()) } - fn get_active_window(&self) -> anyhow::Result { + fn get_active_window(&self) -> anyhow::Result { Ok(0) } - fn minimize_window(&self, _window_id: u32) -> anyhow::Result<()> { + fn minimize_window(&self, _window_id: WindowId) -> anyhow::Result<()> { Ok(()) } - fn restore_window(&self, _window_id: u32) -> anyhow::Result<()> { + fn restore_window(&self, _window_id: WindowId) -> anyhow::Result<()> { Ok(()) } } diff --git a/src/preview_windows/mod.rs b/src/preview_windows/mod.rs index 19962c3..918339b 100644 --- a/src/preview_windows/mod.rs +++ b/src/preview_windows/mod.rs @@ -3,7 +3,7 @@ use crate::cycle_state::CycleState; use crate::preview_common::{ preview_should_hide, snap_position, DragRect, DragState, DRAG_THRESHOLD_PX, SNAP_THRESHOLD_PX, }; -use crate::window_manager::WindowManager; +use crate::window_manager::{WindowId, WindowManager}; use crate::windows_manager::{hwnd_to_id, id_to_hwnd}; use anyhow::{Context, Result}; use std::collections::HashMap; @@ -167,7 +167,7 @@ use crate::preview_positions::PreviewPositions; /// State for a single preview window. The pointer is stored in the /// window's GWLP_USERDATA so the wnd_proc can recover it. struct PreviewWindowState { - source_id: u32, + source_id: WindowId, character_name: String, thumbnail: Hthumbnail, wm: Arc, @@ -184,7 +184,7 @@ struct PreviewWindowState { /// One owned preview window. Drop unregisters the DWM thumbnail. struct OwnedPreview { hwnd: HWND, - source_id: u32, + source_id: WindowId, /// Mirror of `PreviewWindowState.is_active` kept here so reconcile /// can detect changes without dereferencing the GWLP_USERDATA pointer /// on every tick. @@ -232,7 +232,7 @@ struct PreviewManager { list: Option, /// Most recent foreground EVE window id. Used by the list window's /// paint callback to decide which row to highlight. - active_id: u32, + active_id: WindowId, /// Names rendered in the list window on the previous reconcile, in /// order. Used to detect changes (add/remove/reorder) so we only /// invalidate when something actually shifted — calling @@ -607,7 +607,7 @@ impl PreviewManager { /// focuses a non-EVE app, then presses F11, the cycle would step /// from wherever we last cycled to (say A) instead of from B, /// looking like "cycle skipped a client." - fn update_active(&mut self, active_id: u32) { + fn update_active(&mut self, active_id: WindowId) { self.state.lock().unwrap().sync_with_active(active_id); for preview in self.previews.values_mut() { diff --git a/src/wayland_backends.rs b/src/wayland_backends.rs index 95de65c..49c82a0 100644 --- a/src/wayland_backends.rs +++ b/src/wayland_backends.rs @@ -1,6 +1,6 @@ use crate::config::Config; use crate::pointer_nudge::{schedule_nudge, PointerNudger}; -use crate::window_manager::{EveWindow, WindowManager}; +use crate::window_manager::{EveWindow, WindowId, WindowManager}; use anyhow::{Context, Result}; use serde_json::Value; use std::process::Command; @@ -12,6 +12,10 @@ use x11rb::protocol::xproto::{ }; use x11rb::rust_connection::RustConnection; +fn x11_id(window_id: WindowId) -> Result { + u32::try_from(window_id).context("Window ID exceeds the X11 32-bit range") +} + // ============================================================================ // Shared XWayland / EWMH helpers (KWin + GNOME) // ============================================================================ @@ -252,7 +256,8 @@ impl WindowManager for KWinManager { if pid == 0 || !crate::eve_match::pid_is_eve_client(pid) { continue; } - // Parse hex window ID (e.g., "0x06e00008") to u32. + // Parse the X11 window ID (e.g., "0x06e00008") and widen it for + // the shared cross-platform representation. let id = if let Some(hex) = id_str.strip_prefix("0x") { u32::from_str_radix(hex, 16).unwrap_or(0) } else { @@ -262,7 +267,7 @@ impl WindowManager for KWinManager { continue; } eve_windows.push(EveWindow { - id, + id: id.into(), title: title.trim_start_matches("EVE - ").to_string(), }); } @@ -270,7 +275,7 @@ impl WindowManager for KWinManager { Ok(eve_windows) } - fn activate_window(&self, window_id: u32) -> Result<()> { + fn activate_window(&self, window_id: WindowId) -> Result<()> { // Shared XWayland/EWMH activation. `wmctrl -i -a` silently no-ops // under KDE Wayland for XWayland clients, so we send the // `_NET_ACTIVE_WINDOW` ClientMessage ourselves (with the @@ -279,7 +284,7 @@ impl WindowManager for KWinManager { &self.conn, self.screen_num, self.net_active_window_atom, - window_id, + x11_id(window_id)?, )?; // Pointer-focus nudge: KWin won't re-target clicks to the newly // raised client until the pointer moves. See `pointer_nudge`. @@ -294,8 +299,8 @@ impl WindowManager for KWinManager { let height = config.display_height - config.panel_height; for window in windows { - // Convert u32 to hex format for wmctrl - let hex_id = format!("0x{:08x}", window.id); + let window_id = x11_id(window.id)?; + let hex_id = format!("0x{:08x}", window_id); // Move and resize window using wmctrl Command::new("wmctrl") @@ -310,13 +315,13 @@ impl WindowManager for KWinManager { Ok(()) } - fn get_active_window(&self) -> Result { + fn get_active_window(&self) -> Result { // Read _NET_ACTIVE_WINDOW directly via x11rb under XWayland — // matches what X11Manager does and avoids an `xdotool` runtime dep. - ewmh_active_window(&self.conn, self.screen_num, self.net_active_window_atom) + ewmh_active_window(&self.conn, self.screen_num, self.net_active_window_atom).map(Into::into) } - fn minimize_window(&self, window_id: u32) -> Result<()> { + fn minimize_window(&self, window_id: WindowId) -> Result<()> { // Iconify on our own X connection (see `ewmh_iconify`) so it's // ordered after the activation of the newly-focused client, instead // of racing it via a separate `xdotool` connection. @@ -324,12 +329,12 @@ impl WindowManager for KWinManager { &self.conn, self.screen_num, self.wm_change_state_atom, - window_id, + x11_id(window_id)?, ) } - fn restore_window(&self, window_id: u32) -> Result<()> { - let hex_id = format!("0x{:08x}", window_id); + fn restore_window(&self, window_id: WindowId) -> Result<()> { + let hex_id = format!("0x{:08x}", x11_id(window_id)?); // wmctrl -i -a activates and restores from minimized state Command::new("wmctrl") .args(["-i", "-a", &hex_id]) @@ -414,8 +419,8 @@ impl SwayManager { .map(|s| s.to_string()) } - fn get_window_id(window: &Value) -> Option { - window.get("id").and_then(|i| i.as_u64()).map(|i| i as u32) + fn get_window_id(window: &Value) -> Option { + window.get("id").and_then(|i| i.as_u64()) } /// Sway's IPC tree exposes the X11/XWayland or native pid on each @@ -458,7 +463,7 @@ impl WindowManager for SwayManager { Ok(eve_windows) } - fn activate_window(&self, window_id: u32) -> Result<()> { + fn activate_window(&self, window_id: WindowId) -> Result<()> { let output = Command::new("swaymsg") .arg(format!("[con_id={}] focus", window_id)) .output() @@ -501,7 +506,7 @@ impl WindowManager for SwayManager { Ok(()) } - fn get_active_window(&self) -> Result { + fn get_active_window(&self) -> Result { let windows = self.get_all_windows()?; for window in windows { @@ -517,7 +522,7 @@ impl WindowManager for SwayManager { anyhow::bail!("No active window found") } - fn minimize_window(&self, window_id: u32) -> Result<()> { + fn minimize_window(&self, window_id: WindowId) -> Result<()> { Command::new("swaymsg") .arg(format!("[con_id={}] move scratchpad", window_id)) .output() @@ -525,7 +530,7 @@ impl WindowManager for SwayManager { Ok(()) } - fn restore_window(&self, window_id: u32) -> Result<()> { + fn restore_window(&self, window_id: WindowId) -> Result<()> { // Show from scratchpad restores it Command::new("swaymsg") .arg(format!("[con_id={}] scratchpad show", window_id)) @@ -539,6 +544,11 @@ impl WindowManager for SwayManager { // Hyprland Backend (via hyprctl) // ============================================================================ +fn parse_hyprland_address(address: &str) -> Option { + let hex = address.strip_prefix("0x")?; + u64::from_str_radix(hex, 16).ok().filter(|id| *id != 0) +} + pub struct HyprlandManager; impl HyprlandManager { @@ -592,26 +602,22 @@ impl WindowManager for HyprlandManager { if pid == 0 || !crate::eve_match::pid_is_eve_client(pid) { continue; } - if let Some(address) = window.get("address").and_then(|a| a.as_str()) { - // Hyprland addresses are hex; lossy-narrow to u32 for - // the cross-platform EveWindow.id we use elsewhere. - let id = if let Some(hex) = address.strip_prefix("0x") { - u32::from_str_radix(hex, 16).unwrap_or(0) - } else { - 0 - }; - eve_windows.push(EveWindow { - id, - title: title.trim_start_matches("EVE - ").to_string(), - }); - } + let Some(address) = window.get("address").and_then(|a| a.as_str()) else { + continue; + }; + let Some(id) = parse_hyprland_address(address) else { + continue; + }; + eve_windows.push(EveWindow { + id, + title: title.trim_start_matches("EVE - ").to_string(), + }); } Ok(eve_windows) } - fn activate_window(&self, window_id: u32) -> Result<()> { - // Convert u32 back to hex address + fn activate_window(&self, window_id: WindowId) -> Result<()> { let address = format!("0x{:x}", window_id); let output = Command::new("hyprctl") @@ -665,7 +671,7 @@ impl WindowManager for HyprlandManager { Ok(()) } - fn get_active_window(&self) -> Result { + fn get_active_window(&self) -> Result { let output = Command::new("hyprctl") .arg("activewindow") .arg("-j") @@ -675,19 +681,14 @@ impl WindowManager for HyprlandManager { let window: Value = serde_json::from_slice(&output.stdout).context("Failed to parse hyprctl output")?; - if let Some(address) = window.get("address").and_then(|a| a.as_str()) { - let id = if let Some(hex) = address.strip_prefix("0x") { - u32::from_str_radix(hex, 16).unwrap_or(0) - } else { - 0 - }; - return Ok(id); - } - - anyhow::bail!("Failed to get active window ID") + window + .get("address") + .and_then(|a| a.as_str()) + .and_then(parse_hyprland_address) + .ok_or_else(|| anyhow::anyhow!("Failed to get active window ID")) } - fn minimize_window(&self, window_id: u32) -> Result<()> { + fn minimize_window(&self, window_id: WindowId) -> Result<()> { let address = format!("0x{:x}", window_id); Command::new("hyprctl") .args([ @@ -700,7 +701,7 @@ impl WindowManager for HyprlandManager { Ok(()) } - fn restore_window(&self, window_id: u32) -> Result<()> { + fn restore_window(&self, window_id: WindowId) -> Result<()> { let address = format!("0x{:x}", window_id); // Move back to current workspace Command::new("hyprctl") @@ -876,19 +877,19 @@ impl WindowManager for GnomeManager { continue; } eve_windows.push(EveWindow { - id, + id: id.into(), title: title.trim_start_matches("EVE - ").to_string(), }); } Ok(eve_windows) } - fn activate_window(&self, window_id: u32) -> Result<()> { + fn activate_window(&self, window_id: WindowId) -> Result<()> { ewmh_activate( &self.conn, self.screen_num, self.net_active_window_atom, - window_id, + x11_id(window_id)?, )?; // Pointer-focus nudge — Mutter, like KWin, only re-targets clicks // on pointer motion. See `pointer_nudge`. @@ -909,23 +910,43 @@ impl WindowManager for GnomeManager { Ok(()) } - fn get_active_window(&self) -> Result { - ewmh_active_window(&self.conn, self.screen_num, self.net_active_window_atom) + fn get_active_window(&self) -> Result { + ewmh_active_window(&self.conn, self.screen_num, self.net_active_window_atom).map(Into::into) } - fn minimize_window(&self, window_id: u32) -> Result<()> { + fn minimize_window(&self, window_id: WindowId) -> Result<()> { // ICCCM iconify (WM_CHANGE_STATE → IconicState). Mutter honors it for // XWayland windows. Shared with the KWin backend — see `ewmh_iconify`. ewmh_iconify( &self.conn, self.screen_num, self.wm_change_state_atom, - window_id, + x11_id(window_id)?, ) } - fn restore_window(&self, window_id: u32) -> Result<()> { + fn restore_window(&self, window_id: WindowId) -> Result<()> { // Activating an iconified window both un-minimizes and focuses it. self.activate_window(window_id) } } + +#[cfg(test)] +mod tests { + use super::parse_hyprland_address; + + #[test] + fn hyprland_address_preserves_pointer_sized_value() { + let id = parse_hyprland_address("0x56257ff1ebe0").expect("valid Hyprland address"); + assert_eq!(id, 0x56257ff1ebe0); + assert!(id > u32::MAX.into()); + assert_eq!(format!("0x{id:x}"), "0x56257ff1ebe0"); + } + + #[test] + fn hyprland_address_rejects_invalid_or_zero_values() { + assert_eq!(parse_hyprland_address("56257ff1ebe0"), None); + assert_eq!(parse_hyprland_address("0xnot-hex"), None); + assert_eq!(parse_hyprland_address("0x0"), None); + } +} diff --git a/src/window_manager.rs b/src/window_manager.rs index 336588d..b51a177 100644 --- a/src/window_manager.rs +++ b/src/window_manager.rs @@ -1,9 +1,15 @@ use crate::config::Config; use anyhow::Result; +/// Cross-platform window identifier. X11 IDs are 32-bit, but Hyprland +/// reports pointer-sized hexadecimal addresses and Win64 HWND values are +/// pointer-sized as well. Keep the shared representation wide enough to +/// preserve those native values without truncation. +pub type WindowId = u64; + #[derive(Debug, Clone)] pub struct EveWindow { - pub id: u32, + pub id: WindowId, pub title: String, } @@ -13,19 +19,19 @@ pub trait WindowManager: Send + Sync { fn get_eve_windows(&self) -> Result>; /// Activate/focus a specific window by ID - fn activate_window(&self, window_id: u32) -> Result<()>; + fn activate_window(&self, window_id: WindowId) -> Result<()>; /// Stack all EVE windows at the same position (centered) fn stack_windows(&self, windows: &[EveWindow], config: &Config) -> Result<()>; /// Get the currently active window ID - fn get_active_window(&self) -> Result; + fn get_active_window(&self) -> Result; /// Minimize a window - fn minimize_window(&self, window_id: u32) -> Result<()>; + fn minimize_window(&self, window_id: WindowId) -> Result<()>; /// Restore a minimized window - fn restore_window(&self, window_id: u32) -> Result<()>; + fn restore_window(&self, window_id: WindowId) -> Result<()>; } #[cfg(unix)] diff --git a/src/windows_manager.rs b/src/windows_manager.rs index 2e16b1e..f494282 100644 --- a/src/windows_manager.rs +++ b/src/windows_manager.rs @@ -1,6 +1,6 @@ use crate::config::Config; use crate::eve_match::pid_is_eve_client; -use crate::window_manager::{EveWindow, WindowManager}; +use crate::window_manager::{EveWindow, WindowId, WindowManager}; use crate::windows_helpers::should_attach_thread_input; use anyhow::{Context, Result}; use std::ffi::c_void; @@ -22,11 +22,11 @@ impl WindowsManager { } } -pub(crate) fn hwnd_to_id(hwnd: HWND) -> u32 { - hwnd.0 as usize as u32 +pub(crate) fn hwnd_to_id(hwnd: HWND) -> WindowId { + hwnd.0 as usize as WindowId } -pub(crate) fn id_to_hwnd(id: u32) -> HWND { +pub(crate) fn id_to_hwnd(id: WindowId) -> HWND { HWND(id as usize as *mut c_void) } @@ -174,7 +174,7 @@ impl WindowManager for WindowsManager { Ok(windows) } - fn activate_window(&self, window_id: u32) -> Result<()> { + fn activate_window(&self, window_id: WindowId) -> Result<()> { force_activate(id_to_hwnd(window_id)); Ok(()) } @@ -202,12 +202,12 @@ impl WindowManager for WindowsManager { Ok(()) } - fn get_active_window(&self) -> Result { + fn get_active_window(&self) -> Result { let hwnd = unsafe { GetForegroundWindow() }; Ok(hwnd_to_id(hwnd)) } - fn minimize_window(&self, window_id: u32) -> Result<()> { + fn minimize_window(&self, window_id: WindowId) -> Result<()> { let hwnd = id_to_hwnd(window_id); unsafe { // SC_MINIMIZE via WM_SYSCOMMAND is friendlier to applications @@ -224,7 +224,7 @@ impl WindowManager for WindowsManager { Ok(()) } - fn restore_window(&self, window_id: u32) -> Result<()> { + fn restore_window(&self, window_id: WindowId) -> Result<()> { let hwnd = id_to_hwnd(window_id); unsafe { SendMessageW( diff --git a/src/x11_manager.rs b/src/x11_manager.rs index 45bb411..8f199e4 100644 --- a/src/x11_manager.rs +++ b/src/x11_manager.rs @@ -1,6 +1,6 @@ use crate::config::Config; use crate::eve_match::pid_is_eve_client; -use crate::window_manager::{EveWindow, WindowManager}; +use crate::window_manager::{EveWindow, WindowId, WindowManager}; use anyhow::{Context, Result}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -23,6 +23,10 @@ pub struct X11Manager { } impl X11Manager { + fn x11_id(window_id: WindowId) -> Result { + u32::try_from(window_id).context("Window ID exceeds the X11 32-bit range") + } + pub fn new() -> Result { let (conn, screen_num) = RustConnection::connect(None).context("Failed to connect to X11 server")?; @@ -123,7 +127,7 @@ impl X11Manager { continue; } eve_windows.push(EveWindow { - id: window, + id: window.into(), title: title.trim_start_matches("EVE - ").to_string(), }); } @@ -192,6 +196,7 @@ impl X11Manager { height: u32, ) -> Result<()> { for window in windows { + let window_id = Self::x11_id(window.id)?; // Move and resize window let values = ConfigureWindowAux::new() .x(x) @@ -199,7 +204,7 @@ impl X11Manager { .width(width) .height(height); - self.conn.configure_window(window.id, &values)?; + self.conn.configure_window(window_id, &values)?; } self.conn.flush()?; @@ -283,8 +288,8 @@ impl WindowManager for X11Manager { self.get_eve_windows() } - fn activate_window(&self, window_id: u32) -> Result<()> { - self.activate_window(window_id) + fn activate_window(&self, window_id: WindowId) -> Result<()> { + self.activate_window(Self::x11_id(window_id)?) } fn stack_windows(&self, windows: &[EveWindow], config: &Config) -> Result<()> { @@ -296,15 +301,15 @@ impl WindowManager for X11Manager { self.stack_windows_internal(windows, x, y, width, height) } - fn get_active_window(&self) -> Result { - self.get_active_window() + fn get_active_window(&self) -> Result { + self.get_active_window().map(Into::into) } - fn minimize_window(&self, window_id: u32) -> Result<()> { - self.minimize_window(window_id) + fn minimize_window(&self, window_id: WindowId) -> Result<()> { + self.minimize_window(Self::x11_id(window_id)?) } - fn restore_window(&self, window_id: u32) -> Result<()> { - self.restore_window(window_id) + fn restore_window(&self, window_id: WindowId) -> Result<()> { + self.restore_window(Self::x11_id(window_id)?) } }