Phase 2: snapshot - window tree + screenshot via xcap

- Add xcap and image dependencies
- DesktopBackend trait with all 16 methods for future extensibility
- X11Backend with real snapshot() using xcap Window::all() and
  Monitor::all() for z-ordered window enumeration and screenshot
- Stub implementations for input/window management (phases 4-6)
- Wire X11Backend into DaemonState (now returns Result)
- Real snapshot handler replacing placeholder, updates ref map
This commit is contained in:
Harivansh Rathi 2026-03-24 21:24:34 -04:00
parent dfaa339594
commit 79e6e0e25c
8 changed files with 3041 additions and 34 deletions

View file

@ -1,31 +1,52 @@
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::backend::DesktopBackend;
use crate::core::protocol::{Request, Response};
use crate::core::refs::RefEntry;
use super::state::DaemonState;
pub async fn handle_request(
request: &Request,
_state: &Arc<Mutex<DaemonState>>,
state: &Arc<Mutex<DaemonState>>,
) -> Response {
match request.action.as_str() {
"snapshot" => {
Response::ok(serde_json::json!({
"screenshot": "/tmp/desktop-ctl-placeholder.png",
"windows": [
{
"ref_id": "w1",
"xcb_id": 0,
"title": "Placeholder Window",
"app_name": "placeholder",
"x": 0, "y": 0, "width": 1920, "height": 1080,
"focused": true, "minimized": false
}
]
}))
}
action => {
Response::err(format!("Unknown action: {action}"))
}
"snapshot" => handle_snapshot(request, state).await,
action => Response::err(format!("Unknown action: {action}")),
}
}
async fn handle_snapshot(
request: &Request,
state: &Arc<Mutex<DaemonState>>,
) -> Response {
let annotate = request.extra.get("annotate")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let mut state = state.lock().await;
match state.backend.snapshot(annotate) {
Ok(snapshot) => {
// Update ref map
state.ref_map.clear();
for win in &snapshot.windows {
state.ref_map.insert(RefEntry {
xcb_id: win.xcb_id,
app_class: win.app_name.clone(),
title: win.title.clone(),
pid: 0, // xcap doesn't expose PID directly in snapshot
x: win.x,
y: win.y,
width: win.width,
height: win.height,
focused: win.focused,
minimized: win.minimized,
});
}
Response::ok(serde_json::to_value(&snapshot).unwrap_or_default())
}
Err(e) => Response::err(format!("Snapshot failed: {e}")),
}
}

View file

@ -46,7 +46,10 @@ async fn async_run() -> Result<()> {
.context(format!("Failed to bind socket: {}", socket_path.display()))?;
let session = std::env::var("DESKTOP_CTL_SESSION").unwrap_or_else(|_| "default".to_string());
let state = Arc::new(Mutex::new(DaemonState::new(session, socket_path.clone())));
let state = Arc::new(Mutex::new(
DaemonState::new(session, socket_path.clone())
.context("Failed to initialize daemon state")?
));
let shutdown = Arc::new(tokio::sync::Notify::new());
let shutdown_clone = shutdown.clone();

View file

@ -1,4 +1,6 @@
use std::path::PathBuf;
use crate::backend::x11::X11Backend;
use crate::core::refs::RefMap;
#[allow(dead_code)]
@ -6,14 +8,17 @@ pub struct DaemonState {
pub session: String,
pub socket_path: PathBuf,
pub ref_map: RefMap,
pub backend: X11Backend,
}
impl DaemonState {
pub fn new(session: String, socket_path: PathBuf) -> Self {
Self {
pub fn new(session: String, socket_path: PathBuf) -> anyhow::Result<Self> {
let backend = X11Backend::new()?;
Ok(Self {
session,
socket_path,
ref_map: RefMap::new(),
}
backend,
})
}
}