1use 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
106type 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
160const TYPE_ICONS: [(Type, &str, &str); 5] = [
162 (Type::Audio, "icon_audio", icons::MICROPHONE),
163 (Type::AudioSink, "icon_audio_sink", icons::VOLUME),
164 (Type::Video, "icon_video", icons::XRANDR),
165 (Type::Webcam, "icon_webcam", icons::WEBCAM),
166 (Type::Unknown, "icon_unknown", icons::UNKNOWN),
167];
168
169impl PrivacyDriver {
170 fn capture_types(&self) -> &'static [Type] {
172 match self {
173 #[cfg(feature = "pipewire")]
174 PrivacyDriver::Pipewire(_) => &[
175 Type::Audio,
176 Type::AudioSink,
177 Type::Video,
178 Type::Webcam,
179 Type::Unknown,
180 ],
181 PrivacyDriver::V4l(_) => &[Type::Webcam],
182 }
183 }
184}
185
186pub(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
187 let formats = config.formats.with_default_formats(&[
188 "{ $icon_audio |}{ $icon_audio_sink |}{ $icon_video |}{ $icon_webcam |}{ $icon_unknown |}"
189 .parse()?,
190 "{ $icon_audio $info_audio |}{ $icon_audio_sink $info_audio_sink |}{ $icon_video $info_video |}{ $icon_webcam $info_webcam |}{ $icon_unknown $info_unknown |}"
191 .parse()?,
192 ]);
193
194 let declare = |mut output: OutputPlan| {
197 for (ty, placeholder, icon) in &TYPE_ICONS {
198 if config
199 .driver
200 .iter()
201 .any(|driver| driver.capture_types().contains(ty))
202 {
203 output = output.icon(placeholder, IconChoices::one(*icon));
204 }
205 }
206 output
207 };
208 BlockPlan::new(format_outputs(formats, declare))
209}
210
211pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
212 let mut actions = api.get_actions()?;
213 api.set_default_actions(&[
214 (MouseButton::Left, None, "next_format"),
215 (MouseButton::Right, None, "prev_format"),
216 ])?;
217
218 let mut formats = FormatRotation::new(plan)?;
219
220 let mut drivers: Vec<Box<dyn PrivacyMonitor + Send + Sync>> = Vec::new();
221
222 for driver in &config.driver {
223 drivers.push(match driver {
224 #[cfg(feature = "pipewire")]
225 PrivacyDriver::Pipewire(driver_config) => {
226 Box::new(pipewire::Monitor::new(driver_config).await?)
227 }
228 PrivacyDriver::V4l(driver_config) => {
229 Box::new(v4l::Monitor::new(driver_config, api.error_interval).await?)
230 }
231 });
232 }
233
234 loop {
235 let output = formats.current();
236 let mut widget = output.new_widget();
237
238 let mut info = PrivacyInfo::default();
239 for driver_info in try_join_all(drivers.iter_mut().map(|driver| driver.get_info())).await? {
241 for (type_, mapping) in driver_info {
242 let existing_mapping = info.entry(type_).or_default();
243 for (source, dest) in mapping.0 {
244 existing_mapping.entry(source).or_default().extend(dest);
245 }
246 }
247 }
248 if !info.is_empty() {
249 widget.state = State::Warning;
250 }
251
252 let mut values = Values::new();
253
254 if let Some(info_by_type) = info.get(&Type::Audio) {
255 map! { @extend values
256 "icon_audio" => Value::icon(icons::MICROPHONE),
257 "info_audio" => Value::text(info_by_type.to_string())
258 }
259 }
260 if let Some(info_by_type) = info.get(&Type::AudioSink) {
261 map! { @extend values
262 "icon_audio_sink" => Value::icon(icons::VOLUME),
263 "info_audio_sink" => Value::text(info_by_type.to_string())
264 }
265 }
266 if let Some(info_by_type) = info.get(&Type::Video) {
267 map! { @extend values
268 "icon_video" => Value::icon(icons::XRANDR),
269 "info_video" => Value::text(info_by_type.to_string())
270 }
271 }
272 if let Some(info_by_type) = info.get(&Type::Webcam) {
273 map! { @extend values
274 "icon_webcam" => Value::icon(icons::WEBCAM),
275 "info_webcam" => Value::text(info_by_type.to_string())
276 }
277 }
278 if let Some(info_by_type) = info.get(&Type::Unknown) {
279 map! { @extend values
280 "icon_unknown" => Value::icon(icons::UNKNOWN),
281 "info_unknown" => Value::text(info_by_type.to_string())
282 }
283 }
284
285 widget.set_values(values);
286
287 api.set_widget(widget)?;
288
289 select! {
290 _ = api.wait_for_update_request() => (),
291 _ = select_all(drivers.iter_mut().map(|driver| driver.wait_for_change())) =>(),
292 Some(action) = actions.recv() => match action.as_ref() {
293 "next_format" | "toggle_format" => {
294 formats.next();
295 }
296 "prev_format" => {
297 formats.prev();
298 }
299 _ => (),
300 }
301 }
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 fn config() -> Config {
310 Config {
311 formats: Default::default(),
312 driver: Vec::new(),
313 }
314 }
315
316 fn config_with_drivers(toml_drivers: &str) -> Config {
317 toml::from_str(toml_drivers).unwrap()
318 }
319
320 #[test]
321 fn plan_declares_driver_reachable_icons_on_both_outputs() {
322 let config = config_with_drivers("[[driver]]\nname = \"v4l\"");
324 let plan = prepare(&config).unwrap();
325 let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
326 assert_eq!(ids, ["format", "format2"]);
327 for id in ids {
328 let output = plan.output(id).unwrap();
329 assert_eq!(output.single_icon("icon_webcam").unwrap(), "webcam");
330 assert_eq!(
331 output.output().icon_placeholders().count(),
332 1,
333 "{id} must declare only V4L-reachable icons"
334 );
335 }
336 }
337
338 #[cfg(feature = "pipewire")]
339 #[test]
340 fn pipewire_declares_every_type_icon() {
341 let config = config_with_drivers("[[driver]]\nname = \"pipewire\"");
342 let plan = prepare(&config).unwrap();
343 for id in ["format", "format2"] {
344 let output = plan.output(id).unwrap();
345 for (_, placeholder, icon) in &TYPE_ICONS {
346 assert_eq!(
347 output.single_icon(placeholder).unwrap(),
348 *icon,
349 "{id} must declare {icon} for ${placeholder}"
350 );
351 }
352 }
353 }
354
355 #[test]
356 fn every_capture_type_has_a_declared_icon() {
357 let types = [
361 Type::Audio,
362 Type::AudioSink,
363 Type::Video,
364 Type::Webcam,
365 Type::Unknown,
366 ];
367 assert_eq!(types.len(), TYPE_ICONS.len());
368 for type_ in types {
369 let (placeholder, icon) = match type_ {
370 Type::Audio => ("icon_audio", "microphone"),
371 Type::AudioSink => ("icon_audio_sink", "volume"),
372 Type::Video => ("icon_video", "xrandr"),
373 Type::Webcam => ("icon_webcam", "webcam"),
374 Type::Unknown => ("icon_unknown", "unknown"),
375 };
376 assert!(
377 TYPE_ICONS
378 .iter()
379 .any(|(t, p, i)| *t == type_ && *p == placeholder && *i == icon)
380 );
381 }
382 }
383
384 #[test]
385 fn the_second_default_format_is_the_detailed_one() {
386 let plan = prepare(&config()).unwrap();
387 assert!(
388 !plan
389 .output("format")
390 .unwrap()
391 .format()
392 .contains_key("info_audio")
393 );
394 assert!(
395 plan.output("format2")
396 .unwrap()
397 .format()
398 .contains_key("info_audio")
399 );
400 }
401
402 #[test]
403 fn every_configured_format_becomes_an_output() {
404 let config: Config = toml::from_str(
405 "format = [\"{ $icon_webcam |}\", \"{ $icon_webcam $info_webcam |}\"]\n\
406 [[driver]]\nname = \"v4l\"",
407 )
408 .unwrap();
409 let plan = prepare(&config).unwrap();
410 let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
411 assert_eq!(ids, ["format", "format2"]);
412 for id in ids {
413 assert_eq!(
414 plan.output(id).unwrap().single_icon("icon_webcam").unwrap(),
415 "webcam"
416 );
417 }
418 }
419}