refactor(audio): extract pure parse_pw_nodes from device enumeration
Splits the pw-cli output parsing out of enumerate_audio_devices into a pure fn parse_pw_nodes(&str) -> Vec<AudioDevice> (with a push_device helper), leaving only the subprocess call in enumerate_audio_devices. Behavior- preserving — same id-block boundaries, Audio/* filter, Source=>input, description-falls-back-to-name, and sort-by-description. Creates a testable seam (the parsing had zero coverage). Build + clippy clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+46
-39
@@ -14,52 +14,59 @@ impl std::fmt::Display for AudioDevice {
|
||||
}
|
||||
|
||||
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",
|
||||
});
|
||||
match output {
|
||||
Ok(out) => parse_pw_nodes(&String::from_utf8_lossy(&out.stdout)),
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits the in-progress node as an `AudioDevice` if it's a complete Audio/*
|
||||
/// node, then resets the accumulators for the next block. Non-audio or
|
||||
/// 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/") {
|
||||
out.push(AudioDevice {
|
||||
name: name.clone(),
|
||||
description: if desc.is_empty() { name.clone() } else { desc.clone() },
|
||||
is_input: class == "Audio/Source",
|
||||
});
|
||||
}
|
||||
name.clear();
|
||||
desc.clear();
|
||||
class.clear();
|
||||
}
|
||||
|
||||
/// Parses the text of `pw-cli list-objects Node` into the audio devices we care
|
||||
/// about. Each object is a block introduced by an `id N, ...` line; within a
|
||||
/// block we collect `node.name` / `node.description` / `media.class`, and a
|
||||
/// device is emitted at the next `id` (and at EOF) when the class is `Audio/*`
|
||||
/// (`Audio/Source` => input). Description falls back to the node name when
|
||||
/// absent. Returned sorted by description for stable UI display.
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// Sort devices for consistent display
|
||||
push_device(&mut current_name, &mut current_desc, &mut current_class, &mut devices);
|
||||
|
||||
devices.sort_by(|a, b| a.description.cmp(&b.description));
|
||||
devices
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user