File Coverage

blib/lib/App/chot.pm
Criterion Covered Total %
statement 44 160 27.5
branch 0 72 0.0
condition 0 12 0.0
subroutine 15 22 68.1
pod 0 2 0.0
total 59 268 22.0


line stmt bran cond sub pod time code
1             package App::chot;
2              
3             our $VERSION = "1.04";
4              
5 1     1   237676 use v5.14;
  1         3  
6 1     1   3 use warnings;
  1         1  
  1         77  
7              
8 1     1   430 use utf8;
  1         217  
  1         5  
9 1     1   24 use Encode;
  1         2  
  1         107  
10 1     1   510 use open IO => 'utf8', ':std';
  1         1070  
  1         6  
11 1     1   639 use Pod::Usage;
  1         50885  
  1         143  
12 1     1   8 use List::Util qw(any first);
  1         1  
  1         53  
13 1     1   414 use App::chot::Util;
  1         3  
  1         30  
14 1     1   374 use App::chot::Optex qw(detect_optex);
  1         2  
  1         58  
15 1     1   334 use App::chot::Found;
  1         2  
  1         32  
16 1     1   348 use Text::ParseWords qw(shellwords);
  1         1221  
  1         80  
17              
18 1     1   635 use Getopt::EX::Hashed; {
  1         13936  
  1         9  
19             Getopt::EX::Hashed->configure(DEFAULT => [ is => 'rw' ]);
20             has one => ' 1 ' ;
21             has debug => ' d + ' ;
22             has dryrun => ' n ' ;
23             has info => ' i ' ;
24             has raw => ' r ' ;
25             has help => ' h ' , action => sub {
26             pod2usage(-verbose => 99, -sections => [qw(SYNOPSIS)])
27             } ;
28             has list => ' l + ' ;
29             has deref => ' L ' ;
30             has man => ' m ' ;
31             has number => ' N ! ' , default => 0 ;
32             has version => ' v ' , action => sub { say "Version: $VERSION"; exit } ;
33             has pager => ' p =s ' ;
34             has column => ' C :i ' ;
35             has suffix => ' =s ' , default => [ qw( .pm ) ] ;
36             has type => ' t =s ' , default => 'Command:Perl:Python:Ruby:Node' ;
37             has py => ' ' , action => sub { $_->type('Python') } ;
38             has pl => ' ' , action => sub { $_->type('Perl') } ;
39             has rb => ' ' , action => sub { $_->type('Ruby') } ;
40             has nd => ' ' , action => sub { $_->type('Node') } ;
41             has bat_theme => ' % ' ,
42             default => { light => 'Coldark-Cold', dark => 'Coldark-Dark' } ;
43             has skip => ' =s@ ' ,
44             default => [] ;
45 1     1   494 } no Getopt::EX::Hashed;
  1         2  
  1         5  
