commit ee37e61be586345f9672d589d9079cca29227771 Author: Thomas Hain Date: Sun Aug 11 20:47:37 2024 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100755 index 0000000..901fc48 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,625 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "async-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "autosplit" +version = "0.1.0" +dependencies = [ + "async-trait", + "clap", + "env_logger", + "log", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", +] + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "cc" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbb260a053428790f3de475e304ff84cdbc4face759ea7a3e64c1edd938a7fc" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64b17d7ea74e9f833c7dbf2cbe4fb12ff26783eda4782a8975b72f895c9b4d99" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "object" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" +dependencies = [ + "memchr", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ed5f095 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "autosplit" +version = "0.1.0" +edition = "2021" + +[dependencies] +async-trait = "0.1.81" +clap = "4.5.13" +env_logger = "0.11.5" +log = "0.4.22" +serde = { version = "1.0.204", features = ["derive"] } +serde_json = "1.0.121" +thiserror = "1.0.63" +tokio = { version = "1.39.2", features = ["full"] } +tokio-stream = "0.1.15" diff --git a/src/main.rs b/src/main.rs new file mode 100755 index 0000000..05436e6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,72 @@ +mod swayipc; + +use swayipc::{Node, Workspace}; + +use crate::swayipc::{collect_leaves, SwayEvent, SwayEventType, SwayIpc, WindowChangeType}; + +pub fn get_current_leaves_here<'a>(root: &'a Node, workspaces: &'a [Workspace]) -> Vec<&'a Node> { + let focused_ws_id = workspaces + .iter() + .find_map(|ws| { + if ws.focused { + Some(ws.name.clone()) + } else { + None + } + }) + .unwrap(); + + let outputs = &root.nodes; + let focused_workspace = outputs + .iter() + .find_map(|output| { + output + .nodes + .iter() + .find(|workspace| workspace.name.clone().unwrap_or_default() == focused_ws_id) + }) + .unwrap(); + + collect_leaves(focused_workspace) +} + +#[tokio::main] +async fn main() { + env_logger::init(); + let sway_ipc = SwayIpc::connect().await.unwrap(); + + // Subscribe to workspace and window events + let mut workspace_event_rx = sway_ipc + .subscribe([SwayEventType::Workspace, SwayEventType::Window]) + .await + .unwrap(); + + while let Some(event) = workspace_event_rx.recv().await { + if let SwayEvent::Window(window_event) = event { + if let WindowChangeType::New = window_event.change { + let sway_ipc_clone = sway_ipc.clone(); + + // Handle the new layout setup + let root = sway_ipc_clone.get_tree().await.unwrap(); + let workspaces = sway_ipc_clone.get_workspaces().await.unwrap(); + let leaves = get_current_leaves_here(&root, &workspaces); + + if let (Some(first), Some(second)) = (leaves.first(), leaves.get(1)) { + let mut command = format!("[con_id={}] splith", first.id); + match leaves.get(2) { + Some(third) => { + command = format!( + "{};[con_id={}] splitv;[con_id={}] mark target;[con_id={}] move container to mark target;[con_id={}] swap container with mark target;", + command, second.id, third.id, second.id, second.id + ); + } + None => { + command = format!("{};[con_id={}] splitv;", command, second.id); + } + } + sway_ipc_clone.run_command(command).await.unwrap(); + } + } + } + } +} diff --git a/src/swayipc.rs b/src/swayipc.rs new file mode 100644 index 0000000..1416afc --- /dev/null +++ b/src/swayipc.rs @@ -0,0 +1,866 @@ +use log::{debug, error}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::fmt::Display; +use std::io; +use std::io::ErrorKind; +use tokio::io::AsyncReadExt; +use tokio::net::UnixStream; +use tokio::stream; +use tokio::sync::mpsc::{self, UnboundedReceiver}; + +macro_rules! impl_sway_event_deserialize { + ($($enum_variant:ident => $struct_type:ident),*) => { + $( + impl SwayEventDeserialize for $struct_type { + fn event_type() -> SwayEventType { + SwayEventType::$enum_variant + } + + fn deserialize_event(value: Value) -> Option { + serde_json::from_value(value).ok() + } + } + )* + }; +} + +// Use the macro to implement SwayEventDeserialize for multiple event types +impl_sway_event_deserialize!( + Workspace => WorkspaceChange, + Output => OutputChange, + Mode => ModeChange, + Window => WindowChange, + BarconfigUpdate => BarConfigUpdate, + Binding => BindingEvent, + Shutdown => ShutdownEvent, + Tick => TickEvent, + BarStateUpdate => BarStateUpdate, + Input => InputEvent +); + +#[derive(Debug, thiserror::Error)] +pub enum SwayIpcError { + #[error("IO error occurred: {0}")] + IoError(#[from] io::Error), + #[error("Failed to parse JSON: {0}")] + JsonError(#[from] serde_json::Error), + #[error("Received unexpected message format")] + UnexpectedMessageFormat, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SwayMessageType { + RunCommand = 0, + GetWorkspaces = 1, + Subscribe = 2, + GetOutputs = 3, + GetTree = 4, + GetMarks = 5, + GetBarConfig = 6, + GetVersion = 7, + GetBindingModes = 8, + GetConfig = 9, + SendTick = 10, + Sync = 11, + GetBindingState = 12, + GetInputs = 100, + GetSeats = 101, +} + +impl From<&SwayMessage> for SwayMessageType { + fn from(message: &SwayMessage) -> Self { + match message { + SwayMessage::RunCommand { .. } => SwayMessageType::RunCommand, + SwayMessage::GetWorkspaces => SwayMessageType::GetWorkspaces, + SwayMessage::Subscribe { .. } => SwayMessageType::Subscribe, + SwayMessage::GetOutputs => SwayMessageType::GetOutputs, + SwayMessage::GetTree => SwayMessageType::GetTree, + SwayMessage::GetMarks => SwayMessageType::GetMarks, + SwayMessage::GetBarConfig(_) => SwayMessageType::GetBarConfig, + SwayMessage::GetVersion => SwayMessageType::GetVersion, + SwayMessage::GetBindingModes => SwayMessageType::GetBindingModes, + SwayMessage::GetConfig => SwayMessageType::GetConfig, + SwayMessage::SendTick(_) => SwayMessageType::SendTick, + SwayMessage::Sync => SwayMessageType::Sync, + SwayMessage::GetBindingState => SwayMessageType::GetBindingState, + SwayMessage::GetInputs => SwayMessageType::GetInputs, + SwayMessage::GetSeats => SwayMessageType::GetSeats, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(untagged)] +#[serde(rename_all = "snake_case")] +pub enum SwayMessage { + RunCommand(String), + Subscribe(Vec), + GetBarConfig(Option), + SendTick(Option), + GetVersion, + GetWorkspaces, + GetOutputs, + GetTree, + GetMarks, + GetBindingModes, + GetConfig, + Sync, + GetBindingState, + GetInputs, + GetSeats, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SwayReply { + pub success: bool, + pub error: Option, + pub parse_error: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Workspace { + pub num: i32, + pub name: String, + pub visible: bool, + pub focused: bool, + pub urgent: bool, + pub rect: Rect, + pub output: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Rect { + pub x: i32, + pub y: i32, + pub width: i32, + pub height: i32, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Output { + pub name: String, + pub make: String, + pub model: String, + pub serial: String, + pub active: bool, + pub dpms: bool, + pub power: bool, + pub primary: bool, + pub scale: f64, + pub subpixel_hinting: String, + pub transform: String, + pub current_workspace: Option, + pub modes: Vec, + pub current_mode: OutputMode, + pub rect: Rect, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct OutputMode { + pub width: i32, + pub height: i32, + pub refresh: i32, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SwayVersion { + pub major: i32, + pub minor: i32, + pub patch: i32, + pub human_readable: String, + pub loaded_config_file_name: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BarConfig { + pub id: String, + pub mode: String, + pub position: String, + pub status_command: Option, + pub font: Option, + pub workspace_buttons: bool, + pub workspace_min_width: Option, + pub binding_mode_indicator: bool, + pub verbose: bool, + pub colors: BarColors, + pub gaps: BarGaps, + pub bar_height: Option, + pub status_padding: Option, + pub status_edge_padding: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BarColors { + pub background: String, + pub statusline: String, + pub separator: String, + pub focused_background: String, + pub focused_statusline: String, + pub focused_separator: String, + pub focused_workspace_text: String, + pub focused_workspace_bg: String, + pub focused_workspace_border: String, + pub active_workspace_text: String, + pub active_workspace_bg: String, + pub active_workspace_border: String, + pub inactive_workspace_text: String, + pub inactive_workspace_bg: String, + pub inactive_workspace_border: String, + pub urgent_workspace_text: String, + pub urgent_workspace_bg: String, + pub urgent_workspace_border: String, + pub binding_mode_text: String, + pub binding_mode_bg: String, + pub binding_mode_border: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BarGaps { + pub top: i32, + pub right: i32, + pub bottom: i32, + pub left: i32, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct InputDevice { + pub identifier: String, + pub name: String, + pub vendor: i32, + pub product: i32, + pub devtype: String, + pub xkb_active_layout_name: Option, + pub xkb_layout_names: Option>, + pub xkb_active_layout_index: Option, + pub libinput: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct LibInput { + pub send_events: Option, + pub tap: Option, + pub tap_button_map: Option, + pub tap_drag: Option, + pub tap_drag_lock: Option, + pub accel_speed: Option, + pub accel_profile: Option, + pub natural_scroll: Option, + pub left_handed: Option, + pub click_method: Option, + pub middle_emulation: Option, + pub scroll_method: Option, + pub scroll_button: Option, + pub scroll_button_lock: Option, + pub dwt: Option, + pub dwtp: Option, + pub calibration_matrix: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Seat { + pub name: String, + pub capabilities: i32, + pub focus: i64, + pub devices: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SwayEventType { + Workspace, + Output, + Mode, + Window, + BarconfigUpdate, + Binding, + Shutdown, + Tick, + BarStateUpdate, + Input, +} + +impl Display for SwayEventType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + SwayEventType::Workspace => "workspace", + SwayEventType::Output => "output", + SwayEventType::Mode => "mode", + SwayEventType::Window => "window", + SwayEventType::BarconfigUpdate => "barconfig_update", + SwayEventType::Binding => "binding", + SwayEventType::Shutdown => "shutdown", + SwayEventType::Tick => "tick", + SwayEventType::BarStateUpdate => "bar_state_update", + SwayEventType::Input => "input", + } + ) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(untagged, rename_all = "snake_case")] +pub enum SwayEvent { + Workspace(WorkspaceChange), + Output(OutputChange), + Mode(ModeChange), + Window(WindowChange), + BarconfigUpdate(BarConfigUpdate), + Binding(BindingEvent), + Shutdown(ShutdownEvent), + Tick(TickEvent), + BarStateUpdate(BarStateUpdate), + Input(InputEvent), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct WorkspaceChange { + pub change: WorkspaceChangeType, + pub current: Option, + pub old: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum WorkspaceChangeType { + Init, + Empty, + Focus, + Move, + Rename, + Urgent, + Reload, +} + +#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum NodeType { + Root, + Output, + Workspace, + Con, + FloatingCon, + Dockarea, // i3-specific +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Node { + pub id: i64, + pub name: Option, + #[serde(rename = "type")] + pub node_type: NodeType, + pub rect: Rect, + pub focused: bool, + pub focus: Vec, + pub border: String, + pub current_border_width: i32, + pub layout: String, + pub orientation: String, + pub percent: Option, + pub window_rect: Rect, + pub deco_rect: Rect, + pub geometry: Rect, + pub window: Option, + pub urgent: Option, + pub floating_nodes: Vec, + pub sticky: bool, + pub app_id: Option, + pub pid: Option, + pub visible: Option, + pub shell: Option, + pub inhibit_idle: Option, + pub idle_inhibitors: Option, + pub nodes: Vec, + pub fullscreen_mode: Option, + pub marks: Option>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct IdleInhibitors { + pub application: Option, + pub user: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ModeChange { + pub change: String, + pub pango_markup: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct OutputChange { + pub change: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct WindowChange { + pub change: WindowChangeType, + pub container: Node, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum WindowChangeType { + New, + Close, + Focus, + Title, + FullscreenMode, + Move, + Floating, + Urgent, + Mark, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BarConfigUpdate { + pub id: String, + pub mode: String, + pub position: String, + pub status_command: String, + pub font: String, + pub workspace_buttons: bool, + pub binding_mode_indicator: bool, + pub verbose: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BindingEvent { + pub change: String, + pub binding: Binding, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Binding { + pub command: String, + pub event_state_mask: Vec, + pub input_code: i32, + pub symbol: Option, + pub input_type: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ShutdownEvent { + pub change: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TickEvent { + pub first: bool, + pub payload: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BarStateUpdate { + pub id: String, + pub visible_by_modifier: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct InputEvent { + pub change: String, + pub input: InputDevice, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SwayIpc { + socket_path: String, +} + +// Define a trait for event deserialization +pub trait SwayEventDeserialize: Sized { + fn event_type() -> SwayEventType; + fn deserialize_event(value: Value) -> Option; +} + +impl SwayIpc { + pub async fn connect() -> Result { + let socket_path = std::env::var("SWAYSOCK").map_err(|_| { + io::Error::new( + ErrorKind::NotFound, + "SWAYSOCK environment variable not found", + ) + })?; + Ok(SwayIpc { socket_path }) + } + + async fn send_and_receive(&self, message: SwayMessage) -> Result { + let mut stream = UnixStream::connect(&self.socket_path).await?; + + let payload = match &message { + SwayMessage::RunCommand(command) => command.to_owned(), + _ => serde_json::to_string(&message)?, + }; + + let magic_string = "i3-ipc".as_bytes(); + let payload_length = payload.len() as u32; + let _payload_type: u32 = SwayMessageType::from(&message) as u32; + + let mut message_buffer = vec![]; + message_buffer.extend_from_slice(magic_string); + message_buffer.extend_from_slice(&payload_length.to_ne_bytes()); + message_buffer.extend_from_slice(&_payload_type.to_ne_bytes()); + message_buffer.extend_from_slice(payload.as_bytes()); + + stream.writable().await?; + stream.try_write(&message_buffer)?; + + stream.readable().await?; + debug!("Reading response from the stream."); + + // First, read the fixed-size header to determine the length of the payload + let mut header = vec![0; 14]; + stream.try_read(&mut header)?; + + if &header[0..6] != b"i3-ipc" { + return Err(SwayIpcError::UnexpectedMessageFormat); + } + + let response_payload_length = + u32::from_ne_bytes(header[6..10].try_into().unwrap()) as usize; + + // Then, read the payload based on the length specified in the header + let mut response_payload = vec![0; response_payload_length]; + stream.read_exact(&mut response_payload).await?; + + let response: Value = SwayIpc::parse_response(&response_payload)?; + Ok(response) + } + + pub async fn get_workspaces(&self) -> Result, SwayIpcError> { + let response = self.send_and_receive(SwayMessage::GetWorkspaces).await?; + let workspaces: Vec = serde_json::from_value(response)?; + Ok(workspaces) + } + + pub async fn run_command(&self, command: String) -> Result, SwayIpcError> { + let response = self + .send_and_receive(SwayMessage::RunCommand(command)) + .await?; + let results: Vec = serde_json::from_value(response)?; + Ok(results) + } + + pub async fn get_tree(&self) -> Result { + let response = self.send_and_receive(SwayMessage::GetTree).await?; + let tree: Node = serde_json::from_value(response)?; + Ok(tree) + } + + pub async fn get_outputs(&self) -> Result { + let response = self.send_and_receive(SwayMessage::GetOutputs).await?; + let tree: Node = serde_json::from_value(response)?; + Ok(tree) + } + + pub async fn get_version(&self) -> Result { + let response = self.send_and_receive(SwayMessage::GetVersion).await?; + let version: SwayVersion = serde_json::from_value(response)?; + Ok(version) + } + + async fn subscribe_helper( + &self, + event_names: Vec, + process_event: F, + ) -> Result<(), SwayIpcError> + where + F: Fn(Value, u32) + Send + 'static, + { + let socket_path = self.socket_path.clone(); + let stream = UnixStream::connect(&socket_path).await?; + + let subscribe_message = SwayMessage::Subscribe(event_names); + let payload = serde_json::to_string(&subscribe_message)?; + + let mut buf = vec![0; 8192]; + Self::send_and_receive_internal(&stream, &payload, SwayMessageType::Subscribe, &mut buf) + .await?; + + if buf.is_empty() { + return Err(SwayIpcError::UnexpectedMessageFormat); + } + + let json_value: Value = serde_json::from_slice(&buf[14..])?; + if json_value.get("success") == Some(&Value::Bool(false)) { + return Err(SwayIpcError::UnexpectedMessageFormat); + } + + tokio::spawn(async move { + 'outer: loop { + match stream.readable().await { + Ok(_) => { + let mut event_buffer = vec![0; 8192]; + let mut received_data = vec![]; + + loop { + match stream.try_read(&mut event_buffer) { + Ok(n) if n > 0 => { + received_data.extend_from_slice(&event_buffer[..n]); + n + } + Ok(_) => { + error!("Stream closed by peer"); + break 'outer; + } + Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => break, + Err(e) => { + error!("Failed to read from stream: {:?}", e); + break; + } + }; + + if received_data.len() < 14 { + continue; + } + + if &received_data[0..6] != b"i3-ipc" { + error!("Invalid magic string in event response"); + continue; + } + + let payload_length = u32::from_ne_bytes( + received_data[6..10] + .try_into() + .expect("Invalid length bytes"), + ) as usize; + + let payload_type = u32::from_ne_bytes( + received_data[10..14] + .try_into() + .expect("Invalid length bytes"), + ); + + if received_data.len() < 14 + payload_length { + continue; + } + + let json_slice = &received_data[14..14 + payload_length]; + + match serde_json::from_slice(json_slice) { + Ok(json_value) => { + process_event(json_value, payload_type); + } + Err(e) => { + error!("Failed to parse event JSON: {:?}", e); + } + } + + received_data.drain(0..14 + payload_length); + } + } + Err(e) => { + error!("Stream not readable: {:?}", e); + break; + } + } + } + }); + + Ok(()) + } + + pub async fn subscribe( + &self, + events: [SwayEventType; N], + ) -> Result, SwayIpcError> { + let (tx, rx) = mpsc::unbounded_channel(); + let event_names: Vec = events.iter().map(|e| e.to_string()).collect(); + + self.subscribe_helper(event_names, move |json_value, payload_type| { + let event: Option = match payload_type { + 0x80000000 => serde_json::from_value(json_value) + .map(SwayEvent::Workspace) + .ok(), + 0x80000001 => serde_json::from_value(json_value) + .map(SwayEvent::Output) + .ok(), + 0x80000002 => serde_json::from_value(json_value).map(SwayEvent::Mode).ok(), + 0x80000003 => serde_json::from_value(json_value) + .map(SwayEvent::Window) + .ok(), + 0x80000004 => serde_json::from_value(json_value) + .map(SwayEvent::BarconfigUpdate) + .ok(), + 0x80000005 => serde_json::from_value(json_value) + .map(SwayEvent::Binding) + .ok(), + 0x80000006 => serde_json::from_value(json_value) + .map(SwayEvent::Shutdown) + .ok(), + 0x80000007 => serde_json::from_value(json_value).map(SwayEvent::Tick).ok(), + 0x80000014 => serde_json::from_value(json_value) + .map(SwayEvent::BarStateUpdate) + .ok(), + 0x80000015 => serde_json::from_value(json_value) + .map(SwayEvent::Input) + .ok(), + _ => { + error!("Unknown event type received: {:x}", payload_type); + None + } + }; + + if let Some(event) = event { + if tx.send(event).is_err() { + return; + } + } + }) + .await?; + + Ok(rx) + } + + pub async fn subscribe_typed( + &self, + ) -> Result, SwayIpcError> { + let (tx, rx) = mpsc::unbounded_channel(); + let event_name = E::event_type().to_string(); + + self.subscribe_helper(vec![event_name], move |json_value, payload_type| { + if let Some(event) = E::deserialize_event(json_value) { + if tx.send(event).is_err() { + return; + } + } + }) + .await?; + + Ok(rx) + } + + async fn send_and_receive_internal( + stream: &UnixStream, + payload: &str, + message_type: SwayMessageType, + buf: &mut Vec, + ) -> Result<(), SwayIpcError> { + let magic_string = "i3-ipc".as_bytes(); + let payload_length = payload.len() as u32; + let payload_type: u32 = message_type as u32; + + let mut message_buffer = vec![]; + message_buffer.extend_from_slice(magic_string); + message_buffer.extend_from_slice(&payload_length.to_ne_bytes()); + message_buffer.extend_from_slice(&payload_type.to_ne_bytes()); + message_buffer.extend_from_slice(payload.as_bytes()); + + stream.writable().await?; + stream.try_write(&message_buffer)?; + + stream.readable().await?; + buf.clear(); + buf.resize(8192, 0); + + let n = stream.try_read(buf)?; + buf.truncate(n); + + Ok(()) + } + + async fn read_response( + &self, + buf: &mut Vec, + stream: &UnixStream, + ) -> Result { + let n = stream.try_read(buf)?; + + if n == 0 { + return Err(SwayIpcError::IoError(io::Error::new( + io::ErrorKind::UnexpectedEof, + "Socket closed by peer", + ))); + } + + if !buf.starts_with(b"i3-ipc") { + return Err(SwayIpcError::UnexpectedMessageFormat); + } + + Ok(n) + } + + fn parse_response(json_slice: &[u8]) -> Result { + match std::str::from_utf8(json_slice) { + Ok(json_str) => debug!("JSON response: {}", json_str), + Err(e) => { + return Err(SwayIpcError::IoError(io::Error::new( + io::ErrorKind::InvalidData, + format!("Failed to parse UTF-8 from response: {:?}", e), + ))) + } + } + + let response: Value = serde_json::from_slice(json_slice)?; + Ok(response) + } +} + +fn collect_nodes_by_criteria<'a, F>(root: &'a Node, criteria: F) -> Vec<&'a Node> +where + F: Fn(&Node) -> bool, +{ + let mut stack = vec![root]; + let mut collector = Vec::new(); + + while let Some(node) = stack.pop() { + if criteria(node) { + collector.push(node); + } + // Extend the stack with children in reverse order (right to left) + for child in node.nodes.iter().rev() { + stack.push(child); + } + } + + collector +} + +pub fn collect_leaves<'a>(node: &'a Node) -> Vec<&'a Node> { + let criteria = |node: &Node| node.node_type == NodeType::Con && node.name.is_some(); + collect_nodes_by_criteria(node, criteria) +} + +// Tests +#[cfg(test)] +mod tests { + use super::*; + use tokio::runtime::Runtime; + + #[test] + fn test_get_workspaces() { + let rt = Runtime::new().unwrap(); + rt.block_on(async { + let sway_ipc = SwayIpc::connect().await.unwrap(); + let workspaces = sway_ipc.get_workspaces().await.unwrap(); + assert!(!workspaces.is_empty()); + }); + } + + #[test] + fn test_run_command() { + let rt = Runtime::new().unwrap(); + rt.block_on(async { + let sway_ipc = SwayIpc::connect().await.unwrap(); + let result = sway_ipc + .run_command("focus left".to_string()) + .await + .unwrap(); + assert!(result[0].success); + }); + } + + #[test] + fn test_get_version() { + let rt = Runtime::new().unwrap(); + rt.block_on(async { + let sway_ipc = SwayIpc::connect().await.unwrap(); + let version = sway_ipc.get_version().await.unwrap(); + assert_eq!(version.major, 1); + }); + } +}