#!/usr/bin/python3
#Version: 3.27
#This script generates FreeDesktop application menu for any window manager with a template.
#Original copy Written by Konstantin Korikov <lostclus@ua.fm>, put in the public domain
#adapted to use for fluxbox by anticapitalista <antiX@operamail.com>
#adapted to use for jwm by Dave <david@daveserver.info>
#adapted to auto-detect or specify the session and use the resulting template by Dave <david@daveserver.info>
#adapted to use the gtk theme with --use-gtk by Dave <david@daveserver.info>
#adapted to utilize the xdg separator key by Dave <david@daveserver.info>
#adapted to have no icons by Dave <david@daveserver.info>
#adapted to write out to the applications file directly by Dave <david@daveserver.info>
#adapted to detect when the file has very little to no contents and adds a retry button by Dave <david@daveserver.info>
#updated to python3

#Requires pyxdg http://cvs.freedesktop.org/cgi-bin/viewcvs.cgi/pyxdg/

#USAGE EXAMPLE FOR antiX 
#desktop-menu > ~/.WM/applications
#Then link your main WM menu to this application menu.

#To Do:
#Preliminary testing then Rewrite / Clean the code

import sys
import locale
import getopt
import re
import os
import shlex
from subprocess import Popen, PIPE
import xdg.Menu
import xdg.DesktopEntry
import xdg.IconTheme
import xdg.Config
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
import pwd
import grp
        


version = "3.27"


def print_usage(exit_code = 1):
  print("""Usage: %s [options]
Options:
  Locale:
  --locale=locale               set output language and encoding
                                (This uses your session locale as the default)
  
  Filtering:
  --root-folder folder          folder to generate (for example: /Games)
  --menu-programs               Make a list of menu program links
  
  Specify Menu Type:
  --menu-file                   Specify the menu file to use
  --desktop-code                Specify which WM you are using 
                                (for example: icewm, fluxbox, jwm, etc...)
                                (Requires matching template!)
  --names                       Add the Name from the desktop files
                                (Enabled by default unless --generic-names or --comments is used)
  --generic-names               Add the Generic Names from the desktop files if present
  --comments                    Add the Comments from the desktop files if present
  --category-filter             Apply filter to the menu categories.
                                Default is n. 
                                Values:
                                    n = Names
                                    g = Generic Names
                                    c = Comments
  --order                       Specify the order to write the text
                                Note: Requires specifying --generic-names or --comments
                                or both
                                Values: ng[c] -> Name - Generic Name - [Comment]
                                        nc[g] -> Name - Comment - [Generic Name]
                                        cn[g] -> Comment - Name - [Generic Name]
                                        cg[n] -> Comment - Generic Name - [Name]
                                        gn[c] -> Generic Name - Name - [Comment]
                                        gc[n] -> Generic Name - Comment - [Name]
  --pattern                     Specify the style of separator between Name,
                                Generic Name and Comment. This can either be 
                                separating or encapsulating.
                                Examples: 
                                --pattern="|" -> Name | Generic Name | Comment
                                --pattern="." -> Name . Generic Name . Comment
                                --pattern="[]" -> Name [ Generic Name ] [ Comment ]
                                --pattern="{}" -> Name { Generic Name } { Comment }
                                
  Utilities:
  --terminal command            set terminal emulator command 
                                (default: desktop-defaults-run -t %%s)
  --write-out                   Write the output to the applications
                                file from the appropriate template
  --write-out-file              Specify the name of the file rather than 
                                using the template.
  
  Icon Setting:
  --no-gtk                      turn off using gtk to find the menu icons
                                (includes --with-theme-paths function)
  --theme theme                 set icon theme 
                                (Disables gtk theme)
  --with-theme-paths            convert icon base names to icon absolute paths
                                using specified icon theme
  --icon-size                   set default icon size
                                (This is 16 by default)
  --default-folder-icon icon    icon for folders that not provide Icon option
                                (This is blank by default)
  --default-entry-icon icon     icon for entries that not provide Icon option
                                (This is blank by default)
  --no-icons                    Turns off Icons
  --icons-only                  Only use icons, no text for the menu entries
  --theme-only                  Do not search all themes for icon files.
                                Use icons only from the gtk theme in use and those set in
                                /usr/share/pixmaps/ or /usr/share/icons/hicolor
  
  Help and Information:
  --help                        print this help and exit
  --version                     print version and exit
""" % sys.argv[0])
  sys.exit(exit_code)
  
