diff --git a/src/main.rs b/src/main.rs index 61125dc..1eedfbe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use std::collections::HashSet; use std::error::Error; use clap::Parser; @@ -103,23 +104,30 @@ impl X11 { .copied() .unwrap_or(0); - // Try to get active window from _NET_ACTIVE_WINDOW + let client_ids = self.property_u32(self.root, self.atoms.net_client_list)?; + let client_set: HashSet = client_ids.iter().copied().collect(); + + // Try _NET_ACTIVE_WINDOW first, resolving the tree if the reported + // window is not a managed client (e.g. a transient helper window). let active_id = self .property_u32(self.root, self.atoms.net_active_window)? .first() .copied(); - // Get the window with keyboard focus as a fallback - let focused_window = self.get_focused_window().ok().flatten(); - - // Use the focused window if _NET_ACTIVE_WINDOW is 0 or invalid - let effective_active_id = if active_id == Some(0) || active_id.is_none() { - focused_window - } else { - active_id + let resolved_active = match active_id { + Some(0) | None => None, + Some(id) if client_set.contains(&id) => Some(id), + Some(id) => self + .resolve_to_client_window(x::Window::new(id))? + .map(|w| w.resource_id()) + .filter(|rid| client_set.contains(rid)), }; - let client_ids = self.property_u32(self.root, self.atoms.net_client_list)?; + // Fall back to X11 keyboard focus. + let focused_window = self.get_focused_window().ok().flatten(); + + let effective_active_id = + resolved_active.or_else(|| focused_window.filter(|id| client_set.contains(id))); let mut workspaces: Vec = (0..workspace_count) .map(|id| Workspace { @@ -169,13 +177,14 @@ impl X11 { workspace.windows.push(window); } - // If we still don't have an active window, try to find the focused window - if active_window.id == 0 { - if let Some(id) = effective_active_id { - if let Ok(window) = self.window_info(id) { - active_window = window; - } - } + // If we still don't have an active window, try to resolve the + // effective ID directly (it might not be in the client list we + // iterated above due to ordering). + if active_window.id == 0 + && let Some(id) = effective_active_id + && let Ok(window) = self.window_info(id) + { + active_window = window; } Ok(State { @@ -188,7 +197,7 @@ impl X11 { let cookie = self.conn.send_request(&x::GetInputFocus {}); let reply = self.conn.wait_for_reply(cookie)?; - let focus = reply.focus(); + let mut focus = reply.focus(); // If focus is the root window or PointerRoot, there's no focused window if focus == self.root { @@ -198,32 +207,75 @@ impl X11 { // Check for PointerRoot (INPUTFOCUS_POINTER_ROOT is defined as Window with res_id 1) if focus.resource_id() == 1 { // Get the window under the pointer - let cookie = self.conn.send_request(&x::QueryPointer { - window: self.root, - }); + let cookie = self + .conn + .send_request(&x::QueryPointer { window: self.root }); let reply = self.conn.wait_for_reply(cookie)?; let child = reply.child(); - + if child == x::Window::new(0) { return Ok(None); } - return Ok(Some(child.resource_id())); + focus = child; } if focus == x::Window::new(0) { return Ok(None); } - Ok(Some(focus.resource_id())) + // Walk up the tree until we find a window with WM_CLASS set, + // which indicates it's a real managed client window. + let resolved = self.resolve_to_client_window(focus)?; + + Ok(resolved.map(|w| w.resource_id())) + } + + /// Walk up the X11 window tree starting at `window` until a window with + /// a `WM_CLASS` property is found. Returns `None` if no such ancestor + /// exists (e.g. because we hit the root window). + fn resolve_to_client_window( + &self, + mut window: x::Window, + ) -> Result, Box> { + // Limit hops to avoid infinite loops on malformed hierarchies. + for _ in 0..32 { + if window == self.root || window == x::Window::new(0) { + return Ok(None); + } + + // If this window has a non-empty WM_CLASS, it's a managed client. + if let Ok(Some(bytes)) = + self.property_bytes_opt(window, self.atoms.wm_class, x::ATOM_STRING) + && !bytes.is_empty() + { + return Ok(Some(window)); + } + + // Move to parent. + let cookie = self.conn.send_request(&x::QueryTree { window }); + let Ok(reply) = self.conn.wait_for_reply(cookie) else { + return Ok(None); + }; + + let parent = reply.parent(); + if parent == window { + // Reached the top without finding a client window. + return Ok(None); + } + window = parent; + } + + Ok(None) } fn is_dock_window(&self, id: u32) -> bool { let window = x::Window::new(id); // Check if it's a dock type window - if let Ok(Some(window_types)) = self.property_u32_opt(window, self.atoms.net_wm_window_type) { + if let Ok(Some(window_types)) = self.property_u32_opt(window, self.atoms.net_wm_window_type) + { let dock = self.atom_to_u32(self.atoms.net_wm_window_type_dock); - + for window_type in window_types { if window_type == dock { return true; @@ -234,7 +286,7 @@ impl X11 { // Check if it has sticky state (common for docks/panels) if let Ok(Some(states)) = self.property_u32_opt(window, self.atoms.net_wm_state) { let sticky = self.atom_to_u32(self.atoms.net_wm_state_sticky); - + for state in states { if state == sticky { return true; @@ -341,7 +393,9 @@ impl X11 { fn watch(&self) -> Result<(), Box> { self.select_events( self.root, - x::EventMask::PROPERTY_CHANGE | x::EventMask::SUBSTRUCTURE_NOTIFY | x::EventMask::FOCUS_CHANGE, + x::EventMask::PROPERTY_CHANGE + | x::EventMask::SUBSTRUCTURE_NOTIFY + | x::EventMask::FOCUS_CHANGE, ); let client_ids = self.property_u32(self.root, self.atoms.net_client_list)?; @@ -384,19 +438,15 @@ impl X11 { } xcb::Event::X( - x::Event::DestroyNotify(_) | x::Event::MapNotify(_) | x::Event::UnmapNotify(_), + x::Event::DestroyNotify(_) + | x::Event::MapNotify(_) + | x::Event::UnmapNotify(_) + | x::Event::FocusIn(_) + | x::Event::FocusOut(_), ) => { return Ok(()); } - xcb::Event::X(x::Event::FocusIn(_)) => { - return Ok(()); - } - - xcb::Event::X(x::Event::FocusOut(_)) => { - return Ok(()); - } - _ => {} } } @@ -454,4 +504,4 @@ fn main() -> Result<(), Box> { x11.wait_for_relevant_event()?; } -} \ No newline at end of file +}