App-Textcast

 view release on metacpan or  search on metacpan

lib/App/Textcast.pm  view on Meta::CPAN


The arguments are named, order is not important.

=over 2 

=item * COMMAND => $string - the name of the command to tun in a terminal. You most probably wan to run
I<bash> or I<sh>

=item * OUTPUT_DIRECTORY => $directory_path - Optional - the path to the directory where the textcast is to be 
recorded. This subroutine will create a directory if this option is not set. if this option is set, the directory 
should not exist.

=item * COMPRESS => $boolean - Not implemented

=item * COLUMNS => $integer - Optional - Number of columns in the terminal. The current terminal columns
number is used if this argument is not set.

=item * ROWS => $integer - Optional - Number of rows in the terminal. The current terminal rows number is
used if this argument is not set.

=back

I<Returns> - Nothing

I<Exceptions>

=over 2 

=item * See check_output_directory

=item * see create_vt102_sub_process

=item * disk full error

=back

See I<scripts/record_textcast>.

=cut

my (%arguments) = @_;

my ($terminal_columns, $terminal_rows) = $get_terminal_size->() ;

my $output_directory = check_output_directory($arguments{OUTPUT_DIRECTORY}) ;
my $vt_process = create_vt102_sub_process
				(
				$arguments{COMMAND},
				$arguments{COLUMNS} || $terminal_columns,
				$arguments{ROWS}  || $terminal_rows, 
				) ;

print $CLEAR ;
	
my $previous_time = my $start_time = [gettimeofday]  ;

my ($screenshot_index, $sub_process_ended) = (0, 0) ;

while (not $sub_process_ended) 
	{
	($sub_process_ended, my $screen_diff, my $cursor_x, my $cursor_y) = check_sub_process_output($vt_process) ;
	
	my $now = [gettimeofday] ;
	my $elapsed = tv_interval($previous_time, $now);
	$previous_time = $now ;
	
	my $screenshot_file_name = "$output_directory/$screenshot_index" ;
	
	write_file($screenshot_file_name, $screen_diff) ;
			
	my ($terminal_columns, $terminal_rows) = $get_terminal_size->() ;
		
	append_file 
		(
		"$output_directory/index",
		
		'{'
		. "file => $screenshot_index, "
		. sprintf('delay => %0.3f, ', $elapsed)
		. "cursor_x => $cursor_x, "
		. "cursor_y => $cursor_y, "
		. 'size => ' . length($screen_diff) . ', '
		. "terminal_rows => $terminal_rows, "
		. "terminal_columns => $terminal_columns, "
		. "},\n" 
		) ;
			
	$screenshot_index++ ;
	}

my $record_time = tv_interval($start_time, [gettimeofday]);
printf("record_textcast: $screenshot_index frames in %.02f seconds. Textcast is in '$output_directory'.\r\n", $record_time) ;

close_vt102_sub_process($vt_process) ;

return ;
}

#---------------------------------------------------------------------------------------------------------

sub check_output_directory
{

=head2 [p] check_output_directory( $output_directory)

Check that the given output directory does B<not> exist. If B<$output_directory> is not defined, a directory
name is generated.

I<Arguments>

=over 2 

=item * $output_directory - The name of the directory where the textcast is recorded

=back

I<Returns> - The directory where the textcast is recorded.

I<Exceptions>

=over 2 

=item * Textcast directory already exists

=item * Path too long - length must be under 256 characters.

=item * Invalid path - Path can only contain alphanumerics and path separator.

=back

=cut

my ($directory) = @_ ;

unless(defined $directory)
	{
	my $now_string = localtime;  # e.g., "Thu Oct 13 04:54:34 1994"
	$now_string=~ s/[^[:digit:][:alpha:]]/_/sxmg ;
	
	$directory = "textcast_recorded_on_$now_string" ;
	}

lib/App/Textcast.pm  view on Meta::CPAN


=item ROW - row where the status is displayed

=item COLUMNS - column where the status is displayed

=back 

=back

I<Returns> - A list containing

=over 2 

=item * $total_play_time

=item * $played_frames

=item * \@skipped_frames

=back

I<Exceptions> - None

=cut

my ($input_directory, $screenshot_information, $display_status,) = @_ ;

my $total_frames = scalar(@{$screenshot_information}) ;

my ($total_play_time, $played_frames, @skipped_frames) ;

my $frame_display_time = 0 ;

for my $file_information (@{$screenshot_information})
	{
	my $file = "$input_directory/$file_information->{file}" ;
	$total_play_time += $file_information->{delay} ;
	
	if(-e $file)
		{
		$played_frames++ ;
		
		status
			(
			sprintf( "F: $played_frames/$total_frames [%0.2f]", $file_information->{delay}),
			$display_status->{ROW},
			$display_status->{COLUMN},
			) if $display_status->{DISPLAY} ;
		
		my $sleep_time = $file_information->{delay} - $frame_display_time ;
		
		# split sleep time in smaller chunks if we want to handle the user input
		Readonly my $ONE_MILLION => 1_000_000 ;
		
		usleep $sleep_time * $ONE_MILLION if($sleep_time > 0) ;
		
		$frame_display_time = [gettimeofday]  ;
		
		print #$SHOW_CURSOR,
			read_file($file),
			position_cursor($file_information->{cursor_y}, $file_information->{cursor_x}) ;
		
		$frame_display_time = tv_interval($frame_display_time , [gettimeofday]) ;
		}
	else
		{
		carp "Error: Can't find '$file'! Skipping.\n" ;
		push @skipped_frames, $file ;
		}
	}

return ($total_play_time, $played_frames, \@skipped_frames) ;
}