def print_version():
  print("%s version %s" % (os.path.basename(sys.argv[0]), version))
  sys.exit(0)

def get_true_user():
    return os.getlogin() # maybe some more checking needed here

def get_users():
    with os.scandir("/home") as user_homes:
        for entry in user_homes:
            if not entry.name.startswith('.') and entry.is_dir():
                users_list.append(entry.name)
    return(users_list)
  
def write_to_file(STRING):
    text = open((USER_HOME+"/"+Var.Conf_Dir+"/"+Var.App_File), "a")
    text.write (str(STRING)+"\n")
    text.close()

def find_icon(entry):
    appicon = entry.getIcon()
    icon = ""
    if os.path.isfile(appicon):
        icon = appicon
    else:
        if icon_theme.lookup_icon(appicon, icon_size, 0) and not no_gtk:
            icon_info = icon_theme.lookup_icon(appicon, icon_size, 0)
            icon = icon_info.get_filename()
        else:
            for i, item in enumerate(icon_file_list):
                if re.search(appicon, item):
                    icon = item;
                    #Note: removing search for default icon size as preffered and immediately breaking after first matching instance can speed up script.
                    if re.search(str(icon_size), item):
                        break
    if not icon:
        icon = missing_icon

    icon = re.sub(r' ','\ ', icon)
    return icon
    
def get_order(order, name, gname, cname):
    if not gname or gname.lower() in name.lower():
        order=re.sub(r'g', '', order);
        gname="";
    if not name or name.lower() in gname.lower():
        order=re.sub(r'n', '', order);
        name="";
    if not cname or cname.lower() in name.lower() or cname.lower() in gname.lower():
        order=re.sub(r'c', '', order);
        cname="";
        
    order_dict={
        'ngc':  name    +" "+ separator_left + gname    + separator_right +" "+ separator_left + cname   + separator_right,
        'ncg':  name    +" "+ separator_left + cname    + separator_right +" "+ separator_left + gname   + separator_right,
        'cng':  cname   +" "+ separator_left + name     + separator_right +" "+ separator_left + gname   + separator_right,
        'cgn':  cname   +" "+ separator_left + gname    + separator_right +" "+ separator_left + name    + separator_right,
        'gnc':  gname   +" "+ separator_left + name     + separator_right +" "+ separator_left + cname   + separator_right,
        'gcn':  gname   +" "+ separator_left + cname    + separator_right +" "+ separator_left + name    + separator_right,
        'ng':   name    +" "+ separator_left + gname    + separator_right,
        'nc':   name    +" "+ separator_left + cname    + separator_right,
        'cn':   cname   +" "+ separator_left + name     + separator_right,
        'cg':   cname   +" "+ separator_left + gname    + separator_right,
        'gn':   gname   +" "+ separator_left + name     + separator_right,
        'gc':   gname   +" "+ separator_left + cname    + separator_right
    } 
    return order_dict.get(order, name + gname + cname)

