stabilize (#3)

* specs

* Stabilize deskctl runtime foundation

Co-authored-by: Codex <noreply@openai.com>

* opsx archive

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Hari 2026-03-25 18:31:08 -04:00 committed by GitHub
parent d487a60209
commit 6dce22eaef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1289 additions and 295 deletions

View file

@ -1,25 +1,41 @@
pub mod annotate;
pub mod x11;
use crate::core::types::Snapshot;
use anyhow::Result;
use image::RgbaImage;
#[derive(Debug, Clone)]
pub struct BackendWindow {
pub native_id: u32,
pub title: String,
pub app_name: String,
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
pub focused: bool,
pub minimized: bool,
}
#[allow(dead_code)]
pub trait DesktopBackend: Send {
/// Capture a screenshot and return a z-ordered window tree with @wN refs.
fn snapshot(&mut self, annotate: bool) -> Result<Snapshot>;
/// Collect z-ordered windows for read-only queries and targeting.
fn list_windows(&mut self) -> Result<Vec<BackendWindow>>;
/// Focus a window by its X11 window ID.
fn focus_window(&mut self, xcb_id: u32) -> Result<()>;
/// Capture the current desktop image without writing it to disk.
fn capture_screenshot(&mut self) -> Result<RgbaImage>;
/// Focus a window by its backend-native window handle.
fn focus_window(&mut self, native_id: u32) -> Result<()>;
/// Move a window to absolute coordinates.
fn move_window(&mut self, xcb_id: u32, x: i32, y: i32) -> Result<()>;
fn move_window(&mut self, native_id: u32, x: i32, y: i32) -> Result<()>;
/// Resize a window.
fn resize_window(&mut self, xcb_id: u32, w: u32, h: u32) -> Result<()>;
fn resize_window(&mut self, native_id: u32, w: u32, h: u32) -> Result<()>;
/// Close a window gracefully.
fn close_window(&mut self, xcb_id: u32) -> Result<()>;
fn close_window(&mut self, native_id: u32) -> Result<()>;
/// Click at absolute coordinates.
fn click(&mut self, x: i32, y: i32) -> Result<()>;
@ -51,9 +67,6 @@ pub trait DesktopBackend: Send {
/// Get the current mouse position.
fn mouse_position(&self) -> Result<(i32, i32)>;
/// Take a screenshot and save to a path (no window tree).
fn screenshot(&mut self, path: &str, annotate: bool) -> Result<String>;
/// Launch an application.
fn launch(&self, command: &str, args: &[String]) -> Result<u32>;
}

View file

@ -9,8 +9,7 @@ use x11rb::protocol::xproto::{
};
use x11rb::rust_connection::RustConnection;
use super::annotate::annotate_screenshot;
use crate::core::types::{Snapshot, WindowInfo};
use crate::backend::BackendWindow;
struct Atoms {
client_list_stacking: Atom,
@ -71,10 +70,9 @@ impl X11Backend {
Ok(windows)
}
fn collect_window_infos(&self) -> Result<Vec<WindowInfo>> {
fn collect_window_infos(&self) -> Result<Vec<BackendWindow>> {
let active_window = self.active_window()?;
let mut window_infos = Vec::new();
let mut ref_counter = 1usize;
for window in self.stacked_windows()? {
let title = self.window_title(window).unwrap_or_default();
@ -89,9 +87,8 @@ impl X11Backend {
};
let minimized = self.window_is_minimized(window).unwrap_or(false);
window_infos.push(WindowInfo {
ref_id: format!("w{ref_counter}"),
xcb_id: window,
window_infos.push(BackendWindow {
native_id: window,
title,
app_name,
x,
@ -101,7 +98,6 @@ impl X11Backend {
focused: active_window == Some(window),
minimized,
});
ref_counter += 1;
}
Ok(window_infos)
@ -231,32 +227,15 @@ impl X11Backend {
}
impl super::DesktopBackend for X11Backend {
fn snapshot(&mut self, annotate: bool) -> Result<Snapshot> {
let window_infos = self.collect_window_infos()?;
let mut image = self.capture_root_image()?;
// Annotate if requested - draw bounding boxes and @wN labels
if annotate {
annotate_screenshot(&mut image, &window_infos);
}
// Save screenshot
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
let screenshot_path = format!("/tmp/deskctl-{timestamp}.png");
image
.save(&screenshot_path)
.context("Failed to save screenshot")?;
Ok(Snapshot {
screenshot: screenshot_path,
windows: window_infos,
})
fn list_windows(&mut self) -> Result<Vec<BackendWindow>> {
self.collect_window_infos()
}
fn focus_window(&mut self, xcb_id: u32) -> Result<()> {
fn capture_screenshot(&mut self) -> Result<RgbaImage> {
self.capture_root_image()
}
fn focus_window(&mut self, native_id: u32) -> Result<()> {
// Use _NET_ACTIVE_WINDOW client message (avoids focus-stealing prevention)
let net_active = self
.conn
@ -269,7 +248,7 @@ impl super::DesktopBackend for X11Backend {
response_type: x11rb::protocol::xproto::CLIENT_MESSAGE_EVENT,
format: 32,
sequence: 0,
window: xcb_id,
window: native_id,
type_: net_active,
data: ClientMessageData::from([
2u32, 0, 0, 0, 0, // source=2 (pager), timestamp=0, currently_active=0
@ -288,25 +267,25 @@ impl super::DesktopBackend for X11Backend {
Ok(())
}
fn move_window(&mut self, xcb_id: u32, x: i32, y: i32) -> Result<()> {
fn move_window(&mut self, native_id: u32, x: i32, y: i32) -> Result<()> {
self.conn
.configure_window(xcb_id, &ConfigureWindowAux::new().x(x).y(y))?;
.configure_window(native_id, &ConfigureWindowAux::new().x(x).y(y))?;
self.conn
.flush()
.context("Failed to flush X11 connection")?;
Ok(())
}
fn resize_window(&mut self, xcb_id: u32, w: u32, h: u32) -> Result<()> {
fn resize_window(&mut self, native_id: u32, w: u32, h: u32) -> Result<()> {
self.conn
.configure_window(xcb_id, &ConfigureWindowAux::new().width(w).height(h))?;
.configure_window(native_id, &ConfigureWindowAux::new().width(w).height(h))?;
self.conn
.flush()
.context("Failed to flush X11 connection")?;
Ok(())
}
fn close_window(&mut self, xcb_id: u32) -> Result<()> {
fn close_window(&mut self, native_id: u32) -> Result<()> {
// Use _NET_CLOSE_WINDOW for graceful close (respects WM protocols)
let net_close = self
.conn
@ -319,7 +298,7 @@ impl super::DesktopBackend for X11Backend {
response_type: x11rb::protocol::xproto::CLIENT_MESSAGE_EVENT,
format: 32,
sequence: 0,
window: xcb_id,
window: native_id,
type_: net_close,
data: ClientMessageData::from([
0u32, 2, 0, 0, 0, // timestamp=0, source=2 (pager)
@ -463,18 +442,6 @@ impl super::DesktopBackend for X11Backend {
Ok((reply.root_x as i32, reply.root_y as i32))
}
fn screenshot(&mut self, path: &str, annotate: bool) -> Result<String> {
let mut image = self.capture_root_image()?;
if annotate {
let window_infos = self.collect_window_infos()?;
annotate_screenshot(&mut image, &window_infos);
}
image.save(path).context("Failed to save screenshot")?;
Ok(path.to_string())
}
fn launch(&self, command: &str, args: &[String]) -> Result<u32> {
let child = std::process::Command::new(command)
.args(args)