From b3c87b02da13546b579b9cba8b55be57c9af7f2a Mon Sep 17 00:00:00 2001 From: Overlord Date: Tue, 11 Aug 2026 13:44:20 +0200 Subject: [PATCH] Improve error warnings and command behavior for `vmic` - Updated `vmic route` to use "none" instead of "off" for source removal, standardizing terminology. - Added warnings for handling stale `pw-loopback` processes during `vmic edit` operations. - Enhanced logging for `vmic route` to provide detailed feedback on moved streams. - Ensured stable sorting of ports in PipeWire graph for consistent behavior. --- README.md | 2 +- src/cli.rs | 4 ++-- src/commands/edit.rs | 18 ++++++++++++++++++ src/commands/route.rs | 20 ++++++++++++++++++-- src/pw/graph.rs | 8 +++++++- 5 files changed, 46 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 287eec6..7fbc7b7 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ LIBCLANG_PATH = "/home//.local/share/vmic-build/libclang-venv/lib/python3.* vmic create [-l] # create a vmic, optionally with self-monitor vmic route -i # move an app's playback into the vmic vmic route -o # move an app's recording off the vmic -vmic route -s # mix a hardware mic in, or remove it +vmic route -s # mix a hardware mic in, or remove it vmic edit -l # toggle the self-monitor loopback vmic edit -v <0.8|80> # self-monitor loopback volume vmic edit -sv <0.8|80> # mixed-in source volume diff --git a/src/cli.rs b/src/cli.rs index e168579..8d69c9f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -60,8 +60,8 @@ pub struct RouteArgs { #[arg(short, long = "output", value_name = "APP[:MEDIA]")] pub outputs: Vec, - /// Mix a hardware source into the vmic, or "off" to remove it. - #[arg(short, long = "source", value_name = "SOURCE|off")] + /// Mix a hardware source into the vmic, or "none" to remove it. + #[arg(short, long = "source", value_name = "SOURCE|none")] pub source: Option, } diff --git a/src/commands/edit.rs b/src/commands/edit.rs index 3182703..2922eaa 100644 --- a/src/commands/edit.rs +++ b/src/commands/edit.rs @@ -16,6 +16,7 @@ pub fn run(args: EditArgs) -> Result<()> { let name = state::normalize(&args.name); let mut state = VmicState::load(&conn, &name)?; let graph = PwGraph::connect()?; + warn_if_stale(&graph, &state); if let Some(enable) = args.loopback { edit_loopback(&graph, &mut state, enable)?; @@ -30,6 +31,23 @@ pub fn run(args: EditArgs) -> Result<()> { state.save(&conn) } +/// Warns if the tracked pw-loopback processes aren't visible in the live +/// graph, so changes made below may silently fail to apply. +fn warn_if_stale(graph: &PwGraph, state: &VmicState) { + let alive = |id: Option| id.is_some_and(|id| graph.nodes().iter().any(|n| n.id == id)); + if !alive(state.sink_node_id) { + ui::warn(&format!( + "vmic '{}' appears stale (matching pw-loopback process not found); changes may not apply!", + state.name + )); + } else if !alive(state.mix_node_id) { + ui::warn(&format!( + "vmic '{}' mix loopback process not found; -sv changes may not apply!", + state.name + )); + } +} + fn edit_loopback(graph: &PwGraph, state: &mut VmicState, enable: bool) -> Result<()> { match (enable, state.loopback_id) { (true, Some(id)) => { diff --git a/src/commands/route.rs b/src/commands/route.rs index 75d1bae..83bf4b0 100644 --- a/src/commands/route.rs +++ b/src/commands/route.rs @@ -44,12 +44,28 @@ fn move_stream(kind: &str, move_cmd: &str, target: &str, filter: &str) -> Result ui::warn(&format!("'{filter}': matched 0 streams, nothing moved.")); return Ok(()); } + + let mut moved = 0; for m in &matches { - std::process::Command::new("pactl") + let status = std::process::Command::new("pactl") .args([move_cmd, &m.id, target]) .status()?; + if status.success() { + moved += 1; + } + } + + let total = matches.len(); + if moved == total { + ui::info(&format!("'{filter}': matched {total} stream(s) -> moved to '{target}'.")); + } else if moved == 0 { + ui::warn(&format!("'{filter}': matched {total} stream(s), but none could be moved to '{target}'.")); + } else { + ui::warn(&format!( + "'{filter}': matched {total} stream(s), moved {moved} to '{target}' ({} failed).", + total - moved + )); } - ui::info(&format!("'{filter}': matched {} stream(s) -> moved to '{target}'.", matches.len())); Ok(()) } diff --git a/src/pw/graph.rs b/src/pw/graph.rs index 07752ed..73ce47a 100644 --- a/src/pw/graph.rs +++ b/src/pw/graph.rs @@ -165,7 +165,13 @@ impl PwGraph { let Some(node) = s.nodes.values().find(|n| n.name == node_name) else { return Vec::new(); }; - s.ports.values().filter(|p| p.node_id == node.id).cloned().collect() + // Ports come out of a HashMap, whose iteration order is randomized + // per process - sort by id (assigned monotonically by PipeWire) so + // callers that fall back to "first"/"second" port (see + // `loopback::resolve_link_pairs`) get a stable, reproducible pick. + let mut ports: Vec = s.ports.values().filter(|p| p.node_id == node.id).cloned().collect(); + ports.sort_by_key(|p| p.id); + ports } pub fn links(&self) -> Vec {