Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
185 changes: 147 additions & 38 deletions src/config/names.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()],
Expand All @@ -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()],
Expand Down Expand Up @@ -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()],
Expand All @@ -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()],
Expand All @@ -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()],
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")))
}
}
23 changes: 22 additions & 1 deletion src/config/property_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub enum PropertyKey {
Device(String),
Node(String),
Client(String),
Computed(String),
Bare(String),
}

Expand All @@ -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(),
}
}
Expand All @@ -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:") {
Expand Down Expand Up @@ -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,
}
}
}
Expand All @@ -94,6 +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)
}
}
}
}
Expand All @@ -109,6 +119,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,
}
}
}
Expand Down Expand Up @@ -150,6 +161,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());
Expand All @@ -160,6 +179,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]
Expand All @@ -170,7 +190,7 @@ 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);
}
Expand All @@ -181,6 +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::Bare("x".into()).to_string(), "x");
}

Expand Down
Loading
Loading