initial stage of hlctl rust rewrite
This commit is contained in:
parent
e0526addc6
commit
d0c19f7947
|
@ -1,2 +1,4 @@
|
||||||
# Openbsd core dumps
|
# Openbsd core dumps
|
||||||
*.core
|
*.core
|
||||||
|
|
||||||
|
target/
|
||||||
|
|
|
@ -0,0 +1,644 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[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.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "628a8f9bd1e24b4e0db2b4bc2d000b001e7dd032d54afa60a68836aeec5aa54a"
|
||||||
|
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.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
|
||||||
|
dependencies = [
|
||||||
|
"utf8parse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-query"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle-wincon"
|
||||||
|
version = "3.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.79"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap"
|
||||||
|
version = "4.4.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c"
|
||||||
|
dependencies = [
|
||||||
|
"clap_builder",
|
||||||
|
"clap_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_builder"
|
||||||
|
version = "4.4.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"clap_lex",
|
||||||
|
"strsim",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_derive"
|
||||||
|
version = "4.4.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clap_lex"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorchoice"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diff"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_logger"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
|
||||||
|
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.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
||||||
|
|
||||||
|
[[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.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hlctl"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"clap",
|
||||||
|
"log",
|
||||||
|
"paste",
|
||||||
|
"pretty_assertions",
|
||||||
|
"pretty_env_logger",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"strum",
|
||||||
|
"thiserror",
|
||||||
|
"toml",
|
||||||
|
"which",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "home"
|
||||||
|
version = "0.5.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "humantime"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-terminal"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi",
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.153"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pretty_assertions"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
|
||||||
|
dependencies = [
|
||||||
|
"diff",
|
||||||
|
"yansi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pretty_env_logger"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c"
|
||||||
|
dependencies = [
|
||||||
|
"env_logger",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.76"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
|
||||||
|
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 = "rustix"
|
||||||
|
version = "0.38.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.195"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.195"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.25.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustversion",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.48"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termcolor"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.57"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.57"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8parse"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "which"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"home",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
|
||||||
|
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.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.5.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yansi"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "hlctl"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.79"
|
||||||
|
clap = { version = "4.4.18", features = ["derive", "cargo"] }
|
||||||
|
paste = "1.0.14"
|
||||||
|
serde = { version = "1.0.195", features = ["derive"] }
|
||||||
|
serde_json = { version = "1.0.113" }
|
||||||
|
strum = { version = "0.25.0", features = ["derive"] }
|
||||||
|
thiserror = "1.0.57"
|
||||||
|
toml = "0.8.8"
|
||||||
|
which = "6.0.0"
|
||||||
|
log = "0.4"
|
||||||
|
pretty_env_logger = "0.5"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions = "1.4.0"
|
|
@ -1,47 +0,0 @@
|
||||||
package cmdlets
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Usage struct {
|
|
||||||
Name string
|
|
||||||
Short string
|
|
||||||
Long []string
|
|
||||||
Flags map[string]string
|
|
||||||
Examples []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u Usage) String() string {
|
|
||||||
str := strings.Builder{}
|
|
||||||
str.WriteString(
|
|
||||||
fmt.Sprintf("%s SUBCOMMAND\n\n", strings.ToUpper(u.Name)),
|
|
||||||
)
|
|
||||||
str.WriteString(fmt.Sprintf("\t%s\t%s\n\n", u.Name, u.Short))
|
|
||||||
for _, line := range u.Long {
|
|
||||||
str.WriteString(fmt.Sprintf("\t%s\n", line))
|
|
||||||
}
|
|
||||||
str.WriteRune('\n')
|
|
||||||
if len(u.Flags) != 0 {
|
|
||||||
for flag, desc := range u.Flags {
|
|
||||||
str.WriteString(fmt.Sprintf("\t%s\n\t\t%s\n", flag, desc))
|
|
||||||
}
|
|
||||||
str.WriteRune('\n')
|
|
||||||
}
|
|
||||||
if len(u.Examples) != 0 {
|
|
||||||
str.WriteString("EXAMPLES\n\n")
|
|
||||||
for _, example := range u.Examples {
|
|
||||||
str.WriteString(fmt.Sprintf("\t%s\n", example))
|
|
||||||
}
|
|
||||||
str.WriteRune('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
return str.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
type Commandlet interface {
|
|
||||||
Name() string
|
|
||||||
Usage() Usage
|
|
||||||
Invoke(args ...string) error
|
|
||||||
}
|
|
|
@ -1,175 +0,0 @@
|
||||||
package group
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"sectorinf.com/emilis/hlctl/cmdlets"
|
|
||||||
"sectorinf.com/emilis/hlctl/config"
|
|
||||||
"sectorinf.com/emilis/hlctl/ctllog"
|
|
||||||
"sectorinf.com/emilis/hlctl/groupctl"
|
|
||||||
"sectorinf.com/emilis/hlctl/hlcl"
|
|
||||||
"sectorinf.com/emilis/hlctl/notifyctl"
|
|
||||||
)
|
|
||||||
|
|
||||||
var log = ctllog.Logger{}.New("group", ctllog.ColorRGB{}.FromHex("27ee7e"))
|
|
||||||
|
|
||||||
type command struct{}
|
|
||||||
|
|
||||||
var Command command
|
|
||||||
|
|
||||||
func (command) Name() string {
|
|
||||||
return "group"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (command) Usage() cmdlets.Usage {
|
|
||||||
return cmdlets.Usage{
|
|
||||||
Name: Command.Name(),
|
|
||||||
Short: "Controls tag grouping and behavior",
|
|
||||||
Long: []string{
|
|
||||||
"`group` is responsible for controlling how groups behave",
|
|
||||||
"in hlwm, and switching between them",
|
|
||||||
},
|
|
||||||
Flags: map[string]string{
|
|
||||||
"+": "Jump to next group",
|
|
||||||
"-": "Jump to previous group",
|
|
||||||
"[number]": "Jump to [number] group",
|
|
||||||
"move [number]": "Move the currently focused window to group [number]",
|
|
||||||
},
|
|
||||||
Examples: []string{
|
|
||||||
"hcctl group +",
|
|
||||||
"hcctl group -",
|
|
||||||
"hcctl group 2",
|
|
||||||
"hcctl group move 2",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (command) Invoke(args ...string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return errors.New("group requires arguments")
|
|
||||||
}
|
|
||||||
arg := args[0]
|
|
||||||
if arg == "move" && len(args) < 2 {
|
|
||||||
return errors.New("group move requires an argument")
|
|
||||||
}
|
|
||||||
var active uint8
|
|
||||||
currentActiveCfg, err := config.Get(config.ActiveGroup)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf(
|
|
||||||
"error getting active group, defaulting to 1: %s",
|
|
||||||
err.Error(),
|
|
||||||
)
|
|
||||||
active = 1
|
|
||||||
} else {
|
|
||||||
val, err := currentActiveCfg.Uint()
|
|
||||||
mustUint(config.ActiveGroup, err)
|
|
||||||
active = uint8(val)
|
|
||||||
}
|
|
||||||
log.Print("Loading num of tags per group")
|
|
||||||
numCfg, err := config.Get(config.TagsPerGroup)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", config.TagsPerGroup, err)
|
|
||||||
}
|
|
||||||
num, err := numCfg.Uint()
|
|
||||||
mustUint(config.TagsPerGroup, err)
|
|
||||||
log.Print(num, "tags per group")
|
|
||||||
focus, err := config.GetFocusedTag()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get focused tag: %w", err)
|
|
||||||
}
|
|
||||||
log.Print("Currently in", focus, "tag")
|
|
||||||
|
|
||||||
maxCfg, err := config.Get(config.GroupCount)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", config.GroupCount, err)
|
|
||||||
}
|
|
||||||
max, err := maxCfg.Uint()
|
|
||||||
mustUint(config.GroupCount, err)
|
|
||||||
|
|
||||||
var (
|
|
||||||
newActive uint8
|
|
||||||
)
|
|
||||||
|
|
||||||
switch arg {
|
|
||||||
case "+":
|
|
||||||
newActive = shiftGroup(false, active, uint8(max))
|
|
||||||
case "-":
|
|
||||||
newActive = shiftGroup(true, active, uint8(max))
|
|
||||||
case "move":
|
|
||||||
a, err := strconv.ParseUint(arg, 10, 8)
|
|
||||||
target := uint8(a)
|
|
||||||
if err != nil || target < 1 || target > max {
|
|
||||||
return fmt.Errorf("out of range, use [1:%d]", max)
|
|
||||||
}
|
|
||||||
return groupctl.MoveTo(uint8(a))
|
|
||||||
default:
|
|
||||||
a, err := strconv.ParseUint(arg, 10, 8)
|
|
||||||
target := uint8(a)
|
|
||||||
if err != nil || target < 1 || target > max {
|
|
||||||
return fmt.Errorf("out of range, use [1:%d]", max)
|
|
||||||
}
|
|
||||||
newActive = uint8(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
if active != newActive {
|
|
||||||
log.Printf("Group %d -> %d", active, newActive)
|
|
||||||
config.Set(config.ActiveGroup, strconv.Itoa(int(newActive)))
|
|
||||||
// shift the tag index
|
|
||||||
if err := groupctl.Update(); err != nil {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"updating groups [%d -> %d]: %w",
|
|
||||||
active,
|
|
||||||
newActive,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := groupctl.SetKeybinds(newActive, uint8(num)); err != nil {
|
|
||||||
return fmt.Errorf("SetKeybinds: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fg, _ := config.Get(config.ColorText)
|
|
||||||
bg, _ := config.Get(config.ColorNormal)
|
|
||||||
font, _ := config.Get(config.Font)
|
|
||||||
return notifyctl.Display(
|
|
||||||
fmt.Sprintf("Group %d", newActive),
|
|
||||||
2,
|
|
||||||
fg.String(),
|
|
||||||
bg.String(),
|
|
||||||
font.String(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func moveTo(currentTag, currentGroup, newGroup, num uint8) error {
|
|
||||||
relTag := (currentTag - ((currentGroup - 1) * num))
|
|
||||||
newTag := ((newGroup - 1) * num) + relTag
|
|
||||||
return hlcl.MoveIndex(uint8(newTag))
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustUint(cfg string, err error) {
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf(
|
|
||||||
"%s must be set as uint: %s",
|
|
||||||
cfg,
|
|
||||||
err.Error(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func shiftGroup(backwards bool, active, num uint8) uint8 {
|
|
||||||
// Maintain the logic of the fish scripts,
|
|
||||||
// 0 == 1, act as if groups are 1 indexed
|
|
||||||
if backwards && active <= 1 {
|
|
||||||
active = num
|
|
||||||
} else if backwards {
|
|
||||||
active--
|
|
||||||
} else if active >= num {
|
|
||||||
active = 1
|
|
||||||
} else {
|
|
||||||
active++
|
|
||||||
}
|
|
||||||
return uint8(active)
|
|
||||||
}
|
|
|
@ -1,319 +0,0 @@
|
||||||
package init
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"sectorinf.com/emilis/hlctl/cmdlets"
|
|
||||||
"sectorinf.com/emilis/hlctl/config"
|
|
||||||
"sectorinf.com/emilis/hlctl/ctllog"
|
|
||||||
"sectorinf.com/emilis/hlctl/groupctl"
|
|
||||||
"sectorinf.com/emilis/hlctl/hlcl"
|
|
||||||
"sectorinf.com/emilis/hlctl/hlcl/cmd"
|
|
||||||
"sectorinf.com/emilis/hlctl/panelctl"
|
|
||||||
"sectorinf.com/emilis/hlctl/svcctl"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Return = "Return"
|
|
||||||
Shift = "Shift"
|
|
||||||
Tab = "Tab"
|
|
||||||
Left = "Left"
|
|
||||||
Right = "Right"
|
|
||||||
Up = "Up"
|
|
||||||
Down = "Down"
|
|
||||||
Space = "space"
|
|
||||||
Control = "Control"
|
|
||||||
Backtick = "grave"
|
|
||||||
)
|
|
||||||
|
|
||||||
var log = ctllog.Logger{}.New("init", ctllog.Green)
|
|
||||||
|
|
||||||
type command struct{}
|
|
||||||
|
|
||||||
var Command command
|
|
||||||
|
|
||||||
func (command) Name() string {
|
|
||||||
return "init"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (command) Usage() cmdlets.Usage {
|
|
||||||
return cmdlets.Usage{
|
|
||||||
Name: Command.Name(),
|
|
||||||
Short: "Initialize herbstluftwm, should be run from " +
|
|
||||||
"the autostart script",
|
|
||||||
Long: []string{
|
|
||||||
"`init` starts the configuration of herbstluftwm with the",
|
|
||||||
"default configuration, setting up keybinds, hlctl groups,",
|
|
||||||
"and other commands.",
|
|
||||||
},
|
|
||||||
Flags: map[string]string{},
|
|
||||||
Examples: []string{
|
|
||||||
"hcctl init",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func logError(err error) {
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Keybind struct {
|
|
||||||
Keys []string
|
|
||||||
Command cmd.Command
|
|
||||||
Args []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k Keybind) Set() {
|
|
||||||
err := hlcl.Keybind(k.Keys, k.Command, k.Args...)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf(
|
|
||||||
"Keybind [%s]: %s", strings.Join(k.Args, "-"), err,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
binds := strings.Join(k.Keys, "-")
|
|
||||||
command := strings.Join(
|
|
||||||
append([]string{k.Command.String()}, k.Args...),
|
|
||||||
" ",
|
|
||||||
)
|
|
||||||
log.Printf("%s -> [%s]", binds, command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Keybinds(cfg config.HlwmConfig) []Keybind {
|
|
||||||
// convenience for setting []string
|
|
||||||
c := func(v ...string) []string {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
key := func(keys []string, cm cmd.Command, args ...string) Keybind {
|
|
||||||
return Keybind{
|
|
||||||
Keys: keys,
|
|
||||||
Command: cm,
|
|
||||||
Args: args,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Mod := cfg.Mod
|
|
||||||
step := "+0.05"
|
|
||||||
base := []Keybind{
|
|
||||||
key(c(Mod, Shift, "r"), cmd.Reload),
|
|
||||||
key(c(Mod, Shift, "c"), cmd.Close),
|
|
||||||
key(c(Mod, Return), cmd.Spawn, "dmenu_run"),
|
|
||||||
key(c(Mod, "s"), cmd.Spawn, "flameshot", "gui"),
|
|
||||||
key(c(Mod, cfg.TermSpawnKey), cmd.Spawn, cfg.Term),
|
|
||||||
|
|
||||||
// Focus
|
|
||||||
key(c(Mod, Left), cmd.Focus, "left"),
|
|
||||||
key(c(Mod, Right), cmd.Focus, "right"),
|
|
||||||
key(c(Mod, Up), cmd.Focus, "up"),
|
|
||||||
key(c(Mod, Down), cmd.Focus, "down"),
|
|
||||||
key(c(Mod, Tab), cmd.Cycle),
|
|
||||||
key(c(Mod, "i"), cmd.JumpTo, "urgent"),
|
|
||||||
|
|
||||||
// Move frames
|
|
||||||
key(c(Mod, Shift, Left), cmd.Shift, "left"),
|
|
||||||
key(c(Mod, Shift, Right), cmd.Shift, "right"),
|
|
||||||
key(c(Mod, Shift, Up), cmd.Shift, "up"),
|
|
||||||
key(c(Mod, Shift, Down), cmd.Shift, "down"),
|
|
||||||
|
|
||||||
// Frames
|
|
||||||
key(c(Mod, "u"), cmd.Split, "bottom", "0.7"),
|
|
||||||
key(c(Mod, "o"), cmd.Split, "right", "0.5"),
|
|
||||||
key(c(Mod, "r"), cmd.Remove),
|
|
||||||
key(c(Mod, "f"), cmd.Fullscreen, "toggle"),
|
|
||||||
key(c(Mod, Space), cmd.CycleLayout),
|
|
||||||
|
|
||||||
// Resizing
|
|
||||||
key(c(Mod, Control, Left), cmd.Resize, "left", step),
|
|
||||||
key(c(Mod, Control, Right), cmd.Resize, "right", step),
|
|
||||||
key(c(Mod, Control, Up), cmd.Resize, "up", step),
|
|
||||||
key(c(Mod, Control, Down), cmd.Resize, "down", step),
|
|
||||||
|
|
||||||
// Commands
|
|
||||||
key(c(Mod, Backtick), cmd.Spawn, "hlctl", "group", "+"),
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < int(cfg.Groups.Groups); i++ {
|
|
||||||
base = append(
|
|
||||||
base,
|
|
||||||
key(c(Mod, fmt.Sprintf("F%d", i+1)), cmd.Spawn,
|
|
||||||
"hlctl", "group", strconv.Itoa(i+1)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return base
|
|
||||||
}
|
|
||||||
|
|
||||||
type set struct {
|
|
||||||
name string
|
|
||||||
value any
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s set) String() string {
|
|
||||||
return fmt.Sprint(s.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Theme(cfg config.HlwmConfig) {
|
|
||||||
s := func(name string, value any) set {
|
|
||||||
return set{
|
|
||||||
name: name,
|
|
||||||
value: value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
col := cfg.Theme.Colors
|
|
||||||
// Theme sets
|
|
||||||
for _, row := range [...]set{
|
|
||||||
s("frame_border_active_color", col.Active),
|
|
||||||
s("frame_border_normal_color", "black"),
|
|
||||||
s("frame_bg_active_color", col.Active),
|
|
||||||
s("frame_border_width", 2),
|
|
||||||
s("show_frame_decorations", "focused_if_multiple"),
|
|
||||||
s("tree_style", "╾│ ├└╼─┐"),
|
|
||||||
s("frame_bg_transparent", "on"),
|
|
||||||
s("smart_window_surroundings", "off"), // TODO
|
|
||||||
s("smart_frame_surroundings", "on"),
|
|
||||||
s("frame_transparent_width", 5),
|
|
||||||
s("frame_gap", 3),
|
|
||||||
s("window_gap", 0),
|
|
||||||
s("frame_padding", 0),
|
|
||||||
s("mouse_recenter_gap", 0),
|
|
||||||
} {
|
|
||||||
value := row.String()
|
|
||||||
if err := hlcl.Set(row.name, value); err != nil {
|
|
||||||
log.Errorf("Set [%s] -> [%s]: %s", row.name, value, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Theme attrs
|
|
||||||
for _, row := range [...]set{
|
|
||||||
s("reset", 1),
|
|
||||||
s("tiling.reset", 1),
|
|
||||||
s("floating.reset", 1),
|
|
||||||
s("title_height", 15),
|
|
||||||
s("title_when", "multiple_tabs"),
|
|
||||||
s("title_font", cfg.FontBold),
|
|
||||||
s("title_depth", 3),
|
|
||||||
s("inner_width", 0),
|
|
||||||
s("inner_color", "black"),
|
|
||||||
s("border_width", 1),
|
|
||||||
s("floating.border_width", 4),
|
|
||||||
s("floating.outer_width", 1),
|
|
||||||
s("tiling.outer_width", 1),
|
|
||||||
s("background_color", "black"),
|
|
||||||
|
|
||||||
s("active.color", col.Active),
|
|
||||||
s("active.inner_color", col.Active),
|
|
||||||
s("active.outer_color", col.Active),
|
|
||||||
s("normal.color", col.Normal),
|
|
||||||
s("normal.inner_color", col.Normal),
|
|
||||||
s("normal.outer_color", col.Normal),
|
|
||||||
s("urgent.color", col.Urgent),
|
|
||||||
s("urgent.inner_color", col.Urgent),
|
|
||||||
s("urgent.outer_color", col.Urgent),
|
|
||||||
s("title_color", "white"),
|
|
||||||
s("normal.title_color", col.Text),
|
|
||||||
} {
|
|
||||||
attr := fmt.Sprintf("theme.%s", row.name)
|
|
||||||
v := row.String()
|
|
||||||
log.Printf("Setting [%s] -> [%s]", attr, v)
|
|
||||||
if err := hlcl.SetAttr(attr, v); err != nil {
|
|
||||||
log.Errorf("Attr [%s] -> [%s]: %s", attr, v, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetRules() {
|
|
||||||
sFmt := "windowtype~_NET_WM_WINDOW_TYPE_%s"
|
|
||||||
logError(hlcl.Rule("focus=on"))
|
|
||||||
logError(hlcl.Rule("floatplacement=smart"))
|
|
||||||
logError(hlcl.Rule("fixedsize", "floating=on"))
|
|
||||||
logError(hlcl.Rule(
|
|
||||||
fmt.Sprintf(sFmt, "(DIALOG|UTILITY|SPLASH)"),
|
|
||||||
"floating=on",
|
|
||||||
))
|
|
||||||
|
|
||||||
logError(hlcl.Rule(
|
|
||||||
fmt.Sprintf(sFmt, "(NOTIFICATION|DOCK|DESKTOP)"),
|
|
||||||
"manage=off",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddTags(groupCount, tagsPerGroup uint8) error {
|
|
||||||
for group := 0; group < int(groupCount); group++ {
|
|
||||||
for tag := 0; tag < int(tagsPerGroup); tag++ {
|
|
||||||
err := hlcl.AddTag(fmt.Sprintf("%d|%d", group+1, tag+1))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"Adding tag [%d] for group [%d]: %s",
|
|
||||||
tag+1, group+1, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defaultAttr, err := hlcl.GetAttr("tags.by-name.default.index")
|
|
||||||
if err != nil {
|
|
||||||
// no default
|
|
||||||
log.Print("No 'default' tag, continuing...")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
focus, err := config.GetFocusedTag()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("(removing default) getting current tag: %s", err)
|
|
||||||
}
|
|
||||||
def, err := strconv.ParseUint(defaultAttr, 10, 8)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Bug? default index not uint: %s", err)
|
|
||||||
}
|
|
||||||
if uint8(def) == focus {
|
|
||||||
if err := hlcl.UseIndex(focus + 1); err != nil {
|
|
||||||
log.Warnf("Tried switching away from default: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := hlcl.MergeTag("default"); err != nil {
|
|
||||||
return fmt.Errorf("merging default: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (command) Invoke(args ...string) error {
|
|
||||||
log.Print("begining herbstluftwm setup via hlctl")
|
|
||||||
log.Print("loading config")
|
|
||||||
cfg := config.Load()
|
|
||||||
if err := cfg.Validate(); err != nil {
|
|
||||||
return fmt.Errorf("validating config: %s", err)
|
|
||||||
}
|
|
||||||
if err := cfg.SetAll(); err != nil {
|
|
||||||
return fmt.Errorf("setting cfg values to hlwm: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
exec.Command("xsetroot", "-solid", "black").Run()
|
|
||||||
logError(hlcl.KeyUnbind())
|
|
||||||
|
|
||||||
log.Print("Adding tags")
|
|
||||||
logError(AddTags(cfg.Groups.Groups, cfg.Groups.Tags))
|
|
||||||
logError(groupctl.ResetActive())
|
|
||||||
|
|
||||||
for _, key := range Keybinds(cfg) {
|
|
||||||
key.Set()
|
|
||||||
}
|
|
||||||
log.Print("Resetting groups")
|
|
||||||
logError(groupctl.ResetActive())
|
|
||||||
|
|
||||||
log.Print("Theming...")
|
|
||||||
Theme(cfg)
|
|
||||||
|
|
||||||
log.Print("Rules")
|
|
||||||
SetRules()
|
|
||||||
|
|
||||||
log.Print("Unlocking")
|
|
||||||
if err := hlcl.Unlock(); err != nil {
|
|
||||||
return fmt.Errorf("failed unlock: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
svcctl.RestartServices(cfg.Services)
|
|
||||||
|
|
||||||
log.Print("Running panels")
|
|
||||||
return panelctl.Run()
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
package notify
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"sectorinf.com/emilis/hlctl/cmdlets"
|
|
||||||
"sectorinf.com/emilis/hlctl/config"
|
|
||||||
"sectorinf.com/emilis/hlctl/ctllog"
|
|
||||||
"sectorinf.com/emilis/hlctl/notifyctl"
|
|
||||||
)
|
|
||||||
|
|
||||||
var log = ctllog.Logger{}.New("notify", ctllog.ColorRGB{}.FromHex("ff003e"))
|
|
||||||
|
|
||||||
type command struct{}
|
|
||||||
|
|
||||||
var Command command
|
|
||||||
|
|
||||||
func (command) Name() string {
|
|
||||||
return "notify"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (command) Usage() cmdlets.Usage {
|
|
||||||
return cmdlets.Usage{
|
|
||||||
Name: Command.Name(),
|
|
||||||
Short: "Draws notifications using dzen2",
|
|
||||||
Long: []string{
|
|
||||||
"`notify` Will create notifications using the theme colors",
|
|
||||||
"that hlwm was set over",
|
|
||||||
},
|
|
||||||
Flags: map[string]string{
|
|
||||||
"-s": "Amount of seconds to persist for, default 1",
|
|
||||||
},
|
|
||||||
Examples: []string{
|
|
||||||
"notify -s 3 \"Hello world\"",
|
|
||||||
"notify Hey what's up chicken head",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (command) Invoke(args ...string) error {
|
|
||||||
var (
|
|
||||||
secs string = "1"
|
|
||||||
)
|
|
||||||
cleanArgs := make([]string, 0, len(args))
|
|
||||||
for i := 0; i < len(args); i++ {
|
|
||||||
arg := args[i]
|
|
||||||
if len(arg) > 1 && arg[0] == '-' {
|
|
||||||
var next string
|
|
||||||
if i+1 < len(args) {
|
|
||||||
next = args[i+1]
|
|
||||||
}
|
|
||||||
switch arg[1] {
|
|
||||||
case 's':
|
|
||||||
secs = next
|
|
||||||
i++
|
|
||||||
default:
|
|
||||||
cleanArgs = append(cleanArgs, arg)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cleanArgs = append(cleanArgs, arg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(cleanArgs) == 0 {
|
|
||||||
return errors.New("no message")
|
|
||||||
}
|
|
||||||
message := strings.Join(cleanArgs, " ")
|
|
||||||
cfg := config.Load()
|
|
||||||
secsNum, err := strconv.ParseUint(secs, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("seconds[%s]: %w", secs, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := cfg.Theme.Colors
|
|
||||||
return notifyctl.Display(
|
|
||||||
message,
|
|
||||||
uint(secsNum),
|
|
||||||
c.Text,
|
|
||||||
c.Normal,
|
|
||||||
cfg.Font,
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
package save
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sectorinf.com/emilis/hlctl/cmdlets"
|
|
||||||
"sectorinf.com/emilis/hlctl/config"
|
|
||||||
"sectorinf.com/emilis/hlctl/ctllog"
|
|
||||||
)
|
|
||||||
|
|
||||||
var log = ctllog.Logger{}.New("save", ctllog.Green)
|
|
||||||
|
|
||||||
type command struct{}
|
|
||||||
|
|
||||||
var Command command
|
|
||||||
|
|
||||||
func (command) Name() string {
|
|
||||||
return "save"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (command) Usage() cmdlets.Usage {
|
|
||||||
return cmdlets.Usage{
|
|
||||||
Name: Command.Name(),
|
|
||||||
Short: "Save the currently loaded configuration to file",
|
|
||||||
Long: []string{
|
|
||||||
"`save` simply saves the configurable state of hlctl",
|
|
||||||
"to a configuration file located at",
|
|
||||||
"$HOME/.config/herbstluftwm/hlctl.toml",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (command) Invoke(args ...string) error {
|
|
||||||
return config.Collect().Save()
|
|
||||||
}
|
|
101
config/config.go
101
config/config.go
|
@ -1,101 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"sectorinf.com/emilis/hlctl/hlcl"
|
|
||||||
"sectorinf.com/emilis/hlctl/hlcl/cmd"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
Term = "Term"
|
|
||||||
Font = "Font"
|
|
||||||
FontBold = "FontBold"
|
|
||||||
ModKey = "ModKey"
|
|
||||||
TermSpawnKey = "TermSpawnKey"
|
|
||||||
GroupCount = "GroupCount"
|
|
||||||
TagsPerGroup = "TagsPerGroup"
|
|
||||||
ActiveGroup = "ActiveGroup"
|
|
||||||
Services = "Services"
|
|
||||||
|
|
||||||
// Colors
|
|
||||||
ColorActive = "ColorActive"
|
|
||||||
ColorNormal = "ColorNormal"
|
|
||||||
ColorUrgent = "ColorUrgent"
|
|
||||||
ColorText = "ColorText"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
settingFmt = "settings.my_%s"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInvalidType = errors.New("invalid type")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Setting struct {
|
|
||||||
raw string
|
|
||||||
valueType string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Setting) String() string {
|
|
||||||
return s.raw
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Setting) Uint() (uint8, error) {
|
|
||||||
if s.valueType != "uint" {
|
|
||||||
return 0, fmt.Errorf("%s: %w", s.valueType, ErrInvalidType)
|
|
||||||
}
|
|
||||||
val, err := strconv.ParseUint(s.raw, 10, 8)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf(
|
|
||||||
"hc uint setting[%s] failed to parse: %s",
|
|
||||||
s.raw,
|
|
||||||
err.Error(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
return uint8(val), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetFocusedTag() (uint8, error) {
|
|
||||||
attr, err := hlcl.GetAttr("tags.focus.index")
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
val, err := strconv.ParseUint(attr, 10, 8)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("parsing [%s] to uint8: %w", attr, err)
|
|
||||||
}
|
|
||||||
return uint8(val), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Get(name string) (Setting, error) {
|
|
||||||
attrPath := fmt.Sprintf(settingFmt, name)
|
|
||||||
attr, err := hlcl.GetAttr(attrPath)
|
|
||||||
if err != nil {
|
|
||||||
return Setting{}, err
|
|
||||||
}
|
|
||||||
attrType, err := hlcl.AttrType(attrPath)
|
|
||||||
if err != nil {
|
|
||||||
// Default to string, silently
|
|
||||||
attrType = "string"
|
|
||||||
}
|
|
||||||
return Setting{
|
|
||||||
raw: attr,
|
|
||||||
valueType: attrType,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Set(name string, value string) error {
|
|
||||||
return hlcl.SetAttr(fmt.Sprintf(settingFmt, name), value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(name string, value string, attrType cmd.AttributeType) error {
|
|
||||||
if _, err := Get(name); err == nil {
|
|
||||||
// Already exists, just set value
|
|
||||||
return Set(name, value)
|
|
||||||
}
|
|
||||||
return hlcl.NewAttr(fmt.Sprintf(settingFmt, name), value, attrType)
|
|
||||||
}
|
|
198
config/type.go
198
config/type.go
|
@ -1,198 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
|
||||||
"sectorinf.com/emilis/hlctl/ctllog"
|
|
||||||
"sectorinf.com/emilis/hlctl/hlcl/cmd"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
cfgPath = filepath.Join(
|
|
||||||
os.Getenv("HOME"),
|
|
||||||
".config",
|
|
||||||
"herbstluftwm",
|
|
||||||
"hlctl.toml",
|
|
||||||
)
|
|
||||||
|
|
||||||
log = ctllog.Logger{}.New("config", ctllog.ColorRGB{}.FromHex("ff5aff"))
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
HlwmConfig struct {
|
|
||||||
Font string
|
|
||||||
FontBold string
|
|
||||||
Mod string
|
|
||||||
TermSpawnKey string
|
|
||||||
Term string
|
|
||||||
Groups GroupConfig
|
|
||||||
Theme Theme
|
|
||||||
Services ServicesConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
ServicesConfig struct {
|
|
||||||
Services []string
|
|
||||||
}
|
|
||||||
|
|
||||||
GroupConfig struct {
|
|
||||||
Groups uint8
|
|
||||||
Tags uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
Theme struct {
|
|
||||||
Colors Colors
|
|
||||||
}
|
|
||||||
|
|
||||||
Colors struct {
|
|
||||||
Active string
|
|
||||||
Normal string
|
|
||||||
Urgent string
|
|
||||||
Text string
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (h HlwmConfig) Save() error {
|
|
||||||
log.Printf("Saving to [%s]", cfgPath)
|
|
||||||
cfg, err := os.Create(cfgPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("opening cfg path: [%s]: %w", cfgPath, err)
|
|
||||||
}
|
|
||||||
defer cfg.Close()
|
|
||||||
err = toml.NewEncoder(cfg).Encode(h)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("encoding config to toml: %w", err)
|
|
||||||
}
|
|
||||||
return cfg.Sync()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h HlwmConfig) Validate() error {
|
|
||||||
if h.Font == "" ||
|
|
||||||
h.FontBold == "" ||
|
|
||||||
h.Mod == "" ||
|
|
||||||
h.TermSpawnKey == "" ||
|
|
||||||
h.Term == "" ||
|
|
||||||
h.Groups.Groups == 0 ||
|
|
||||||
h.Groups.Tags == 0 ||
|
|
||||||
len(h.Theme.Colors.Active) < 6 ||
|
|
||||||
len(h.Theme.Colors.Normal) < 6 ||
|
|
||||||
len(h.Theme.Colors.Urgent) < 6 ||
|
|
||||||
len(h.Theme.Colors.Text) < 6 {
|
|
||||||
return errors.New("some configuration options are missing")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect the config from loaded attributes. Or get default
|
|
||||||
// if that fails.
|
|
||||||
func Collect() HlwmConfig {
|
|
||||||
getOrNothing := func(settingName string, value *string) {
|
|
||||||
v, err := Get(settingName)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
*value = v.String()
|
|
||||||
}
|
|
||||||
getUints := func(settingName string, value *uint8) {
|
|
||||||
v, err := Get(settingName)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val, err := v.Uint()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
*value = uint8(val)
|
|
||||||
}
|
|
||||||
cfg := Default
|
|
||||||
getOrNothing(Font, &cfg.Font)
|
|
||||||
getOrNothing(FontBold, &cfg.FontBold)
|
|
||||||
getOrNothing(ModKey, &cfg.Mod)
|
|
||||||
getOrNothing(TermSpawnKey, &cfg.TermSpawnKey)
|
|
||||||
getOrNothing(Term, &cfg.Term)
|
|
||||||
getUints(GroupCount, &cfg.Groups.Groups)
|
|
||||||
getUints(TagsPerGroup, &cfg.Groups.Tags)
|
|
||||||
getOrNothing(ColorActive, &cfg.Theme.Colors.Active)
|
|
||||||
getOrNothing(ColorNormal, &cfg.Theme.Colors.Normal)
|
|
||||||
getOrNothing(ColorUrgent, &cfg.Theme.Colors.Urgent)
|
|
||||||
getOrNothing(ColorText, &cfg.Theme.Colors.Text)
|
|
||||||
svcsString, err := Get(Services)
|
|
||||||
if err == nil {
|
|
||||||
cfg.Services.Services = strings.Split(svcsString.String(), ";")
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h HlwmConfig) SetAll() error {
|
|
||||||
err := New(GroupCount, strconv.Itoa(int(h.Groups.Groups)), cmd.Uint)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", GroupCount, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = New(TagsPerGroup, strconv.Itoa(int(h.Groups.Tags)), cmd.Uint)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", TagsPerGroup, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sets := map[string]string{
|
|
||||||
Font: h.Font,
|
|
||||||
FontBold: h.FontBold,
|
|
||||||
ModKey: h.Mod,
|
|
||||||
TermSpawnKey: h.TermSpawnKey,
|
|
||||||
Term: h.Term,
|
|
||||||
ColorActive: h.Theme.Colors.Active,
|
|
||||||
ColorNormal: h.Theme.Colors.Normal,
|
|
||||||
ColorUrgent: h.Theme.Colors.Urgent,
|
|
||||||
ColorText: h.Theme.Colors.Text,
|
|
||||||
Services: strings.Join(h.Services.Services, ";"),
|
|
||||||
}
|
|
||||||
for cfg, val := range sets {
|
|
||||||
if err := New(cfg, val, cmd.String); err != nil {
|
|
||||||
return fmt.Errorf("%s[%s]: %w", cfg, val, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Load() HlwmConfig {
|
|
||||||
cfg := HlwmConfig{}
|
|
||||||
if _, err := toml.DecodeFile(cfgPath, &cfg); err != nil {
|
|
||||||
log.Warnf(
|
|
||||||
"could not read file [%s] for config, using default",
|
|
||||||
cfgPath,
|
|
||||||
)
|
|
||||||
return Default
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
Default = HlwmConfig{
|
|
||||||
Font: "-*-fixed-medium-*-*-*-12-*-*-*-*-*-*-*",
|
|
||||||
FontBold: "-*-fixed-bold-*-*-*-12-*-*-*-*-*-*-*",
|
|
||||||
Mod: "Mod4",
|
|
||||||
TermSpawnKey: "Home",
|
|
||||||
Term: "alacritty",
|
|
||||||
Groups: GroupConfig{
|
|
||||||
Groups: 3,
|
|
||||||
Tags: 5,
|
|
||||||
},
|
|
||||||
Theme: Theme{
|
|
||||||
Colors: Colors{
|
|
||||||
Active: "#800080",
|
|
||||||
Normal: "#330033",
|
|
||||||
Urgent: "#7811A1",
|
|
||||||
Text: "#898989",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Services: ServicesConfig{
|
|
||||||
Services: []string{"fcitx5"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
102
ctllog/colors.go
102
ctllog/colors.go
|
@ -1,102 +0,0 @@
|
||||||
package ctllog
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// \x1b[38;2;255;100;0mTRUECOLOR\x1b[0m
|
|
||||||
// ESC[ 38;2;⟨r⟩;⟨g⟩;⟨b⟩ m Select RGB foreground color
|
|
||||||
|
|
||||||
const (
|
|
||||||
escape byte = 0x1B
|
|
||||||
csi byte = '['
|
|
||||||
separator byte = ';'
|
|
||||||
csiEnd byte = 'm'
|
|
||||||
fgColor byte = '3'
|
|
||||||
bgColor byte = '4'
|
|
||||||
trueColor byte = '8'
|
|
||||||
rgb byte = '2'
|
|
||||||
reset byte = '0'
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
trueColorFMT = string([]byte{
|
|
||||||
escape,
|
|
||||||
csi,
|
|
||||||
'%', 'c',
|
|
||||||
trueColor,
|
|
||||||
separator,
|
|
||||||
rgb,
|
|
||||||
separator,
|
|
||||||
'%', 'd',
|
|
||||||
separator,
|
|
||||||
'%', 'd',
|
|
||||||
separator,
|
|
||||||
'%', 'd',
|
|
||||||
csiEnd,
|
|
||||||
'%', 's',
|
|
||||||
escape,
|
|
||||||
csi,
|
|
||||||
reset,
|
|
||||||
csiEnd,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
type Variant byte
|
|
||||||
|
|
||||||
const (
|
|
||||||
Foreground = Variant(fgColor)
|
|
||||||
Background = Variant(bgColor)
|
|
||||||
)
|
|
||||||
|
|
||||||
type ColorRGB struct {
|
|
||||||
r uint8
|
|
||||||
g uint8
|
|
||||||
b uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ColorRGB) FromHex(hexString string) ColorRGB {
|
|
||||||
if len(hexString) < 6 {
|
|
||||||
return White
|
|
||||||
}
|
|
||||||
if hexString[0] == '#' {
|
|
||||||
hexString = hexString[1:]
|
|
||||||
}
|
|
||||||
if len(hexString) != 6 {
|
|
||||||
return White
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := hex.DecodeString(hexString)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
return White
|
|
||||||
}
|
|
||||||
|
|
||||||
return ColorRGB{
|
|
||||||
r: res[0],
|
|
||||||
g: res[1],
|
|
||||||
b: res[2],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getStr(str string, typeByte Variant, color ColorRGB) string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
trueColorFMT,
|
|
||||||
typeByte,
|
|
||||||
color.r,
|
|
||||||
color.g,
|
|
||||||
color.b,
|
|
||||||
str,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
Black ColorRGB
|
|
||||||
White = ColorRGB{255, 255, 255}
|
|
||||||
Green = ColorRGB{0, 255, 0}
|
|
||||||
Red = ColorRGB{255, 0, 0}
|
|
||||||
Blue = ColorRGB{0, 0, 255}
|
|
||||||
Purple = ColorRGB{255, 0, 255}
|
|
||||||
Yellow = ColorRGB{255, 255, 0}
|
|
||||||
)
|
|
|
@ -1,22 +0,0 @@
|
||||||
package ctllog
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
// "sectorinf.com/emilis/hlctl/ctllog"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestColorFromHex(t *testing.T) {
|
|
||||||
testCases := map[string]ColorRGB{
|
|
||||||
"000000": {},
|
|
||||||
"0c2238": {12, 34, 56},
|
|
||||||
}
|
|
||||||
|
|
||||||
for hexColor, expected := range testCases {
|
|
||||||
t.Run(hexColor, func(t *testing.T) {
|
|
||||||
actual := ColorRGB{}.FromHex(hexColor)
|
|
||||||
if expected != actual {
|
|
||||||
t.Fatalf("expected %+v got %+v", expected, actual)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
package ctllog
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"runtime/debug"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
logfile io.Writer
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
if os.Getenv("debug") == "true" {
|
|
||||||
path := fmt.Sprintf("%s/.hlctl.log", os.Getenv("HOME"))
|
|
||||||
file, err := os.Create(path)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Could not open [%s] for logging: %s\n", path, err)
|
|
||||||
fmt.Fprintln(os.Stderr, "Defaulting to stderr")
|
|
||||||
logfile = os.Stderr
|
|
||||||
}
|
|
||||||
logfile = file
|
|
||||||
} else {
|
|
||||||
logfile = os.Stderr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Logger struct {
|
|
||||||
coloredName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Logger) New(name string, color ColorRGB) Logger {
|
|
||||||
coloredName := getStr("[ "+name+" ]", Foreground, color)
|
|
||||||
return Logger{
|
|
||||||
coloredName: coloredName,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Logger) print(str string) {
|
|
||||||
lines := strings.Split(str, "\n")
|
|
||||||
for i, line := range lines {
|
|
||||||
lines[i] = fmt.Sprintf("%s: %s", l.coloredName, line)
|
|
||||||
}
|
|
||||||
str = strings.Join(lines, "\n")
|
|
||||||
fmt.Fprintln(logfile, str)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Logger) colored(color ColorRGB, args ...any) {
|
|
||||||
l.print(getStr(fmt.Sprint(args...), Foreground, color))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Logger) coloredf(color ColorRGB, format string, args ...any) {
|
|
||||||
l.print(getStr(fmt.Sprintf(format, args...), Foreground, color))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Logger) fatal(str string) {
|
|
||||||
l.print(getStr(str, Background, Red))
|
|
||||||
debug.PrintStack()
|
|
||||||
os.Exit(16)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Logger) Print(args ...any) {
|
|
||||||
l.print(fmt.Sprint(args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Logger) Printf(format string, args ...any) {
|
|
||||||
l.print(fmt.Sprintf(format, args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Logger) Warn(args ...any) {
|
|
||||||
l.colored(Yellow, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Logger) Warnf(format string, args ...any) {
|
|
||||||
l.coloredf(Yellow, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Logger) Error(args ...any) {
|
|
||||||
l.colored(Red, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Logger) Errorf(format string, args ...any) {
|
|
||||||
l.coloredf(Red, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Logger) Fatal(args ...any) {
|
|
||||||
l.fatal(fmt.Sprint(args...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l Logger) Fatalf(format string, args ...any) {
|
|
||||||
l.fatal(fmt.Sprintf(format, args...))
|
|
||||||
}
|
|
5
go.mod
5
go.mod
|
@ -1,5 +0,0 @@
|
||||||
module sectorinf.com/emilis/hlctl
|
|
||||||
|
|
||||||
go 1.19
|
|
||||||
|
|
||||||
require github.com/BurntSushi/toml v1.2.1
|
|
2
go.sum
2
go.sum
|
@ -1,2 +0,0 @@
|
||||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
|
||||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
|
|
@ -1,106 +0,0 @@
|
||||||
package groupctl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"sectorinf.com/emilis/hlctl/config"
|
|
||||||
"sectorinf.com/emilis/hlctl/ctllog"
|
|
||||||
"sectorinf.com/emilis/hlctl/hlcl"
|
|
||||||
"sectorinf.com/emilis/hlctl/hlcl/cmd"
|
|
||||||
)
|
|
||||||
|
|
||||||
var log = ctllog.Logger{}.New("group", ctllog.ColorRGB{}.FromHex("27ee7e"))
|
|
||||||
|
|
||||||
func ResetActive() error {
|
|
||||||
err := config.New(config.ActiveGroup, "1", cmd.Uint)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("new active group: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Update()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update switches to the correct tag based on the active group
|
|
||||||
func Update() error {
|
|
||||||
activeCfg, err := config.Get(config.ActiveGroup)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", config.ActiveGroup, err)
|
|
||||||
}
|
|
||||||
active, err := activeCfg.Uint()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", config.ActiveGroup, err)
|
|
||||||
}
|
|
||||||
focus, err := config.GetFocusedTag()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("focus: %w", err)
|
|
||||||
}
|
|
||||||
numCfg, err := config.Get(config.TagsPerGroup)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get %s: %w", config.TagsPerGroup, err)
|
|
||||||
}
|
|
||||||
num, err := numCfg.Uint()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", config.TagsPerGroup, err)
|
|
||||||
}
|
|
||||||
relativeTag := focus % uint8(num)
|
|
||||||
absTag := ((active - 1) * num) + relativeTag
|
|
||||||
if focus != absTag {
|
|
||||||
log.Printf("Tag %d -> %d", focus, absTag)
|
|
||||||
}
|
|
||||||
if err := hlcl.UseIndex(absTag); err != nil {
|
|
||||||
return fmt.Errorf("UseIndex[%d]: %w", absTag, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return SetKeybinds(active, num)
|
|
||||||
}
|
|
||||||
|
|
||||||
func MoveTo(group uint8) error {
|
|
||||||
numCfg, err := config.Get(config.TagsPerGroup)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get %s: %w", config.TagsPerGroup, err)
|
|
||||||
}
|
|
||||||
num, err := numCfg.Uint()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", config.TagsPerGroup, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
focus, err := config.GetFocusedTag()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("focus: %w", err)
|
|
||||||
}
|
|
||||||
relative := focus % num
|
|
||||||
newTag := ((group - 1) * num) + relative
|
|
||||||
log.Printf("Moving window to tag -> %d", newTag)
|
|
||||||
return hlcl.MoveIndex(newTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetKeybinds(group uint8, num uint8) error {
|
|
||||||
offset := int((group - 1) * num)
|
|
||||||
mod, err := config.Get(config.ModKey)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", config.ModKey, err)
|
|
||||||
}
|
|
||||||
log.Printf("Setting %d keybinds for group %d", num, group)
|
|
||||||
for i := 0; i < int(num); i++ {
|
|
||||||
actualTag := strconv.Itoa(offset + i)
|
|
||||||
tag := strconv.Itoa(i + 1)
|
|
||||||
err = hlcl.Keybind(
|
|
||||||
[]string{mod.String(), tag},
|
|
||||||
cmd.UseIndex,
|
|
||||||
actualTag,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Setting use_index for [%d]: %w", i+1, err)
|
|
||||||
}
|
|
||||||
err = hlcl.Keybind(
|
|
||||||
[]string{mod.String(), "Shift", tag},
|
|
||||||
cmd.MoveIndex,
|
|
||||||
actualTag,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Setting move_index for [%d]: %w", i+1, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
103
hlcl/cmd/enum.go
103
hlcl/cmd/enum.go
|
@ -1,103 +0,0 @@
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Command string
|
|
||||||
|
|
||||||
func (c Command) String() string {
|
|
||||||
return string(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Command) Pretty() string {
|
|
||||||
parts := strings.Split(string(c), "_")
|
|
||||||
for i, part := range parts {
|
|
||||||
// Set the first character to be upper case
|
|
||||||
if len(part) == 0 {
|
|
||||||
continue
|
|
||||||
} else if len(part) == 1 {
|
|
||||||
parts[i] = strings.ToUpper(part)
|
|
||||||
}
|
|
||||||
var first rune
|
|
||||||
for _, r := range part {
|
|
||||||
first = r
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
parts[i] = fmt.Sprintf("%c%s", unicode.ToUpper(first), part[1:])
|
|
||||||
}
|
|
||||||
return strings.Join(parts, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
Reload Command = "reload"
|
|
||||||
Close Command = "close"
|
|
||||||
Spawn Command = "spawn"
|
|
||||||
ListMonitors Command = "list_monitors"
|
|
||||||
Lock Command = "lock"
|
|
||||||
Unlock Command = "unlock"
|
|
||||||
|
|
||||||
// Attr family
|
|
||||||
GetAttr Command = "get_attr"
|
|
||||||
SetAttr Command = "set_attr"
|
|
||||||
Attr Command = "attr"
|
|
||||||
NewAttr Command = "new_attr"
|
|
||||||
AttrType Command = "attr_type"
|
|
||||||
RemoveAttr Command = "remove_attr"
|
|
||||||
|
|
||||||
Set Command = "set"
|
|
||||||
EmitHook Command = "emit_hook"
|
|
||||||
Rule Command = "rule"
|
|
||||||
Unrule Command = "unrule"
|
|
||||||
Substitute Command = "substitute"
|
|
||||||
|
|
||||||
// Controls
|
|
||||||
Keybind Command = "keybind"
|
|
||||||
Keyunbind Command = "keyunbind"
|
|
||||||
Mousebind Command = "mousebind"
|
|
||||||
Mouseunbind Command = "mouseunbind"
|
|
||||||
UseIndex Command = "use_index"
|
|
||||||
MoveIndex Command = "move_index"
|
|
||||||
JumpTo Command = "jumpto"
|
|
||||||
|
|
||||||
// Views/Frames
|
|
||||||
AddTag Command = "add"
|
|
||||||
MergeTag Command = "merge_tag"
|
|
||||||
Cycle Command = "cycle"
|
|
||||||
Focus Command = "focus"
|
|
||||||
Shift Command = "shift"
|
|
||||||
Split Command = "split"
|
|
||||||
Remove Command = "remove"
|
|
||||||
Fullscreen Command = "fullscreen"
|
|
||||||
CycleLayout Command = "cycle_layout"
|
|
||||||
Resize Command = "resize"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MouseButton string
|
|
||||||
|
|
||||||
func (m MouseButton) String() string {
|
|
||||||
return string(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
Mouse1 MouseButton = "Mouse1"
|
|
||||||
Mouse2 MouseButton = "Mouse2"
|
|
||||||
Mouse3 MouseButton = "Mouse3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AttributeType string
|
|
||||||
|
|
||||||
func (a AttributeType) String() string {
|
|
||||||
return string(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
String AttributeType = "string"
|
|
||||||
Bool AttributeType = "bool"
|
|
||||||
Color AttributeType = "color"
|
|
||||||
Int AttributeType = "int"
|
|
||||||
Uint AttributeType = "uint"
|
|
||||||
)
|
|
|
@ -1,190 +0,0 @@
|
||||||
// package hlcl contains logic for interacting with the
|
|
||||||
// herbstclient
|
|
||||||
package hlcl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"sectorinf.com/emilis/hlctl/ctllog"
|
|
||||||
"sectorinf.com/emilis/hlctl/hlcl/cmd"
|
|
||||||
)
|
|
||||||
|
|
||||||
var log = ctllog.Logger{}.New("hlcl", ctllog.Purple)
|
|
||||||
|
|
||||||
func runGeneric(command string, stdin string, args ...string) (string, error) {
|
|
||||||
cmd := exec.Command(command, args...)
|
|
||||||
log.Printf("Running command [%s] with args: %v", command, args)
|
|
||||||
var stdout, stderr = bytes.Buffer{}, bytes.Buffer{}
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
if stdin != "" {
|
|
||||||
pipe, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("stdin pipe: %w", err)
|
|
||||||
}
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return "", fmt.Errorf("start: %w", err)
|
|
||||||
}
|
|
||||||
io.WriteString(pipe, stdin+"\n")
|
|
||||||
_, err = pipe.Write([]byte(stdin))
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("write stdin: %w", err)
|
|
||||||
}
|
|
||||||
pipe.Close()
|
|
||||||
} else {
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return "", fmt.Errorf("start: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
status, ok := err.(*exec.ExitError)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("%s exec error: %w", command, err)
|
|
||||||
}
|
|
||||||
exitCode := status.ExitCode()
|
|
||||||
return "", fmt.Errorf(
|
|
||||||
"%s error (%d): %s",
|
|
||||||
command,
|
|
||||||
exitCode,
|
|
||||||
strings.TrimSpace(stderr.String()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.TrimSpace(stdout.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func run(command cmd.Command, args ...string) (string, error) {
|
|
||||||
args = append([]string{command.String()}, args...)
|
|
||||||
return runGeneric("herbstclient", "", args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// query executes a command and pretty formats and error message
|
|
||||||
// based on the command's Pretty(), and returns the output
|
|
||||||
func query(c cmd.Command, args ...string) (string, error) {
|
|
||||||
value, err := run(c, args...)
|
|
||||||
if err != nil {
|
|
||||||
return value, fmt.Errorf(
|
|
||||||
"%s(%s): %w",
|
|
||||||
c.Pretty(),
|
|
||||||
strings.Join(args, ", "),
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// execute: wrapper for [query], discarding the stdout result
|
|
||||||
func execute(c cmd.Command, args ...string) error {
|
|
||||||
_, err := query(c, args...)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAttr(path string, value string, aType cmd.AttributeType) error {
|
|
||||||
return execute(cmd.NewAttr, aType.String(), path, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetAttr(path string, value string) error {
|
|
||||||
return execute(cmd.SetAttr, path, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAttr(path string) (string, error) {
|
|
||||||
return query(cmd.GetAttr, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AttrType(path string) (string, error) {
|
|
||||||
return query(cmd.AttrType, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func KeyUnbind() error {
|
|
||||||
return execute(cmd.Keyunbind, "--all")
|
|
||||||
}
|
|
||||||
|
|
||||||
func MouseUnbind() error {
|
|
||||||
return execute(cmd.Mouseunbind, "--all")
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListMonitors() ([]Screen, error) {
|
|
||||||
out, err := query(cmd.ListMonitors)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
outs := strings.Split(out, "\n")
|
|
||||||
scrs := make([]Screen, len(outs))
|
|
||||||
for i, line := range outs {
|
|
||||||
scrs[i] = Screen{}.FromString(line)
|
|
||||||
}
|
|
||||||
return scrs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func bindCMD(
|
|
||||||
bindCommand cmd.Command,
|
|
||||||
keys []string,
|
|
||||||
command cmd.Command,
|
|
||||||
args ...string,
|
|
||||||
) error {
|
|
||||||
bindKeys := strings.Join(keys, "-")
|
|
||||||
args = append([]string{bindKeys, command.String()}, args...)
|
|
||||||
return execute(bindCommand, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Keybind(keys []string, command cmd.Command, args ...string) error {
|
|
||||||
return bindCMD(cmd.Keybind, keys, command, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Mousebind(keys []string, command cmd.Command, args ...string) error {
|
|
||||||
return bindCMD(cmd.Mousebind, keys, command, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Set(name, value string) error {
|
|
||||||
return execute(cmd.Set, name, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UseIndex(index uint8) error {
|
|
||||||
return execute(cmd.UseIndex, strconv.Itoa(int(index)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeTag(name string) error {
|
|
||||||
return execute(cmd.MergeTag, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func MoveIndex(index uint8) error {
|
|
||||||
return execute(cmd.MoveIndex, strconv.Itoa(int(index)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TextWidth(font, message string) (string, error) {
|
|
||||||
return runGeneric("textwidth", "", font, message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Rule(name string, args ...string) error {
|
|
||||||
return execute(cmd.Rule, append([]string{name}, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Unrule() error {
|
|
||||||
return execute(cmd.Unrule, "-F")
|
|
||||||
}
|
|
||||||
|
|
||||||
func SilentSpawn(command string, args ...string) error {
|
|
||||||
return execute(cmd.Spawn, append([]string{command}, args...)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Lock() error {
|
|
||||||
return execute(cmd.Lock)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Unlock() error {
|
|
||||||
return execute(cmd.Unlock)
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddTag(name string) error {
|
|
||||||
return execute(cmd.AddTag, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Dzen2(message string, args ...string) error {
|
|
||||||
_, err := runGeneric("dzen2", message, args...)
|
|
||||||
return err
|
|
||||||
}
|
|
29
hlcl/type.go
29
hlcl/type.go
|
@ -1,29 +0,0 @@
|
||||||
package hlcl
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
type Screen struct {
|
|
||||||
ID string
|
|
||||||
Resolution string
|
|
||||||
WithTag string
|
|
||||||
Focused bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Screen) FromString(v string) Screen {
|
|
||||||
parts := strings.Split(v, " ")
|
|
||||||
if len(parts) < 5 {
|
|
||||||
return Screen{
|
|
||||||
ID: v[:1],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, part := range parts {
|
|
||||||
parts[i] = strings.TrimSpace(
|
|
||||||
strings.TrimRight(
|
|
||||||
strings.Trim(part, "\""), ":"))
|
|
||||||
}
|
|
||||||
return Screen{
|
|
||||||
ID: parts[0],
|
|
||||||
Resolution: parts[1],
|
|
||||||
// Maybe later
|
|
||||||
}
|
|
||||||
}
|
|
80
main.go
80
main.go
|
@ -1,80 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"sectorinf.com/emilis/hlctl/cmdlets"
|
|
||||||
"sectorinf.com/emilis/hlctl/cmdlets/group"
|
|
||||||
initcmd "sectorinf.com/emilis/hlctl/cmdlets/init"
|
|
||||||
"sectorinf.com/emilis/hlctl/cmdlets/notify"
|
|
||||||
"sectorinf.com/emilis/hlctl/cmdlets/save"
|
|
||||||
"sectorinf.com/emilis/hlctl/ctllog"
|
|
||||||
)
|
|
||||||
|
|
||||||
var log = ctllog.Logger{}.New("hlctl", ctllog.Green)
|
|
||||||
|
|
||||||
var cmds = []cmdlets.Commandlet{
|
|
||||||
initcmd.Command,
|
|
||||||
group.Command,
|
|
||||||
notify.Command,
|
|
||||||
save.Command,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrNotFound = errors.New("not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
func getCmdlet(name string) (cmdlets.Commandlet, error) {
|
|
||||||
for _, c := range cmds {
|
|
||||||
if c.Name() == name {
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("%s: %w", name, ErrNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func help() {
|
|
||||||
commands := make([]string, len(cmds))
|
|
||||||
for i, c := range cmds {
|
|
||||||
commands[i] = c.Name()
|
|
||||||
}
|
|
||||||
fmt.Fprint(os.Stderr, "USAGE: hlctl [command] [arguments]\n")
|
|
||||||
fmt.Fprintf(os.Stderr, "\nAvailable commands: %v\n", commands)
|
|
||||||
fmt.Fprint(os.Stderr, "\nSee hlctl help [command] for details\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// omit the program name
|
|
||||||
args := os.Args[1:]
|
|
||||||
if len(args) == 0 {
|
|
||||||
help()
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
command := strings.ToLower(args[0])
|
|
||||||
if command == "help" {
|
|
||||||
if len(args) == 1 {
|
|
||||||
help()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
name := args[1]
|
|
||||||
c, err := getCmdlet(name)
|
|
||||||
if err != nil {
|
|
||||||
help()
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
fmt.Println(c.Usage())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c, err := getCmdlet(command)
|
|
||||||
if err != nil {
|
|
||||||
help()
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.Invoke(args[1:]...); err != nil {
|
|
||||||
log.Fatalf(" %s > %s", c.Name(), err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
package notifyctl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"sectorinf.com/emilis/hlctl/ctllog"
|
|
||||||
"sectorinf.com/emilis/hlctl/hlcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
var log = ctllog.Logger{}.New("notify", ctllog.ColorRGB{}.FromHex("ff003e"))
|
|
||||||
|
|
||||||
func Display(message string, duration uint, fg, bg, font string) error {
|
|
||||||
width, err := hlcl.TextWidth(font, message)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("%s; defaulting to width 100", err.Error())
|
|
||||||
width = "100"
|
|
||||||
}
|
|
||||||
widthNum, err := strconv.ParseUint(width, 10, 8)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return hlcl.Dzen2(
|
|
||||||
message,
|
|
||||||
"-p", strconv.Itoa(int(duration)),
|
|
||||||
"-fg", fg,
|
|
||||||
"-bg", bg,
|
|
||||||
"-fn", font,
|
|
||||||
"-ta", "c",
|
|
||||||
"-w", strconv.Itoa(int(widthNum)+30),
|
|
||||||
"-h", "16",
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
package panelctl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"sectorinf.com/emilis/hlctl/hlcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Run() error {
|
|
||||||
screens, err := hlcl.ListMonitors()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
panel := func(mon string) error {
|
|
||||||
return exec.Command(
|
|
||||||
"bash",
|
|
||||||
fmt.Sprintf(
|
|
||||||
"%s/.config/herbstluftwm/panel.sh",
|
|
||||||
os.Getenv("HOME"),
|
|
||||||
),
|
|
||||||
mon,
|
|
||||||
).Run()
|
|
||||||
}
|
|
||||||
for i := 1; i < len(screens); i++ {
|
|
||||||
id := screens[i].ID
|
|
||||||
go func() {
|
|
||||||
if err := panel(id); err != nil {
|
|
||||||
log.Fatalf("Monitor [%s] panel: %s", id, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
return panel(screens[0].ID)
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,289 @@
|
||||||
|
use std::{num::ParseIntError, str::FromStr};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::hlwm::hex::ParseHex;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
color::{self, Color, X11Color},
|
||||||
|
hex::HexError,
|
||||||
|
octal::{OctalError, ParseOctal},
|
||||||
|
StringParseError,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AttributeError {
|
||||||
|
#[error("error parsing integer value: {0:?}")]
|
||||||
|
ParseIntError(#[from] ParseIntError),
|
||||||
|
#[error("error parsing bool value: [{0}]")]
|
||||||
|
ParseBoolError(String),
|
||||||
|
#[error("error parsing color value: {0}")]
|
||||||
|
ParseColorError(#[from] color::ParseError),
|
||||||
|
#[error("unknown attribute type [{0}]")]
|
||||||
|
UnknownType(String),
|
||||||
|
#[error("not a valid rectangle: [{0}]")]
|
||||||
|
NotRectangle(String),
|
||||||
|
#[error("hex parsing error: [{0}]")]
|
||||||
|
HexError(#[from] HexError),
|
||||||
|
#[error("octal parsing error: [{0}]")]
|
||||||
|
OctalError(#[from] OctalError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum AttributeOption {
|
||||||
|
Bool(Option<bool>),
|
||||||
|
Color(Option<Color>),
|
||||||
|
Int(Option<i32>),
|
||||||
|
String(Option<String>),
|
||||||
|
Uint(Option<u32>),
|
||||||
|
Rectangle(Option<(u32, u32)>),
|
||||||
|
WindowID(Option<u32>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AttributeOption {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Int(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttributeOption {
|
||||||
|
pub fn new(type_string: &str, value_string: &Option<String>) -> Result<Self, AttributeError> {
|
||||||
|
if let Some(val) = value_string {
|
||||||
|
return Ok(Attribute::new(type_string, val)?.into());
|
||||||
|
}
|
||||||
|
match type_string {
|
||||||
|
"int" => Ok(Self::Int(None)),
|
||||||
|
"bool" => Ok(Self::Bool(None)),
|
||||||
|
"uint" => Ok(Self::Uint(None)),
|
||||||
|
"color" => Ok(Self::Color(None)),
|
||||||
|
"windowid" => Ok(Self::WindowID(None)),
|
||||||
|
"rectangle" => Ok(Self::Rectangle(None)),
|
||||||
|
"string" | "names" | "regex" => Ok(Self::String(None)),
|
||||||
|
_ => Err(AttributeError::UnknownType(type_string.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_default_attr(&self) -> Attribute {
|
||||||
|
match self {
|
||||||
|
AttributeOption::Int(_) => Attribute::Int(Default::default()),
|
||||||
|
AttributeOption::Uint(_) => Attribute::Uint(Default::default()),
|
||||||
|
AttributeOption::Bool(_) => Attribute::Bool(Default::default()),
|
||||||
|
AttributeOption::Color(_) => Attribute::Color(Default::default()),
|
||||||
|
AttributeOption::String(_) => Attribute::String(Default::default()),
|
||||||
|
AttributeOption::WindowID(_) => Attribute::WindowID(Default::default()),
|
||||||
|
AttributeOption::Rectangle(_) => Attribute::Rectangle {
|
||||||
|
x: Default::default(),
|
||||||
|
y: Default::default(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn type_string(&self) -> &'static str {
|
||||||
|
self.to_default_attr().type_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value_string(&self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
AttributeOption::Int(val) => val.map(|val| val.to_string()),
|
||||||
|
AttributeOption::Bool(val) => val.map(|val| val.to_string()),
|
||||||
|
AttributeOption::Uint(val) => val.map(|val| val.to_string()),
|
||||||
|
AttributeOption::Color(val) => val.clone().map(|val| val.to_string()),
|
||||||
|
AttributeOption::String(val) => val.clone().map(|val| val.to_string()),
|
||||||
|
AttributeOption::WindowID(val) => val.map(|w| format!("{w:#x}")),
|
||||||
|
AttributeOption::Rectangle(val) => val.map(|(x, y)| format!("{x}x{y}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Attribute> for AttributeOption {
|
||||||
|
fn from(value: Attribute) -> Self {
|
||||||
|
match value {
|
||||||
|
Attribute::Bool(val) => Self::Bool(Some(val)),
|
||||||
|
Attribute::Color(val) => Self::Color(Some(val)),
|
||||||
|
Attribute::Int(val) => Self::Int(Some(val)),
|
||||||
|
Attribute::String(val) => Self::String(Some(val)),
|
||||||
|
Attribute::Uint(val) => Self::Uint(Some(val)),
|
||||||
|
Attribute::Rectangle { x, y } => Self::Rectangle(Some((x, y))),
|
||||||
|
Attribute::WindowID(win) => Self::WindowID(Some(win)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, strum::EnumDiscriminants)]
|
||||||
|
#[strum_discriminants(
|
||||||
|
name(AttributeType),
|
||||||
|
derive(strum::Display, strum::EnumIter),
|
||||||
|
strum(serialize_all = "lowercase")
|
||||||
|
)]
|
||||||
|
pub enum Attribute {
|
||||||
|
Bool(bool),
|
||||||
|
Color(Color),
|
||||||
|
Int(i32),
|
||||||
|
String(String),
|
||||||
|
Uint(u32),
|
||||||
|
Rectangle { x: u32, y: u32 },
|
||||||
|
WindowID(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i32> for Attribute {
|
||||||
|
fn from(value: i32) -> Self {
|
||||||
|
Self::Int(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u32> for Attribute {
|
||||||
|
fn from(value: u32) -> Self {
|
||||||
|
Self::Uint(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Attribute {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self::String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for Attribute {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
Self::String(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Color> for Attribute {
|
||||||
|
fn from(value: Color) -> Self {
|
||||||
|
Self::Color(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bool> for Attribute {
|
||||||
|
fn from(value: bool) -> Self {
|
||||||
|
Self::Bool(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(u32, u32)> for Attribute {
|
||||||
|
fn from((x, y): (u32, u32)) -> Self {
|
||||||
|
Self::Rectangle { x, y }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Attribute {
|
||||||
|
pub fn new(type_string: &str, value_string: &str) -> Result<Self, AttributeError> {
|
||||||
|
match type_string {
|
||||||
|
"bool" => match value_string {
|
||||||
|
"on" | "true" => Ok(Attribute::Bool(true)),
|
||||||
|
"off" | "false" => Ok(Attribute::Bool(false)),
|
||||||
|
_ => Err(AttributeError::ParseBoolError(type_string.to_string())),
|
||||||
|
},
|
||||||
|
"color" => Ok(Attribute::Color(Color::from_str(value_string)?)),
|
||||||
|
"int" => Ok(Attribute::Int(value_string.parse()?)),
|
||||||
|
"string" | "names" | "regex" => Ok(Attribute::String(value_string.to_string())),
|
||||||
|
"uint" => Ok(Attribute::Uint(value_string.parse()?)),
|
||||||
|
"rectangle" => {
|
||||||
|
let parts = value_string.split('x').collect::<Vec<_>>();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err(AttributeError::NotRectangle(value_string.to_string()));
|
||||||
|
}
|
||||||
|
Ok(Attribute::Rectangle {
|
||||||
|
x: parts.get(0).unwrap().parse()?,
|
||||||
|
y: parts.get(1).unwrap().parse()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"windowid" => {
|
||||||
|
if let Some(hex) = value_string.strip_prefix("0x") {
|
||||||
|
Ok(Attribute::WindowID(hex.parse_hex()?))
|
||||||
|
} else if let Some(octal) = value_string.strip_prefix("0") {
|
||||||
|
Ok(Attribute::WindowID(octal.parse_octal()?))
|
||||||
|
} else {
|
||||||
|
Ok(Attribute::WindowID(value_string.parse()?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(AttributeError::UnknownType(type_string.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guess_from_value(value: &str) -> Option<Attribute> {
|
||||||
|
match value {
|
||||||
|
"on" | "true" => Some(Self::Bool(true)),
|
||||||
|
"off" | "false" => Some(Self::Bool(false)),
|
||||||
|
_ => {
|
||||||
|
// Match for all colors first
|
||||||
|
if let Some(color) = X11Color::iter()
|
||||||
|
.into_iter()
|
||||||
|
.find(|c| c.to_string() == value)
|
||||||
|
{
|
||||||
|
return Some(Attribute::Color(Color::X11(color)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match for primitive types or color string
|
||||||
|
let mut chars = value.chars();
|
||||||
|
match chars.next().unwrap() {
|
||||||
|
'+' => Self::guess_from_value(&chars.collect::<String>()),
|
||||||
|
'0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '-' => {
|
||||||
|
i32::from_str(value).ok().map(|i| Attribute::Int(i))
|
||||||
|
}
|
||||||
|
'#' => Color::from_hex(value).ok().map(|c| Attribute::Color(c)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn guess_type(value: &str) -> Self {
|
||||||
|
if value.is_empty() {
|
||||||
|
Self::String(String::new())
|
||||||
|
} else {
|
||||||
|
Self::guess_from_value(value).unwrap_or(Attribute::String(value.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn type_string(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Attribute::Bool(_) => "bool",
|
||||||
|
Attribute::Color(_) => "color",
|
||||||
|
Attribute::Int(_) => "int",
|
||||||
|
Attribute::String(_) => "string",
|
||||||
|
Attribute::Uint(_) => "uint",
|
||||||
|
Attribute::Rectangle { x: _, y: _ } => "rectangle",
|
||||||
|
Attribute::WindowID(_) => "windowid",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Attribute {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Attribute::Bool(b) => write!(f, "{b}"),
|
||||||
|
Attribute::Color(c) => write!(f, "{c}"),
|
||||||
|
Attribute::Int(i) => write!(f, "{i}"),
|
||||||
|
Attribute::String(s) => f.write_str(s),
|
||||||
|
Attribute::Uint(u) => write!(f, "{u}"),
|
||||||
|
Attribute::Rectangle { x, y } => write!(f, "{x}x{y}"),
|
||||||
|
Attribute::WindowID(win) => write!(f, "{win:#x}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Attribute {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Int(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AttributeType {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for AttributeType {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::iter()
|
||||||
|
.find(|t| t.to_string() == s)
|
||||||
|
.ok_or(StringParseError::UnknownValue)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,426 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use serde::{de::Unexpected, Deserialize, Serialize};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::hlwm::StringParseError;
|
||||||
|
|
||||||
|
use super::hex::{HexError, ParseHex};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Color {
|
||||||
|
RGB { r: u8, g: u8, b: u8 },
|
||||||
|
X11(X11Color),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Color {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Color {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct ExpectedColor;
|
||||||
|
impl serde::de::Expected for ExpectedColor {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("an X11 color string or a hex color string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let str_val: String = Deserialize::deserialize(deserializer)?;
|
||||||
|
Ok(Color::from_str(&str_val).map_err(|_| {
|
||||||
|
serde::de::Error::invalid_value(Unexpected::Str(&str_val), &ExpectedColor)
|
||||||
|
})?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Color {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::RGB {
|
||||||
|
r: Default::default(),
|
||||||
|
g: Default::default(),
|
||||||
|
b: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Error)]
|
||||||
|
pub enum ParseError {
|
||||||
|
#[error("length must be either 6 characters or 7 with '#' prefix")]
|
||||||
|
InvalidLength,
|
||||||
|
#[error("invalid hex value: [{0}]")]
|
||||||
|
HexError(#[from] HexError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Color {
|
||||||
|
type Err = ParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Ok(x11) = X11Color::from_str(s) {
|
||||||
|
return Ok(Self::X11(x11));
|
||||||
|
}
|
||||||
|
Self::from_hex(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Color {
|
||||||
|
pub const BLACK: Color = Color::rgb(0, 0, 0);
|
||||||
|
|
||||||
|
pub fn from_hex(hex: &str) -> Result<Self, ParseError> {
|
||||||
|
let expected_len = if hex.starts_with('#') { 7 } else { 6 };
|
||||||
|
if hex.len() != expected_len {
|
||||||
|
return Err(ParseError::InvalidLength);
|
||||||
|
}
|
||||||
|
let hex = hex.strip_prefix('#').unwrap_or(hex);
|
||||||
|
|
||||||
|
Ok(Self::RGB {
|
||||||
|
r: (&hex[0..2]).parse_hex()?,
|
||||||
|
g: (&hex[2..4]).parse_hex()?,
|
||||||
|
b: (&hex[4..6]).parse_hex()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
|
||||||
|
Self::RGB { r, g, b }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Color {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Color::RGB { r, g, b } => write!(f, "#{r:02X}{g:02X}{b:02X}"),
|
||||||
|
Color::X11(color) => f.write_str(&color.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, strum::Display, strum::EnumIter, PartialEq, Eq)]
|
||||||
|
pub enum X11Color {
|
||||||
|
GhostWhite,
|
||||||
|
WhiteSmoke,
|
||||||
|
FloralWhite,
|
||||||
|
OldLace,
|
||||||
|
AntiqueWhite,
|
||||||
|
PapayaWhip,
|
||||||
|
BlanchedAlmond,
|
||||||
|
PeachPuff,
|
||||||
|
NavajoWhite,
|
||||||
|
LemonChiffon,
|
||||||
|
MintCream,
|
||||||
|
AliceBlue,
|
||||||
|
LavenderBlush,
|
||||||
|
MistyRose,
|
||||||
|
DarkSlateGray,
|
||||||
|
DarkSlateGrey,
|
||||||
|
DimGray,
|
||||||
|
DimGrey,
|
||||||
|
SlateGray,
|
||||||
|
SlateGrey,
|
||||||
|
LightSlateGray,
|
||||||
|
LightSlateGrey,
|
||||||
|
X11Gray,
|
||||||
|
X11Grey,
|
||||||
|
WebGray,
|
||||||
|
WebGrey,
|
||||||
|
LightGrey,
|
||||||
|
LightGray,
|
||||||
|
MidnightBlue,
|
||||||
|
NavyBlue,
|
||||||
|
CornflowerBlue,
|
||||||
|
DarkSlateBlue,
|
||||||
|
SlateBlue,
|
||||||
|
MediumSlateBlue,
|
||||||
|
LightSlateBlue,
|
||||||
|
MediumBlue,
|
||||||
|
RoyalBlue,
|
||||||
|
DodgerBlue,
|
||||||
|
DeepSkyBlue,
|
||||||
|
SkyBlue,
|
||||||
|
LightSkyBlue,
|
||||||
|
SteelBlue,
|
||||||
|
LightSteelBlue,
|
||||||
|
LightBlue,
|
||||||
|
PowderBlue,
|
||||||
|
PaleTurquoise,
|
||||||
|
DarkTurquoise,
|
||||||
|
MediumTurquoise,
|
||||||
|
LightCyan,
|
||||||
|
CadetBlue,
|
||||||
|
MediumAquamarine,
|
||||||
|
DarkGreen,
|
||||||
|
DarkOliveGreen,
|
||||||
|
DarkSeaGreen,
|
||||||
|
SeaGreen,
|
||||||
|
MediumSeaGreen,
|
||||||
|
LightSeaGreen,
|
||||||
|
PaleGreen,
|
||||||
|
SpringGreen,
|
||||||
|
LawnGreen,
|
||||||
|
X11Green,
|
||||||
|
WebGreen,
|
||||||
|
MediumSpringGreen,
|
||||||
|
GreenYellow,
|
||||||
|
LimeGreen,
|
||||||
|
YellowGreen,
|
||||||
|
ForestGreen,
|
||||||
|
OliveDrab,
|
||||||
|
DarkKhaki,
|
||||||
|
PaleGoldenrod,
|
||||||
|
LightGoldenrodYellow,
|
||||||
|
LightYellow,
|
||||||
|
LightGoldenrod,
|
||||||
|
DarkGoldenrod,
|
||||||
|
RosyBrown,
|
||||||
|
IndianRed,
|
||||||
|
SaddleBrown,
|
||||||
|
SandyBrown,
|
||||||
|
DarkSalmon,
|
||||||
|
LightSalmon,
|
||||||
|
DarkOrange,
|
||||||
|
LightCoral,
|
||||||
|
OrangeRed,
|
||||||
|
HotPink,
|
||||||
|
DeepPink,
|
||||||
|
LightPink,
|
||||||
|
PaleVioletRed,
|
||||||
|
X11Maroon,
|
||||||
|
WebMaroon,
|
||||||
|
MediumVioletRed,
|
||||||
|
VioletRed,
|
||||||
|
MediumOrchid,
|
||||||
|
DarkOrchid,
|
||||||
|
DarkViolet,
|
||||||
|
BlueViolet,
|
||||||
|
X11Purple,
|
||||||
|
WebPurple,
|
||||||
|
MediumPurple,
|
||||||
|
AntiqueWhite1,
|
||||||
|
AntiqueWhite2,
|
||||||
|
AntiqueWhite3,
|
||||||
|
AntiqueWhite4,
|
||||||
|
PeachPuff1,
|
||||||
|
PeachPuff2,
|
||||||
|
PeachPuff3,
|
||||||
|
PeachPuff4,
|
||||||
|
NavajoWhite1,
|
||||||
|
NavajoWhite2,
|
||||||
|
NavajoWhite3,
|
||||||
|
NavajoWhite4,
|
||||||
|
LemonChiffon1,
|
||||||
|
LemonChiffon2,
|
||||||
|
LemonChiffon3,
|
||||||
|
LemonChiffon4,
|
||||||
|
LavenderBlush1,
|
||||||
|
LavenderBlush2,
|
||||||
|
LavenderBlush3,
|
||||||
|
LavenderBlush4,
|
||||||
|
MistyRose1,
|
||||||
|
MistyRose2,
|
||||||
|
MistyRose3,
|
||||||
|
MistyRose4,
|
||||||
|
SlateBlue1,
|
||||||
|
SlateBlue2,
|
||||||
|
SlateBlue3,
|
||||||
|
SlateBlue4,
|
||||||
|
RoyalBlue1,
|
||||||
|
RoyalBlue2,
|
||||||
|
RoyalBlue3,
|
||||||
|
RoyalBlue4,
|
||||||
|
DodgerBlue1,
|
||||||
|
DodgerBlue2,
|
||||||
|
DodgerBlue3,
|
||||||
|
DodgerBlue4,
|
||||||
|
SteelBlue1,
|
||||||
|
SteelBlue2,
|
||||||
|
SteelBlue3,
|
||||||
|
SteelBlue4,
|
||||||
|
DeepSkyBlue1,
|
||||||
|
DeepSkyBlue2,
|
||||||
|
DeepSkyBlue3,
|
||||||
|
DeepSkyBlue4,
|
||||||
|
SkyBlue1,
|
||||||
|
SkyBlue2,
|
||||||
|
SkyBlue3,
|
||||||
|
SkyBlue4,
|
||||||
|
LightSkyBlue1,
|
||||||
|
LightSkyBlue2,
|
||||||
|
LightSkyBlue3,
|
||||||
|
LightSkyBlue4,
|
||||||
|
SlateGray1,
|
||||||
|
SlateGray2,
|
||||||
|
SlateGray3,
|
||||||
|
SlateGray4,
|
||||||
|
LightSteelBlue1,
|
||||||
|
LightSteelBlue2,
|
||||||
|
LightSteelBlue3,
|
||||||
|
LightSteelBlue4,
|
||||||
|
LightBlue1,
|
||||||
|
LightBlue2,
|
||||||
|
LightBlue3,
|
||||||
|
LightBlue4,
|
||||||
|
LightCyan1,
|
||||||
|
LightCyan2,
|
||||||
|
LightCyan3,
|
||||||
|
LightCyan4,
|
||||||
|
PaleTurquoise1,
|
||||||
|
PaleTurquoise2,
|
||||||
|
PaleTurquoise3,
|
||||||
|
PaleTurquoise4,
|
||||||
|
CadetBlue1,
|
||||||
|
CadetBlue2,
|
||||||
|
CadetBlue3,
|
||||||
|
CadetBlue4,
|
||||||
|
DarkSlateGray1,
|
||||||
|
DarkSlateGray2,
|
||||||
|
DarkSlateGray3,
|
||||||
|
DarkSlateGray4,
|
||||||
|
DarkSeaGreen1,
|
||||||
|
DarkSeaGreen2,
|
||||||
|
DarkSeaGreen3,
|
||||||
|
DarkSeaGreen4,
|
||||||
|
SeaGreen1,
|
||||||
|
SeaGreen2,
|
||||||
|
SeaGreen3,
|
||||||
|
SeaGreen4,
|
||||||
|
PaleGreen1,
|
||||||
|
PaleGreen2,
|
||||||
|
PaleGreen3,
|
||||||
|
PaleGreen4,
|
||||||
|
SpringGreen1,
|
||||||
|
SpringGreen2,
|
||||||
|
SpringGreen3,
|
||||||
|
SpringGreen4,
|
||||||
|
OliveDrab1,
|
||||||
|
OliveDrab2,
|
||||||
|
OliveDrab3,
|
||||||
|
OliveDrab4,
|
||||||
|
DarkOliveGreen1,
|
||||||
|
DarkOliveGreen2,
|
||||||
|
DarkOliveGreen3,
|
||||||
|
DarkOliveGreen4,
|
||||||
|
LightGoldenrod1,
|
||||||
|
LightGoldenrod2,
|
||||||
|
LightGoldenrod3,
|
||||||
|
LightGoldenrod4,
|
||||||
|
LightYellow1,
|
||||||
|
LightYellow2,
|
||||||
|
LightYellow3,
|
||||||
|
LightYellow4,
|
||||||
|
DarkGoldenrod1,
|
||||||
|
DarkGoldenrod2,
|
||||||
|
DarkGoldenrod3,
|
||||||
|
DarkGoldenrod4,
|
||||||
|
RosyBrown1,
|
||||||
|
RosyBrown2,
|
||||||
|
RosyBrown3,
|
||||||
|
RosyBrown4,
|
||||||
|
IndianRed1,
|
||||||
|
IndianRed2,
|
||||||
|
IndianRed3,
|
||||||
|
IndianRed4,
|
||||||
|
LightSalmon1,
|
||||||
|
LightSalmon2,
|
||||||
|
LightSalmon3,
|
||||||
|
LightSalmon4,
|
||||||
|
DarkOrange1,
|
||||||
|
DarkOrange2,
|
||||||
|
DarkOrange3,
|
||||||
|
DarkOrange4,
|
||||||
|
OrangeRed1,
|
||||||
|
OrangeRed2,
|
||||||
|
OrangeRed3,
|
||||||
|
OrangeRed4,
|
||||||
|
DeepPink1,
|
||||||
|
DeepPink2,
|
||||||
|
DeepPink3,
|
||||||
|
DeepPink4,
|
||||||
|
HotPink1,
|
||||||
|
HotPink2,
|
||||||
|
HotPink3,
|
||||||
|
HotPink4,
|
||||||
|
LightPink1,
|
||||||
|
LightPink2,
|
||||||
|
LightPink3,
|
||||||
|
LightPink4,
|
||||||
|
PaleVioletRed1,
|
||||||
|
PaleVioletRed2,
|
||||||
|
PaleVioletRed3,
|
||||||
|
PaleVioletRed4,
|
||||||
|
VioletRed1,
|
||||||
|
VioletRed2,
|
||||||
|
VioletRed3,
|
||||||
|
VioletRed4,
|
||||||
|
MediumOrchid1,
|
||||||
|
MediumOrchid2,
|
||||||
|
MediumOrchid3,
|
||||||
|
MediumOrchid4,
|
||||||
|
DarkOrchid1,
|
||||||
|
DarkOrchid2,
|
||||||
|
DarkOrchid3,
|
||||||
|
DarkOrchid4,
|
||||||
|
MediumPurple1,
|
||||||
|
MediumPurple2,
|
||||||
|
MediumPurple3,
|
||||||
|
MediumPurple4,
|
||||||
|
DarkGrey,
|
||||||
|
DarkGray,
|
||||||
|
DarkBlue,
|
||||||
|
DarkCyan,
|
||||||
|
DarkMagenta,
|
||||||
|
DarkRed,
|
||||||
|
LightGreen,
|
||||||
|
RebeccaPurple,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for X11Color {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::iter()
|
||||||
|
.into_iter()
|
||||||
|
.find(|i| i.to_string() == s)
|
||||||
|
.ok_or(StringParseError::UnknownValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::{Color, X11Color};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ColorContainer {
|
||||||
|
color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn color_serialize_deserialize(color: Color) {
|
||||||
|
let original_color = color.clone();
|
||||||
|
let color_str =
|
||||||
|
toml::to_string_pretty(&ColorContainer { color }).expect("serialization failure");
|
||||||
|
let color_parsed: ColorContainer =
|
||||||
|
toml::from_str(color_str.as_str()).expect("deserialization failure");
|
||||||
|
assert_eq!(original_color, color_parsed.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn color_serialize_deserialize_rgb() {
|
||||||
|
color_serialize_deserialize(Color::RGB {
|
||||||
|
r: 128,
|
||||||
|
g: 128,
|
||||||
|
b: 128,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn color_serialize_deserialize_x11() {
|
||||||
|
color_serialize_deserialize(Color::X11(X11Color::HotPink2));
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,108 @@
|
||||||
|
use std::num::TryFromIntError;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
macro_rules! hex_digit {
|
||||||
|
($digit:expr) => {
|
||||||
|
match $digit {
|
||||||
|
'0' => Ok(0),
|
||||||
|
'1' => Ok(1),
|
||||||
|
'2' => Ok(2),
|
||||||
|
'3' => Ok(3),
|
||||||
|
'4' => Ok(4),
|
||||||
|
'5' => Ok(5),
|
||||||
|
'6' => Ok(6),
|
||||||
|
'7' => Ok(7),
|
||||||
|
'8' => Ok(8),
|
||||||
|
'9' => Ok(9),
|
||||||
|
'a' | 'A' => Ok(10),
|
||||||
|
'b' | 'B' => Ok(11),
|
||||||
|
'c' | 'C' => Ok(12),
|
||||||
|
'd' | 'D' => Ok(13),
|
||||||
|
'e' | 'E' => Ok(14),
|
||||||
|
'f' | 'F' => Ok(15),
|
||||||
|
_ => Err(HexError::InvalidCharacter($digit)),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error, Clone)]
|
||||||
|
pub enum HexError {
|
||||||
|
#[error("invalid/non-hex character: [{0}]")]
|
||||||
|
InvalidCharacter(char),
|
||||||
|
|
||||||
|
#[error("try_from_int failed: [{0}]")]
|
||||||
|
TryFromIntError(#[from] TryFromIntError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait FromHex: Sized {
|
||||||
|
fn from_hex(hex: &str) -> Result<Self, HexError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ParseHex<T> {
|
||||||
|
fn parse_hex(&self) -> Result<T, HexError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, V: AsRef<str>> ParseHex<T> for V
|
||||||
|
where
|
||||||
|
T: FromHex,
|
||||||
|
{
|
||||||
|
fn parse_hex(&self) -> Result<T, HexError> {
|
||||||
|
T::from_hex(self.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_from_hex {
|
||||||
|
($ty:ty) => {
|
||||||
|
impl FromHex for $ty {
|
||||||
|
fn from_hex(hex: &str) -> Result<Self, HexError> {
|
||||||
|
if let Some(without_prefix) = hex.strip_prefix("0x") {
|
||||||
|
return Self::from_hex(without_prefix);
|
||||||
|
}
|
||||||
|
Ok(hex
|
||||||
|
.chars()
|
||||||
|
.map(|c| hex_digit!(c))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
|
.into_iter()
|
||||||
|
.rev()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(idx, value)| -> Result<_, TryFromIntError> {
|
||||||
|
Ok((16 as $ty).pow(idx.try_into()?) * value)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
|
.into_iter()
|
||||||
|
.sum())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($($ty:ty),+) => {
|
||||||
|
$(
|
||||||
|
impl_from_hex!($ty);
|
||||||
|
)+
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_from_hex!(i8, i16, i32, i64, u8, u16, u32, u64);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::FromHex;
|
||||||
|
macro_rules! hex {
|
||||||
|
($val:literal) => {
|
||||||
|
(stringify!($val), $val as u32)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_hex_parse() {
|
||||||
|
for (str, expected) in [
|
||||||
|
hex!(0x15),
|
||||||
|
hex!(0xFFFF),
|
||||||
|
hex!(0xD3ADBEEF),
|
||||||
|
hex!(0x0),
|
||||||
|
hex!(0x10),
|
||||||
|
] {
|
||||||
|
let actual = u32::from_hex(str).expect("from_hex");
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::StringParseError;
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn from_hlwm_string(s: &str) -> Result<bool, StringParseError> {
|
||||||
|
match s {
|
||||||
|
"on" | "true" => Ok(true),
|
||||||
|
"off" | "false" => Ok(false),
|
||||||
|
_ => Err(StringParseError::BoolError(s.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum ToggleBool {
|
||||||
|
Bool(bool),
|
||||||
|
Toggle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bool> for ToggleBool {
|
||||||
|
fn from(value: bool) -> Self {
|
||||||
|
Self::Bool(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ToggleBool {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Ok(b) = from_hlwm_string(s) {
|
||||||
|
Ok(Self::Bool(b))
|
||||||
|
} else if s == "toggle" {
|
||||||
|
Ok(Self::Toggle)
|
||||||
|
} else {
|
||||||
|
Err(StringParseError::UnknownValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ToggleBool {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Toggle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ToggleBool {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
ToggleBool::Bool(b) => write!(f, "{b}"),
|
||||||
|
ToggleBool::Toggle => f.write_str("toggle"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
use crate::gen_parse;
|
||||||
|
|
||||||
|
use super::{command::CommandParseError, window::Window, Monitor, Tag, ToCommandString};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, strum::Display, strum::EnumIter, PartialEq)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum Hook {
|
||||||
|
/// The attribute `path` was changed from `old` to `new`.
|
||||||
|
/// Requires that the attribute `path` has been passed to the [HlwmCommand::Watch] command before.
|
||||||
|
AttributeChanged {
|
||||||
|
path: String,
|
||||||
|
old: String,
|
||||||
|
new: String,
|
||||||
|
},
|
||||||
|
/// The fullscreen state of `window` was changed to [on|off].
|
||||||
|
Fullscreen {
|
||||||
|
on: bool,
|
||||||
|
window: Window,
|
||||||
|
},
|
||||||
|
/// The `tag` was selected on `monitor`.
|
||||||
|
TagChanged {
|
||||||
|
tag: String,
|
||||||
|
monitor: Monitor,
|
||||||
|
},
|
||||||
|
/// The `window` with title `title` was focused
|
||||||
|
FocusChanged {
|
||||||
|
window: Window,
|
||||||
|
title: String,
|
||||||
|
},
|
||||||
|
WindowTitleChanged {
|
||||||
|
window: Window,
|
||||||
|
title: String,
|
||||||
|
},
|
||||||
|
/// The flags (i.e. urgent or filled state) have been changed.
|
||||||
|
TagFlags,
|
||||||
|
TagAdded(Tag),
|
||||||
|
TagRenamed {
|
||||||
|
old: String,
|
||||||
|
new: String,
|
||||||
|
},
|
||||||
|
Urgent {
|
||||||
|
on: bool,
|
||||||
|
window: Window,
|
||||||
|
},
|
||||||
|
/// A `window` appeared which triggered a rule/hook.
|
||||||
|
Rule {
|
||||||
|
hook: String,
|
||||||
|
window: Window,
|
||||||
|
},
|
||||||
|
/// Tells a panel to quit. The default panel.sh quits on this hook. Many scripts are using this hook.
|
||||||
|
QuitPanel,
|
||||||
|
/// Tells all daemons that the autostart file is reloaded — and tells them to quit.
|
||||||
|
/// This hook **should** be emitted in the first line of every autostart file.
|
||||||
|
Reload,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hook {
|
||||||
|
fn from_raw_parts(command: &str, args: Vec<String>) -> Result<Self, CommandParseError> {
|
||||||
|
let command = Self::iter()
|
||||||
|
.find(|cmd| cmd.to_string() == command)
|
||||||
|
.ok_or(CommandParseError::UnknownCommand(command.to_string()))?;
|
||||||
|
gen_parse!(command, args);
|
||||||
|
|
||||||
|
match command {
|
||||||
|
Hook::AttributeChanged {
|
||||||
|
path: _,
|
||||||
|
old: _,
|
||||||
|
new: _,
|
||||||
|
} => parse!(path: String, old: String, new: String => AttributeChanged),
|
||||||
|
Hook::Fullscreen { on: _, window: _ } => {
|
||||||
|
parse!(on: FromStr, window: FromStr => Fullscreen)
|
||||||
|
}
|
||||||
|
Hook::TagChanged { tag: _, monitor: _ } => {
|
||||||
|
parse!(tag: String, monitor: FromStr => TagChanged)
|
||||||
|
}
|
||||||
|
Hook::FocusChanged {
|
||||||
|
window: _,
|
||||||
|
title: _,
|
||||||
|
} => parse!(window: FromStr, title: String => FocusChanged),
|
||||||
|
Hook::WindowTitleChanged {
|
||||||
|
window: _,
|
||||||
|
title: _,
|
||||||
|
} => parse!(window: FromStr, title: String => WindowTitleChanged),
|
||||||
|
Hook::TagFlags => Ok(Hook::TagFlags),
|
||||||
|
Hook::TagAdded(_) => parse!(FromStr => TagAdded),
|
||||||
|
Hook::TagRenamed { old: _, new: _ } => parse!(old: String, new: String => TagRenamed),
|
||||||
|
Hook::Urgent { on: _, window: _ } => parse!(on: Bool, window: FromStr => Urgent),
|
||||||
|
Hook::Rule { hook: _, window: _ } => parse!(hook: String, window: FromStr => Rule),
|
||||||
|
Hook::QuitPanel => Ok(Hook::QuitPanel),
|
||||||
|
Hook::Reload => Ok(Hook::Reload),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Hook {
|
||||||
|
type Err = CommandParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut split = s.split("\t");
|
||||||
|
Hook::from_raw_parts(
|
||||||
|
split
|
||||||
|
.next()
|
||||||
|
.ok_or(CommandParseError::UnknownCommand(format!("hook {s}")))?,
|
||||||
|
split.map(String::from).collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Hook {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Reload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToCommandString for Hook {
|
||||||
|
fn to_command_string(&self) -> String {
|
||||||
|
[self.to_string()]
|
||||||
|
.into_iter()
|
||||||
|
.chain(match self {
|
||||||
|
Hook::AttributeChanged { path, old, new } => {
|
||||||
|
vec![path.to_string(), old.to_string(), new.to_string()].into_iter()
|
||||||
|
}
|
||||||
|
Hook::Fullscreen { on, window } => {
|
||||||
|
vec![on.to_string(), window.to_string()].into_iter()
|
||||||
|
}
|
||||||
|
Hook::TagChanged { tag, monitor } => {
|
||||||
|
vec![tag.to_string(), monitor.to_string()].into_iter()
|
||||||
|
}
|
||||||
|
Hook::WindowTitleChanged { window, title }
|
||||||
|
| Hook::FocusChanged { window, title } => {
|
||||||
|
vec![window.to_string(), title.to_string()].into_iter()
|
||||||
|
}
|
||||||
|
Hook::QuitPanel | Hook::Reload | Hook::TagFlags => vec![].into_iter(),
|
||||||
|
Hook::TagAdded(tag) => vec![tag.to_string()].into_iter(),
|
||||||
|
Hook::TagRenamed { old, new } => vec![old.to_string(), new.to_string()].into_iter(),
|
||||||
|
Hook::Urgent { on, window } => vec![on.to_string(), window.to_string()].into_iter(),
|
||||||
|
Hook::Rule { hook, window } => {
|
||||||
|
vec![hook.to_string(), window.to_string()].into_iter()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\t")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,497 @@
|
||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::hlwm::split;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
command::{CommandParseError, HlwmCommand},
|
||||||
|
StringParseError, ToCommandString,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, strum::EnumIter)]
|
||||||
|
pub enum Key {
|
||||||
|
Mod1Alt,
|
||||||
|
Mod4Super,
|
||||||
|
Return,
|
||||||
|
Shift,
|
||||||
|
Tab,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Space,
|
||||||
|
Control,
|
||||||
|
Backtick,
|
||||||
|
F1,
|
||||||
|
F2,
|
||||||
|
F3,
|
||||||
|
F4,
|
||||||
|
F5,
|
||||||
|
F6,
|
||||||
|
F7,
|
||||||
|
F8,
|
||||||
|
F9,
|
||||||
|
F10,
|
||||||
|
F11,
|
||||||
|
F12,
|
||||||
|
Home,
|
||||||
|
Delete,
|
||||||
|
Char(char),
|
||||||
|
Mouse(MouseButton),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Key {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Key {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct ExpectedKey;
|
||||||
|
impl serde::de::Expected for ExpectedKey {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("Expected a supported key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let str_val: String = Deserialize::deserialize(deserializer)?;
|
||||||
|
Ok(Self::from_str(&str_val).map_err(|_| {
|
||||||
|
serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &ExpectedKey)
|
||||||
|
})?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Key {
|
||||||
|
type Err = KeyParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"Button1" | "Button2" | "Button3" | "Button4" | "Button5" => {
|
||||||
|
Ok(Self::Mouse(MouseButton::from_str(s)?))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if let Some(key) = Self::iter().into_iter().find(|key| key.to_string() == s) {
|
||||||
|
match key {
|
||||||
|
Key::Char(_) | Key::Mouse(_) => (),
|
||||||
|
_ => return Ok(key),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.len() == 1 {
|
||||||
|
Ok(Self::Char(s.chars().next().unwrap()))
|
||||||
|
} else {
|
||||||
|
Err(KeyParseError::ExpectedCharKey(s.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum MouseButton {
|
||||||
|
Button1,
|
||||||
|
Button2,
|
||||||
|
Button3,
|
||||||
|
Button4,
|
||||||
|
Button5,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MouseButton {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Button1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for MouseButton {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"Button1" => Ok(Self::Button1),
|
||||||
|
"Button2" => Ok(Self::Button2),
|
||||||
|
"Button3" => Ok(Self::Button3),
|
||||||
|
"Button4" => Ok(Self::Button4),
|
||||||
|
"Button5" => Ok(Self::Button5),
|
||||||
|
_ => Err(StringParseError::UnknownValue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for MouseButton {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
MouseButton::Button1 => write!(f, "Button1"),
|
||||||
|
MouseButton::Button2 => write!(f, "Button2"),
|
||||||
|
MouseButton::Button3 => write!(f, "Button3"),
|
||||||
|
MouseButton::Button4 => write!(f, "Button4"),
|
||||||
|
MouseButton::Button5 => write!(f, "Button5"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Key {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let data = match self {
|
||||||
|
Key::Return => "Return".to_string(),
|
||||||
|
Key::Shift => "Shift".to_string(),
|
||||||
|
Key::Tab => "Tab".to_string(),
|
||||||
|
Key::Left => "Left".to_string(),
|
||||||
|
Key::Right => "Right".to_string(),
|
||||||
|
Key::Up => "Up".to_string(),
|
||||||
|
Key::Down => "Down".to_string(),
|
||||||
|
Key::Space => "space".to_string(),
|
||||||
|
Key::Control => "Control".to_string(),
|
||||||
|
Key::Backtick => "grave".to_string(),
|
||||||
|
Key::Char(c) => c.to_string(),
|
||||||
|
Key::Mouse(m) => m.to_string(),
|
||||||
|
Key::F1 => "F1".to_string(),
|
||||||
|
Key::F2 => "F2".to_string(),
|
||||||
|
Key::F3 => "F3".to_string(),
|
||||||
|
Key::F4 => "F4".to_string(),
|
||||||
|
Key::F5 => "F5".to_string(),
|
||||||
|
Key::F6 => "F6".to_string(),
|
||||||
|
Key::F7 => "F7".to_string(),
|
||||||
|
Key::F8 => "F8".to_string(),
|
||||||
|
Key::F9 => "F9".to_string(),
|
||||||
|
Key::F10 => "F10".to_string(),
|
||||||
|
Key::F11 => "F11".to_string(),
|
||||||
|
Key::F12 => "F12".to_string(),
|
||||||
|
Key::Home => "Home".to_string(),
|
||||||
|
Key::Delete => "Delete".to_string(),
|
||||||
|
Key::Mod1Alt => "Mod1".to_string(),
|
||||||
|
Key::Mod4Super => "Mod4".to_string(),
|
||||||
|
};
|
||||||
|
f.write_str(&data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, strum::Display, strum::EnumIter, PartialEq)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum MousebindAction {
|
||||||
|
Move,
|
||||||
|
Resize,
|
||||||
|
Zoom,
|
||||||
|
Call(Box<HlwmCommand>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for MousebindAction {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_command_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for MousebindAction {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct ExpectedKey;
|
||||||
|
impl serde::de::Expected for ExpectedKey {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("Expected a supported key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let str_val: String = Deserialize::deserialize(deserializer)?;
|
||||||
|
Ok(Self::from_str(&str_val).map_err(|_| {
|
||||||
|
serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &ExpectedKey)
|
||||||
|
})?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for MousebindAction {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut parts = split::tab_or_space(s).into_iter();
|
||||||
|
let first = parts.next().ok_or(StringParseError::UnknownValue)?;
|
||||||
|
let act = Self::iter()
|
||||||
|
.find(|i| i.to_string() == first)
|
||||||
|
.ok_or(StringParseError::UnknownValue)?;
|
||||||
|
|
||||||
|
match act {
|
||||||
|
MousebindAction::Move | MousebindAction::Resize | MousebindAction::Zoom => Ok(act),
|
||||||
|
MousebindAction::Call(_) => {
|
||||||
|
let command = parts.next().ok_or(StringParseError::UnknownValue)?;
|
||||||
|
let args = parts.collect();
|
||||||
|
Ok(MousebindAction::Call(Box::new(
|
||||||
|
HlwmCommand::from_raw_parts(&command, args)
|
||||||
|
.map_err(|err| StringParseError::CommandParseError(err.to_string()))?,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MousebindAction {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Move
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToCommandString for MousebindAction {
|
||||||
|
fn to_command_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
MousebindAction::Move | MousebindAction::Resize | MousebindAction::Zoom => {
|
||||||
|
self.to_string()
|
||||||
|
}
|
||||||
|
MousebindAction::Call(cmd) => format!("{self}\t{}", cmd.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum KeyParseError {
|
||||||
|
#[error("value too short (expected >= 2 parts, got {0} parts)")]
|
||||||
|
TooShort(usize),
|
||||||
|
#[error("no keys in keybind")]
|
||||||
|
NoKeys,
|
||||||
|
#[error("command parse error: {0}")]
|
||||||
|
CommandParseError(String),
|
||||||
|
#[error("expected char key, got: [{0}]")]
|
||||||
|
ExpectedCharKey(String),
|
||||||
|
#[error("string parse error: [{0}]")]
|
||||||
|
StringParseError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StringParseError> for KeyParseError {
|
||||||
|
fn from(value: StringParseError) -> Self {
|
||||||
|
KeyParseError::StringParseError(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CommandParseError> for KeyParseError {
|
||||||
|
fn from(value: CommandParseError) -> Self {
|
||||||
|
KeyParseError::CommandParseError(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct Keybind {
|
||||||
|
pub keys: Vec<Key>,
|
||||||
|
pub command: Box<HlwmCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Keybind {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
keys: Default::default(),
|
||||||
|
command: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Keybind {
|
||||||
|
type Err = KeyParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let parts = s.split('\t').collect::<Vec<&str>>();
|
||||||
|
if parts.len() < 2 {
|
||||||
|
return Err(KeyParseError::TooShort(s.len()));
|
||||||
|
}
|
||||||
|
let mut parts = parts.into_iter();
|
||||||
|
|
||||||
|
let keys = parts
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.split("+")
|
||||||
|
.map(|key| Key::from_str(key))
|
||||||
|
.collect::<Result<Vec<Key>, _>>()?;
|
||||||
|
|
||||||
|
let command = parts.next().unwrap();
|
||||||
|
let args: Vec<String> = parts.map(String::from).collect();
|
||||||
|
Ok(Self {
|
||||||
|
keys,
|
||||||
|
command: Box::new(HlwmCommand::from_raw_parts(command, args)?),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToCommandString for Keybind {
|
||||||
|
fn to_command_string(&self) -> String {
|
||||||
|
format!("{self}\t{}", self.command.to_command_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keybind {
|
||||||
|
pub fn new<I: IntoIterator<Item = Key>>(keys: I, command: HlwmCommand) -> Self {
|
||||||
|
Self {
|
||||||
|
keys: keys.into_iter().collect(),
|
||||||
|
command: Box::new(command),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Keybind {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(
|
||||||
|
&(&self.keys)
|
||||||
|
.into_iter()
|
||||||
|
.map(|key| key.to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("+"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct Mousebind {
|
||||||
|
pub keys: Vec<Key>,
|
||||||
|
pub action: MousebindAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Mousebind {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut parts = s.split("\t");
|
||||||
|
let keys = parts
|
||||||
|
.next()
|
||||||
|
.ok_or(StringParseError::UnknownValue)?
|
||||||
|
.split("-")
|
||||||
|
.map(|key| key.parse())
|
||||||
|
.collect::<Result<Vec<Key>, _>>()?;
|
||||||
|
let action = parts.next().ok_or(StringParseError::UnknownValue)?;
|
||||||
|
let mut action = MousebindAction::iter()
|
||||||
|
.into_iter()
|
||||||
|
.find(|act| act.to_string() == action)
|
||||||
|
.ok_or(StringParseError::UnknownValue)?;
|
||||||
|
if let MousebindAction::Call(_) = action {
|
||||||
|
let command = parts.next().ok_or(StringParseError::UnknownValue)?;
|
||||||
|
action = MousebindAction::Call(Box::new(
|
||||||
|
HlwmCommand::from_raw_parts(command, parts.map(String::from).collect())
|
||||||
|
.map_err(|_| StringParseError::UnknownValue)?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { keys, action })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mousebind {
|
||||||
|
pub fn new<I: Iterator<Item = Key>>(mod_key: Key, keys: I, action: MousebindAction) -> Self {
|
||||||
|
let keys = [mod_key].into_iter().chain(keys).collect();
|
||||||
|
Self { keys, action }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Mousebind {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
keys: Default::default(),
|
||||||
|
action: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Mousebind {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{keys}\t{action}",
|
||||||
|
keys = (&self.keys)
|
||||||
|
.into_iter()
|
||||||
|
.map(|key| key.to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("-"),
|
||||||
|
action = self.action.to_command_string(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum KeyUnbind {
|
||||||
|
Keybind(Vec<Key>),
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for KeyUnbind {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"--all" => Ok(Self::All),
|
||||||
|
_ => Ok(KeyUnbind::Keybind(
|
||||||
|
s.split("-")
|
||||||
|
.map(|key| key.parse())
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for KeyUnbind {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::All
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for KeyUnbind {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
KeyUnbind::Keybind(keys) => f.write_str(
|
||||||
|
&keys
|
||||||
|
.into_iter()
|
||||||
|
.map(|k| k.to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("-"),
|
||||||
|
),
|
||||||
|
KeyUnbind::All => f.write_str("--all"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
use crate::hlwm::command::HlwmCommand;
|
||||||
|
|
||||||
|
use super::{Key, MousebindAction};
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct KeyWrapper {
|
||||||
|
key: Key,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_serialize_deserialize() {
|
||||||
|
for (original_key, wrapper) in Key::iter().map(|key| (key, KeyWrapper { key })) {
|
||||||
|
let wrapper_str = toml::to_string_pretty(&wrapper).unwrap();
|
||||||
|
let parsed: KeyWrapper = toml::from_str(&wrapper_str).unwrap();
|
||||||
|
assert_eq!(original_key, parsed.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct MousebindActionWrapper {
|
||||||
|
action: MousebindAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mousebindaction_serialize_deserialize() {
|
||||||
|
for (original_key, wrapper) in [
|
||||||
|
MousebindAction::Call(Box::new(HlwmCommand::Cycle)),
|
||||||
|
MousebindAction::Move,
|
||||||
|
MousebindAction::Resize,
|
||||||
|
MousebindAction::Zoom,
|
||||||
|
]
|
||||||
|
.map(|action| (action.clone(), MousebindActionWrapper { action }))
|
||||||
|
{
|
||||||
|
let wrapper_str = toml::to_string_pretty(&wrapper).unwrap();
|
||||||
|
let parsed: MousebindActionWrapper = toml::from_str(&wrapper_str).unwrap();
|
||||||
|
assert_eq!(original_key, parsed.action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! gen_parse {
|
||||||
|
($command:ident, $args:ident) => {
|
||||||
|
macro_rules! parse {
|
||||||
|
(And_Or => $$val:tt) => {
|
||||||
|
{
|
||||||
|
let mut args = $args.into_iter();
|
||||||
|
let separator: Separator = args
|
||||||
|
.next()
|
||||||
|
.ok_or(CommandParseError::MissingArgument)?
|
||||||
|
.parse()?;
|
||||||
|
let sep_str = separator.to_string();
|
||||||
|
let args = args.collect::<Vec<_>>();
|
||||||
|
let commands = args
|
||||||
|
.split(|itm| itm.eq(&sep_str))
|
||||||
|
.map(|itm| match itm.len() {
|
||||||
|
0 => Err(CommandParseError::MissingArgument),
|
||||||
|
1 => HlwmCommand::from_str(itm.first().unwrap()),
|
||||||
|
_ => {
|
||||||
|
let mut args = itm.into_iter();
|
||||||
|
HlwmCommand::from_raw_parts(
|
||||||
|
&args.next().unwrap(),
|
||||||
|
args.map(|i| i.to_owned()).collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
Ok(Self::$$val{
|
||||||
|
separator,
|
||||||
|
commands,
|
||||||
|
})}
|
||||||
|
};
|
||||||
|
($$arg_type:tt => $$val:tt) => {
|
||||||
|
{
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut args_iter = $args.into_iter();
|
||||||
|
Ok(Self::$$val(parse!(Argument args_iter: $$arg_type)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($$($$arg:tt: $$arg_type:tt),+ => $val:tt) => {
|
||||||
|
{
|
||||||
|
#[allow(unused_mut)]
|
||||||
|
let mut args_iter = $args.into_iter();
|
||||||
|
Ok(Self::$val{
|
||||||
|
$$(
|
||||||
|
$$arg: parse!(Argument args_iter: $$arg_type)
|
||||||
|
),+
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(Argument $$args:ident: String) => {
|
||||||
|
$$args
|
||||||
|
.next()
|
||||||
|
.ok_or(CommandParseError::BadArgument {
|
||||||
|
command: $command.to_string(),
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
(Argument $$args:ident: FromStrAll) => {
|
||||||
|
$$args
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\t")
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| CommandParseError::BadArgument {
|
||||||
|
command: $command.to_string(),
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
(Argument $$args:ident: FromStr) => {
|
||||||
|
parse!(Argument $$args: String)
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| CommandParseError::BadArgument {
|
||||||
|
command: $command.to_string(),
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
(Argument $$args:ident: [Option<FromStr>]) => {
|
||||||
|
{
|
||||||
|
let args: Vec<_> = $$args.collect();
|
||||||
|
if args.len() == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let mut args = args.into_iter();
|
||||||
|
Some(parse!(Argument args: String)
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| CommandParseError::BadArgument {
|
||||||
|
command: $command.to_string(),
|
||||||
|
})?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(Argument $$args:ident: [Vec<String>]) => {
|
||||||
|
$$args.collect::<Vec<String>>()
|
||||||
|
};
|
||||||
|
(Argument $$args:ident: Bool) => {
|
||||||
|
match parse!(Argument $$args: String).as_str() {
|
||||||
|
"on" | "true" => true,
|
||||||
|
"off" | "false" => false,
|
||||||
|
_ => return Err(CommandParseError::BadArgument {
|
||||||
|
command: $command.to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(Argument $$args:ident: [Option<String>]) => {
|
||||||
|
{
|
||||||
|
let args = $$args.collect::<Vec<String>>();
|
||||||
|
if args.len() == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(args.join("\t"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(Argument $$args:ident: [Option<Vec<String>>]) => {
|
||||||
|
{
|
||||||
|
let args = $$args.collect::<Vec<String>>();
|
||||||
|
if args.len() == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(Argument $$args:ident: [Option<FromStr>]) => {
|
||||||
|
{
|
||||||
|
let args = $$args.map(|item| item.parse().map_err(|_| CommandParseError::BadArgument {
|
||||||
|
command: $command.to_string(),
|
||||||
|
})).collect::<Result<Vec<_>, _>>()?;
|
||||||
|
if args.len() == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(Argument $$args:ident: [Vec<FromStr>]) => {
|
||||||
|
{
|
||||||
|
$$args.map(|item| item.parse().map_err(|_| CommandParseError::BadArgument {
|
||||||
|
command: $command.to_string(),
|
||||||
|
})).collect::<Result<Vec<_>, _>>()?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,465 @@
|
||||||
|
use std::{
|
||||||
|
convert::Infallible,
|
||||||
|
fmt::Display,
|
||||||
|
num::{ParseFloatError, ParseIntError},
|
||||||
|
os::unix::process::ExitStatusExt,
|
||||||
|
process::{self, Child, Stdio},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use log::{debug, error};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use self::{
|
||||||
|
attribute::{Attribute, AttributeError},
|
||||||
|
command::{CommandError, HlwmCommand},
|
||||||
|
key::KeyParseError,
|
||||||
|
setting::{Setting, SettingName},
|
||||||
|
window::TagStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod attribute;
|
||||||
|
pub mod color;
|
||||||
|
pub mod command;
|
||||||
|
mod hex;
|
||||||
|
mod hlwmbool;
|
||||||
|
pub mod hook;
|
||||||
|
pub mod key;
|
||||||
|
mod octal;
|
||||||
|
pub mod rule;
|
||||||
|
pub mod setting;
|
||||||
|
mod split;
|
||||||
|
pub mod theme;
|
||||||
|
pub mod window;
|
||||||
|
#[macro_use]
|
||||||
|
mod macros;
|
||||||
|
|
||||||
|
pub use hlwmbool::ToggleBool;
|
||||||
|
|
||||||
|
pub type Monitor = u32;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Client;
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn herbstclient() -> std::process::Command {
|
||||||
|
std::process::Command::new("herbstclient")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the command and wait for it to finish.
|
||||||
|
///
|
||||||
|
/// To run the command and return a handle instead of waiting,
|
||||||
|
/// see [Client::spawn]
|
||||||
|
pub fn execute(&self, command: HlwmCommand) -> Result<process::Output, CommandError> {
|
||||||
|
let mut args = command.args();
|
||||||
|
debug!("running command: [{}]", (&args).join(" "),);
|
||||||
|
let output = Self::herbstclient()
|
||||||
|
.args(args)
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()?
|
||||||
|
.wait_with_output()?;
|
||||||
|
let exit_status = output.status;
|
||||||
|
if let Some(code) = exit_status.code() {
|
||||||
|
if code == 0 {
|
||||||
|
return Ok(output);
|
||||||
|
} else {
|
||||||
|
let output = String::from_utf8(output.stdout)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
error!("command failed with error code [{code}]");
|
||||||
|
if !output.is_empty() {
|
||||||
|
error!("command output: {output}");
|
||||||
|
}
|
||||||
|
return Err(CommandError::StatusCode(
|
||||||
|
code,
|
||||||
|
if output.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(output)
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(signal) = exit_status.signal() {
|
||||||
|
return Err(CommandError::KilledBySignal {
|
||||||
|
signal,
|
||||||
|
core_dumped: exit_status.core_dumped(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(signal) = exit_status.stopped_signal() {
|
||||||
|
return Err(CommandError::StoppedBySignal(signal));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(CommandError::OtherExitStatus(exit_status))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute_iter<I>(&self, commands: I) -> Result<(), (HlwmCommand, CommandError)>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = HlwmCommand>,
|
||||||
|
{
|
||||||
|
for cmd in commands.into_iter() {
|
||||||
|
if let Err(err) = self.execute(cmd.clone()) {
|
||||||
|
return Err((cmd, err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn(self, command: HlwmCommand) -> Result<Child, CommandError> {
|
||||||
|
Ok(Self::herbstclient().args(command.args()).spawn()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_setting(&self, setting: Setting) -> Result<(), CommandError> {
|
||||||
|
self.execute(HlwmCommand::Set(setting))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_attr_out(&self, attr: String, out: &mut Attribute) -> Result<(), CommandError> {
|
||||||
|
Ok(*out = Attribute::new(
|
||||||
|
&out.type_string(),
|
||||||
|
&self
|
||||||
|
.query(HlwmCommand::GetAttr(attr))?
|
||||||
|
.first()
|
||||||
|
.ok_or(CommandError::Empty)?,
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_attr(&self, attr: String) -> Result<Attribute, CommandError> {
|
||||||
|
let attr_type = self
|
||||||
|
.query(HlwmCommand::AttrType(attr.clone()))?
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.ok_or(CommandError::Empty)?;
|
||||||
|
let attr_val = self
|
||||||
|
.query(HlwmCommand::GetAttr(attr))?
|
||||||
|
.first()
|
||||||
|
.cloned()
|
||||||
|
.ok_or(CommandError::Empty)?;
|
||||||
|
Ok(Attribute::new(&attr_type, &attr_val)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_attr(&self, path: String, new_value: Attribute) -> Result<(), CommandError> {
|
||||||
|
self.execute(HlwmCommand::SetAttr { path, new_value })?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_setting(&self, setting: SettingName) -> Result<Setting, CommandError> {
|
||||||
|
Ok(Setting::from_str(&String::from_utf8(
|
||||||
|
self.execute(HlwmCommand::Get(setting))?.stdout,
|
||||||
|
)?)
|
||||||
|
.map_err(|_| StringParseError::UnknownValue)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn query(&self, command: HlwmCommand) -> Result<Vec<String>, CommandError> {
|
||||||
|
let lines = String::from_utf8(self.execute(command)?.stdout)?
|
||||||
|
.split("\n")
|
||||||
|
.map(|l| l.trim())
|
||||||
|
.filter(|l| !l.is_empty())
|
||||||
|
.map(|l| l.to_owned())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if lines.is_empty() {
|
||||||
|
return Err(CommandError::Empty);
|
||||||
|
}
|
||||||
|
Ok(lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tag_status(&self) -> Result<Vec<TagStatus>, CommandError> {
|
||||||
|
Ok(self
|
||||||
|
.query(HlwmCommand::TagStatus { monitor: None })?
|
||||||
|
.first()
|
||||||
|
.ok_or(CommandError::Empty)?
|
||||||
|
.split("\t")
|
||||||
|
.filter(|f| !f.is_empty())
|
||||||
|
.map(|i| i.parse())
|
||||||
|
.collect::<Result<Vec<_>, _>>()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ToCommandString {
|
||||||
|
fn to_command_string(&self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum StringParseError {
|
||||||
|
#[error("unknown value")]
|
||||||
|
UnknownValue,
|
||||||
|
#[error("failed parsing float: [{0}]")]
|
||||||
|
FloatError(#[from] ParseFloatError),
|
||||||
|
#[error("invalid bool value: [{0}]")]
|
||||||
|
BoolError(String),
|
||||||
|
#[error("failed parsing int: [{0}]")]
|
||||||
|
IntError(#[from] ParseIntError),
|
||||||
|
#[error("command parse error")]
|
||||||
|
CommandParseError(String),
|
||||||
|
#[error("invalid length for part [{1}]: [{0}]")]
|
||||||
|
InvalidLength(usize, &'static str),
|
||||||
|
#[error("required arguments missing: [{0}]")]
|
||||||
|
RequiredArgMissing(String),
|
||||||
|
#[error("attribute error: [{0}]")]
|
||||||
|
AttributeError(#[from] AttributeError),
|
||||||
|
#[error("key parse error: [{0}]")]
|
||||||
|
KeyParseError(#[from] KeyParseError),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum Tag {
|
||||||
|
Index(i32),
|
||||||
|
Name(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Tag {
|
||||||
|
type Err = Infallible;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Ok(val) = i32::from_str(s) {
|
||||||
|
Ok(Self::Index(val))
|
||||||
|
} else {
|
||||||
|
Ok(Self::Name(s.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Tag {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Index(Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Tag {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Tag::Index(i) => write!(f, "{i}"),
|
||||||
|
Tag::Name(name) => f.write_str(name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum Index<N>
|
||||||
|
where
|
||||||
|
N: PartialEq,
|
||||||
|
{
|
||||||
|
Relative(N),
|
||||||
|
Absolute(N),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N> FromStr for Index<N>
|
||||||
|
where
|
||||||
|
N: PartialEq + FromStr,
|
||||||
|
{
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut chars = s.chars();
|
||||||
|
let prefix = chars.next().ok_or(StringParseError::UnknownValue)?;
|
||||||
|
match prefix {
|
||||||
|
'+' => Ok(Self::Relative(
|
||||||
|
N::from_str(&chars.collect::<String>())
|
||||||
|
.map_err(|_| StringParseError::UnknownValue)?,
|
||||||
|
)),
|
||||||
|
'-' => Ok(Self::Relative(
|
||||||
|
N::from_str(s).map_err(|_| StringParseError::UnknownValue)?,
|
||||||
|
)),
|
||||||
|
_ => Ok(Self::Absolute(
|
||||||
|
N::from_str(s).map_err(|_| StringParseError::UnknownValue)?,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N> Default for Index<N>
|
||||||
|
where
|
||||||
|
N: PartialEq + Default,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Absolute(Default::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<N> Display for Index<N>
|
||||||
|
where
|
||||||
|
N: Display + PartialOrd + Default + PartialEq,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Index::Relative(rel) => {
|
||||||
|
if *rel > N::default() {
|
||||||
|
write!(f, "+{rel}")
|
||||||
|
} else {
|
||||||
|
write!(f, "{rel}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Index::Absolute(abs) => write!(f, "{abs}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, strum::EnumIter, PartialEq)]
|
||||||
|
pub enum Separator {
|
||||||
|
Comma,
|
||||||
|
Period,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Separator {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::iter()
|
||||||
|
.into_iter()
|
||||||
|
.find(|i| i.to_string() == s)
|
||||||
|
.ok_or(StringParseError::UnknownValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Separator {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Comma
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Separator {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Separator::Comma => f.write_str(","),
|
||||||
|
Separator::Period => f.write_str("."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, strum::EnumIter, PartialEq)]
|
||||||
|
pub enum Operator {
|
||||||
|
/// =
|
||||||
|
Equal,
|
||||||
|
/// !=
|
||||||
|
NotEqual,
|
||||||
|
/// <=
|
||||||
|
LessThanEqual,
|
||||||
|
/// <
|
||||||
|
LessThan,
|
||||||
|
/// \>=
|
||||||
|
GreaterThanEqual,
|
||||||
|
/// \>
|
||||||
|
GreaterThan,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Operator {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::iter()
|
||||||
|
.into_iter()
|
||||||
|
.find(|i| i.to_string() == s)
|
||||||
|
.ok_or(StringParseError::UnknownValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Operator {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Equal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Operator {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Operator::Equal => f.write_str("="),
|
||||||
|
Operator::NotEqual => f.write_str("!="),
|
||||||
|
Operator::LessThanEqual => f.write_str("<="),
|
||||||
|
Operator::LessThan => f.write_str("<"),
|
||||||
|
Operator::GreaterThanEqual => f.write_str(">="),
|
||||||
|
Operator::GreaterThan => f.write_str(">"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, strum::Display, strum::EnumIter, PartialEq)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum Direction {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Direction {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::iter()
|
||||||
|
.into_iter()
|
||||||
|
.find(|dir| dir.to_string() == s)
|
||||||
|
.ok_or(StringParseError::UnknownValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Direction {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Up
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, strum::Display, strum::EnumIter, PartialEq)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum Align {
|
||||||
|
Top(Option<f64>),
|
||||||
|
Left(Option<f64>),
|
||||||
|
Right(Option<f64>),
|
||||||
|
Bottom(Option<f64>),
|
||||||
|
Explode,
|
||||||
|
Auto,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Align {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut parts = s.split("\t");
|
||||||
|
let align = parts.next().ok_or(StringParseError::UnknownValue)?;
|
||||||
|
let fraction = match parts.next().map(|f| f64::from_str(f)) {
|
||||||
|
Some(val) => Some(val?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
match align {
|
||||||
|
"bottom" | "vertical" | "horiz" | "v" => Ok(Self::Bottom(fraction)),
|
||||||
|
"left" => Ok(Self::Left(fraction)),
|
||||||
|
"right" | "horizontal" | "horiz" | "h" => Ok(Self::Right(fraction)),
|
||||||
|
"top" => Ok(Self::Top(fraction)),
|
||||||
|
"explode" => Ok(Self::Explode),
|
||||||
|
"auto" => Ok(Self::Auto),
|
||||||
|
_ => Err(StringParseError::UnknownValue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Align {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Top(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToCommandString for Align {
|
||||||
|
fn to_command_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Align::Top(fraction)
|
||||||
|
| Align::Left(fraction)
|
||||||
|
| Align::Right(fraction)
|
||||||
|
| Align::Bottom(fraction) => match fraction {
|
||||||
|
Some(fraction) => vec![self.to_string(), fraction.to_string()],
|
||||||
|
None => vec![self.to_string()],
|
||||||
|
},
|
||||||
|
Align::Explode | Align::Auto => vec![self.to_string()],
|
||||||
|
}
|
||||||
|
.join("\t")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
use std::num::TryFromIntError;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
macro_rules! octal_digit {
|
||||||
|
($digit:expr) => {
|
||||||
|
match $digit {
|
||||||
|
'0' => Ok(0),
|
||||||
|
'1' => Ok(1),
|
||||||
|
'2' => Ok(2),
|
||||||
|
'3' => Ok(3),
|
||||||
|
'4' => Ok(4),
|
||||||
|
'5' => Ok(5),
|
||||||
|
'6' => Ok(6),
|
||||||
|
'7' => Ok(7),
|
||||||
|
_ => Err(OctalError::InvalidCharacter($digit)),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error, Clone)]
|
||||||
|
pub enum OctalError {
|
||||||
|
#[error("invalid/non-octal character: [{0}]")]
|
||||||
|
InvalidCharacter(char),
|
||||||
|
#[error("try_from_int failed: [{0}]")]
|
||||||
|
TryFromIntError(#[from] TryFromIntError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait FromOctal: Sized {
|
||||||
|
fn from_octal(octal: &str) -> Result<Self, OctalError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ParseOctal<T> {
|
||||||
|
fn parse_octal(&self) -> Result<T, OctalError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, V: AsRef<str>> ParseOctal<T> for V
|
||||||
|
where
|
||||||
|
T: FromOctal,
|
||||||
|
{
|
||||||
|
fn parse_octal(&self) -> Result<T, OctalError> {
|
||||||
|
T::from_octal(self.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_from_octal {
|
||||||
|
($ty:ty) => {
|
||||||
|
impl FromOctal for $ty {
|
||||||
|
fn from_octal(octal: &str) -> Result<Self, OctalError> {
|
||||||
|
if let Some(without_prefix) = octal.strip_prefix("0o") {
|
||||||
|
return Self::from_octal(without_prefix);
|
||||||
|
}
|
||||||
|
Ok(octal
|
||||||
|
.chars()
|
||||||
|
.map(|c| octal_digit!(c))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
|
.into_iter()
|
||||||
|
.rev()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(idx, value)| -> Result<_, TryFromIntError> {
|
||||||
|
Ok((8 as $ty).pow(idx.try_into()?) * value)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
|
.into_iter()
|
||||||
|
.sum())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
($($ty:ty),+) => {
|
||||||
|
$(
|
||||||
|
impl_from_octal!($ty);
|
||||||
|
)+
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_from_octal!(i8, i16, i32, i64, u8, u16, u32, u64);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::FromOctal;
|
||||||
|
macro_rules! octal {
|
||||||
|
($val:literal) => {
|
||||||
|
(stringify!($val), $val as u32)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_hex_parse() {
|
||||||
|
for (str, expected) in [
|
||||||
|
octal!(0o15),
|
||||||
|
octal!(0o777),
|
||||||
|
octal!(0o1337),
|
||||||
|
octal!(0o0),
|
||||||
|
octal!(0o10),
|
||||||
|
] {
|
||||||
|
let actual = u32::from_octal(str).expect("from_hex");
|
||||||
|
assert_eq!(expected, actual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,666 @@
|
||||||
|
use std::{
|
||||||
|
borrow::BorrowMut,
|
||||||
|
fmt::{Display, Write},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::{de::Expected, Deserialize, Serialize};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
use super::{hlwmbool, hook::Hook, split, StringParseError, ToCommandString};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Rule {
|
||||||
|
/// Rule labels default to an incremental index.
|
||||||
|
/// These default labels are unique, unless you assign a different
|
||||||
|
/// rule a custom integer LABEL. Default labels can be captured
|
||||||
|
/// with the printlabel flag.
|
||||||
|
label: Option<String>,
|
||||||
|
flag: Option<Flag>,
|
||||||
|
/// If each condition of this rule matches against this client,
|
||||||
|
/// then every [Consequence] is executed.
|
||||||
|
/// (If there are no conditions given, then this rule is executed for each client)
|
||||||
|
condition: Option<Condition>,
|
||||||
|
consequences: Vec<Consequence>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Rule {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
label: Default::default(),
|
||||||
|
flag: Default::default(),
|
||||||
|
condition: Some(Condition::FixedSize),
|
||||||
|
consequences: vec![Consequence::Focus(true)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToCommandString for Rule {
|
||||||
|
fn to_command_string(&self) -> String {
|
||||||
|
let mut args = Vec::with_capacity(4);
|
||||||
|
if let Some(label) = self.label.as_ref() {
|
||||||
|
args.push(format!("--label={}", label));
|
||||||
|
}
|
||||||
|
if let Some(flag) = self.flag.as_ref() {
|
||||||
|
args.push(format!("{flag}"));
|
||||||
|
}
|
||||||
|
if let Some(cond) = self.condition.as_ref() {
|
||||||
|
args.push(cond.to_command_string());
|
||||||
|
}
|
||||||
|
args.push(
|
||||||
|
(&self.consequences)
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| format!("--{}", c.to_command_string()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\t"),
|
||||||
|
);
|
||||||
|
|
||||||
|
args.join("\t")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rule {
|
||||||
|
pub fn new(
|
||||||
|
condition: Option<Condition>,
|
||||||
|
consequences: Vec<Consequence>,
|
||||||
|
label: Option<String>,
|
||||||
|
flag: Option<Flag>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
label,
|
||||||
|
flag,
|
||||||
|
condition,
|
||||||
|
consequences,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Rule {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let parts = split::tab_or_space(s);
|
||||||
|
if parts.is_empty() {
|
||||||
|
return Err(StringParseError::InvalidLength(0, "parse rule"));
|
||||||
|
}
|
||||||
|
let mut condition: Option<Condition> = None;
|
||||||
|
let mut consequences = vec![];
|
||||||
|
let mut label: Option<String> = None;
|
||||||
|
let mut flag: Option<Flag> = None;
|
||||||
|
|
||||||
|
for arg in (&parts)
|
||||||
|
.into_iter()
|
||||||
|
.map(|a| a.strip_prefix("--"))
|
||||||
|
.filter(|a| a.is_some())
|
||||||
|
{
|
||||||
|
let arg = arg.unwrap();
|
||||||
|
if condition.is_none() && arg == Condition::FixedSize.to_string() {
|
||||||
|
condition = Some(Condition::FixedSize);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let parts = arg.split(['=', '~']).collect::<Vec<_>>();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err(StringParseError::InvalidLength(parts.len(), "rule=parts"));
|
||||||
|
}
|
||||||
|
let mut parts = parts.into_iter();
|
||||||
|
let (name, value) = (parts.next().unwrap(), parts.next().unwrap());
|
||||||
|
|
||||||
|
if name == "label" && label.is_none() {
|
||||||
|
label = Some(value.to_string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if condition.is_none() && Condition::iter().any(|prop| prop.to_string() == name) {
|
||||||
|
condition = Some(match Condition::from_str(arg) {
|
||||||
|
Ok(arg) => arg,
|
||||||
|
Err(err) => panic!("<<condition>>\n\n{err}\n\n\n"),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if Consequence::iter().any(|cons| cons.to_string() == name) {
|
||||||
|
consequences.push(match Consequence::from_str(arg) {
|
||||||
|
Ok(arg) => arg,
|
||||||
|
Err(err) => panic!("<<consequence>>\n\n{err}\n\n\n"),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut args = parts.into_iter().filter(|a| !a.starts_with("--"));
|
||||||
|
while let Some(arg) = args.next() {
|
||||||
|
if flag.is_none() {
|
||||||
|
if let Ok(flag_res) = Flag::from_str(&arg) {
|
||||||
|
flag = Some(flag_res);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if condition.is_none() {
|
||||||
|
if let Ok(cond) = Condition::from_str(&arg) {
|
||||||
|
condition = Some(cond);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(cons) = Consequence::from_str(&arg) {
|
||||||
|
consequences.push(cons);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if consequences.is_empty() {
|
||||||
|
return Err(StringParseError::RequiredArgMissing(
|
||||||
|
"condition and/or consequences".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
label,
|
||||||
|
flag,
|
||||||
|
condition: condition,
|
||||||
|
consequences: consequences,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Rule {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_command_string().replace("\t", " "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Rule {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
pub struct Expect;
|
||||||
|
impl Expected for Expect {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(f, "a valid herbstluftwm rule")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let str_val: String = Deserialize::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
Ok(Self::from_str(&str_val).map_err(|_| {
|
||||||
|
serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &Expect)
|
||||||
|
})?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, strum::EnumIter)]
|
||||||
|
pub enum RuleOperator {
|
||||||
|
/// ~ matches if client’s property is matched by the regex value.
|
||||||
|
Regex,
|
||||||
|
/// = matches if client’s property string is equal to value.
|
||||||
|
Equal,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuleOperator {
|
||||||
|
pub const fn char(&self) -> char {
|
||||||
|
match self {
|
||||||
|
RuleOperator::Regex => '~',
|
||||||
|
RuleOperator::Equal => '=',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn match_set() -> [char; 2] {
|
||||||
|
[Self::Regex.char(), Self::Equal.char()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<char> for RuleOperator {
|
||||||
|
type Error = StringParseError;
|
||||||
|
|
||||||
|
fn try_from(value: char) -> Result<Self, Self::Error> {
|
||||||
|
Self::iter()
|
||||||
|
.find(|i| i.char() == value)
|
||||||
|
.ok_or(StringParseError::UnknownValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RuleOperator {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Equal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for RuleOperator {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_char(self.char())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for RuleOperator {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
s.chars()
|
||||||
|
.next()
|
||||||
|
.ok_or(StringParseError::InvalidLength(s.len(), "rule operator"))?
|
||||||
|
.try_into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, strum::Display, strum::EnumIter)]
|
||||||
|
#[strum(serialize_all = "lowercase")]
|
||||||
|
pub enum Condition {
|
||||||
|
/// the first entry in client’s WM_CLASS.
|
||||||
|
Instance {
|
||||||
|
operator: RuleOperator,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
/// the second entry in client’s WM_CLASS.
|
||||||
|
Class {
|
||||||
|
operator: RuleOperator,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
/// client’s window title.
|
||||||
|
Title {
|
||||||
|
operator: RuleOperator,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
/// the client’s process id (Warning: the pid is not available for every client.
|
||||||
|
/// This only matches if the client sets _NET_WM_PID to the pid itself).
|
||||||
|
Pid {
|
||||||
|
operator: RuleOperator,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
/// this client’s process group id. Since the pgid of a window is derived
|
||||||
|
/// from its pid the same restrictions apply as above.
|
||||||
|
Pgid {
|
||||||
|
operator: RuleOperator,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
/// matches if the age of the rule measured in seconds does not exceed value.
|
||||||
|
/// This condition only can be used with the = operator. If maxage already is
|
||||||
|
/// exceeded (and never will match again), then this rule is removed.
|
||||||
|
/// (With this you can build rules that only live for a certain time.)
|
||||||
|
MaxAge {
|
||||||
|
operator: RuleOperator,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
/// matches the _NET_WM_WINDOW_TYPE property of a window.
|
||||||
|
/// If _NET_WM_WINDOW_TYPE has multiple entries, then only the first entry is used here.
|
||||||
|
WindowType {
|
||||||
|
operator: RuleOperator,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
/// matches the WM_WINDOW_ROLE property of a window if it is set by the window.
|
||||||
|
WindowRole {
|
||||||
|
operator: RuleOperator,
|
||||||
|
value: String,
|
||||||
|
},
|
||||||
|
/// matches if the window does not allow being resized (i.e. if the minimum
|
||||||
|
/// size matches the maximum size). This condition does not take a parameter.
|
||||||
|
FixedSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToCommandString for Condition {
|
||||||
|
fn to_command_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Condition::Instance { operator, value }
|
||||||
|
| Condition::Class { operator, value }
|
||||||
|
| Condition::Title { operator, value }
|
||||||
|
| Condition::Pid { operator, value }
|
||||||
|
| Condition::Pgid { operator, value }
|
||||||
|
| Condition::MaxAge { operator, value }
|
||||||
|
| Condition::WindowType { operator, value }
|
||||||
|
| Condition::WindowRole { operator, value } => format!("--{self}{operator}{value}"),
|
||||||
|
// Note: There might be a bug where if you use --fixedsize
|
||||||
|
// herbstclient treats it as if fixedsize requires an argument.
|
||||||
|
//
|
||||||
|
// To deal with this on our end, we omit the -- prefix for fixedsize
|
||||||
|
Condition::FixedSize => self.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Condition {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
// Handle the case for fixedsize first so that we can treat the rest
|
||||||
|
// of the variants as having arguments
|
||||||
|
if s == Self::FixedSize.to_string() {
|
||||||
|
return Ok(Self::FixedSize);
|
||||||
|
}
|
||||||
|
let ((name, match_val), match_char) =
|
||||||
|
match split::on_first_match(s, &RuleOperator::match_set()) {
|
||||||
|
Some(parts) => parts,
|
||||||
|
None => return Err(StringParseError::InvalidLength(1, "property")),
|
||||||
|
};
|
||||||
|
let mut prop = Self::iter()
|
||||||
|
.find(|i| i.to_string() == name)
|
||||||
|
.ok_or(StringParseError::UnknownValue)?;
|
||||||
|
|
||||||
|
match prop.borrow_mut() {
|
||||||
|
Condition::Instance { operator, value }
|
||||||
|
| Condition::Class { operator, value }
|
||||||
|
| Condition::Title { operator, value }
|
||||||
|
| Condition::Pid { operator, value }
|
||||||
|
| Condition::Pgid { operator, value }
|
||||||
|
| Condition::MaxAge { operator, value }
|
||||||
|
| Condition::WindowType { operator, value }
|
||||||
|
| Condition::WindowRole { operator, value } => {
|
||||||
|
*operator = match_char.try_into()?;
|
||||||
|
*value = match_val;
|
||||||
|
}
|
||||||
|
// Should be handled at the top of the function. If it's here that's not a valid
|
||||||
|
// use of fixedsize.
|
||||||
|
Condition::FixedSize => return Err(StringParseError::UnknownValue),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(prop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, strum::Display, strum::EnumIter)]
|
||||||
|
#[strum(serialize_all = "lowercase")]
|
||||||
|
pub enum Consequence {
|
||||||
|
/// moves the client to tag value.
|
||||||
|
Tag(String),
|
||||||
|
/// moves the client to the tag on monitor VALUE.
|
||||||
|
/// If the tag consequence was also specified,
|
||||||
|
/// and switchtag is set for the client, move the client to that tag,
|
||||||
|
/// then display that tag on monitor VALUE. If the tag consequence was specified,
|
||||||
|
/// but switchtag was not, ignore this consequence.
|
||||||
|
Monitor(String),
|
||||||
|
/// decides whether the client gets the input focus in its tag. The default is off.
|
||||||
|
Focus(bool),
|
||||||
|
/// if focus is activated and the client is put to a not focused tag,
|
||||||
|
/// then switchtag tells whether the client’s tag will be shown or not.
|
||||||
|
/// If the tag is shown on any monitor but is not focused, the client’s tag
|
||||||
|
/// only is brought to the current monitor if swap_monitors_to_get_tag is activated.
|
||||||
|
SwitchTag(bool),
|
||||||
|
/// decides whether the client will be managed or not. The default is on.
|
||||||
|
Manage(bool),
|
||||||
|
/// moves the window to a specified index in the tree.
|
||||||
|
Index(i32),
|
||||||
|
/// sets the floating state of the client.
|
||||||
|
Floating(bool),
|
||||||
|
/// sets the pseudotile state of the client.
|
||||||
|
PseudoTile(bool),
|
||||||
|
/// sets the sticky attribute of the client.
|
||||||
|
Sticky(bool),
|
||||||
|
/// sets whether the window state (the fullscreen state and the demands attention flag)
|
||||||
|
/// can be changed by the application via ewmh itself. This does not affect the
|
||||||
|
/// initial fullscreen state requested by the window.
|
||||||
|
EwmhRequests(bool),
|
||||||
|
/// sets whether hlwm should let the client know about EMWH changes
|
||||||
|
/// (currently only the fullscreen state). If this is set, applications do
|
||||||
|
/// not change to their fullscreen-mode while still being fullscreen.
|
||||||
|
EwmhNotify(bool),
|
||||||
|
/// sets the fullscreen flag of the client.
|
||||||
|
Fullscreen(bool),
|
||||||
|
/// emits the custom hook rule VALUE WINID when this rule is triggered
|
||||||
|
/// by a new window with the id WINID. This consequence can be used multiple times,
|
||||||
|
/// which will cause a hook to be emitted for each occurrence of a hook consequence.
|
||||||
|
Hook(Hook),
|
||||||
|
/// sets the keymask for a client.
|
||||||
|
/// A regular expression that is matched against the string representation of
|
||||||
|
/// all key bindings (as they are printed by list_keybinds).
|
||||||
|
/// While this client is focused, only bindings that match the expression will be active.
|
||||||
|
/// Any other bindings will be disabled. The default keymask is an empty string (),
|
||||||
|
/// which does not disable any keybinding
|
||||||
|
KeyMask(String),
|
||||||
|
/// sets a regex that determines which key bindings are inactive
|
||||||
|
/// for a client.
|
||||||
|
/// A regular expression that describes which keybindings are inactive while
|
||||||
|
/// the client is focused. If a key combination is pressed and its string
|
||||||
|
/// representation (as given by list_keybinds) matches the regex,
|
||||||
|
/// then the key press is propagated to the client
|
||||||
|
KeysInactive(String),
|
||||||
|
/// changes the floating position of a window
|
||||||
|
FloatPlacement(FloatPlacement),
|
||||||
|
/// Sets the client’s floating_geometry attribute.
|
||||||
|
/// The VALUE is a rectangle, interpreted relatively to the monitor.
|
||||||
|
/// If floatplacement is also specified for the client (possibly by another rule),
|
||||||
|
/// then only the size of the floating_geometry is used.
|
||||||
|
/// In order to force the position from the geometry, it is necessary to add
|
||||||
|
/// floatplacement=none.
|
||||||
|
FloatingGeometry { x: u32, y: u32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToCommandString for Consequence {
|
||||||
|
fn to_command_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Consequence::Focus(value)
|
||||||
|
| Consequence::SwitchTag(value)
|
||||||
|
| Consequence::Manage(value)
|
||||||
|
| Consequence::Floating(value)
|
||||||
|
| Consequence::PseudoTile(value)
|
||||||
|
| Consequence::Sticky(value)
|
||||||
|
| Consequence::EwmhRequests(value)
|
||||||
|
| Consequence::EwmhNotify(value)
|
||||||
|
| Consequence::Fullscreen(value) => vec![
|
||||||
|
self.to_string(),
|
||||||
|
match value {
|
||||||
|
true => "on".into(),
|
||||||
|
false => "off".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Consequence::Tag(value)
|
||||||
|
| Consequence::Monitor(value)
|
||||||
|
| Consequence::KeyMask(value)
|
||||||
|
| Consequence::KeysInactive(value) => vec![self.to_string(), value.to_string()],
|
||||||
|
Consequence::Index(value) => vec![self.to_string(), value.to_string()],
|
||||||
|
Consequence::Hook(value) => vec![self.to_string(), value.to_string()],
|
||||||
|
Consequence::FloatPlacement(value) => vec![self.to_string(), value.to_string()],
|
||||||
|
Consequence::FloatingGeometry { x, y } => vec![self.to_string(), format!("{x}x{y}")],
|
||||||
|
}
|
||||||
|
.join("=")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Consequence {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let parts = split::tab_or_space(s);
|
||||||
|
let mut parts = parts.into_iter();
|
||||||
|
let name = parts.next().unwrap();
|
||||||
|
let (name, value_str) = if name.contains('=') {
|
||||||
|
let parts = name.split('=').collect::<Vec<_>>();
|
||||||
|
if parts.len() != 2 {
|
||||||
|
return Err(StringParseError::UnknownValue);
|
||||||
|
}
|
||||||
|
let mut parts = parts.into_iter();
|
||||||
|
(
|
||||||
|
parts.next().unwrap().to_string(),
|
||||||
|
parts.next().unwrap().to_string(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
match parts.next() {
|
||||||
|
Some(op) => {
|
||||||
|
if op != "=" {
|
||||||
|
return Err(StringParseError::UnknownValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => return Err(StringParseError::UnknownValue),
|
||||||
|
};
|
||||||
|
let value = parts.collect::<Vec<_>>().join("\t");
|
||||||
|
(name, value)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cons = Self::iter()
|
||||||
|
.find(|i| i.to_string() == name)
|
||||||
|
.ok_or(StringParseError::UnknownValue)?;
|
||||||
|
match cons.borrow_mut() {
|
||||||
|
Consequence::Focus(value)
|
||||||
|
| Consequence::SwitchTag(value)
|
||||||
|
| Consequence::Floating(value)
|
||||||
|
| Consequence::PseudoTile(value)
|
||||||
|
| Consequence::Sticky(value)
|
||||||
|
| Consequence::EwmhRequests(value)
|
||||||
|
| Consequence::EwmhNotify(value)
|
||||||
|
| Consequence::Fullscreen(value)
|
||||||
|
| Consequence::Manage(value) => {
|
||||||
|
*value = hlwmbool::from_hlwm_string(&value_str)?;
|
||||||
|
}
|
||||||
|
Consequence::Tag(value)
|
||||||
|
| Consequence::Monitor(value)
|
||||||
|
| Consequence::KeyMask(value)
|
||||||
|
| Consequence::KeysInactive(value) => *value = value_str,
|
||||||
|
Consequence::Index(value) => *value = i32::from_str(&value_str)?,
|
||||||
|
Consequence::Hook(value) => {
|
||||||
|
*value = Hook::from_str(&value_str).map_err(|_| StringParseError::UnknownValue)?
|
||||||
|
}
|
||||||
|
Consequence::FloatPlacement(value) => *value = FloatPlacement::from_str(&value_str)?,
|
||||||
|
Consequence::FloatingGeometry { x, y } => {
|
||||||
|
let mut values = value_str.split('=');
|
||||||
|
*x = u32::from_str(values.next().ok_or(StringParseError::UnknownValue)?)
|
||||||
|
.map_err(|_| StringParseError::UnknownValue)?;
|
||||||
|
*y = u32::from_str(values.next().ok_or(StringParseError::UnknownValue)?)
|
||||||
|
.map_err(|_| StringParseError::UnknownValue)?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(cons)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, strum::Display, strum::EnumIter)]
|
||||||
|
#[strum(serialize_all = "lowercase")]
|
||||||
|
pub enum FloatPlacement {
|
||||||
|
/// does not change the placement at all
|
||||||
|
None,
|
||||||
|
/// centers the window on the monitor
|
||||||
|
Center,
|
||||||
|
/// tries to place it with as little overlap to other floating windows as possible.
|
||||||
|
/// If there are multiple options with the least overlap, then the position with
|
||||||
|
/// the least overlap to tiling windows is chosen
|
||||||
|
Smart,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for FloatPlacement {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::iter()
|
||||||
|
.find(|i| i.to_string() == s)
|
||||||
|
.ok_or(StringParseError::UnknownValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FloatPlacement {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Smart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, strum::EnumIter)]
|
||||||
|
pub enum Flag {
|
||||||
|
/// negates the next CONDITION.
|
||||||
|
Not,
|
||||||
|
/// only apply this rule once (and delete it afterwards).
|
||||||
|
Once,
|
||||||
|
/// prints the label of the newly created rule to stdout.
|
||||||
|
PrintLabel,
|
||||||
|
/// prepend the rule to the list of rules instead of appending it.
|
||||||
|
/// So its consequences may be overwritten by already existing rules.
|
||||||
|
Prepend,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Flag {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Flag::Not => f.write_str("not"),
|
||||||
|
Flag::Once => f.write_str("once"),
|
||||||
|
Flag::PrintLabel => f.write_str("printlabel"),
|
||||||
|
Flag::Prepend => f.write_str("prepend"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Flag {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"!" | "not" => Ok(Self::Not),
|
||||||
|
"once" => Ok(Self::Once),
|
||||||
|
"printlabel" => Ok(Self::PrintLabel),
|
||||||
|
"prepend" => Ok(Self::Prepend),
|
||||||
|
_ => Err(StringParseError::UnknownValue),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! rule {
|
||||||
|
($consequence:expr) => {
|
||||||
|
crate::hlwm::rule::Rule::new(None, vec![$consequence], None, None)
|
||||||
|
};
|
||||||
|
($condition:expr, $consequence:expr) => {
|
||||||
|
crate::hlwm::rule::Rule::new(Some($condition), vec![$consequence], None, None)
|
||||||
|
};
|
||||||
|
($flag:tt: $condition:expr, $consequence:expr) => {
|
||||||
|
crate::hlwm::rule::Rule::new(
|
||||||
|
Some($condition),
|
||||||
|
vec![$consequence],
|
||||||
|
None,
|
||||||
|
Some(rule!(Flag $flag)),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
($label:literal => $consequence:expr) => {
|
||||||
|
crate::hlwm::rule::Rule::new(None, vec![$consequence], Some($label.into()), None)
|
||||||
|
};
|
||||||
|
|
||||||
|
($label:literal => $condition:expr, $consequence:expr) => {
|
||||||
|
crate::hlwm::rule::Rule::new(Some($condition), vec![$consequence], Some($label.into()), None)
|
||||||
|
};
|
||||||
|
|
||||||
|
($label:literal => $flag:tt: $condition:expr, $consequence:expr) => {
|
||||||
|
crate::hlwm::rule::Rule::new(Some($condition), vec![$consequence], Some($label.into()), Some(rule!(Flag $flag)))
|
||||||
|
};
|
||||||
|
|
||||||
|
(Flag Not) => {
|
||||||
|
crate::hlwm::rule::Flag::Not
|
||||||
|
};
|
||||||
|
(Flag Once) => {
|
||||||
|
crate::hlwm::rule::Flag::Once
|
||||||
|
};
|
||||||
|
(Flag PrintLabel) => {
|
||||||
|
crate::hlwm::rule::Flag::PrintLabel
|
||||||
|
};
|
||||||
|
(Flag Prepend) => {
|
||||||
|
crate::hlwm::rule::Flag::Prepend
|
||||||
|
};
|
||||||
|
(Flag $other:tt) => {
|
||||||
|
compile_error!("flag can be one of: Not, Once, PrintLabel, Prepend")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::{Condition, Consequence, Flag, Rule, RuleOperator};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct RuleWrapper {
|
||||||
|
rule: Rule,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rule_serialize_deserialize() {
|
||||||
|
for rule in [Rule::new(
|
||||||
|
Some(Condition::Class {
|
||||||
|
operator: RuleOperator::Equal,
|
||||||
|
value: "Netscape".into(),
|
||||||
|
}),
|
||||||
|
vec![Consequence::Tag(1.to_string())],
|
||||||
|
Some("label".into()),
|
||||||
|
Some(Flag::Not),
|
||||||
|
)] {
|
||||||
|
let serialized = toml::to_string_pretty(&RuleWrapper { rule: rule.clone() })
|
||||||
|
.expect("serializing rule");
|
||||||
|
let deserialized: RuleWrapper =
|
||||||
|
toml::from_str(&serialized).expect("deserializing rule");
|
||||||
|
assert_eq!(rule, deserialized.rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,481 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{gen_parse, hlwm::command::CommandParseError};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
color::Color,
|
||||||
|
hlwmbool::ToggleBool,
|
||||||
|
split::{self},
|
||||||
|
StringParseError, ToCommandString,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, strum::Display, strum::EnumIter, PartialEq, strum::EnumDiscriminants)]
|
||||||
|
#[strum_discriminants(
|
||||||
|
name(SettingName),
|
||||||
|
derive(strum::Display, strum::EnumIter),
|
||||||
|
strum(serialize_all = "snake_case")
|
||||||
|
)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum Setting {
|
||||||
|
/// If set, detect_monitors is automatically executed every time a monitor is connected, disconnected or resized.
|
||||||
|
AutoDetectMonitors(ToggleBool),
|
||||||
|
/// If set, EWMH panels are automatically detected and reserve space at the side of the monitors
|
||||||
|
/// they are on (via pad attributes of each monitor).
|
||||||
|
/// This setting is activated per default.
|
||||||
|
AutoDetectPanels(ToggleBool),
|
||||||
|
/// This setting controls the behaviour of focus and shift if no -e or -i argument is given.
|
||||||
|
/// If set, then focus and shift changes the focused frame even if there are other clients
|
||||||
|
/// in this frame in the specified direction.
|
||||||
|
/// Else, a client within current frame is selected if it is in the specified direction.
|
||||||
|
DefaultDirectionExternalOnly(ToggleBool),
|
||||||
|
/// Name of the layout algorithm, which is used if a new frame is created (on a new tag or by a non-trivial split).
|
||||||
|
DefaultFrameLayout(FrameLayout),
|
||||||
|
/// String to append when window or tab titles are shortened to fit in the available space
|
||||||
|
Ellipsis(String),
|
||||||
|
/// If set, commands [HlwmCommand::Focus] and [HlwmCommand::Shift] cross monitor boundaries.
|
||||||
|
/// If there is no client in the direction given to focus, then the monitor in the specified
|
||||||
|
/// direction is focused.
|
||||||
|
/// Similarly, if shift cannot move a window within a tag, the window is moved to the
|
||||||
|
/// neighbour monitor in the desired direction.
|
||||||
|
FocusCrossesMonitorBoundaries(ToggleBool),
|
||||||
|
/// If set and a window is focused by mouse cursor, this window is focused
|
||||||
|
/// (this feature is also known as sloppy focus).
|
||||||
|
/// If unset, you need to click to change the window focus by mouse.
|
||||||
|
///
|
||||||
|
/// If another window is hidden by the focus change
|
||||||
|
/// (e.g. when having pseudotiled windows in the max layout)
|
||||||
|
/// then an extra click is required to change the focus.
|
||||||
|
FocusFollowsMouse(ToggleBool),
|
||||||
|
/// If set, only pagers and taskbars are allowed to change the focus. If unset, all applications can request a
|
||||||
|
/// focus change
|
||||||
|
FocusStealingPrevention(ToggleBool),
|
||||||
|
/// Focused frame opacity in percent. Requires a running compositing manager to take actual effect.
|
||||||
|
FrameActiveOpacity(u8),
|
||||||
|
/// The fill color of a focused frame
|
||||||
|
FrameBgActiveColor(Color),
|
||||||
|
/// The fill color of an unfocused frame
|
||||||
|
/// It is only visible if non-focused frames are configured to be visible, see [ShowFrameDecoration].
|
||||||
|
FrameBgNormalColor(Color),
|
||||||
|
/// If set, the background of frames are transparent.
|
||||||
|
/// That means a rectangle is cut out from the inner such that only the frame border
|
||||||
|
/// and a stripe of width [Setting::FrameTransparentWidth] can be seen.
|
||||||
|
/// Use [Setting::FrameActiveOpacity] and [Setting::FrameNormalOpacity] for real transparency.
|
||||||
|
FrameBgTransparent(ToggleBool),
|
||||||
|
/// The border color of a focused frame
|
||||||
|
FrameBorderActiveColor(Color),
|
||||||
|
/// The color of the inner border of a frame
|
||||||
|
FrameBorderInnerColor(Color),
|
||||||
|
/// The width of the inner border of a frame.
|
||||||
|
/// Must be less than [Setting::FrameBorderWidth], since it does not add to the frame border width but is a
|
||||||
|
/// part of it.
|
||||||
|
FrameBorderInnerWidth(u32),
|
||||||
|
/// The border color of an unfocused frame
|
||||||
|
FrameBorderNormalColor(Color),
|
||||||
|
/// Border width of a frame
|
||||||
|
FrameBorderWidth(u32),
|
||||||
|
/// The gap between frames in the tiling mode
|
||||||
|
FrameGap(u32),
|
||||||
|
/// Unfocused frame opacity in percent.
|
||||||
|
/// Requires a running compositing manager to take actual effect
|
||||||
|
FrameNormalOpacity(u8),
|
||||||
|
/// The padding within a frame in the tiling mode
|
||||||
|
/// i.e. the space between the border of a frame and the windows within it
|
||||||
|
FramePadding(u32),
|
||||||
|
/// Specifies the width of the remaining frame colored with [Setting::FrameBgActiveColor]
|
||||||
|
/// if [Setting::FrameBgTransparent] is set
|
||||||
|
FrameTransparentWidth(u32),
|
||||||
|
/// This setting affects the size of the last client in a frame that is arranged by grid layout.
|
||||||
|
/// If set, then the last client always fills the gap within this frame.
|
||||||
|
/// If unset, then the last client has the same size as all other clients in this frame
|
||||||
|
GaplessGrid(ToggleBool),
|
||||||
|
/// If activated, windows are explicitly hidden when they are covered by another window in a frame with max layout.
|
||||||
|
/// This only has a visible effect if a compositor is used.
|
||||||
|
/// If activated, shadows do not stack up and transparent windows show the wallpaper behind them instead of
|
||||||
|
/// the other clients in the max layout.
|
||||||
|
HideCoveredWindows(ToggleBool),
|
||||||
|
/// If greater than 0, then the clients on all monitors aren’t moved or resized anymore.
|
||||||
|
/// If it is set to 0, then the arranging of monitors is enabled again, and all monitors are rearranged
|
||||||
|
/// if their content has changed in the meantime.
|
||||||
|
/// You should not change this setting manually due to concurrency issues;
|
||||||
|
/// use the commands [HlwmCommand::Lock] and [HlwmCommand::Unlock] instead.
|
||||||
|
MonitorsLocked(u8),
|
||||||
|
/// Specifies the gap around a monitor.
|
||||||
|
/// If the monitor is selected and the mouse position would be restored into this gap,
|
||||||
|
/// it is set to the center of the monitor.
|
||||||
|
/// This is useful, when the monitor was left via mouse movement, but is reselected by keyboard.
|
||||||
|
/// If the gap is 0 (default), the mouse is never recentered
|
||||||
|
MouseRecenterGap(u32),
|
||||||
|
/// If greater than 0, it specifies the least distance between a centered pseudotile window
|
||||||
|
/// and the border of the frame or tile it is assigned to.
|
||||||
|
/// If this distance is lower than pseudotile_center_threshold, it is aligned to the top left
|
||||||
|
/// of the client’s tile.
|
||||||
|
PseudotileCenterThreshold(u32),
|
||||||
|
/// If set, a window is raised if it is clicked. The value of this setting is only noticed in floating mode
|
||||||
|
RaiseOnClick(ToggleBool),
|
||||||
|
/// If set, a window is raised if it is focused. The value of this setting is only used in floating mode
|
||||||
|
RaiseOnFocus(ToggleBool),
|
||||||
|
/// If set, a window is raised temporarily if it is focused on its tag.
|
||||||
|
/// Temporarily in this case means that the window will return to its previous stacking position
|
||||||
|
/// if another window is focused
|
||||||
|
RaiseOnFocusTemporarily(ToggleBool),
|
||||||
|
/// This controls, which frame decorations are shown (or none at all)
|
||||||
|
ShowFrameDecorations(ShowFrameDecoration),
|
||||||
|
/// See the docs for members of [SmartFrameSurroundings]
|
||||||
|
SmartFrameSurroundings(SmartFrameSurroundings),
|
||||||
|
/// If set, window borders and gaps will be removed and minimal when there’s no
|
||||||
|
/// ambiguity regarding the focused window.
|
||||||
|
/// This minimal window decoration can be configured by the `theme.minimal` object.
|
||||||
|
SmartWindowSurroundings(ToggleBool),
|
||||||
|
/// If a client is dragged in floating mode, then it snaps to neighbour clients
|
||||||
|
/// if the distance between them is smaller than SnapDistance
|
||||||
|
SnapDistance(u32),
|
||||||
|
/// Specifies the remaining gap if a dragged client snaps to an edge in floating mode.
|
||||||
|
/// If SnapGap is set to 0, no gap will remain
|
||||||
|
SnapGap(u32),
|
||||||
|
/// If set: If you want to view a tag, that already is viewed on another monitor,
|
||||||
|
/// then the monitor contents will be swapped and you see the wanted tag on the focused monitor.
|
||||||
|
/// If not set, the other monitor is focused if it shows the desired tag.
|
||||||
|
SwapMonitorsToGetTag(ToggleBool),
|
||||||
|
/// If activated, multiple windows in a frame with the max layout algorithm are drawn as tabs
|
||||||
|
TabbedMax(ToggleBool),
|
||||||
|
/// It contains the chars that are used to print a nice ascii tree.
|
||||||
|
/// It must contain at least 8 characters. e.g. `X|:#+*-.` produces a tree like:
|
||||||
|
/// ```
|
||||||
|
/// X-.
|
||||||
|
/// #-. child 0
|
||||||
|
/// | #-* child 00
|
||||||
|
/// | +-* child 01
|
||||||
|
/// +-. child 1
|
||||||
|
/// : #-* child 10
|
||||||
|
/// : +-* child 11
|
||||||
|
/// ```
|
||||||
|
/// Useful values for tree_style are: `╾│ ├└╼─┐` or `-| |'--.` or `╾│ ├╰╼─╮.`
|
||||||
|
TreeStyle(String),
|
||||||
|
/// If set, a client's window content is resized immediately during resizing it with the mouse.
|
||||||
|
/// If unset, the client's content is resized after the mouse button is released.
|
||||||
|
UpdateDraggedClients(ToggleBool),
|
||||||
|
/// If set, verbose output is logged to herbstluftwm’s stderr.
|
||||||
|
/// The default value is controlled by the `--verbose` command line flag
|
||||||
|
Verbose(ToggleBool),
|
||||||
|
/// Border color of a focused window
|
||||||
|
/// **Warning:**
|
||||||
|
/// This only exists for compatibility reasons; it is only an alias for the attribute `theme.active.color`
|
||||||
|
WindowBorderActiveColor(Color),
|
||||||
|
/// Color of the inner border of a window.
|
||||||
|
/// **Warning:**
|
||||||
|
/// This only exists for compatibility reasons; it is only an alias for the attribute `theme.inner_color`
|
||||||
|
WindowBorderInnerColor(Color),
|
||||||
|
/// The width of the inner border of a window.
|
||||||
|
/// Must be less than window_border_width, since it does not add to the window border width but is a part of it.
|
||||||
|
/// **Warning:**
|
||||||
|
/// This only exists for compatibility reasons; it is only an alias for the attribute `theme.inner_width`
|
||||||
|
WindowBorderInnerWidth(u32),
|
||||||
|
/// Border color of an unfocused window
|
||||||
|
/// **Warning:**
|
||||||
|
/// This only exists for compatibility reasons; it is only an alias for the attribute `theme.normal.color`
|
||||||
|
WindowBorderNormalColor(Color),
|
||||||
|
/// Border color of an unfocused but urgent window.
|
||||||
|
/// **Warning:**
|
||||||
|
/// This only exists for compatibility reasons; it is only an alias for the attribute `theme.urgent.color`
|
||||||
|
WindowBorderUrgentColor(Color),
|
||||||
|
/// Border width of a window
|
||||||
|
/// **Warning:**
|
||||||
|
/// This only exists for compatibility reasons; it is only an alias for the attribute `theme.border_width`
|
||||||
|
WindowBorderWidth(u32),
|
||||||
|
/// The gap between windows within one frame in the tiling mode.
|
||||||
|
WindowGap(u32),
|
||||||
|
/// It controls the value of the `_NET_WM_NAME` property on the root window,
|
||||||
|
/// which specifies the name of the running window manager.
|
||||||
|
/// The value of this setting is not updated if the actual `_NET_WM_NAME` property
|
||||||
|
/// on the root window is changed externally. Example usage:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// cycle_value wmname herbstluftwm LG3D
|
||||||
|
/// ```
|
||||||
|
Wmname(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SettingName {
|
||||||
|
fn default() -> Self {
|
||||||
|
SettingName::AutoDetectMonitors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for SettingName {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::iter()
|
||||||
|
.find(|i| i.to_string() == s)
|
||||||
|
.ok_or(StringParseError::UnknownValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Setting {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_command_string().replace("\t", " = "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Setting {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let str_val: String = Deserialize::deserialize(deserializer)?;
|
||||||
|
let ((command, arg), _) = split::on_first_match(&str_val, &['='])
|
||||||
|
.ok_or(CommandParseError::InvalidArgumentCount(0, "setting".into()))
|
||||||
|
.map_err(|err| {
|
||||||
|
serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &err)
|
||||||
|
})?;
|
||||||
|
Ok(
|
||||||
|
Self::from_raw_parts(&command.trim(), &arg.trim()).map_err(|err| {
|
||||||
|
serde::de::Error::invalid_value(serde::de::Unexpected::Str(&str_val), &err)
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Setting {
|
||||||
|
type Err = CommandParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let ((command, arg), _) = split::on_first_match(s, &['\t', ' '])
|
||||||
|
.ok_or(CommandParseError::InvalidArgumentCount(0, "setting".into()))?;
|
||||||
|
|
||||||
|
Self::from_raw_parts(&command.trim(), &arg.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Setting {
|
||||||
|
pub fn from_raw_parts(command: &str, arg: &str) -> Result<Self, CommandParseError> {
|
||||||
|
let command = Self::iter()
|
||||||
|
.find(|cmd| cmd.to_string() == command)
|
||||||
|
.ok_or(CommandParseError::UnknownCommand(command.to_string()))?;
|
||||||
|
|
||||||
|
let args = [arg.to_string()];
|
||||||
|
gen_parse!(command, args);
|
||||||
|
|
||||||
|
match command {
|
||||||
|
Setting::Verbose(_) => parse!(FromStr => Verbose),
|
||||||
|
Setting::TabbedMax(_) => parse!(FromStr => TabbedMax),
|
||||||
|
Setting::GaplessGrid(_) => parse!(FromStr => GaplessGrid),
|
||||||
|
Setting::RaiseOnClick(_) => parse!(FromStr => RaiseOnClick),
|
||||||
|
Setting::RaiseOnFocus(_) => parse!(FromStr => RaiseOnFocus),
|
||||||
|
Setting::AutoDetectPanels(_) => parse!(FromStr => AutoDetectPanels),
|
||||||
|
Setting::FocusFollowsMouse(_) => parse!(FromStr => FocusFollowsMouse),
|
||||||
|
Setting::HideCoveredWindows(_) => parse!(FromStr => HideCoveredWindows),
|
||||||
|
Setting::AutoDetectMonitors(_) => parse!(FromStr => AutoDetectMonitors),
|
||||||
|
Setting::FrameBgTransparent(_) => parse!(FromStr => FrameBgTransparent),
|
||||||
|
Setting::SwapMonitorsToGetTag(_) => parse!(FromStr => SwapMonitorsToGetTag),
|
||||||
|
Setting::UpdateDraggedClients(_) => parse!(FromStr => UpdateDraggedClients),
|
||||||
|
Setting::FocusStealingPrevention(_) => parse!(FromStr => FocusStealingPrevention),
|
||||||
|
Setting::RaiseOnFocusTemporarily(_) => parse!(FromStr => RaiseOnFocusTemporarily),
|
||||||
|
Setting::SmartWindowSurroundings(_) => parse!(FromStr => SmartWindowSurroundings),
|
||||||
|
Setting::DefaultDirectionExternalOnly(_) => {
|
||||||
|
parse!(FromStr => DefaultDirectionExternalOnly)
|
||||||
|
}
|
||||||
|
Setting::FocusCrossesMonitorBoundaries(_) => {
|
||||||
|
parse!(FromStr => FocusCrossesMonitorBoundaries)
|
||||||
|
}
|
||||||
|
|
||||||
|
Setting::FrameBgActiveColor(_) => parse!(FromStr => FrameBgActiveColor),
|
||||||
|
Setting::FrameBgNormalColor(_) => parse!(FromStr => FrameBgNormalColor),
|
||||||
|
Setting::DefaultFrameLayout(_) => parse!(FromStr => DefaultFrameLayout),
|
||||||
|
Setting::ShowFrameDecorations(_) => parse!(FromStr => ShowFrameDecorations),
|
||||||
|
Setting::FrameBorderInnerColor(_) => parse!(FromStr => FrameBorderInnerColor),
|
||||||
|
Setting::FrameBorderActiveColor(_) => parse!(FromStr => FrameBorderActiveColor),
|
||||||
|
Setting::FrameBorderNormalColor(_) => parse!(FromStr => FrameBorderNormalColor),
|
||||||
|
Setting::SmartFrameSurroundings(_) => parse!(FromStr => SmartFrameSurroundings),
|
||||||
|
Setting::WindowBorderInnerColor(_) => parse!(FromStr => WindowBorderInnerColor),
|
||||||
|
Setting::WindowBorderActiveColor(_) => parse!(FromStr => WindowBorderActiveColor),
|
||||||
|
Setting::WindowBorderNormalColor(_) => parse!(FromStr => WindowBorderNormalColor),
|
||||||
|
Setting::WindowBorderUrgentColor(_) => parse!(FromStr => WindowBorderUrgentColor),
|
||||||
|
|
||||||
|
Setting::SnapGap(_) => parse!(FromStr => SnapGap),
|
||||||
|
Setting::FrameGap(_) => parse!(FromStr => FrameGap),
|
||||||
|
Setting::WindowGap(_) => parse!(FromStr => WindowGap),
|
||||||
|
Setting::SnapDistance(_) => parse!(FromStr => SnapDistance),
|
||||||
|
Setting::FramePadding(_) => parse!(FromStr => FramePadding),
|
||||||
|
Setting::MonitorsLocked(_) => parse!(FromStr => MonitorsLocked),
|
||||||
|
Setting::MouseRecenterGap(_) => parse!(FromStr => MouseRecenterGap),
|
||||||
|
Setting::FrameBorderWidth(_) => parse!(FromStr => FrameBorderWidth),
|
||||||
|
Setting::WindowBorderWidth(_) => parse!(FromStr => WindowBorderWidth),
|
||||||
|
Setting::FrameActiveOpacity(_) => parse!(FromStr => FrameActiveOpacity),
|
||||||
|
Setting::FrameNormalOpacity(_) => parse!(FromStr => FrameNormalOpacity),
|
||||||
|
Setting::FrameTransparentWidth(_) => parse!(FromStr => FrameTransparentWidth),
|
||||||
|
Setting::FrameBorderInnerWidth(_) => parse!(FromStr => FrameBorderInnerWidth),
|
||||||
|
Setting::WindowBorderInnerWidth(_) => parse!(FromStr => WindowBorderInnerWidth),
|
||||||
|
Setting::PseudotileCenterThreshold(_) => parse!(FromStr => PseudotileCenterThreshold),
|
||||||
|
|
||||||
|
Setting::Wmname(_) => parse!(String => Wmname),
|
||||||
|
Setting::Ellipsis(_) => parse!(String => Ellipsis),
|
||||||
|
Setting::TreeStyle(_) => parse!(String => TreeStyle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Setting {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::AutoDetectMonitors(ToggleBool::Bool(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToCommandString for Setting {
|
||||||
|
fn to_command_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Setting::AutoDetectMonitors(value)
|
||||||
|
| Setting::AutoDetectPanels(value)
|
||||||
|
| Setting::DefaultDirectionExternalOnly(value)
|
||||||
|
| Setting::FocusCrossesMonitorBoundaries(value)
|
||||||
|
| Setting::FocusFollowsMouse(value)
|
||||||
|
| Setting::FocusStealingPrevention(value)
|
||||||
|
| Setting::FrameBgTransparent(value)
|
||||||
|
| Setting::GaplessGrid(value)
|
||||||
|
| Setting::HideCoveredWindows(value)
|
||||||
|
| Setting::RaiseOnClick(value)
|
||||||
|
| Setting::RaiseOnFocus(value)
|
||||||
|
| Setting::RaiseOnFocusTemporarily(value)
|
||||||
|
| Setting::SmartWindowSurroundings(value)
|
||||||
|
| Setting::TabbedMax(value)
|
||||||
|
| Setting::UpdateDraggedClients(value)
|
||||||
|
| Setting::Verbose(value)
|
||||||
|
| Setting::SwapMonitorsToGetTag(value) => vec![self.to_string(), value.to_string()],
|
||||||
|
|
||||||
|
Setting::TreeStyle(value) | Setting::Wmname(value) | Setting::Ellipsis(value) => {
|
||||||
|
vec![self.to_string(), value.to_string()]
|
||||||
|
}
|
||||||
|
|
||||||
|
Setting::FrameActiveOpacity(value)
|
||||||
|
| Setting::FrameNormalOpacity(value)
|
||||||
|
| Setting::MonitorsLocked(value) => vec![self.to_string(), value.to_string()],
|
||||||
|
|
||||||
|
Setting::FrameBorderInnerWidth(value)
|
||||||
|
| Setting::FrameBorderWidth(value)
|
||||||
|
| Setting::FrameGap(value)
|
||||||
|
| Setting::FramePadding(value)
|
||||||
|
| Setting::FrameTransparentWidth(value)
|
||||||
|
| Setting::MouseRecenterGap(value)
|
||||||
|
| Setting::PseudotileCenterThreshold(value)
|
||||||
|
| Setting::SnapDistance(value)
|
||||||
|
| Setting::SnapGap(value)
|
||||||
|
| Setting::WindowBorderInnerWidth(value)
|
||||||
|
| Setting::WindowBorderWidth(value)
|
||||||
|
| Setting::WindowGap(value) => vec![self.to_string(), value.to_string()],
|
||||||
|
|
||||||
|
Setting::FrameBgActiveColor(value)
|
||||||
|
| Setting::FrameBgNormalColor(value)
|
||||||
|
| Setting::FrameBorderActiveColor(value)
|
||||||
|
| Setting::FrameBorderInnerColor(value)
|
||||||
|
| Setting::FrameBorderNormalColor(value)
|
||||||
|
| Setting::WindowBorderActiveColor(value)
|
||||||
|
| Setting::WindowBorderInnerColor(value)
|
||||||
|
| Setting::WindowBorderNormalColor(value)
|
||||||
|
| Setting::WindowBorderUrgentColor(value) => vec![self.to_string(), value.to_string()],
|
||||||
|
|
||||||
|
Setting::DefaultFrameLayout(value) => vec![self.to_string(), value.to_string()],
|
||||||
|
Setting::ShowFrameDecorations(value) => vec![self.to_string(), value.to_string()],
|
||||||
|
Setting::SmartFrameSurroundings(value) => vec![self.to_string(), value.to_string()],
|
||||||
|
}
|
||||||
|
.join("\t")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, strum::Display, strum::EnumString, PartialEq)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum ShowFrameDecoration {
|
||||||
|
/// Show no frame decorations at all
|
||||||
|
None,
|
||||||
|
/// Show decorations of frames that have client windows,
|
||||||
|
Nonempty,
|
||||||
|
/// Show decorations on the tags with at least two frames,
|
||||||
|
IfMultiple,
|
||||||
|
/// Show decorations of frames that have no client windows,
|
||||||
|
IfEmpty,
|
||||||
|
/// Show the decoration of focused and nonempty frames,
|
||||||
|
Focused,
|
||||||
|
/// Show decorations of focused and non-empty frames on tags with at least two frames.
|
||||||
|
FocusedIfMultiple,
|
||||||
|
/// Show all frame decorations.
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ShowFrameDecoration {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::FocusedIfMultiple
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, strum::Display, strum::EnumString, PartialEq)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum SmartFrameSurroundings {
|
||||||
|
/// Frame borders and gaps will be removed when there is no ambiguity regarding the focused frame
|
||||||
|
HideAll,
|
||||||
|
/// Only frame gaps will be removed when there is no ambiguity regarding the focused frame
|
||||||
|
HideGaps,
|
||||||
|
/// Always show frame borders and gaps
|
||||||
|
Off,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SmartFrameSurroundings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, strum::Display, strum::EnumString, PartialEq)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum FrameLayout {
|
||||||
|
/// clients are placed below each other
|
||||||
|
Vertical,
|
||||||
|
/// clients are placed next to each other
|
||||||
|
Horizontal,
|
||||||
|
/// all clients are maximized in this frame
|
||||||
|
Max,
|
||||||
|
/// clients are arranged in an almost quadratic grid
|
||||||
|
Grid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FrameLayout {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Vertical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::hlwm::{setting::SettingName, ToggleBool};
|
||||||
|
|
||||||
|
use super::Setting;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SettingWrapper {
|
||||||
|
setting: Setting,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn setting_serialize_deserialize() {
|
||||||
|
let setting = SettingWrapper {
|
||||||
|
setting: Setting::AutoDetectMonitors(ToggleBool::Toggle),
|
||||||
|
};
|
||||||
|
let serialized = toml::to_string_pretty(&setting).expect("serializing");
|
||||||
|
let deserialized: SettingWrapper = toml::from_str(&serialized).expect("deserializing");
|
||||||
|
assert_eq!(setting.setting, deserialized.setting);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn setting_name() {
|
||||||
|
assert_eq!(
|
||||||
|
"auto_detect_monitors",
|
||||||
|
SettingName::AutoDetectMonitors.to_string()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub enum SplitArg<'a> {
|
||||||
|
Normal(&'a str),
|
||||||
|
Once(&'a str),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SplitArg<'a> {
|
||||||
|
fn once(&self) -> bool {
|
||||||
|
if let Self::Once(_) = self {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Deref for SplitArg<'a> {
|
||||||
|
type Target = str;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
match self {
|
||||||
|
SplitArg::Normal(s) | SplitArg::Once(s) => s,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a str> for SplitArg<'a> {
|
||||||
|
fn from(value: &'a str) -> Self {
|
||||||
|
SplitArg::Normal(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a String> for SplitArg<'a> {
|
||||||
|
fn from(value: &'a String) -> Self {
|
||||||
|
SplitArg::Normal(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tab_or_space<'a, S>(s: S) -> Vec<String>
|
||||||
|
where
|
||||||
|
S: Into<SplitArg<'a>>,
|
||||||
|
{
|
||||||
|
let value = s.into();
|
||||||
|
let mut chars = value.chars();
|
||||||
|
let mut working: Vec<char> = Vec::with_capacity(value.len());
|
||||||
|
let mut out: Vec<String> = Vec::new();
|
||||||
|
let mut match_quote = false;
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
match c {
|
||||||
|
'\\' => {
|
||||||
|
working.push(c);
|
||||||
|
if let Some(c) = chars.next() {
|
||||||
|
working.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'"' => {
|
||||||
|
match_quote = !match_quote;
|
||||||
|
working.push(c);
|
||||||
|
}
|
||||||
|
'\t' | ' ' => {
|
||||||
|
if match_quote {
|
||||||
|
working.push(c);
|
||||||
|
} else if working.is_empty() {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
out.push((&working).into_iter().collect());
|
||||||
|
working.clear();
|
||||||
|
if value.once() {
|
||||||
|
out.push(chars.collect());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => working.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !working.is_empty() {
|
||||||
|
out.push(working.into_iter().collect());
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_first_match(s: &str, match_set: &[char]) -> Option<((String, String), char)> {
|
||||||
|
let mut first: Vec<char> = Vec::with_capacity(s.len());
|
||||||
|
let mut chars = s.chars();
|
||||||
|
while let Some(char) = chars.next() {
|
||||||
|
if (&match_set).into_iter().any(|m| m.eq(&char)) {
|
||||||
|
return Some(((first.into_iter().collect(), chars.collect()), char));
|
||||||
|
}
|
||||||
|
first.push(char);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
#[test]
|
||||||
|
fn test_tab_or_space_split() {
|
||||||
|
for (base, expected) in [
|
||||||
|
("hello world", vec!["hello", "world"]),
|
||||||
|
("hello\tworld", vec!["hello", "world"]),
|
||||||
|
(
|
||||||
|
r#"hello "world! I never made it!" up til now"#,
|
||||||
|
vec!["hello", "\"world! I never made it!\"", "up", "til", "now"],
|
||||||
|
),
|
||||||
|
("hello world", vec!["hello", "world"]),
|
||||||
|
] {
|
||||||
|
assert_eq!(expected, super::tab_or_space(base))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,238 @@
|
||||||
|
use std::{borrow::BorrowMut, num::ParseIntError};
|
||||||
|
|
||||||
|
use serde::{de::Expected, Deserialize, Serialize};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::hlwm::split;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
attribute::Attribute,
|
||||||
|
color::{self, Color},
|
||||||
|
StringParseError,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ThemeAttrParseError {
|
||||||
|
#[error("path not found")]
|
||||||
|
PathNotFound,
|
||||||
|
#[error("integer value parse error: [{0}]")]
|
||||||
|
ParseIntError(#[from] ParseIntError),
|
||||||
|
#[error("color value parse error: [{0}]")]
|
||||||
|
ColorParseError(#[from] color::ParseError),
|
||||||
|
#[error("string value parse error: [{0}]")]
|
||||||
|
StringParseError(#[from] StringParseError),
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! theme_attr {
|
||||||
|
($($name:tt($value:tt),)*) => {
|
||||||
|
#[derive(Debug, Clone, strum::Display, strum::EnumIter)]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum ThemeAttr {
|
||||||
|
$(
|
||||||
|
$name($value),
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for ThemeAttr {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
$(
|
||||||
|
(Self::$name(_), Self::$name(_)) => true,
|
||||||
|
)+
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ThemeAttr> for Attribute {
|
||||||
|
fn from(value: ThemeAttr) -> Self {
|
||||||
|
match value {
|
||||||
|
$(
|
||||||
|
ThemeAttr::$name(val) => theme_attr!(Attr $value)(val),
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ThemeAttr {
|
||||||
|
pub fn from_raw_parts(path: &str, value: &str) -> Result<Self, ThemeAttrParseError> {
|
||||||
|
match ThemeAttr::iter()
|
||||||
|
.find(|attr| attr.attr_path() == path)
|
||||||
|
.map(|attr| -> Result<Self, ThemeAttrParseError> {
|
||||||
|
let mut attr = attr.clone();
|
||||||
|
match attr.borrow_mut() {
|
||||||
|
$(
|
||||||
|
ThemeAttr::$name(val) => *val = theme_attr!(Parse value: $value),
|
||||||
|
)+
|
||||||
|
};
|
||||||
|
Ok(attr)
|
||||||
|
})
|
||||||
|
.map(|res| if let Err(err) = res {panic!("::: {err}")} else {res})
|
||||||
|
.ok_or(ThemeAttrParseError::PathNotFound)
|
||||||
|
{
|
||||||
|
Ok(res) => res,
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(Attr u32) => {
|
||||||
|
Attribute::Uint
|
||||||
|
};
|
||||||
|
(Attr String) => {
|
||||||
|
Attribute::String
|
||||||
|
};
|
||||||
|
(Attr bool) => {
|
||||||
|
Attribute::Bool
|
||||||
|
};
|
||||||
|
(Attr Color) => {
|
||||||
|
Attribute::Color
|
||||||
|
};
|
||||||
|
(Attr i32) => {
|
||||||
|
Attribute::Int
|
||||||
|
};
|
||||||
|
(Default u32) => {
|
||||||
|
0u32
|
||||||
|
};
|
||||||
|
(Default i32) => {
|
||||||
|
0i32
|
||||||
|
};
|
||||||
|
(Default Color) => {
|
||||||
|
Color::BLACK
|
||||||
|
};
|
||||||
|
(Default String) => {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
(Parse $v:tt: String) => {
|
||||||
|
$v.to_string()
|
||||||
|
};
|
||||||
|
(Parse $v:tt: $v_ty:tt) => {
|
||||||
|
$v.parse::<$v_ty>()?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
theme_attr!(
|
||||||
|
TitleHeight(u32),
|
||||||
|
TitleWhen(String),
|
||||||
|
TitleFont(String),
|
||||||
|
TitleDepth(i32),
|
||||||
|
InnerWidth(u32),
|
||||||
|
InnerColor(Color),
|
||||||
|
BorderWidth(u32),
|
||||||
|
FloatingBorderWidth(u32),
|
||||||
|
FloatingOuterWidth(u32),
|
||||||
|
TilingOuterWidth(u32),
|
||||||
|
BackgroundColor(Color),
|
||||||
|
ActiveColor(Color),
|
||||||
|
ActiveInnerColor(Color),
|
||||||
|
ActiveOuterColor(Color),
|
||||||
|
NormalColor(Color),
|
||||||
|
NormalInnerColor(Color),
|
||||||
|
NormalOuterColor(Color),
|
||||||
|
NormalTitleColor(Color),
|
||||||
|
UrgentColor(Color),
|
||||||
|
UrgentInnerColor(Color),
|
||||||
|
UrgentOuterColor(Color),
|
||||||
|
TitleColor(Color),
|
||||||
|
);
|
||||||
|
|
||||||
|
impl ThemeAttr {
|
||||||
|
pub fn attr_path(&self) -> String {
|
||||||
|
macro_rules! match_section {
|
||||||
|
($self:ident => $($section:literal),+) => {
|
||||||
|
{
|
||||||
|
let attr_str = $self.to_string();
|
||||||
|
let mut attr_parts = attr_str.split('_');
|
||||||
|
let first = attr_parts.next().unwrap();
|
||||||
|
let remainder = attr_parts.collect::<Vec<_>>().join("_");
|
||||||
|
match first {
|
||||||
|
$(
|
||||||
|
$section => ["theme", $section, &remainder].join("."),
|
||||||
|
)+
|
||||||
|
_ => ["theme", &attr_str].join("."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
match_section!(self => "floating", "tiling", "active", "normal", "urgent")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for ThemeAttr {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&format!(
|
||||||
|
"{} = {}",
|
||||||
|
self.attr_path(),
|
||||||
|
Attribute::from(self.clone())
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for ThemeAttr {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
pub enum Expect {
|
||||||
|
NoEquals,
|
||||||
|
ThemeAttrParseError(ThemeAttrParseError),
|
||||||
|
}
|
||||||
|
impl Expected for Expect {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Expect::NoEquals => write!(f, "value having an equals sign"),
|
||||||
|
Expect::ThemeAttrParseError(err) => write!(f, "parsing error: {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let str_val: String = Deserialize::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
let ((first, second), _) =
|
||||||
|
split::on_first_match(&str_val, &['=']).ok_or(serde::de::Error::invalid_value(
|
||||||
|
serde::de::Unexpected::Str(&str_val),
|
||||||
|
&Expect::NoEquals,
|
||||||
|
))?;
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
Self::from_raw_parts(&first.trim(), &second.trim()).map_err(|err| {
|
||||||
|
serde::de::Error::invalid_value(
|
||||||
|
serde::de::Unexpected::Str(&str_val),
|
||||||
|
&Expect::ThemeAttrParseError(err),
|
||||||
|
)
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::hlwm::theme::ThemeAttr;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_partial_eq_theme_attr_string() {
|
||||||
|
assert!(ThemeAttr::TitleFont(String::from("hello"))
|
||||||
|
.eq(&ThemeAttr::TitleFont(String::from("world"))))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ThemeAttrWrapper {
|
||||||
|
attr: ThemeAttr,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn theme_serialize_deserialize() {
|
||||||
|
let theme = ThemeAttrWrapper {
|
||||||
|
attr: ThemeAttr::TitleHeight(15),
|
||||||
|
};
|
||||||
|
let theme_str = toml::to_string_pretty(&theme).expect("serialization");
|
||||||
|
let theme_parsed: ThemeAttrWrapper = toml::from_str(&theme_str).expect("deserialization");
|
||||||
|
assert_eq!(theme.attr, theme_parsed.attr);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
use std::{
|
||||||
|
fmt::{Display, Write},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
rule::{Condition, RuleOperator},
|
||||||
|
StringParseError,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, strum::EnumIter, PartialEq)]
|
||||||
|
pub enum Window {
|
||||||
|
Urgent,
|
||||||
|
X11(i32),
|
||||||
|
/// references the minimized window on the focused tag
|
||||||
|
/// that has been minimized for the longest time
|
||||||
|
LongestMinimized,
|
||||||
|
/// references the minimized window on the focused tag
|
||||||
|
/// that has been minimized most recently
|
||||||
|
LastMinimized,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Window {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Ok(val) = i32::from_str(s) {
|
||||||
|
Ok(Self::X11(val))
|
||||||
|
} else {
|
||||||
|
Window::iter()
|
||||||
|
.find(|w| w.to_string() == s)
|
||||||
|
.ok_or(StringParseError::UnknownValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Window {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Urgent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i32> for Window {
|
||||||
|
fn from(value: i32) -> Self {
|
||||||
|
Self::X11(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Window {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Window::Urgent => f.write_str("urgent"),
|
||||||
|
Window::X11(id) => write!(f, "{id}"),
|
||||||
|
Window::LongestMinimized => f.write_str("longest-minimized"),
|
||||||
|
Window::LastMinimized => f.write_str("last-minimized"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TagStatus {
|
||||||
|
name: String,
|
||||||
|
state: TagState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TagStatus {
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn state(&self) -> TagState {
|
||||||
|
self.state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for TagStatus {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut parts = s.chars();
|
||||||
|
let state = parts
|
||||||
|
.next()
|
||||||
|
.ok_or(StringParseError::UnknownValue)?
|
||||||
|
.try_into()?;
|
||||||
|
let name = parts.collect();
|
||||||
|
|
||||||
|
Ok(Self { name, state })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, strum::EnumIter)]
|
||||||
|
pub enum TagState {
|
||||||
|
Empty,
|
||||||
|
NotEmpty,
|
||||||
|
/// The tag contains an urgent window
|
||||||
|
Urgent,
|
||||||
|
/// The tag is viewed on the specified MONITOR and it is focused
|
||||||
|
SameMonitorFocused,
|
||||||
|
/// The tag is viewed on the specified MONITOR, but this monitor is not focused
|
||||||
|
SameMonitor,
|
||||||
|
/// The tag is viewed on a different MONITOR and it is focused
|
||||||
|
DifferentMonitorFocused,
|
||||||
|
/// The tag is viewed on a different MONITOR, but this monitor is not focused
|
||||||
|
DifferentMonitor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<char> for TagState {
|
||||||
|
type Error = StringParseError;
|
||||||
|
|
||||||
|
fn try_from(value: char) -> Result<Self, Self::Error> {
|
||||||
|
Self::iter()
|
||||||
|
.into_iter()
|
||||||
|
.find(|i| char::from(i) == value)
|
||||||
|
.ok_or(StringParseError::UnknownValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for TagState {
|
||||||
|
type Err = StringParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Self::iter()
|
||||||
|
.into_iter()
|
||||||
|
.find(|i| i.to_string() == s)
|
||||||
|
.ok_or(StringParseError::UnknownValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&TagState> for char {
|
||||||
|
fn from(value: &TagState) -> Self {
|
||||||
|
match value {
|
||||||
|
TagState::SameMonitorFocused => '#',
|
||||||
|
TagState::SameMonitor => '+',
|
||||||
|
TagState::DifferentMonitorFocused => '%',
|
||||||
|
TagState::DifferentMonitor => '-',
|
||||||
|
TagState::Empty => '.',
|
||||||
|
TagState::NotEmpty => ':',
|
||||||
|
TagState::Urgent => '!',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for TagState {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_char(self.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, strum::Display)]
|
||||||
|
#[strum(serialize_all = "UPPERCASE")]
|
||||||
|
pub enum WindowType {
|
||||||
|
Dialog,
|
||||||
|
Utility,
|
||||||
|
Splash,
|
||||||
|
Notification,
|
||||||
|
Dock,
|
||||||
|
Desktop,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowType {
|
||||||
|
pub fn or<I: Iterator<Item = Self>>(types: I) -> Condition {
|
||||||
|
Condition::WindowType {
|
||||||
|
operator: RuleOperator::Regex,
|
||||||
|
value: format!(
|
||||||
|
"_NET_WM_WINDOW_TYPE_({})",
|
||||||
|
types.map(|t| t.to_string()).collect::<Vec<_>>().join("|")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! window_types {
|
||||||
|
($($ty:tt),+) => {
|
||||||
|
crate::hlwm::window::WindowType::or([$(
|
||||||
|
crate::hlwm::window::WindowType::$ty
|
||||||
|
),+].into_iter())
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
#![feature(macro_metavar_expr)]
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use config::Config;
|
||||||
|
use log::{error, info};
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod hlwm;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug, Clone)]
|
||||||
|
#[command(name = "hlctl")]
|
||||||
|
struct Args {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: HlctlCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug, Clone)]
|
||||||
|
enum HlctlCommand {
|
||||||
|
/// Initialize herbstluftwm, should be run from the autostart script
|
||||||
|
Init,
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
/// Draws notifications using dzen2
|
||||||
|
Notify {
|
||||||
|
/// Amount of seconds to persist for
|
||||||
|
#[arg(short = 's', long = None, default_value_t = 1)]
|
||||||
|
seconds: u8,
|
||||||
|
},
|
||||||
|
/// Save the currently loaded configuration to file
|
||||||
|
#[command(long_about = r#"
|
||||||
|
`save` Tries to find an existing config file. If not present, the default config is used.
|
||||||
|
Whichever one is loaded, its tags are used. All other values are collected from the environment
|
||||||
|
(or default values) and this new config is saved.
|
||||||
|
|
||||||
|
The configuration file located at $HOME/.config/herbstluftwm/hlctl.toml"#)]
|
||||||
|
Save,
|
||||||
|
PrintConfig,
|
||||||
|
Panel,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
pretty_env_logger::init();
|
||||||
|
|
||||||
|
let args = Args::parse();
|
||||||
|
match args.command {
|
||||||
|
HlctlCommand::Init => init(),
|
||||||
|
HlctlCommand::Notify { seconds } => println!("notify for {seconds}"),
|
||||||
|
HlctlCommand::Save => save(),
|
||||||
|
HlctlCommand::PrintConfig => print_config(),
|
||||||
|
HlctlCommand::Panel => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_or_default_config() -> Config {
|
||||||
|
Config::from_file(&Path::new(&Config::default_path().unwrap())).unwrap_or(Config::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn x_set_root(path: PathBuf) {
|
||||||
|
match std::process::Command::new(path)
|
||||||
|
.args(["-solid", "black"])
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
|
Ok(mut child) => {
|
||||||
|
if let Err(err) = child.wait() {
|
||||||
|
error!("running xsetroot: [{err}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => error!("running xsetroot: [{err}]"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init() {
|
||||||
|
info!("begining herbstluftwm setup via hlctl");
|
||||||
|
if let Ok(path) = which::which("xsetroot") {
|
||||||
|
x_set_root(path);
|
||||||
|
}
|
||||||
|
info!("loading config");
|
||||||
|
hlwm::Client::new()
|
||||||
|
.execute_iter(
|
||||||
|
load_or_default_config()
|
||||||
|
.to_command_set()
|
||||||
|
.expect("marshalling init command set"),
|
||||||
|
)
|
||||||
|
.expect("running init command set");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merged_config() -> Config {
|
||||||
|
let default = load_or_default_config();
|
||||||
|
let mut collected = Config::from_herbstluft();
|
||||||
|
collected.tags = default.tags;
|
||||||
|
|
||||||
|
collected
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_config() {
|
||||||
|
println!("{}", merged_config().serialize().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save() {
|
||||||
|
merged_config()
|
||||||
|
.write_to_file(&Path::new(&Config::default_path().unwrap()))
|
||||||
|
.expect("failed writing to file");
|
||||||
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
package svcctl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
"sectorinf.com/emilis/hlctl/config"
|
|
||||||
"sectorinf.com/emilis/hlctl/ctllog"
|
|
||||||
"sectorinf.com/emilis/hlctl/hlcl"
|
|
||||||
)
|
|
||||||
|
|
||||||
var log = ctllog.Logger{}.New("config", ctllog.ColorRGB{}.FromHex("2e8647"))
|
|
||||||
|
|
||||||
func RestartServices(cfg config.ServicesConfig) {
|
|
||||||
for _, svc := range cfg.Services {
|
|
||||||
log.Printf("Restarting [%s]", svc)
|
|
||||||
if err := exec.Command("pkill", "-9", svc).Run(); err != nil {
|
|
||||||
log.Errorf("Killing [%s]: %s", svc, err)
|
|
||||||
}
|
|
||||||
if err := hlcl.SilentSpawn(svc); err != nil {
|
|
||||||
log.Errorf("Starting [%s]: %s", svc, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue