use asciicast::{Entry, EventType, Header};
use console::style;
use failure::Error;
use serde::Deserialize;
use serde_json::{from_str, to_string};
use simplelog::{Config, TermLogger, TerminalMode};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::process::exit;
use structopt::StructOpt;
use structopt_flags::{LogLevel, Verbose};
#[derive(Deserialize, Debug)]
struct ScenarioHeader {
#[serde(default = "default_step")]
step: f64,
#[serde(default = "default_weight")]
width: u32,
#[serde(default = "default_height")]
height: u32,
}
fn default_step() -> f64 {
0.10
}
fn default_weight() -> u32 {
77
}
fn default_height() -> u32 {
20
}
fn print_entry(entry: Entry) -> Result<(), Error> {
println!("{}", to_string(&entry)?);
Ok(())
}
fn clear_terminal(time: &mut f64, step: &f64) -> Result<(), Error> {
*time += 3.0 * step;
print_entry(Entry {
time: *time,
event_type: EventType::Output,
event_data: "\r\x1b[2J\r\x1b[H".to_string(),
})?;
Ok(())
}
fn echo_typing(time: &mut f64, step: &f64, line_raw: &str, nl: bool) -> Result<(), Error> {
let mut bright_applied = false;
for char in line_raw.to_string().chars() {
*time += step;
if char == '#' {
print_entry(Entry {
time: *time,
event_type: EventType::Output,
event_data: "\x1b[1m".to_string(),
})?;
bright_applied = true;
}
print_entry(Entry {
time: *time,
event_type: EventType::Output,
event_data: char.to_string(),
})?;
}
if bright_applied {
print_entry(Entry {
time: *time,
event_type: EventType::Output,
event_data: "\x1b[0m".to_string(),
})?;
}
if nl {
*time += 3.0 * step;
print_entry(Entry {
time: *time,
event_type: EventType::Output,
event_data: "\r\n".to_string(),
})?;
}
Ok(())
}
fn echo_console_line(time: &mut f64, step: &f64, prompt: &str, line: &str) -> Result<(), Error> {
*time += step;
let prompt_line = if prompt != "" {
format!("\x1b[32m{}\x1b[0m$ ", prompt)
} else {
"$ ".to_string()
};
print_entry(Entry {
time: *time,
event_type: EventType::Output,
event_data: prompt_line,
})?;
*time += 3.0 * step;
echo_typing(time, step, line, true)?;
Ok(())
}
#[derive(Debug, StructOpt)]
#[structopt(name = "verbose", about = "Show logs in stderr.")]
struct Cli {
#[structopt(flatten)]
verbose: Verbose,
scenario_file: String,
}
fn main() -> Result<(), Error> {
let cli = Cli::from_args();
let log_level = cli.verbose.get_level_filter();
TermLogger::init(
log_level, Config::default(), TerminalMode::Stderr, )?;
if !Path::new(&cli.scenario_file).exists() {
println!(
"{} scenario file `{}` does not exist!",
style("ERROR:").red(),
cli.scenario_file
);
exit(1);
}
let f = File::open(cli.scenario_file)?;
let reader = BufReader::new(f);
let mut time = 0.0;
let mut step = 0.10;
for (index, maybe_line) in reader.lines().enumerate() {
let line = maybe_line?;
if index == 0 && line.starts_with("#! ") {
let header: ScenarioHeader = from_str(&line[3..])?;
step = header.step;
let asciicast_header = Header {
version: 2,
width: header.height,
height: header.width,
timestamp: None,
duration: None,
idle_time_limit: None,
command: None,
title: None,
env: None,
};
println!("{}", to_string(&asciicast_header)?);
} else if line.starts_with("#") {
continue;
} else if line.starts_with("$ ") {
echo_console_line(&mut time, &step, "", &line[2..])?;
} else if line.starts_with("(nix-shell) $ ") {
echo_console_line(&mut time, &step, "(nix-shell) ", &line[14..])?;
} else if line.starts_with("--") {
clear_terminal(&mut time, &step)?;
} else if line.trim() == "" {
time += 3.0 * step;
} else {
if line.contains("#") {
let mut parts: Vec<&str> = line.splitn(2, '#').collect();
parts.insert(1, "\x1b[1m#");
parts.push("\x1b[0m");
let styled = parts.join("");
print_entry(Entry {
time: time,
event_type: EventType::Output,
event_data: format!("{}\r\n", styled),
})?;
} else {
print_entry(Entry {
time: time,
event_type: EventType::Output,
event_data: format!("{}\r\n", line),
})?;
}
}
}
Ok(())
}