diff --git a/example.config.toml b/example.config.toml
index ca14178..f3e9e68 100644
--- a/example.config.toml
+++ b/example.config.toml
@@ -29,40 +29,49 @@ backlight = 13
dnd = 14
[network]
+# enabled = false # set to false to disable this module at runtime
# tokens: {interface}, {ip}, {rx}, {tx}
format = "{interface} ({ip}): {rx:^4.1} MB/s {tx:^4.1} MB/s"
[cpu]
+# enabled = false # set to false to disable this module at runtime
# tokens: {usage}, {temp}, {model}
format = "CPU: {usage:^4.1}% {temp:^4.1}C"
[memory]
+# enabled = false # set to false to disable this module at runtime
# tokens: {used}, {total}
format = "MEM: {used:^4.1}/{total:^4.1}GB"
[gpu]
+# enabled = false # set to false to disable this module at runtime
# tokens: {usage}, {vram_used}, {vram_total}, {temp}
format_amd = "AMD: {usage:>3.0}% {vram_used:>4.1}/{vram_total:>4.1}GB {temp:>4.1}C"
format_intel = "iGPU: {usage:>3.0}%"
format_nvidia = "NV: {usage:>3.0}% {vram_used:>4.1}/{vram_total:>4.1}GB {temp:>4.1}C"
[sys]
+# enabled = false # set to false to disable this module at runtime
# tokens: {uptime}, {load1}, {load5}, {load15}, {procs}
format = "UP: {uptime} LOAD: {load1:^3.1} "
[disk]
+# enabled = false # set to false to disable this module at runtime
# tokens: {mount}, {used}, {total}
format = "{mount} {used:^3.0}/{total:^3.0}G"
[pool]
+# enabled = false # set to false to disable this module at runtime
# tokens: {used}, {total}
format = "{used:>4.0}G / {total:>4.0}G"
[power]
+# enabled = false # set to false to disable this module at runtime
# tokens: {percentage}, {icon}
format = "{percentage:>3}% {icon}"
[audio]
+# enabled = false # set to false to disable this module at runtime
# tokens: {name}, {volume}, {icon}
format_sink_unmuted = "{name} {volume:>3}% {icon}"
format_sink_muted = "{name} {icon}"
@@ -70,6 +79,7 @@ format_source_unmuted = "{name} {volume:>3}% {icon}"
format_source_muted = "{name} {icon}"
[bt]
+# enabled = false # set to false to disable this module at runtime
# tokens: {alias}, {mac}, {left}, {right}, {anc}
format_plugin = "{alias} [{left}|{right}] {anc} "
format_connected = " {alias}"
@@ -77,21 +87,26 @@ format_disconnected = " Disconnected"
format_disabled = " Off"
[game]
+# enabled = false # set to false to disable this module at runtime
format_active = ""
format_inactive = ""
[mpris]
+# enabled = false # set to false to disable this module at runtime
# tokens: {artist}, {title}, {album}, {status_icon}
format = "{status_icon} {artist} - {title}"
[backlight]
+# enabled = false # set to false to disable this module at runtime
# tokens: {percentage}, {icon}
format = "{percentage:>3}% {icon}"
[keyboard]
+# enabled = false # set to false to disable this module at runtime
# tokens: {layout}
format = "{layout}"
[dnd]
+# enabled = false # set to false to disable this module at runtime
format_dnd = ""
format_normal = ""
diff --git a/src/config.rs b/src/config.rs
index 2d38aab..eb3e33a 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -91,12 +91,15 @@ pub struct SignalsConfig {
#[derive(Deserialize, Clone)]
pub struct NetworkConfig {
+ #[serde(default = "default_true")]
+ pub enabled: bool,
pub format: String,
}
impl Default for NetworkConfig {
fn default() -> Self {
Self {
+ enabled: true,
format: "{interface} ({ip}): {rx:>5.2} MB/s {tx:>5.2} MB/s".to_string(),
}
}
@@ -104,12 +107,15 @@ impl Default for NetworkConfig {
#[derive(Deserialize, Clone)]
pub struct CpuConfig {
+ #[serde(default = "default_true")]
+ pub enabled: bool,
pub format: String,
}
impl Default for CpuConfig {
fn default() -> Self {
Self {
+ enabled: true,
format: "CPU: {usage:>4.1}% {temp:>4.1}C".to_string(),
}
}
@@ -117,12 +123,15 @@ impl Default for CpuConfig {
#[derive(Deserialize, Clone)]
pub struct MemoryConfig {
+ #[serde(default = "default_true")]
+ pub enabled: bool,
pub format: String,
}
impl Default for MemoryConfig {
fn default() -> Self {
Self {
+ enabled: true,
format: "{used:>5.2}/{total:>5.2}GB".to_string(),
}
}
@@ -130,6 +139,8 @@ impl Default for MemoryConfig {
#[derive(Deserialize, Clone)]
pub struct GpuConfig {
+ #[serde(default = "default_true")]
+ pub enabled: bool,
pub format_amd: String,
pub format_intel: String,
pub format_nvidia: String,
@@ -138,6 +149,7 @@ pub struct GpuConfig {
impl Default for GpuConfig {
fn default() -> Self {
Self {
+ enabled: true,
format_amd: "AMD: {usage:>3.0}% {vram_used:>4.1}/{vram_total:>4.1}GB {temp:>4.1}C"
.to_string(),
format_intel: "iGPU: {usage:>3.0}%".to_string(),
@@ -149,12 +161,15 @@ impl Default for GpuConfig {
#[derive(Deserialize, Clone)]
pub struct SysConfig {
+ #[serde(default = "default_true")]
+ pub enabled: bool,
pub format: String,
}
impl Default for SysConfig {
fn default() -> Self {
Self {
+ enabled: true,
format: "UP: {uptime} | LOAD: {load1:>4.2} {load5:>4.2} {load15:>4.2}".to_string(),
}
}
@@ -162,12 +177,15 @@ impl Default for SysConfig {
#[derive(Deserialize, Clone)]
pub struct DiskConfig {
+ #[serde(default = "default_true")]
+ pub enabled: bool,
pub format: String,
}
impl Default for DiskConfig {
fn default() -> Self {
Self {
+ enabled: true,
format: "{mount} {used:>5.1}/{total:>5.1}G".to_string(),
}
}
@@ -175,12 +193,15 @@ impl Default for DiskConfig {
#[derive(Deserialize, Clone)]
pub struct PoolConfig {
+ #[serde(default = "default_true")]
+ pub enabled: bool,
pub format: String,
}
impl Default for PoolConfig {
fn default() -> Self {
Self {
+ enabled: true,
format: "{used:>4.0}G / {total:>4.0}G".to_string(),
}
}
@@ -188,12 +209,15 @@ impl Default for PoolConfig {
#[derive(Deserialize, Clone)]
pub struct PowerConfig {
+ #[serde(default = "default_true")]
+ pub enabled: bool,
pub format: String,
}
impl Default for PowerConfig {
fn default() -> Self {
Self {
+ enabled: true,
format: "{percentage:>3}% {icon}".to_string(),
}
}
@@ -201,6 +225,8 @@ impl Default for PowerConfig {
#[derive(Deserialize, Clone)]
pub struct AudioConfig {
+ #[serde(default = "default_true")]
+ pub enabled: bool,
pub format_sink_unmuted: String,
pub format_sink_muted: String,
pub format_source_unmuted: String,
@@ -210,6 +236,7 @@ pub struct AudioConfig {
impl Default for AudioConfig {
fn default() -> Self {
Self {
+ enabled: true,
format_sink_unmuted: "{name} {volume:>3}% {icon}".to_string(),
format_sink_muted: "{name} {icon}".to_string(),
format_source_unmuted: "{name} {volume:>3}% {icon}".to_string(),
@@ -220,6 +247,8 @@ impl Default for AudioConfig {
#[derive(Deserialize, Clone)]
pub struct BtConfig {
+ #[serde(default = "default_true")]
+ pub enabled: bool,
pub format_connected: String,
pub format_plugin: String,
pub format_disconnected: String,
@@ -229,6 +258,7 @@ pub struct BtConfig {
impl Default for BtConfig {
fn default() -> Self {
Self {
+ enabled: true,
format_connected: "{alias} ".to_string(),
format_plugin: "{alias} [{left}|{right}] {anc} ".to_string(),
format_disconnected: "".to_string(),
@@ -239,6 +269,8 @@ impl Default for BtConfig {
#[derive(Deserialize, Clone)]
pub struct GameConfig {
+ #[serde(default = "default_true")]
+ pub enabled: bool,
pub format_active: String,
pub format_inactive: String,
}
@@ -246,6 +278,7 @@ pub struct GameConfig {
impl Default for GameConfig {
fn default() -> Self {
Self {
+ enabled: true,
format_active: "".to_string(),
format_inactive: "".to_string(),
}
@@ -254,12 +287,15 @@ impl Default for GameConfig {
#[derive(Deserialize, Clone)]
pub struct MprisConfig {
+ #[serde(default = "default_true")]
+ pub enabled: bool,
pub format: String,
}
impl Default for MprisConfig {
fn default() -> Self {
Self {
+ enabled: true,
format: "{status_icon} {artist} - {title}".to_string(),
}
}
@@ -267,12 +303,15 @@ impl Default for MprisConfig {
#[derive(Deserialize, Clone)]
pub struct BacklightConfig {
+ #[serde(default = "default_true")]
+ pub enabled: bool,
pub format: String,
}
impl Default for BacklightConfig {
fn default() -> Self {
Self {
+ enabled: true,
format: "{percentage:>3}% {icon}".to_string(),
}
}
@@ -280,12 +319,15 @@ impl Default for BacklightConfig {
#[derive(Deserialize, Clone)]
pub struct KeyboardConfig {
+ #[serde(default = "default_true")]
+ pub enabled: bool,
pub format: String,
}
impl Default for KeyboardConfig {
fn default() -> Self {
Self {
+ enabled: true,
format: "{layout}".to_string(),
}
}
@@ -293,13 +335,20 @@ impl Default for KeyboardConfig {
#[derive(Deserialize, Clone)]
pub struct DndConfig {
+ #[serde(default = "default_true")]
+ pub enabled: bool,
pub format_dnd: String,
pub format_normal: String,
}
+fn default_true() -> bool {
+ true
+}
+
impl Default for DndConfig {
fn default() -> Self {
Self {
+ enabled: true,
format_dnd: "".to_string(),
format_normal: "".to_string(),
}
@@ -325,6 +374,44 @@ fn validate_format(label: &str, format_str: &str, known_tokens: &[&str]) {
}
impl Config {
+ /// Check if a module is enabled in the configuration.
+ /// Returns false if the module is explicitly disabled; true if enabled or unknown.
+ pub fn is_module_enabled(&self, module_name: &str) -> bool {
+ match module_name {
+ #[cfg(feature = "mod-network")]
+ "net" | "network" => self.network.enabled,
+ #[cfg(feature = "mod-hardware")]
+ "cpu" => self.cpu.enabled,
+ #[cfg(feature = "mod-hardware")]
+ "mem" | "memory" => self.memory.enabled,
+ #[cfg(feature = "mod-hardware")]
+ "gpu" => self.gpu.enabled,
+ #[cfg(feature = "mod-hardware")]
+ "sys" => self.sys.enabled,
+ #[cfg(feature = "mod-hardware")]
+ "disk" => self.disk.enabled,
+ #[cfg(feature = "mod-hardware")]
+ "pool" | "btrfs" => self.pool.enabled,
+ #[cfg(feature = "mod-hardware")]
+ "power" => self.power.enabled,
+ #[cfg(feature = "mod-hardware")]
+ "game" => self.game.enabled,
+ #[cfg(feature = "mod-audio")]
+ "vol" | "audio" | "mic" => self.audio.enabled,
+ #[cfg(feature = "mod-bt")]
+ "bt" | "bluetooth" => self.bt.enabled,
+ #[cfg(feature = "mod-dbus")]
+ "mpris" => self.mpris.enabled,
+ #[cfg(feature = "mod-dbus")]
+ "backlight" => self.backlight.enabled,
+ #[cfg(feature = "mod-dbus")]
+ "kbd" | "keyboard" => self.keyboard.enabled,
+ #[cfg(feature = "mod-dbus")]
+ "dnd" => self.dnd.enabled,
+ _ => true,
+ }
+ }
+
pub fn validate(&self) {
#[cfg(feature = "mod-network")]
validate_format(
diff --git a/src/daemon.rs b/src/daemon.rs
index 7b7d120..7df1990 100644
--- a/src/daemon.rs
+++ b/src/daemon.rs
@@ -183,7 +183,7 @@ pub async fn run_daemon(config_path: Option) -> Result<()> {
// 1. Network Task
#[cfg(feature = "mod-network")]
- {
+ if config.read().await.network.enabled {
let token = cancel_token.clone();
let net_health = Arc::clone(&health);
tokio::spawn(async move {
@@ -206,48 +206,58 @@ pub async fn run_daemon(config_path: Option) -> Result<()> {
// 2. Fast Hardware Task (CPU, Mem, Load)
#[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;
+ let cfg = config.read().await;
+ let fast_enabled = cfg.cpu.enabled || cfg.memory.enabled || cfg.sys.enabled;
+ drop(cfg);
+ if fast_enabled {
+ 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)
#[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;
+ let cfg = config.read().await;
+ let slow_enabled = cfg.gpu.enabled || cfg.disk.enabled;
+ drop(cfg);
+ if slow_enabled {
+ 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
#[cfg(feature = "mod-bt")]
- {
+ if config.read().await.bt.enabled {
let token = cancel_token.clone();
let bt_health = Arc::clone(&health);
let poll_config = Arc::clone(&config);
@@ -272,35 +282,35 @@ pub async fn run_daemon(config_path: Option) -> Result<()> {
// 5. Audio Thread (Event driven)
#[cfg(feature = "mod-audio")]
- {
+ if config.read().await.audio.enabled {
let audio_daemon = AudioDaemon::new();
audio_daemon.start(&audio_tx, audio_cmd_rx);
}
// 5.1 Backlight Thread (Event driven)
#[cfg(feature = "mod-dbus")]
- {
+ if config.read().await.backlight.enabled {
let backlight_daemon = BacklightDaemon::new();
backlight_daemon.start(backlight_tx);
}
// 5.2 Keyboard Thread (Event driven)
#[cfg(feature = "mod-dbus")]
- {
+ if config.read().await.keyboard.enabled {
let keyboard_daemon = KeyboardDaemon::new();
keyboard_daemon.start(keyboard_tx);
}
// 5.3 DND Thread (Event driven)
#[cfg(feature = "mod-dbus")]
- {
+ if config.read().await.dnd.enabled {
let dnd_daemon = DndDaemon::new();
dnd_daemon.start(dnd_tx);
}
// 5.4 MPRIS Thread
#[cfg(feature = "mod-dbus")]
- {
+ if config.read().await.mpris.enabled {
let mpris_daemon = MprisDaemon::new();
mpris_daemon.start(mpris_tx);
}
@@ -416,6 +426,9 @@ async fn handle_request(
match result {
Ok(output) => serde_json::to_string(&output).unwrap_or_else(|_| "{}".to_string()),
+ Err(crate::error::FluxoError::Disabled(_)) => {
+ "{\"text\":\"\",\"tooltip\":\"Module disabled\",\"class\":\"disabled\"}".to_string()
+ }
Err(e) => crate::health::error_response(module_name, &e, cached_output),
}
}
diff --git a/src/error.rs b/src/error.rs
index a8c669c..4a8c78b 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -33,6 +33,9 @@ pub enum FluxoError {
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
+ #[error("Module disabled: {0}")]
+ Disabled(String),
+
#[error("Other error: {0}")]
Other(#[from] anyhow::Error),
}
diff --git a/src/health.rs b/src/health.rs
index a1260ad..bff2e45 100644
--- a/src/health.rs
+++ b/src/health.rs
@@ -36,6 +36,7 @@ pub async fn update_health(
health.backoff_until = None;
health.last_successful_output = Some(output.clone());
}
+ Err(crate::error::FluxoError::Disabled(_)) => {}
Err(e) => {
health.consecutive_failures += 1;
health.last_failure = Some(Instant::now());
diff --git a/src/registry.rs b/src/registry.rs
index 006f42f..d70dda0 100644
--- a/src/registry.rs
+++ b/src/registry.rs
@@ -12,6 +12,10 @@ pub async fn dispatch(
#[allow(unused)] state: &AppReceivers,
#[allow(unused)] args: &[&str],
) -> FluxoResult {
+ if !config.is_module_enabled(module_name) {
+ return Err(FluxoError::Disabled(module_name.to_string()));
+ }
+
match module_name {
#[cfg(feature = "mod-network")]
"net" | "network" => {