Well, it bothers me to build menus for every shell application that needs one.

So, I've written a module to handle that, like in the good old turbo pascal times. :)

Here it is, the module comments are pretty explainatory, I think, and the sample usage if run as script should enlighten the concept.

Use it, give comments, improvements, have fun...

Cheers and Happy coding

Edited 6 Years Ago by Beat_Slayer: n/a

Attachments scrennshot.jpg 52.82 KB
import string
from textwrap import wrap

try:
    from cStringIO import StringIO
except:
    from StringIO import StringIO

class Console_App_Menu():
    
    """Console Application Menu module.
        It implements basic menu and dialogs on a console window."""

    def __init__(self, title='Console Application Menu', size=(80, 25), border='#', log_title='Log Output:'):

        """The class initialization takes as optional arguments:
            - title: title text of the menu
            - size: the size in characteres of the console window menu
            - border: character to be printed as border for the menu and dialogs
            - log_title: title text of the log window"""
        
        self.__width = size[0]
        self.__height = size[1] - 1
        self.__line = lambda(x): x * self.__width
        self.__ruler = lambda((x, l)): (x / 2) - (l / 2)
        self.__content = StringIO()
        self.__content.write((' ' * self.__width) * self.__height)
        self.__border = border[0]
        self.__log_title = log_title
        self.__log = ['', '', '', '']
        self.__draw_border()
        self.__writeline(3, title)
        self.__log_box()

    def __draw_border(self):
        self.__content.seek(0)
        self.__content.write(self.__border * self.__width)
        for i in range (1, self.__height):
            self.__content.seek(self.__line(i))
            self.__content.write(self.__border)
            self.__content.seek(self.__line(i + 1) - 1)
            self.__content.write(self.__border)
        self.__content.seek(self.__line(i))
        self.__content.write(self.__border * self.__width)

    def __writeline(self, line_n, text):
        self.__content.seek(self.__line(line_n) + self.__ruler((self.__width, len(text))))
        self.__content.write(text)

    def __log_box(self):
        self.__content.seek(self.__line(self.__height - 8) + 10)
        self.__content.write(self.__border + self.__border + self.__log_title)
        self.__content.write(self.__border * (self.__width - 22 - len(self.__log_title)))
        for i in reversed(range(4, 8)):
            self.__content.seek(self.__line(self.__height - i) + 10)
            self.__content.write(self.__border)
            self.__content.seek(self.__line(self.__height - (i - 1)) - 11)
            self.__content.write(self.__border)
        self.__content.seek(self.__line(self.__height - 3) + 10)
        self.__content.write(self.__border * (self.__width - 20))

    def __update_display(self):
        print self.__content.getvalue(),
        print self.__question

    def __log_write(self, output):
        for i in range(len(self.__log)):
            if self.__log[i] == '':
                self.__log[i] = output
                break
        else:
            self.__log.pop(0)
            self.__log.append(output)
        ylines = 7
        self.__content.seek(self.__line(self.__height - ylines) + 12)
        for i in range(len(self.__log)):
            self.__content.write(self.__log[i] + (' ' * (self.__width - 24 - len(self.__log[i]))))
            ylines -= 1
            self.__content.seek(self.__line(self.__height - ylines) + 12)
        
    def __dialog(self, dlg_type, dlg_text):
        scr_buffer = self.__content.getvalue()
        dlg_types = 'error', 'info', 'query_str', 'query_int', 'query_bool'
        dlg_titles = 'Error:', 'Information:', 'Question:', 'Question:', 'Question:'
        line = (self.__height / 2) - 3
        self.__content.seek(self.__line(line) + (self.__width / 4) - 1)
        self.__content.write(' ' * ((self.__width / 2) + 2))
        line += 1
        self.__content.seek(self.__line(line) + (self.__width / 4) - 1)
        self.__content.write(' ' + self.__border + self.__border + dlg_titles[dlg_types.index(dlg_type)])
        self.__content.write(self.__border * ((self.__width / 2) - len(dlg_titles[dlg_types.index(dlg_type)])) + ' ')
        for i in range(1, 4):
            self.__content.seek(self.__line(line + i) + (self.__width / 4) - 1)
            self.__content.write(' ' + self.__border + (' ' * (self.__width / 2) + self.__border + ' '))
        self.__content.seek(self.__line(line + 4) + (self.__width / 4) - 1)
        self.__content.write(' ' + (self.__border * ((self.__width / 2) + 2)) + ' ')
        self.__content.seek(self.__line(line + 2) + (self.__width / 4) + 1)
        self.__content.write((' ' * ((self.__width / 4) - (len(dlg_text) / 2) - 1)) + dlg_text  + (' ' * ((self.__width / 4) - (len(dlg_text) / 2))))
        print self.__content.getvalue(),
        if dlg_type in ('error', 'info'):
            raw_input('Press Enter to continue... ')
            self.__content.seek(0)
            self.__content.write(scr_buffer)
            self.__update_display()
        elif dlg_type == 'query_str':
            value = raw_input('Enter string: ')
            self.__content.seek(0)
            self.__content.write(scr_buffer)
            return value
        elif dlg_type == 'query_int':
            while True:
                value = raw_input('Enter number: ')
                try:
                    value = int(value)
                    self.__content.seek(0)
                    self.__content.write(scr_buffer)
                    return value
                except:
                    self.__content.seek(0)
                    self.__content.write(scr_buffer)
                    return self.__dialog(dlg_type, dlg_text)
        elif dlg_type == 'query_bool':
            while True:
                value = raw_input('Enter \'Y(y)...\' or \'N(n)...\': ')
                if value:
                    if value[0].lower() == 'y':
                        self.__content.seek(0)
                        self.__content.write(scr_buffer)
                        return True
                    elif value[0].lower() == 'n':
                        self.__content.seek(0)
                        self.__content.write(scr_buffer)
                        return None
                self.__content.seek(0)
                self.__content.write(scr_buffer)
                return self.__dialog(dlg_type, dlg_text)

    def set_options(self, index_type, options_list, separator=') ', question='Insert option: '):

        """Sets menu options. It takes as arguments:
            - index type: 'numbers', 'low' and  'caps'
            - options: list of menu options
            and as optional arguments:
            - separator: characteres between the index and option
            - question: text following the menu
            Usage: Console_App_Menu.set_options(index, options)"""

        self.__separator = separator
        self.__question = question
        lenght = len(options_list)
        start = self.__ruler((self.__height - (self.__height / 4), lenght)) 
        string_size = max([len(item) for item in options_list])
        self.__options_list = [string.ljust(item, string_size) for item in options_list]
        if index_type == 'numbers':
            self.__indexs = string.digits
        elif index_type == 'low':
            self.__indexs = string.lowercase
        elif index_type == 'caps':
            self.__indexs = string.uppercase
        else:
            print 'Invalid Index Type'
        self.__valids = []
        index = 0
        for i in range(start, start + lenght):
            self.__writeline(i, self.__indexs[index] + self.__separator + self.__options_list[index])
            self.__valids.append(self.__indexs[index])
            index += 1

    def option(self):

        """Returns user selected menu option as a tuple containing:
            - the option index order
            - the option index character
            - the option text
            Usage: option = Console_App_Menu.option()"""

        print self.__content.getvalue(),
        selected = raw_input(self.__question)
        if selected not in self.__valids:
            self.__dialog('error', 'Invalid Option Selected!!!')
            return self.option()
        else:
            return (self.__valids.index(selected), selected, self.__options_list[self.__valids.index(selected)].strip())

    def log(self, output):

        """Prints given text to the log window.
            Usage: Console_App_Menu.log(text)"""
        
        if len(output) > 56:
            list_output = wrap(output, 56)
            for output in list_output:
                self.__log_write(output)
            self.__update_display()
        else:
            self.__log_write(output)
            self.__update_display()
        
    def info(self, text):

        """Displays a information dialog with the given text.
            Usage: Console_App_Menu.info(text)"""
        
        self.__dialog('info', text[0:37])

    def error(self, text):

        """Displays a error dialog with the given text.
            Usage: Console_App_Menu.error(text)"""

        self.__dialog('error', text[0:37])

    def query(self, input_type, text):

        """Displays a query dialog with the given text and can return
            three different types of data:
                - an integer: Console_App_Menu.query('int', text)
                - a string: Console_App_Menu.query('str', text)
                - a boolean: Console_App_Menu.query('bool', text)
            Usage: input = Console_App_Menu.query(type, text)"""
        
        if input_type in ('int', 'str', 'bool'):
            return self.__dialog('query_' + input_type, text[0:37])
        
