File Coverage

blib/lib/App/VTide/Command/Run.pm
Criterion Covered Total %
statement 39 273 14.2
branch 0 124 0.0
condition 0 72 0.0
subroutine 13 35 37.1
pod 12 13 92.3
total 64 517 12.3


line stmt bran cond sub pod time code
1             package App::VTide::Command::Run;
2              
3             # Created on: 2016-01-30 15:06:40
4             # Create by: Ivan Wills
5             # $Id$
6             # $Revision$, $HeadURL$, $Date$
7             # $Revision$, $Source$, $Date$
8              
9 4     4   2113 use Moo;
  4         8  
  4         33  
10 4     4   11353 use warnings;
  4         10  
  4         120  
11 4     4   22 use version;
  4         7  
  4         28  
12 4     4   304 use Carp qw/carp longmess/;
  4         10  
  4         221  
13 4     4   25 use English qw/ -no_match_vars /;
  4         8  
  4         32  
14 4     4   1586 use Hash::Merge::Simple qw/ merge /;
  4         27  
  4         185  
15 4     4   26 use Path::Tiny;
  4         7  
  4         214  
16 4     4   2101 use File::stat;
  4         28101  
  4         20  
17 4     4   1716 use File::chdir;
  4         5564  
  4         381  
18 4     4   2369 use IO::Prompt qw/prompt/;
  4         75973  
  4         27  
19 4     4   2314 use Algorithm::Cron;
  4         54240  
  4         161  
20 4     4   1795 use List::MoreUtils qw/uniq/;
  4         40067  
  4         35  
21 4     4   4044 use Data::Dumper qw/Dumper/;
  4         14  
  4         13447  
