App-Textcast

 view release on metacpan or  search on metacpan

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


package App::Textcast ;

use strict;
use warnings ;
use Carp qw(carp croak confess) ;

use English qw( -no_match_vars ) ;
$OUTPUT_AUTOFLUSH++;

my $get_terminal_size ;

BEGIN
{
if($OSNAME ne 'MSWin32')
	{
	eval 'use Term::Size;' ; ## no critic (BuiltinFunctions::ProhibitStringyEval)
	croak "Error: $EVAL_ERROR" if $EVAL_ERROR;
	
	$get_terminal_size = eval ' sub { Term::Size::chars *STDOUT{IO} } ' ; ## no critic (BuiltinFunctions::ProhibitStringyEval)
	croak "Error: $EVAL_ERROR" if $EVAL_ERROR ;
	}
else
	{
	eval 'use Win32::Console;' ; ## no critic (BuiltinFunctions::ProhibitStringyEval)
	croak "Error: $EVAL_ERROR" if $EVAL_ERROR ;
	
	my $WIN32_CONSOLE = new Win32::Console;
	$get_terminal_size = eval { sub { $WIN32_CONSOLE->Size() } } ;
	croak "Error: $EVAL_ERROR" if $EVAL_ERROR ;
	}
}

BEGIN 
{
use Sub::Exporter -setup => 
	{
	exports => [ qw(record_textcast play_textcast) ],
	groups  => 
		{
		all  => [ qw() ],
		},
	};
 
use vars qw ($VERSION);
$VERSION = '0.06';
}

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

use Readonly ;

#~ http://www.termsys.demon.co.uk/vtansi.htm
Readonly my $CLEAR => "\e[2J" ; 
Readonly my $HOME => "\e[1;1H" ;
Readonly my $CLEAR_LINE => "\e[2K" ;
Readonly my $SAVE_CURSOR_POSITION => "\e7"  ; 
Readonly my $RESTORE_CURSOR_POSITION => "\e8"  ; 
Readonly my $HIDE_CURSOR => "\e[?25l" ;
Readonly my $SHOW_CURSOR => "\e[?25h" ;

Readonly my $EMPTY_STRING => q{} ;

use IO::Handle;
use POSIX ':sys_wait_h';
use IO::Pty;

use Term::VT102;
use File::Slurp ;

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

=item * Size,	I did a screen cast of a completion script, the size was 1.5 MB and
	it didn't look as good as the terminal. The same textcast was 10 KB (yes,
	10 Kilo Bytes) and it looked good. 
	

=item *  It is not possible to make a screencast of a real terminal, maybe via
	vnc but that's already too complicated

=item * Documentation. I believe it is sometimes better to show "live" documentation
	than static text. I am planning to write a module that plays a textcast
	embedded in ones terminal. The text cast being controlled by the application
	that displays help. I also believe that it could be used as a complement
	to showing static logs or screenshots; an example is when someone describe
	a problem on IRC. Seeing what is being done is sometimes very helpful.

=item * Editing.
	possibility to add message
	possibility to add sound
	possibility to extend the time an image or a range of images is displayed
	concatenate text casts (and their indexes)
	remove portions of a text cast
	name part of the text cast (shows in the index)

=back

=head1 DOCUMENTATION

See L<record_textcast> and L<play_textcast> subbroutines.

=head1 SCRIPTS

Two commands, B<record_textcast> and B<play_textcast>, are installed on your computer when you install this module. Use
them to record and replay your text casts.

=head2 Output

The textcast is a serie of files recorded in a directory. Tar/gzip the files before you send them. the compression ratio averages 95%.


=head1 SUBROUTINES/METHODS

=cut

#---------------------------------------------------------------------------------------------------------
# recording
#---------------------------------------------------------------------------------------------------------

sub record_textcast
{

=head2 record_textcast( %named_arguments )

Records the terminal output of a command. The output is stored as a set of files in a directory. The
directory is later passed as argument to L<play_textcast> for display.

  use App::Textcast 'record_textcast' ;
  
  record_textcast
	(
	COMMAND => 'bash',
	OUTPUT_DIRECTORY => shift @ARGV,
	COMPRESS => $compress,
	COLUMNS => $columns,
	ROWS => $rows,
	) ;

I<Arguments>

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)

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


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

