mirror of
https://github.com/harivansh-afk/deskctl.git
synced 2026-04-15 04:03:28 +00:00
- grouped runtime reads and waits selector modes - Fix wait command client timeouts and test failures
772 lines
27 KiB
Rust
772 lines
27 KiB
Rust
use std::sync::Arc;
|
|
|
|
use anyhow::{Context, Result};
|
|
use tokio::sync::Mutex;
|
|
use tokio::time::{sleep, Duration, Instant};
|
|
|
|
use super::state::DaemonState;
|
|
use crate::backend::annotate::annotate_screenshot;
|
|
use crate::core::protocol::{Request, Response};
|
|
use crate::core::refs::{ResolveResult, SelectorQuery};
|
|
use crate::core::types::{MonitorInfo, ScreenSize, Snapshot, SystemInfo, VersionInfo, WindowInfo};
|
|
|
|
pub async fn handle_request(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
match request.action.as_str() {
|
|
"ping" => Response::ok(serde_json::json!({"message": "pong"})),
|
|
"snapshot" => handle_snapshot(request, state).await,
|
|
"click" => handle_click(request, state).await,
|
|
"dblclick" => handle_dblclick(request, state).await,
|
|
"type" => handle_type(request, state).await,
|
|
"press" => handle_press(request, state).await,
|
|
"hotkey" => handle_hotkey(request, state).await,
|
|
"mouse-move" => handle_mouse_move(request, state).await,
|
|
"mouse-scroll" => handle_mouse_scroll(request, state).await,
|
|
"mouse-drag" => handle_mouse_drag(request, state).await,
|
|
"focus" => handle_window_action(request, state, "focus").await,
|
|
"close" => handle_window_action(request, state, "close").await,
|
|
"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,
|
|
"get-active-window" => handle_get_active_window(state).await,
|
|
"get-monitors" => handle_get_monitors(state).await,
|
|
"get-version" => handle_get_version(state).await,
|
|
"get-systeminfo" => handle_get_systeminfo(state).await,
|
|
"wait-window" => handle_wait(request, state, WaitKind::Window).await,
|
|
"wait-focus" => handle_wait(request, state, WaitKind::Focus).await,
|
|
"screenshot" => handle_screenshot(request, state).await,
|
|
"launch" => handle_launch(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 capture_snapshot(&mut state, annotate, None) {
|
|
Ok(snapshot) => Response::ok(serde_json::to_value(&snapshot).unwrap_or_default()),
|
|
Err(error) => Response::err(format!("Snapshot failed: {error}")),
|
|
}
|
|
}
|
|
|
|
async fn handle_click(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
let selector = match request.extra.get("selector").and_then(|v| v.as_str()) {
|
|
Some(selector) => selector.to_string(),
|
|
None => return Response::err("Missing 'selector' field"),
|
|
};
|
|
|
|
let mut state = state.lock().await;
|
|
let selector_query = SelectorQuery::parse(&selector);
|
|
|
|
if let Some((x, y)) = parse_coords(&selector) {
|
|
return match state.backend.click(x, y) {
|
|
Ok(()) => Response::ok(serde_json::json!({"clicked": {"x": x, "y": y}})),
|
|
Err(error) => Response::err(format!("Click failed: {error}")),
|
|
};
|
|
}
|
|
|
|
if selector_query.needs_live_refresh() {
|
|
if let Err(error) = refresh_windows(&mut state) {
|
|
return Response::err(format!("Click failed: {error}"));
|
|
}
|
|
}
|
|
|
|
match state.ref_map.resolve_to_center(&selector) {
|
|
ResolveResult::Match(entry) => {
|
|
let (x, y) = entry.center();
|
|
match state.backend.click(x, y) {
|
|
Ok(()) => Response::ok(
|
|
serde_json::json!({"clicked": {"x": x, "y": y, "selector": selector, "window_id": entry.window_id}}),
|
|
),
|
|
Err(error) => Response::err(format!("Click failed: {error}")),
|
|
}
|
|
}
|
|
outcome => selector_failure_response(outcome),
|
|
}
|
|
}
|
|
|
|
async fn handle_dblclick(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
let selector = match request.extra.get("selector").and_then(|v| v.as_str()) {
|
|
Some(selector) => selector.to_string(),
|
|
None => return Response::err("Missing 'selector' field"),
|
|
};
|
|
|
|
let mut state = state.lock().await;
|
|
let selector_query = SelectorQuery::parse(&selector);
|
|
|
|
if let Some((x, y)) = parse_coords(&selector) {
|
|
return match state.backend.dblclick(x, y) {
|
|
Ok(()) => Response::ok(serde_json::json!({"double_clicked": {"x": x, "y": y}})),
|
|
Err(error) => Response::err(format!("Double-click failed: {error}")),
|
|
};
|
|
}
|
|
|
|
if selector_query.needs_live_refresh() {
|
|
if let Err(error) = refresh_windows(&mut state) {
|
|
return Response::err(format!("Double-click failed: {error}"));
|
|
}
|
|
}
|
|
|
|
match state.ref_map.resolve_to_center(&selector) {
|
|
ResolveResult::Match(entry) => {
|
|
let (x, y) = entry.center();
|
|
match state.backend.dblclick(x, y) {
|
|
Ok(()) => Response::ok(
|
|
serde_json::json!({"double_clicked": {"x": x, "y": y, "selector": selector, "window_id": entry.window_id}}),
|
|
),
|
|
Err(error) => Response::err(format!("Double-click failed: {error}")),
|
|
}
|
|
}
|
|
outcome => selector_failure_response(outcome),
|
|
}
|
|
}
|
|
|
|
async fn handle_type(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
let text = match request.extra.get("text").and_then(|v| v.as_str()) {
|
|
Some(text) => text.to_string(),
|
|
None => return Response::err("Missing 'text' field"),
|
|
};
|
|
|
|
let mut state = state.lock().await;
|
|
match state.backend.type_text(&text) {
|
|
Ok(()) => Response::ok(serde_json::json!({"typed": text})),
|
|
Err(error) => Response::err(format!("Type failed: {error}")),
|
|
}
|
|
}
|
|
|
|
async fn handle_press(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
let key = match request.extra.get("key").and_then(|v| v.as_str()) {
|
|
Some(key) => key.to_string(),
|
|
None => return Response::err("Missing 'key' field"),
|
|
};
|
|
|
|
let mut state = state.lock().await;
|
|
match state.backend.press_key(&key) {
|
|
Ok(()) => Response::ok(serde_json::json!({"pressed": key})),
|
|
Err(error) => Response::err(format!("Key press failed: {error}")),
|
|
}
|
|
}
|
|
|
|
async fn handle_hotkey(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
let keys: Vec<String> = match request.extra.get("keys").and_then(|v| v.as_array()) {
|
|
Some(keys) => keys
|
|
.iter()
|
|
.filter_map(|value| value.as_str().map(|s| s.to_string()))
|
|
.collect(),
|
|
None => return Response::err("Missing 'keys' field"),
|
|
};
|
|
|
|
let mut state = state.lock().await;
|
|
match state.backend.hotkey(&keys) {
|
|
Ok(()) => Response::ok(serde_json::json!({"hotkey": keys})),
|
|
Err(error) => Response::err(format!("Hotkey failed: {error}")),
|
|
}
|
|
}
|
|
|
|
async fn handle_mouse_move(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
let x = match request.extra.get("x").and_then(|v| v.as_i64()) {
|
|
Some(value) => value as i32,
|
|
None => return Response::err("Missing 'x' field"),
|
|
};
|
|
let y = match request.extra.get("y").and_then(|v| v.as_i64()) {
|
|
Some(value) => value as i32,
|
|
None => return Response::err("Missing 'y' field"),
|
|
};
|
|
|
|
let mut state = state.lock().await;
|
|
match state.backend.mouse_move(x, y) {
|
|
Ok(()) => Response::ok(serde_json::json!({"moved": {"x": x, "y": y}})),
|
|
Err(error) => Response::err(format!("Mouse move failed: {error}")),
|
|
}
|
|
}
|
|
|
|
async fn handle_mouse_scroll(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
let amount = match request.extra.get("amount").and_then(|v| v.as_i64()) {
|
|
Some(value) => value as i32,
|
|
None => return Response::err("Missing 'amount' field"),
|
|
};
|
|
let axis = request
|
|
.extra
|
|
.get("axis")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("vertical")
|
|
.to_string();
|
|
|
|
let mut state = state.lock().await;
|
|
match state.backend.scroll(amount, &axis) {
|
|
Ok(()) => Response::ok(serde_json::json!({"scrolled": {"amount": amount, "axis": axis}})),
|
|
Err(error) => Response::err(format!("Scroll failed: {error}")),
|
|
}
|
|
}
|
|
|
|
async fn handle_mouse_drag(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
let x1 = match request.extra.get("x1").and_then(|v| v.as_i64()) {
|
|
Some(value) => value as i32,
|
|
None => return Response::err("Missing 'x1' field"),
|
|
};
|
|
let y1 = match request.extra.get("y1").and_then(|v| v.as_i64()) {
|
|
Some(value) => value as i32,
|
|
None => return Response::err("Missing 'y1' field"),
|
|
};
|
|
let x2 = match request.extra.get("x2").and_then(|v| v.as_i64()) {
|
|
Some(value) => value as i32,
|
|
None => return Response::err("Missing 'x2' field"),
|
|
};
|
|
let y2 = match request.extra.get("y2").and_then(|v| v.as_i64()) {
|
|
Some(value) => value as i32,
|
|
None => return Response::err("Missing 'y2' field"),
|
|
};
|
|
|
|
let mut state = state.lock().await;
|
|
match state.backend.drag(x1, y1, x2, y2) {
|
|
Ok(()) => Response::ok(serde_json::json!({
|
|
"dragged": {
|
|
"from": {"x": x1, "y": y1},
|
|
"to": {"x": x2, "y": y2}
|
|
}
|
|
})),
|
|
Err(error) => Response::err(format!("Drag failed: {error}")),
|
|
}
|
|
}
|
|
|
|
async fn handle_window_action(
|
|
request: &Request,
|
|
state: &Arc<Mutex<DaemonState>>,
|
|
action: &str,
|
|
) -> Response {
|
|
let selector = match request.extra.get("selector").and_then(|v| v.as_str()) {
|
|
Some(selector) => selector.to_string(),
|
|
None => return Response::err("Missing 'selector' field"),
|
|
};
|
|
|
|
let mut state = state.lock().await;
|
|
let selector_query = SelectorQuery::parse(&selector);
|
|
if selector_query.needs_live_refresh() {
|
|
if let Err(error) = refresh_windows(&mut state) {
|
|
return Response::err(format!("{action} failed: {error}"));
|
|
}
|
|
}
|
|
let entry = match state.ref_map.resolve(&selector) {
|
|
ResolveResult::Match(entry) => entry,
|
|
outcome => return selector_failure_response(outcome),
|
|
};
|
|
|
|
let result = match action {
|
|
"focus" => state.backend.focus_window(entry.backend_window_id),
|
|
"close" => state.backend.close_window(entry.backend_window_id),
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
match result {
|
|
Ok(()) => Response::ok(serde_json::json!({
|
|
"action": action,
|
|
"window": entry.title,
|
|
"window_id": entry.window_id,
|
|
})),
|
|
Err(error) => Response::err(format!("{action} failed: {error}")),
|
|
}
|
|
}
|
|
|
|
async fn handle_move_window(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
let selector = match request.extra.get("selector").and_then(|v| v.as_str()) {
|
|
Some(selector) => selector.to_string(),
|
|
None => return Response::err("Missing 'selector' field"),
|
|
};
|
|
let x = request.extra.get("x").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
|
|
let y = request.extra.get("y").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
|
|
|
|
let mut state = state.lock().await;
|
|
let selector_query = SelectorQuery::parse(&selector);
|
|
if selector_query.needs_live_refresh() {
|
|
if let Err(error) = refresh_windows(&mut state) {
|
|
return Response::err(format!("Move failed: {error}"));
|
|
}
|
|
}
|
|
let entry = match state.ref_map.resolve(&selector) {
|
|
ResolveResult::Match(entry) => entry,
|
|
outcome => return selector_failure_response(outcome),
|
|
};
|
|
|
|
match state.backend.move_window(entry.backend_window_id, x, y) {
|
|
Ok(()) => Response::ok(serde_json::json!({
|
|
"moved": entry.title,
|
|
"window_id": entry.window_id,
|
|
"x": x,
|
|
"y": y,
|
|
})),
|
|
Err(error) => Response::err(format!("Move failed: {error}")),
|
|
}
|
|
}
|
|
|
|
async fn handle_resize_window(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
let selector = match request.extra.get("selector").and_then(|v| v.as_str()) {
|
|
Some(selector) => selector.to_string(),
|
|
None => return Response::err("Missing 'selector' field"),
|
|
};
|
|
let width = request
|
|
.extra
|
|
.get("w")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(800) as u32;
|
|
let height = request
|
|
.extra
|
|
.get("h")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(600) as u32;
|
|
|
|
let mut state = state.lock().await;
|
|
let selector_query = SelectorQuery::parse(&selector);
|
|
if selector_query.needs_live_refresh() {
|
|
if let Err(error) = refresh_windows(&mut state) {
|
|
return Response::err(format!("Resize failed: {error}"));
|
|
}
|
|
}
|
|
let entry = match state.ref_map.resolve(&selector) {
|
|
ResolveResult::Match(entry) => entry,
|
|
outcome => return selector_failure_response(outcome),
|
|
};
|
|
|
|
match state
|
|
.backend
|
|
.resize_window(entry.backend_window_id, width, height)
|
|
{
|
|
Ok(()) => Response::ok(serde_json::json!({
|
|
"resized": entry.title,
|
|
"window_id": entry.window_id,
|
|
"width": width,
|
|
"height": height,
|
|
})),
|
|
Err(error) => Response::err(format!("Resize failed: {error}")),
|
|
}
|
|
}
|
|
|
|
async fn handle_list_windows(state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
let mut state = state.lock().await;
|
|
match refresh_windows(&mut state) {
|
|
Ok(windows) => Response::ok(serde_json::json!({"windows": windows})),
|
|
Err(error) => Response::err(format!("List windows failed: {error}")),
|
|
}
|
|
}
|
|
|
|
async fn handle_get_screen_size(state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
let state = state.lock().await;
|
|
match state.backend.screen_size() {
|
|
Ok((width, height)) => Response::ok(serde_json::json!({"width": width, "height": height})),
|
|
Err(error) => Response::err(format!("Failed: {error}")),
|
|
}
|
|
}
|
|
|
|
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(error) => Response::err(format!("Failed: {error}")),
|
|
}
|
|
}
|
|
|
|
async fn handle_get_active_window(state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
let mut state = state.lock().await;
|
|
let active_backend_window = match state.backend.active_window() {
|
|
Ok(window) => window,
|
|
Err(error) => return Response::err(format!("Failed: {error}")),
|
|
};
|
|
|
|
let windows = match refresh_windows(&mut state) {
|
|
Ok(windows) => windows,
|
|
Err(error) => return Response::err(format!("Failed: {error}")),
|
|
};
|
|
|
|
let active_window = if let Some(active_backend_window) = active_backend_window {
|
|
state
|
|
.ref_map
|
|
.entries()
|
|
.find_map(|(_, entry)| {
|
|
(entry.backend_window_id == active_backend_window.native_id)
|
|
.then(|| entry.to_window_info())
|
|
})
|
|
.or_else(|| windows.iter().find(|window| window.focused).cloned())
|
|
} else {
|
|
windows.iter().find(|window| window.focused).cloned()
|
|
};
|
|
|
|
if let Some(window) = active_window {
|
|
Response::ok(serde_json::json!({"window": window}))
|
|
} else {
|
|
Response::err_with_data(
|
|
"No focused window is available",
|
|
serde_json::json!({"kind": "not_found", "mode": "focused"}),
|
|
)
|
|
}
|
|
}
|
|
|
|
async fn handle_get_monitors(state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
let state = state.lock().await;
|
|
match state.backend.list_monitors() {
|
|
Ok(monitors) => {
|
|
let monitors: Vec<MonitorInfo> = monitors.into_iter().map(Into::into).collect();
|
|
Response::ok(serde_json::json!({
|
|
"count": monitors.len(),
|
|
"monitors": monitors,
|
|
}))
|
|
}
|
|
Err(error) => Response::err(format!("Failed: {error}")),
|
|
}
|
|
}
|
|
|
|
async fn handle_get_version(state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
let state = state.lock().await;
|
|
let info = VersionInfo {
|
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
backend: state.backend.backend_name().to_string(),
|
|
};
|
|
Response::ok(serde_json::to_value(info).unwrap_or_default())
|
|
}
|
|
|
|
async fn handle_get_systeminfo(state: &Arc<Mutex<DaemonState>>) -> Response {
|
|
let state = state.lock().await;
|
|
let screen = match state.backend.screen_size() {
|
|
Ok((width, height)) => ScreenSize { width, height },
|
|
Err(error) => return Response::err(format!("Failed: {error}")),
|
|
};
|
|
let monitors = match state.backend.list_monitors() {
|
|
Ok(monitors) => monitors.into_iter().map(Into::into).collect::<Vec<_>>(),
|
|
Err(error) => return Response::err(format!("Failed: {error}")),
|
|
};
|
|
|
|
let info = SystemInfo {
|
|
backend: state.backend.backend_name().to_string(),
|
|
display: std::env::var("DISPLAY")
|
|
.ok()
|
|
.filter(|value| !value.is_empty()),
|
|
session_type: std::env::var("XDG_SESSION_TYPE")
|
|
.ok()
|
|
.filter(|value| !value.is_empty()),
|
|
session: state.session.clone(),
|
|
socket_path: state.socket_path.display().to_string(),
|
|
screen,
|
|
monitor_count: monitors.len(),
|
|
monitors,
|
|
};
|
|
|
|
Response::ok(serde_json::to_value(info).unwrap_or_default())
|
|
}
|
|
|
|
async fn handle_wait(
|
|
request: &Request,
|
|
state: &Arc<Mutex<DaemonState>>,
|
|
wait_kind: WaitKind,
|
|
) -> Response {
|
|
let selector = match request.extra.get("selector").and_then(|v| v.as_str()) {
|
|
Some(selector) => selector.to_string(),
|
|
None => return Response::err("Missing 'selector' field"),
|
|
};
|
|
let timeout_ms = request
|
|
.extra
|
|
.get("timeout_ms")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(10_000);
|
|
let poll_ms = request
|
|
.extra
|
|
.get("poll_ms")
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(250);
|
|
|
|
let start = Instant::now();
|
|
let deadline = Instant::now() + Duration::from_millis(timeout_ms);
|
|
let mut last_observation: serde_json::Value;
|
|
|
|
loop {
|
|
let outcome = {
|
|
let mut state = state.lock().await;
|
|
if let Err(error) = refresh_windows(&mut state) {
|
|
return Response::err(format!("Wait failed: {error}"));
|
|
}
|
|
observe_wait(&state, &selector, wait_kind)
|
|
};
|
|
|
|
match outcome {
|
|
WaitObservation::Satisfied(window) => {
|
|
let elapsed_ms = start.elapsed().as_millis() as u64;
|
|
return Response::ok(serde_json::json!({
|
|
"wait": wait_kind.as_str(),
|
|
"selector": selector,
|
|
"elapsed_ms": elapsed_ms,
|
|
"window": window,
|
|
}));
|
|
}
|
|
WaitObservation::Retry { observation } => {
|
|
last_observation = observation;
|
|
}
|
|
WaitObservation::Failure(response) => return response,
|
|
}
|
|
|
|
if Instant::now() >= deadline {
|
|
return Response::err_with_data(
|
|
format!(
|
|
"Timed out waiting for {} to match selector: {}",
|
|
wait_kind.as_str(),
|
|
selector
|
|
),
|
|
serde_json::json!({
|
|
"kind": "timeout",
|
|
"wait": wait_kind.as_str(),
|
|
"selector": selector,
|
|
"timeout_ms": timeout_ms,
|
|
"poll_ms": poll_ms,
|
|
"last_observation": last_observation,
|
|
}),
|
|
);
|
|
}
|
|
|
|
sleep(Duration::from_millis(poll_ms)).await;
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum WaitKind {
|
|
Window,
|
|
Focus,
|
|
}
|
|
|
|
impl WaitKind {
|
|
fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::Window => "window",
|
|
Self::Focus => "focus",
|
|
}
|
|
}
|
|
}
|
|
|
|
enum WaitObservation {
|
|
Satisfied(WindowInfo),
|
|
Retry { observation: serde_json::Value },
|
|
Failure(Response),
|
|
}
|
|
|
|
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(|value| value.to_string())
|
|
.unwrap_or_else(temp_screenshot_path);
|
|
|
|
let mut state = state.lock().await;
|
|
let windows = if annotate {
|
|
match refresh_windows(&mut state) {
|
|
Ok(windows) => Some(windows),
|
|
Err(error) => return Response::err(format!("Screenshot failed: {error}")),
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
match capture_and_save_screenshot(&mut state, &path, annotate, windows.as_deref()) {
|
|
Ok(saved) => {
|
|
if let Some(windows) = windows {
|
|
Response::ok(serde_json::json!({"screenshot": saved, "windows": windows}))
|
|
} else {
|
|
Response::ok(serde_json::json!({"screenshot": saved}))
|
|
}
|
|
}
|
|
Err(error) => Response::err(format!("Screenshot failed: {error}")),
|
|
}
|
|
}
|
|
|
|
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(command) => command.to_string(),
|
|
None => return Response::err("Missing 'command' field"),
|
|
};
|
|
let args: Vec<String> = request
|
|
.extra
|
|
.get("args")
|
|
.and_then(|v| v.as_array())
|
|
.map(|args| {
|
|
args.iter()
|
|
.filter_map(|value| value.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(error) => Response::err(format!("Launch failed: {error}")),
|
|
}
|
|
}
|
|
|
|
fn refresh_windows(state: &mut DaemonState) -> Result<Vec<WindowInfo>> {
|
|
let windows = state.backend.list_windows()?;
|
|
Ok(state.ref_map.rebuild(&windows))
|
|
}
|
|
|
|
fn selector_failure_response(result: ResolveResult) -> Response {
|
|
match result {
|
|
ResolveResult::NotFound { selector, mode } => Response::err_with_data(
|
|
format!("Could not resolve selector: {selector}"),
|
|
serde_json::json!({
|
|
"kind": "selector_not_found",
|
|
"selector": selector,
|
|
"mode": mode,
|
|
}),
|
|
),
|
|
ResolveResult::Ambiguous {
|
|
selector,
|
|
mode,
|
|
candidates,
|
|
} => Response::err_with_data(
|
|
format!("Selector is ambiguous: {selector}"),
|
|
serde_json::json!({
|
|
"kind": "selector_ambiguous",
|
|
"selector": selector,
|
|
"mode": mode,
|
|
"candidates": candidates,
|
|
}),
|
|
),
|
|
ResolveResult::Invalid {
|
|
selector,
|
|
mode,
|
|
message,
|
|
} => Response::err_with_data(
|
|
format!("Invalid selector '{selector}': {message}"),
|
|
serde_json::json!({
|
|
"kind": "selector_invalid",
|
|
"selector": selector,
|
|
"mode": mode,
|
|
"message": message,
|
|
}),
|
|
),
|
|
ResolveResult::Match(_) => unreachable!(),
|
|
}
|
|
}
|
|
|
|
fn observe_wait(state: &DaemonState, selector: &str, wait_kind: WaitKind) -> WaitObservation {
|
|
match state.ref_map.resolve(selector) {
|
|
ResolveResult::Match(entry) => {
|
|
let window = entry.to_window_info();
|
|
match wait_kind {
|
|
WaitKind::Window => WaitObservation::Satisfied(window),
|
|
WaitKind::Focus if window.focused => WaitObservation::Satisfied(window),
|
|
WaitKind::Focus => WaitObservation::Retry {
|
|
observation: serde_json::json!({
|
|
"kind": "window_not_focused",
|
|
"window": window,
|
|
}),
|
|
},
|
|
}
|
|
}
|
|
ResolveResult::NotFound { selector, mode } => WaitObservation::Retry {
|
|
observation: serde_json::json!({
|
|
"kind": "selector_not_found",
|
|
"selector": selector,
|
|
"mode": mode,
|
|
}),
|
|
},
|
|
ResolveResult::Ambiguous {
|
|
selector,
|
|
mode,
|
|
candidates,
|
|
} => WaitObservation::Failure(Response::err_with_data(
|
|
format!("Selector is ambiguous: {selector}"),
|
|
serde_json::json!({
|
|
"kind": "selector_ambiguous",
|
|
"selector": selector,
|
|
"mode": mode,
|
|
"candidates": candidates,
|
|
}),
|
|
)),
|
|
ResolveResult::Invalid {
|
|
selector,
|
|
mode,
|
|
message,
|
|
} => WaitObservation::Failure(Response::err_with_data(
|
|
format!("Invalid selector '{selector}': {message}"),
|
|
serde_json::json!({
|
|
"kind": "selector_invalid",
|
|
"selector": selector,
|
|
"mode": mode,
|
|
"message": message,
|
|
}),
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn capture_snapshot(
|
|
state: &mut DaemonState,
|
|
annotate: bool,
|
|
path: Option<String>,
|
|
) -> Result<Snapshot> {
|
|
let windows = refresh_windows(state)?;
|
|
let screenshot_path = path.unwrap_or_else(temp_screenshot_path);
|
|
let screenshot =
|
|
capture_and_save_screenshot(state, &screenshot_path, annotate, Some(&windows))?;
|
|
|
|
Ok(Snapshot {
|
|
screenshot,
|
|
windows,
|
|
})
|
|
}
|
|
|
|
fn capture_and_save_screenshot(
|
|
state: &mut DaemonState,
|
|
path: &str,
|
|
annotate: bool,
|
|
windows: Option<&[WindowInfo]>,
|
|
) -> Result<String> {
|
|
let mut image = state.backend.capture_screenshot()?;
|
|
if annotate {
|
|
let windows = windows.context("Annotated screenshots require current window data")?;
|
|
annotate_screenshot(&mut image, windows);
|
|
}
|
|
image
|
|
.save(path)
|
|
.with_context(|| format!("Failed to save screenshot to {path}"))?;
|
|
Ok(path.to_string())
|
|
}
|
|
|
|
fn temp_screenshot_path() -> String {
|
|
let timestamp = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_millis();
|
|
format!("/tmp/deskctl-{timestamp}.png")
|
|
}
|
|
|
|
fn parse_coords(value: &str) -> Option<(i32, i32)> {
|
|
let parts: Vec<&str> = value.split(',').collect();
|
|
if parts.len() != 2 {
|
|
return None;
|
|
}
|
|
|
|
let x = parts[0].trim().parse().ok()?;
|
|
let y = parts[1].trim().parse().ok()?;
|
|
Some((x, y))
|
|
}
|
|
|
|
impl From<crate::backend::BackendMonitor> for MonitorInfo {
|
|
fn from(value: crate::backend::BackendMonitor) -> Self {
|
|
Self {
|
|
name: value.name,
|
|
x: value.x,
|
|
y: value.y,
|
|
width: value.width,
|
|
height: value.height,
|
|
width_mm: value.width_mm,
|
|
height_mm: value.height_mm,
|
|
primary: value.primary,
|
|
automatic: value.automatic,
|
|
}
|
|
}
|
|
}
|