File Coverage

lib/ChordPro/Paths.pm
Criterion Covered Total %
statement 130 195 66.6
branch 28 70 40.0
condition 3 12 25.0
subroutine 21 31 67.7
pod 0 22 0.0
total 182 330 55.1


line stmt bran cond sub pod time code
1             #! perl
2              
3 90     90   1446 use v5.26;
  90         406  
4 90     90   70452 use Object::Pad;
  90         1319329  
  90         562  
5 90     90   16869 use utf8;
  90         218  
  90         975  
6 90     90   3367 use Carp;
  90         210  
  90         24994  
7              
8             class ChordPro::Paths;
9              
10             my $instance;
11              
12             # Work around Object::Pad 0.817 breakage.
13             #method get :common ( $reset = 0 ) {
14             # undef $instance if $reset;
15             # $instance //= $class->new;
16             #}
17              
18 379     379 0 1004 sub get( $class, $reset = 0 ) {
  379         905  
  379         934  
  379         955  
19 379 50       1587 undef $instance if $reset;
20 379   66     6281 $instance //= $class->new;
21             }
22              
23 90     90   21855 use Cwd qw(realpath);
  90         254  
  90         6716  
24 90     90   59824 use File::HomeDir;
  90         843472  
  90         9700  
25 90     90   852 use ChordPro::Files;
  90         193  
  90         889871  
26              
27 0     0   0 field $home :reader; # dir
  0         0  
28 0     0 0 0 field $configdir :reader; # dir
  0         0  
29 0     0 0 0 field $privlib :reader; # dir
  0         0  
30 9     9 0 29 field $resdirs :reader; # [ dir, ... ]
  9         74  
31 91     91 0 547 field $configs :reader; # { config => dir, ... }
  91         593  
32 0     0 0 0 field $pathsep :reader; # : or ;
  0         0  
33              
34 49     49 0 243 field $packager :reader;
  49         250  
