#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "windows")]
mod windows;
#[cfg(any(target_os = "windows", target_os = "macos"))]
mod xcap_common;
#[cfg(target_os = "linux")]
use linux as imp;
#[cfg(target_os = "macos")]
use macos as imp;
#[cfg(target_os = "windows")]
use windows as imp;
use crate::{extensions::computer::state::Rect, utils::process::spawn_detached};
use serde_json::{Value, json};
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct Display {
pub name: String,
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
pub scale: f64,
pub primary: bool,
}
impl Display {
pub fn render(&self) -> String {
format!(
"{} {},{} {}x{} scale {:.2}{}",
self.name,
self.x,
self.y,
self.width,
self.height,
self.scale,
if self.primary { " (primary)" } else { "" }
)
}
}
#[derive(Debug, Clone)]
pub struct Window {
pub id: u64,
pub title: String,
pub app: String,
pub pid: u32,
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
pub focused: bool,
}
impl Window {
pub fn rect(&self) -> Rect {
Rect {
x: self.x,
y: self.y,
width: self.width,
height: self.height,
}
}
pub fn render(&self) -> String {
format!(
"{} {:?} — {} [{}x{} at {},{}]{}",
self.id,
self.app,
if self.title.is_empty() {
"(untitled)"
} else {
&self.title
},
self.width,
self.height,
self.x,
self.y,
if self.focused { " (focused)" } else { "" }
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShotFormat {
Png,
Jpeg,
}
impl ShotFormat {
pub fn mime(&self) -> &'static str {
match self {
ShotFormat::Png => "image/png",
ShotFormat::Jpeg => "image/jpeg",
}
}
pub fn parse(name: &str) -> Option<Self> {
match name.to_ascii_lowercase().as_str() {
"png" => Some(ShotFormat::Png),
"jpeg" | "jpg" => Some(ShotFormat::Jpeg),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct Launched {
pub window: Option<Window>,
pub pid: u32,
pub log: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Shot {
pub bytes: Vec<u8>,
pub format: ShotFormat,
pub width: u32,
pub height: u32,
pub origin: (i32, i32),
pub display: String,
}
#[derive(Debug, Clone)]
pub enum CaptureTarget {
All,
Display(String),
Region(Rect),
Window(u64),
}
#[derive(Debug, Clone)]
pub struct CaptureRequest {
pub target: CaptureTarget,
pub scale: f64,
pub format: ShotFormat,
pub jpeg_quality: u8,
pub cursor: bool,
}
pub fn available() -> bool {
imp::available()
}
pub fn describe() -> String {
imp::describe()
}
pub fn displays() -> Result<Vec<Display>, String> {
imp::displays()
}
pub fn windows() -> Result<Vec<Window>, String> {
imp::windows()
}
pub fn window(id: u64) -> Result<Window, String> {
windows()?.into_iter().find(|w| w.id == id).ok_or_else(|| {
format!(
"window {id} is not in the current window list; list them again with computer_windows"
)
})
}
pub fn focus_window(id: u64) -> Result<(), String> {
imp::focus_window(id)
}
pub fn capture(req: &CaptureRequest) -> Result<Shot, String> {
imp::capture(req)
}
pub fn cursor() -> Option<(i32, i32)> {
imp::cursor()
}
pub fn launch(target: &str, args: &[String], wait_ms: u64) -> Result<Launched, String> {
imp::launch(target, args, wait_ms)
}
fn launch_process(target: &str, args: &[String]) -> Result<(u32, Option<PathBuf>), String> {
spawn_detached("computer-launch", target, args)
.map_err(|e| format!("could not start {target:?}: {e}"))
}
pub fn window_action(id: u64, action: &str) -> Result<String, String> {
imp::window_action(id, action)
}
pub fn find_window<'a>(list: &'a [Window], filter: &str) -> Vec<&'a Window> {
let needle = filter.to_lowercase();
if needle.is_empty() {
return list.iter().collect();
}
list.iter()
.filter(|w| {
w.title.to_lowercase().contains(&needle) || w.app.to_lowercase().contains(&needle)
})
.collect()
}
pub fn render_displays(list: &[Display]) -> String {
list.iter()
.map(Display::render)
.collect::<Vec<_>>()
.join("\n")
}
pub fn render_windows(list: &[Window]) -> String {
list.iter()
.map(Window::render)
.collect::<Vec<_>>()
.join("\n")
}
pub fn diagnose() -> Value {
json!({
"backend": describe(),
"available": available(),
"displays": displays().map(|d| d.iter().map(Display::render).collect::<Vec<_>>()).unwrap_or_default(),
"cursor": cursor(),
})
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
mod imp {
use super::*;
pub fn available() -> bool {
false
}
pub fn describe() -> String {
format!("no desktop backend for {}", std::env::consts::OS)
}
pub fn displays() -> Result<Vec<Display>, String> {
Err(format!(
"computer control has no backend for {} (supported: Linux Wayland, Windows, macOS)",
std::env::consts::OS
))
}
pub fn windows() -> Result<Vec<Window>, String> {
displays().map(|_| Vec::new())
}
pub fn focus_window(_id: u64) -> Result<(), String> {
displays().map(|_| ())
}
pub fn capture(_req: &CaptureRequest) -> Result<Shot, String> {
displays().map(|_| unreachable!())
}
pub fn cursor() -> Option<(i32, i32)> {
None
}
pub fn launch(_target: &str, _args: &[String], _wait_ms: u64) -> Result<Launched, String> {
displays().map(|_| unreachable!())
}
pub fn window_action(_id: u64, _action: &str) -> Result<String, String> {
displays().map(|_| String::new())
}
}