feat: Add persistent configuration and enumerate PipeWire audio devices

This commit is contained in:
2026-05-27 16:03:07 -04:00
parent e0ba002c12
commit dac53fc2ad
8 changed files with 352 additions and 24 deletions
+65
View File
@@ -0,0 +1,65 @@
use std::process::Command;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AudioDevice {
pub name: String,
pub description: String,
pub is_input: bool,
}
impl std::fmt::Display for AudioDevice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.description)
}
}
pub fn enumerate_audio_devices() -> Vec<AudioDevice> {
let mut devices = Vec::new();
let output = Command::new("pw-cli")
.arg("list-objects")
.arg("Node")
.output();
if let Ok(out) = output {
let text = String::from_utf8_lossy(&out.stdout);
let mut current_name = String::new();
let mut current_desc = String::new();
let mut current_class = String::new();
for line in text.lines() {
let line = line.trim();
if line.starts_with("id ") {
// Save previous if valid
if !current_name.is_empty() && current_class.starts_with("Audio/") {
devices.push(AudioDevice {
name: current_name.clone(),
description: if current_desc.is_empty() { current_name.clone() } else { current_desc.clone() },
is_input: current_class == "Audio/Source",
});
}
current_name.clear();
current_desc.clear();
current_class.clear();
} else if let Some(val) = line.strip_prefix("node.name = \"") {
current_name = val.trim_end_matches('"').to_string();
} else if let Some(val) = line.strip_prefix("node.description = \"") {
current_desc = val.trim_end_matches('"').to_string();
} else if let Some(val) = line.strip_prefix("media.class = \"") {
current_class = val.trim_end_matches('"').to_string();
}
}
// Final one
if !current_name.is_empty() && current_class.starts_with("Audio/") {
devices.push(AudioDevice {
name: current_name.clone(),
description: if current_desc.is_empty() { current_name } else { current_desc },
is_input: current_class == "Audio/Source",
});
}
}
// Sort devices for consistent display
devices.sort_by(|a, b| a.description.cmp(&b.description));
devices
}