From 2b871d7e0991ef9a60709ab068c632be0b5d123f Mon Sep 17 00:00:00 2001 From: Milosz Blizniak Date: Wed, 18 Jun 2025 21:56:53 +0200 Subject: [PATCH 01/18] chore(rs): move client to i3 backend --- rust/src/{connection/i3client.rs => backend/i3/client.rs} | 0 rust/src/backend/i3/mod.rs | 1 + rust/src/backend/mod.rs | 1 + rust/src/connection/mod.rs | 1 - rust/src/main.rs | 8 ++++---- 5 files changed, 6 insertions(+), 5 deletions(-) rename rust/src/{connection/i3client.rs => backend/i3/client.rs} (100%) create mode 100644 rust/src/backend/i3/mod.rs create mode 100644 rust/src/backend/mod.rs delete mode 100644 rust/src/connection/mod.rs 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/backend/i3/mod.rs b/rust/src/backend/i3/mod.rs new file mode 100644 index 0000000..b9babe5 --- /dev/null +++ b/rust/src/backend/i3/mod.rs @@ -0,0 +1 @@ +pub mod client; diff --git a/rust/src/backend/mod.rs b/rust/src/backend/mod.rs new file mode 100644 index 0000000..c784dbd --- /dev/null +++ b/rust/src/backend/mod.rs @@ -0,0 +1 @@ +pub mod i3; diff --git a/rust/src/connection/mod.rs b/rust/src/connection/mod.rs deleted file mode 100644 index 867b6bd..0000000 --- a/rust/src/connection/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod i3client; diff --git a/rust/src/main.rs b/rust/src/main.rs index b9d54da..4dc4c4c 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -5,7 +5,7 @@ use serde_json as json; mod planar; mod linear; -mod connection; +mod backend; mod navigation; mod converters; mod logging; @@ -106,9 +106,9 @@ fn main() { .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 = connection::i3client::Client::new(&i3_path.trim()) + let mut client = backend::i3::client::Client::new(&i3_path.trim()) .expect_log("Failed to connect to i3 IPC server"); - let tree = client.request(connection::i3client::Request::GetTree, "") + let tree = client.request(backend::i3::client::Request::GetTree, "") .expect_log("Failed to get i3 tree JSON"); // Parse the i3 tree to get the current workspace and window information @@ -136,7 +136,7 @@ fn main() { // Focus the window with the determined ID logging::info!("Focusing window with ID: {}", window_id); let payload = format!("[con_id={}] focus", window_id); - client.request(connection::i3client::Request::Command, &payload) + client.request(backend::i3::client::Request::Command, &payload) .expect_log("Failed to send focus command"); std::process::exit(0); From 0619b6ffe6a9274ff96e9bd83368a124d2433e57 Mon Sep 17 00:00:00 2001 From: Milosz Blizniak Date: Wed, 18 Jun 2025 22:47:28 +0200 Subject: [PATCH 02/18] refactor(rs): move json logic to backend - Consolidate planar::Window with converters::Window into types::Window - Move planar::Rect into types - Replace to_window with From<&Value> for Window --- rust/src/backend/i3/compass.rs | 631 +++++++++++++++++++++++++++ rust/src/backend/i3/mod.rs | 1 + rust/src/converters.rs | 664 +---------------------------- rust/src/main.rs | 1 + rust/src/navigation.rs | 13 +- rust/src/planar/alignment.rs | 7 +- rust/src/planar/arrangement.rs | 9 +- rust/src/planar/mod.rs | 3 - rust/src/types/mod.rs | 6 + rust/src/{planar => types}/rect.rs | 0 rust/src/types/window.rs | 19 + 11 files changed, 679 insertions(+), 675 deletions(-) create mode 100644 rust/src/backend/i3/compass.rs create mode 100644 rust/src/types/mod.rs rename rust/src/{planar => types}/rect.rs (100%) create mode 100644 rust/src/types/window.rs diff --git a/rust/src/backend/i3/compass.rs b/rust/src/backend/i3/compass.rs new file mode 100644 index 0000000..91e9714 --- /dev/null +++ b/rust/src/backend/i3/compass.rs @@ -0,0 +1,631 @@ +use serde_json::Value; +use crate::logging; +use crate::types::{Rect, Window, Windows}; + + +/// This enum represents the layout type of a node in a window manager's tree structure. +#[derive(Debug, Clone, PartialEq, Eq)] +enum Layout { + AllVisible, + OneVisible, + Skipped, + Invalid, +} + +/// 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. +pub fn visible_nodes<'a>(node: &'a Value) -> Vec<&'a Value> { + logging::debug!("V Node iterated id:{} type:{} layout:{}, name:{}", + node.get("id").and_then(|v| v.as_u64()).unwrap_or(0), + node.get("type").and_then(|v| v.as_str()).unwrap_or("null"), + node.get("layout").and_then(|v| v.as_str()).unwrap_or("null"), + node.get("name").and_then(|v| v.as_str()).unwrap_or("null")); + if is_end_node(node) { + if is_invisible_node(node) { + return vec![]; + } + return vec![node]; + } + + let layout = get_layout(node); + match layout { + Layout::AllVisible => { + let mut nodes: Vec<&'a Value> = vec![]; + if let Some(floating_nodes) = node.get("floating_nodes").unwrap_or(&Value::Null).as_array() { + nodes.extend(floating_nodes.iter()); + } + node.get("nodes").unwrap().as_array().unwrap().iter().for_each(|subnode| { + nodes.extend(visible_nodes(subnode)); + }); + return nodes; + } + Layout::OneVisible => { + let mut nodes: Vec<&'a Value> = vec![]; + if let Some(focused_node) = focused_subnode(node) { + nodes.extend(visible_nodes(focused_node)); + } + return nodes; + } + Layout::Skipped => vec![], + Layout::Invalid => { + logging::error!("Invalid layout encountered: {:?}", layout); + return vec![] + } + } +} + +/// Finds deepest focused tabbed container in the provided node, and then for each tab, +/// returns the deepest focused node within that tab, this is to preserve the +/// focused state within tabs of a tabbed layout. +pub fn available_tabs(node: &Value) -> Vec<&Value> { + if let Some(subnode) = find_deepest_focused_tabbed(node) { + let tabs_node = subnode.get("nodes").unwrap_or(&Value::Null); + if let Some(tabs_node) = tabs_node.as_array() { + return tabs_node.iter().map(|tab| find_deepest_focused(tab).unwrap_or(tab)).collect(); + } + } + logging::info!("No available tabs found in the provided node."); + vec![] +} + +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(Window::from) + .collect() +} + +/// 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 { + node["rect"]["width"].as_i64().unwrap_or(0) == 0 + && node["rect"]["height"].as_i64().unwrap_or(0) == 0 +} + +/// Checks if a node is an end node, which is defined as having no subnodes and being of type +/// "con". +fn is_end_node(node: &Value) -> bool { + let is_empty = node.get("nodes").and_then(|n| n.as_array()).unwrap_or(&vec![]).is_empty(); + let type_str = node.get("type").and_then(|t| t.as_str()).unwrap_or(""); + is_empty && ( type_str == "con" || type_str == "floating_con" ) +} + +/// Checks if a node is a workspace, which is defined as having a type of "workspace". +fn is_content_node(node: &Value) -> bool { + node["name"].as_str().unwrap_or("") == "content" && !is_end_node(node) +} + +/// Returns the subnode that is currently focused, if any. If the node is an end node or has no +/// focus, it returns `None`. +/// Requires an array "focus" field and a "nodes" field containing subnodes. +fn focused_subnode(node: &Value) -> Option<&Value> { + let focus: &Value = node.get("focus").unwrap_or(&Value::Null); + if is_end_node(node) || focus.is_null() || focus.as_array().unwrap().is_empty() { + return None; + } + let focus_id = focus[0].as_u64().unwrap_or(0); + node.get("nodes")? + .as_array()? + .iter() + .find(|n| n["id"].as_u64() == Some(focus_id)) +} + +/// Determines the layout type of a node based on its properties. +/// Requires a "layout" field and a "type" field in the node. +fn get_layout(node: &Value) -> Layout { + let layout = node.get("layout").and_then(|l| l.as_str()).unwrap_or(""); + let node_type = node.get("type").and_then(|t| t.as_str()).unwrap_or(""); + if is_content_node(node) || layout == "stacked" || layout == "tabbed" { + Layout::OneVisible + } else if layout == "splith" || layout == "splitv" || layout == "output" || node_type == "workspace" || node_type == "root" { + Layout::AllVisible + } else if layout == "dockarea" { + Layout::Skipped + } else { + Layout::Invalid + } +} + +/// Finds the deepest focused node in a tree structure, starting from the given node. +fn find_deepest_focused(node: &Value) -> Option<&Value> { + logging::debug!("F Node iterated id:{} type:{} layout:{}", + node.get("id").and_then(|v| v.as_u64()).unwrap_or(0), + node.get("type").and_then(|v| v.as_str()).unwrap_or("null"), + node.get("layout").and_then(|v| v.as_str()).unwrap_or("null")); + let subnode = focused_subnode(node); + if subnode.is_some() { + let deepest = find_deepest_focused(subnode?); + if deepest.is_some() { + return deepest; + } + } + subnode +} + +/// Finds the deepest focused node that is tabbed, meaning it has a layout of `tabbed` or +/// `stacked`. +fn find_deepest_focused_tabbed(node: &Value) -> Option<&Value> { + logging::debug!("T Node iterated id:{} type:{} layout:{}", + node.get("id").and_then(|v| v.as_u64()).unwrap_or(0), + node.get("type").and_then(|v| v.as_str()).unwrap_or("null"), + node.get("layout").and_then(|v| v.as_str()).unwrap_or("null")); + if let Some(subnode) = focused_subnode(node) { + let endnode = find_deepest_focused_tabbed(subnode); + if endnode.is_some() { + return endnode; + } else if get_layout(node) == Layout::OneVisible { + return Some(node); + } + } + None +} + +#[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. + #[test] + fn test_visible_nodes() { + let node = json!({ + "id": 1, + "type": "con", + "layout": "splith", + "nodes": [ + { + "id": 2, + "type": "con", + "rect": {"width": 100, "height": 100}, + "focused": true, + "nodes": [] + }, + { + "id": 3, + "type": "con", + "rect": {"width": 0, "height": 0}, + "focused": false, + "nodes": [] + } + ], + "rect": {"width": 100, "height": 100} + }); + let nodes = visible_nodes(&node); + assert_eq!(nodes.len(), 1); + assert_eq!(nodes[0]["id"], 2); + + let node = json!({ + "id": 0, + "type": "root", + "layout": "splith", + "nodes": [ + { + "id": 1, + "type": "output", + "layout": "output", + "nodes": [ + { + "id": 2, + "type": "con", + "layout": "splith", + "name": "content", + "nodes": [ + { + "id": 3, + "type": "workspace", + "layout": "splith", + "nodes": [ + { + "id": 4, + "type": "con", + "layout": "splith", + "nodes": [ + { + "id": 5, + "focused": false, + "nodes": [], + "type": "con", + "focus": [], + "rect": {"width": 100, "height": 100} + }, + { + "id": 6, + "focused": true, + "nodes": [], + "type": "con", + "focus": [], + "rect": {"width": 100, "height": 100} + } + ], + "focus": [6, 5], + "focused": false, + "rect": {"width": 100, "height": 100} + } + ], + "focus": [4], + "focused": false, + "rect": {"width": 100, "height": 100} + } + ], + "focus": [3], + "focused": false, + "rect": {"width": 100, "height": 100} + } + ], + "focus": [2], + "focused": false, + "rect": {"width": 100, "height": 100} + }, + { + "id": 7, + "type": "output", + "layout": "output", + "nodes": [ + { + "id": 8, + "type": "con", + "layout": "splith", + "name": "content", + "nodes": [ + { + "id": 12, + "type": "workspace", + "layout": "splith", + "nodes": [ + { + "id": 13, + "focused": false, + "nodes": [], + "type": "con", + "focus": [], + "rect": {"width": 100, "height": 100} + } + ], + "focus": [13], + "focused": false, + "rect": {"width": 100, "height": 100} + }, + { + "id": 9, + "type": "workspace", + "layout": "splith", + "nodes": [ + { + "id": 10, + "type": "con", + "layout": "tabbed", + "nodes": [ + { + "id": 14, + "focused": false, + "nodes": [], + "type": "con", + "focus": [], + "rect": {"width": 100, "height": 100} + }, + { + "id": 11, + "focused": false, + "nodes": [], + "type": "con", + "focus": [], + "rect": {"width": 100, "height": 100} + } + ], + "focus": [11, 14], + "focused": false, + } + ], + "floating_nodes": [ + { + "id": 20, + "focused": false, + "nodes": [], + "type": "con", + "focus": [], + "rect": {"width": 100, "height": 100} + } + ], + "focus": [10, 20], + "focused": false, + "rect": {"width": 100, "height": 100} + } + ], + "focus": [9, 12], + "focused": false, + "rect": {"width": 100, "height": 100} + } + ], + "focus": [8], + "focused": false, + "rect": {"width": 100, "height": 100} + } + ], + "focus": [1, 7], + "focused": false, + "rect": {"width": 100, "height": 100} + }); + let nodes = visible_nodes(&node); + assert!(nodes.iter().any(|n| n["id"] == 5)); + assert!(nodes.iter().any(|n| n["id"] == 6)); + assert!(nodes.iter().any(|n| n["id"] == 11)); + assert!(nodes.iter().any(|n| n["id"] == 20)); + assert_eq!(nodes.len(), 4); + } + + /// Tests for extracting available tabs from a node. + /// We expect the function to return a vector of leaf nodes that are focused of a tabbed + /// layout, or none if there are no tabs. + #[test] + fn test_available_tabs() { + let node = json!({ + "id": 1, + "type": "con", + "layout": "splith", + "nodes": [ + { + "id": 2, + "type": "con", + "layout": "tabbed", + "nodes": [ + {"id": 3, "focused": true, "nodes": [], "type": "con", "focus": []}, + { + "id": 4, + "type": "con", + "layout": "splith", + "nodes": [ + {"id": 5, "focused": true, "nodes": [], "type": "con", "focus": []}, + {"id": 6, "focused": false, "nodes": [], "type": "con", "focus": []} + ], + "focus": [5], + "focused": true, + "rect": {"width": 100, "height": 100} + } + ], + "focus": [3], + "focused": true, + "rect": {"width": 100, "height": 100} + } + ], + "focus": [2], + "focused": true, + "rect": {"width": 100, "height": 100} + }); + let tabs = available_tabs(&node); + assert_eq!(tabs.len(), 2); + assert!(tabs.iter().any(|tab| tab["id"] == 3)); + assert!(tabs.iter().any(|tab| tab["id"] == 5)); + } + + /// Tests for converting JSON nodes to windows. + #[test] + fn test_to_windows() { + let nodes = vec![ + json!({"id": 1, "rect": {"x": 0, "y": 0, "width": 100, "height": 100}, "focused": true, "type": "con", "nodes": []}), + json!({"id": 2, "rect": {"x": 300, "y": 450, "width": 15, "height": 200}, "focused": false, "type": "con", "nodes": []}), + ]; + let node_refs: Vec<&Value> = nodes.iter().collect(); + let windows = to_windows(node_refs); + assert_eq!(windows.len(), 2); + assert_eq!(windows[0].id, 1); + assert!(windows[0].focused); + assert!(!windows[0].floating); + assert_eq!(windows[0].rect, Rect { x: 0, y: 0, w: 100, h: 100 }); + assert_eq!(windows[1].id, 2); + assert!(!windows[1].focused); + assert!(!windows[1].floating); + assert_eq!(windows[1].rect, Rect { x: 300, y: 450, w: 15, h: 200 }); + } + + /// Tests for layout extraction. + /// We expect the function to return the correct layout type based on the "layout" and "type" + /// fields. + /// Workspaces are switchable, + /// Split layouts are directional, + /// Tabbed layouts are switchable, + /// Stacked layouts are switchable, + /// Dock areas are opaque to the layout system, + /// Invalid layouts are marked as invalid. + #[test] + fn test_get_layout() { + let node = json!({ + "layout": "splith", + "name": "content", + "type": "con", + "nodes": [ + {"id": 1, "type": "workspace", "layout": "splith", "nodes": []}, + ], + }); + assert_eq!(get_layout(&node), Layout::OneVisible); + + let node = json!({ + "layout": "splith", + "type": "workspace" + }); + assert_eq!(get_layout(&node), Layout::AllVisible); + + let node = json!({ + "layout": "splith", + "type": "con" + }); + assert_eq!(get_layout(&node), Layout::AllVisible); + + let node = json!({ + "layout": "splitv", + "type": "con" + }); + assert_eq!(get_layout(&node), Layout::AllVisible); + + let node = json!({ + "layout": "tabbed", + "type": "con" + }); + assert_eq!(get_layout(&node), Layout::OneVisible); + + let node = json!({ + "layout": "stacked", + "type": "con" + }); + assert_eq!(get_layout(&node), Layout::OneVisible); + + let node = json!({ + "layout": "dockarea", + "type": "con" + }); + assert_eq!(get_layout(&node), Layout::Skipped); + + let node = json!({ + "layout": "invalid", + "type": "con" + }); + assert_eq!(get_layout(&node), Layout::Invalid); + } + + /// Tests for invisible node detection. + /// We expect the function to return true for nodes that have a rectangle with zero width and + /// height. + #[test] + fn test_is_invisible_node() { + let node = json!({ + "rect": {"width": 0, "height": 0} + }); + assert!(is_invisible_node(&node)); + + let node = json!({ + "rect": {"width": 100, "height": 100} + }); + assert!(!is_invisible_node(&node)); + } + + /// Tests for end node detection. + /// We expect the function to return true for nodes that have no subnodes and are a container. + #[test] + fn test_is_end_node() { + let node = json!({ + "nodes": [], + "type": "con" + }); + assert!(is_end_node(&node)); + + let node = json!({ + "nodes": [{"id": 1}], + "type": "con" + }); + assert!(!is_end_node(&node)); + + let node = json!({ + "nodes": [], + "type": "workspace" + }); + assert!(!is_end_node(&node)); + } + + /// Tests for focused subnode extraction. + /// We expect the function to return the node that is indicated based on the "focus" field. + #[test] + fn test_focused_subnode() { + let node = json!({ + "nodes": [ + {"id": 1, "focused": true, "nodes": []}, + {"id": 2, "focused": false, "nodes": []} + ], + "focus": [1] + }); + assert_eq!(focused_subnode(&node).unwrap()["id"].as_u64().unwrap(), 1); + + let node = json!({ + "nodes": [], + "focus": [] + }); + assert!(focused_subnode(&node).is_none()); + } + + /// Tests for finding the deepest focused node. + /// We expect the function to traverse the tree and find the deepest node that is focused. + #[test] + fn test_find_deepest_focused() { + let node = json!({ + "nodes": [ + {"id": 1, "focused": true, "nodes": []}, + {"id": 2, "focused": false, "nodes": []} + ], + "focus": [1] + }); + assert_eq!(find_deepest_focused(&node).unwrap()["id"], 1); + + let node = json!({ + "nodes": [], + "focus": [] + }); + assert!(find_deepest_focused(&node).is_none()); + } + + /// Tests for finding the deepest focused tabbed node. + /// We expect the function to find the deepest node that is focused and has focusable tabs. + #[test] + fn test_find_deepest_focused_tabbed() { + // Test with a focused tabbed node. + // Tabs require elements and focus to be recognized as switchable. + let node = json!({ + "nodes": [ + {"id": 1, "layout": "tabbed", "focused": true, "nodes": [ + {"id": 3, "focused": true, "nodes": [], "focus": []}, + {"id": 4, "focused": false, "nodes": [], "focus": []} + ], "focus": [3]}, + {"id": 2, "layout": "tabbed", "focused": false, "nodes": [], "focus": []} + ], + "focus": [1, 3] + }); + assert_eq!(find_deepest_focused_tabbed(&node).unwrap()["id"], 1); + + // Test with a focused tabbed node that has no subnodes + // This should return None since it has no tabs. + let node = json!({ + "nodes": [ + {"id": 1, "layout": "tabbed", "focused": true, "nodes": [], "focus": []} + ], + "focus": [1] + }); + assert_eq!(find_deepest_focused_tabbed(&node), None); + + // Test with no focused tabbed node + // This should return None since there are no tabs. + let node = json!({ + "nodes": [], + "focus": [] + }); + assert!(find_deepest_focused_tabbed(&node).is_none()); + } + +} diff --git a/rust/src/backend/i3/mod.rs b/rust/src/backend/i3/mod.rs index b9babe5..7c7837b 100644 --- a/rust/src/backend/i3/mod.rs +++ b/rust/src/backend/i3/mod.rs @@ -1 +1,2 @@ pub mod client; +pub mod compass; diff --git a/rust/src/converters.rs b/rust/src/converters.rs index c3b45f3..33f48c2 100644 --- a/rust/src/converters.rs +++ b/rust/src/converters.rs @@ -6,93 +6,11 @@ /// /// 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; - -/// 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)] -enum Layout { - AllVisible, - OneVisible, - Skipped, - 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. -pub fn visible_nodes<'a>(node: &'a Value) -> Vec<&'a Value> { - logging::debug!("V Node iterated id:{} type:{} layout:{}, name:{}", - node.get("id").and_then(|v| v.as_u64()).unwrap_or(0), - node.get("type").and_then(|v| v.as_str()).unwrap_or("null"), - node.get("layout").and_then(|v| v.as_str()).unwrap_or("null"), - node.get("name").and_then(|v| v.as_str()).unwrap_or("null")); - if is_end_node(node) { - if is_invisible_node(node) { - return vec![]; - } - return vec![node]; - } - - let layout = get_layout(node); - match layout { - Layout::AllVisible => { - let mut nodes: Vec<&'a Value> = vec![]; - if let Some(floating_nodes) = node.get("floating_nodes").unwrap_or(&Value::Null).as_array() { - nodes.extend(floating_nodes.iter()); - } - node.get("nodes").unwrap().as_array().unwrap().iter().for_each(|subnode| { - nodes.extend(visible_nodes(subnode)); - }); - return nodes; - } - Layout::OneVisible => { - let mut nodes: Vec<&'a Value> = vec![]; - if let Some(focused_node) = focused_subnode(node) { - nodes.extend(visible_nodes(focused_node)); - } - return nodes; - } - Layout::Skipped => vec![], - Layout::Invalid => { - logging::error!("Invalid layout encountered: {:?}", layout); - return vec![] - } - } -} - -/// Finds deepest focused tabbed container in the provided node, and then for each tab, -/// returns the deepest focused node within that tab, this is to preserve the -/// focused state within tabs of a tabbed layout. -pub fn available_tabs(node: &Value) -> Vec<&Value> { - if let Some(subnode) = find_deepest_focused_tabbed(node) { - let tabs_node = subnode.get("nodes").unwrap_or(&Value::Null); - if let Some(tabs_node) = tabs_node.as_array() { - return tabs_node.iter().map(|tab| find_deepest_focused(tab).unwrap_or(tab)).collect(); - } - } - logging::info!("No available tabs found in the provided node."); - vec![] -} +use crate::types::Windows; /// Returns a collection of windows that are floating, i.e., those that are not tiled. pub fn floating(windows: &Windows) -> Windows { @@ -110,26 +28,12 @@ pub fn tiled(windows: &Windows) -> Windows { .collect() } -/// Returns whether any window in the provided `Windows` is focused. -pub fn any_focused(windows: &Windows) -> bool { - windows.iter().any(|w| w.focused) -} - -/// 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) - .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)) + planar::Arrangement::new(windows.clone(), Some(current), Some(relation)) } /// Converts a collection of `Windows` to a `linear::Sequence`. @@ -139,34 +43,9 @@ pub fn as_sequence(windows: &Windows) -> linear::Sequence { 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 whether any window in the provided `Windows` is focused. +pub fn any_focused(windows: &Windows) -> bool { + windows.iter().any(|w| w.focused) } /// Returns the index of the currently focused window, if any. @@ -175,96 +54,13 @@ fn focused_index(windows: &Windows) -> Option { 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 { - node["rect"]["width"].as_i64().unwrap_or(0) == 0 - && node["rect"]["height"].as_i64().unwrap_or(0) == 0 -} - -/// Checks if a node is an end node, which is defined as having no subnodes and being of type -/// "con". -fn is_end_node(node: &Value) -> bool { - let is_empty = node.get("nodes").and_then(|n| n.as_array()).unwrap_or(&vec![]).is_empty(); - let type_str = node.get("type").and_then(|t| t.as_str()).unwrap_or(""); - is_empty && ( type_str == "con" || type_str == "floating_con" ) -} - -/// Checks if a node is a workspace, which is defined as having a type of "workspace". -fn is_content_node(node: &Value) -> bool { - node["name"].as_str().unwrap_or("") == "content" && !is_end_node(node) -} - -/// Returns the subnode that is currently focused, if any. If the node is an end node or has no -/// focus, it returns `None`. -/// Requires an array "focus" field and a "nodes" field containing subnodes. -fn focused_subnode(node: &Value) -> Option<&Value> { - let focus: &Value = node.get("focus").unwrap_or(&Value::Null); - if is_end_node(node) || focus.is_null() || focus.as_array().unwrap().is_empty() { - return None; - } - let focus_id = focus[0].as_u64().unwrap_or(0); - node.get("nodes")? - .as_array()? - .iter() - .find(|n| n["id"].as_u64() == Some(focus_id)) -} - -/// Determines the layout type of a node based on its properties. -/// Requires a "layout" field and a "type" field in the node. -fn get_layout(node: &Value) -> Layout { - let layout = node.get("layout").and_then(|l| l.as_str()).unwrap_or(""); - let node_type = node.get("type").and_then(|t| t.as_str()).unwrap_or(""); - if is_content_node(node) || layout == "stacked" || layout == "tabbed" { - Layout::OneVisible - } else if layout == "splith" || layout == "splitv" || layout == "output" || node_type == "workspace" || node_type == "root" { - Layout::AllVisible - } else if layout == "dockarea" { - Layout::Skipped - } else { - Layout::Invalid - } -} - -/// Finds the deepest focused node in a tree structure, starting from the given node. -fn find_deepest_focused(node: &Value) -> Option<&Value> { - logging::debug!("F Node iterated id:{} type:{} layout:{}", - node.get("id").and_then(|v| v.as_u64()).unwrap_or(0), - node.get("type").and_then(|v| v.as_str()).unwrap_or("null"), - node.get("layout").and_then(|v| v.as_str()).unwrap_or("null")); - let subnode = focused_subnode(node); - if subnode.is_some() { - let deepest = find_deepest_focused(subnode?); - if deepest.is_some() { - return deepest; - } - } - subnode -} - -/// Finds the deepest focused node that is tabbed, meaning it has a layout of `tabbed` or -/// `stacked`. -fn find_deepest_focused_tabbed(node: &Value) -> Option<&Value> { - logging::debug!("T Node iterated id:{} type:{} layout:{}", - node.get("id").and_then(|v| v.as_u64()).unwrap_or(0), - node.get("type").and_then(|v| v.as_str()).unwrap_or("null"), - node.get("layout").and_then(|v| v.as_str()).unwrap_or("null")); - if let Some(subnode) = focused_subnode(node) { - let endnode = find_deepest_focused_tabbed(subnode); - if endnode.is_some() { - return endnode; - } else if get_layout(node) == Layout::OneVisible { - return Some(node); - } - } - None -} - #[cfg(test)] mod tests { - use super::*; + use crate::types::Rect; + use crate::types::Window; use crate::logging; - use serde_json::json; + use super::*; + use ctor::ctor; #[ctor] @@ -272,239 +68,6 @@ mod tests { logging::init(); } - /// Tests for visible nodes extraction. - /// We expect the function to return all nodes that are not invisible. - #[test] - fn test_visible_nodes() { - let node = json!({ - "id": 1, - "type": "con", - "layout": "splith", - "nodes": [ - { - "id": 2, - "type": "con", - "rect": {"width": 100, "height": 100}, - "focused": true, - "nodes": [] - }, - { - "id": 3, - "type": "con", - "rect": {"width": 0, "height": 0}, - "focused": false, - "nodes": [] - } - ], - "rect": {"width": 100, "height": 100} - }); - let nodes = visible_nodes(&node); - assert_eq!(nodes.len(), 1); - assert_eq!(nodes[0]["id"], 2); - - let node = json!({ - "id": 0, - "type": "root", - "layout": "splith", - "nodes": [ - { - "id": 1, - "type": "output", - "layout": "output", - "nodes": [ - { - "id": 2, - "type": "con", - "layout": "splith", - "name": "content", - "nodes": [ - { - "id": 3, - "type": "workspace", - "layout": "splith", - "nodes": [ - { - "id": 4, - "type": "con", - "layout": "splith", - "nodes": [ - { - "id": 5, - "focused": false, - "nodes": [], - "type": "con", - "focus": [], - "rect": {"width": 100, "height": 100} - }, - { - "id": 6, - "focused": true, - "nodes": [], - "type": "con", - "focus": [], - "rect": {"width": 100, "height": 100} - } - ], - "focus": [6, 5], - "focused": false, - "rect": {"width": 100, "height": 100} - } - ], - "focus": [4], - "focused": false, - "rect": {"width": 100, "height": 100} - } - ], - "focus": [3], - "focused": false, - "rect": {"width": 100, "height": 100} - } - ], - "focus": [2], - "focused": false, - "rect": {"width": 100, "height": 100} - }, - { - "id": 7, - "type": "output", - "layout": "output", - "nodes": [ - { - "id": 8, - "type": "con", - "layout": "splith", - "name": "content", - "nodes": [ - { - "id": 12, - "type": "workspace", - "layout": "splith", - "nodes": [ - { - "id": 13, - "focused": false, - "nodes": [], - "type": "con", - "focus": [], - "rect": {"width": 100, "height": 100} - } - ], - "focus": [13], - "focused": false, - "rect": {"width": 100, "height": 100} - }, - { - "id": 9, - "type": "workspace", - "layout": "splith", - "nodes": [ - { - "id": 10, - "type": "con", - "layout": "tabbed", - "nodes": [ - { - "id": 14, - "focused": false, - "nodes": [], - "type": "con", - "focus": [], - "rect": {"width": 100, "height": 100} - }, - { - "id": 11, - "focused": false, - "nodes": [], - "type": "con", - "focus": [], - "rect": {"width": 100, "height": 100} - } - ], - "focus": [11, 14], - "focused": false, - } - ], - "floating_nodes": [ - { - "id": 20, - "focused": false, - "nodes": [], - "type": "con", - "focus": [], - "rect": {"width": 100, "height": 100} - } - ], - "focus": [10, 20], - "focused": false, - "rect": {"width": 100, "height": 100} - } - ], - "focus": [9, 12], - "focused": false, - "rect": {"width": 100, "height": 100} - } - ], - "focus": [8], - "focused": false, - "rect": {"width": 100, "height": 100} - } - ], - "focus": [1, 7], - "focused": false, - "rect": {"width": 100, "height": 100} - }); - let nodes = visible_nodes(&node); - assert!(nodes.iter().any(|n| n["id"] == 5)); - assert!(nodes.iter().any(|n| n["id"] == 6)); - assert!(nodes.iter().any(|n| n["id"] == 11)); - assert!(nodes.iter().any(|n| n["id"] == 20)); - assert_eq!(nodes.len(), 4); - } - - /// Tests for extracting available tabs from a node. - /// We expect the function to return a vector of leaf nodes that are focused of a tabbed - /// layout, or none if there are no tabs. - #[test] - fn test_available_tabs() { - let node = json!({ - "id": 1, - "type": "con", - "layout": "splith", - "nodes": [ - { - "id": 2, - "type": "con", - "layout": "tabbed", - "nodes": [ - {"id": 3, "focused": true, "nodes": [], "type": "con", "focus": []}, - { - "id": 4, - "type": "con", - "layout": "splith", - "nodes": [ - {"id": 5, "focused": true, "nodes": [], "type": "con", "focus": []}, - {"id": 6, "focused": false, "nodes": [], "type": "con", "focus": []} - ], - "focus": [5], - "focused": true, - "rect": {"width": 100, "height": 100} - } - ], - "focus": [3], - "focused": true, - "rect": {"width": 100, "height": 100} - } - ], - "focus": [2], - "focused": true, - "rect": {"width": 100, "height": 100} - }); - let tabs = available_tabs(&node); - assert_eq!(tabs.len(), 2); - assert!(tabs.iter().any(|tab| tab["id"] == 3)); - assert!(tabs.iter().any(|tab| tab["id"] == 5)); - } - /// Tests for floating and tiled windows. #[test] fn test_floating_and_tiled() { @@ -528,26 +91,6 @@ mod tests { assert!(any_focused(&windows)); } - /// Tests for converting JSON nodes to windows. - #[test] - fn test_to_windows() { - let nodes = vec![ - json!({"id": 1, "rect": {"x": 0, "y": 0, "width": 100, "height": 100}, "focused": true, "type": "con", "nodes": []}), - json!({"id": 2, "rect": {"x": 300, "y": 450, "width": 15, "height": 200}, "focused": false, "type": "con", "nodes": []}), - ]; - let node_refs: Vec<&Value> = nodes.iter().collect(); - let windows = to_windows(node_refs); - assert_eq!(windows.len(), 2); - assert_eq!(windows[0].id, 1); - assert!(windows[0].focused); - assert!(!windows[0].floating); - assert_eq!(windows[0].rect, Rect { x: 0, y: 0, w: 100, h: 100 }); - assert_eq!(windows[1].id, 2); - assert!(!windows[1].focused); - assert!(!windows[1].floating); - assert_eq!(windows[1].rect, Rect { x: 300, y: 450, w: 15, h: 200 }); - } - /// Tests for visible nodes extraction. #[test] fn test_as_arrangement() { @@ -560,79 +103,6 @@ mod tests { 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. - /// Workspaces are switchable, - /// Split layouts are directional, - /// Tabbed layouts are switchable, - /// Stacked layouts are switchable, - /// Dock areas are opaque to the layout system, - /// Invalid layouts are marked as invalid. - #[test] - fn test_get_layout() { - let node = json!({ - "layout": "splith", - "name": "content", - "type": "con", - "nodes": [ - {"id": 1, "type": "workspace", "layout": "splith", "nodes": []}, - ], - }); - assert_eq!(get_layout(&node), Layout::OneVisible); - - let node = json!({ - "layout": "splith", - "type": "workspace" - }); - assert_eq!(get_layout(&node), Layout::AllVisible); - - let node = json!({ - "layout": "splith", - "type": "con" - }); - assert_eq!(get_layout(&node), Layout::AllVisible); - - let node = json!({ - "layout": "splitv", - "type": "con" - }); - assert_eq!(get_layout(&node), Layout::AllVisible); - - let node = json!({ - "layout": "tabbed", - "type": "con" - }); - assert_eq!(get_layout(&node), Layout::OneVisible); - - let node = json!({ - "layout": "stacked", - "type": "con" - }); - assert_eq!(get_layout(&node), Layout::OneVisible); - - let node = json!({ - "layout": "dockarea", - "type": "con" - }); - assert_eq!(get_layout(&node), Layout::Skipped); - - let node = json!({ - "layout": "invalid", - "type": "con" - }); - 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] @@ -650,122 +120,6 @@ mod tests { 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. - #[test] - fn test_is_invisible_node() { - let node = json!({ - "rect": {"width": 0, "height": 0} - }); - assert!(is_invisible_node(&node)); - - let node = json!({ - "rect": {"width": 100, "height": 100} - }); - assert!(!is_invisible_node(&node)); - } - - /// Tests for end node detection. - /// We expect the function to return true for nodes that have no subnodes and are a container. - #[test] - fn test_is_end_node() { - let node = json!({ - "nodes": [], - "type": "con" - }); - assert!(is_end_node(&node)); - - let node = json!({ - "nodes": [{"id": 1}], - "type": "con" - }); - assert!(!is_end_node(&node)); - - let node = json!({ - "nodes": [], - "type": "workspace" - }); - assert!(!is_end_node(&node)); - } - - /// Tests for focused subnode extraction. - /// We expect the function to return the node that is indicated based on the "focus" field. - #[test] - fn test_focused_subnode() { - let node = json!({ - "nodes": [ - {"id": 1, "focused": true, "nodes": []}, - {"id": 2, "focused": false, "nodes": []} - ], - "focus": [1] - }); - assert_eq!(focused_subnode(&node).unwrap()["id"].as_u64().unwrap(), 1); - - let node = json!({ - "nodes": [], - "focus": [] - }); - assert!(focused_subnode(&node).is_none()); - } - - /// Tests for finding the deepest focused node. - /// We expect the function to traverse the tree and find the deepest node that is focused. - #[test] - fn test_find_deepest_focused() { - let node = json!({ - "nodes": [ - {"id": 1, "focused": true, "nodes": []}, - {"id": 2, "focused": false, "nodes": []} - ], - "focus": [1] - }); - assert_eq!(find_deepest_focused(&node).unwrap()["id"], 1); - - let node = json!({ - "nodes": [], - "focus": [] - }); - assert!(find_deepest_focused(&node).is_none()); - } - - /// Tests for finding the deepest focused tabbed node. - /// We expect the function to find the deepest node that is focused and has focusable tabs. - #[test] - fn test_find_deepest_focused_tabbed() { - // Test with a focused tabbed node. - // Tabs require elements and focus to be recognized as switchable. - let node = json!({ - "nodes": [ - {"id": 1, "layout": "tabbed", "focused": true, "nodes": [ - {"id": 3, "focused": true, "nodes": [], "focus": []}, - {"id": 4, "focused": false, "nodes": [], "focus": []} - ], "focus": [3]}, - {"id": 2, "layout": "tabbed", "focused": false, "nodes": [], "focus": []} - ], - "focus": [1, 3] - }); - assert_eq!(find_deepest_focused_tabbed(&node).unwrap()["id"], 1); - - // Test with a focused tabbed node that has no subnodes - // This should return None since it has no tabs. - let node = json!({ - "nodes": [ - {"id": 1, "layout": "tabbed", "focused": true, "nodes": [], "focus": []} - ], - "focus": [1] - }); - assert_eq!(find_deepest_focused_tabbed(&node), None); - - // Test with no focused tabbed node - // This should return None since there are no tabs. - let node = json!({ - "nodes": [], - "focus": [] - }); - 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. diff --git a/rust/src/main.rs b/rust/src/main.rs index 4dc4c4c..0e71130 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -9,6 +9,7 @@ mod backend; mod navigation; mod converters; mod logging; +mod types; use crate::logging::ResultExt; diff --git a/rust/src/navigation.rs b/rust/src/navigation.rs index dd66c03..e858e8c 100644 --- a/rust/src/navigation.rs +++ b/rust/src/navigation.rs @@ -1,3 +1,4 @@ +use crate::backend; use crate::converters; use crate::linear; use crate::planar; @@ -62,8 +63,8 @@ pub fn get_window_of_number(tree: &json::Value, number: usize) -> u64 { /// If there are focused floating windows, it will return the sequence of those windows. /// Otherwise, it will return the sequence of available tabs in the current workspace. fn get_linear_sequence(tree: &json::Value) -> linear::Sequence { - let visible_nodes = converters::visible_nodes(tree); - let windows = converters::to_windows(visible_nodes); + let visible_nodes = backend::i3::compass::visible_nodes(tree); + let windows = backend::i3::compass::to_windows(visible_nodes); let mut floating = converters::floating(&windows); logging::debug!("Floating windows: {:?}", floating); @@ -74,8 +75,8 @@ fn get_linear_sequence(tree: &json::Value) -> linear::Sequence { converters::as_sequence(&floating) } else { logging::debug!("Using available tabs for linear sequence."); - let nodes = converters::available_tabs(tree); - let windows = converters::to_windows(nodes); + let nodes = backend::i3::compass::available_tabs(tree); + let windows = backend::i3::compass::to_windows(nodes); converters::as_sequence(&windows) } } @@ -84,8 +85,8 @@ fn get_linear_sequence(tree: &json::Value) -> linear::Sequence { /// If there are focused floating windows, it will return the arrangement of those windows. /// Otherwise, it will return the arrangement of visible windows in the current workspace. fn get_planar_arrangement(tree: &json::Value) -> planar::Arrangement { - let visible_nodes = converters::visible_nodes(tree); - let windows = converters::to_windows(visible_nodes); + let visible_nodes = backend::i3::compass::visible_nodes(tree); + let windows = backend::i3::compass::to_windows(visible_nodes); let floating = converters::floating(&windows); // TODO: Consider allowing directional movement in mixed mode (floating + tiled). diff --git a/rust/src/planar/alignment.rs b/rust/src/planar/alignment.rs index cc117c6..4e9de84 100644 --- a/rust/src/planar/alignment.rs +++ b/rust/src/planar/alignment.rs @@ -2,9 +2,8 @@ /// specified directions. /// /// ``` -/// // TODO: Fix tests /// use i3switch::planar::alignment::{get_properties, next_in_direction, Direction, Relation}; -/// use i3switch::planar::rect::Rect; +/// use i3switch::types::rect::Rect; /// /// let rects = vec![ /// Rect { x: 0, y: 0, w: 10, h: 10 }, @@ -19,7 +18,7 @@ /// assert_eq!(next, Some(4)); /// ``` -use crate::planar::Rect; +use crate::types::Rect; use crate::logging; /// This enum is used to specify the direction in which the focus should be moved. @@ -185,7 +184,7 @@ pub fn first_of_direction<'a>(rects: &'a [&Rect], current: &Rect, properties: &P #[cfg(test)] mod tests { use super::*; - use crate::planar::Rect; + use crate::types::Rect; // In this test we check if the properties for each relation and direction correctly describe // the extents and direction comparison for a rectangle with coordinates for the left top diff --git a/rust/src/planar/arrangement.rs b/rust/src/planar/arrangement.rs index 03685d8..069d4ea 100644 --- a/rust/src/planar/arrangement.rs +++ b/rust/src/planar/arrangement.rs @@ -1,14 +1,9 @@ -use crate::planar::Rect; +use crate::types::Rect; +use crate::types::Window; use crate::planar::Relation; use crate::planar::Direction; use crate::planar::alignment; -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Window { - pub id: u64, - pub rect: Rect, -} - pub struct Arrangement { pub windows: Vec, pub relation: Relation, diff --git a/rust/src/planar/mod.rs b/rust/src/planar/mod.rs index f929d2a..8fcbaad 100644 --- a/rust/src/planar/mod.rs +++ b/rust/src/planar/mod.rs @@ -1,9 +1,6 @@ pub mod alignment; pub mod arrangement; -pub mod rect; pub use alignment::Direction; pub use alignment::Relation; pub use arrangement::Arrangement; -pub use rect::Rect; -pub use arrangement::Window; diff --git a/rust/src/types/mod.rs b/rust/src/types/mod.rs new file mode 100644 index 0000000..4881222 --- /dev/null +++ b/rust/src/types/mod.rs @@ -0,0 +1,6 @@ +pub mod window; +pub mod rect; + +pub use window::Window; +pub use window::Windows; +pub use rect::Rect; diff --git a/rust/src/planar/rect.rs b/rust/src/types/rect.rs similarity index 100% rename from rust/src/planar/rect.rs rename to rust/src/types/rect.rs diff --git a/rust/src/types/window.rs b/rust/src/types/window.rs new file mode 100644 index 0000000..8d76699 --- /dev/null +++ b/rust/src/types/window.rs @@ -0,0 +1,19 @@ +use crate::types::Rect; + +/// A collection of windows, represented as a vector of `Window` structs. +pub type Windows = Vec; + +/// 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, +} + +impl From<&Window> for Rect { + fn from(window: &Window) -> Self { + window.rect + } +} From 54ab984bb4872997c3e5786f9de9b1f73c0bf20b Mon Sep 17 00:00:00 2001 From: Milosz Blizniak Date: Wed, 18 Jun 2025 22:56:47 +0200 Subject: [PATCH 03/18] chore(rs): create backend traits --- rust/src/backend/traits.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 rust/src/backend/traits.rs diff --git a/rust/src/backend/traits.rs b/rust/src/backend/traits.rs new file mode 100644 index 0000000..6ce7ca0 --- /dev/null +++ b/rust/src/backend/traits.rs @@ -0,0 +1,13 @@ +use crate::types::Windows; + +trait GetTabs { + fn get_tabs(&self) -> Windows; +} + +trait GetVisible { + fn get_windows(&self) -> Windows; +} + +trait SetFocus { + fn set_focus(&self, window_id: &u64) -> Result<(), String>; +} From 4b2f7e8b02518b7e485aeaefb9d6307df6321166 Mon Sep 17 00:00:00 2001 From: Milosz Blizniak Date: Sun, 22 Jun 2025 12:54:43 +0200 Subject: [PATCH 04/18] feat(rs): encapsulate i3 in a backend --- rust/src/backend/i3/backend.rs | 60 ++++++++++++++++++++++++++++++++++ rust/src/backend/i3/mod.rs | 7 ++-- rust/src/backend/mod.rs | 1 + rust/src/backend/traits.rs | 10 +++--- rust/src/main.rs | 38 +++++++-------------- rust/src/navigation.rs | 32 ++++++++---------- 6 files changed, 96 insertions(+), 52 deletions(-) create mode 100644 rust/src/backend/i3/backend.rs diff --git a/rust/src/backend/i3/backend.rs b/rust/src/backend/i3/backend.rs new file mode 100644 index 0000000..c7f5bcc --- /dev/null +++ b/rust/src/backend/i3/backend.rs @@ -0,0 +1,60 @@ +use crate::backend::traits::{GetVisible, GetTabs, SetFocus}; +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) -> Windows { + let nodes = compass::available_tabs(&self.root); + compass::to_windows(nodes) + } +} + +impl GetVisible for Backend { + fn get_visible(&self) -> Windows { + let nodes = compass::visible_nodes(&self.root); + 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/backend/i3/mod.rs b/rust/src/backend/i3/mod.rs index 7c7837b..e7ec11d 100644 --- a/rust/src/backend/i3/mod.rs +++ b/rust/src/backend/i3/mod.rs @@ -1,2 +1,5 @@ -pub mod client; -pub mod compass; +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 index c784dbd..b7721b2 100644 --- a/rust/src/backend/mod.rs +++ b/rust/src/backend/mod.rs @@ -1 +1,2 @@ pub mod i3; +pub mod traits; diff --git a/rust/src/backend/traits.rs b/rust/src/backend/traits.rs index 6ce7ca0..c3134bd 100644 --- a/rust/src/backend/traits.rs +++ b/rust/src/backend/traits.rs @@ -1,13 +1,13 @@ use crate::types::Windows; -trait GetTabs { +pub trait GetTabs { fn get_tabs(&self) -> Windows; } -trait GetVisible { - fn get_windows(&self) -> Windows; +pub trait GetVisible { + fn get_visible(&self) -> Windows; } -trait SetFocus { - fn set_focus(&self, window_id: &u64) -> Result<(), String>; +pub trait SetFocus { + fn set_focus(& mut self, window_id: &u64); } diff --git a/rust/src/main.rs b/rust/src/main.rs index 0e71130..e6387ad 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,17 +1,19 @@ #![recursion_limit = "256"] // Required for tests with older serde_json use clap::{Parser, ValueEnum, Subcommand, ArgAction}; use std::process; -use serde_json as json; -mod planar; -mod linear; mod backend; -mod navigation; mod converters; +mod linear; mod logging; +mod navigation; +mod planar; mod types; -use crate::logging::ResultExt; +use crate::backend::i3; +use crate::backend::traits::SetFocus; + +use clap::{Parser, ValueEnum, Subcommand}; /// i3switch - A simple command-line utility to switch focus in i3 window manager #[derive(Parser, Debug)] @@ -102,43 +104,27 @@ fn main() { let wrap = bool::from(cli.wrap); - // 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 = backend::i3::client::Client::new(&i3_path.trim()) - .expect_log("Failed to connect to i3 IPC server"); - let tree = client.request(backend::i3::client::Request::GetTree, "") - .expect_log("Failed to get i3 tree JSON"); - - // Parse the i3 tree to get the current workspace and window information - let tree = json::from_str::(&tree) - .expect_log("Failed to parse i3 tree JSON"); + let mut backend = i3::Backend::new(); // Determine the window ID to switch focus to based on the command let window_id: u64; if let Some(direction) = get_linear_direction(&cli.root_command) { logging::info!("Switching focus in linear direction: {:?}", direction); - window_id = navigation::get_window_to_switch_to(&tree, direction, wrap); + window_id = navigation::get_window_to_switch_to(&backend, direction, wrap); } else if let Some(direction) = get_planar_direction(&cli.root_command) { logging::info!("Switching focus in planar direction: {:?}", direction); - window_id = navigation::get_window_in_direction(&tree, direction, wrap); + window_id = navigation::get_window_in_direction(&backend, direction, wrap); } else if let RootCommand::Number { number } = &cli.root_command { logging::info!("Switching focus to window number: {}", number); if wrap { logging::warning!("Wrap option is ignored for number switching."); } - window_id = navigation::get_window_of_number(&tree, *number as usize); + window_id = navigation::get_window_of_number(&backend, *number as usize); } else { logging::critical!("Invalid command provided: {:?}", cli.root_command); } - // Focus the window with the determined ID - logging::info!("Focusing window with ID: {}", window_id); - let payload = format!("[con_id={}] focus", window_id); - client.request(backend::i3::client::Request::Command, &payload) - .expect_log("Failed to send focus command"); + backend.set_focus(&window_id); std::process::exit(0); } diff --git a/rust/src/navigation.rs b/rust/src/navigation.rs index e858e8c..cc154e0 100644 --- a/rust/src/navigation.rs +++ b/rust/src/navigation.rs @@ -1,16 +1,14 @@ -use crate::backend; +use crate::backend::traits::{GetVisible, GetTabs}; use crate::converters; use crate::linear; -use crate::planar; use crate::logging; - -use serde_json as json; +use crate::planar; /// Get window to switch to in tabbed, stacked or floating layout. /// If `wrap` is true, it will wrap around to the first/last window if no next/previous window is /// available. -pub fn get_window_to_switch_to(tree: &json::Value, direction: linear::Direction, wrap: bool) -> u64 { - let sequence = get_linear_sequence(tree); +pub fn get_window_to_switch_to(backend: &B, direction: linear::Direction, wrap: bool) -> u64 { + let sequence = get_linear_sequence(backend); if let Some(window_id) = sequence.next(direction) { window_id } else if wrap { @@ -32,8 +30,8 @@ pub fn get_window_to_switch_to(tree: &json::Value, direction: linear::Direction, /// If no window is available in the specified direction, it will print an error message and exit /// the program. /// If `wrap` is false, it will print an info message and exit the program without switching. -pub fn get_window_in_direction(tree: &json::Value, direction: planar::Direction, wrap: bool) -> u64 { - let mut arrangement = get_planar_arrangement(tree); +pub fn get_window_in_direction(backend: &B, direction: planar::Direction, wrap: bool) -> u64 { + let mut arrangement = get_planar_arrangement(backend); if let Some(window) = arrangement.next(direction) { window.id } else if wrap { @@ -51,8 +49,8 @@ pub fn get_window_in_direction(tree: &json::Value, direction: planar::Direction, /// Get the window ID of a specific window number for tabbed, stacked and floating layouts. /// If the number is out of bounds, it will print an error message and exit the program. -pub fn get_window_of_number(tree: &json::Value, number: usize) -> u64 { - let sequence = get_linear_sequence(tree); +pub fn get_window_of_number(backend: &B, number: usize) -> u64 { + let sequence = get_linear_sequence(backend); if number >= sequence.size() { logging::critical!("Invalid window number: {}. There are only {} windows available.", number, sequence.size()); } @@ -62,9 +60,8 @@ pub fn get_window_of_number(tree: &json::Value, number: usize) -> u64 { /// Get the linear sequence of windows based on the i3 tree structure. /// If there are focused floating windows, it will return the sequence of those windows. /// Otherwise, it will return the sequence of available tabs in the current workspace. -fn get_linear_sequence(tree: &json::Value) -> linear::Sequence { - let visible_nodes = backend::i3::compass::visible_nodes(tree); - let windows = backend::i3::compass::to_windows(visible_nodes); +fn get_linear_sequence(backend: &B) -> linear::Sequence { + let windows = backend.get_visible(); let mut floating = converters::floating(&windows); logging::debug!("Floating windows: {:?}", floating); @@ -75,8 +72,7 @@ fn get_linear_sequence(tree: &json::Value) -> linear::Sequence { converters::as_sequence(&floating) } else { logging::debug!("Using available tabs for linear sequence."); - let nodes = backend::i3::compass::available_tabs(tree); - let windows = backend::i3::compass::to_windows(nodes); + let windows = backend.get_tabs(); converters::as_sequence(&windows) } } @@ -84,12 +80,10 @@ fn get_linear_sequence(tree: &json::Value) -> linear::Sequence { /// Get the planar arrangement of windows based on the i3 tree structure. /// If there are focused floating windows, it will return the arrangement of those windows. /// Otherwise, it will return the arrangement of visible windows in the current workspace. -fn get_planar_arrangement(tree: &json::Value) -> planar::Arrangement { - let visible_nodes = backend::i3::compass::visible_nodes(tree); - let windows = backend::i3::compass::to_windows(visible_nodes); +fn get_planar_arrangement(backend: &B) -> planar::Arrangement { + let windows = backend.get_visible(); let floating = converters::floating(&windows); - // TODO: Consider allowing directional movement in mixed mode (floating + tiled). if converters::any_focused(&floating) { logging::debug!("Using floating windows for planar arrangement."); return converters::as_arrangement(&floating, planar::Relation::Center); From 4c0bdc41aafa485555dda8f95da3e9603189634b Mon Sep 17 00:00:00 2001 From: Milosz Blizniak Date: Thu, 19 Jun 2025 10:14:53 +0200 Subject: [PATCH 05/18] refactor(rs): move cli to its own module --- rust/src/cli.rs | 102 +++++++++++++++++++++++++++++++++++++++++++++++ rust/src/main.rs | 100 ++++------------------------------------------ 2 files changed, 110 insertions(+), 92 deletions(-) create mode 100644 rust/src/cli.rs diff --git a/rust/src/cli.rs b/rust/src/cli.rs new file mode 100644 index 0000000..49148a1 --- /dev/null +++ b/rust/src/cli.rs @@ -0,0 +1,102 @@ +use clap::{Parser, ValueEnum, Subcommand, ArgAction}; +use crate::planar; +use crate::linear; + +// Public interface for the CLI module + +/// We cant use Cli::parse() directly in the main function because it requires +/// the command line arguments to be passed in, which is not possible +/// in a library context. Instead, we define this function to be used +/// in the main function to parse the command line arguments. +pub fn get_parsed_command() -> Cli { + Cli::parse() +} + +/// Determine if the wrap option is enabled +pub fn wrap(cli: &Cli) -> bool { + match cli.wrap { + WrapOption::Wrap => true, + WrapOption::NoWrap => false, + } +} + +/// Convert the root command to a planar direction if applicable +pub fn planar_direction(cli: &Cli) -> Option { + match cli.root_command { + RootCommand::Right => Some(planar::Direction::Right), + RootCommand::Down => Some(planar::Direction::Down), + RootCommand::Left => Some(planar::Direction::Left), + RootCommand::Up => Some(planar::Direction::Up), + _ => None, + } +} + +/// Convert the root command to a linear direction if applicable +pub fn linear_direction(cli: &Cli) -> Option { + match cli.root_command { + RootCommand::Next => Some(linear::Direction::Next), + RootCommand::Prev => Some(linear::Direction::Prev), + _ => None, + } +} + +/// Get the specific tab/window number to switch focus to if applicable +pub fn number(cli: &Cli) -> Option { + match cli.root_command { + RootCommand::Number { number } => Some(number), + _ => None, + } +} + +// Define the command-line interface (CLI) structure using Clap + +/// i3switch - A simple command-line utility to switch focus in i3 window manager +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +pub struct Cli { + /// Root command for focus switching + #[clap(subcommand)] + root_command: RootCommand, + + /// Wrap around when reaching the edge of the workspace + #[clap(arg_enum, action=ArgAction::Set, default_value = "nowrap", global = true)] + wrap: WrapOption, +} + +/// Define the subcommand for switching focus to a specific tab/window number +#[derive(Subcommand, Debug)] +enum RootCommand { + // Move focus to next/prev tab/window + /// Move focus to next tab/window + Next, + /// Move focus to previous tab/window + Prev, + + // Move focus in a specific direction + /// Move focus right + Right, + /// Move focus down + Down, + /// Move focus left + Left, + /// Move focus up + Up, + + // Move focus to a specific tab/window number + /// Switch focus to a specific tab/window number + Number { + /// The tab/window number to switch focus to + #[clap(value_parser, value_name="num")] + number: usize, + }, +} + +/// Define the wrap option for focus switching +#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)] +#[clap(rename_all = "lower")] +enum WrapOption { + /// Enable wrap around + Wrap, + /// Disable wrap around + NoWrap, +} diff --git a/rust/src/main.rs b/rust/src/main.rs index e6387ad..8a3ab41 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,6 +1,4 @@ #![recursion_limit = "256"] // Required for tests with older serde_json -use clap::{Parser, ValueEnum, Subcommand, ArgAction}; -use std::process; mod backend; mod converters; @@ -9,119 +7,37 @@ mod logging; mod navigation; mod planar; mod types; +mod cli; use crate::backend::i3; use crate::backend::traits::SetFocus; -use clap::{Parser, ValueEnum, Subcommand}; - -/// i3switch - A simple command-line utility to switch focus in i3 window manager -#[derive(Parser, Debug)] -#[clap(author, version, about, long_about = None)] -struct Cli { - /// Root command for focus switching - #[clap(subcommand)] - root_command: RootCommand, - - /// Wrap around when reaching the edge of the workspace - #[clap(arg_enum, action=ArgAction::Set, default_value = "nowrap", global = true)] - wrap: WrapOption, -} - -/// Define the subcommand for switching focus to a specific tab/window number -#[derive(Subcommand, Debug)] -enum RootCommand { - // Move focus to next/prev tab/window - /// Move focus to next tab/window - Next, - /// Move focus to previous tab/window - Prev, - - // Move focus in a specific direction - /// Move focus right - Right, - /// Move focus down - Down, - /// Move focus left - Left, - /// Move focus up - Up, - - // Move focus to a specific tab/window number - /// Switch focus to a specific tab/window number - Number { - /// The tab/window number to switch focus to - #[clap(value_parser, value_name="num")] - number: u32, - }, -} - -/// Define the wrap option for focus switching -#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)] -#[clap(rename_all = "lower")] -enum WrapOption { - /// Enable wrap around - Wrap, - /// Disable wrap around - NoWrap, -} - -/// Implement conversion from WrapOption to bool -impl From for bool { - fn from(option: WrapOption) -> Self { - match option { - WrapOption::Wrap => true, - WrapOption::NoWrap => false, - } - } -} - -/// Get the linear direction based on the command -fn get_linear_direction(command: &RootCommand) -> Option { - match command { - RootCommand::Next => Some(linear::Direction::Next), - RootCommand::Prev => Some(linear::Direction::Prev), - _ => None, - } -} - -/// Get the planar direction based on the command -fn get_planar_direction(command: &RootCommand) -> Option { - match command { - RootCommand::Right => Some(planar::Direction::Right), - RootCommand::Down => Some(planar::Direction::Down), - RootCommand::Left => Some(planar::Direction::Left), - RootCommand::Up => Some(planar::Direction::Up), - _ => None, - } -} - fn main() { // Initialize logging logging::init(); - let cli = Cli::parse(); + let cli = cli::get_parsed_command(); - let wrap = bool::from(cli.wrap); + let wrap = cli::wrap(&cli); let mut backend = i3::Backend::new(); // Determine the window ID to switch focus to based on the command let window_id: u64; - if let Some(direction) = get_linear_direction(&cli.root_command) { + if let Some(direction) = cli::linear_direction(&cli) { logging::info!("Switching focus in linear direction: {:?}", direction); window_id = navigation::get_window_to_switch_to(&backend, direction, wrap); - } else if let Some(direction) = get_planar_direction(&cli.root_command) { + } else if let Some(direction) = cli::planar_direction(&cli) { logging::info!("Switching focus in planar direction: {:?}", direction); window_id = navigation::get_window_in_direction(&backend, direction, wrap); - } else if let RootCommand::Number { number } = &cli.root_command { + } else if let Some(number) = cli::number(&cli) { logging::info!("Switching focus to window number: {}", number); if wrap { logging::warning!("Wrap option is ignored for number switching."); } - window_id = navigation::get_window_of_number(&backend, *number as usize); + window_id = navigation::get_window_of_number(&backend, number); } else { - logging::critical!("Invalid command provided: {:?}", cli.root_command); + logging::critical!("Invalid command provided: {:?}", cli); } backend.set_focus(&window_id); From 97d17baf344d03c8240282a37e6d22fd7dfff145 Mon Sep 17 00:00:00 2001 From: Milosz Blizniak Date: Fri, 20 Jun 2025 11:06:13 +0200 Subject: [PATCH 06/18] feat(rs): add wmctl backend --- rust/Cargo.toml.in | 4 +- rust/src/backend/mod.rs | 1 + rust/src/backend/wmctl/backend.rs | 124 ++++++++++++++++++++++++++++++ rust/src/backend/wmctl/mod.rs | 3 + 4 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 rust/src/backend/wmctl/backend.rs create mode 100644 rust/src/backend/wmctl/mod.rs diff --git a/rust/Cargo.toml.in b/rust/Cargo.toml.in index fa66446..9d7fb20 100644 --- a/rust/Cargo.toml.in +++ b/rust/Cargo.toml.in @@ -5,9 +5,11 @@ edition = "2021" [dependencies] clap = { version = "3.2", features = ["derive"] } -serde_json = "1.0" +libwmctl = "0.0.51" log = "0.4" +serde_json = "1.0" simplelog = "0.12" +x11rb = "0.13" [dev-dependencies] ctor = "0.1" diff --git a/rust/src/backend/mod.rs b/rust/src/backend/mod.rs index b7721b2..ed94fa8 100644 --- a/rust/src/backend/mod.rs +++ b/rust/src/backend/mod.rs @@ -1,2 +1,3 @@ pub mod i3; +pub mod wmctl; pub mod traits; diff --git a/rust/src/backend/wmctl/backend.rs b/rust/src/backend/wmctl/backend.rs new file mode 100644 index 0000000..6e83dbe --- /dev/null +++ b/rust/src/backend/wmctl/backend.rs @@ -0,0 +1,124 @@ +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::{GetVisible, SetFocus}; +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::(); + + Backend { windows, visibility } + } +} + +impl GetVisible for Backend { + fn get_visible(&self) -> Windows { + 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..9b71644 --- /dev/null +++ b/rust/src/backend/wmctl/mod.rs @@ -0,0 +1,3 @@ +pub mod backend; + +use crate::backend::wmctl::backend::Backend; From ecc69a79246cec88b945e623f08d6229dcef1864 Mon Sep 17 00:00:00 2001 From: Milosz Blizniak Date: Sun, 22 Jun 2025 09:10:32 +0200 Subject: [PATCH 07/18] feat(rs): support backend selection --- rust/src/backend/backend.rs | 48 +++++++++++++++++++++++++++++++ rust/src/backend/i3/backend.rs | 10 +++---- rust/src/backend/mod.rs | 7 +++++ rust/src/backend/traits.rs | 4 +-- rust/src/backend/wmctl/backend.rs | 23 ++++++++++----- rust/src/backend/wmctl/mod.rs | 2 +- rust/src/main.rs | 5 ++-- rust/src/navigation.rs | 9 ++++-- 8 files changed, 87 insertions(+), 21 deletions(-) create mode 100644 rust/src/backend/backend.rs diff --git a/rust/src/backend/backend.rs b/rust/src/backend/backend.rs new file mode 100644 index 0000000..19c5498 --- /dev/null +++ b/rust/src/backend/backend.rs @@ -0,0 +1,48 @@ +use crate::backend::i3; +use crate::backend::wmctl; +use crate::backend::traits::*; +use crate::types::Windows; + +pub enum UsedBackend { + I3(i3::Backend), + WmCtl(wmctl::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 { + UsedBackend::I3(ref i3) => i3.get_tabs(), + UsedBackend::WmCtl(ref wmctl) => wmctl.get_tabs(), + } + } +} + +impl GetVisible for Backend { + fn get_visible(&self) -> Result { + match self.used_backend { + UsedBackend::I3(ref i3) => i3.get_visible(), + UsedBackend::WmCtl(ref wmctl) => wmctl.get_visible(), + } + } +} + +impl SetFocus for Backend { + fn set_focus(& mut self, id: &u64) { + match self.used_backend { + UsedBackend::I3(ref mut i3) => i3.set_focus(id), + UsedBackend::WmCtl(ref mut wmctl) => wmctl.set_focus(id), + } + } +} diff --git a/rust/src/backend/i3/backend.rs b/rust/src/backend/i3/backend.rs index c7f5bcc..48d3a11 100644 --- a/rust/src/backend/i3/backend.rs +++ b/rust/src/backend/i3/backend.rs @@ -1,4 +1,4 @@ -use crate::backend::traits::{GetVisible, GetTabs, SetFocus}; +use crate::backend::traits::*; use crate::logging::ResultExt; use crate::logging; use crate::types::Windows; @@ -36,16 +36,16 @@ impl Backend { } impl GetTabs for Backend { - fn get_tabs(&self) -> Windows { + fn get_tabs(&self) -> Result { let nodes = compass::available_tabs(&self.root); - compass::to_windows(nodes) + Ok(compass::to_windows(nodes)) } } impl GetVisible for Backend { - fn get_visible(&self) -> Windows { + fn get_visible(&self) -> Result { let nodes = compass::visible_nodes(&self.root); - compass::to_windows(nodes) + Ok(compass::to_windows(nodes)) } } diff --git a/rust/src/backend/mod.rs b/rust/src/backend/mod.rs index ed94fa8..e906309 100644 --- a/rust/src/backend/mod.rs +++ b/rust/src/backend/mod.rs @@ -1,3 +1,10 @@ pub mod i3; pub mod wmctl; pub mod traits; +pub mod backend; + +pub use i3::Backend as I3Backend; +pub use wmctl::Backend as WmctlBackend; +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 index c3134bd..b89271c 100644 --- a/rust/src/backend/traits.rs +++ b/rust/src/backend/traits.rs @@ -1,11 +1,11 @@ use crate::types::Windows; pub trait GetTabs { - fn get_tabs(&self) -> Windows; + fn get_tabs(&self) -> Result; } pub trait GetVisible { - fn get_visible(&self) -> Windows; + fn get_visible(&self) -> Result; } pub trait SetFocus { diff --git a/rust/src/backend/wmctl/backend.rs b/rust/src/backend/wmctl/backend.rs index 6e83dbe..3452560 100644 --- a/rust/src/backend/wmctl/backend.rs +++ b/rust/src/backend/wmctl/backend.rs @@ -4,7 +4,7 @@ use x11rb::rust_connection::RustConnection; use x11rb::protocol::xproto::ClientMessageEvent; use libwmctl::prelude::{windows, active, State}; -use crate::backend::traits::{GetVisible, SetFocus}; +use crate::backend::traits::*; use crate::types::{Rect, Window, Windows}; pub struct Backend { @@ -13,7 +13,7 @@ pub struct Backend { } impl Backend { - pub fn new() -> Self { + fn new() -> Self { let show_hidden = false; let wm_windows = windows(show_hidden) .expect("Failed to connect to the window manager"); @@ -24,7 +24,7 @@ impl Backend { let windows = wm_windows.iter() .inspect(|w| { visibility.push(is_visible(&w.state() - .expect("Failed to get window state"))); + .expect("Failed to get window state"))); }) .map(|w| { let wm_win_geometry = w.geometry() @@ -47,13 +47,22 @@ impl Backend { }) .collect::(); - Backend { windows, visibility } + 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) -> Windows { - self.windows.iter() + fn get_visible(&self) -> Result { + Ok(self.windows.iter() .enumerate() .filter_map(|(i, window)| { if self.visibility[i] { @@ -62,7 +71,7 @@ impl GetVisible for Backend { None } }) - .collect() + .collect()) } } diff --git a/rust/src/backend/wmctl/mod.rs b/rust/src/backend/wmctl/mod.rs index 9b71644..f6eee67 100644 --- a/rust/src/backend/wmctl/mod.rs +++ b/rust/src/backend/wmctl/mod.rs @@ -1,3 +1,3 @@ pub mod backend; -use crate::backend::wmctl::backend::Backend; +pub use crate::backend::wmctl::backend::Backend; diff --git a/rust/src/main.rs b/rust/src/main.rs index 8a3ab41..66e1b06 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -9,8 +9,7 @@ mod planar; mod types; mod cli; -use crate::backend::i3; -use crate::backend::traits::SetFocus; +use crate::backend::*; fn main() { // Initialize logging @@ -20,7 +19,7 @@ fn main() { let wrap = cli::wrap(&cli); - let mut backend = i3::Backend::new(); + let mut backend = Backend::new(UsedBackend::I3(I3Backend::new())); // Determine the window ID to switch focus to based on the command let window_id: u64; diff --git a/rust/src/navigation.rs b/rust/src/navigation.rs index cc154e0..a8707ee 100644 --- a/rust/src/navigation.rs +++ b/rust/src/navigation.rs @@ -61,7 +61,8 @@ pub fn get_window_of_number(backend: &B, number: usize) /// If there are focused floating windows, it will return the sequence of those windows. /// Otherwise, it will return the sequence of available tabs in the current workspace. fn get_linear_sequence(backend: &B) -> linear::Sequence { - let windows = backend.get_visible(); + let windows = backend.get_visible() + .expect("Failed to get visible windows from backend"); let mut floating = converters::floating(&windows); logging::debug!("Floating windows: {:?}", floating); @@ -72,7 +73,8 @@ fn get_linear_sequence(backend: &B) -> linear::Sequence converters::as_sequence(&floating) } else { logging::debug!("Using available tabs for linear sequence."); - let windows = backend.get_tabs(); + let windows = backend.get_tabs() + .expect("Failed to get tabs from backend"); converters::as_sequence(&windows) } } @@ -81,7 +83,8 @@ fn get_linear_sequence(backend: &B) -> linear::Sequence /// If there are focused floating windows, it will return the arrangement of those windows. /// Otherwise, it will return the arrangement of visible windows in the current workspace. fn get_planar_arrangement(backend: &B) -> planar::Arrangement { - let windows = backend.get_visible(); + let windows = backend.get_visible() + .expect("Failed to get visible windows from backend"); let floating = converters::floating(&windows); if converters::any_focused(&floating) { From e3bcc0e3c659e82c56f06fead1e067c837303632 Mon Sep 17 00:00:00 2001 From: Milosz Blizniak Date: Sun, 22 Jun 2025 13:18:25 +0200 Subject: [PATCH 08/18] feat(rs): add backend cli option --- rust/src/backend/wmctl/backend.rs | 2 +- rust/src/cli.rs | 13 +++++++++++++ rust/src/main.rs | 13 ++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/rust/src/backend/wmctl/backend.rs b/rust/src/backend/wmctl/backend.rs index 3452560..b828617 100644 --- a/rust/src/backend/wmctl/backend.rs +++ b/rust/src/backend/wmctl/backend.rs @@ -13,7 +13,7 @@ pub struct Backend { } impl Backend { - fn new() -> Self { + pub fn new() -> Self { let show_hidden = false; let wm_windows = windows(show_hidden) .expect("Failed to connect to the window manager"); diff --git a/rust/src/cli.rs b/rust/src/cli.rs index 49148a1..f24f253 100644 --- a/rust/src/cli.rs +++ b/rust/src/cli.rs @@ -61,6 +61,10 @@ pub struct Cli { /// Wrap around when reaching the edge of the workspace #[clap(arg_enum, action=ArgAction::Set, default_value = "nowrap", global = true)] wrap: WrapOption, + + /// Wrap around when reaching the edge of the workspace + #[clap(arg_enum, action=ArgAction::Set, default_value = "i3", global = true)] + pub backend: BackendOption, } /// Define the subcommand for switching focus to a specific tab/window number @@ -100,3 +104,12 @@ enum WrapOption { /// Disable wrap around NoWrap, } + +#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)] +#[clap(rename_all = "lower")] +pub enum BackendOption { + /// Use the i3 IPC backend + I3, + /// Use the sway IPC backend + WmCtrl, +} diff --git a/rust/src/main.rs b/rust/src/main.rs index 66e1b06..43f009c 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -19,7 +19,18 @@ fn main() { let wrap = cli::wrap(&cli); - let mut backend = Backend::new(UsedBackend::I3(I3Backend::new())); + let mut backend: Backend; + let selected_backend = cli.backend; + match selected_backend { + cli::BackendOption::I3 => { + logging::info!("Using I3 backend."); + backend = Backend::new(UsedBackend::I3(I3Backend::new())); + } + cli::BackendOption::WmCtrl => { + logging::info!("Using WmCtrl backend."); + backend = Backend::new(UsedBackend::WmCtl(WmctlBackend::new())); + } + } // Determine the window ID to switch focus to based on the command let window_id: u64; From fd1c1132e84e72f659fd4bfdef757c43e3563ec2 Mon Sep 17 00:00:00 2001 From: Milosz Blizniak Date: Tue, 24 Jun 2025 10:53:18 +0200 Subject: [PATCH 09/18] feat(rs): add xcb backend --- rust/Cargo.toml.in | 1 + rust/src/backend/mod.rs | 1 + rust/src/backend/xcb/backend.rs | 73 ++++++++++++ rust/src/backend/xcb/client.rs | 198 ++++++++++++++++++++++++++++++++ rust/src/backend/xcb/mod.rs | 2 + 5 files changed, 275 insertions(+) create mode 100644 rust/src/backend/xcb/backend.rs create mode 100644 rust/src/backend/xcb/client.rs create mode 100644 rust/src/backend/xcb/mod.rs diff --git a/rust/Cargo.toml.in b/rust/Cargo.toml.in index 9d7fb20..aa6bc8f 100644 --- a/rust/Cargo.toml.in +++ b/rust/Cargo.toml.in @@ -10,6 +10,7 @@ log = "0.4" serde_json = "1.0" simplelog = "0.12" x11rb = "0.13" +xcb = "1" [dev-dependencies] ctor = "0.1" diff --git a/rust/src/backend/mod.rs b/rust/src/backend/mod.rs index e906309..916e221 100644 --- a/rust/src/backend/mod.rs +++ b/rust/src/backend/mod.rs @@ -1,5 +1,6 @@ pub mod i3; pub mod wmctl; +pub mod xcb; pub mod traits; pub mod backend; diff --git a/rust/src/backend/xcb/backend.rs b/rust/src/backend/xcb/backend.rs new file mode 100644 index 0000000..25cf9cc --- /dev/null +++ b/rust/src/backend/xcb/backend.rs @@ -0,0 +1,73 @@ +use super::client::Client; +use crate::types::{Window, Windows}; +use crate::backend::traits::*; +use xcb::Xid; +use xcb::XidNew; +use xcb::x::Window as XWindow; + +struct Backend { + client: Client, + windows: Windows, +} + +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 x_windows = client.get_client_list(); + + // Get the list of windows + let rects = client.get_windows_rects(&x_windows); + + // Get the list of properties for the windows + let properties = client.get_windows_properties(&x_windows); + + // Create the Windows structure + let windows = x_windows.iter() + .zip(rects.iter()) + .zip(properties.iter()) + .map(|((x_window, rect), properties)| { + Window { + id: x_window.resource_id().into(), + rect: rect.clone(), + floating: client.is_floating(&properties), + focused: *x_window == active_window, + } + }) + .collect(); + + Backend { client, windows } + } +} + +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) { + // Set focus to the specified window + unsafe { + self.client.set_focus(XWindow::new(*window_id as u32)) + .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..dee5cbd --- /dev/null +++ b/rust/src/backend/xcb/client.rs @@ -0,0 +1,198 @@ +use xcb::{x, Connection}; +use crate::types::Rect; + +// 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_MAXIMIZED_HORZ => b"_NET_WM_STATE_MAXIMIZED_HORZ", + pub _NET_WM_STATE_MAXIMIZED_VERT => b"_NET_WM_STATE_MAXIMIZED_VERT", + } +} + +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, + ] { + if !supported_atoms.contains(&atom) { + let cookie = self.conn.send_request(&x::GetAtomName { + atom: *atom, + }); + let atom_name = self.conn.wait_for_reply(cookie) + .map(|reply| reply.name().to_string()) + .unwrap_or_else(|_| "unknown".to_string()); + return Err(format!("Required atom '{}' is not supported", atom_name)); + } + } + 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) -> Option { + // 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 + }); + + match self.conn.wait_for_reply(cookie) { + Ok(reply) => { + if reply.length() > 0 { + Some(reply.value::()[0]) + } else { + None + } + }, + Err(_) => None, + } + } + + pub fn get_windows_rects(&self, window_ids: &[x::Window]) -> Vec { + // Build requests for all window geometries + let mut cookies = Vec::new(); + for &window_id in window_ids { + cookies.push(self.conn.send_request(&x::GetGeometry { + drawable: x::Drawable::Window(window_id), + })); + } + // Wait for all replies + let mut rects = Vec::new(); + for cookie in cookies { + match self.conn.wait_for_reply(cookie) { + Ok(reply) => { + let rect = Rect { + x: reply.x() as i32, + y: reply.y() as i32, + w: reply.width() as i32, + h: reply.height() as i32, + }; + rects.push(rect); + }, + Err(err) => { + eprintln!("Failed to get geometry: {}", err); + } + } + } + rects + } + + pub fn get_windows_properties(&self, window_ids: &[x::Window]) -> Vec> { + // Build requests to get properties for each window + let mut cookies = Vec::new(); + for &window_id in window_ids { + cookies.push(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 + })); + } + + // Wait for all replies and collect properties + let mut properties = Vec::new(); + for cookie in cookies { + match self.conn.wait_for_reply(cookie) { + Ok(reply) => { + properties.push(reply.value().to_vec()); + }, + Err(err) => { + eprintln!("Failed to get properties: {}", err); + properties.push(vec![]); + } + } + } + properties + } + + pub fn is_floating(&self, properties: &Vec) -> bool { + // Check if the window is floating + ! (properties.contains(&self.atoms._NET_WM_STATE_MAXIMIZED_HORZ.into()) || + properties.contains(&self.atoms._NET_WM_STATE_MAXIMIZED_VERT.into())) + } + + 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)), + } + } +} diff --git a/rust/src/backend/xcb/mod.rs b/rust/src/backend/xcb/mod.rs new file mode 100644 index 0000000..20c408b --- /dev/null +++ b/rust/src/backend/xcb/mod.rs @@ -0,0 +1,2 @@ +pub mod backend; +mod client; From 73e5fdd6e38afc0132e0dc3486d5d7f67fe4bfe1 Mon Sep 17 00:00:00 2001 From: Milosz Blizniak Date: Tue, 24 Jun 2025 13:30:41 +0200 Subject: [PATCH 10/18] fix(rs): use normalized positions and withdrawn state i3 window manager doesn't report proper positions for contenarized windows, so we have to translate the positions to root window. As far as visibility goes, some i3 builds (arch i3-gaps) dont show window hints as expected (_NET_WM_STATE_HIDDEN, _NET_WM_STATE_MAXIMIZED_VERT, _NET_WM_STATE_MAXIMIZED_HORZ), this makes distinction through EWMH for visible windows impossible. Instead we use WM_STATE_WITHDRAWN, to distinguish between hidden windows and not. Known-Bugs: Floating state is not properly detected. --- rust/src/backend/xcb/backend.rs | 55 +++++++++++----- rust/src/backend/xcb/client.rs | 109 +++++++++++++++++++++++++------- rust/src/backend/xcb/mod.rs | 2 + 3 files changed, 129 insertions(+), 37 deletions(-) diff --git a/rust/src/backend/xcb/backend.rs b/rust/src/backend/xcb/backend.rs index 25cf9cc..e8284e5 100644 --- a/rust/src/backend/xcb/backend.rs +++ b/rust/src/backend/xcb/backend.rs @@ -2,12 +2,13 @@ use super::client::Client; use crate::types::{Window, Windows}; use crate::backend::traits::*; use xcb::Xid; -use xcb::XidNew; use xcb::x::Window as XWindow; +use std::collections::HashMap; -struct Backend { +pub struct Backend { client: Client, windows: Windows, + xwindows: HashMap, } impl Backend { @@ -27,26 +28,47 @@ impl Backend { let x_windows = client.get_client_list(); // Get the list of windows - let rects = client.get_windows_rects(&x_windows); + let rects = client.get_normalized_windows_rects(&x_windows); // Get the list of properties for the windows let properties = client.get_windows_properties(&x_windows); + // Get window withdrawn states + let withdrawns = client.get_windows_withdrawn(&x_windows); + + // Create a map to hold the XWindow to u64 resource ID mapping + let mut xwindows: HashMap = HashMap::new(); + // Create the Windows structure - let windows = x_windows.iter() + let windows: Windows = x_windows.iter() .zip(rects.iter()) .zip(properties.iter()) - .map(|((x_window, rect), properties)| { - Window { - id: x_window.resource_id().into(), - rect: rect.clone(), - floating: client.is_floating(&properties), - focused: *x_window == active_window, + .zip(withdrawns.iter()) + .filter_map(|(((x_window, rect), properties), withdrawn)| { + println!("Properties are: {:?}", properties); + match client.is_visible(properties) && !withdrawn { + false => return None, // Skip invisible windows + true => { + let window_id: u64 = x_window.resource_id().into(); + xwindows.insert(window_id.clone(), *x_window); + Some(Window { + id: window_id, + rect: *rect, + floating: client.is_floating(&properties), + focused: *x_window == active_window, + }) + } } }) .collect(); - Backend { client, windows } + // Log the windows + for window in &windows { + println!("Window ID: {}, Rect: {}, Floating: {}, Focused: {}", + window.id, window.rect.to_string(), window.floating, window.focused); + } + + Backend { client, windows, xwindows } } } @@ -64,10 +86,13 @@ impl GetTabs for Backend { impl SetFocus for Backend { fn set_focus(&mut self, window_id: &u64) { - // Set focus to the specified window - unsafe { - self.client.set_focus(XWindow::new(*window_id as u32)) - .expect("Failed to set focus"); + // Check if the window ID exists in the map + if !self.xwindows.contains_key(window_id) { + eprintln!("Window ID {} does not exist", window_id); + return; } + // Set focus to the specified window + self.client.set_focus(self.xwindows[window_id]) + .expect("Failed to set focus"); } } diff --git a/rust/src/backend/xcb/client.rs b/rust/src/backend/xcb/client.rs index dee5cbd..921cc10 100644 --- a/rust/src/backend/xcb/client.rs +++ b/rust/src/backend/xcb/client.rs @@ -6,12 +6,15 @@ use crate::types::Rect; 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_MAXIMIZED_HORZ => b"_NET_WM_STATE_MAXIMIZED_HORZ", - pub _NET_WM_STATE_MAXIMIZED_VERT => b"_NET_WM_STATE_MAXIMIZED_VERT", + 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", } } @@ -47,7 +50,7 @@ impl Client { let cookie = self.conn.send_request(&x::GetProperty { delete: false, window: self.root, - property: self.atoms._NET_SUPPORTED, + property: self.atoms._net_supported, r#type: x::ATOM_ATOM, long_offset: 0, long_length: 1024, // Number of atoms to fetch @@ -58,11 +61,11 @@ impl Client { // 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_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, ] { if !supported_atoms.contains(&atom) { let cookie = self.conn.send_request(&x::GetAtomName { @@ -82,7 +85,7 @@ impl Client { let cookie = self.conn.send_request(&x::GetProperty { delete: false, window: self.root, - property: self.atoms._NET_CLIENT_LIST, + property: self.atoms._net_client_list, r#type: x::ATOM_WINDOW, long_offset: 0, long_length: 1024, // Number of windows to fetch @@ -99,7 +102,7 @@ impl Client { let cookie = self.conn.send_request(&x::GetProperty { delete: false, window: self.root, - property: self.atoms._NET_ACTIVE_WINDOW, + property: self.atoms._net_active_window, r#type: x::ATOM_WINDOW, long_offset: 0, long_length: 1, // Only one active window @@ -117,8 +120,38 @@ impl Client { } } - pub fn get_windows_rects(&self, window_ids: &[x::Window]) -> Vec { - // Build requests for all window geometries + pub fn get_normalized_windows_rects(&self, window_ids: &[x::Window]) -> Vec { + let mut rects = self.get_windows_rects(window_ids); + // Translate rects to root window coordinates + let mut cookies = Vec::new(); + for &window_id in window_ids { + cookies.push(self.conn.send_request(&x::TranslateCoordinates { + src_window: window_id, + dst_window: self.root, + src_x: 0, + src_y: 0, + })); + } + // Wait for all replies + let mut i: usize = 0; + for cookie in cookies { + match self.conn.wait_for_reply(cookie) { + Ok(reply) => { + // Adjust the geometry based on the translation + rects[i].x += reply.dst_x() as i32; + rects[i].y += reply.dst_y() as i32; + }, + Err(err) => { + eprintln!("Failed to translate coordinates: {}", err); + } + } + i += 1; + } + rects + } + + fn get_windows_rects(&self, window_ids: &[x::Window]) -> Vec { + // Build requests for all window rects let mut cookies = Vec::new(); for &window_id in window_ids { cookies.push(self.conn.send_request(&x::GetGeometry { @@ -130,13 +163,12 @@ impl Client { for cookie in cookies { match self.conn.wait_for_reply(cookie) { Ok(reply) => { - let rect = Rect { + rects.push(Rect { x: reply.x() as i32, y: reply.y() as i32, w: reply.width() as i32, h: reply.height() as i32, - }; - rects.push(rect); + }); }, Err(err) => { eprintln!("Failed to get geometry: {}", err); @@ -146,6 +178,34 @@ impl Client { rects } + pub fn get_windows_withdrawn(&self, window_ids: &[x::Window]) -> Vec { + let cookies = window_ids.iter().map(|&window_id| { + 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, + }) + }).collect::>(); + + let mut withdrawns = Vec::new(); + for cookie in cookies { + let reply = self.conn.wait_for_reply(cookie) + .expect("Failed to get WM_STATE property"); + println!("Reply length: {}", reply.length()); + if reply.length() > 0 { + let state = reply.value::()[0]; + println!("Window state: {:?}", state); + withdrawns.push(state == self.atoms.wm_state_withdrawn); + } else { + withdrawns.push(false); + } + } + withdrawns + } + pub fn get_windows_properties(&self, window_ids: &[x::Window]) -> Vec> { // Build requests to get properties for each window let mut cookies = Vec::new(); @@ -153,7 +213,7 @@ impl Client { cookies.push(self.conn.send_request(&x::GetProperty { delete: false, window: window_id, - property: self.atoms._NET_WM_STATE, + property: self.atoms._net_wm_state, r#type: x::ATOM_ATOM, long_offset: 0, long_length: 1024, // Number of properties to fetch @@ -178,8 +238,13 @@ impl Client { pub fn is_floating(&self, properties: &Vec) -> bool { // Check if the window is floating - ! (properties.contains(&self.atoms._NET_WM_STATE_MAXIMIZED_HORZ.into()) || - properties.contains(&self.atoms._NET_WM_STATE_MAXIMIZED_VERT.into())) + ! (properties.contains(&self.atoms._net_wm_state_maximized_horz.into()) || + properties.contains(&self.atoms._net_wm_state_maximized_vert.into())) + } + + pub fn is_visible(&self, properties: &Vec) -> bool { + // Check if the window is visible + !properties.contains(&self.atoms._net_wm_state_hidden.into()) } pub fn set_focus(&self, window_id: x::Window) -> Result<(), String> { diff --git a/rust/src/backend/xcb/mod.rs b/rust/src/backend/xcb/mod.rs index 20c408b..dda2e5f 100644 --- a/rust/src/backend/xcb/mod.rs +++ b/rust/src/backend/xcb/mod.rs @@ -1,2 +1,4 @@ pub mod backend; mod client; + +pub use crate::backend::xcb::backend::Backend; From 849acd182f413c13825616ef3fca2bf12ff7870f Mon Sep 17 00:00:00 2001 From: Milosz Blizniak Date: Tue, 24 Jun 2025 13:32:31 +0200 Subject: [PATCH 11/18] feat(rs): add cli option for xcb --- rust/src/backend/backend.rs | 5 +++++ rust/src/backend/mod.rs | 1 + rust/src/cli.rs | 2 ++ rust/src/main.rs | 4 ++++ 4 files changed, 12 insertions(+) diff --git a/rust/src/backend/backend.rs b/rust/src/backend/backend.rs index 19c5498..363c20f 100644 --- a/rust/src/backend/backend.rs +++ b/rust/src/backend/backend.rs @@ -1,11 +1,13 @@ use crate::backend::i3; use crate::backend::wmctl; +use crate::backend::xcb; use crate::backend::traits::*; use crate::types::Windows; pub enum UsedBackend { I3(i3::Backend), WmCtl(wmctl::Backend), + Xcb(xcb::Backend), } pub struct Backend { @@ -25,6 +27,7 @@ impl GetTabs for Backend { match self.used_backend { UsedBackend::I3(ref i3) => i3.get_tabs(), UsedBackend::WmCtl(ref wmctl) => wmctl.get_tabs(), + UsedBackend::Xcb(ref xcb) => xcb.get_tabs(), } } } @@ -34,6 +37,7 @@ impl GetVisible for Backend { match self.used_backend { UsedBackend::I3(ref i3) => i3.get_visible(), UsedBackend::WmCtl(ref wmctl) => wmctl.get_visible(), + UsedBackend::Xcb(ref xcb) => xcb.get_visible(), } } } @@ -43,6 +47,7 @@ impl SetFocus for Backend { match self.used_backend { UsedBackend::I3(ref mut i3) => i3.set_focus(id), UsedBackend::WmCtl(ref mut wmctl) => wmctl.set_focus(id), + UsedBackend::Xcb(ref mut xcb) => xcb.set_focus(id), } } } diff --git a/rust/src/backend/mod.rs b/rust/src/backend/mod.rs index 916e221..a624ab0 100644 --- a/rust/src/backend/mod.rs +++ b/rust/src/backend/mod.rs @@ -6,6 +6,7 @@ pub mod backend; pub use i3::Backend as I3Backend; pub use wmctl::Backend as WmctlBackend; +pub use xcb::Backend as XcbBackend; pub use backend::Backend; pub use backend::UsedBackend; pub use traits::*; diff --git a/rust/src/cli.rs b/rust/src/cli.rs index f24f253..2dce1c8 100644 --- a/rust/src/cli.rs +++ b/rust/src/cli.rs @@ -112,4 +112,6 @@ pub enum BackendOption { I3, /// Use the sway IPC backend WmCtrl, + /// Use the xcb backend + Xcb, } diff --git a/rust/src/main.rs b/rust/src/main.rs index 43f009c..654eade 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -30,6 +30,10 @@ fn main() { logging::info!("Using WmCtrl backend."); backend = Backend::new(UsedBackend::WmCtl(WmctlBackend::new())); } + cli::BackendOption::Xcb => { + logging::info!("Using XCB backend."); + backend = Backend::new(UsedBackend::Xcb(XcbBackend::new())); + } } // Determine the window ID to switch focus to based on the command From 9b12725c62b92de9556bb2e45c3a84b5b052af28 Mon Sep 17 00:00:00 2001 From: Milosz Blizniak Date: Tue, 24 Jun 2025 21:29:51 +0200 Subject: [PATCH 12/18] fix(rs): fetch full window info per window Previous fetching by type for each window, didn't guarantee the order of the replies, because each reply was of the same fingerprint. Now we request different properties per window which solved issues with incorrect data for each window. It also cleaned up the API a bunch. --- rust/src/backend/xcb/backend.rs | 67 +++----- rust/src/backend/xcb/client.rs | 284 +++++++++++++++++--------------- 2 files changed, 174 insertions(+), 177 deletions(-) diff --git a/rust/src/backend/xcb/backend.rs b/rust/src/backend/xcb/backend.rs index e8284e5..460cc78 100644 --- a/rust/src/backend/xcb/backend.rs +++ b/rust/src/backend/xcb/backend.rs @@ -1,5 +1,5 @@ use super::client::Client; -use crate::types::{Window, Windows}; +use crate::types::Windows; use crate::backend::traits::*; use xcb::Xid; use xcb::x::Window as XWindow; @@ -8,7 +8,7 @@ use std::collections::HashMap; pub struct Backend { client: Client, windows: Windows, - xwindows: HashMap, + xid_map: HashMap, } impl Backend { @@ -25,50 +25,35 @@ impl Backend { .expect("Failed to get active window"); // Get the list of windows - let x_windows = client.get_client_list(); + let xwindows = client.get_client_list(); - // Get the list of windows - let rects = client.get_normalized_windows_rects(&x_windows); - - // Get the list of properties for the windows - let properties = client.get_windows_properties(&x_windows); - - // Get window withdrawn states - let withdrawns = client.get_windows_withdrawn(&x_windows); - - // Create a map to hold the XWindow to u64 resource ID mapping - let mut xwindows: HashMap = HashMap::new(); - - // Create the Windows structure - let windows: Windows = x_windows.iter() - .zip(rects.iter()) - .zip(properties.iter()) - .zip(withdrawns.iter()) - .filter_map(|(((x_window, rect), properties), withdrawn)| { - println!("Properties are: {:?}", properties); - match client.is_visible(properties) && !withdrawn { - false => return None, // Skip invisible windows - true => { - let window_id: u64 = x_window.resource_id().into(); - xwindows.insert(window_id.clone(), *x_window); - Some(Window { - id: window_id, - rect: *rect, - floating: client.is_floating(&properties), - focused: *x_window == active_window, - }) + // 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) => { + eprintln!("Failed to fetch window info for {}: {}", xwindow.resource_id(), e); + return None; } - } + }; + xid_map.insert(xwindow.resource_id().into(), xwindow); + Some(window) }) - .collect(); + .collect::(); - // Log the windows - for window in &windows { + // Find the focused window + windows.iter_mut().for_each(|window| { + if xid_map[&window.id] == active_window { + window.focused = true; + } println!("Window ID: {}, Rect: {}, Floating: {}, Focused: {}", window.id, window.rect.to_string(), window.floating, window.focused); - } + }); - Backend { client, windows, xwindows } + Backend { client, windows, xid_map } } } @@ -87,12 +72,12 @@ impl GetTabs for Backend { impl SetFocus for Backend { fn set_focus(&mut self, window_id: &u64) { // Check if the window ID exists in the map - if !self.xwindows.contains_key(window_id) { + if !self.xid_map.contains_key(window_id) { eprintln!("Window ID {} does not exist", window_id); return; } // Set focus to the specified window - self.client.set_focus(self.xwindows[window_id]) + 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 index 921cc10..c0a12b8 100644 --- a/rust/src/backend/xcb/client.rs +++ b/rust/src/backend/xcb/client.rs @@ -1,5 +1,7 @@ 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. @@ -15,6 +17,8 @@ xcb::atoms_struct! { 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", } } @@ -66,15 +70,13 @@ impl Client { &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) { - let cookie = self.conn.send_request(&x::GetAtomName { - atom: *atom, - }); - let atom_name = self.conn.wait_for_reply(cookie) - .map(|reply| reply.name().to_string()) - .unwrap_or_else(|_| "unknown".to_string()); - return Err(format!("Required atom '{}' is not supported", atom_name)); + return Err(format!( + "Required atom '{}' is not supported", + self.get_atom_name(*atom)? + )); } } Ok(()) @@ -97,7 +99,7 @@ impl Client { reply.value::().to_vec() } - pub fn get_active_window(&self) -> Option { + 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, @@ -108,143 +110,67 @@ impl Client { long_length: 1, // Only one active window }); - match self.conn.wait_for_reply(cookie) { - Ok(reply) => { - if reply.length() > 0 { - Some(reply.value::()[0]) - } else { - None - } - }, - Err(_) => None, + 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 get_normalized_windows_rects(&self, window_ids: &[x::Window]) -> Vec { - let mut rects = self.get_windows_rects(window_ids); - // Translate rects to root window coordinates - let mut cookies = Vec::new(); - for &window_id in window_ids { - cookies.push(self.conn.send_request(&x::TranslateCoordinates { - src_window: window_id, - dst_window: self.root, - src_x: 0, - src_y: 0, - })); - } - // Wait for all replies - let mut i: usize = 0; - for cookie in cookies { - match self.conn.wait_for_reply(cookie) { - Ok(reply) => { - // Adjust the geometry based on the translation - rects[i].x += reply.dst_x() as i32; - rects[i].y += reply.dst_y() as i32; - }, - Err(err) => { - eprintln!("Failed to translate coordinates: {}", err); - } - } - i += 1; - } - rects - } + 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()), + ); - fn get_windows_rects(&self, window_ids: &[x::Window]) -> Vec { - // Build requests for all window rects - let mut cookies = Vec::new(); - for &window_id in window_ids { - cookies.push(self.conn.send_request(&x::GetGeometry { - drawable: x::Drawable::Window(window_id), - })); - } - // Wait for all replies - let mut rects = Vec::new(); - for cookie in cookies { - match self.conn.wait_for_reply(cookie) { - Ok(reply) => { - rects.push(Rect { - x: reply.x() as i32, - y: reply.y() as i32, - w: reply.width() as i32, - h: reply.height() as i32, - }); - }, - Err(err) => { - eprintln!("Failed to get geometry: {}", err); - } - } - } - rects - } + // 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), + ); - pub fn get_windows_withdrawn(&self, window_ids: &[x::Window]) -> Vec { - let cookies = window_ids.iter().map(|&window_id| { - 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, - }) - }).collect::>(); - - let mut withdrawns = Vec::new(); - for cookie in cookies { - let reply = self.conn.wait_for_reply(cookie) - .expect("Failed to get WM_STATE property"); - println!("Reply length: {}", reply.length()); - if reply.length() > 0 { - let state = reply.value::()[0]; - println!("Window state: {:?}", state); - withdrawns.push(state == self.atoms.wm_state_withdrawn); - } else { - withdrawns.push(false); - } - } - withdrawns - } + // 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)), + }; - pub fn get_windows_properties(&self, window_ids: &[x::Window]) -> Vec> { - // Build requests to get properties for each window - let mut cookies = Vec::new(); - for &window_id in window_ids { - cookies.push(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 - })); - } + // 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)), + }; - // Wait for all replies and collect properties - let mut properties = Vec::new(); - for cookie in cookies { - match self.conn.wait_for_reply(cookie) { - Ok(reply) => { - properties.push(reply.value().to_vec()); - }, - Err(err) => { - eprintln!("Failed to get properties: {}", err); - properties.push(vec![]); - } - } - } - properties - } + // 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)), + }; - pub fn is_floating(&self, properties: &Vec) -> bool { - // Check if the window is floating - ! (properties.contains(&self.atoms._net_wm_state_maximized_horz.into()) || - properties.contains(&self.atoms._net_wm_state_maximized_vert.into())) - } + // 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()); + } - pub fn is_visible(&self, properties: &Vec) -> bool { - // Check if the window is visible - !properties.contains(&self.atoms._net_wm_state_hidden.into()) + Ok(Window { + id: window_id.resource_id().into(), + rect: rect, + floating: self.is_floating(&ewmh_state), + focused: false, // Focus state will be set later + }) } pub fn set_focus(&self, window_id: x::Window) -> Result<(), String> { @@ -260,4 +186,90 @@ impl Client { 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, properties: &Vec) -> bool { + // Check if the window is floating + return false; // Placeholder for floating logic + // ! (properties.contains(&self.atoms._net_wm_state_maximized_horz.into()) || + // properties.contains(&self.atoms._net_wm_state_maximized_vert.into())) + } + + 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; } From f553fdb98316fde46315eb4fad98019d8d8cfe4b Mon Sep 17 00:00:00 2001 From: Milosz Blizniak Date: Wed, 25 Jun 2025 02:12:30 +0200 Subject: [PATCH 13/18] feat(rs): enjoyable cli interface --- rust/Cargo.toml.in | 1 - rust/src/cli.rs | 275 +++++++++++++++++++++++++++++---------------- rust/src/main.rs | 21 ++-- 3 files changed, 190 insertions(+), 107 deletions(-) diff --git a/rust/Cargo.toml.in b/rust/Cargo.toml.in index aa6bc8f..9395ef7 100644 --- a/rust/Cargo.toml.in +++ b/rust/Cargo.toml.in @@ -4,7 +4,6 @@ version = "0.0.0" # This is a placeholder version edition = "2021" [dependencies] -clap = { version = "3.2", features = ["derive"] } libwmctl = "0.0.51" log = "0.4" serde_json = "1.0" diff --git a/rust/src/cli.rs b/rust/src/cli.rs index 2dce1c8..9969288 100644 --- a/rust/src/cli.rs +++ b/rust/src/cli.rs @@ -1,117 +1,202 @@ -use clap::{Parser, ValueEnum, Subcommand, ArgAction}; use crate::planar; use crate::linear; -// Public interface for the CLI module +macro_rules! die { + ($code:expr, $format:expr $(, $args:expr)*) => {{ + eprintln!($format $(, $args)*); + std::process::exit($code); + }}; +} -/// We cant use Cli::parse() directly in the main function because it requires -/// the command line arguments to be passed in, which is not possible -/// in a library context. Instead, we define this function to be used -/// in the main function to parse the command line arguments. -pub fn get_parsed_command() -> Cli { - Cli::parse() +pub struct Cli { + pub backend: UseBackend, + pub command: String, + pub number: Option, + pub wrap: bool, } -/// Determine if the wrap option is enabled -pub fn wrap(cli: &Cli) -> bool { - match cli.wrap { - WrapOption::Wrap => true, - WrapOption::NoWrap => false, +static HELP: &str = " +i3switch - A simple command-line utility to switch focus in i3 window manager + +Usage: i3switch (