def process_menu(menu):
  name="";  gname=""; cname="";
  
  for entry in menu.getEntries():
    #Check if menu item is a separator
    if isinstance(entry, xdg.Menu.Separator):
      if write_out:
        write_to_file(Var.Separator)
      else:
        print(Var.Separator)
        
    #Check if menu item is a another menu
    elif isinstance(entry, xdg.Menu.Menu):
      if program_names and not icons_only and "n" in category_filter:
          name = entry.getName() or entry.DesktopFileID
      
      if generic_names and not icons_only and "g" in category_filter:
          gname = entry.getGenericName()
        
      if comments and not icons_only and "c" in category_filter:
          cname = entry.getComment()
      
      if icons_only:
          ordered_text = ""
      else:
          ordered_text = get_order(order,name,gname,cname)
      
      if not no_icons:
          icon = find_icon(entry) or default_folder_icon

      if menu_programs:
        print ((Var.Menu_Program % (ordered_text, icon, sys.argv[0])) +
          (" --root-folder \"%s\"" % entry.getPath(org=True)) +
          (" --terminal \"%s\"" % terminal)).encode(encoding),
        if not no_icons:
          print ((" --default-folder-icon \"%s\"" % default_folder_icon) +
          (" --default-entry-icon \"%s\"" % default_entry_icon) +
          (" --theme \"%s\"" % xdg.Config.icon_theme) +
          (" --icon-size \"%d\"" % icon_size) +
          (with_theme_paths and " --with-theme-paths" or "")).encode(encoding),
        if locale_str:
          print (" --locale \"%s\"" % locale_str).encode(encoding),
        print()
      else:
        if no_icons:
          SUB_ORDER('menu',ordered_text,'','')
        else:
          SUB_ORDER('menu',ordered_text,icon,'')
        if write_out:
          if Var.Menu_ID == '1':
            write_to_file(Var.Menu_Start % (SUB_ORDER.sub0, SUB_ORDER.sub1, SUB_ORDER.sub2))
          else:
            write_to_file(Var.Menu_Start % (SUB_ORDER.sub0, SUB_ORDER.sub1))
        else:
          if Var.Menu_ID == '1':
            print (Var.Menu_Start % (SUB_ORDER.sub0, SUB_ORDER.sub1, SUB_ORDER.sub2))
          else:
            print (Var.Menu_Start % (SUB_ORDER.sub0, SUB_ORDER.sub1))
        process_menu(entry)
        if write_out:
          write_to_file(Var.Menu_End)
        else:
          print (Var.Menu_End)
    
    #Check if menu item is an application
    elif isinstance(entry, xdg.Menu.MenuEntry):
      de = entry.DesktopEntry
      desktopFile=str(entry)
      OnlyShowIn = de.getOnlyShowIn()
      if not os.path.isfile("/usr/share/applications/antix/"+desktopFile) and not OnlyShowIn:
        
        if program_names and not icons_only:
            name = de.getName() or entry.DesktopFileID
        
        if generic_names and not icons_only:
            gname = de.getGenericName()
        
        if comments and not icons_only:
            cname = de.getComment()
        
        if icons_only:
            ordered_text = " "
        else:
            ordered_text = get_order(order,name,gname,cname)

        execute = exec_clean2_re.sub('%', exec_clean1_re.sub('', de.getExec()))
        if de.getTerminal(): execute = terminal % execute      
        if no_icons:
          SUB_ORDER('prog',ordered_text,'',execute)
        else:
          icon = find_icon(de) or default_entry_icon
          SUB_ORDER('prog',ordered_text,icon,execute)
        if write_out:
          write_to_file(Var.Program % (SUB_ORDER.sub0, SUB_ORDER.sub1, SUB_ORDER.sub2))
        else:
          print (Var.Program % (SUB_ORDER.sub0, SUB_ORDER.sub1, SUB_ORDER.sub2))
          
