#!/usr/bin/env python
#
# kdemenu2ice:		Convert KDE menus to Icewm menu file
#
# Author:		Christopher Arndt <chris.arndt@gmx.net>
# Version:		1.0
# Date:			14.10.2000
# Copyright:		GPL
#
# Acknowledgements:	This script is based (although nearly completely
#			rewritten) on 'icewm.py' from Moritz Moeller-Herrmann
#			<mmh@gmx.net>, available at:
#			http://webrum.uni-mannheim.de/jura/moritz/icewm.py

_progname = "kdemenu2ice"

_version = "1.0"

__doc__ = """NAME
	%(_progname)s - convert KDE menus to a Icewm menu file

SYNOPSIS
	%(_progname)s [OPTIONS]

DESCRIPTION
	reads the directories with the KDE menu files and builds
	a Icewm menu file from them.

FEATURES
	- can handle menu names in foreign languages
	- handles icons for programs and submenus and can check for their
	  existence and link them to your ~/.icewm/icons dir
	- supports kfm 'c' place holder and strips off other kfm quirks
	- starts console apps in a terminal emulator
	- runs commandlines with shell special chars through a shell,
	  so you can use shell variables and command substitution.
	- can sort the menus alphabetically and/or with submenus first
	- can integrate personal menu files into global menu (tricky)
	- can put all KDE menus in a submenu
	- the personal menu can appear above or beneath the global one
	- can insert a menu file with static menus ('~/.icewm/menu.static')
	- saves a backup of your old menu file
	- does some sanity checks on the kdelnk files

OPTIONS
	--dirs-first=<yes|no>
		sort submenus before regular menu entries (default: yes).

	-h --help
		show usage information.

	--link-icons=<yes|no>
		search the needed icons in the KDE icon directories and link
		them to your personal Icewm icon directory with proper names
		(default: yes).

	--kdemenu-name=<name>
		set the name of the submenu for the KDE menus. Only in effect
		when --use-submenu=yes. Default depends on currnet locale.

	--merge-usermenu=<yes|no>
		merge the personal menu entries, so that they override the
		corrsponding global menu entries (default: yes).

	-o <file> --outfile=<file>
		output goes to <file> instead of '~/.icewm/menu'. If the file
		already exists, save a backup copy with ~ added to the name.
		To send output to standard output set --outfile=-.

	-q --quiet
		only report errors during execution.

	--sort-menu=<yes|no>
		sort the menu entries alphabetically according to current 
		locale (default: yes).

	--use-submenu=<yes|no>
		put all KDE menus in their own submenu (default: no).

	--usermenu-first=<yes|no>
		sort the submenu with the personal menu entries before the
		global entries. Only in effect when --merge-usermenu=no is
		also given (default: no).

	--usermenu-name=<name>
		set the name of the submenu for the personal menus. Only in
		effect when --merge-usermenu=no. Default depends on current
		locale.

	-v --verbose
		turn on debugging output.

TODO
	- add more translations for menu names
	- eliminate dependency on cmdline.py (which is buggy)
	- provide commandline argument for the starting directory (?)
	- provide filter for generated menu (?)

BUGS/CAVEATS
	Mandrake rpm packages of Icewm are built with a default icon search
	paththat includes the global icon directory in /usr/share/icons.
	Sometimes you don't get the icon you want, because another one takes
	precedence.

	The global Icewm icon directories are not checked for the existence of
	a specified icon. If you don't want icons from KDE to overwrite the 
	ones from Icewm, copy the latter to your personal icon directory (or
	change check_icon().

	When you alter system menu entries in KDE, the subdirectories under
	~/.kde/share/applnk sometimes get created without the .directory files
	This will lead to menu entries with no/default icon and English titles.

	The program currently only keeps one backup copy of the previously
	generated menu file. So, if you altered this menu file by hand, these
	changes will be lost if you afterwards run kdemenu2ice twice.
	Nevertheless, if a menu file is found, the first time the program 
	is run, it is renamed to 'menu.old' and never touched again.

AUTHOR
	Christopher Arndt <chris.arndt@gmx.net>

"""

import string, re, os, sys, locale
from glob import glob
from fnmatch import fnmatch

try: import cmdline
except ImportError:
    sys.stderr.write(
    """You don't seem to have the module 'cmdline.py' installed,
which I need for command line parsing.
You can download it at:
    
     http://members.home.com/gindikin/dev/python/cmdline/cmdline.py

If you already have this module installed , please, check your PYTHON_PATH!
""")
    sys.exit(1)

