File Coverage

blib/lib/App/Textcast.pm
Criterion Covered Total %
statement 48 267 17.9
branch 3 88 3.4
condition 0 29 0.0
subroutine 16 34 47.0
pod 17 17 100.0
total 84 435 19.3


line stmt bran cond sub pod time code
1              
2             package App::Textcast ;
3              
4 2     2   76407 use strict;
  2         6  
  2         83  
5 2     2   12 use warnings ;
  2         4  
  2         70  
6 2     2   13 use Carp qw(carp croak confess) ;
  2         9  
  2         141  
7              
8 2     2   2112 use English qw( -no_match_vars ) ;
  2         10655  
  2         11  
9             $OUTPUT_AUTOFLUSH++;
10              
11             my $get_terminal_size ;
12              
13             BEGIN
14             {
15 2 50   2   1217 if($OSNAME ne 'MSWin32')
16             {
17 2     2   131 eval 'use Term::Size;' ; ## no critic (BuiltinFunctions::ProhibitStringyEval)
  2         1728  
  2         6177  
  2         59  
18 2 50       11 croak "Error: $EVAL_ERROR" if $EVAL_ERROR;
19            
20 2         124 $get_terminal_size = eval ' sub { Term::Size::chars *STDOUT{IO} } ' ; ## no critic (BuiltinFunctions::ProhibitStringyEval)
21 2 50       84 croak "Error: $EVAL_ERROR" if $EVAL_ERROR ;
22             }
23             else
24             {
25 0         0 eval 'use Win32::Console;' ; ## no critic (BuiltinFunctions::ProhibitStringyEval)
26 0 0       0 croak "Error: $EVAL_ERROR" if $EVAL_ERROR ;
27            
28 0         0 my $WIN32_CONSOLE = new Win32::Console;
29 0         0 $get_terminal_size = eval { sub { $WIN32_CONSOLE->Size() } } ;
  0         0  
  0         0  
30 0 0       0 croak "Error: $EVAL_ERROR" if $EVAL_ERROR ;
31             }
32             }
33              
34             BEGIN
35             {
36 2         23 use Sub::Exporter -setup =>
37             {
38             exports => [ qw(record_textcast play_textcast) ],
39             groups =>
40             {
41             all => [ qw() ],
42             },
43 2     2   1760 };
  2         26120  
44            
45 2     2   864 use vars qw ($VERSION);
  2         5  
  2         88  
46 2     2   45 $VERSION = '0.06';
47             }
48              
49             #-------------------------------------------------------------------------------
50              
51 2     2   1834 use Readonly ;
  2         5789  
  2         329  
52              
53             #~ http://www.termsys.demon.co.uk/vtansi.htm
54             Readonly my $CLEAR => "\e[2J" ;
55             Readonly my $HOME => "\e[1;1H" ;
56             Readonly my $CLEAR_LINE => "\e[2K" ;
57             Readonly my $SAVE_CURSOR_POSITION => "\e7" ;
58             Readonly my $RESTORE_CURSOR_POSITION => "\e8" ;
59             Readonly my $HIDE_CURSOR => "\e[?25l" ;
60             Readonly my $SHOW_CURSOR => "\e[?25h" ;
61              
62             Readonly my $EMPTY_STRING => q{} ;
63              
64 2     2   1963 use IO::Handle;
  2         13836  
  2         89  
65 2     2   1705 use POSIX ':sys_wait_h';
  2         16182  
  2         13  
66 2     2   4182 use IO::Pty;
  2         19118  
  2         93  
67              
68 2     2   2148 use Term::VT102;
  2         19764  
  2         130  
69 2     2   1922 use File::Slurp ;
  2         8745  
  2         174  
70 2     2   1893 use Time::HiRes qw(gettimeofday tv_interval usleep);
  2         3763  
  2         10  
