#!/usr/bin/perl

# Copyright (C) 2010-2021 Daniel "Trizen" Șuteu <echo dHJpemVuQHByb3Rvbm1haWwuY29tCg== | base64 -d>.
#
# 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 <https://www.gnu.org/licenses/>.

# Openbox Menu Generator
# A fast menu generator for the Openbox Window Manager, with support for icons.

# Edited on 07 December 2014 by Bob Currey
#   added cmd line option -t "Menu Label Text" with default of "Applications"

# Name: obmenu-generator
# License: GPLv3
# Created: 25 March 2011
# Latest edit: 22 August 2021
# https://github.com/trizen/obmenu-generator

use 5.014;

#use strict;
#use warnings;

use Linux::DesktopFiles;

my $pkgname = 'obmenu-generator';
my $version = '0.90';

our ($CONFIG, $SCHEMA);
my $output_h = \*STDOUT;

my ($pipe, $static, $with_icons, $reload_config, $db_clean, $update_config, $reconf_openbox);

my $home_dir =
     $ENV{HOME}
  || $ENV{LOGDIR}
  || (getpwuid($<))[7]
  || `echo -n ~`;

my $xdg_config_home = "$home_dir/.config";

my $menu_id         = "root-menu";
my $menu_label_text = "Applications";
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 $cache_db        = "$config_dir/cache.db";
my $icons_dir       = "$config_dir/icons";

sub usage {
    print <<"HELP";
usage: $0 [options]

menu:
    -p         : generate a dynamic menu (pipe)
    -s         : generate a static menu
    -i         : include icons
    -m <id>    : menu id (default: 'root-menu')
    -t <label> : menu label text (default: 'Applications')

misc:
    -u         : update the config file
    -d         : regenerate the cache file
    -c         : reconfigure openbox automatically
    -R         : reconfigure openbox and exit
    -S <file>  : absolute path to the schema.pl file
    -C <file>  : absolute path to the config.pl file
    -o <file>  : absolute path to the menu.xml file

info:
    -h         : print this message and exit
    -v         : print version and exit

examples:
        $0 -p -i     # dynamic menu with icons
        $0 -s -c     # static menu without icons

=> 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. (e.g.: name.desktop)
                            Example: qr/^(?:gimp|xterm)\\b/,    # skips 'gimp' and 'xterm'

    | skip_entry          : Skip a desktop file if the value from a given key matches the regex.
                            Example: [
                                {key => 'Name',       re => qr/(?:about|terminal)/i},
                                {key => 'Exec',       re => qr/^xterm/},
                                {key => 'OnlyShowIn', re => qr/XFCE/},
                            ],

    | substitutions       : Substitute, by using a regex, in the values from the desktop files.
                            Example: [
                                {key => 'Exec', re => qr/xterm/, value => 'tilix', global => 1},
                            ],

|| ICON SETTINGS
    | gtk_version         : The version of the Gtk library used for resolving the icon paths. (default: 3)
    | gtk_rc_filename     : Absolute path to the Gtk configuration file.
    | missing_icon        : Use this icon for missing icons (default: gtk-missing-image)
    | icon_size           : Preferred size for icons. (default: 48)
    | generic_fallback    : Try to shorten icon name at '-' characters before looking at inherited themes. (default: 0)
    | force_icon_size     : Always get the icon scaled to the requested size. (default: 0)

|| PATHS
    | desktop_files_paths   : Absolute paths which contain .desktop files.
                              Example: [
                                '/usr/share/applications',
                                "\$ENV{HOME}/.local/share/applications",
                                glob("\$ENV{HOME}/.local/share/applications/wine/Programs/*"),
                              ],

|| NOTES
    | Regular expressions:
        * use qr/.../ instead of '...'
        * use qr/.../i for case insensitive mode
HELP

sub remove_database {
    my ($db) = @_;

    foreach my $file ($db, "$db.dir", "$db.pag") {
        unlink($file) if (-e $file);
    }
}

if (@ARGV) {
    while (defined(my $arg = shift @ARGV)) {
        if ($arg eq '-i') {
            $with_icons = 1;
        }
        elsif ($arg eq '-p') {
            $pipe = 1;
        }
        elsif ($arg eq '-s') {
            $static = 1;
        }
        elsif ($arg eq '-d') {
            $db_clean = 1;
            remove_database($cache_db);
        }
        elsif ($arg eq '-u') {
            $update_config = 1;
        }
        elsif ($arg eq '-v') {
            print "$pkgname $version\n";
            exit 0;
        }
        elsif ($arg eq '-c') {
            $reconf_openbox = 1;
        }
        elsif ($arg eq '-R') {
            exec 'openbox', '--reconfigure';
        }
        elsif ($arg eq '-S') {
            $schema_file = shift(@ARGV) // die "$0: option '-S' requires an argument!\n";
        }
        elsif ($arg eq '-C') {
            $reload_config = 1;
            $config_file   = shift(@ARGV) // die "$0: options '-C' requires an argument!\n";
        }
        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 '-t') {
            $menu_label_text = shift(@ARGV) // die "$0: option '-t' 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 "$0: can't create configuration directory `$config_dir': $!\n";
}

if ($with_icons and not -d $icons_dir) {
    remove_database($cache_db);
    require File::Path;
    File::Path::make_path($icons_dir)
      or warn "$0: can't create icon path `$icons_dir': $!\n";
}

my $config_documentation = <<"EOD";
#!/usr/bin/perl

# $pkgname - configuration file
# This file will be updated automatically.
# 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',

        skip_entry       => undef,
        substitutions    => undef,
        skip_filename_re => undef,

        terminalize            => 1,
        terminalization_format => q{%s -e '%s'},

#<<<
        desktop_files_paths => [
            '/usr/share/applications',
            '/usr/local/share/applications',
            '/usr/share/applications/kde4',
            "$home_dir/.local/share/applications",
        ],
#>>>

    },

    terminal        => 'xterm',
    editor          => 'geany',
    missing_icon    => 'gtk-missing-image',
    gtk_rc_filename => "$home_dir/.config/gtk-3.0/settings.ini",

    icon_size        => 48,
    force_icon_size  => 0,
    generic_fallback => 0,
    locale_support   => 1,
    gtk_version      => 3,

    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) . "\n";
    $dumped_config =~ s/\Q$home_dir\E/\$ENV{HOME}/g if ($home_dir eq $ENV{HOME});
    print $config_fh $config_documentation, $dumped_config;
    close $config_fh;
}

