use std::io; use std::collections::HashSet; use std::process::Command; use std::num::ParseIntError; use std::str::FromStr; use std::string::FromUtf8Error; /// Converts a resolution to a string. pub fn resolution_to_string(res: (u32, u32)) -> String { format!("{}x{}", res.0, res.1).to_owned() } /// Sets the sound to the HDMI output. pub fn enable_hdmi_sound() -> Result<(), Error>{ Command::new("pactl") .args(&[ "set-card-profile", "0", "output:hdmi-stereo", ]) .output()?; Ok(()) } /// Disables the HDMI sound output. pub fn disable_hdmi_sound() -> Result<(), Error>{ Command::new("pactl") .args(&[ "set-card-profile", "0", "output:analog-stereo+input:analog-stereo", ]) .output()?; Ok(()) } /// The different kinds of errors that can happen. #[derive(Debug)] pub enum Error { /// An io::Error happened while running the xrandr command. IoError(io::Error), /// An error happened while parsing the xrandr output to a string Utf8Error(FromUtf8Error), /// An error happened while parsing the resolution of a xrandr screen. ParseIntError(ParseIntError), /// Not enough screens detected. NoScreenDetected, } #[derive(Debug, Copy, Clone)] /// The relative location of the second screen compared to the first one. pub enum Side { /// On the right of the main screen. Right, /// On the left of the main screen. Left, /// Above the main screen. Above, /// Below the main screen. Below, } impl Side { /// Returns the corresponding xrandr argument. pub fn to_argument(&self) -> String { match *self { Side::Right => "--right-of", Side::Left => "--left-of", Side::Above => "--above", Side::Below => "--below", }.to_owned() } } impl FromStr for Side { type Err = (); fn from_str(s: &str) -> Result { match s { "left" => Ok(Side::Left), "right" => Ok(Side::Right), "above" => Ok(Side::Above), "below" => Ok(Side::Below), _ => Err(()), } } } impl From for Error { fn from(e: io::Error) -> Error { Error::IoError(e) } } impl From for Error { fn from(e: FromUtf8Error) -> Error { Error::Utf8Error(e) } } impl From for Error { fn from(e: ParseIntError) -> Error { Error::ParseIntError(e) } } /// A struct that represents a screen and its supported resolutions. #[derive(Debug)] pub struct Screen { /// The name of the screen received from xrandr. pub name: String, /// The available resolutions of the screen. pub resolutions: Vec<(u32, u32)>, /// Whether the screen is primary or not pub primary: bool, /// Whether the screen is connected or not. pub connected: bool, /// Whether the screen is active or not. pub active: bool, } impl Screen { /// Creates a screen from the first line in the xrandr command. pub fn from_line(line: &str) -> Screen { let split = line.split_whitespace().collect::>(); Screen { name: split[0].to_string(), resolutions: vec![], primary: line.contains("primary"), connected: line.contains(" connected"), active: false, } } /// Finds the best common resolution between two screens. fn best_common_resolution(&self, other: &Screen) -> Option<(u32, u32)> { let self_res: HashSet<(u32, u32)> = self.resolutions.clone().into_iter().collect(); let other_res: HashSet<(u32, u32)> = other.resolutions.clone().into_iter().collect(); self_res.intersection(&other_res).into_iter().max().cloned() } } /// A struct that contains all the screens and allows to manage them. #[derive(Debug)] pub struct MultiScreen { /// All the contained screens. screens: Vec, } impl MultiScreen { /// Finds all the screens by spawning and parsing the output of xrandr. pub fn detect() -> Result { let mut screens = vec![]; let xrandr = Command::new("xrandr") .output()?; let output = String::from_utf8(xrandr.stdout)?; let output = output.split('\n').skip(1); for line in output { if line.is_empty() { continue; } // This will match whether its connected or disconnected // It tells us it is a new screen if line.contains("connected") { screens.push(Screen::from_line(line)); } else if let Some(last) = screens.last_mut() { // Get the first thing let split = line.split_whitespace().collect::>(); // Remove the _ with the frequency let resolution = split[0] .split("_").collect::>()[0] .split("x").collect::>(); if split[1].contains("*") { last.active = true; } if let Ok(width) = resolution[0].parse::() { if let Ok(height) = resolution[1].parse::() { last.resolutions.push((width, height)); } else { eprintln!("Warning: couldn't parse height: {}, skipping...", resolution[1]); } } else { eprintln!("Warning: couldn't parse width: {}, skipping...", resolution[0]); } } } for screen in &mut screens { screen.resolutions.sort(); } Ok(MultiScreen { screens: screens }) } /// Returns a pair of screen corresponding to the two main screens. pub fn main_screens(&self) -> Option<(&Screen, &Screen)> { if let Some(main) = self.screens.iter().filter(|x| x.primary).next() { if let Some(second) = self.screens.iter().filter(|x| !x.primary && x.connected).next() { Some((main, second)) } else { None } } else { None } } /// Returns the arguments of xrandr that disable all screens except the primary. pub fn only_primary_args(&self) -> Vec { let mut primary_found = false; let mut args = vec![]; for screen in &self.screens { if ! screen.primary || primary_found { args.push("--output".to_owned()); args.push(screen.name.clone()); args.push("--off".to_owned()); } else { args.push("--output".to_owned()); args.push(screen.name.clone()); args.push("--auto".to_owned()); primary_found = true; } } args } /// Returns the arguments to duplicate the screens. pub fn duplicate_args(&self) -> Option> { if let Some((ref main, ref second)) = self.main_screens() { if let Some(best_resolution) = main.best_common_resolution(second) { Some(vec![ "--output".to_owned(), main.name.clone(), "--mode".to_owned(), resolution_to_string(best_resolution), "--output".to_owned(), second.name.clone(), "--mode".to_owned(), resolution_to_string(best_resolution), "--same-as".to_owned(), main.name.clone(), ]) } else { None } } else { None } } /// Returns the arguments for two screens. pub fn two_screen_args(&self, side: Side) -> Option> { if let Some((ref main, ref second)) = self.main_screens() { Some(vec![ "--output".to_owned(), main.name.clone(), "--auto".to_owned(), "--output".to_owned(), second.name.clone(), "--auto".to_owned(), side.to_argument(), main.name.clone(), ]) } else { None } } /// Returns the arguments for enabling only the second screen. pub fn only_secondary_args(&self) -> Option> { let (main, second) = match self.main_screens() { Some(x) => x, None => return None, }; let (width, height) = match second.resolutions.iter().max() { Some(x) => x, None => return None, }; Some(vec![ "--output".to_owned(), main.name.clone(), "--off".to_owned(), "--output".to_owned(), second.name.clone(), "--mode".to_owned(), format!("{}x{}", width, height), ]) } /// Calls the xrandr program to disable all screens except the primary. pub fn only_primary(&self) -> Result<(), Error> { self.run_command(self.only_primary_args()) } /// Calls the xrandr program to duplicate the screens. pub fn duplicate(&self) -> Result<(), Error> { if let Some(args) = self.duplicate_args() { self.run_command(args) } else { Err(Error::NoScreenDetected) } } /// Calls the xrandr program to set two screens. pub fn two_screens(&self, side: Side) -> Result<(), Error> { if let Some(args) = self.two_screen_args(side) { self.run_command(args) } else { Err(Error::NoScreenDetected) } } /// Calls the xrandr program to enable only the second screen. pub fn only_secondary(&self) -> Result<(), Error> { if let Some(args) = self.only_secondary_args() { self.run_command(args) } else { Err(Error::NoScreenDetected) } } /// Run an xrandr command with its args. pub fn run_command(&self, args: Vec) -> Result<(), Error> { Command::new("xrandr") .args(args) .spawn()? .wait()?; Ok(()) } /// If a HDMI screen is detected, puts the sound on it. Otherwise, disables it. pub fn enable_hdmi_sound_if_present(&self) -> Result<(), Error> { let hdmi = self.screens .iter() .fold(false, |acc, x| acc || x.name.to_lowercase().contains("hdmi") && x.active); Ok(if hdmi { enable_hdmi_sound()? } else { disable_hdmi_sound()? }) } }