diff --git a/Cargo.lock b/Cargo.lock index 66cffb298..b301b9a82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -924,6 +924,7 @@ dependencies = [ "libdisplay-info", "ron 0.12.0", "serde", + "slotmap", "tracing", ] @@ -4787,7 +4788,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -5758,7 +5759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5f7c95348f20c1c913d72157b3c6dee6ea3e30b3d19502c5a7f6d3f160dacbf" dependencies = [ "cc", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -6630,15 +6631,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" diff --git a/cosmic-comp-config/Cargo.toml b/cosmic-comp-config/Cargo.toml index 66cdbfff7..0af1fb708 100644 --- a/cosmic-comp-config/Cargo.toml +++ b/cosmic-comp-config/Cargo.toml @@ -9,13 +9,14 @@ cosmic-randr-shell = { git = "https://github.com/pop-os/cosmic-randr/", optional input = "0.9.1" libdisplay-info = { version = "0.3.0", optional = true } serde = { version = "1", features = ["derive"] } +slotmap = { version = "1.0.7", optional = true } ron = { version = "0.12", optional = true } tracing = { version = "0.1.44", features = [ - "max_level_debug", - "release_max_level_info", + "max_level_debug", + "release_max_level_info", ], optional = true } [features] default = [] output = ["ron", "tracing"] -randr = ["cosmic-randr-shell", "output"] +randr = ["cosmic-randr-shell", "slotmap", "output"] diff --git a/cosmic-comp-config/src/output/comp.rs b/cosmic-comp-config/src/output/comp.rs index 1810b9d43..75cc48ae5 100644 --- a/cosmic-comp-config/src/output/comp.rs +++ b/cosmic-comp-config/src/output/comp.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only +use crate::EdidProduct; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fs::OpenOptions, path::Path}; use tracing::{error, warn}; @@ -70,9 +71,12 @@ impl Default for OutputConfig { #[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct OutputInfo { - pub connector: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub connector: Option, pub make: String, pub model: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub edid: Option, } pub fn load_outputs(path: Option>) -> OutputsConfig { @@ -87,11 +91,9 @@ pub fn load_outputs(path: Option>) -> OutputsConfig { let config_clone = config.clone(); for conf in config.iter_mut() { if let OutputState::Mirroring(conn) = &conf.enabled { - if let Some((j, _)) = info - .iter() - .enumerate() - .find(|(_, info)| &info.connector == conn) - { + if let Some((j, _)) = info.iter().enumerate().find(|(_, info)| { + info.connector.as_deref() == Some(conn.as_str()) + }) { if config_clone[j].enabled != OutputState::Enabled { warn!( "Invalid Mirroring tag, overriding with `Enabled` instead" diff --git a/cosmic-comp-config/src/output/randr.rs b/cosmic-comp-config/src/output/randr.rs index dd6cb6c60..1cf5d752b 100644 --- a/cosmic-comp-config/src/output/randr.rs +++ b/cosmic-comp-config/src/output/randr.rs @@ -2,81 +2,110 @@ use std::path::Path; use cosmic_randr_shell::{AdaptiveSyncState, List}; +use crate::EdidProduct; use crate::output::comp::OutputState; +/// Represents a currently connected output from the Wayland compositor +#[derive(Debug, Clone)] +pub struct CurrentOutput { + /// Connector name (e.g., "DP-5", "eDP-1") + pub connector: String, + pub make: String, + pub model: String, + /// EDID product info for precise matching + pub edid: Option, +} + pub struct CompList { infos: Vec, outputs: Vec, } +/// Convert a saved OutputConfig to a randr Output, using the provided connector name +fn config_to_randr_output( + info: &super::comp::OutputInfo, + output: &super::comp::OutputConfig, + connector: String, + modes: &mut slotmap::SlotMap, +) -> cosmic_randr_shell::Output { + let current = modes.insert(cosmic_randr_shell::Mode { + size: (output.mode.0.0 as u32, output.mode.0.1 as u32), + refresh_rate: output.mode.1.unwrap_or_default(), + preferred: false, + }); + + cosmic_randr_shell::Output { + serial_number: info + .edid + .as_ref() + .and_then(|edid| edid.serial.map(|s| s.to_string())) + .unwrap_or_default(), + name: connector, + enabled: !matches!(output.enabled, OutputState::Disabled), + mirroring: match &output.enabled { + OutputState::Mirroring(m) => Some(m.clone()), + _ => None, + }, + make: Some(info.make.clone()).filter(|make| make != "Unknown"), + model: if info.model.as_str() == "Unknown" { + String::new() + } else { + info.model.clone() + }, + position: (output.position.0 as i32, output.position.1 as i32), + scale: output.scale, + transform: Some(match output.transform { + crate::output::comp::TransformDef::Normal => cosmic_randr_shell::Transform::Normal, + crate::output::comp::TransformDef::_90 => cosmic_randr_shell::Transform::Rotate90, + crate::output::comp::TransformDef::_180 => cosmic_randr_shell::Transform::Rotate180, + crate::output::comp::TransformDef::_270 => cosmic_randr_shell::Transform::Rotate270, + crate::output::comp::TransformDef::Flipped => cosmic_randr_shell::Transform::Flipped, + crate::output::comp::TransformDef::Flipped90 => { + cosmic_randr_shell::Transform::Flipped90 + } + crate::output::comp::TransformDef::Flipped180 => { + cosmic_randr_shell::Transform::Flipped180 + } + crate::output::comp::TransformDef::Flipped270 => { + cosmic_randr_shell::Transform::Flipped270 + } + }), + modes: vec![current], + current: Some(current), + adaptive_sync: Some(match output.vrr { + crate::output::comp::AdaptiveSync::Enabled => AdaptiveSyncState::Auto, + crate::output::comp::AdaptiveSync::Disabled => AdaptiveSyncState::Disabled, + crate::output::comp::AdaptiveSync::Force => AdaptiveSyncState::Always, + }), + xwayland_primary: Some(output.xwayland_primary), + physical: (0, 0), + adaptive_sync_availability: None, + } +} + +/// Check if a saved OutputInfo matches a current output by EDID or make/model +fn info_matches_output(info: &super::comp::OutputInfo, current: &CurrentOutput) -> bool { + // First try to match by EDID (most precise) + if let (Some(saved_edid), Some(current_edid)) = (&info.edid, ¤t.edid) { + return saved_edid == current_edid; + } + + // Fall back to make/model matching + info.make == current.make && info.model == current.model +} + impl From for cosmic_randr_shell::List { fn from(CompList { infos, outputs }: CompList) -> cosmic_randr_shell::List { let mut list = cosmic_randr_shell::List::default(); for (info, output) in infos.into_iter().zip(outputs.into_iter()) { - let current = list.modes.insert(cosmic_randr_shell::Mode { - size: (output.mode.0.0 as u32, output.mode.0.1 as u32), - refresh_rate: output.mode.1.unwrap_or_default(), - // XXX not in config as far as i can tell - preferred: false, - }); - let modes = vec![current]; - - // for mode in output. {} - list.outputs.insert(cosmic_randr_shell::Output { - name: info.connector, - enabled: !matches!(output.enabled, OutputState::Disabled), - mirroring: match output.enabled { - OutputState::Mirroring(m) => Some(m), - _ => None, - }, - make: Some(info.make).filter(|make| make != "Unknown"), - model: if info.model.as_str() == "Unknown" { - String::new() - } else { - info.model - }, - position: (output.position.0 as i32, output.position.1 as i32), - scale: output.scale, - transform: Some(match output.transform { - crate::output::comp::TransformDef::Normal => { - cosmic_randr_shell::Transform::Normal - } - crate::output::comp::TransformDef::_90 => { - cosmic_randr_shell::Transform::Rotate90 - } - crate::output::comp::TransformDef::_180 => { - cosmic_randr_shell::Transform::Rotate180 - } - crate::output::comp::TransformDef::_270 => { - cosmic_randr_shell::Transform::Rotate270 - } - crate::output::comp::TransformDef::Flipped => { - cosmic_randr_shell::Transform::Flipped - } - crate::output::comp::TransformDef::Flipped90 => { - cosmic_randr_shell::Transform::Flipped90 - } - crate::output::comp::TransformDef::Flipped180 => { - cosmic_randr_shell::Transform::Flipped180 - } - crate::output::comp::TransformDef::Flipped270 => { - cosmic_randr_shell::Transform::Flipped270 - } - }), - modes, - current: Some(current), - adaptive_sync: Some(match output.vrr { - crate::output::comp::AdaptiveSync::Enabled => AdaptiveSyncState::Auto, - crate::output::comp::AdaptiveSync::Disabled => AdaptiveSyncState::Disabled, - crate::output::comp::AdaptiveSync::Force => AdaptiveSyncState::Always, - }), - xwayland_primary: Some(output.xwayland_primary), - // XXX no physical output size in the config - physical: (0, 0), - adaptive_sync_availability: None, - }); + // Use connector if available, otherwise use make/model as fallback for display + let connector = info + .connector + .clone() + .unwrap_or_else(|| format!("{} {}", info.make, info.model)); + let randr_output = config_to_randr_output(&info, &output, connector, &mut list.modes); + list.outputs.insert(randr_output); } - list } } @@ -92,3 +121,64 @@ pub fn load_outputs(path: Option>) -> Vec { }) .collect() } + +/// Given currently connected outputs, find the best matching saved config +/// and return it with correct connector names filled in from the current outputs. +/// +/// This is the preferred way to get output config when you have access to +/// live output information (e.g., in cosmic-greeter). +pub fn get_matching_config( + path: Option>, + current_outputs: &[CurrentOutput], +) -> Option { + let output_config = crate::output::comp::load_outputs(path); + + // Find the best matching saved config + let mut best_match: Option<( + &Vec, + &Vec, + )> = None; + + for (saved_infos, saved_configs) in output_config.config.iter() { + // Must have same number of outputs + if saved_infos.len() != current_outputs.len() { + continue; + } + + // Check if all saved infos match a current output + let all_match = saved_infos.iter().all(|saved_info| { + current_outputs + .iter() + .any(|current| info_matches_output(saved_info, current)) + }); + + if all_match { + // Prefer configs with more outputs (more specific match) + if best_match.is_none_or(|(infos, _)| infos.len() < saved_infos.len()) { + best_match = Some((saved_infos, saved_configs)); + } + } + } + + // Convert the matched config to a List with correct connector names + let (saved_infos, saved_configs) = best_match?; + + let mut list = cosmic_randr_shell::List::default(); + + for (saved_info, saved_config) in saved_infos.iter().zip(saved_configs.iter()) { + // Find the matching current output to get the actual connector name + let current = current_outputs + .iter() + .find(|c| info_matches_output(saved_info, c))?; + + let randr_output = config_to_randr_output( + saved_info, + saved_config, + current.connector.clone(), + &mut list.modes, + ); + list.outputs.insert(randr_output); + } + + Some(list) +} diff --git a/src/config/mod.rs b/src/config/mod.rs index ac8043d36..10cf5eff8 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -409,13 +409,10 @@ impl Config { clock: &Clock, ) -> anyhow::Result<()> { let outputs = output_state.outputs().collect::>(); - let mut infos = outputs - .iter() - .cloned() - .map(Into::::into) - .map(|i| i.0) - .collect::>(); - infos.sort(); + let (infos, _): (Vec, Vec) = + prepare_infos(outputs.clone().into_iter()) + .into_iter() + .unzip(); if let Some(configs) = self .dynamic_conf @@ -453,9 +450,27 @@ impl Config { .collect::>(); let mut found_outputs = Vec::new(); - for (name, output_config) in infos.iter().map(|o| &o.connector).zip(configs.into_iter()) - { - let output = outputs.iter().find(|o| &o.name() == name).unwrap().clone(); + for (info, output_config) in infos.iter().zip(configs.into_iter()) { + // Match by connector if present, otherwise match by EDID (position in sorted list) + let output = if let Some(connector_name) = &info.connector { + outputs + .iter() + .find(|o| &o.name() == connector_name) + .unwrap() + .clone() + } else { + // When connector is not in saved config, match by EDID/make/model + outputs + .iter() + .find(|o| { + let o_info = Into::::into((*o).clone()).0; + o_info.make == info.make + && o_info.model == info.model + && o_info.edid == info.edid + }) + .unwrap() + .clone() + }; let enabled = output_config.enabled.clone(); *output .user_data() @@ -589,21 +604,9 @@ impl Config { &mut self, outputs: impl Iterator>, ) { - let mut infos = outputs - .map(|o| { - let o = o.borrow(); - ( - Into::::into(o.clone()).0, - o.user_data() - .get::>() - .unwrap() - .borrow() - .clone(), - ) - }) - .collect::>(); - infos.sort_by(|(a, _), (b, _)| a.cmp(b)); - let (infos, configs) = infos.into_iter().unzip(); + let (infos, configs): (Vec, Vec) = + prepare_infos(outputs).into_iter().unzip(); + self.dynamic_conf .outputs_mut() .config @@ -964,9 +967,75 @@ impl From for CompOutputInfo { fn from(o: Output) -> CompOutputInfo { let physical = o.physical_properties(); CompOutputInfo(OutputInfo { - connector: o.name(), + connector: Some(o.name()), make: physical.make, model: physical.model, + edid: o.edid().cloned(), }) } } + +fn prepare_infos( + outputs: impl Iterator>, +) -> Vec<(OutputInfo, OutputConfig)> { + let mut output_infos = outputs + .map(|o| { + let o = o.borrow(); + ( + Into::::into(o.clone()).0, + o.user_data() + .get::>() + .unwrap() + .borrow() + .clone(), + ) + }) + .collect::>(); + + // Check if any monitors have identical EDID info (same make/model/edid). + let has_identical_outputs = { + let mut seen = std::collections::HashSet::new(); + !output_infos.iter().all(|(info, _)| { + let key = (&info.make, &info.model, &info.edid); + seen.insert(key) + }) + }; + + // If all monitors are unique, strip connectors for lookup. + // If there are identical monitors, keep connectors to distinguish them. + if has_identical_outputs { + // since edid doesn't have any useful information, remove it, which makes info + // compatible with the old configs making the fallback simpler + output_infos = output_infos + .iter() + .map(|(info, output)| { + ( + OutputInfo { + connector: info.connector.clone(), + make: info.make.clone(), + model: info.model.clone(), + edid: None, + }, + output.clone(), + ) + }) + .collect(); + } else { + output_infos = output_infos + .iter() + .map(|(info, output)| { + ( + OutputInfo { + connector: None, + make: info.make.clone(), + model: info.model.clone(), + edid: info.edid.clone(), + }, + output.clone(), + ) + }) + .collect(); + } + output_infos.sort_by(|(a, _), (b, _)| a.cmp(b)); + output_infos +}