diff --git a/rust/Cargo.toml.in b/rust/Cargo.toml.in index fa66446..18dff41 100644 --- a/rust/Cargo.toml.in +++ b/rust/Cargo.toml.in @@ -4,10 +4,13 @@ version = "0.0.0" # This is a placeholder version edition = "2021" [dependencies] -clap = { version = "3.2", features = ["derive"] } -serde_json = "1.0" -log = "0.4" -simplelog = "0.12" +libwmctl = { version = "0.0.51", optional = true } +serde_json = { version = "1.0", optional = true } +x11rb = { version = "0.13", optional = true } +xcb = { version = "1", optional = true } -[dev-dependencies] -ctor = "0.1" +[features] +default = ["i3", "xcb"] +i3 = ["dep:serde_json"] +xcb = ["dep:xcb"] +wmctl = ["dep:libwmctl", "dep:x11rb"] diff --git a/rust/Makefile b/rust/Makefile index 01a4b52..5a754b0 100644 --- a/rust/Makefile +++ b/rust/Makefile @@ -32,7 +32,7 @@ dist/i3switch: target/x86_64-unknown-linux-gnu/release/i3switch .PHONY: test test: Cargo.toml $(SOURCE_FILES) - cargo test + cargo test --all-features .PHONY: all all: dist/i3switch diff --git a/rust/README.md b/rust/README.md index 7ffbedc..806c754 100644 --- a/rust/README.md +++ b/rust/README.md @@ -43,3 +43,23 @@ or install to rust binary directory (must be in PATH to be usable) with: To build tests just use make. make test + +### Build features + +The project provides option to enable or disable features. By default features that +are known to be stable are enabled, and features that are known to be unstable +are disabled. You can enable or disable features by setting `RUSTFLAGS` environment +variable before running `make`: + + # enable unstable features + RUSTFLAGS="--all-features" \ + make + # enable stable features + RUSTFLAGS="-F i3,xcb --no-default-features" \ + make + +#### Available features: + +- `i3`: i3ipc-based backend for window switching (default) +- `xcb`: xcb-based backend for window switching (default) +- `wmctl`: wmctl-based backend for window switching (non-default) diff --git a/rust/src/backend/backend.rs b/rust/src/backend/backend.rs new file mode 100644 index 0000000..2a003be --- /dev/null +++ b/rust/src/backend/backend.rs @@ -0,0 +1,69 @@ +#[cfg(feature = "i3")] +use crate::backend::i3; +#[cfg(feature = "wmctl")] +use crate::backend::wmctl; +#[cfg(feature = "xcb")] +use crate::backend::xcb; + +use crate::backend::traits::*; +use crate::types::Windows; + +pub enum UsedBackend { + #[cfg(feature = "i3")] + I3(i3::Backend), + #[cfg(feature = "wmctl")] + WmCtl(wmctl::Backend), + #[cfg(feature = "xcb")] + Xcb(xcb::Backend), +} + +pub struct Backend { + used_backend: UsedBackend, +} + +impl Backend { + pub fn new(use_backend: UsedBackend) -> Self { + Self { + used_backend: use_backend, + } + } +} + +impl GetTabs for Backend { + fn get_tabs(&self) -> Result { + match self.used_backend { + #[cfg(feature = "i3")] + UsedBackend::I3(ref i3) => i3.get_tabs(), + #[cfg(feature = "wmctl")] + UsedBackend::WmCtl(ref wmctl) => wmctl.get_tabs(), + #[cfg(feature = "xcb")] + UsedBackend::Xcb(ref xcb) => xcb.get_tabs(), + } + } +} + +impl GetVisible for Backend { + fn get_visible(&self) -> Result { + match self.used_backend { + #[cfg(feature = "i3")] + UsedBackend::I3(ref i3) => i3.get_visible(), + #[cfg(feature = "wmctl")] + UsedBackend::WmCtl(ref wmctl) => wmctl.get_visible(), + #[cfg(feature = "xcb")] + UsedBackend::Xcb(ref xcb) => xcb.get_visible(), + } + } +} + +impl SetFocus for Backend { + fn set_focus(& mut self, id: &u64) { + match self.used_backend { + #[cfg(feature = "i3")] + UsedBackend::I3(ref mut i3) => i3.set_focus(id), + #[cfg(feature = "wmctl")] + UsedBackend::WmCtl(ref mut wmctl) => wmctl.set_focus(id), + #[cfg(feature = "xcb")] + UsedBackend::Xcb(ref mut xcb) => xcb.set_focus(id), + } + } +} diff --git a/rust/src/backend/i3/backend.rs b/rust/src/backend/i3/backend.rs new file mode 100644 index 0000000..48d3a11 --- /dev/null +++ b/rust/src/backend/i3/backend.rs @@ -0,0 +1,60 @@ +use crate::backend::traits::*; +use crate::logging::ResultExt; +use crate::logging; +use crate::types::Windows; +use super::client::{Client, Request}; +use super::compass; + +use serde_json as json; +use std::process; + +pub struct Backend { + client: Client, + root: json::Value, +} + +impl Backend { + pub fn new() -> Self { + // Establish a connection to the i3 IPC server and get the tree structure + let i3_socket_path_output = process::Command::new("i3").arg("--get-socketpath").output() + .expect_log("Failed to get i3 socket path"); + let i3_path = String::from_utf8(i3_socket_path_output.stdout) + .expect_log("Failed to parse i3 socket path output"); + let mut client = Client::new(&i3_path.trim()) + .expect_log("Failed to connect to i3 IPC server"); + let root_string = client.request(Request::GetTree, "") + .expect_log("Failed to get i3 tree JSON"); + + // Parse the i3 tree to get the current workspace and window information + let root = json::from_str::(&root_string) + .expect_log("Failed to parse i3 tree JSON"); + Self { + client, + root, + } + } +} + +impl GetTabs for Backend { + fn get_tabs(&self) -> Result { + let nodes = compass::available_tabs(&self.root); + Ok(compass::to_windows(nodes)) + } +} + +impl GetVisible for Backend { + fn get_visible(&self) -> Result { + let nodes = compass::visible_nodes(&self.root); + Ok(compass::to_windows(nodes)) + } +} + +impl SetFocus for Backend { + fn set_focus(& mut self, window_id: &u64) { + // Focus the window with the determined ID + logging::info!("Focusing window with ID: {}", window_id); + let payload = format!("[con_id={}] focus", window_id); + self.client.request(Request::Command, &payload) + .expect_log("Failed to send focus command"); + } +} diff --git a/rust/src/connection/i3client.rs b/rust/src/backend/i3/client.rs similarity index 100% rename from rust/src/connection/i3client.rs rename to rust/src/backend/i3/client.rs diff --git a/rust/src/converters.rs b/rust/src/backend/i3/compass.rs similarity index 76% rename from rust/src/converters.rs rename to rust/src/backend/i3/compass.rs index c3b45f3..522441b 100644 --- a/rust/src/converters.rs +++ b/rust/src/backend/i3/compass.rs @@ -1,29 +1,7 @@ -/// This file provides functions to convert JSON nodes representing windows in a window manager's -/// tree structure. It includes functions to extract visible nodes, available tabs, floating and -/// tiled windows, and to convert these nodes into a structured format for further processing. -/// It also includes functions to determine the layout of nodes and to find focused windows in -/// the tree structure. -/// -/// This module is part of a window manager's arrangement system, which allows for -/// manipulating and querying the layout of windows in a graphical user interface. -// TODO: Use references instead of cloning values where possible. - use serde_json::Value; - -use crate::linear; -use crate::planar; -use crate::planar::Rect; use crate::logging; -use crate::logging::OptionExt; +use crate::types::{Rect, Window, Windows}; -/// This enum represents a window in a window manager's tree structure. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Window { - pub id: u64, - pub rect: Rect, - pub focused: bool, - pub floating: bool, -} /// This enum represents the layout type of a node in a window manager's tree structure. #[derive(Debug, Clone, PartialEq, Eq)] @@ -34,9 +12,6 @@ enum Layout { Invalid, } -/// A collection of windows, represented as a vector of `Window` structs. -pub type Windows = Vec; - /// Returns a collection of visible nodes in the provided JSON node. /// This function traverses the node structure and collects nodes that are not invisible /// or end nodes. @@ -94,87 +69,35 @@ pub fn available_tabs(node: &Value) -> Vec<&Value> { vec![] } -/// Returns a collection of windows that are floating, i.e., those that are not tiled. -pub fn floating(windows: &Windows) -> Windows { - windows.iter() - .filter(|w| w.floating) - .cloned() - .collect() -} - -/// Returns a collection of windows that are not floating, i.e., those that are tiled. -pub fn tiled(windows: &Windows) -> Windows { - windows.iter() - .filter(|w| !w.floating) - .cloned() - .collect() -} - -/// Returns whether any window in the provided `Windows` is focused. -pub fn any_focused(windows: &Windows) -> bool { - windows.iter().any(|w| w.focused) +impl From<&Value> for Window { + fn from(node: &Value) -> Self { + let id = node["id"].as_u64().unwrap_or(0); + let rect = Rect { + x: node["rect"]["x"].as_i64().unwrap_or(0) as i32, + y: node["rect"]["y"].as_i64().unwrap_or(0) as i32, + w: node["rect"]["width"].as_i64().unwrap_or(0) as i32, + h: node["rect"]["height"].as_i64().unwrap_or(0) as i32, + }; + let floating = node["type"].as_str().map_or(false, |t| t == "floating_con"); + let focused: bool; + if floating { + focused = node["nodes"].get(0).and_then(|n| n["focused"].as_bool()).unwrap_or(false); + } else { + focused = node["focused"].as_bool().unwrap_or(false); + }; + + Window { id, rect, focused, floating, } + } } /// Converts a JSON node to a `Windows`. pub fn to_windows(nodes: Vec<&Value>) -> Windows { nodes.into_iter() .filter(|node| !is_invisible_node(node)) - .map(to_window) + .map(Window::from) .collect() } -/// Converts a JSON node to a `planar::Arrangement`. -/// This function assumes that the node represents a workspace or root node -/// and contains a list of windows. -pub fn as_arrangement(windows: &Windows, relation: planar::Relation) -> planar::Arrangement { - let current = focused_index(windows).unwrap_or(0); - let windows: Vec = windows.iter().map(to_planar).collect(); - planar::Arrangement::new(windows, Some(current), Some(relation)) -} - -/// Converts a collection of `Windows` to a `linear::Sequence`. -/// This function creates a sequence of window IDs and marks the focused window by its index. -pub fn as_sequence(windows: &Windows) -> linear::Sequence { - let focused = focused_index(windows).unwrap_or(0); - return linear::Sequence::new(windows.iter().map(|w| w.id).collect(), focused); -} - -/// Converts a JSON node to a `Window`. -/// This function extracts the window's ID, rectangle dimensions, and focus state -/// from the JSON structure. -fn to_window(node: &Value) -> Window { - let id = node["id"].as_u64().unwrap_or(0); - let rect = Rect { - x: node["rect"]["x"].as_i64().unwrap_or(0) as i32, - y: node["rect"]["y"].as_i64().unwrap_or(0) as i32, - w: node["rect"]["width"].as_i64().unwrap_or(0) as i32, - h: node["rect"]["height"].as_i64().unwrap_or(0) as i32, - }; - let floating = node["type"].as_str().map_or(false, |t| t == "floating_con"); - let focused: bool; - if floating { - focused = node["nodes"].get(0).and_then(|n| n["focused"].as_bool()).unwrap_or(false); - } else { - focused = node["focused"].as_bool().unwrap_or(false); - }; - - Window { id, rect, focused, floating, } -} - -/// Converts a `Window` to a `planar::Window`. -fn to_planar(window: &Window) -> planar::Window { - planar::Window { - id: window.id, - rect: window.rect.clone(), - } -} - -/// Returns the index of the currently focused window, if any. -fn focused_index(windows: &Windows) -> Option { - windows.iter().position(|w| w.focused).wanted( - format!("No focused window found in windows: {:?}", windows).as_str()) -} - /// Checks if a node is an invisible node, which is defined as having a rectangle with zero width /// and height. fn is_invisible_node(node: &Value) -> bool { @@ -263,14 +186,7 @@ fn find_deepest_focused_tabbed(node: &Value) -> Option<&Value> { #[cfg(test)] mod tests { use super::*; - use crate::logging; use serde_json::json; - use ctor::ctor; - - #[ctor] - fn setup() { - logging::init(); - } /// Tests for visible nodes extraction. /// We expect the function to return all nodes that are not invisible. @@ -505,29 +421,6 @@ mod tests { assert!(tabs.iter().any(|tab| tab["id"] == 5)); } - /// Tests for floating and tiled windows. - #[test] - fn test_floating_and_tiled() { - let windows = vec![ - Window { id: 1, rect: Rect {x: 0, y: 0, w: 100, h: 100}, focused: true, floating: false }, - Window { id: 2, rect: Rect {x: 100, y: 100, w: 200, h: 200}, focused: false, floating: true }, - ]; - let floating_windows = floating(&windows); - let tiled_windows = tiled(&windows); - assert_eq!(floating_windows.len(), 1); - assert_eq!(tiled_windows.len(), 1); - } - - /// Tests for checking if any window is focused. - #[test] - fn test_any_focused() { - let windows = vec![ - Window { id: 1, rect: Rect {x: 0, y: 0, w: 100, h: 100}, focused: true, floating: false }, - Window { id: 2, rect: Rect {x: 100, y: 100, w: 200, h: 200}, focused: false, floating: true }, - ]; - assert!(any_focused(&windows)); - } - /// Tests for converting JSON nodes to windows. #[test] fn test_to_windows() { @@ -548,27 +441,6 @@ mod tests { assert_eq!(windows[1].rect, Rect { x: 300, y: 450, w: 15, h: 200 }); } - /// Tests for visible nodes extraction. - #[test] - fn test_as_arrangement() { - let windows = vec![ - Window { id: 1, rect: Rect { x: 0, y: 0, w: 100, h: 100 }, focused: true, floating: false }, - Window { id: 2, rect: Rect { x: 100, y: 100, w: 200, h: 200 }, focused: false, floating: true }, - ]; - let arrangement = as_arrangement(&windows, planar::Relation::Border); - assert_eq!(arrangement.windows.len(), 2); - assert_eq!(arrangement.current, 0); - } - - /// Tests for window conversion. - #[test] - fn test_to_planar() { - let window = Window { id: 1, rect: Rect { x: 0, y: 0, w: 100, h: 100 }, focused: true, floating: false }; - let planar_window = to_planar(&window); - assert_eq!(planar_window.id, 1); - assert_eq!(planar_window.rect, Rect { x: 0, y: 0, w: 100, h: 100 }); - } - /// Tests for layout extraction. /// We expect the function to return the correct layout type based on the "layout" and "type" /// fields. @@ -633,23 +505,6 @@ mod tests { assert_eq!(get_layout(&node), Layout::Invalid); } - /// Tests for focused index extraction. - /// We expect the function to return the index of the first focused window in the provided - #[test] - fn test_focused_index() { - let windows = vec![ - Window { id: 1, rect: Rect { x: 0, y: 0, w: 100, h: 100 }, focused: true, floating: false }, - Window { id: 2, rect: Rect { x: 100, y: 100, w: 200, h: 200 }, focused: false, floating: true }, - ]; - assert_eq!(focused_index(&windows), Some(0)); - - let windows = vec![ - Window { id: 1, rect: Rect { x: 0, y: 0, w: 100, h: 100 }, focused: false, floating: false }, - Window { id: 2, rect: Rect { x: 100, y: 100, w: 200, h: 200 }, focused: false, floating: true }, - ]; - assert_eq!(focused_index(&windows), None); - } - /// Tests for invisible node detection. /// We expect the function to return true for nodes that have a rectangle with zero width and /// height. @@ -766,17 +621,4 @@ mod tests { assert!(find_deepest_focused_tabbed(&node).is_none()); } - /// Tests sequence conversion. - /// We expect the function to return a sequence of window IDs and the index of the focused - /// window. - #[test] - fn test_as_sequence() { - let windows = vec![ - Window { id: 1, rect: Rect { x: 0, y: 0, w: 100, h: 100 }, focused: true, floating: false }, - Window { id: 2, rect: Rect { x: 100, y: 100, w: 200, h: 200 }, focused: false, floating: true }, - ]; - let sequence = as_sequence(&windows); - assert_eq!(sequence[0], 1); - assert_eq!(sequence[1], 2); - } } diff --git a/rust/src/backend/i3/mod.rs b/rust/src/backend/i3/mod.rs new file mode 100644 index 0000000..e7ec11d --- /dev/null +++ b/rust/src/backend/i3/mod.rs @@ -0,0 +1,5 @@ +mod client; +mod compass; +pub mod backend; + +pub use crate::backend::i3::backend::Backend; diff --git a/rust/src/backend/mod.rs b/rust/src/backend/mod.rs new file mode 100644 index 0000000..0660cd3 --- /dev/null +++ b/rust/src/backend/mod.rs @@ -0,0 +1,20 @@ +#[cfg(feature = "i3")] +pub mod i3; +#[cfg(feature = "wmctl")] +pub mod wmctl; +#[cfg(feature = "xcb")] +pub mod xcb; + +pub mod traits; +pub mod backend; + +#[cfg(feature = "i3")] +pub use i3::Backend as I3Backend; +#[cfg(feature = "wmctl")] +pub use wmctl::Backend as WmctlBackend; +#[cfg(feature = "xcb")] +pub use xcb::Backend as XcbBackend; + +pub use backend::Backend; +pub use backend::UsedBackend; +pub use traits::*; diff --git a/rust/src/backend/traits.rs b/rust/src/backend/traits.rs new file mode 100644 index 0000000..b89271c --- /dev/null +++ b/rust/src/backend/traits.rs @@ -0,0 +1,13 @@ +use crate::types::Windows; + +pub trait GetTabs { + fn get_tabs(&self) -> Result; +} + +pub trait GetVisible { + fn get_visible(&self) -> Result; +} + +pub trait SetFocus { + fn set_focus(& mut self, window_id: &u64); +} diff --git a/rust/src/backend/wmctl/backend.rs b/rust/src/backend/wmctl/backend.rs new file mode 100644 index 0000000..b828617 --- /dev/null +++ b/rust/src/backend/wmctl/backend.rs @@ -0,0 +1,133 @@ +use x11rb::connection::Connection; +use x11rb::protocol::xproto::{ConnectionExt, EventMask}; +use x11rb::rust_connection::RustConnection; +use x11rb::protocol::xproto::ClientMessageEvent; + +use libwmctl::prelude::{windows, active, State}; +use crate::backend::traits::*; +use crate::types::{Rect, Window, Windows}; + +pub struct Backend { + windows: Windows, + visibility: Vec, +} + +impl Backend { + pub fn new() -> Self { + let show_hidden = false; + let wm_windows = windows(show_hidden) + .expect("Failed to connect to the window manager"); + + let wm_focused = active().id; + + let mut visibility = Vec::with_capacity(wm_windows.len()); + let windows = wm_windows.iter() + .inspect(|w| { + visibility.push(is_visible(&w.state() + .expect("Failed to get window state"))); + }) + .map(|w| { + let wm_win_geometry = w.geometry() + .expect("Failed to get window geometry"); + let wm_win_states = w.state() + .expect("Failed to get window state"); + let focused = w.id == wm_focused; + let floating = is_floating(&wm_win_states); + Window { + id: w.id as u64, + rect: Rect { + x: wm_win_geometry.0, + y: wm_win_geometry.1, + w: wm_win_geometry.2 as i32, + h: wm_win_geometry.3 as i32, + }, + focused: focused, + floating: floating, + } + }) + .collect::(); + + Self { + windows: windows, + visibility: visibility, + } + } +} + +impl GetTabs for Backend { + fn get_tabs(&self) -> Result { + Err("Tabs not supported in this backend".to_string()) + } +} + +impl GetVisible for Backend { + fn get_visible(&self) -> Result { + Ok(self.windows.iter() + .enumerate() + .filter_map(|(i, window)| { + if self.visibility[i] { + Some(window.clone()) + } else { + None + } + }) + .collect()) + } +} + +impl SetFocus for Backend { + fn set_focus(&mut self, window_id: &u64) { + // Connect to the X server + let (conn, screen_num) = RustConnection::connect(None) + .expect("Failed to connect to the X server"); + let screen = &conn.setup().roots[screen_num]; + + // Get the atom for _NET_ACTIVE_WINDOW + let atom_name = b"_NET_ACTIVE_WINDOW"; + let net_active_window = conn.intern_atom(false, atom_name) + .expect("Failed to intern atom") + .reply() + .expect("Failed to get atom reply") + .atom; + + // Get the atom for _NET_WM_WINDOW_TYPE_NORMAL if needed (not strictly necessary for focusing) + // let net_wm_window_type_normal = conn.intern_atom(false, b"_NET_WM_WINDOW_TYPE_NORMAL")?.reply()?.atom; + + // Construct and send the client message event + let event = ClientMessageEvent { + response_type: 33, // CLIENT_MESSAGE + format: 32, + sequence: 0, + window: *window_id as u32, + type_: net_active_window, + data: x11rb::protocol::xproto::ClientMessageData::from([ + 1, // source indication (1 = application) + x11rb::CURRENT_TIME, + 0, + 0, + 0, + ]), + }; + + conn.send_event( + false, + screen.root, + EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY, + event, + ).expect("Failed to send event"); + conn.flush() + .expect("Failed to flush connection"); + } +} + +fn is_tiled(states: &Vec) -> bool { + states.iter().any(|state| matches!(state, State::MaxHorz | State::MaxVert)) +} + +fn is_floating(states: &Vec) -> bool { + !is_tiled(states) +} + +fn is_visible(states: &Vec) -> bool { + !states.iter().any(|state| matches!(state, State::Hidden)) +} diff --git a/rust/src/backend/wmctl/mod.rs b/rust/src/backend/wmctl/mod.rs new file mode 100644 index 0000000..f6eee67 --- /dev/null +++ b/rust/src/backend/wmctl/mod.rs @@ -0,0 +1,3 @@ +pub mod backend; + +pub use crate::backend::wmctl::backend::Backend; diff --git a/rust/src/backend/xcb/backend.rs b/rust/src/backend/xcb/backend.rs new file mode 100644 index 0000000..767a240 --- /dev/null +++ b/rust/src/backend/xcb/backend.rs @@ -0,0 +1,83 @@ +use super::client::Client; +use crate::types::Windows; +use crate::backend::traits::*; +use xcb::Xid; +use xcb::x::Window as XWindow; +use std::collections::HashMap; +use crate::logging; + +pub struct Backend { + client: Client, + windows: Windows, + xid_map: HashMap, +} + +impl Backend { + pub fn new() -> Self { + // Initialize the X client + let client = Client::new(); + + // Verify required atoms + client.verify_required_atoms() + .expect("Window manager does not support required atoms"); + + // Get the active window + let active_window = client.get_active_window() + .expect("Failed to get active window"); + + // Get the list of windows + let xwindows = client.get_client_list(); + + // Get the full window properties for each window + let mut xid_map: HashMap = HashMap::new(); + let mut windows = xwindows.into_iter() + .filter_map(|xwindow| { + // Fetch window info and add to the windows vector + let window = match client.fetch_window_info(&xwindow) { + Ok(info) => info, + Err(e) => { + logging::error!("Failed to fetch window info for {}: {}", xwindow.resource_id(), e); + return None; + } + }; + xid_map.insert(xwindow.resource_id().into(), xwindow); + Some(window) + }) + .collect::(); + + // Find the focused window + windows.iter_mut().for_each(|window| { + if xid_map[&window.id] == active_window { + window.focused = true; + } + logging::debug!("Window ID: {}, Rect: {}, Floating: {}, Focused: {}", + window.id, window.rect.to_string(), window.floating, window.focused); + }); + + Backend { client, windows, xid_map } + } +} + +impl GetVisible for Backend { + fn get_visible(&self) -> Result { + Ok(self.windows.clone()) + } +} + +impl GetTabs for Backend { + fn get_tabs(&self) -> Result { + Err("Not implemented".to_string()) + } +} + +impl SetFocus for Backend { + fn set_focus(&mut self, window_id: &u64) { + // Check if the window ID exists in the map + if !self.xid_map.contains_key(window_id) { + logging::critical!("Window ID {} does not exist", window_id); + } + // Set focus to the specified window + self.client.set_focus(self.xid_map[window_id]) + .expect("Failed to set focus"); + } +} diff --git a/rust/src/backend/xcb/client.rs b/rust/src/backend/xcb/client.rs new file mode 100644 index 0000000..e80fcc7 --- /dev/null +++ b/rust/src/backend/xcb/client.rs @@ -0,0 +1,272 @@ +use xcb::{x, Connection}; +use xcb::Xid; +use crate::types::Rect; +use crate::types::Window; + +// Helper struct to initialize xcb atoms. +// This helps initialize the atoms taking advantage of xcb concurrency. +xcb::atoms_struct! { + #[derive(Copy, Clone, Debug)] + pub struct Atoms { + pub _net_active_window => b"_NET_ACTIVE_WINDOW", + pub _net_client_list => b"_NET_CLIENT_LIST", + pub _net_supported => b"_NET_SUPPORTED", + pub _net_wm_state => b"_NET_WM_STATE", + pub _net_wm_state_hidden => b"_NET_WM_STATE_HIDDEN", + pub _net_wm_state_maximized_horz => b"_NET_WM_STATE_MAXIMIZED_HORZ", + pub _net_wm_state_maximized_vert => b"_NET_WM_STATE_MAXIMIZED_VERT", + pub wm_state => b"WM_STATE", + pub wm_state_withdrawn => b"WM_STATE_WITHDRAWN", + pub wm_state_normal => b"WM_STATE_NORMAL", + pub wm_state_iconic => b"WM_STATE_ICONIC", + } +} + +pub struct Client { + conn: Connection, + root: x::Window, + atoms: Atoms, +} + +impl Client { + pub fn new() -> Self { + // Connect to the X server + let (conn, screen_num) = Connection::connect(None) + .expect("Failed to connect to X server"); + + // Get the default screen + // Modern setups use a single screen and distribute windows using XRandR + let screen = conn.get_setup().roots().nth(screen_num as usize) + .expect("Failed to get screen"); + + // Get the root window of the screen + let root = screen.root(); + + // Get all required atoms + let atoms = Atoms::intern_all(&conn) + .expect("Failed to intern required atoms"); + + Client { conn, root, atoms } + } + + pub fn verify_required_atoms(&self) -> Result<(), String> { + // Get the _NET_SUPPORTED property from the root window + let cookie = self.conn.send_request(&x::GetProperty { + delete: false, + window: self.root, + property: self.atoms._net_supported, + r#type: x::ATOM_ATOM, + long_offset: 0, + long_length: 1024, // Number of atoms to fetch + }); + let supported_atoms = self.conn.wait_for_reply(cookie) + .expect("Failed to get _NET_SUPPORTED property"); + let supported_atoms = supported_atoms.value::().to_vec(); + + // Check if all required atoms are supported + for atom in [ + &self.atoms._net_active_window, + &self.atoms._net_client_list, + &self.atoms._net_wm_state, + &self.atoms._net_wm_state_maximized_horz, + &self.atoms._net_wm_state_maximized_vert, + &self.atoms._net_wm_state_hidden, + ] { + if !supported_atoms.contains(&atom) { + return Err(format!( + "Required atom '{}' is not supported", + self.get_atom_name(*atom)? + )); + } + } + Ok(()) + } + + pub fn get_client_list(&self) -> Vec { + // Get the list of client windows from the root window + let cookie = self.conn.send_request(&x::GetProperty { + delete: false, + window: self.root, + property: self.atoms._net_client_list, + r#type: x::ATOM_WINDOW, + long_offset: 0, + long_length: 1024, // Number of windows to fetch + }); + + let reply = self.conn.wait_for_reply(cookie) + .expect("Failed to get _NET_CLIENT_LIST property"); + + reply.value::().to_vec() + } + + pub fn get_active_window(&self) -> Result { + // Get the active window from the root window + let cookie = self.conn.send_request(&x::GetProperty { + delete: false, + window: self.root, + property: self.atoms._net_active_window, + r#type: x::ATOM_WINDOW, + long_offset: 0, + long_length: 1, // Only one active window + }); + + let x_windows = self.conn.wait_for_reply(cookie) + .expect("Failed to get _NET_ACTIVE_WINDOW property") + .value::().to_vec(); + match x_windows.first() { + Some(window) => Ok(*window), + None => Err("No active window found".to_string()), + } + } + + pub fn fetch_window_info(&self, window_id: &x::Window) -> Result { + // Request all necessary information about the window + // asynchronously to use xcb properly. + let cookies = ( + self.request_geometry(window_id.clone()), + self.request_normalized_offset(window_id.clone()), + self.request_wm_state(window_id.clone()), + self.request_ewmh_state(window_id.clone()), + ); + + // Wait for all requests to complete + let replies = ( + self.conn.wait_for_reply(cookies.0), + self.conn.wait_for_reply(cookies.1), + self.conn.wait_for_reply(cookies.2), + self.conn.wait_for_reply(cookies.3), + ); + + // Get geometry of the window + let mut rect = match replies.0 { + Ok(reply) => Rect::from(&reply), + Err(err) => return Err(format!("Failed to get window geometry: {}", err)), + }; + + // Get the normalized offset of the window + match replies.1 { + Ok(reply) => translate_rect(&mut rect, &reply), + Err(err) => return Err(format!("Failed to translate coordinates: {}", err)), + }; + + // Match WM state + let wm_state = match replies.2 { + Ok(reply) => reply.value::().to_vec(), + Err(err) => return Err(format!("Failed to get WM state: {}", err)), + }; + + // Match EWMH state + let ewmh_state = match replies.3 { + Ok(reply) => reply.value::().to_vec(), + Err(err) => return Err(format!("Failed to get EWMH state: {}", err)), + }; + + if self.is_hidden(&wm_state, &ewmh_state) { + return Err("Skipping invisible window".to_string()); + } + + Ok(Window { + id: window_id.resource_id().into(), + rect: rect, + floating: self.is_floating(), + focused: false, // Focus state will be set later + }) + } + + pub fn set_focus(&self, window_id: x::Window) -> Result<(), String> { + // Set focus to the specified window + let cookie = self.conn.send_request_checked(&x::SetInputFocus { + focus: window_id, + revert_to: x::InputFocus::None, + time: x::CURRENT_TIME, + }); + + match self.conn.check_request(cookie) { + Ok(_) => Ok(()), + Err(err) => Err(format!("Failed to set focus: {}", err)), + } + } + + fn get_atom_name(&self, atom: x::Atom) -> Result { + // Get the name of an atom + let cookie = self.conn.send_request(&x::GetAtomName { + atom: atom, + }); + match self.conn.wait_for_reply(cookie) { + Ok(reply) => Ok(reply.name().to_string()), + Err(err) => Err(format!("Failed to get atom name: {}", err)), + } + } + + fn request_normalized_offset(&self, src_window: x::Window) + -> x::TranslateCoordinatesCookie { + // Request to translate coordinates from one window to another + self.conn.send_request(&x::TranslateCoordinates { + src_window: src_window, + dst_window: self.root, + src_x: 0, + src_y: 0, + }) + } + + fn request_geometry(&self, window_id: x::Window) + -> x::GetGeometryCookie { + // Request to get the geometry of a window + self.conn.send_request(&x::GetGeometry { + drawable: x::Drawable::Window(window_id), + }) + } + + fn request_wm_state(&self, window_id: x::Window) + -> x::GetPropertyCookie { + // Request to get properties of a window + self.conn.send_request(&x::GetProperty { + delete: false, + window: window_id, + property: self.atoms.wm_state, + r#type: x::ATOM_ANY, + long_offset: 0, + long_length: 1024, // Number of properties to fetch + }) + } + + fn request_ewmh_state(&self, window_id: x::Window) + -> x::GetPropertyCookie { + // Request to get EWMH states of a window + self.conn.send_request(&x::GetProperty { + delete: false, + window: window_id, + property: self.atoms._net_wm_state, + r#type: x::ATOM_ATOM, + long_offset: 0, + long_length: 1024, // Number of properties to fetch + }) + } + + fn is_floating(&self) -> bool { + return false; // Placeholder for floating logic + } + + fn is_hidden(&self, wm_state: &Vec, ewmh_state: &Vec) -> bool { + wm_state.first().expect("WM_STATE is missing window state") + == &self.atoms.wm_state_withdrawn.into() || + ewmh_state.contains(&self.atoms._net_wm_state_hidden.into()) + } +} + +impl From<&x::GetGeometryReply> for Rect { + fn from(reply: &x::GetGeometryReply) -> Self { + Rect { + x: reply.x() as i32, + y: reply.y() as i32, + w: reply.width() as i32, + h: reply.height() as i32, + } + } +} + +fn translate_rect(rect: &mut Rect, translation: &x::TranslateCoordinatesReply) { + // Translate the rectangle coordinates based on the translation reply + rect.x += translation.dst_x() as i32; + rect.y += translation.dst_y() as i32; +} diff --git a/rust/src/backend/xcb/mod.rs b/rust/src/backend/xcb/mod.rs new file mode 100644 index 0000000..dda2e5f --- /dev/null +++ b/rust/src/backend/xcb/mod.rs @@ -0,0 +1,4 @@ +pub mod backend; +mod client; + +pub use crate::backend::xcb::backend::Backend; diff --git a/rust/src/cli.rs b/rust/src/cli.rs new file mode 100644 index 0000000..cdd8818 --- /dev/null +++ b/rust/src/cli.rs @@ -0,0 +1,232 @@ +use crate::planar; +use crate::linear; +use std::slice::Iter; + +macro_rules! die { + ($code:expr, $format:expr $(, $args:expr)*) => {{ + eprintln!($format $(, $args)*); + std::process::exit($code); + }}; +} + +pub struct Cli { + pub backend: UseBackend, + pub command: String, + pub number: Option, + pub wrap: bool, +} + +// The help message will be built at runtime, because rust does not support +// macro expansion in const contexts, and we want to use the `#[cfg]` attributes +// to conditionally include backend options based on the features enabled. +// This should not have a significant performance impact, as the help message is only +// used when the user requests it, and it is built once at runtime. +const HELP: &'static [&'static str] = &[" +i3switch - A simple command-line utility to switch focus in i3 window manager + +Usage: i3switch (