1,105,197 Community Members

Console Application Menu Module

Member Avatar
(Beat_Slayer)
Reputation Points: 17 [?]
Q&As Helped to Solve: 105 [?]
Skill Endorsements: 1 [?]
 
0
 

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

Attachments scrennshot.jpg 52.82KB
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)
Member Avatar
lrh9
Posting Whiz in Training
250 posts since Oct 2009
Reputation Points: 95 [?]
Q&As Helped to Solve: 37 [?]
Skill Endorsements: 6 [?]
 
0
 

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!

Member Avatar
Beat_Slayer
Posting Pro in Training
401 posts since Jun 2010
Reputation Points: 17 [?]
Q&As Helped to Solve: 105 [?]
Skill Endorsements: 1 [?]
 
0
 

Thanks for the enlightement mate.

Cheers and Happy coding

Member Avatar
Gallefray
Newbie Poster
1 post since Jun 2011
Reputation Points: 0 [?]
Q&As Helped to Solve: 0 [?]
Skill Endorsements: 0 [?]
 
0
 

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

Member Avatar
Beat_Slayer
Posting Pro in Training
401 posts since Jun 2010
Reputation Points: 17 [?]
Q&As Helped to Solve: 105 [?]
Skill Endorsements: 1 [?]
 
0
 
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

You
Post:
Start New Discussion
View similar articles that have also been tagged: