Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 3 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions cosmic-comp-config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
14 changes: 8 additions & 6 deletions cosmic-comp-config/src/output/comp.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<String>,
pub make: String,
pub model: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub edid: Option<EdidProduct>,
}

pub fn load_outputs(path: Option<impl AsRef<Path>>) -> OutputsConfig {
Expand All @@ -87,11 +91,9 @@ pub fn load_outputs(path: Option<impl AsRef<Path>>) -> 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"
Expand Down
216 changes: 153 additions & 63 deletions cosmic-comp-config/src/output/randr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<EdidProduct>,
}

pub struct CompList {
infos: Vec<super::comp::OutputInfo>,
outputs: Vec<super::comp::OutputConfig>,
}

/// 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::ModeKey, cosmic_randr_shell::Mode>,
) -> 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, &current.edid) {
return saved_edid == current_edid;
}

// Fall back to make/model matching
info.make == current.make && info.model == current.model
}

impl From<CompList> 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
}
}
Expand All @@ -92,3 +121,64 @@ pub fn load_outputs(path: Option<impl AsRef<Path>>) -> Vec<List> {
})
.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<impl AsRef<Path>>,
current_outputs: &[CurrentOutput],
) -> Option<List> {
let output_config = crate::output::comp::load_outputs(path);

// Find the best matching saved config
let mut best_match: Option<(
&Vec<super::comp::OutputInfo>,
&Vec<super::comp::OutputConfig>,
)> = 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)
}
Loading