tastty-driver 0.1.0

Terminal automation driver built on tastty
use std::time::{Duration, Instant};

use tastty::Terminal;

use crate::snapshot::snapshot_from_screen;
use crate::wait::condition::WaitCondition;
use crate::wait::error::WaitError;
use crate::wait::outcome::WaitOutcome;
use crate::wait::output::OutputNotifier;
use crate::wait::probe::{CompiledCondition, Probe, probe};
use crate::{ExitStatus, Session, Snapshot};

pub(crate) fn wait(
    session: &Session,
    condition: WaitCondition,
    timeout: Duration,
) -> Result<WaitOutcome, WaitError> {
    wait_blocking(
        &session.terminal_handle(),
        &session.output_notifier(),
        condition,
        timeout,
    )
}

/// Tick wait is bounded by `min(poll, deadline - now)` so a silent
/// child exit is detected within one poll interval even when no
/// parser tick fires.
pub(super) fn wait_blocking(
    terminal: &Terminal,
    notifier: &OutputNotifier,
    condition: WaitCondition,
    timeout: Duration,
) -> Result<WaitOutcome, WaitError> {
    let poll = condition.poll;
    let mut compiled = CompiledCondition::compile(&condition)?;
    let start = Instant::now();
    let deadline = start + timeout;
    let exit_short_circuits = compiled.is_exit_or_stable();

    loop {
        let probe_result = terminal.with_screen(|screen| probe(terminal, screen, &mut compiled));
        match probe_result? {
            Probe::NotYet => {}
            Probe::Matched(wait_match) => {
                let snapshot = snapshot(terminal);
                let exit_status = try_wait(terminal).ok().flatten();
                return Ok(WaitOutcome {
                    snapshot,
                    elapsed: start.elapsed(),
                    wait_match,
                    exit_status,
                });
            }
        }

        if Instant::now() >= deadline {
            return Err(WaitError::Timeout {
                condition,
                elapsed: start.elapsed(),
                snapshot: Box::new(snapshot(terminal)),
            });
        }

        // Exit and Stable are passively-satisfiable from a dead process: Exit
        // is the literal condition, Stable's screen-unchanged predicate is
        // trivially true once the child stops emitting. Every other condition
        // is content- or cursor-driven, so a clean child exit without a match
        // is a terminal failure.
        if !exit_short_circuits && let Ok(Some(exit_status)) = try_wait(terminal) {
            return Err(WaitError::ProcessExitedBeforeMatch {
                condition,
                exit_status,
                snapshot: Box::new(snapshot(terminal)),
            });
        }

        let remaining = deadline.saturating_duration_since(Instant::now());
        notifier.wait_tick_blocking(poll.min(remaining));
    }
}

pub(super) fn snapshot(terminal: &Terminal) -> Snapshot {
    terminal.with_screen(snapshot_from_screen)
}

pub(super) fn try_wait(terminal: &Terminal) -> tastty::Result<Option<ExitStatus>> {
    terminal
        .try_wait()
        .map(|maybe| maybe.map(ExitStatus::from_tastty))
}