From 0f850b7d77c6dfbd25068291b9d19f18b985b604 Mon Sep 17 00:00:00 2001 From: candifloss Date: Mon, 24 Aug 2026 22:39:39 +0530 Subject: [PATCH] Print X11 Desktop Information --- src/main.rs | 315 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 313 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index e7a11a9..64b4c4d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,314 @@ -fn main() { - println!("Hello, world!"); +use std::error::Error; + +use clap::Parser; +use serde::Serialize; +use xcb::{XidNew, x}; + +#[derive(Debug, Parser)] +#[command(name = "xdi", about = "Print X11 desktop information as JSON")] +struct Args { + /// Print a new JSON object whenever the desktop state changes. + #[arg(short, long)] + follow: bool, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +struct State { + active_window: Option, + workspaces: Vec, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +struct Workspace { + id: u32, + current: bool, + windows: Vec, +} + +#[derive(Debug, Clone, Serialize, PartialEq, Eq)] +struct WindowInfo { + id: u32, + name: Option, + class: Option, +} + +xcb::atoms_struct! { + #[derive(Debug, Copy, Clone)] + struct Atoms { + net_number_of_desktops => b"_NET_NUMBER_OF_DESKTOPS", + net_current_desktop => b"_NET_CURRENT_DESKTOP", + net_client_list => b"_NET_CLIENT_LIST", + 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", + } +} + +struct X11 { + conn: xcb::Connection, + root: x::Window, + atoms: Atoms, +} + +impl X11 { + fn connect() -> Result> { + let (conn, screen_num) = xcb::Connection::connect(None)?; + + #[allow(clippy::cast_sign_loss)] + let screen = conn + .get_setup() + .roots() + .nth(screen_num as usize) + .ok_or("X11 screen not found")?; + + let atoms = Atoms::intern_all(&conn)?; + let root = screen.root(); + + Ok(Self { conn, root, atoms }) + } + + fn state(&self) -> Result> { + let workspace_count = self + .property_u32(self.root, self.atoms.net_number_of_desktops)? + .first() + .copied() + .unwrap_or(0); + + let current = self + .property_u32(self.root, self.atoms.net_current_desktop)? + .first() + .copied() + .unwrap_or(0); + + let active_id = self + .property_u32(self.root, self.atoms.net_active_window)? + .first() + .copied(); + + let client_ids = self.property_u32(self.root, self.atoms.net_client_list)?; + + let mut workspaces: Vec = (0..workspace_count) + .map(|id| Workspace { + id, + current: id == current, + windows: Vec::new(), + }) + .collect(); + + let mut active_window = None; + + for id in client_ids { + let window = self.window_info(id)?; + + let desktop = self + .property_u32(x::Window::new(id), self.atoms.net_wm_desktop)? + .first() + .copied(); + + let Some(desktop) = desktop else { + continue; + }; + + // EWMH uses 0xFFFFFFFF for windows that appear on + // every desktop. Do not assign them to a specific workspace. + if desktop == u32::MAX { + continue; + } + + let Some(workspace) = workspaces.get_mut(desktop as usize) else { + continue; + }; + + if active_id == Some(id) { + active_window = Some(window.clone()); + } + + 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)?); + } + + Ok(State { + active_window, + workspaces, + }) + } + + fn window_info(&self, id: u32) -> Result> { + let window = x::Window::new(id); + + let name = self + .property_bytes(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)? + .map(|bytes| parse_wm_class(&bytes)); + + Ok(WindowInfo { id, name, class }) + } + + fn property_u32( + &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 reply = self.conn.wait_for_reply(cookie)?; + + Ok(reply.value::().to_vec()) + } + + fn property_bytes( + &self, + window: x::Window, + property: x::Atom, + property_type: x::Atom, + ) -> Result>, Box> { + let cookie = self.conn.send_request(&x::GetProperty { + delete: false, + window, + property, + r#type: property_type, + long_offset: 0, + long_length: u32::MAX, + }); + + let reply = self.conn.wait_for_reply(cookie)?; + + let value = reply.value::(); + + if value.is_empty() { + return Ok(None); + } + + Ok(Some(value.to_vec())) + } + + fn watch(&self) -> Result<(), Box> { + 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)?; + } + + self.conn.flush()?; + + Ok(()) + } + + fn select_events( + &self, + window: x::Window, + event_mask: x::EventMask, + ) -> Result<(), Box> { + let cookie = self.conn.send_request_checked(&x::ChangeWindowAttributes { + window, + value_list: &[x::Cw::EventMask(event_mask)], + }); + + self.conn.check_request(cookie)?; + + Ok(()) + } + + fn wait_for_relevant_event(&self) -> Result<(), Box> { + loop { + let event = self.conn.wait_for_event()?; + + match event { + xcb::Event::X(x::Event::PropertyNotify(event)) => { + if event.window() == self.root || self.is_relevant_property(event.atom()) { + return Ok(()); + } + } + + xcb::Event::X(x::Event::CreateNotify(event)) => { + let _ = self.select_events(event.window(), x::EventMask::PROPERTY_CHANGE); + + self.conn.flush()?; + + return Ok(()); + } + + xcb::Event::X( + x::Event::DestroyNotify(_) | x::Event::MapNotify(_) | x::Event::UnmapNotify(_), + ) => { + 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_wm_desktop + || atom == self.atoms.net_active_window + || atom == self.atoms.net_wm_name + || atom == self.atoms.wm_class + } +} + +fn parse_wm_class(bytes: &[u8]) -> String { + let mut parts = bytes.split(|byte| *byte == 0); + + let _instance = parts.next(); + let class = parts.next().unwrap_or_default(); + + String::from_utf8_lossy(class).into_owned() +} + +fn print_state(state: &State) -> Result<(), Box> { + println!("{}", serde_json::to_string(state)?); + Ok(()) +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + let x11 = X11::connect()?; + + if !args.follow { + print_state(&x11.state()?)?; + return Ok(()); + } + + x11.watch()?; + + let mut previous = None; + + loop { + let state = x11.state()?; + + if previous.as_ref() != Some(&state) { + print_state(&state)?; + previous = Some(state); + } + + x11.wait_for_relevant_event()?; + } }