358 lines
9.7 KiB
Rust
358 lines
9.7 KiB
Rust
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<Side, Self::Err> {
|
|
match s {
|
|
"left" => Ok(Side::Left),
|
|
"right" => Ok(Side::Right),
|
|
"above" => Ok(Side::Above),
|
|
"below" => Ok(Side::Below),
|
|
_ => Err(()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<io::Error> for Error {
|
|
fn from(e: io::Error) -> Error {
|
|
Error::IoError(e)
|
|
}
|
|
}
|
|
|
|
impl From<FromUtf8Error> for Error {
|
|
fn from(e: FromUtf8Error) -> Error {
|
|
Error::Utf8Error(e)
|
|
}
|
|
}
|
|
|
|
impl From<ParseIntError> 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,
|
|
}
|
|
|
|
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::<Vec<_>>();
|
|
Screen {
|
|
name: split[0].to_string(),
|
|
resolutions: vec![],
|
|
primary: line.contains("primary"),
|
|
connected: line.contains(" connected"),
|
|
}
|
|
}
|
|
|
|
/// 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();
|
|
let intersection = self_res.intersection(&other_res);
|
|
|
|
let mut output = None;
|
|
|
|
for element in intersection {
|
|
match output {
|
|
None => output = Some(*element),
|
|
Some(res) if &res < element => output = Some(*element),
|
|
_ => (),
|
|
}
|
|
}
|
|
|
|
output
|
|
}
|
|
}
|
|
|
|
/// A struct that contains all the screens and allows to manage them.
|
|
#[derive(Debug)]
|
|
pub struct MultiScreen {
|
|
/// All the contained screens.
|
|
screens: Vec<Screen>,
|
|
}
|
|
|
|
impl MultiScreen {
|
|
|
|
/// Finds all the screens by spawning and parsing the output of xrandr.
|
|
pub fn detect() -> Result<MultiScreen, Error> {
|
|
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(mut last) = screens.last_mut() {
|
|
|
|
// Get the first thing
|
|
let split = line.split_whitespace().collect::<Vec<_>>();
|
|
|
|
// Remove the _ with the frequency
|
|
let resolution = split[0]
|
|
.split("_").collect::<Vec<_>>()[0]
|
|
.split("x").collect::<Vec<_>>();
|
|
|
|
if let Ok(width) = resolution[0].parse::<u32>() {
|
|
if let Ok(height) = resolution[1].parse::<u32>() {
|
|
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<String> {
|
|
|
|
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<Vec<String>> {
|
|
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<Vec<String>> {
|
|
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
|
|
}
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
}
|
|
|
|
/// Run an xrandr command with its args.
|
|
pub fn run_command(&self, args: Vec<String>) -> 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"));
|
|
|
|
Ok(if hdmi {
|
|
enable_hdmi_sound()?
|
|
} else {
|
|
disable_hdmi_sound()?
|
|
})
|
|
}
|
|
|
|
}
|