fixscreen/src/lib.rs
2023-06-23 17:10:51 -07:00

309 lines
9.0 KiB
Rust

use std::{
collections::HashMap,
fmt,
fs::File,
io::{self, BufRead, BufReader},
path::Path,
process::{Command, Output},
};
use regex::Regex;
use thiserror::Error;
use xrandr::{XHandle, XrandrError};
pub fn swap_workspaces(cfg: &Config) -> Result<Vec<Result<Output, CommandError>>, CommandError> {
let monitors = XHandle::open()?.monitors()?;
// TODO(wathiede): if I ever get two monitors with the same resolution, use EDID to find a
// better key.
let map: HashMap<_, _> = monitors
.iter()
.map(|m| ((m.width_px as usize, m.height_px as usize), m.name.clone()))
.collect();
// TODO(wathiede):
// i3-msg commands to move them.
// Example:
// i3-msg '[workspace="trustee"]' move workspace to output DP-0
Ok(cfg
.screens
.iter()
.map(|s| {
let key = match s.orientation {
Orientation::None | Orientation::Invert => {
(s.resolution.width, s.resolution.height)
}
Orientation::Left | Orientation::Right => (s.resolution.height, s.resolution.width),
};
if let Some(m) = map.get(&key) {
println!("Moving {:?} to {}", s.workspaces, m);
run_move_workspace_cmd(&s.workspaces, m)
} else {
Vec::new()
}
})
.flatten()
.collect())
}
fn run_move_workspace_cmd(
workspaces: &[String],
monitor: &str,
) -> Vec<Result<Output, CommandError>> {
workspaces
.iter()
.map(|workspace| {
let s = format!(r#"[workspace="{workspace}"]"#);
let args = vec!["i3-msg", &s, "move", "workspace", "to", "output", monitor];
println!("{}", args.join(" "));
if cfg!(debug_assertions) {
Command::new("echo").args(args).output()
} else {
Command::new(&args[0]).args(&args[1..]).output()
}
})
.map(|r| r.map_err(CommandError::from))
.collect()
}
#[derive(Error, Debug)]
pub enum ParseError {
#[error("error reading file")]
Io(#[from] io::Error),
#[error("invalid data: {0}")]
Parse(String),
#[error("xrandr: {0}")]
XrandrError(#[from] XrandrError),
}
// Map monitor name to DFP connection.
pub type ScreenMapping = HashMap<String, String>;
pub fn screen_mapping_from_xorg_log<P: AsRef<Path>>(path: P) -> Result<ScreenMapping, ParseError> {
let f = File::open(path)?;
let f = BufReader::new(f);
let re = Regex::new(r".*: (.*)\s+\(([^)]+)\): connected").unwrap();
Ok(f.lines()
.filter_map(|line| line.ok())
.filter_map(|line| {
if let Some(cap) = re.captures(&line) {
Some((cap[1].to_string(), cap[2].to_string()))
} else {
None
}
})
.collect())
}
#[derive(Error, Debug)]
pub enum CommandError {
#[error("missing monitor: {0}")]
MissingMonitor(String),
#[error("error executing command")]
Io(#[from] io::Error),
#[error("xrandr error: {0}")]
XrandrError(#[from] XrandrError),
}
#[derive(Clone, Debug, Default)]
pub struct Resolution {
pub width: usize,
pub height: usize,
}
#[derive(Clone, Debug, Default)]
pub struct Offset {
pub x: isize,
pub y: isize,
}
#[derive(Clone)]
pub enum Orientation {
None,
Right,
Left,
Invert,
}
impl Default for Orientation {
fn default() -> Self {
Orientation::None
}
}
impl fmt::Debug for Orientation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Orientation::None => f.write_str("No Rotation"),
Orientation::Right => f.write_str("Rotate Right"),
Orientation::Left => f.write_str("Rotate Left"),
Orientation::Invert => f.write_str("Invert"),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Screen {
pub name: String,
pub resolution: Resolution,
pub offset: Offset,
pub orientation: Orientation,
pub primary: bool,
pub workspaces: Vec<String>,
}
impl Screen {
fn metamode(&self, map: &ScreenMapping) -> Result<String, CommandError> {
let Resolution { width, height } = self.resolution;
let Offset { x, y } = self.offset;
let (_in_w, _in_h) = match self.orientation {
Orientation::None | Orientation::Invert => (width, height),
Orientation::Right | Orientation::Left => (height, width),
};
let rotation = match self.orientation {
Orientation::None => "0",
Orientation::Invert => "180",
Orientation::Right => "270",
Orientation::Left => "90",
};
let connection = match map.get(&self.name) {
Some(connection) => connection,
None => return Err(CommandError::MissingMonitor(self.name.to_string())),
};
Ok(format!(
"{connection}: {w}x{h} {x:+}{y:+} {{ForceCompositionPipeline=On, Rotation={rotation}}}",
connection = connection,
w = width,
h = height,
x = x,
y = y,
rotation = rotation,
))
/*
Ok(format!("{connection}: {w}x{h} @{in_w}x{in_h} {x:+}{y:+} {{ForceCompositionPipeline=On, ViewPortIn={in_w}x{in_h}, ViewPortOut={w}x{h}+0+0, Rotation={rotation}}}",
connection=connection,
w=width,
h=height,
in_w=in_w,
in_h=in_h,
x=x,
y=y,
rotation=rotation,
))
*/
}
}
#[derive(Debug, Default)]
pub struct Config {
pub screens: Vec<Screen>,
}
pub fn run_cmd(screen_mapping: &ScreenMapping, cfg: &Config) -> Result<Output, CommandError> {
let args = build_cmd_args(screen_mapping, cfg)?;
if cfg!(debug_assertions) {
Ok(Command::new("echo").args(args).output()?)
} else {
Ok(Command::new(&args[0]).args(&args[1..]).output()?)
}
}
fn build_cmd_args(
screen_mapping: &ScreenMapping,
cfg: &Config,
) -> Result<Vec<String>, CommandError> {
let metamode = cfg
.screens
.iter()
.map(|c| c.metamode(screen_mapping))
.collect::<Result<Vec<String>, CommandError>>()?
.join(", ");
Ok(vec![
"nvidia-settings",
"--assign",
&format!("CurrentMetaMode={}", metamode),
]
.iter()
.map(|s| s.to_string())
.collect())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_cmd() {
let cfg = Config {
screens: vec![
Screen {
name: "DELL U2415".to_string(),
resolution: Resolution {
width: 1920,
height: 1200,
},
offset: Offset { x: 0, y: 0 },
orientation: Orientation::Right,
..Default::default()
},
Screen {
name: "LG Electronics 34UM95".to_string(),
resolution: Resolution {
width: 3440,
height: 1440,
},
offset: Offset { x: 1200, y: 0 },
..Default::default()
},
Screen {
name: "Lenovo Group Limited P27h-20".to_string(),
resolution: Resolution {
width: 2560,
height: 1440,
},
offset: Offset { x: 4640, y: 0 },
..Default::default()
},
],
};
let map = vec![
("Lenovo Group Limited P27h-20", "DFP-0.8"),
("LG Electronics 34UM95", "DFP-0.1"),
("DELL U2415", "DFP-5.8"),
]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
assert_eq!(
build_cmd_args(&map, cfg).expect("failed build_cmd_args"),
vec![
"nvidia-settings",
"--assign",
"CurrentMetaMode=\
DFP-5.8: 1920x1200 @1200x1920 +0+0 {ForceCompositionPipeline=On, ViewPortIn=1200x1920, ViewPortOut=1920x1200+0+0, Rotation=270}, \
DFP-0.1: 3440x1440 @3440x1440 +1200+0 {ForceCompositionPipeline=On, ViewPortIn=3440x1440, ViewPortOut=3440x1440+0+0, Rotation=0}, \
DFP-0.8: 2560x1440 @2560x1440 +4640+0 {ForceCompositionPipeline=On, ViewPortIn=2560x1440, ViewPortOut=2560x1440+0+0, Rotation=0}"
]
);
}
#[test]
fn parse() {
let testdir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let map = screen_mapping_from_xorg_log(&testdir.join("testdata/Xorg.0.log")).unwrap();
assert_eq!(
map,
vec![
("Lenovo Group Limited P27h-20", "DFP-0.8"),
("LG Electronics 34UM95", "DFP-0.1"),
("DELL U2415", "DFP-5.8"),
]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
);
}
}