From aa866c4e59bf7e1c07a1a2deb0be81bae4177415 Mon Sep 17 00:00:00 2001 From: Mark Stosberg Date: Wed, 16 Feb 2022 21:49:14 -0500 Subject: [PATCH] feature: add support for Sway Currently only either i3 or Sway can be selected as a compile-time feature with one of: cargo build --features i3 wmfocus cargo build --features sway wmfocus Based on a first draft by @JayceFayne --- Cargo.lock | 37 +++++++++++++++ Cargo.toml | 2 + src/main.rs | 28 +++++++++--- src/wm_sway.rs | 120 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 src/wm_sway.rs diff --git a/Cargo.lock b/Cargo.lock index 9cbc1ba..6cc517a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -600,6 +600,20 @@ name = "serde" version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" @@ -651,6 +665,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "swayipc" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40cc7e2bba9f31e7c46b119d9c542496806b9114676d8f46aa5c9c950ececaec" +dependencies = [ + "serde", + "serde_json", + "swayipc-types", +] + +[[package]] +name = "swayipc-types" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d63c88513504fd598a6c2218bd83d19e1f8cc6dd1a9892f2f982b223f01a803" +dependencies = [ + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "syn" version = "1.0.86" @@ -796,6 +832,7 @@ dependencies = [ "log", "pretty_env_logger", "regex", + "swayipc", "x11", "xcb", "xcb-util", diff --git a/Cargo.toml b/Cargo.toml index 36a7331..2b83e98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,12 +16,14 @@ codegen-units = 1 [features] i3 = ["i3ipc"] +sway = ["swayipc"] [dependencies] cairo-sys-rs = "0.15" css-color-parser = "0.1" font-loader = "0.11" i3ipc = { version = "0.10", optional = true } +swayipc = { version = "3.0.0", optional = true } itertools = "0.10" log = "0.4" pretty_env_logger = "0.4" diff --git a/src/main.rs b/src/main.rs index 6bf4006..421db03 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,8 @@ use xkbcommon::xkb; mod args; mod utils; +// begin i3-specific + #[cfg(feature = "i3")] extern crate i3ipc; @@ -17,10 +19,25 @@ mod wm_i3; #[cfg(feature = "i3")] use crate::wm_i3 as wm; +// end i3-specific + +// begin sway-specific + +#[cfg(feature = "sway")] +extern crate swayipc; + +#[cfg(feature = "sway")] +mod wm_sway; + +#[cfg(feature = "sway")] +use crate::wm_sway as wm; + +// end sway-specific + #[derive(Debug)] pub struct DesktopWindow { id: i64, - x_window_id: Option, + x_window_id: Option, pos: (i32, i32), size: (i32, i32), is_focused: bool, @@ -34,7 +51,7 @@ pub struct RenderWindow<'a> { rect: (i32, i32, i32, i32), } -#[cfg(any(feature = "i3", feature = "add_some_other_wm_here"))] +#[cfg(any(feature = "i3", feature = "sway"))] fn main() -> Result<()> { pretty_env_logger::init(); let app_config = args::parse_args(); @@ -325,12 +342,13 @@ fn main() -> Result<()> { Ok(()) } -#[cfg(not(any(feature = "i3", feature = "add_some_other_wm_here")))] +#[cfg(not(any(feature = "i3", feature = "sway")))] fn main() -> Result<()> { eprintln!( - "You need to enable support for at least one window manager.\n + "You need to enable support for one window manager.\n Currently supported: - --features i3" + --features i3 + --features sway" ); Ok(()) diff --git a/src/wm_sway.rs b/src/wm_sway.rs new file mode 100644 index 0000000..5bdd2dd --- /dev/null +++ b/src/wm_sway.rs @@ -0,0 +1,120 @@ +use anyhow::{Context, Result}; +use log::{debug, info}; +use swayipc::{Connection, Node, NodeLayout, NodeType, Workspace}; + +use crate::DesktopWindow; + +/// Find first `Node` that fulfills a given criterion. +fn find_first_node_with_attr(start_node: &Node, predicate: F) -> Option<&Node> +where + F: Fn(&Node) -> bool, +{ + let mut nodes_to_explore: Vec<&Node> = start_node.nodes.iter().collect(); + while !nodes_to_explore.is_empty() { + let mut next_vec = vec![]; + for node in &nodes_to_explore { + if predicate(node) { + return Some(node); + } + next_vec.extend(node.nodes.iter()); + } + nodes_to_explore = next_vec; + } + None +} + +/// Find parent of `child`. +fn find_parent_of<'a>(start_node: &'a Node, child: &'a Node) -> Option<&'a Node> { + let mut nodes_to_explore: Vec<&Node> = start_node.nodes.iter().collect(); + while !nodes_to_explore.is_empty() { + let mut next_vec = vec![]; + for node in &nodes_to_explore { + if node.nodes.iter().any(|x| child.id == x.id) { + return Some(node); + } + next_vec.extend(node.nodes.iter()); + } + nodes_to_explore = next_vec; + } + None +} + +/// Return a list of all `DesktopWindow`s for the given `Workspace`. +fn crawl_windows(root_node: &Node, workspace: &Workspace) -> Result> { + let workspace_node = find_first_node_with_attr(root_node, |x| { + x.name == Some(workspace.name.clone()) && x.node_type == NodeType::Workspace + }) + .context("Couldn't find the Workspace node")?; + + let mut nodes_to_explore: Vec<&Node> = workspace_node.nodes.iter().collect(); + nodes_to_explore.extend(workspace_node.floating_nodes.iter()); + let mut windows = vec![]; + while !nodes_to_explore.is_empty() { + let mut next_vec = vec![]; + for node in &nodes_to_explore { + next_vec.extend(node.nodes.iter()); + next_vec.extend(node.floating_nodes.iter()); + + let root_node = find_parent_of(root_node, node); + + let (pos_x, size_x) = if let Some(root_node) = root_node { + if root_node.layout == NodeLayout::Tabbed { + (node.rect.x + node.deco_rect.x, node.deco_rect.width) + } else { + (node.rect.x, node.rect.width) + } + } else { + (node.rect.x, node.rect.width) + }; + + let pos_y = if let Some(root_node) = root_node { + if root_node.layout == NodeLayout::Stacked { + root_node.rect.y + node.deco_rect.y + } else { + node.rect.y - node.deco_rect.height + } + } else { + node.rect.y - node.deco_rect.height + }; + + let window = DesktopWindow { + id: node.id, + x_window_id: node.window, + pos: (pos_x, pos_y), + size: (size_x, (node.rect.height + node.deco_rect.height)), + is_focused: node.focused, + }; + debug!("Found {:?}", window); + windows.push(window); + } + nodes_to_explore = next_vec; + } + Ok(windows) +} + +/// Return a list of all windows. +pub fn get_windows() -> Result> { + // Establish a connection to Sway over a Unix socket + let mut connection = Connection::new().expect("Couldn't acquire Sway IPC connection"); + let workspaces = connection + .get_workspaces() + .expect("Problem communicating with IPC"); + let visible_workspaces = workspaces.iter().filter(|w| w.visible); + let root_node = connection.get_tree()?; + let mut windows = vec![]; + for workspace in visible_workspaces { + windows.extend(crawl_windows(&root_node, workspace)?); + } + Ok(windows) +} + +/// Focus a specific `window`. +pub fn focus_window(window: &DesktopWindow) -> Result<()> { + let mut connection = Connection::new().expect("Couldn't acquire Sway IPC connection"); + let command_str = format!("[con_id=\"{}\"] focus", window.id); + let command = connection + .run_command(&command_str) + .context("Couldn't communicate with Sway")?; + info!("Sending to Sway: {:?}", command); + Ok(()) +}