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