#---------------------------------------------------------------------------------------------------------

sub print_play_information
{

=head2 [p] print_play_information($total_play_time, $played_frames, \@skipped_frames)

Displays information about the textcast replay.

  print_play_information
	(
	$total_play_time,
	$total_frames,
	$played_frames,
	\@skipped_frames,
	) ;

I<Arguments>

=over 2 

=item * $total_play_time - Float - play time in seconds

=item * $played_frames - Integer - number of framed played, maybe less than $total_frames

=item * \@skipped_frames - Integer - number of frames skipped because they couldn't be found

=back

I<Returns> - Nothing

I<Exceptions> - None

=cut

my ($total_play_time, $played_frames, $skipped_frames) = @_ ;

my $play_time = sprintf('%0.2f', $total_play_time) ;

print "play_textcast: $played_frames frames played in $play_time seconds.\n" ;

if(@{$skipped_frames})
	{
	print "Skipped:\n\t" . join("\n\t", @{$skipped_frames}) . "\n" ;
	}
	
return ;
}

#---------------------------------------------------------------------------------------------------------

sub status
{

=head2 [p] status($status, $status_row, $status_column)

Displays a status on the status line.

I<Arguments>

=over 2 

=item * $status - String to be displayed on the terminal

=item * $status_row - Integer - row position for the status

=item * $status_column - Integer - column position for the status

=back

I<Returns> - Nothing

I<Exceptions> - None

=cut

my ($status, $status_row, $status_column) = @_ ;

print $SAVE_CURSOR_POSITION, 
	position_cursor($status_row, $status_column),
	$CLEAR_LINE,
	$status,
	$RESTORE_CURSOR_POSITION ;

return ;
}

#---------------------------------------------------------------------------------------------------------

sub position_cursor
{

=head2 [p] position_cursor($row, $column)

Create an ANSI command to position the cursor on the terminal.

I<Arguments>

=over 2 

=item * $row - Integer - row position for the status

=item * $column - Integer - column position for the status

=back

I<Returns> - A string containing the ANSI command.

I<Exceptions> - None

See C<xxx>.

=cut

my ($row, $column) = @_ ;

return "\e[${row};${column}H" ;
}

#---------------------------------------------------------------------------------------------------------