def build_menu():
    Var().read()
    if write_out_file:
        Var.App_File = write_out_file
    
    if write_out:
        if not os.path.isdir(USER_HOME+"/"+Var.Conf_Dir+"/"):
            os.system("mkdir -p %s" % (USER_HOME+"/"+Var.Conf_Dir+"/"))
            os.system("touch %s" % (USER_HOME+"/"+Var.Conf_Dir+"/"+Var.App_File))
        else:
            #open((USER_HOME+"/"+Var.Conf_Dir+"/"+Var.App_File), 'w').close()  
            text = open((USER_HOME+"/"+Var.Conf_Dir+"/"+Var.App_File), "w")
            text.write("")
            text.close()
        

    menu = xdg.Menu.parse(menu_file)
    #menu = xdg.Menu.parse("/etc/xdg/menus/TCM-MENU.menu")
    if root_folder: menu = menu.getMenu(root_folder)
  
    if write_out:
        write_to_file(Var.File_Start)
    else:
        print(Var.File_Start)
  
    process_menu(menu)
  
    if write_out:
        write_to_file(Var.File_End)
    else:
        print (Var.File_End)

    if write_out:
        if not (os.path.getsize((USER_HOME+"/"+Var.Conf_Dir+"/"+Var.App_File)) > 50):
            SUB_ORDER('prog',"Retry",'',"desktop-menu --write-out")
            write_to_file(Var.Program % (SUB_ORDER.sub0, SUB_ORDER.sub1, SUB_ORDER.sub2))

        if os.geteuid() == 0:
            os.chown(USER_HOME+"/"+Var.Conf_Dir+"/"+Var.App_File, 0, grp.getgrnam(username).gr_gid) # make sure the file is writable for respective user even when script was called by root.
            os.chmod(USER_HOME+"/"+Var.Conf_Dir+"/"+Var.App_File, 0o664)

        if Var.Restart_Command and 'DISPLAY' in os.environ:
            os.system("%s" % (Var.Restart_Command))

class Var: 
  def read(self):
    var = Var
    for line in open("/usr/share/desktop-menu/templates/"+DESKTOP+".template", "r"):
      if "#" not in line:
        if re.search(r'^.*=', line):
          pieces = line.split(' = ')
          var.VARIABLE=(pieces[0])
          var.VARIABLE = re.sub(r'\n', '', var.VARIABLE)
          OBJECT=(pieces[1])
          OBJECT = re.sub(r'\n', '', OBJECT)
          setattr(var, var.VARIABLE, OBJECT)
          
class SUB_ORDER: 
  def __init__(self,menuORprog,NAME,ICON,EXECUTE):
    var = SUB_ORDER
    count=0
    if menuORprog == "menu":
      SUB = Var.Menu_Sub_Order.split(',')
    elif menuORprog == "prog":
      SUB = Var.Program_Sub_Order.split(',')
      
    for value in SUB:
      str_count=str(count)
      var.VARIABLE="sub"+str_count
      if re.search(r'icon', value):
        setattr(var, var.VARIABLE, ICON)
      elif re.search(r'execute', value):
        setattr(var, var.VARIABLE, EXECUTE)
      elif re.search(r'name', value):
        setattr(var, var.VARIABLE, NAME)
      count=count+1
      
      
root_folder = ""
username = get_true_user()
config_folder = "/home/" + username + "/.config/desktop-menu"
config_file = "settings"
users_list = []
menu_file = "/etc/xdg/menus/applications.menu"
terminal = "desktop-defaults-run -t %s"
default_folder_icon = "folder"
default_entry_icon = "-"
menu_programs = False
with_theme_paths = False
write_out = False
write_out_global = False
write_out_file = ""
no_gtk = False
no_icons = False
icons_only = False
icon_size = 48
icon_dirs = ['/usr/share/icons', '/usr/share/pixmaps']
icon_file_list = []
theme_only = False
missing_icon = "gtk-missing-image"
DESKTOP_CODE = ""
program_names = False
generic_names = False
comments = False
order = ""
theme = ""
separator_left = "["
separator_right = "]"
category_filter = "n"

exec_clean1_re = re.compile(r'%[a-zA-Z]')
exec_clean2_re = re.compile(r'%%')
encoding = None
locale_str = None

try: opts, args = getopt.getopt(sys.argv[1:], "", 
  ("help", "version", "locale=",
   "root-folder=", "terminal=", "default-folder-icon=", 
   "default-entry-icon=", "menu-programs", "theme=", "with-theme-paths",
   "icon-size=", "write-out", "write-out-global", "write-out-file=", 
   "desktop-code=", "no-gtk","no-icons", "menu-file=", "names", 
   "generic-names", "comments", "order=", "theme-only", "pattern=",
   "icons-only", "category-filter="))
