bpeterso2000
8/2/2014 - 3:10 AM

text_menu.py

import logging
import sys

import color

if sys.version_info.major < 3:
    input = raw_input


class Menu:
    "Console text-based interactive menu class"

    def __init__(self, items, title=None, prompt=None, exit=None,
                 enumerated=True, returns='value', case_sensitive=False,
                 required=True):
        """
        :param items: list of menu items in one of the following formats:
            Formats permitted when 'enumerated' is:
                True:
                    - [label, ...]                Notes: key=enum, value=label
                    - [(label, value, ...]        Notes: key=enum
                False:
                    - [key, ...]                  Notes: label=key, value=key
                    - [(key, label), ...]         Notes: value=label
                    - [(key, label, value), ...]
        :param title:        The caption above the menu.
        :param prompt:       The prompt for user input located below the menu.
        :param exit_on_quit: Disabled when set to None, otherwise whenever
                             the specified string/character is enetered the
                             program will gracefully exit.
        :param enumerated:   When True the menu items will be enumerated.
        :param returns:      'value', 'function_name' or 'method_name'.
                             function_name: converts return value to lower
                                            case and replaces spaces with
                                            underscores (PEP8 funct naming)
                             method_name:   converts return values to CamelCase
                                            (PEP8 method naming)
                             else:          return unaltered value.
        :param case_sensitive: When True keys and user responses will be
                               converted to lower case.
        :param required:       When True requires a reponse, otherwise a
                               carriage return becomes a valid response.
        """
        self.items = items
        self.title = title
        self.prompt = prompt
        self.enumerated = enumerated
        self.returns = returns
        self.case_sensitive = case_sensitive
        self.required = required
        self.exit = exit if case_sensitive or not exit else exit.lower()
        self.set_colors()

    def set_colors(self):
        self.title_color = color.GRN
        self.key_color = color.LT_GRN
        self.label_color = color.YEL
        self.prompt_color = color.GRN
        self.error_color = color.RED

    def log_error(self, reason, error):
        logger = logging.getLogger(__name__)
        logger.error(reason)
        logger.error(error)
        sys.exit(1)

    def process_response(self, response, keys, values):
        if not (self.required or response):
            return 0, None
        if self.exit and response == 'q':
            return 'q', None
        if self.enumerated:
            try:
                item_num = int(response)
                if item_num < 1 or item_num > len(self.items):
                    raise ValueError
                return item_num, values[item_num - 1]
            except ValueError:
                print(self.error_color + 'Warning: Invalid Response.')
        else:
            if not self.case_sensitive:
                response = response.lower()
            for key, value in zip(keys, values):
                print("KEY, VALUE:", key, value)
                if response == key:
                    return key, value
        print(self.error_color + 'Warning: Invalid Response.')
        return None, None

    def get_keys(self):
        if self.enumerated:
            return [str(i + 1) for i, _ in enumerate(self.items)]
        try:
            keys = [str(i[0]) for i in self.items]
            return map(str.lower, keys) if self.case_sensitive else keys
        except (TypeError, IndexError) as error:
            self.log_error('Unable to set menu item key.', error)

    def get_labels(self):
        label_idx = 0
        if not self.enumerated and len(self.items[0]) >= 2:
            label_idx = 1
        try:
            result = [i[label_idx] for i in self.items]
        except (TypeError, IndexError) as error:
            self.log_error('Unable to set menu item label.', error)
        if hasattr(result[0], '__call__'):
            result = [i.__name__ for i in self.items]
        return result

    def get_values(self):
        values = [i[-1] for i in self.items]
        try:
            if self.returns == 'function_name':
                return [i.lower().replace(' ', '_') for i in values]
            elif self.returns == 'method_name':
                return [''.join([j.title() for j in i.split()])
                        for i in values]
            return values
        except TypeError as error:
            self.log_error('Unable to set menu item return value.', error)

    def select(self):
        if not self.items:
            return 0, ''
        if isinstance(self.items[0], str):
            self.items = [[i] for i in self.items]
        item_num = None
        keys = self.get_keys()
        #print("KEYS:", keys)
        labels = self.get_labels()
        #print('LABELS:', labels)
        values = self.get_values()
        #print('VALUES:', values)
        key_padding = max(map(len, keys))
        padded_keys = [i.rjust(key_padding) for i in keys]
        #print("PADDED KEYS:", padded_keys)
        sep_length = max(map(len, labels)) + key_padding + 2
        line_sep = self.title_color + '-' * sep_length
        if self.prompt is None:
            prompt = 'Select option '
        if self.enumerated:
            prompt += ' (1-{})'.format(len(self.items))
        prompt += '? '
        while item_num is None:
            if self.title:
                print('\n{}{}'.format(self.title_color, self.title))
                print(line_sep)
            for key, label in zip(padded_keys, labels):
                print('{} {}  {}{}'.format(self.key_color, key,
                                           self.label_color, label))
            print(line_sep)
            sys.stdout.write('{}{}'.format(self.prompt_color, prompt))
            sys.stdout.write(color.RESET)
            response = input()
            if not self.case_sensitive:
                response = response.lower()
            if self.exit and response == self.exit:
                sys.exit(0)
            item_num, item = self.process_response(response, keys, values)
        return item_num, item

if __name__ == '__main__':
    items = ['apple', 'banana', 'cantalope']
    item = Menu(items, quit=True).select()
    print(item)