1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::fmt;
4use std::path::PathBuf;
5use std::str::FromStr;
6use std::sync::Arc;
7use std::time::Duration;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct TerminalResize {
11 pub cols: u16,
12 pub rows: u16,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum TerminalKey {
17 Backspace,
18 CtrlC,
19 CtrlD,
20 Down,
21 Enter,
22 Escape,
23 Left,
24 Right,
25 Tab,
26 Up,
27 Raw(String),
28}
29
30impl TerminalKey {
31 pub(crate) fn sequence(&self) -> &str {
32 match self {
33 Self::Backspace => "\u{7f}",
34 Self::CtrlC => "\u{3}",
35 Self::CtrlD => "\u{4}",
36 Self::Down => "\u{1b}[B",
37 Self::Enter => "\r",
38 Self::Escape => "\u{1b}",
39 Self::Left => "\u{1b}[D",
40 Self::Right => "\u{1b}[C",
41 Self::Tab => "\t",
42 Self::Up => "\u{1b}[A",
43 Self::Raw(sequence) => sequence,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Default)]
49pub struct TerminalInteraction {
50 pub after: Option<String>,
51 pub after_regex: Option<String>,
52 pub idle_duration: Duration,
53 pub text: Option<String>,
54 pub key: Option<TerminalKey>,
55 pub resize: Option<TerminalResize>,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub enum TerminalArtifact {
62 Transcript,
64 Frames,
66 Cast,
68 Snapshot,
70 Recording,
72 Gif,
74}
75
76impl TerminalArtifact {
77 pub const ALL: [Self; 6] = [
79 Self::Transcript,
80 Self::Frames,
81 Self::Cast,
82 Self::Snapshot,
83 Self::Recording,
84 Self::Gif,
85 ];
86
87 pub fn name(self) -> &'static str {
89 match self {
90 Self::Transcript => "transcript",
91 Self::Frames => "frames",
92 Self::Cast => "cast",
93 Self::Snapshot => "snapshot",
94 Self::Recording => "recording",
95 Self::Gif => "gif",
96 }
97 }
98
99 pub fn file_name(self) -> &'static str {
101 match self {
102 Self::Transcript => "transcript.txt",
103 Self::Frames => "frames.json",
104 Self::Cast => "session.cast",
105 Self::Snapshot => "snapshot.svg",
106 Self::Recording => "recording.svg",
107 Self::Gif => "recording.gif",
108 }
109 }
110}
111
112impl FromStr for TerminalArtifact {
113 type Err = TerminalCaptureError;
114
115 fn from_str(name: &str) -> Result<Self, Self::Err> {
116 Self::ALL
117 .into_iter()
118 .find(|artifact| artifact.name() == name)
119 .ok_or_else(|| {
120 let expected = Self::ALL.map(Self::name).join(", ");
121 TerminalCaptureError::new(
122 format!("unknown terminal artifact {name:?}; expected one of {expected}"),
123 None,
124 )
125 })
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
133pub enum TerminalTraceEvent {
134 InteractionScheduled { interaction_index: usize },
136 InteractionApplied { interaction_index: usize },
138 Timeout { timeout_milliseconds: u128 },
140 Output { data: String },
142 Exit {
144 exit_code: i32,
145 signal: Option<String>,
146 },
147}
148
149impl TerminalTraceEvent {
150 pub fn kind(&self) -> &'static str {
152 match self {
153 Self::InteractionScheduled { .. } => "interaction-scheduled",
154 Self::InteractionApplied { .. } => "interaction-applied",
155 Self::Timeout { .. } => "timeout",
156 Self::Output { .. } => "output",
157 Self::Exit { .. } => "exit",
158 }
159 }
160}
161
162#[derive(Debug, Clone, PartialEq)]
164pub struct TerminalTrace {
165 pub time: f64,
166 pub event: TerminalTraceEvent,
167}
168
169#[derive(Clone)]
172pub struct TerminalTraceCallback(Arc<dyn Fn(&TerminalTrace) + Send + Sync>);
173
174impl TerminalTraceCallback {
175 pub fn new(callback: impl Fn(&TerminalTrace) + Send + Sync + 'static) -> Self {
176 Self(Arc::new(callback))
177 }
178
179 pub(crate) fn call(&self, trace: &TerminalTrace) {
180 (self.0)(trace);
181 }
182}
183
184impl fmt::Debug for TerminalTraceCallback {
185 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
186 formatter.write_str("TerminalTraceCallback")
187 }
188}
189
190#[derive(Debug, Clone)]
191pub struct TerminalCaptureOptions {
192 pub file: String,
193 pub args: Vec<String>,
194 pub cwd: Option<PathBuf>,
195 pub env: HashMap<String, String>,
197 pub env_remove: Vec<String>,
199 pub env_clear: bool,
202 pub cols: u16,
203 pub rows: u16,
205 pub aspect_ratio: Option<f64>,
209 pub settle_duration: Duration,
210 pub frames_on_write: bool,
212 pub recording: bool,
214 pub interactions: Vec<TerminalInteraction>,
215 pub stop_marker: Option<String>,
216 pub stop_marker_grace: Duration,
217 pub timeout: Option<Duration>,
220 pub artifact_directory: Option<PathBuf>,
221 pub artifacts: Option<Vec<TerminalArtifact>>,
224 pub on_trace: Option<TerminalTraceCallback>,
226}
227
228impl Default for TerminalCaptureOptions {
229 fn default() -> Self {
230 Self {
231 file: String::new(),
232 args: Vec::new(),
233 cwd: None,
234 env: HashMap::new(),
235 env_remove: Vec::new(),
236 env_clear: false,
237 cols: 80,
238 rows: 24,
239 aspect_ratio: None,
240 settle_duration: Duration::from_millis(35),
241 frames_on_write: false,
242 recording: true,
243 interactions: Vec::new(),
244 stop_marker: None,
245 stop_marker_grace: Duration::from_millis(250),
246 timeout: Some(Duration::from_secs(30)),
247 artifact_directory: None,
248 artifacts: None,
249 on_trace: None,
250 }
251 }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255pub struct TerminalCursor {
256 pub x: u16,
257 pub y: u16,
258}
259
260#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
261pub struct TerminalFrame {
262 pub time: f64,
263 pub cols: u16,
264 pub rows: u16,
265 pub cursor: TerminalCursor,
266 pub alternate: bool,
267 pub lines: Vec<String>,
268 pub screen: Vec<String>,
269}
270
271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
272pub struct AsciicastHeader {
273 pub version: u8,
274 pub width: u16,
275 pub height: u16,
276 pub timestamp: i64,
277 pub env: HashMap<String, String>,
278}
279
280#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
281pub struct AsciicastEvent {
282 pub time: f64,
283 pub code: String,
284 pub data: String,
285}
286
287#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
288pub struct Asciicast {
289 pub header: AsciicastHeader,
290 pub events: Vec<AsciicastEvent>,
291}
292
293#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
294pub struct TerminalCapture {
295 pub exit_code: i32,
296 pub signal: Option<String>,
297 pub output: String,
298 pub transcript: String,
299 pub frames: Vec<TerminalFrame>,
300 pub interaction_count: usize,
301 pub asciicast: Asciicast,
302}
303
304#[derive(Debug)]
305pub struct TerminalCaptureError {
306 message: String,
307 partial: Option<Box<TerminalCapture>>,
308}
309
310impl TerminalCaptureError {
311 pub(crate) fn new(message: impl Into<String>, partial: Option<TerminalCapture>) -> Self {
312 Self {
313 message: message.into(),
314 partial: partial.map(Box::new),
315 }
316 }
317
318 pub fn partial_capture(&self) -> Option<&TerminalCapture> {
319 self.partial.as_deref()
320 }
321}
322
323impl fmt::Display for TerminalCaptureError {
324 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
325 formatter.write_str(&self.message)
326 }
327}
328
329impl std::error::Error for TerminalCaptureError {}