feat: show sidetone level in Waybar tooltip

Tooltip now shows "HS80: 42% — Discharging | Sidetone: 10/23" when
the ALSA sidetone control is available. Falls back gracefully to
battery-only tooltip when sidetone cannot be read.

Bump version to 0.1.1.
This commit is contained in:
nevaforget 2026-04-09 17:13:41 +02:00
parent 46978af7b8
commit 960bc60b20
4 changed files with 38 additions and 16 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "corsairctl" name = "corsairctl"
version = "0.1.0" version = "0.1.1"
edition = "2024" edition = "2024"
description = "CLI tool for Corsair Bragi-protocol devices (HS80, etc.)" description = "CLI tool for Corsair Bragi-protocol devices (HS80, etc.)"
license = "MIT" license = "MIT"

View File

@ -58,7 +58,8 @@ fn run() -> error::Result<()> {
let device = bragi::BragiDevice::open_with_verbose(cli.verbose)?; let device = bragi::BragiDevice::open_with_verbose(cli.verbose)?;
let level = device.battery_level()?; let level = device.battery_level()?;
let status = device.battery_status()?; let status = device.battery_status()?;
println!("{}", output::format_waybar_json(level, &status)); let sidetone = sidetone::get_level().ok();
println!("{}", output::format_waybar_json(level, &status, sidetone));
} }
Command::Udev => { Command::Udev => {

View File

@ -26,7 +26,7 @@ pub fn format_info(vid: u16, pid: u16, fw_app: u16, fw_build: u16) -> String {
/// Formatiert Waybar-kompatibles JSON. /// Formatiert Waybar-kompatibles JSON.
/// ///
/// Waybar erwartet: {"text": "...", "tooltip": "...", "class": "...", "percentage": N} /// Waybar erwartet: {"text": "...", "tooltip": "...", "class": "...", "percentage": N}
pub fn format_waybar_json(level: f32, status: &BatteryStatus) -> String { pub fn format_waybar_json(level: f32, status: &BatteryStatus, sidetone: Option<i64>) -> String {
let icon = status.icon(); let icon = status.icon();
let label = status.label(); let label = status.label();
let percentage = level.round() as u32; let percentage = level.round() as u32;
@ -41,9 +41,14 @@ pub fn format_waybar_json(level: f32, status: &BatteryStatus) -> String {
BatteryStatus::Unknown(_) => "unknown", BatteryStatus::Unknown(_) => "unknown",
}; };
let tooltip = match sidetone {
Some(st) => format!("HS80: {percentage}% — {label} | Sidetone: {st}/23"),
None => format!("HS80: {percentage}% — {label}"),
};
let json = serde_json::json!({ let json = serde_json::json!({
"text": format!("{icon} {percentage}%"), "text": format!("{icon} {percentage}%"),
"tooltip": format!("HS80: {percentage}% — {label}"), "tooltip": tooltip,
"class": class, "class": class,
"percentage": percentage, "percentage": percentage,
}); });

View File

@ -45,14 +45,14 @@ fn format_info_values() {
#[test] #[test]
fn waybar_json_is_valid_json() { fn waybar_json_is_valid_json() {
let json_str = output::format_waybar_json(50.0, &BatteryStatus::Discharging); let json_str = output::format_waybar_json(50.0, &BatteryStatus::Discharging, None);
let parsed: serde_json::Value = serde_json::from_str(&json_str).expect("muss valides JSON sein"); let parsed: serde_json::Value = serde_json::from_str(&json_str).expect("muss valides JSON sein");
assert!(parsed.is_object()); assert!(parsed.is_object());
} }
#[test] #[test]
fn waybar_json_has_required_fields() { fn waybar_json_has_required_fields() {
let json_str = output::format_waybar_json(50.0, &BatteryStatus::Discharging); let json_str = output::format_waybar_json(50.0, &BatteryStatus::Discharging, None);
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
assert!(parsed["text"].is_string(), "text fehlt"); assert!(parsed["text"].is_string(), "text fehlt");
assert!(parsed["tooltip"].is_string(), "tooltip fehlt"); assert!(parsed["tooltip"].is_string(), "tooltip fehlt");
@ -62,56 +62,56 @@ fn waybar_json_has_required_fields() {
#[test] #[test]
fn waybar_json_percentage_matches_level() { fn waybar_json_percentage_matches_level() {
let json_str = output::format_waybar_json(73.5, &BatteryStatus::Discharging); let json_str = output::format_waybar_json(73.5, &BatteryStatus::Discharging, None);
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed["percentage"], 74); // 73.5 rounded assert_eq!(parsed["percentage"], 74); // 73.5 rounded
} }
#[test] #[test]
fn waybar_json_class_charging() { fn waybar_json_class_charging() {
let json_str = output::format_waybar_json(50.0, &BatteryStatus::Charging); let json_str = output::format_waybar_json(50.0, &BatteryStatus::Charging, None);
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed["class"], "charging"); assert_eq!(parsed["class"], "charging");
} }
#[test] #[test]
fn waybar_json_class_fully_charged() { fn waybar_json_class_fully_charged() {
let json_str = output::format_waybar_json(100.0, &BatteryStatus::FullyCharged); let json_str = output::format_waybar_json(100.0, &BatteryStatus::FullyCharged, None);
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed["class"], "charging"); assert_eq!(parsed["class"], "charging");
} }
#[test] #[test]
fn waybar_json_class_low() { fn waybar_json_class_low() {
let json_str = output::format_waybar_json(20.0, &BatteryStatus::Low); let json_str = output::format_waybar_json(20.0, &BatteryStatus::Low, None);
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed["class"], "low"); assert_eq!(parsed["class"], "low");
} }
#[test] #[test]
fn waybar_json_class_critical_when_discharging_below_15() { fn waybar_json_class_critical_when_discharging_below_15() {
let json_str = output::format_waybar_json(10.0, &BatteryStatus::Discharging); let json_str = output::format_waybar_json(10.0, &BatteryStatus::Discharging, None);
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed["class"], "critical"); assert_eq!(parsed["class"], "critical");
} }
#[test] #[test]
fn waybar_json_class_warning_when_discharging_below_30() { fn waybar_json_class_warning_when_discharging_below_30() {
let json_str = output::format_waybar_json(25.0, &BatteryStatus::Discharging); let json_str = output::format_waybar_json(25.0, &BatteryStatus::Discharging, None);
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed["class"], "warning"); assert_eq!(parsed["class"], "warning");
} }
#[test] #[test]
fn waybar_json_class_normal_when_discharging_above_30() { fn waybar_json_class_normal_when_discharging_above_30() {
let json_str = output::format_waybar_json(50.0, &BatteryStatus::Discharging); let json_str = output::format_waybar_json(50.0, &BatteryStatus::Discharging, None);
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed["class"], "normal"); assert_eq!(parsed["class"], "normal");
} }
#[test] #[test]
fn waybar_json_class_at_boundary_15() { fn waybar_json_class_at_boundary_15() {
let json_str = output::format_waybar_json(15.0, &BatteryStatus::Discharging); let json_str = output::format_waybar_json(15.0, &BatteryStatus::Discharging, None);
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
// 15.0 ist <= 15.0 → critical // 15.0 ist <= 15.0 → critical
assert_eq!(parsed["class"], "critical"); assert_eq!(parsed["class"], "critical");
@ -119,7 +119,7 @@ fn waybar_json_class_at_boundary_15() {
#[test] #[test]
fn waybar_json_class_at_boundary_30() { fn waybar_json_class_at_boundary_30() {
let json_str = output::format_waybar_json(30.0, &BatteryStatus::Discharging); let json_str = output::format_waybar_json(30.0, &BatteryStatus::Discharging, None);
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
// 30.0 ist <= 30.0 → warning // 30.0 ist <= 30.0 → warning
assert_eq!(parsed["class"], "warning"); assert_eq!(parsed["class"], "warning");
@ -127,7 +127,23 @@ fn waybar_json_class_at_boundary_30() {
#[test] #[test]
fn waybar_json_offline() { fn waybar_json_offline() {
let json_str = output::format_waybar_json(0.0, &BatteryStatus::Offline); let json_str = output::format_waybar_json(0.0, &BatteryStatus::Offline, None);
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
assert_eq!(parsed["class"], "offline"); assert_eq!(parsed["class"], "offline");
} }
#[test]
fn waybar_json_tooltip_without_sidetone() {
let json_str = output::format_waybar_json(50.0, &BatteryStatus::Discharging, None);
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
let tooltip = parsed["tooltip"].as_str().unwrap();
assert!(!tooltip.contains("Sidetone"), "tooltip should not contain sidetone when None");
}
#[test]
fn waybar_json_tooltip_with_sidetone() {
let json_str = output::format_waybar_json(50.0, &BatteryStatus::Discharging, Some(10));
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
let tooltip = parsed["tooltip"].as_str().unwrap();
assert!(tooltip.contains("Sidetone: 10/23"), "tooltip should contain sidetone value");
}