#!/usr/bin/env Perl

# scriptreplay - play back TypeScript of terminal session
# Author(s):    
#     Joey Hess <[email protected]>
#       Marc Schoechlin <[email protected]>
#       Hendrik Brueckner <[email protected]>
use strict;
use warnings;
use File::Basename;
use Getopt::Long;
use IO::Select;
use POSIX;
use Term::ReadKey;

sub main();
sub show_usage();
sub __exit($;@);
sub open_expr($);

my $progname = fileparse($0, qr/\.[^.]+/);
$SIG{__WARN__} = sub { print STDERR "$progname: $_[0]"; };
$SIG{__DIE__}  = sub { print STDERR "$progname: $_[0]"; __exit 254; };

sub main() {
    my $time_file;
    my $script_file;
    my $accel = 1;

    # parse command line options
    unless (GetOptions("t|timing=s"     => \$time_file,
               "a|accelerate=f" => \$accel,
               "<>"         => sub { $script_file = shift; },
               "h|help"     => sub { show_usage(); exit 0; })) {
        exit 1;

    # check parameters
    die "You need to specify a script file (see also option '-h')\n" unless defined $script_file;
    die "Acceleration factor must be greater than 0\n" unless $accel > 0;

    # open script_file
    open (SCRIPT, open_expr($script_file))
        or die "Cannot open TypeScript file $script_file: $!\n";
    unless (<SCRIPT> =~ /^S.*:.*/i) {
        die "$script_file is not a valid TypeScript from script(1)\n";

    # automatic discovery of a (compressed) time_file
    unless ($time_file) {
        my $tmp = $script_file;
        if ($tmp =~ /(\.(?:bz2|gz|lz|lzma))$/) {
            $tmp =~ s/($1)$/.timing$1/;
        } else {
            $tmp = $tmp . ".timing";
        $time_file = $tmp if -r $tmp;

    # open time_file
    if ($time_file) {
        open (TIMING, open_expr($time_file))
            or die "Cannot open timing data file $time_file: $!\n";

    # enable autoflush
    select STDERR; $| = 1;
    select STDOUT; $| = 1;

    # set up acceleration
    $accel = 1 / $accel;

    # Term::ReadKey setup

    # declare timing and replay block variables
    my $replay_time = 0;            # time of the TypeScript
    my $accel_time  = 0;            # accelerated TypeScript
    my ($block, $oldblock)  = ("", ""); # script block
    my ($delay, $blocksize) = (.005, 1);    # timing parameter

    # install signal handler to reset Term::ReadKey modes
    my $sigaction = POSIX::SigAction->new(sub { __exit 0; },
    POSIX::sigaction(&POSIX::SIGINT,  $sigaction);
    POSIX::sigaction(&POSIX::SIGTERM, $sigaction);

    # use select for timeouts and to monitor stdin activity
    my $select = IO::Select->new();

    # start replaying...
    REPLAY: while (1) {
        if ($time_file) {
            my $timing_line = <TIMING>;
            last REPLAY unless defined $timing_line;
         # Skip this line if this line contains Shell tracing information
         next REPLAY if $timing_line =~ /^\+\+/;
         # This line doesn't seem to a valid timing, somthing is wrong here
         if ($timing_line !~ /([.\d]+)\s+(\d+)/){
            print "ERROR: malformed timing line '".$timing_line."'\n";
            last REPLAY
            ($delay, $blocksize) = ($1, $2);

        # calculate timeout
        my $timeout = $delay * $accel**3;

        # count delays (may vary depending on select)
        $replay_time += $delay;
        $accel_time  += $timeout;

        # Sleep, unless the delay is really tiny. Really tiny delays
        # cannot be accurately done, because the system calls in this
        # loop will have more overhead. The 0.0001 is arbitrary, but
        # works fairly well.
        my @fdset = $select->can_read($timeout) if $timeout > 0.0001;

        # handle read terminal keys
        if (@fdset) {
            my $key = ReadKey(0);

            $accel += 0.1 if $key =~ /-|d/i;
            $accel -= 0.1 if $key =~ /\+|i/i && $accel > 0.11;
            last REPLAY   if $key =~ /q|f/i;
            if ($key =~ /s|p/i) {
                while (ReadKey(0) =~ /c/i) { next; }

        # read TypeScript
        my $cnt;
        unless (defined($cnt = read(SCRIPT, $block, $blocksize))) {
            warn "read failure on script file ($script_file): $!";
            last REPLAY;
        last REPLAY unless $cnt;    # EoF

        print $oldblock;        # write delayed block
        $oldblock = $block;
    print $oldblock;

    close TIMING if $time_file;
    close SCRIPT;

    __exit 0, $replay_time, $accel_time;

sub show_usage() {
    print <<EoUsage;
Usage: $progname [-h|--help]
       $progname [-a <num>] [-t <timing file>] <TypeScript>
       -t, --timing     Path to timing data file.
       -a, --accelerate     Acceleration of TypeScript timing (> 0).
       -h, --help       Print this help, then exit.
Detailed Documentation:
       perldoc $0

sub __exit($;@) {
    my $exitcode = shift();
    my @times    = @_;

    if (@times) {
        printf "\n$progname: %s %5.0f seconds (%2.0f minutes)\n",
            "TypeScript time (normal):", $times[0], $times[0]/60;
        printf "$progname: %s %5.0f seconds (%2.0f minutes)\n",
            "TypeScript time (accel) :",
            $times[1], $times[1]/60;
    exit $exitcode;

sub open_expr($) {
    $_ = shift();

    /\.bz2$/i and return "bzcat $_|";   # block-sorting file compressor
    /\.gz$/i  and return "zcat  $_|";   # Lempel-Ziv coding (LZ77)
    /\.lz(?:ma)?$/i and return "lzcat $_|"; # Lempel-Ziv-Markov chain

    return "<$_";

# start script


=head1 NAME

scriptreplay - play back TypeScript of terminal session


B<scriptreplay> -h|--help

B<scriptreplay> [-a|--accelerate <num>] [-t|--timing <timingfile>] <TypeScript>


B<scriptreplay> replays a TypeScript of a terminal session; optionally, using
timing data to ensure realistic typing and output delays.

The timing data consists of two fields, separated by a space. The first field
indicates how much time elapsed since the previous output. The second field
indicates how many characters were output this time.

I<TypeScript> is the path to the TypeScript file. If the file
I<TypeScript>.timing exists then it is automatically used as timing data
file.  Use parameter B<-t> or B<--timing> to specify an alternative timing data

This version of B<scriptreplay> supports reading of compressed I<TypeScript>
files. If I<timingfile> is not specified, B<scriptreplay> tries to open a
timing data file that uses the same compression algorithm as I<TypeScript>.
The decompression method is determined by examining the file extension of the
I<TypeScript> file. Recognized file extensions of compressed I<TypeScript>
files are: C<bz2>, C<gz>, C<lz> or C<lzma>.

=head2 Controlling the playback

=over 4

=item *

"-" or "d" decreases display speed.

=item *

"+" or "i" increases display speed.

=item *

"s" or "p" pauses the playback; and "c" continues again.

=item *

"f" or "q" stops the playback and exits B<scriptreplay>.


Pressing any other key jumps to the next output (useful if there is no output
activity due to a long delay).

=head1 OPTIONS

=over 8

=item B<-a>, B<--accelerate> I<num>

Accelerates timing by factor I<num>. I<num> must be greater than 0.
A I<num> value less than 1 slows down the playback speed; and a value
greater than 1 increases the playback speed.

=item B<-t>, B<--timing> I<timingfile>

Specify the file path to the timing data file.



=head2 Create a new TypeScript with timing data

 [email protected]:~$ script -t TypeScript 2>TypeScript.timing
 Script started, file is TypeScript
 [email protected]:~$ ls
 [email protected]:~$ exit
 Script done, file is TypeScript

=head2 Replay a TypeScript

 [email protected]:~$ scriptreplay TypeScript
 [email protected]:~$ ls
 [email protected]:~$ exit

 scriptreplay: TypeScript time (normal):    14 seconds ( 0 minutes)
 scriptreplay: TypeScript time (accel) :     1 seconds ( 0 minutes)

=head1 NOTES

The playback might not work properly if the TypeScript contains output from
applications that have been recorded with different termio settings and/or
terminal window sizes.


This program is in the public domain.

=head1 AUTHORS

Joey Hess <[email protected]>

Marc Schoechlin <[email protected]>

Hendrik Brueckner <[email protected]>

=head1 SEE ALSO


# vim: set ai noet ts=8 sw=8 tw=80:
Sergio Llorente