if (not -e $config_file) {
    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 warn "$0: can't copy file `$etc_schema_file' to `$schema_file': $!\n";
    }
    else {
        die "$0: schema file `$schema_file' does not exists!\n";
    }
}

# Load the configuration files
require $schema_file;
require $config_file if $reload_config;

# Remove invalid user-defined keys
my @valid_keys = grep { exists $CONFIG{$_} } keys %$CONFIG;
@CONFIG{@valid_keys} = @{$CONFIG}{@valid_keys};

if ($CONFIG{VERSION} != $version) {
    $CONFIG{VERSION} = $version;
    dump_configuration();
}

my $desk_obj = Linux::DesktopFiles->new(
    %{$CONFIG{'Linux::DesktopFiles'}},

    categories => [map { exists($_->{cat}) ? $_->{cat}[0] : () } @$SCHEMA],

    keys_to_keep => ['Name', 'Exec',
                     ($with_icons ? 'Icon' : ()),
                     (
                      ref($CONFIG{'Linux::DesktopFiles'}{skip_entry}) eq 'ARRAY'
                      ? (map { $_->{key} } @{$CONFIG{'Linux::DesktopFiles'}{skip_entry}})
                      : ()
                     ),
                    ],

    terminal              => $CONFIG{terminal},
    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);

        if (not -x $exec_name) {
            $exec_name = "$^X $exec_name";
        }

        $with_icons && ($exec_name .= q{ -i});

        open my $fh, '>', $menufile
          or die "Can't open file '${menufile}' for write: $!";
        print $fh <<"PIPE_MENU_HEADER";
<?xml version="1.0" encoding="utf-8"?>
<openbox_menu xmlns="https://openbox.org/"
 xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="https://openbox.org/">
    <menu id="$menu_id" label="$pkgname" execute="$exec_name" />
</openbox_menu>
PIPE_MENU_HEADER
        close $fh;

        print STDERR <<'EOT';
:: A dynamic menu has been successfully generated!
EOT

        exec 'openbox', '--reconfigure';
    }
}

my $generated_menu = $static
  ? <<"STATIC_MENU_HEADER"
<?xml version="1.0" encoding="utf-8"?>
<openbox_menu xmlns="https://openbox.org/"
 xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="https://openbox.org/">
  <menu id="$menu_id" label="$menu_label_text">
STATIC_MENU_HEADER
  : "<openbox_pipe_menu>\n";

