Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
add caching to i3-dmenu-desktop
--- /usr/bin/i3-dmenu-desktop 2016-11-12 13:32:44.000000000 +0100
+++ i3-dmenu-desktop 2017-06-18 02:31:25.736657222 +0200
@@ -7,7 +7,8 @@
use strict;
use warnings qw(FATAL utf8);
-use Data::Dumper;
+use Storable;
+#use Data::Dumper;
use IPC::Open2;
use POSIX qw(locale_h);
use File::Find;
@@ -45,8 +46,12 @@
my @entry_types;
my $dmenu_cmd = 'dmenu -i';
+my $store;
+my $load;
my $result = GetOptions(
'dmenu=s' => \$dmenu_cmd,
+ 'load=s' => \$load,
+ 'store=s' => \$store,
'entry-type=s' => \@entry_types,
'version' => sub {
say "dmenu-desktop 1.5 © 2012 Michael Stapelberg";
@@ -103,211 +108,225 @@
# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
my %desktops;
-# See http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables
-my $xdg_data_home = $ENV{XDG_DATA_HOME};
-$xdg_data_home = $ENV{HOME} . '/.local/share' if
- !defined($xdg_data_home) ||
- $xdg_data_home eq '' ||
- ! -d $xdg_data_home;
-
-my $xdg_data_dirs = $ENV{XDG_DATA_DIRS};
-$xdg_data_dirs = '/usr/local/share/:/usr/share/' if
- !defined($xdg_data_dirs) ||
- $xdg_data_dirs eq '';
-
-my @searchdirs = ("$xdg_data_home/applications/");
-for my $dir (split(':', $xdg_data_dirs)) {
- push @searchdirs, "$dir/applications/";
-}
-
-# Cleanup the paths, maybe some application does not cope with double slashes
-# (the field code %k is replaced with the .desktop file location).
-@searchdirs = map { s,//,/,g; $_ } @searchdirs;
-
-# To avoid errors by File::Find’s find(), only pass existing directories.
-@searchdirs = grep { -d $_ } @searchdirs;
-
-find(
- {
- wanted => sub {
- return unless substr($_, -1 * length('.desktop')) eq '.desktop';
- my $relative = $File::Find::name;
-
- # + 1 for the trailing /, which is missing in ::topdir.
- substr($relative, 0, length($File::Find::topdir) + 1) = '';
-
- # Don’t overwrite files with the same relative path, we search in
- # descending order of importance.
- return if exists($desktops{$relative});
+my %apps;
+my %choices;
- $desktops{$relative} = $File::Find::name;
+if (!$load) {
+ # See http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html#variables
+ my $xdg_data_home = $ENV{XDG_DATA_HOME};
+ $xdg_data_home = $ENV{HOME} . '/.local/share' if
+ !defined($xdg_data_home) ||
+ $xdg_data_home eq '' ||
+ ! -d $xdg_data_home;
+
+ my $xdg_data_dirs = $ENV{XDG_DATA_DIRS};
+ $xdg_data_dirs = '/usr/local/share/:/usr/share/' if
+ !defined($xdg_data_dirs) ||
+ $xdg_data_dirs eq '';
+
+ my @searchdirs = ("$xdg_data_home/applications/");
+ for my $dir (split(':', $xdg_data_dirs)) {
+ push @searchdirs, "$dir/applications/";
+ }
+
+ # Cleanup the paths, maybe some application does not cope with double slashes
+ # (the field code %k is replaced with the .desktop file location).
+ @searchdirs = map { s,//,/,g; $_ } @searchdirs;
+
+ # To avoid errors by File::Find’s find(), only pass existing directories.
+ @searchdirs = grep { -d $_ } @searchdirs;
+
+ find(
+ {
+ wanted => sub {
+ return unless substr($_, -1 * length('.desktop')) eq '.desktop';
+ my $relative = $File::Find::name;
+
+ # + 1 for the trailing /, which is missing in ::topdir.
+ substr($relative, 0, length($File::Find::topdir) + 1) = '';
+
+ # Don’t overwrite files with the same relative path, we search in
+ # descending order of importance.
+ return if exists($desktops{$relative});
+
+ $desktops{$relative} = $File::Find::name;
+ },
+ no_chdir => 1,
},
- no_chdir => 1,
- },
- @searchdirs
-);
+ @searchdirs
+ );
-my %apps;
+ for my $file (values %desktops) {
+ my $base = basename($file);
-for my $file (values %desktops) {
- my $base = basename($file);
+ # _ is an invalid character for a key, so we can use it for our own keys.
+ $apps{$base}->{_Location} = $file;
- # _ is an invalid character for a key, so we can use it for our own keys.
- $apps{$base}->{_Location} = $file;
+ # Extract all “Name” and “Exec” keys from the [Desktop Entry] group
+ # and store them in $apps{$base}.
+ my %names;
+ my $content = slurp($file);
+ next unless defined($content);
+ my @lines = split("\n", $content);
+ for my $line (@lines) {
+ my $first = substr($line, 0, 1);
+ next if $line eq '' || $first eq '#';
+ next unless ($line eq '[Desktop Entry]' ..
+ ($first eq '[' &&
+ substr($line, -1) eq ']' &&
+ $line ne '[Desktop Entry]'));
+ next if $first eq '[';
+
+ my ($key, $value) = ($line =~ /^
+ (
+ [A-Za-z0-9-]+ # the spec specifies these as valid key characters
+ (?:\[[^]]+\])? # possibly, there as a locale suffix
+ )
+ \s* = \s* # whitespace around = should be ignored
+ (.*) # no restrictions on the values
+ $/x);
+
+ if ($key =~ /^Name/) {
+ $names{$key} = $value;
+ } elsif ($key eq 'Exec' ||
+ $key eq 'TryExec' ||
+ $key eq 'Path' ||
+ $key eq 'Type') {
+ $apps{$base}->{$key} = $value;
+ } elsif ($key eq 'NoDisplay' ||
+ $key eq 'Hidden' ||
+ $key eq 'StartupNotify' ||
+ $key eq 'Terminal') {
+ # Values of type boolean must either be string true or false,
+ # see “Possible value types”:
+ # http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s03.html
+ $apps{$base}->{$key} = ($value eq 'true');
+ }
+ }
- # Extract all “Name” and “Exec” keys from the [Desktop Entry] group
- # and store them in $apps{$base}.
- my %names;
- my $content = slurp($file);
- next unless defined($content);
- my @lines = split("\n", $content);
- for my $line (@lines) {
- my $first = substr($line, 0, 1);
- next if $line eq '' || $first eq '#';
- next unless ($line eq '[Desktop Entry]' ..
- ($first eq '[' &&
- substr($line, -1) eq ']' &&
- $line ne '[Desktop Entry]'));
- next if $first eq '[';
-
- my ($key, $value) = ($line =~ /^
- (
- [A-Za-z0-9-]+ # the spec specifies these as valid key characters
- (?:\[[^]]+\])? # possibly, there as a locale suffix
- )
- \s* = \s* # whitespace around = should be ignored
- (.*) # no restrictions on the values
- $/x);
-
- if ($key =~ /^Name/) {
- $names{$key} = $value;
- } elsif ($key eq 'Exec' ||
- $key eq 'TryExec' ||
- $key eq 'Path' ||
- $key eq 'Type') {
- $apps{$base}->{$key} = $value;
- } elsif ($key eq 'NoDisplay' ||
- $key eq 'Hidden' ||
- $key eq 'StartupNotify' ||
- $key eq 'Terminal') {
- # Values of type boolean must either be string true or false,
- # see “Possible value types”:
- # http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s03.html
- $apps{$base}->{$key} = ($value eq 'true');
+ for my $suffix (@suffixes) {
+ next unless exists($names{"Name[$suffix]"});
+ $apps{$base}->{Name} = $names{"Name[$suffix]"};
+ last;
}
- }
- for my $suffix (@suffixes) {
- next unless exists($names{"Name[$suffix]"});
- $apps{$base}->{Name} = $names{"Name[$suffix]"};
- last;
+ # Fallback to unlocalized “Name”.
+ $apps{$base}->{Name} = $names{Name} unless exists($apps{$base}->{Name});
}
- # Fallback to unlocalized “Name”.
- $apps{$base}->{Name} = $names{Name} unless exists($apps{$base}->{Name});
-}
+ # %apps now looks like this:
+ #
+ # %apps = {
+ # 'evince.desktop' => {
+ # 'Exec' => 'evince %U',
+ # 'Name' => 'Dokumentenbetrachter',
+ # '_Location' => '/usr/share/applications/evince.desktop'
+ # },
+ # 'gedit.desktop' => {
+ # 'Exec' => 'gedit %U',
+ # 'Name' => 'gedit',
+ # '_Location' => '/usr/share/applications/gedit.desktop'
+ # }
+ # };
+
+ # ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+ # ┃ Turn %apps inside out to provide Name → filename lookup. ┃
+ # ┃ The Name is what we display in dmenu later. ┃
+ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+ for my $app (keys %apps) {
+ my $name = $apps{$app}->{Name};
+
+ # Don’t try to use .desktop files which don’t have Type=application
+ next if (!exists($apps{$app}->{Type}) ||
+ $apps{$app}->{Type} ne 'Application');
+
+ # Skip broken files (Type=application, but no Exec key).
+ if (!exists($apps{$app}->{Exec}) ||
+ $apps{$app}->{Exec} eq '') {
+ warn 'File ' . $apps{$app}->{_Location} . ' is broken: it contains Type=Application, but no Exec key/value pair.';
+ next;
+ }
-# %apps now looks like this:
-#
-# %apps = {
-# 'evince.desktop' => {
-# 'Exec' => 'evince %U',
-# 'Name' => 'Dokumentenbetrachter',
-# '_Location' => '/usr/share/applications/evince.desktop'
-# },
-# 'gedit.desktop' => {
-# 'Exec' => 'gedit %U',
-# 'Name' => 'gedit',
-# '_Location' => '/usr/share/applications/gedit.desktop'
-# }
-# };
+ # Don’t offer apps which have NoDisplay == true or Hidden == true.
+ # See http://wiki.xfce.org/howto/customize-menu#hide_menu_entries
+ # for the difference between NoDisplay and Hidden.
+ next if (exists($apps{$app}->{NoDisplay}) && $apps{$app}->{NoDisplay}) ||
+ (exists($apps{$app}->{Hidden}) && $apps{$app}->{Hidden});
+
+ if (exists($apps{$app}->{TryExec})) {
+ my $tryexec = $apps{$app}->{TryExec};
+ if (substr($tryexec, 0, 1) eq '/') {
+ # Skip if absolute path is not executable.
+ next unless -x $tryexec;
+ } else {
+ # Search in $PATH for the executable.
+ my $found = 0;
+ for my $path (split(':', $ENV{PATH})) {
+ next unless -x "$path/$tryexec";
+ $found = 1;
+ last;
+ }
+ next unless $found;
+ }
+ }
-# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
-# ┃ Turn %apps inside out to provide Name → filename lookup. ┃
-# ┃ The Name is what we display in dmenu later. ┃
-# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ if ((scalar grep { $_ eq 'name' } @entry_types) > 0) {
+ if (exists($choices{$name})) {
+ # There are two .desktop files which contain the same “Name” value.
+ # I’m not sure if that is allowed to happen, but we disambiguate the
+ # situation by appending “ (2)”, “ (3)”, etc. to the name.
+ #
+ # An example of this happening is exo-file-manager.desktop and
+ # thunar-settings.desktop, both of which contain “Name=File Manager”.
+ my $inc = 2;
+ $inc++ while exists($choices{"$name ($inc)"});
+ $name = "$name ($inc)";
+ }
-my %choices;
-for my $app (keys %apps) {
- my $name = $apps{$app}->{Name};
+ $choices{$name} = $app;
+ }
- # Don’t try to use .desktop files which don’t have Type=application
- next if (!exists($apps{$app}->{Type}) ||
- $apps{$app}->{Type} ne 'Application');
-
- # Skip broken files (Type=application, but no Exec key).
- if (!exists($apps{$app}->{Exec}) ||
- $apps{$app}->{Exec} eq '') {
- warn 'File ' . $apps{$app}->{_Location} . ' is broken: it contains Type=Application, but no Exec key/value pair.';
- next;
- }
+ if ((scalar grep { $_ eq 'command' } @entry_types) > 0) {
+ my ($command) = split(' ', $apps{$app}->{Exec});
- # Don’t offer apps which have NoDisplay == true or Hidden == true.
- # See http://wiki.xfce.org/howto/customize-menu#hide_menu_entries
- # for the difference between NoDisplay and Hidden.
- next if (exists($apps{$app}->{NoDisplay}) && $apps{$app}->{NoDisplay}) ||
- (exists($apps{$app}->{Hidden}) && $apps{$app}->{Hidden});
-
- if (exists($apps{$app}->{TryExec})) {
- my $tryexec = $apps{$app}->{TryExec};
- if (substr($tryexec, 0, 1) eq '/') {
- # Skip if absolute path is not executable.
- next unless -x $tryexec;
- } else {
- # Search in $PATH for the executable.
- my $found = 0;
- for my $path (split(':', $ENV{PATH})) {
- next unless -x "$path/$tryexec";
- $found = 1;
- last;
- }
- next unless $found;
- }
- }
+ # Don’t add “geany” if “Geany” is already present.
+ my @keys = map { lc } keys %choices;
+ next if (scalar grep { $_ eq lc(basename($command)) } @keys) > 0;
- if ((scalar grep { $_ eq 'name' } @entry_types) > 0) {
- if (exists($choices{$name})) {
- # There are two .desktop files which contain the same “Name” value.
- # I’m not sure if that is allowed to happen, but we disambiguate the
- # situation by appending “ (2)”, “ (3)”, etc. to the name.
- #
- # An example of this happening is exo-file-manager.desktop and
- # thunar-settings.desktop, both of which contain “Name=File Manager”.
- my $inc = 2;
- $inc++ while exists($choices{"$name ($inc)"});
- $name = "$name ($inc)";
+ $choices{basename($command)} = $app;
}
- $choices{$name} = $app;
- }
-
- if ((scalar grep { $_ eq 'command' } @entry_types) > 0) {
- my ($command) = split(' ', $apps{$app}->{Exec});
+ if ((scalar grep { $_ eq 'filename' } @entry_types) > 0) {
+ my $filename = basename($app, '.desktop');
- # Don’t add “geany” if “Geany” is already present.
- my @keys = map { lc } keys %choices;
- next if (scalar grep { $_ eq lc(basename($command)) } @keys) > 0;
+ # Don’t add “geany” if “Geany” is already present.
+ my @keys = map { lc } keys %choices;
+ next if (scalar grep { $_ eq lc($filename) } @keys) > 0;
- $choices{basename($command)} = $app;
+ $choices{$filename} = $app;
+ }
}
- if ((scalar grep { $_ eq 'filename' } @entry_types) > 0) {
- my $filename = basename($app, '.desktop');
-
- # Don’t add “geany” if “Geany” is already present.
- my @keys = map { lc } keys %choices;
- next if (scalar grep { $_ eq lc($filename) } @keys) > 0;
+ # %choices now looks like this:
+ #
+ # %choices = {
+ # 'Dokumentenbetrachter' => 'evince.desktop',
+ # 'gedit' => 'gedit.desktop'
+ # };
+} else {
+ my $r = retrieve($load);
+ %apps = %{$r->[0]};
+ %choices = %{$r->[1]};
+}
- $choices{$filename} = $app;
- }
+if ($store) {
+ store [\%apps, \%choices], $store;
}
-# %choices now looks like this:
-#
-# %choices = {
-# 'Dokumentenbetrachter' => 'evince.desktop',
-# 'gedit' => 'gedit.desktop'
-# };
+
+
+
# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
# ┃ Run dmenu to ask the user for their choice ┃
@@ -502,6 +521,14 @@
=over
+=item B<--load=file>
+
+Load a file instead of parsing Desktop files. Save time and energy.
+
+=item B<--store=file>
+
+Store list of parsed Desktop files for a future use.
+
=item B<--dmenu=command>
Execute command instead of 'dmenu -i'. This option can be used to pass custom
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.