File Coverage

blib/lib/Term/Filter.pm
Criterion Covered Total %
statement 25 88 28.4
branch 0 18 0.0
condition 0 6 0.0
subroutine 9 28 32.1
pod 9 9 100.0
total 43 149 28.8


line stmt bran cond sub pod time code
1             package Term::Filter;
2             BEGIN {
3 1     1   850 $Term::Filter::AUTHORITY = 'cpan:DOY';
4             }
5             {
6             $Term::Filter::VERSION = '0.03';
7             }
8 1     1   1410 use Moose::Role;
  1         5009  
  1         5  
9             # ABSTRACT: Run an interactive terminal session, filtering the input and output
10              
11 1     1   7267 use IO::Pty::Easy ();
  1         29386  
  1         19  
12 1     1   867 use IO::Select ();
  1         1643  
  1         30  
13 1     1   8 use Moose::Util::TypeConstraints 'subtype', 'as', 'where', 'message';
  1         3  
  1         16  
14 1     1   1712 use Scope::Guard ();
  1         419  
  1         16  
15 1     1   818 use Term::ReadKey ();
  1         4578  
  1         1116  
16              
17              
18             subtype 'Term::Filter::TtyFileHandle',
19             as 'FileHandle',
20             where { -t $_ },
21             message { "Term::Filter requires input and output filehandles to be attached to a terminal" };
22              
23              
24             has input => (
25             is => 'ro',
26             isa => 'Term::Filter::TtyFileHandle',
27             lazy => 1,
28             builder => '_build_input',
29             );
30              
31 0     0     sub _build_input { \*STDIN }
32              
33              
34             has output => (
35             is => 'ro',
36             isa => 'Term::Filter::TtyFileHandle',
37             lazy => 1,
38             builder => '_build_output',
39             );
40              
41 0     0     sub _build_output { \*STDOUT }
42              
43              
44              
45              
46             has input_handles => (
47             traits => ['Array'],
48             isa => 'ArrayRef[FileHandle]',
49             lazy => 1,
50             init_arg => undef,
51             builder => '_build_input_handles',
52             writer => '_set_input_handles',
53             handles => {
54             input_handles => 'elements',
55             add_input_handle => 'push',
56             _grep_input_handles => 'grep',
57             },
58             trigger => sub {
59             my $self = shift;
60             $self->_clear_select;
61             },
62             );
63              
64             sub _build_input_handles {
65 0     0     my $self = shift;
66 0           [ $self->input, $self->pty ]
67             }
68              
69             sub remove_input_handle {
70 0     0 1   my $self = shift;
71 0           my ($fh) = @_;
72             $self->_set_input_handles(
73 0     0     [ $self->_grep_input_handles(sub { $_ != $fh }) ]
  0            
74             );
75             }
76              
77              
78             has pty => (
79             is => 'ro',
80             isa => 'IO::Pty::Easy',
81             lazy => 1,
82             builder => '_build_pty',
83             );
84              
85 0     0     sub _build_pty { IO::Pty::Easy->new(raw => 0) }
86              
87             has _select => (
88             is => 'ro',
89             isa => 'IO::Select',
90             lazy => 1,
91             builder => '_build_select',
92             clearer => '_clear_select',
93             );
94              
95             sub _build_select {
96 0     0     my $self = shift;
97 0           return IO::Select->new($self->input_handles);
98             }
99              
100             has _raw_mode => (
101             is => 'rw',
102             isa => 'Bool',
103             default => 0,
104             init_arg => undef,
105             trigger => sub {
106             my $self = shift;
107             my ($val) = @_;
108             if ($val) {
109             Term::ReadKey::ReadMode(5, $self->input);
110             }
111             else {
112             Term::ReadKey::ReadMode(0, $self->input);
113             }
114             },
115             );
116              
117              
118             sub run {
119 0     0 1   my $self = shift;
120 0           my @cmd = @_;
121              
122 0           my $guard = $self->_setup(@cmd);
123              
124 0           LOOP: while (1) {
125 0           my ($r, undef, $e) = IO::Select->select(
126             $self->_select, undef, $self->_select,
127             );
128              
129 0           for my $fh (@$e) {
130 0           $self->read_error($fh);
131             }
132              
133 0           for my $fh (@$r) {
134 0 0         if ($fh == $self->input) {
    0          
135 0           my $got = $self->_read_from_handle($self->input, "STDIN");
136 0 0         last LOOP unless defined $got;
137              
138 0           $got = $self->munge_input($got);
139              
140             # XXX should i select here, or buffer, to make sure this
141             # doesn't block?
142 0           syswrite $self->pty, $got;
143             }
144             elsif ($fh == $self->pty) {
145 0           my $got = $self->_read_from_handle($self->pty, "pty");
146 0 0         last LOOP unless defined $got;
147              
148 0           $got = $self->munge_output($got);
149              
150             # XXX should i select here, or buffer, to make sure this
151             # doesn't block?
152 0           syswrite $self->output, $got;
153             }
154             else {
155 0           $self->read($fh);
156             }
157             }
158             }
159             }
160              
161             sub _setup {
162 0     0     my $self = shift;
163 0           my (@cmd) = @_;
164              
165 0 0 0       Carp::croak("Must be run attached to a tty")
166             unless -t $self->input && -t $self->output;
167              
168 0 0         $self->pty->spawn(@cmd) || Carp::croak("Couldn't spawn @cmd: $!");
169              
170 0           $self->_raw_mode(1);
171              
172 0           my $prev_winch = $SIG{WINCH};
173             $SIG{WINCH} = sub {
174 0     0     $self->pty->slave->clone_winsize_from($self->input);
175              
176 0           $self->pty->kill('WINCH', 1);
177              
178 0           $self->winch;
179              
180 0           $prev_winch->();
181 0           };
182              
183 0           my $setup_called;
184             my $guard = Scope::Guard->new(sub {
185 0     0     $SIG{WINCH} = $prev_winch;
186 0           $self->_raw_mode(0);
187 0 0         $self->cleanup if $setup_called;
188 0           });
189              
190 0           $self->setup(@cmd);
191 0           $setup_called = 1;
192              
193 0           return $guard;
194             }
195              
196             sub _read_from_handle {
197 0     0     my $self = shift;
198 0           my ($handle, $name) = @_;
199              
200 0           my $buf;
201 0           sysread $handle, $buf, 4096;
202 0 0 0       if (!defined $buf || length $buf == 0) {
203 0 0         Carp::croak("Error reading from $name: $!")
204             unless defined $buf;
205 0           return;
206             }
207              
208 0           return $buf;
209             }
210              
211              
212 0     0 1   sub setup { }
213 0     0 1   sub cleanup { }
214 0     0 1   sub munge_input { $_[1] }
215 0     0 1   sub munge_output { $_[1] }
216 0     0 1   sub read { }
217 0     0 1   sub read_error { }
218 0     0 1   sub winch { }
219              
220 1     1   9 no Moose::Role;
  1         2  
  1         9  
