package Linux::DesktopFiles; # This module is designed to be pretty fast. # The best uses of this module is to generate real # time menus, based on the content of desktop files. use 5.014; #use strict; #use warnings; our $VERSION = '0.22'; our %TRUE_VALUES = ( 'true' => 1, 'True' => 1, '1' => 1 ); sub new { my ($class, %opt) = @_; my %data = ( keep_unknown_categories => 0, unknown_category_key => 'other', case_insensitive_cats => 0, skip_filename_re => undef, skip_entry => undef, substitutions => undef, terminal => (defined($opt{terminal}) ? undef : $ENV{TERM}), terminalize => 0, terminalization_format => q{%s -e '%s'}, desktop_files_paths => [ qw( /usr/local/share/applications /usr/share/applications ) ], keys_to_keep => [qw(Exec Name Icon)], categories => [ qw( Utility Development Education Game Graphics AudioVideo Network Office Settings System ) ], %opt, ); $data{_file_keys_re} = do { my %seen; my @keys = map { quotemeta($_) } grep { !$seen{$_}++ } (@{$data{keys_to_keep}}, qw(Hidden NoDisplay Categories), ($data{terminalize} ? qw(Terminal) : ())); local $" = q{|}; qr/^(@keys)=(.*\S)/m; }; if ($data{case_insensitive_cats}) { @{$data{_categories}}{map { (lc $_) =~ tr/_a-z0-9/_/cr } @{$data{categories}}} = (); } else { @{$data{_categories}}{@{$data{categories}}} = (); } bless \%data, $class; } sub get_desktop_files { my ($self) = @_; my %table; foreach my $dir (@{$self->{desktop_files_paths}}) { opendir(my $dir_h, $dir) or next; #<<< my $is_local = ( index($dir, '/local/') != -1 or index($dir, '/.local/') != -1 ); #>>> foreach my $file (readdir $dir_h) { if (substr($file, -8) eq '.desktop') { if ($is_local or not exists($table{$file})) { $table{$file} = "$dir/$file"; } } } } wantarray ? values(%table) : [values(%table)]; } sub parse { my ($self, $file_data, @desktop_files) = @_; foreach my $desktop_file (@desktop_files) { # Check the filename and skip it if it matches `skip_filename_re` if (defined $self->{skip_filename_re}) { substr($desktop_file, rindex($desktop_file, '/') + 1) =~ /$self->{skip_filename_re}/ && next; } # Open and read the desktop file sysopen my $desktop_fh, $desktop_file, 0 or next; sysread $desktop_fh, (my $file), -s $desktop_file; # Locate the "[Desktop Entry]" section if ((my $index = index($file, "]\n", index($file, "[Desktop Entry]") + 15)) != -1) { $file = substr($file, 0, $index); } # Parse the entry data my %info = $file =~ /$self->{_file_keys_re}/g; # Ignore the file when `NoDisplay` is true if (exists $info{NoDisplay}) { next if exists $TRUE_VALUES{$info{NoDisplay}}; } # Ignore the file when `Hidden` is true if (exists $info{Hidden}) { next if exists $TRUE_VALUES{$info{Hidden}}; } # If no 'Name' entry is defined, create one with the name of the file $info{Name} //= substr($desktop_file, rindex($desktop_file, '/') + 1, -8); # Handle `skip_entry` if (defined($self->{skip_entry}) and ref($self->{skip_entry}) eq 'ARRAY') { my $skip; foreach my $pair_ref (@{$self->{skip_entry}}) { if (exists($info{$pair_ref->{key}}) and $info{$pair_ref->{key}} =~ /$pair_ref->{re}/) { $skip = 1; last; } } $skip and next; } # Make user-defined substitutions if (defined($self->{substitutions}) and ref($self->{substitutions}) eq 'ARRAY') { foreach my $pair_ref (@{$self->{substitutions}}) { if (exists $info{$pair_ref->{key}}) { if ($pair_ref->{global}) { $info{$pair_ref->{key}} =~ s/$pair_ref->{re}/$pair_ref->{value}/g; } else { $info{$pair_ref->{key}} =~ s/$pair_ref->{re}/$pair_ref->{value}/; } } } } # Parse categories (and remove any duplicates) my %categories; #<<< @categories{ grep { exists $self->{_categories}{$_} } ( $self->{case_insensitive_cats} ? (map { lc($_) =~ tr/_a-z0-9/_/cr } split(/;/, $info{Categories} // '')) : (split(/;/, $info{Categories} // '')) ) } = (); #>>> # Skip entry when there are no categories and `keep_unknown_categories` is false scalar(%categories) or $self->{keep_unknown_categories} or next; # Remove `% ...` from the value of `Exec` index($info{Exec}, ' %') != -1 and $info{Exec} =~ s/ +%.*//s; # Terminalize if ( $self->{terminalize} and defined($info{Terminal}) and exists($TRUE_VALUES{$info{Terminal}})) { $info{Exec} = sprintf($self->{terminalization_format}, $self->{terminal}, $info{Exec}); } # Check and clean the icon name if (exists $info{Icon}) { my $icon = $info{Icon}; my $abs; if (substr($icon, 0, 1) eq '/') { if (-f $icon) { # icon is specified as an absolute path $abs = 1; } else { # ... otherwise, take its basename $icon = substr($icon, 1 + rindex($icon, '/')); } } # Remove any icon extension if (!$abs) { $icon =~ s/\.(?:png|jpe?g|svg|xpm)\z//i; } # Store the icon back into `%info` $info{Icon} = $icon; } # Push the entry into its belonging categories if (scalar(%categories)) { foreach my $category (keys %categories) { push @{$file_data->{$category}}, {map { $_ => $info{$_} } @{$self->{keys_to_keep}}}; } } elsif ($self->{keep_unknown_categories}) { push @{$file_data->{$self->{unknown_category_key}}}, {map { $_ => $info{$_} } @{$self->{keys_to_keep}}}; } } } sub parse_desktop_files { my ($self) = @_; my %categories; $self->parse(\%categories, $self->get_desktop_files); \%categories; } 1; __END__ =encoding utf8 =head1 NAME Linux::DesktopFiles - Fast parsing of the Linux desktop files. =head1 SYNOPSIS use Linux::DesktopFiles; my $obj = Linux::DesktopFiles->new( terminalize => 1 ); print join("\n", $obj->get_desktop_files); my $hash_ref = $obj->parse_desktop_files; =head1 DESCRIPTION The C, a very fast and simple way to parse the Linux desktop files. =head1 CONSTRUCTOR METHODS The following constructor methods are available: =over 4 =item $obj = Linux::DesktopFiles->new( %options ) This method constructs a new C object and returns it. Key/value pair arguments may be provided to set up the initial state. By default, Linux::DesktopFiles->new(); is equivalent with: Linux::DesktopFiles->new( terminal => $ENV{TERM}, terminalize => 0, terminalization_format => "%s -e '%s'", skip_entry => [], skip_filename_re => [], substitutions => [], desktop_files_paths => ['/usr/local/share/applications', '/usr/share/applications'], keys_to_keep => ["Name", "Exec", "Icon"], categories => [ qw( Utility Development Education Game Graphics AudioVideo Network Office Settings System ) ], case_insensitive_cats => 0, keep_unknown_categories => 0, unknown_category_key => 'other', ); =back =head2 Main options =over 4 =item desktop_files_paths => ['dir1', 'dir2', ...] Sets the directories where to find the desktop files. =item keys_to_keep => [qw(Name Exec Icon Comment ...)] Any valid keys from the desktop files. This keys will be stored in the returned hash reference when calling C<$obj-Eparse_desktop_files>. =item categories => [qw(Graphics Network AudioVideo ...)] Any valid categories from the desktop files. Any category not listed will be ignored or stored in the B when C is set to a true value. =back =head2 Other options =over 4 =item keep_unknown_categories => $bool When an item is not part of any specified category, will be stored inside the unknown category, specified by B. =item unknown_category_key => $name Category name where to store the applications which do not belong to any specified category. =item case_insensitive_cats => $bool This option makes the category names case insensitive, by lowercasing and replacing any non-alpha numeric characters with an underscore. For example, "X-XFCE" becomes "x_xfce". =item terminal => $command This terminal command will be used when B is set to a true value. =item terminalize => $bool When the value of B is true, modify the B value to something like: terminal -e 'command' =item terminalization_format => q{%s -e '%s'} Format used by C to terminalize a command which requires to be executed inside a terminal. Used internally as: sprintf($self->{terminalization_format}, $self->{terminal}, $command); =back =head2 Regex options =over 4 =item skip_filename_re => qr/regex/ Skip any desktop file if its file name matches the regex. B File names are from the last slash to the end. =item skip_entry => [{key => 'KeyName', re => qr/REGEX/i}, {...}] Skip any desktop file if the value from a given key matches the specified regular expression. The B can be any valid key from the desktop files. Example: skip_entry => [ {key => 'Name', re => qr/(?:about|terminal)/i}, {key => 'Exec', re => qr/xterm/}, ], =item substitutions => [{key => 'KeyName', re => qr/REGEX/i, value => 'Value'}, {...}] Substitute, by using a regex, in the values of the desktop files. The B can be any valid key from the desktop files. The B can be any valid regular expression. Anything matched by the regex, will be replaced the string stored in B. For global matching/substitution, you need to set the B key to a true value. Example: substitutions => [ {key => 'Exec', re => qr/xterm/, value => 'sakura'}, {key => 'Exec', re => qr/\$HOME\b/, value => '/my/home', global => 1}, ], =back =head1 SUBROUTINES/METHODS =over 4 =item $obj->get_desktop_files() Get all desktop files. In list context returns a list, but in scalar context, it returns an array reference containing the full names of the desktop files. =item $obj->parse(\%hash, @desktop_files) Parse a list of desktop files into a HASH ref. =item $obj->parse_desktop_files() It returns a HASH reference which categories names as keys, and ARRAY references as values which contains HASH references with the keys specified in the B option, and values from the desktop files. The returned HASH reference might look something like this: { utility => [ {Exec => "...", Name => "..."}, {Exec => "...", Name => "..."} ], network => [ {Exec => "...", Name => "..."}, {Exec => "...", Name => "..."} ], } This function is equivalent with: $obj->parse(\%hash, $obj->get_desktop_files); =back =head1 REPOSITORY L =head1 AUTHOR Daniel "Trizen" Șuteu, Etrizenx@gmail.comE =head1 COPYRIGHT AND LICENSE Copyright (C) 2012-2017 This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.14.2 or, at your option, any later version of Perl 5 you may have available. =head1 SEE ALSO L and L =cut