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.
This commit is contained in:
2026-08-11 13:44:20 +02:00
parent e19f398397
commit b3c87b02da
5 changed files with 46 additions and 6 deletions

View File

@@ -48,7 +48,7 @@ LIBCLANG_PATH = "/home/<you>/.local/share/vmic-build/libclang-venv/lib/python3.*
vmic create <name> [-l] # create a vmic, optionally with self-monitor
vmic route <name> -i <app[:media]> # move an app's playback into the vmic
vmic route <name> -o <app[:media]> # move an app's recording off the vmic
vmic route <name> -s <source|off> # mix a hardware mic in, or remove it
vmic route <name> -s <source|none> # mix a hardware mic in, or remove it
vmic edit <name> -l <true|false> # toggle the self-monitor loopback
vmic edit <name> -v <0.8|80> # self-monitor loopback volume
vmic edit <name> -sv <0.8|80> # mixed-in source volume

View File

@@ -60,8 +60,8 @@ pub struct RouteArgs {
#[arg(short, long = "output", value_name = "APP[:MEDIA]")]
pub outputs: Vec<String>,
/// 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<String>,
}

View File

@@ -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<u32>| 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)) => {

View File

@@ -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(())
}

View File

@@ -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<Port> = 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<Link> {