#!/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 . # # Openbox Menu Generator # A really fast pipe/static menu generator for the Openbox Window Manager. # Should be installed as '/usr/bin/obmenu-generator' before execution! # # License: GPLv3 # Created on: 25 March 2011 # Latest edit on: 02 March 2012 # Website: http://trizen.googlecode.com # use strict; # use warnings; my $appname = 'obmenu-generator'; my $version = '0.5.0'; our ($CONFIG, $SCHEMA); my $output_h = *STDOUT; my ($pipe, $static, $icons, $reconfigure, $stdout_config); my (@desktop_files, %categories, %categories_icons, %icons_db); my $home_dir = $ENV{HOME} // $ENV{LOGDIR} // (getpwuid($<))[7] // `echo -n ~`; my $xdg_config_home = defined $ENV{XDG_CONFIG_HOME} ? $ENV{XDG_CONFIG_HOME} : $home_dir . '/.config'; my $obmenugen_dir = "$xdg_config_home/obmenu-generator"; my $config_file = "$obmenugen_dir/configuration.pl"; my $openbox_conf = "$xdg_config_home/openbox"; my $ob_menu_file = "$openbox_conf/menu.xml"; my $icons_db = "$obmenugen_dir/icons.db"; sub usage { print <<"HELP"; usage: $0 [options]\n Options: -p : (re)generate a pipe menu -s : (re)generate a static menu -d : (re)generate icons.db (with -i) -r : (re)generate config file -i : use icons in menus -C : print configuration to STDOUT -R : reconfigure openbox Examples: ** Static menu without icons: $0 -s ** Pipe menu with icons: $0 -p -i NOTE: After a pipe menu is generated, '-p' is not not needed anymore. ** Config file: $config_file HELP exit 0; } if (@ARGV) { foreach my $arg (@ARGV) { if ($arg eq '-s') { $static = 1; } elsif ($arg eq '-p') { $pipe = 1; } elsif ($arg eq '-r') { $reconfigure = 1; } elsif ($arg eq '-C') { $stdout_config = 1; $reconfigure = 1; } elsif ($arg eq '-i') { $icons = 1; } elsif ($arg eq '-d') { unlink $icons_db; } elsif ($arg eq '-h') { usage(); } elsif ($arg eq '-v') { print "$appname $version\n"; exit 0; } elsif ($arg eq '-R') { exec 'openbox', '--reconfigure'; } } } unless (-e $obmenugen_dir) { require File::Path; File::Path::make_path($obmenugen_dir) or die "Unable to create $obmenugen_dir: $!\n"; } if (not -e $config_file or $reconfigure) { my $fh = *STDOUT; unless ($stdout_config) { open $fh, '>', $config_file or die "Open failed: $!"; } print {$fh} <<'CONFIG_FILE'; #!/usr/bin/perl # obmenu-generator config file # # SCHEMA supports the following keys: item, cat, exit, raw, sep, submenu # # Posible values for each of this types are: # For 'item': [COMMAND, LABEL, ICON] - icon is optional # For 'sep' : A string representing the LABEL for the separator or undef for none # For 'cat' : Any of the possible categories. 'cat => [CATEGORY, LABEL, ICON]' - icon is optional # For 'raw' : A hardcoded XML line in the Openbox's menu.xml file format # Example : {raw => ''}, # 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', # Giving this an undef value don't hides the menuitem # but uses the default OpenBox action "Exit" exit_command => undef, ); our $CONFIG = { # Example: [ "$ENV{'HOME'}/.local/share/applications", '/my/path' ] desktop_files_paths => ['/usr/share/applications'], # File where to look for icon theme (default: ~/.gtkrc-2.0) gtk_rc_file => undef, # When 'Terminal=true' open_in_terminal => "$items{terminal} -e %s", # Editor command open_in_editor => "$items{editor}", # Ignore desktop files if their filenames match a regex ignore_file_name_re => undef, # Ignore applications if their names match a regex ignore_app_name_re => undef, # Ignore applications if their commands match a regex ignore_app_command_re => undef, # Ignore desktop files if their content match a regex ignore_file_content_re => undef, # Remove from every command something matched by a regex (/g) command_rem_re => undef, # Look in this directories first (when generating icons.db) dirs_first_to_look => [], # Look in this directories as a second icon theme (when generating icons.db) dirs_middle_to_look => [], # Look in this directories, as a backup plan (when generating icons.db) dirs_last_to_look => [], }; 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']}, {sep => 'Applications'}, # 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 => 'Openbox Settings'}, {sep => undef}, {item => [$items{lock_command}, 'Lock', 'lock']}, {exit => [$items{exit_command}, 'Exit', 'exit']}, ]; CONFIG_FILE close $fh; exit 0; } 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; } if ($pipe or $static) { my $menu_backup = $ob_menu_file . '.bak'; if (not -e $menu_backup and -e $ob_menu_file) { require File::Copy; File::Copy::copy($ob_menu_file, $menu_backup); } } if ($pipe or not -e $ob_menu_file || $static) { mkdir $openbox_conf unless -e $openbox_conf; require Cwd; my $exec_name = Cwd::abs_path($0) . ($icons ? ' -i' : ''); open my $fh, '>', $ob_menu_file or die "Unable to open $ob_menu_file for write: $!\n"; print {$fh} <<"PIPE_MENU_HEADER"; PIPE_MENU_HEADER close $fh; } if ($static) { open $output_h, '>', $ob_menu_file or die "$ob_menu_file: Open failed: $!"; } 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="); 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="); 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; } if ($name =~ tr/"&//) { $name =~ s/&/&/g; $name =~ s/"/"/g; } push @{$categories{$category}}, [ $name, $execname, $icons ? do { $i = index($file, "\nIcon="); substr $file, $i + 6, index($file, "\n", $i + 6) - $i - 6; } : undef ]; } } close $desktop_fh; my $generated_menu = $static ? <<'STATIC_MENU' STATIC_MENU : "\n"; if ($icons) { # Categories: name => icon (%categories_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', ); # Load/Create icon database require GDBM_File; GDBM_File->import; tie %icons_db, 'GDBM_File', $icons_db, &GDBM_WRCREAT, 0640; } sub store_icons { return if substr($File::Find::name, -4, 1) ne q{.}; return if substr($_, -4, 4, '') eq '.svg'; return if exists $icons_db{$_}; $icons_db{$_} = $File::Find::name; } sub get_icon { my ($icon_name) = @_; if (length $icon_name) { $icon_name = unpack('A*', $icon_name) if substr($icon_name, -1) eq q{ }; if (chr ord $icon_name eq '/') { return $icon_name if -f $icon_name; } else { $icon_name =~ s/\.\w{3}$//; } } else { return ''; } my $icon = $icons_db{$icon_name}; unless (defined $icon) { my $gtk_rc_file = $CONFIG->{gtk_rc_file} // "$home_dir/.gtkrc-2.0"; my $icon_theme; if (-r $gtk_rc_file) { if (sysopen my $gtk_rc_fh, $gtk_rc_file, 0) { my $gtk_rc_content; sysread $gtk_rc_fh, $gtk_rc_content, -s $gtk_rc_file; if ($gtk_rc_content =~ /^\s*gtk-icon-theme-name\s*=\s*["']?([^'"\r\n]+)/im) { $icon_theme = $1; } close $gtk_rc_fh; } } my $personal_theme; require File::Find; # Look in this directories first if (ref $CONFIG->{dirs_first_to_look} eq 'ARRAY' and (my @dirs = grep { -d } @{$CONFIG->{dirs_first_to_look}})) { File::Find::find(\&store_icons, @dirs); } if (defined $icon_theme) { if (-d "/usr/share/icons/$icon_theme/") { $personal_theme = "/usr/share/icons/$icon_theme/"; } elsif (-d "$home_dir/.icons/$icon_theme/") { $personal_theme = "$home_dir/.icons/$icon_theme/"; } elsif (-d "$home_dir/.local/share/icons/$icon_theme/") { $personal_theme = "$home_dir/.local/share/icons/$icon_theme/"; } if (defined $personal_theme) { File::Find::find(\&store_icons, $personal_theme); } # Look in this directories as a second icon theme if (ref $CONFIG->{dirs_middle_to_look} eq 'ARRAY' and (my @dirs = grep { -d } @{$CONFIG->{dirs_middle_to_look}})) { File::Find::find(\&store_icons, @dirs); } File::Find::find( \&store_icons, grep { -d } ( '/usr/share/pixmaps/' => '/usr/share/icons/hicolor/', "$home_dir/.local/share/icons" => "$home_dir/.icons/" ) ); if (-d '/usr/share/icons/gnome/' and not $icon_theme =~ /^gnome$/i) { File::Find::find(\&store_icons, '/usr/share/icons/gnome/'); } } else { File::Find::find( \&store_icons, grep { -d } ( '/usr/share/pixmaps/' => "$home_dir/.local/share/icons", "$home_dir/.icons/" => '/usr/share/icons/' ) ); } # Look in this directories last if (ref $CONFIG->{dirs_last_to_look} eq 'ARRAY' and (my @dirs = grep { -d } @{$CONFIG->{dirs_last_to_look}})) { File::Find::find(\&store_icons, @dirs); } } unless (defined $icon) { $icon = $icons_db{$icon_name}; unless (defined $icon) { $icons_db{$icon_name} = ''; $icon = ''; } } $icon; # return icon } sub add_item { $generated_menu .= $icons && defined $_[2] ? do { my $icon = get_icon($_[2]); <<"ITEM_WITH_ICON" $_[0] ITEM_WITH_ICON } : <<"ITEM"; $_[0] ITEM } foreach my $schema (@$SCHEMA) { if (exists $schema->{cat}) { next unless defined $categories{$schema->{cat}[0]}; next unless @{$categories{$schema->{cat}[0]}}; $generated_menu .= $icons ? do { my $icon = defined $schema->{cat}[2] && -f $schema->{cat}[2] ? $schema->{cat}[2] : get_icon($schema->{cat}[2] // $categories_icons{$schema->{cat}[0]}); <<"MENU_WITH_ICON" MENU_WITH_ICON } : <<"MENU"; MENU $generated_menu .= join( '', ( sort { lc $a cmp lc $b } map { $icons ? do { my $icon = get_icon($_->[2]); <<"ITEM_WITH_ICON" $_->[1] ITEM_WITH_ICON } : <<"ITEM"; $_->[1] ITEM } @{$categories{$schema->{cat}[0]}} ) ); $generated_menu .= " \n"; } elsif (exists $schema->{item}) { add_item(@{$schema->{item}}); } elsif (exists $schema->{sep}) { $generated_menu .= defined $schema->{sep} ? qq[ \n] : " \n"; } elsif (exists $schema->{exit}) { if (defined $schema->{exit}[0]) { add_item(@{$schema->{exit}}); } else { $generated_menu .= $icons ? do { my $icon = defined $schema->{exit}[2] ? -f $schema->{exit}[2] ? $schema->{exit}[2] : get_icon($schema->{exit}[2]) : ''; <<"EXIT_WITH_ICON" EXIT_WITH_ICON } : <<'EXIT'; EXIT } } elsif (exists $schema->{raw}) { $generated_menu .= " $schema->{raw}\n"; } elsif (exists $schema->{submenu}) { $generated_menu .= <<"CONFIG_MENU"; CONFIG_MENU if (-e '/usr/bin/obconf') { $generated_menu .= <<'EOL'; obconf EOL } $generated_menu .= <<"CONFIG_MENU"; $CONFIG->{open_in_editor} $openbox_conf/autostart $CONFIG->{open_in_editor} $openbox_conf/rc.xml $0 -p $0 -s $0 -p -i $0 -s -i $CONFIG->{open_in_editor} $ob_menu_file $CONFIG->{open_in_editor} $config_file $0 -r CONFIG_MENU } } untie %icons_db if $icons; print {$output_h} $generated_menu, $static ? " \n\n" : "\n";