sub close_vt102_sub_process
{

=head2 [p] close_vt102_sub_process( $vt_process)

I<Arguments>

=over 2 

=item * $vt_process -  vt_process handle created by L<create_vt102_sub_process>

=back

I<Returns> - Nothing

I<Exceptions> - None

=cut

my ($vt_process) = @_ ;
$vt_process->{PTY}->close;

# Reset the terminal parameters.
system 'stty sane';

return ;
}

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

sub create_vt102_terminal
{

=head2 [p] create_vt102_terminal($pty, $columns, $rows)

I<Arguments>

=over 2 

=item $pty, $columns, $rows - 

=back

I<Returns> - $vt, $terminal_change_buffer 

I<Exceptions> - None

=cut

my ($pty, $columns, $rows) = @_ ;

my $terminal_change_buffer = {};
my $vt = Term::VT102->new (cols => $columns, rows => $rows,);

$vt->option_set ('LFTOCRLF', 1); # Convert linefeeds to linefeed + carriage return.
$vt->option_set ('LINEWRAP', 1); # Make sure line wrapping is switched on.

# Set up the callback for OUTPUT; this callback function simply sends
# whatever the Term::VT102 module wants to send back to the terminal and
# sends it to the child process - see its definition below.
$vt->callback_set ('OUTPUT', \&vt_output, $pty);

# Set up a callback for row changes, so we can process updates and display
# them without having to redraw the whole screen every time. We catch CLEAR,
# SCROLL_UP, and SCROLL_DOWN with another function that triggers a
# whole-screen repaint. You could process SCROLL_UP and SCROLL_DOWN more
# elegantly, but this is just an example.
$vt->callback_set ('ROWCHANGE', \&vt_rowchange, $terminal_change_buffer );
$vt->callback_set ('CLEAR', \&vt_changeall, $terminal_change_buffer );
$vt->callback_set ('SCROLL_UP', \&vt_changeall, $terminal_change_buffer );
$vt->callback_set ('SCROLL_DOWN', \&vt_changeall, $terminal_change_buffer );

# Set stdin's terminal to raw mode so we can pass all keypresses straight
# through immediately.
system 'stty raw -echo';

return ($vt, $terminal_change_buffer ) ;
}

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

sub vt_output 
{

=head2 [p] vt_output($vtobject, $type, $arg1, $arg2, $private)

Callback for OUTPUT events - for Term::VT102.

I<Arguments>

=over 2 

=item $vtobject, $type, $arg1, $arg2, $private - 

=back

I<Returns> - Nothing

I<Exceptions> - Nothing

See L<Term::VT102>.

=cut

my ($vtobject, $type, $arg1, $arg2, $private) = @_;

if ($type eq 'OUTPUT') 
	{
	$private->syswrite ($arg1, length $arg1);
	}

return ;
}

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

sub vt_rowchange 
{

=head2 [p] vt_rowchange($vtobject, $type, $arg1, $arg2, $private)

Callback for ROWCHANGE events. This just sets a time value for the changed
row using the private data as a hash reference - the time represents the
earliest that row was changed since the last screen update.

I<Arguments>

=over 2 

=item $vtobject, $type, $arg1, $arg2, $private - 

=back

I<Returns> - Nothing

I<Exceptions> - Nothing

See L<Term::VT102>.

=cut

my ($vtobject, $type, $arg1, $arg2, $private) = @_;
$private->{$arg1} = time if (not exists $private->{$arg1});

return ;
}

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

sub vt_changeall 
{

=head2 [p] vt_changeall($vtobject, $type, $arg1, $arg2, $private)

Callback to trigger a full-screen repaint.

I<Arguments>

=over 2 

=item $vtobject, $type, $arg1, $arg2, $private - 

=back

I<Returns> - Nothing

I<Exceptions> - None



( run in 0.736 second using v1.01-cache-2.11-cpan-13bb782fe5a )