Print X11 Desktop Information
This commit is contained in:
parent
b765f38977
commit
0f850b7d77
315
src/main.rs
315
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<WindowInfo>,
|
||||
workspaces: Vec<Workspace>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
struct Workspace {
|
||||
id: u32,
|
||||
current: bool,
|
||||
windows: Vec<WindowInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
struct WindowInfo {
|
||||
id: u32,
|
||||
name: Option<String>,
|
||||
class: Option<String>,
|
||||
}
|
||||
|
||||
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<Self, Box<dyn Error>> {
|
||||
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<State, Box<dyn Error>> {
|
||||
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<Workspace> = (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<WindowInfo, Box<dyn Error>> {
|
||||
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<Vec<u32>, Box<dyn Error>> {
|
||||
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::<u32>().to_vec())
|
||||
}
|
||||
|
||||
fn property_bytes(
|
||||
&self,
|
||||
window: x::Window,
|
||||
property: x::Atom,
|
||||
property_type: x::Atom,
|
||||
) -> Result<Option<Vec<u8>>, Box<dyn Error>> {
|
||||
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::<u8>();
|
||||
|
||||
if value.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(value.to_vec()))
|
||||
}
|
||||
|
||||
fn watch(&self) -> Result<(), Box<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
println!("{}", serde_json::to_string(state)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
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()?;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user