Merge branch 'refactor/pw-cli-parse-seam': pure parse_pw_nodes seam

This commit is contained in:
2026-06-02 17:03:25 -04:00
+46 -39
View File
@@ -14,52 +14,59 @@ impl std::fmt::Display for AudioDevice {
} }
pub fn enumerate_audio_devices() -> Vec<AudioDevice> { pub fn enumerate_audio_devices() -> Vec<AudioDevice> {
let mut devices = Vec::new();
let output = Command::new("pw-cli") let output = Command::new("pw-cli")
.arg("list-objects") .arg("list-objects")
.arg("Node") .arg("Node")
.output(); .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() { match output {
let line = line.trim(); Ok(out) => parse_pw_nodes(&String::from_utf8_lossy(&out.stdout)),
if line.starts_with("id ") { Err(_) => Vec::new(),
// Save previous if valid }
if !current_name.is_empty() && current_class.starts_with("Audio/") { }
devices.push(AudioDevice {
name: current_name.clone(), /// Emits the in-progress node as an `AudioDevice` if it's a complete Audio/*
description: if current_desc.is_empty() { current_name.clone() } else { current_desc.clone() }, /// node, then resets the accumulators for the next block. Non-audio or
is_input: current_class == "Audio/Source", /// incomplete blocks are dropped (but still reset).
}); fn push_device(name: &mut String, desc: &mut String, class: &mut String, out: &mut Vec<AudioDevice>) {
} if !name.is_empty() && class.starts_with("Audio/") {
current_name.clear(); out.push(AudioDevice {
current_desc.clear(); name: name.clone(),
current_class.clear(); description: if desc.is_empty() { name.clone() } else { desc.clone() },
} else if let Some(val) = line.strip_prefix("node.name = \"") { is_input: class == "Audio/Source",
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(); name.clear();
} else if let Some(val) = line.strip_prefix("media.class = \"") { desc.clear();
current_class = val.trim_end_matches('"').to_string(); class.clear();
} }
}
/// Parses the text of `pw-cli list-objects Node` into the audio devices we care
// Final one /// about. Each object is a block introduced by an `id N, ...` line; within a
if !current_name.is_empty() && current_class.starts_with("Audio/") { /// block we collect `node.name` / `node.description` / `media.class`, and a
devices.push(AudioDevice { /// device is emitted at the next `id` (and at EOF) when the class is `Audio/*`
name: current_name.clone(), /// (`Audio/Source` => input). Description falls back to the node name when
description: if current_desc.is_empty() { current_name } else { current_desc }, /// absent. Returned sorted by description for stable UI display.
is_input: current_class == "Audio/Source", fn parse_pw_nodes(text: &str) -> Vec<AudioDevice> {
}); let mut devices = Vec::new();
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 ") {
push_device(&mut current_name, &mut current_desc, &mut current_class, &mut devices);
} 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();
} }
} }
push_device(&mut current_name, &mut current_desc, &mut current_class, &mut devices);
// Sort devices for consistent display
devices.sort_by(|a, b| a.description.cmp(&b.description)); devices.sort_by(|a, b| a.description.cmp(&b.description));
devices devices
} }