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)