nils-macos-agent 0.7.3

CLI crate for nils-macos-agent in the nils-cli workspace.
Documentation
use crate::backend::AutoAxBackend;
use crate::backend::applescript;
use crate::backend::process::{ProcessRunner, RealProcessRunner};
use std::time::Instant;

use crate::cli::{
    OutputFormat, WaitAppActiveArgs, WaitAxPresentArgs, WaitAxUniqueArgs, WaitSleepArgs,
    WaitWindowPresentArgs,
};
use crate::commands::ax_common::{
    build_selector_from_args, build_target_from_args, evaluate_selector_against_backend,
};
use crate::commands::{emit_json_success, reject_tsv_for_list_only};
use crate::error::CliError;
use crate::model::WaitResult;
use crate::run::ActionPolicy;
use crate::targets::{self, TargetSelector};
use crate::wait;

pub fn run_sleep(format: OutputFormat, args: &WaitSleepArgs) -> Result<(), CliError> {
    let started = Instant::now();
    wait::sleep_ms(args.ms);
    let result = WaitResult {
        condition: "wait.sleep",
        attempts: 1,
        elapsed_ms: started.elapsed().as_millis() as u64,
        terminal_status: "satisfied",
        matched_count: None,
        selector_explain: None,
    };
    emit_wait_result(format, "wait.sleep", result)
}

pub fn run_app_active(format: OutputFormat, args: &WaitAppActiveArgs) -> Result<(), CliError> {
    let runner = RealProcessRunner;
    let probe_timeout_ms = args.timeout_ms.max(2_000);
    let check = || {
        if let Some(app) = args.app.as_deref() {
            applescript::frontmost_app_name(&runner, probe_timeout_ms)
                .map(|frontmost| frontmost.eq_ignore_ascii_case(app))
        } else if let Some(bundle_id) = args.bundle_id.as_deref() {
            applescript::frontmost_bundle_id(&runner, probe_timeout_ms)
                .map(|frontmost| frontmost.eq_ignore_ascii_case(bundle_id))
        } else {
            Ok(false)
        }
    };

    let policy = wait::WaitPolicy::new(args.timeout_ms, args.poll_ms);
    let outcome = wait::wait_until_with_policy("app-active", policy, check)?;
    let result = WaitResult {
        condition: "wait.app-active",
        attempts: outcome.attempts,
        elapsed_ms: outcome.elapsed_ms,
        terminal_status: "satisfied",
        matched_count: None,
        selector_explain: None,
    };
    emit_wait_result(format, "wait.app-active", result)
}

pub fn run_window_present(
    format: OutputFormat,
    args: &WaitWindowPresentArgs,
) -> Result<(), CliError> {
    let selector = TargetSelector {
        window_id: args.window_id,
        active_window: args.active_window,
        app: args.app.clone(),
        window_name: args.window_name.clone(),
    };

    let check = || targets::window_present(&selector);
    let policy = wait::WaitPolicy::new(args.timeout_ms, args.poll_ms);
    let outcome = wait::wait_until_with_policy("window-present", policy, check)?;

    let result = WaitResult {
        condition: "wait.window-present",
        attempts: outcome.attempts,
        elapsed_ms: outcome.elapsed_ms,
        terminal_status: "satisfied",
        matched_count: None,
        selector_explain: None,
    };
    emit_wait_result(format, "wait.window-present", result)
}

pub fn run_ax_present(
    format: OutputFormat,
    args: &WaitAxPresentArgs,
    policy: ActionPolicy,
    runner: &dyn ProcessRunner,
) -> Result<(), CliError> {
    run_ax_selector_wait(
        format,
        "wait.ax-present",
        &args.selector,
        &args.target,
        args.timeout_ms,
        args.poll_ms,
        policy.timeout_ms,
        runner,
        |matched_count| matched_count >= 1,
    )
}

pub fn run_ax_unique(
    format: OutputFormat,
    args: &WaitAxUniqueArgs,
    policy: ActionPolicy,
    runner: &dyn ProcessRunner,
) -> Result<(), CliError> {
    run_ax_selector_wait(
        format,
        "wait.ax-unique",
        &args.selector,
        &args.target,
        args.timeout_ms,
        args.poll_ms,
        policy.timeout_ms,
        runner,
        |matched_count| matched_count == 1,
    )
}

#[allow(clippy::too_many_arguments)]
fn run_ax_selector_wait<F>(
    format: OutputFormat,
    command: &'static str,
    selector_args: &crate::cli::AxSelectorArgs,
    target_args: &crate::cli::AxTargetArgs,
    timeout_ms: u64,
    poll_ms: u64,
    backend_timeout_ms: u64,
    runner: &dyn ProcessRunner,
    predicate: F,
) -> Result<(), CliError>
where
    F: Fn(usize) -> bool,
{
    let selector = build_selector_from_args(selector_args)?;
    let target = build_target_from_args(target_args)?;
    let backend = AutoAxBackend::default();
    let mut last_matched_count = 0usize;
    let mut last_explain = None;

    let policy = wait::WaitPolicy::new(timeout_ms, poll_ms);
    let outcome = wait::wait_until_with_policy(command, policy, || {
        let evaluation = evaluate_selector_against_backend(
            runner,
            &backend,
            &target,
            &selector,
            backend_timeout_ms,
        )?;
        last_matched_count = evaluation.matched_count;
        last_explain = evaluation.explain;
        Ok(predicate(evaluation.matched_count))
    })
    .map_err(|error| {
        error.with_operation(command).with_hint(format!(
            "Last selector match count before timeout: {last_matched_count}"
        ))
    })?;

    let result = WaitResult {
        condition: command,
        attempts: outcome.attempts,
        elapsed_ms: outcome.elapsed_ms,
        terminal_status: "satisfied",
        matched_count: Some(last_matched_count),
        selector_explain: if format == OutputFormat::Json {
            last_explain
        } else {
            None
        },
    };

    emit_wait_result(format, command, result)
}

fn emit_wait_result(
    format: OutputFormat,
    command: &'static str,
    result: WaitResult,
) -> Result<(), CliError> {
    match format {
        OutputFormat::Json => {
            emit_json_success(command, result)?;
        }
        OutputFormat::Text => {
            println!(
                "{}\tattempts={}\telapsed_ms={}\tterminal_status={}",
                command, result.attempts, result.elapsed_ms, result.terminal_status
            );
        }
        OutputFormat::Tsv => {
            return reject_tsv_for_list_only();
        }
    }

    Ok(())
}