221 1     1   248 no Moose::Util::TypeConstraints;
  1         1  
  1         10  
222              
223              
224             1;
225              
226             __END__
227             =pod
228              
229             =head1 NAME
230              
231             Term::Filter - Run an interactive terminal session, filtering the input and output
232              
233             =head1 VERSION
234              
235             version 0.03
236              
237             =head1 SYNOPSIS
238              
239             package My::Term::Filter;
240             use Moose;
241             with 'Term::Filter';
242              
243             sub munge_input {
244             my $self = shift;
245             my ($got) = @_;
246             $got =~ s/\ce/E- Elbereth\n/g;
247             $got;
248             }
249              
250             sub munge_output {
251             my $self = shift;
252             my ($got) = @_;
253             $got =~ s/(Elbereth)/\e[35m$1\e[m/g;
254             $got;
255             }
256              
257             My::Term::Filter->new->run('nethack');
258              
259             =head1 DESCRIPTION
260              
261             This module is a L<Moose role|Moose::Role> which implements running a program
262             in a pty while being able to filter the data that goes into and out of it. This
263             can be used to alter the inputs and outputs of a terminal based program (as in
264             the L</SYNOPSIS>), or to intercept the data going in or out to record it or
265             rebroadcast it (L<App::Ttyrec> or L<App::Termcast>, for instance).
266              
267             This role is intended to be consumed by a class which implements its callbacks
268             as methods; for a simpler callback-based API, you may want to use
269             L<Term::Filter::Callback> instead.
270              
271             =head1 ATTRIBUTES
272              
273             =head2 input
274              
275             The input filehandle to attach to the pty's input. Defaults to STDIN.
276              
277             =head2 output
278              
279             The output filehandle to attach the pty's output to. Defaults to STDOUT.
280              
281             =head2 pty
282              
283             The L<IO::Pty::Easy> object that the subprocess will be run under. Defaults to
284             a newly created instance.
285              
286             =head1 METHODS
287              
288             =head2 input_handles
289              
290             Returns the filehandles which will be monitored for reading. This list defaults
291             to C<input> and C<pty>.
292              
293             =head2 add_input_handle($fh)
294              
295             Add an input handle to monitor for reading. After calling this method, the
296             C<read> callback will be called with C<$fh> as an argument whenever data is
297             available to be read from C<$fh>.
298              
299             =head2 remove_input_handle($fh)
300              
301             Remove C<$fh> from the list of input handles being watched for reading.
302              
303             =head2 run(@cmd)
304              
305             Run the command specified by C<@cmd>, as though via C<system>. The callbacks
306             that have been defined will be called at the appropriate times, to allow for
307             manipulating the data that is sent or received.
308              
309             =head1 CALLBACKS
310              
311             The following methods may be defined to interact with the subprocess:
312              
313             =over 4
314              
315             =item setup
316              
317             Called when the process has just been started. The parameters to C<run> are
318             passed to this callback.
319              
320             =item cleanup
321              
322             Called when the process terminates. Will not be called if C<setup> is never run
323             (for instance, if the process fails to start).
324              
325             =item munge_input
326              
327             Called whenever there is new data coming from the C<input> handle, before it is
328             passed to the pty. Must return the data to send to the pty (and the default
329             implementation does this), but can do other things with the data as well.
330              
331             =item munge_output
332              
333             Called whenever the process running on the pty has produced new data, before it
334             is passed to the C<output> handle. Must return the data to send to the
335             C<output> handle (and the default implementation does this), but can do other
336             things with the data as well.
337              
338             =item read
339              
340             Called when a filehandle other than C<input> or C<pty> has data available (so
341             will never be called unless you call C<add_input_handle> to register your
342             handle with the event loop). Receives the handle with data available as its
343             only argument.
344              
345             =item read_error
346              
347             Called when an exception state is detected in any handle in C<input_handles>
348             (including the default ones). Receives the handle with the exception state as
349             its only argument.
350              
351             =item winch
352              
353             Called whenever the parent process receives a C<SIGWINCH> signal, after it
354             propagates that signal to the subprocess. C<SIGWINCH> is sent to a process
355             running on a terminal whenever the dimensions of that terminal change. This
356             callback can be used to update any other handles watching the subprocess about
357             the new terminal size.
358              
359             =back
360              
361             =head1 BUGS
362              
363             No known bugs.
364              
365             Please report any bugs through RT: email
366             C<bug-term-filter at rt.cpan.org>, or browse to
367             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Term-Filter>.
368              
369             =head1 SEE ALSO
370              
371             L<IO::Pty::Easy>
372              
373             L<App::Termcast>
374              
375             L<App::Ttyrec>
376              
377             =head1 SUPPORT
378              
379             You can find this documentation for this module with the perldoc command.
380              
381             perldoc Term::Filter
382              
383             You can also look for information at:
384              
385             =over 4
386              
387             =item * AnnoCPAN: Annotated CPAN documentation
388              
389             L<http://annocpan.org/dist/Term-Filter>
390              
391             =item * CPAN Ratings
392              
393             L<http://cpanratings.perl.org/d/Term-Filter>
394              
395             =item * RT: CPAN's request tracker
396              
397             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Term-Filter>
398              
399             =item * Search CPAN
400              
401             L<http://search.cpan.org/dist/Term-Filter>
402              
403             =back
404              
405             =head1 AUTHOR
406              
407             Jesse Luehrs <doy at tozt dot net>
408              
409             =head1 COPYRIGHT AND LICENSE
410              
411             This software is copyright (c) 2012 by Jesse Luehrs.
412              
413             This is free software; you can redistribute it and/or modify it under
414             the same terms as the Perl 5 programming language system itself.
415              
416             =cut
417