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}
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}