Compare commits

..

No commits in common. "main" and "v1.0.2" have entirely different histories.
main ... v1.0.2

2 changed files with 44 additions and 108 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "xdi" name = "xdi"
version = "1.0.3" version = "1.0.2"
edition = "2024" edition = "2024"
description = "X11 Desktop Information" description = "X11 Desktop Information"

View File

@ -1,4 +1,3 @@
use std::collections::HashSet;
use std::error::Error; use std::error::Error;
use clap::Parser; use clap::Parser;
@ -15,7 +14,7 @@ struct Args {
#[derive(Debug, Clone, Serialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
struct State { struct State {
active_window: WindowInfo, active_window: Option<WindowInfo>,
workspaces: Vec<Workspace>, workspaces: Vec<Workspace>,
} }
@ -29,18 +28,8 @@ struct Workspace {
#[derive(Debug, Clone, Serialize, PartialEq, Eq)] #[derive(Debug, Clone, Serialize, PartialEq, Eq)]
struct WindowInfo { struct WindowInfo {
id: u32, id: u32,
name: String, name: Option<String>,
class: String, class: Option<String>,
}
impl WindowInfo {
fn placeholder() -> Self {
Self {
id: 0,
name: String::new(),
class: String::new(),
}
}
} }
xcb::atoms_struct! { xcb::atoms_struct! {
@ -104,30 +93,23 @@ impl X11 {
.copied() .copied()
.unwrap_or(0); .unwrap_or(0);
let client_ids = self.property_u32(self.root, self.atoms.net_client_list)?; // Try to get active window from _NET_ACTIVE_WINDOW
let client_set: HashSet<u32> = 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 let active_id = self
.property_u32(self.root, self.atoms.net_active_window)? .property_u32(self.root, self.atoms.net_active_window)?
.first() .first()
.copied(); .copied();
let resolved_active = match active_id { // Get the window with keyboard focus as a fallback
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)),
};
// Fall back to X11 keyboard focus.
let focused_window = self.get_focused_window().ok().flatten(); let focused_window = self.get_focused_window().ok().flatten();
let effective_active_id = // Use the focused window if _NET_ACTIVE_WINDOW is 0 or invalid
resolved_active.or_else(|| focused_window.filter(|id| client_set.contains(id))); 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)?;
let mut workspaces: Vec<Workspace> = (0..workspace_count) let mut workspaces: Vec<Workspace> = (0..workspace_count)
.map(|id| Workspace { .map(|id| Workspace {
@ -137,7 +119,7 @@ impl X11 {
}) })
.collect(); .collect();
let mut active_window = WindowInfo::placeholder(); let mut active_window = None;
for id in client_ids { for id in client_ids {
// Skip dock/panel windows // Skip dock/panel windows
@ -171,20 +153,17 @@ impl X11 {
// Check if this is the active window // Check if this is the active window
if effective_active_id == Some(id) { if effective_active_id == Some(id) {
active_window = window.clone(); active_window = Some(window.clone());
} }
workspace.windows.push(window); workspace.windows.push(window);
} }
// If we still don't have an active window, try to resolve the // If we still don't have an active window, try to find the focused window
// effective ID directly (it might not be in the client list we if active_window.is_none() {
// iterated above due to ordering). if let Some(id) = effective_active_id {
if active_window.id == 0 active_window = self.window_info(id).ok();
&& let Some(id) = effective_active_id }
&& let Ok(window) = self.window_info(id)
{
active_window = window;
} }
Ok(State { Ok(State {
@ -197,7 +176,7 @@ impl X11 {
let cookie = self.conn.send_request(&x::GetInputFocus {}); let cookie = self.conn.send_request(&x::GetInputFocus {});
let reply = self.conn.wait_for_reply(cookie)?; let reply = self.conn.wait_for_reply(cookie)?;
let mut focus = reply.focus(); let focus = reply.focus();
// If focus is the root window or PointerRoot, there's no focused window // If focus is the root window or PointerRoot, there's no focused window
if focus == self.root { if focus == self.root {
@ -207,75 +186,32 @@ impl X11 {
// Check for PointerRoot (INPUTFOCUS_POINTER_ROOT is defined as Window with res_id 1) // Check for PointerRoot (INPUTFOCUS_POINTER_ROOT is defined as Window with res_id 1)
if focus.resource_id() == 1 { if focus.resource_id() == 1 {
// Get the window under the pointer // Get the window under the pointer
let cookie = self let cookie = self.conn.send_request(&x::QueryPointer {
.conn window: self.root,
.send_request(&x::QueryPointer { window: self.root }); });
let reply = self.conn.wait_for_reply(cookie)?; let reply = self.conn.wait_for_reply(cookie)?;
let child = reply.child(); let child = reply.child();
if child == x::Window::new(0) { if child == x::Window::new(0) {
return Ok(None); return Ok(None);
} }
focus = child; return Ok(Some(child.resource_id()));
} }
if focus == x::Window::new(0) { if focus == x::Window::new(0) {
return Ok(None); return Ok(None);
} }
// Walk up the tree until we find a window with WM_CLASS set, Ok(Some(focus.resource_id()))
// 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<Option<x::Window>, Box<dyn Error>> {
// 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 { fn is_dock_window(&self, id: u32) -> bool {
let window = x::Window::new(id); let window = x::Window::new(id);
// Check if it's a dock type window // 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); let dock = self.atom_to_u32(self.atoms.net_wm_window_type_dock);
for window_type in window_types { for window_type in window_types {
if window_type == dock { if window_type == dock {
return true; return true;
@ -286,7 +222,7 @@ impl X11 {
// Check if it has sticky state (common for docks/panels) // 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) { 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); let sticky = self.atom_to_u32(self.atoms.net_wm_state_sticky);
for state in states { for state in states {
if state == sticky { if state == sticky {
return true; return true;
@ -306,13 +242,11 @@ impl X11 {
let name = self let name = self
.property_bytes_opt(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()) .and_then(|bytes| String::from_utf8(bytes).ok());
.unwrap_or_default();
let class = self let class = self
.property_bytes_opt(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)) .map(|bytes| parse_wm_class(&bytes));
.unwrap_or_default();
Ok(WindowInfo { id, name, class }) Ok(WindowInfo { id, name, class })
} }
@ -393,9 +327,7 @@ impl X11 {
fn watch(&self) -> Result<(), Box<dyn Error>> { fn watch(&self) -> Result<(), Box<dyn Error>> {
self.select_events( self.select_events(
self.root, self.root,
x::EventMask::PROPERTY_CHANGE x::EventMask::PROPERTY_CHANGE | x::EventMask::SUBSTRUCTURE_NOTIFY | x::EventMask::FOCUS_CHANGE,
| x::EventMask::SUBSTRUCTURE_NOTIFY
| x::EventMask::FOCUS_CHANGE,
); );
let client_ids = self.property_u32(self.root, self.atoms.net_client_list)?; let client_ids = self.property_u32(self.root, self.atoms.net_client_list)?;
@ -438,15 +370,19 @@ impl X11 {
} }
xcb::Event::X( xcb::Event::X(
x::Event::DestroyNotify(_) x::Event::DestroyNotify(_) | x::Event::MapNotify(_) | x::Event::UnmapNotify(_),
| x::Event::MapNotify(_)
| x::Event::UnmapNotify(_)
| x::Event::FocusIn(_)
| x::Event::FocusOut(_),
) => { ) => {
return Ok(()); return Ok(());
} }
xcb::Event::X(x::Event::FocusIn(_)) => {
return Ok(());
}
xcb::Event::X(x::Event::FocusOut(_)) => {
return Ok(());
}
_ => {} _ => {}
} }
} }
@ -504,4 +440,4 @@ fn main() -> Result<(), Box<dyn Error>> {
x11.wait_for_relevant_event()?; x11.wait_for_relevant_event()?;
} }
} }