use anyhow::{Context, Result, bail, ensure};
use x11rb::{
connect,
connection::Connection,
protocol::{
randr::ConnectionExt as RandrConnectionExt, xproto::ConnectionExt as XprotoConnectionExt,
},
rust_connection::RustConnection,
};
use crate::pipeline::CaptureArea;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Monitor {
pub index: usize,
pub name: String,
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
pub primary: bool,
}
impl Monitor {
pub fn area(&self) -> Result<CaptureArea> {
CaptureArea::new(self.x, self.y, self.width, self.height)
}
}
pub fn list_x11_monitors() -> Result<Vec<Monitor>> {
let (conn, screen_num) = connect_x11("X11 monitors cannot be listed")?;
let screen = &conn.setup().roots[screen_num];
let root = screen.root;
let monitors_reply = conn
.randr_get_monitors(root, true)
.ok()
.and_then(|cookie| cookie.reply().ok());
match monitors_reply {
Some(reply) if !reply.monitors.is_empty() => reply
.monitors
.into_iter()
.enumerate()
.map(|(index, monitor)| {
let name = conn
.get_atom_name(monitor.name)
.ok()
.and_then(|cookie| cookie.reply().ok())
.and_then(|reply| String::from_utf8(reply.name).ok())
.filter(|name| !name.is_empty())
.unwrap_or_else(|| format!("Monitor {}", index + 1));
Ok(Monitor {
index: index + 1,
name,
x: i32::from(monitor.x),
y: i32::from(monitor.y),
width: i32::from(monitor.width),
height: i32::from(monitor.height),
primary: monitor.primary,
})
})
.collect(),
_ => Ok(vec![Monitor {
index: 1,
name: "Screen".to_string(),
x: 0,
y: 0,
width: i32::from(screen.width_in_pixels),
height: i32::from(screen.height_in_pixels),
primary: true,
}]),
}
}
pub fn x11_monitor_area(index: usize) -> Result<CaptureArea> {
let monitors = list_x11_monitors()?;
let monitor = monitors
.iter()
.find(|monitor| monitor.index == index)
.with_context(|| format!("monitor {index} was not found; run --list-monitors"))?;
monitor.area()
}
pub fn validate_x11_capture_area(area: CaptureArea) -> Result<()> {
let (width, height) = x11_screen_size()?;
validate_area_within_bounds(area, width, height)
}
pub fn x11_screen_size() -> Result<(i32, i32)> {
let (conn, screen_num) = connect_x11("X11 screen size cannot be detected")?;
let screen = &conn.setup().roots[screen_num];
let width = i32::from(screen.width_in_pixels);
let height = i32::from(screen.height_in_pixels);
ensure!(width > 0, "X11 screen width must be > 0");
ensure!(height > 0, "X11 screen height must be > 0");
Ok((width, height))
}
pub fn x11_available() -> bool {
std::env::var_os("DISPLAY").is_some() && connect(None).is_ok()
}
fn connect_x11(action: &str) -> Result<(RustConnection, usize)> {
ensure!(
std::env::var_os("DISPLAY").is_some(),
"DISPLAY is not set, so {action}"
);
connect(None).with_context(|| format!("failed to connect to X11 display, so {action}"))
}
fn validate_area_within_bounds(area: CaptureArea, width: i32, height: i32) -> Result<()> {
ensure!(width > 0, "X11 screen width must be > 0");
ensure!(height > 0, "X11 screen height must be > 0");
ensure!(
area.right() <= width && area.bottom() <= height,
"selection area {}x{}+{}+{} extends outside X11 screen {}x{}",
area.width,
area.height,
area.x,
area.y,
width,
height
);
Ok(())
}
pub fn format_monitor(monitor: &Monitor) -> String {
let primary = if monitor.primary { " primary" } else { "" };
format!(
"{}: {} {}x{}+{}+{}{}",
monitor.index, monitor.name, monitor.width, monitor.height, monitor.x, monitor.y, primary
)
}
pub fn validate_monitor_index(index: usize, monitor_count: usize) -> Result<()> {
if index == 0 || index > monitor_count {
bail!("monitor index must be between 1 and {monitor_count}");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_monitor_marks_primary() {
let monitor = Monitor {
index: 2,
name: "HDMI-1".to_string(),
x: 1920,
y: 0,
width: 2560,
height: 1440,
primary: true,
};
assert_eq!(
format_monitor(&monitor),
"2: HDMI-1 2560x1440+1920+0 primary"
);
}
#[test]
fn validate_monitor_index_rejects_out_of_range() {
assert!(validate_monitor_index(1, 2).is_ok());
assert!(validate_monitor_index(0, 2).is_err());
assert!(validate_monitor_index(3, 2).is_err());
}
#[test]
fn validate_area_within_bounds_rejects_out_of_range_area() {
assert!(
validate_area_within_bounds(CaptureArea::new(0, 0, 640, 480).unwrap(), 640, 480)
.is_ok()
);
assert!(
validate_area_within_bounds(CaptureArea::new(639, 0, 1, 480).unwrap(), 640, 480)
.is_ok()
);
assert!(
validate_area_within_bounds(CaptureArea::new(640, 0, 1, 1).unwrap(), 640, 480).is_err()
);
assert!(
validate_area_within_bounds(CaptureArea::new(0, 479, 640, 2).unwrap(), 640, 480)
.is_err()
);
}
}