#!/usr/bin/perl # Copyright (C) 2011-2012 Trizen . # # 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 . # Gtk2 Menutray - Application menu # License: GPLv3 # Created on: 03 March 2011 # Latest edit on: 22 February 2012 # Website: http://trizen.googlecode.com use strict; my $pkgname = 'menutray'; my $version = '0.3.3'; my $icons = 0; my $generate_menu = 0; my $reconfigure = 0; my $menutray_cat_icons = 0; our ($CONFIG, $SCHEMA); my $xdg_config_home = exists $ENV{XDG_CONFIG_HOME} ? $ENV{XDG_CONFIG_HOME} : ($ENV{HOME} // $ENV{LOGDIR} // (getpwuid($<))[7] // `echo -n ~`) . '/.config'; my $config_dir = "$xdg_config_home/$pkgname"; my $icons_dir = "$config_dir/icons"; my $config_file = "$config_dir/configuration.pl"; my $menufile = "$config_dir/menu.pl"; my $icons_path; if (-d $icons_dir) { $icons_path = $icons_dir; } elsif (-d 'icons') { require Cwd; $icons_path = Cwd::getcwd() . '/icons'; } else { $icons_path = ''; } foreach my $arg (@ARGV) { if ($arg =~ /^-+(?:h|help|usage|\?)$/) { help(); } elsif ($arg eq '-i') { $icons = 1; } elsif ($arg eq '-g') { $generate_menu = 1; } elsif ($arg eq '-r') { $reconfigure = 1; } elsif ($arg eq '-c') { $menutray_cat_icons = 1; $icons = 1; } elsif ($arg eq '-v') { print "$pkgname $version\n"; exit; } } unless (-e $config_dir) { require File::Path; File::Path::make_path($config_dir) or die "Unable to create $config_dir: $!\n"; } if (not -e $config_file or $reconfigure) { open my $fh, '>', $config_file or die "Open failed: $!"; print {$fh} <<'CONFIG_FILE'; #!/usr/bin/perl # menutray config file # # SCHEMA supports the following keys: item, cat, quit, regenerate, raw, sep, submenu # # Posible values for each of this types are: # For 'item': [COMMAND, LABEL, ICON] # For 'sep' : Horizontal separator # For 'cat' : Any of the posible categories. 'cat => [CATEGORY, LABEL, ICON]' - icon is optional # For 'raw' : [COMMAND, LABEL, ICON] - same as item # Example : {raw => ['pcmanfm','File manager','file-manager']}, # {raw => ['pcmanfm','File manager','/path/to/icon/icon.png']}, # NOTE: # * Keys and values are case sensitive. Keep all keys lowercase. # * ICON can be a either a direct path to a icon or a valid icon name # * Category names are case insensitive. (ex: X-XFCE and x_xfce are equivalent) # For regular expressions # * is better to use qr/REGEX/ instead of 'REGEX' my %items = ( terminal => 'xterm', editor => "xterm -e $ENV{EDITOR}", file_manager => 'pcmanfm', web_browser => 'firefox', instant_messaging => 'pidgin', run_command => 'gmrun', lock_command => 'xscreensaver-command -lock', ); our $CONFIG = { # Example: [ "$ENV{'HOME'}/.local/share/applications", '/my/path' ] desktop_files_paths => ['/usr/share/applications'], # When 'Terminal=true' open_in_terminal => "$items{terminal} -e %s", # Editor command open_in_editor => "$items{editor}", # Ignore desktop files which their filenames match a regex ignore_file_name_re => undef, # Ignore applications which their names match a regex ignore_app_name_re => undef, # Ignore applications which their commands match a regex ignore_app_command_re => undef, # Ignore desktop files which their content match a regex ignore_file_content_re => undef, # Remove from every command something matched by a regex (/g) command_rem_re => undef, # Icons type (ex: menu, small-toolbar, large-toolbar, button, dialog) icon_type => 'menu', # Icons default size icon_size => [16, 16], # Default missing image missing_image => 'gtk-missing-image', }; our $SCHEMA = [ # COMMAND LABEL ICON {item => [$items{file_manager}, 'File Manager', 'file-manager']}, {item => [$items{terminal}, 'Terminal', 'terminal']}, {item => [$items{editor}, 'Editor', 'text-editor']}, {item => [$items{web_browser}, 'Web Browser', 'web-browser']}, {item => [$items{run_command}, 'Run command', 'system-run']}, {item => [$items{instant_messaging}, 'Instant messaging', 'system-users']}, #{item => [$items{lock_command}, 'Lock', 'lock']}, {sep => undef}, # NAME LABEL ICON {cat => ['utility', 'Accessories', 'applications-utilities']}, {cat => ['development', 'Development', 'applications-development']}, {cat => ['education', 'Education', 'applications-science']}, {cat => ['game', 'Games', 'applications-games']}, {cat => ['graphics', 'Graphics', 'applications-graphics']}, {cat => ['audiovideo', 'Multimedia', 'applications-multimedia']}, {cat => ['network', 'Network', 'applications-internet']}, {cat => ['office', 'Office', 'applications-office']}, {cat => ['settings', 'Settings', 'applications-accessories']}, {cat => ['system', 'System', 'applications-system']}, #{cat => ['qt', 'QT Applications', 'qtlogo']}, #{cat => ['gtk', 'GTK Applications', 'gnome-applications']}, #{cat => ['x_xfce', 'XFCE Applications', 'applications-other']}, #{cat => ['gnome', 'GNOME Applications', 'gnome-applications']}, #{cat => ['consoleonly', 'CLI Applications', 'applications-utilities']}, {submenu => 'Configure menu'}, {sep => undef}, {regenerate => 'Regenerate'}, {quit => 'Quit'}, ]; CONFIG_FILE } if (-d "/usr/share/$pkgname/icons") { if (not -d $icons_dir) { mkdir $icons_dir or warn "(x_x) Unable to create dir: $icons_dir\n"; } if (opendir(my $dir_h, "/usr/share/$pkgname/icons")) { while (defined(my $filename = readdir $dir_h)) { if ($filename eq q{.} or $filename eq q{..}) { next } next if -e "$icons_dir/$filename"; require File::Copy; File::Copy::copy("/usr/share/$pkgname/icons/$filename", $icons_dir) or warn "Unable to copy $filename: $!"; } closedir $dir_h; } } sub help { print <<"HELP"; usage: $0 [options]\n options : -g : (re)generate a simple menu -i : (re)generate a menu with icons -r : rewrite the configuration file -c : use category icons from $icons_path * Menu : $menufile * Icons : $icons_path * Config : $config_file\n HELP exit 0; } my @desktop_files; sub push_more { opendir(my $dir_h, $_[0]) or return; foreach my $file (readdir $dir_h) { push @desktop_files, "$_[0]/$file" if substr($file, -8) eq '.desktop'; } closedir $dir_h; } do $config_file; foreach my $dir (@{$CONFIG->{desktop_files_paths}}) { opendir(my $dir_h, $dir) or next; foreach my $file (readdir $dir_h) { push @desktop_files, "$dir/$file" and next if substr($file, -8) eq '.desktop'; if ($file eq q{.} or $file eq q{..}) { next } push_more("$dir/$file") if -d "$dir/$file"; } closedir $dir_h; } my $number = 0; my $comment = '# # ' x 10; my ($generated_menu, $cat_name); my (%category_system_icons) = ( 'utility' => 'applications-accessories', 'development' => 'applications-development', 'education' => 'applications-science', 'game' => 'applications-games', 'graphics' => 'applications-graphics', 'audiovideo' => 'applications-multimedia', 'network' => 'applications-internet', 'office' => 'applications-office', 'settings' => 'preferences-system', 'system' => 'applications-system', 'submenu' => 'preferences-desktop' ); if ($generate_menu or $icons) { print "\n* Generating menu...\n"; generate_menu(); } sub look_for_icons { my ($icon_name) = @_; if (length $icon_name) { $icon_name =~ s/\s+$//; if (substr($icon_name, 0, 1) eq '/') { return $CONFIG->{missing_image} unless -f $icon_name; } else { $icon_name =~ s/\.\w{3}$//; } } else { return $CONFIG->{missing_image}; } return $icon_name; } sub push_app { return unless @{$_[0]}; my (undef, $cat_id, $cat_name, $icon_name) = @_; $generated_menu .= <<"CATEGORY_HEADER"; \n $comment\U$cat_name\E $comment \$all_apps = Gtk2::Menu->new; CATEGORY_HEADER if ($icons) { ++$number; $generated_menu .= <<"IMAGE_MENU_ITEM"; my \$category_$number = Gtk2::ImageMenuItem->new(q{$cat_name}); IMAGE_MENU_ITEM if (not $menutray_cat_icons) { if (defined $icon_name) { if (substr($icon_name, 0, 1) eq '/') { $generated_menu .= <<"ICON_FROM_FILE"; \$category_$number->set_image(Gtk2::Image->new_from_pixbuf(Gtk2::Gdk::Pixbuf->new_from_file_at_size( q{$icon_name},$CONFIG->{icon_size}[0],$CONFIG->{icon_size}[0]))); ICON_FROM_FILE } else { $generated_menu .= <<"ICON_FROM_NAME"; \$category_$number->set_image(Gtk2::Image->new_from_icon_name(q{$icon_name},q{$CONFIG->{icon_type}})); ICON_FROM_NAME } } else { $generated_menu .= <<"ICON_FROM_NAME"; \$category_$number->set_image(Gtk2::Image->new_from_icon_name(q{$category_system_icons{$cat_id}},q{$CONFIG->{icon_type}})); ICON_FROM_NAME } } else { my $image = -f "$icons_path/$cat_id.png" ? "$icons_path/$cat_id.png" : undef; if (defined $image) { $generated_menu .= <<"ICON_FROM_FILE"; \$category_$number->set_image(Gtk2::Image->new_from_pixbuf(Gtk2::Gdk::Pixbuf->new_from_file_at_size( q{$image},$CONFIG->{icon_size}[0],$CONFIG->{icon_size}[0]))); ICON_FROM_FILE } else { $generated_menu .= <<"ICON_FROM_STOCK"; \$category_$number->set_image(Gtk2::Image->new_from_stock(q{$CONFIG->{missing_image}},q{$CONFIG->{icon_type}})); ICON_FROM_STOCK } } } else { ++$number; $generated_menu .= <<"MENU_ITEM"; my \$category_$number = Gtk2::MenuItem->new(q{$cat_name}); MENU_ITEM } $generated_menu .= join( '', ( sort { lc $a cmp lc $b } map { my ($number) = reverse(rand) =~ /^(\d{1,4})/; my $app = lc $$_[0]; $app =~ tr/_0-9a-z/_/cd; $$_[0] = "\n my \$app_${app}_$number = Gtk2::" . do { $icons ? 'Image' : '' } . "MenuItem->new(q{$_->[0]});\n" . <<"SIGNAL_CONNECT"; \$app_${app}_$number->signal_connect('activate',sub {system q{$_->[1] &}}); SIGNAL_CONNECT if ($icons) { if (length $_->[2] and substr($_->[2], 0, 1) ne '/') { $_->[0] .= $_->[2] eq $CONFIG->{missing_image} ? <<"MISSING_IMAGE" \$app_${app}_$number->set_image(Gtk2::Image->new_from_stock(q{$CONFIG->{missing_image}},q{$CONFIG->{icon_type}})); MISSING_IMAGE : <<"IMAGE_FROM_ICON_NAME"; \$app_${app}_$number->set_image(Gtk2::Image->new_from_icon_name(q{$_->[2]},q{$CONFIG->{icon_type}})); IMAGE_FROM_ICON_NAME } else { if (defined $_->[2] and -f $_->[2]) { $_->[0] .= <<"SET_IMAGE"; \$app_${app}_$number->set_image(Gtk2::Image->new_from_pixbuf(Gtk2::Gdk::Pixbuf->new_from_file_at_size( q{$_->[2]},$CONFIG->{icon_size}[0],$CONFIG->{icon_size}[0]))); SET_IMAGE } else { $_->[0] .= <<"MISSING_IMAGE"; \$app_${app}_$number->set_image(Gtk2::Image->new_from_stock(q{$CONFIG->{missing_image}},q{$CONFIG->{icon_type}})); MISSING_IMAGE } } } $$_[0] .= <<"APPEND"; \$all_apps->append(\$app_${app}_$number); APPEND } @{$_[0]} ) ); $generated_menu .= <<"SUBMENU"; \$category_$number->set_submenu(\$all_apps); \$menu->append(\$category_$number); SUBMENU } sub add_item { my ($command, $name, $icon) = @_; my $uniq_name = lc $command; $uniq_name =~ tr/_0-9a-z/_/cd; $uniq_name .= "_$1" if reverse(rand) =~ /^(\d{1,4})/; if ($icons) { $icon //= $command; $generated_menu .= <<"GTK_IMAGE"; my \$item_$uniq_name = Gtk2::ImageMenuItem->new(q{$name}); GTK_IMAGE if (substr($icon, 0, 1) eq '/' and -f $icon) { $generated_menu .= <<"IMAGE_MENU_ITEM"; \$item_$uniq_name->set_image(Gtk2::Image->new_from_pixbuf(Gtk2::Gdk::Pixbuf->new_from_file_at_size( q{$_[2]},$CONFIG->{icon_size}[0],$CONFIG->{icon_size}[0]))); IMAGE_MENU_ITEM } else { $generated_menu .= <<"IMAGE_MENU_ITEM"; \$item_$uniq_name->set_image(Gtk2::Image->new_from_icon_name(q{$_[2]},q{$CONFIG->{icon_type}})); IMAGE_MENU_ITEM } } else { $generated_menu .= <<"IMAGE_MENU_ITEM"; my \$item_$uniq_name = Gtk2::MenuItem->new(q{$name}); IMAGE_MENU_ITEM } $generated_menu .= <<"CODE"; \$item_$uniq_name->signal_connect('activate',sub {system q{$command &}}); \$menu->append(\$item_$uniq_name);\n CODE } sub generate_menu { $generated_menu = <<'HEADER'; #!/usr/bin/perl # Menutray static use Gtk2 ('-init'); my $icon = Gtk2::StatusIcon->new; $icon->set_from_icon_name('start-here'); $icon->set_visible(1); $icon->signal_connect('button-release-event', sub {show_icon_menu()}); $icon->set_tooltip('Click for menu...'); sub show_icon_menu { my $menu = Gtk2::Menu->new; my $all_apps; HEADER my (@menutray_config_menu) = ( ["Edit ${pkgname}'s menu", "$CONFIG->{open_in_editor} $menufile", "$icons_dir/submenu.png"], ["Edit ${pkgname}'s config", "$CONFIG->{open_in_editor} $config_file", "$icons_dir/submenu.png"], ); my %categories; foreach my $category (@$SCHEMA) { next unless exists $category->{cat}; my $cat = lc $category->{cat}[0]; $cat =~ tr/_a-z/_/c; $categories{$cat} = (); $category->{cat}[0] = $cat; } my $desktop_fh; foreach my $desktop_file (@desktop_files) { if (defined $CONFIG->{ignore_file_name_re}) { next if substr($desktop_file, rindex($desktop_file, '/') + 1) =~ /$CONFIG->{ignore_file_name_re}/o; } my $file; sysopen $desktop_fh, $desktop_file, 0; sysread $desktop_fh, $file, -s $desktop_file; next if index($file, "\nNoDisplay=true") != -1; if (defined $CONFIG->{ignore_file_content_re}) { next if $file =~ /$CONFIG->{ignore_file_content_re}/o; } foreach my $category ( split( /;/, do { my $i = index($file, "\nCategories="); next if $i == -1; lc substr($file, $i + 12, index($file, "\n", $i + 12) - $i - 12); } ) ) { $category =~ tr/_a-z/_/c; next unless exists $categories{$category}; my $i = index($file, "\nExec="); next if $i == -1; my $execname = substr($file, $i + 6, index($file, "\n", $i + 6) - $i - 6); $execname =~ s/ +%.*// if index($execname, '%') != -1; if (defined $CONFIG->{ignore_app_command_re}) { next if $execname =~ /$CONFIG->{ignore_app_command_re}/o; } if (defined $CONFIG->{command_rem_re}) { $execname =~ s/$CONFIG->{command_rem_re}//go; } if (index($file, "\nTerminal=true") != -1 or index($file, "\nTerminal=1") != -1) { $execname = sprintf($CONFIG->{open_in_terminal}, $execname); } $i = index($file, "\nName="); next if $i == -1; my $name = substr($file, $i + 6, index($file, "\n", $i + 6) - $i - 6); if (defined $CONFIG->{ignore_app_name_re}) { next if $name =~ /$CONFIG->{ignore_app_name_re}/o; } push @{$categories{$category}}, [ $name, $execname, $icons ? do { $i = index($file, "\nIcon="); my $icon = substr $file, $i + 6, index($file, "\n", $i + 6) - $i - 6; $icon =~ s/\.\w{3}$// unless chr ord $icon eq '/'; $icon; } : undef ]; } } close $desktop_fh; foreach my $schema (@$SCHEMA) { if (exists $schema->{cat}) { next unless defined $categories{$schema->{cat}[0]}; next unless @{$categories{$schema->{cat}[0]}}; push_app(\@{$categories{$schema->{cat}[0]}}, $schema->{cat}[0], $schema->{cat}[1], ($schema->{cat}[2] // undef)); @{$categories{$schema->{cat}[0]}} = (); } elsif (exists $schema->{item}) { add_item(@{$schema->{item}}); } elsif (exists $schema->{submenu}) { push_app(\@menutray_config_menu, 'submenu', $schema->{submenu}); } elsif (exists $schema->{quit}) { if ($icons) { $generated_menu .= <<"QUIT_WITH_ICON"; my \$quit = Gtk2::ImageMenuItem->new(q{$schema->{quit}}); \$quit->set_image(Gtk2::Image->new_from_stock('gtk-quit', q{$CONFIG->{icon_type}})); QUIT_WITH_ICON } else { $generated_menu .= <<"QUIT_WITHOUT_ICON"; my \$quit = Gtk2::MenuItem->new(q{$schema->{quit}}); QUIT_WITHOUT_ICON } $generated_menu .= <<"QUIT"; \$quit->signal_connect('activate', sub { Gtk2->main_quit; exit }); \$menu->append(\$quit);\n QUIT } elsif (exists $schema->{regenerate}) { my $regenerate_exec = "$^X $0"; $regenerate_exec .= $icons ? $menutray_cat_icons ? ' -c' : ' -i' : ' -g'; if ($icons) { $generated_menu .= <<"REGENERATE_MENU_WITH_ICON"; my \$regenerate = Gtk2::ImageMenuItem->new(q{$schema->{regenerate}}); \$regenerate->set_image(Gtk2::Image->new_from_stock('gtk-refresh',q{$CONFIG->{icon_type}})); REGENERATE_MENU_WITH_ICON } else { $generated_menu .= <<"REGENERATE_MENU_WITHOUT_ICON"; my \$regenerate = Gtk2::MenuItem->new(q{$schema->{regenerate}}); REGENERATE_MENU_WITHOUT_ICON } $generated_menu .= <<"REGENERATE_MENU"; \$regenerate->signal_connect('activate',sub {system q{$regenerate_exec &}; Gtk2->main_quit; exit}); \$menu->append(\$regenerate);\n REGENERATE_MENU } elsif (exists $schema->{raw}) { add_item(@{$schema->{raw}}); } elsif (exists $schema->{sep}) { $generated_menu .= <<'SEPARATOR'; $menu->append(Gtk2::SeparatorMenuItem->new); SEPARATOR } } $generated_menu .= <<'FOOTER'; $menu->show_all; $menu->popup(undef, undef, sub {return Gtk2::StatusIcon::position_menu($menu, 0, 0, $icon)}, [1, 1], 0, 0); return 1; } 'Gtk2'->main; FOOTER open my $menu_fh, '>', $menufile or die "\nCannot open $menufile for write: $!\n"; print {$menu_fh} $generated_menu; close $menu_fh; } -e $menufile ? do $menufile : help();