initial stage of hlctl rust rewrite
This commit is contained in:
parent
e0526addc6
commit
d0c19f7947
|
@ -1,2 +1,4 @@
|
|||
# Openbsd core dumps
|
||||
*.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