diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ec9390d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,851 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[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.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "hashbrown" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "i3toolwait" +version = "0.1.0" +dependencies = [ + "anyhow", + "byteorder", + "clap", + "env_logger", + "futures", + "log", + "rust_lisp", + "serde", + "serde_json", + "serde_yaml", + "strfmt", + "tokio", + "xdg", +] + +[[package]] +name = "indexmap" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "libc" +version = "0.2.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" + +[[package]] +name = "linux-raw-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaac441002f822bc9705a681810a4dd2963094b9ca0ddc41cb963a4c189189ea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5011c7e263a695dc8ca064cddb722af1be54e517a280b12a5356f98366899e5d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rust_lisp" +version = "0.18.0" +source = "git+https://github.com/brundonsmith/rust_lisp.git?branch=arc-feature-addition#6c4445965c027bd4d3cf1f3154e9145bd45e8ba6" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[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.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" + +[[package]] +name = "socket2" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strfmt" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a8348af2d9fc3258c8733b8d9d8db2e56f54b2363a4b5b81585c7875ed65e65" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "tokio" +version = "1.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f832c4c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "i3toolwait" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.75" +byteorder = "1.5.0" +clap = { version = "4.4.6", features = ["derive"] } +env_logger = "0.10.0" +futures = "0.3.28" +log = "0.4.20" +rust_lisp = { git = "https://github.com/brundonsmith/rust_lisp.git", branch = "arc-feature-addition", features = ["arc"] } +serde = { version = "1.0.188", features = ["std", "derive", "serde_derive"] } +serde_json = "1.0.107" +serde_yaml = "0.9.25" +strfmt = "0.2.4" +tokio = { version = "1.33.0", features = ["full"] } +xdg = "2.5.2" diff --git a/Makefile b/Makefile index 4e2e896..550e2d9 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,17 @@ +EXEC := i3toolwait INSTALL_BASE ?= /usr/local -install: i3toolwait install-modules - install -Dm0755 -oroot -groot $< ${INSTALL_BASE}/bin/$< +default: target/debug/${EXEC} +release: target/release/${EXEC} +default: target/debug/${EXEC} -install-modules: requirements.txt - python3 -mpip install --upgrade --requirement $< +install: target/release/${EXEC} + install -Dm0755 -oroot -groot $< ${INSTALL_BASE}/bin/${EXEC} -.PHONY: install install-modules +target/release/${EXEC}: + @cargo build --release + +target/debug/${EXEC}: + @cargo build + +.PHONY: install diff --git a/README.md b/README.md index f439ad3..af7f385 100644 --- a/README.md +++ b/README.md @@ -4,114 +4,125 @@ Launch a program and move it to the correct workspace. ## Usage -- **simple:** `i3toolwait simple ...` -- **config:** `i3toolwait config ...` +`i3toolwait -c FILE` -### Simple +Optionally start multiple programs and wait for their windows to appear. +Once these windows appeared a custom i3 command can be specified. -Run only one program. - -### Config - -Run multiple programs by specifying a yaml configuration file: +## Example ```yaml --- -signal: signal number or name, optional. Should program entries which have signal: true wait for this signal before continuing to the next one. -timeout: timeout in milliseconds -init: a lisp program, optional. Used to initialize the environment, useful to define custom functions which should be available everywhere. -programs: -- match: a filter with which to match the window - workspace: string or null, the workspace to move windows to - cmd: string or list, the command to execute - signal: boolean, should we wait before continuing with the next entry - timeout: timeout in milliseconds, used only if signal: true - how long to wait for the signal -``` - -The programs will be started asynchronously, except when `signal = true` which means that, before continuing -to the next program we wait for a signal. I would start all programs, which do not wait for a signal first -and then only the ones depending on the signal to reduce the startup delay. - -## Installing - -Use the makefile: `INSTALL_BASE=/usr/local/ make install` or install all dependencies -`python3 -mpip install --upgrade -r requirements.txt` and copy the script to your -path: `cp i3toolwait /usr/local/bin/i3toolwait`. - -## Filtering - -The program allows to match the window or container based on the returned IPC data. -Some programs might open multiple windows (looking at you, Discord). - -In order to move the correct window to the desired workspace a filter can be defined. - -The syntax for the filter is lisp-like. To view all spawned containers run the program -with `--debug --filter=False` which will not match any windows and print their properties. - -It is then possible to construct a filter for any program. - -Available Operators: - -- and: `&`: logical and, ungreedy -- or: `|`: logical or, ungreedy -- if: `?`: branch, if the first argument evaluates to `True` return the second, otherwise the third -- eq: `=`: equality -- neq: `!=`: inequality -- gt: `>`: greater than -- lt: `<`: less than -- load: `load`: load a key from the provided input `(load ".container.app_id")` -- has-key: `has-key`: check if a key is in the input: `(has-key ".container.app_id")` -- let: `let`: assign a local variable: `(let x 10)` -- setq: `setq`: assign a global variable: `(setq x 11)` -- defun: `defun`: user-defined functions: `((defun greet (a) (write (+ "Hello " a "!"))) (greet "Alice"))` - -For example: `(> (load ".container.geometry.width") 300)` would match the first window where the width is greater than 300. - -Multiple filters are combined via nesting: `(& (> (load ".container.geometry.width") 300) (= (load ".container.window_properties.class") "discord"))`. - -## Starting tray programs in a specific order - -To start tray programs in a specific order it is possible to specify the `signal` parameter. -Starting of programs will be halted until the program has received the corresponding signal. - -This could be combined with waybar to enforce an ordering of tray applications: - -`~/.config/waybar/config` -```json -"tray": { - "on-update": "pkill --full --signal SIGUSR1 i3toolwait", - "reverse-direction": true, -} -``` - -`config-file` -```yaml -signal: SIGUSR1 -timeout: 2000 +timeout: 10000 init: | - ( - (setq i3_path ".container.window_properties.class") - (setq sway_path ".container.app_id") - (defun "idmatch" (name) (= (? (has-key sway_path) (load sway_path) (load i3_path)) name)) + (begin + (define i3_path ".container.window_properties.class") + (define sway_path ".container.app_id") + (defun idmatch (name) (== (if (has-key sway_path) (load sway_path) (load i3_path)) name)) + (defun match (name) (and (== (load ".change") "new") (idmatch name))) + (defun match-load (name) (if (match name) (load ".container.id") F)) ) +cmd: 'workspace 1' programs: -- cmd: 'nm-applet --indicator' - match: '(False)' - timeout: 1000 - signal: true -- cmd: 'blueman-applet' - match: '(False)' - timeout: 1000 - signal: true -- ... +- run: 'exec gtk-launch librewolf' + cmd: 'for_window [con_id="{result}"] focus; move container to workspace 1' + match: '(match-load "LibreWolf")' +- run: 'exec gtk-launch nheko || gtk-launch io.element.Element' + cmd: 'for_window [con_id="{result}"] focus; move container to workspace 2' + match: '(if (or (match "Electron") (match "nheko")) (load ".container.id") F)' +- run: 'exec gtk-launch thunderbird' + cmd: 'for_window [con_id="{result}"] focus; move container to workspace 3' + match: '(match-load "thunderbird")' +- run: 'exec nm-applet --indicator' +- run: 'exec blueman-applet' +- run: 'exec gtk-launch org.kde.kdeconnect.nonplasma' +- run: 'exec gtk-launch syncthing-gtk' ``` -This setup would order the icons in waybar from left-to-right like in the config file. +## Configuration -## Troubleshooting +The configuration file is in YAML format. -### My windows do not get rearranged -It is very likely that the timeout is too short and the program exits before the window spawns. -Alternatively your filter might just be wrong. To debug execute the script with the `--debug` -flag to see if the window is recognized. +### Configuration + +#### timeout: int + +_Optional_ _Default_ `3000` + +Total program timeout in ms. + +#### init: String + +_Optional_ _Default_ `""` + +Initialization program; Used to initialize the environment, useful +to define custom functions which should be available everywhere. + +#### cmd: String + +_Optional_ _Default_ `""` + +A final i3 command to be executed before exiting. + +#### programs: List[Union[[Program](#program), [Signal](#signal)]] + +_Optional_ _Default_ `[]` + +A list of programs to execute. + +### Program + +Launch all programs using [`run`](#run-string) and execute +[`cmd`](#cmd-string-1) once [`match`](#match-string) matches +a window. + +#### match: String + +_Required_ + +A lisp program which analyzes the i3 window event and returns a value. +If the return value is `false` the window does not match and no +further processing occurs. Otherwise the i3 command +[`cmd`](#cmd-string-1). +will be executed. + +#### cmd: String + +_Required_ + +A i3 command. Can contain a format `{result}` which gets replaced +by the output of the match command. + +**Example:** + +`for_window [con_id="{result}"] focus; move container to window 1` + +#### run: String + +_Optional_ _Default_ `null` + +A i3 command which is run at program startup, can be used to launch +programs. + +**Example:** + +`exec gtk-launch firefox` + +### Signal + +Programs are launched in order and only advance after +[`timeout`](#timeout-int-1) or after receiving signal +`SIGUSR1`. + +#### run: String + +_Optional_ _Default_ `null` + +A i3 command. + +#### timeout: int + +_Optional_ _Default_ `500` + +How long to wait for the signal in ms. diff --git a/i3toolwait b/i3toolwait deleted file mode 100755 index 1ab3bf2..0000000 --- a/i3toolwait +++ /dev/null @@ -1,724 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -import string -import typing -import asyncio -import signal -import os -import time -import functools -import json -import logging - -import yaml -import click -import pydantic -import i3ipc -import i3ipc.aio - -try: - from yaml import CSafeLoader as SafeLoader -except ImportError: - from yaml import SafeLoader - -LOGGER = logging.getLogger('i3toolwait' if __name__ == '__main__' else __name__) - -def lazy_fc_if(env, local, a, b, c): - a.reduce(env, local) - if a.reduced: - b.reduce(env, local) - return b.reduced - c.reduce(env, local) - return c.reduced - -def lazy_fc_nif(env, local, a, b, c): - a.reduce(env, local) - if not a.reduced: - b.reduce(env, local) - return b.reduced - c.reduce(env, local) - c.reduced - -def lazy_fc_defun(env, local, name, variables, func): - _ = local - # need ugly hack, because variables are actually a function with n-1 args - varnames = [variables._fc] + [v._value for v in variables._args] - env.set_lisp_function(name._value, varnames, func) - -def fc_load(env, local, path): - _ = local - ipc_value = env.input - for k in path.strip('.').split('.'): - ipc_value = ipc_value[k] - return ipc_value - -def fc_has_key(env, local, path): - _ = local - ipc_value = env.input - for k in path.strip('.').split('.'): - try: - ipc_value = ipc_value[k] - except KeyError: - return False - return True - -class Environment: - - def __init__(self, input): - self._input = input - self._variables = {} - self._functions = { - '__last__': lambda _env, _local, *a: a[-1], # special function, if multiple expressions, execute all and return result of last one - 'setq': lambda env, _, n, v: env.set_variable(n, v), - 'let': lambda _, local, n, v: local.set_variable(n, v), - 'write': lambda _env, _local, a: print(a), - 'load': fc_load, - 'has-key': fc_has_key, - '=': lambda _, _l, a, b: a == b, - '!=': lambda _, _l, a, b: a != b, - '>': lambda _, _l, a, b: a > b, - '<': lambda _, _l, a, b: a < b, - '>=': lambda _, _l, a, b: a >= b, - '<=': lambda _, _l, a, b: a <= b, - '+': lambda _, _l, *a: functools.reduce(lambda a, b: a + b, a), - '-': lambda _, _l, a, b: a - b, - '*': lambda _, _l, *a: functools.reduce(lambda a, b: a * b, a), - '/': lambda _, _l, a, b: a // b, - '|': lambda _, _l, *a: functools.reduce(lambda a, b: a or b, a), - '&': lambda _, _l, *a: functools.reduce(lambda a, b: a and b, a), - } - self._lazy_functions = { - '?': lazy_fc_if, - '!?': lazy_fc_nif, - 'defun': lazy_fc_defun, - } - self._lisp_functions = {} - - @property - def input(self): - return self._input - - def set_variable(self, name: str, value: object): - self._variables[name] = value - - def get_variable(self, name: str): - return self._variables[name] - - def get_function(self, name: str): - return self._functions[name] - - def get_lazy_function(self, name: str): - return self._lazy_functions[name] - - def set_lisp_function(self, name: str, vars: list[object], e: object): - self._lisp_functions[name] = vars, e - - def get_lisp_function(self, name: str) -> tuple[list[str], object]: - return self._lisp_functions[name] - -class LocalEnvironment: - - def __init__(self): - self._variables = {} - - def copy(self) -> 'LocalEnvironment': - n = LocalEnvironment() - n._variables = self._variables.copy() - return n - - def set_variable(self, name: str, value: object): - self._variables[name] = value - - def get_variable(self, name: str): - return self._variables[name] - -class Expression: - - STATE_CONSTRUCTED = 0 - STATE_REDUCED = 1 - - def __init__(self): - self._state = Expression.STATE_CONSTRUCTED - self._reduced = None - - def _reduce(self, env: Environment, local: LocalEnvironment, args: list[object]): - _ = env, local, args - raise NotImplementedError('Implement in subclass') - - def reduce(self, env: Environment, local: LocalEnvironment): - self._reduced = self._reduce(env, local, []) - self._state = Expression.STATE_REDUCED - - @property - def reduced(self) -> object: - if self._state != Expression.STATE_REDUCED: - raise RuntimeError('Tried to get the reduced value before reducing') - return self._reduced - -class Constant(Expression): - - def __init__(self, value): - super().__init__() - self._value = value - - def __repr__(self): - if isinstance(self._value, str): - return f'"{self._value}"' - return repr(self._value) - - def _reduce(self, env: Environment, local: LocalEnvironment, args: list[Expression]): - _ = env, local, args - return self._value - -class VariableSet(Constant): - - def __repr__(self): - return self._value - -class VariableGet(Constant): - - def __repr__(self): - return self._value - - def _reduce(self, env: Environment, local: LocalEnvironment, args: list[Expression]): - _ = args - try: - return local.get_variable(self._value) - except KeyError: - return env.get_variable(self._value) - -class Function(Expression): - - def __init__(self, fc, args: list[Expression]): - super().__init__() - self._fc = fc - self._args = args - - def __repr__(self): - a = ' '.join([repr(a) for a in self._args]) - return f'({self._fc} {a})' - - def _reduce(self, env: Environment, local: LocalEnvironment, args: list[Expression]): - try: - argnames, fc = env.get_lisp_function(self._fc) - assert isinstance(fc, Expression) - l = local.copy() - for an, av in zip(argnames, args): - av.reduce(env, l) - l.set_variable(an, av.reduced) - fc.reduce(env, l) - r = fc.reduced - except KeyError as e: - try: - fc = env.get_function(self._fc) - [a.reduce(env, local) for a in args] - r = fc(env, local, *[a.reduced for a in args]) - except KeyError: - fc = env.get_lazy_function(self._fc) - r = fc(env, local, *args) - return r - - def reduce(self, env: Environment, local: LocalEnvironment): - self._reduced = self._reduce(env, local, self._args) - self._state = Expression.STATE_REDUCED - -class Token: - - CONSTANT_STRING = 0 - CONSTANT_INTEGER = 10 - CONSTANT_BOOLEAN = 20 - KEYWORD = 30 - VARIABLE_SET = 40 - VARIABLE_GET = 50 - FUNCTION = 60 - GROUPING_OPEN = 70 - GROUPING_CLOSE = 80 - WHITESPACE = 90 - - def __init__(self, t, v): - self.t = t - self.v = v - - def __repr__(self): - return f'{self.v}::{self.t}' - - def to_expression(self): - if self.t == Token.CONSTANT_STRING: - return Constant(self.v[1:-1]) # slice away the quotes - if self.t == Token.CONSTANT_INTEGER: - return Constant(int(self.v, base=0)) - if self.t == Token.CONSTANT_BOOLEAN: - return Constant(self.v == 'True') - if self.t == Token.KEYWORD: - raise RuntimeError(f'This is a meta token type and should be swallowed by the sanitizer: {self}') - if self.t == Token.VARIABLE_GET: - return VariableGet(self.v) - if self.t == Token.VARIABLE_SET: - return VariableSet(self.v) - if self.t == Token.FUNCTION: - raise RuntimeError('Cant construct function just from its token') - if self.t == Token.GROUPING_OPEN or self.t == Token.GROUPING_CLOSE: - raise RuntimeError('Groupings should never be constructed, this is a bug') - if self.t == Token.WHITESPACE: - raise RuntimeError('Whitespaces should not be present in this stage of the build') - raise RuntimeError(f'The token type {self.t} is not implemented') - -def token_extract_string(stream: str) -> tuple[Token, str]: - if stream[0] != '"': - raise ValueError('No such token in stream') - i = stream.find('"', 1) - return Token(Token.CONSTANT_STRING, stream[:i+1]), stream[i+1:] - -def token_extract_integer(stream: str) -> tuple[Token, str]: - i = 0 - base = None - if stream[i] in '+-': - i += 1 - if stream[i] in '0123456789': - i += 1 - else: - raise ValueError('Malformed integer') - - if stream[i] in 'xbo': - base = stream[i] - i += 1 - int_set = {None: '0123456789', 'x': '0123456789abcdefABCDEF', 'b': '01', 'o': '01234567'}[base] - while stream[i] in int_set: - i += 1 - return Token(Token.CONSTANT_INTEGER, stream[:i]), stream[i:] - -def token_extract_boolean(stream: str) -> tuple[Token, str]: - if stream.startswith('True'): - return Token(Token.CONSTANT_BOOLEAN, stream[:4]), stream[4:] - elif stream.startswith('False'): - return Token(Token.CONSTANT_BOOLEAN, stream[:5]), stream[5:] - raise ValueError('No such token in stream') - -def token_extract_keyword(stream: str) -> tuple[Token, str]: - i = 0 - if stream[i] in string.ascii_letters + '_-><=!+-*/?&|': - i += 1 - else: - raise ValueError('No keyword in stream') - while stream[i] in string.ascii_letters + string.digits + '_-><=!+-*/?&|': - i += 1 - return Token(Token.KEYWORD, stream[:i]), stream[i:] - -def token_extract_grouping_open(stream: str) -> tuple[Token, str]: - if stream[0] == '(': - return Token(Token.GROUPING_OPEN, '('), stream[1:] - raise ValueError('No such token in stream') - -def token_extract_grouping_close(stream: str) -> tuple[Token, str]: - if stream[0] == ')': - return Token(Token.GROUPING_CLOSE, ')'), stream[1:] - raise ValueError('No such token in stream') - -def token_extract_space(stream: str) -> tuple[Token, str]: - i = 0 - try: - while stream[i] in string.whitespace: - i += 1 - except IndexError: - pass - return Token(Token.WHITESPACE, stream[:i]), stream[i:] - -def tokenize(program: str) -> list[Token]: - extractors = [ - token_extract_boolean, - token_extract_integer, - token_extract_string, - token_extract_keyword, - token_extract_grouping_open, - token_extract_grouping_close, - token_extract_space, - ] - p = program - tokens = [] - while p: - success = False - for e in extractors: - try: - t, p = e(p) - tokens += [t] - success = True - break - except ValueError: - pass - if not success: - raise ValueError('Program is invalid') - return [t for t in tokens if t.t != Token.WHITESPACE] - -def tokenize_sanitize_function(token_before: Token | None, token: Token, token_after: Token | None) -> Token | None: - if token_before is None: - return - if token_before.t == Token.GROUPING_OPEN and token.t == Token.KEYWORD: - return Token(Token.FUNCTION, token.v) - -def tokenize_sanitize_setvar(token_before: Token | None, token: Token, token_after: Token | None) -> Token | None: - if token_before is None: - return - if (token_before.t == Token.FUNCTION and token_before.v in ('setq', 'let')) and token.t == Token.KEYWORD: - return Token(Token.VARIABLE_SET, token.v) - -def tokenize_sanitize_getvar(token_before: Token | None, token: Token, token_after: Token | None) -> Token | None: - if token_before is None: - if token.t == Token.KEYWORD: - return Token(Token.VARIABLE_GET, token.v) - return - if (token_before.t != Token.FUNCTION or token_before.v not in ('setq', 'let')) and token.t == Token.KEYWORD: - return Token(Token.VARIABLE_GET, token.v) - -def _tokenize_sanitize(tokens: list[Token]) -> tuple[bool, list[Token]]: - sanitizers = [ - tokenize_sanitize_function, - tokenize_sanitize_setvar, - tokenize_sanitize_getvar, - ] - new_tokens = [] - changed = False - for i in range(len(tokens)): - for s in sanitizers: - p_token = new_tokens[i-1] if i > 0 else None - n_token = tokens[i+1] if i < (len(tokens)-1) else None - new_token = s(p_token, tokens[i], n_token) - if new_token is not None: - changed = True - new_tokens += [new_token] - break - else: - new_tokens += [tokens[i]] - return changed, new_tokens - -def tokenize_sanitize(tokens: list[Token]) -> list[Token]: - _, tokens = _tokenize_sanitize(tokens) - return tokens - -def take_token_group(tokens: list[Token], n: int = 1) -> list[Token]: - i = 0 - start = i - group_count = 0 - consider_groups = False - while n: - if tokens[i].t == Token.GROUPING_OPEN: - consider_groups = True - if group_count == 0: - start = i - group_count += 1 - elif tokens[i].t == Token.GROUPING_CLOSE: - group_count -= 1 - if group_count == 0: - consider_groups = False - else: - if not consider_groups: - start = i - if group_count == 0: - n -= 1 - if group_count < 0: - raise ValueError('reached past end') - i += 1 - return tokens[start:i] - -def unwrap_token_group(tokens: list[Token]) -> list[Token]: - if tokens[0].t != Token.GROUPING_OPEN: - return tokens - - brace_count = 0 - for i, t in enumerate(tokens): - brace_count += int(t.t == Token.GROUPING_OPEN) - brace_count -= int(t.t == Token.GROUPING_CLOSE) - if i == len(tokens) - 2: - if brace_count > 0: - tokens = tokens[1:-1] - break - return tokens - -def build(tokens: list[Token]) -> Expression: - tokens = unwrap_token_group(tokens) - token_groups: list[list[Token]] = [] - i = 1 - while True: - try: - token_groups += [take_token_group(tokens, n=i)] - i += 1 - except IndexError: - break - - # special function case - if len(token_groups[0]) == 1 and token_groups[0][0].t == Token.FUNCTION: - token_0 = token_groups[0][0] - args = [build(tg) for tg in token_groups[1:]] - return Function(token_0.v, args) - - # combine to multiple statements - if len(token_groups) > 1: - return Function('__last__', [build(tg) for tg in token_groups]) - - # create a basic expression - if len(token_groups) == 1 and len(token_groups[0]) == 1: - return token_groups[0][0].to_expression() - - raise RuntimeError(f'Did not handle token case in build function, token_groups: {token_groups}') - -def parse(program: str) -> Expression: - tokens = tokenize_sanitize(tokenize(program)) - expression = build(tokens) - return expression - -class Filter(Expression): - - @classmethod - def __get_validators__(cls): - yield cls.validate - - @classmethod - def __modify_schema__(cls, field_schema): - pass - - @classmethod - def validate(cls, v): - if not isinstance(v, str): - raise TypeError('Must be string') - return parse(v) - -class Command(str): - - @classmethod - def __get_validators__(cls): - yield cls.validate - - @classmethod - def __modify_schema__(cls, field_schema): - pass - - @classmethod - def validate(cls, v): - if not isinstance(v, (str, list, tuple)): - raise TypeError('Must be string or list') - if isinstance(v, (list, tuple)): - v = ' '.join([f"'{x}'" for x in v]) - return v - -class Signal(int): - - @classmethod - def __get_validators__(cls): - yield cls.validate - - @classmethod - def __modify_schema__(cls, field_schema): - pass - - @classmethod - def validate(cls, v): - if not isinstance(v, (str, int)): - raise TypeError('Must be string or int') - if isinstance(v, str) and v.isnumeric(): - return signal.Signals(int(v)) - elif isinstance(v, int): - return signal.Signals(v) - return getattr(signal.Signals, v) - -class Lock(asyncio.Lock): - @classmethod - def __get_validators__(cls): - yield cls.validate - @classmethod - def __modify_schema__(cls, field_schema): - pass - @classmethod - def validate(cls, v): - if not isinstance(v, asyncio.Lock): - raise TypeError('Must be a asyncio.Lock') - return v - -class Event(asyncio.Event): - @classmethod - def __get_validators__(cls): - yield cls.validate - @classmethod - def __modify_schema__(cls, field_schema): - pass - @classmethod - def validate(cls, v): - if not isinstance(v, asyncio.Event): - raise TypeError('Must be a asyncio.Event') - return v - -class Connection(i3ipc.aio.Connection): - @classmethod - def __get_validators__(cls): - yield cls.validate - @classmethod - def __modify_schema__(cls, field_schema): - pass - @classmethod - def validate(cls, v): - if not isinstance(v, i3ipc.aio.Connection): - raise TypeError('Must be a i3ipc.aio.Connection') - return v - - -class ProgramConfig(pydantic.BaseModel): - cmd: Command - workspace: typing.Optional[str] = None - signal: bool = False - timeout: int = 1000 - match: Filter - -class Config(pydantic.BaseModel): - signal: typing.Optional[Signal] = None - timeout: int = 3000 - init: typing.Optional[Filter] = None - programs: typing.List[ProgramConfig] - final_workspace: typing.Optional[str] = None - final_workspace_delay: int = 100 - -class RuntimeData(pydantic.BaseModel): - init: typing.Optional[str] - programs: typing.List[ProgramConfig] = [] - lock: Lock - event: Event - ipc: Connection - -def window_new(runtime_data: RuntimeData, *, debug): - async def callback(ipc: i3ipc.aio.Connection, e: i3ipc.WorkspaceEvent): - assert e.change == 'new' - LOGGER.debug('New window: %s', json.dumps(e.ipc_data)) - async with runtime_data.lock: - env = Environment(e.ipc_data) - local = LocalEnvironment() - if runtime_data.init is not None: - parse(runtime_data.init).reduce(env, local) - for i, cfg in enumerate(runtime_data.programs): - cfg.match.reduce(env, local) - LOGGER.debug('Tried to match %s, result: %s', cfg.match, cfg.match.reduced) - if cfg.match.reduced: - container_id = e.ipc_data['container']['id'] - await ipc.command(f'for_window [con_id="{container_id}"] focus') - await ipc.command(f'move container to workspace {cfg.workspace}') - runtime_data.programs.pop(i) - if not runtime_data.programs: - ipc.main_quit() - return callback - -async def wait_signal(rt: RuntimeData): - await rt.event.wait() - rt.event.clear() - -async def coro_wait_signal(coro, rt: RuntimeData): - await coro - await wait_signal(rt) - -async def init(config: Config, *, debug: bool) -> RuntimeData: - rd = RuntimeData( - init=str(config.init), - programs=[p for p in config.programs if p.workspace is not None], - lock=Lock(), - event=Event(), - ipc=Connection(), - ) - logging.basicConfig(level=logging.WARNING) - if debug: - LOGGER.setLevel(logging.DEBUG) - else: - LOGGER.setLevel(logging.INFO) - if config.signal is not None: - asyncio.get_running_loop().add_signal_handler(config.signal, lambda: rd.event.set()) - return rd - -async def run(config: Config, *, debug: bool): - runtime_data = await init(config, debug=debug) - await runtime_data.ipc.connect() - handler = window_new(runtime_data, debug=debug) - runtime_data.ipc.on('window::new', handler) - - variables = { - 'pid': os.getpid(), - } - coroutines = [] - timeout = config.timeout - started_at = time.monotonic_ns() - for cfg in config.programs: - p = cfg.cmd.format(**variables) - coro = runtime_data.ipc.command(f'exec {p}') - if cfg.signal: - coro = coro_wait_signal(coro, runtime_data) - if cfg.timeout is not None: - timeout = max(timeout, cfg.timeout) - try: - await asyncio.wait_for(coro, timeout=cfg.timeout/1000 if cfg.timeout is not None else 0) - except asyncio.TimeoutError: - pass - else: - coroutines += [coro] - await asyncio.gather(*coroutines) - try: - if runtime_data.programs: - # run main loop only if we wait for something - diff = (time.monotonic_ns() - started_at) / (1000*1000) - new_timeout = max(timeout - diff, 0) - await asyncio.wait_for(runtime_data.ipc.main(), timeout=new_timeout/1000) - except asyncio.TimeoutError: - runtime_data.ipc.off(handler) - if runtime_data.programs: - LOGGER.debug('Not all programs consumed: %s', runtime_data.programs) - LOGGER.debug('Maybe the timeouts are too short?') - return 1 - finally: - if config.final_workspace is not None: - await asyncio.sleep(config.final_workspace_delay/1000) - await runtime_data.ipc.command(f'workspace {config.final_workspace}') - return 0 - -@click.group() -@click.pass_context -@click.option('--debug', '-d', default=False, is_flag=True, help="Enable debug mode, will log ipc dictionary.") -def main(ctx, debug): - ctx.ensure_object(dict) - ctx.obj['DEBUG'] = debug - -@main.command() -@click.pass_context -@click.option('--filter', '-f', default='True', help="A filter expression for the raw ipc dictionary.") -@click.option('--timeout', '-t', default=3000, help="Wait time for a window to appear (and match) in milliseconds.") -@click.option('--workspace', '-w', default=None, help="The workspace to move to.") -@click.argument('command', nargs=-1) -def simple(ctx, filter, timeout, workspace, command): - """ - Start a program and move it's created window to the desired i3 workspace. - - \b - Exist status: - 0 on success, - 1 when no window has been found. - """ - debug = ctx.obj['DEBUG'] - config = Config(programs=[ProgramConfig( - cmd=command, - workspace=workspace, - match=filter, - )], timeout=timeout) - ctx.exit(asyncio.run(run(config, debug=debug))) - -@main.command() -@click.pass_context -@click.argument('config', type=click.File('r'), default='-') -def config(ctx, config): - """ - Start a program and move it's created window to the desired i3 workspace. - - \b - Exist status: - 0 on success, - 1 when no window has been found. - """ - debug = ctx.obj['DEBUG'] - config = Config(**yaml.load(config, Loader=SafeLoader)) - ctx.exit(asyncio.run(run(config, debug=debug))) - -if __name__ == '__main__': - main() - diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 634d488..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -click -pydantic -pyyaml -i3ipc - diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..4b2f237 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,105 @@ +use std::fmt::{Display, Formatter}; + +use rust_lisp::model::Value as RValue; +use serde::{Deserialize, Deserializer}; + +#[derive(Clone, Debug)] +pub struct Value(Vec); +unsafe impl Send for Value {} +unsafe impl Sync for Value {} + +impl Into for RValue { + fn into(self) -> Value { + Value(vec![self]) + } +} + +impl Into> for Value { + fn into(self) -> Vec { + self.0 + } +} + +impl Display for Value { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + let mut s = String::new(); + s.push_str("(begin\n"); + for i in &self.0 { + s.push_str(&format!("{}\n", i)); + } + s.push_str(")"); + write!(f, "{}", &s) + } +} + +impl<'de> Deserialize<'de> for Value { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s: String = Deserialize::deserialize(deserializer)?; + let r: Vec = rust_lisp::parser::parse(&s) + .filter_map(|x| x.ok()) + .collect(); + Ok(Value(r)) + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Program { + #[serde(rename = "match")] + pub match_: Value, + pub cmd: String, + #[serde(default)] + pub run: Option, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Signal { + #[serde(default)] + pub run: Option, + #[serde(default = "Signal::default_timeout")] + pub timeout: u64, +} +impl Signal { + fn default_timeout() -> u64 { + 500 + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +pub enum ProgramEntry { + Program(Program), + Signal(Signal), +} + +// Program is only unsafe because Value has dyn Any in it (via Foreign). +// if we don't use !Send in Foreign everything is fine. +unsafe impl Send for Program {} + +#[derive(Clone, Debug, Deserialize)] +pub struct Config { + #[serde(default = "Config::default_timeout")] + pub timeout: u64, + #[serde(default = "Config::default_init")] + pub init: Value, + #[serde(default)] + pub cmd: Option, + #[serde(default = "Config::default_programs")] + pub programs: Vec, +} +// Config is only unsafe because Value has dyn Any in it (via Foreign). +// if we don't use !Send in Foreign everything is fine. +unsafe impl Send for Config {} +impl Config { + fn default_timeout() -> u64 { + 3000 + } + fn default_init() -> Value { + Value(vec![]) + } + fn default_programs() -> Vec { + vec![] + } +} diff --git a/src/i3ipc.rs b/src/i3ipc.rs new file mode 100644 index 0000000..b4c2024 --- /dev/null +++ b/src/i3ipc.rs @@ -0,0 +1,236 @@ +use std::collections::HashMap; +use std::pin::Pin; +use std::str::FromStr; + + +use anyhow::Result; + +use serde::{Deserialize, Serialize}; +use tokio::io::{AsyncReadExt, AsyncWriteExt, BufStream}; +use tokio::net::UnixStream; + +pub async fn get_socket_path() -> Result { + if let Ok(p) = std::env::var("I3SOCK") { + return Ok(std::path::PathBuf::from_str(&p).unwrap()); + } + if let Ok(p) = std::env::var("SWAYSOCK") { + return Ok(std::path::PathBuf::from_str(&p).unwrap()); + } + + for command_name in ["i3", "sway"] { + let output = tokio::process::Command::new(command_name) + .arg("--get-socketpath") + .output() + .await?; + if output.status.success() { + return Ok(std::path::PathBuf::from_str( + String::from_utf8_lossy(&output.stdout).trim_end_matches('\n'), + ) + .unwrap()); + } + } + + Err(tokio::io::Error::new(tokio::io::ErrorKind::Other, ""))? +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[repr(u32)] +pub enum MessageType { + Command = 0, + Workspace = 1, + Subscribe = 2, + Outputs = 3, + Tree = 4, + Marks = 5, + BarConfig = 6, + Version = 7, + BindingModes = 8, + Config = 9, + Tick = 10, + Sync = 11, + BindingState = 12, + #[serde(rename = "workspace")] + SubWorkspace = 0 | 1 << 31, + #[serde(rename = "output")] + SubOutput = 1 | 1 << 31, + #[serde(rename = "mode")] + SubMode = 2 | 1 << 31, + #[serde(rename = "window")] + SubWindow = 3 | 1 << 31, + #[serde(rename = "barconfig_update")] + SubBarConfig = 4 | 1 << 31, + #[serde(rename = "binding")] + SubBinding = 5 | 1 << 31, + #[serde(rename = "shutdown")] + SubShutdown = 6 | 1 << 31, + #[serde(rename = "tick")] + SubTick = 7 | 1 << 31, +} + +impl MessageType { + pub fn is_subscription(&self) -> bool { + ((*self as u32) & (1 << 31)) != 0 + } +} + +impl TryFrom for MessageType { + type Error = &'static str; + fn try_from(value: u32) -> Result { + Ok(match value { + 0x00000000 => Self::Command, + 0x00000001 => Self::Workspace, + 0x00000002 => Self::Subscribe, + 0x00000003 => Self::Outputs, + 0x00000004 => Self::Tree, + 0x00000005 => Self::Marks, + 0x00000006 => Self::BarConfig, + 0x00000007 => Self::Version, + 0x00000008 => Self::BindingModes, + 0x00000009 => Self::Config, + 0x0000000a => Self::Tick, + 0x0000000b => Self::Sync, + 0x0000000c => Self::BindingState, + 0x80000000 => Self::SubWorkspace, + 0x80000001 => Self::SubOutput, + 0x80000002 => Self::SubMode, + 0x80000003 => Self::SubWindow, + 0x80000004 => Self::SubBarConfig, + 0x80000005 => Self::SubBinding, + 0x80000006 => Self::SubShutdown, + 0x80000007 => Self::SubTick, + _ => return Err(""), + }) + } +} + +type SubscriptionCallback = + dyn Fn( + MessageType, + serde_json::Value, + ) -> Pin)>> + Send>>; + +pub struct Connection<'a> { + stream: BufStream, + subscriptions: HashMap>, +} + +impl<'a> Connection<'a> { + pub fn connect(path: &std::path::Path) -> Result { + let stream = std::os::unix::net::UnixStream::connect(path)?; + stream.set_nonblocking(true)?; + let stream = BufStream::new(UnixStream::from_std(stream)?); + let subscriptions = HashMap::new(); + Ok(Self { + stream, + subscriptions, + }) + } + + pub async fn send_message( + &mut self, + message_type: &MessageType, + message: &[u8], + ) -> Result<(), anyhow::Error> { + self.stream.write_all(b"i3-ipc").await?; + self.stream.write_u32_le(message.len() as u32).await?; + self.stream.write_u32_le(*message_type as u32).await?; + self.stream.write_all(message).await?; + self.stream.flush().await?; + Ok(()) + } + + pub async fn receive_message(&mut self) -> Result<(MessageType, Vec), anyhow::Error> { + let mut buffer = vec![0u8; 6]; + self.stream.read_exact(&mut buffer).await?; + if buffer != b"i3-ipc" { + return Err(tokio::io::Error::new(tokio::io::ErrorKind::Other, ""))?; + } + let message_len = self.stream.read_u32_le().await?; + let message_type = self.stream.read_u32_le().await?.try_into().unwrap(); + let mut buffer = vec![0u8; message_len as usize]; + self.stream.read_exact(&mut buffer).await?; + Ok((message_type, buffer)) + } + + pub async fn communicate( + &mut self, + message_type: &MessageType, + message: &[u8], + ) -> Result<(MessageType, serde_json::Value), anyhow::Error> { + self.send_message(message_type, message).await?; + let (message_type, response) = self.receive_message().await?; + Ok(( + message_type, + serde_json::from_str(String::from_utf8_lossy(response.as_ref()).as_ref())?, + )) + } + + pub async fn subscribe( + &mut self, + events: &[MessageType], + callback: &'a SubscriptionCallback, + ) -> Result<(), anyhow::Error> { + let json = serde_json::to_string(events)?; + let (message_type, response) = self + .communicate(&MessageType::Subscribe, json.as_bytes()) + .await?; + for s in events { + self.subscriptions.insert(*s, Box::new(callback)); + } + Ok(()) + } + + pub async fn call_callback( + &mut self, + subscription: &MessageType, + response: serde_json::Value, + ) -> Vec<(MessageType, Vec)> { + let cb = self.subscriptions.get(subscription); + if cb.is_none() { + return Vec::new(); + } + (*cb.unwrap())(*subscription, response).await + } + + pub async fn run( + &mut self, + rx: &mut tokio::sync::broadcast::Receiver<()>, + ) -> Result<(), anyhow::Error> { + loop { + let stop_task = rx.recv(); + let receive_message_task = self.receive_message(); + let result = tokio::select! { + _ = stop_task => {return Ok(())}, + result = receive_message_task => result?, + }; + let (message_type, response) = result; + if !message_type.is_subscription() { + continue; + } + + let json_response = + serde_json::from_str(String::from_utf8_lossy(response.as_ref()).as_ref())?; + let messages: Vec<(MessageType, Vec)> = + self.call_callback(&message_type, json_response).await; + for (message_type, message) in messages { + // TODO maybe log responses? + self.communicate(&message_type, &message).await?; + } + } + } +} + +impl<'a> Clone for Connection<'a> { + fn clone(&self) -> Self { + let path: std::path::PathBuf = self + .stream + .get_ref() + .peer_addr() + .unwrap() + .as_pathname() + .unwrap() + .into(); + Self::connect(path.as_ref()).unwrap() + } +} diff --git a/src/lisp.rs b/src/lisp.rs new file mode 100644 index 0000000..3e2a0b4 --- /dev/null +++ b/src/lisp.rs @@ -0,0 +1,119 @@ +use std::collections::HashMap; + +use rust_lisp::model::{reference, reference::Reference, Env, FloatType, IntType, List, Value}; + +fn serde_lisp_value(value: &serde_json::Value) -> Value { + match value { + serde_json::Value::Null => Value::NIL, + serde_json::Value::Bool(b) => { + if *b { + Value::True + } else { + Value::False + } + } + serde_json::Value::Number(n) => { + if n.is_i64() { + Value::Int(n.as_i64().unwrap() as IntType) + } else if n.is_u64() { + Value::Int(n.as_u64().unwrap() as IntType) + } else if n.is_f64() { + Value::Float(n.as_f64().unwrap() as FloatType) + } else { + panic!("should never happen"); + } + } + serde_json::Value::String(s) => Value::String(s.clone()), + serde_json::Value::Array(a) => { + let mut l = List::NIL; + for li in a.into_iter().rev() { + l = l.cons(serde_lisp_value(li)); + } + Value::List(l) + } + serde_json::Value::Object(o) => { + let mut r = HashMap::new(); + for (k, v) in o.into_iter() { + let k_ = Value::String(k.clone()); + let v_ = serde_lisp_value(v); + r.insert(k_, v_); + } + Value::HashMap(reference::new(r)) + } + } +} + +pub fn env(value: &serde_json::Value) -> Env { + let mut environment = rust_lisp::default_env(); + environment.define( + rust_lisp::model::Symbol::from("__input__"), + serde_lisp_value(value), + ); + environment.define( + rust_lisp::model::Symbol::from("load"), + rust_lisp::model::Value::NativeClosure(reference::new( + move |e: Reference, args: Vec| { + let path: &String = + rust_lisp::utils::require_typed_arg::<&String>("load", &args, 0)?; + let path = (*path).as_str().split('.'); + let mut i: rust_lisp::model::Value = reference::borrow(&e) + .get(&rust_lisp::model::Symbol::from("__input__")) + .unwrap(); + for p in path + .into_iter() + .filter(|x| !(*x).eq("")) + .map(|x| rust_lisp::model::Value::String(x.into())) + { + match i { + rust_lisp::model::Value::HashMap(x) => { + if let Some(_i) = reference::borrow(&x).get(&p) { + i = _i.clone(); + } else { + return Err(rust_lisp::model::RuntimeError { + msg: format!(r#"No such key {:?}"#, p).into(), + }); + } + } + _ => { + return Err(rust_lisp::model::RuntimeError { + msg: format!(r#"No such key {:?}"#, p).into(), + }) + } + }; + } + Ok(i) + }, + )), + ); + environment.define( + rust_lisp::model::Symbol::from("has-key"), + rust_lisp::model::Value::NativeClosure(reference::new( + move |e: Reference, args: Vec| { + let path: &String = + rust_lisp::utils::require_typed_arg::<&String>("has-key", &args, 0)?; + let path = (*path).as_str().split('.'); + let mut i: rust_lisp::model::Value = reference::borrow(&e) + .get(&rust_lisp::model::Symbol::from("__input__")) + .unwrap(); + for p in path + .into_iter() + .filter(|x| !(*x).eq("")) + .map(|x| rust_lisp::model::Value::String(x.into())) + { + match i { + rust_lisp::model::Value::HashMap(x) => { + if let Some(_i) = reference::borrow(&x).get(&p) { + i = _i.clone(); + } else { + return Ok(rust_lisp::model::Value::False); + } + } + _ => return Ok(rust_lisp::model::Value::False), + }; + } + Ok(rust_lisp::model::Value::True) + }, + )), + ); + environment +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c88504d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,213 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::str::FromStr; + +use anyhow::Result; +use clap::Parser; +use log::{debug, info, warn}; +use tokio::io::AsyncReadExt; +use tokio::time::{timeout, Duration}; + +mod config; +mod i3ipc; +mod lisp; + +use config::{Config, ProgramEntry}; +use i3ipc::{Connection, MessageType}; + +#[derive(Debug, Clone, Parser)] +#[command(author, version, about, long_about = None)] +struct Args { + #[arg(short, long, value_name = "FILE")] + config: Option, +} + +impl Args { + fn finish(&mut self) { + // TODO maybe return separate type + if self.config.is_none() { + self.config = Some( + xdg::BaseDirectories::with_prefix("i3toolwait") + .unwrap() + .get_config_file("config.yaml"), + ); + } + } +} + +fn new_window_cb( + _b: MessageType, + c: serde_json::Value, + config: &Config, + _args: &Args, + programs: &std::sync::Arc>>, + tx: &tokio::sync::broadcast::Sender<()>, +) -> futures::future::BoxFuture<'static, Vec<(MessageType, Vec)>> { + let config_ = config.clone(); + let tx_ = tx.clone(); + let programs_ = programs.clone(); + Box::pin(async move { + let mut command = None; + let mut index = None; + debug!("Received window event: {}", &c); + for (i, p) in programs_.lock().await.iter().enumerate() { + match p { + ProgramEntry::Program(p) => { + debug!("Evaluating program: {}", &p.match_); + let e = lisp::env(&c); + let init: Vec = config_.init.clone().into(); + let prog: Vec = p.match_.clone().into(); + let m = init.into_iter().chain(prog.into_iter()); + let result = + rust_lisp::interpreter::eval_block(rust_lisp::model::reference::new(e), m); + if let Ok(v) = &result { + debug!("Received result: {}", v); + if *v == rust_lisp::model::Value::False { + continue; + } + debug!("Match found"); + let mut vars = HashMap::with_capacity(1); + vars.insert("result".to_string(), v.to_string()); + let cmd = strfmt::strfmt(&p.cmd, &vars).unwrap(); + debug!("Command: {}", &cmd); + + index = Some(i); + command = Some(cmd); + break; + } else { + warn!("Program produced an error: {:?}", &result); + } + } + _ => { + // Ignore signal entries + () + } + }; + } + if let Some(index) = index { + let mut plock = programs_.lock().await; + plock.remove(index); + if plock.len() == 0 { + tx_.send(()).unwrap(); + } + return vec![(MessageType::Command, command.unwrap().into_bytes())]; + } + debug!("No match found"); + Vec::new() + }) +} + +async fn run_command<'a>( + connection: &mut Connection<'a>, + command: &str, +) -> Result<(), anyhow::Error> { + let (_, responses) = connection + .communicate(&MessageType::Command, command.as_bytes()) + .await?; + match responses { + serde_json::Value::Array(responses) => { + for response in responses { + if let serde_json::Value::Bool(v) = response.get("success").unwrap() { + if !v { + warn!("Failed to run command {}: {}", command, response); + } + } + } + } + _ => panic!("invalid response"), + }; + Ok(()) +} + +async fn run<'a>(connection: &mut Connection<'a>, config: &Config) -> Result<(), anyhow::Error> { + let (_, resp) = connection.communicate(&MessageType::Version, b"").await?; + info!("i3 version is {}", resp.get("human_readable").unwrap()); + + let mut signal_stream = + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::user_defined1())?; + + for p in config.programs.iter() { + match p { + ProgramEntry::Program(p) => { + if let Some(r) = &p.run { + run_command(connection, r).await?; + } + } + ProgramEntry::Signal(p) => { + if let Some(r) = &p.run { + run_command(connection, r).await?; + } + if let Err(_) = + timeout(Duration::from_millis(p.timeout), signal_stream.recv()).await + { + warn!( + "Ran into timeout when waiting for signal, program: {:?}", + p.run + ); + } + } + }; + } + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<()> { + env_logger::init_from_env( + env_logger::Env::new() + .filter("I3TOOLWAIT_LOG") + .write_style("I3TOOLWAIT_LOG_STYLE"), + ); + + let mut args = Args::parse(); + args.finish(); + let args = std::sync::Arc::new(args); + let mut config = String::new(); + if args.config.as_ref().unwrap() == &PathBuf::from_str("-").unwrap() { + tokio::io::stdin().read_to_string(&mut config).await?; + } else { + tokio::fs::File::open(args.config.as_ref().unwrap()) + .await? + .read_to_string(&mut config) + .await?; + } + let config: Config = serde_yaml::from_str(&config)?; + let config = std::sync::Arc::new(config); + let programs = std::sync::Arc::new(tokio::sync::Mutex::new(config.programs.clone())); + + let mut connection = Connection::connect((i3ipc::get_socket_path().await?).as_ref())?; + let mut sub_connection = connection.clone(); + let cb_config = config.clone(); + let cb_args = args.clone(); + + let (tx, mut rx) = tokio::sync::broadcast::channel::<()>(1); + + let cb_programs = programs.clone(); + let cb = move |a, b| new_window_cb(a, b, &cb_config, &cb_args, &cb_programs, &tx); + sub_connection + .subscribe(&[MessageType::SubWindow], &cb) + .await?; + + tokio::join!( + timeout( + Duration::from_millis(config.timeout), + sub_connection.run(&mut rx) + ), + run(&mut connection, &config), + ) + .1?; + { + let p = programs.lock().await; + if p.len() != 0 { + warn!("Not all programs consumed: {:?}", &p); + info!("Maybe the timouts are too short?"); + } + } + + if let Some(cmd) = &config.cmd { + connection + .communicate(&MessageType::Command, cmd.as_bytes()) + .await?; + } + Ok(()) +}