Skip to main content

i3status_rs/blocks/
privacy.rs

1//! Privacy Monitor
2//!
3//! # Configuration
4//!
5//! Key        | Values | Default|
6//! -----------|--------|--------|
7//! `driver` | The configuration of a driver (see below). | **Required**
8//! `format`   | [MultiFormat][MaybeMultiFormatConfig] string. | <code>[\"{ $icon_audio \|}{ $icon_audio_sink \|}{ $icon_video \|}{ $icon_webcam \|}{ $icon_unknown \|}\", \"{ $icon_audio $info_audio \|}{ $icon_audio_sink $info_audio_sink \|}{ $icon_video $info_video \|}{ $icon_webcam $info_webcam \|}{ $icon_unknown $info_unknown \|}\"]</code> |
9//!
10//! # pipewire Options (requires the pipewire feature to be enabled)
11//!
12//! Key | Values | Required | Default
13//! ----|--------|----------|--------
14//! `name` | `pipewire` | Yes | None
15//! `exclude_output` | An output node to ignore, example: `["HD Pro Webcam C920"]` | No | `[]`
16//! `exclude_input` | An input node to ignore, example: `["openrgb"]` | No | `[]`
17//! `display`   | Which node field should be used as a display name, options: `name`, `description`, `nickname` | No | `name`
18//!
19//! # vl4 Options
20//!
21//! Key | Values | Required | Default
22//! ----|--------|----------|--------
23//! `name` | `vl4` | Yes | None
24//! `exclude_device` | A device to ignore, example: `["/dev/video5"]` | No | `[]`
25//! `exclude_consumer` | Processes to ignore | No | `["pipewire", "wireplumber"]`
26//!
27//! # Available Format Keys
28//!
29//! Placeholder                                      | Value                                          | Type     | Unit
30//! -------------------------------------------------|------------------------------------------------|----------|-----
31//! `icon_{audio,audio_sink,video,webcam,unknown}`   | A static icon                                  | Icon     | -
32//! `info_{audio,audio_sink,video,webcam,unknown}`   | The mapping of which source are being consumed | Text     | -
33//!
34//! You can use the suffixes noted above to get the following:
35//!
36//! Suffix       | Description
37//! -------------|------------
38//! `audio`      | Captured audio (ex. Mic)
39//! `audio_sink` | Audio captured from a sink (ex. openrgb)
40//! `video`      | Video capture (ex. screen capture)
41//! `webcam`     | Webcam capture
42//! `unknown`    | Anything else
43//!
44//! # Available Actions
45//!
46//! Action          | Description                               | Default button
47//! ----------------|-------------------------------------------|---------------
48//! `toggle_format` **DEPRECATED** | Toggles between `format` and `format_alt` | -
49//! `next_format`  | Switches to the next format in the list     | Left
50//! `prev_format`  | Switches to the previous format in the list | Right
51//!
52//! # Example
53//!
54//! ```toml
55//! [[block]]
56//! block = "privacy"
57//! [[block.driver]]
58//! name = "v4l"
59//! [[block.driver]]
60//! name = "pipewire"
61//! exclude_input = ["openrgb"]
62//! display = "nickname"
63//! ```
64//!
65//! # Icons Used
66//! - `microphone`
67//! - `volume`
68//! - `xrandr`
69//! - `webcam`
70//! - `unknown`
71
72use futures::future::{select_all, try_join_all};
73
74use super::prelude::*;
75
76make_log_macro!(debug, "privacy");
77
78#[cfg(feature = "pipewire")]
79mod pipewire;
80mod v4l;
81
82#[derive(Deserialize, Debug)]
83pub struct Config {
84    #[serde(flatten)]
85    pub formats: MaybeMultiFormatConfig,
86    pub driver: Vec<PrivacyDriver>,
87}
88
89#[derive(Deserialize, Debug)]
90#[serde(tag = "name", rename_all = "snake_case")]
91pub enum PrivacyDriver {
92    #[cfg(feature = "pipewire")]
93    Pipewire(pipewire::Config),
94    V4l(v4l::Config),
95}
96
97#[derive(Debug, Clone, Eq, Hash, PartialEq)]
98enum Type {
99    Audio,
100    AudioSink,
101    Video,
102    Webcam,
103    Unknown,
104}
105
106// {type: {source: {destination: count}}
107type PrivacyInfo = HashMap<Type, PrivacyInfoInner>;
108
109type PrivacyInfoInnerType = HashMap<String, HashMap<String, usize>>;
110#[derive(Default, Debug)]
111struct PrivacyInfoInner(PrivacyInfoInnerType);
112
113impl std::ops::Deref for PrivacyInfoInner {
114    type Target = PrivacyInfoInnerType;
115    fn deref(&self) -> &Self::Target {
116        &self.0
117    }
118}
119
120impl std::ops::DerefMut for PrivacyInfoInner {
121    fn deref_mut(&mut self) -> &mut Self::Target {
122        &mut self.0
123    }
124}
125
126impl std::fmt::Display for PrivacyInfoInner {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        write!(
129            f,
130            "{{ {} }}",
131            itertools::join(
132                self.iter().map(|(source, destinations)| {
133                    format!(
134                        "{} => [ {} ]",
135                        source,
136                        itertools::join(
137                            destinations
138                                .iter()
139                                .map(|(destination, count)| if count == &1 {
140                                    destination.into()
141                                } else {
142                                    format!("{destination} (x{count})")
143                                }),
144                            ", "
145                        )
146                    )
147                }),
148                ", ",
149            )
150        )
151    }
152}
153
154#[async_trait]
155trait PrivacyMonitor {
156    async fn get_info(&mut self) -> Result<PrivacyInfo>;
157    async fn wait_for_change(&mut self) -> Result<()>;
158}
159
160pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
161    let mut actions = api.get_actions()?;
162    api.set_default_actions(&[
163        (MouseButton::Left, None, "next_format"),
164        (MouseButton::Right, None, "prev_format"),
165    ])?;
166
167    let mut formats = config
168        .formats
169        .with_default_formats(&[
170            "{ $icon_audio |}{ $icon_audio_sink |}{ $icon_video |}{ $icon_webcam |}{ $icon_unknown |}"
171                .parse()?,
172            "{ $icon_audio $info_audio |}{ $icon_audio_sink $info_audio_sink |}{ $icon_video $info_video |}{ $icon_webcam $info_webcam |}{ $icon_unknown $info_unknown |}"
173                .parse()?
174        ]);
175
176    let mut drivers: Vec<Box<dyn PrivacyMonitor + Send + Sync>> = Vec::new();
177
178    for driver in &config.driver {
179        drivers.push(match driver {
180            #[cfg(feature = "pipewire")]
181            PrivacyDriver::Pipewire(driver_config) => {
182                Box::new(pipewire::Monitor::new(driver_config).await?)
183            }
184            PrivacyDriver::V4l(driver_config) => {
185                Box::new(v4l::Monitor::new(driver_config, api.error_interval).await?)
186            }
187        });
188    }
189
190    loop {
191        let mut widget = Widget::new().with_format(formats.get_format());
192
193        let mut info = PrivacyInfo::default();
194        //Merge driver info
195        for driver_info in try_join_all(drivers.iter_mut().map(|driver| driver.get_info())).await? {
196            for (type_, mapping) in driver_info {
197                let existing_mapping = info.entry(type_).or_default();
198                for (source, dest) in mapping.0 {
199                    existing_mapping.entry(source).or_default().extend(dest);
200                }
201            }
202        }
203        if !info.is_empty() {
204            widget.state = State::Warning;
205        }
206
207        let mut values = Values::new();
208
209        if let Some(info_by_type) = info.get(&Type::Audio) {
210            map! { @extend values
211                "icon_audio" => Value::icon("microphone"),
212                "info_audio" => Value::text(info_by_type.to_string())
213            }
214        }
215        if let Some(info_by_type) = info.get(&Type::AudioSink) {
216            map! { @extend values
217                "icon_audio_sink" => Value::icon("volume"),
218                "info_audio_sink" => Value::text(info_by_type.to_string())
219            }
220        }
221        if let Some(info_by_type) = info.get(&Type::Video) {
222            map! { @extend values
223                "icon_video" => Value::icon("xrandr"),
224                "info_video" => Value::text(info_by_type.to_string())
225            }
226        }
227        if let Some(info_by_type) = info.get(&Type::Webcam) {
228            map! { @extend values
229                "icon_webcam" => Value::icon("webcam"),
230                "info_webcam" => Value::text(info_by_type.to_string())
231            }
232        }
233        if let Some(info_by_type) = info.get(&Type::Unknown) {
234            map! { @extend values
235                "icon_unknown" => Value::icon("unknown"),
236                "info_unknown" => Value::text(info_by_type.to_string())
237            }
238        }
239
240        widget.set_values(values);
241
242        api.set_widget(widget)?;
243
244        select! {
245            _ = api.wait_for_update_request() => (),
246            _ = select_all(drivers.iter_mut().map(|driver| driver.wait_for_change())) =>(),
247            Some(action) = actions.recv() => match action.as_ref() {
248                "next_format" | "toggle_format" => {
249                    formats.next_format();
250                }
251                "prev_format" => {
252                    formats.prev_format();
253                }
254                _ => (),
255            }
256        }
257    }
258}