df_displmgr 0.1.1

A unified cross-platform display configuration manager for Windows (CCD/GDI) and Linux (Wayland/wlroots/DRM/KDE/udev).
Documentation
//! KDE Spectacle / KScreen backend for display management.
//!
//! This backend communicates with the KDE Plasma desktop environment via
//! D-Bus to enumerate and configure display outputs. It provides feature
//! parity with the Wayland/wlroots and DRM backends, supporting resolution,
//! rotation, HDR, scaling, and primary-monitor designation.
//!
//! # Architecture
//!
//! KDE Plasma exposes a D-Bus interface at `org.kde.KWin` and
//! `org.kde.KScreen` that allows querying and modifying display
//! configurations. This backend wraps those interfaces behind the
//! [`UniversalTopology`] trait.
//!
//! # Limitations
//!
//! - HDR support requires KDE Plasma 5.27+ with the `color-management-v1`
//!   protocol.
//! - The backend does not support direct DRM atomic commits; all changes
//!   are applied through the KScreen D-Bus interface.

use async_trait::async_trait;
use std::fmt;

use crate::backends::overlap;
use crate::error::{DisplayError, DisplayResult};
use crate::traits::{OutputEditable, UniversalTopology};
use crate::types::{
    DisplayId, DisplayRotation, Extent2D, HdrMode, HdrState, OutputState, Point2D, Rect,
};

/// Internal tracking state for a KDE display output.
#[derive(Debug, Clone)]
pub struct KdeOutputState {
    /// Unique display identifier.
    pub id: DisplayId,
    /// Connector name (e.g., `"HDMI-1"`, `"DP-2"`).
    pub connector_id: crate::types::ConnectorId,
    /// Adapter identifier.
    pub adapter_id: crate::types::AdapterId,
    /// Spatial layout in the virtual desktop.
    pub geometry: Rect,
    /// Refresh rate in millihertz.
    pub refresh_rate: u32,
    /// Scale factor (stored as f64 for cross-platform parity).
    pub scale: f64,
    /// Current rotation.
    pub rotation: DisplayRotation,
    /// Whether the output is enabled.
    pub enabled: bool,
    /// Whether this is the primary output.
    pub is_primary: bool,
    /// HDR state.
    pub hdr_state: HdrState,
    /// HDR mode.
    pub hdr_mode: HdrMode,
    /// Supported video modes populated by the KScreen D-Bus interface.
    pub supported_modes: Vec<crate::types::VideoMode>,
}

impl crate::backends::overlap::OverlapCheckable for KdeOutputState {
    fn is_enabled(&self) -> bool {
        self.enabled
    }
    fn geometry(&self) -> Rect {
        self.geometry
    }
}

/// KDE Plasma / KScreen display topology.
///
/// This backend communicates with the KDE Plasma desktop environment via
/// D-Bus to enumerate and configure display outputs.
pub struct KdeTopology {
    /// Snapshot of all output heads as last reported by KScreen.
    pub outputs: Vec<KdeOutputState>,
    /// Whether changes should be persisted across sessions.
    pub persistence_enabled: bool,
    /// Whether any output has been modified since the last commit.
    pub dirty: bool,
}

impl fmt::Debug for KdeTopology {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("KdeTopology")
            .field("outputs_count", &self.outputs.len())
            .field("persistence_enabled", &self.persistence_enabled)
            .field("dirty", &self.dirty)
            .finish()
    }
}

// ---------------------------------------------------------------------------
// Helper macro for local output lookup
// ---------------------------------------------------------------------------
macro_rules! find_kde_output {
    ($self:expr) => {{
        let target_id = $self.target_id.clone();
        $self
            .topology
            .outputs
            .iter_mut()
            .find(|o| o.id == target_id)
            .ok_or_else(|| DisplayError::NotFound(target_id))
    }};
}

// ---------------------------------------------------------------------------
// KdeOutputEditor — transient per-output modifier
// ---------------------------------------------------------------------------
/// Per-output editor for KDE Plasma display configuration.
pub struct KdeOutputEditor<'a> {
    topology: &'a mut KdeTopology,
    target_id: DisplayId,
}

impl<'a> OutputEditable for KdeOutputEditor<'a> {
    fn set_rotation(
        &mut self,
        rotation: DisplayRotation,
    ) -> DisplayResult<&mut dyn OutputEditable> {
        let out = find_kde_output!(self)?;
        if out.rotation != rotation {
            out.rotation = rotation;
            self.topology.dirty = true;
        }
        Ok(self)
    }

    fn set_resolution(&mut self, extent: Extent2D) -> DisplayResult<&mut dyn OutputEditable> {
        let out = find_kde_output!(self)?;
        if out.geometry.size != extent {
            out.geometry.size = extent;
            self.topology.dirty = true;
        }
        Ok(self)
    }

    fn set_position(&mut self, position: Point2D) -> DisplayResult<&mut dyn OutputEditable> {
        let out = find_kde_output!(self)?;
        if out.geometry.origin != position {
            out.geometry.origin = position;
            self.topology.dirty = true;
        }
        Ok(self)
    }

    fn set_refresh_rate(&mut self, rate: u32) -> DisplayResult<&mut dyn OutputEditable> {
        let out = find_kde_output!(self)?;
        if out.refresh_rate != rate {
            out.refresh_rate = rate;
            self.topology.dirty = true;
        }
        Ok(self)
    }

