Phase 6: utility commands, SKILL.md, AGENTS.md, README.md

- Implement screen_size via xcap Monitor, mouse_position via x11rb
  query_pointer, standalone screenshot with optional annotation,
  launch for spawning detached processes
- Handler dispatchers for get-screen-size, get-mouse-position,
  screenshot, launch
- SKILL.md agent discovery file with allowed-tools frontmatter
- AGENTS.md contributor guidelines for AI agents
- README.md with installation, quick start, architecture overview
This commit is contained in:
Harivansh Rathi 2026-03-24 21:40:29 -04:00
parent 567115a6c2
commit 03dfd6b6ea
5 changed files with 339 additions and 8 deletions

View file

@ -288,22 +288,71 @@ impl super::DesktopBackend for X11Backend {
Ok(())
}
// Phase 6: utility stubs
fn screen_size(&self) -> Result<(u32, u32)> {
anyhow::bail!("Utility commands not yet implemented (Phase 6)")
let monitors = xcap::Monitor::all().context("Failed to enumerate monitors")?;
let monitor = monitors.into_iter().next().context("No monitor found")?;
let w = monitor.width().context("Failed to get monitor width")?;
let h = monitor.height().context("Failed to get monitor height")?;
Ok((w, h))
}
fn mouse_position(&self) -> Result<(i32, i32)> {
anyhow::bail!("Utility commands not yet implemented (Phase 6)")
let reply = self
.conn
.query_pointer(self.root)?
.reply()
.context("Failed to query pointer")?;
Ok((reply.root_x as i32, reply.root_y as i32))
}
fn screenshot(&mut self, _path: &str, _annotate: bool) -> Result<String> {
anyhow::bail!("Standalone screenshot not yet implemented (Phase 6)")
fn screenshot(&mut self, path: &str, annotate: bool) -> Result<String> {
let monitors = xcap::Monitor::all().context("Failed to enumerate monitors")?;
let monitor = monitors.into_iter().next().context("No monitor found")?;
let mut image = monitor
.capture_image()
.context("Failed to capture screenshot")?;
if annotate {
let windows = xcap::Window::all().unwrap_or_default();
let mut window_infos = Vec::new();
let mut ref_counter = 1usize;
for win in &windows {
let title = win.title().unwrap_or_default();
let app_name = win.app_name().unwrap_or_default();
if title.is_empty() && app_name.is_empty() {
continue;
}
window_infos.push(crate::core::types::WindowInfo {
ref_id: format!("w{ref_counter}"),
xcb_id: win.id().unwrap_or(0),
title,
app_name,
x: win.x().unwrap_or(0),
y: win.y().unwrap_or(0),
width: win.width().unwrap_or(0),
height: win.height().unwrap_or(0),
focused: win.is_focused().unwrap_or(false),
minimized: win.is_minimized().unwrap_or(false),
});
ref_counter += 1;
}
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> {
anyhow::bail!("Launch not yet implemented (Phase 6)")
fn launch(&self, command: &str, args: &[String]) -> Result<u32> {
let child = std::process::Command::new(command)
.args(args)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.with_context(|| format!("Failed to launch: {command}"))?;
Ok(child.id())
}
}

View file

@ -25,6 +25,10 @@ pub async fn handle_request(
"move-window" => handle_move_window(request, state).await,
"resize-window" => handle_resize_window(request, state).await,
"list-windows" => handle_list_windows(state).await,
"get-screen-size" => handle_get_screen_size(state).await,
"get-mouse-position" => handle_get_mouse_position(state).await,
"screenshot" => handle_screenshot(request, state).await,
"launch" => handle_launch(request, state).await,
action => Response::err(format!("Unknown action: {action}")),
}
}
@ -372,6 +376,75 @@ async fn handle_list_windows(
}
}
async fn handle_get_screen_size(state: &Arc<Mutex<DaemonState>>) -> Response {
let state = state.lock().await;
match state.backend.screen_size() {
Ok((w, h)) => Response::ok(serde_json::json!({"width": w, "height": h})),
Err(e) => Response::err(format!("Failed: {e}")),
}
}
async fn handle_get_mouse_position(state: &Arc<Mutex<DaemonState>>) -> Response {
let state = state.lock().await;
match state.backend.mouse_position() {
Ok((x, y)) => Response::ok(serde_json::json!({"x": x, "y": y})),
Err(e) => Response::err(format!("Failed: {e}")),
}
}
async fn handle_screenshot(
request: &Request,
state: &Arc<Mutex<DaemonState>>,
) -> Response {
let annotate = request
.extra
.get("annotate")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let path = request
.extra
.get("path")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
format!("/tmp/desktop-ctl-{ts}.png")
});
let mut state = state.lock().await;
match state.backend.screenshot(&path, annotate) {
Ok(saved) => Response::ok(serde_json::json!({"screenshot": saved})),
Err(e) => Response::err(format!("Screenshot failed: {e}")),
}
}
async fn handle_launch(
request: &Request,
state: &Arc<Mutex<DaemonState>>,
) -> Response {
let command = match request.extra.get("command").and_then(|v| v.as_str()) {
Some(c) => c.to_string(),
None => return Response::err("Missing 'command' field"),
};
let args: Vec<String> = request
.extra
.get("args")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let state = state.lock().await;
match state.backend.launch(&command, &args) {
Ok(pid) => Response::ok(serde_json::json!({"pid": pid, "command": command})),
Err(e) => Response::err(format!("Launch failed: {e}")),
}
}
fn parse_coords(s: &str) -> Option<(i32, i32)> {
let parts: Vec<&str> = s.split(',').collect();
if parts.len() == 2 {