if __name__ == '__main__':

    menu = Console_App_Menu(title='Console Application Menu Calculator Demo')

    menu.set_options('low', ('Add x to y', 'Subtract x to y', 'Multiply x by y', 'Divide x by y', 'Help', 'Exit'))

    menu.log('Welcome, please select an option to continue...')

    exit_flag = None

    while not exit_flag: 

        selected = menu.option()

        if selected == (0, 'a', 'Add x to y'):
            x = menu.query('int', 'Insert number x')
            y = menu.query('int', 'Insert number y')
            menu.log('The result of adding %s to %s is %s.' % (x, y, (x + y)))
        elif selected[0] == 1:
            x = menu.query('int', 'Insert number x')
            y = menu.query('int', 'Insert number y')
            menu.log('The result of subtracting %s to %s is %s.' % (x, y, (y - x)))    
        elif selected[1] == 'c':
            x = menu.query('int', 'Insert number x')
            y = menu.query('int', 'Insert number y')
            menu.log('The result of multiplying %s by %s is %s.' % (x, y, (x * y)))    
        elif selected[0] == 3:
            x = menu.query('int', 'Insert number x')
            y = menu.query('int', 'Insert number y')
            menu.log('The result of dividing %s by %s is %s.' % (x, y, (x / y)))    
        elif selected[2] == 'Help':
            menu.log('Select a calculation option. The numbers will be asked, the calculation is done and the result will be displayed on this log window.')    
        elif selected[2] == 'Exit':
            if menu.query('bool', 'Are you sure you want to exit?'):
                exit_flag = True
    else:
        name = menu.query('str', 'Insert your name')
        menu.info('Bye, %s!' % name)

