Compare commits

..

13 commits

11 changed files with 1670 additions and 832 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

851
Cargo.lock generated Normal file
View file

@ -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.3.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"

21
Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "i3toolwait"
version = "0.3.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"

View file

@ -1,9 +1,17 @@
EXEC := i3toolwait
INSTALL_BASE ?= /usr/local INSTALL_BASE ?= /usr/local
install: i3toolwait install-modules default: target/debug/${EXEC}
install -Dm0755 -oroot -groot $< ${INSTALL_BASE}/bin/$< release: target/release/${EXEC}
default: target/debug/${EXEC}
install-modules: requirements.txt install: target/release/${EXEC}
python3 -mpip install --upgrade --requirement $< 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

207
README.md
View file

@ -4,114 +4,125 @@ Launch a program and move it to the correct workspace.
## Usage ## Usage
- **simple:** `i3toolwait simple ...` `i3toolwait -c FILE`
- **config:** `i3toolwait config ...`
### 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. ## Example
### Config
Run multiple programs by specifying a yaml configuration file:
```yaml ```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: 10000
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
init: | init: |
( (begin
(setq i3_path ".container.window_properties.class") (define i3_path ".container.window_properties.class")
(setq sway_path ".container.app_id") (define sway_path ".container.app_id")
(defun "idmatch" (name) (= (? (has-key sway_path) (load sway_path) (load i3_path)) name)) (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: programs:
- cmd: 'nm-applet --indicator' - run: 'exec gtk-launch librewolf'
match: '(False)' cmd: 'for_window [con_id="{result}"] focus; move container to workspace 1'
timeout: 1000 match: '(match-load "LibreWolf")'
signal: true - run: 'exec gtk-launch nheko || gtk-launch io.element.Element'
- cmd: 'blueman-applet' cmd: 'for_window [con_id="{result}"] focus; move container to workspace 2'
match: '(False)' match: '(if (or (match "Electron") (match "nheko")) (load ".container.id") F)'
timeout: 1000 - run: 'exec gtk-launch thunderbird'
signal: true 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. ### Configuration
Alternatively your filter might just be wrong. To debug execute the script with the `--debug`
flag to see if the window is recognized. #### 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.

View file

@ -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()

View file

@ -1,5 +0,0 @@
click
pydantic
pyyaml
i3ipc

105
src/config.rs Normal file
View file

@ -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<RValue>);
unsafe impl Send for Value {}
unsafe impl Sync for Value {}
impl Into<Value> for RValue {
fn into(self) -> Value {
Value(vec![self])
}
}
impl Into<Vec<RValue>> for Value {
fn into(self) -> Vec<RValue> {
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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
let r: Vec<RValue> = 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<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Signal {
#[serde(default)]
pub run: Option<String>,
#[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<String>,
#[serde(default = "Config::default_programs")]
pub programs: Vec<ProgramEntry>,
}
// 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<ProgramEntry> {
vec![]
}
}

236
src/i3ipc.rs Normal file
View file

@ -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<std::path::PathBuf, anyhow::Error> {
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<u32> for MessageType {
type Error = &'static str;
fn try_from(value: u32) -> Result<Self, Self::Error> {
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<Box<dyn std::future::Future<Output = Vec<(MessageType, Vec<u8>)>> + Send>>;
pub struct Connection<'a> {
stream: BufStream<UnixStream>,
subscriptions: HashMap<MessageType, Box<&'a SubscriptionCallback>>,
}
impl<'a> Connection<'a> {
pub fn connect(path: &std::path::Path) -> Result<Self, anyhow::Error> {
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<u8>), 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<u8>)> {
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<u8>)> =
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()
}
}

119
src/lisp.rs Normal file
View file

@ -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<rust_lisp::model::Env>, args: Vec<rust_lisp::model::Value>| {
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<rust_lisp::model::Env>, args: Vec<rust_lisp::model::Value>| {
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
}

215
src/main.rs Normal file
View file

@ -0,0 +1,215 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::{Context, 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<PathBuf>,
}
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<tokio::sync::Mutex<Vec<ProgramEntry>>>,
tx: &tokio::sync::broadcast::Sender<()>,
) -> futures::future::BoxFuture<'static, Vec<(MessageType, Vec<u8>)>> {
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<rust_lisp::model::Value> = config_.init.clone().into();
let prog: Vec<rust_lisp::model::Value> = 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.with_context(
|| format!("Failed to read config file {}", args.config.as_ref().unwrap().to_string_lossy())
)?
.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(())
}