Skip to main content

allwright_surface_desktop/
lib.rs

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