22              
23             extends 'App::VTide::Command';
24              
25             our $VERSION = version->new('1.0.5');
26             our $NAME = 'run';
27             our $OPTIONS = [ 'name|n=s', 'test|T!', 'save|s=s', 'verbose|v+', ];
28             our $LOCAL = 1;
29 0     0 1   sub details_sub { return ( $NAME, $OPTIONS, $LOCAL ) }
30              
31             has first => (
32             is => 'rw',
33             default => 1,
34             );
35              
36             has base => ( is => 'rw', default => $CWD );
37              
38             sub run {
39 0     0 1   my ($self) = @_;
40              
41 0           my ($name) = $self->session_dir( $self->defaults->{name} );
42 0   0       my $cmd = $self->options->files->[0] || '';
43 0           $ENV{VTIDE_TERM} = $cmd;
44              
45 0           my $params = $self->params($cmd);
46 0           my @cmd = $self->command($params);
47 0           $self->log( 'START', @cmd );
48              
49 0           @ARGV = ();
50 0 0 0       if (
      0        
      0        
51             !(
52             $self->first
53             && ( $params->{watch} || $params->{cron} )
54             && $params->{wait}
55             )
56             )
57             {
58              
59 0 0         if ( $params->{clear} ) {
60 0           system 'clear';
61             }
62 0 0         if ( $self->first ) {
63 0           print "Running $name - $cmd\n";
64             }
65              
66 0 0         if ( $params->{heading} ) {
67              
68             # show terminal heading if desired
69 0           print $params->{heading}, "\n";
70             }
71              
72 0 0 0       if ( !$self->defaults->{test} && $params->{wait} ) {
73 0           print join ' ', @cmd, "\n";
74 0           print "Press enter to start : ";
75 0           my $ans = <ARGV>;
76 0 0 0       if ( !$ans || !ord $ans ) {
77 0           print "\n";
78 0           return;
79             }
80             }
81              
82 0           $self->load_env( $params->{env} );
83 0           local $CWD = $CWD;
84 0           $self->base($CWD);
85 0 0 0       if ( $params->{dir} && -d $params->{dir} ) {
86 0           $CWD = $params->{dir};
87             }
88              
89 0 0 0       if ( $self->defaults->{verbose} || $self->defaults->{test} ) {
90 0 0         warn "Will wait before starting\n" if $params->{wait};
91 0 0         warn "Will restart on exit\n" if $params->{restart};
92             }
93              
94             # run any hooks for run_running
95 0           $self->hooks->run( 'run_running', \@cmd );
96              
97             # start the terminal
98             eval {
99 0           $self->runit(@cmd);
100 0           1;
101 0 0         } or do {
102 0           $self->log( "RUN ERROR", @cmd, $@ );
103             }
104             }
105              
106             # flag this is no longer the first run
107 0           $self->first(0);
108              
109 0 0 0       if ( !$self->defaults->{test} && $self->restart($cmd) ) {
110 0           return $self->run;
111             }
112              
113 0           return;
114             }
115              
116             sub restart {
117 0     0 1   my ( $self, $cmd, $no_watch ) = @_;
118              
119 0           my $params = $self->params($cmd);
120              
121 0 0 0       return $self->watch($cmd) if !$no_watch && $params->{watch};
122 0 0 0       return $self->cron($cmd) if !$no_watch && $params->{cron};
123              
124 0 0         return if !$params->{restart};
125              
126             my %action = (
127             q => {
128             msg => 'quit',
129 0     0     exec => sub { 0; },
130             },
131             c => {
132             msg => 'clear screen',
133             exec => sub {
134 0     0     system "clear";
135 0           $self->restart( $cmd, $no_watch );
136             },
137             },
138             s => {
139             msg => 'Show command',
140             exec => sub {
141 0     0     my $params = $self->params($cmd);
142 0           print "\nThis terminals command:\n";
143 0           print join ' ', $self->command($params), "\n\n";
144 0           $self->restart( $cmd, $no_watch );
145             },
146             },
147             r => {
148             msg => 'restart',
149 0     0     exec => sub { 1 },
150             },
151 0           );
152              
153 0 0         if ( $params->{restart} ne 1 ) {
154 0           my ($letter) = $params->{restart} =~ /^(.)/xms;
155             $action{$letter} = {
156             msg => $params->{restart},
157 0     0     exec => sub { exec $params->{restart}; },
158 0           };
159             }
160              
161             # show restart menu
162 0           my $menu = "Options:\n";
163 0           for my $letter ( sort keys %action ) {
164 0           $menu .= "$letter - $action{$letter}{msg}\n";
165             }
166 0           print $menu;
167              
168             # get answer
169 0           my $answer = <ARGV>;
170              
171 0 0         return if !$answer;
172              
173 0 0         chomp $answer if $answer;
174 0   0       $answer ||= $params->{default} || '';
      0        
175              
176             # ask the question
177 0           while ( !$action{$answer} ) {
178 0           print $menu;
179 0           print "Please choose one of " . ( join ', ', sort keys %action ) . "\n";
180 0           $answer = <ARGV>;
181 0 0         chomp $answer if $answer;
182 0   0       $answer ||= $params->{default} || '';
      0        
183             }
184              
185 0           return $action{$answer}{exec}->();
186             }
187              
188             sub watch {
189 0     0 1   my ( $self, $cmd ) = @_;
190              
191 0           my $params = $self->params($cmd);
192             my @files = $self->command(
193             {
194             editor => { command => undef },
195             edit => $params->{watch},
196             },
197 0           );
198              
199 0           my %stats;
200 0           for my $file (@files) {
201 0 0 0       next if !$file || !-f $file;
202 0           $stats{$file} = stat $file;
203             }
204              
205 0           while (1) {
206 0           my $done = 0;
207             local $SIG{INT} =
208 0 0   0     sub { $done = $self->restart( $cmd, 1 ) ? 1 : undef; };
  0            
209              
210 0           sleep 1;
211              
212 0           for my $file (@files) {
213              
214             # return if interrupted
215 0 0         return 1 if $done;
216              
217             # return if asked to quit
218 0 0         return if !defined $done;
219              
220 0 0 0       next if !$file || !-f $file;
221 0           my $stat = stat $file;
222 0 0         return 1 if $stats{$file}->mtime ne $stat->mtime;
223             }
224             }
225              
226 0           return;
227             }
228              
229             sub cron {
230 0     0 1   my ( $self, $cmd ) = @_;
231              
232 0           my $params = $self->params($cmd);
233             my $cron = Algorithm::Cron->new(
234             base => 'local',
235             crontab => $params->{cron},
236 0           );
237              
238 0           while (1) {
239 0           my $done = 0;
240             local $SIG{INT} =
241 0 0   0     sub { $done = $self->restart( $cmd, 1 ) ? 1 : undef; };
  0            
242              
243 0           my $next_time = $cron->next_time(time);
244              
245             #sleep $next_time - time;
246 0           sleep 1;
247              
248             # return if interrupted
249 0 0         return 1 if $done;
250              
251             # return if asked to quit
252 0 0         return if !defined $done;
253              
254 0 0         if ( $params->{cron_verbose} ) {
255 0           print {*STDERR} "\33[2K\r" . pretty_time( $next_time - time );
  0            
256             }
257 0 0         if ( $next_time <= time ) {
258 0 0         if ( $params->{cron_verbose} ) {
259 0           print {*STDERR} "\33[2K\r";
  0            
260             }
261 0           return 1;
262             }
263             }
264              
265 0           return;
266             }
267              
268             sub pretty_time {
269 0     0 1   my ($time) = @_;
270              
271 0           my $pretty = '';
272 0           my $days = int $time / ( 24 * 60 * 60 );
273 0           my $hours = int $time / ( 60 * 60 ) - $days * 24;
274 0           my $minutes = int $time / 60 - $days * 24 * 60 - $hours * 60;
275 0           my $seconds = $time % 60;
276              
277 0           return "$days days $hours hours $minutes minutes $seconds seconds";
278             }
279              
280             sub params {
281 0     0 1   my ( $self, $cmd ) = @_;
282              
283 0 0         if ( !$cmd ) {
284 0           warn "No \$cmd passed to params()\n", longmess();
285             }
286              
287 0           my $config = $self->config->get;
288 0   0       my $params = $config->{terminals}{$cmd} || {};
289              
290 0 0         if ( ref $params eq 'ARRAY' ) {
291 0 0         $params = { command => @{$params} ? $params : '' };
  0            
292             }
293              
294 0 0 0       if ( !$params->{command} && !$params->{edit} ) {
295 0           $params->{command} = 'bash';
296 0           $params->{wait} = 0;
297             }
298              
299 0   0       return merge $config->{default} || {}, $params;
300             }
301              
302             sub command_param {
303 0     0 1   my ( $self, $param ) = @_;
304              
305 0           my ($user_param) = $param =~ /^[{]:(\w+):[}]$/;
306              
307 0 0         return $param if !$user_param;
308              
309 0           my $value = prompt "$user_param : ";
310 0           chomp $value;
311              
312 0           return $value;
313             }
314              
315             sub command {
316 0     0 1   my ( $self, $params, $recurse ) = @_;
317              
318 0 0         if ( !$params->{edit} ) {
319             return
320             ref $params->{command}
321 0           ? map { $self->command_param($_) } @{ $params->{command} }
  0            
322 0 0         : ( $params->{command} );
323             }
324              
325             my $editor =
326             ref $params->{editor}{command}
327             ? $params->{editor}{command}
328 0 0         : $self->config->get->{editor}{command};
329              
330             my @globs =
331 0 0         ref $params->{edit} ? @{ $params->{edit} } : ( $params->{edit} );
  0            
332              
333 0   0       my $title = $params->{title} || $globs[0];
334 0           my $max = 15;
335 0 0         if ( length $title > $max ) {
336 0           $title = substr $title, ( length $title ) - $max, $max + 1;
337             }
338              
339 0 0         eval { require Term::Title; }
  0            
340             and Term::Title::set_titlebar($title);
341 0           system 'tmux', 'rename-window', $title;
342              
343 0           my $helper_text = $self->config->get->{editor}{helper};
344 0           my $helper;
345             eval {
346 0 0         if ($helper_text) {
    0          
347 0           $helper = eval $helper_text; ## no critic
348 0 0         die "No helper generated!" if !$helper;
349             }
350             elsif ( $self->defaults->{verbose} ) {
351 0           warn "No helper text";
352             }
353 0           1;
354 0 0         } or do {
355 0           warn $@;
356 0           warn $helper_text;
357             };
358              
359 0           my $groups = $self->config->get->{editor}{files};
360 0           my @files = $self->_globs2files( $groups, $helper, $recurse, @globs );
361              
362 0           return ( @$editor, @files );
363             }
364              
365             sub _globs2files {
366 0     0     my ( $self, $groups, $helper, $recurse, @globs ) = @_;
367 0           my @files;
368 0           my $count = 0;
369              
370             GLOB:
371 0           while ( my $glob = shift @globs ) {
372 0 0         last if $count++ > 30;
373 0           my ($not_glob) = $glob =~ /^[!](.*)$/;
374              
375 0 0         if ($not_glob) {
    0          
    0          
376 0           my %not_files = map { $_ => 1 }
  0            
377             $self->_globs2files( $groups, $helper, $recurse, $not_glob );
378 0           @files = grep { !$not_files{$_} } @files;
  0            
379 0           next GLOB;
380             }
381             elsif ( $groups->{$glob} ) {
382 0           unshift @globs, @{ $groups->{$glob} };
  0            
383 0           next GLOB;
384             }
385             elsif ($helper) {
386 0           my @g;
387             eval {
388 0           @g = $helper->( $self, $glob );
389 0           1;
390 0 0         } or do { warn $@ };
  0            
391              
392 0 0         if (@g) {
393 0           push @files, grep { -f $_ } @g;
  0            
394 0           unshift @globs, grep { !-f $_ } @g;
  0            
395 0           next GLOB;
396             }
397             }
398 0 0 0       if ( $recurse && -d $glob ) {
399             push @files, map {
400 0 0         -d $_
  0            
401             ? $self->_globs2files( $groups, $helper, $recurse, $_ )
402             : $_
403             } $self->_globs2files( $groups, $helper, $recurse, "$glob/*" );
404 0           next GLOB;
405             }
406              
407 0           push @files, $self->_dglob($glob);
408             }
409              
410 0           return uniq @files;
411             }
412              
413             sub _shell_quote {
414 0     0     my ($file) = @_;
415 0           $file =~ s/([\s&;*'"])/\\$1/gxms;
416 0           return $file;
417             }
418              
419             sub load_env {
420 0     0 1   my ( $self, $env_extra ) = @_;
421 0 0 0       if ( $env_extra && ref $env_extra eq 'HASH' ) {
422 0           for my $env ( keys %{$env_extra} ) {
  0            
423 0   0       my $orig = $ENV{$env} // '';
424 0           $ENV{$env} = $env_extra->{$env};
425 0           $ENV{$env} =~ s/[\$]$env/$orig/xms;
426             }
427             }
428              
429 0           return;
430             }
431              
432             sub runit {
433 0     0 1   my ( $self, @cmd ) = @_;
434              
435             print +( join " \\\n ", @cmd ), "\n"
436 0 0 0       if $self->defaults->{test} || $self->defaults->{verbose};
437              
438 0 0         return if $self->defaults->{test};
439              
440 0 0         if ( @cmd > 1 ) {
441 0           my $found = 0;
442 0           for my $dir ( split /:/xms, $ENV{PATH} ) {
443 0 0 0       if ( -d $dir && -x path $dir, $cmd[0] ) {
444 0           $found = 1;
445 0           last;
446             }
447             }
448              
449 0 0         if ( !$found ) {
450 0           @cmd = ( join ' ', @cmd );
451             }
452             }
453              
454 0           $self->log( 'SYSTEM', @cmd );
455 0           my $err = system @cmd;
456 0 0         if ($err) {
457 0           $self->log( "ERRORED", @cmd );
458             }
459             }
460              
461             sub log {
462 0     0 0   my ( $self, @msg ) = @_;
463 0           my $fh = path( $self->base, '.vtide', 'run.log' )->opena;
464 0           print {$fh} '[' . localtime . "] RUN $ENV{VTIDE_TERM} " . join ' ',
  0            
465             @msg . "\n";
466             }
467              
468             sub auto_complete {
469 0     0 1   my ($self) = @_;
470              
471 0           my $env = $self->options->files->[-1];
472 0           my @files = sort keys %{ $self->config->get->{terminals} };
  0            
473              
474 0 0         print join ' ', grep { $env ne 'run' ? /^$env/xms : 1 } @files;
  0            
475              
476 0           return;
477             }
478              
479             1;
480              
481             __END__
482              
483             =head1 NAME
484              
485             App::VTide::Command::Run - Run a terminal command
486              
487             =head1 VERSION
488              
489             This documentation refers to App::VTide::Command::Run version 1.0.5
490              
491             =head1 SYNOPSIS
492              
493             vtide run [(-n|--name) project] [--test] terminal
494             vtide run [--help|--man]
495              
496             OPTIONS:
497             -n --name[=]str The name of the terminal to run
498             -T --test Test the running of the terminal (shows the commands
499             that would be executed)
500             -v --verbose Show more verbose output.
501             --help Show this help
502             --man Show full documentation
503              
504             =head1 DESCRIPTION
505              
506             The C<run> command runs a terminal with what ever is configured for that
507             terminal. A full description of the terminal configuration can be found in
508             L<App::VTide::Configuration/terminals>.
509              
510             =head1 SUBROUTINES/METHODS
511              
512             =head2 C<run ()>
513              
514             Runs the terminal command
515              
516             =head2 C<restart ( $cmd )>
517              
518             Checks if the terminal command should be restarted on exit (and asks if it should)
519              
520             =head2 C<params ( $cmd )>
521              
522             Gets the configuration for the command C<$cmd>
523              
524             =head2 C<command ( $params )>
525              
526             Gets the command to execute, either a simple command or an "editor" command
527             where the files are got from the groups
528              
529             =head2 C<command_param ( $params )>
530              
531             Processes any found user parameters
532              
533             =head2 C<_shell_quote ( $file )>
534              
535             Quote C<$file> for shell execution
536              
537             =head2 C<load_env ( %env )>
538              
539             Put the values of %env into the %ENV variable.
540              
541             =head2 C<runit ( @cmd )>
542              
543             Executes a command (with --test skipping)
544              
545             =head2 C<watch ( $cmd )>
546              
547             Watches files till they change then returns.
548              
549             =head2 C<cron ( $cmd )>
550              
551             Runs the command based on cron tab settings in params
552              
553             =head2 C<pretty_time ( $time )>
554              
555             Creates a mildly pretty version of the number of seconds
556              
557             =head2 C<auto_complete ()>
558              
559             Auto completes terminal names
560              
561             =head2 C<details_sub ()>
562              
563             Returns the commands details.
564              
565             =head1 ATTRIBUTES
566              
567             =head2 first
568              
569             Track first run vs later runs (for things like waiting)
570              
571             =head1 HOOKS
572              
573             =head2 C<run_running ($cmd)>
574              
575             Called just before execution, the command that will be executed is
576             passed and can be modified.
577              
578             =head1 DIAGNOSTICS
579              
580             =head1 CONFIGURATION AND ENVIRONMENT
581              
582             =head1 DEPENDENCIES
583              
584             =head1 INCOMPATIBILITIES
585              
586             =head1 BUGS AND LIMITATIONS
587              
588             There are no known bugs in this module.
589              
590             Please report problems to Ivan Wills (ivan.wills@gmail.com).
591              
592             Patches are welcome.
593              
594             =head1 AUTHOR
595              
596             Ivan Wills - (ivan.wills@gmail.com)
597              
598             =head1 LICENSE AND COPYRIGHT
599              
600             Copyright (c) 2016 Ivan Wills (14 Mullion Close, Hornsby Heights, NSW Australia 2077).
601             All rights reserved.
602              
603             This module is free software; you can redistribute it and/or modify it under
604             the same terms as Perl itself. See L<perlartistic>. This program is
605             distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
606             without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
607             PARTICULAR PURPOSE.
608              
609             =cut