use std::collections::HashMap;
use std::fmt;
use std::io;
use std::time::Duration;
use anyhow::{anyhow, bail, Context, Result};
use serde::{Deserialize, Deserializer, Serialize};
use super::{util, Asciicast, Event, EventData, Header, Version};
use crate::tty::TtyTheme;
use crate::util::Quantizer;
#[derive(Deserialize)]
struct V3Header {
version: u8,
term: V3Term,
timestamp: Option<u64>,
idle_time_limit: Option<f64>,
command: Option<String>,
title: Option<String>,
env: Option<HashMap<String, String>>,
}
#[derive(Deserialize)]
struct V3Term {
cols: u16,
rows: u16,
#[serde(rename = "type")]
type_: Option<String>,
version: Option<String>,
theme: Option<V3Theme>,
}
#[derive(Deserialize, Serialize, Clone)]
struct V3Theme {
#[serde(deserialize_with = "deserialize_color")]
fg: RGB8,
#[serde(deserialize_with = "deserialize_color")]
bg: RGB8,
#[serde(deserialize_with = "deserialize_palette")]
palette: V3Palette,
}
#[derive(Clone)]
struct RGB8(rgb::RGB8);
#[derive(Clone)]
struct V3Palette(Vec<RGB8>);
#[derive(Debug, Deserialize)]
struct V3Event {
#[serde(deserialize_with = "util::deserialize_time")]
time: Duration,
#[serde(deserialize_with = "deserialize_code")]
code: V3EventCode,
data: String,
}
#[derive(PartialEq, Debug)]
enum V3EventCode {
Output,
Input,
Resize,
Marker,
Exit,
Other(char),
}
pub struct Parser {
header: V3Header,
prev_time: Duration,
}
pub fn open(header_line: &str) -> Result<Parser> {
let header = serde_json::from_str::<V3Header>(header_line)?;
if header.version != 3 {
bail!("not an asciicast v3 file")
}
Ok(Parser {
header,
prev_time: Duration::from_micros(0),
})
}
impl Parser {
pub fn parse<'a, I: Iterator<Item = io::Result<String>> + Send + 'a>(
mut self,
lines: I,
) -> Asciicast<'a> {
let term_theme = self.header.term.theme.as_ref().map(|t| t.into());
let header = Header {
term_cols: self.header.term.cols,
term_rows: self.header.term.rows,
term_type: self.header.term.type_.clone(),
term_version: self.header.term.version.clone(),
term_theme,
timestamp: self.header.timestamp,
idle_time_limit: self.header.idle_time_limit,
command: self.header.command.clone(),
title: self.header.title.clone(),
env: self.header.env.clone(),
};
let events = Box::new(lines.filter_map(move |line| self.parse_line(line)));
Asciicast {
version: Version::Three,
header,
events,
}
}
fn parse_line(&mut self, line: io::Result<String>) -> Option<Result<Event>> {
match line {
Ok(line) => {
if line.is_empty() || line.starts_with('#') {
None
} else {
Some(self.parse_event(line))
}
}
Err(e) => Some(Err(e.into())),
}
}
fn parse_event(&mut self, line: String) -> Result<Event> {
let event = serde_json::from_str::<V3Event>(&line).context("asciicast v3 parse error")?;
let data = match event.code {
V3EventCode::Output => EventData::Output(event.data),
V3EventCode::Input => EventData::Input(event.data),
V3EventCode::Resize => match event.data.split_once('x') {
Some((cols, rows)) => {
let cols: u16 = cols
.parse()
.map_err(|e| anyhow!("invalid cols value in resize event: {e}"))?;
let rows: u16 = rows
.parse()
.map_err(|e| anyhow!("invalid rows value in resize event: {e}"))?;
EventData::Resize(cols, rows)
}
None => {
bail!("invalid size value in resize event");
}
},
V3EventCode::Marker => EventData::Marker(event.data),
V3EventCode::Exit => EventData::Exit(event.data.parse()?),
V3EventCode::Other(c) => EventData::Other(c, event.data),
};
let time = self.prev_time + event.time;
self.prev_time = time;
Ok(Event { time, data })
}
}
fn deserialize_code<'de, D>(deserializer: D) -> Result<V3EventCode, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
use V3EventCode::*;
let value: &str = Deserialize::deserialize(deserializer)?;
match value {
"o" => Ok(Output),
"i" => Ok(Input),
"r" => Ok(Resize),
"m" => Ok(Marker),
"x" => Ok(Exit),
"" => Err(Error::custom("missing event code")),
s => Ok(Other(s.chars().next().unwrap())),
}
}
pub struct V3Encoder {
prev_time: Duration,
time_quantizer: Quantizer,
}
impl V3Encoder {
pub fn new() -> Self {
Self {
prev_time: Duration::from_micros(0),
time_quantizer: Quantizer::new(1_000_000),
}
}
pub fn header(&mut self, header: &Header) -> Vec<u8> {
let header: V3Header = header.into();
let mut data = serde_json::to_string(&header).unwrap().into_bytes();
data.push(b'\n');
data
}
pub fn event(&mut self, event: &Event) -> Vec<u8> {
let mut data = self.serialize_event(event).into_bytes();
data.push(b'\n');
data
}
fn serialize_event(&mut self, event: &Event) -> String {
use EventData::*;
let (code, data) = match &event.data {
Output(data) => ('o', self.to_json_string(data)),
Input(data) => ('i', self.to_json_string(data)),
Resize(cols, rows) => ('r', self.to_json_string(&format!("{cols}x{rows}"))),
Marker(data) => ('m', self.to_json_string(data)),
Exit(data) => ('x', self.to_json_string(&data.to_string())),
Other(code, data) => (*code, self.to_json_string(data)),
};
let dt = event.time - self.prev_time;
self.prev_time = event.time;
let dt = Duration::from_nanos(self.time_quantizer.next(dt.as_nanos()) as u64);
format!(
"[{}, {}, {}]",
format_duration(dt),
self.to_json_string(&code.to_string()),
data,
)
}
fn to_json_string(&self, s: &str) -> String {
serde_json::to_string(s).unwrap()
}
}
fn format_duration(duration: Duration) -> String {
let time_ms = duration.as_millis();
let secs = time_ms / 1_000;
let millis = time_ms % 1_000;
format!("{}.{}", secs, format!("{:03}", millis))
}
impl serde::Serialize for V3Header {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
let mut len = 2;
if self.timestamp.is_some() {
len += 1;
}
if self.idle_time_limit.is_some() {
len += 1;
}
if self.command.is_some() {
len += 1;
}
if self.title.is_some() {
len += 1;
}
if self.env.as_ref().is_some_and(|env| !env.is_empty()) {
len += 1;
}
let mut map = serializer.serialize_map(Some(len))?;
map.serialize_entry("version", &3)?;
map.serialize_entry("term", &self.term)?;
if let Some(timestamp) = self.timestamp {
map.serialize_entry("timestamp", ×tamp)?;
}
if let Some(limit) = self.idle_time_limit {
map.serialize_entry("idle_time_limit", &limit)?;
}
if let Some(command) = &self.command {
map.serialize_entry("command", &command)?;
}
if let Some(title) = &self.title {
map.serialize_entry("title", &title)?;
}
if let Some(env) = &self.env {
if !env.is_empty() {
map.serialize_entry("env", &env)?;
}
}
map.end()
}
}
impl serde::Serialize for V3Term {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
let mut len = 2;
if self.type_.is_some() {
len += 1;
}
if self.version.is_some() {
len += 1;
}
if self.theme.is_some() {
len += 1;
}
let mut map = serializer.serialize_map(Some(len))?;
map.serialize_entry("cols", &self.cols)?;
map.serialize_entry("rows", &self.rows)?;
if let Some(type_) = &self.type_ {
map.serialize_entry("type", &type_)?;
}
if let Some(version) = &self.version {
map.serialize_entry("version", &version)?;
}
if let Some(theme) = &self.theme {
map.serialize_entry("theme", &theme)?;
}
map.end()
}
}
fn deserialize_color<'de, D>(deserializer: D) -> Result<RGB8, D::Error>
where
D: Deserializer<'de>,
{
let value: &str = Deserialize::deserialize(deserializer)?;
parse_hex_color(value).ok_or(serde::de::Error::custom("invalid hex triplet"))
}
fn parse_hex_color(rgb: &str) -> Option<RGB8> {
if rgb.len() != 7 {
return None;
}
let r = u8::from_str_radix(&rgb[1..3], 16).ok()?;
let g = u8::from_str_radix(&rgb[3..5], 16).ok()?;
let b = u8::from_str_radix(&rgb[5..7], 16).ok()?;
Some(RGB8(rgb::RGB8::new(r, g, b)))
}
fn deserialize_palette<'de, D>(deserializer: D) -> Result<V3Palette, D::Error>
where
D: Deserializer<'de>,
{
let value: &str = Deserialize::deserialize(deserializer)?;
let mut colors: Vec<RGB8> = value.split(':').filter_map(parse_hex_color).collect();
let len = colors.len();
if len == 8 {
colors.extend_from_within(..);
} else if len != 16 {
return Err(serde::de::Error::custom("expected 8 or 16 hex triplets"));
}
Ok(V3Palette(colors))
}
impl serde::Serialize for RGB8 {
fn serialize<S>(&self, serializer: S) -> std::prelude::v1::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl fmt::Display for RGB8 {
fn fmt(&self, f: &mut std::fmt::Formatter) -> fmt::Result {
write!(f, "#{:0>2x}{:0>2x}{:0>2x}", self.0.r, self.0.g, self.0.b)
}
}
impl serde::Serialize for V3Palette {
fn serialize<S>(&self, serializer: S) -> std::prelude::v1::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let palette = self
.0
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join(":");
serializer.serialize_str(&palette)
}
}
impl From<&Header> for V3Header {
fn from(header: &Header) -> Self {
V3Header {
version: 3,
term: V3Term {
cols: header.term_cols,
rows: header.term_rows,
type_: header.term_type.clone(),
version: header.term_version.clone(),
theme: header.term_theme.as_ref().map(|t| t.into()),
},
timestamp: header.timestamp,
idle_time_limit: header.idle_time_limit,
command: header.command.clone(),
title: header.title.clone(),
env: header.env.clone(),
}
}
}
impl From<&TtyTheme> for V3Theme {
fn from(tty_theme: &TtyTheme) -> Self {
let palette = tty_theme.palette.iter().copied().map(RGB8).collect();
V3Theme {
fg: RGB8(tty_theme.fg),
bg: RGB8(tty_theme.bg),
palette: V3Palette(palette),
}
}
}
impl From<&V3Theme> for TtyTheme {
fn from(tty_theme: &V3Theme) -> Self {
let palette = tty_theme.palette.0.iter().map(|c| c.0).collect();
TtyTheme {
fg: tty_theme.fg.0,
bg: tty_theme.bg.0,
palette,
}
}
}
#[cfg(test)]
mod tests {
use super::format_duration;
use std::time::Duration;
#[test]
fn format_time() {
assert_eq!(format_duration(Duration::from_millis(0)), "0.000");
assert_eq!(format_duration(Duration::from_millis(666)), "0.666");
assert_eq!(format_duration(Duration::from_millis(1000)), "1.000");
assert_eq!(format_duration(Duration::from_millis(12345)), "12.345");
}
}