tvrs/src/lib.rs

387 lines
11 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,
/// 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::<Vec<_>>();
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<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(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 split[1].contains("*") {
last.active = true;
}
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
}
}
/// Returns the arguments for enabling only the second screen.
pub fn only_secondary_args(&self) -> Option<Vec<String>> {
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<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") && x.active);
Ok(if hdmi {
enable_hdmi_sound()?
} else {
disable_hdmi_sound()?
})
}
}