i3status_rs/blocks/privacy/
pipewire.rs

1use itertools::Itertools as _;
2use tokio::sync::mpsc::{UnboundedReceiver, unbounded_channel};
3
4use super::*;
5use crate::pipewire::{CLIENT, EventKind, Link, Node};
6
7#[derive(Deserialize, Debug, SmartDefault)]
8#[serde(rename_all = "lowercase", deny_unknown_fields, default)]
9pub struct Config {
10    exclude_output: Vec<String>,
11    exclude_input: Vec<String>,
12    display: NodeDisplay,
13}
14
15#[derive(Deserialize, Debug, SmartDefault)]
16#[serde(rename_all = "snake_case")]
17enum NodeDisplay {
18    #[default]
19    Name,
20    Description,
21    Nickname,
22}
23
24impl NodeDisplay {
25    fn map_node(&self, node: &Node) -> String {
26        match self {
27            NodeDisplay::Name => node.name.clone(),
28            NodeDisplay::Description => node.description.clone().unwrap_or(node.name.clone()),
29            NodeDisplay::Nickname => node.nick.clone().unwrap_or(node.name.clone()),
30        }
31    }
32}
33
34pub(super) struct Monitor<'a> {
35    config: &'a Config,
36    updates: UnboundedReceiver<EventKind>,
37}
38
39impl<'a> Monitor<'a> {
40    pub(super) async fn new(config: &'a Config) -> Result<Self> {
41        let client = CLIENT.as_ref().error("Could not get client")?;
42        let (tx, rx) = unbounded_channel();
43        client.add_event_listener(tx);
44        Ok(Self {
45            config,
46            updates: rx,
47        })
48    }
49}
50
51#[async_trait]
52impl PrivacyMonitor for Monitor<'_> {
53    async fn get_info(&mut self) -> Result<PrivacyInfo> {
54        let client = CLIENT.as_ref().error("Could not get client")?;
55        let data = client.data.lock().unwrap();
56        let mut mapping: PrivacyInfo = PrivacyInfo::new();
57
58        for node in data.nodes.values() {
59            debug! {"{:?}", node};
60        }
61
62        // The links must be sorted and then dedup'ed since you can multiple links between any given pair of nodes
63        for Link {
64            link_output_node,
65            link_input_node,
66            ..
67        } in data.links.values().sorted().dedup()
68        {
69            if let Some(output_node) = data.nodes.get(link_output_node)
70                && let Some(input_node) = data.nodes.get(link_input_node)
71                && input_node.media_class != Some("Audio/Sink".into())
72                && !self.config.exclude_output.contains(&output_node.name)
73                && !self.config.exclude_input.contains(&input_node.name)
74            {
75                let type_ = if input_node.media_class == Some("Stream/Input/Video".into()) {
76                    if output_node.media_role == Some("Camera".into()) {
77                        Type::Webcam
78                    } else {
79                        Type::Video
80                    }
81                } else if input_node.media_class == Some("Stream/Input/Audio".into()) {
82                    if output_node.media_class == Some("Audio/Sink".into()) {
83                        Type::AudioSink
84                    } else {
85                        Type::Audio
86                    }
87                } else {
88                    Type::Unknown
89                };
90                *mapping
91                    .entry(type_)
92                    .or_default()
93                    .entry(self.config.display.map_node(output_node))
94                    .or_default()
95                    .entry(self.config.display.map_node(input_node))
96                    .or_default() += 1;
97            }
98        }
99
100        Ok(mapping)
101    }
102
103    async fn wait_for_change(&mut self) -> Result<()> {
104        while let Some(event) = self.updates.recv().await {
105            if event.intersects(
106                EventKind::NODE_ADDED
107                    | EventKind::NODE_REMOVED
108                    | EventKind::LINK_ADDED
109                    | EventKind::LINK_REMOVED,
110            ) {
111                break;
112            }
113        }
114        Ok(())
115    }
116}