1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
use std::time::Duration;
use tastty::AbsolutePosition;
use crate::{ExitStatus, Snapshot};
/// Successful return value of [`Session::wait`](crate::Session::wait).
#[derive(Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct WaitOutcome {
/// Snapshot captured at the moment the condition matched.
pub snapshot: Snapshot,
/// Wall-clock time spent polling before the match.
pub elapsed: Duration,
/// Match metadata for conditions that produce it
/// ([`WaitCondition::text`], [`WaitCondition::regex`],
/// [`WaitCondition::row_regex`], [`WaitCondition::cell_text`]).
/// `None` for conditions that do not (cursor, exit, stable),
/// unless they are part of a [`WaitCondition::any_of`] in which
/// case a bare match carrying [`WaitMatch::condition_index`] is
/// produced so the caller can identify the winning sub-condition.
///
/// [`WaitCondition::text`]: crate::WaitCondition::text
/// [`WaitCondition::regex`]: crate::WaitCondition::regex
/// [`WaitCondition::row_regex`]: crate::WaitCondition::row_regex
/// [`WaitCondition::cell_text`]: crate::WaitCondition::cell_text
/// [`WaitCondition::any_of`]: crate::WaitCondition::any_of
pub wait_match: Option<WaitMatch>,
/// Exit status observed at the moment the wait succeeded, when the
/// child had already terminated. `None` if the child was still
/// running when the wait returned.
///
/// Always populated for a successful [`WaitCondition::exit`]; for
/// other conditions it is populated when the child happened to
/// exit before or during the matching tick (a regex matched on the
/// final flush, an [`WaitCondition::any_of`] selected a non-exit
/// sub-condition while the child died on the same poll, ...).
///
/// [`WaitCondition::exit`]: crate::WaitCondition::exit
/// [`WaitCondition::any_of`]: crate::WaitCondition::any_of
pub exit_status: Option<ExitStatus>,
}
impl WaitOutcome {
/// Borrow the inner [`WaitMatch`].
#[must_use]
pub fn matched(&self) -> Option<&WaitMatch> {
self.wait_match.as_ref()
}
/// Read one capture group from the inner [`WaitMatch`].
///
/// `idx` follows the regex crate's convention: index 0 is the full
/// match, indices 1.. are capturing groups in pattern order.
/// Returns `None` for non-regex conditions, for indices outside the
/// capture list, and for capturing groups that did not participate
/// in the match.
#[must_use]
pub fn capture(&self, idx: usize) -> Option<&str> {
self.wait_match.as_ref().and_then(|m| m.capture(idx))
}
/// Index of the winning sub-condition when the outer condition was
/// a [`WaitCondition::any_of`]. `None` for flat conditions and for
/// waits that produced no match at all.
///
/// [`WaitCondition::any_of`]: crate::WaitCondition::any_of
#[must_use]
pub fn winning_index(&self) -> Option<usize> {
self.wait_match.as_ref().and_then(|m| m.condition_index)
}
/// Borrow the observed [`ExitStatus`], if the child had exited.
///
/// See [`Self::exit_status`] for population semantics.
#[must_use]
pub fn exit_status(&self) -> Option<&ExitStatus> {
self.exit_status.as_ref()
}
}
/// Match metadata attached to a [`WaitOutcome`].
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct WaitMatch {
/// Buffer-stable [absolute](AbsolutePosition) position where the match
/// starts. The row is monotonic across the entire history of the buffer,
/// so it remains valid after scrolling, including for matches that have
/// scrolled out of the viewport via
/// [`RegexCondition::include_scrollback`]. The column is the character
/// index within that row (not necessarily the cell column for wide
/// characters), or the cell column for cell-text conditions.
///
/// `None` for cursor/exit/stable bare matches reported via
/// [`WaitCondition::any_of`], and for the rare case where the match's
/// row could not be resolved to an [`AbsolutePosition`] (e.g. a viewport
/// row that is outside the current viewport bounds).
///
/// Map back to the current viewport coordinate when needed via
/// [`tastty::Screen::absolute_to_visible`]; that returns `None` when
/// the row has scrolled off-screen, which is the right semantics for a
/// caller drawing only the visible viewport.
///
/// [`RegexCondition::include_scrollback`]: crate::RegexCondition::include_scrollback
/// [`WaitCondition::any_of`]: crate::WaitCondition::any_of
pub position: Option<AbsolutePosition>,
/// Capture groups, indexed in regex order. Index 0 is the full match.
/// Empty for non-regex conditions.
pub captures: Vec<Option<String>>,
/// Index of the winning sub-condition when the outer condition is a
/// [`WaitCondition::any_of`], or `None` for flat conditions. Always
/// populated for `any_of` matches, including when the winning
/// sub-condition would not produce a [`WaitMatch`] on its own (e.g.
/// [`WaitCondition::exit`]).
///
/// [`WaitCondition::any_of`]: crate::WaitCondition::any_of
/// [`WaitCondition::exit`]: crate::WaitCondition::exit
pub condition_index: Option<usize>,
/// Full text of the line containing the match start, taken verbatim
/// from the buffer the wait engine was scanning (visible rows, or
/// retained scrollback rows followed by visible rows when the
/// condition opted into scrollback via
/// [`RegexCondition::include_scrollback`]). Trailing spaces from
/// empty cells are preserved. `None` for cursor/exit/stable matches,
/// which are not line-bound.
///
/// [`RegexCondition::include_scrollback`]: crate::RegexCondition::include_scrollback
pub matched_line: Option<String>,
/// Lines preceding [`Self::matched_line`] in the scanned buffer, in
/// display order (oldest first). For visible-only scans this starts
/// at the topmost visible row; for `include_scrollback` regex scans
/// it starts at the oldest retained scrollback row. Empty for
/// matches on the first row, for cursor/exit/stable matches, and
/// for any condition whose match position could not be resolved to
/// a line index.
pub preceding_lines: Vec<String>,
}
impl WaitMatch {
/// Read one capture group by index.
///
/// Index 0 is the full match; indices 1.. are capturing groups in
/// pattern order. Returns `None` for indices outside the capture
/// list (non-regex conditions report an empty
/// [`Self::captures`]) and for capturing groups that did not
/// participate in the match.
#[must_use]
pub fn capture(&self, idx: usize) -> Option<&str> {
self.captures.get(idx).and_then(Option::as_deref)
}
}