# Constants
KDE_DIR = os.environ.get('KDEDIR', '/usr')
KDEMENU_DIR = KDE_DIR + '/share/applnk'
USERMENU_DIR = os.environ['HOME'] + '/.kde/share/applnk'

ICEWM_DIR = os.environ['HOME'] + '/.icewm/'
ICEWM_MENUFILE = ICEWM_DIR + 'menu'
ICEWM_PROGFILE = ICEWM_DIR + 'programs'
ICEWM_ICONDIR = ICEWM_DIR + 'icons'

KDE_ICONDIR  = KDE_DIR + '/share/icons'
USER_ICONDIR = os.environ['HOME'] + '/.kde/share/icons'

kdemenu_names = {'en': "KDE Programs", 'de': "KDE Programme"}
usermenu_names = {'en': "Personal", 'de': "Persönlich"}

LANGUAGE = string.split(os.environ['LANG'], ".")[0][:2]

INDENT = " " * 4

# Defaults for preferences

DEBUG = 0
QUIET = 0
LINK_ICONS = 1

# name of icon for folders (submenus) that have no .directory file 
# or where (Mini)Icon is not set
DEF_DIR_ICON = 'folder'
# name of icon for *.kdelnk files where (Mini)Icon is not set
DEF_APP_ICON = 'app'
# terminal application for commandline or terminal apps
# (must accept -e option)
TERMINAL = 'konsole'
TERMINAL_OPTIONS = r'-nowelcome -sl 10000 -caption "%c"'
# should menus be sorted alphabetically?
SORT_MENUS = 1
# should all KDE menus be placed in an own submenu?
USE_SUBMENU = 0
# if yes, how should submenu be named?
# default is set in kdemenu_names['en']
KDEMENU_NAME = ""
# Icon for this submenu
KDEMENU_ICON = 'go.xpm'
# should submenus appear above program entries?
DIRS_FIRST = 1
# should personal and global entries be merged?
MERGE_USERMENU = 1
# if not, show personal entries above global ones?
USERMENU_FIRST = 0
# how should the submenu for personal entries be called?
# default is set in usermenu_names['en']
USERMENU_NAME = ""
# Icon for personal menu
USERMENU_ICON = 'folder'
# where the output goes
OUTFILE = ""

### functions

# print docstring
def usage(dir = vars()):
    print __doc__ % dir

# option handling

def parse_options():
    
    global DEBUG, QUIET, LINK_ICONS, SORT_MENUS
    global DIR_FIRST, USERMENU_FIRST, MERGE_USERMENU, USE_SUBMENU
    global KDEMENU_NAME, USERMENU_NAME, OUTFILE
    
    if cmdline.receivedOption('-h --help'):
	usage()
	sys.exit()

    if cmdline.receivedOption('-v --verbose'):
	DEBUG = 1
    elif cmdline.receivedOption('-q --quiet'):
	QUIET = 1
	
    if cmdline.getStringValueOf('--link-icons', 'yes') == 'no':
	LINK_ICONS = 0

    if cmdline.getStringValueOf('--sort-menus', 'yes') == 'no':
	SORT_MENUS = 0

    if cmdline.getStringValueOf('--dirs-first', 'yes') == 'no':
	DIRS_FIRST = 0

    if cmdline.getStringValueOf('--usermenu-first', 'no') == 'yes':
	USERMENU_FIRST = 1

    if cmdline.getStringValueOf('--merge-usermenu', 'yes') == 'no':
	MERGE_USERMENU = 0

    if cmdline.getStringValueOf('--use-submenu', 'no') == 'yes':
	USE_SUBMENU = 1

    if cmdline.receivedOption('-o --outfile'):
	OUTFILE = cmdline.getStringValueOf('-o --outfile')
        if DEBUG: sys.stderr.write('Output goes to: ' + OUTFILE + '\n')

    KDEMENU_NAME = cmdline.getStringValueOf('--kdemenu-name', "")

    USERMENU_NAME = cmdline.getStringValueOf('--usermenu-name', "")


