1#!@PYTHON@
2
3# Modification History
4
5# Changed on 18/03/19 by Jaimos Skriletz:
6# - Updated script for and require Python 3.
7# - Drop support for Python 2.
8# - Added support for xdg.Menu.Separator.
9# - Added option --term-cmd to state the terminal emulator command
10#   to use with Terminal=True .desktop entries. Default: xterm -e
11
12# Changed on 16/12/31 by Jaimos Skriletz:
13# - Added check for FVWM_USERDIR env variable.
14# - Added check for python-xdg module to print less errors if not found.
15# - Added option -e/--menu-error to output phython-xdg not found as
16#   a menu for the default-config.
17
18# Changed on 16/10/27 by Jaimos Skrietz:
19# - Renamed default menu to XDGMenu and changed the name of the
20#   FvwmForm to FvwmForm-XDGMenu-Config
21# - Modified the FvwmForm and added the abilty to load defaults from
22#   the Form's data file.
23# - Changed default to generate menu titles. Disable with --without-titles
24# - The top level menu now has two additional items:
25#     'Regenerate' - Regenerates menu.
26#     'Configure' - Opens up FvwmForm-XDGMenu-Config.
27# - Added --regen-cmd "CMD" for a fvwm CMD to use on the Regenerate item.
28#   Default: PipeRead `fvwm-menu-desktop`
29# - Added --include-items [config|regenerate|both|none] option
30#   to control if the additional items are included in the menu.
31# - Added --dynamic option to be used with dynamic menus.
32# - Added --all-menus option to generate all menus and not try to determine
33#   which one is best
34# - Changed default behavior to include menu titles.
35# - Added new option --without-titles
36
37# Changed on 25/02/14 by Thomas Funk:
38# - Converting of icons always to png
39
40# Changed on 06/10/13 by Thomas Funk:
41# Some Bugfixes:
42# - DecodeEncodeErrors in menu names
43# - no output appears with 'fvwm-menu-desktop --get-menus all|desktop'
44# - No entry "Regenerate XDG menu(s)" appears with
45#   'fvwm-menu-desktop --insert-in-menu MenuRoot'
46# - exchange all tabs with spaces to prevent indention errors
47# - add two new options: --app-icon --dir-icon
48#   to handle default icons for not available app/dir icons
49# - fix bug in convert icon routine that background of svg icons are
50#   transparent
51
52# Changed on 15/06/13 by Thomas Funk:
53# support for python-xdg > 0.19.
54# add gettext localization.
55
56# Changed on 10/01/12 by Thomas Funk:
57# Unicode support.
58
59# Changed on 01/26/12 by Dan Espen (dane):
60# Make compatible with fvwm-menu-desktop.
61# Restored DestroyMenu, needed for reload menus.
62# Remove bug, was printing iconpath on converted icons
63# Replace obsolete optparse, use getopt instead
64# Change from command line arg for applications.menu
65# change to using ?$XDG_MENU_PREFIX or theme? fixme
66# - use "Exec exec" for all commands, remove option.
67
68# fixme, fix documentation, FvwmForm-Desktop, usage prompt is wrong
69# change, mini icons are enabled by default.
70# there are rescalable icons.
71
72# Author: Piotr Zielinski (http://www.cl.cam.ac.uk/~pz215/)
73# Licence: GPL 2
74# Date: 03.12.2005
75
76# This script takes names of menu files conforming to the XDG Desktop
77# Menu Specification, and outputs their FVWM equivalents to the
78# standard output.
79#
80#   http://standards.freedesktop.org/menu-spec/latest/
81
82# This script requires the python-xdg module, which in Debian can be
83# installed by typing
84#
85#   apt-get install python3-xdg
86#
87# On Fedora, python-xdg is installed by default.
88
89import sys
90import getopt
91import os.path
92import os
93import fnmatch
94import time
95
96# Test for python-xdg
97try:
98    import xdg.Menu
99except ImportError:
100    xdg_import_error = True
101else:
102    xdg_import_error = False
103    import xdg.IconTheme
104    import xdg.Locale
105    from xdg.DesktopEntry import *
106    from xdg.BaseDirectory import *
107
108
109# Main Function
110def main ():
111
112    description = """
113Generate Fvwm Menu from xdg files.
114Standard output is a series Fvwm commands."""
115
116    obs_args=['check-app',
117              'enable-style',
118              'enable-tran-style',
119              'fvwm-icons',
120              'kde_config',
121              'mini-icon-path',
122              'merge-user-menu',
123              'su_gui',
124              'utf8',
125              'wm-icons']
126    dashed_obs_args=[]
127    for a in obs_args :
128        dashed_obs_args.append('--'+a)
129
130    obs_parms=['check-icons',
131               'check-mini-icon',
132               'destroy-type',
133               'dir',
134               'icon-app',
135               'icon-folder',
136               'icon-style',
137               'icon-title',
138               'icon-toptitle',
139               'icons-path',
140               'lang',
141               'menu-style',
142               'name',
143               'png-icons-path',
144               'submenu-name-prefix',
145               'time-limit',
146               'tran-icons-path',
147               'tran-mini-icons-path',
148               'type',
149               'uniconv-exec',
150               'uniconv',
151               'xterm']
152    equaled_obs_parms=[]
153    for a in obs_parms :
154        equaled_obs_parms.append(a+'=')
155    dashed_obs_parms=[]
156    for a in obs_parms :
157        dashed_obs_parms.append('--'+a)
158
159    try:
160        opts, args = getopt.getopt(sys.argv[1:], "hs:t:vwe",
161                                   ["help", "verbose", "enable-mini-icons", "with-titles", "without-titles", "version",
162                                    "desktop=", "size=", "theme=", "install-prefix=", "menu-type=", "regen-cmd=", "term-cmd=",
163                                    "title=", "get-menus=", "set-menus=", "insert-in-menu=", "mini-icon-dir=", "menu-error",
164                                    "app-icon=", "dir-icon=", "include-items=", "dynamic", "all-menus"]+obs_args+equaled_obs_parms)
165    except getopt.GetoptError as err:
166        # print help information and exit:
167        print(str(err)) # will print something like "option -a not recognized"
168        print(usage)
169        sys.exit(2)
170    global verbose, force, size, current_theme, icon_dir, top, install_prefix, menu_type, menu_list_length, term_cmd
171    global with_titles, menu_entry_count, get_menus, timestamp, set_menus, printmode, insert_in_menu, previous_theme
172    global default_app_icon, default_dir_icon, include_items, config_menus, regen_cmd, dynamic_menu, build_all_menus
173    version = "2.4"
174    verbose = False
175    force = False
176    desktop=''
177    size=24
178    current_theme='gnome'
179    previous_theme='gnome'
180    icon_dir="~/.fvwm/icons"
181    top='XDGMenu'
182    insert_in_menu = False
183    install_prefix = ''
184    menu_type = ''
185    with_titles = True
186    menu_entry_count = 0
187    menu_list_length = 0
188    get_menus = ''
189    printmode = True
190    set_menus = []
191    build_all_menus = False
192    config_menus = []
193    default_app_icon = "gnome-applications"
194    default_dir_icon = "gnome-fs-directory"
195    include_items = 'both'
196    regen_cmd = 'PipeRead `fvwm-menu-desktop`'
197    dynamic_menu = False
198    menu_error = False
199    term_cmd = "xterm -e"
200
201    # Loads config options from $FVWM_USERDIR/.FvwmForm-XDGMenu-Config
202    if "FVWM_USERDIR" in os.environ:
203        config_file = "%s/.FvwmForm-XDGMenu-Config" % os.environ['FVWM_USERDIR']
204    else:
205        config_file = "%s/.fvwm/.FvwmForm-XDGMenu-Config" % os.environ['HOME']
206    if os.path.isfile(config_file):
207        fvwmform_config = open(config_file, "r", errors="ignore")
208
209        for l in fvwmform_config:
210            o = l.split()
211            if len(o)>2 and o[0] != '#':
212                if o[1][:3] == 'MEN' and o[2] == 'on':
213                    config_menus.append(o[1][3:])
214                if o[1] == 'IconsOn' and o[2] == 'on':
215                    force = True
216                elif o[1] == 'Size':
217                    size = int(o[2])
218                elif o[1] == 'TitlesOn' and o[2] == 'on':
219                    with_titles = True
220                elif o[1] == 'Theme':
221                    current_theme = o[2]
222                elif o[1] == 'Title':
223                    top = o[2]
224                elif o[1] == 'InsertInto':
225                    top = o[2]
226                    insert_in_menu = True
227                elif o[1] == 'Installprefix':
228                    install_prefix = o[2]
229                elif o[1] == 'IconDir':
230                    icon_dir = o[2]
231                elif o[1] == 'DirIcon':
232                    default_dir_icon = o[2]
233                elif o[1] == 'AppIcon':
234                    default_app_icon = o[2]
235                elif o[1] == 'IncludeConfig' and o[2] == 'on':
236                    include_items = "config"
237                elif o[1] == 'IncludeRegen' and o[2] == 'on':
238                    include_items = "regenerate"
239                elif o[1] == 'IncludeBoth' and o[2] == 'on':
240                    include_items = "both"
241                elif o[1] == 'IncludeNone' and o[2] == 'on':
242                    include_items = "none"
243                elif o[1] == 'TermCmd':
244                    term_cmd = " ".join(o[2:])
245        fvwmform_config.close()
246
247    for o, a in opts:
248        if o in ("-v", "--verbose"):
249            verbose = True
250            if os.path.isfile(config_file):
251                vprint("Defaults loaded from %s\n" % config_file)
252            else:
253                vprint("Config file not found: %s\nUsing built-in defaults.\n" % config_file)
254        elif o in ("-h", "--help") :
255            print(usage)
256            sys.exit()
257        elif o in ("--version") :
258            print("fvwm-menu-desktop version " + version)
259            sys.exit()
260        elif o in ("-e", "--menu-error") :
261            menu_error = True
262        elif o in ("--enable-mini-icons") :
263            force=True
264        elif o in ("--insert-in-menu") :
265            top=a
266            insert_in_menu = True
267        elif o in ("--desktop") :
268            desktop=a
269        elif o in ("-t", "--title") :
270            top=a
271        elif o in ("--get-menus") :
272            if a == 'all' or a == 'desktop' :
273                get_menus=a
274                printmode = False
275            else :
276                sys.stderr.write( "--get-menus argument must be 'all' or 'desktop' found "+a )
277                print(usage)
278                sys.exit(1)
279        elif o in ("-s","--size") :
280            size = int(a)
281        elif o in ("--mini-icon-dir") :
282            icon_dir = a
283        elif o in ("--set-menus") :
284            if a[-1] == ' ':
285                a = a[:-1]
286            set_menus=a.split(' ')
287        elif o in ("--install-prefix") :
288            if a and not os.path.isabs(a):
289                assert False, "install-prefix must be an absolute path"
290            # add trailing slash if not there already
291            if not a[-1] == '/' : # trailing slash
292                a=a + '/'
293            install_prefix = a
294        elif o in ("--theme") :
295            current_theme = a
296        elif o in ("--menu-type") :
297            menu_type = a
298        elif o in ("-w", "--with-titles") :
299            with_titles = True
300        elif o in ("--without-titles") :
301            with_titles = False
302        elif o in ("--app-icon") :
303            default_app_icon = a
304        elif o in ("--dir-icon") :
305            default_dir_icon = a
306        elif o in ("--include-items") :
307            if a in ("both", "none", "config", "regenerate") :
308                include_items = a
309            else:
310                sys.stderr.write( "--include-items argument must be 'config', 'regenerate', 'both' or 'none' found "+a )
311                print(usage)
312                sys.exit(1)
313        elif o in ("--regen-cmd") :
314            regen_cmd = a
315        elif o in ("--term-cmd") :
316            term_cmd = a
317        elif o in ("--dynamic") :
318            dynamic_menu = True
319        elif o in ("--all-menus") :
320            build_all_menus = True
321        elif o in (str(dashed_obs_args+dashed_obs_parms)) :
322            # Ignore
323            sys.stderr.write( "Warning: Arg "+o+" is obsolete and ignored\n" )
324        else:
325            assert False, "unhandled option"
326
327    # Exit if python-xdg not found
328    if xdg_import_error:
329        if menu_error:
330            printtext('DestroyMenu "%s"' % top)
331            printtext('AddToMenu "%s" "%s" Title' % (top, top))
332            printtext('+ "Error: python-xdg not found" Nop')
333            printtext('+ "" Nop')
334            printtext('+ "Regenerate" PipeRead `fvwm-menu-desktop -e`')
335        else:
336            sys.stderr.write('Python module python-xdg not found.')
337        sys.exit(1)
338
339
340    timestamp = time.time()
341
342    if len(set_menus) == 0:
343        xdg_menu_prefix = ((os.environ['XDG_MENU_PREFIX'] if 'XDG_MENU_PREFIX' in os.environ else ''))
344
345        # First check if no user presettings made
346        if desktop == '':
347            # check if $XDG_MENU_PREFIX is set
348            if not xdg_menu_prefix == '':
349                desktop = xdg_menu_prefix.replace('-', '').lower()
350
351        vprint("Parameters for creating menu list:")
352        vprint(" XDG_MENU_PREFIX:  \'%s\'" %xdg_menu_prefix)
353        vprint(" --install-prefix: \'%s\'" %install_prefix)
354        vprint(" --desktop:        \'%s\'" %desktop)
355        vprint(" --menu-type:      \'%s\'" %menu_type)
356
357        vprint("\nStart search ...")
358        menulist, desktop_temp = getmenulist(desktop, menu_type)
359        if not desktop_temp == '':
360            desktop = desktop_temp
361
362    else:
363        menulist = set_menus
364
365    vprint(" Menu list: %s\n" %menulist)
366    menu_list_length =  len(menulist)
367
368    if menu_list_length == 0:
369        if not desktop == '':
370            desktop = desktop + '-'
371        if menu_error:
372            printtext('DestroyMenu "%s"' % top)
373            printtext('AddToMenu "%s" "%s" Title' % (top, top))
374            printtext('+ "Error: No menus found" Nop')
375            printtext('+ "" Nop')
376            printtext('+ "Regenerate" PipeRead `fvwm-menu-desktop -e`')
377        else:
378            sys.stderr.write(install_prefix+desktop+menu_type+".menu not available on this system. Exiting...\n")
379        sys.exit(1)
380    else:
381        # set previous_theme if <icon_dir>/.theme exist
382        if os.path.exists(os.path.join(os.path.expanduser(icon_dir), ".theme")):
383            previous_theme = next(open(os.path.join(os.path.expanduser(icon_dir), ".theme"), 'r')).replace('\n', '')
384        vprint(" Previous used theme: %s" %previous_theme)
385        vprint(" Current used theme: %s\n" %current_theme)
386
387        sys.stderr.flush()
388        parsemenus(menulist, desktop)
389
390    # write current_theme to <icon_dir>/.theme if --enable-mini-icons and printmode is set
391    if printmode and force:
392        fh = open(os.path.join(os.path.expanduser(icon_dir), ".theme"), "w")
393        fh.write(current_theme)
394        fh.close()
395
396    sys.stdout.flush()
397    vprint("\nProcess took " + str(time.time()-timestamp) + " seconds")
398
399def getmenulist(desktop, menu_type):
400    menudict = {}
401    config_dirs = []
402    if not install_prefix == '':
403        config_dirs = [install_prefix]
404    else:
405        config_dirs = xdg_config_dirs # xdg_config_dirs is a built-in list from python-xdg
406
407    found_menus = 0
408    for dir in config_dirs:
409        if install_prefix == '':
410            dir = os.path.join(dir, 'menus')
411        # skipping all paths which not available
412        if os.path.exists(dir):
413            filelist = set([])
414            dir_list = os.listdir(dir)
415            #pattern = '*'+desktop+'*'+menu_type+'*.menu'
416            # Always find all menus
417            pattern = '*.menu'
418            for filename in fnmatch.filter(dir_list, pattern):
419                filelist.add(filename)
420
421            # the menudict dictionary has a unsorted list (set) for the values.
422            # set is easier to use then a list for removing items
423            menudict[dir] = filelist
424            found_menus += len(filelist)
425            vprint(" found in %s: %s" %(dir, list(filelist)))
426
427    desktop_dict = {}
428    if not found_menus == 0:
429        all_menus = []
430        # remove all menus in /usr/local/etc/xdg/menus if exist in user dir
431        for path in list(menudict.keys()):
432            if not path == '/usr/local/etc/xdg/menus':
433                if path == os.path.join(os.getenv("HOME"), '.config/menus'):
434                    menudict['/usr/local/etc/xdg/menus'] = menudict['/usr/local/etc/xdg/menus'] - menudict[path]
435                #else:
436                #    menudict[path] = menudict[path] - menudict['/usr/local/etc/xdg/menus']
437                for menu in list(menudict[path]):
438                    all_menus.append(path + '/' + menu)
439
440        if not menudict['/usr/local/etc/xdg/menus'] == 0:
441            for menu in list(menudict['/usr/local/etc/xdg/menus']):
442                all_menus.append('/usr/local/etc/xdg/menus/' + menu)
443
444        if get_menus == 'all' or (build_all_menus and desktop == '' and menu_type == ''):
445            return all_menus, ''
446
447        # get menus selected in config file
448        if len(config_menus) > 0:
449            config_menulist = []
450            for i in config_menus:
451                for j in all_menus:
452                    if fnmatch.fnmatch( j, "*%s.menu" % i ):
453                        config_menulist.append(j)
454            vprint("\n Selected menus from config file: %s " % list(config_menulist))
455            # Use config file if --dekstop not set
456            if len(config_menulist) == 0:
457                vprint(" No menus in config found. Using all menus.")
458            elif desktop == '' and menu_type == '':
459                vprint(" Using menus from config file.")
460                return config_menulist, ''
461            else:
462                vprint(" Ignoring menus in config file, due to --desktop or --menu-type.")
463
464        # filter --desktop and --menu-type options
465        if desktop != '' or menu_type != '':
466            vprint("\n Filtering menus according to --desktop %s and --menu-type %s" % (desktop, menu_type) )
467            pattern = '*'+desktop+'*'+menu_type+'*'
468            for path in list(menudict.keys()):
469                for menu in list(menudict[path]):
470                    if not fnmatch.fnmatch( menu, pattern ):
471                        menudict[path].remove(menu)
472                if menudict[path] == set([]):
473                    del menudict[path]
474            if menudict == {}:
475                sys.stderr.write("No menus found matching --desktop %s and --menu-type %s. Exiting...\n" % (desktop, menu_type) )
476                sys.exit(1)
477
478        vprint("\n Finding best menu in Menu List: %s" % menudict )
479        if build_all_menus:
480            all_menus = []
481            for key in menudict:
482                for i in menudict[key]:
483                  all_menus.append(key+'/'+i)
484            return all_menus, ''
485
486        # sort menus related to desktops and create a weighting
487        vprint("\n DE weighting search: DE => [user menus, system menus, overall]")
488        weight_dict = {}
489        if desktop == '':
490            # first the desktops, then debian (shouldn't appear in others) then others holding
491            # all other non DE menus e.g. tools and at the end the nones without prefixes
492            # If there're other prefixes from other WMs - should be added BEFORE debian
493            DEs = ['gnome', 'kde', 'xfce', 'lxde', 'cinnamon', 'mate', 'debian', 'others', 'none']
494        else:
495            DEs = [desktop]
496        for de in DEs:
497            menus = set([])
498            user_menus = 0
499            system_menus = 0
500            filled = False
501            for path in list(menudict.keys()):
502                if de == 'none':
503                    pattern = '*'
504                elif de == 'others':
505                    pattern = '*-*'
506                else:
507                    pattern = '*'+de+'*'
508                # fnmatch.filter returns a list of files the pattern match
509                menu_names = fnmatch.filter(menudict[path], pattern)
510                if not len(menu_names) == 0:
511                    filled = True
512                for name in menu_names:
513                    menus.add(path+'/'+name)
514                # delete each found DE menu from the actual path. So, the menus will be reduced loop by loop.
515                menudict[path] = menudict[path]-set(menu_names)
516                # count the menus found in the users and systems menu path for later weighting
517                if not path == '/usr/local/etc/xdg/menus':
518                    user_menus = len(menu_names)
519                else:
520                    system_menus = len(menu_names)
521            if filled:
522                desktop_dict[de] = menus
523                filled = False
524            # fill the weight dictionary with the counts
525            weight_dict[de] = [user_menus, system_menus, user_menus+system_menus]
526            vprint(" %s => %s" %(de, weight_dict[de]))
527
528        # get the highest rated desktop
529        highest = 0
530        de_highest = ''
531        for de in sorted(weight_dict.keys()):
532            de_user = weight_dict[de][0]
533            de_system = weight_dict[de][1]
534            de_total = weight_dict[de][2]
535            higher = False
536            if not de_highest == '':
537                # don't weight 'none' and 'others cause both not DEs
538                if not de == 'none' and not de == 'others':
539                    highest_user = weight_dict[de_highest][0]
540                    highest_system = weight_dict[de_highest][1]
541                    highest_total = weight_dict[de_highest][2]
542                    # first compare the total counts
543                    if highest < de_total:
544                        higher = True
545                    elif highest == de_total:
546                        # if the totals equal compare the users
547                        if highest_user < de_user:
548                            higher = True
549                        elif highest_user == de_user:
550                            # it the users equal compare the system menus
551                            if highest_system < de_system:
552                                higher = True
553                            # if the systems equal the last wins
554                            elif highest_system == de_system:
555                                higher = True # fixme, should be biunique. -but how? With atime?
556            else:
557                higher = True
558
559            if higher:
560                highest = de_total
561                de_highest = de
562
563        if highest == 0 : # no dev environments?
564            de_highest = 'others' # use 'others'
565        vprint( "\n Winner: %s" %de_highest)
566
567        # Perhaps there're a global menus available which are not in the highest rated list
568        if 'none' in desktop_dict:
569            for menu in desktop_dict['none']:
570                name = menu.replace('.menu', '').split('/')
571                # the fnmatch.filter will be used to find NO match because then
572                # the menu is not in the list
573                found = fnmatch.filter(desktop_dict[de_highest], '*'+name[-1]+'*')
574                if found == []:
575                    desktop_dict[de_highest].add(menu)
576
577        # Add 'others' menus to list, because these could be tool menus like yast, etc
578        if 'others' in desktop_dict:
579            for menu in desktop_dict['others']:
580                desktop_dict[de_highest].add(menu)
581
582    if len(desktop_dict) == 0:
583        return [], ''
584    else:
585        return list(desktop_dict[de_highest]), de_highest
586
587def vprint(text):
588    if verbose:
589        sys.stderr.write(text+"\n")
590
591# Encoding error handling of menu entries.
592def printtext(text):
593    try: print(text)
594    except UnicodeEncodeError:
595        if verbose: print("# UnicodeEncodeError - Attempting to encode")
596        try:
597            sys.stdout.flush()
598            sys.stdout.buffer.write(text.encode())
599            print()
600        except: print(text.encode("ascii", errors="replace").decode("ascii"))
601    except:
602        if verbose: print("# Unknown error - Skipping entry")
603
604def geticonfile(icon):
605    iconpath = xdg.IconTheme.getIconPath(icon, size, current_theme, ["png", "xpm", "svg"])
606
607    if not iconpath == None:
608        extension = os.path.splitext(iconpath)[1]
609
610    if not iconpath:
611        return None
612
613    if not force:
614        return iconpath
615
616    if iconpath.find("%ix%i" % (size, size)) >= 0: # ugly hack!!!
617        return iconpath
618
619    if not os.path.isdir(os.path.expanduser(icon_dir)):
620        os.makedirs(os.path.expanduser(icon_dir))
621
622    iconfile = os.path.join(os.path.expanduser(icon_dir),
623                            "%ix%i-" % (size, size) +
624                            os.path.basename(iconpath))
625
626    if os.path.exists(iconpath):
627        iconfile = iconfile.replace(extension, '.png')
628        if extension == '.svg':
629            os.system("if test \\( \\( ! -f '%s' \\) -o \\( '%s' -nt '%s' \\) \\) -o \\( '%s' != '%s' \\); then convert -background none '%s' -resize %i '%s' ; fi"%
630                          (iconfile, iconpath, iconfile, current_theme, previous_theme, iconpath, size, iconfile))
631        else:
632            os.system("if test \\( \\( ! -f '%s' \\) -o \\( '%s' -nt '%s' \\) \\) -o \\( '%s' != '%s' \\); then convert '%s' -resize %i '%s' ; fi"%
633                          (iconfile, iconpath, iconfile, current_theme, previous_theme, iconpath, size, iconfile))
634    else:
635        sys.stderr.write("%s not found! Using default icon.\n" % iconpath)
636        return None
637    return iconfile
638
639
640def getdefaulticonfile(command):
641    if command.startswith("Popup"):
642        return geticonfile(default_dir_icon)
643    else:
644        return geticonfile(default_app_icon)
645
646def printmenu(name, icon, command):
647    iconfile = ''
648    if force :
649        iconfile = geticonfile(icon) or getdefaulticonfile(command) or icon
650        if not (iconfile == '' or iconfile == None):
651            iconfile = '%'+iconfile+'%'
652        else:
653            sys.stderr.write("%s icon or default icon not found!\n")
654    printtext('+ "%s%s" %s' % (name, iconfile, command))
655
656def parsemenus(menulist, desktop):
657    global menu_entry_count
658    if menu_list_length == 1:
659        new_menulist = menulist
660        # user defines only one special menu
661        parsemenu(xdg.Menu.parse(menulist[0]), top)
662    else:
663        # create a top title list
664        top_titles = []
665        for file in menulist:
666            # extract and split the filename and set first char of each word to capital
667            name_parts = file.replace('.menu', '').split('/')[-1].split('-')
668            name_parts = [name[0].replace(name[0], name[0].upper())+name[1:] for name in name_parts]
669            top_titles.append(' '.join(name_parts))
670
671        # create the submenus
672        new_toptitles = []
673        new_menulist = []
674        for title, menu in zip(top_titles, menulist):
675            name = 'Fvwm'+title
676            vprint("Create submenu \'%s\' from \'%s\'" %(name, menu))
677            parsemenu(xdg.Menu.parse(menu), name, title)
678            # remove a menu if no menu entry was created in its sub menus
679            if not menu_entry_count == 0:
680                new_toptitles.append(title)
681                new_menulist.append(menu)
682                menu_entry_count = 0
683            else:
684                vprint(" Menu is empty - won't be used!")
685
686        # create the root menu
687        if printmode:
688            if not insert_in_menu:
689                if dynamic_menu:
690                    printtext('DestroyMenu recreate "%s"' % top)
691                else:
692                    printtext('DestroyMenu "%s"' % top)
693            if with_titles and not insert_in_menu:
694                printtext('AddToMenu "%s" "%s" Title' % (top, top))
695            else:
696                printtext('AddToMenu "%s"' % top)
697
698            for title in sorted(new_toptitles):
699                name = 'Fvwm'+title
700                printmenu(title, '', 'Popup "%s"' % name)
701
702            if include_items != 'none':
703                printtext('+ "" Nop')
704                if include_items in ("both", "regenerate"):
705                    printmenu("$[gt.Regenerate]", "system-software-update", regen_cmd )
706                if include_items in ("both", "config"):
707                    printmenu("$[gt.Configure]", "system-software-update", "Module FvwmPerl -l fvwm-menu-desktop-config.fpl" )
708
709    if not get_menus == '':
710        printtext('%s' % ' '.join(new_menulist))
711
712def parsemenu(menu, name="", title=""):
713    global menu_entry_count
714    m = re.compile('%[A-Z]?', re.I) # Pattern for %A-Z (meant for %U)
715    if not name :
716        name = menu.getPath()
717    if not title:
718        title = name
719    if printmode:
720        if not insert_in_menu or not (insert_in_menu and name == top and menu_list_length == 1):
721            if name == top and dynamic_menu:
722                printtext('DestroyMenu recreate "%s"' % name)
723            else:
724                printtext('DestroyMenu "%s"' % name)
725        if with_titles:
726            # for insert-in-menu AddToMenu doesn't have a title for top menu
727            # because this will appear then in the other menu
728            if insert_in_menu and name == top and menu_list_length == 1:
729                printtext('AddToMenu "%s"' % name)
730            else:
731                printtext('AddToMenu "%s" "%s" Title' % (name, title))
732        else:
733            printtext('AddToMenu "%s"' % name)
734    for entry in menu.getEntries():
735        if isinstance(entry, xdg.Menu.Menu):
736            if printmode:
737                printmenu(entry.getName(), entry.getIcon(), 'Popup "%s"' % entry.getPath())
738        elif isinstance(entry, xdg.Menu.MenuEntry):
739            if printmode:
740                desktop = DesktopEntry(entry.DesktopEntry.getFileName())
741                # eliminate '%U' etc behind execute string
742                execProgram = m.sub('', desktop.getExec())
743                if desktop.getTerminal():
744                    execProgram = "%s %s" % (term_cmd, execProgram)
745                printmenu(desktop.getName(), desktop.getIcon(), "Exec exec " + execProgram)
746            menu_entry_count += 1
747        elif isinstance(entry, xdg.Menu.Separator):
748            if printmode:
749                printtext( '+ "" Nop' )
750        else:
751            if printmode:
752                printtext('# not supported: ' + str(entry))
753
754    if printmode:
755        # should only appear in a single menu. For more it will insert in parsemenus() when the top menu will built
756        if menu_list_length == 1 and name == top and include_items != 'none':
757            printtext('+ "" Nop')
758            if include_items in ("both", "regenerate"):
759                printmenu("$[gt.Regenerate]", "system-software-update", regen_cmd )
760            if include_items in ("both", "config"):
761                printmenu("$[gt.Configure]", "system-software-update", "Module FvwmPerl -l fvwm-menu-desktop-config.fpl" )
762        printtext('')
763
764    for entry in menu.getEntries():
765        if isinstance(entry, xdg.Menu.Menu):
766            parsemenu(entry)
767
768usage="""
769A script which parses xdg menu definitions to build
770the corresponding fvwm menus.
771
772Usage: $0 [OPTIONS]
773Options:
774    -h, --help                show this help and exit.
775    --version                 show version and exit.
776    --install-prefix DIR      install prefix of the desktop menu files.
777                              Per default not set. For system wide menus
778                              use /usr/local/etc/xdg/menus/.
779    --desktop NAME            use menus that include NAME in the file name:
780                              gnome, kde, xfce, lxde, debian, etc.
781    --menu-type NAME          use menus that include NAME in the file name:
782                              applications, settings, preferences, etc. When
783                              used with --desktop only menus whose file name
784                              mathces '*desktop*menutype*' are used.
785    --theme NAME              icon theme: gnome (default), oxygen, etc. Don't
786                              use hicolor. It's the default fallback theme if
787                              no icon is found.
788    -w, --with-titles         generate menus with titles. Default.
789    --without-titles          generate menus without titles.
790    --enable-mini-icons       enable mini-icons in menu.
791    -s, --size NUM            set size of mini-icons in menu. Default is 24.
792    --mini-icon-dir DIR       set directory for mini-icons.
793                              Default is ~/.fvwm/icons.
794    --app-icon NAME           set default application icon if no others found.
795                              Default is 'gnome-applications'.
796    --dir-icon NAME           set default directory icon if no others found.
797                              Default is 'gnome-fs-directory'.
798    -t, --title NAME          menu title of the top menu used by Popup command.
799                              Default is XDGMenu.
800    --insert-in-menu NAME     generates a menu to place it in the root level
801                              of the menu NAME.
802    --get-menus all|desktop   prints a space separated list of full menu paths.
803                              'all' is all menus on the system except empty
804                              ones. 'desktop' list the menus that would have
805                              been generated. No menu generation is done.
806    --set-menus menu_paths    expects a space separated list of full menu paths
807                              to generate user specified menus.
808    --all-menus               generate all menus found.
809    --include-items NAME      include additional menu items NAME in top level
810                              menu. NAME can be 'config', 'regenerate', 'both'
811                              or 'none'. Default both.
812    --regen-cmd ACTION        The fvwm ACTION for the 'Regenerate' menu item.
813                              Default: 'PipeRead `fvwm-menu-desktop`'
814    --term-cmd CMD            Terminal emulator CMD used on terminal entries.
815                              Default: xterm -e
816    --dynamic                 used with dynamic menus.
817    -e, --menu-error          out python-xdg not found error in menu.
818    -v, --verbose             run and display debug info on STDERR."""
819
820if __name__ == "__main__":
821    main()
822
823# Local Variables:
824# mode: python
825# compile-command: "python3 fvwm-menu-desktop.in --version"
826# End:
827