35              
36             # Cwd::realpath always returns forward slashes.
37             # On Windows, Cwd::realpath always returns a volume.
38              
39             BUILD {
40             my $app = "ChordPro";
41             my $app_lc = lc($app);
42              
43             $pathsep = is_msw ? ';' : ':';
44              
45             $home = realpath( $ENV{HOME} = File::HomeDir->my_home );
46              
47             # $desktop = File::HomeDir->my_desktop;
48             # $docs = File::HomeDir->my_documents;
49             # $music = File::HomeDir->my_music;
50             # $pics = File::HomeDir->my_pictures;
51             # $videos = File::HomeDir->my_videos;
52             # $data = File::HomeDir->my_data;
53             # $dist = File::HomeDir->my_dist_data('ChordPro');
54             # $dist = File::HomeDir->my_dist_config('ChordPro');
55              
56             # Establish config files. Global config is easy.
57             for ( $self->normalize("/etc/$app_lc.json") ) {
58             next unless $_ && -f;
59             $configs->{sysconfig} = $_;
60             }
61              
62             $configs = {};
63             # The user specific config requires some alternatives.
64             # -d $XDG_CONFIG_HOME/$app_lc
65             # -d ~/.config/$app_lc
66             # -d ~/.$app_lc
67             # -d my_dist_config
68             my @try;
69             if ( defined( $ENV{XDG_CONFIG_HOME} ) && $ENV{XDG_CONFIG_HOME} ne "" ) {
70             push( @try,
71             # See https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
72             # fn_catdir( $ENV{XDG_CONFIG_HOME}, ".config", $app_lc ),
73             # fn_catdir( $ENV{XDG_CONFIG_HOME}, ".config" ),
74             # fn_catdir( $ENV{XDG_CONFIG_HOME}, ".$app_lc" ) );
75             fn_catdir( $ENV{XDG_CONFIG_HOME}, "$app_lc" ) );
76             }
77             else {
78             push( @try,
79             fn_catdir( $home, ".config", $app_lc ),
80             fn_catdir( $home, ".$app_lc" ),
81             File::HomeDir->my_dist_config($app) );
82             }
83              
84             for ( @try ) {
85             next unless $_ && fs_test( d => $_);
86             my $path = $self->normalize($_);
87             warn("Paths: configdir try $_ => $path\n") if $self->debug > 1;
88             next unless $path && fs_test( d => $path);
89             $configdir = $path;
90             for ( $self->normalize( fn_catfile( $path, "$app_lc.prp" ) ),
91             $self->normalize( fn_catfile( $path, "$app_lc.json" ) ) ) {
92             next unless $_ && fs_test( f => $_ );
93             $configs->{userconfig} = $_;
94             last;
95             }
96             last if $configdir;
97             }
98             warn("Paths: configdir = ", $configdir // "", "\n") if $self->debug;
99              
100             for ( $self->normalize(".$app_lc.json"),
101             $self->normalize("$app_lc.json") ) {
102             next unless $_ && fs_test( f => $_ );
103             $configs->{config} = $_;
104             last;
105             }
106             if ( $self->debug ) {
107             for ( qw( sysconfig userconfig config ) ) {
108             warn(sprintf("Paths: %-10s = %s\n",
109             $_, $configs->{$_} // "" ) );
110             }
111             }
112              
113             # Private lib.
114             $privlib = $INC{'ChordPro.pm'} =~ s/\.pm$/\/lib/r;
115              
116             # Now for the resources.
117             $self->setup_resdirs;
118              
119             # Check for packaged image.
120             for ( qw( OCI Docker AppImage PPL ) ) {
121             next unless exists $ENV{uc($_)."_PACKAGED"}
122             && $ENV{uc($_)."_PACKAGED"};
123             $packager = $_;
124             last;
125             }
126              
127             };
128              
129             # We need this to be able to re-establish the resdirs, e.g. after a change
130             # of CHORDPRO_LIB.
131             method setup_resdirs {
132             $resdirs = [];
133             my @try = ();
134             push( @try, $self->path($ENV{CHORDPRO_LIB}) )
135             if defined($ENV{CHORDPRO_LIB});
136             push( @try, $configdir ) if $configdir;
137             push( @try, $INC{'ChordPro.pm'} =~ s/\.pm$/\/res/r );
138              
139             for ( @try ) {
140             next unless $_;
141             my $path = $self->normalize($_);
142             warn("Paths: resdirs try $_ => $path\n") if $self->debug > 1;
143             next unless $path && fs_test( d => $path );
144             push( @$resdirs, $path );
145             }
146              
147             if ( $self->debug ) {
148             for ( 0..$#{$resdirs} ) {
149             warn("Paths: resdirs[$_] = $resdirs->[$_]\n");
150             }
151             }
152              
153             unless ( @$resdirs ) {
154             warn("Paths: Cannot find resources, prepare for disaster\n");
155             }
156             }
157              
158             method debug {
159             # We need to take an env var into account, since the Paths
160             # singleton is created far before any config processing.
161             $ENV{CHORDPRO_DEBUG_PATHS} || $::config->{debug}->{paths} || 0;
162             }
163              
164             # Is absolute.
165              
166 0     0 0 0 method is_absolute ( $p ) {
  0         0  
  0         0  
  0         0  
167 0         0 fn_is_absolute( $p );
168             }
169              
170             # Is bare (no volume/dir).
171              
172 2     2 0 3 method is_here ( $p ) {
  2         7  
  2         3  
  2         3  
173 2         9 my ( $v, $d, $f ) = fn_splitpath($p);
174 2 50       17 $v eq '' && $d eq '';
175             }
176              
177             # Normalize - full path, forward slashes, ~ expanded.
178              
179 241     241 0 538 method normalize ( $p, %opts ) {
  241         740  
  241         483  
  241         725  
  241         401  
180 241 50       1042 $p = $home . "/$1" if $p =~ /~[\\\/](.*)/;
181 241         12314 realpath($p)
182             }
183              
184             # This is only used in ::runtimeinfo for display purposes.
185              
186 117     117 0 337 method display ( $p ) {
  117         531  
  117         355  
  117         191  
187 117 50       374 return "" unless defined $p;
188 117         417 $p = $self->normalize($p);
189 117 100       598 if ( index( $p, $home ) == 0 ) {
190 27         105 substr( $p, 0, length($home), '~' );
191             }
192 117         1260 return $p;
193             }
194              
195 0     0 0 0 method path ( $p = undef ) {
  0         0  
  0         0  
  0         0  
196 0 0       0 if ( defined($p) ) {
197 0         0 local $ENV{PATH} = $p;
198 0         0 my @p = File::Spec->path();
199             # On MSWindows, '.' is always prepended.
200 0 0       0 shift(@p) if is_msw;
201 0         0 return @p;
202             }
203 0         0 return File::Spec->path();
204             }
205              
206             # Prepend/append dirs to path.
207              
208 0     0 0 0 method pathprepend( @d ) {
  0         0  
  0         0  
  0         0  
209 0         0 $ENV{PATH} = $self->pathcombine( @d, $ENV{PATH} );
210             }
211              
212 0     0 0 0 method pathappend( @d ) {
  0         0  
  0         0  
  0         0  
213 0         0 $ENV{PATH} = $self->pathcombine( $ENV{PATH}, @d );
214             }
215              
216 0     0 0 0 method pathcombine( @d ) {
  0         0  
  0         0  
  0         0  
217 0         0 join( $pathsep, @d );
218             }
219              
220             # Locate an executable file (program) using PATH.
221              
222 0     0 0 0 method findexe ( $p, %opts ) {
  0         0  
  0         0  
  0         0  
  0         0  
223 0         0 my $try = $p;
224 0         0 my $found;
225 0 0       0 if ( is_msw ) {
226 0         0 $try .= ".exe";
227             }
228              
229 0 0 0     0 if ( fn_is_absolute($p)
230             && ChordPro::Files::fs_test( fx => $p ) ) {
231 0 0       0 warn("Paths: findexe $p => ", $self->display($p), "\n")
232             if $self->debug;
233 0         0 return $p;
234             }
235              
236 0         0 for ( $self->path ) {
237 0         0 my $e = fn_catfile( $_, $try );
238 0 0       0 $found = realpath($e), last if fs_test( fx => $e );
239             }
240 0 0 0     0 if ( $self->debug ) {
    0          
241 0         0 warn("Paths: findexe $p => ", $self->display($found), "\n");
242             }
243             elsif ( !$found && !$opts{silent} ) {
244             warn("Could not find $p in ",
245 0         0 join( " ", map { qq{"$_"} } $self->path ), "\n");
  0         0  
246             }
247 0         0 return $found;
248             }
249              
250             # Locate a config file (prp or json) using respath.
251              
252 119     119 0 312 method findcfg ( $p ) {
  119         784  
  119         306  
  119         196  
253 119         302 my $found;
254             my @p;
255 119 50       699 if ( $p =~ /\.\w+$/ ) {
256 0 0       0 $found = realpath($p) if fs_test( fs => $p );
257 0         0 @p = ( $p );
258             }
259             else {
260 119         378 $p =~ s/:+/\//g;
261 119         593 @p = ( "$p.prp", "$p.json" );
262             }
263 119 50       439 unless ( $found ) {
264 119         390 for ( @$resdirs ) {
265 119         310 for my $cfg ( @p ) {
266 238         10624 my $f = fn_catfile( $_, "config", $cfg );
267 238 100       1012 $found = realpath($f), last if fs_test( fs => $f );
268             }
269             }
270             }
271 119 50       1128 warn("Paths: findcfg $p => ", $self->display($found), "\n")
272             if $self->debug;
273 119         721 return $found;
274             }
275              
276             # Locate a resource file (optionally classified) using respath.
277              
278 42     42 0 99 method findres ( $p, %opts ) {
  42         256  
  42         115  
  42         138  
  42         99  
279 42         98 my $try = $p;
280 42         91 my $found;
281 42 50       300 if ( fn_is_absolute($p) ) {
282 0         0 $found = realpath($p);
283             }
284             else {
285 42 100       180 if ( defined $opts{class} ) {
286 29         263 $try = fn_catfile( $opts{class}, $try );
287             }
288 42         184 for ( @$resdirs ) {
289 42         297 my $f = fn_catfile( $_, $try );
290 42 100       1516 $found = realpath($f), last if fs_test( fs => $f );
291             }
292             }
293 42 0       396 warn("Paths: findres", $opts{class} ? " [$opts{class}]" : "",
    50          
294             " $p => ", $self->display($found), "\n")
295             if $self->debug;
296 42         957 return $found;
297             }
298              
299             # Locate resource directories (optionally classified) using respath.
300              
301 49     49 0 134 method findresdirs ( $p, %opts ) {
  49         327  
  49         145  
  49         114  
  49         92  
302 49         126 my $try = $p;
303 49         123 my @found;
304 49 50       255 if ( defined $opts{class} ) {
305 0         0 $p = fn_catdir( $opts{class}, $p );
306             }
307 49         206 for ( @$resdirs ) {
308 49         566 my $d = fn_catdir( $_, $p );
309 49 50       439 push( @found, realpath($d) ) if fs_test( d => $d );
310             }
311 49 50       526 if ( $self->debug ) {
312 0         0 my $i = 0;
313 0 0       0 @found = ( "" ) unless @found;
314             warn("Paths: findresdirs[",
315             $opts{class} ? "$opts{class}:" : "",
316             $i++, "]",
317 0 0       0 " $p => ", $self->display($_), "\n") for @found;
318             }
319 49         474 return \@found;
320             }
321              
322             # Return the name of a sibling (i.e., same place, different name
323             # and/or extension).
324              
325 10     10 0 41 method sibling ( $orig, %opts ) {
  10         89  
  10         53  
  10         67  
  10         21  
326             # Split.
327 10         76 my ( $v, $d, $f ) = fn_splitpath($orig);
328 10         32 my $res;
329 10 100       53 if ( $opts{name} ) {
330 2         7 $res = fn_catpath( $v, $d, $opts{name} );
331             }
332             else {
333             # Get base and extension.
334 8         130 my ( $b, $e ) = $f =~ /^(.*)(?:\.(\w+))$/;
335             # Adjust.
336 8 50       53 $b = $opts{base} if defined $opts{base};
337 8 50       52 $e = $opts{ext} if defined $opts{ext};
338             # New file name.
339 8         55 $f = $b;
340 8 50       37 $f .= $e if defined $e;
341             # Join with path.
342 8         71 $res = fn_catpath( $v, $d, $f );
343             }
344 10 50       128 warn("Paths: sibling $orig => ", $self->display($res), "\n")
345             if $self->debug;
346 10         54 return $res;
347             }
348              
349             # Given a file and a name, try name as a sibling, otherwise look it up.
350              
351 2     2 0 3 method siblingres ( $orig, $name, %opts ) {
  2         23  
  2         4  
  2         3  
  2         6  
  2         3  
352 2 50       6 return unless defined $orig;
353 2         7 my $try = $self->sibling( $orig, name => $name );
354             my $found = ( $try && fs_test( s => $try ) )
355             ? $try
356 2 50 33     9 : $self->findres( $name, class => $opts{class} );
357 2         10 return $found;
358             }
359              
360             method packager_version {
361             return unless $packager;
362             $ENV{uc($packager)."_PACKAGED"};
363             }
364              
365             ################ Export ################
366              
367             # For convenience.
368              
369 90     90   1190 use Exporter 'import';
  90         210  
  90         37964  
370             our @EXPORT;
371              
372 362     362 0 277054 sub CP() { __PACKAGE__->get }
  362         905  
  362         3411  
373              
374             push( @EXPORT, 'CP' );
375              
376             1;