tastty-driver 0.1.0

Terminal automation driver built on tastty
//! Knobs for [`Session::find`](crate::Session::find).

use std::time::Duration;

use tastty::AbsolutePosition;

/// Direction in which [`Session::find`](crate::Session::find) walks
/// the logical-line stream when collecting matches.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)]
pub enum SearchDirection {
    /// Walk oldest row first; matches are returned in reading order.
    #[default]
    Forward,
    /// Walk newest row first; matches are returned in reverse reading
    /// order.
    Backward,
}

/// Knobs for [`Session::find`](crate::Session::find).
///
/// Construct via [`SearchOptions::default`] and chain builder
/// methods to customize. Defaults: forward direction, viewport only,
/// case-sensitive literal matching, no result cap, no start pivot,
/// no deadline, no NFA / DFA cache overrides, no per-line haystack
/// byte cap. The 256 KB NFA cap is applied at
/// [`SearchPattern::regex`](crate::SearchPattern::regex) construction time, not as a
/// [`SearchOptions`] default; the
/// [`SearchOptions::nfa_size_limit`] setter is an override that
/// triggers a per-find recompile from the pattern source.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SearchOptions {
    pub(super) include_scrollback: bool,
    pub(super) direction: SearchDirection,
    pub(super) case_sensitive: bool,
    pub(super) max_results: Option<usize>,
    pub(super) start_from: Option<AbsolutePosition>,
    pub(super) deadline: Option<Duration>,
    pub(super) nfa_size_limit: Option<usize>,
    pub(super) cache_capacity: Option<usize>,
    pub(super) max_logical_line_bytes: Option<usize>,
}

impl Default for SearchOptions {
    fn default() -> Self {
        Self {
            include_scrollback: false,
            direction: SearchDirection::Forward,
            case_sensitive: true,
            max_results: None,
            start_from: None,
            deadline: None,
            nfa_size_limit: None,
            cache_capacity: None,
            max_logical_line_bytes: None,
        }
    }
}

impl SearchOptions {
    /// Walk scrollback rows oldest-first before the live drawing
    /// region. The viewport scroll offset is ignored.
    #[must_use]
    pub fn include_scrollback(mut self) -> Self {
        self.include_scrollback = true;
        self
    }

    /// Walk newest-first; matches are returned in reverse reading order.
    #[must_use]
    pub fn backward(mut self) -> Self {
        self.direction = SearchDirection::Backward;
        self
    }

    /// Fold case for [`SearchPattern::Literal`](crate::SearchPattern::Literal) matches. Has no
    /// effect on regex patterns; regex case folding is governed by
    /// embedded flags such as `(?i)`.
    #[must_use]
    pub fn case_insensitive(mut self) -> Self {
        self.case_sensitive = false;
        self
    }

    /// Cap the number of returned matches at `n`. The cap is applied
    /// after [`SearchOptions::start_from`] filtering and follows the
    /// configured walk direction.
    #[must_use]
    pub fn max_results(mut self, n: usize) -> Self {
        self.max_results = Some(n);
        self
    }

    /// Drop matches whose [`SearchMatch::start`](crate::SearchMatch::start) precedes `pos` in
    /// the configured walk direction. For a forward walk, "precedes"
    /// means strictly less than `pos`; for a backward walk, strictly
    /// greater than. Useful for paginating results or for
    /// "find next from cursor" UX.
    #[must_use]
    pub fn start_from(mut self, pos: AbsolutePosition) -> Self {
        self.start_from = Some(pos);
        self
    }

    /// Cap the wall-clock time spent collecting matches at `budget`.
    ///
    /// A regex against deep scrollback can run for an unbounded time
    /// before yielding all matches. This budget gives the caller a
    /// knob to bound that latency: the engine records the start
    /// instant when [`Session::find`](crate::Session::find) (or
    /// [`Session::find_async`](crate::Session::find_async)) is
    /// invoked, and surfaces
    /// [`SearchError::DeadlineExceeded`](crate::SearchError::DeadlineExceeded)
    /// once `budget` elapses. The clock starts at the find call
    /// boundary, not at [`SearchOptions`] construction; reusing the
    /// same options across multiple finds gives each find its own
    /// fresh budget.
    ///
    /// The check is cooperative and runs once per logical-line scan,
    /// not per byte scanned or per match yielded. Partial matches
    /// collected before expiration are dropped: a truncated result
    /// vector cannot be distinguished from a complete one.
    #[must_use]
    pub fn deadline(mut self, budget: Duration) -> Self {
        self.deadline = Some(budget);
        self
    }

    /// Override the NFA size cap for this find.
    ///
    /// [`SearchPattern::regex`](crate::SearchPattern::regex) already applies a 256 KB cap at
    /// construction time (see
    /// [`DEFAULT_NFA_SIZE_LIMIT`](crate::DEFAULT_NFA_SIZE_LIMIT)),
    /// which is the canonical place the security default lives.
    /// Setting this knob triggers a per-find recompile from the
    /// pattern source with `bytes` as the new cap, useful when the
    /// caller wants to tighten the budget for an individual scan
    /// against untrusted input even after construction succeeded
    /// under the default ceiling. A pattern whose compiled NFA would
    /// exceed `bytes` surfaces [`SearchError::InvalidRegex`](crate::SearchError::InvalidRegex) carrying
    /// the regex engine's compiled-size diagnostic.
    /// To raise the cap above 256 KB at construction time, build the
    /// pattern via [`SearchPattern::regex_with_limit`](crate::SearchPattern::regex_with_limit).
    #[must_use]
    pub fn nfa_size_limit(mut self, bytes: usize) -> Self {
        self.nfa_size_limit = Some(bytes);
        self
    }

    /// Cap the lazy-DFA transition cache at `bytes`.
    ///
    /// The DFA cache is reset when full, which on degenerate
    /// patterns like `[01]*1[01]{N}` can dominate scan time as it
    /// thrashes between rebuilds. Raising `bytes` reduces reset
    /// frequency at the cost of resident memory. The default leaves
    /// the cap unset, which is the regex crate's own default. Set
    /// this when scanning haystacks against patterns prone to DFA
    /// cache pressure.
    #[must_use]
    pub fn cache_capacity(mut self, bytes: usize) -> Self {
        self.cache_capacity = Some(bytes);
        self
    }

    /// Skip logical lines whose haystack would exceed `bytes`.
    ///
    /// "Haystack bytes" measures the joined UTF-8 string built from
    /// the line's populated cells (one cell-text fragment per cell),
    /// not the raw cell count: a wide character contributes multiple
    /// bytes, an empty cell contributes one space byte. Lines past
    /// the cap are skipped wholesale rather than truncated, so the
    /// scanner never emits a match that crosses a half-line
    /// boundary. Useful for bounding worst-case scan cost when the
    /// scrollback may contain unbounded single lines (escape-stripped
    /// log dumps, build output, etc.).
    #[must_use]
    pub fn max_logical_line_bytes(mut self, bytes: usize) -> Self {
        self.max_logical_line_bytes = Some(bytes);
        self
    }
}