def parse_lnkfile(lnkfile, language = None):
    """Extract key, value pairs  from a kdelnk or .directory file
    
    Parameters: lnkfile - path of kdelnk or .directory file or file object
    Returns:	Dictionary with with keys corresponding to the entries in 
                the file. Values not found are not set.
    """
   
    lnk = {}
    
    # regular expression for key recognition
    key_re = re.compile(r'(^[^=]+)\s*=\s*(.*)$')
    # for comments
    comment_re = re.compile(r'^#.*$')
    # for locales
    lang_re = re.compile(r'([^[]+)(\[(.*)])?')
    
    if type(lnkfile) == type(""):
	try:
	    f = open(lnkfile, "r")
	except IOError:
	    return lnk
    lnkcontents = f.readlines(50000)
    try:
        f.close()
    except:
	pass

    for i in range(len(lnkcontents)):
	# strip leading/trailing space
        lnkcontents[i] = string.strip(lnkcontents[i])
	# ignore comment lines
	if comment_re.match(lnkcontents[i]): continue
	key_match = key_re.match(lnkcontents[i])
	if key_match:
	    key, value = key_match.groups()
	    lang_match = lang_re.match(key)
	    key, subkey = lang_match.group(1), lang_match.group(3)
	    key = string.lower(key)
	    if subkey: subkey = string.lower(subkey)
	    if subkey:
		if lnk.has_key(key):
		    if not type(lnk[key]) == type({}):
			default = lnk[key]
			lnk[key] = {}
			lnk[key]['default'] = value
		else:
		    lnk[key] = {}
		lnk[key][subkey] = value
	    elif type(lnk.get(key)) == type({}):
		lnk[key]['default'] = value
	    else:
		lnk[key] = value

    # set name for current language or try several defaults
    if type(lnk.get('name')) == type({}):
	for lang in [language, 'default', 'en', 'c']:
	    if lnk['name'].has_key(lang):
		lnk['name'] = lnk['name'][lang]
		break

    return lnk

def check_icon(lnk, default):
    """Checks for existence of an Icon and returns fixed entry.
    
    'lnk' is a dictionary as returned by parse_lnkfile().
    
    If the icon can not be found in the personal icewm icon directory,
    optionally link it there from one of several KDE icon directories.
    If it can not be found even there, substitute the given default icon name
    Finally, the icon name is stripped of it's extension.
    """
    
    icon = lnk.get('miniicon', lnk.get('icon', default))
    if not icon[0] == '/':
	icontarget = ICEWM_ICONDIR + '/' + os.path.splitext(icon)[0] + \
	  '_16x16.xpm'
	if not os.path.exists(icontarget) and LINK_ICONS:
	   dirlist = [USER_ICONDIR + '/mini', KDE_ICONDIR + '/mini',
	     USER_ICONDIR, USER_ICONDIR + '/large', KDE_ICONDIR, 
	     KDE_ICONDIR + '/large', KDE_DIR + '/share/apps/kpanel/pics/mini']
           for p in dirlist:
	      if os.path.exists(p + '/' + icon):
		  if DEBUG: sys.stderr.write("Linking " + p + '/' + icon + \
		    " to " + icontarget + ".\n")
		  os.symlink(p + '/' + icon, icontarget)
		  break
	      elif os.path.exists(p + '/' + 'mini-' + icon):
		  if DEBUG: sys.stderr.write("Linking " + p + '/mini-' + \
		   icon + " to " + icontarget + ".\n")
		  os.symlink(p + '/mini-' + icon, icontarget)
		  break
           else:
	       icon = default
	icon = os.path.splitext(icon)[0]

    return icon

def getdirinfo(dirpath):
    """Return info for a directoty in a tuple.
    
    The format of the tuple is (name, icon, None).
    
    - Uses the directory name as fallback name
    - If icon is not set, use a default icon name (see check_icon()).
    """

    dir = {}
    
    if os.path.exists(dirpath + "/.directory"):
        dir = parse_lnkfile(dirpath + "/.directory", language = LANGUAGE)
	dirname = dir.get('name')
    if not dirname:
        dirname = os.path.basename(dirpath)
    
    return (dirname, check_icon(dir, DEF_DIR_ICON), None)

def getlnkinfo(lnkfile):
    """Read a kdelnk (Type=Application) file and return the info in a tuple.
    
    The format of the tuple is (name, icon, exec).
    - If the exec string contains shell special chars an invocation of 'sh -c'
      is inserted.
    - If the Terminal parameter is true, an invocation of a terminal emulator
      (see global variable TERMINAL) is added to the exec string.
    - If the exec string contains the placeholder %c, the name of the
      application is substituted. Other placeholders are stripped off.
    - the icon name is checked for existence (see check_icon())
    
    Raises an IOError if the kkdelnk file is invalid.
    """

    lnk = parse_lnkfile(lnkfile, language = LANGUAGE)
    
    if not lnk.get('type') == 'Application' or \
      not lnk.has_key('exec') or not lnk.has_key('name'):
	raise IOError, "Unvalid kdelnk file"
    
