use crate::commands::topology; use crate::error::Result; use crate::pw::PwGraph; use crate::state::{self, VmicState}; use crate::ui; pub fn run() -> Result<()> { let conn = state::open_db()?; let states = VmicState::list_all(&conn)?; if states.is_empty() { ui::info("No virtual mics created."); return Ok(()); } let graph = PwGraph::connect().ok(); for (i, state) in states.iter().enumerate() { let status = liveness(graph.as_ref(), state); let status_colored = if status == "active" { ui::green(&status) } else { ui::yellow(&status) }; println!("{}", ui::blue(&state.name)); println!(" output device: {}", state.sink_name); println!(" mic device: {}", state.source_name); println!(" status: {status_colored}"); println!(" architecture: {}", topology::architecture_label(state)); println!( " self-monitor loopback: {}", state .loopback_id .map(|id| format!("yes (module {id})")) .unwrap_or_else(|| "no".into()) ); if let Some(pct) = state.volume_pct { println!(" loopback volume: {}", ui::blue(&format!("{pct}%"))); } if let Some(mic) = &state.mic_source { println!(" mixed source: {mic}"); if let Some(pct) = state.mic_volume_pct { println!(" source volume: {}", ui::blue(&format!("{pct}%"))); } } if i + 1 < states.len() { println!(); } } Ok(()) } /// Checks `sink_node_id`/`mix_node_id` against the live graph snapshot. /// `mix_node_id` is only expected to be alive when this vmic is currently /// running as `Pure4Node` (`mix_pid.is_some()`) - a `Simple2Node` vmic never /// has one, and that's not a problem. fn liveness(graph: Option<&PwGraph>, state: &VmicState) -> String { let Some(graph) = graph else { return "unknown (could not connect to PipeWire)".to_string(); }; let nodes = graph.nodes(); let sink_alive = state.sink_node_id.is_some_and(|id| nodes.iter().any(|n| n.id == id)); if topology::current_topology(state) == topology::Topology::Simple2Node { return if sink_alive { "active".to_string() } else { "stale (matching pw-loopback process not found)".to_string() }; } let mix_alive = state.mix_node_id.is_some_and(|id| nodes.iter().any(|n| n.id == id)); match (sink_alive, mix_alive) { (true, true) => "active".to_string(), (true, false) => "degraded (mix loopback process not found)".to_string(), (false, true) => "degraded (sink loopback process not found)".to_string(), (false, false) => "stale (matching pw-loopback process not found)".to_string(), } }