From 88dcf48780284966a999eb0d26852ca12a3e0e67 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Sat, 21 Mar 2026 06:15:50 -0400 Subject: [PATCH 1/2] refactor: add physical endpoint label plumbing --- README.md | 5 + src/config/names.rs | 185 +++++++++++++++++++++------ src/config/property_key.rs | 29 ++++- src/view.rs | 85 ++++--------- src/wirehose/device.rs | 53 ++++++++ src/wirehose/event.rs | 2 + src/wirehose/state.rs | 253 +++++++++++++++++++++++++++++++++++++ wiremix.toml | 4 + 8 files changed, 517 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index d35a821..8ca4ccc 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,11 @@ Not all nodes and devices have the same properties present, so if multiple naming templates are specified, wiremix will try to resolve them in order and use the first one that works. +Templates and match rules can also use computed values. Currently +`computed:physical.endpoint.label` is available for endpoint nodes. It resolves +from `node.nick` first, then falls back to the active route's +`device.product.name` when there is a single unambiguous physical label. + For ncpamixer-style names you can use: ```toml diff --git a/src/config/names.rs b/src/config/names.rs index 09f6087..72b9012 100644 --- a/src/config/names.rs +++ b/src/config/names.rs @@ -225,19 +225,66 @@ mod tests { node_props, } } + + fn set_endpoint(&mut self) { + self.node_props.set_media_class(String::from("Audio/Sink")); + self.state.update(StateEvent::NodeProperties { + object_id: self.node_id, + props: self.node_props.clone(), + }); + } + + fn link_node_to_device(&mut self, card_profile_device: i32) { + self.node_props.set_device_id(self.device_id); + self.node_props.set_card_profile_device(card_profile_device); + self.state.update(StateEvent::NodeProperties { + object_id: self.node_id, + props: self.node_props.clone(), + }); + } + + fn set_route_label(&mut self, label: &str) { + self.state.update(StateEvent::DeviceEnumProfile { + object_id: self.device_id, + index: 1, + description: String::from("Digital Stereo Output"), + available: true, + classes: vec![(String::from("Audio/Sink"), vec![0])], + }); + self.state.update(StateEvent::DeviceProfile { + object_id: self.device_id, + index: 1, + }); + self.state.update(StateEvent::DeviceRoute { + object_id: self.device_id, + index: 7, + device: 0, + profiles: vec![1], + description: String::from("HDMI"), + available: true, + channel_volumes: vec![1.0, 1.0], + mute: false, + }); + self.state.update(StateEvent::DeviceEnumRoute { + object_id: self.device_id, + index: 7, + description: String::from("HDMI"), + available: true, + profiles: vec![1], + devices: vec![0], + info: HashMap::from([( + String::from("device.product.name"), + String::from(label), + )]), + }); + } } #[test] fn render_endpoint() { let mut fixture = Fixture::new(); - fixture - .node_props - .set_media_class(String::from("Audio/Sink")); - fixture.state.update(StateEvent::NodeProperties { - object_id: fixture.node_id, - props: fixture.node_props, - }); + fixture.set_endpoint(); let names = Names { endpoint: vec!["{node:node.nick}".parse().unwrap()], @@ -253,13 +300,7 @@ mod tests { fn render_endpoint_missing_key() { let mut fixture = Fixture::new(); - fixture - .node_props - .set_media_class(String::from("Audio/Sink")); - fixture.state.update(StateEvent::NodeProperties { - object_id: fixture.node_id, - props: fixture.node_props, - }); + fixture.set_endpoint(); let names = Names { endpoint: vec!["{node:node.description}".parse().unwrap()], @@ -291,14 +332,8 @@ mod tests { fn render_endpoint_linked_device() { let mut fixture = Fixture::new(); - fixture - .node_props - .set_media_class(String::from("Audio/Sink")); - fixture.node_props.set_device_id(fixture.device_id); - fixture.state.update(StateEvent::NodeProperties { - object_id: fixture.node_id, - props: fixture.node_props, - }); + fixture.set_endpoint(); + fixture.link_node_to_device(0); let names = Names { endpoint: vec!["{device:device.nick}".parse().unwrap()], @@ -314,14 +349,8 @@ mod tests { fn render_endpoint_linked_device_missing_key() { let mut fixture = Fixture::new(); - fixture - .node_props - .set_media_class(String::from("Audio/Sink")); - fixture.node_props.set_device_id(fixture.device_id); - fixture.state.update(StateEvent::NodeProperties { - object_id: fixture.node_id, - props: fixture.node_props, - }); + fixture.set_endpoint(); + fixture.link_node_to_device(0); let names = Names { endpoint: vec!["{device:device.description}".parse().unwrap()], @@ -338,13 +367,7 @@ mod tests { fn render_endpoint_no_linked_device() { let mut fixture = Fixture::new(); - fixture - .node_props - .set_media_class(String::from("Audio/Sink")); - fixture.state.update(StateEvent::NodeProperties { - object_id: fixture.node_id, - props: fixture.node_props, - }); + fixture.set_endpoint(); let names = Names { endpoint: vec!["{device:device.nick}".parse().unwrap()], @@ -378,7 +401,7 @@ mod tests { fixture.node_props.set_client_id(fixture.client_id); fixture.state.update(StateEvent::NodeProperties { object_id: fixture.node_id, - props: fixture.node_props, + props: fixture.node_props.clone(), }); let names = Names { @@ -494,4 +517,90 @@ mod tests { let result = names.resolve(&fixture.state, node); assert_eq!(result, Some(String::from("Node name"))) } + + #[test] + fn render_endpoint_computed_label_from_node_nick() { + let mut fixture = Fixture::new(); + fixture.set_endpoint(); + + let names = Names { + endpoint: vec![ + "{computed:physical.endpoint.label}".parse().unwrap(), + ], + ..Default::default() + }; + + let node = fixture.state.nodes.get(&fixture.node_id).unwrap(); + let result = names.resolve(&fixture.state, node); + assert_eq!(result, Some(String::from("Node nick"))) + } + + #[test] + fn render_endpoint_computed_label_from_route_info() { + let mut fixture = Fixture::new(); + fixture.node_props = PropertyStore::default(); + fixture.node_props.set_node_name(String::from("Node name")); + fixture.set_endpoint(); + fixture.link_node_to_device(0); + fixture.set_route_label("DELL U2723QE"); + + let names = Names { + endpoint: vec![ + "{computed:physical.endpoint.label}".parse().unwrap(), + ], + ..Default::default() + }; + + let node = fixture.state.nodes.get(&fixture.node_id).unwrap(); + let result = names.resolve(&fixture.state, node); + assert_eq!(result, Some(String::from("DELL U2723QE"))) + } + + #[test] + fn render_endpoint_computed_label_missing_falls_back() { + let mut fixture = Fixture::new(); + fixture.node_props = PropertyStore::default(); + fixture.node_props.set_node_name(String::from("Node name")); + fixture.set_endpoint(); + fixture.link_node_to_device(0); + + let names = Names { + endpoint: vec![ + "{computed:physical.endpoint.label}".parse().unwrap(), + ], + ..Default::default() + }; + + let node = fixture.state.nodes.get(&fixture.node_id).unwrap(); + let result = names.resolve(&fixture.state, node); + assert_eq!(result, Some(String::from("Node name"))) + } + + #[test] + fn render_override_match_on_computed_label() { + let mut fixture = Fixture::new(); + fixture.node_props = PropertyStore::default(); + fixture.node_props.set_node_name(String::from("Node name")); + fixture.set_endpoint(); + fixture.link_node_to_device(0); + fixture.set_route_label("DELL U2723QE"); + + let names = Names { + overrides: vec![NameOverride { + types: vec![OverrideType::Endpoint], + matches: vec![MatchCondition(HashMap::from([( + PropertyKey::Computed(String::from( + "physical.endpoint.label", + )), + MatchValue::Literal(String::from("DELL U2723QE")), + )]))], + templates: vec!["{node:node.name}".parse().unwrap()], + }], + ..Default::default() + }; + + let node = fixture.state.nodes.get(&fixture.node_id).unwrap(); + let result = names.resolve(&fixture.state, node); + assert_eq!(result, Some(String::from("Node name"))) + } } diff --git a/src/config/property_key.rs b/src/config/property_key.rs index d1e5ba4..abfac91 100644 --- a/src/config/property_key.rs +++ b/src/config/property_key.rs @@ -9,6 +9,7 @@ pub enum PropertyKey { Device(String), Node(String), Client(String), + Computed(String), Bare(String), } @@ -25,6 +26,9 @@ impl ToString for PropertyKey { PropertyKey::Client(s) => { format!("client:{s}") } + PropertyKey::Computed(s) => { + format!("computed:{s}") + } PropertyKey::Bare(s) => s.to_string(), } } @@ -37,6 +41,8 @@ impl std::str::FromStr for PropertyKey { let (variant, key): (fn(String) -> PropertyKey, &str) = if let Some(key) = s.strip_prefix("client:") { (PropertyKey::Client, key) + } else if let Some(key) = s.strip_prefix("computed:") { + (PropertyKey::Computed, key) } else if let Some(key) = s.strip_prefix("device:") { (PropertyKey::Device, key) } else if let Some(key) = s.strip_prefix("node:") { @@ -72,6 +78,7 @@ impl PropertyResolver for state::Device { PropertyKey::Device(s) | PropertyKey::Bare(s) => self.props.raw(s), PropertyKey::Node(_) => None, PropertyKey::Client(_) => None, + PropertyKey::Computed(_) => None, } } } @@ -94,6 +101,7 @@ impl PropertyResolver for state::Node { let client = state.clients.get(self.props.client_id()?)?; client.resolve_key(state, key) } + PropertyKey::Computed(s) => state.resolve_computed_node_key(self, s), } } } @@ -109,6 +117,7 @@ impl PropertyResolver for state::Client { PropertyKey::Client(s) | PropertyKey::Bare(s) => self.props.raw(s), PropertyKey::Node(_) => None, PropertyKey::Device(_) => None, + PropertyKey::Computed(_) => None, } } } @@ -150,6 +159,14 @@ mod tests { ); } + #[test] + fn parse_computed() { + assert_eq!( + PropertyKey::from_str("computed:physical.endpoint.label").unwrap(), + PropertyKey::Computed("physical.endpoint.label".into()) + ); + } + #[test] fn empty_bare_is_error() { assert!(PropertyKey::from_str("").is_err()); @@ -160,6 +177,7 @@ mod tests { assert!(PropertyKey::from_str("device:").is_err()); assert!(PropertyKey::from_str("node:").is_err()); assert!(PropertyKey::from_str("client:").is_err()); + assert!(PropertyKey::from_str("computed:").is_err()); } #[test] @@ -170,7 +188,12 @@ mod tests { #[test] fn roundtrip_prefixed() { - for input in ["device:foo", "node:bar", "client:baz"] { + for input in [ + "device:foo", + "node:bar", + "client:baz", + "computed:qux", + ] { let key = PropertyKey::from_str(input).unwrap(); assert_eq!(key.to_string(), input); } @@ -181,6 +204,10 @@ mod tests { assert_eq!(PropertyKey::Device("x".into()).to_string(), "device:x"); assert_eq!(PropertyKey::Node("x".into()).to_string(), "node:x"); assert_eq!(PropertyKey::Client("x".into()).to_string(), "client:x"); + assert_eq!( + PropertyKey::Computed("x".into()).to_string(), + "computed:x" + ); assert_eq!(PropertyKey::Bare("x".into()).to_string(), "x"); } diff --git a/src/view.rs b/src/view.rs index 08877d8..3da3ced 100644 --- a/src/view.rs +++ b/src/view.rs @@ -155,61 +155,6 @@ impl ListKind { } } -/// Gets the potential Target::Routes for a device and media class. -/// These come from the EnumRoutes where profiles contains the active profile's -/// index, and devices contains at least one of the profile's devices for the -/// given media class. -fn route_targets( - device: &state::Device, - media_class: &String, -) -> Option> { - let profile_index = device.profile_index?; - let profile = device.profiles.get(&profile_index)?; - let profile_devices = profile - .classes - .iter() - .find_map(|(mc, devices)| (mc == media_class).then_some(devices))?; - Some( - device - .enum_routes - .values() - .filter_map(|route| { - if !route.profiles.contains(&profile_index) { - return None; - } - let route_device = - route.devices.iter().find(|route_device| { - profile_devices.contains(route_device) - })?; - let title = if route.available { - route.description.clone() - } else { - format!("{} (unavailable)", route.description) - }; - Some(( - Target::Route(device.object_id, route.index, *route_device), - title, - )) - }) - .collect(), - ) -} - -/// Get the active route for a device and card device. -/// This is the route on a device Node IF the route's profile matches the -/// device's current profile. Otherwise, there is no valid route. -fn active_route( - device: &state::Device, - card_device: i32, -) -> Option<&state::Route> { - let profile_index = device.profile_index?; - - device - .routes - .get(&card_device) - .filter(|route| route.profiles.contains(&profile_index)) -} - impl Node { fn from( state: &state::State, @@ -231,9 +176,9 @@ impl Node { // Nodes for devices should get their volume and mute status // from the associated device's active route which is also used // for changing the volume and mute status. - let device = state.devices.get(device_id)?; - let card_device = *node.props.card_profile_device()?; - if let Some(route) = active_route(device, card_device) { + if let Some((_, route, card_device)) = + state.active_route_for_node(node) + { let route_index = route.index; ( route.volumes.clone(), @@ -257,11 +202,31 @@ impl Node { let card_device = *node.props.card_profile_device()?; let mut routes: Vec<_> = - route_targets(device, &media_class).unwrap_or_default(); + state + .route_targets(device, &media_class) + .unwrap_or_default() + .into_iter() + .map(|(route, route_device)| { + let title = if route.available { + route.description.clone() + } else { + format!("{} (unavailable)", route.description) + }; + ( + Target::Route( + device.object_id, + route.index, + route_device, + ), + title, + ) + }) + .collect(); routes.sort_by(|(_, a), (_, b)| a.cmp(b)); let routes = routes; - let (target, target_title) = match active_route(device, card_device) + let (target, target_title) = + match state.active_route(device, card_device) { Some(route) => { let target_title = if route.available { diff --git a/src/wirehose/device.rs b/src/wirehose/device.rs index 9b236ca..d3d567e 100644 --- a/src/wirehose/device.rs +++ b/src/wirehose/device.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::rc::Rc; use pipewire::{ @@ -94,12 +95,38 @@ pub fn monitor_device( Some((device, Box::new(listener))) } +fn parse_route_info(value: Value) -> HashMap { + let Value::Struct(info_struct) = value else { + return HashMap::new(); + }; + + let skip = match info_struct.first() { + Some(Value::Int(_)) => 1, + _ => 0, + }; + + let mut info = HashMap::new(); + let mut iter = info_struct.into_iter().skip(skip); + loop { + match (iter.next(), iter.next()) { + (Some(Value::String(key)), Some(Value::String(value))) => { + info.insert(key, value); + } + (Some(_), Some(_)) => continue, + _ => break, + } + } + + info +} + fn device_enum_route(object_id: ObjectId, param: Object) -> Option { let mut index = None; let mut description = None; let mut available = None; let mut profiles = None; let mut devices = None; + let mut info = HashMap::new(); for prop in param.properties { match prop.key { @@ -129,6 +156,9 @@ fn device_enum_route(object_id: ObjectId, param: Object) -> Option { devices = Some(value); } } + libspa_sys::SPA_PARAM_ROUTE_info => { + info = parse_route_info(prop.value); + } _ => {} } } @@ -140,6 +170,7 @@ fn device_enum_route(object_id: ObjectId, param: Object) -> Option { available: available?, profiles: profiles?, devices: devices?, + info, }) } @@ -312,3 +343,25 @@ fn device_info_props( let props = PropertyStore::from(props); sender.send(StateEvent::DeviceProperties { object_id, props }); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_route_info_collects_string_pairs() { + let info = parse_route_info(Value::Struct(vec![ + Value::Int(4), + Value::String(String::from("device.product.name")), + Value::String(String::from("DELL U2723QE")), + Value::String(String::from("device.api")), + Value::String(String::from("alsa")), + ])); + + assert_eq!( + info.get("device.product.name").map(String::as_str), + Some("DELL U2723QE") + ); + assert_eq!(info.get("device.api").map(String::as_str), Some("alsa")); + } +} diff --git a/src/wirehose/event.rs b/src/wirehose/event.rs index 8785fe5..e214088 100644 --- a/src/wirehose/event.rs +++ b/src/wirehose/event.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::sync::Arc; use pipewire::link::LinkInfoRef; @@ -28,6 +29,7 @@ pub enum StateEvent { available: bool, profiles: Vec, devices: Vec, + info: HashMap, }, DeviceEnumProfile { object_id: ObjectId, diff --git a/src/wirehose/state.rs b/src/wirehose/state.rs index 7e93071..2514008 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 info: HashMap, } #[derive(Debug)] @@ -162,6 +163,7 @@ impl State { available, profiles, devices, + info, } => { self.device_entry(object_id).enum_routes.insert( index, @@ -171,6 +173,7 @@ impl State { available, profiles, devices, + info, }, ); } @@ -357,6 +360,120 @@ impl State { .collect() } + pub fn active_route<'a>( + &'a self, + device: &'a Device, + card_device: i32, + ) -> Option<&'a Route> { + let profile_index = device.profile_index?; + + device + .routes + .get(&card_device) + .filter(|route| route.profiles.contains(&profile_index)) + } + + pub fn active_route_for_node<'a>( + &'a self, + node: &'a Node, + ) -> Option<(&'a Device, &'a Route, i32)> { + let device = self.devices.get(node.props.device_id()?)?; + let card_device = *node.props.card_profile_device()?; + let route = self.active_route(device, card_device)?; + Some((device, route, card_device)) + } + + pub fn route_targets<'a>( + &'a self, + device: &'a Device, + media_class: &str, + ) -> Option> { + let profile_index = device.profile_index?; + let profile = device.profiles.get(&profile_index)?; + let profile_devices = profile + .classes + .iter() + .find_map(|(mc, devices)| (mc == media_class).then_some(devices))?; + + Some( + device + .enum_routes + .values() + .filter_map(|route| { + if !route.profiles.contains(&profile_index) { + return None; + } + let route_device = route + .devices + .iter() + .find(|route_device| profile_devices.contains(route_device))?; + Some((route, *route_device)) + }) + .collect(), + ) + } + + pub fn endpoint_physical_label<'a>( + &'a self, + node: &'a Node, + ) -> Option<&'a str> { + let media_class = node.props.media_class()?; + if !( + media_class::is_sink(media_class) + || media_class::is_source(media_class) + ) { + return None; + } + + if let Some(node_nick) = + node.props.node_nick().filter(|node_nick| !node_nick.is_empty()) + { + return Some(node_nick.as_str()); + } + + let (device, route, _) = self.active_route_for_node(node)?; + device.enum_routes.get(&route.index)?.physical_label() + } + + pub fn device_profile_physical_label<'a>( + &'a self, + device: &'a Device, + profile_index: i32, + ) -> Option<&'a str> { + let profile = device.profiles.get(&profile_index)?; + let profile_devices: Vec = profile + .classes + .iter() + .flat_map(|(_, devices)| devices.iter().copied()) + .collect(); + + let mut matching_routes = device.enum_routes.values().filter(|route| { + route.profiles.contains(&profile_index) + && route + .devices + .iter() + .any(|route_device| profile_devices.contains(route_device)) + }); + + let route = matching_routes.next()?; + if matching_routes.next().is_some() { + return None; + } + + route.physical_label() + } + + pub fn resolve_computed_node_key<'a>( + &'a self, + node: &'a Node, + key: &str, + ) -> Option<&'a str> { + match key { + "physical.endpoint.label" => self.endpoint_physical_label(node), + _ => None, + } + } + /// Call when a node's capture eligibility might have changed. fn on_node(&self, node: &Node) -> Option { if !node @@ -397,10 +514,49 @@ impl State { } } +impl EnumRoute { + pub fn info(&self, key: &str) -> Option<&str> { + self.info.get(key).map(String::as_str) + } + + pub fn physical_label(&self) -> Option<&str> { + self.info("device.product.name") + .filter(|label| !label.is_empty()) + } +} + #[cfg(test)] mod tests { use super::*; + fn create_device_with_profile(state: &mut State, object_id: ObjectId) { + state.update(StateEvent::DeviceProperties { + object_id, + props: PropertyStore::default(), + }); + state.update(StateEvent::DeviceEnumProfile { + object_id, + index: 1, + description: String::from("Digital Stereo Output"), + available: true, + classes: vec![(String::from("Audio/Sink"), vec![0])], + }); + state.update(StateEvent::DeviceProfile { + object_id, + index: 1, + }); + state.update(StateEvent::DeviceRoute { + object_id, + index: 7, + device: 0, + profiles: vec![1], + description: String::from("HDMI"), + available: true, + channel_volumes: vec![1.0, 1.0], + mute: false, + }); + } + fn create_node( state: &mut State, object_id: ObjectId, @@ -550,6 +706,103 @@ mod tests { assert!(!get_metadata_properties(&state, &object_id, 1).is_empty()); } + #[test] + fn endpoint_physical_label_prefers_node_nick() { + let mut state = State::default(); + let device_id = ObjectId::from_raw_id(10); + let node_id = ObjectId::from_raw_id(11); + + create_device_with_profile(&mut state, device_id); + state.update(StateEvent::DeviceEnumRoute { + object_id: device_id, + index: 7, + description: String::from("HDMI"), + available: true, + profiles: vec![1], + devices: vec![0], + info: HashMap::from([( + String::from("device.product.name"), + String::from("Route label"), + )]), + }); + + let mut props = PropertyStore::default(); + props.set_media_class(String::from("Audio/Sink")); + props.set_node_name(String::from("sink")); + props.set_node_nick(String::from("Node label")); + props.set_device_id(device_id); + props.set_card_profile_device(0); + props.set_object_serial(11); + state.update(StateEvent::NodeProperties { + object_id: node_id, + props, + }); + + let node = state.nodes.get(&node_id).unwrap(); + assert_eq!(state.endpoint_physical_label(node), Some("Node label")); + } + + #[test] + fn device_profile_physical_label_requires_unique_route() { + let mut state = State::default(); + let device_id = ObjectId::from_raw_id(10); + + create_device_with_profile(&mut state, device_id); + state.update(StateEvent::DeviceEnumRoute { + object_id: device_id, + index: 7, + description: String::from("HDMI"), + available: true, + profiles: vec![1], + devices: vec![0], + info: HashMap::from([( + String::from("device.product.name"), + String::from("DELL U2723QE"), + )]), + }); + + let device = state.devices.get(&device_id).unwrap(); + assert_eq!( + state.device_profile_physical_label(device, 1), + Some("DELL U2723QE") + ); + } + + #[test] + fn device_profile_physical_label_is_none_for_multiple_routes() { + let mut state = State::default(); + let device_id = ObjectId::from_raw_id(10); + + create_device_with_profile(&mut state, device_id); + state.update(StateEvent::DeviceEnumRoute { + object_id: device_id, + index: 7, + description: String::from("HDMI 1"), + available: true, + profiles: vec![1], + devices: vec![0], + info: HashMap::from([( + String::from("device.product.name"), + String::from("DELL U2723QE"), + )]), + }); + state.update(StateEvent::DeviceEnumRoute { + object_id: device_id, + index: 8, + description: String::from("HDMI 2"), + available: true, + profiles: vec![1], + devices: vec![0], + info: HashMap::from([( + String::from("device.product.name"), + String::from("PA32QCV"), + )]), + }); + + let device = state.devices.get(&device_id).unwrap(); + assert_eq!(state.device_profile_physical_label(device, 1), None); + } + #[test] fn capture_eligible_for_stream() { let mut state = State::default(); diff --git a/wiremix.toml b/wiremix.toml index d1e521f..791bacb 100644 --- a/wiremix.toml +++ b/wiremix.toml @@ -157,6 +157,10 @@ keybindings = [ # uses a property which doesn't exit on a given object, wiremix tries the next # template in the sequence. If none of them can be resolved, it falls back on # node.name for nodes or device.name for devices. +# Templates and match rules can also use computed values. Currently +# computed:physical.endpoint.label is available for endpoint nodes. It resolves +# from node.nick first, then falls back to the active route's +# device.product.name when that mapping is unambiguous. # # The overall order of precedence for name resolution is: # 1. Matching override templates, if any (see the Name Overrides section) From f1cf7d89dad04ec58241129708293227c8c018b5 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Sat, 21 Mar 2026 06:22:48 -0400 Subject: [PATCH 2/2] style: apply rustfmt --- src/config/names.rs | 18 +++++----- src/config/property_key.rs | 16 +++------ src/view.rs | 70 ++++++++++++++++++-------------------- src/wirehose/state.rs | 21 ++++++------ 4 files changed, 59 insertions(+), 66 deletions(-) diff --git a/src/config/names.rs b/src/config/names.rs index 72b9012..27267e4 100644 --- a/src/config/names.rs +++ b/src/config/names.rs @@ -524,9 +524,9 @@ mod tests { fixture.set_endpoint(); let names = Names { - endpoint: vec![ - "{computed:physical.endpoint.label}".parse().unwrap(), - ], + endpoint: vec!["{computed:physical.endpoint.label}" + .parse() + .unwrap()], ..Default::default() }; @@ -545,9 +545,9 @@ mod tests { fixture.set_route_label("DELL U2723QE"); let names = Names { - endpoint: vec![ - "{computed:physical.endpoint.label}".parse().unwrap(), - ], + endpoint: vec!["{computed:physical.endpoint.label}" + .parse() + .unwrap()], ..Default::default() }; @@ -565,9 +565,9 @@ mod tests { fixture.link_node_to_device(0); let names = Names { - endpoint: vec![ - "{computed:physical.endpoint.label}".parse().unwrap(), - ], + endpoint: vec!["{computed:physical.endpoint.label}" + .parse() + .unwrap()], ..Default::default() }; diff --git a/src/config/property_key.rs b/src/config/property_key.rs index abfac91..790efa1 100644 --- a/src/config/property_key.rs +++ b/src/config/property_key.rs @@ -101,7 +101,9 @@ impl PropertyResolver for state::Node { let client = state.clients.get(self.props.client_id()?)?; client.resolve_key(state, key) } - PropertyKey::Computed(s) => state.resolve_computed_node_key(self, s), + PropertyKey::Computed(s) => { + state.resolve_computed_node_key(self, s) + } } } } @@ -188,12 +190,7 @@ mod tests { #[test] fn roundtrip_prefixed() { - for input in [ - "device:foo", - "node:bar", - "client:baz", - "computed:qux", - ] { + for input in ["device:foo", "node:bar", "client:baz", "computed:qux"] { let key = PropertyKey::from_str(input).unwrap(); assert_eq!(key.to_string(), input); } @@ -204,10 +201,7 @@ mod tests { assert_eq!(PropertyKey::Device("x".into()).to_string(), "device:x"); assert_eq!(PropertyKey::Node("x".into()).to_string(), "node:x"); assert_eq!(PropertyKey::Client("x".into()).to_string(), "client:x"); - assert_eq!( - PropertyKey::Computed("x".into()).to_string(), - "computed:x" - ); + assert_eq!(PropertyKey::Computed("x".into()).to_string(), "computed:x"); assert_eq!(PropertyKey::Bare("x".into()).to_string(), "x"); } diff --git a/src/view.rs b/src/view.rs index 3da3ced..f9cdd73 100644 --- a/src/view.rs +++ b/src/view.rs @@ -201,50 +201,48 @@ impl Node { let device = state.devices.get(device_id)?; let card_device = *node.props.card_profile_device()?; - let mut routes: Vec<_> = - state - .route_targets(device, &media_class) - .unwrap_or_default() - .into_iter() - .map(|(route, route_device)| { - let title = if route.available { - route.description.clone() - } else { - format!("{} (unavailable)", route.description) - }; - ( - Target::Route( - device.object_id, - route.index, - route_device, - ), - title, - ) - }) - .collect(); - routes.sort_by(|(_, a), (_, b)| a.cmp(b)); - let routes = routes; - - let (target, target_title) = - match state.active_route(device, card_device) - { - Some(route) => { - let target_title = if route.available { + let mut routes: Vec<_> = state + .route_targets(device, &media_class) + .unwrap_or_default() + .into_iter() + .map(|(route, route_device)| { + let title = if route.available { route.description.clone() } else { format!("{} (unavailable)", route.description) }; ( - Some(Target::Route( + Target::Route( device.object_id, route.index, - card_device, - )), - target_title, + route_device, + ), + title, ) - } - None => (None, String::from("No route selected")), - }; + }) + .collect(); + routes.sort_by(|(_, a), (_, b)| a.cmp(b)); + let routes = routes; + + let (target, target_title) = + match state.active_route(device, card_device) { + Some(route) => { + let target_title = if route.available { + route.description.clone() + } else { + format!("{} (unavailable)", route.description) + }; + ( + Some(Target::Route( + device.object_id, + route.index, + card_device, + )), + target_title, + ) + } + None => (None, String::from("No route selected")), + }; (Some(routes), target, target_title) } else if media_class::is_sink_input(&media_class) { diff --git a/src/wirehose/state.rs b/src/wirehose/state.rs index 2514008..70bbb5a 100644 --- a/src/wirehose/state.rs +++ b/src/wirehose/state.rs @@ -403,10 +403,10 @@ impl State { if !route.profiles.contains(&profile_index) { return None; } - let route_device = route - .devices - .iter() - .find(|route_device| profile_devices.contains(route_device))?; + let route_device = + route.devices.iter().find(|route_device| { + profile_devices.contains(route_device) + })?; Some((route, *route_device)) }) .collect(), @@ -418,15 +418,16 @@ impl State { node: &'a Node, ) -> Option<&'a str> { let media_class = node.props.media_class()?; - if !( - media_class::is_sink(media_class) - || media_class::is_source(media_class) - ) { + if !(media_class::is_sink(media_class) + || media_class::is_source(media_class)) + { return None; } - if let Some(node_nick) = - node.props.node_nick().filter(|node_nick| !node_nick.is_empty()) + if let Some(node_nick) = node + .props + .node_nick() + .filter(|node_nick| !node_nick.is_empty()) { return Some(node_nick.as_str()); }