1use super::{UIElement, UiError};
2
3pub struct Desktop {
5 #[cfg(target_os = "windows")]
6 browser: crate::browser::WindowsBrowser,
7}
8
9impl Desktop {
10 pub fn new() -> Self {
11 Desktop {
12 #[cfg(target_os = "windows")]
13 browser: crate::browser::WindowsBrowser::new(automata_browser::DEFAULT_PORT),
14 }
15 }
16}
17
18impl Default for Desktop {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24#[cfg(not(target_os = "windows"))]
27impl Desktop {
28 pub fn application_windows(&self) -> Result<Vec<UIElement>, UiError> {
29 Err(UiError::Platform("Windows only".into()))
30 }
31
32 pub fn open_application(&self, _exe: &str) -> Result<u32, UiError> {
33 Err(UiError::Platform("Windows only".into()))
34 }
35}
36
37#[cfg(not(target_os = "windows"))]
38impl ui_automata::Desktop for Desktop {
39 type Elem = UIElement;
40 type Browser = crate::element::NoBrowser;
41
42 fn browser(&self) -> &crate::element::NoBrowser {
43 unimplemented!("Browser not supported on this platform")
44 }
45
46 fn application_windows(&self) -> Result<Vec<UIElement>, ui_automata::AutomataError> {
47 Err(ui_automata::AutomataError::Platform("Windows only".into()))
48 }
49
50 fn open_application(&self, _exe: &str) -> Result<u32, ui_automata::AutomataError> {
51 Err(ui_automata::AutomataError::Platform("Windows only".into()))
52 }
53
54 fn foreground_window(&self) -> Option<UIElement> {
55 None
56 }
57
58 fn foreground_hwnd(&self) -> Option<u64> {
59 None
60 }
61}
62
63#[cfg(target_os = "windows")]
66impl Desktop {
67 pub fn application_windows(&self) -> Result<Vec<UIElement>, UiError> {
69 use crate::util::window_pane_condition;
70 use uiautomation::UIAutomation;
71 use uiautomation::types::TreeScope;
72
73 let auto = UIAutomation::new_direct().map_err(|e| UiError::Platform(e.to_string()))?;
74 let root = auto
75 .get_root_element()
76 .map_err(|e| UiError::Platform(e.to_string()))?;
77 let cond = window_pane_condition(&auto).map_err(|e| UiError::Platform(e.to_string()))?;
78 root.find_all(TreeScope::Children, &cond)
79 .map_err(|e| UiError::Platform(e.to_string()))
80 .map(|v| v.into_iter().map(UIElement::new).collect())
81 }
82
83 pub fn open_application(&self, app_id: &str) -> Result<u32, UiError> {
97 if app_id.starts_with('{') && app_id.contains('\\') {
99 if let Ok(pid) = self.activate_via_com(app_id) {
100 return Ok(pid);
101 }
102 let shell_path = format!("shell:AppsFolder\\{app_id}");
103 return self.shell_open(&shell_path);
104 }
105
106 if app_id.contains('!') && !app_id.contains(['/', '\\']) {
109 if let Ok(pid) = self.activate_via_com(app_id) {
110 return Ok(pid);
111 }
112 let shell_path = format!("shell:AppsFolder\\{app_id}");
114 return self.shell_open(&shell_path);
115 }
116
117 let is_bare = !app_id.contains(['/', '\\']) && app_id.to_lowercase().ends_with(".exe");
119 if is_bare {
120 if let Some(full_path) = self.resolve_app_path(app_id) {
122 return self.shell_execute(&full_path);
123 }
124 if let Some(full_path) = self.find_start_menu_app(app_id) {
126 return self.shell_execute(&full_path);
127 }
128 }
129
130 let is_explicit_uri = app_id
134 .find(':')
135 .map(|i| i >= 2 && !app_id[..i].contains(['/', '\\']))
136 .unwrap_or(false);
137 if is_explicit_uri {
138 return self.shell_open(app_id);
139 }
140
141 let is_bare_scheme = !app_id.contains(['.', '/', '\\', ':'])
144 && app_id
145 .chars()
146 .all(|c| c.is_alphanumeric() || c == '-' || c == '_');
147 if is_bare_scheme {
148 let uri = format!("{app_id}:");
149 return self.shell_open(&uri);
150 }
151
152 self.shell_execute(app_id)
154 }
155
156 fn find_start_menu_app(&self, exe_name: &str) -> Option<String> {
159 let needle = exe_name.to_lowercase();
160 for root in self.start_menu_roots() {
161 if let Some(id) = Self::search_start_menu_dir(&root, &needle) {
162 return Some(id);
163 }
164 }
165 None
166 }
167
168 fn search_start_menu_dir(dir: &std::path::Path, needle: &str) -> Option<String> {
172 let entries = std::fs::read_dir(dir).ok()?;
173 for entry in entries.flatten() {
174 let path = entry.path();
175 if path.is_dir() {
176 if let Some(id) = Self::search_start_menu_dir(&path, needle) {
177 return Some(id);
178 }
179 } else if path.extension().and_then(|e| e.to_str()) == Some("lnk") {
180 if let Some(target) = Self::resolve_lnk(&path) {
181 let stem = std::path::Path::new(&target)
182 .file_name()
183 .and_then(|n| n.to_str())
184 .unwrap_or("")
185 .to_lowercase();
186 if stem == needle {
187 return Some(target);
188 }
189 }
190 }
191 }
192 None
193 }
194
195 fn resolve_lnk(lnk: &std::path::Path) -> Option<String> {
197 use windows::Win32::Foundation::HWND;
198 use windows::Win32::System::Com::{
199 CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx,
200 IPersistFile,
201 };
202 use windows::Win32::UI::Shell::{IShellLinkW, ShellLink};
203 use windows::core::{HRESULT, Interface, PCWSTR};
204
205 unsafe {
206 let hr = CoInitializeEx(None, COINIT_MULTITHREADED);
207 if hr.is_err() && hr != HRESULT(0x80010106u32 as i32) {
208 return None;
209 }
210 let shell_link: IShellLinkW =
211 CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER).ok()?;
212 let persist: IPersistFile = shell_link.cast().ok()?;
213 let path_wide: Vec<u16> = lnk
214 .to_string_lossy()
215 .encode_utf16()
216 .chain(std::iter::once(0))
217 .collect();
218 use windows::Win32::System::Com::STGM;
219 persist.Load(PCWSTR(path_wide.as_ptr()), STGM(0)).ok()?;
220 shell_link.Resolve(HWND(std::ptr::null_mut()), 0).ok()?;
221 let mut buf = vec![0u16; 260];
222 shell_link.GetPath(&mut buf, std::ptr::null_mut(), 0).ok()?;
223 let nul = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
224 Some(String::from_utf16_lossy(&buf[..nul]))
225 }
226 }
227
228 fn start_menu_roots(&self) -> Vec<std::path::PathBuf> {
230 use windows::Win32::UI::Shell::{
231 FOLDERID_CommonPrograms, FOLDERID_Programs, KF_FLAG_DEFAULT, SHGetKnownFolderPath,
232 };
233 let mut roots = Vec::new();
234 for folder_id in [&FOLDERID_CommonPrograms, &FOLDERID_Programs] {
235 unsafe {
236 if let Ok(p) = SHGetKnownFolderPath(folder_id, KF_FLAG_DEFAULT, None) {
237 let wide = p.as_wide();
238 let path = String::from_utf16_lossy(wide);
239 roots.push(std::path::PathBuf::from(path));
240 }
241 }
242 }
243 roots
244 }
245
246 fn resolve_app_path(&self, exe: &str) -> Option<String> {
249 use windows::Win32::System::Registry::{HKEY_LOCAL_MACHINE, RRF_RT_REG_SZ, RegGetValueW};
250
251 let subkey: Vec<u16> =
252 format!("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\{exe}\0")
253 .encode_utf16()
254 .collect();
255
256 let mut buf = vec![0u16; 512];
257 let mut len = (buf.len() * 2) as u32;
258
259 let ok = unsafe {
260 RegGetValueW(
261 HKEY_LOCAL_MACHINE,
262 windows::core::PCWSTR(subkey.as_ptr()),
263 None,
264 RRF_RT_REG_SZ,
265 None,
266 Some(buf.as_mut_ptr() as *mut _),
267 Some(&mut len),
268 )
269 };
270
271 if ok.is_ok() {
272 let nul = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
273 Some(String::from_utf16_lossy(&buf[..nul]))
274 } else {
275 None
276 }
277 }
278
279 fn activate_via_com(&self, app_id: &str) -> Result<u32, UiError> {
281 use windows::Win32::System::Com::{
282 CLSCTX_ALL, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx,
283 };
284 use windows::Win32::UI::Shell::{
285 ACTIVATEOPTIONS, ApplicationActivationManager, IApplicationActivationManager,
286 };
287 use windows::core::{HRESULT, HSTRING};
288
289 unsafe {
290 let hr = CoInitializeEx(None, COINIT_MULTITHREADED);
291 if hr.is_err() && hr != HRESULT(0x80010106u32 as i32) {
293 return Err(UiError::Platform(format!("CoInitializeEx failed: {hr}")));
294 }
295
296 let manager: IApplicationActivationManager =
297 CoCreateInstance(&ApplicationActivationManager, None, CLSCTX_ALL)
298 .map_err(|e| UiError::Platform(format!("CoCreateInstance failed: {e}")))?;
299
300 let pid = manager
301 .ActivateApplication(
302 &HSTRING::from(app_id),
303 &HSTRING::from(""),
304 ACTIVATEOPTIONS(0),
305 )
306 .map_err(|e| UiError::Platform(format!("ActivateApplication failed: {e}")))?;
307
308 Ok(pid)
309 }
310 }
311
312 fn shell_open(&self, target: &str) -> Result<u32, UiError> {
316 use windows::Win32::UI::Shell::ShellExecuteW;
317 use windows::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL;
318 use windows::core::PCWSTR;
319
320 let verb: Vec<u16> = "open\0".encode_utf16().collect();
321 let file: Vec<u16> = target.encode_utf16().chain(std::iter::once(0)).collect();
322
323 let result = unsafe {
324 ShellExecuteW(
325 None,
326 PCWSTR(verb.as_ptr()),
327 PCWSTR(file.as_ptr()),
328 PCWSTR::null(),
329 PCWSTR::null(),
330 SW_SHOWNORMAL,
331 )
332 };
333
334 let code = result.0 as usize;
336 if code <= 32 {
337 return Err(UiError::Platform(format!(
338 "ShellExecuteW failed for '{target}': error code {code}"
339 )));
340 }
341 Ok(0) }
343
344 fn shell_execute(&self, target: &str) -> Result<u32, UiError> {
346 use windows::Win32::Foundation::CloseHandle;
347 use windows::Win32::System::Threading::CREATE_NO_WINDOW;
348 use windows::Win32::System::Threading::{
349 CreateProcessW, PROCESS_INFORMATION, STARTUPINFOW,
350 };
351
352 let mut cmdline: Vec<u16> = target.encode_utf16().chain(std::iter::once(0)).collect();
353 let startup_info = STARTUPINFOW {
354 cb: std::mem::size_of::<STARTUPINFOW>() as u32,
355 ..Default::default()
356 };
357 let mut process_info = PROCESS_INFORMATION::default();
358
359 unsafe {
360 CreateProcessW(
361 None,
362 Some(windows::core::PWSTR::from_raw(cmdline.as_mut_ptr())),
363 None,
364 None,
365 false,
366 CREATE_NO_WINDOW,
367 None,
368 None,
369 &startup_info,
370 &mut process_info,
371 )
372 .map_err(|e| UiError::Platform(format!("CreateProcessW failed for '{target}': {e}")))?;
373
374 let pid = process_info.dwProcessId;
375 let _ = CloseHandle(process_info.hProcess);
376 let _ = CloseHandle(process_info.hThread);
377 Ok(pid)
378 }
379 }
380}
381
382#[cfg(target_os = "windows")]
383impl ui_automata::Desktop for Desktop {
384 type Elem = UIElement;
385 type Browser = crate::browser::WindowsBrowser;
386
387 fn browser(&self) -> &crate::browser::WindowsBrowser {
388 &self.browser
389 }
390
391 fn application_windows(&self) -> Result<Vec<UIElement>, ui_automata::AutomataError> {
392 self.application_windows().map_err(Into::into)
393 }
394
395 fn open_application(&self, exe: &str) -> Result<u32, ui_automata::AutomataError> {
396 self.open_application(exe).map_err(Into::into)
397 }
398
399 fn foreground_window(&self) -> Option<UIElement> {
400 use uiautomation::UIAutomation;
401 use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
402
403 let hwnd = unsafe { GetForegroundWindow() };
404 if hwnd.is_invalid() {
405 return None;
406 }
407 let auto = UIAutomation::new_direct().ok()?;
408 auto.element_from_handle(hwnd.into())
409 .ok()
410 .map(UIElement::new)
411 }
412
413 fn foreground_hwnd(&self) -> Option<u64> {
414 use windows::Win32::UI::WindowsAndMessaging::GetForegroundWindow;
415 let hwnd = unsafe { GetForegroundWindow() };
416 if hwnd.is_invalid() {
417 None
418 } else {
419 Some(hwnd.0 as u64)
420 }
421 }
422
423 fn hwnd_z_order(&self) -> Vec<u64> {
424 use windows::Win32::UI::WindowsAndMessaging::{
425 GW_HWNDNEXT, GetTopWindow, GetWindow, IsWindowVisible,
426 };
427
428 let mut result = Vec::new();
429 unsafe {
430 let mut hwnd = GetTopWindow(None);
432 while let Ok(h) = hwnd {
433 if !h.is_invalid() {
434 if IsWindowVisible(h).as_bool() {
435 result.push(h.0 as usize as u64);
436 }
437 hwnd = GetWindow(h, GW_HWNDNEXT);
438 } else {
439 break;
440 }
441 }
442 }
443 result
444 }
445
446 fn tooltip_windows(&self) -> Vec<UIElement> {
447 use uiautomation::UIAutomation;
448 use uiautomation::controls::ControlType;
449 use uiautomation::types::{TreeScope, UIProperty};
450 use uiautomation::variants::Variant;
451
452 let Ok(auto) = UIAutomation::new_direct() else {
453 return vec![];
454 };
455 let Ok(root) = auto.get_root_element() else {
456 return vec![];
457 };
458 let Ok(cond) = auto.create_property_condition(
459 UIProperty::ControlType,
460 Variant::from(ControlType::ToolTip as i32),
461 None,
462 ) else {
463 return vec![];
464 };
465 root.find_all(TreeScope::Children, &cond)
466 .unwrap_or_default()
467 .into_iter()
468 .map(UIElement::new)
469 .collect()
470 }
471}