use std::{
collections::BTreeSet,
ops::{Deref, DerefMut},
};
use color_eyre::Result;
use log::{info, trace};
use swayipc::{Connection, Mode};
use crate::cfg::{Cfgs, Config, DesiredOutput};
#[derive(Debug, Clone, Default)]
pub struct Output {
name: String,
model: String,
position: (i32, i32),
resolution: (u32, u32),
scale: f64,
enabled: bool,
modes: Vec<Mode>,
}
impl Output {
fn new(
name: String,
model: String,
position: (i32, i32),
resolution: (u32, u32),
scale: f64,
enabled: bool,
modes: Vec<Mode>,
) -> Self {
Self {
name,
model,
position,
resolution,
scale,
enabled,
modes,
}
}
pub fn name(&self) -> &str {
self.name.as_ref()
}
pub fn model(&self) -> &str {
self.model.as_ref()
}
pub fn enable(self) -> Self {
Self {
enabled: true,
..self
}
}
pub fn with_scale(self, scale: f64) -> Self {
Self { scale, ..self }
}
pub fn disable(self) -> Self {
Self {
enabled: false,
..self
}
}
fn display(&self, verbose: bool, name_pad: usize) -> String {
let pad = name_pad.saturating_sub(self.name.len()) + 1;
let modes = self.modes.iter().fold(String::new(), |mut acc, m| {
let refresh = m.refresh as f32 / 1000.0;
acc = acc + ", " + &format!("{}x{} ({} Hz)", m.width, m.height, refresh);
acc
});
let details = if verbose {
", modes: ".to_string() + &modes
} else {
"".to_string()
};
let resolution = format!("{}x{}", self.resolution.0, self.resolution.1);
format!(
"{}:{:0pad$}position: {:4}/{}, resolution: {:>9}, scale: {:1.1}, model: {}{}",
self.name,
" ",
self.position.0,
self.position.1,
resolution,
self.scale,
self.model.as_str(),
details
)
}
pub fn best_mode(&'_ self) -> Option<&'_ Mode> {
self.modes
.iter()
.max_by_key(|mode| mode.width * mode.height)
}
pub fn enabled(&self) -> bool {
self.enabled
}
pub fn scale(&self) -> f64 {
self.scale
}
}
impl std::fmt::Display for Output {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let fmt = self.display(false, 0);
write!(f, "{}", fmt)
}
}
impl PartialOrd for Output {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Output {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.model.cmp(&other.model)
}
}
impl PartialEq for Output {
fn eq(&self, other: &Self) -> bool {
self.model == other.model
}
}
impl Eq for Output {}
#[derive(Debug, PartialEq)]
pub struct Outputs(BTreeSet<Output>);
impl Outputs {
pub fn list() -> Result<Self> {
let raw_outputs = Connection::new()?.get_outputs()?;
let outputs = raw_outputs
.iter()
.map(|o| {
let resolution = o
.current_mode
.map(|m| (m.width as u32, m.height as u32))
.unwrap_or((0, 0));
let model = o.make.clone() + " " + &o.model;
Output::new(
o.name.clone(),
model,
(o.rect.x, o.rect.y),
resolution,
o.scale.unwrap_or(1.0),
o.active,
o.modes.clone(),
)
})
.collect();
let outputs = Self(outputs);
Ok(outputs)
}
fn longest_name(&self) -> usize {
self.0
.iter()
.fold(0, |len, output| len.max(output.name.len()))
}
pub fn set_models(&self, setup: &[DesiredOutput]) -> Result<()> {
let disable: Vec<Output> = self
.0
.iter()
.filter_map(|o| {
if !setup.iter().any(|d| d.name == o.model) {
Some(o.clone().disable())
} else {
None
}
})
.collect();
let new_setup: Result<Vec<Output>> = setup
.iter()
.map(|desired| {
self.0
.iter()
.find(|o| o.model == desired.name)
.ok_or(color_eyre::eyre::eyre!(
"Display '{}' is not connected",
desired.name
))
.map(|o| o.clone().enable().with_scale(desired.scale.unwrap_or(1.0)))
})
.collect();
let new_setup = new_setup?;
self.set(new_setup.iter())?;
self.set(disable.iter())
}
pub fn set_by_name(&self, setup: &[String]) -> Result<()> {
let outputs: Vec<_> = self
.0
.iter()
.map(|o| {
if setup.iter().any(|desired| **desired == o.name) {
o.clone().enable()
} else {
o.clone().disable()
}
})
.collect();
self.set(outputs.iter())
}
fn set<'a>(&self, new_setup: impl Iterator<Item = &'a Output>) -> Result<()> {
let mut cmd_con = swayipc::Connection::new()?;
let mut last_x = 0;
for o in new_setup {
let payload = if o.enabled {
let desired_mode = o.best_mode();
let (width, height) = desired_mode.map(|m| (m.width, m.height)).unwrap_or((0, 0));
let payload = format!(
"output {} enable position {} 0 resolution {}x{} scale {}",
o.name(),
last_x,
width,
height,
o.scale
);
let scaled_w = width as f64 / o.scale;
last_x += scaled_w as i32;
payload
} else {
format!("output {} disable", o.name())
};
cmd_con.run_command(payload)?;
}
Ok(())
}
pub fn activate_config(&self, cfgs: &Cfgs) -> Result<()> {
let connected_names: BTreeSet<String> =
self.iter().map(|o| o.model().to_string()).collect();
trace!("connected displays: {:?}", connected_names);
let mut valid_cfgs: Vec<(&String, &Config)> = Vec::new();
for (k, v) in cfgs.iter() {
let names: BTreeSet<_> = v.outputs.iter().map(|d| d.name.clone()).collect();
if names.is_subset(&connected_names) {
valid_cfgs.push((k, v));
}
}
valid_cfgs.sort_by(|a, b| {
let pa = a.1.priority.unwrap_or(0);
let pb = b.1.priority.unwrap_or(0);
pa.cmp(&pb).then(a.1.outputs.len().cmp(&b.1.outputs.len()))
});
trace!("relevant cfgs: {:?}", valid_cfgs);
if let Some((name, best_cfg)) = valid_cfgs.last() {
info!(
"activating config '{}' (priority: {})",
name,
best_cfg.priority.unwrap_or(0)
);
self.set_models(&best_cfg.outputs)?;
}
Ok(())
}
}
impl std::fmt::Display for Outputs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let verbose = f.alternate();
let name_pad = self.longest_name();
self.0.iter().try_fold((), |_, output| {
writeln!(f, "{}", output.display(verbose, name_pad))
})
}
}
impl Deref for Outputs {
type Target = BTreeSet<Output>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Outputs {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> FromIterator<&'a Output> for Outputs {
fn from_iter<T: IntoIterator<Item = &'a Output>>(iter: T) -> Self {
let mut vec: BTreeSet<Output> = BTreeSet::new();
for n in iter {
vec.insert(n.clone());
}
Self(vec)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn padding() {
let output = Output::new(
"1234".to_owned(),
"model".to_owned(),
(0, 0),
(0, 0),
1.0,
true,
Vec::new(),
);
let display = output.display(false, 8);
assert_eq!(&display[..10], "1234: ");
}
}