Compare commits

...

23 commits

Author SHA1 Message Date
56c497b5dd
Update cargo.lock. 2023-10-15 16:50:31 +02:00
599a4dfac0
Bump version to 0.3.0. 2023-10-15 15:45:39 +02:00
4c0bcac50b
More meaningful error message when config file does not exist. 2023-10-15 15:44:49 +02:00
690777aa16
Merge branch 'rust-rewrite' 2023-10-15 15:35:18 +02:00
114e7892d7
Add cargo.lock. 2023-10-15 15:16:19 +02:00
1cb9672b48
Reorder config fields. 2023-10-15 15:14:10 +02:00
d0ad88af6a
Add readme. 2023-10-15 15:13:52 +02:00
5189ba323b
Add signal processing, more logs. 2023-10-15 14:41:02 +02:00
1b6c9e46be
Add argument parsing and default config file. 2023-10-15 01:18:00 +02:00
b76d325d60
Add logging. 2023-10-15 00:33:55 +02:00
23dc47a008
Mvp finished, can run programs and move them to windows. 2023-10-14 23:54:06 +02:00
0f24273c93
Begin integrating lisp. 2023-10-14 02:08:41 +02:00
81790d4308
Initial rust rewrite commit, still wip. 2023-10-14 00:26:34 +02:00
d3d4530cfd
Decrease final_workspace_delay from 1s to 100ms. 2022-12-13 16:40:14 +01:00
4e1e241bf1
Switch to logging module.
Add hint that it might be necessary to increase the timeout
if programs are left after execution.

Turn off the new window callback to avoid asyncio exceptions.
2022-12-13 16:35:14 +01:00
7b8c6df478
Always re-reduce expression. 2022-11-27 03:49:14 +01:00
015849345d
Fix not able to execute init repeatedly. 2022-11-27 03:02:56 +01:00
8763d8a382
More fixes for defun and more elaborate example in readme. 2022-11-27 02:46:38 +01:00
74e98f7e3b
Small fixes for user-defined functions, update readme. 2022-11-27 01:51:38 +01:00
ddabe282a4
Add defun (custom functions).
Rename defvar to setq, add local environment and let.
2022-11-27 01:43:27 +01:00
8b7e6ae7ba
Improve lisp compiler and interpreter. 2022-11-27 00:41:52 +01:00
be97b30e86
Improve lisp compiler and interpreter. 2022-11-27 00:32:20 +01:00
4180e3b427
Add branching operator, explicitly load values from input dict. 2022-11-09 11:59:25 +01:00
11 changed files with 1654 additions and 592 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: 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

165
README.md
View file