sub load_index
{

=head2 [p] load_index($input_directory)

Loads the screencast meta-data.

I<Arguments>

=over 2 

=item * $input_directory - The directory containing the textcast

=back

I<Returns> - The screencast meta-data, see the index file for format information.

I<Exceptions>

=over 2

=item * Index not found

=item * Invalid data in index

=back

=cut

my ($input_directory) = @_ ;

my @screenshot_information ;

if(-e "$input_directory/index")
	{
	print "Parsing index ...\n" ;
	my @entries = read_file("$input_directory/index") ;
	
	my $line = 0 ;
	
	my $regex = '{file => 0, delay => 0.0, cursor_x => 1, cursor_y => 1, size => 1, terminal_rows => 1, terminal_columns => 1, },' ;
	$regex =~ s/^{/^{/sxm ;
	$regex =~ s/([^[:digit:]]+)$/$1\$/sxmg ;
	$regex =~ s/[[:digit:]]+/[[:digit:]]+/sxmg ;
	
	my @errors ;
	
	for my $entry (@entries)
		{
		unless($entry =~ $regex)
			{
			push @errors, "\tInvalid index entry at line $line!\n" ;
			}
			
		$line++ ;
		}
	
	if(@errors)
		{
		local $ERRNO = 2 ;
		croak "Error: Invalid index!\n@errors" ;
		}
	
	@screenshot_information = eval "@entries"  ## no critic (BuiltinFunctions::ProhibitStringyEval)
		or croak "Error: Couldn't parse index file! $@ $!\n" ;
	}
else
	{
	local $ERRNO = 2 ;
	croak "Error: No index found! $!\n"  ;
	}
	
return \@screenshot_information ;
}

#---------------------------------------------------------------------------------------------------------
# VT102
# Everything below is based on the Term::VT102 example
# Logs all terminal output to STDERR if STDERR is redirected to a file.
#---------------------------------------------------------------------------------------------------------

sub create_vt102_sub_process
{

=head2 [p] create_vt102_sub_process($shell_command, $columns, $rows)


I<Arguments>

=over 2 

=item * $shell_command, $columns, $rows - 

=back

I<Returns> - a vt_process handle

I<Exceptions>

=cut

lib/App/Textcast.pm  view on Meta::CPAN


# Child process - set up stdin/out/err and run the command.
# Become process group leader.
if (not POSIX::setsid ()) 
	{
	carp "Couldn't perform setsid: $!";
	}

# Get details of the slave side of the pty.
my $tty = $pty->slave ();
my $tty_name = $tty->ttyname();

# Linux specific - commented out, we'll just use stty below.
#
#	# Set the window size - this may only work on Linux.
#	#
#	my $winsize = pack ('SSSS', $vt->rows, $vt->cols, 0, 0);
#	ioctl ($tty, &IO::Tty::Constant::TIOCSWINSZ, $winsize);

# File descriptor shuffling - close the pty master, then close
# stdin/out/err and reopen them to point to the pty slave.
close ($pty);

close (STDIN);
open (STDIN, '<&' . $tty->fileno ()) || croak 'Error: Couldn\'t reopen ' . $tty_name . " for reading: $!";

close (STDOUT);
open (STDOUT, '>&' . $tty->fileno()) || croak 'Error: Couldn\'t reopen ' . $tty_name . " for writing: $!";

close (STDERR);
open (STDERR, '>&' . $tty->fileno()) || croak "Error: Couldn't redirect STDERR: $!";

# Set sane terminal parameters.
system 'stty sane';

# Set the terminal size with stty.
system 'stty rows ' . $vt->rows;
system 'stty cols ' . $vt->cols;

# Finally, run the command, and die if we can't.
exec $command or croak "Error: Cannot exec '$command': $!";
}

#---------------------------------------------------------------------------------------------------------

sub check_sub_process_output
{

=head2 [p] check_sub_process_output( $vt_process)

Check the sub process output.

I<Arguments>

=over 2 

=item * $vt_process - 

=back

I<Returns> - $eof, $screen_data, $cursor_x, $cursor_y

I<Exceptions> - None

=cut

my ($vt_process) = @_;	
my $vt = $vt_process->{VT} ;

my ($eof, $screen_data) ;

my $rin = $EMPTY_STRING ;
vec ($rin, $vt_process->{PTY}->fileno, 1) = 1;
vec ($rin, $vt_process->{IOT}->fileno, 1) = 1;

my ($win, $ein) = ($EMPTY_STRING, $EMPTY_STRING) ;
my($rout, $wout, $eout) ;
select ($rout=$rin, $wout=$win, $eout=$ein, 1);

# Read from the command if there is anything coming in, and
# pass any data on to the Term::VT102 object.
my $cmdbuf = $EMPTY_STRING ;

Readonly my $BUFFER_READ_SIZE => 1024 ;

if (vec($rout, $vt_process->{PTY}->fileno, 1)) 
	{
	my $bytes_read = $vt_process->{PTY}->sysread ($cmdbuf, $BUFFER_READ_SIZE);
	$eof = 1 if ((defined $bytes_read) && ($bytes_read == 0));
	
	if ((defined $bytes_read) && ($bytes_read > 0)) 
		{
		$vt->process ($cmdbuf);
		syswrite STDERR, $cmdbuf if (! -t STDERR);
		}
	}
	
# End processing if we've gone 1 round after command died with no output.
$eof = 1 if ($vt_process->{DIED} && $cmdbuf eq $EMPTY_STRING);

# Do your stuff here - use $vt->row_plaintext() to see what's on various
# rows of the screen, for instance, or before this main loop you could set
# up a ROWCHANGE callback which checks the changed row, or whatever.

# In this example, we just pass standard input to the SSH command, and we
# take the data coming back from SSH and pass it to the Term::VT102 object,
# and then we repeatedly dump the Term::VT102 screen.

# Read key presses from standard input and pass them to the command
# running in the child process.
if (vec ($rout, $vt_process->{IOT}->fileno, 1)) 
	{
	my $stdinbuf = $EMPTY_STRING ;
	my $bytes_read = $vt_process->{IOT}->sysread ($stdinbuf, $BUFFER_READ_SIZE );
	$eof = 1 if ((defined $bytes_read) && ($bytes_read == 0));
	$vt_process->{PTY}->syswrite ($stdinbuf, $bytes_read) if ((defined $bytes_read) && ($bytes_read > 0));
	}

# Dump what Term::VT102 thinks is on the screen. We only output rows
# we know have changed, to avoid generating too much output.
my $didout = 0;



( run in 0.903 second using v1.01-cache-2.11-cpan-cdf2f3d4e48 )