From 31072bc645b01a3f497b1f17adc51f17349f6241 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 4 Apr 2026 00:18:22 +0200 Subject: [PATCH] refactor + feature flags --- Cargo.lock | 251 ++++----------------- Cargo.toml | 26 ++- src/config.rs | 163 ++++++++------ src/daemon.rs | 506 +++++++++++++----------------------------- src/health.rs | 135 +++++++++++ src/main.rs | 3 + src/modules/btrfs.rs | 10 +- src/modules/cpu.rs | 10 +- src/modules/disk.rs | 10 +- src/modules/dnd.rs | 11 +- src/modules/gpu.rs | 10 +- src/modules/memory.rs | 10 +- src/modules/mod.rs | 16 ++ src/registry.rs | 121 ++++++++++ src/signaler.rs | 144 ++++++++---- src/state.rs | 79 ++++++- src/utils.rs | 58 +++-- 17 files changed, 816 insertions(+), 747 deletions(-) create mode 100644 src/health.rs create mode 100644 src/registry.rs diff --git a/Cargo.lock b/Cargo.lock index dac669a..64995e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,17 +111,6 @@ dependencies = [ "slab", ] -[[package]] -name = "async-fs" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - [[package]] name = "async-io" version = "2.6.0" @@ -239,15 +228,6 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "block2" version = "0.6.2" @@ -378,31 +358,12 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "ctrlc" version = "3.5.2" @@ -504,16 +465,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - [[package]] name = "dispatch2" version = "0.3.1" @@ -628,6 +579,7 @@ dependencies = [ "clap", "ctrlc", "futures", + "libc", "libpulse-binding", "maestro", "nix 0.31.2", @@ -768,27 +720,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - [[package]] name = "getrandom" version = "0.4.2" @@ -1071,7 +1002,6 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", - "memoffset", ] [[package]] @@ -1304,15 +1234,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "prettyplease" version = "0.2.37" @@ -1407,36 +1328,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - [[package]] name = "regex" version = "1.12.3" @@ -1563,17 +1454,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -1615,12 +1495,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "strsim" version = "0.11.1" @@ -1692,7 +1566,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom", "once_cell", "rustix", "windows-sys 0.61.2", @@ -1791,7 +1665,7 @@ dependencies = [ "toml_datetime", "toml_parser", "toml_writer", - "winnow", + "winnow 1.0.1", ] [[package]] @@ -1805,14 +1679,14 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.8+spec-1.1.0" +version = "0.25.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" +checksum = "a82418ca169e235e6c399a84e395ab6debeb3bc90edc959bf0f48647c6a32d1b" dependencies = [ "indexmap", "toml_datetime", "toml_parser", - "winnow", + "winnow 1.0.1", ] [[package]] @@ -1821,7 +1695,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.1", ] [[package]] @@ -1891,12 +1765,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - [[package]] name = "uds_windows" version = "1.2.1" @@ -1932,8 +1800,9 @@ version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "getrandom 0.4.2", + "getrandom", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -1943,12 +1812,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "walkdir" version = "2.5.0" @@ -2194,15 +2057,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.59.0" @@ -2368,6 +2222,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + [[package]] name = "winnow" version = "1.0.1" @@ -2465,25 +2328,14 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "zbus" -version = "4.4.0" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" dependencies = [ "async-broadcast", "async-executor", - "async-fs", "async-io", "async-lock", "async-process", @@ -2494,20 +2346,18 @@ dependencies = [ "enumflags2", "event-listener", "futures-core", - "futures-sink", - "futures-util", + "futures-lite", "hex", - "nix 0.29.0", + "libc", "ordered-stream", - "rand", + "rustix", "serde", "serde_repr", - "sha1", - "static_assertions", "tracing", "uds_windows", - "windows-sys 0.52.0", - "xdg-home", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.15", "zbus_macros", "zbus_names", "zvariant", @@ -2515,48 +2365,30 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "4.4.0" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", + "zbus_names", + "zvariant", "zvariant_utils", ] [[package]] name = "zbus_names" -version = "3.0.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "static_assertions", + "winnow 0.7.15", "zvariant", ] -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zmij" version = "1.0.21" @@ -2565,22 +2397,23 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zvariant" -version = "4.2.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" dependencies = [ "endi", "enumflags2", "serde", - "static_assertions", + "winnow 0.7.15", "zvariant_derive", + "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "4.2.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2591,11 +2424,13 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "2.1.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", + "serde", "syn", + "winnow 0.7.15", ] diff --git a/Cargo.toml b/Cargo.toml index b2b78fa..ad9afb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,19 @@ name = "fluxo-rs" version = "0.4.0" edition = "2024" +[features] +default = ["mod-audio", "mod-bt", "mod-network", "mod-hardware", "mod-dbus"] +mod-audio = ["dep:libpulse-binding"] +mod-bt = ["dep:bluer", "dep:maestro"] +mod-network = ["dep:nix"] +mod-hardware = [] +mod-dbus = ["dep:zbus", "dep:futures"] + [dependencies] anyhow = "1.0.102" clap = { version = "4.6.0", features = ["derive"] } ctrlc = "3" +libc = "0.2" regex = "1.12" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" @@ -15,16 +24,17 @@ thiserror = "2.0" toml = "1.1.2" tracing = "0.1.44" tracing-subscriber = { version = "0.3.23", features = ["env-filter"] } -maestro = { git = "https://github.com/qzed/pbpctrl", package = "maestro" } -bluer = { version = "0.17", features = ["bluetoothd", "rfcomm", "id"] } -tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync", "time", "macros", "signal", "process"] } +tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync", "time", "macros", "signal", "process", "io-util", "net"] } tokio-util = { version = "0.7", features = ["codec", "time"] } -futures = "0.3" -libpulse-binding = "2.30" -nix = { version = "0.31", features = ["net"] } notify = "8.2.0" -zbus = "4" + +# Optional module dependencies +maestro = { git = "https://github.com/qzed/pbpctrl", package = "maestro", optional = true } +bluer = { version = "0.17", features = ["bluetoothd", "rfcomm", "id"], optional = true } +futures = { version = "0.3", optional = true } +libpulse-binding = { version = "2.30", optional = true } +nix = { version = "0.31", features = ["net"], optional = true } +zbus = { version = "5", optional = true } [dev-dependencies] - tempfile = "3" diff --git a/src/config.rs b/src/config.rs index d1289a9..2d38aab 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,6 @@ -use regex::Regex; use serde::Deserialize; use std::fs; use std::path::PathBuf; -use std::sync::LazyLock; use tracing::{debug, info, warn}; #[derive(Deserialize, Default, Clone)] @@ -11,34 +9,49 @@ pub struct Config { pub general: GeneralConfig, #[serde(default)] pub signals: SignalsConfig, + #[cfg(feature = "mod-network")] #[serde(default)] pub network: NetworkConfig, + #[cfg(feature = "mod-hardware")] #[serde(default)] pub cpu: CpuConfig, + #[cfg(feature = "mod-hardware")] #[serde(default)] pub memory: MemoryConfig, + #[cfg(feature = "mod-hardware")] #[serde(default)] pub gpu: GpuConfig, + #[cfg(feature = "mod-hardware")] #[serde(default)] pub sys: SysConfig, + #[cfg(feature = "mod-hardware")] #[serde(default)] pub disk: DiskConfig, + #[cfg(feature = "mod-hardware")] #[serde(default)] pub pool: PoolConfig, + #[cfg(feature = "mod-hardware")] #[serde(default)] pub power: PowerConfig, + #[cfg(feature = "mod-audio")] #[serde(default)] pub audio: AudioConfig, + #[cfg(feature = "mod-bt")] #[serde(default)] pub bt: BtConfig, + #[cfg(feature = "mod-hardware")] #[serde(default)] pub game: GameConfig, + #[cfg(feature = "mod-dbus")] #[serde(default)] pub mpris: MprisConfig, + #[cfg(feature = "mod-dbus")] #[serde(default)] pub backlight: BacklightConfig, + #[cfg(feature = "mod-dbus")] #[serde(default)] pub keyboard: KeyboardConfig, + #[cfg(feature = "mod-dbus")] #[serde(default)] pub dnd: DndConfig, } @@ -293,11 +306,8 @@ impl Default for DndConfig { } } -static TOKEN_RE: LazyLock = - LazyLock::new(|| Regex::new(r"\{([a-zA-Z0-9_]+)(?::([<>\^])?(\d+)?(?:\.(\d+))?)?\}").unwrap()); - fn extract_tokens(format_str: &str) -> Vec { - TOKEN_RE + crate::utils::TOKEN_RE .captures_iter(format_str) .map(|cap| cap[1].to_string()) .collect() @@ -316,78 +326,93 @@ fn validate_format(label: &str, format_str: &str, known_tokens: &[&str]) { impl Config { pub fn validate(&self) { + #[cfg(feature = "mod-network")] validate_format( "network", &self.network.format, &["interface", "ip", "rx", "tx"], ); - validate_format("cpu", &self.cpu.format, &["usage", "temp"]); - validate_format("memory", &self.memory.format, &["used", "total"]); - validate_format( - "gpu.amd", - &self.gpu.format_amd, - &["usage", "vram_used", "vram_total", "temp"], - ); - validate_format("gpu.intel", &self.gpu.format_intel, &["usage", "freq"]); - validate_format( - "gpu.nvidia", - &self.gpu.format_nvidia, - &["usage", "vram_used", "vram_total", "temp"], - ); - validate_format( - "sys", - &self.sys.format, - &["uptime", "load1", "load5", "load15", "procs"], - ); - validate_format("disk", &self.disk.format, &["mount", "used", "total"]); - validate_format("pool", &self.pool.format, &["used", "total"]); - validate_format("power", &self.power.format, &["percentage", "icon"]); - validate_format( - "audio.sink_unmuted", - &self.audio.format_sink_unmuted, - &["name", "icon", "volume"], - ); - validate_format( - "audio.sink_muted", - &self.audio.format_sink_muted, - &["name", "icon"], - ); - validate_format( - "audio.source_unmuted", - &self.audio.format_source_unmuted, - &["name", "icon", "volume"], - ); - validate_format( - "audio.source_muted", - &self.audio.format_source_muted, - &["name", "icon"], - ); - validate_format("bt.connected", &self.bt.format_connected, &["alias"]); - validate_format( - "bt.plugin", - &self.bt.format_plugin, - &["alias", "left", "right", "anc", "mac"], - ); - validate_format( - "mpris", - &self.mpris.format, - &["artist", "title", "album", "status_icon"], - ); - validate_format("backlight", &self.backlight.format, &["percentage", "icon"]); - validate_format("keyboard", &self.keyboard.format, &["layout"]); + #[cfg(feature = "mod-hardware")] + { + validate_format("cpu", &self.cpu.format, &["usage", "temp"]); + validate_format("memory", &self.memory.format, &["used", "total"]); + validate_format( + "gpu.amd", + &self.gpu.format_amd, + &["usage", "vram_used", "vram_total", "temp"], + ); + validate_format("gpu.intel", &self.gpu.format_intel, &["usage", "freq"]); + validate_format( + "gpu.nvidia", + &self.gpu.format_nvidia, + &["usage", "vram_used", "vram_total", "temp"], + ); + validate_format( + "sys", + &self.sys.format, + &["uptime", "load1", "load5", "load15", "procs"], + ); + validate_format("disk", &self.disk.format, &["mount", "used", "total"]); + validate_format("pool", &self.pool.format, &["used", "total"]); + validate_format("power", &self.power.format, &["percentage", "icon"]); + } + #[cfg(feature = "mod-audio")] + { + validate_format( + "audio.sink_unmuted", + &self.audio.format_sink_unmuted, + &["name", "icon", "volume"], + ); + validate_format( + "audio.sink_muted", + &self.audio.format_sink_muted, + &["name", "icon"], + ); + validate_format( + "audio.source_unmuted", + &self.audio.format_source_unmuted, + &["name", "icon", "volume"], + ); + validate_format( + "audio.source_muted", + &self.audio.format_source_muted, + &["name", "icon"], + ); + } + #[cfg(feature = "mod-bt")] + { + validate_format("bt.connected", &self.bt.format_connected, &["alias"]); + validate_format( + "bt.plugin", + &self.bt.format_plugin, + &["alias", "left", "right", "anc", "mac"], + ); + } + #[cfg(feature = "mod-dbus")] + { + validate_format( + "mpris", + &self.mpris.format, + &["artist", "title", "album", "status_icon"], + ); + validate_format("backlight", &self.backlight.format, &["percentage", "icon"]); + validate_format("keyboard", &self.keyboard.format, &["layout"]); + } } } +pub fn default_config_path() -> PathBuf { + let config_dir = std::env::var("XDG_CONFIG_HOME") + .map(PathBuf::from) + .unwrap_or_else(|_| { + let home = std::env::var("HOME").unwrap_or_else(|_| String::from("/")); + PathBuf::from(home).join(".config") + }); + config_dir.join("fluxo/config.toml") +} + pub fn load_config(custom_path: Option) -> Config { - let config_path = custom_path.unwrap_or_else(|| { - let config_dir = std::env::var("XDG_CONFIG_HOME") - .map(PathBuf::from) - .unwrap_or_else(|_| { - let home = std::env::var("HOME").unwrap_or_else(|_| String::from("/")); - PathBuf::from(home).join(".config") - }); - config_dir.join("fluxo/config.toml") - }); + let config_path = custom_path.unwrap_or_else(default_config_path); if let Ok(content) = fs::read_to_string(&config_path) { match toml::from_str::(&content) { diff --git a/src/daemon.rs b/src/daemon.rs index 062a80c..7b7d120 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,14 +1,20 @@ use crate::config::Config; -use crate::error::FluxoError; use crate::ipc::socket_path; -use crate::modules::WaybarModule; +#[cfg(feature = "mod-audio")] use crate::modules::audio::AudioDaemon; +#[cfg(feature = "mod-dbus")] use crate::modules::backlight::BacklightDaemon; +#[cfg(feature = "mod-bt")] use crate::modules::bt::BtDaemon; +#[cfg(feature = "mod-dbus")] use crate::modules::dnd::DndDaemon; +#[cfg(feature = "mod-hardware")] use crate::modules::hardware::HardwareDaemon; +#[cfg(feature = "mod-dbus")] use crate::modules::keyboard::KeyboardDaemon; +#[cfg(feature = "mod-dbus")] use crate::modules::mpris::MprisDaemon; +#[cfg(feature = "mod-network")] use crate::modules::network::NetworkDaemon; use crate::signaler::WaybarSignaler; use crate::state::AppReceivers; @@ -21,9 +27,9 @@ use std::sync::Arc; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::UnixListener; use tokio::sync::{RwLock, mpsc, watch}; -use tokio::time::{Duration, Instant, sleep}; +use tokio::time::{Duration, sleep}; use tokio_util::sync::CancellationToken; -use tracing::{debug, error, info, warn}; +use tracing::{debug, error, info}; struct SocketGuard { path: String, @@ -37,15 +43,7 @@ impl Drop for SocketGuard { } fn get_config_path(custom_path: Option) -> PathBuf { - custom_path.unwrap_or_else(|| { - let config_dir = std::env::var("XDG_CONFIG_HOME") - .map(PathBuf::from) - .unwrap_or_else(|_| { - let home = std::env::var("HOME").unwrap_or_else(|_| String::from("/")); - PathBuf::from(home).join(".config") - }); - config_dir.join("fluxo/config.toml") - }) + custom_path.unwrap_or_else(crate::config::default_config_path) } pub async fn run_daemon(config_path: Option) -> Result<()> { @@ -56,37 +54,65 @@ pub async fn run_daemon(config_path: Option) -> Result<()> { fs::remove_file(&sock_path)?; } + #[cfg(feature = "mod-network")] let (net_tx, net_rx) = watch::channel(Default::default()); + #[cfg(feature = "mod-hardware")] let (cpu_tx, cpu_rx) = watch::channel(Default::default()); + #[cfg(feature = "mod-hardware")] let (mem_tx, mem_rx) = watch::channel(Default::default()); + #[cfg(feature = "mod-hardware")] let (sys_tx, sys_rx) = watch::channel(Default::default()); + #[cfg(feature = "mod-hardware")] let (gpu_tx, gpu_rx) = watch::channel(Default::default()); + #[cfg(feature = "mod-hardware")] let (disks_tx, disks_rx) = watch::channel(Default::default()); + #[cfg(feature = "mod-bt")] let (bt_tx, bt_rx) = watch::channel(Default::default()); + #[cfg(feature = "mod-audio")] let (audio_tx, audio_rx) = watch::channel(Default::default()); + #[cfg(feature = "mod-dbus")] let (mpris_tx, mpris_rx) = watch::channel(Default::default()); + #[cfg(feature = "mod-dbus")] let (backlight_tx, backlight_rx) = watch::channel(Default::default()); + #[cfg(feature = "mod-dbus")] let (keyboard_tx, keyboard_rx) = watch::channel(Default::default()); + #[cfg(feature = "mod-dbus")] let (dnd_tx, dnd_rx) = watch::channel(Default::default()); let health = Arc::new(RwLock::new(HashMap::new())); + #[cfg(feature = "mod-bt")] let (bt_force_tx, mut bt_force_rx) = mpsc::channel(1); + #[cfg(feature = "mod-audio")] let (audio_cmd_tx, audio_cmd_rx) = mpsc::channel(8); let receivers = AppReceivers { + #[cfg(feature = "mod-network")] network: net_rx, + #[cfg(feature = "mod-hardware")] cpu: cpu_rx, + #[cfg(feature = "mod-hardware")] memory: mem_rx, + #[cfg(feature = "mod-hardware")] sys: sys_rx, + #[cfg(feature = "mod-hardware")] gpu: gpu_rx, + #[cfg(feature = "mod-hardware")] disks: disks_rx, + #[cfg(feature = "mod-bt")] bluetooth: bt_rx, + #[cfg(feature = "mod-audio")] audio: audio_rx, + #[cfg(feature = "mod-dbus")] mpris: mpris_rx, + #[cfg(feature = "mod-dbus")] backlight: backlight_rx, + #[cfg(feature = "mod-dbus")] keyboard: keyboard_rx, + #[cfg(feature = "mod-dbus")] dnd: dnd_rx, health: Arc::clone(&health), + #[cfg(feature = "mod-bt")] bt_force_poll: bt_force_tx, + #[cfg(feature = "mod-audio")] audio_cmd_tx, }; @@ -156,101 +182,128 @@ pub async fn run_daemon(config_path: Option) -> Result<()> { }); // 1. Network Task - let token = cancel_token.clone(); - let net_health = Arc::clone(&health); - tokio::spawn(async move { - info!("Starting Network polling task"); - let mut daemon = NetworkDaemon::new(); - loop { - if !is_in_backoff("net", &net_health).await { - let res = daemon.poll(&net_tx).await; - handle_poll_result("net", res, &net_health).await; + #[cfg(feature = "mod-network")] + { + let token = cancel_token.clone(); + let net_health = Arc::clone(&health); + tokio::spawn(async move { + info!("Starting Network polling task"); + let mut daemon = NetworkDaemon::new(); + loop { + if !crate::health::is_poll_in_backoff("net", &net_health).await { + let res = daemon.poll(&net_tx).await; + crate::health::handle_poll_result("net", res, &net_health).await; + } + tokio::select! { + _ = token.cancelled() => break, + _ = sleep(Duration::from_secs(1)) => {} + } } - tokio::select! { - _ = token.cancelled() => break, - _ = sleep(Duration::from_secs(1)) => {} - } - } - info!("Network task shut down."); - }); + info!("Network task shut down."); + }); + } // 2. Fast Hardware Task (CPU, Mem, Load) - let token = cancel_token.clone(); - let hw_health = Arc::clone(&health); - tokio::spawn(async move { - info!("Starting Fast Hardware polling task"); - let mut daemon = HardwareDaemon::new(); - loop { - if !is_in_backoff("cpu", &hw_health).await { - daemon.poll_fast(&cpu_tx, &mem_tx, &sys_tx).await; + #[cfg(feature = "mod-hardware")] + { + let token = cancel_token.clone(); + let hw_health = Arc::clone(&health); + tokio::spawn(async move { + info!("Starting Fast Hardware polling task"); + let mut daemon = HardwareDaemon::new(); + loop { + if !crate::health::is_poll_in_backoff("cpu", &hw_health).await { + daemon.poll_fast(&cpu_tx, &mem_tx, &sys_tx).await; + } + tokio::select! { + _ = token.cancelled() => break, + _ = sleep(Duration::from_secs(1)) => {} + } } - tokio::select! { - _ = token.cancelled() => break, - _ = sleep(Duration::from_secs(1)) => {} - } - } - info!("Fast Hardware task shut down."); - }); + info!("Fast Hardware task shut down."); + }); + } // 3. Slow Hardware Task (GPU, Disks) - let token = cancel_token.clone(); - let slow_health = Arc::clone(&health); - tokio::spawn(async move { - info!("Starting Slow Hardware polling task"); - let mut daemon = HardwareDaemon::new(); - loop { - if !is_in_backoff("gpu", &slow_health).await { - daemon.poll_slow(&gpu_tx, &disks_tx).await; + #[cfg(feature = "mod-hardware")] + { + let token = cancel_token.clone(); + let slow_health = Arc::clone(&health); + tokio::spawn(async move { + info!("Starting Slow Hardware polling task"); + let mut daemon = HardwareDaemon::new(); + loop { + if !crate::health::is_poll_in_backoff("gpu", &slow_health).await { + daemon.poll_slow(&gpu_tx, &disks_tx).await; + } + tokio::select! { + _ = token.cancelled() => break, + _ = sleep(Duration::from_secs(5)) => {} + } } - tokio::select! { - _ = token.cancelled() => break, - _ = sleep(Duration::from_secs(5)) => {} - } - } - info!("Slow Hardware task shut down."); - }); + info!("Slow Hardware task shut down."); + }); + } // 4. Bluetooth Task - let token = cancel_token.clone(); - let bt_health = Arc::clone(&health); - let poll_config = Arc::clone(&config); - let poll_receivers = receivers.clone(); - tokio::spawn(async move { - info!("Starting Bluetooth polling task"); - let mut daemon = BtDaemon::new(); - loop { - if !is_in_backoff("bt", &bt_health).await { - let config = poll_config.read().await; - daemon.poll(&bt_tx, &poll_receivers, &config).await; + #[cfg(feature = "mod-bt")] + { + let token = cancel_token.clone(); + let bt_health = Arc::clone(&health); + let poll_config = Arc::clone(&config); + let poll_receivers = receivers.clone(); + tokio::spawn(async move { + info!("Starting Bluetooth polling task"); + let mut daemon = BtDaemon::new(); + loop { + if !crate::health::is_poll_in_backoff("bt", &bt_health).await { + let config = poll_config.read().await; + daemon.poll(&bt_tx, &poll_receivers, &config).await; + } + tokio::select! { + _ = token.cancelled() => break, + _ = bt_force_rx.recv() => {}, + _ = sleep(Duration::from_secs(2)) => {} + } } - tokio::select! { - _ = token.cancelled() => break, - _ = bt_force_rx.recv() => {}, - _ = sleep(Duration::from_secs(2)) => {} - } - } - info!("Bluetooth task shut down."); - }); + info!("Bluetooth task shut down."); + }); + } // 5. Audio Thread (Event driven) - let audio_daemon = AudioDaemon::new(); - audio_daemon.start(&audio_tx, audio_cmd_rx); + #[cfg(feature = "mod-audio")] + { + let audio_daemon = AudioDaemon::new(); + audio_daemon.start(&audio_tx, audio_cmd_rx); + } // 5.1 Backlight Thread (Event driven) - let backlight_daemon = BacklightDaemon::new(); - backlight_daemon.start(backlight_tx); + #[cfg(feature = "mod-dbus")] + { + let backlight_daemon = BacklightDaemon::new(); + backlight_daemon.start(backlight_tx); + } // 5.2 Keyboard Thread (Event driven) - let keyboard_daemon = KeyboardDaemon::new(); - keyboard_daemon.start(keyboard_tx); + #[cfg(feature = "mod-dbus")] + { + let keyboard_daemon = KeyboardDaemon::new(); + keyboard_daemon.start(keyboard_tx); + } // 5.3 DND Thread (Event driven) - let dnd_daemon = DndDaemon::new(); - dnd_daemon.start(dnd_tx); + #[cfg(feature = "mod-dbus")] + { + let dnd_daemon = DndDaemon::new(); + dnd_daemon.start(dnd_tx); + } // 5.4 MPRIS Thread - let mpris_daemon = MprisDaemon::new(); - mpris_daemon.start(mpris_tx); + #[cfg(feature = "mod-dbus")] + { + let mpris_daemon = MprisDaemon::new(); + mpris_daemon.start(mpris_tx); + } // 6. Waybar Signaler Task let signaler = WaybarSignaler::new(); @@ -324,59 +377,6 @@ pub async fn run_daemon(config_path: Option) -> Result<()> { Ok(()) } -async fn handle_poll_result( - module_name: &str, - result: crate::error::Result<()>, - health_lock: &Arc>>, -) { - let mut lock = health_lock.write().await; - let health = lock.entry(module_name.to_string()).or_default(); - - match result { - Ok(_) => { - if health.consecutive_failures > 0 { - info!( - module = module_name, - "Module recovered after {} failures", health.consecutive_failures - ); - } - health.consecutive_failures = 0; - health.backoff_until = None; - } - Err(e) => { - health.consecutive_failures += 1; - health.last_failure = Some(Instant::now()); - - if !e.is_transient() { - // Fatal errors trigger immediate long backoff - health.backoff_until = Some(Instant::now() + Duration::from_secs(60)); - error!(module = module_name, error = %e, "Fatal module error, entering long cooldown"); - } else if health.consecutive_failures >= 3 { - // Exponential backoff for transient errors: 30s, 60s, 120s... - let backoff_secs = 30 * (2u64.pow(health.consecutive_failures.saturating_sub(3))); - let backoff_secs = backoff_secs.min(3600); // Cap at 1 hour - health.backoff_until = Some(Instant::now() + Duration::from_secs(backoff_secs)); - warn!(module = module_name, error = %e, backoff = backoff_secs, "Repeated transient failures, entering backoff"); - } else { - debug!(module = module_name, error = %e, "Transient module failure (attempt {})", health.consecutive_failures); - } - } - } -} - -async fn is_in_backoff( - module_name: &str, - health_lock: &Arc>>, -) -> bool { - let lock = health_lock.read().await; - if let Some(health) = lock.get(module_name) - && let Some(until) = health.backoff_until - { - return Instant::now() < until; - } - false -} - pub async fn reload_config(config_lock: &Arc>, path: Option) { info!("Reloading configuration..."); let new_config = crate::config::load_config(path); @@ -390,74 +390,11 @@ pub async fn evaluate_module_for_signaler( state: &AppReceivers, config: &Config, ) -> Option { - let result = match module_name { - "net" | "network" => { - crate::modules::network::NetworkModule - .run(config, state, &[]) - .await - } - "cpu" => crate::modules::cpu::CpuModule.run(config, state, &[]).await, - "mem" | "memory" => { - crate::modules::memory::MemoryModule - .run(config, state, &[]) - .await - } - "disk" => { - crate::modules::disk::DiskModule - .run(config, state, &["/"]) - .await - } - "pool" | "btrfs" => { - crate::modules::btrfs::BtrfsModule - .run(config, state, &["btrfs"]) - .await - } - "vol" | "audio" => { - crate::modules::audio::AudioModule - .run(config, state, &["sink", "show"]) - .await - } - "mic" => { - crate::modules::audio::AudioModule - .run(config, state, &["source", "show"]) - .await - } - "gpu" => crate::modules::gpu::GpuModule.run(config, state, &[]).await, - "sys" => crate::modules::sys::SysModule.run(config, state, &[]).await, - "bt" | "bluetooth" => { - crate::modules::bt::BtModule - .run(config, state, &["show"]) - .await - } - "power" => { - crate::modules::power::PowerModule - .run(config, state, &[]) - .await - } - "game" => { - crate::modules::game::GameModule - .run(config, state, &[]) - .await - } - "backlight" => { - crate::modules::backlight::BacklightModule - .run(config, state, &[]) - .await - } - "kbd" | "keyboard" => { - crate::modules::keyboard::KeyboardModule - .run(config, state, &[]) - .await - } - "dnd" => crate::modules::dnd::DndModule.run(config, state, &[]).await, - "mpris" => { - crate::modules::mpris::MprisModule - .run(config, state, &[]) - .await - } - _ => return None, - }; - result.ok().and_then(|out| serde_json::to_string(&out).ok()) + let args = crate::registry::signaler_default_args(module_name); + crate::registry::dispatch(module_name, config, state, args) + .await + .ok() + .and_then(|out| serde_json::to_string(&out).ok()) } async fn handle_request( @@ -466,160 +403,19 @@ async fn handle_request( state: &AppReceivers, config_lock: &Arc>, ) -> String { - // 1. Check Circuit Breaker status - let (is_in_backoff, cached_output) = { - let lock = state.health.read().await; - if let Some(health) = lock.get(module_name) { - let in_backoff = if let Some(until) = health.backoff_until { - Instant::now() < until - } else { - false - }; - (in_backoff, health.last_successful_output.clone()) - } else { - (false, None) - } - }; + let (is_in_backoff, cached_output) = crate::health::check_backoff(module_name, state).await; if is_in_backoff { - if let Some(mut cached) = cached_output { - // Add a "warning" class to indicate stale data - let class = cached.class.unwrap_or_default(); - cached.class = Some(format!("{} warning", class).trim().to_string()); - return serde_json::to_string(&cached).unwrap_or_else(|_| "{}".to_string()); - } - return format!( - "{{\"text\":\"\u{200B}Cooling down ({})\u{200B}\",\"class\":\"error\"}}", - module_name - ); + return crate::health::backoff_response(module_name, cached_output); } let config = config_lock.read().await; + let result = crate::registry::dispatch(module_name, &config, state, args).await; - let result = match module_name { - "net" | "network" => { - crate::modules::network::NetworkModule - .run(&config, state, args) - .await - } - "cpu" => { - crate::modules::cpu::CpuModule - .run(&config, state, args) - .await - } - "mem" | "memory" => { - crate::modules::memory::MemoryModule - .run(&config, state, args) - .await - } - "disk" => { - crate::modules::disk::DiskModule - .run(&config, state, args) - .await - } - "pool" | "btrfs" => { - crate::modules::btrfs::BtrfsModule - .run(&config, state, args) - .await - } - "vol" | "audio" => { - crate::modules::audio::AudioModule - .run(&config, state, args) - .await - } - "mic" => { - crate::modules::audio::AudioModule - .run(&config, state, args) - .await - } - "gpu" => { - crate::modules::gpu::GpuModule - .run(&config, state, args) - .await - } - "sys" => { - crate::modules::sys::SysModule - .run(&config, state, args) - .await - } - "bt" | "bluetooth" => crate::modules::bt::BtModule.run(&config, state, args).await, - "power" => { - crate::modules::power::PowerModule - .run(&config, state, args) - .await - } - "game" => { - crate::modules::game::GameModule - .run(&config, state, args) - .await - } - "backlight" => { - crate::modules::backlight::BacklightModule - .run(&config, state, args) - .await - } - "kbd" | "keyboard" => { - crate::modules::keyboard::KeyboardModule - .run(&config, state, args) - .await - } - "dnd" => { - crate::modules::dnd::DndModule - .run(&config, state, args) - .await - } - "mpris" => { - crate::modules::mpris::MprisModule - .run(&config, state, args) - .await - } - _ => { - warn!("Received request for unknown module: '{}'", module_name); - Err(FluxoError::Ipc(format!("Unknown module: {}", module_name))) - } - }; - - // 2. Update Health and Cache based on result - { - let mut lock = state.health.write().await; - let health = lock.entry(module_name.to_string()).or_default(); - match &result { - Ok(output) => { - health.consecutive_failures = 0; - health.backoff_until = None; - health.last_successful_output = Some(output.clone()); - } - Err(e) => { - health.consecutive_failures += 1; - health.last_failure = Some(Instant::now()); - if health.consecutive_failures >= 3 { - // Backoff for 30 seconds after 3 failures - health.backoff_until = Some(Instant::now() + Duration::from_secs(30)); - warn!(module = module_name, error = %e, "Module entered backoff state due to repeated failures"); - } - } - } - } + crate::health::update_health(module_name, &result, state).await; match result { Ok(output) => serde_json::to_string(&output).unwrap_or_else(|_| "{}".to_string()), - Err(e) => { - // If we have a cached output, return it as fallback even on immediate error - if let Some(mut cached) = cached_output { - let class = cached.class.unwrap_or_default(); - cached.class = Some(format!("{} warning", class).trim().to_string()); - return serde_json::to_string(&cached).unwrap_or_else(|_| "{}".to_string()); - } - - let error_msg = e.to_string(); - error!(module = module_name, error = %error_msg, "Module execution failed"); - let err_out = crate::output::WaybarOutput { - text: "\u{200B}Error\u{200B}".to_string(), - tooltip: Some(error_msg), - class: Some("error".to_string()), - percentage: None, - }; - serde_json::to_string(&err_out).unwrap_or_else(|_| "{}".to_string()) - } + Err(e) => crate::health::error_response(module_name, &e, cached_output), } } diff --git a/src/health.rs b/src/health.rs new file mode 100644 index 0000000..a1260ad --- /dev/null +++ b/src/health.rs @@ -0,0 +1,135 @@ +use crate::output::WaybarOutput; +use crate::state::{AppReceivers, ModuleHealth}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +use tokio::time::{Duration, Instant}; +use tracing::{debug, error, info, warn}; + +/// Check if a module is in backoff (used by request handler). +pub async fn check_backoff( + module_name: &str, + state: &AppReceivers, +) -> (bool, Option) { + let lock = state.health.read().await; + if let Some(health) = lock.get(module_name) { + let in_backoff = health + .backoff_until + .is_some_and(|until| Instant::now() < until); + (in_backoff, health.last_successful_output.clone()) + } else { + (false, None) + } +} + +/// Update health after a request dispatch (used by request handler). +pub async fn update_health( + module_name: &str, + result: &Result, + state: &AppReceivers, +) { + let mut lock = state.health.write().await; + let health = lock.entry(module_name.to_string()).or_default(); + match result { + Ok(output) => { + health.consecutive_failures = 0; + health.backoff_until = None; + health.last_successful_output = Some(output.clone()); + } + Err(e) => { + health.consecutive_failures += 1; + health.last_failure = Some(Instant::now()); + if health.consecutive_failures >= 3 { + health.backoff_until = Some(Instant::now() + Duration::from_secs(30)); + warn!(module = module_name, error = %e, "Module entered backoff state due to repeated failures"); + } + } + } +} + +/// Check if a polling daemon module is in backoff. +pub async fn is_poll_in_backoff( + module_name: &str, + health_lock: &Arc>>, +) -> bool { + let lock = health_lock.read().await; + if let Some(health) = lock.get(module_name) + && let Some(until) = health.backoff_until + { + return Instant::now() < until; + } + false +} + +/// Update health after a polling daemon result. +pub async fn handle_poll_result( + module_name: &str, + result: crate::error::Result<()>, + health_lock: &Arc>>, +) { + let mut lock = health_lock.write().await; + let health = lock.entry(module_name.to_string()).or_default(); + + match result { + Ok(_) => { + if health.consecutive_failures > 0 { + info!( + module = module_name, + "Module recovered after {} failures", health.consecutive_failures + ); + } + health.consecutive_failures = 0; + health.backoff_until = None; + } + Err(e) => { + health.consecutive_failures += 1; + health.last_failure = Some(Instant::now()); + + if !e.is_transient() { + health.backoff_until = Some(Instant::now() + Duration::from_secs(60)); + error!(module = module_name, error = %e, "Fatal module error, entering long cooldown"); + } else if health.consecutive_failures >= 3 { + let backoff_secs = 30 * (2u64.pow(health.consecutive_failures.saturating_sub(3))); + let backoff_secs = backoff_secs.min(3600); + health.backoff_until = Some(Instant::now() + Duration::from_secs(backoff_secs)); + warn!(module = module_name, error = %e, backoff = backoff_secs, "Repeated transient failures, entering backoff"); + } else { + debug!(module = module_name, error = %e, "Transient module failure (attempt {})", health.consecutive_failures); + } + } + } +} + +pub fn backoff_response(module_name: &str, cached: Option) -> String { + if let Some(mut cached) = cached { + let class = cached.class.unwrap_or_default(); + cached.class = Some(format!("{} warning", class).trim().to_string()); + return serde_json::to_string(&cached).unwrap_or_else(|_| "{}".to_string()); + } + format!( + "{{\"text\":\"\u{200B}Cooling down ({})\u{200B}\",\"class\":\"error\"}}", + module_name + ) +} + +pub fn error_response( + module_name: &str, + e: &crate::error::FluxoError, + cached: Option, +) -> String { + if let Some(mut cached) = cached { + let class = cached.class.unwrap_or_default(); + cached.class = Some(format!("{} warning", class).trim().to_string()); + return serde_json::to_string(&cached).unwrap_or_else(|_| "{}".to_string()); + } + + let error_msg = e.to_string(); + error!(module = module_name, error = %error_msg, "Module execution failed"); + let err_out = WaybarOutput { + text: "\u{200B}Error\u{200B}".to_string(), + tooltip: Some(error_msg), + class: Some("error".to_string()), + percentage: None, + }; + serde_json::to_string(&err_out).unwrap_or_else(|_| "{}".to_string()) +} diff --git a/src/main.rs b/src/main.rs index a50441d..17f0e48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,11 @@ mod config; mod daemon; mod error; +mod health; mod ipc; mod modules; mod output; +mod registry; mod signaler; mod state; mod utils; @@ -76,6 +78,7 @@ fn main() { if let Some(module) = &cli.module { // Special case for client-side Bluetooth menu which requires UI + #[cfg(feature = "mod-bt")] if module == "bt" && cli.args.first().map(|s| s.as_str()) == Some("menu") { let config = config::load_config(None); let mut items = Vec::new(); diff --git a/src/modules/btrfs.rs b/src/modules/btrfs.rs index 096926c..3a268fc 100644 --- a/src/modules/btrfs.rs +++ b/src/modules/btrfs.rs @@ -3,7 +3,7 @@ use crate::error::Result; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::AppReceivers; -use crate::utils::{TokenValue, format_template}; +use crate::utils::{TokenValue, classify_usage, format_template}; pub struct BtrfsModule; @@ -44,13 +44,7 @@ impl WaybarModule for BtrfsModule { let size_gb = total_size / 1024.0 / 1024.0 / 1024.0; let percentage = (total_used / total_size) * 100.0; - let class = if percentage > 95.0 { - "max" - } else if percentage > 80.0 { - "high" - } else { - "normal" - }; + let class = classify_usage(percentage, 80.0, 95.0); let text = format_template( &config.pool.format, diff --git a/src/modules/cpu.rs b/src/modules/cpu.rs index 619a6c1..fd91578 100644 --- a/src/modules/cpu.rs +++ b/src/modules/cpu.rs @@ -3,7 +3,7 @@ use crate::error::Result; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::AppReceivers; -use crate::utils::{TokenValue, format_template}; +use crate::utils::{TokenValue, classify_usage, format_template}; pub struct CpuModule; @@ -27,13 +27,7 @@ impl WaybarModule for CpuModule { ], ); - let class = if usage > 95.0 { - "max" - } else if usage > 75.0 { - "high" - } else { - "normal" - }; + let class = classify_usage(usage, 75.0, 95.0); Ok(WaybarOutput { text, diff --git a/src/modules/disk.rs b/src/modules/disk.rs index 1433474..f350e28 100644 --- a/src/modules/disk.rs +++ b/src/modules/disk.rs @@ -3,7 +3,7 @@ use crate::error::{FluxoError, Result}; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::AppReceivers; -use crate::utils::{TokenValue, format_template}; +use crate::utils::{TokenValue, classify_usage, format_template}; pub struct DiskModule; @@ -41,13 +41,7 @@ impl WaybarModule for DiskModule { 0.0 }; - let class = if percentage > 95.0 { - "max" - } else if percentage > 80.0 { - "high" - } else { - "normal" - }; + let class = classify_usage(percentage, 80.0, 95.0); let text = format_template( &config.disk.format, diff --git a/src/modules/dnd.rs b/src/modules/dnd.rs index 672c4b7..786337b 100644 --- a/src/modules/dnd.rs +++ b/src/modules/dnd.rs @@ -20,10 +20,13 @@ impl WaybarModule for DndModule { let action = args.first().unwrap_or(&"show"); if *action == "toggle" { - let connection = Connection::session().await.map_err(|e| crate::error::FluxoError::Module { - module: "dnd", - message: format!("DBus connection failed: {}", e), - })?; + let connection = + Connection::session() + .await + .map_err(|e| crate::error::FluxoError::Module { + module: "dnd", + message: format!("DBus connection failed: {}", e), + })?; // Try toggling SwayNC if let Ok(proxy) = SwayncControlProxy::new(&connection).await { diff --git a/src/modules/gpu.rs b/src/modules/gpu.rs index db5023c..2c1ee76 100644 --- a/src/modules/gpu.rs +++ b/src/modules/gpu.rs @@ -3,7 +3,7 @@ use crate::error::Result; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::AppReceivers; -use crate::utils::{TokenValue, format_template}; +use crate::utils::{TokenValue, classify_usage, format_template}; pub struct GpuModule; @@ -36,13 +36,7 @@ impl WaybarModule for GpuModule { }); } - let class = if usage > 95.0 { - "max" - } else if usage > 75.0 { - "high" - } else { - "normal" - }; + let class = classify_usage(usage, 75.0, 95.0); let format_str = match vendor.as_str() { "Intel" => &config.gpu.format_intel, diff --git a/src/modules/memory.rs b/src/modules/memory.rs index 83ed9eb..29fc178 100644 --- a/src/modules/memory.rs +++ b/src/modules/memory.rs @@ -3,7 +3,7 @@ use crate::error::Result; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::AppReceivers; -use crate::utils::{TokenValue, format_template}; +use crate::utils::{TokenValue, classify_usage, format_template}; pub struct MemoryModule; @@ -33,13 +33,7 @@ impl WaybarModule for MemoryModule { ], ); - let class = if ratio > 95.0 { - "max" - } else if ratio > 75.0 { - "high" - } else { - "normal" - }; + let class = classify_usage(ratio, 75.0, 95.0); Ok(WaybarOutput { text, diff --git a/src/modules/mod.rs b/src/modules/mod.rs index cf2dac8..658c2c7 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -1,18 +1,34 @@ +#[cfg(feature = "mod-audio")] pub mod audio; +#[cfg(feature = "mod-dbus")] pub mod backlight; +#[cfg(feature = "mod-bt")] pub mod bt; +#[cfg(feature = "mod-hardware")] pub mod btrfs; +#[cfg(feature = "mod-hardware")] pub mod cpu; +#[cfg(feature = "mod-hardware")] pub mod disk; +#[cfg(feature = "mod-dbus")] pub mod dnd; +#[cfg(feature = "mod-hardware")] pub mod game; +#[cfg(feature = "mod-hardware")] pub mod gpu; +#[cfg(feature = "mod-hardware")] pub mod hardware; +#[cfg(feature = "mod-dbus")] pub mod keyboard; +#[cfg(feature = "mod-hardware")] pub mod memory; +#[cfg(feature = "mod-dbus")] pub mod mpris; +#[cfg(feature = "mod-network")] pub mod network; +#[cfg(feature = "mod-hardware")] pub mod power; +#[cfg(feature = "mod-hardware")] pub mod sys; use crate::config::Config; diff --git a/src/registry.rs b/src/registry.rs new file mode 100644 index 0000000..006f42f --- /dev/null +++ b/src/registry.rs @@ -0,0 +1,121 @@ +use crate::config::Config; +use crate::error::{FluxoError, Result as FluxoResult}; +use crate::output::WaybarOutput; +use crate::state::AppReceivers; + +#[allow(unused_imports)] +use crate::modules::WaybarModule; + +pub async fn dispatch( + module_name: &str, + #[allow(unused)] config: &Config, + #[allow(unused)] state: &AppReceivers, + #[allow(unused)] args: &[&str], +) -> FluxoResult { + match module_name { + #[cfg(feature = "mod-network")] + "net" | "network" => { + crate::modules::network::NetworkModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-hardware")] + "cpu" => { + crate::modules::cpu::CpuModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-hardware")] + "mem" | "memory" => { + crate::modules::memory::MemoryModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-hardware")] + "disk" => { + crate::modules::disk::DiskModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-hardware")] + "pool" | "btrfs" => { + crate::modules::btrfs::BtrfsModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-audio")] + "vol" | "audio" => { + crate::modules::audio::AudioModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-audio")] + "mic" => { + crate::modules::audio::AudioModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-hardware")] + "gpu" => { + crate::modules::gpu::GpuModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-hardware")] + "sys" => { + crate::modules::sys::SysModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-bt")] + "bt" | "bluetooth" => crate::modules::bt::BtModule.run(config, state, args).await, + #[cfg(feature = "mod-hardware")] + "power" => { + crate::modules::power::PowerModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-hardware")] + "game" => { + crate::modules::game::GameModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-dbus")] + "backlight" => { + crate::modules::backlight::BacklightModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-dbus")] + "kbd" | "keyboard" => { + crate::modules::keyboard::KeyboardModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-dbus")] + "dnd" => { + crate::modules::dnd::DndModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-dbus")] + "mpris" => { + crate::modules::mpris::MprisModule + .run(config, state, args) + .await + } + _ => Err(FluxoError::Ipc(format!("Unknown module: {}", module_name))), + } +} + +/// Returns the default args used by the signaler when evaluating a module. +pub fn signaler_default_args(module_name: &str) -> &'static [&'static str] { + match module_name { + "disk" => &["/"], + "vol" | "audio" => &["sink", "show"], + "mic" => &["source", "show"], + "bt" | "bluetooth" => &["show"], + _ => &[], + } +} diff --git a/src/signaler.rs b/src/signaler.rs index 0ea9f86..fcc95e2 100644 --- a/src/signaler.rs +++ b/src/signaler.rs @@ -1,8 +1,5 @@ use crate::config::Config; use crate::state::AppReceivers; -use nix::libc; -use nix::sys::signal::{Signal, kill}; -use nix::unistd::Pid; use std::collections::HashMap; use std::sync::Arc; use sysinfo::{ProcessesToUpdate, System}; @@ -11,7 +8,7 @@ use tokio::time::{Duration, Instant, sleep}; use tracing::{debug, warn}; pub struct WaybarSignaler { - cached_pid: Option, + cached_pid: Option, sys: System, last_signal_sent: HashMap, } @@ -25,11 +22,11 @@ impl WaybarSignaler { } } - fn find_waybar_pid(&mut self) -> Option { + fn find_waybar_pid(&mut self) -> Option { self.sys.refresh_processes(ProcessesToUpdate::All, true); for (pid, process) in self.sys.processes() { if process.name() == "waybar" { - return Some(Pid::from_raw(pid.as_u32() as i32)); + return Some(pid.as_u32() as i32); } } None @@ -44,7 +41,7 @@ impl WaybarSignaler { let mut valid_pid = false; if let Some(pid) = self.cached_pid - && kill(pid, None).is_ok() + && unsafe { libc::kill(pid, 0) } == 0 { valid_pid = true; } @@ -54,27 +51,13 @@ impl WaybarSignaler { } if let Some(pid) = self.cached_pid { - let rt_sig = match Signal::try_from(libc::SIGRTMIN() + signal_num) { - Ok(sig) => sig, - Err(_) => { - unsafe { - if libc::kill(pid.as_raw(), libc::SIGRTMIN() + signal_num) == 0 { - debug!("Sent raw SIGRTMIN+{} to waybar (PID: {})", signal_num, pid); - self.last_signal_sent.insert(signal_num, Instant::now()); - } else { - warn!("Failed to send raw SIGRTMIN+{} to waybar", signal_num); - } - } - return; - } - }; - - if let Err(e) = kill(pid, rt_sig) { - warn!("Failed to signal waybar: {}", e); - self.cached_pid = None; - } else { + let sig = libc::SIGRTMIN() + signal_num; + if unsafe { libc::kill(pid, sig) } == 0 { debug!("Sent SIGRTMIN+{} to waybar (PID: {})", signal_num, pid); self.last_signal_sent.insert(signal_num, Instant::now()); + } else { + warn!("Failed to send SIGRTMIN+{} to waybar", signal_num); + self.cached_pid = None; } } else { debug!("Waybar process not found, skipping signal."); @@ -107,44 +90,129 @@ impl WaybarSignaler { }; } + // For disabled features, create futures that never resolve + #[cfg(not(feature = "mod-network"))] + let net_changed = std::future::pending::< + std::result::Result<(), tokio::sync::watch::error::RecvError>, + >(); + #[cfg(feature = "mod-network")] + let net_changed = receivers.network.changed(); + + #[cfg(not(feature = "mod-hardware"))] + let cpu_changed = std::future::pending::< + std::result::Result<(), tokio::sync::watch::error::RecvError>, + >(); + #[cfg(feature = "mod-hardware")] + let cpu_changed = receivers.cpu.changed(); + + #[cfg(not(feature = "mod-hardware"))] + let mem_changed = std::future::pending::< + std::result::Result<(), tokio::sync::watch::error::RecvError>, + >(); + #[cfg(feature = "mod-hardware")] + let mem_changed = receivers.memory.changed(); + + #[cfg(not(feature = "mod-hardware"))] + let sys_changed = std::future::pending::< + std::result::Result<(), tokio::sync::watch::error::RecvError>, + >(); + #[cfg(feature = "mod-hardware")] + let sys_changed = receivers.sys.changed(); + + #[cfg(not(feature = "mod-hardware"))] + let gpu_changed = std::future::pending::< + std::result::Result<(), tokio::sync::watch::error::RecvError>, + >(); + #[cfg(feature = "mod-hardware")] + let gpu_changed = receivers.gpu.changed(); + + #[cfg(not(feature = "mod-hardware"))] + let disks_changed = std::future::pending::< + std::result::Result<(), tokio::sync::watch::error::RecvError>, + >(); + #[cfg(feature = "mod-hardware")] + let disks_changed = receivers.disks.changed(); + + #[cfg(not(feature = "mod-bt"))] + let bt_changed = std::future::pending::< + std::result::Result<(), tokio::sync::watch::error::RecvError>, + >(); + #[cfg(feature = "mod-bt")] + let bt_changed = receivers.bluetooth.changed(); + + #[cfg(not(feature = "mod-audio"))] + let audio_changed = std::future::pending::< + std::result::Result<(), tokio::sync::watch::error::RecvError>, + >(); + #[cfg(feature = "mod-audio")] + let audio_changed = receivers.audio.changed(); + + #[cfg(not(feature = "mod-dbus"))] + let backlight_changed = std::future::pending::< + std::result::Result<(), tokio::sync::watch::error::RecvError>, + >(); + #[cfg(feature = "mod-dbus")] + let backlight_changed = receivers.backlight.changed(); + + #[cfg(not(feature = "mod-dbus"))] + let keyboard_changed = std::future::pending::< + std::result::Result<(), tokio::sync::watch::error::RecvError>, + >(); + #[cfg(feature = "mod-dbus")] + let keyboard_changed = receivers.keyboard.changed(); + + #[cfg(not(feature = "mod-dbus"))] + let dnd_changed = std::future::pending::< + std::result::Result<(), tokio::sync::watch::error::RecvError>, + >(); + #[cfg(feature = "mod-dbus")] + let dnd_changed = receivers.dnd.changed(); + + #[cfg(not(feature = "mod-dbus"))] + let mpris_changed = std::future::pending::< + std::result::Result<(), tokio::sync::watch::error::RecvError>, + >(); + #[cfg(feature = "mod-dbus")] + let mpris_changed = receivers.mpris.changed(); + tokio::select! { - res = receivers.network.changed(), if signals.network.is_some() => { + res = net_changed, if signals.network.is_some() => { if res.is_ok() { check_and_signal!("net", signals.network); } } - res = receivers.cpu.changed(), if signals.cpu.is_some() => { + res = cpu_changed, if signals.cpu.is_some() => { if res.is_ok() { check_and_signal!("cpu", signals.cpu); } } - res = receivers.memory.changed(), if signals.memory.is_some() => { + res = mem_changed, if signals.memory.is_some() => { if res.is_ok() { check_and_signal!("mem", signals.memory); } } - res = receivers.sys.changed(), if signals.sys.is_some() => { + res = sys_changed, if signals.sys.is_some() => { if res.is_ok() { check_and_signal!("sys", signals.sys); } } - res = receivers.gpu.changed(), if signals.gpu.is_some() => { + res = gpu_changed, if signals.gpu.is_some() => { if res.is_ok() { check_and_signal!("gpu", signals.gpu); } } - res = receivers.disks.changed(), if signals.disk.is_some() => { + res = disks_changed, if signals.disk.is_some() => { if res.is_ok() { check_and_signal!("disk", signals.disk); } } - res = receivers.bluetooth.changed(), if signals.bt.is_some() => { + res = bt_changed, if signals.bt.is_some() => { if res.is_ok() { check_and_signal!("bt", signals.bt); } } - res = receivers.audio.changed(), if signals.audio.is_some() => { + res = audio_changed, if signals.audio.is_some() => { if res.is_ok() { check_and_signal!("vol", signals.audio); check_and_signal!("mic", signals.audio); } } - res = receivers.backlight.changed(), if signals.backlight.is_some() => { + res = backlight_changed, if signals.backlight.is_some() => { if res.is_ok() { check_and_signal!("backlight", signals.backlight); } } - res = receivers.keyboard.changed(), if signals.keyboard.is_some() => { + res = keyboard_changed, if signals.keyboard.is_some() => { if res.is_ok() { check_and_signal!("keyboard", signals.keyboard); } } - res = receivers.dnd.changed(), if signals.dnd.is_some() => { + res = dnd_changed, if signals.dnd.is_some() => { if res.is_ok() { check_and_signal!("dnd", signals.dnd); } } - res = receivers.mpris.changed(), if signals.mpris.is_some() => { + res = mpris_changed, if signals.mpris.is_some() => { if res.is_ok() { check_and_signal!("mpris", signals.mpris); } } _ = sleep(Duration::from_secs(5)) => { diff --git a/src/state.rs b/src/state.rs index dad6b42..b99462e 100644 --- a/src/state.rs +++ b/src/state.rs @@ -6,20 +6,34 @@ use tokio::time::Instant; #[derive(Clone)] pub struct AppReceivers { + #[cfg(feature = "mod-network")] pub network: watch::Receiver, + #[cfg(feature = "mod-hardware")] pub cpu: watch::Receiver, + #[cfg(feature = "mod-hardware")] pub memory: watch::Receiver, + #[cfg(feature = "mod-hardware")] pub sys: watch::Receiver, + #[cfg(feature = "mod-hardware")] pub gpu: watch::Receiver, + #[cfg(feature = "mod-hardware")] pub disks: watch::Receiver>, + #[cfg(feature = "mod-bt")] pub bluetooth: watch::Receiver, + #[cfg(feature = "mod-audio")] pub audio: watch::Receiver, + #[cfg(feature = "mod-dbus")] pub mpris: watch::Receiver, + #[cfg(feature = "mod-dbus")] pub backlight: watch::Receiver, + #[cfg(feature = "mod-dbus")] pub keyboard: watch::Receiver, + #[cfg(feature = "mod-dbus")] pub dnd: watch::Receiver, pub health: Arc>>, + #[cfg(feature = "mod-bt")] pub bt_force_poll: mpsc::Sender<()>, + #[cfg(feature = "mod-audio")] pub audio_cmd_tx: mpsc::Sender, } @@ -168,85 +182,148 @@ pub struct MprisState { #[cfg(test)] pub struct MockState { pub receivers: AppReceivers, - // Keep senders alive so receivers don't return Closed errors + #[cfg(feature = "mod-network")] _net_tx: watch::Sender, + #[cfg(feature = "mod-hardware")] _cpu_tx: watch::Sender, + #[cfg(feature = "mod-hardware")] _mem_tx: watch::Sender, + #[cfg(feature = "mod-hardware")] _sys_tx: watch::Sender, + #[cfg(feature = "mod-hardware")] _gpu_tx: watch::Sender, + #[cfg(feature = "mod-hardware")] _disks_tx: watch::Sender>, + #[cfg(feature = "mod-bt")] _bt_tx: watch::Sender, + #[cfg(feature = "mod-audio")] _audio_tx: watch::Sender, + #[cfg(feature = "mod-dbus")] _mpris_tx: watch::Sender, + #[cfg(feature = "mod-dbus")] _backlight_tx: watch::Sender, + #[cfg(feature = "mod-dbus")] _keyboard_tx: watch::Sender, + #[cfg(feature = "mod-dbus")] _dnd_tx: watch::Sender, } #[cfg(test)] #[derive(Default, Clone)] pub struct AppState { + #[cfg(feature = "mod-network")] pub network: NetworkState, + #[cfg(feature = "mod-hardware")] pub cpu: CpuState, + #[cfg(feature = "mod-hardware")] pub memory: MemoryState, + #[cfg(feature = "mod-hardware")] pub sys: SysState, + #[cfg(feature = "mod-hardware")] pub gpu: GpuState, + #[cfg(feature = "mod-hardware")] pub disks: Vec, + #[cfg(feature = "mod-bt")] pub bluetooth: BtState, + #[cfg(feature = "mod-audio")] pub audio: AudioState, + #[cfg(feature = "mod-dbus")] pub mpris: MprisState, + #[cfg(feature = "mod-dbus")] pub backlight: BacklightState, + #[cfg(feature = "mod-dbus")] pub keyboard: KeyboardState, + #[cfg(feature = "mod-dbus")] pub dnd: DndState, pub health: HashMap, } #[cfg(test)] pub fn mock_state(state: AppState) -> MockState { + #[cfg(feature = "mod-network")] let (net_tx, net_rx) = watch::channel(state.network); + #[cfg(feature = "mod-hardware")] let (cpu_tx, cpu_rx) = watch::channel(state.cpu); + #[cfg(feature = "mod-hardware")] let (mem_tx, mem_rx) = watch::channel(state.memory); + #[cfg(feature = "mod-hardware")] let (sys_tx, sys_rx) = watch::channel(state.sys); + #[cfg(feature = "mod-hardware")] let (gpu_tx, gpu_rx) = watch::channel(state.gpu); + #[cfg(feature = "mod-hardware")] let (disks_tx, disks_rx) = watch::channel(state.disks); + #[cfg(feature = "mod-bt")] let (bt_tx, bt_rx) = watch::channel(state.bluetooth); + #[cfg(feature = "mod-audio")] let (audio_tx, audio_rx) = watch::channel(state.audio); + #[cfg(feature = "mod-dbus")] let (mpris_tx, mpris_rx) = watch::channel(state.mpris); + #[cfg(feature = "mod-dbus")] let (backlight_tx, backlight_rx) = watch::channel(state.backlight); + #[cfg(feature = "mod-dbus")] let (keyboard_tx, keyboard_rx) = watch::channel(state.keyboard); + #[cfg(feature = "mod-dbus")] let (dnd_tx, dnd_rx) = watch::channel(state.dnd); + #[cfg(feature = "mod-bt")] let (bt_force_tx, _) = mpsc::channel(1); + #[cfg(feature = "mod-audio")] let (audio_cmd_tx, _) = mpsc::channel(1); MockState { receivers: AppReceivers { + #[cfg(feature = "mod-network")] network: net_rx, + #[cfg(feature = "mod-hardware")] cpu: cpu_rx, + #[cfg(feature = "mod-hardware")] memory: mem_rx, + #[cfg(feature = "mod-hardware")] sys: sys_rx, + #[cfg(feature = "mod-hardware")] gpu: gpu_rx, + #[cfg(feature = "mod-hardware")] disks: disks_rx, + #[cfg(feature = "mod-bt")] bluetooth: bt_rx, + #[cfg(feature = "mod-audio")] audio: audio_rx, + #[cfg(feature = "mod-dbus")] mpris: mpris_rx, + #[cfg(feature = "mod-dbus")] backlight: backlight_rx, + #[cfg(feature = "mod-dbus")] keyboard: keyboard_rx, + #[cfg(feature = "mod-dbus")] dnd: dnd_rx, health: Arc::new(RwLock::new(state.health)), + #[cfg(feature = "mod-bt")] bt_force_poll: bt_force_tx, + #[cfg(feature = "mod-audio")] audio_cmd_tx, }, + #[cfg(feature = "mod-network")] _net_tx: net_tx, + #[cfg(feature = "mod-hardware")] _cpu_tx: cpu_tx, + #[cfg(feature = "mod-hardware")] _mem_tx: mem_tx, + #[cfg(feature = "mod-hardware")] _sys_tx: sys_tx, + #[cfg(feature = "mod-hardware")] _gpu_tx: gpu_tx, + #[cfg(feature = "mod-hardware")] _disks_tx: disks_tx, + #[cfg(feature = "mod-bt")] _bt_tx: bt_tx, + #[cfg(feature = "mod-audio")] _audio_tx: audio_tx, + #[cfg(feature = "mod-dbus")] _mpris_tx: mpris_tx, + #[cfg(feature = "mod-dbus")] _backlight_tx: backlight_tx, + #[cfg(feature = "mod-dbus")] _keyboard_tx: keyboard_tx, + #[cfg(feature = "mod-dbus")] _dnd_tx: dnd_tx, } } diff --git a/src/utils.rs b/src/utils.rs index 16893ea..4cf95ef 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -70,42 +70,52 @@ pub fn get_hyprland_socket(socket_name: &str) -> Result { use regex::Regex; use std::sync::LazyLock; +pub fn classify_usage(value: f64, high: f64, max: f64) -> &'static str { + if value > max { + "max" + } else if value > high { + "high" + } else { + "normal" + } +} + pub enum TokenValue { Float(f64), Int(i64), String(String), } +pub static TOKEN_RE: LazyLock = + LazyLock::new(|| Regex::new(r"\{([a-zA-Z0-9_]+)(?::([<>\^])?(\d+)?(?:\.(\d+))?)?\}").unwrap()); + pub fn format_template(template: &str, values: &[(K, TokenValue)]) -> String where K: AsRef, { - static RE: LazyLock = LazyLock::new(|| { - Regex::new(r"\{([a-zA-Z0-9_]+)(?::([<>\^])?(\d+)?(?:\.(\d+))?)?\}").unwrap() - }); + TOKEN_RE + .replace_all(template, |caps: ®ex::Captures| { + let name = &caps[1]; + if let Some((_, val)) = values.iter().find(|(k, _)| k.as_ref() == name) { + let align = caps.get(2).map(|m| m.as_str()).unwrap_or(">"); + let width = caps + .get(3) + .map(|m| m.as_str().parse::().unwrap_or(0)) + .unwrap_or(0); + let precision = caps + .get(4) + .map(|m| m.as_str().parse::().unwrap_or(0)); - RE.replace_all(template, |caps: ®ex::Captures| { - let name = &caps[1]; - if let Some((_, val)) = values.iter().find(|(k, _)| k.as_ref() == name) { - let align = caps.get(2).map(|m| m.as_str()).unwrap_or(">"); - let width = caps - .get(3) - .map(|m| m.as_str().parse::().unwrap_or(0)) - .unwrap_or(0); - let precision = caps - .get(4) - .map(|m| m.as_str().parse::().unwrap_or(0)); - - match val { - TokenValue::Float(f) => format_float(*f, align, width, precision), - TokenValue::Int(i) => format_int(*i, align, width), - TokenValue::String(s) => format_str(s, align, width), + match val { + TokenValue::Float(f) => format_float(*f, align, width, precision), + TokenValue::Int(i) => format_int(*i, align, width), + TokenValue::String(s) => format_str(s, align, width), + } + } else { + caps[0].to_string() } - } else { - caps[0].to_string() - } - }) - .into_owned() + }) + .into_owned() } fn format_float(f: f64, align: &str, width: usize, precision: Option) -> String {