i3status-rs 0.36.0

A feature-rich and resource-friendly replacement for i3status, written in Rust.
Documentation
use inotify::{EventStream, Inotify, WatchDescriptor, WatchMask, Watches};
use tokio::fs::{File, read_dir};
use tokio::time::{Interval, interval};

use std::path::PathBuf;

use super::*;

#[derive(Deserialize, Debug, SmartDefault)]
#[serde(rename_all = "lowercase", deny_unknown_fields, default)]
pub struct Config {
    exclude_device: Vec<PathBuf>,
    #[default(vec!["pipewire".into(), "wireplumber".into()])]
    exclude_consumer: Vec<String>,
}

pub(super) struct Monitor<'a> {
    config: &'a Config,
    devices: HashMap<PathBuf, WatchDescriptor>,
    interval: Interval,
    watches: Watches,
    stream: EventStream<[u8; 1024]>,
}

impl<'a> Monitor<'a> {
    pub(super) async fn new(config: &'a Config, duration: Duration) -> Result<Self> {
        let notify = Inotify::init().error("Failed to start inotify")?;
        let watches = notify.watches();

        let stream = notify
            .into_event_stream([0; 1024])
            .error("Failed to create event stream")?;

        let mut s = Self {
            config,
            devices: HashMap::new(),
            interval: interval(duration),
            watches,
            stream,
        };
        s.update_devices().await?;

        Ok(s)
    }

    async fn update_devices(&mut self) -> Result<bool> {
        let mut changes = false;
        let mut devices_to_remove: HashMap<PathBuf, WatchDescriptor> = self.devices.clone();
        let mut sysfs_paths = read_dir("/dev").await.error("Unable to read /dev")?;
        while let Some(entry) = sysfs_paths
            .next_entry()
            .await
            .error("Unable to get next device in /dev")?
        {
            if let Some(file_name) = entry.file_name().to_str()
                && !file_name.starts_with("video")
            {
                continue;
            }

            let sysfs_path = entry.path();

            if self.config.exclude_device.contains(&sysfs_path) {
                debug!("ignoring {:?}", sysfs_path);
                continue;
            }

            if self.devices.contains_key(&sysfs_path) {
                devices_to_remove.remove(&sysfs_path);
            } else {
                debug!("adding watch {:?}", sysfs_path);
                self.devices.insert(
                    sysfs_path.clone(),
                    self.watches
                        .add(&sysfs_path, WatchMask::OPEN | WatchMask::CLOSE)
                        .error("Failed to watch data location")?,
                );
                changes = true;
            }
        }
        for (sysfs_path, wd) in devices_to_remove {
            debug!("removing watch {:?}", sysfs_path);
            self.devices.remove(&sysfs_path);
            self.watches
                .remove(wd)
                .error("Failed to unwatch data location")?;
            changes = true;
        }

        Ok(changes)
    }
}

#[async_trait]
impl PrivacyMonitor for Monitor<'_> {
    async fn get_info(&mut self) -> Result<PrivacyInfo> {
        let mut mapping: PrivacyInfo = PrivacyInfo::new();

        let mut proc_paths = read_dir("/proc").await.error("Unable to read /proc")?;
        while let Some(proc_path) = proc_paths
            .next_entry()
            .await
            .error("Unable to get next device in /proc")?
        {
            let proc_path = proc_path.path();
            let fd_path = proc_path.join("fd");
            let Ok(mut fd_paths) = read_dir(fd_path).await else {
                continue;
            };
            while let Ok(Some(fd_path)) = fd_paths.next_entry().await {
                let mut contents = String::new();
                if let Ok(link_path) = fd_path.path().read_link()
                    && self.devices.contains_key(&link_path)
                    && let Ok(mut file) = File::open(proc_path.join("comm")).await
                    && file.read_to_string(&mut contents).await.is_ok()
                {
                    let reader = contents.trim_end().to_string();
                    if self.config.exclude_consumer.contains(&reader) {
                        continue;
                    }
                    debug!("{} {:?}", reader, link_path);
                    *mapping
                        .entry(Type::Webcam)
                        .or_default()
                        .entry(link_path.to_string_lossy().to_string())
                        .or_default()
                        .entry(reader)
                        .or_default() += 1;
                    debug!("{:?}", mapping);
                }
            }
        }
        Ok(mapping)
    }

    async fn wait_for_change(&mut self) -> Result<()> {
        loop {
            select! {
                _ = self.interval.tick() => {
                    if self.update_devices().await? {
                        break;
                    }
                },
                _ = self.stream.next_debounced() => break
            }
        }
        Ok(())
    }
}