Introduce initial implementation of the vmic CLI with PipeWire integration
- Added `Cargo.toml` to define dependencies and project metadata. - Implemented core CLI functionality for managing virtual microphones (`create`, `edit`, `delete`, `list`, `route`, and `wipe` commands) using `clap`. - Integrated `rusqlite` for persistent state storage and `pipewire` to manage PipeWire nodes. - Ensured graceful handling of feedback loops and system defaults during virtual mic creation and deletion. - Added error handling via `thiserror` for cleaner error definitions.
This commit is contained in:
62
src/commands/list.rs
Normal file
62
src/commands/list.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
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!(
|
||||
" 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.
|
||||
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));
|
||||
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(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user