From e19fb69c72d8953f794aa8611f7b35a947d7afa3 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Fri, 13 Mar 2026 16:53:31 +0100 Subject: [PATCH] added power, pixelbuds and gamemode support --- src/daemon.rs | 3 ++ src/main.rs | 18 +++++++ src/modules/buds.rs | 123 +++++++++++++++++++++++++++++++++++++++++++ src/modules/game.rs | 45 ++++++++++++++++ src/modules/mod.rs | 3 ++ src/modules/power.rs | 100 +++++++++++++++++++++++++++++++++++ 6 files changed, 292 insertions(+) create mode 100644 src/modules/buds.rs create mode 100644 src/modules/game.rs create mode 100644 src/modules/power.rs diff --git a/src/daemon.rs b/src/daemon.rs index fc47de8..d817123 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -86,6 +86,9 @@ fn handle_request(module_name: &str, args: &[&str], state: &SharedState, config: "gpu" => crate::modules::gpu::GpuModule.run(config, state, args), "sys" => crate::modules::sys::SysModule.run(config, state, args), "bt" | "bluetooth" => crate::modules::bt::BtModule.run(config, state, args), + "buds" => crate::modules::buds::BudsModule.run(config, state, args), + "power" => crate::modules::power::PowerModule.run(config, state, args), + "game" => crate::modules::game::GameModule.run(config, state, args), _ => { warn!("Received request for unknown module: '{}'", module_name); Err(anyhow::anyhow!("Unknown module: {}", module_name)) diff --git a/src/main.rs b/src/main.rs index 2446625..4eccf47 100644 --- a/src/main.rs +++ b/src/main.rs @@ -63,6 +63,15 @@ enum Commands { #[arg(default_value = "show")] action: String, }, + /// Pixel Buds Pro ANC and Battery + Buds { + #[arg(default_value = "show")] + action: String, + }, + /// System power and battery status + Power, + /// Hyprland gamemode status + Game, } fn main() { @@ -114,6 +123,15 @@ fn main() { Commands::Bt { action } => { handle_ipc_response(ipc::request_data("bt", &[action.clone()])); } + Commands::Buds { action } => { + handle_ipc_response(ipc::request_data("buds", &[action.clone()])); + } + Commands::Power => { + handle_ipc_response(ipc::request_data("power", &[])); + } + Commands::Game => { + handle_ipc_response(ipc::request_data("game", &[])); + } } } diff --git a/src/modules/buds.rs b/src/modules/buds.rs new file mode 100644 index 0000000..8470e97 --- /dev/null +++ b/src/modules/buds.rs @@ -0,0 +1,123 @@ +use crate::config::Config; +use crate::modules::WaybarModule; +use crate::output::WaybarOutput; +use crate::state::SharedState; +use anyhow::Result; +use std::process::Command; + +pub struct BudsModule; + +const MAC_ADDRESS: &str = "B4:23:A2:09:D3:53"; + +impl WaybarModule for BudsModule { + fn run(&self, _config: &Config, _state: &SharedState, args: &[&str]) -> Result { + let action = args.first().unwrap_or(&"show"); + + match *action { + "cycle_anc" => { + let output = Command::new("pbpctrl").args(["get", "anc"]).output()?; + let current_mode = String::from_utf8_lossy(&output.stdout).trim().to_string(); + + let next_mode = match current_mode.as_str() { + "active" => "aware", + "aware" => "off", + _ => "active", // default or off goes to active + }; + + Command::new("pbpctrl").args(["set", "anc", next_mode]).status()?; + return Ok(WaybarOutput { + text: String::new(), + tooltip: None, + class: None, + percentage: None, + }); + } + "connect" => { + Command::new("bluetoothctl").args(["connect", MAC_ADDRESS]).status()?; + return Ok(WaybarOutput { + text: String::new(), + tooltip: None, + class: None, + percentage: None, + }); + } + "disconnect" => { + Command::new("bluetoothctl").args(["disconnect", MAC_ADDRESS]).status()?; + return Ok(WaybarOutput { + text: String::new(), + tooltip: None, + class: None, + percentage: None, + }); + } + "show" | _ => {} + } + + // Check if connected + let bt_info = Command::new("bluetoothctl").args(["info", MAC_ADDRESS]).output()?; + let bt_str = String::from_utf8_lossy(&bt_info.stdout); + + if !bt_str.contains("Connected: yes") { + return Ok(WaybarOutput { + text: "".to_string(), + tooltip: Some("Pixel Buds Pro 2 not connected".to_string()), + class: Some("disconnected".to_string()), + percentage: None, + }); + } + + // Get battery output + let bat_cmd = Command::new("pbpctrl").args(["show", "battery"]).output(); + if bat_cmd.is_err() || !bat_cmd.as_ref().unwrap().status.success() { + return Ok(WaybarOutput { + text: "".to_string(), + tooltip: Some("Pixel Buds Pro 2 connected (No Data)".to_string()), + class: Some("disconnected".to_string()), + percentage: None, + }); + } + + let bat_result = bat_cmd.unwrap(); + let bat_output = String::from_utf8_lossy(&bat_result.stdout); + let mut left_bud = "unknown"; + let mut right_bud = "unknown"; + + for line in bat_output.lines() { + if line.contains("left bud:") { + left_bud = line.split_whitespace().nth(2).unwrap_or("unknown"); + } else if line.contains("right bud:") { + right_bud = line.split_whitespace().nth(2).unwrap_or("unknown"); + } + } + + if left_bud == "unknown" && right_bud == "unknown" { + return Ok(WaybarOutput { + text: "{}".to_string(), + tooltip: None, + class: None, + percentage: None, + }); + } + + let left_display = if left_bud == "unknown" { "L: ---".to_string() } else { format!("L: {}", left_bud) }; + let right_display = if right_bud == "unknown" { "R: ---".to_string() } else { format!("R: {}", right_bud) }; + + // Get ANC info + let anc_cmd = Command::new("pbpctrl").args(["get", "anc"]).output()?; + let current_mode = String::from_utf8_lossy(&anc_cmd.stdout).trim().to_string(); + + let (anc_icon, class) = match current_mode.as_str() { + "active" => ("ANC", "anc-active"), + "aware" => ("Aware", "anc-aware"), + "off" => ("Off", "anc-off"), + _ => ("?", "anc-unknown"), + }; + + Ok(WaybarOutput { + text: format!("{} | {} | {}", left_display, right_display, anc_icon), + tooltip: Some("Pixel Buds Pro 2".to_string()), + class: Some(class.to_string()), + percentage: None, + }) + } +} diff --git a/src/modules/game.rs b/src/modules/game.rs new file mode 100644 index 0000000..b627dde --- /dev/null +++ b/src/modules/game.rs @@ -0,0 +1,45 @@ +use crate::config::Config; +use crate::modules::WaybarModule; +use crate::output::WaybarOutput; +use crate::state::SharedState; +use anyhow::Result; +use std::process::Command; + +pub struct GameModule; + +impl WaybarModule for GameModule { + fn run(&self, _config: &Config, _state: &SharedState, _args: &[&str]) -> Result { + let output = Command::new("hyprctl") + .args(["getoption", "animations:enabled", "-j"]) + .output(); + + let mut is_gamemode = false; // default to deactivated + + if let Ok(out) = output { + let stdout = String::from_utf8_lossy(&out.stdout); + + // The JSON from hyprctl looks like {"int": 0, "float": 0.0, ...} + // If int is 0, animations are disabled (Gamemode active) + // If int is 1, animations are enabled (Gamemode deactivated) + if stdout.contains("\"int\": 0") { + is_gamemode = true; + } + } + + if is_gamemode { + Ok(WaybarOutput { + text: "󰊖".to_string(), + tooltip: Some("Gamemode activated".to_string()), + class: Some("active".to_string()), + percentage: None, + }) + } else { + Ok(WaybarOutput { + text: "".to_string(), + tooltip: Some("Gamemode deactivated".to_string()), + class: None, + percentage: None, + }) + } + } +} diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 331fb18..b0f809c 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -8,6 +8,9 @@ pub mod audio; pub mod gpu; pub mod sys; pub mod bt; +pub mod buds; +pub mod power; +pub mod game; use crate::config::Config; use crate::output::WaybarOutput; diff --git a/src/modules/power.rs b/src/modules/power.rs new file mode 100644 index 0000000..469ce1c --- /dev/null +++ b/src/modules/power.rs @@ -0,0 +1,100 @@ +use crate::config::Config; +use crate::modules::WaybarModule; +use crate::output::WaybarOutput; +use crate::state::SharedState; +use anyhow::Result; +use std::fs; + +pub struct PowerModule; + +impl WaybarModule for PowerModule { + fn run(&self, _config: &Config, _state: &SharedState, _args: &[&str]) -> Result { + let critical_threshold = 15; + let warning_threshold = 50; + + // Find the first battery + let mut battery_path = None; + if let Ok(entries) = fs::read_dir("/sys/class/power_supply") { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if name.starts_with("BAT") { + battery_path = Some(entry.path()); + break; + } + } + } + + // Check AC status as fallback or TLP proxy + let mut ac_online = false; + if let Ok(entries) = fs::read_dir("/sys/class/power_supply") { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if name.starts_with("AC") || name.starts_with("ADP") { + let online_path = entry.path().join("online"); + if let Ok(online_str) = fs::read_to_string(online_path) { + if online_str.trim() == "1" { + ac_online = true; + break; + } + } + } + } + } + + let Some(bat_path) = battery_path else { + if ac_online { + return Ok(WaybarOutput { + text: "".to_string(), + tooltip: Some("AC Power (No Battery)".to_string()), + class: Some("ac".to_string()), + percentage: None, + }); + } else { + return Ok(WaybarOutput { + text: "".to_string(), + tooltip: Some("Error: Battery not found".to_string()), + class: Some("unknown".to_string()), + percentage: None, + }); + } + }; + + // Read battery capacity and status + let capacity_str = fs::read_to_string(bat_path.join("capacity")).unwrap_or_else(|_| "0".to_string()); + let percentage: u8 = capacity_str.trim().parse().unwrap_or(0); + let status_str = fs::read_to_string(bat_path.join("status")).unwrap_or_else(|_| "Unknown".to_string()); + let state = status_str.trim().to_lowercase(); + + let (icon, class, tooltip) = if state == "charging" || ac_online { + ( + "", + "charging", + format!("TLP: AC | Charging at {}%", percentage), + ) + } else if state == "discharging" { + let t = format!("TLP: Battery | Discharging at {}%", percentage); + if percentage <= critical_threshold { + ("", "critical", t) + } else if percentage <= warning_threshold { + ("", "warning", t) + } else if percentage <= 85 { + ("", "bat", t) + } else { + ("", "bat", t) + } + } else { + ( + "", + "charging", + format!("TLP: AC | Fully Charged at {}%", percentage), + ) + }; + + Ok(WaybarOutput { + text: format!("{}% {}", percentage, icon), + tooltip: Some(tooltip), + class: Some(class.to_string()), + percentage: Some(percentage), + }) + } +}