use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::element::{Element, ElementData, TreeNode};
use crate::error::{Diagnosis, Error, Result};
use crate::event_provider::Subscription;
use crate::locator::Locator;
use crate::provider::Provider;
const LOOKUP_POLL_INTERVAL: Duration = Duration::from_millis(100);
const DIAG_APP_LIST_LIMIT: usize = 20;
fn poll_lookup<F, D>(timeout: Duration, mut attempt: F, diagnose: D) -> Result<App>
where
F: FnMut() -> Result<App>,
D: FnOnce() -> Diagnosis,
{
let start = Instant::now();
loop {
match attempt() {
Ok(app) => return Ok(app),
Err(e @ Error::SelectorNotMatched { .. }) => {
if start.elapsed() >= timeout {
return Err(merge_diagnosis(e, diagnose()));
}
}
Err(e) => return Err(e),
}
std::thread::sleep(LOOKUP_POLL_INTERVAL);
}
}
fn merge_diagnosis(err: Error, extra: Diagnosis) -> Error {
let mut d = err.diagnosis().cloned().unwrap_or_default();
if d.condition.is_none() {
d.condition = extra.condition;
}
if d.last_observed.is_none() {
d.last_observed = extra.last_observed;
}
if d.candidates.is_empty() {
d.candidates = extra.candidates;
}
if d.scope.is_none() {
d.scope = extra.scope;
}
err.diagnose(d)
}
fn running_apps_diagnosis(provider: &Arc<dyn Provider>) -> Diagnosis {
let candidates = match provider.list_apps() {
Ok(apps) => {
let total = apps.len();
let mut out: Vec<String> = apps
.iter()
.take(DIAG_APP_LIST_LIMIT)
.map(|a| {
let pid = a.pid.map(|p| format!(" (pid={p})")).unwrap_or_default();
format!("\"{}\"{pid}", a.name.clone().unwrap_or_default())
})
.collect();
if total > DIAG_APP_LIST_LIMIT {
out.push(format!("… (+{} more)", total - DIAG_APP_LIST_LIMIT));
}
out
}
Err(e) => vec![format!("(application enumeration failed: {e})")],
};
Diagnosis {
condition: Some("application discovery".to_string()),
candidates,
..Diagnosis::default()
}
}
pub struct App {
pub name: String,
pub pid: Option<u32>,
pub data: ElementData,
provider: Arc<dyn Provider>,
}
impl App {
pub fn find_with<F>(
provider: Arc<dyn Provider>,
timeout: Duration,
predicate: F,
) -> Result<Self>
where
F: Fn(&ElementData) -> bool,
{
Self::try_find_with(provider, timeout, move |d| Ok(predicate(d)))
}
pub fn try_find_with<F>(
provider: Arc<dyn Provider>,
timeout: Duration,
predicate: F,
) -> Result<Self>
where
F: Fn(&ElementData) -> Result<bool>,
{
Self::find_matching(provider, timeout, predicate, || {
"application matching predicate".to_string()
})
}
fn find_matching<F, D>(
provider: Arc<dyn Provider>,
timeout: Duration,
predicate: F,
describe: D,
) -> Result<Self>
where
F: Fn(&ElementData) -> Result<bool>,
D: Fn() -> String,
{
let diag_provider = Arc::clone(&provider);
poll_lookup(
timeout,
|| {
let apps = provider.list_apps()?;
for data in apps {
if predicate(&data)? {
return Ok(Self::from_data(Arc::clone(&provider), data));
}
}
Err(Error::selector_not_matched(describe()))
},
|| running_apps_diagnosis(&diag_provider),
)
}
pub fn by_name_with(
provider: Arc<dyn Provider>,
name: &str,
timeout: Duration,
) -> Result<Self> {
if name.contains('"') {
return Err(Error::InvalidSelector {
selector: name.to_string(),
message: "app name contains a double quote, which cannot be escaped in the \
selector grammar; use App::find_with with a name predicate instead"
.to_string(),
});
}
Self::find_matching(
provider,
timeout,
|d| Ok(d.name.as_deref() == Some(name)),
|| format!(r#"application[name="{}"]"#, name),
)
}
pub fn by_pid_with(provider: Arc<dyn Provider>, pid: u32, timeout: Duration) -> Result<Self> {
let diag_provider = Arc::clone(&provider);
poll_lookup(
timeout,
|| {
let data = provider.app_by_pid(pid)?;
Ok(Self::from_data(Arc::clone(&provider), data))
},
|| running_apps_diagnosis(&diag_provider),
)
}
pub fn list_with(provider: Arc<dyn Provider>) -> Result<Vec<Self>> {
let datas = provider.list_apps()?;
Ok(datas
.into_iter()
.map(|d| Self::from_data(Arc::clone(&provider), d))
.collect())
}
fn from_data(provider: Arc<dyn Provider>, data: ElementData) -> Self {
let name = data.name.clone().unwrap_or_default();
let pid = data.pid;
Self {
name,
pid,
data,
provider,
}
}
pub fn locator(&self, selector: &str) -> Locator {
Locator::new(
Arc::clone(&self.provider),
Some(self.data.clone()),
selector,
)
}
pub fn subscribe(&self) -> Result<Subscription> {
self.provider.subscribe(&self.data)
}
pub fn children(&self) -> Result<Vec<Element>> {
let children = self.provider.get_children(Some(&self.data))?;
Ok(children
.into_iter()
.map(|d| Element::new(d, Arc::clone(&self.provider)))
.collect())
}
pub fn tree(&self, max_depth: Option<usize>) -> Result<TreeNode> {
self.as_element().tree(max_depth)
}
pub fn dump(&self, max_depth: Option<usize>) -> Result<String> {
self.as_element().dump(max_depth)
}
pub fn as_element(&self) -> Element {
Element::new(self.data.clone(), Arc::clone(&self.provider))
}
pub fn provider(&self) -> &Arc<dyn Provider> {
&self.provider
}
}
impl std::fmt::Display for App {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "application \"{}\"", self.name)
}
}
impl std::fmt::Debug for App {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("App")
.field("name", &self.name)
.field("pid", &self.pid)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mock::build_provider;
use crate::role::Role;
fn mock_app() -> App {
let provider: Arc<dyn Provider> = build_provider();
App::by_name_with(provider, "TestApp", Duration::ZERO)
.expect("TestApp must exist in mock tree")
}
#[test]
fn app_tree_returns_application_root() {
let node = mock_app().tree(None).expect("tree must succeed");
assert_eq!(node.role, "application");
assert_eq!(node.name.as_deref(), Some("TestApp"));
assert!(
!node.children.is_empty(),
"TestApp must have at least one window child"
);
}
#[test]
fn app_tree_max_depth_zero_has_no_children() {
let node = mock_app().tree(Some(0)).expect("tree must succeed");
assert_eq!(node.role, "application");
assert!(node.children.is_empty());
}
#[test]
fn app_tree_max_depth_one_stops_at_direct_children() {
let node = mock_app().tree(Some(1)).expect("tree must succeed");
assert!(!node.children.is_empty());
for child in &node.children {
assert!(
child.children.is_empty(),
"max_depth=1 must stop after direct children"
);
}
}
#[test]
fn app_dump_contains_application_root() {
let s = mock_app().dump(None).expect("dump must succeed");
assert!(
s.contains(r#"application "TestApp""#),
"dump output should include the application root: {s}"
);
}
#[test]
fn app_dump_max_depth_zero_is_one_line() {
let s = mock_app().dump(Some(0)).expect("dump must succeed");
let non_empty: Vec<&str> = s.lines().filter(|l| !l.trim().is_empty()).collect();
assert_eq!(non_empty.len(), 1, "max_depth=0 should be a single line");
assert!(non_empty[0].contains("application"));
}
#[test]
fn app_as_element_is_root() {
let app = mock_app();
let el = app.as_element();
assert_eq!(el.data().role, Role::Application);
assert_eq!(el.data().name.as_deref(), Some("TestApp"));
}
#[test]
fn by_name_with_rejects_double_quote_in_name() {
let provider: Arc<dyn Provider> = build_provider();
let err = App::by_name_with(provider, r#"My "Quoted" App"#, Duration::ZERO)
.expect_err("names containing '\"' must be rejected");
match err {
Error::InvalidSelector { selector, message } => {
assert_eq!(selector, r#"My "Quoted" App"#);
assert!(
message.contains("double quote"),
"message must explain the quote limitation: {message}"
);
assert!(
message.contains("find_with"),
"message must point at the predicate-based alternative: {message}"
);
}
other => panic!("expected InvalidSelector, got: {other:?}"),
}
}
#[test]
fn find_with_matches_by_predicate() {
let provider: Arc<dyn Provider> = build_provider();
let app = App::find_with(provider, Duration::ZERO, |d| {
d.name.as_deref() == Some("TestApp")
})
.expect("predicate must match TestApp in mock tree");
assert_eq!(app.name, "TestApp");
}
#[test]
fn find_with_no_match_returns_selector_not_matched() {
let provider: Arc<dyn Provider> = build_provider();
let err = App::find_with(provider, Duration::ZERO, |_| false)
.expect_err("a never-true predicate must not match any app");
assert!(matches!(err, Error::SelectorNotMatched { .. }));
}
#[test]
fn try_find_with_propagates_predicate_error_and_fails_fast() {
let provider: Arc<dyn Provider> = build_provider();
let start = Instant::now();
let err = App::try_find_with(provider, Duration::from_secs(30), |_| {
Err(Error::Platform {
code: 7,
message: "boom".to_string(),
})
})
.expect_err("a predicate error must propagate, not retry");
assert!(matches!(err, Error::Platform { code: 7, .. }));
assert!(
start.elapsed() < Duration::from_secs(1),
"predicate error must fail fast, not wait out the timeout"
);
}
#[test]
fn try_find_with_ok_false_keeps_polling_then_times_out() {
let provider: Arc<dyn Provider> = build_provider();
let err = App::try_find_with(provider, Duration::ZERO, |_| Ok(false))
.expect_err("an always-Ok(false) predicate must not match");
assert!(matches!(err, Error::SelectorNotMatched { .. }));
}
}