use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TerminalResize {
pub cols: u16,
pub rows: u16,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TerminalKey {
Backspace,
CtrlC,
CtrlD,
Down,
Enter,
Escape,
Left,
Right,
Tab,
Up,
Raw(String),
}
impl TerminalKey {
pub(crate) fn sequence(&self) -> &str {
match self {
Self::Backspace => "\u{7f}",
Self::CtrlC => "\u{3}",
Self::CtrlD => "\u{4}",
Self::Down => "\u{1b}[B",
Self::Enter => "\r",
Self::Escape => "\u{1b}",
Self::Left => "\u{1b}[D",
Self::Right => "\u{1b}[C",
Self::Tab => "\t",
Self::Up => "\u{1b}[A",
Self::Raw(sequence) => sequence,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TerminalInteraction {
pub after: Option<String>,
pub after_regex: Option<String>,
pub idle_duration: Duration,
pub text: Option<String>,
pub key: Option<TerminalKey>,
pub resize: Option<TerminalResize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TerminalArtifact {
Transcript,
Frames,
Cast,
Snapshot,
Recording,
Gif,
}
impl TerminalArtifact {
pub const ALL: [Self; 6] = [
Self::Transcript,
Self::Frames,
Self::Cast,
Self::Snapshot,
Self::Recording,
Self::Gif,
];
pub fn name(self) -> &'static str {
match self {
Self::Transcript => "transcript",
Self::Frames => "frames",
Self::Cast => "cast",
Self::Snapshot => "snapshot",
Self::Recording => "recording",
Self::Gif => "gif",
}
}
pub fn file_name(self) -> &'static str {
match self {
Self::Transcript => "transcript.txt",
Self::Frames => "frames.json",
Self::Cast => "session.cast",
Self::Snapshot => "snapshot.svg",
Self::Recording => "recording.svg",
Self::Gif => "recording.gif",
}
}
}
impl FromStr for TerminalArtifact {
type Err = TerminalCaptureError;
fn from_str(name: &str) -> Result<Self, Self::Err> {
Self::ALL
.into_iter()
.find(|artifact| artifact.name() == name)
.ok_or_else(|| {
let expected = Self::ALL.map(Self::name).join(", ");
TerminalCaptureError::new(
format!("unknown terminal artifact {name:?}; expected one of {expected}"),
None,
)
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TerminalTraceEvent {
InteractionScheduled { interaction_index: usize },
InteractionApplied { interaction_index: usize },
Timeout { timeout_milliseconds: u128 },
Output { data: String },
Exit {
exit_code: i32,
signal: Option<String>,
},
}
impl TerminalTraceEvent {
pub fn kind(&self) -> &'static str {
match self {
Self::InteractionScheduled { .. } => "interaction-scheduled",
Self::InteractionApplied { .. } => "interaction-applied",
Self::Timeout { .. } => "timeout",
Self::Output { .. } => "output",
Self::Exit { .. } => "exit",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TerminalTrace {
pub time: f64,
pub event: TerminalTraceEvent,
}
#[derive(Clone)]
pub struct TerminalTraceCallback(Arc<dyn Fn(&TerminalTrace) + Send + Sync>);
impl TerminalTraceCallback {
pub fn new(callback: impl Fn(&TerminalTrace) + Send + Sync + 'static) -> Self {
Self(Arc::new(callback))
}
pub(crate) fn call(&self, trace: &TerminalTrace) {
(self.0)(trace);
}
}
impl fmt::Debug for TerminalTraceCallback {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("TerminalTraceCallback")
}
}
#[derive(Debug, Clone)]
pub struct TerminalCaptureOptions {
pub file: String,
pub args: Vec<String>,
pub cwd: Option<PathBuf>,
pub env: HashMap<String, String>,
pub env_remove: Vec<String>,
pub env_clear: bool,
pub cols: u16,
pub rows: u16,
pub aspect_ratio: Option<f64>,
pub settle_duration: Duration,
pub frames_on_write: bool,
pub recording: bool,
pub interactions: Vec<TerminalInteraction>,
pub stop_marker: Option<String>,
pub stop_marker_grace: Duration,
pub timeout: Option<Duration>,
pub artifact_directory: Option<PathBuf>,
pub artifacts: Option<Vec<TerminalArtifact>>,
pub on_trace: Option<TerminalTraceCallback>,
}
impl Default for TerminalCaptureOptions {
fn default() -> Self {
Self {
file: String::new(),
args: Vec::new(),
cwd: None,
env: HashMap::new(),
env_remove: Vec::new(),
env_clear: false,
cols: 80,
rows: 24,
aspect_ratio: None,
settle_duration: Duration::from_millis(35),
frames_on_write: false,
recording: true,
interactions: Vec::new(),
stop_marker: None,
stop_marker_grace: Duration::from_millis(250),
timeout: Some(Duration::from_secs(30)),
artifact_directory: None,
artifacts: None,
on_trace: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TerminalCursor {
pub x: u16,
pub y: u16,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TerminalFrame {
pub time: f64,
pub cols: u16,
pub rows: u16,
pub cursor: TerminalCursor,
pub alternate: bool,
pub lines: Vec<String>,
pub screen: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AsciicastHeader {
pub version: u8,
pub width: u16,
pub height: u16,
pub timestamp: i64,
pub env: HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AsciicastEvent {
pub time: f64,
pub code: String,
pub data: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Asciicast {
pub header: AsciicastHeader,
pub events: Vec<AsciicastEvent>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TerminalCapture {
pub exit_code: i32,
pub signal: Option<String>,
pub output: String,
pub transcript: String,
pub frames: Vec<TerminalFrame>,
pub interaction_count: usize,
pub asciicast: Asciicast,
}
#[derive(Debug)]
pub struct TerminalCaptureError {
message: String,
partial: Option<Box<TerminalCapture>>,
}
impl TerminalCaptureError {
pub(crate) fn new(message: impl Into<String>, partial: Option<TerminalCapture>) -> Self {
Self {
message: message.into(),
partial: partial.map(Box::new),
}
}
pub fn partial_capture(&self) -> Option<&TerminalCapture> {
self.partial.as_deref()
}
}
impl fmt::Display for TerminalCaptureError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)
}
}
impl std::error::Error for TerminalCaptureError {}