From 3b39bf1e7b3de357c5a6510d0f82625642fbd134 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Wed, 18 Mar 2026 12:57:44 -0400 Subject: [PATCH 1/4] fix: show monitor name for HDMI/DP audio endpoints When using NVIDIA GPU audio in pro-audio mode (or any HDMI/DP output), all sinks share the same PipeWire device. The previous default endpoint template resolved {device:device.nick} first, which returns the GPU name ("HDA NVidia") for every output, making them indistinguishable. PipeWire populates node.nick with the monitor name sourced from EDID/ELD data (e.g. "PA32QCV", "DELL U2723QE"). By trying {node:node.nick} first, each HDMI/DP sink now shows its connected monitor's name. For devices where node.nick is absent, the template falls through to device.nick as before. --- src/config/names.rs | 1 + wiremix.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/config/names.rs b/src/config/names.rs index 09f6087..8af89ba 100644 --- a/src/config/names.rs +++ b/src/config/names.rs @@ -16,6 +16,7 @@ impl Names { pub fn default_endpoint() -> Vec { vec![ + "{node:node.nick}".parse().unwrap(), "{device:device.nick}".parse().unwrap(), "{node:node.description}".parse().unwrap(), ] diff --git a/wiremix.toml b/wiremix.toml index d1e521f..4621f2d 100644 --- a/wiremix.toml +++ b/wiremix.toml @@ -166,7 +166,7 @@ keybindings = [ # Streams in the Playback/Recording tabs stream = [ "{node:node.name}: {node:media.name}" ] # Endpoints in the Input/Output Devices tabs -endpoint = [ "{device:device.nick}", "{node:node.description}" ] +endpoint = [ "{node:node.nick}", "{device:device.nick}", "{node:node.description}" ] # Devices in the Configuration tab device = [ "{device:device.nick}", "{device:device.description}" ] From d84cb5e6bc72ae20ee6ace97f978c4a647a24258 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Wed, 18 Mar 2026 13:10:12 -0400 Subject: [PATCH 2/4] feat: show monitor name in Configuration tab profile list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse SPA_PARAM_ROUTE_info from EnumRoute params to extract device.product.name (sourced from EDID/ELD data). When a profile maps to exactly one route with a known monitor name, append it to the profile description in the Configuration tab dropdown. Before: Digital Stereo (HDMI 2) Output After: Digital Stereo (HDMI 2) Output — PA32QCV Profiles that map to multiple routes (e.g. Pro Audio) or no routes (e.g. Off) are left unchanged. --- src/view.rs | 37 +++++++++++++++++++++++++++++++++---- src/wirehose/device.rs | 21 +++++++++++++++++++++ src/wirehose/event.rs | 1 + src/wirehose/state.rs | 3 +++ 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/view.rs b/src/view.rs index 08877d8..69517a7 100644 --- a/src/view.rs +++ b/src/view.rs @@ -365,10 +365,39 @@ impl Device { .profiles .values() .map(|profile| { - let title = if profile.available { - profile.description.clone() - } else { - format!("{} (unavailable)", profile.description) + let profile_devices: Vec = profile + .classes + .iter() + .flat_map(|(_, devices)| devices.iter().copied()) + .collect(); + let matching_routes: Vec<_> = device + .enum_routes + .values() + .filter(|route| { + route + .devices + .iter() + .any(|d| profile_devices.contains(d)) + }) + .collect(); + let monitor_name = + if matching_routes.len() == 1 { + matching_routes[0].product_name.as_deref() + } else { + None + }; + let title = match (profile.available, monitor_name) { + (true, Some(name)) => { + format!("{} \u{2014} {}", profile.description, name) + } + (false, Some(name)) => format!( + "{} \u{2014} {} (unavailable)", + profile.description, name + ), + (true, None) => profile.description.clone(), + (false, None) => { + format!("{} (unavailable)", profile.description) + } }; (profile.index, title) }) diff --git a/src/wirehose/device.rs b/src/wirehose/device.rs index 9b236ca..ff18b78 100644 --- a/src/wirehose/device.rs +++ b/src/wirehose/device.rs @@ -100,6 +100,7 @@ fn device_enum_route(object_id: ObjectId, param: Object) -> Option { let mut available = None; let mut profiles = None; let mut devices = None; + let mut product_name = None; for prop in param.properties { match prop.key { @@ -129,6 +130,25 @@ fn device_enum_route(object_id: ObjectId, param: Object) -> Option { devices = Some(value); } } + libspa_sys::SPA_PARAM_ROUTE_info => { + if let Value::Struct(info_struct) = prop.value { + let skip = match info_struct.first() { + Some(Value::Int(_)) => 1, + _ => 0, + }; + let mut iter = info_struct.into_iter().skip(skip); + while let ( + Some(Value::String(key)), + Some(Value::String(val)), + ) = (iter.next(), iter.next()) + { + if key == "device.product.name" { + product_name = Some(val); + break; + } + } + } + } _ => {} } } @@ -140,6 +160,7 @@ fn device_enum_route(object_id: ObjectId, param: Object) -> Option { available: available?, profiles: profiles?, devices: devices?, + product_name, }) } diff --git a/src/wirehose/event.rs b/src/wirehose/event.rs index 8785fe5..852de8b 100644 --- a/src/wirehose/event.rs +++ b/src/wirehose/event.rs @@ -28,6 +28,7 @@ pub enum StateEvent { available: bool, profiles: Vec, devices: Vec, + product_name: Option, }, DeviceEnumProfile { object_id: ObjectId, diff --git a/src/wirehose/state.rs b/src/wirehose/state.rs index 7e93071..b6d02f6 100644 --- a/src/wirehose/state.rs +++ b/src/wirehose/state.rs @@ -21,6 +21,7 @@ pub struct EnumRoute { pub available: bool, pub profiles: Vec, pub devices: Vec, + pub product_name: Option, } #[derive(Debug)] @@ -162,6 +163,7 @@ impl State { available, profiles, devices, + product_name, } => { self.device_entry(object_id).enum_routes.insert( index, @@ -171,6 +173,7 @@ impl State { available, profiles, devices, + product_name, }, ); } From fb53335dc9ec527d711fa79776fb940eb88d339c Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Wed, 18 Mar 2026 13:12:43 -0400 Subject: [PATCH 3/4] style: apply rustfmt --- src/view.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/view.rs b/src/view.rs index 69517a7..0d931d7 100644 --- a/src/view.rs +++ b/src/view.rs @@ -380,12 +380,11 @@ impl Device { .any(|d| profile_devices.contains(d)) }) .collect(); - let monitor_name = - if matching_routes.len() == 1 { - matching_routes[0].product_name.as_deref() - } else { - None - }; + let monitor_name = if matching_routes.len() == 1 { + matching_routes[0].product_name.as_deref() + } else { + None + }; let title = match (profile.available, monitor_name) { (true, Some(name)) => { format!("{} \u{2014} {}", profile.description, name) From bcf19b8aa505ed8b75ac6750ef7f9908a87635f8 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Wed, 18 Mar 2026 13:21:34 -0400 Subject: [PATCH 4/4] fix: show monitor name in selected profile display Reuse the enriched profile title (which includes the monitor name) for the target_title shown below the device name in the Configuration tab, instead of re-deriving it from the raw profile description. --- src/view.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/view.rs b/src/view.rs index 0d931d7..aab096e 100644 --- a/src/view.rs +++ b/src/view.rs @@ -402,19 +402,19 @@ impl Device { }) .collect(); profiles.sort_by_key(|&(index, _)| index); - let profiles = profiles + let profiles: Vec<(Target, String)> = profiles .into_iter() .map(|(index, title)| (Target::Profile(object_id, index), title)) .collect(); - let target_profile = device.profiles.get(&device.profile_index?)?; - let target_title = if target_profile.available { - target_profile.description.clone() - } else { - format!("{} (unavailable)", target_profile.description) - }; + let profile_index = device.profile_index?; + let target_title = profiles + .iter() + .find(|(t, _)| *t == Target::Profile(object_id, profile_index)) + .map(|(_, title)| title.clone()) + .unwrap_or_default(); - let target = Some(Target::Profile(object_id, device.profile_index?)); + let target = Some(Target::Profile(object_id, profile_index)); let object_serial = *device.props.object_serial()?;