use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::element::{Element, ElementData};
use crate::error::{Error, Result};
use crate::event_provider::Subscription;
use crate::locator::Locator;
use crate::provider::Provider;
use crate::role::Role;
use crate::selector::{
AttrFilter, Combinator, MatchOp, RoleMatch, Selector, SelectorSegment, SimpleSelector,
};
fn role_named(role: Role, name: &str) -> Selector {
Selector {
segments: vec![SelectorSegment {
combinator: Combinator::Root,
simple: SimpleSelector {
role: Some(RoleMatch::Normalized(role)),
filters: vec![AttrFilter {
attr: "name".to_string(),
op: MatchOp::Exact,
value: name.to_string(),
}],
nth: None,
},
}],
}
}
const LOOKUP_POLL_INTERVAL: Duration = Duration::from_millis(100);
fn poll_lookup<F>(timeout: Duration, mut attempt: F) -> Result<App>
where
F: FnMut() -> Result<App>,
{
let start = Instant::now();
loop {
match attempt() {
Ok(app) => return Ok(app),
Err(e @ Error::SelectorNotMatched { .. }) => {
if start.elapsed() >= timeout {
return Err(e);
}
}
Err(e) => return Err(e),
}
std::thread::sleep(LOOKUP_POLL_INTERVAL);
}
}
pub struct App {
pub name: String,
pub pid: Option<u32>,
pub data: ElementData,
provider: Arc<dyn Provider>,
}
impl App {
pub fn by_name_with(provider: Arc<dyn Provider>, name: &str) -> Result<Self> {
let app_selector = role_named(Role::Application, name);
let results = provider.find_elements(None, &app_selector, Some(1), Some(0))?;
if let Some(data) = results.into_iter().next() {
return Ok(Self::from_data(provider, data));
}
let win_selector = role_named(Role::Window, name);
let results = provider.find_elements(None, &win_selector, Some(1), Some(0))?;
let data = results
.into_iter()
.next()
.ok_or_else(|| Error::SelectorNotMatched {
selector: format!(r#"application[name="{}"]"#, name),
})?;
Ok(Self::from_data(provider, data))
}
pub fn by_name_with_timeout(
provider: Arc<dyn Provider>,
name: &str,
timeout: Duration,
) -> Result<Self> {
poll_lookup(timeout, || Self::by_name_with(Arc::clone(&provider), name))
}
pub fn by_pid_with(provider: Arc<dyn Provider>, pid: u32) -> Result<Self> {
for role in ["application", "window"] {
let selector = Selector::parse(role)?;
let results = provider.find_elements(None, &selector, None, Some(0))?;
if let Some(data) = results.into_iter().find(|d| d.pid == Some(pid)) {
return Ok(Self::from_data(provider, data));
}
}
Err(Error::SelectorNotMatched {
selector: format!("application with pid={}", pid),
})
}
pub fn by_pid_with_timeout(
provider: Arc<dyn Provider>,
pid: u32,
timeout: Duration,
) -> Result<Self> {
poll_lookup(timeout, || Self::by_pid_with(Arc::clone(&provider), pid))
}
pub fn list_with(provider: Arc<dyn Provider>) -> Result<Vec<Self>> {
let mut apps = Vec::new();
let mut seen_pids = std::collections::HashSet::new();
for role in ["application", "window"] {
let selector = Selector::parse(role)?;
let results = provider.find_elements(None, &selector, None, Some(0))?;
for d in results {
if let Some(pid) = d.pid {
if !seen_pids.insert(pid) {
continue; }
}
apps.push(Self::from_data(Arc::clone(&provider), d));
}
}
Ok(apps)
}
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 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::selector::{matches_simple, Combinator};
use serde_json::json;
#[test]
fn role_named_preserves_literal_name_with_special_chars() {
let name = r#"My "Weird" App ]["#;
let sel = role_named(Role::Application, name);
assert_eq!(sel.segments.len(), 1);
assert_eq!(sel.segments[0].combinator, Combinator::Root);
let simple = &sel.segments[0].simple;
assert_eq!(simple.filters.len(), 1);
assert_eq!(simple.filters[0].attr, "name");
assert_eq!(simple.filters[0].value, name);
}
#[test]
fn role_named_matches_element_with_quoted_name() {
let name = r#"Name"With"Quote"#;
let data = ElementData {
role: Role::Application,
name: Some(name.to_string()),
value: None,
description: None,
bounds: None,
actions: vec![],
states: crate::element::StateSet::default(),
numeric_value: None,
min_value: None,
max_value: None,
stable_id: None,
pid: Some(1),
raw: std::collections::HashMap::from([("app_name".to_string(), json!(name))]),
handle: 0,
};
let sel = role_named(Role::Application, name);
assert!(matches_simple(&data, &sel.segments[0].simple));
}
}