use anyhow::{bail, Context};
use std::{
process::Command,
thread::sleep,
time::{Duration, Instant},
};
use xcap::image::RgbaImage;
use x11rb::{
protocol::xproto::{Atom, ConnectionExt},
rust_connection::RustConnection,
};
const SINGLE_WAIT_DURATION: Duration = Duration::from_millis(200);
const DEFAULT_WAIT_DURATION: Duration = Duration::from_secs(5);
pub struct Connection {
connection: RustConnection,
net_wm_pid: Atom,
cardinal: Atom,
wait_duration: Duration,
}
fn get_or_intern_atom(conn: &RustConnection, name: &[u8]) -> Atom {
let result = conn
.intern_atom(false, name)
.expect("Failed to intern atom")
.reply()
.expect("Failed receive interned atom");
result.atom
}
impl Connection {
#[allow(clippy::new_without_default)]
pub fn new() -> anyhow::Result<Self> {
let (connection, _screen_num) = x11rb::connect(None)?;
let net_wm_pid = get_or_intern_atom(&connection, b"_NET_WM_PID");
let cardinal = get_or_intern_atom(&connection, b"CARDINAL");
Ok(Self {
connection,
net_wm_pid,
cardinal,
wait_duration: DEFAULT_WAIT_DURATION,
})
}
pub fn all_windows(&self) -> anyhow::Result<Vec<Window>> {
xcap::Window::all()?
.into_iter()
.map(|w| Window::new(self, w))
.collect()
}
pub fn windows_by_pid(&self, pid: u32) -> anyhow::Result<Vec<Window>> {
let windows = self.all_windows()?;
Ok(windows.into_iter().filter(|w| w.pid == pid).collect())
}
pub fn wait_for_windows_by_pid(&self, pid: u32) -> anyhow::Result<Vec<Window>> {
let started = Instant::now();
while started.elapsed() < self.wait_duration {
let windows = self.windows_by_pid(pid)?;
if !windows.is_empty() {
return Ok(windows);
}
sleep(SINGLE_WAIT_DURATION);
}
bail!(
"couldn't find a window with pid={} after {:?}",
pid,
self.wait_duration
);
}
pub fn active_window_id(&self) -> anyhow::Result<u32> {
let output = Command::new("xdotool")
.arg("getactivewindow")
.output()
.with_context(|| "failed to execute command: xdotool getactivewindow")?;
if !output.status.success() {
bail!("xdotool failed: {:?}", output);
}
Ok(String::from_utf8(output.stdout)?.trim().parse()?)
}
fn run_xdotool(&self, args: &[&str]) -> anyhow::Result<()> {
let status = Command::new("xdotool")
.args(args)
.status()
.with_context(|| format!("failed to execute command: xdotool {:?}", args))?;
if !status.success() {
bail!("xdotool failed with status {:?}", status);
}
Ok(())
}
pub fn mouse_click(&self, button: u32) -> anyhow::Result<()> {
self.run_xdotool(&["click", &button.to_string()])
}
pub fn mouse_down(&self, button: u32) -> anyhow::Result<()> {
self.run_xdotool(&["mousedown", &button.to_string()])
}
pub fn mouse_up(&self, button: u32) -> anyhow::Result<()> {
self.run_xdotool(&["mouseup", &button.to_string()])
}
pub fn key(&self, key: &str) -> anyhow::Result<()> {
self.run_xdotool(&["key", key])
}
pub fn type_text(&self, text: &str) -> anyhow::Result<()> {
self.run_xdotool(&["type", text])
}
}
pub struct Window {
pid: u32,
inner: xcap::Window,
}
impl Window {
fn new(connection: &Connection, inner: xcap::Window) -> anyhow::Result<Self> {
let pid = connection
.connection
.get_property(
false,
inner.id(),
connection.net_wm_pid,
connection.cardinal,
0,
u32::MAX,
)?
.reply()?
.value32()
.unwrap()
.next()
.unwrap();
Ok(Self { pid, inner })
}
pub fn pid(&self) -> u32 {
self.pid
}
pub fn id(&self) -> u32 {
self.inner.id()
}
pub fn app_name(&self) -> &str {
self.inner.app_name()
}
pub fn title(&self) -> &str {
self.inner.title()
}
pub fn x(&self) -> i32 {
self.inner.x()
}
pub fn y(&self) -> i32 {
self.inner.y()
}
pub fn width(&self) -> u32 {
self.inner.width()
}
pub fn height(&self) -> u32 {
self.inner.height()
}
pub fn is_minimized(&self) -> bool {
self.inner.is_minimized()
}
pub fn is_maximized(&self) -> bool {
self.inner.is_maximized()
}
pub fn capture_image(&mut self) -> anyhow::Result<RgbaImage> {
self.inner.refresh()?;
Ok(self.inner.capture_image()?)
}
pub fn activate(&self) -> anyhow::Result<()> {
let status = Command::new("xdotool")
.arg("windowactivate")
.arg("--sync")
.arg(self.id().to_string())
.status()?;
if !status.success() {
bail!("xdotool failed: {:?}", status);
}
Ok(())
}
pub fn mouse_move(&self, x: u32, y: u32) -> anyhow::Result<()> {
let status = Command::new("xdotool")
.arg("mousemove")
.arg("--window")
.arg(self.id().to_string())
.arg("--sync")
.arg(x.to_string())
.arg(y.to_string())
.status()?;
if !status.success() {
bail!("xdotool failed: {:?}", status);
}
Ok(())
}
pub fn minimize(&self) -> anyhow::Result<()> {
let status = Command::new("xdotool")
.arg("windowminimize")
.arg("--sync")
.arg(self.id().to_string())
.status()?;
if !status.success() {
bail!("xdotool failed: {:?}", status);
}
Ok(())
}
pub fn close(&self) -> anyhow::Result<()> {
let status = Command::new("wmctrl")
.arg("-i")
.arg("-c")
.arg(self.id().to_string())
.status()?;
if !status.success() {
bail!("wmctrl failed: {:?}", status);
}
Ok(())
}
pub fn resize(&self, width: i32, height: i32) -> anyhow::Result<()> {
let status = Command::new("xdotool")
.arg("windowsize")
.arg("--sync")
.arg(self.id().to_string())
.arg(width.to_string())
.arg(height.to_string())
.status()?;
if !status.success() {
bail!("xdotool failed: {:?}", status);
}
Ok(())
}
}