Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e84799a92b | |||
| bbfd77880d | |||
| 987e07982e |
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "xdi"
|
name = "xdi"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "X11 Desktop Information"
|
description = "X11 Desktop Information"
|
||||||
|
|
||||||
|
|||||||
142
src/main.rs
142
src/main.rs
@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
@ -14,7 +15,7 @@ struct Args {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||||
struct State {
|
struct State {
|
||||||
active_window: Option<WindowInfo>,
|
active_window: WindowInfo,
|
||||||
workspaces: Vec<Workspace>,
|
workspaces: Vec<Workspace>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,8 +29,18 @@ struct Workspace {
|
|||||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||||
struct WindowInfo {
|
struct WindowInfo {
|
||||||
id: u32,
|
id: u32,
|
||||||
name: Option<String>,
|
name: String,
|
||||||
class: Option<String>,
|
class: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowInfo {
|
||||||
|
fn placeholder() -> Self {
|
||||||
|
Self {
|
||||||
|
id: 0,
|
||||||
|
name: String::new(),
|
||||||
|
class: String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
xcb::atoms_struct! {
|
xcb::atoms_struct! {
|
||||||
@ -93,23 +104,30 @@ impl X11 {
|
|||||||
.copied()
|
.copied()
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
// Try to get active window from _NET_ACTIVE_WINDOW
|
let client_ids = self.property_u32(self.root, self.atoms.net_client_list)?;
|
||||||
|
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();
|
||||||
|
|
||||||
// Get the window with keyboard focus as a fallback
|
let resolved_active = match active_id {
|
||||||
let focused_window = self.get_focused_window().ok().flatten();
|
Some(0) | None => None,
|
||||||
|
Some(id) if client_set.contains(&id) => Some(id),
|
||||||
// Use the focused window if _NET_ACTIVE_WINDOW is 0 or invalid
|
Some(id) => self
|
||||||
let effective_active_id = if active_id == Some(0) || active_id.is_none() {
|
.resolve_to_client_window(x::Window::new(id))?
|
||||||
focused_window
|
.map(|w| w.resource_id())
|
||||||
} else {
|
.filter(|rid| client_set.contains(rid)),
|
||||||
active_id
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let client_ids = self.property_u32(self.root, self.atoms.net_client_list)?;
|
// Fall back to X11 keyboard focus.
|
||||||
|
let focused_window = self.get_focused_window().ok().flatten();
|
||||||
|
|
||||||
|
let effective_active_id =
|
||||||
|
resolved_active.or_else(|| focused_window.filter(|id| client_set.contains(id)));
|
||||||
|
|
||||||
let mut workspaces: Vec<Workspace> = (0..workspace_count)
|
let mut workspaces: Vec<Workspace> = (0..workspace_count)
|
||||||
.map(|id| Workspace {
|
.map(|id| Workspace {
|
||||||
@ -119,7 +137,7 @@ impl X11 {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut active_window = None;
|
let mut active_window = WindowInfo::placeholder();
|
||||||
|
|
||||||
for id in client_ids {
|
for id in client_ids {
|
||||||
// Skip dock/panel windows
|
// Skip dock/panel windows
|
||||||
@ -153,17 +171,20 @@ 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 = Some(window.clone());
|
active_window = window.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
workspace.windows.push(window);
|
workspace.windows.push(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we still don't have an active window, try to find the focused window
|
// If we still don't have an active window, try to resolve the
|
||||||
if active_window.is_none() {
|
// effective ID directly (it might not be in the client list we
|
||||||
if let Some(id) = effective_active_id {
|
// iterated above due to ordering).
|
||||||
active_window = self.window_info(id).ok();
|
if active_window.id == 0
|
||||||
}
|
&& let Some(id) = effective_active_id
|
||||||
|
&& let Ok(window) = self.window_info(id)
|
||||||
|
{
|
||||||
|
active_window = window;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(State {
|
Ok(State {
|
||||||
@ -176,7 +197,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 focus = reply.focus();
|
let mut 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 {
|
||||||
@ -186,30 +207,73 @@ 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.conn.send_request(&x::QueryPointer {
|
let cookie = self
|
||||||
window: self.root,
|
.conn
|
||||||
});
|
.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);
|
||||||
}
|
}
|
||||||
return Ok(Some(child.resource_id()));
|
focus = child;
|
||||||
}
|
}
|
||||||
|
|
||||||
if focus == x::Window::new(0) {
|
if focus == x::Window::new(0) {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some(focus.resource_id()))
|
// Walk up the tree until we find a window with WM_CLASS set,
|
||||||
|
// 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 {
|
||||||
@ -242,11 +306,13 @@ 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 })
|
||||||
}
|
}
|
||||||
@ -327,7 +393,9 @@ 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::SUBSTRUCTURE_NOTIFY | x::EventMask::FOCUS_CHANGE,
|
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)?;
|
let client_ids = self.property_u32(self.root, self.atoms.net_client_list)?;
|
||||||
@ -370,19 +438,15 @@ impl X11 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
xcb::Event::X(
|
xcb::Event::X(
|
||||||
x::Event::DestroyNotify(_) | x::Event::MapNotify(_) | x::Event::UnmapNotify(_),
|
x::Event::DestroyNotify(_)
|
||||||
|
| 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(());
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user