1#[cfg(target_os = "windows")]
18pub use windows_impl::*;
19
20#[cfg(target_os = "windows")]
21mod windows_impl {
22 use std::thread;
23 use std::time::{Duration, Instant};
24
25 use uiautomation::controls::ControlType;
26 use uiautomation::inputs::Mouse;
27 use uiautomation::patterns::UIInvokePattern;
28 use uiautomation::types::{Point, TreeScope, UIProperty};
29 use uiautomation::variants::Variant;
30 use uiautomation::{UIAutomation, UIElement};
31 use windows::Win32::UI::WindowsAndMessaging::{
32 FindWindowExW, GetForegroundWindow, GetWindowThreadProcessId,
33 };
34
35 use crate::process::get_process_name;
36 use crate::util::{find_named_timeout, invoke_or_click};
37
38 #[derive(Debug, Clone)]
40 pub struct TaskbarButton {
41 pub name: String,
43 pub display_name: String,
45 pub window_count: u32,
47 pub pid: u32,
49 pub cx: i32,
51 pub cy: i32,
52 pub win11: bool,
55 pub element: UIElement,
57 }
58
59 pub fn list_taskbar_buttons() -> Vec<TaskbarButton> {
66 let Ok(auto) = UIAutomation::new_direct() else {
67 return vec![];
68 };
69 let Ok(root) = auto.get_root_element() else {
70 return vec![];
71 };
72
73 if let Some(buttons) = collect_buttons_win10(&auto, &root) {
76 if !buttons.is_empty() {
77 return buttons;
78 }
79 }
80 collect_buttons_win11(&auto, &root).unwrap_or_default()
81 }
82
83 fn collect_buttons_win10(auto: &UIAutomation, root: &UIElement) -> Option<Vec<TaskbarButton>> {
87 let name_cond = auto
88 .create_property_condition(
89 UIProperty::Name,
90 Variant::from("Running applications"),
91 None,
92 )
93 .ok()?;
94 let toolbar = root.find_first(TreeScope::Descendants, &name_cond).ok()?;
95 let btn_cond = button_or_split_or_menu(auto)?;
96 let buttons = toolbar.find_all(TreeScope::Children, &btn_cond).ok()?;
97 Some(to_taskbar_buttons(buttons, false))
98 }
99
100 fn collect_buttons_win11(auto: &UIAutomation, root: &UIElement) -> Option<Vec<TaskbarButton>> {
108 let frame_cond = auto
109 .create_property_condition(
110 UIProperty::AutomationId,
111 Variant::from("TaskbarFrame"),
112 None,
113 )
114 .ok()?;
115 let frame = root.find_first(TreeScope::Descendants, &frame_cond).ok()?;
116
117 let btn_cond = button_or_split_or_menu(auto)?;
118 let children = frame.find_all(TreeScope::Children, &btn_cond).ok()?;
119
120 let system_names: &[&str] = &["start", "search", "task view", "chat", "copilot"];
121
122 let app_buttons: Vec<UIElement> = children
123 .into_iter()
124 .filter(|el| {
125 let name = el.get_name().unwrap_or_default().to_lowercase();
126 !name.is_empty()
127 && !system_names.iter().any(|s| name == *s)
128 && !name.starts_with("widgets")
129 })
130 .collect();
131
132 Some(to_taskbar_buttons(app_buttons, true))
133 }
134
135 fn to_taskbar_buttons(elements: Vec<UIElement>, win11: bool) -> Vec<TaskbarButton> {
138 elements
139 .iter()
140 .filter_map(|btn| {
141 let name = btn.get_name().unwrap_or_default();
142 if name.is_empty() {
143 return None;
144 }
145 let rect = btn.get_bounding_rectangle().ok()?;
146 if rect.get_width() == 0 && rect.get_height() == 0 {
148 return None;
149 }
150 let cx = rect.get_left() + rect.get_width() / 2;
151 let cy = rect.get_top() + rect.get_height() / 2;
152 let display_name = strip_app_suffixes(&name);
153 let window_count = parse_window_count(&name);
154 let pid = btn.get_process_id().unwrap_or(0);
155 Some(TaskbarButton {
156 name,
157 display_name,
158 window_count,
159 pid,
160 cx,
161 cy,
162 win11,
163 element: btn.clone(),
164 })
165 })
166 .collect()
167 }
168
169 fn button_or_split_or_menu(auto: &UIAutomation) -> Option<uiautomation::core::UICondition> {
170 let c_btn = auto
171 .create_property_condition(
172 UIProperty::ControlType,
173 Variant::from(ControlType::Button as i32),
174 None,
175 )
176 .ok()?;
177 let c_split = auto
178 .create_property_condition(
179 UIProperty::ControlType,
180 Variant::from(ControlType::SplitButton as i32),
181 None,
182 )
183 .ok()?;
184 let c_menu = auto
185 .create_property_condition(
186 UIProperty::ControlType,
187 Variant::from(ControlType::MenuItem as i32),
188 None,
189 )
190 .ok()?;
191 let or1 = auto.create_or_condition(c_btn, c_split).ok()?;
192 auto.create_or_condition(or1, c_menu).ok()
193 }
194
195 pub fn focus_app_by_name(
200 button: &str,
201 process_name: &str,
202 window_index: usize,
203 ) -> Result<(), String> {
204 let button_lower = button.to_lowercase();
205
206 let buttons = list_taskbar_buttons();
207 if buttons.is_empty() {
208 return Err("taskbar not found or has no running app buttons".into());
209 }
210
211 let btn = buttons
212 .iter()
213 .find(|b| b.display_name.to_lowercase() == button_lower)
214 .ok_or_else(|| {
215 format!(
216 "no taskbar button named '{button}' \
217 (buttons visible: {})",
218 buttons
219 .iter()
220 .map(|b| b.display_name.as_str())
221 .collect::<Vec<_>>()
222 .join(", ")
223 )
224 })?;
225
226 Mouse::new()
228 .click(Point::new(btn.cx, btn.cy))
229 .map_err(|e| format!("taskbar click failed: {e}"))?;
230
231 let picker_timeout = Duration::from_secs(2);
234 if btn.win11 {
235 pick_win11_thumbnail(window_index, picker_timeout);
236 } else {
237 pick_win10_task_switcher(window_index, picker_timeout);
238 }
239
240 let process_lower = process_name.to_lowercase();
242 let deadline = Instant::now() + Duration::from_secs(2);
243 loop {
244 let fg_pid = unsafe {
245 let fg = GetForegroundWindow();
246 let mut pid = 0u32;
247 GetWindowThreadProcessId(fg, Some(&mut pid));
248 pid
249 };
250 let fg_proc = get_process_name(fg_pid as i32)
251 .unwrap_or_default()
252 .to_lowercase();
253 if fg_proc == process_lower {
254 return Ok(());
255 }
256 if Instant::now() >= deadline {
257 break;
258 }
259 thread::sleep(Duration::from_millis(50));
260 }
261
262 Err(format!(
263 "focus not confirmed: process '{process_name}' is not in the foreground"
264 ))
265 }
266
267 fn pick_win10_task_switcher(index: usize, timeout: Duration) -> bool {
268 let Ok(auto) = UIAutomation::new_direct() else {
269 return false;
270 };
271 let Ok(root) = auto.get_root_element() else {
272 return false;
273 };
274 let Some(switcher) = find_named_timeout(&auto, &root, "Task Switcher", timeout) else {
275 return false;
276 };
277 let Ok(item_cond) = auto.create_property_condition(
278 UIProperty::ControlType,
279 Variant::from(ControlType::ListItem as i32),
280 None,
281 ) else {
282 return false;
283 };
284 let Ok(items) = switcher.find_all(TreeScope::Descendants, &item_cond) else {
285 return false;
286 };
287 let target = items.get(index).or_else(|| items.iter().next());
288 if let Some(el) = target {
289 let _ = invoke_or_click(el);
290 true
291 } else {
292 false
293 }
294 }
295
296 fn pick_win11_thumbnail(index: usize, timeout: Duration) {
300 use windows::Win32::UI::Input::KeyboardAndMouse::{
301 INPUT, INPUT_KEYBOARD, KEYBD_EVENT_FLAGS, KEYBDINPUT, KEYEVENTF_KEYUP, SendInput,
302 VK_RETURN, VK_RIGHT, VK_TAB,
303 };
304
305 let class_wide: Vec<u16> = "Xaml_WindowedPopupClass\0".encode_utf16().collect();
307 let title_wide: Vec<u16> = "PopupHost\0".encode_utf16().collect();
308 let deadline = Instant::now() + timeout;
309 loop {
310 let found = unsafe {
311 FindWindowExW(
312 None,
313 None,
314 windows::core::PCWSTR(class_wide.as_ptr()),
315 windows::core::PCWSTR(title_wide.as_ptr()),
316 )
317 };
318 if found.is_ok_and(|h| !h.is_invalid()) {
319 break;
320 }
321 if Instant::now() >= deadline {
322 return; }
324 thread::sleep(Duration::from_millis(50));
325 }
326
327 fn key(vk: windows::Win32::UI::Input::KeyboardAndMouse::VIRTUAL_KEY, up: bool) -> INPUT {
328 INPUT {
329 r#type: INPUT_KEYBOARD,
330 Anonymous: windows::Win32::UI::Input::KeyboardAndMouse::INPUT_0 {
331 ki: KEYBDINPUT {
332 wVk: vk,
333 wScan: 0,
334 dwFlags: if up {
335 KEYEVENTF_KEYUP
336 } else {
337 KEYBD_EVENT_FLAGS(0)
338 },
339 time: 0,
340 dwExtraInfo: 0,
341 },
342 },
343 }
344 }
345
346 let mut inputs: Vec<INPUT> = Vec::new();
347 for _ in 0..2 {
349 inputs.push(key(VK_TAB, false));
350 inputs.push(key(VK_TAB, true));
351 }
352 for _ in 0..index {
354 inputs.push(key(VK_RIGHT, false));
355 inputs.push(key(VK_RIGHT, true));
356 }
357 inputs.push(key(VK_RETURN, false));
359 inputs.push(key(VK_RETURN, true));
360
361 unsafe {
362 SendInput(&inputs, std::mem::size_of::<INPUT>() as i32);
363 }
364 }
365
366 pub fn show_desktop() -> Result<(), String> {
369 let Ok(auto) = UIAutomation::new_direct() else {
370 return Err("UIAutomation init failed".into());
371 };
372 let Ok(root) = auto.get_root_element() else {
373 return Err("could not get desktop root".into());
374 };
375 let Ok(name_cond) =
376 auto.create_property_condition(UIProperty::Name, Variant::from("Show desktop"), None)
377 else {
378 return Err("could not create name condition".into());
379 };
380 let btn = root
381 .find_first(TreeScope::Descendants, &name_cond)
382 .map_err(|_| "Show desktop button not found".to_string())?;
383 btn.get_pattern::<UIInvokePattern>()
384 .and_then(|p| p.invoke())
385 .map_err(|e| format!("Show desktop invoke failed: {e}"))
386 }
387
388 fn strip_app_suffixes(name: &str) -> String {
400 let name = name.strip_suffix(" pinned").unwrap_or(name);
402 if let Some(pos) = name.rfind(" - ") {
404 let suffix = &name[pos + 3..];
405 if suffix.ends_with("running window") || suffix.ends_with("running windows") {
406 return name[..pos].to_string();
407 }
408 }
409 name.to_string()
410 }
411
412 pub fn parse_window_count(name: &str) -> u32 {
419 let was_pinned_only = name.ends_with(" pinned") && !name.contains(" running window");
420 let name = name.strip_suffix(" pinned").unwrap_or(name);
422 if let Some(pos) = name.rfind(" - ") {
423 let suffix = &name[pos + 3..];
424 if suffix.ends_with("running window") || suffix.ends_with("running windows") {
425 if let Some(n_str) = suffix.splitn(2, ' ').next() {
426 if let Ok(n) = n_str.parse::<u32>() {
427 return n;
428 }
429 }
430 }
431 }
432 if was_pinned_only { 0 } else { 1 }
434 }
435}
436
437#[cfg(not(target_os = "windows"))]
440#[derive(Debug, Clone)]
441pub struct TaskbarButton {
442 pub name: String,
443 pub display_name: String,
444 pub window_count: u32,
445 pub pid: u32,
446 pub cx: i32,
447 pub cy: i32,
448 pub win11: bool,
449}
450
451#[cfg(not(target_os = "windows"))]
452pub fn list_taskbar_buttons() -> Vec<TaskbarButton> {
453 vec![]
454}
455
456#[cfg(not(target_os = "windows"))]
457pub fn parse_window_count(_name: &str) -> u32 {
458 0
459}
460
461#[cfg(not(target_os = "windows"))]
462pub fn focus_app_by_name(
463 _button: &str,
464 _process_name: &str,
465 _window_index: usize,
466) -> Result<(), String> {
467 Err("ListTaskbar / FocusApp is only supported on Windows".into())
468}
469
470#[cfg(not(target_os = "windows"))]
471pub fn show_desktop() -> Result<(), String> {
472 Err("ShowDesktop is only supported on Windows".into())
473}