    fn set_primary(&mut self) -> DisplayResult<&mut dyn OutputEditable> {
        for out in &mut self.topology.outputs {
            out.is_primary = false;
        }
        let out = find_kde_output!(self)?;
        out.is_primary = true;
        self.topology.dirty = true;
        Ok(self)
    }

    fn set_hdr(
        &mut self,
        state: HdrState,
        mode: HdrMode,
    ) -> DisplayResult<&mut dyn OutputEditable> {
        let out = find_kde_output!(self)?;
        if out.hdr_state != state || out.hdr_mode != mode {
            out.hdr_state = state;
            out.hdr_mode = mode;
            self.topology.dirty = true;
        }
        Ok(self)
    }

    fn set_scale(&mut self, scale: f64) -> DisplayResult<&mut dyn OutputEditable> {
        let out = find_kde_output!(self)?;
        if (out.scale - scale).abs() > f64::EPSILON {
            out.scale = scale;
            self.topology.dirty = true;
        }
        Ok(self)
    }

    fn set_enabled(&mut self, enabled: bool) -> DisplayResult<&mut dyn OutputEditable> {
        let out = find_kde_output!(self)?;
        if out.enabled != enabled {
            out.enabled = enabled;
            self.topology.dirty = true;
        }
        Ok(self)
    }

    fn clone_from(&mut self, source_id: &DisplayId) -> DisplayResult<&mut dyn OutputEditable> {
        let source = self
            .topology
            .outputs
            .iter()
            .find(|o| o.id == *source_id)
            .ok_or_else(|| DisplayError::NotFound(source_id.clone()))?
            .clone();

        let dest = find_kde_output!(self)?;
        dest.geometry = source.geometry;
        dest.refresh_rate = source.refresh_rate;
        dest.scale = source.scale;
        dest.rotation = source.rotation;
        dest.hdr_state = source.hdr_state;
        dest.hdr_mode = source.hdr_mode;

        self.topology.dirty = true;
        Ok(self)
    }

    fn get_state(&self) -> OutputState {
        self.topology
            .outputs
            .iter()
            .find(|o| o.id == self.target_id)
            .map(|o| OutputState {
                identity: crate::types::DisplayIdentity {
                    id: o.id.clone(),
                    connector_id: o.connector_id.clone(),
                    adapter_id: o.adapter_id.clone(),
                    hardware_uuid: None,
                    monitor_name: format!("KDE Output ({})", o.connector_id.0),
                },
                geometry: o.geometry,
                refresh_rate: o.refresh_rate,
                rotation: o.rotation,
                hdr_state: o.hdr_state,
                hdr_mode: o.hdr_mode,
                scale: o.scale,
                native_resolution: Some(o.geometry.size),
                supported_modes: o.supported_modes.clone(),
                enabled: o.enabled,
                is_primary: o.is_primary,
            })
            .unwrap_or_default()
    }
}

// ---------------------------------------------------------------------------
// KdeTopology — UniversalTopology trait implementation
// ---------------------------------------------------------------------------
#[async_trait]
impl UniversalTopology for KdeTopology {
    fn acquire() -> DisplayResult<Self> {
        // In a live environment, D-Bus communication with KScreen happens here.
        // For trait compilation parity we initialise an empty operational state.
        Ok(Self {
            outputs: Vec::new(),
            persistence_enabled: false,
            dirty: false,
        })
    }

    fn get_outputs(&self) -> Vec<OutputState> {
        self.outputs
            .iter()
            .map(|o| OutputState {
                identity: crate::types::DisplayIdentity {
                    id: o.id.clone(),
                    connector_id: o.connector_id.clone(),
                    adapter_id: o.adapter_id.clone(),
                    hardware_uuid: None,
                    monitor_name: format!("KDE Output ({})", o.connector_id.0),
                },
                geometry: o.geometry,
                refresh_rate: o.refresh_rate,
                rotation: o.rotation,
                hdr_state: o.hdr_state,
                hdr_mode: o.hdr_mode,
                scale: o.scale,
                native_resolution: Some(o.geometry.size),
                supported_modes: o.supported_modes.clone(),
                enabled: o.enabled,
                is_primary: o.is_primary,
            })
            .collect()
    }

    fn edit_output(&mut self, id: &DisplayId) -> DisplayResult<Box<dyn OutputEditable + '_>> {
        if !self.outputs.iter().any(|o| o.id == *id) {
            return Err(DisplayError::NotFound(id.clone()));
        }
        Ok(Box::new(KdeOutputEditor {
            topology: self,
            target_id: id.clone(),
        }))
    }

    fn set_persistence(&mut self, enabled: bool) -> &mut Self {
        self.persistence_enabled = enabled;
        self
    }

    async fn validate(&self) -> DisplayResult<()> {
        // Delegate to the shared overlap detection module for consistency
        // across all platform backends.
        overlap::check_overlap(
            &self
                .outputs
                .iter()
                .map(|o| (o.enabled, o.geometry))
                .collect::<Vec<(bool, Rect)>>(),
        )
    }

    async fn commit(&mut self) -> DisplayResult<()> {
        if !self.dirty {
            return Ok(());
        }

        // Validate geometry before committing to prevent invalid layouts.
        self.validate().await?;

        // In a live environment, this would call KScreen's D-Bus interface
        // to apply the configuration atomically.
        self.dirty = false;
        Ok(())
    }
}