5 # Changed on 11/30/10 by (dane):
6 # - changed DIR filehandles to scalars, they're stepping on each other.
7 # was a bug in xdg-menu.
8 # - changed default (only) format to fvwm2 and removed logic for other wms.
9 # - move mainline before all subs (move it back?).
10 # - removed the xdg_menu cache logic.
11 # - Remove $flag, $MENU_TYPE, $MENU_DESTROY for gtk vs. fvwm menu mode.
12 # Actually removed all GTK MENU logic.
13 # - Added option to menu to regenerate the menus.
14 # - Remove xdg option root_menu.
15 # - Added fvwm-menu option to use Fvwm Icons package.
16 # - Style options from fvwmmenudesktop not carried over,
17 # WM hints are sufficient to set mini-icon and icon.
18 # - Removed prototypes. None were needed.
19 # - All filehandle names made local.
20 # - New option for su_gui, do path check for alternatives.
22 # - Change menu prefix from xdg-menu to FvwmMenu
25 # - No testing for wm_icons has been done (is that fvwm-themes?).
26 # - Docs need to be updated.
27 # - fvwm-icons: okay, maybe the cat isn't a good default for Exec...
28 # - Running alacarte, I can see there are Application menus, and System menus.
29 # Figure out the options to get both.
30 # - There are good programs like alacarte that are marked GNOME-ONLY.
31 # Figure out how to get them in menus.
32 # - I think this should default to mini-icons enabled, but which
33 # icon package should be assumed?
34 # - Need a way to select the /usr/share/icons icon sets with themes like
35 # default/hicolor and size like 16x16 and 24x24.
36 # - Regenerate menus should prompt for a new command line.
37 # - sidepics, etc not tested.
38 # - iconname::: syntax needs investigation, I think some of it is obsolete,
39 # needs a verifier, and some defaults.
40 # - Far too many lexical globals not bound to subroutines (c.f.
42 # - I don't see the point to the path arguments. I think they should
44 # - If I pass --fvwm-icons I don't need to set --enable-mini-icons.
45 # Therefore I think enable mini-icons is pointless.
46 # - The "check" functions are useless. Since this module doesn't get the
47 # built in path, or the users path, it can't do the check.
48 # - Looks like there is no such thing as fvwm_toptitle, must have been a gtk thing?
50 # ---------------------------------------------------------------------------
52 # See the man page fvwm-menu-desktop.1 for instructions.
54 # Created on 22/07/1999 by Olivier Chapuis
56 # Updated on 15/08/1999 by Mikhael Goikhman
58 # Updated on 24/12/2010 by Dan Espen for xdg menus (see copyright below)
59 # ---------------------------------------------------------------------------
62 # The script is distributed by the same terms as fvwm itself.
63 # See GNU General Public License for details.
65 #----------------------------------------------------------------------------
67 # 25/12/2010 re-written, based heavily on
68 # xdg-menu for archlinux. based on suse xdg-menu written by <nadvornik@suse.cz>
69 # Sergej Pupykin <pupykin.s@gmail.com>
71 # >> Copyright (c) 2003 SuSE Linux AG, Nuernberg, Germany. All rights reserved.
73 # >> Author: nadvornik@suse.cz
80 use I18N::Langinfo qw(langinfo CODESET);
81 use POSIX qw(locale_h);
82 use Digest::MD5 qw(md5_hex);
85 my $xdg_data_dirs = $ENV{XDG_DATA_DIRS} || '';
86 my $xdg_config_dirs = $ENV{XDG_CONFIG_DIRS} || '';
87 my @PATH_DIRS = split(':',$ENV{PATH}); # for checking if applications exist
89 my $version = '@VERSION@';
90 my $menu_prefix='FvwmMenu';
92 my $DefaultDirectoryDirs;
94 my $desktop_name = 'fvwm2';
96 my $charset = 'iso-8859-1';
104 my $TERM_CMD = "xterm -e";
108 my %Directory_entries;
113 # Default for the mini-icons is mini/ (relatively to the ImagePath)
114 my $MINI_ICONS_DIR = 'mini/';
115 # Then the default for icon is ImagePath (consistent with kde, fvwm2gnome
116 # and almost consistent with wm-icons)
120 my $TRAN_MINI_ICONS = 'mini/';
122 my $MINI_ICONS = 0; # mini-icon disabled (enable =1)
123 my $TRAN = 0; # mini-icon translation disabled (enable =1)
124 my $wm_icons = 0; # use wm-icons compatible menu icon names
125 my $fvwm_icons = 0; # use fvwm-icons compatible menu icon names
127 my $check_icons = "";
128 my $check_mini_icons = "";
129 my @check_icons_path = ();
130 my @check_mini_icons_path = ();
135 my @menus_for_style = ();
138 $DI{fvwm_app} = 'mini-x.xpm:dh:%::'; # micon:law:placement:unused
139 $DI{fvwm_folder} = 'folder.xpm:dh:%::'; # idem
140 $DI{fvwm_title} = 'folder.xpm:dh:%::'; # micon:law:place:spic:color
141 $DI{fvwm_toptitle} = 'mini-k.xpm:no:%::'; # idem
142 my $OPT_INSTALL_PREFIX = '';
144 warn "invoked with args @ARGV\n" if $verbose;
147 "enable-mini-icons" => \$MINI_ICONS,
148 "enable-tran-mini-icons" => \$TRAN,
149 "mini-icons-path:s" => \$MINI_ICONS_DIR,
150 "png-icons-path:s" => \$PNG_ICONS,
151 "tran-mini-icons-path:s" => \$TRAN_MINI_ICONS,
152 "icon-toptitle:s" => \$DI{"fvwm_toptitle"},
153 "icon-title:s" => \$DI{"fvwm_title"},
154 "icon-folder:s" => \$DI{"fvwm_folder"},
155 "icon-app:s" => \$DI{"fvwm_app"},
156 "icon-style:s" => \&obsolete,
157 "icons-path:s" => \$ICONS_DIR,
158 "tran-icons-path:s" => \$TRAN_ICONS,
159 "wm-icons" => \$wm_icons,
160 "fvwm-icons" => \$fvwm_icons,
161 "check-mini-icon=s" => \$check_mini_icons,
162 "check-icons=s" => \$check_icons,
163 "help|h|?" => \&show_help,
164 "version|V" => \&show_version,
165 "install-prefix:s" => $OPT_INSTALL_PREFIX,
166 "type:s" => \&obsolete,
167 "fvwmgtk-alias=s" => \&obsolete,
168 "title:s" => \&obsolete,
169 "name:s" => \&obsolete,
170 "enable-style" => \&obsolete,
171 "enable-tran-style" => \&obsolete,
172 "submenu-name-prefix:s" => \&obsolete,
173 "dir:s" => \&obsolete,
174 "destroy-type:s" => \&obsolete,
175 "xterm:s" => \$TERM_CMD,
176 "lang:s" => \$language,
177 "utf8" => \&obsolete,
178 "uniconv=s" => \$charset,
179 "uniconv-exec=s" => \&obsolete,
180 "menu-style=s" => \$MENU_STYLE,
181 "check-app!" => \&obsolete,
182 "time-limit=s" => \&obsolete,
183 "merge-user-menu" => \&obsolete,
185 "su_gui" => \$root_cmd,
186 "verbose" => \$verbose
191 $DefaultAppDirs = get_app_dirs();
192 $DefaultDirectoryDirs = get_desktop_dirs();
194 $root_menu = get_root_menu();
196 @KDELegacyDirs = get_KDE_legacy_dirs();
198 $charset = langinfo(CODESET);
199 $language = setlocale(LC_MESSAGES);
201 if (! defined $root_cmd )
203 foreach (qw(gnomesu kdesu xdg_menu_su))
216 DEBUG: root menu is $root_menu
217 DEBUG: charset is $charset.
218 DEBUG: language is $language.
219 DEBUG: root-cmd is $root_cmd.|;
222 @language_keys = prepare_language_keys($language);
224 unless (-f $root_menu)
226 warn "ERROR: Can't find root menu file.\n";
230 my $tree = read_menu($root_menu);
235 my $menu = interpret_root($tree, '');
237 remove_allocated($menu);
238 preprocess_menu($menu);
239 remove_empty_menus($menu);
241 my $output = output_fvwm2_menu($menu);
244 # output the menu style
245 if ($MENU_STYLE ne "")
247 foreach (@menus_for_style)
249 print qq|ChangeMenuStyle "$MENU_STYLE" "$_"\n|;
255 # Set DI to list of icons to use:
269 $MINI_ICONS_DIR = "";
270 $DI{"fvwm_toptitle"} = "menu/folder-open.xpm:ow";
271 $DI{"fvwm_title"} = "menu/folder-open.xpm:ow";
272 $DI{"fvwm_folder"} = "menu/folder.xpm:ow";
273 $DI{"fvwm_app"} = "menu/utility.xpm:ow";
278 $MINI_ICONS_DIR = "";
279 $DI{"fvwm_toptitle"} = "mini.fvwm.xpm::%";
280 $DI{"fvwm_title"} = "mini.folder.xpm::%";
281 $DI{"fvwm_folder"} = "mini.ofolder.xpm::%";
282 $DI{"fvwm_app"} = "mini.cat.xpm::%";
285 foreach my $i (keys(%DI)) {
286 @list = split(':',$DI{$i});
287 $dmicon{$i} = $list[0]; # "default" mini-icon
288 $law{$i} = $list[1]; # default law
289 $place{$i} = $list[2]; # default position
290 $spic{$i} = $list[3]; # sidepic icon
291 $scolor{$i} = $list[4]; # color for sidepic
299 if ($MINI_ICONS_DIR ne 'mini/' or $ICONS_DIR ne '') {
300 if ($MINI_ICONS_DIR ne '') {
301 $MINI_ICONS_DIR =~ s/\/*$/\//;
304 if ($ICONS_DIR eq '') {
305 $ICONS_DIR = up_directory($MINI_ICONS_DIR);
306 } elsif ($ICONS_DIR eq 'inpath') {
309 $ICONS_DIR =~ s/\/*$/\// if $MINI_ICONS_DIR ne '';
313 if ($TRAN_MINI_ICONS ne 'mini/' or $TRAN_ICONS ne '') {
314 if ($TRAN_MINI_ICONS ne '') {
315 $TRAN_MINI_ICONS =~ s/\/*$/\//;
318 if ($TRAN_ICONS eq '') {
319 $TRAN_ICONS = up_directory($TRAN_MINI_ICONS);
320 } elsif ($TRAN_ICONS eq 'inpath') {
323 $TRAN_ICONS =~ s/\/*$/\// if $TRAN_ICONS ne '';
327 $PNG_ICONS =~ s/\/*$/\// if $PNG_ICONS ne '';
329 # init default mini-icons, law, place, sidepic, color
330 foreach my $i (qw(fvwm_app fvwm_folder fvwm_title fvwm_toptitle)) {
331 warn "DEBUG: foreach $i.\n" if $verbose; # dje debug
332 # With the mini-icons-tran options we "use" gtk default
334 $j = substr($i,index($i,'_'));
337 $tmp_icon = $dmicon{$j};
338 $tmp_icon =~ s/\.png$/\.xpm/;
339 $dmicon{$i} = "$TRAN_MINI_ICONS$tmp_icon";
341 $dmicon{$i} = "$MINI_ICONS_DIR$dmicon{$i}";
345 while (my ($key,$value) = each %dmicon) {
346 warn "INTERMEDIATE icons to use $key -> $value.\n";
349 @list = split(':',$DI{$i});
355 $law{$i} = $list[1] if ($list[1] eq 'no' or $list[1] eq 'ow'
356 or $list[1] eq 're' or $list[1] eq 'dh');
357 $dmicon{$i} = "$MINI_ICONS_DIR$list[0]" if $list[0] ne '';
358 $place{$i} = '*' if $list[2] eq 'up';
359 $dmicon{$i} = "$place{$i}$dmicon{$i}$place{$i}";
360 $dmicon{$i} = '' if $law{$i} eq 'no' or $MINI_ICONS == 0;
362 if ($list[3] ne '') {
363 $spic{$i} = "\@$list[3]\@";
368 if ($list[4] ne '' and $list[3] ne '') {
369 $scolor{$i} = "\^$list[4]\^";
374 if ($check_mini_icons ne "") {
375 @check_mini_icons_path = split(":", $check_mini_icons);
378 if ($check_icons ne "") {
379 @check_icons_path = split(":", $check_icons);
385 while (my ($key, $value) = each %dmicon) {
386 warn "icons to use $key -> $value.\n";
408 $dir = substr($dir, 0, rindex($dir, '/') + 1);
431 my ($pool, $dir, $topdir) = @_;
435 $topdir = $dir unless defined $topdir;
437 return if check_file($dir) ne 'D';
439 opendir( my $dir_fh, $dir ) or return;
440 foreach my $entry ( readdir($dir_fh) )
442 if (-f "$dir/$entry" and $entry =~ /\.desktop$/)
444 read_desktop_entry($pool, "$dir/$entry", $topdir);
446 elsif (-d "$dir/$entry" and $entry !~ /^\.{1,2}$/
447 and $entry ne '.hidden')
449 scan_AppDir($pool, "$dir/$entry", $topdir);
455 sub scan_DirectoryDir
457 my ($pool, $dir, $topdir) = @_;
460 $topdir = $dir unless defined $topdir;
462 opendir( my $dir_fh, $dir ) or return;
463 foreach my $entry (readdir($dir_fh))
465 if (-f "$dir/$entry" and $entry =~ /\.directory$/)
467 read_directory_entry( $pool, "$dir/$entry", $topdir );
469 elsif (-d "$dir/$entry" and $entry !~ /^\.{1,2}$/
470 and $entry ne '.hidden')
472 scan_DirectoryDir($pool, "$dir/$entry", $topdir);
478 sub read_directory_entry
480 my ($pool, $file, $topdir) = @_;
482 unless (defined $Directory_entries{$file})
488 warn "Read directory entry, opening file $file.\n";
490 open( my $file_fh, "<", $file ) or return;
491 my $in_desktop_entry = 0;
497 if (/^\[Desktop Entry\]/)
499 $in_desktop_entry = 1;
503 $in_desktop_entry = 0;
506 elsif ($in_desktop_entry and /^([^=]*)=([^[:cntrl:]]*)/)
518 $Directory_entries{$file} = \%entry;
520 my $entry = $Directory_entries{$file};
521 $pool->{'Directory_entries'}{ $entry->{'id'} } = $entry;
528 return 1 unless defined $entry;
530 my (%OnlyShowIn, %NotShowIn);
532 if (defined $entry->{'OnlyShowIn'})
534 foreach my $showin (split /;/, $entry->{'OnlyShowIn'})
536 $OnlyShowIn{$showin} = 1;
539 return 0 unless defined $OnlyShowIn{$desktop_name};
542 if (defined $entry->{'NotShowIn'})
544 foreach my $showin (split /;/, $entry->{'NotShowIn'})
546 $NotShowIn{$showin} = 1;
549 return 0 if defined $NotShowIn{$desktop_name};
555 sub read_desktop_entry
557 my ($pool, $file, $topdir) = @_;
559 unless (defined $Desktop_entries{$file}) {
562 warn "Read desktop entry, opening file $file.\n";
564 open( my $file_fh, "<", $file ) or return;
566 my $in_desktop_entry = 0;
570 if (/^\[Desktop Entry\]/) {
571 $in_desktop_entry = 1;
572 } elsif (/^\[.*\]/) {
573 $in_desktop_entry = 0;
575 } elsif ($in_desktop_entry && /^([^=]*)=([^[:cntrl:]]*)/) {
587 $entry{'refcount'} = 0;
588 $Desktop_entries{$file} = \%entry;
591 my $entry = $Desktop_entries{$file};
593 if (! defined $entry->{'Name'}) { # dje debug
594 warn "Name is not defined\n";
597 return unless defined $entry->{'Name'};
598 return unless defined $entry->{'Exec'};
600 if (defined $entry->{'Hidden'} and $entry->{'Hidden'} eq 'true') {
604 #FIXME, an option for this would be good
606 if (defined $entry->{'NoDisplay'} and $entry->{'NoDisplay'} eq 'true') {
610 return unless check_show_in($entry);
612 if (defined $entry->{'NotShowIn'} and
613 $entry->{'NotShowIn'} eq $desktop_name) {
617 if (defined $pool and defined $entry->{'Categories'}) {
618 foreach my $category (split /;/, $entry->{'Categories'}) {
619 $pool->{'Categories'}{$category} = []
620 unless defined $pool->{'Categories'}{$category};
621 push @{ $pool->{'Categories'}{$category} }, $entry;
623 $pool->{'Desktop_entries'}{ $entry->{'id'} } = $entry;
628 sub read_desktop_entries
630 my ($directory_paths, $desktop_paths) = @_;
632 'Desktop_entries' => {},
634 'Directory_entries' => {},
635 'Directory_paths' => $directory_paths,
636 'Desktop_paths' => $desktop_paths
639 foreach my $dir (split /:/, $directory_paths)
641 next if $dir =~ /^\s*$/;
642 scan_DirectoryDir( $pool, $dir );
645 foreach my $dir (split /:/, $desktop_paths)
647 next if $dir =~ /^\s*$/;
648 scan_AppDir( $pool, $dir );
654 sub get_directory_entry
656 my ($entry, $pool) = @_;
657 return $pool->{'Directory_entries'}{$entry};
660 sub interpret_Include
662 my ( $tree, $entries, $pool ) = @_;
664 my @list = interpret_entry_node( $tree, 'Or', $pool );
665 foreach my $e (@$entries)
667 if ( $e->{type} eq 'desktop' )
669 $exist{ $e->{desktop} } = 1;
673 foreach my $entry (@list)
675 next if $exist{$entry};
676 push @$entries, { type => 'desktop', desktop => $entry };
677 $entry->{'refcount'}++;
682 sub interpret_Exclude
684 my ( $tree, $entries, $pool ) = @_;
685 my @list = interpret_entry_node( $tree, 'Or', $pool );
686 foreach my $entry (@list)
689 while ( defined $entries->[$i] )
691 my $exist = $entries->[$i];
692 if ($exist->{type} eq 'desktop' and
693 $exist->{desktop} eq $entry )
695 splice @$entries, $i, 1;
696 $entry->{'refcount'}--;
706 sub interpret_entry_node
708 my ( $tree, $node, $pool ) = @_;
710 $i++ if ( ref( $tree->[$i] ) eq 'HASH' );
712 while (defined $tree->[$i])
714 if ($tree->[$i] eq 'Filename')
717 if (ref( $tree->[$i][0] ) eq 'HASH' and $tree->[$i][1] eq '0')
719 my $entry = $tree->[$i][2];
720 if (defined $pool->{'Desktop_entries'}{$entry})
722 push @subtree, [ $pool->{'Desktop_entries'}{$entry} ];
732 exit 1 if $die_on_error;
736 elsif ($tree->[$i] eq 'Category')
739 if (ref( $tree->[$i][0] ) eq 'HASH' and $tree->[$i][1] eq '0')
741 my $category = $tree->[$i][2];
742 if (defined $pool->{'Categories'}{$category})
744 push @subtree, $pool->{'Categories'}{$category};
754 exit 1 if $die_on_error;
758 elsif ($tree->[$i] eq 'All')
761 if (values %{ $pool->{'Desktop_entries'} } > 0)
763 push @subtree, [ values %{ $pool->{'Desktop_entries'} } ];
771 elsif ($tree->[$i] eq '0')
778 my @res = interpret_entry_node(
779 $tree->[ $i + 1 ], $tree->[$i], $pool
782 push @subtree, \@res;
792 foreach my $st (@subtree)
794 foreach my $entry (@$st)
796 if (!defined $used{$entry})
805 elsif ($node eq 'And')
810 my $min = @{ $subtree[0] };
813 foreach my $st (@subtree)
823 foreach my $entry (@$st)
825 next if $dupes{$entry};
828 if (!defined $used{$entry})
840 return () if $cnt == 0;
842 foreach my $entry (@{ $subtree[$min_idx] })
844 push @res, $entry if $used{$entry} == $cnt;
849 elsif ($node eq 'Not')
855 foreach my $st (@subtree) {
856 foreach my $entry (@$st) {
861 foreach my $entry (values %{ $pool->{'Desktop_entries'} })
863 push @res, $entry if !defined $used{$entry};
870 warn "Can't use '$node' inside <Include> or <Exclude>\n";
871 exit 1 if $die_on_error;
878 my ($tree, $topdir) = @_;
880 if ($tree->[0] eq 'Menu')
882 return interpret_menu( $tree->[1] );
886 warn "No toplevel Menu\n";
887 exit 1 if $die_on_error;
894 my ($tree, $directory_paths, $desktop_paths) = @_;
896 $directory_paths = '' unless defined $directory_paths;
897 $desktop_paths = '' unless defined $desktop_paths;
901 'OnlyUnallocated' => 0,
902 'DontShowIfEmpty' => 0,
908 $i++ if ref $tree->[$i] eq 'HASH';
910 while (defined $tree->[$i])
912 if ($tree->[$i] eq 'AppDir')
914 if (ref( $tree->[ $i + 1 ][0] ) eq 'HASH'
915 and $tree->[ $i + 1 ][1] eq '0')
917 $desktop_paths .= ':' . $tree->[ $i + 1 ][2];
918 splice @$tree, $i, 2;
922 warn "wrong AppDir\n";
923 exit 1 if $die_on_error;
928 elsif ($tree->[$i] eq 'DefaultAppDirs')
930 $desktop_paths .= ':' . $DefaultAppDirs;
931 splice @$tree, $i, 2;
933 elsif ($tree->[$i] eq 'DirectoryDir')
935 if (ref( $tree->[ $i + 1 ][0]) eq 'HASH'
936 and $tree->[ $i + 1 ][1] eq '0')
938 $directory_paths .= ':' . $tree->[ $i + 1 ][2];
939 splice @$tree, $i, 2;
943 warn "wrong DirectoryDir\n";
944 exit 1 if $die_on_error;
949 elsif ($tree->[$i] eq 'DefaultDirectoryDirs')
951 $directory_paths .= ':' . $DefaultDirectoryDirs;
952 splice @$tree, $i, 2;
961 $menu{directory_paths} = $directory_paths;
962 $menu{desktop_paths} = $desktop_paths;
964 my $pool = read_desktop_entries( $directory_paths, $desktop_paths );
967 $i++ if ref $tree->[$i] eq 'HASH';
969 while (defined $tree->[$i])
971 if ($tree->[$i] eq 'Menu')
974 my $submenu = interpret_menu(
975 $tree->[$i], $directory_paths, $desktop_paths
978 push( @{ $menu{'entries'} },
979 { type => 'menu', menu => $submenu }
983 elsif ($tree->[$i] eq 'Name')
986 if (ref($tree->[$i][0]) eq 'HASH' and $tree->[$i][1] eq '0')
988 $menu{'Name'} = $tree->[$i][2];
989 exit 1 if $die_on_error;
993 elsif ($tree->[$i] eq 'Directory')
996 if (ref( $tree->[$i][0] ) eq 'HASH' and $tree->[$i][1] eq '0')
998 $menu{'Directory'} = get_directory_entry(
999 $tree->[$i][2], $pool
1004 warn "wrong Directory\n";
1005 exit 1 if $die_on_error;
1009 elsif ($tree->[$i] eq 'OnlyUnallocated')
1011 $menu{'OnlyUnallocated'} = 1;
1015 elsif ($tree->[$i] eq 'DontShowIfEmpty')
1017 $menu{'DontShowIfEmpty'} = 1;
1021 elsif ($tree->[$i] eq 'Deleted')
1023 $menu{'Deleted'} = 1;
1027 elsif ($tree->[$i] eq 'NotDeleted')
1029 $menu{'Deleted'} = 0;
1033 elsif ($tree->[$i] eq 'Include')
1036 interpret_Include($tree->[$i], $menu{'entries'}, $pool);
1039 elsif ($tree->[$i] eq 'Exclude')
1042 interpret_Exclude($tree->[$i], $menu{'entries'}, $pool);
1045 elsif ($tree->[$i] eq '0')
1048 if ($tree->[$i] !~ /^\s*$/)
1050 print STDERR "skip '$tree->[$i]'\n";
1051 exit 1 if $die_on_error;
1057 warn "Unknown '$tree->[$i]':\n";
1059 warn " '@{$tree->[$i]}'\n";
1061 exit 1 if $die_on_error;
1070 my ($file, $basedir) = @_;
1072 if ($file !~ /^\// and defined $basedir)
1074 $file = "$basedir/$file";
1077 unless (defined $basedir)
1080 $basedir =~ s/\/[^\/]*$//;
1083 unless (check_file($file))
1085 warn "WARNING: '$file' does not exist\n";
1086 return [ 'Menu', [ {} ] ];
1089 warn "reading '$file'\n" if $verbose;
1091 my $parser = XML::Parser->new(Style => 'Tree');
1092 my $tree = $parser->parsefile($file);
1094 my $DefaultMergeDir = $file;
1095 $DefaultMergeDir =~ s/^.*\///;
1096 $DefaultMergeDir =~ s/\.menu$/-merged/;
1098 read_includes($tree, $basedir, $DefaultMergeDir);
1105 my ($dir, $basedir) = @_;
1109 if ($dir !~ /^\// and defined $basedir)
1111 $dir = "$basedir/$dir";
1114 if (check_file($dir) ne 'D')
1119 opendir( my $dir_fh, $dir );
1120 foreach my $entry (readdir($dir_fh))
1123 if (-f "$dir/$entry" and $entry =~ /\.menu$/)
1125 my $menu = read_menu("$dir/$entry");
1126 $menu = remove_toplevel_Menu($menu);
1139 $txt =~ s/&/&/g;
1147 my ($dir, $basedir) = @_;
1152 $basedir = $dir unless defined $basedir;
1154 return "" if check_file($dir) ne 'D';
1158 if ($dir eq $basedir)
1160 my $xmldir = quote_xml($dir);
1162 $out .= "<AppDir>$xmldir</AppDir>\n";
1163 $out .= "<DirectoryDir>$xmldir</DirectoryDir>\n";
1171 $name = quote_xml($name);
1172 $out .= "<Name>$name</Name>\n";
1175 if (-f "$dir/.directory")
1177 my $dir_id = "$dir/.directory";
1178 $dir_id =~ s/^$basedir//;
1180 $dir_id = quote_xml($dir_id);
1182 $out .= "<Directory>$dir_id</Directory>\n";
1185 if (opendir(my $dir_fh, $dir))
1187 foreach my $entry (readdir($dir_fh))
1189 if (-f "$dir/$entry" and $entry =~ /\.desktop$/)
1191 my $id = "$dir/$entry";
1192 $id =~ s/^$basedir//;
1195 $id = quote_xml($id);
1197 my $desktop = read_desktop_entry(
1198 undef, "$dir/$entry", $basedir
1201 $out .= "<Include><Filename>$id</Filename></Include>\n"
1202 unless defined $desktop->{'Categories'};
1204 elsif (-d "$dir/$entry" and $entry !~ /^\.{1,2}$/ and
1205 $entry ne '.hidden')
1207 $out .= read_legacy_dir( "$dir/$entry", $basedir );
1213 $out .= "</Menu>\n";
1217 sub remove_toplevel_Menu
1221 if ($tree->[0] eq 'Menu')
1223 shift @{ $tree->[1] } if ref $tree->[1][0] eq 'HASH';
1228 warn "No toplevel Menu\n";
1229 exit 1 if $die_on_error;
1236 my ($tree, $basedir, $DefaultMergeDir) = @_;
1238 $i++ if ref $tree->[$i] eq 'HASH';
1240 while (defined $tree->[$i])
1242 if ($tree->[$i] eq 'MergeFile')
1244 if (ref( $tree->[ $i + 1 ][0]) eq 'HASH'
1245 and $tree->[ $i + 1 ][1] eq '0')
1247 my $add_tree = read_menu(
1248 $tree->[ $i + 1 ][2], $basedir
1250 $add_tree = remove_toplevel_Menu($add_tree);
1252 splice @$tree, $i, 2, @$add_tree;
1256 warn "wrong MergeFile\n";
1257 exit 1 if $die_on_error;
1262 elsif ($tree->[$i] eq 'MergeDir')
1264 if (ref( $tree->[ $i + 1 ][0] ) eq 'HASH'
1265 and $tree->[ $i + 1 ][1] eq '0')
1267 my $add_tree = read_menu_dir( $tree->[ $i + 1 ][2], $basedir );
1268 splice @$tree, $i, 2, @$add_tree;
1272 warn "wrong MergeFile\n";
1273 exit 1 if $die_on_error;
1278 elsif ($tree->[$i] eq 'DefaultMergeDirs')
1280 my $add_tree = read_menu_dir( $DefaultMergeDir, $basedir );
1281 splice @$tree, $i, 2, @$add_tree;
1283 elsif ($tree->[$i] eq 'LegacyDir')
1285 if (ref( $tree->[ $i + 1 ][0] ) eq 'HASH'
1286 and $tree->[ $i + 1 ][1] eq '0')
1288 if (-d $tree->[ $i + 1 ][2])
1290 my $xml = read_legacy_dir( $tree->[ $i + 1 ][2] );
1291 warn "reading legacy directory '" . $tree->[ $i + 1 ][2] .
1294 my $parser = XML::Parser->new(Style => 'Tree');
1295 my $add_tree = $parser->parse($xml);
1296 $add_tree = remove_toplevel_Menu($add_tree);
1297 splice @$tree, $i, 2, @$add_tree;
1301 warn "legacy directory '"
1302 . $tree->[ $i + 1 ][2]
1305 splice @$tree, $i, 2, ();
1310 warn "wrong LegacyDir\n";
1311 exit 1 if $die_on_error;
1316 elsif ($tree->[$i] eq 'KDELegacyDirs')
1319 foreach my $dir (@KDELegacyDirs)
1321 my $xml = read_legacy_dir($dir);
1322 warn "reading legacy directory '$dir'\n" if $verbose;
1324 my $parser = new XML::Parser( Style => 'Tree' );
1325 my $add_tree = $parser->parse($xml);
1326 $add_tree = remove_toplevel_Menu($add_tree);
1327 push @out, @$add_tree;
1329 splice @$tree, $i, 2, @out;
1331 elsif ($tree->[$i] eq 'Menu')
1334 read_includes( $tree->[$i], $basedir, $DefaultMergeDir );
1352 $i++ if ref $tree->[$i] eq 'HASH';
1354 while (defined $tree->[$i])
1356 if ($tree->[$i] eq 'Name')
1359 if (ref( $tree->[$i][0] ) eq 'HASH' and $tree->[$i][1] eq '0')
1361 $name = $tree->[$i][2];
1366 warn "wrong Name\n";
1377 unless (defined $name)
1379 warn "Menu has no name element\n";
1387 my ($target, $source) = @_;
1390 $i++ if ref $source->[$i] eq 'HASH';
1392 while (defined $source->[$i])
1394 if ($source->[$i] ne 'Name')
1396 push @$target, $source->[$i];
1397 push @$target, $source->[ $i + 1 ];
1408 my %used; #menu name already used
1411 $i++ if ref $tree->[$i] eq 'HASH';
1413 while (defined $tree->[$i])
1415 if ($tree->[$i] eq 'Menu')
1417 my $name = get_menu_name($tree->[ $i + 1 ]);
1418 if (defined $used{$name})
1420 my $target = $used{$name};
1421 append_menu($tree->[$target], $tree->[ $i + 1 ]);
1423 splice @$tree, $i, 2;
1426 { # first appearance
1427 $used{$name} = $i + 1;
1440 $i++ if ref $tree->[$i] eq 'HASH';
1442 while (defined $tree->[$i])
1444 if ($tree->[$i] eq 'Menu')
1446 merge_menus($tree->[ $i + 1 ]);
1455 my ( $tree, $hash ) = @_;
1458 $i++ if ref $tree->[$i] eq 'HASH';
1460 while (defined $tree->[$i])
1462 if ($tree->[$i] eq 'Old')
1465 if (ref( $tree->[$i][0] ) eq 'HASH' and $tree->[$i][1] eq '0')
1467 $old = $tree->[$i][2];
1472 exit 1 if $die_on_error;
1476 if ($tree->[$i] eq 'New')
1479 if (ref( $tree->[$i][0] ) eq 'HASH' and $tree->[$i][1] eq '0')
1481 $hash->{$old} = $tree->[$i][2];
1486 exit 1 if $die_on_error;
1498 sub find_menu_in_tree
1500 my ( $path, $tree ) = @_;
1505 my $subpath = $path;
1506 $subpath =~ s/^[^\/]*\/*//;
1509 $i++ if ref $tree->[$i] eq 'HASH';
1511 while (defined $tree->[$i])
1513 if ($tree->[$i] eq 'Menu')
1515 if ($root eq get_menu_name( $tree->[ $i + 1 ]))
1522 'menu' => $tree->[ $i + 1 ]
1525 return find_menu_in_tree( $subpath, $tree->[ $i + 1 ] );
1532 #FIXME - TA: Don't return undef here, it's bad.
1538 my ($path, $tree) = @_;
1542 foreach my $elem (reverse split( /\//, $path))
1544 next if $elem eq '';
1545 my $menu = [ {}, 'Name', [ {}, 0, $elem ] ];
1546 push @$menu, ( 'Menu', $child ) if defined $child;
1547 $tail = $menu unless defined $tail;
1550 append_menu( $tail, $tree );
1559 $i++ if ref $tree->[$i] eq 'HASH';
1561 while (defined $tree->[$i])
1563 if ($tree->[$i] eq 'Move')
1565 read_Move($tree->[ $i + 1 ], \%move);
1566 splice @$tree, $i, 2;
1575 foreach my $source (keys %move)
1577 my $sourceinfo = find_menu_in_tree($source, $tree);
1579 if (defined $sourceinfo)
1581 my $target = copy_menu($move{$source}, $sourceinfo->{'menu'});
1582 splice @{ $sourceinfo->{'parent'} }, $sourceinfo->{'index'}, 2;
1583 push @$tree, ('Menu', $target);
1588 $i++ if ref $tree->[$i] eq 'HASH';
1590 while (defined $tree->[$i])
1592 if ($tree->[$i] eq 'Menu')
1594 move_menus($tree->[ $i + 1 ]);
1601 sub remove_allocated
1606 while ($i < @{ $menu->{'entries'} })
1608 my $entry = $menu->{'entries'}[$i];
1610 if ($entry->{type} eq 'menu')
1612 remove_allocated( $entry->{menu} );
1615 elsif ($entry->{type} eq 'desktop'
1616 and $menu->{'OnlyUnallocated'}
1617 and $entry->{desktop}{'refcount'} > 1)
1619 $entry->{desktop}{'refcount'}--;
1620 splice @{ $menu->{'entries'} }, $i, 1;
1630 sub remove_empty_menus
1635 while ($i < @{ $menu->{'entries'} })
1637 my $entry = $menu->{'entries'}[$i];
1639 if ($entry->{type} eq 'menu' and remove_empty_menus($entry->{menu}))
1641 splice @{ $menu->{'entries'} }, $i, 1;
1649 @{ $menu->{'entries'} } == 0 ? return 1 : return 0;
1654 my ( $exec, $desktop ) = @_;
1656 # Take out filename flags, etc.
1670 my $caption = $desktop->{Name};
1671 $exec =~ s/%c/$caption/g;
1674 if (defined $desktop->{Terminal})
1676 if ($desktop->{Terminal} eq '1' or $desktop->{Terminal} eq 'true')
1678 $exec = "$TERM_CMD $exec";
1682 if (defined $desktop->{'X-KDE-SubstituteUID'})
1684 if ($desktop->{'X-KDE-SubstituteUID'} eq '1'
1685 or $desktop->{'X-KDE-SubstituteUID'} eq 'true')
1687 $exec = "$root_cmd $exec"
1695 my ( $desktop, $entry ) = @_;
1697 foreach my $key (@language_keys)
1699 my $loc_entry = $entry . "[$key]";
1701 if (defined $desktop->{$loc_entry} and
1702 $desktop->{$loc_entry} !~ /^\s*$/)
1704 return $desktop->{$loc_entry};
1708 return $desktop->{$entry};
1713 # localize, sort, prepare_exec
1716 return 0 if $menu->{'Deleted'};
1717 return 0 unless check_show_in( $menu->{'Directory'} );
1719 if( defined $menu->{'Directory'} and
1720 defined $menu->{'Directory'}->{'NoDisplay'} and
1721 $menu->{'Directory'}->{'NoDisplay'} eq 'true')
1726 my $menu_name = $menu->{'Name'};
1727 if (defined $menu->{'Directory'})
1729 my $directory = $menu->{'Directory'};
1730 my $directory_name = get_loc_entry( $directory, 'Name' );
1732 if (defined $directory_name)
1734 if( !defined $directory->{"Encoding"} or
1735 $directory->{"Encoding"} eq 'UTF-8')
1737 Encode::from_to($directory_name, "utf8", $charset);
1740 $menu_name = $directory_name;
1744 $menu->{'PrepName'} = $menu_name;
1747 while ( defined $menu->{'entries'}[$i] )
1749 my $entry = $menu->{'entries'}[$i];
1750 if ( $entry->{'type'} eq 'desktop' )
1752 my $desktop = $entry->{desktop};
1753 my $name = $desktop->{'id'};
1754 my $desktop_name = get_loc_entry( $desktop, 'Name' );
1755 if ( defined $desktop_name )
1757 Encode::from_to( $desktop_name, "utf8", $charset )
1758 if !defined $desktop->{"Encoding"}
1759 || $desktop->{"Encoding"} eq 'UTF-8';
1760 $name = $desktop_name;
1762 $desktop->{'PrepName'} = $name;
1763 $entry->{'Name'} = $name;
1764 $entry->{'PrepName'} = $name;
1765 $desktop->{'PrepExec'} = prepare_exec( $desktop->{Exec}, $desktop );
1768 elsif ( $entry->{type} eq 'menu' )
1770 if ( preprocess_menu( $entry->{'menu'} ) )
1772 $entry->{'Name'} = $entry->{'menu'}{'Name'};
1773 $entry->{'PrepName'} = $entry->{'menu'}{'PrepName'};
1778 splice @{ $menu->{'entries'} }, $i, 1;
1783 warn "wrong menu entry type: $entry->{type}";
1784 exit 1 if $die_on_error;
1785 splice @{ $menu->{'entries'} }, $i, 1;
1789 $menu->{'entries'} = [
1791 $b->{'type'} cmp $a->{'type'}
1792 || $a->{'PrepName'} cmp $b->{'PrepName'}
1793 } @{ $menu->{'entries'} }
1798 while ( defined $menu->{'entries'}[$i] )
1800 my $entry = $menu->{'entries'}[$i];
1801 if (defined $prev_entry
1802 and $entry->{'type'} eq 'desktop'
1803 and $prev_entry->{'type'} eq 'desktop'
1804 and $prev_entry->{'PrepName'} eq $entry->{'PrepName'}
1805 and $prev_entry->{'desktop'}->{'PrepExec'} eq
1806 $entry->{'desktop'}->{'PrepExec'} )
1808 splice @{ $menu->{'entries'} }, $i, 1;
1812 $prev_entry = $entry;
1819 sub output_fvwm2_menu
1821 my ($menu, $toplevel, $path) = @_;
1823 $path = '' unless defined $path;
1824 $toplevel = 1 unless defined $toplevel;
1828 my $menu_name = $menu->{'PrepName'};
1829 my $menu_id = "$path-" . $menu->{'Name'};
1830 $menu_id =~ s/\s/_/g;
1831 $menu_id = $menu_prefix if $toplevel;
1832 foreach my $entry ( @{ $menu->{'entries'} } )
1834 if ( $entry->{type} eq 'menu' ) {
1835 $output .= output_fvwm2_menu( $entry->{'menu'}, 0, $menu_id );
1838 $output .= "DestroyMenu \"$menu_id\"\n";
1839 $output .= "AddToMenu \"$menu_id\" \"$dmicon{'fvwm_title'}$label$menu_name\" Title\n";
1841 if ($MENU_STYLE ne '')
1843 push @menus_for_style, $menu_id;
1846 foreach my $entry ( @{ $menu->{'entries'} } )
1848 if ( $entry->{type} eq 'desktop' )
1850 my $desktop = $entry->{desktop};
1851 my $name = $desktop->{'PrepName'};
1852 my $exec = $desktop->{'PrepExec'};
1853 $output .= "+ \"$dmicon{'fvwm_app'}$name\" Exec $exec\n";
1855 elsif ( $entry->{type} eq 'menu')
1857 my $name = $entry->{'menu'}{'PrepName'};
1858 my $id = "$menu_id-" . $entry->{'menu'}{'Name'};
1860 $output .= "+ \"$dmicon{'fvwm_folder'}$name\" Popup \"$id\"\n";
1864 warn "wrong menu entry type: $entry->{type}";
1869 if ("$menu_id" eq "$menu_prefix-System_Tools")
1871 $output .= "AddToMenu \"$menu_prefix-System_Tools\" " .
1872 "\"$dmicon{'fvwm_app'}Regenerate Applications Menu\" " .
1873 "FvwmForm FvwmForm-Desktop\n";
1880 my @menu_bases = (qw(
1886 # XXX - TA: 2011-04-10: Is it enough to assume only one match here is
1888 foreach my $dir ( split( /:/, $xdg_config_dirs ), "/etc/xdg" )
1890 foreach my $menu_name (@menu_bases)
1892 check_file("$dir/menus/$menu_name.menu");
1893 if ( -f "$dir/menus/$menu_name.menu" ) {
1894 return "$dir/menus/$menu_name.menu";
1906 return $ret unless check_app("kde-config");
1908 my @kde_xdgdata = split( /:/, `kde-config --path xdgdata-apps` );
1910 foreach (@kde_xdgdata)
1912 s/\/applications\/*\s*$//;
1915 foreach my $d (split( /:/, $xdg_data_dirs ),
1916 @kde_xdgdata, "/usr/share", "/opt/gnome/share")
1920 next if defined $used{$dir};
1921 next if check_file("$dir/applications") ne 'D';
1922 $ret .= ':' if $ret ne '';
1923 $ret .= "$dir/applications";
1928 foreach ( split( ':', $ret ) )
1930 warn "app dirs $_\n";
1936 sub get_desktop_dirs
1940 foreach my $dir ( split( /:/, $xdg_data_dirs ),
1941 qw(/usr/share /opt/kde3/share /opt/gnome/share) )
1943 next if defined $used{$dir};
1944 next if check_file("$dir/desktop-directories") ne 'D';
1945 $ret .= ':' if $ret ne '';
1946 $ret .= "$dir/desktop-directories";
1949 warn "desktop dirs $ret\n" if $verbose;
1953 sub get_KDE_legacy_dirs
1957 my @legacy_dirs = (qw(
1958 /etc/opt/kde3/share/applnk
1959 /opt/kde3/share/applnk
1963 if (check_app("kde-config"))
1965 push @legacy_dirs, reverse(split(/:/,`kde-config --path apps` ));
1968 foreach my $d ( @legacy_dirs )
1973 next if defined $used{$dir};
1974 next if check_file("$dir") ne 'D';
1978 warn "KDE legacy dirs @ret\n" if $verbose;
1982 sub prepare_language_keys
1984 my ($language) = @_;
1987 $language =~ s/\.[^@]*//; # remove .ENCODING
1989 if ( $language =~ /^([^_]*)_([^@]*)@(.*)$/)
1991 # LANG_COUNTRY@MODIFIER
1992 push @keys, $1 . '_' . $2 . '@' . $3;
1993 push @keys, $1 . '_' . $2;
1994 push @keys, $1 . '@' . $3;
1997 elsif ($language =~ /^([^_]*)_([^@]*)$/)
2000 push @keys, $1 . '_' . $2;
2003 elsif ($language =~ /^([^_]*)@(.*)$/)
2006 push @keys, $1 . '@' . $2;
2009 elsif ($language =~ /^([^_@]*)$/)
2018 # Fixme, remove unsupported options.
2022 A perl script which parses xdg menu definitions to build
2023 the corresponding fvwm menus. The script can also build
2024 Icon and MiniIcon styles for the desktop applications.
2028 --help show this help and exit
2029 --version show version and exit
2030 --install-prefix DIR install prefix of the desktop
2031 --desktop NAME desktop to build the menu for it:
2032 gnome-sys (default), gnome-user, gnome-redhat, gnome-madriva,
2034 --type NAME fvwm (default) or gtk for a FvwmGtk menu
2035 --fvwmgtk-alias NAME FvwmGtk module name, default is FvwmGtk
2036 --title NAME menu title, default depends on --desktop
2037 --name NAME menu name, default depends on --desktop
2038 --merge-user-menu merge the system menu with the user menu
2039 --enable-mini-icons enable mini-icons in menu
2040 --enable-tran-mini-icons enable mini-icons in menu and
2041 translation of foo.png icon names to foo.xpm
2042 --mini-icons-path DIR path of menus icons (relative to your
2043 ImagePath), default is 'mini/'
2044 --png-icons-path DIR path of .png icons, default is your ImagePath
2045 --tran-mini-icons-path DIR path of menus icons for translation
2046 --check-mini-icons PATH check if the mini icons are in PATH
2047 --icon-toptitle micon:law:place:sidepic:color mini-icon for the top
2048 title and sidepic for the top menu
2049 --icon-title micon:law:place:sidepic:color as above for sub menus
2050 --icon-folder micon:law:place mini-icons for folder item
2051 --icon-app micon:law:place mini-icon for applications item
2052 --wm-icons define menu icon names to use with wm-icons
2053 --enable-style build icons and mini-icons style
2054 --enable-tran-style as above with translation (for FvwmGtk menus)
2055 --icon-style micon:icon:law icons for style
2056 --icons-path DIR define the directory of the icons,
2057 the default is very good
2058 --tran-icons-path DIR similar to the above option.
2059 --check-icons PATH check if the icons are in the PATH
2060 --submenu-name-prefix NAME in general not useful
2061 --dir DIR use path as desktop menu description
2062 --destroy-type FLAG how to destroy menu, valid values:
2063 'yes', 'no', 'dynamic', the default depends on --type
2064 --xterm CMD complete terminal command to run applications
2065 in it, default is 'xterm -e'
2066 --lang NAME language, default is \$LANG
2067 --utf8 For desktop entries coded in UTF-8 (KDE2)
2068 --uniconv Use (un)iconv for UTF-8 translation
2069 --uniconv-exec uniconv or iconv (default)
2070 --menu-style name assign specified MenuStyle name to menus
2071 --[no]check-app [do not] check that apps are in your path
2072 --time-limit NUM limit script running time to NUM seconds
2073 --verbose display debug type info oni STDERR
2074 Short options are ok if not ambiguous: -h, -x, -icon-a.
2079 # Check if application binary is executable and reachable
2083 # If full path, dont use path, just check path
2084 if ( substr($app,0,1) eq '/' and -x $app )
2089 # Check if an application is in the path
2090 foreach (@PATH_DIRS)
2092 return 1 if -x "$_/$app";
2107 print "WARNING: Argument \"$arg\" obsolete. Ignored.\n";
2110 # compile-command: "perl fvwm-menu-desktop.in --enable-mini-icons --fvwm-icons"