Skip to main content

liboswo/
outputs.rs

1use std::{
2    collections::BTreeSet,
3    ops::{Deref, DerefMut},
4};
5
6use color_eyre::Result;
7use log::{info, trace};
8use swayipc::{Connection, Mode};
9
10use crate::cfg::{Cfgs, Config, DesiredOutput};
11
12#[derive(Debug, Clone, Default)]
13pub struct Output {
14    name: String,
15    model: String,
16    position: (i32, i32),
17    resolution: (u32, u32),
18    scale: f64,
19    enabled: bool,
20    modes: Vec<Mode>,
21}
22
23impl Output {
24    /// Creates a new [`Output`].
25    fn new(
26        name: String,
27        model: String,
28        position: (i32, i32),
29        resolution: (u32, u32),
30        scale: f64,
31        enabled: bool,
32        modes: Vec<Mode>,
33    ) -> Self {
34        Self {
35            name,
36            model,
37            position,
38            resolution,
39            scale,
40            enabled,
41            modes,
42        }
43    }
44
45    pub fn name(&self) -> &str {
46        self.name.as_ref()
47    }
48
49    pub fn model(&self) -> &str {
50        self.model.as_ref()
51    }
52
53    pub fn enable(self) -> Self {
54        Self {
55            enabled: true,
56            ..self
57        }
58    }
59
60    pub fn with_scale(self, scale: f64) -> Self {
61        Self { scale, ..self }
62    }
63
64    pub fn disable(self) -> Self {
65        Self {
66            enabled: false,
67            ..self
68        }
69    }
70
71    fn display(&self, verbose: bool, name_pad: usize) -> String {
72        // we want at least one whitespace, hence + 1
73        let pad = name_pad.saturating_sub(self.name.len()) + 1;
74        let modes = self.modes.iter().fold(String::new(), |mut acc, m| {
75            let refresh = m.refresh as f32 / 1000.0;
76            acc = acc + ", " + &format!("{}x{} ({} Hz)", m.width, m.height, refresh);
77            acc
78        });
79
80        let details = if verbose {
81            ", modes: ".to_string() + &modes
82        } else {
83            "".to_string()
84        };
85        let resolution = format!("{}x{}", self.resolution.0, self.resolution.1);
86        format!(
87            "{}:{:0pad$}position: {:4}/{}, resolution: {:>9}, scale: {:1.1}, model: {}{}",
88            self.name,
89            " ",
90            self.position.0,
91            self.position.1,
92            resolution,
93            self.scale,
94            self.model.as_str(),
95            details
96        )
97    }
98
99    pub fn best_mode(&'_ self) -> Option<&'_ Mode> {
100        self.modes
101            .iter()
102            .max_by_key(|mode| mode.width * mode.height)
103    }
104
105    pub fn enabled(&self) -> bool {
106        self.enabled
107    }
108
109    pub fn scale(&self) -> f64 {
110        self.scale
111    }
112}
113
114impl std::fmt::Display for Output {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        let fmt = self.display(false, 0);
117        write!(f, "{}", fmt)
118    }
119}
120
121impl PartialOrd for Output {
122    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
123        Some(self.cmp(other))
124    }
125}
126
127impl Ord for Output {
128    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
129        self.model.cmp(&other.model)
130    }
131}
132
133impl PartialEq for Output {
134    fn eq(&self, other: &Self) -> bool {
135        self.model == other.model
136    }
137}
138
139impl Eq for Output {}
140
141#[derive(Debug, PartialEq)]
142pub struct Outputs(BTreeSet<Output>);
143
144impl Outputs {
145    pub fn list() -> Result<Self> {
146        let raw_outputs = Connection::new()?.get_outputs()?;
147
148        let outputs = raw_outputs
149            .iter()
150            .map(|o| {
151                let resolution = o
152                    .current_mode
153                    .map(|m| (m.width as u32, m.height as u32))
154                    .unwrap_or((0, 0));
155                let model = o.make.clone() + " " + &o.model;
156                Output::new(
157                    o.name.clone(),
158                    model,
159                    (o.rect.x, o.rect.y),
160                    resolution,
161                    o.scale.unwrap_or(1.0),
162                    o.active,
163                    o.modes.clone(),
164                )
165            })
166            .collect();
167
168        let outputs = Self(outputs);
169        Ok(outputs)
170    }
171
172    fn longest_name(&self) -> usize {
173        self.0
174            .iter()
175            .fold(0, |len, output| len.max(output.name.len()))
176    }
177
178    pub fn set_models(&self, setup: &[DesiredOutput]) -> Result<()> {
179        let disable: Vec<Output> = self
180            .0
181            .iter()
182            .filter_map(|o| {
183                if !setup.iter().any(|d| d.name == o.model) {
184                    Some(o.clone().disable())
185                } else {
186                    None
187                }
188            })
189            .collect();
190
191        let new_setup: Result<Vec<Output>> = setup
192            .iter()
193            .map(|desired| {
194                self.0
195                    .iter()
196                    .find(|o| o.model == desired.name)
197                    .ok_or(color_eyre::eyre::eyre!(
198                        "Display '{}' is not connected",
199                        desired.name
200                    ))
201                    .map(|o| o.clone().enable().with_scale(desired.scale.unwrap_or(1.0)))
202            })
203            .collect();
204        let new_setup = new_setup?;
205        self.set(new_setup.iter())?;
206        self.set(disable.iter())
207    }
208
209    pub fn set_by_name(&self, setup: &[String]) -> Result<()> {
210        let outputs: Vec<_> = self
211            .0
212            .iter()
213            .map(|o| {
214                if setup.iter().any(|desired| **desired == o.name) {
215                    o.clone().enable()
216                } else {
217                    o.clone().disable()
218                }
219            })
220            .collect();
221        self.set(outputs.iter())
222    }
223
224    fn set<'a>(&self, new_setup: impl Iterator<Item = &'a Output>) -> Result<()> {
225        let mut cmd_con = swayipc::Connection::new()?;
226        let mut last_x = 0;
227        for o in new_setup {
228            let payload = if o.enabled {
229                let desired_mode = o.best_mode();
230                let (width, height) = desired_mode.map(|m| (m.width, m.height)).unwrap_or((0, 0));
231                let payload = format!(
232                    "output {} enable position {} 0 resolution {}x{} scale {}",
233                    o.name(),
234                    last_x,
235                    width,
236                    height,
237                    o.scale
238                );
239                let scaled_w = width as f64 / o.scale;
240                last_x += scaled_w as i32;
241                payload
242            } else {
243                format!("output {} disable", o.name())
244            };
245            // println!("{}", payload);
246            cmd_con.run_command(payload)?;
247        }
248
249        Ok(())
250    }
251
252    pub fn activate_config(&self, cfgs: &Cfgs) -> Result<()> {
253        let connected_names: BTreeSet<String> =
254            self.iter().map(|o| o.model().to_string()).collect();
255        trace!("connected displays: {:?}", connected_names);
256        // collect configs where all required outputs are connected
257        let mut valid_cfgs: Vec<(&String, &Config)> = Vec::new();
258        for (k, v) in cfgs.iter() {
259            let names: BTreeSet<_> = v.outputs.iter().map(|d| d.name.clone()).collect();
260            if names.is_subset(&connected_names) {
261                valid_cfgs.push((k, v));
262            }
263        }
264
265        // Sort ascending so last() is the best: priority (higher wins), then number of outputs (bigger wins)
266        valid_cfgs.sort_by(|a, b| {
267            let pa = a.1.priority.unwrap_or(0);
268            let pb = b.1.priority.unwrap_or(0);
269            pa.cmp(&pb).then(a.1.outputs.len().cmp(&b.1.outputs.len()))
270        });
271
272        trace!("relevant cfgs: {:?}", valid_cfgs);
273        if let Some((name, best_cfg)) = valid_cfgs.last() {
274            info!(
275                "activating config '{}' (priority: {})",
276                name,
277                best_cfg.priority.unwrap_or(0)
278            );
279            self.set_models(&best_cfg.outputs)?;
280        }
281        Ok(())
282    }
283}
284
285impl std::fmt::Display for Outputs {
286    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287        let verbose = f.alternate();
288        let name_pad = self.longest_name();
289        self.0.iter().try_fold((), |_, output| {
290            writeln!(f, "{}", output.display(verbose, name_pad))
291        })
292    }
293}
294
295impl Deref for Outputs {
296    type Target = BTreeSet<Output>;
297
298    fn deref(&self) -> &Self::Target {
299        &self.0
300    }
301}
302
303impl DerefMut for Outputs {
304    fn deref_mut(&mut self) -> &mut Self::Target {
305        &mut self.0
306    }
307}
308
309impl<'a> FromIterator<&'a Output> for Outputs {
310    fn from_iter<T: IntoIterator<Item = &'a Output>>(iter: T) -> Self {
311        let mut vec: BTreeSet<Output> = BTreeSet::new();
312        for n in iter {
313            vec.insert(n.clone());
314        }
315
316        Self(vec)
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn padding() {
326        let output = Output::new(
327            "1234".to_owned(),
328            "model".to_owned(),
329            (0, 0),
330            (0, 0),
331            1.0,
332            true,
333            Vec::new(),
334        );
335        let display = output.display(false, 8);
336        assert_eq!(&display[..10], "1234:     ");
337    }
338}