diff --git a/crates/headroom-core/src/ipc/ops.rs b/crates/headroom-core/src/ipc/ops.rs index 1230e43..3b480b0 100644 --- a/crates/headroom-core/src/ipc/ops.rs +++ b/crates/headroom-core/src/ipc/ops.rs @@ -193,6 +193,7 @@ fn profile_use(id: u64, name: &str, state: &SharedState) -> Response { publish_profile_changed(&mut s, name); let control = s.filter_control.clone(); let snap = build_dsp_configs(&s); + post_reevaluate(&s); drop(s); push_dsp_update(control.as_ref(), snap); ok(id, &json!({ "name": name })) @@ -234,6 +235,7 @@ pub(crate) fn execute_reload( publish_profile_reloaded(&mut s, &report.loaded); let control = s.filter_control.clone(); let snap = build_dsp_configs(&s); + post_reevaluate(&s); drop(s); push_dsp_update(control.as_ref(), snap); Ok(report) @@ -245,6 +247,7 @@ fn route_set(id: u64, app: &str, to: Route, state: &SharedState) -> Response { Ok(()) => { tracing::info!(app, ?to, "route.set applied"); publish_rule_changed(&mut s); + post_reevaluate(&s); drop(s); ok(id, &Value::Null) } @@ -258,6 +261,7 @@ fn route_unset(id: u64, app: &str, state: &SharedState) -> Response { Ok(()) => { tracing::info!(app, "route.unset applied"); publish_rule_changed(&mut s); + post_reevaluate(&s); drop(s); ok(id, &Value::Null) } @@ -271,6 +275,25 @@ fn publish_rule_changed(state: &mut crate::state::DaemonState) { } } +/// Ask the PipeWire main loop to re-run `routing::evaluate` against +/// every known stream. Called after any IPC mutation that changes +/// the inputs to that decision: active profile, profile contents +/// reloaded from disk, or a `route.set` / `route.unset` overlay +/// edit. Without this, the new policy only applies to *future* +/// streams; everything already routed keeps its old links until the +/// app reconnects. A stale or duplicate post is harmless — the +/// handler reads current state at apply time and is idempotent +/// when nothing changed. +fn post_reevaluate(state: &crate::state::DaemonState) { + let Some(tx) = state.pw_command_tx.as_ref() else { + tracing::debug!("no PipeWire command channel; reevaluation skipped (test mode)"); + return; + }; + if tx.send(PwCommand::ReevaluateAll).is_err() { + tracing::warn!("PipeWire command channel closed; reevaluation lost"); + } +} + fn publish_profile_changed(state: &mut crate::state::DaemonState, name: &str) { if let Ok(event) = Event::new(Topic::Profile, "used", &json!({ "name": name })) { state.broadcaster.publish(Topic::Profile, event);