@ -4,104 +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
timeout: 10000
init: |
(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:
- 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
- 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'
```
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.
## Configuration
## Installing
The configuration file is in YAML format.
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
### Configuration
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).
#### timeout: int
In order to move the correct window to the desired workspace a filter can be defined.
_Optional_ _Default_ `3000`
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.
Total program timeout in ms.
It is then possible to construct a filter for any program.
#### init: String
Available Operators:
_Optional_ _Default_ `""`
- and: `&`
- or: `|`
- eq: `=`
- neq: `!=`
- gt: `>`
- lt: `<`
Initialization program; Used to initialize the environment, useful
to define custom functions which should be available everywhere.
The filter usually operates on the dictionary, and thus the *first* argument to every normal filter
is the dictionary element, in `.` notation, as might be customary in `jq`.
#### cmd: String
For example: `(> ".container.geometry.width" 300)` would match the first window where the width is greater than 300.
_Optional_ _Default_ `""`
Multiple filters are combined via nesting: `(& (> ".container.geometry.width" 300) (= ".container.window_properties.class" "discord"))`.
A final i3 command to be executed before exiting.
## Starting tray programs in a specific order
#### programs: List[Union[[Program](#program), [Signal](#signal)]]
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.
_Optional_ _Default_ `[]`
This could be combined with waybar to enforce an ordering of tray applications:
A list of programs to execute.
`~/.config/waybar/config`
```json
"tray": {
"on-update": "pkill --full --signal SIGUSR1 i3toolwait",
"reverse-direction": true,
}
```
### Program
`config-file`
```yaml
signal: SIGUSR1
timeout: 2000
programs:
- cmd: 'nm-applet --indicator'
match: '(False)'
timeout: 1000
signal: true
- cmd: 'blueman-applet'
match: '(False)'
timeout: 1000
signal: true
- ...
```
Launch all programs using [`run`](#run-string) and execute
[`cmd`](#cmd-string-1) once [`match`](#match-string) matches
a window.
This setup would order the icons in waybar from left-to-right like in the config file.
#### match: String
## Troubleshooting
_Required_
### My windows do not get rearranged
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.
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.
#### 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,510 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import typing
import asyncio
import signal
import os
import time
import functools
import json
import yaml
import click
import pydantic
import i3ipc
import i3ipc.aio
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
class Expression:
def __init__(self):
pass
def reduce(self, ipc_data):
return functools.reduce(self.reduce_function(ipc_data), self.children)
@property
def children(self):
raise NotImplemented('TODO: implement in subclass')
def reduce_function(self, ipc_data):
raise NotImplemented('TODO: implement in subclass')
class LiteralExpression(Expression):
def __init__(self, value):
self._value = value
def __repr__(self) -> str:
return f'"{self._value}"'
@property
def children(self):
return [self._value]
def reduce_function(self, ipc_data):
def reduce(a, b):
raise NotImplemented('I should never be called')
class IntLiteralExpression(LiteralExpression):
def __repr__(self) -> str:
return str(self._value)
class BoolLiteralExpression(LiteralExpression):
def __repr__(self) -> str:
return str(self._value)
class AndExpression(Expression):
def __init__(self, children, *args, **kwargs):
self._children = children
super().__init__(*args, **kwargs)
def __repr__(self) -> str:
cs = ' '.join([repr(c) for c in self.children])
return f'(& {cs})'
@property
def children(self):
return self._children
def reduce_function(self, ipc_data):
return lambda a, b: a.reduce(ipc_data) and b.reduce(ipc_data)
class OrExpression(Expression):
def __init__(self, children, *args, **kwargs):
self._children = children
super().__init__(*args, **kwargs)
def __repr__(self) -> str:
cs = ' '.join([repr(c) for c in self.children])
return f'(| {cs})'
@property
def children(self):
return self._children
def reduce_function(self, ipc_data):
return lambda a, b: a.reduce(ipc_data) or b.reduce(ipc_data)
class EqExpression(Expression):
def __init__(self, children, *args, **kwargs):
self._children = children
super().__init__(*args, **kwargs)
def __repr__(self) -> str:
cs = ' '.join([repr(c) for c in self.children])
return f'(= {cs})'
@property
def children(self):
return self._children
def reduce_function(self, ipc_data):
def reduce(key, value):
ipc_value = ipc_data
for k in key.reduce(ipc_data).strip('.').split('.'):
ipc_value = ipc_value[k]
return ipc_value == value.reduce(ipc_data)
return reduce
class NeqExpression(Expression):
def __init__(self, children, *args, **kwargs):
self._children = children
super().__init__(*args, **kwargs)
def __repr__(self) -> str:
cs = ' '.join([repr(c) for c in self.children])
return f'(!= {cs})'
@property
def children(self):
return self._children
def reduce_function(self, ipc_data):
def reduce(key, value):
ipc_value = ipc_data
for k in key.reduce(ipc_data).strip('.').split('.'):
ipc_value = ipc_value[k]
return ipc_value != value.reduce(ipc_data)
return reduce
class GtExpression(Expression):
def __init__(self, children, *args, **kwargs):
self._children = children
super().__init__(*args, **kwargs)
def __repr__(self) -> str:
cs = ' '.join([repr(c) for c in self.children])
return f'(> {cs})'
@property
def children(self):
return self._children
def reduce_function(self, ipc_data):
def reduce(key, value):
ipc_value = ipc_data
for k in key.reduce(ipc_data).strip('.').split('.'):
ipc_value = ipc_value[k]
return ipc_value > value.reduce(ipc_data)
return reduce
class LtExpression(Expression):
def __init__(self, children, *args, **kwargs):
self._children = children
super().__init__(*args, **kwargs)
def __repr__(self) -> str:
cs = ' '.join([repr(c) for c in self.children])
return f'(< {cs})'
@property
def children(self):
return self._children
def reduce_function(self, ipc_data):
def reduce(key, value):
ipc_value = ipc_data
for k in key.reduce(ipc_data).strip('.').split('.'):
ipc_value = ipc_value[k]
return ipc_value < value.reduce(ipc_data)
return reduce
expression_mapping = {
'&': AndExpression,
'|': OrExpression,
'=': EqExpression,
'!=': NeqExpression,
'>': GtExpression,
'<': LtExpression,
}
def group_tokens(tokens: list[str]) -> list[list[str]]:
groups = []
current_group = []
brace_count = 0
for token in tokens:
if token == '(':
brace_count += 1
elif token == ')':
brace_count -= 1
if brace_count == 0:
groups += [current_group]
current_group = []
elif brace_count == 0:
groups += [[token]]
else:
current_group += [token]
return groups
def build_expression(tokens: list[str]) -> Expression:
if tokens[0] == '(' and tokens[-1] == ')':
tokens = tokens[1:-1]
token_groups = group_tokens(tokens)
expressions = [build_expression(ts) for ts in token_groups[1:]] if len(token_groups) > 1 else []
root_expression = None
token = token_groups[0][0]
if token in expression_mapping:
root_expression = expression_mapping[token](expressions)
elif token.startswith('"'):
root_expression = LiteralExpression(token[1:-1])
elif token.isnumeric():
root_expression = IntLiteralExpression(int(token))
elif token in ('True', 'False'):
root_expression = BoolLiteralExpression(token == 'True')
assert isinstance(root_expression, Expression)
return root_expression
def take_space(s: str) -> tuple:
if s[0] in ' \n':
return None, s[1:]
return None, s
def take_operator(s: str) -> tuple:
token = ''
for c in s:
if c in ''.join(set(expression_mapping.keys())):
token += c
else:
break
if token == '':
return None, s
else:
return token, s[len(token):]
def take_brace(s: str) -> tuple:
if s[0] in '()':
return s[0], s[1:]
else:
return None, s
def take_literal(s: str) -> tuple:
token = '"'
if s[0] != '"':
return None, s
for c in s[1:]:
token += c
if c == '"':
break
if not token.endswith('"'):
raise ValueError('Missing closing quotes (`"`)')
return token, s[len(token):]
def take_int_literal(s: str) -> tuple:
token = ''
for c in s:
if not c.isnumeric():
break
token += c
if token == '':
return None, s
return token, s[len(token):]
def take_bool_literal(s: str) -> tuple:
if s.startswith('True'):
return 'True', s[len('True'):]
if s.startswith('False'):
return 'False', s[len('False'):]
return None, s
def tokenize(s: str) -> list[str]:
operator_extractors = [
take_operator,
take_brace,
take_literal,
take_int_literal,
take_bool_literal,
take_space,
]
tokens = []
while s != '':
previous_len = len(s)
for operator_extractor in operator_extractors:
token, s = operator_extractor(s)
if token is not None:
tokens += [token]
break
if len(s) == previous_len:
raise ValueError(f'Could not tokenize string {s}')
return tokens
def parse(s: str) -> Expression:
tokens = tokenize(s)
return build_expression(tokens)
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
programs: typing.List[ProgramConfig]
final_workspace: typing.Optional[str] = None
class RuntimeData(pydantic.BaseModel):
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'
if debug:
print(json.dumps(e.ipc_data))
async with runtime_data.lock:
for i, cfg in enumerate(runtime_data.programs):
if cfg.match.reduce(e.ipc_data):
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(
programs=[p for p in config.programs if p.workspace is not None],
lock=Lock(),
event=Event(),
ipc=Connection(),
)
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()
runtime_data.ipc.on('window::new', window_new(runtime_data, debug=debug))
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:
return 1
finally:
if config.final_workspace is not None:
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(())
}