File Coverage

blib/lib/Linux/DesktopFiles.pm
Criterion Covered Total %
statement 57 99 57.5
branch 17 60 28.3
condition 8 37 21.6
subroutine 4 6 66.6
pod 5 5 100.0
total 91 207 43.9


line stmt bran cond sub pod time code
1             package Linux::DesktopFiles;
2              
3             # This module is designed to be pretty fast.
4             # The best uses of this module is to generate real
5             # time menus, based on the content of desktop files.
6              
7 1     1   44672 use 5.014;
  1         3  
8              
9             #use strict;
10             #use warnings;
11              
12             our $VERSION = '0.23';
13              
14             our %TRUE_VALUES = (
15             'true' => 1,
16             'True' => 1,
17             '1' => 1
18             );
19              
20             sub new {
21 1     1 1 78 my ($class, %opt) = @_;
22              
23             my %data = (
24             keep_unknown_categories => 0,
25             unknown_category_key => 'Other',
26              
27             case_insensitive_cats => 0,
28              
29             skip_filename_re => undef,
30             skip_entry => undef,
31             substitutions => undef,
32              
33 1 50       18 terminal => (defined($opt{terminal}) ? undef : $ENV{TERM}),
34              
35             terminalize => 0,
36             terminalization_format => q{%s -e '%s'},
37              
38             desktop_files_paths => [
39             qw(
40             /usr/local/share/applications
41             /usr/share/applications
42             )
43             ],
44              
45             keys_to_keep => [qw(Exec Name Icon)],
46              
47             categories => [
48             qw(
49             Utility
50             Development
51             Education
52             Game
53             Graphics
54             AudioVideo
55             Network
56             Office
57             Settings
58             System
59             )
60             ],
61              
62             %opt,
63             );
64              
65 1         3 $data{_file_keys_re} = do {
66 1         2 my %seen;
67 10         17 my @keys = map { quotemeta($_) } grep { !$seen{$_}++ }
  10         21  
68 1 50       2 (@{$data{keys_to_keep}}, qw(Hidden NoDisplay Categories), ($data{terminalize} ? qw(Terminal) : ()));
  1         4  
69              
70 1         2 local $" = q{|};
71 1         58 qr/^(@keys)=(.*\S)/m;
72             };
73              
74 1 50       4 if ($data{case_insensitive_cats}) {
75 0         0 @{$data{_categories}}{map { (lc $_) =~ tr/_a-z0-9/_/cr } @{$data{categories}}} = ();
  0         0  
  0         0  
  0         0  
76             }
77             else {
78 1         3 @{$data{_categories}}{@{$data{categories}}} = ();
  1         4  
  1         1  
79             }
80              
81 1         4 bless \%data, $class;
82             }
83              
84             sub get_desktop_files {
85 0     0 1 0 my ($self) = @_;
86              
87 0         0 my %table;
88 0         0 foreach my $dir (@{$self->{desktop_files_paths}}) {
  0         0  
89 0 0       0 opendir(my $dir_h, $dir) or next;
90              
91             #<<<
92 0   0     0 my $is_local = (
93             index($dir, '/local/') != -1
94             or index($dir, '/.local/') != -1
95             );
96             #>>>
97              
98 0         0 foreach my $file (readdir $dir_h) {
99 0 0       0 if (substr($file, -8) eq '.desktop') {
100 0 0 0     0 if ($is_local or not exists($table{$file})) {
101 0         0 $table{$file} = "$dir/$file";
102             }
103             }
104             }
105             }
106              
107 0 0       0 wantarray ? values(%table) : [values(%table)];
108             }
109              
110             # Used for unescaping strings
111             my %Chr = (s => ' ', n => "\n", r => "\r", t => "\t", '\\' => '\\');
112              
113             sub parse_desktop_file {
114 2     2 1 1618 my ($self, $desktop_file) = @_;
115              
116             # Check the filename and skip it if it matches `skip_filename_re`
117 2 50       9 if (defined $self->{skip_filename_re}) {
118 0 0       0 substr($desktop_file, rindex($desktop_file, '/') + 1) =~ /$self->{skip_filename_re}/ && return;
119             }
120              
121             # Open and read the desktop file
122 2 50       43 sysopen my $desktop_fh, $desktop_file, 0 or return;
123 2         21 sysread $desktop_fh, (my $file), -s $desktop_file;
124              
125             # Locate the "[Desktop Entry]" section
126 2 50       17 if ((my $index = index($file, "]\n", index($file, "[Desktop Entry]") + 15)) != -1) {
127 2         8 $file = substr($file, 0, $index);
128             }
129              
130             # Parse the entry data
131 2         40 my %info = $file =~ /$self->{_file_keys_re}/g;
132              
133             # Ignore the file when `NoDisplay` is true
134 2 50       7 if (exists $info{NoDisplay}) {
135 0 0       0 return if exists $TRUE_VALUES{$info{NoDisplay}};
136             }
137              
138             # Ignore the file when `Hidden` is true
139 2 50       4 if (exists $info{Hidden}) {
140 0 0       0 return if exists $TRUE_VALUES{$info{Hidden}};
141             }
142              
143             # If no 'Name' entry is defined, create one with the name of the file
144 2   33     5 $info{Name} //= substr($desktop_file, rindex($desktop_file, '/') + 1, -8);
145              
146             # Unescape string escapes (\n, \t, etc.)
147 2   0     13 $info{$_} =~ s{\\(.)}{ $Chr{$1} // $1 }eg for (keys %info);
  0         0  
148              
149             # Handle `skip_entry`
150 2 50 33     8 if (defined($self->{skip_entry}) and ref($self->{skip_entry}) eq 'ARRAY') {
151 0         0 foreach my $pair_ref (@{$self->{skip_entry}}) {
  0         0  
152 0 0 0     0 if (exists($info{$pair_ref->{key}}) and $info{$pair_ref->{key}} =~ /$pair_ref->{re}/) {
153 0         0 return;
154             }
155             }
156             }
157              
158             # Make user-defined substitutions
159 2 50 33     6 if (defined($self->{substitutions}) and ref($self->{substitutions}) eq 'ARRAY') {
160 0         0 foreach my $pair_ref (@{$self->{substitutions}}) {
  0         0  
161 0 0       0 if (exists $info{$pair_ref->{key}}) {
162 0 0       0 if ($pair_ref->{global}) {
163 0         0 $info{$pair_ref->{key}} =~ s/$pair_ref->{re}/$pair_ref->{value}/g;
164             }
165             else {
166 0         0 $info{$pair_ref->{key}} =~ s/$pair_ref->{re}/$pair_ref->{value}/;
167             }
168             }
169             }
170             }
171              
172             # Parse categories (and remove any duplicates)
173 2         3 my %categories;
174              
175             #<<<
176             @categories{
177 8         17 grep { exists $self->{_categories}{$_} } (
178             $self->{case_insensitive_cats}
179 0         0 ? (map { lc($_) =~ tr/_a-z0-9/_/cr } split(/;/, $info{Categories} // ''))
180 2 50 0     11 : (split(/;/, $info{Categories} // ''))
      50        
181             )
182             } = ();
183             #>>>
184              
185             # Skip entry when there are no categories and `keep_unknown_categories` is false
186 2 0 33     6 %categories or $self->{keep_unknown_categories} or return;
187              
188             # Store the categories
189 2         6 $info{Categories} = [keys %categories];
190              
191             # Remove `% ...` from the value of `Exec`
192 2 50       14 index($info{Exec}, ' %') != -1 and $info{Exec} =~ s/ +%.*//s;
193              
194             # Terminalize
195 2 0 33     6 if ( $self->{terminalize}
      33        
196             and defined($info{Terminal})
197             and exists($TRUE_VALUES{$info{Terminal}})) {
198 0         0 $info{Exec} = sprintf($self->{terminalization_format}, $self->{terminal}, $info{Exec});
199             }
200              
201             # Check and clean the icon name
202 2 50       4 if (exists $info{Icon}) {
203 2         11 my $icon = $info{Icon};
204              
205 2         3 my $abs;
206 2 50       6 if (substr($icon, 0, 1) eq '/') {
207 0 0       0 if (-f $icon) { # icon is specified as an absolute path
208 0         0 $abs = 1;
209             }
210             else { # otherwise, take its basename
211 0         0 $icon = substr($icon, 1 + rindex($icon, '/'));
212             }
213             }
214              
215             # Remove any icon extension
216 2 50       5 if (!$abs) {
217 2         4 $icon =~ s/\.(?:png|jpe?g|svg|xpm)\z//i;
218             }
219              
220             # Store the icon back into `%info`
221 2         3 $info{Icon} = $icon;
222             }
223              
224 2         25 return %info;
225             }
226              
227             sub parse {
228 1     1 1 20 my ($self, $hash_ref, @desktop_files) = @_;
229              
230 1         3 foreach my $desktop_file (@desktop_files) {
231 1         3 my %info = $self->parse_desktop_file($desktop_file);
232              
233             # Skip when %info is empty
234 1 50       8 %info || next;
235              
236             # Push the entry into its belonging categories
237 1 50 33     4 if (exists($info{Categories}) and @{$info{Categories}}) {
  1         5  
238 1         2 foreach my $category (@{$info{Categories}}) {
  1         4  
239 1         1 push @{$hash_ref->{$category}}, {map { $_ => $info{$_} } @{$self->{keys_to_keep}}};
  1         3  
  7         24  
  1         3  
240             }
241             }
242             else {
243 0           push @{$hash_ref->{$self->{unknown_category_key}}}, {map { $_ => $info{$_} } @{$self->{keys_to_keep}}};
  0            
  0            
  0            
244             }
245             }
246             }
247              
248             sub parse_desktop_files {
249 0     0 1   my ($self) = @_;
250 0           my %categories;
251 0           $self->parse(\%categories, $self->get_desktop_files);
252 0           \%categories;
253             }
254              
255             1;
256              
257             __END__