File Coverage

blib/lib/App/chot/Optex.pm
Criterion Covered Total %
statement 11 99 11.1
branch 0 58 0.0
condition 0 18 0.0
subroutine 4 13 30.7
pod 0 2 0.0
total 15 190 7.8


line stmt bran cond sub pod time code
1             package App::chot::Optex;
2 1     1   9 use v5.14;
  1         2  
3 1     1   3 use warnings;
  1         1  
  1         35  
4              
5 1     1   4 use Exporter 'import';
  1         1  
  1         38  
6             our @EXPORT_OK = qw(detect_optex resolve_optex);
7              
8 1     1   3 use File::Basename qw(basename dirname);
  1         4  
  1         1154  
9              
10             my $DEBUG;
11              
12             sub _optex_dir {
13 0 0   0     $ENV{OPTEX_ROOT} || "$ENV{HOME}/.optex.d";
14             }
15              
16             sub detect_optex {
17 0     0 0   my $path = shift;
18 0 0         return unless -l $path;
19 0 0         return if basename($path) eq 'optex'; # optex itself, not managed by optex
20 0   0       my $target = readlink $path // return;
21 0           basename($target) eq 'optex';
22             }
23              
24             sub resolve_optex {
25 0     0 0   my($app, $name, $path) = @_;
26 0           $DEBUG = $app->debug;
27              
28 0           my $orig_name = $name;
29 0           my @result;
30              
31             # 1. optex symlink found
32 0           warn " optex: $path\n";
33 0           push @result, $path;
34              
35             # 2. Check alias in config.toml
36 0           my $alias_val = _get_alias($name);
37 0 0         if (defined $alias_val) {
38 0           my $config = _optex_dir() . "/config.toml";
39 0           warn " config: $config\n";
40 0           _print_alias($orig_name, $alias_val);
41 0           my $alias_cmd = _alias_command($alias_val);
42 0 0         if (defined $alias_cmd) {
43 0 0         warn " => $alias_cmd\n" if $DEBUG;
44 0           $name = $alias_cmd;
45             }
46             }
47              
48             # Search for real command in PATH, skipping optex symlinks
49 0           my @real = _find_real_command($name);
50 0 0         if (@real) {
51 0 0         warn " optex resolved '$name' => @real\n" if $DEBUG;
52 0           push @result, @real;
53             } else {
54 0 0         warn " optex: real command not found for '$name'\n" if $DEBUG;
55             }
56              
57             # Check for rc file (always use original name)
58 0           my $rc = _optex_dir() . "/$orig_name.rc";
59 0 0         if (-f $rc) {
60 0 0         warn " optex rc: $rc\n" if $DEBUG;
61 0           push @result, $rc;
62             }
63              
64 0           @result;
65             }
66              
67             sub _find_real_command {
68 0     0     my $name = shift;
69 0           my @path = split /:/, $ENV{PATH};
70 0           my @found;
71 0           for my $dir (@path) {
72 0           my $cmd = "$dir/$name";
73 0 0 0       next unless -x $cmd && ! -d $cmd;
74 0 0         next if detect_optex($cmd);
75 0           push @found, $cmd;
76             }
77 0           @found;
78             }
79              
80             my $_aliases;
81              
82             sub _get_alias {
83 0     0     my $name = shift;
84 0   0       $_aliases //= _load_aliases();
85 0           $_aliases->{$name};
86             }
87              
88             sub _alias_command {
89 0   0 0     my $val = shift // return;
90 0           my $cmd;
91 0 0 0       if (!ref $val) {
    0          
92 0           ($cmd) = $val =~ /^(\S+)/;
93             } elsif (ref $val eq 'ARRAY' && @$val) {
94 0           $cmd = $val->[0];
95             }
96 0 0         return unless defined $cmd;
97             # Skip wrapper commands (bash -c, env ..., exec ..., etc.)
98 0 0         return if $cmd =~ m{^(?:.*/)?(?:(?:ba)?sh|env|exec|expr)$};
99 0           return $cmd;
100             }
101              
102             sub _print_alias {
103 0     0     my($name, $val) = @_;
104 0 0         if (!ref $val) {
    0          
105 0           warn " alias: $name = $val\n";
106             } elsif (ref $val eq 'ARRAY') {
107 0           require JSON::PP;
108 0           my $json = JSON::PP->new->indent->space_after->canonical->encode($val);
109 0           $json =~ s/\n$//;
110 0           warn " alias: $name = $json\n";
111             }
112             }
113              
114             sub _load_aliases {
115 0     0     my $config = _optex_dir() . "/config.toml";
116 0 0         return {} unless -f $config;
117              
118             # Try TOML module
119 0           my $data = eval {
120 0           require TOML;
121 0 0         open my $fh, '<', $config or die "$config: $!\n";
122 0           local $/;
123 0           my $text = <$fh>;
124 0           my($hash, $err) = TOML::from_toml($text);
125 0 0         die $err if $err;
126 0           $hash;
127             };
128 0 0         if ($data) {
129 0   0       return $data->{alias} || {};
130             }
131 0 0 0       warn " optex: TOML parse failed: $@\n" if $DEBUG && $@;
132              
133             # Fallback: simple parser for string aliases
134 0           _parse_aliases_simple($config);
135             }
136              
137             sub _parse_aliases_simple {
138 0     0     my $config = shift;
139 0           my %alias;
140 0 0         open my $fh, '<', $config or return {};
141 0           my $in_alias = 0;
142 0           while (<$fh>) {
143 0           chomp;
144 0 0         if (/^\[alias\]/) {
145 0           $in_alias = 1;
146 0           next;
147             }
148 0 0         if (/^\[/) {
149 0           $in_alias = 0;
150 0           next;
151             }
152 0 0         next unless $in_alias;
153             # Match simple string assignments: key = "value"
154 0 0         if (/^\s*(\w[\w-]*)\s*=\s*"([^"]*)"/) {
155 0           $alias{$1} = $2;
156             }
157             }
158 0           \%alias;
159             }
160              
161             1;