46              
47             sub run {
48 0     0 0   my $app = shift;
49 0 0         @_ = map { utf8::is_utf8($_) ? $_ : decode('utf8', $_) } @_;
  0            
50 0           local @ARGV = splice @_;
51              
52 1     1   670 use Getopt::EX::Long qw(GetOptions Configure ExConfigure);
  1         112164  
  1         1794  
53 0           ExConfigure BASECLASS => [ __PACKAGE__, "Getopt::EX" ];
54 0           Configure qw(bundling no_getopt_compat);
55 0 0         $app->getopt || pod2usage();
56              
57 0           my $name = pop @ARGV;
58 0 0         if (!defined $name) {
59 0 0         if ($app->man) {
60 0   0       my $script = $ENV{CHOT_SCRIPT_PATH} // $0;
61 0           exec 'perldoc', $script;
62 0           die "perldoc: $!\n";
63             }
64 0           pod2usage();
65             }
66 0           my @option = splice @ARGV;
67 0   0       my $pager = $app->pager || $ENV{'CHOT_PAGER'} || _default_pager($app);
68              
69             #
70             # Load and instantiate all finder objects once.
71             # Each finder gets the same $app, $name, and shared $found,
72             # and is reused across -i, main, and -m dispatch below.
73             #
74 0           my $found = App::chot::Found->new;
75 0           my @finders; # [ [$type, $finder_obj], ... ]
76 0           for my $type (split /:+/, $app->type) {
77 0           $type = _normalize_type($type);
78 0           my $class = __PACKAGE__ . '::' . $type;
79 0 0         eval "require $class" or do { warn $@ if $app->debug; next };
  0 0          
  0            
80 0           push @finders, [
81             $type,
82             $class->new(app => $app, name => $name, found => $found),
83             ];
84             }
85              
86             # -i mode: print trace/resolution info and exit
87 0 0         if ($app->info) {
88 0           for my $pair (@finders) {
89 0           my($type, $h) = @$pair;
90 0 0         $h->get_info if $h->can('get_info');
91             }
92 0           return 0;
93             }
94              
95             #
96             # Main discovery loop: try each finder in order.
97             # Results are accumulated in $found so that later finders
98             # (e.g., Python) can use paths found by earlier ones (e.g., Command).
99             #
100 0           my @found;
101 0           for my $pair (@finders) {
102 0           my($type, $h) = @$pair;
103 0 0         warn "Trying finder: $type\n" if $app->debug;
104 0           my @paths = grep { defined } $h->get_path;
  0            
105 0 0         if (@paths) {
106 0 0         warn "Found by $type: @paths\n" if $app->debug;
107 0           push @found, @paths;
108 0           $found->add($type, @paths);
109 0 0         last if $app->one;
110             } else {
111 0 0         warn "Not found by $type\n" if $app->debug;
112             }
113             }
114              
115 0 0         if (not @found) {
116 0           warn "$name: Nothing found.\n";
117 0           return 1;
118             }
119              
120 0 0         if (my $level = $app->list) {
121 0 0         if ($level > 1) {
122 0 0         system 'ls', ($app->deref ? '-lL' : '-l'), @found;
123             } else {
124 0           say for @found;
125             }
126 0           return 0;
127             }
128              
129             #
130             # -m mode: try each finder's man_cmd in the order results were found.
131             # Finders return empty list to skip, allowing fallback to the next.
132             # Uses exec (not system) to preserve terminal/signal handling.
133             #
134 0 0         if ($app->man) {
135 0           my %finder_by_type = map { @$_ } @finders;
  0            
136 0           my $tried;
137 0           for my $type (@{$found->types}) {
  0            
138 0 0         my $h = $finder_by_type{$type} or next;
139 0 0         next unless $h->can('man_cmd');
140 0 0         my @cmd = $h->man_cmd or next;
141 0 0         if ($app->dryrun) {
142 0           say "@cmd";
143 0           $tried++;
144 0           next;
145             }
146 0           exec @cmd;
147 0           die "$type man: $!\n";
148             }
149 0 0         return $tried ? 0 : 1;
150             }
151              
152 0           @found = grep { !detect_optex($_) } @found;
  0            
153             @found = grep {
154 0 0         not &is_binary($_) or do {
  0 0          
155 0           system 'file', $_;
156 0           0;
157             }
158             } @found or return 0;
159              
160 0           my @pager_opts;
161 0 0         if (defined $app->number) {
162 0           my $pager_name = (shellwords($pager))[0];
163 0           $pager_name =~ s{.*/}{}; # basename
164 0 0         if ($pager_name eq 'bat') {
    0          
165 0 0         push @pager_opts, $app->number ? '--style=full' : '--style=header,grid,snip';
166             } elsif ($pager_name eq 'less') {
167 0 0         push @pager_opts, '-N' if $app->number;
168             }
169             }
170 0           my @cmd = (shellwords($pager), @pager_opts, @option, @found);
171 0 0         if (defined(my $col = $app->column)) {
172 0           @cmd = grep { $_ ne '--force-colorization' } @cmd;
  0            
173 0 0         unshift @cmd, 'nup', '-e', ($col ? ("-C$col") : ());
174             }
175 0 0         if ($app->dryrun) {
176 0           say "@cmd";
177 0           return 0;
178             }
179 0           exec @cmd;
180 0           die "$pager: $!\n";
181             }
182              
183 1     1   13 use List::Util qw(none);
  1         2  
  1         798  
184              
185             sub valid {
186 0     0 0   my $app = shift;
187 0           state $sub = do {
188 0           my @re = map { qr/\Q$_\E$/ } @{$app->skip};
  0            
  0            
189 0     0     sub { none { $_[0] =~ $_ } @re };
  0            
  0            
190             };
191 0           $sub->(@_);
192             }
193              
194             sub _default_pager {
195 0     0     my $app = shift;
196 0           state $pager = do {
197 0     0     my $bat = first { -x } map { "$_/bat" } split /:/, $ENV{PATH};
  0            
  0            
198 0 0         if ($bat) {
199 0   0       $ENV{BAT_THEME} //= _bat_theme($app->bat_theme);
200 0           "$bat --force-colorization";
201             } else {
202 0           'less';
203             }
204             };
205             }
206              
207             sub _bat_theme {
208 0     0     my $themes = shift;
209 0           my $lum = eval {
210 0           require Getopt::EX::termcolor;
211 0           Getopt::EX::termcolor::get_luminance();
212             };
213 0 0         return () unless defined $lum;
214 0 0         $themes->{$lum < 50 ? 'dark' : 'light'};
215             }
216              
217             sub _normalize_type {
218 0     0     my $type = shift;
219 0           state $map = {
220             command => 'Command',
221             perl => 'Perl',
222             python => 'Python',
223             ruby => 'Ruby',
224             node => 'Node',
225             };
226 0   0       $map->{lc $type} // ucfirst lc $type;
227             }
228              
229             1;
230              
231             __END__