wayle-brightness 0.1.2

Backlight control for internal displays
Documentation
use std::{collections::HashMap, sync::Arc};

use tokio::sync::broadcast::error::RecvError;
use tokio_util::sync::CancellationToken;
use tracing::{info, warn};
use wayle_core::Property;
use wayle_traits::{ModelMonitoring, ServiceMonitoring};

use crate::{
    Error,
    backend::types::{BrightnessEvent, CommandSender, EventSender},
    core::BacklightDevice,
    service::BrightnessService,
    types::{BacklightInfo, DeviceName},
};

type DeviceMap = HashMap<DeviceName, Arc<BacklightDevice>>;

impl ServiceMonitoring for BrightnessService {
    type Error = Error;

    async fn start_monitoring(&self) -> Result<(), Self::Error> {
        let mut event_rx = self.event_tx.subscribe();

        let command_tx = self.command_tx.clone();
        let event_tx = self.event_tx.clone();
        let devices = self.devices.clone();
        let primary = self.primary.clone();
        let cancellation_token = self.cancellation_token.clone();

        tokio::spawn(async move {
            let mut devices_map: DeviceMap = HashMap::new();

            loop {
                tokio::select! {
                    _ = cancellation_token.cancelled() => {
                        info!("brightness monitoring stopped");
                        return;
                    }

                    result = event_rx.recv() => {
                        match result {
                            Ok(event) => handle_event(
                                event,
                                &mut devices_map,
                                &command_tx,
                                &event_tx,
                                &cancellation_token,
                                &devices,
                                &primary,
                            ).await,

                            Err(RecvError::Lagged(count)) => {
                                warn!(
                                    count,
                                    "brightness events lost, state may be stale"
                                );
                            }

                            Err(RecvError::Closed) => return,
                        }
                    }
                }
            }
        });

        Ok(())
    }
}

async fn handle_event(
    event: BrightnessEvent,
    devices_map: &mut DeviceMap,
    command_tx: &CommandSender,
    event_tx: &EventSender,
    parent_token: &CancellationToken,
    devices: &Property<Vec<Arc<BacklightDevice>>>,
    primary: &Property<Option<Arc<BacklightDevice>>>,
) {
    match event {
        BrightnessEvent::DeviceAdded(info) => {
            upsert_device(
                info,
                devices_map,
                command_tx,
                event_tx,
                parent_token,
                devices,
                primary,
            )
            .await;
        }

        BrightnessEvent::DeviceChanged(info) => {
            if let Some(existing) = devices_map.get(&info.name) {
                existing.update_brightness(info.brightness);
                return;
            }

            upsert_device(
                info,
                devices_map,
                command_tx,
                event_tx,
                parent_token,
                devices,
                primary,
            )
            .await;
        }

        BrightnessEvent::DeviceRemoved(name) => {
            let key = DeviceName::new(name);
            cancel_existing(devices_map, &key);
            devices_map.remove(&key);
            update_properties(devices_map, devices, primary);
        }
    }
}

async fn upsert_device(
    info: BacklightInfo,
    devices_map: &mut DeviceMap,
    command_tx: &CommandSender,
    event_tx: &EventSender,
    parent_token: &CancellationToken,
    devices: &Property<Vec<Arc<BacklightDevice>>>,
    primary: &Property<Option<Arc<BacklightDevice>>>,
) {
    cancel_existing(devices_map, &info.name);

    let device = create_device(&info, command_tx, event_tx, parent_token);

    if let Err(err) = device.clone().start_monitoring().await {
        warn!(error = %err, "cannot start device monitoring");
    }

    devices_map.insert(info.name, device);
    update_properties(devices_map, devices, primary);
}

fn create_device(
    info: &BacklightInfo,
    command_tx: &CommandSender,
    event_tx: &EventSender,
    parent_token: &CancellationToken,
) -> Arc<BacklightDevice> {
    Arc::new(BacklightDevice::from_info(
        info,
        command_tx.clone(),
        Some(event_tx.clone()),
        Some(parent_token.child_token()),
    ))
}

fn cancel_existing(devices_map: &DeviceMap, name: &DeviceName) {
    let token = devices_map
        .get(name)
        .and_then(|device| device.cancellation_token.as_ref());

    if let Some(token) = token {
        token.cancel();
    }
}

fn update_properties(
    devices_map: &DeviceMap,
    devices: &Property<Vec<Arc<BacklightDevice>>>,
    primary: &Property<Option<Arc<BacklightDevice>>>,
) {
    let device_list: Vec<_> = devices_map.values().cloned().collect();
    devices.set(device_list);

    let best = devices_map
        .values()
        .max_by_key(|device| device.backlight_type)
        .cloned();

    primary.set(best);
}