//! Pure monitor selection. DRM handles and side effects never enter this module. use std::collections::{BTreeSet, HashSet}; use crate::config::{LidPolicy, OutputLayout, OutputProfile, OutputSettings, OutputTransform}; #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct Monitor { /// Qualified hardware connector (GPU identity + connector), never a runtime Output handle. pub key: String, pub connector: String, pub identity: String, pub aliases: Vec, pub internal: bool, pub usable: bool, } impl Monitor { pub fn matches(&self, selector: &str) -> bool { selector == self.connector || selector == self.identity || self.aliases.iter().any(|alias| alias == selector) } } #[derive(Clone, Debug, PartialEq)] pub(crate) struct DesiredOutput { pub key: String, pub settings: OutputSettings, /// A mirror target has no workspace ownership of its own. pub mirror_source: Option, } #[derive(Clone, Debug, Default, PartialEq)] pub(crate) struct DesiredOutputConfiguration { pub profile: Option, pub outputs: Vec, pub suspend: bool, } /// Overrides expire on a *material* topology/lid change, independent of event ordering. #[derive(Clone, Debug, Default)] pub(crate) struct ManualOverride { pub profile: Option, pub internal: Option, pub layout: Option, topology: Option<(BTreeSet, bool)>, } impl ManualOverride { pub fn observe(&mut self, monitors: &[Monitor], lid_closed: bool, profiles: &[OutputProfile]) { let topology = ( monitors .iter() .map(|monitor| format!("{}={}", monitor.key, monitor.identity)) .collect(), lid_closed, ); if self.topology.as_ref().is_some_and(|old| old != &topology) { self.profile = None; self.internal = None; self.layout = None; } if self.profile.as_ref().is_some_and(|name| { !profiles.iter().any(|profile| { profile.name == *name && profile_matches(profile, monitors, lid_closed) }) }) { self.profile = None; } self.topology = Some(topology); } } fn profile_matches(profile: &OutputProfile, monitors: &[Monitor], lid_closed: bool) -> bool { let mut matched = HashSet::new(); if profile.outputs.iter().any(|settings| { monitors .iter() .filter(|monitor| monitor.matches(&settings.matcher)) .any(|monitor| !matched.insert(&monitor.key)) }) { return false; } profile .lid_closed .is_none_or(|required| required == lid_closed) && profile .outputs .iter() .filter(|settings| settings.required) .all(|settings| { // Ambiguous selectors cannot silently configure two monitors as one. monitors .iter() .filter(|monitor| monitor.matches(&settings.matcher)) .count() == 1 }) } pub(crate) fn defaults(matcher: String) -> OutputSettings { OutputSettings { auto_refresh: false, matcher, required: true, enabled: true, mode: None, scale: 1.0, transform: OutputTransform::Normal, position: None, } } /// Most required monitors wins; equal specificity retains config order. /// Unlisted outputs keep working in extend profiles. Exclusive layouts deliberately filter them. pub(crate) fn select_profile( monitors: &[Monitor], lid_closed: bool, profiles: &[OutputProfile], manual: &ManualOverride, ) -> DesiredOutputConfiguration { let selected = manual .profile .as_ref() .and_then(|name| { profiles.iter().find(|profile| { profile.name == *name && profile_matches(profile, monitors, lid_closed) }) }) .or_else(|| { profiles .iter() .enumerate() .filter(|(_, profile)| profile_matches(profile, monitors, lid_closed)) .max_by_key(|(index, profile)| { ( profile .outputs .iter() .filter(|output| output.required) .count(), std::cmp::Reverse(*index), ) }) .map(|(_, profile)| profile) }); let layout = manual .layout .unwrap_or_else(|| selected.map_or(OutputLayout::Extend, |profile| profile.layout)); let manage_lid = selected.is_none_or(|profile| profile.lid_policy == LidPolicy::DockOrSuspend); let external = monitors .iter() .any(|monitor| monitor.usable && !monitor.internal); let mut outputs = monitors .iter() .map(|monitor| { let mut settings = selected .and_then(|profile| { profile .outputs .iter() .find(|settings| monitor.matches(&settings.matcher)) }) .cloned() .unwrap_or_else(|| defaults(monitor.identity.clone())); if manual.layout.is_some() { settings.enabled = true; } settings.enabled &= monitor.usable; settings.enabled &= match layout { OutputLayout::InternalOnly => monitor.internal, OutputLayout::ExternalOnly => !monitor.internal, OutputLayout::Extend | OutputLayout::Mirror => true, }; if monitor.internal { if let Some(enabled) = manual.internal { settings.enabled = enabled && monitor.usable; } else if manage_lid && lid_closed && external { settings.enabled = false; } } DesiredOutput { key: monitor.key.clone(), settings, mirror_source: None, } }) .collect::>(); if !outputs.iter().any(|output| output.settings.enabled) { // Policy never disables the last usable display, even for an inapplicable exclusive layout. if let Some((index, _)) = monitors .iter() .enumerate() .filter(|(_, monitor)| monitor.usable) .min_by_key(|(_, monitor)| (!monitor.internal, &monitor.key)) { outputs[index].settings = defaults(monitors[index].identity.clone()); } } if layout == OutputLayout::Mirror { let source = selected .and_then(|profile| profile.mirror_source.as_ref()) .and_then(|selector| { monitors .iter() .zip(&outputs) .find(|(monitor, output)| monitor.matches(selector) && output.settings.enabled) .map(|(_, output)| output.key.clone()) }) .or_else(|| { outputs .iter() .filter(|output| output.settings.enabled) .map(|output| output.key.clone()) .min() }); if let Some(source) = source { for output in &mut outputs { if output.settings.enabled && output.key != source { output.mirror_source = Some(source.clone()); } } } } DesiredOutputConfiguration { profile: selected.map(|profile| profile.name.clone()), outputs, suspend: manage_lid && lid_closed && !external, } } /// Restore actual settings only for surviving hardware; new monitors remain safe fallbacks. pub(crate) fn restore_configuration( previous: &DesiredOutputConfiguration, monitors: &[Monitor], ) -> DesiredOutputConfiguration { let mut restored = previous.clone(); restored .outputs .retain(|output| monitors.iter().any(|monitor| monitor.key == output.key)); for monitor in monitors { if !restored .outputs .iter() .any(|output| output.key == monitor.key) { let mut settings = defaults(monitor.identity.clone()); settings.enabled = false; restored.outputs.push(DesiredOutput { key: monitor.key.clone(), settings, mirror_source: None, }); } } let source_keys = restored .outputs .iter() .filter(|output| output.settings.enabled) .map(|output| output.key.clone()) .collect::>(); for output in &mut restored.outputs { if output .mirror_source .as_ref() .is_some_and(|source| !source_keys.contains(source)) { output.mirror_source = None; } } if !restored .outputs .iter() .any(|output| output.settings.enabled) { return select_profile(monitors, false, &[], &ManualOverride::default()); } restored } /// A diff carries only affected connectors; removing a connector is an explicit disable. pub(crate) fn changed_outputs<'a>( old: &'a DesiredOutputConfiguration, new: &'a DesiredOutputConfiguration, ) -> Vec<&'a str> { let mut keys = HashSet::new(); old.outputs .iter() .chain(&new.outputs) .filter_map(|output| { let key = output.key.as_str(); if !keys.insert(key) { return None; } let previous = old.outputs.iter().find(|candidate| candidate.key == key); let next = new.outputs.iter().find(|candidate| candidate.key == key); let same = match (previous, next) { (Some(previous), Some(next)) => { previous.settings.same_configuration(&next.settings) && previous.mirror_source == next.mirror_source } (None, None) => true, _ => false, }; (!same).then_some(key) }) .collect() } #[derive(Default)] pub(crate) struct Debouncer { deadline: Option, } impl Debouncer { pub fn request( &mut self, now: std::time::Duration, active: bool, ) -> Option { self.deadline = active.then(|| now + std::time::Duration::from_millis(150)); self.deadline } pub fn clear(&mut self) { self.deadline = None; } pub fn take_due(&mut self, now: std::time::Duration) -> bool { if self.deadline.is_some_and(|deadline| deadline <= now) { self.deadline = None; true } else { false } } } pub(crate) struct Placement { pub key: String, pub size: (i32, i32), pub previous: Option<[i32; 2]>, } /// Explicit positions win. Preserve an automatic output's old position when /// it still fits; append new/conflicting outputs in stable hardware-key order. pub(crate) fn arrange_positions( desired: &mut DesiredOutputConfiguration, placements: &[Placement], ) -> Result<(), String> { let mut occupied = Vec::new(); let mut automatic = Vec::new(); for (index, output) in desired.outputs.iter().enumerate() { if !output.settings.enabled || output.mirror_source.is_some() { continue; } let placement = placements .iter() .find(|placement| placement.key == output.key) .ok_or("missing output dimensions")?; if let Some(position) = output.settings.position { occupied.push((position[0], position[1], placement.size.0, placement.size.1)); } else { automatic.push((index, placement)); } } automatic.sort_by(|(_, a), (_, b)| { (a.previous.is_none(), &a.key).cmp(&(b.previous.is_none(), &b.key)) }); for (index, placement) in automatic { let fits = |position: [i32; 2]| { let (x, y, w, h) = ( position[0] as i64, position[1] as i64, placement.size.0 as i64, placement.size.1 as i64, ); !occupied.iter().any(|&(ox, oy, ow, oh)| { x < ox as i64 + ow as i64 && x + w > ox as i64 && y < oy as i64 + oh as i64 && y + h > oy as i64 }) }; let previous = placement.previous.filter(|position| fits(*position)); let position = if let Some(previous) = previous { previous } else { let right = occupied .iter() .map(|&(x, _, w, _)| x as i64 + w as i64) .max() .unwrap_or(0) .max(0); let x = i32::try_from(right).map_err(|_| "automatic output position overflows")?; x.checked_add(placement.size.0) .ok_or("automatic output geometry overflows")?; [x, 0] }; desired.outputs[index].settings.position = Some(position); occupied.push((position[0], position[1], placement.size.0, placement.size.1)); } let source_positions = desired .outputs .iter() .filter(|output| output.mirror_source.is_none()) .map(|output| (output.key.clone(), output.settings.position)) .collect::>(); for output in &mut desired.outputs { if let Some(source) = &output.mirror_source { output.settings.position = source_positions.get(source).copied().flatten(); } } Ok(()) } #[cfg(test)] mod placement_tests { use super::*; #[test] fn automatic_placement_is_stable_and_only_moves_overlapping_neighbors() { let monitors = [ Monitor { key: "a".into(), connector: "a".into(), identity: "a".into(), aliases: vec![], internal: true, usable: true, }, Monitor { key: "b".into(), connector: "b".into(), identity: "b".into(), aliases: vec![], internal: false, usable: true, }, ]; let mut desired = select_profile(&monitors, false, &[], &ManualOverride::default()); let placements = [ Placement { key: "b".into(), size: (100, 100), previous: Some([100, 0]), }, Placement { key: "a".into(), size: (100, 100), previous: Some([0, 0]), }, ]; arrange_positions(&mut desired, &placements).unwrap(); assert_eq!(desired.outputs[0].settings.position, Some([0, 0])); assert_eq!(desired.outputs[1].settings.position, Some([100, 0])); let mut expanded = select_profile(&monitors, false, &[], &ManualOverride::default()); let placements = [ Placement { key: "a".into(), size: (150, 100), previous: Some([0, 0]), }, Placement { key: "b".into(), size: (100, 100), previous: Some([100, 0]), }, ]; arrange_positions(&mut expanded, &placements).unwrap(); assert_eq!(expanded.outputs[0].settings.position, Some([0, 0])); assert_eq!(expanded.outputs[1].settings.position, Some([150, 0])); } } #[cfg(test)] mod tests { use super::*; fn monitor(name: &str, internal: bool) -> Monitor { Monitor { key: name.into(), connector: name.into(), identity: format!("edid:{name}"), aliases: vec![], internal, usable: true, } } fn profile(name: &str, outputs: &[&str], layout: OutputLayout) -> OutputProfile { OutputProfile { name: name.into(), outputs: outputs .iter() .map(|name| defaults((*name).into())) .collect(), layout, confirm_timeout: 15, lid_policy: LidPolicy::DockOrSuspend, lid_closed: None, mirror_source: None, } } #[test] fn manual_layouts_reuse_profiles_and_expire_on_monitor_changes() { let connected = [monitor("eDP-1", true), monitor("DP-1", false)]; let mut settings = defaults("DP-1".into()); settings.enabled = false; settings.scale = 1.5; let mut mobile = profile("mobile", &["eDP-1"], OutputLayout::InternalOnly); mobile.outputs.push(settings); let profiles = [mobile]; for layout in [ OutputLayout::InternalOnly, OutputLayout::ExternalOnly, OutputLayout::Extend, OutputLayout::Mirror, ] { let mut manual = ManualOverride { layout: Some(layout), ..ManualOverride::default() }; manual.observe(&connected, false, &profiles); let desired = select_profile(&connected, false, &profiles, &manual); assert_eq!( desired.outputs[0].settings.enabled, layout != OutputLayout::ExternalOnly ); assert_eq!( desired.outputs[1].settings.enabled, layout != OutputLayout::InternalOnly ); assert_eq!( desired.outputs[1].settings.scale, 1.5, "manual layout must preserve configured scale" ); assert_eq!( desired .outputs .iter() .filter(|output| output.mirror_source.is_some()) .count(), usize::from(layout == OutputLayout::Mirror) ); manual.observe(&connected[..1], false, &profiles); assert_eq!(manual.layout, None); assert!( select_profile(&connected[..1], false, &profiles, &manual).outputs[0] .settings .enabled ); } } #[test] fn specificity_and_config_order() { let monitors = [monitor("eDP-1", true), monitor("DP-1", false)]; let profiles = [ profile("mobile", &["eDP-1"], OutputLayout::Extend), profile("desk", &["eDP-1", "DP-1"], OutputLayout::Extend), profile("tie", &["DP-1", "eDP-1"], OutputLayout::Extend), ]; assert_eq!( select_profile(&monitors, false, &profiles, &ManualOverride::default()) .profile .as_deref(), Some("desk") ); } #[test] fn layouts_unknown_outputs_and_safe_fallback() { let monitors = [ monitor("eDP-1", true), monitor("DP-1", false), monitor("HDMI-A-1", false), ]; for (layout, count) in [ (OutputLayout::InternalOnly, 1), (OutputLayout::ExternalOnly, 2), (OutputLayout::Extend, 3), (OutputLayout::Mirror, 3), ] { let desired = select_profile( &monitors, false, &[profile("test", &["eDP-1"], layout)], &ManualOverride::default(), ); assert_eq!( desired .outputs .iter() .filter(|output| output.settings.enabled) .count(), count ); assert_eq!( desired .outputs .iter() .filter(|output| output.mirror_source.is_some()) .count(), if layout == OutputLayout::Mirror { 2 } else { 0 } ); } let missing = [profile("absent", &["DP-9"], OutputLayout::ExternalOnly)]; let fallback = select_profile(&monitors, false, &missing, &ManualOverride::default()); assert_eq!(fallback.profile, None); assert!( fallback .outputs .iter() .all(|output| output.settings.enabled) ); let internal = &monitors[..1]; let mut disabled = profile("disabled", &["eDP-1"], OutputLayout::Extend); disabled.outputs[0].enabled = false; assert!( select_profile(internal, false, &[disabled], &ManualOverride::default()).outputs[0] .settings .enabled ); } #[test] fn override_expires_only_on_material_change_or_invalid_reload() { let monitors = [monitor("eDP-1", true), monitor("DP-1", false)]; let profiles = [profile("mobile", &["eDP-1"], OutputLayout::InternalOnly)]; let mut manual = ManualOverride::default(); manual.observe(&monitors, false, &profiles); manual.profile = Some("mobile".into()); manual.observe(&monitors, false, &profiles); assert_eq!(manual.profile.as_deref(), Some("mobile")); manual.observe(&monitors, true, &profiles); assert_eq!(manual.profile, None); manual.profile = Some("mobile".into()); manual.observe(&monitors, true, &[]); assert_eq!(manual.profile, None); } #[test] fn lid_dock_undock_and_internal_toggle_never_disable_last_output() { let monitors = [monitor("eDP-1", true), monitor("DP-1", false)]; let docked = select_profile(&monitors, true, &[], &ManualOverride::default()); assert!(!docked.suspend); assert!(!docked.outputs[0].settings.enabled); assert!(docked.outputs[1].settings.enabled); let manual = ManualOverride { internal: Some(false), ..ManualOverride::default() }; let undocked = select_profile(&monitors[..1], true, &[], &manual); assert!(undocked.suspend); assert!(undocked.outputs[0].settings.enabled); } #[test] fn desired_diff_is_local() { let monitors = [monitor("eDP-1", true), monitor("DP-1", false)]; let old = select_profile(&monitors, false, &[], &ManualOverride::default()); assert!(changed_outputs(&old, &old).is_empty()); let mut new = old.clone(); new.outputs[1].settings.scale = 1.5; assert_eq!(changed_outputs(&old, &new), vec!["DP-1"]); } #[test] fn mirror_removal_and_extend_restore_single_ownership_plan() { let monitors = [monitor("eDP-1", true), monitor("DP-1", false)]; let mut mirror = profile("mirror", &["eDP-1", "DP-1"], OutputLayout::Mirror); mirror.mirror_source = Some("eDP-1".into()); let old = select_profile( &monitors, false, &[mirror.clone()], &ManualOverride::default(), ); assert_eq!(old.outputs[1].mirror_source.as_deref(), Some("eDP-1")); let removed = select_profile(&monitors[..1], false, &[mirror], &ManualOverride::default()); assert!(removed.outputs[0].mirror_source.is_none()); assert!(removed.outputs[0].settings.enabled); let extended = select_profile(&monitors, false, &[], &ManualOverride::default()); assert!( extended .outputs .iter() .all(|output| output.mirror_source.is_none()) ); assert_eq!(changed_outputs(&old, &extended), vec!["DP-1"]); } #[test] fn optional_outputs_and_lid_specific_profiles() { let monitors = [monitor("eDP-1", true)]; let mut mobile = profile("mobile", &["eDP-1", "DP-1"], OutputLayout::Extend); mobile.outputs[1].required = false; mobile.lid_closed = Some(false); assert_eq!( select_profile( &monitors, false, &[mobile.clone()], &ManualOverride::default() ) .profile .as_deref(), Some("mobile") ); assert_eq!( select_profile(&monitors, true, &[mobile], &ManualOverride::default()).profile, None ); } #[test] fn swapping_monitor_on_same_connector_expires_override() { let mut monitors = [monitor("eDP-1", true), monitor("DP-1", false)]; let profiles = [profile("mobile", &["eDP-1"], OutputLayout::InternalOnly)]; let mut manual = ManualOverride::default(); manual.observe(&monitors, false, &profiles); manual.profile = Some("mobile".into()); monitors[1].identity = "edid:new-monitor".into(); manual.observe(&monitors, false, &profiles); assert!(manual.profile.is_none()); } #[test] fn restoration_survives_disconnect_and_keeps_new_monitor_as_emergency_fallback() { let monitors = [monitor("eDP-1", true), monitor("DP-1", false)]; let previous = select_profile(&monitors, false, &[], &ManualOverride::default()); let restored = restore_configuration(&previous, &monitors[..1]); assert_eq!(restored.outputs.len(), 1); assert!(restored.outputs[0].settings.enabled); let unknown = [monitor("HDMI-A-2", false)]; assert!( restore_configuration(&previous, &unknown).outputs[0] .settings .enabled ); } } #[cfg(test)] mod debounce_tests { use super::*; use std::time::Duration; #[test] fn a_dock_burst_produces_one_reconciliation_after_last_event() { let mut debounce = Debouncer::default(); for time in [0, 40, 110] { debounce.request(Duration::from_millis(time), true); } assert!(!debounce.take_due(Duration::from_millis(150))); assert!(!debounce.take_due(Duration::from_millis(259))); assert!(debounce.take_due(Duration::from_millis(260))); assert!(!debounce.take_due(Duration::from_millis(400))); } #[test] fn inactivity_cancels_deadlines_and_resume_uses_fresh_state() { let mut debounce = Debouncer::default(); debounce.request(Duration::ZERO, true); assert_eq!(debounce.request(Duration::from_millis(30), false), None); assert!(!debounce.take_due(Duration::from_secs(10))); debounce.request(Duration::from_secs(10), true); assert!(debounce.take_due(Duration::from_millis(10150))); } }