use std::time::Duration;
use serde::Serialize;
use xa11y::{App, AppExt};
use super::types::{AppInfo, AppsOutput, AppsTarget, ComputerApps, FocusedElement, FocusedOutput};
const DEFAULT_TIMEOUT_MS: u64 = 3000;
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum AppsResult {
All(AppsOutput),
Focused(FocusedOutput),
}
pub async fn apps(input: &ComputerApps) -> Result<AppsResult, String> {
let input = input.clone();
tokio::task::spawn_blocking(move || apps_blocking(&input))
.await
.map_err(|e| format!("computer_apps: blocking task failed: {e}"))?
}
fn apps_blocking(input: &ComputerApps) -> Result<AppsResult, String> {
match input.target.unwrap_or_default() {
AppsTarget::All => list_all().map(AppsResult::All),
AppsTarget::Focused => focused_blocking(input).map(AppsResult::Focused),
}
}
fn list_all() -> Result<AppsOutput, String> {
let list =
App::list().map_err(|e| super::errors::render("computer_apps", "list applications", &e))?;
let apps: Vec<AppInfo> = list
.iter()
.map(|app| AppInfo {
name: app.name.clone(),
pid: app.pid,
foreground: app.is_foreground(),
})
.collect();
let count = apps.len();
Ok(AppsOutput { apps, count })
}
fn focused_blocking(input: &ComputerApps) -> Result<FocusedOutput, String> {
let timeout = Duration::from_millis(input.timeout_ms.unwrap_or(DEFAULT_TIMEOUT_MS));
let app = App::foreground(timeout).map_err(|e| {
super::errors::render("computer_apps", "resolve foreground application", &e)
})?;
let focused_element =
find_focused_below(&app.as_element()).map(|(path, leaf)| FocusedElement {
role: leaf.role.to_snake_case().to_string(),
name: leaf.name.clone(),
value: leaf.value.clone(),
path,
});
Ok(FocusedOutput {
app: app.name.clone(),
pid: app.pid,
focused_element,
})
}
pub(crate) const FOCUSED_WALK_MAX_DEPTH: usize = 15;
pub(crate) fn find_focused_below(
root: &xa11y::Element,
) -> Option<(Vec<String>, xa11y::ElementData)> {
let root_role = root.data().role.to_snake_case().to_string();
for child in root.children().ok()? {
if let Some((path, leaf)) = find_focused(&child, 1) {
let mut full = Vec::with_capacity(path.len() + 1);
full.push(root_role);
full.extend(path);
return Some((full, leaf));
}
}
None
}
pub(crate) fn find_focused(
element: &xa11y::Element,
depth: usize,
) -> Option<(Vec<String>, xa11y::ElementData)> {
if depth > FOCUSED_WALK_MAX_DEPTH {
return None;
}
let data = element.data();
let role = data.role.to_snake_case().to_string();
if data.states.focused {
return Some((vec![role], data.clone()));
}
if depth == FOCUSED_WALK_MAX_DEPTH {
return None;
}
for child in element.children().ok()? {
if let Some((mut path, leaf)) = find_focused(&child, depth + 1) {
path.insert(0, role.clone());
return Some((path, leaf));
}
}
None
}