except getopt.GetoptError: print_usage()

locale.setlocale(locale.LC_ALL, "")

for o, v in opts:
  if o == "--locale":
    locale_str = v
    locale.setlocale(locale.LC_ALL, locale_str)
  if o == "--root-folder": root_folder = v
  elif o == "--menu-file": menu_file = v
  elif o == "--terminal": terminal = v
  elif o == "--default-folder-icon": default_folder_icon = v
  elif o == "--default-entry-icon": default_entry_icon = v
  elif o == "--menu-programs" : menu_programs = True
  elif o == "--write-out" : write_out = True
  elif o == "--write-out-global" : write_out = True; write_out_global = True; 
  elif o == "--write-out-file" : write_out_file = v
  elif o == "--with-theme-paths" : with_theme_paths = True
  elif o == "--no-gtk" : no_gtk = True
  elif o == "--no-icons" : no_icons = True; no_gtk = True;
  elif o == "--icons-only" : icons_only = True;
  elif o == "--icon-size": icon_size = int(v)
  elif o == "--theme" :  xdg.Config.setIconTheme(v); theme = v;
  elif o == "--desktop-code" : DESKTOP_CODE = v
  elif o == "--names" : program_names = True;
  elif o == "--generic-names" : generic_names = True
  elif o == "--comments" : comments = True
  elif o == "--order" : order = v ;
  elif o == "--theme-only" : icon_dirs = ['/usr/share/icons/hicolor', '/usr/share/pixmaps']
  elif o == "--pattern" : separator_left = v[:1]; separator_right = v[1:2];
  elif o == "--category-filter" : category_filter = v
  elif o in ("-h", "-?", "--help"): print_usage(0)
  elif o in ("-v", "--version"): print_version()

encoding = locale.getlocale()[1] or 'UTF-8'
#Make sure name is turned on if no text option is specified
if not program_names and not generic_names and not comments: 
    program_names = True;

#Build Icon Location Array
for path in icon_dirs:
    for (dirpath, dirnames, filenames) in os.walk(path):
        for filename in filenames:
            if filename.endswith(( '.png', '.svg', '.xpm')):
                icon_file_list.append(dirpath+"/"+filename)

#Set GTK Icon Theme
if theme and not no_gtk:
    icon_theme = Gtk.IconTheme.new()
    icon_theme.set_custom_theme(theme)
else:
    icon_theme = Gtk.IconTheme.get_default()

if write_out_global:
    USER_HOME="/usr/share/desktop-menu/"
    for i in os.listdir('/usr/share/desktop-menu/templates'):
        if i.endswith(".template"):
            checkwmname = "which " + i.split('.',1)[0]
            myprocess = Popen( shlex.split(checkwmname), stdout=PIPE)
            mystdout = myprocess.communicate()[0]
            ### if the corresponding wm is not currently installed, skip this template
            ###  (to avoid logspam e.g. "Writing Menu: jwm... sh: 1: jwm: not found")
            if mystdout and mystdout[0] != '':
                i = re.sub(r'\..*', '', i)
                print ('Writing Menu: %s' % i)
                DESKTOP = i
                build_menu()
else:
    USER_HOME = os.environ['HOME']
    if 'DISPLAY' in os.environ and not write_out_global:
        DISPLAY = os.environ['DISPLAY']
        DISPLAY = re.sub(r':', '', DISPLAY)
        DISPLAY_SPLIT = DISPLAY.split('.')
        DISPLAY = DISPLAY_SPLIT[0]
        if not DESKTOP_CODE:
            with open(USER_HOME+"/.desktop-session/desktop-code."+DISPLAY, "r") as f:
                DESKTOP_CODE = f.readline()
                DESKTOP_CODE = re.sub(r'\n', '', DESKTOP_CODE)
            
    if not DESKTOP_CODE:
      print ('No Desktop Code. Please specify a desktop code with --desktop-code')
      exit
    else:  
      DESKTOP = re.sub(r'.*-', '', DESKTOP_CODE)
      build_menu()