Smart thinking. I might drop back by and add some stuff.

I would make a minor gripe. Attributes and methods with two leading underscores and no trailing underscores are mangled by Python. I know your intention is to indicate that these attributes and methods should not be accessed externally, but typically a single leading underscore indicates internal use. A double leading underscore is primarily intended to prevent name clashes with derived classes.

I take my information from PEP 8, Python community guidelines on style.

Use one leading underscore only for non-public methods and instance variables.

To avoid name clashes with subclasses, use two leading underscores to invoke Python's name mangling rules.

http://www.python.org/dev/peps/pep-0008/

Cheers!

out of curiosity, how would i change the ### to ===
(im learning python as i go along)

def __init__(self, title='Console Application Menu', size=(80, 25), border='#', log_title='Log Output:'):
 
"""The class initialization takes as optional arguments:
- title: title text of the menu
- size: the size in characteres of the console window menu
- border: character to be printed as border for the menu and dialogs
- log_title: title text of the log window"""

You change the

border='#'

to

border='='

in there. If you read the comments it gets pretty clear.

Hope I helped. Cheers and Happy Coding

P.S.: I'm back. =D

Edited 5 Years Ago by Beat_Slayer: n/a

The article starter has earned a lot of community kudos, and such articles offer a bounty for quality replies.