use axum::extract::{Query, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::server::DevServerState;
#[derive(Debug, Deserialize)]
pub struct ElementQuery {
pub selector: Option<String>,
pub text: Option<String>,
pub role: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct AgentAction {
pub action: ActionType,
pub selector: String,
pub value: Option<String>,
pub coordinates: Option<(f64, f64)>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ActionType {
Click,
DoubleClick,
RightClick,
Type,
Fill,
Clear,
Scroll,
Hover,
Focus,
Select,
}
#[derive(Debug, Serialize)]
pub struct DomSnapshot {
pub html: String,
pub url: String,
pub title: String,
pub timestamp: String,
}
#[derive(Debug, Serialize)]
pub struct ElementInfo {
pub selector: String,
pub tag: String,
pub text: String,
pub attributes: HashMap<String, String>,
pub visible: bool,
pub enabled: bool,
pub role: Option<String>,
pub label: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ActionResult {
pub success: bool,
pub error: Option<String>,
pub side_effects: Vec<String>,
}
pub async fn get_dom(
State(state): State<DevServerState>,
) -> impl IntoResponse {
let snapshot = state.last_dom_snapshot.read().await;
let html = snapshot.clone().unwrap_or_else(|| {
r#"<!DOCTYPE html>
<html>
<head><title>rdesktop</title></head>
<body>
<p>No DOM snapshot available yet. Make sure the app is loaded in the browser.</p>
<p>The bridge script will send DOM updates automatically.</p>
</body>
</html>"#
.to_string()
});
let dom = DomSnapshot {
html,
url: "http://localhost".to_string(),
title: "rdesktop App".to_string(),
timestamp: timestamp(),
};
Json(dom).into_response()
}
pub async fn query_elements(
State(state): State<DevServerState>,
Query(query): Query<ElementQuery>,
) -> impl IntoResponse {
let snapshot = state.last_dom_snapshot.read().await;
let elements: Vec<ElementInfo> = if let Some(ref html) = *snapshot {
find_elements(html, &query)
} else {
vec![]
};
Json(serde_json::json!({
"query": {
"selector": query.selector,
"text": query.text,
"role": query.role,
},
"count": elements.len(),
"elements": elements,
}))
.into_response()
}
pub async fn execute_action(
State(_state): State<DevServerState>,
Json(action): Json<AgentAction>,
) -> impl IntoResponse {
tracing::info!(
action = ?action.action,
selector = %action.selector,
"Agent action received"
);
let result = ActionResult {
success: true,
error: None,
side_effects: vec![format!("Action {:?} on '{}' queued", action.action, action.selector)],
};
Json(result).into_response()
}
pub async fn get_state(
State(state): State<DevServerState>,
) -> impl IntoResponse {
let app_state = state.last_app_state.read().await;
match app_state.as_ref() {
Some(state) => Json(state.clone()).into_response(),
None => Json(serde_json::json!({
"message": "No application state available yet.",
"hint": "Use fetch('/__rdesktop__/state', { method: 'POST', body: JSON.stringify(state) }) from your app."
}))
.into_response(),
}
}
pub async fn send_ipc(
State(_state): State<DevServerState>,
Json(message): Json<serde_json::Value>,
) -> impl IntoResponse {
let cmd = message["cmd"].as_str().unwrap_or("unknown");
let payload = message["payload"].clone();
let id = message["id"].as_str().unwrap_or("0");
tracing::info!(cmd = cmd, "Agent IPC message received");
let response = match cmd {
"greet" => {
let name = payload["name"].as_str().unwrap_or("World");
serde_json::json!({
"id": id,
"success": true,
"data": { "message": format!("Hello, {}!", name) }
})
}
"ping" => {
serde_json::json!({
"id": id,
"success": true,
"data": { "pong": true }
})
}
_ => {
serde_json::json!({
"id": id,
"success": false,
"data": { "error": format!("Unknown command: {}", cmd) }
})
}
};
Json(response).into_response()
}
pub async fn take_screenshot(
State(_state): State<DevServerState>,
) -> impl IntoResponse {
(
StatusCode::NOT_IMPLEMENTED,
Json(serde_json::json!({
"message": "Screenshot not implemented in browser mode.",
"hint": "Use Playwright's page.screenshot() directly."
})),
)
.into_response()
}
fn timestamp() -> String {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
format!("{}", now.as_secs())
}
fn find_elements(html: &str, query: &ElementQuery) -> Vec<ElementInfo> {
let mut elements = vec![];
if let Some(ref selector) = query.selector {
let tag = selector.trim_start_matches('<').trim_end_matches('>');
let open_tag = format!("<{}", tag);
let mut start = 0;
while let Some(pos) = html[start..].find(&open_tag) {
let abs_pos = start + pos;
let end = html[abs_pos..].find('>').unwrap_or(0);
let _tag_content = &html[abs_pos..abs_pos + end + 1];
let close_tag = format!("</{}>", tag);
let text_start = abs_pos + end + 1;
let text = if let Some(text_end) = html[text_start..].find(&close_tag) {
html[text_start..text_start + text_end].trim().to_string()
} else {
String::new()
};
elements.push(ElementInfo {
selector: format!("{}:nth-of-type({})", tag, elements.len() + 1),
tag: tag.to_string(),
text,
attributes: HashMap::new(),
visible: true,
enabled: true,
role: None,
label: None,
});
start = abs_pos + end + 1;
}
}
if let Some(ref text_query) = query.text {
let lower_html = html.to_lowercase();
let lower_query = text_query.to_lowercase();
if lower_html.contains(&lower_query) {
elements.push(ElementInfo {
selector: format!("*:contains(\"{}\")", text_query),
tag: "*".to_string(),
text: text_query.clone(),
attributes: HashMap::new(),
visible: true,
enabled: true,
role: None,
label: None,
});
}
}
elements
}