#!/usr/bin/perl # Copyright (C) 2011-2014 Daniel "Trizen" Șuteu . # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # Openbox Menu Generator # A fast pipe/static menu generator for the Openbox Window Manager. # Program: obmenu-generator # License: GPLv3 # Created on: 25 March 2011 # Latest edit on: 26 March 2014 # Websites: http://trizen.googlecode.com # http://trizenx.blogspot.com #use 5.014; #use strict; #use warnings; require Linux::DesktopFiles; $Linux::DesktopFiles::VERSION >= 0.08 || die "Update Linux::DesktopFiles to a newer version! (requires >=0.08)\n"; my $pkgname = 'obmenu-generator'; my $version = 0.59; our ($CONFIG, $SCHEMA); my $output_h = *STDOUT; my ($pipe, $static, $icons, $reconfigure, $stdout_config, $update_config); my $home_dir = $ENV{HOME} || $ENV{LOGDIR} || (getpwuid($<))[7] || `echo -n ~`; my $xdg_config_home = "$home_dir/.config"; my $menu_id = "root-menu"; my $config_dir = "$xdg_config_home/$pkgname"; my $schema_file = "$config_dir/schema.pl"; my $config_file = "$config_dir/config.pl"; my $openbox_conf = "$xdg_config_home/openbox"; my $menufile = "$openbox_conf/menu.xml"; my $icons_db = "$config_dir/icons.db"; sub usage { print <<"HELP"; usage: $0 [options] Options: -p : (re)generate a pipe menu -s : (re)generate a static menu -o : static menu file (default: ~/.config/openbox/menu.xml) -m : menu id (default: 'root-menu') -r : regenerate the config files -i : use icons in menus -d : regenerate icons.db (with -i) -u : update the config file -R : reconfigure openbox Help: -h : print this message -v : print the version number Examples: ** Static menu without icons: $0 -s ** Pipe menu with icons: $0 -p -i ** Reconfigure openbox: $0 -R ** Config file: $config_file ** Schema file: $schema_file HELP exit 0; } my $config_help = <<"HELP"; || FILTERING | skip_filename_re : Skip a .desktop file if its name matches the regex. Name is from the last slash to the end. (filename.desktop) Example: qr/^(?:gimp|xterm)\\b/, # skips 'gimp' and 'xterm' | skip_entry : Skip a destkop file if the value from a given key matches the regex. Example: [ {key => 'Name', re => qr/(?:about|terminal)/i}, {key => 'Exec', re => qr/^xterm/}, ], | substitutions : Substitute, by using a regex, in the values of the desktop files. Example: [ {key => 'Exec', re => qr/xterm/, value => 'sakura'}, {key => 'Exec', re => qr/\\\\\\\\/, value => '\\\\', global => 1}, # for wine apps ], || ICON SETTINGS | icon_dirs_first : When looking for icons, look in this directories first, before looking in the directories of the current icon theme. Example: [ "\$ENV{HOME}/My icons", ], | icon_dirs_second : Look in this directories after looked in the directories of the current icon theme. (Before /usr/share/pixmaps) Example: [ "/usr/share/icons/gnome", ], | icon_dirs_last : Look in this directories at the very last, after looked in /usr/share/pixmaps, /usr/share/icons/hicolor and some other directories. Example: [ "/usr/share/icons/Tango", ], | strict_icon_dirs : A true value will make the module to look only inside the directories specified by you in either one of the above three options. | gtk_rc_filename : Absolute path to the GTK configuration file. | missing_image : Use this icon for missing icons (default: gtk-missing-image) || KEYS | name_keys : Valid keys for the item names. Example: ['Name[fr]', 'GenericName[fr]', 'Name'], # french menu || PATHS | desktop_files_paths : Absolute paths which contains .desktop files. Example: [ '/usr/share/applications', "\$ENV{HOME}/.local/share/applications", glob("\$ENV{HOME}/.local/share/applications/wine/Programs/*"), ], || NOTES | Regular expressions: * use qr/RE/ instead of 'RE' * use qr/RE/i for case insenstive mode HELP if (@ARGV) { while (defined(my $arg = shift @ARGV)) { if ($arg eq '-i') { $icons = 1; } elsif ($arg eq '-S') { $stdout_config = 1; } elsif ($arg eq '-p') { $pipe = 1; } elsif ($arg eq '-r') { $reconfigure = 1; } elsif ($arg eq '-s') { $static = 1; } elsif ($arg eq '-d') { unlink $icons_db; } elsif ($arg eq '-u') { $update_config = 1; } elsif ($arg eq '-v') { print "$pkgname $version\n"; exit 0; } elsif ($arg eq '-R') { exec 'openbox', '--reconfigure'; } elsif ($arg eq '-o') { $menufile = shift(@ARGV) // die "$0: option '-o' requires an argument!\n"; } elsif ($arg eq '-m') { $menu_id = shift(@ARGV) // die "$0: option '-m' requires an argument!\n"; } elsif ($arg eq '-h') { usage(); } else { die "$0: option `$arg' is invalid!\n"; } } } if (not -d $config_dir) { require File::Path; File::Path::make_path($config_dir) or die "Can't create directory '${config_dir}': $!"; } my $config_documentation = <<"EOD"; #!/usr/bin/perl # $pkgname - configuration file # This file will be updated automatically every time when is needed. # Any additional comment and/or indentation will be lost. =for comment $config_help =cut EOD my %CONFIG = ( 'Linux::DesktopFiles' => { keep_unknown_categories => 1, unknown_category_key => 'other', gtk_rc_filename => "$home_dir/.gtkrc-2.0", skip_entry => undef, substitutions => undef, skip_filename_re => undef, terminalize => 1, terminalization_format => q{%s -e '%s'}, desktop_files_paths => ['/usr/share/applications'], icon_dirs_first => undef, icon_dirs_second => undef, icon_dirs_last => undef, strict_icon_dirs => undef, skip_svg_icons => 1, }, name_keys => ['Name'], terminal => 'xterm', editor => 'geany', missing_icon => 'gtk-missing-image', VERSION => $version, ); sub dump_configuration { require Data::Dump; open my $config_fh, '>', $config_file or die "Can't open file '${config_file}' for write: $!"; my $dumped_config = q{our $CONFIG = } . Data::Dump::dump(\%CONFIG); print $config_fh $config_documentation, $dumped_config; close $config_fh; } if (not -e $config_file or $reconfigure) { dump_configuration(); } if (not -e $schema_file) { if (-e (my $etc_schema_file = "/etc/xdg/$pkgname/schema.pl")) { require File::Copy; File::Copy::copy($etc_schema_file, $schema_file) or die "$0: can't copy file `$etc_schema_file' to `$schema_file': $!\n"; } else { die "$0: schema file `$schema_file' does not exists!\n"; } } require $schema_file; # Load the configuration files # Remove user's defined values my @valid_keys = grep exists $CONFIG{$_}, keys $CONFIG; @CONFIG{@valid_keys} = @{$CONFIG}{@valid_keys}; # Keep user's defined values #@CONFIG{keys %{$CONFIG}} = values %{$CONFIG}; if ($CONFIG{VERSION} != $version) { $update_config = 1; $CONFIG{VERSION} = $version; } if ($icons && !$pipe) { # Performance improvement # XXX: don't do this in production code! @INC{ qw( strict.pm warnings.pm Tie/Hash.pm Carp.pm Exporter.pm warnings/register.pm ) } = (); *{'warnings::warnif'} = sub { }; } my $desk_obj = Linux::DesktopFiles->new( %{$CONFIG{'Linux::DesktopFiles'}}, home_dir => $home_dir, categories => [map $_->{cat}[0], grep exists $_->{cat}, @$SCHEMA], keys_to_keep => [@{$CONFIG{name_keys}}, 'Exec', $icons ? 'Icon' : ()], $icons ? ( abs_icon_paths => 1, icon_db_filename => $icons_db, ) : (), terminal => $CONFIG{terminal}, keep_empty_categories => 0, case_insensitive_cats => 1, ); if ($pipe or $static) { my $menu_backup = $menufile . '.bak'; if (not -e $menu_backup and -e $menufile) { require File::Copy; File::Copy::copy($menufile, $menu_backup); } if ($static) { open $output_h, '>', $menufile or die "Can't open file '${menufile}' for write: $!"; } elsif ($pipe) { if (not -d $openbox_conf) { require File::Path; File::Path::make_path($openbox_conf) or die "Can't create directory '${openbox_conf}': $!"; } require Cwd; my $exec_name = Cwd::abs_path($0) . ($icons ? q{ -i} : q{}); if (not -x $exec_name) { $exec_name = "$^X $exec_name"; } open my $fh, '>', $menufile or die "Can't open file '${menufile}' for write: $!"; print $fh <<"PIPE_MENU_HEADER"; PIPE_MENU_HEADER close $fh; print <<'EOT'; ** The pipe menu has been successfully generated! EOT exec 'openbox', '--reconfigure'; } } my $generated_menu = $static ? <<"STATIC_MENU_HEADER" STATIC_MENU_HEADER : "\n"; { my %cache; sub check_icon { $_[0] // return ''; $cache{$_[0]} //= ((chr ord($_[0]) eq '/') ? $_[0] : $desk_obj->get_icon_path($_[0]) || $desk_obj->get_icon_path($CONFIG{missing_icon})); } } sub prepare_item { $icons ? <<"ITEM_WITH_ICON" $_[0] ITEM_WITH_ICON : <<"ITEM"; $_[0] ITEM } sub begin_category { $icons ? <<"MENU_WITH_ICON" MENU_WITH_ICON : <<"MENU" MENU } my $categories = $desk_obj->parse_desktop_files(); foreach my $schema (@$SCHEMA) { if (exists $schema->{cat}) { exists($categories->{my $category = lc($schema->{cat}[0]) =~ tr/_a-z0-9/_/cr}) || next; $generated_menu .= begin_category($schema->{cat}[1], ($icons ? $schema->{cat}[2] : ())) . join( q{}, ( map $_->[1], sort { $a->[0] cmp $b->[0] } map [lc($_) => $_], map { my $name; my $exec = $_->{Exec}; foreach my $key (@{$CONFIG{name_keys}}) { if (defined $_->{$key}) { $name = $_->{$key}; last; } } foreach my $item ($name, $exec) { if ($item =~ tr/"&//) { $item =~ s/&/&/g; $item =~ s/"/"/g; } } $icons ? <<"ITEM_WITH_ICON" $exec ITEM_WITH_ICON : <<"ITEM"; $exec ITEM } @{$categories->{$category}} ) ) . qq[ \n]; } elsif (exists $schema->{item}) { $generated_menu .= prepare_item(@{$schema->{item}}); } elsif (exists $schema->{sep}) { $generated_menu .= defined $schema->{sep} ? qq[ \n] : qq[ \n]; } elsif (exists $schema->{begin_cat}) { $generated_menu .= begin_category(@{$schema->{begin_cat}}); } elsif (exists $schema->{end_cat}) { $generated_menu .= qq[ \n]; } elsif (exists $schema->{exit}) { my ($label, $icon) = @{$schema->{exit}}; $generated_menu .= $icons ? <<"EXIT_WITH_ICON" EXIT_WITH_ICON : <<"EXIT"; EXIT } elsif (exists $schema->{raw}) { $generated_menu .= qq[ $schema->{raw}\n]; } elsif (exists $schema->{pipe}) { my ($command, $label, $icon) = @{$schema->{pipe}}; $generated_menu .= $icons ? <<"PIPE_WITH_ICON" PIPE_WITH_ICON : <<"PIPE"; PIPE } elsif (exists $schema->{obgenmenu}) { my ($name, $icon) = ref($schema->{obgenmenu}) eq 'ARRAY' ? @{$schema->{obgenmenu}} : $schema->{obgenmenu}; $generated_menu .= ($icons ? <<"CONFIG_MENU_WITH_ICON" : <<"CONFIG_MENU") . <<'RECONFIGURE'; CONFIG_MENU_WITH_ICON CONFIG_MENU RECONFIGURE -e '/usr/bin/obconf' && ($generated_menu .= <<'EOL'); obconf EOL $generated_menu .= <<"CONFIG_MENU"; $CONFIG{editor} $openbox_conf/autostart $CONFIG{editor} $openbox_conf/rc.xml $0 -p $0 -s $0 -p -i $0 -s -i $CONFIG{editor} $menufile $CONFIG{editor} $schema_file $CONFIG{editor} $config_file $0 -r CONFIG_MENU } else { warn "$0: Invalid key '", (keys %{$schema})[0], "' in schema file!\n"; } } print $output_h $generated_menu, $static ? qq[ \n\n] : qq[\n]; dump_configuration() if $update_config;