rust_expect/transcript/
format.rs

1//! Transcript format definitions.
2
3use std::time::Duration;
4
5/// A transcript event.
6#[derive(Debug, Clone)]
7pub struct TranscriptEvent {
8    /// Timestamp from start.
9    pub timestamp: Duration,
10    /// Event type.
11    pub event_type: EventType,
12    /// Event data.
13    pub data: Vec<u8>,
14}
15
16/// Event types in a transcript.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum EventType {
19    /// Output from the session.
20    Output,
21    /// Input to the session.
22    Input,
23    /// Window resize.
24    Resize,
25    /// Marker/annotation.
26    Marker,
27}
28
29impl TranscriptEvent {
30    /// Create an output event.
31    #[must_use]
32    pub fn output(timestamp: Duration, data: impl Into<Vec<u8>>) -> Self {
33        Self {
34            timestamp,
35            event_type: EventType::Output,
36            data: data.into(),
37        }
38    }
39
40    /// Create an input event.
41    #[must_use]
42    pub fn input(timestamp: Duration, data: impl Into<Vec<u8>>) -> Self {
43        Self {
44            timestamp,
45            event_type: EventType::Input,
46            data: data.into(),
47        }
48    }
49
50    /// Create a resize event.
51    #[must_use]
52    pub fn resize(timestamp: Duration, cols: u16, rows: u16) -> Self {
53        Self {
54            timestamp,
55            event_type: EventType::Resize,
56            data: format!("{cols}x{rows}").into_bytes(),
57        }
58    }
59
60    /// Create a marker event.
61    #[must_use]
62    pub fn marker(timestamp: Duration, label: &str) -> Self {
63        Self {
64            timestamp,
65            event_type: EventType::Marker,
66            data: label.as_bytes().to_vec(),
67        }
68    }
69}
70
71/// Transcript metadata.
72#[derive(Debug, Clone, Default)]
73pub struct TranscriptMetadata {
74    /// Terminal width.
75    pub width: u16,
76    /// Terminal height.
77    pub height: u16,
78    /// Command that was run.
79    pub command: Option<String>,
80    /// Title for the transcript.
81    pub title: Option<String>,
82    /// Environment info.
83    pub env: std::collections::HashMap<String, String>,
84    /// Start time.
85    pub timestamp: Option<u64>,
86    /// Total duration.
87    pub duration: Option<Duration>,
88}
89
90impl TranscriptMetadata {
91    /// Create new metadata with dimensions.
92    #[must_use]
93    pub fn new(width: u16, height: u16) -> Self {
94        Self {
95            width,
96            height,
97            ..Default::default()
98        }
99    }
100
101    /// Set the command.
102    #[must_use]
103    pub fn with_command(mut self, cmd: impl Into<String>) -> Self {
104        self.command = Some(cmd.into());
105        self
106    }
107
108    /// Set the title.
109    #[must_use]
110    pub fn with_title(mut self, title: impl Into<String>) -> Self {
111        self.title = Some(title.into());
112        self
113    }
114}
115
116/// A complete transcript.
117#[derive(Debug, Clone)]
118pub struct Transcript {
119    /// Metadata.
120    pub metadata: TranscriptMetadata,
121    /// Events.
122    pub events: Vec<TranscriptEvent>,
123}
124
125impl Transcript {
126    /// Create a new transcript.
127    #[must_use]
128    pub const fn new(metadata: TranscriptMetadata) -> Self {
129        Self {
130            metadata,
131            events: Vec::new(),
132        }
133    }
134
135    /// Add an event.
136    pub fn push(&mut self, event: TranscriptEvent) {
137        self.events.push(event);
138    }
139
140    /// Get total duration.
141    #[must_use]
142    pub fn duration(&self) -> Duration {
143        self.events.last().map_or(Duration::ZERO, |e| e.timestamp)
144    }
145
146    /// Get all output as a string.
147    #[must_use]
148    pub fn output_text(&self) -> String {
149        let output: Vec<u8> = self
150            .events
151            .iter()
152            .filter(|e| e.event_type == EventType::Output)
153            .flat_map(|e| e.data.clone())
154            .collect();
155        String::from_utf8_lossy(&output).into_owned()
156    }
157
158    /// Get all input as a string.
159    #[must_use]
160    pub fn input_text(&self) -> String {
161        let input: Vec<u8> = self
162            .events
163            .iter()
164            .filter(|e| e.event_type == EventType::Input)
165            .flat_map(|e| e.data.clone())
166            .collect();
167        String::from_utf8_lossy(&input).into_owned()
168    }
169
170    /// Filter events by type.
171    #[must_use]
172    pub fn filter(&self, event_type: EventType) -> Vec<&TranscriptEvent> {
173        self.events
174            .iter()
175            .filter(|e| e.event_type == event_type)
176            .collect()
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn transcript_events() {
186        let mut transcript = Transcript::new(TranscriptMetadata::new(80, 24));
187        transcript.push(TranscriptEvent::output(
188            Duration::from_millis(100),
189            b"hello",
190        ));
191        transcript.push(TranscriptEvent::input(Duration::from_millis(200), b"world"));
192
193        assert_eq!(transcript.events.len(), 2);
194        assert_eq!(transcript.duration(), Duration::from_millis(200));
195    }
196
197    #[test]
198    fn transcript_output_text() {
199        let mut transcript = Transcript::new(TranscriptMetadata::new(80, 24));
200        transcript.push(TranscriptEvent::output(Duration::ZERO, b"hello "));
201        transcript.push(TranscriptEvent::output(
202            Duration::from_millis(100),
203            b"world",
204        ));
205
206        assert_eq!(transcript.output_text(), "hello world");
207    }
208}