From 8212f4d89c17c2555f848cbbbc84c64c5a01d674 Mon Sep 17 00:00:00 2001 From: candifloss Date: Thu, 3 Sep 2026 12:12:01 +0530 Subject: [PATCH] Improve active window determination --- .gitignore | 1 + src/main.rs | 180 +++++++++++++++++++++++++++++++++------------------- 2 files changed, 116 insertions(+), 65 deletions(-) diff --git a/.gitignore b/.gitignore index 620f8f6..068c7e1 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ Cargo.lock # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +/test/ # Added by cargo diff --git a/src/main.rs b/src/main.rs index 2508527..d5cfa35 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use std::error::Error; use clap::Parser; use serde::Serialize; -use xcb::{XidNew, x}; +use xcb::{Xid, XidNew, x}; #[derive(Debug, Parser)] #[command(name = "xdi", about = "Print X11 desktop information as JSON")] @@ -44,10 +44,16 @@ xcb::atoms_struct! { net_wm_name => b"_NET_WM_NAME", utf8_string => b"UTF8_STRING", wm_class => b"WM_CLASS", - wm_state => b"WM_STATE", + net_wm_window_type => b"_NET_WM_WINDOW_TYPE", + net_wm_window_type_dock => b"_NET_WM_WINDOW_TYPE_DOCK", + net_wm_window_type_desktop => b"_NET_WM_WINDOW_TYPE_DESKTOP", + net_wm_window_type_toolbar => b"_NET_WM_WINDOW_TYPE_TOOLBAR", + net_wm_window_type_menu => b"_NET_WM_WINDOW_TYPE_MENU", + net_wm_window_type_utility => b"_NET_WM_WINDOW_TYPE_UTILITY", net_wm_state => b"_NET_WM_STATE", - net_wm_state_above => b"_NET_WM_STATE_ABOVE", net_wm_state_sticky => b"_NET_WM_STATE_STICKY", + net_wm_state_above => b"_NET_WM_STATE_ABOVE", + wm_state => b"WM_STATE", } } @@ -87,15 +93,21 @@ impl X11 { .copied() .unwrap_or(0); + // Try to get active window from _NET_ACTIVE_WINDOW let active_id = self .property_u32(self.root, self.atoms.net_active_window)? .first() .copied(); - // Try to get stacking order to determine active window - let stacking_ids = self - .property_u32(self.root, self.atoms.net_client_list_stacking) - .unwrap_or_default(); + // 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 client_ids = self.property_u32(self.root, self.atoms.net_client_list)?; @@ -108,9 +120,13 @@ impl X11 { .collect(); let mut active_window = None; - let mut windows_on_current = Vec::new(); for id in client_ids { + // Skip dock/panel windows + if self.is_dock_window(id) { + continue; + } + // Try to get window info, skip if it fails (window might be invalid) let Ok(window) = self.window_info(id) else { continue; @@ -135,59 +151,17 @@ impl X11 { continue; }; - if desktop == current { - windows_on_current.push(id); - } - - if active_id == Some(id) { + // Check if this is the active window + if effective_active_id == Some(id) { active_window = Some(window.clone()); } workspace.windows.push(window); } - // If _NET_ACTIVE_WINDOW is 0 or not set, try to determine active window - // from the stacking order on the current workspace - if active_window.is_none() || active_id == Some(0) { - // Find the topmost window on the current workspace - let mut current_workspace_windows: Vec<_> = windows_on_current - .iter() - .filter(|id| { - // Check if this window is on the current workspace - self.property_u32_opt(x::Window::new(**id), self.atoms.net_wm_desktop) - .ok() - .flatten() - .and_then(|v| v.first().copied()) - == Some(current) - }) - .collect(); - - // Sort by stacking order (if available) - if !stacking_ids.is_empty() { - current_workspace_windows.sort_by_key(|id| { - stacking_ids - .iter() - .position(|&stack_id| stack_id == **id) - .unwrap_or(usize::MAX) - }); - // The last one in stacking order is on top - if let Some(&top_id) = current_workspace_windows.last() - && let Ok(info) = self.window_info(*top_id) - { - active_window = Some(info); - } - } else if let Some(&top_id) = current_workspace_windows.last() { - // If no stacking info, just use the last found - if let Ok(info) = self.window_info(*top_id) { - active_window = Some(info); - } - } - - // If we still don't have an active window, try the old method - if active_window.is_none() - && let Some(id) = active_id - && id != 0 - { + // If we still don't have an active window, try to find the focused window + if active_window.is_none() { + if let Some(id) = effective_active_id { active_window = self.window_info(id).ok(); } } @@ -198,6 +172,71 @@ impl X11 { }) } + fn get_focused_window(&self) -> Result, Box> { + let cookie = self.conn.send_request(&x::GetInputFocus {}); + + let reply = self.conn.wait_for_reply(cookie)?; + let focus = reply.focus(); + + // If focus is the root window or PointerRoot, there's no focused window + if focus == self.root { + return Ok(None); + } + + // 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 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())); + } + + if focus == x::Window::new(0) { + return Ok(None); + } + + Ok(Some(focus.resource_id())) + } + + 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) { + 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; + } + } + } + + // 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; + } + } + } + + false + } + + fn atom_to_u32(&self, atom: x::Atom) -> u32 { + atom.resource_id() + } + fn window_info(&self, id: u32) -> Result> { let window = x::Window::new(id); @@ -288,7 +327,7 @@ impl X11 { fn watch(&self) -> Result<(), Box> { self.select_events( self.root, - x::EventMask::PROPERTY_CHANGE | x::EventMask::SUBSTRUCTURE_NOTIFY, + 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)?; @@ -336,20 +375,31 @@ impl X11 { return Ok(()); } + xcb::Event::X(x::Event::FocusIn(_)) => { + return Ok(()); + } + + xcb::Event::X(x::Event::FocusOut(_)) => { + return Ok(()); + } + _ => {} } } } fn is_relevant_property(&self, atom: x::Atom) -> bool { - atom == self.atoms.net_number_of_desktops - || atom == self.atoms.net_current_desktop - || atom == self.atoms.net_client_list - || atom == self.atoms.net_client_list_stacking - || atom == self.atoms.net_wm_desktop - || atom == self.atoms.net_active_window - || atom == self.atoms.net_wm_name - || atom == self.atoms.wm_class + let atom_u32 = self.atom_to_u32(atom); + atom_u32 == self.atom_to_u32(self.atoms.net_number_of_desktops) + || atom_u32 == self.atom_to_u32(self.atoms.net_current_desktop) + || atom_u32 == self.atom_to_u32(self.atoms.net_client_list) + || atom_u32 == self.atom_to_u32(self.atoms.net_client_list_stacking) + || atom_u32 == self.atom_to_u32(self.atoms.net_wm_desktop) + || atom_u32 == self.atom_to_u32(self.atoms.net_active_window) + || atom_u32 == self.atom_to_u32(self.atoms.net_wm_name) + || atom_u32 == self.atom_to_u32(self.atoms.wm_class) + || atom_u32 == self.atom_to_u32(self.atoms.net_wm_window_type) + || atom_u32 == self.atom_to_u32(self.atoms.net_wm_state) } } @@ -390,4 +440,4 @@ fn main() -> Result<(), Box> { x11.wait_for_relevant_event()?; } -} +} \ No newline at end of file