sub get_icon_path {
    my ($name) = @_;

    state $gtk = do {

        require Digest::MD5;

        ($CONFIG{gtk_version} == 3)
          ? do {
            eval "use Gtk3";
            'Gtk3'->init;
            'Gtk3';
          }
          : do {
            require Gtk2;
            'Gtk2'->init;
            'Gtk2';
          };
    };

    state $theme =
      ($gtk eq 'Gtk2')
      ? Gtk2::IconTheme->get_default
      : Gtk3::IconTheme::get_default();

#<<<
    state $flags = "${gtk}::IconLookupFlags"->new(
        [
            ($CONFIG{force_icon_size}  ? 'force-size'        : ()),
            ($CONFIG{generic_fallback} ? 'generic-fallback'  : ()),
        ]
    );
#>>>

    foreach my $icon_name ($name, $CONFIG{missing_icon}) {

#<<<
        my $pixbuf = eval {
            (substr($icon_name, 0, 1) eq '/')
            ? (substr($icon_name, -4) eq '.xpm')
                ? "${gtk}::Gdk::Pixbuf"->new_from_file($icon_name)->scale_simple($CONFIG{icon_size}, $CONFIG{icon_size}, 'hyper')
                : "${gtk}::Gdk::Pixbuf"->new_from_file_at_size($icon_name, $CONFIG{icon_size}, $CONFIG{icon_size})
            : $theme->load_icon($icon_name, $CONFIG{icon_size}, $flags);
        };
#>>>

        if (defined($pixbuf)) {
            my $md5  = Digest::MD5::md5_hex($pixbuf->get_pixels);
            my $path = "$icons_dir/$md5.png";
            $pixbuf->save($path, 'png') if not -e $path;
            return $path;
        }
    }

    return '';
}

# Regenerate the cache db if the config or schema file has been modified
if (!$db_clean and ((-M $config_file) < (-M $cache_db) or (-M _) > (-M $schema_file))) {
    print STDERR ":: Regenerating the cache DB...\n";
    remove_database($cache_db);
    $db_clean = 1;
}

eval { require GDBM_File };

dbmopen(my %cache_db, $cache_db, 0777)
  or die "Can't create/access database <<$cache_db>>: $!";

# Regenerate the icon db if the GTKRC file has been modified
if ($with_icons) {
    my $gtkrc_mtime = (stat $CONFIG{gtk_rc_filename})[9];

    if ($db_clean) {
        $cache_db{__GTKRC_MTIME__} = $gtkrc_mtime;
    }
    else {
        my $old_mtime = exists($cache_db{__GTKRC_MTIME__}) ? $cache_db{__GTKRC_MTIME__} : -1;
        if ($old_mtime != $gtkrc_mtime) {
            print STDERR ":: Regenerating the cache DB...\n";

            dbmclose(%cache_db);
            remove_database($cache_db);

            dbmopen(%cache_db, $cache_db, 0777)
              or die "Can't create database <<$cache_db>>: $!";

            $cache_db{__GTKRC_MTIME__} = $gtkrc_mtime;
        }
    }
}

{
    my %fast_cache;

    sub check_icon {
        $fast_cache{$_[0] // return undef} //= do {
            exists($cache_db{$_[0]})
              ? $cache_db{$_[0]}
              : do { $cache_db{$_[0]} = get_icon_path($_[0]) }
        };
    }
}

sub begin_category {
    if ($with_icons and (my $icon_path = check_icon($_[1]))) {
        <<"MENU_WITH_ICON";
  <menu id="${\rand()}" icon="$icon_path" label="$_[0]">
MENU_WITH_ICON
    }
    else {
        <<"MENU";
  <menu id="${\rand()}" label="$_[0]">
MENU
    }
}

