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:
77
src/commands/create.rs
Normal file
77
src/commands/create.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
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<String> {
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user