#     # no icon, try program name (KDE does this)
#     if not lnk.has_key('MiniIcon') and not lnk.has_key('Icon'):
#         lnk['Icon'] = string.split(Exec)[0]
# I, personally, don't like this behaviour. I'd rather have a default icon

    # exec lines with shell special chars ($~`|) are run through a shell
    for c in list('$|~`'):
	if c in lnk['exec']:
	    lnk['exec'] = 'sh -c "' + string.replace(lnk['exec'], '"',
	      r'\"') + '"'
	    break

    # run console apps in terminal emulator
    if int(lnk.get('terminal', 0)):
	lnk['exec'] = TERMINAL + " " + lnk.get('terminaloptions', \
	  TERMINAL_OPTIONS) + " -e " + lnk['exec']

    # fill in application name for %c place holder in kfm command line
    lnk['exec'] = string.replace(lnk['exec'], '%c', lnk['name'])
    # strip other kfm placeholders
    lnk['exec'] = re.sub('%\\w+', '', lnk['exec'])

    return lnk['name'], check_icon(lnk, DEF_APP_ICON), lnk['exec']
    
def entriessortfunc(x, y):
    """Sort menu entries alphabetically (according to locale) and place submenu
    entries first. Both can be turned off via preferences.
    """

    if x[0] == 0 and y[0] == 1 and DIRS_FIRST:
	return -1
    elif x[0] == 1 and y[0] == 0 and DIRS_FIRST:
	return 1
    elif x[0] ==  y[0] and SORT_MENUS:
        return locale.strcoll(x[2],y[2])
    else:
	return 0

def listdir(directory):
    """Return list of files in a directory, omitting dotfiles (.*)."""
    return glob(directory + "/*")

def do_dir(startdirs):
    """Find all directories and *.kdelnk files in startdirs
    get the necessary infos and print a line for the menu file
    
    If startdirs is a list or tuple of directories, all these directories 
    are scanned and the entries are merged together. 
    
    This is what is done when an entry is seen twice: If the entry is a
    subdirectory, it's path is appended to the info for later recursal. 
    If it's a *.kdelnk file, the entry is unaltered.
    
    This means, the info in the entries in the first directory listed takes
    precedence over the entries in the following ones. So you should put your
    personal menu directory first in the list.
    """
    
    global ILEVEL
    ILEVEL = ILEVEL + 1
    
    entries = {}
    
    if type(startdirs) == type(""):
	startdirs = [startdirs]
    elif type(startdirs) == type(()):
	startdirs = list(startdirs) 
    
    # collate directory entries in a directory of lists containing the info
    for dir in startdirs:
	if DEBUG: sys.stderr.write("Entering " + dir + "\n")
	for entry in listdir(dir):
	    entryname = os.path.basename(entry)
	    if os.path.isdir(entry):
	        if DEBUG:  sys.stderr.write("Submenu: " + entry + "\n")
		if entries.has_key(entryname):
		    if DEBUG: sys.stderr.write("Submenu '" +
		      entryname + "' already defined: appending path\n")
		    entries[entryname][1] = [entries[entryname][1], entry]
		else:
		    entries[entryname] = [0, entry] + list(getdirinfo(entry))
	    elif os.path.isfile(entry) and fnmatch(entry, "*.kdelnk"):
	        if DEBUG:  sys.stderr.write("Entry: " + entry + "\n")
		if entries.has_key(entryname):
		    if DEBUG: sys.stderr.write("Entry '" +
		      entryname + "' already defined: omitting it\n")
		else:
		    try:
			info = getlnkinfo(entry)
			entries[entryname] = [1, entry] + list(info)
		    except IOError:
			sys.stderror.write("Unvalid kdelnk file '" + entry + 
			  "'. Omitting entry")

    # get list of entries and sort them (see entriessortfunc)
    entries_sorted = entries.values()
    entries_sorted.sort(entriessortfunc)

    # print menu file lines
    for entry in entries_sorted:
	# it's a directory entry: print 'menu' line and recurse into directory
	if entry[0] == 0:
	    print INDENT * (ILEVEL - 1) + "menu " + '"' + entry[2] + '" "' + \
	      entry[3] + '" {'
	    do_dir(entry[1])
	    print INDENT * (ILEVEL - 1) + "}\n"
	# it's a *.kdelnk file: print 'prog' line
	else:
	    print INDENT * (ILEVEL - 1) + "prog " + '"' + entry[2] + '" "' + \
	      entry[3] + '" ' + entry[4]

    ILEVEL = ILEVEL - 1

