term_transcript/types.rs
1//! Core type definitions.
2
3use std::{borrow::Cow, fmt, io};
4
5use styled_str::{AnsiError, StyledString};
6
7pub(crate) type BoxedError = Box<dyn std::error::Error + Send + Sync>;
8
9/// Errors that can occur when processing terminal output.
10#[derive(Debug)]
11#[non_exhaustive]
12pub enum TermError {
13 /// Ansi escape sequence parsing error.
14 Ansi(AnsiError),
15 /// IO error.
16 Io(io::Error),
17 /// Font embedding error.
18 FontEmbedding(BoxedError),
19}
20
21impl fmt::Display for TermError {
22 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
23 match self {
24 Self::Ansi(err) => write!(formatter, "ANSI escape sequence parsing error: {err}"),
25 Self::Io(err) => write!(formatter, "I/O error: {err}"),
26 Self::FontEmbedding(err) => write!(formatter, "font embedding error: {err}"),
27 }
28 }
29}
30
31impl std::error::Error for TermError {
32 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
33 match self {
34 Self::Ansi(err) => Some(err),
35 Self::Io(err) => Some(err),
36 _ => None,
37 }
38 }
39}
40
41/// Transcript of a user interacting with the terminal.
42#[derive(Debug, Clone, Default)]
43pub struct Transcript {
44 interactions: Vec<Interaction>,
45}
46
47impl Transcript {
48 /// Creates an empty transcript.
49 pub fn new() -> Self {
50 Self::default()
51 }
52
53 /// Returns interactions in this transcript.
54 pub fn interactions(&self) -> &[Interaction] {
55 &self.interactions
56 }
57
58 /// Returns a mutable reference to interactions in this transcript.
59 pub fn interactions_mut(&mut self) -> &mut [Interaction] {
60 &mut self.interactions
61 }
62}
63
64impl Transcript {
65 /// Manually adds a new interaction to the end of this transcript.
66 ///
67 /// This method allows capturing interactions that are difficult or impossible to capture
68 /// using more high-level methods: [`Self::from_inputs()`] or [`Self::capture_output()`].
69 /// The resulting transcript will [render](crate::svg) just fine, but there could be issues
70 /// with [testing](crate::test) it.
71 pub fn add_existing_interaction(&mut self, interaction: Interaction) -> &mut Self {
72 self.interactions.push(interaction);
73 self
74 }
75
76 /// Manually adds a new interaction to the end of this transcript.
77 ///
78 /// This is a shortcut for calling [`Self::add_existing_interaction()`].
79 pub fn add_interaction(
80 &mut self,
81 input: impl Into<UserInput>,
82 output: StyledString,
83 ) -> &mut Self {
84 self.add_existing_interaction(Interaction::new(input, output))
85 }
86}
87
88/// Portable, platform-independent version of [`ExitStatus`] from the standard library.
89///
90/// # Capturing `ExitStatus`
91///
92/// Some shells have means to check whether the input command was executed successfully.
93/// For example, in `sh`-like shells, one can compare the value of `$?` to 0, and
94/// in PowerShell to `True`. The exit status can be captured when creating a [`Transcript`]
95/// by setting a *checker* in [`ShellOptions::with_status_check()`](crate::ShellOptions::with_status_check()):
96///
97/// # Examples
98///
99/// ```
100/// # use term_transcript::{ExitStatus, ShellOptions, Transcript, UserInput};
101/// # fn test_wrapper() -> anyhow::Result<()> {
102/// let options = ShellOptions::default();
103/// let mut options = options.with_status_check("echo $?", |captured| {
104/// // Get the plain text from the styled string. This transform
105/// // is especially important in transcripts captured from PTY
106/// // since they can contain a *wild* amount of escape sequences.
107/// let captured = captured.text();
108/// let code: i32 = captured.trim().parse().ok()?;
109/// Some(ExitStatus(code))
110/// });
111///
112/// let transcript = Transcript::from_inputs(&mut options, [
113/// UserInput::command("echo \"Hello world\""),
114/// UserInput::command("some-non-existing-command"),
115/// ])?;
116/// let status = transcript.interactions()[0].exit_status();
117/// assert!(status.unwrap().is_success());
118/// // The assertion above is equivalent to:
119/// assert_eq!(status, Some(ExitStatus(0)));
120///
121/// let status = transcript.interactions()[1].exit_status();
122/// assert!(!status.unwrap().is_success());
123/// # Ok(())
124/// # }
125/// # // We can compile test in any case, but it successfully executes only on *nix.
126/// # #[cfg(unix)] fn main() { test_wrapper().unwrap() }
127/// # #[cfg(not(unix))] fn main() { }
128/// ```
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
130pub struct ExitStatus(pub i32);
131
132impl ExitStatus {
133 /// Checks if this is the successful status.
134 pub fn is_success(self) -> bool {
135 self.0 == 0
136 }
137}
138
139/// One-time interaction with the terminal.
140#[derive(Debug, Clone)]
141pub struct Interaction {
142 input: UserInput,
143 output: StyledString,
144 exit_status: Option<ExitStatus>,
145}
146
147impl Interaction {
148 /// Creates a new interaction.
149 ///
150 /// Any newlines at the end of the output will be trimmed.
151 pub fn new(input: impl Into<UserInput>, mut output: StyledString) -> Self {
152 while output.text().ends_with('\n') {
153 output.pop();
154 }
155
156 Self {
157 input: input.into(),
158 output,
159 exit_status: None,
160 }
161 }
162
163 /// Sets an exit status for this interaction.
164 pub fn set_exit_status(&mut self, exit_status: Option<ExitStatus>) {
165 self.exit_status = exit_status;
166 }
167
168 /// Assigns an exit status to this interaction.
169 #[must_use]
170 pub fn with_exit_status(mut self, exit_status: ExitStatus) -> Self {
171 self.exit_status = Some(exit_status);
172 self
173 }
174}
175
176impl Interaction {
177 /// Input provided by the user.
178 pub fn input(&self) -> &UserInput {
179 &self.input
180 }
181
182 /// Output to the terminal.
183 pub fn output(&self) -> &StyledString {
184 &self.output
185 }
186
187 /// Sets the output for this interaction.
188 pub fn set_output(&mut self, output: StyledString) {
189 self.output = output;
190 }
191
192 /// Returns exit status of the interaction, if available.
193 pub fn exit_status(&self) -> Option<ExitStatus> {
194 self.exit_status
195 }
196}
197
198/// User input during interaction with a terminal.
199#[derive(Debug, Clone, PartialEq, Eq)]
200#[cfg_attr(feature = "svg", derive(serde::Serialize))]
201pub struct UserInput {
202 text: String,
203 prompt: Option<Cow<'static, str>>,
204 hidden: bool,
205}
206
207impl UserInput {
208 #[cfg(feature = "test")]
209 pub(crate) const EMPTY: Self = Self {
210 text: String::new(),
211 prompt: None,
212 hidden: false,
213 };
214
215 #[cfg(feature = "test")]
216 pub(crate) fn new(text: String) -> Self {
217 Self {
218 prompt: None,
219 text,
220 hidden: false,
221 }
222 }
223
224 #[cfg(feature = "test")]
225 #[must_use]
226 pub(crate) fn with_prompt(mut self, prompt: Option<String>) -> Self {
227 self.prompt = prompt.map(|prompt| match prompt.as_str() {
228 "$" => Cow::Borrowed("$"),
229 ">>>" => Cow::Borrowed(">>>"),
230 "..." => Cow::Borrowed("..."),
231 _ => Cow::Owned(prompt),
232 });
233 self
234 }
235
236 /// Creates a command input.
237 pub fn command(text: impl Into<String>) -> Self {
238 Self {
239 text: text.into(),
240 prompt: Some(Cow::Borrowed("$")),
241 hidden: false,
242 }
243 }
244
245 /// Creates a standalone / starting REPL command input with the `>>>` prompt.
246 pub fn repl(text: impl Into<String>) -> Self {
247 Self {
248 text: text.into(),
249 prompt: Some(Cow::Borrowed(">>>")),
250 hidden: false,
251 }
252 }
253
254 /// Creates a REPL command continuation input with the `...` prompt.
255 pub fn repl_continuation(text: impl Into<String>) -> Self {
256 Self {
257 text: text.into(),
258 prompt: Some(Cow::Borrowed("...")),
259 hidden: false,
260 }
261 }
262
263 /// Returns the prompt part of this input.
264 pub fn prompt(&self) -> Option<&str> {
265 self.prompt.as_deref()
266 }
267
268 /// Marks this input as hidden (one that should not be displayed in the rendered transcript).
269 #[must_use]
270 pub fn hide(mut self) -> Self {
271 self.hidden = true;
272 self
273 }
274
275 /// Checks whether this input is hidden.
276 pub fn is_hidden(&self) -> bool {
277 self.hidden
278 }
279}
280
281/// Returns the command part of the input without the prompt.
282impl AsRef<str> for UserInput {
283 fn as_ref(&self) -> &str {
284 &self.text
285 }
286}
287
288/// Calls [`Self::command()`] on the provided string reference.
289impl From<&str> for UserInput {
290 fn from(command: &str) -> Self {
291 Self::command(command)
292 }
293}