oswo 2.1.0

Organize your sway outputs
Documentation
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 {
    /// Creates a new [`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 {
        // we want at least one whitespace, hence + 1
        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())
            };
            // println!("{}", payload);
            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);
        // collect configs where all required outputs are connected
        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));
            }
        }

        // Sort ascending so last() is the best: priority (higher wins), then number of outputs (bigger wins)
        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:     ");
    }
}