#! /usr/bin/env python
# $Id: menumaker.py,v 1.5 2004/01/29 19:03:28 david Exp $
# by David Goodger <goodger@python.org>

"""\
Converts a menu-definition table into emacs-lisp code for use by easy-menu.

Input must be of this form::

    # MENU NAME     DESCRIPTION                 SYMBOL
    Dir             My Custom Directory Menu    my-dir-menu
    #
    # MENU ITEM/-   KEY/-   SYMBOL              PATH or LISP CODE
    D:/             f5 w d  find-file-in-d      d:/
    -
    ~/.emacs        f5 . .  find-file-.emacs    w:/.emacs
    # submenu titles stand alone on a line 
    ~/signatures
    # submenu items are indented one tab per level
        /           -       find-in-signatures  ~/signatures/
    # BLANK LINE ENDS MENU DEFINITION, ALLOWS NEW DEFINITION TO BEGIN

Columns are separated by one or more tabs (*not* spaces). Hyphens ("-") may be
used to indicate menu separators and "no key equivalent". If a symbol is used
for more than one menu item, the lisp code should only be defined the first
time.

The output looks like this::

    (defun find-file-in-d () (interactive)
      (find-file-in "d:/"))
    (defun find-file-.emacs () (interactive)
      (find-file "w:/.emacs"))
    (defun find-in-signatures () (interactive)
      (find-in "~/signatures/"))
    
    (define-prefix-command 'f5-map)
    (global-set-key [f5]   'f5-map)
    
    (define-prefix-command 'f5-.-map)
    (define-key f5-map "." 'f5-.-map)
    (define-key f5-.-map "." 'find-file-.emacs)
    
    (define-prefix-command 'f5-w-map)
    (define-key f5-map "w" 'f5-w-map)
    (define-key f5-w-map "d" 'find-file-in-d)
    
    (easy-menu-define
     my-dir-menu (current-global-map) "My Custom Directory Menu"
     '("Dir"
       ["D:/"  find-file-in-d t]
       "-"
       ["~/.emacs"  find-file-.emacs t]
       ("~/signatures"
        ["/"  find-in-home-signatures t]
        )
       ))
    
    (easy-menu-add my-dir-menu)

Copy this elisp function definition (required by the generated code) to your
.emacs file::

    (defun find-in (dir)
      "Do find-file in minibuffer, starting with the directory given."
      (let (new-buffer)
        (let ((default-directory dir))
          (save-excursion
            (save-window-excursion
              (setq new-buffer (call-interactively 'find-file)))))
        (switch-to-buffer new-buffer)))

Finally, your .emacs file needs to load the elisp produced.  For example::

    (load-file "~/.emacslib/initmenus.el")
"""

import sys
import time
import re
import fileinput
import optparse


(hNAME, hDESC, hSYMBOL) = range(3)
(iNAME, iKEY, iSYMBOL, iLISP) = range(4)
(mNAME, mSYMBOL) = range(2)


