diff --git a/Cargo.toml b/Cargo.toml index ea23a88..d83b897 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xdi" -version = "1.0.0" +version = "1.0.1" edition = "2024" description = "X11 Desktop Information" diff --git a/src/main.rs b/src/main.rs index 64b4c4d..2508527 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,11 +38,16 @@ xcb::atoms_struct! { net_number_of_desktops => b"_NET_NUMBER_OF_DESKTOPS", net_current_desktop => b"_NET_CURRENT_DESKTOP", net_client_list => b"_NET_CLIENT_LIST", + net_client_list_stacking => b"_NET_CLIENT_LIST_STACKING", net_wm_desktop => b"_NET_WM_DESKTOP", net_active_window => b"_NET_ACTIVE_WINDOW", net_wm_name => b"_NET_WM_NAME", utf8_string => b"UTF8_STRING", wm_class => b"WM_CLASS", + wm_state => b"WM_STATE", + 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", } } @@ -87,6 +92,11 @@ impl X11 { .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(); + let client_ids = self.property_u32(self.root, self.atoms.net_client_list)?; let mut workspaces: Vec = (0..workspace_count) @@ -98,14 +108,18 @@ impl X11 { .collect(); let mut active_window = None; + let mut windows_on_current = Vec::new(); for id in client_ids { - let window = self.window_info(id)?; + // Try to get window info, skip if it fails (window might be invalid) + let Ok(window) = self.window_info(id) else { + continue; + }; + // Try to get desktop property, skip if it fails let desktop = self - .property_u32(x::Window::new(id), self.atoms.net_wm_desktop)? - .first() - .copied(); + .property_u32_opt(x::Window::new(id), self.atoms.net_wm_desktop)? + .and_then(|v| v.first().copied()); let Some(desktop) = desktop else { continue; @@ -121,6 +135,10 @@ impl X11 { continue; }; + if desktop == current { + windows_on_current.push(id); + } + if active_id == Some(id) { active_window = Some(window.clone()); } @@ -128,12 +146,50 @@ impl X11 { workspace.windows.push(window); } - // Keep the active window available even if its desktop could - // not be resolved. - if active_window.is_none() - && let Some(id) = active_id - { - active_window = Some(self.window_info(id)?); + // 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 + { + active_window = self.window_info(id).ok(); + } } Ok(State { @@ -146,11 +202,11 @@ impl X11 { let window = x::Window::new(id); let name = self - .property_bytes(window, self.atoms.net_wm_name, self.atoms.utf8_string)? + .property_bytes_opt(window, self.atoms.net_wm_name, self.atoms.utf8_string)? .and_then(|bytes| String::from_utf8(bytes).ok()); let class = self - .property_bytes(window, self.atoms.wm_class, x::ATOM_STRING)? + .property_bytes_opt(window, self.atoms.wm_class, x::ATOM_STRING)? .map(|bytes| parse_wm_class(&bytes)); Ok(WindowInfo { id, name, class }) @@ -175,7 +231,33 @@ impl X11 { Ok(reply.value::().to_vec()) } - fn property_bytes( + fn property_u32_opt( + &self, + window: x::Window, + property: x::Atom, + ) -> Result>, Box> { + let cookie = self.conn.send_request(&x::GetProperty { + delete: false, + window, + property, + r#type: x::ATOM_ANY, + long_offset: 0, + long_length: u32::MAX, + }); + + let Ok(reply) = self.conn.wait_for_reply(cookie) else { + return Ok(None); + }; + + let value = reply.value::(); + if value.is_empty() { + return Ok(None); + } + + Ok(Some(value.to_vec())) + } + + fn property_bytes_opt( &self, window: x::Window, property: x::Atom, @@ -190,7 +272,9 @@ impl X11 { long_length: u32::MAX, }); - let reply = self.conn.wait_for_reply(cookie)?; + let Ok(reply) = self.conn.wait_for_reply(cookie) else { + return Ok(None); + }; let value = reply.value::(); @@ -205,12 +289,13 @@ impl X11 { self.select_events( self.root, x::EventMask::PROPERTY_CHANGE | x::EventMask::SUBSTRUCTURE_NOTIFY, - )?; + ); let client_ids = self.property_u32(self.root, self.atoms.net_client_list)?; for id in client_ids { - self.select_events(x::Window::new(id), x::EventMask::PROPERTY_CHANGE)?; + // Ignore errors when selecting events on windows that might be gone + let () = self.select_events(x::Window::new(id), x::EventMask::PROPERTY_CHANGE); } self.conn.flush()?; @@ -218,19 +303,14 @@ impl X11 { Ok(()) } - fn select_events( - &self, - window: x::Window, - event_mask: x::EventMask, - ) -> Result<(), Box> { + fn select_events(&self, window: x::Window, event_mask: x::EventMask) { let cookie = self.conn.send_request_checked(&x::ChangeWindowAttributes { window, value_list: &[x::Cw::EventMask(event_mask)], }); - self.conn.check_request(cookie)?; - - Ok(()) + // Ignore errors for windows that might be gone + let _ = self.conn.check_request(cookie); } fn wait_for_relevant_event(&self) -> Result<(), Box> { @@ -245,10 +325,8 @@ impl X11 { } xcb::Event::X(x::Event::CreateNotify(event)) => { - let _ = self.select_events(event.window(), x::EventMask::PROPERTY_CHANGE); - + let () = self.select_events(event.window(), x::EventMask::PROPERTY_CHANGE); self.conn.flush()?; - return Ok(()); } @@ -267,6 +345,7 @@ impl X11 { 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