71              
72             #-------------------------------------------------------------------------------
73              
74             =head1 NAME
75              
76             App::Textcast - Light weight text casting
77              
78             =head1 SYNOPSIS
79              
80             use App::Textcast qw(record_textcast play_textcast) ;
81            
82             record_textcast(COMMAND => 'bash') ;
83             play_textcast(TEXTCAST_DIRECTORY => $input_directory) ;
84            
85              
86             =head1 DESCRIPTION
87              
88             What's a textcast?
89              
90             It's a screencast of a terminal session. The idea is to record the terminal session and replay
91             it in another terminal without loosing resolution, as screencasts do, nor using much disk space due to
92             conversion from text to video. The terminal session can run a shell or any other program.
93              
94             Why textcasts?
95              
96             =over 2
97              
98             =item * Size, I did a screen cast of a completion script, the size was 1.5 MB and
99             it didn't look as good as the terminal. The same textcast was 10 KB (yes,
100             10 Kilo Bytes) and it looked good.
101            
102              
103             =item * It is not possible to make a screencast of a real terminal, maybe via
104             vnc but that's already too complicated
105              
106             =item * Documentation. I believe it is sometimes better to show "live" documentation
107             than static text. I am planning to write a module that plays a textcast
108             embedded in ones terminal. The text cast being controlled by the application
109             that displays help. I also believe that it could be used as a complement
110             to showing static logs or screenshots; an example is when someone describe
111             a problem on IRC. Seeing what is being done is sometimes very helpful.
112              
113             =item * Editing.
114             possibility to add message
115             possibility to add sound
116             possibility to extend the time an image or a range of images is displayed
117             concatenate text casts (and their indexes)
118             remove portions of a text cast
119             name part of the text cast (shows in the index)
120              
121             =back
122              
123             =head1 DOCUMENTATION
124              
125             See L and L subbroutines.
126              
127             =head1 SCRIPTS
128              
129             Two commands, B and B, are installed on your computer when you install this module. Use
130             them to record and replay your text casts.
131              
132             =head2 Output
133              
134             The textcast is a serie of files recorded in a directory. Tar/gzip the files before you send them. the compression ratio averages 95%.
135              
136              
137             =head1 SUBROUTINES/METHODS
138              
139             =cut
140              
141             #---------------------------------------------------------------------------------------------------------
142             # recording
143             #---------------------------------------------------------------------------------------------------------
144              
145             sub record_textcast
146             {
147              
148             =head2 record_textcast( %named_arguments )
149              
150             Records the terminal output of a command. The output is stored as a set of files in a directory. The
151             directory is later passed as argument to L for display.
152              
153             use App::Textcast 'record_textcast' ;
154            
155             record_textcast
156             (
157             COMMAND => 'bash',
158             OUTPUT_DIRECTORY => shift @ARGV,
159             COMPRESS => $compress,
160             COLUMNS => $columns,
161             ROWS => $rows,
162             ) ;
163              
164             I
165              
166             The arguments are named, order is not important.
167              
168             =over 2
169              
170             =item * COMMAND => $string - the name of the command to tun in a terminal. You most probably wan to run
171             I or I
172              
173             =item * OUTPUT_DIRECTORY => $directory_path - Optional - the path to the directory where the textcast is to be
174             recorded. This subroutine will create a directory if this option is not set. if this option is set, the directory
175             should not exist.
176              
177             =item * COMPRESS => $boolean - Not implemented
178              
179             =item * COLUMNS => $integer - Optional - Number of columns in the terminal. The current terminal columns
180             number is used if this argument is not set.
181              
182             =item * ROWS => $integer - Optional - Number of rows in the terminal. The current terminal rows number is
183             used if this argument is not set.
184              
185             =back
186              
187             I - Nothing
188              
189             I
190              
191             =over 2
192              
193             =item * See check_output_directory
194              
195             =item * see create_vt102_sub_process
196              
197             =item * disk full error
198              
199             =back
200              
201             See I.
202              
203             =cut
204              
205 0     0 1   my (%arguments) = @_;
206              
207 0           my ($terminal_columns, $terminal_rows) = $get_terminal_size->() ;
208              
209 0           my $output_directory = check_output_directory($arguments{OUTPUT_DIRECTORY}) ;
210 0   0       my $vt_process = create_vt102_sub_process
      0        
211             (
212             $arguments{COMMAND},
213             $arguments{COLUMNS} || $terminal_columns,
214             $arguments{ROWS} || $terminal_rows,
215             ) ;
216              
217 0           print $CLEAR ;
218            
219 0           my $previous_time = my $start_time = [gettimeofday] ;
220              
221 0           my ($screenshot_index, $sub_process_ended) = (0, 0) ;
222              
223 0           while (not $sub_process_ended)
224             {
225 0           ($sub_process_ended, my $screen_diff, my $cursor_x, my $cursor_y) = check_sub_process_output($vt_process) ;
226            
227 0           my $now = [gettimeofday] ;
228 0           my $elapsed = tv_interval($previous_time, $now);
229 0           $previous_time = $now ;
230            
231 0           my $screenshot_file_name = "$output_directory/$screenshot_index" ;
232            
233 0           write_file($screenshot_file_name, $screen_diff) ;
234            
235 0           my ($terminal_columns, $terminal_rows) = $get_terminal_size->() ;
236            
237 0           append_file
238             (
239             "$output_directory/index",
240            
241             '{'
242             . "file => $screenshot_index, "
243             . sprintf('delay => %0.3f, ', $elapsed)
244             . "cursor_x => $cursor_x, "
245             . "cursor_y => $cursor_y, "
246             . 'size => ' . length($screen_diff) . ', '
247             . "terminal_rows => $terminal_rows, "
248             . "terminal_columns => $terminal_columns, "
249             . "},\n"
250             ) ;
251            
252 0           $screenshot_index++ ;
253             }
254              
255 0           my $record_time = tv_interval($start_time, [gettimeofday]);
256 0           printf("record_textcast: $screenshot_index frames in %.02f seconds. Textcast is in '$output_directory'.\r\n", $record_time) ;
257              
258 0           close_vt102_sub_process($vt_process) ;
259              
260 0           return ;
261             }
262              
263             #---------------------------------------------------------------------------------------------------------
264              
265             sub check_output_directory
266             {
267              
268             =head2 [p] check_output_directory( $output_directory)
269              
270             Check that the given output directory does B exist. If B<$output_directory> is not defined, a directory
271             name is generated.
272              
273             I
274              
275             =over 2
276              
277             =item * $output_directory - The name of the directory where the textcast is recorded
278              
279             =back
280              
281             I - The directory where the textcast is recorded.
282              
283             I
284              
285             =over 2
286              
287             =item * Textcast directory already exists
288              
289             =item * Path too long - length must be under 256 characters.
290              
291             =item * Invalid path - Path can only contain alphanumerics and path separator.
292              
293             =back
294              
295             =cut
296              
297 0     0 1   my ($directory) = @_ ;
298              
299 0 0         unless(defined $directory)
300             {
301 0           my $now_string = localtime; # e.g., "Thu Oct 13 04:54:34 1994"
302 0           $now_string=~ s/[^[:digit:][:alpha:]]/_/sxmg ;
303            
304 0           $directory = "textcast_recorded_on_$now_string" ;
305             }
306            
307 0 0         if(-e $directory)
308             {
309 0           local $ERRNO = 1 ;
310 0           croak "Error: Textcast directory '$directory' already exists!\n" ;
311             }
312             else
313             {
314             #todo: get the max path on this platform
315 0           local $ERRNO = 2 ;
316            
317 0           Readonly my $MAX_PATH_LENGTH => 256 ;
318 0 0         croak 'Error: Path too long' if length($directory) > $MAX_PATH_LENGTH ;
319              
320 0 0         if($directory =~ /([[:alnum:]\/_-]+)/sxm)
321             {
322 0           $directory = $1 ;
323             }
324             else
325             {
326 0           Readonly my $ERRNO_INVALID_PATH => 3 ;
327 0           local $ERRNO = $ERRNO_INVALID_PATH ;
328 0           croak 'Error: Invalid path! Path can only contain alphanumerics and path separator.'
329             }
330            
331 0 0         mkdir $directory or croak "Can't create directory '$directory'! $!\n" ;
332             }
333              
334 0           return $directory ;
335             }
336              
337             #---------------------------------------------------------------------------------------------------------
338             # Playing
339             #---------------------------------------------------------------------------------------------------------
340              
341             sub play_textcast
342             {
343              
344             =head2 play_textcast( %named_arguments)
345              
346             Loads, checks, and initiates the textcast replay. Displays information after the textcast replay.
347              
348             use App::Textcast 'play_textcast' ;
349            
350             play_textcast
351             (
352             TEXTCAST_DIRECTORY => $input_directory,
353             OVERLAY_DIRECTORY => $overlay_directory,
354             DISPLAY_STATUS => $display_status,
355             START_PAUSED => $start_paused,
356             ) ;
357              
358             I
359              
360             =over 2
361              
362             =item * TEXTCAST_DIRECTORY - String - directory containing the textcast
363              
364             =item * OVERLAY_DIRECTORY - not implemented
365              
366             =item * DISPLAY_STATUS - Boolean -
367              
368             =item * START_PAUSED - not implemented
369              
370             =back
371              
372             I - Nothing
373              
374             I
375              
376             =over 2
377              
378             =item * Terminal too small
379              
380             =item * interrupted by user
381              
382             =item * load_index
383              
384             =back
385              
386             =cut
387              
388 0     0 1   my (%arguments) = @_ ;
389              
390 0 0         my $input_directory = $arguments{TEXTCAST_DIRECTORY} or croak 'Error: Expected textcast location!' ;
391 0   0       my $display_status = $arguments{DISPLAY_STATUS} || 0 ;
392              
393             local $SIG{INT} = sub
394             {
395 0     0     print "\n" ;
396 0           local $ERRNO = 1 ;
397 0           croak "Caught interrupt signal!\n" ;
398 0           } ;
399              
400 0           my $screenshot_information = load_index($input_directory) ;
401              
402 0           my ($max_rows, $max_columns) = (-1, -1) ;
403              
404 0           for my $screenshot_data (@{$screenshot_information})
  0            
405             {
406             #~ print "$screenshot_data->{terminal_rows}, $screenshot_data->{terminal_columns} \n" ;
407            
408 0 0         $max_rows = $screenshot_data->{terminal_rows} if $screenshot_data->{terminal_rows} > $max_rows ;
409 0 0         $max_columns = $screenshot_data->{terminal_columns} if $screenshot_data->{terminal_columns} > $max_columns ;
410             }
411              
412 0           my ($terminal_columns, $terminal_rows) = $get_terminal_size->() ;
413              
414 0           my ($status_row,$status_column) = (1, 1) ;
415              
416 0 0 0       if($max_rows + $display_status > $terminal_rows || $max_columns > $terminal_columns)
417             {
418 0           Readonly my $ERRNO_TERMINAL_TOO_SMALL => 3 ;
419 0           local $ERRNO = $ERRNO_TERMINAL_TOO_SMALL ;
420 0           croak "Error: Terminal too small [$terminal_columns, $terminal_rows] need at least [$max_columns, $max_rows]!\n" ;
421             }
422             else
423             {
424 0           $status_row = $max_rows + 1 ;
425             }
426            
427             #~ print DumpTree \@screenshot_information ;
428              
429 0           print $CLEAR, $HOME ;
430              
431 0           my ($total_play_time, $played_frames, $skipped_frames)
432             = display_text_cast_data
433             (
434             $input_directory,
435             $screenshot_information,
436             {
437             DISPLAY => $display_status,
438             ROW => $status_row,
439             COLUMN => $status_column,
440             }
441             ) ;
442            
443 0           print_play_information($total_play_time, $played_frames, $skipped_frames) ;
444              
445 0           return ;
446             }
447              
448             #---------------------------------------------------------------------------------------------------------
449              
450             sub display_text_cast_data
451             {
452              
453             =head2 [p] display_text_cast_data($input_directory, \@screenshot_information, \%display_status )
454              
455             Plays a screencast.
456              
457             I
458              
459             =over 2
460              
461             =item * $input_directory - String - directory containing the textcast
462              
463             =item * \@screenshot_information - see L
464              
465             =item * \%display_status -
466              
467             =over 2
468              
469             =item DISPLAY - Boolean - status is displayed during the replay if this is set
470              
471             =item ROW - row where the status is displayed
472              
473             =item COLUMNS - column where the status is displayed
474              
475             =back
476              
477             =back
478              
479             I - A list containing
480              
481             =over 2
482              
483             =item * $total_play_time
484              
485             =item * $played_frames
486              
487             =item * \@skipped_frames
488              
489             =back
490              
491             I - None
492              
493             =cut
494              
495 0     0 1   my ($input_directory, $screenshot_information, $display_status,) = @_ ;
496              
497 0           my $total_frames = scalar(@{$screenshot_information}) ;
  0            
498              
499 0           my ($total_play_time, $played_frames, @skipped_frames) ;
500              
501 0           my $frame_display_time = 0 ;
502              
503 0           for my $file_information (@{$screenshot_information})
  0            
504             {
505 0           my $file = "$input_directory/$file_information->{file}" ;
506 0           $total_play_time += $file_information->{delay} ;
507            
508 0 0         if(-e $file)
509             {
510 0           $played_frames++ ;
511            
512 0 0         status
513             (
514             sprintf( "F: $played_frames/$total_frames [%0.2f]", $file_information->{delay}),
515             $display_status->{ROW},
516             $display_status->{COLUMN},
517             ) if $display_status->{DISPLAY} ;
518            
519 0           my $sleep_time = $file_information->{delay} - $frame_display_time ;
520            
521             # split sleep time in smaller chunks if we want to handle the user input
522 0           Readonly my $ONE_MILLION => 1_000_000 ;
523            
524 0 0         usleep $sleep_time * $ONE_MILLION if($sleep_time > 0) ;
525            
526 0           $frame_display_time = [gettimeofday] ;
527            
528 0           print #$SHOW_CURSOR,
529             read_file($file),
530             position_cursor($file_information->{cursor_y}, $file_information->{cursor_x}) ;
531            
532 0           $frame_display_time = tv_interval($frame_display_time , [gettimeofday]) ;
533             }
534             else
535             {
536 0           carp "Error: Can't find '$file'! Skipping.\n" ;
537 0           push @skipped_frames, $file ;
538             }
539             }
540              
541 0           return ($total_play_time, $played_frames, \@skipped_frames) ;
542             }
543              
544             #---------------------------------------------------------------------------------------------------------
545              
546             sub print_play_information
547             {
548              
549             =head2 [p] print_play_information($total_play_time, $played_frames, \@skipped_frames)
550              
551             Displays information about the textcast replay.
552              
553             print_play_information
554             (
555             $total_play_time,
556             $total_frames,
557             $played_frames,
558             \@skipped_frames,
559             ) ;
560              
561             I
562              
563             =over 2
564              
565             =item * $total_play_time - Float - play time in seconds
566              
567             =item * $played_frames - Integer - number of framed played, maybe less than $total_frames
568              
569             =item * \@skipped_frames - Integer - number of frames skipped because they couldn't be found
570              
571             =back
572              
573             I - Nothing
574              
575             I - None
576              
577             =cut
578              
579 0     0 1   my ($total_play_time, $played_frames, $skipped_frames) = @_ ;
580              
581 0           my $play_time = sprintf('%0.2f', $total_play_time) ;
582              
583 0           print "play_textcast: $played_frames frames played in $play_time seconds.\n" ;
584              
585 0 0         if(@{$skipped_frames})
  0            
586             {
587 0           print "Skipped:\n\t" . join("\n\t", @{$skipped_frames}) . "\n" ;
  0            
588             }
589            
590 0           return ;
591             }
592              
593             #---------------------------------------------------------------------------------------------------------
594              
595             sub status
596             {
597              
598             =head2 [p] status($status, $status_row, $status_column)
599              
600             Displays a status on the status line.
601              
602             I
603              
604             =over 2
605              
606             =item * $status - String to be displayed on the terminal
607              
608             =item * $status_row - Integer - row position for the status
609              
610             =item * $status_column - Integer - column position for the status
611              
612             =back
613              
614             I - Nothing
615              
616             I - None
617              
618             =cut
619              
620 0     0 1   my ($status, $status_row, $status_column) = @_ ;
621              
622 0           print $SAVE_CURSOR_POSITION,
623             position_cursor($status_row, $status_column),
624             $CLEAR_LINE,
625             $status,
626             $RESTORE_CURSOR_POSITION ;
627              
628 0           return ;
629             }
630              
631             #---------------------------------------------------------------------------------------------------------
632              
633             sub position_cursor
634             {
635              
636             =head2 [p] position_cursor($row, $column)
637              
638             Create an ANSI command to position the cursor on the terminal.
639              
640             I
641              
642             =over 2
643              
644             =item * $row - Integer - row position for the status
645              
646             =item * $column - Integer - column position for the status
647              
648             =back
649              
650             I - A string containing the ANSI command.
651              
652             I - None
653              
654             See C.
655              
656             =cut
657              
658 0     0 1   my ($row, $column) = @_ ;
659              
660 0           return "\e[${row};${column}H" ;
661             }
662              
663             #---------------------------------------------------------------------------------------------------------
664              
665             sub load_index
666             {
667              
668             =head2 [p] load_index($input_directory)
669              
670             Loads the screencast meta-data.
671              
672             I
673              
674             =over 2
675              
676             =item * $input_directory - The directory containing the textcast
677              
678             =back
679              
680             I - The screencast meta-data, see the index file for format information.
681              
682             I
683              
684             =over 2
685              
686             =item * Index not found
687              
688             =item * Invalid data in index
689              
690             =back
691              
692             =cut
693              
694 0     0 1   my ($input_directory) = @_ ;
695              
696 0           my @screenshot_information ;
697              
698 0 0         if(-e "$input_directory/index")
699             {
700 0           print "Parsing index ...\n" ;
701 0           my @entries = read_file("$input_directory/index") ;
702            
703 0           my $line = 0 ;
704            
705 0           my $regex = '{file => 0, delay => 0.0, cursor_x => 1, cursor_y => 1, size => 1, terminal_rows => 1, terminal_columns => 1, },' ;
706 0           $regex =~ s/^{/^{/sxm ;
707 0           $regex =~ s/([^[:digit:]]+)$/$1\$/sxmg ;
708 0           $regex =~ s/[[:digit:]]+/[[:digit:]]+/sxmg ;
709            
710 0           my @errors ;
711            
712 0           for my $entry (@entries)
713             {
714 0 0         unless($entry =~ $regex)
715             {
716 0           push @errors, "\tInvalid index entry at line $line!\n" ;
717             }
718            
719 0           $line++ ;
720             }
721            
722 0 0         if(@errors)
723             {
724 0           local $ERRNO = 2 ;
725 0           croak "Error: Invalid index!\n@errors" ;
726             }
727            
728 0 0         @screenshot_information = eval "@entries" ## no critic (BuiltinFunctions::ProhibitStringyEval)
729             or croak "Error: Couldn't parse index file! $@ $!\n" ;
730             }
731             else
732             {
733 0           local $ERRNO = 2 ;
734 0           croak "Error: No index found! $!\n" ;
735             }
736            
737 0           return \@screenshot_information ;
738             }
739              
740             #---------------------------------------------------------------------------------------------------------
741             # VT102
742             # Everything below is based on the Term::VT102 example
743             # Logs all terminal output to STDERR if STDERR is redirected to a file.
744             #---------------------------------------------------------------------------------------------------------
745              
746             sub create_vt102_sub_process
747             {
748              
749             =head2 [p] create_vt102_sub_process($shell_command, $columns, $rows)
750              
751              
752             I
753              
754             =over 2
755              
756             =item * $shell_command, $columns, $rows -
757              
758             =back
759              
760             I - a vt_process handle
761              
762             I
763              
764             =cut
765              
766 0     0 1   my ($shell_command, $columns, $rows) = @_ ;
767              
768             # Create a pty for the command to run.
769 0           my $pty = new IO::Pty;
770 0           $pty->autoflush();
771              
772 0 0         croak 'Error: Could not assign a pty' if (not defined $pty->ttyname()) ;
773              
774             # Create the terminal object.
775 0           my ($vt, $terminal_change_buffer) = create_vt102_terminal($pty, $columns, $rows) ;
776              
777             # Run the command in a child process.
778 0           my $pid = create_child_process($shell_command, $pty, $vt) ;
779              
780             # IO::Handle for standard input - unbuffered.
781 0           my $iot = new IO::Handle;
782 0           $iot->fdopen (fileno(STDIN), 'r');
783              
784             return
785             {
786 0           PTY => $pty,
787             VT => $vt,
788             TERMINAL_CHANGE_BUFFER => $terminal_change_buffer,
789             IOT => $iot,
790             PREVXY => $EMPTY_STRING,
791            
792             PID => $pid,
793             DIED => 0,
794             } ;
795             }
796              
797             #---------------------------------------------------------------------------------------------------------
798              
799             sub close_vt102_sub_process
800             {
801              
802             =head2 [p] close_vt102_sub_process( $vt_process)
803              
804             I
805              
806             =over 2
807              
808             =item * $vt_process - vt_process handle created by L
809              
810             =back
811              
812             I - Nothing
813              
814             I - None
815              
816             =cut
817              
818 0     0 1   my ($vt_process) = @_ ;
819 0           $vt_process->{PTY}->close;
820              
821             # Reset the terminal parameters.
822 0           system 'stty sane';
823              
824 0           return ;
825             }
826              
827             #---------------------------------------------------------------------------------------------------------
828              
829             sub create_vt102_terminal
830             {
831              
832             =head2 [p] create_vt102_terminal($pty, $columns, $rows)
833              
834             I
835              
836             =over 2
837              
838             =item $pty, $columns, $rows -
839              
840             =back
841              
842             I - $vt, $terminal_change_buffer
843              
844             I - None
845              
846             =cut
847              
848 0     0 1   my ($pty, $columns, $rows) = @_ ;
849              
850 0           my $terminal_change_buffer = {};
851 0           my $vt = Term::VT102->new (cols => $columns, rows => $rows,);
852              
853 0           $vt->option_set ('LFTOCRLF', 1); # Convert linefeeds to linefeed + carriage return.
854 0           $vt->option_set ('LINEWRAP', 1); # Make sure line wrapping is switched on.
855              
856             # Set up the callback for OUTPUT; this callback function simply sends
857             # whatever the Term::VT102 module wants to send back to the terminal and
858             # sends it to the child process - see its definition below.
859 0           $vt->callback_set ('OUTPUT', \&vt_output, $pty);
860              
861             # Set up a callback for row changes, so we can process updates and display
862             # them without having to redraw the whole screen every time. We catch CLEAR,
863             # SCROLL_UP, and SCROLL_DOWN with another function that triggers a
864             # whole-screen repaint. You could process SCROLL_UP and SCROLL_DOWN more
865             # elegantly, but this is just an example.
866 0           $vt->callback_set ('ROWCHANGE', \&vt_rowchange, $terminal_change_buffer );
867 0           $vt->callback_set ('CLEAR', \&vt_changeall, $terminal_change_buffer );
868 0           $vt->callback_set ('SCROLL_UP', \&vt_changeall, $terminal_change_buffer );
869 0           $vt->callback_set ('SCROLL_DOWN', \&vt_changeall, $terminal_change_buffer );
870              
871             # Set stdin's terminal to raw mode so we can pass all keypresses straight
872             # through immediately.
873 0           system 'stty raw -echo';
874              
875 0           return ($vt, $terminal_change_buffer ) ;
876             }
877              
878             #---------------------------------------------------------------------------------------------------------
879              
880             sub vt_output
881             {
882              
883             =head2 [p] vt_output($vtobject, $type, $arg1, $arg2, $private)
884              
885             Callback for OUTPUT events - for Term::VT102.
886              
887             I
888              
889             =over 2
890              
891             =item $vtobject, $type, $arg1, $arg2, $private -
892              
893             =back
894              
895             I - Nothing
896              
897             I - Nothing
898              
899             See L.
900              
901             =cut
902              
903 0     0 1   my ($vtobject, $type, $arg1, $arg2, $private) = @_;
904              
905 0 0         if ($type eq 'OUTPUT')
906             {
907 0           $private->syswrite ($arg1, length $arg1);
908             }
909              
910 0           return ;
911             }
912              
913             #---------------------------------------------------------------------------------------------------------
914              
915             sub vt_rowchange
916             {
917              
918             =head2 [p] vt_rowchange($vtobject, $type, $arg1, $arg2, $private)
919              
920             Callback for ROWCHANGE events. This just sets a time value for the changed
921             row using the private data as a hash reference - the time represents the
922             earliest that row was changed since the last screen update.
923              
924             I
925              
926             =over 2
927              
928             =item $vtobject, $type, $arg1, $arg2, $private -
929              
930             =back
931              
932             I - Nothing
933              
934             I - Nothing
935              
936             See L.
937              
938             =cut
939              
940 0     0 1   my ($vtobject, $type, $arg1, $arg2, $private) = @_;
941 0 0         $private->{$arg1} = time if (not exists $private->{$arg1});
942              
943 0           return ;
944             }
945              
946             #---------------------------------------------------------------------------------------------------------
947              
948             sub vt_changeall
949             {
950              
951             =head2 [p] vt_changeall($vtobject, $type, $arg1, $arg2, $private)
952              
953             Callback to trigger a full-screen repaint.
954              
955             I
956              
957             =over 2
958              
959             =item $vtobject, $type, $arg1, $arg2, $private -
960              
961             =back
962              
963             I - Nothing
964              
965             I - None
966              
967             See L.
968              
969             =cut
970              
971 0     0 1   my ($vtobject, $type, $arg1, $arg2, $private) = @_;
972 0           for my $row (1 .. $vtobject->rows)
973             {
974 0           $private->{$row} = 0;
975             }
976            
977 0           return ;
978             }
979              
980             #---------------------------------------------------------------------------------------------------------
981              
982             sub create_child_process
983             {
984              
985             =head2 [p] create_child_process($shell_command, $pty, $vt)
986              
987             Creqtes a child process to run a command in.
988              
989             I
990              
991             =over 2
992              
993             =item $shell_command, $pty, $vt -
994              
995             =back
996              
997             I - Nothing
998              
999             I - Can not fork to run sub process
1000              
1001             See C.
1002              
1003             =cut
1004              
1005 0     0 1   my ($shell_command, $pty, $vt) = @_ ;
1006 0           my $pid = fork;
1007              
1008 0 0         croak "Error: Can not fork to run sub process, $!" if (not defined $pid) ;
1009              
1010 0 0         if ($pid == 0)
1011             {
1012             # never comes back
1013 0           run_child_process($shell_command, $pty, $vt) ;
1014             }
1015              
1016 0           return $pid ;
1017             }
1018              
1019             #---------------------------------------------------------------------------------------------------------
1020              
1021             sub run_child_process
1022             {
1023              
1024             =head2 [p] run_child_process($command, $pty, $vt)
1025              
1026             I
1027              
1028             =over 2
1029              
1030             =item $command, $pty, $vt -
1031              
1032             =back
1033              
1034             I - Nothing
1035              
1036             I - Error redirecting streams
1037              
1038             =cut
1039              
1040 0     0 1   my ($command, $pty, $vt) = @_ ;
1041              
1042             # Child process - set up stdin/out/err and run the command.
1043             # Become process group leader.
1044 0 0         if (not POSIX::setsid ())
1045             {
1046 0           carp "Couldn't perform setsid: $!";
1047             }
1048              
1049             # Get details of the slave side of the pty.
1050 0           my $tty = $pty->slave ();
1051 0           my $tty_name = $tty->ttyname();
1052              
1053             # Linux specific - commented out, we'll just use stty below.
1054             #
1055             # # Set the window size - this may only work on Linux.
1056             # #
1057             # my $winsize = pack ('SSSS', $vt->rows, $vt->cols, 0, 0);
1058             # ioctl ($tty, &IO::Tty::Constant::TIOCSWINSZ, $winsize);
1059              
1060             # File descriptor shuffling - close the pty master, then close
1061             # stdin/out/err and reopen them to point to the pty slave.
1062 0           close ($pty);
1063              
1064 0           close (STDIN);
1065 0 0         open (STDIN, '<&' . $tty->fileno ()) || croak 'Error: Couldn\'t reopen ' . $tty_name . " for reading: $!";
1066              
1067 0           close (STDOUT);
1068 0 0         open (STDOUT, '>&' . $tty->fileno()) || croak 'Error: Couldn\'t reopen ' . $tty_name . " for writing: $!";
1069              
1070 0           close (STDERR);
1071 0 0         open (STDERR, '>&' . $tty->fileno()) || croak "Error: Couldn't redirect STDERR: $!";
1072              
1073             # Set sane terminal parameters.
1074 0           system 'stty sane';
1075              
1076             # Set the terminal size with stty.
1077 0           system 'stty rows ' . $vt->rows;
1078 0           system 'stty cols ' . $vt->cols;
1079              
1080             # Finally, run the command, and die if we can't.
1081 0 0         exec $command or croak "Error: Cannot exec '$command': $!";
1082             }
1083              
1084             #---------------------------------------------------------------------------------------------------------
1085              
1086             sub check_sub_process_output
1087             {
1088              
1089             =head2 [p] check_sub_process_output( $vt_process)
1090              
1091             Check the sub process output.
1092              
1093             I
1094              
1095             =over 2
1096              
1097             =item * $vt_process -
1098              
1099             =back
1100              
1101             I - $eof, $screen_data, $cursor_x, $cursor_y
1102              
1103             I - None
1104              
1105             =cut
1106              
1107 0     0 1   my ($vt_process) = @_;
1108 0           my $vt = $vt_process->{VT} ;
1109              
1110 0           my ($eof, $screen_data) ;
1111              
1112 0           my $rin = $EMPTY_STRING ;
1113 0           vec ($rin, $vt_process->{PTY}->fileno, 1) = 1;
1114 0           vec ($rin, $vt_process->{IOT}->fileno, 1) = 1;
1115              
1116 0           my ($win, $ein) = ($EMPTY_STRING, $EMPTY_STRING) ;
1117 0           my($rout, $wout, $eout) ;
1118 0           select ($rout=$rin, $wout=$win, $eout=$ein, 1);
1119              
1120             # Read from the command if there is anything coming in, and
1121             # pass any data on to the Term::VT102 object.
1122 0           my $cmdbuf = $EMPTY_STRING ;
1123              
1124 0           Readonly my $BUFFER_READ_SIZE => 1024 ;
1125              
1126 0 0         if (vec($rout, $vt_process->{PTY}->fileno, 1))
1127             {
1128 0           my $bytes_read = $vt_process->{PTY}->sysread ($cmdbuf, $BUFFER_READ_SIZE);
1129 0 0 0       $eof = 1 if ((defined $bytes_read) && ($bytes_read == 0));
1130            
1131 0 0 0       if ((defined $bytes_read) && ($bytes_read > 0))
1132             {
1133 0           $vt->process ($cmdbuf);
1134 0 0         syswrite STDERR, $cmdbuf if (! -t STDERR);
1135             }
1136             }
1137            
1138             # End processing if we've gone 1 round after command died with no output.
1139 0 0 0       $eof = 1 if ($vt_process->{DIED} && $cmdbuf eq $EMPTY_STRING);
1140              
1141             # Do your stuff here - use $vt->row_plaintext() to see what's on various
1142             # rows of the screen, for instance, or before this main loop you could set
1143             # up a ROWCHANGE callback which checks the changed row, or whatever.
1144              
1145             # In this example, we just pass standard input to the SSH command, and we
1146             # take the data coming back from SSH and pass it to the Term::VT102 object,
1147             # and then we repeatedly dump the Term::VT102 screen.
1148              
1149             # Read key presses from standard input and pass them to the command
1150             # running in the child process.
1151 0 0         if (vec ($rout, $vt_process->{IOT}->fileno, 1))
1152             {
1153 0           my $stdinbuf = $EMPTY_STRING ;
1154 0           my $bytes_read = $vt_process->{IOT}->sysread ($stdinbuf, $BUFFER_READ_SIZE );
1155 0 0 0       $eof = 1 if ((defined $bytes_read) && ($bytes_read == 0));
1156 0 0 0       $vt_process->{PTY}->syswrite ($stdinbuf, $bytes_read) if ((defined $bytes_read) && ($bytes_read > 0));
1157             }
1158              
1159             # Dump what Term::VT102 thinks is on the screen. We only output rows
1160             # we know have changed, to avoid generating too much output.
1161 0           my $didout = 0;
1162 0           foreach my $row (sort keys %{ $vt_process->{TERMINAL_CHANGE_BUFFER} })
  0            
1163             {
1164 0           printf "\e[%dH%s\r", $row, $vt->row_sgrtext ($row);
1165 0           $screen_data .= sprintf "\e[%dH%s\r", $row, $vt->row_sgrtext ($row);
1166            
1167 0           delete $vt_process->{TERMINAL_CHANGE_BUFFER}{$row};
1168 0           $didout ++;
1169             }
1170            
1171 0 0 0       if (($didout > 0) || ($vt_process->{PREVXY} ne $EMPTY_STRING . $vt->x . q{,} . $vt->y))
1172             {
1173 0 0         printf "\e[%d;%dH", $vt->y, ($vt->x > $vt->cols ? $vt->cols : $vt->x);
1174            
1175 0 0         $screen_data .= sprintf "\e[%d;%dH", $vt->y, ($vt->x > $vt->cols ? $vt->cols : $vt->x);
1176             #todo: shouldn't prevxy be updated here?
1177             }
1178              
1179             # Make sure the child process has not died.
1180 0 0         $vt_process->{DIED} = 1 if (waitpid ($vt_process->{PID}, WNOHANG) > 0);
1181              
1182 0           return($eof, $screen_data, $vt->x(), $vt->y()) ;
1183             }
1184              
1185             #---------------------------------------------------------------------------------------------------------
1186              
1187             1 ;
1188              
1189             =head1 BUGS AND LIMITATIONS
1190              
1191             None so far.
1192              
1193             =head1 AUTHOR
1194              
1195             Nadim ibn hamouda el Khemir
1196             CPAN ID: NH
1197             mailto: nadim@cpan.org
1198              
1199             =head1 LICENSE AND COPYRIGHT
1200              
1201             This program is free software; you can redistribute
1202             it and/or modify it under the same terms as Perl itself.
1203              
1204             =head1 SUPPORT
1205              
1206             You can find documentation for this module with the perldoc command.
1207              
1208             perldoc App::Textcast
1209              
1210             You can also look for information at:
1211              
1212             =over 4
1213              
1214             =item * AnnoCPAN: Annotated CPAN documentation
1215              
1216             L
1217              
1218             =item * RT: CPAN's request tracker
1219              
1220             Please report any bugs or feature requests to L .
1221              
1222             We will be notified, and then you'll automatically be notified of progress on
1223             your bug as we make changes.
1224              
1225             =item * Search CPAN
1226              
1227             L
1228              
1229             =back
1230              
1231             =head1 SEE ALSO
1232              
1233             screen (1), script(1), aewan, vte(1), evilvte(1).
1234              
1235             =cut