my %categories;
foreach my $file ($desk_obj->get_desktop_files) {

    my %info = split("\0\1\0", (exists($cache_db{$file}) ? $cache_db{$file} : ''), -1);

    next if exists $info{__IGNORE__};

    my $mtime    = (stat $file)[9];
    my $cache_ok = (%info and $info{__MTIME__} == $mtime);

    if ($with_icons and $cache_ok and not exists $info{Icon}) {
        $cache_ok = 0;
    }

    if (not $cache_ok) {

        my $entry = $desk_obj->parse_desktop_file($file) // do {
            $cache_db{$file} = join("\0\1\0", __IGNORE__ => 1);
            next;
        };

#<<<
        %info = (
            Name => $entry->{Name} // next,
            Exec => $entry->{Exec} // next,

            (
             $with_icons
             ? (Icon => check_icon($entry->{Icon}))
             : ()
            ),

            __CATEGORIES__ => join(';', @{$entry->{Categories}}),
            __MTIME__      => $mtime,
        );
#>>>

        eval {

            state $x = do {
                require Encode;
                require File::DesktopEntry;
            };

            $info{Name} = Encode::encode_utf8(File::DesktopEntry->new($file)->get('Name') // '');

        } if $CONFIG{locale_support};

        state $entities = {
                           '&' => '&amp;',
                           '"' => '&quot;',
                           '_' => '__',
                           '<' => '&lt;',
                           '>' => '&gt;',
                          };

        # Encode XML entities (if any)
        $info{Name} =~ tr/"&_<>//
          && $info{Name} =~ s/([&"_<>])/$entities->{$1}/g;

        $cache_db{$file} = join("\0\1\0", %info);
    }

    foreach my $category (split(/;/, $info{__CATEGORIES__})) {
        push @{$categories{$category}}, \%info;
    }
}

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], ($with_icons ? $schema->{cat}[2] : ())) . join(
            q{},
            map    { $_->[1] }
              sort { $a->[0] cmp $b->[0] }
              map  { [lc($_), $_] }
              map {
                ($with_icons and $_->{Icon})
                  ? <<"ITEM_WITH_ICON"
    <item label="$_->{Name}" icon="$_->{Icon}"><action name="Execute"><command><![CDATA[$_->{Exec}]]></command></action></item>
ITEM_WITH_ICON
                  : <<"ITEM";
    <item label="$_->{Name}"><action name="Execute"><command><![CDATA[$_->{Exec}]]></command></action></item>
ITEM
              } @{$categories{$category}}
          )
          . qq[  </menu>\n];
    }
    elsif (exists $schema->{item}) {
        my ($command, $label, $icon) = @{$schema->{item}};
        if ($with_icons and (my $icon_path = check_icon($icon))) {
            $generated_menu .= <<"ITEM_WITH_ICON";
    <item label="$label" icon="$icon_path"><action name="Execute"><command><![CDATA[$command]]></command></action></item>
ITEM_WITH_ICON
        }
        else {
            $generated_menu .= <<"ITEM";
    <item label="$label"><action name="Execute"><command><![CDATA[$command]]></command></action></item>
ITEM
        }
    }
    elsif (exists $schema->{sep}) {
        $generated_menu .=
          defined($schema->{sep})
          ? qq[  <separator label="$schema->{sep}"/>\n]
          : qq[  <separator/>\n];
    }
    elsif (exists $schema->{beg}) {
        $generated_menu .= begin_category(@{$schema->{beg}});
    }
    elsif (exists $schema->{begin_cat}) {
        $generated_menu .= begin_category(@{$schema->{begin_cat}});
    }
    elsif (exists $schema->{end}) {
        $generated_menu .= qq[  </menu>\n];
    }
    elsif (exists $schema->{end_cat}) {
        $generated_menu .= qq[  </menu>\n];
    }
    elsif (exists $schema->{exit}) {
        my ($label, $icon) = @{$schema->{exit}};
        if ($with_icons and (my $icon_path = check_icon($icon))) {
            $generated_menu .= <<"EXIT_WITH_ICON";
    <item label="$label" icon="$icon_path"><action name="Exit"/></item>
EXIT_WITH_ICON
        }
        else {
            $generated_menu .= <<"EXIT";
    <item label="$label"><action name="Exit"/></item>
EXIT
        }
    }
    elsif (exists $schema->{raw}) {
        $generated_menu .= qq[    $schema->{raw}\n];
    }
    elsif (exists $schema->{file}) {
        sysopen(my $fh, $schema->{file}, 0) or die "Can't open file <<$schema->{file}>>: $!";
        sysread($fh, $generated_menu, -s $schema->{file}, length($generated_menu));
    }
    elsif (exists $schema->{pipe}) {
        my ($command, $label, $icon) = @{$schema->{pipe}};
        if ($with_icons and (my $icon_path = check_icon($icon))) {
            $generated_menu .= <<"PIPE_WITH_ICON";
    <menu id="${\rand()}" label="$label" execute="$command" icon="$icon_path"/>
PIPE_WITH_ICON
        }
        else {
            $generated_menu .= <<"PIPE";
    <menu id="${\rand()}" label="$label" execute="$command"/>
PIPE
        }
    }
    elsif (exists $schema->{obgenmenu}) {
        my ($name, $icon) = ref($schema->{obgenmenu}) eq 'ARRAY' ? @{$schema->{obgenmenu}} : $schema->{obgenmenu};
        if ($with_icons and (my $icon_path = check_icon($icon))) {
            $generated_menu .= <<"MENU_WITH_ICON";
  <menu id="${\rand()}" label="$name" icon="$icon_path">
MENU_WITH_ICON
        }
        else {
            $generated_menu .= <<"MENU";
  <menu id="${\rand()}" label="$name">
MENU
        }

        $generated_menu .= ($with_icons ? <<"ITEMS_WITH_ICONS" : <<"ITEMS");
    <item label="Menu Schema" icon="${\check_icon('text-x-generic')}"><action name="Execute"><command><![CDATA[$CONFIG{editor} $schema_file]]></command></action></item>
    <item label="Menu Config" icon="${\check_icon('text-x-generic')}"><action name="Execute"><command><![CDATA[$CONFIG{editor} $config_file]]></command></action></item>
  <separator/>
    <item label="Generate a static menu" icon="${\check_icon('accessories-text-editor')}"><action name="Execute"><command><![CDATA[obmenu-generator -s -c]]></command></action></item>
    <item label="Generate a static menu with icons" icon="${\check_icon('accessories-text-editor')}"><action name="Execute"><command><![CDATA[obmenu-generator -s -i -c]]></command></action></item>
  <separator/>
    <item label="Generate a dynamic menu" icon="${\check_icon('accessories-text-editor')}"><action name="Execute"><command><![CDATA[obmenu-generator -p]]></command></action></item>
    <item label="Generate a dynamic menu with icons" icon="${\check_icon('accessories-text-editor')}"><action name="Execute"><command><![CDATA[obmenu-generator -p -i]]></command></action></item>
  <separator/>
  <menu id="${\rand()}" label="Openbox" icon="${\check_icon('openbox')}">
    <item label="Openbox Autostart" icon="${\check_icon('text-x-generic')}"><action name="Execute"><command><![CDATA[$CONFIG{editor} $openbox_conf/autostart]]></command></action></item>
    <item label="Openbox RC" icon="${\check_icon('text-x-generic')}"><action name="Execute"><command><![CDATA[$CONFIG{editor} $openbox_conf/rc.xml]]></command></action></item>
    <item label="Openbox Menu" icon="${\check_icon('text-x-generic')}"><action name="Execute"><command><![CDATA[$CONFIG{editor} $openbox_conf/menu.xml]]></command></action></item>
    <item label="Reconfigure" icon="${\check_icon('openbox')}"><action name="Reconfigure"/></item>
  </menu>
  <separator/>
    <item label="Refresh Icon Set" icon="${\check_icon('view-refresh')}"><action name="Execute"><command><![CDATA[obmenu-generator -d]]></command></action></item>
  </menu>
ITEMS_WITH_ICONS
    <item label="Menu Schema"><action name="Execute"><command><![CDATA[$CONFIG{editor} $schema_file]]></command></action></item>
    <item label="Menu Config"><action name="Execute"><command><![CDATA[$CONFIG{editor} $config_file]]></command></action></item>
  <separator/>
    <item label="Generate a static menu"><action name="Execute"><command><![CDATA[obmenu-generator -s -c]]></command></action></item>
    <item label="Generate a static menu with icons"><action name="Execute"><command><![CDATA[obmenu-generator -s -i -c]]></command></action></item>
  <separator/>
    <item label="Generate a dynamic menu"><action name="Execute"><command><![CDATA[obmenu-generator -p]]></command></action></item>
    <item label="Generate a dynamic menu with icons"><action name="Execute"><command><![CDATA[obmenu-generator -p -i]]></command></action></item>
  <separator/>
  <menu id="${\rand()}" label="Openbox">
    <item label="Openbox Autostart"><action name="Execute"><command><![CDATA[$CONFIG{editor} $openbox_conf/autostart]]></command></action></item>
    <item label="Openbox RC"><action name="Execute"><command><![CDATA[$CONFIG{editor} $openbox_conf/rc.xml]]></command></action></item>
    <item label="Openbox Menu"><action name="Execute"><command><![CDATA[$CONFIG{editor} $openbox_conf/menu.xml]]></command></action></item>
    <item label="Reconfigure Openbox"><action name="Reconfigure" /></item>
  </menu>
  <separator/>
    <item label="Refresh Icon Set"><action name="Execute"><command><![CDATA[obmenu-generator -d]]></command></action></item>
  </menu>
ITEMS
    }
    else {
        warn "$0: invalid key '", (keys %{$schema})[0], "' in schema file!\n";
    }
}

print $output_h $generated_menu, $static
  ? qq[  </menu>\n</openbox_menu>\n]
  : qq[</openbox_pipe_menu>\n];

dump_configuration() if $update_config;

if ($static) {
    print STDERR <<'EOT';
:: A static menu has been successfully generated!
EOT
    if ($reconf_openbox) {
        exec 'openbox', '--reconfigure';
    }
}
