use crate::cli::CreateArgs; use crate::error::{Result, VmicError}; use crate::pw::{loopback, PwGraph}; use crate::state::{self, VmicState}; use crate::ui; use std::process::Command; pub fn run(args: CreateArgs) -> Result<()> { state::require_valid_name(&args.name)?; let name = state::normalize(&args.name); let conn = state::open_db()?; if VmicState::exists(&conn, &name)? { return Err(VmicError::AlreadyExists(name)); } let names = loopback::NodeNames::for_vmic(&name); let graph = PwGraph::connect()?; // pw-loopback may steal the default sink/source; restore them afterward. let orig_sink = default_output("get-default-sink"); let orig_source = default_output("get-default-source"); let stages = loopback::create_stages(&graph, &names)?; if let Some(sink) = &orig_sink { let _ = Command::new("pactl").args(["set-default-sink", sink]).status(); } if let Some(source) = &orig_source { let _ = Command::new("pactl").args(["set-default-source", source]).status(); } // Insurance: a sink monitor feeding any vmic node is a feedback ring; // it should never exist, but tear it down if one appears anyway. let prefix = format!("vmic_{name}_"); let removed = graph.break_feedback_rings(&prefix)?; if removed > 0 { ui::warn(&format!("removed {removed} unexpected monitor feedback link(s).")); } let mut state = VmicState { name: name.clone(), sink_name: names.sink.clone(), mid_name: names.mid, mix_name: names.mix, source_name: names.source.clone(), sink_node_id: Some(stages.sink_node_id), mix_node_id: Some(stages.mix_node_id), sink_pid: Some(stages.sink_pid as i64), mix_pid: Some(stages.mix_pid as i64), ..Default::default() }; if args.loopback { let loop_id = loopback::enable_self_monitor(&graph, &names.sink)?; state.loopback_id = Some(loop_id); } state.save(&conn)?; ui::ok(&format!("Created virtual microphone '{name}'.")); if let (Some(sink), Some(source)) = (&orig_sink, &orig_source) { ui::info(&format!(" Default Output/Input kept intact ('{sink}' / '{source}').")); } println!(" In the source app, set output device to:"); println!(" '{}'", ui::blue(&names.sink)); println!(" In the receiving app, set input/mic device to:"); println!(" '{}'", ui::blue(&names.source)); Ok(()) } fn default_output(subcommand: &str) -> Option { let out = Command::new("pactl").arg(subcommand).output().ok()?; let s = String::from_utf8_lossy(&out.stdout).trim().to_string(); (!s.is_empty()).then_some(s) }