1use allwright_plugin_sdk::{AccessibilitySnapshotInfo, SurfaceFamily, SurfacePluginDescriptor};
2use serde::{Deserialize, Serialize};
3use tokio::time::{Duration, sleep};
4
5pub const SURFACE_ID: &str = "desktop";
6
7#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(rename_all = "snake_case")]
9pub enum DesktopPlatform {
10 Mac,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14pub struct ConnectOptions {
15 pub platform: DesktopPlatform,
16 pub agent_endpoint: Option<String>,
17 pub timeout_ms: Option<u32>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21pub struct LaunchOptions {
22 pub app_id: String,
23 pub terminate_running: bool,
24 pub timeout_ms: Option<u32>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28pub struct DesktopSessionHandle {
29 pub platform: DesktopPlatform,
30 pub endpoint: String,
31 pub session_id: String,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35pub struct DesktopAppSessionHandle {
36 pub app_session_id: String,
37 pub app_id: Option<String>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
41pub struct DesktopAppInfo {
42 pub note: String,
43 pub app_session: DesktopAppSessionHandle,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
47pub struct DesktopConnectInfo {
48 pub host_name: String,
49 pub note: String,
50 pub desktop_session: DesktopSessionHandle,
51 pub initial_app: DesktopAppInfo,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
55pub struct DesktopActionInfo {
56 pub selector: String,
57 pub note: String,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
61pub struct DesktopElementCountInfo {
62 pub selector: String,
63 pub count: u32,
64 pub note: String,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
68pub struct DesktopTextInfo {
69 pub selector: String,
70 pub text: String,
71 pub note: String,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
75pub struct DesktopWaitInfo {
76 pub selector: String,
77 pub visible: bool,
78 pub note: String,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
82pub struct DesktopScreenshotInfo {
83 pub png_data: Vec<u8>,
84 pub note: String,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
88#[serde(tag = "command", rename_all = "snake_case")]
89pub enum DesktopCommand {
90 Connect(ConnectOptions),
91 LaunchApp {
92 desktop_session: DesktopSessionHandle,
93 options: LaunchOptions,
94 },
95 CloseApp {
96 desktop_session: DesktopSessionHandle,
97 app_session: DesktopAppSessionHandle,
98 },
99 ClickElement {
100 desktop_session: DesktopSessionHandle,
101 app_session: DesktopAppSessionHandle,
102 selector: String,
103 timeout_ms: Option<u32>,
104 },
105 CountElements {
106 desktop_session: DesktopSessionHandle,
107 app_session: DesktopAppSessionHandle,
108 selector: String,
109 timeout_ms: Option<u32>,
110 },
111 FocusElement {
112 desktop_session: DesktopSessionHandle,
113 app_session: DesktopAppSessionHandle,
114 selector: String,
115 timeout_ms: Option<u32>,
116 },
117 FillElement {
118 desktop_session: DesktopSessionHandle,
119 app_session: DesktopAppSessionHandle,
120 selector: String,
121 value: String,
122 timeout_ms: Option<u32>,
123 },
124 PressKey {
125 desktop_session: DesktopSessionHandle,
126 app_session: DesktopAppSessionHandle,
127 selector: String,
128 key: String,
129 text: Option<String>,
130 timeout_ms: Option<u32>,
131 },
132 GetText {
133 desktop_session: DesktopSessionHandle,
134 app_session: DesktopAppSessionHandle,
135 selector: String,
136 timeout_ms: Option<u32>,
137 },
138 GetInnerText {
139 desktop_session: DesktopSessionHandle,
140 app_session: DesktopAppSessionHandle,
141 selector: String,
142 timeout_ms: Option<u32>,
143 },
144 WaitForSelector {
145 desktop_session: DesktopSessionHandle,
146 app_session: DesktopAppSessionHandle,
147 selector: String,
148 visible: bool,
149 timeout_ms: Option<u32>,
150 },
151 Screenshot {
152 desktop_session: DesktopSessionHandle,
153 app_session: DesktopAppSessionHandle,
154 timeout_ms: Option<u32>,
155 },
156 AccessibilitySnapshot {
157 desktop_session: DesktopSessionHandle,
158 app_session: DesktopAppSessionHandle,
159 format: String,
160 mode: String,
161 },
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
165#[serde(tag = "result", rename_all = "snake_case")]
166pub enum DesktopCommandResult {
167 Connect(DesktopConnectInfo),
168 LaunchApp(DesktopAppInfo),
169 CloseApp,
170 ClickElement(DesktopActionInfo),
171 CountElements(DesktopElementCountInfo),
172 FocusElement(DesktopActionInfo),
173 FillElement(DesktopActionInfo),
174 PressKey(DesktopActionInfo),
175 GetText(DesktopTextInfo),
176 GetInnerText(DesktopTextInfo),
177 WaitForSelector(DesktopWaitInfo),
178 Screenshot(DesktopScreenshotInfo),
179 AccessibilitySnapshot(AccessibilitySnapshotInfo),
180}
181
182pub fn shared_descriptor() -> SurfacePluginDescriptor {
183 SurfacePluginDescriptor {
184 id: SURFACE_ID,
185 family: SurfaceFamily::Desktop,
186 version: env!("CARGO_PKG_VERSION"),
187 description: "Shared desktop surface abstractions for macOS, Windows, and Linux plugins.",
188 }
189}
190
191pub async fn boot_surface(label: &str, delay_ms: u64) -> String {
192 sleep(Duration::from_millis(delay_ms)).await;
193 format!("{label} ready")
194}
195
196pub async fn boot() -> String {
197 boot_surface("desktop", 20).await
198}
199
200pub fn normalize_selector_for_transport(selector: &str) -> String {
201 let trimmed = selector.trim();
202 for prefix in ["css=", "xpath=", "aw=", "uia="] {
203 if let Some(value) = trimmed.strip_prefix(prefix) {
204 if serde_json::from_str::<String>(value).is_ok() {
205 return trimmed.to_string();
206 }
207 return format!("{prefix}{}", serde_json::to_string(value).unwrap());
208 }
209 }
210 if let Some(value) = trimmed.strip_prefix("id=") {
211 return format!(
212 "css={}",
213 serde_json::to_string(&format!("#{value}")).unwrap()
214 );
215 }
216 format!("css={}", serde_json::to_string(trimmed).unwrap())
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[tokio::test]
224 async fn boots_desktop_runtime() {
225 assert_eq!(boot().await, "desktop ready");
226 }
227
228 #[test]
229 fn command_round_trips() {
230 let command = DesktopCommand::Connect(ConnectOptions {
231 platform: DesktopPlatform::Mac,
232 agent_endpoint: None,
233 timeout_ms: Some(1_000),
234 });
235 let json = serde_json::to_string(&command).unwrap();
236 assert_eq!(
237 serde_json::from_str::<DesktopCommand>(&json).unwrap(),
238 command
239 );
240 }
241}