class MenuMaker:

    header = """\
;; -*- coding: utf-8 -*-
;; Generated by menumaker.py (%s)
;; %s"""
    colsep = re.compile(r'\t+')
    functiondef = '(defun %s () (interactive)\n  %s)'
    keymapdef = '(define-prefix-command \'%s-map)'
    globalsetkeydef = '(global-set-key [%s]   \'%s)'
    definekeydef = '(define-key %s-map "%s" \'%s)'
    menustartdef = """\
(easy-menu-define
 %s (current-global-map)
 "%s"
 `(,(encode-coding-string "%s" 'mac-roman)"""
    menuitemdef = '   ["%s"  %s t]'
    menusepdef = '   "-"'
    menuenddef = '   ))\n(easy-menu-add %s)'
    sublevel = 0

    def __init__(self, args=None, test=False):
        self.input = fileinput.input(args)
        self.menus = []
        self.keymaps = {}
        self.functions = {}
        self.test = test

    def mainloop(self):
        try:
            while 1:
                self.getmenu()
        except IndexError:
            if self.test:
                self.writedata_test()
            else:
                self.writedata()

    def getmenu(self):
        name, description, symbol = self.getheader()
        menu = []
        self.menus.append((name, description, symbol, menu))
        self.getitems(menu)

    def getheader(self):
        while 1:
            line = self.getline()
            if line:
                break
        parts = [s.strip() for s in self.colsep.split(line)]
        if len(parts) != 3:
            raise InputError(
                'Exactly 3 columns required in memu header. (file %s line %s)'
                % (self.input.filename(), self.input.filelineno()))
        return parts

    def getitems(self, menu):
        while 1:
            line = self.getline()
            if not line:
                break
            parts = [s.strip() for s in self.colsep.split(line)]
            if parts[0]:
                self.sublevel = 0
            else:
                match = self.colsep.match(line)
                if not match:
                    raise InputError(
                        'Only tabs allowed for indentation. (file %s line %s)'
                        % (self.input.filename(), self.input.filelineno()))
                sublevel = match.end()
                if sublevel > self.sublevel:
                    raise InputError(
                        'Submenu level change without submenu title.'
                        '(file %s line %s)'
                        % (self.input.filename(), self.input.filelineno()))
                del parts[0]
                self.sublevel = sublevel
            if len(parts) == 1:
                if parts[0] != '-':
                    self.sublevel += 1
                menu.append((self.sublevel, parts[0]))
                continue
            if not (3 <= len(parts) <= 4):
                raise InputError(
                    '3 or 4 columns required per memu item. (file %s line %s)'
                    % (self.input.filename(), self.input.filelineno()))
            menu.append((self.sublevel, (parts[iNAME], parts[iSYMBOL])))
            self.setkeys(parts[iKEY], parts[iSYMBOL])
            if (len(parts) == 4) and parts[iLISP]:
                lisp = parts[iLISP]
                if lisp.startswith('(') and lisp.endswith(')'):
                    self.functions[parts[iSYMBOL]] = lisp
                elif lisp.endswith('/'):
                    self.functions[parts[iSYMBOL]] = '(find-in "%s")' % lisp
                else:
                    self.functions[parts[iSYMBOL]] = '(find-file "%s")' % lisp
                
    def setkeys(self, keystring, symbol):
        if keystring == '-':
            return
        keys = keystring.split()
        keymap = self.keymaps
        for i in range(len(keys) - 1):
            keymap = keymap.setdefault(keys[i], {})
        keymap[keys[-1]] = symbol

    def getline(self):
        while 1:
            line = self.input[self.input.lineno()].rstrip()
            if (not line) or (line[0] != '#'):
                break
        return line

    def writedata(self):
        self.writeheader()
        self.writefunctions()
        self.writekeymaps()
        self.writemenus()

    def writeheader(self):
        print self.header % (sys.modules[self.__class__.__module__].__file__,
                             time.strftime('%Y-%m-%dT%H:%M:%S',
                                           time.localtime(time.time())))

    def writefunctions(self):
        print
        for (symbol, lisp) in self.functions.items():
            print self.functiondef % (symbol, lisp)

    def writekeymaps(self):
        for key, value in self.keymaps.items():
            if type(value) == type(''):
                print self.globalsetkeydef % (key, key)
            elif type(value) == type({}):
                print
                print self.keymapdef % (key)
                print self.globalsetkeydef % (key, key + '-map')
                self.writenestedkeymaps(key, value)
            else:
                raise TypeError('Bad type for keymap value')

    def writenestedkeymaps(self, mapkey, map):
        for key, value in map.items():
            if type(value) == type(''): # symbol
                print self.definekeydef % (mapkey, key, value)
            elif type(value) == type({}):
                print
                print self.keymapdef % (mapkey + '-' + key)
                print self.definekeydef % (mapkey, key,
                                           mapkey + '-' + key + '-map')
                self.writenestedkeymaps(mapkey + '-' + key, value)
            else:
                raise TypeError('Bad type for nested keymap value')

    def writemenus(self):
        menus = self.menus
        menus.reverse()
        for name, description, symbol, menu in menus:
            print
            print self.menustartdef % (symbol, description, name)
            self.sublevel = 0
            for level, item in menu:
                if item == '-':
                    self.writeitem(level, self.menusepdef)
                elif isinstance(item, tuple):
                    self.writeitem(
                        level, self.menuitemdef % (item[mNAME], item[mSYMBOL]))
                else:
                    self.writeitem(level, '  ("%s"' % item, new=True)
            if self.sublevel:
                print '   ', ')' * self.sublevel
            print self.menuenddef % (symbol)

    def writeitem(self, level, text, new=False):
        if level < self.sublevel or new and level == self.sublevel:
            print ' ',  ' ' * self.sublevel, ')' * (self.sublevel - level
                                                    + new)
        print level * ' ' + text
        self.sublevel = level

    def writedata_test(self):
        from pprint import pprint
        print 'functions:'
        pprint(self.functions)
        print '\nkeymaps:'
        pprint(self.keymaps)
        print '\nmenus:'
        pprint(self.menus)


class InputError(Exception):
    """InputError"""
    pass


usage = '%prog [options] < input > output'
description = ('Convert a menu-definition table into emacs-lisp code for '
               'use by easy-menu.')

def main(argv=None):
    parser = optparse.OptionParser(usage=usage, description=description)
    parser.add_option('-d', '--describe', action='store_true',
                      help='describe the input data format')
    parser.add_option('-t', '--test', action='store_true',
                      help='produce test output (internal data structures)')
    (options, args) = parser.parse_args()
    if options.describe:
        print >>sys.stderr, __doc__,
        sys.exit(0)
    maker = MenuMaker(args, test=options.test)
    maker.mainloop()


if __name__ == '__main__':
    main()