class printtofile:
    "Redefine print output by assigning stderr to filename"
    
    def __init__(self, filename):
        self.filehandle = open(filename, "w" )
    def write (self, string):
        self.filehandle.write(string)
    def close(self):
        self.__del__()
    def __del__(self):
        self.filehandle.close


### Main ###

if __name__ == '__main__':
    
    locale.setlocale(locale.LC_ALL, "")
    
    parse_options()

    if not os.path.exists(USERMENU_DIR):
	sys.stderr.write(USERMENU_DIR + ", the directory for personal" + \
	  " settings\nin KDE menus does not exist.\nRun startkde at least" + \
	  " once!\nExiting...\n")
	sys.exit()

    if not os.path.exists(ICEWM_DIR):
	os.mkdir (ICEWM_DIR)
	if not QUIET: sys.stderr.write("Icewm Configuration dir created.\n")

    if not os.path.exists(ICEWM_ICONDIR):
	os.mkdir(ICEWM_ICONDIR)
	if not QUIET: sys.stderr.write("Icewm Icon dir created.\n")

    if not OUTFILE:
	OUTFILE = ICEWM_MENUFILE
    # backup old menu file, overwrite old backup
    if OUTFILE == ICEWM_MENUFILE:
	if not os.path.exists(ICEWM_MENUFILE + '.old'):
	    try: os.rename(ICEWM_MENUFILE, ICEWM_MENUFILE + '.old')
	    except: pass

    if os.path.exists(OUTFILE) and not OUTFILE == '-':
	try: os.rename(OUTFILE, OUTFILE + '~')
	except: pass

    # assign stdout to output file
    if not OUTFILE == '-':
       sys.stdout = printtofile(OUTFILE)

    ILEVEL = 0
    
    if USE_SUBMENU:
	# get name of the global menu
	# if user didn't specify one, first try .directory file, then
	# dictionary of language specific defaults
	
	lnk = parselnkfile(KDEMENU_DIR + '/.directory')
	if not KDEMENU_NAME:
	    if DEBUG: sys.stderr.write("Checking for global menu name\n")
	    KDEMENU_NAME = lnk.get('name', kdemenu_names.get(LANGUAGE,
	      kdemenu_names['en']))

	KDEMENU_ICON = check_icon(lnk, KDEMENU_ICON)
	
	print "menu " + '"' + KDEMENU_NAME + '" "' + KDEMENU_ICON + '" {'
	ILEVEL = ILEVEL + 1

    if MERGE_USERMENU:
	do_dir((USERMENU_DIR, KDEMENU_DIR))
    else:
	lnk = parselnkfile(USERMENU_DIR + '/.directory')
	if not USERMENU_NAME:
	    if DEBUG: sys.stderr.write("Checking for personal menu name\n")
	    USERMENU_NAME = lnk.get('Name', usermenu_names.get(LANGUAGE,
	      usermenu_names['en']))

	USERMENU_ICON = check_icon(lnk, USERMENU_ICON)

	if not USERMENU_FIRST:
	    do_dir(KDEMENU_DIR)
	    print INDENT * ILEVEL +  "separator"
	print INDENT * ILEVEL + "menu " + '"' + USERMENU_NAME + \
	  '" "' + USERMENU_ICON + '" {'
	ILEVEL = ILEVEL + 1
	do_dir(USERMENU_DIR)
	ILEVEL = ILEVEL - 1
	print INDENT * ILEVEL + "}"
	if USERMENU_FIRST:
	    print INDENT * ILEVEL + "separator"
	    do_dir(KDEMENU_DIR)
    if USE_SUBMENU:
	ILEVEL = ILEVEL - 1
	print "}"
    print "separator"

    # Insert entries from a static menu file
    try:
	static_menufile = open(os.environ['HOME'] + "/.icewm/menu.static")
	print static_menufile.read()
	static_menufile.close()
    except:
	pass

    sys.stdout.close()