mirror of
https://github.com/lahfir/agent-desktop.git
synced 2026-07-26 17:12:15 +00:00
fix: harden macos ax window fallback
This commit is contained in:
parent
a52b7c704a
commit
3b266fdf04
4 changed files with 73 additions and 5 deletions
|
|
@ -166,8 +166,12 @@ fn ax_window_for_app(app_info: &AppInfo) -> Option<WindowInfo> {
|
|||
let window = focused_window_element(&app)
|
||||
.or_else(|| crate::tree::copy_element_attr(&app, "AXMainWindow"))
|
||||
.or_else(|| {
|
||||
crate::tree::copy_ax_array(&app, "AXWindows")
|
||||
.and_then(|windows| windows.into_iter().next())
|
||||
let windows = crate::tree::copy_ax_array(&app, "AXWindows")?;
|
||||
child_bearing_window_index(&windows, |window| {
|
||||
crate::tree::element::count_children(window, None)
|
||||
})
|
||||
.map(|index| windows[index].clone())
|
||||
.or_else(|| windows.into_iter().next())
|
||||
})?;
|
||||
if crate::tree::copy_string_attr(&window, "AXRole").as_deref() != Some("AXWindow") {
|
||||
return None;
|
||||
|
|
@ -195,6 +199,13 @@ fn ax_window_info(
|
|||
}
|
||||
}
|
||||
|
||||
fn child_bearing_window_index<T>(
|
||||
windows: &[T],
|
||||
mut count_children: impl FnMut(&T) -> u32,
|
||||
) -> Option<usize> {
|
||||
windows.iter().position(|window| count_children(window) > 0)
|
||||
}
|
||||
|
||||
type FocusedWindowIdentity = Option<(Option<String>, Option<i64>)>;
|
||||
|
||||
fn focused_window_identity(pid: i32) -> FocusedWindowIdentity {
|
||||
|
|
|
|||
|
|
@ -213,6 +213,23 @@ fn ax_window_info_uses_resolved_app_identity() {
|
|||
assert_eq!(window.id, "w-7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_bearing_window_index_prefers_first_window_with_children() {
|
||||
let windows = [0, 7, 3];
|
||||
|
||||
assert_eq!(
|
||||
child_bearing_window_index(&windows, |count| *count),
|
||||
Some(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn child_bearing_window_index_returns_none_when_all_windows_are_empty() {
|
||||
let windows = [0, 0];
|
||||
|
||||
assert_eq!(child_bearing_window_index(&windows, |count| *count), None);
|
||||
}
|
||||
|
||||
fn app(name: &str, pid: i32) -> AppInfo {
|
||||
AppInfo {
|
||||
name: name.to_string(),
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ pub fn window_element_for(pid: i32, win_title: &str) -> AXElement {
|
|||
if let Some(windows) = copy_ax_array(&app, kAXWindowsAttribute) {
|
||||
for win in &windows {
|
||||
let title = copy_string_attr(win, kAXTitleAttribute);
|
||||
if title.as_deref() == Some(win_title) {
|
||||
if title
|
||||
.as_deref()
|
||||
.is_some_and(|title| window_titles_are_exact_match(title, win_title))
|
||||
{
|
||||
return win.clone();
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +34,7 @@ pub fn window_element_for(pid: i32, win_title: &str) -> AXElement {
|
|||
let title = copy_string_attr(win, kAXTitleAttribute);
|
||||
if title
|
||||
.as_deref()
|
||||
.is_some_and(|t| !t.is_empty() && (t.contains(win_title) || win_title.contains(t)))
|
||||
.is_some_and(|title| window_titles_are_partial_match(title, win_title))
|
||||
{
|
||||
return win.clone();
|
||||
}
|
||||
|
|
@ -49,6 +52,18 @@ pub fn window_element_for(pid: i32, win_title: &str) -> AXElement {
|
|||
app
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn window_titles_are_exact_match(candidate_title: &str, requested_title: &str) -> bool {
|
||||
!candidate_title.is_empty() && !requested_title.is_empty() && candidate_title == requested_title
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn window_titles_are_partial_match(candidate_title: &str, requested_title: &str) -> bool {
|
||||
!candidate_title.is_empty()
|
||||
&& !requested_title.is_empty()
|
||||
&& (candidate_title.contains(requested_title) || requested_title.contains(candidate_title))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn build_subtree(
|
||||
el: &AXElement,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
use super::{child_attributes, redact_secure_value};
|
||||
use super::{
|
||||
child_attributes, redact_secure_value, window_titles_are_exact_match,
|
||||
window_titles_are_partial_match,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_browser_children_use_columns() {
|
||||
|
|
@ -27,3 +30,25 @@ fn test_secure_text_value_is_redacted() {
|
|||
Some("visible".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_title_matching_rejects_empty_titles() {
|
||||
assert!(!window_titles_are_exact_match("", ""));
|
||||
assert!(!window_titles_are_exact_match("Inbox", ""));
|
||||
assert!(!window_titles_are_exact_match("", "Inbox"));
|
||||
assert!(!window_titles_are_partial_match("Inbox", ""));
|
||||
assert!(!window_titles_are_partial_match("", "Inbox"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_title_matching_accepts_exact_and_truncated_titles() {
|
||||
assert!(window_titles_are_exact_match("Inbox", "Inbox"));
|
||||
assert!(window_titles_are_partial_match(
|
||||
"noy4/agent-desktop: Native desktop automation",
|
||||
"noy4/agent-desktop"
|
||||
));
|
||||
assert!(window_titles_are_partial_match(
|
||||
"noy4/agent-desktop: Native desk...",
|
||||
"noy4/agent-desktop: Native desk"
|
||||
));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue