diff --git a/Cargo.lock b/Cargo.lock index c4612063e0..8a321be33b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1296,6 +1296,7 @@ dependencies = [ "icu_datetime", "icu_locid", "inotify 0.10.2", + "itertools", "libc", "libpulse-binding", "log", diff --git a/Cargo.toml b/Cargo.toml index 8817e46cd3..7e863bec5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ icu_datetime = { version = "1.3.0", optional = true } icu_calendar = { version = "1.3.0", optional = true } icu_locid = { version = "1.3.0", optional = true } inotify = "0.10" +itertools = "0.12" libc = "0.2" libpulse-binding = { version = "2.0", default-features = false, optional = true } log = "0.4" diff --git a/cspell.yaml b/cspell.yaml index f79d1c28c8..840995cfa3 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -74,6 +74,7 @@ words: - inotify - iowait - ipapi + - itertools - iwctl - kbdd - kbddbus diff --git a/src/blocks/privacy.rs b/src/blocks/privacy.rs index 385366b23a..6867addeef 100644 --- a/src/blocks/privacy.rs +++ b/src/blocks/privacy.rs @@ -68,8 +68,6 @@ //! - `webcam` //! - `unknown` -use std::collections::HashSet; - use futures::future::{select_all, try_join_all}; use super::prelude::*; @@ -114,8 +112,53 @@ enum Type { Unknown, } -// {type: {name: {reader}} -type PrivacyInfo = HashMap>>; +// {type: {source: {destination: count}} +type PrivacyInfo = HashMap; + +type PrivacyInfoInnerType = HashMap>; +#[derive(Default, Debug)] +struct PrivacyInfoInner(PrivacyInfoInnerType); + +impl std::ops::Deref for PrivacyInfoInner { + type Target = PrivacyInfoInnerType; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for PrivacyInfoInner { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl std::fmt::Display for PrivacyInfoInner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{{ {} }}", + itertools::join( + self.iter().map(|(source, destinations)| { + format!( + "{} => [ {} ]", + source, + itertools::join( + destinations + .iter() + .map(|(destination, count)| if count == &1 { + destination.into() + } else { + format!("{} (x{})", destination, count) + }), + ", " + ) + ) + }), + ", ", + ) + ) + } +} #[async_trait] trait PrivacyMonitor { @@ -149,12 +192,12 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { loop { let mut widget = Widget::new().with_format(format.clone()); - let mut info = PrivacyInfo::new(); + let mut info = PrivacyInfo::default(); //Merge driver info for driver_info in try_join_all(drivers.iter_mut().map(|driver| driver.get_info())).await? { for (type_, mapping) in driver_info { let existing_mapping = info.entry(type_).or_default(); - for (source, dest) in mapping { + for (source, dest) in mapping.0 { existing_mapping.entry(source).or_default().extend(dest); } } @@ -168,31 +211,31 @@ pub async fn run(config: &Config, api: &CommonApi) -> Result<()> { if let Some(info_by_type) = info.get(&Type::Audio) { values.extend(map! { "icon_audio" => Value::icon("microphone"), - "info_audio" => Value::text(format!("{:?}", info_by_type)) + "info_audio" => Value::text(format!("{}", info_by_type)) }); } if let Some(info_by_type) = info.get(&Type::AudioSink) { values.extend(map! { "icon_audio_sink" => Value::icon("volume"), - "info_audio_sink" => Value::text(format!("{:?}", info_by_type)) + "info_audio_sink" => Value::text(format!("{}", info_by_type)) }); } if let Some(info_by_type) = info.get(&Type::Video) { values.extend(map! { "icon_video" => Value::icon("xrandr"), - "info_video" => Value::text(format!("{:?}", info_by_type)) + "info_video" => Value::text(format!("{}", info_by_type)) }); } if let Some(info_by_type) = info.get(&Type::Webcam) { values.extend(map! { "icon_webcam" => Value::icon("webcam"), - "info_webcam" => Value::text(format!("{:?}", info_by_type)) + "info_webcam" => Value::text(format!("{}", info_by_type)) }); } if let Some(info_by_type) = info.get(&Type::Unknown) { values.extend(map! { "icon_unknown" => Value::icon("unknown"), - "info_unknown" => Value::text(format!("{:?}", info_by_type)) + "info_unknown" => Value::text(format!("{}", info_by_type)) }); } diff --git a/src/blocks/privacy/pipewire.rs b/src/blocks/privacy/pipewire.rs index 51c948dbc5..d44a797e25 100644 --- a/src/blocks/privacy/pipewire.rs +++ b/src/blocks/privacy/pipewire.rs @@ -2,6 +2,7 @@ use ::pipewire::{ context::Context, core::PW_ID_CORE, keys, main_loop::MainLoop, properties::properties, spa::utils::dict::DictRef, types::ObjectType, }; +use itertools::Itertools; use tokio::sync::mpsc::{self, Receiver, Sender}; use std::{collections::HashMap, sync::Mutex, thread}; @@ -35,7 +36,7 @@ impl Node { } } -#[derive(Debug)] +#[derive(Debug, PartialEq, PartialOrd, Eq, Ord)] struct Link { link_output_node: u32, link_input_node: u32, @@ -185,6 +186,16 @@ enum NodeDisplay { Nickname, } +impl NodeDisplay { + fn map_node(&self, node: &Node) -> String { + match self { + NodeDisplay::Name => node.name.clone(), + NodeDisplay::Description => node.description.clone().unwrap_or(node.name.clone()), + NodeDisplay::Nickname => node.nick.clone().unwrap_or(node.name.clone()), + } + } +} + pub(super) struct Monitor<'a> { config: &'a Config, updates: Receiver<()>, @@ -215,73 +226,45 @@ impl<'a> PrivacyMonitor for Monitor<'a> { debug! {"{:?}", node}; } + // The links must be sorted and then dedup'ed since you can multiple links between any given pair of nodes for Link { link_output_node, link_input_node, .. - } in data.links.values() + } in data.links.values().sorted().dedup() { - if let (Some(output_node), Some(input_node)) = ( + let (Some(output_node), Some(input_node)) = ( data.nodes.get(link_output_node), data.nodes.get(link_input_node), - ) { - if input_node.media_class != Some("Audio/Sink".into()) - && !self.config.exclude_output.contains(&output_node.name) - && !self.config.exclude_input.contains(&input_node.name) - { - let type_ = if input_node.media_class == Some("Stream/Input/Video".into()) { - if output_node.media_role == Some("Camera".into()) { - Type::Webcam - } else { - Type::Video - } - } else if input_node.media_class == Some("Stream/Input/Audio".into()) { - if output_node.media_class == Some("Audio/Sink".into()) { - Type::AudioSink - } else { - Type::Audio - } + ) else { + continue; + }; + if input_node.media_class != Some("Audio/Sink".into()) + && !self.config.exclude_output.contains(&output_node.name) + && !self.config.exclude_input.contains(&input_node.name) + { + let type_ = if input_node.media_class == Some("Stream/Input/Video".into()) { + if output_node.media_role == Some("Camera".into()) { + Type::Webcam } else { - Type::Unknown - }; - use NodeDisplay::*; - match self.config.display { - Name => { - mapping - .entry(type_) - .or_default() - .entry(output_node.name.clone()) - .or_default() - .insert(input_node.name.clone()); - } - Description => { - mapping - .entry(type_) - .or_default() - .entry( - output_node - .description - .clone() - .unwrap_or(output_node.name.clone()), - ) - .or_default() - .insert( - input_node - .description - .clone() - .unwrap_or(input_node.name.clone()), - ); - } - Nickname => { - mapping - .entry(type_) - .or_default() - .entry(output_node.nick.clone().unwrap_or(output_node.name.clone())) - .or_default() - .insert(input_node.nick.clone().unwrap_or(input_node.name.clone())); - } + Type::Video } - } + } else if input_node.media_class == Some("Stream/Input/Audio".into()) { + if output_node.media_class == Some("Audio/Sink".into()) { + Type::AudioSink + } else { + Type::Audio + } + } else { + Type::Unknown + }; + *mapping + .entry(type_) + .or_default() + .entry(self.config.display.map_node(output_node)) + .or_default() + .entry(self.config.display.map_node(input_node)) + .or_default() += 1; } } diff --git a/src/blocks/privacy/v4l.rs b/src/blocks/privacy/v4l.rs index 4681269c2b..1b961b581e 100644 --- a/src/blocks/privacy/v4l.rs +++ b/src/blocks/privacy/v4l.rs @@ -128,12 +128,13 @@ impl<'a> PrivacyMonitor for Monitor<'a> { continue; } debug!("{} {:?}", reader, link_path); - mapping + *mapping .entry(Type::Webcam) .or_default() .entry(link_path.to_string_lossy().to_string()) .or_default() - .insert(reader); + .entry(reader) + .or_default() += 1; debug!("{:?}", mapping); } }