Source code for consolemenu.console_menu
from __future__ import print_function
import platform
import threading
import os
from consolemenu.menu_formatter import MenuFormatBuilder
from consolemenu.screen import Screen
[docs]class ConsoleMenu(object):
"""
A class that displays a menu and allows the user to select an option.
Args:
title (str): The title of the menu, or a method reference that returns a string.
subtitle (str): The subtitle of the menu, or a method reference that returns a string.
screen (:obj:`consolemenu.screen.Screen`): The screen object associated with this menu.
formatter (:obj:`MenuFormatBuilder`): The MenuFormatBuilder instance used to format this menu.
prologue_text (str): Text or method reference to include in the "prologue" section of the menu.
epilogue_text (str): Text or method reference to include in the "epilogue" section of the menu.
show_exit_option (bool): Specifies whether this menu should show an exit item by default. Defaults to True.
Can be overridden when the menu is started.
exit_option_text (str): Text for the Exit menu item. Defaults to 'Exit'.
exit_menu_char (str): Character to use for exiting the menu. Defaults to None.
clear_screen (bool): Set to False to disable clearing of screen between menus
Attributes:
cls.currently_active_menu (:obj:`ConsoleMenu`): Class variable that holds the currently active menu or None
if no menu is currently active (e.g. when switching between menus)
items (:obj:`list` of :obj:`MenuItem`): The list of MenuItems that the menu will display
parent (:obj:`ConsoleMenu`): The parent of this menu
previous_active_menu (:obj:`ConsoleMenu`): the previously active menu to be restored into the class's
currently active menu
current_option (int): The currently highlighted menu option
selected_option (int): The option that the user has most recently selected
"""
currently_active_menu = None
def __init__(self, title=None, subtitle=None, screen=None, formatter=None,
prologue_text=None, epilogue_text=None, clear_screen=True,
show_exit_option=True, exit_option_text='Exit', exit_menu_char=None):
if screen is None:
screen = Screen()
self.screen = screen
self.clear_screen_before_render = clear_screen
if formatter is None:
formatter = MenuFormatBuilder()
self.formatter = formatter
self.title = title
self.subtitle = subtitle
self.prologue_text = prologue_text
self.epilogue_text = epilogue_text
self.highlight = None
self.normal = None
self.show_exit_option = show_exit_option
self.items = list()
self.parent = None
self.exit_item = ExitItem(menu=self, text=exit_option_text, menu_char=exit_menu_char)
self.current_option = 0
self.selected_option = -1
self.returned_value = None
self.should_exit = False
self.previous_active_menu = None
self._main_thread = None
self._running = threading.Event()
def __repr__(self):
return "%s: %s. %d items" % (self.get_title(), self.get_subtitle(), len(self.items))
@property
def current_item(self):
"""
:obj:`consolemenu.items.MenuItem`: The item corresponding to the menu option that is currently highlighted,
or None.
"""
if self.items:
return self.items[self.current_option]
else:
return None
@property
def selected_item(self):
"""
:obj:`consolemenu.items.MenuItem`: The item in :attr:`items` that the user most recently selected, or None.
"""
if self.items and self.selected_option != -1:
return self.items[self.current_option]
else:
return None
[docs] def append_item(self, item):
"""
Add an item to the end of the menu before the exit item.
Args:
item (MenuItem): The item to be added.
"""
did_remove = self.remove_exit()
item.menu = self
self.items.append(item)
if did_remove:
self.add_exit()
def remove_item(self, item):
"""
Remove the specified item from the menu.
Args:
item (MenuItem): the item to be removed.
Returns:
bool: True if the item was removed; False otherwise.
"""
for idx, _item in enumerate(self.items):
if item == _item:
del self.items[idx]
return True
return False
[docs] def add_exit(self):
"""
Add the exit item if necessary. Used to make sure there aren't multiple exit items.
Returns:
bool: True if item needed to be added, False otherwise.
"""
if not self.items or self.items[-1] is not self.exit_item:
self.items.append(self.exit_item)
return True
return False
[docs] def remove_exit(self):
"""
Remove the exit item if necessary. Used to make sure we only remove the exit item, not something else.
Returns:
bool: True if item needed to be removed, False otherwise.
"""
if self.items:
if self.items[-1] is self.exit_item:
del self.items[-1]
return True
return False
def is_selected_item_exit(self):
"""
Checks to determine if the currently selected item is the Exit Menu item.
Returns:
bool: True if the currently selected item is the Exit Menu item; False otherwise.
"""
return self.selected_item and self.selected_item is self.exit_item
def _wrap_start(self):
self._main_loop()
ConsoleMenu.currently_active_menu = None
self.clear_screen()
ConsoleMenu.currently_active_menu = self.previous_active_menu
[docs] def start(self, show_exit_option=None):
"""
Start the menu in a new thread and allow the user to interact with it.
The thread is a daemon, so :meth:`join()<consolemenu.ConsoleMenu.join>` should be called if there's a
possibility that the main thread will exit before the menu is done
Args:
show_exit_option (bool): Specify whether the exit item should be shown, defaults to the value
set in the constructor
"""
self.previous_active_menu = ConsoleMenu.currently_active_menu
ConsoleMenu.currently_active_menu = None
self.should_exit = False
if show_exit_option is None:
show_exit_option = self.show_exit_option
if show_exit_option:
self.add_exit()
else:
self.remove_exit()
try:
self._main_thread = threading.Thread(target=self._wrap_start, daemon=True)
except TypeError:
self._main_thread = threading.Thread(target=self._wrap_start)
self._main_thread.daemon = True
self._main_thread.start()
[docs] def show(self, show_exit_option=None):
"""
Calls start and then immediately joins.
Args:
show_exit_option (bool): Specify whether the exit item should be shown, defaults to the value set
in the constructor
"""
self.start(show_exit_option)
self.join()
def _main_loop(self):
self._set_up_colors()
ConsoleMenu.currently_active_menu = self
self._running.set()
while self._running.wait() is not False and not self.should_exit:
self.clear_screen()
self.draw()
self.process_user_input()
[docs] def draw(self):
"""
Refresh the screen and redraw the menu. Should be called whenever something changes that needs to be redrawn.
"""
self.screen.printf(self.formatter.format(title=self.get_title(),
subtitle=self.get_subtitle(),
items=self.items,
prologue_text=self.get_prologue_text(),
epilogue_text=self.get_epilogue_text()))
[docs] def is_running(self):
"""
Check if the menu has been started and is not paused.
Returns:
bool: True if the menu is started and hasn't been paused; False otherwise.
"""
return self._running.is_set()
[docs] def wait_for_start(self, timeout=None):
"""
Block until the menu is started.
Args:
timeout: How long to wait before timing out.
Returns:
bool: False if timeout is given and operation times out, True otherwise. None before Python 2.7.
"""
return self._running.wait(timeout)
[docs] def is_alive(self):
"""
Check whether the thread is stil alive.
Returns:
bool: True if the thread is still alive; False otherwise.
"""
return self._main_thread.is_alive()
[docs] def pause(self):
"""
Temporarily pause the menu until resume is called.
"""
self._running.clear()
[docs] def resume(self):
"""
Sets the currently active menu to this one and resumes it.
"""
ConsoleMenu.currently_active_menu = self
self._running.set()
[docs] def join(self, timeout=None):
"""
Should be called at some point after :meth:`start()<consolemenu.ConsoleMenu.start>` to block until
the menu exits.
Args:
timeout (Number): How long to wait before timing out.
"""
self._main_thread.join(timeout=timeout)
[docs] def get_input(self):
"""
Can be overridden to change the input method.
Called in :meth:`process_user_input()<consolemenu.ConsoleMenu.process_user_input>`
:return: the ordinal value of a single character
:rtype: int
"""
return self.screen.input()
[docs] def process_user_input(self):
"""
Gets the next single character and decides what to do with it
"""
try:
user_input = self.get_input()
except EOFError:
self.should_exit = True
return
# Process menu characters first
for i, cm in enumerate(self.items):
if cm.menu_char == user_input:
self.current_option = i
self.select()
return user_input
try:
num = int(user_input)
except Exception:
return
if 0 < num < len(self.items) + 1:
self.current_option = num - 1
self.select()
return user_input
[docs] def go_to(self, option):
"""
Go to the option entered by the user as a number
:param option: the option to go to
:type option: int
"""
self.current_option = option
self.draw()
[docs] def go_down(self):
"""
Go down one, wrap to beginning if necessary
"""
if self.current_option < len(self.items) - 1:
self.current_option += 1
else:
self.current_option = 0
self.draw()
[docs] def go_up(self):
"""
Go up one, wrap to end if necessary
"""
if self.current_option > 0:
self.current_option += -1
else:
self.current_option = len(self.items) - 1
self.draw()
[docs] def select(self):
"""
Select the current item and run it
"""
self.selected_option = self.current_option
self.selected_item.set_up()
self.selected_item.action()
self.selected_item.clean_up()
self.returned_value = self.selected_item.get_return()
self.should_exit = self.selected_item.should_exit
[docs] def exit(self):
"""
Signal the menu to exit, then block until it's done cleaning up
"""
self.should_exit = True
self.join()
def _set_up_colors(self):
# TODO add color support
# curses.init_pair(1, curses.COLOR_BLACK, curses.COLOR_WHITE)
# self.highlight = curses.color_pair(1)
# self.normal = curses.A_NORMAL
pass
def clear_screen(self):
"""
Clear the screen belonging to this menu
"""
if self.clear_screen_before_render:
self.screen.clear()
# Getters to get text in case method reference
def get_title(self):
return self.title() if callable(self.title) else self.title
def get_subtitle(self):
return self.subtitle() if callable(self.subtitle) else self.subtitle
def get_prologue_text(self):
return self.prologue_text() if callable(self.prologue_text) else self.prologue_text
def get_epilogue_text(self):
return self.epilogue_text() if callable(self.epilogue_text) else self.epilogue_text
[docs]class MenuItem(object):
"""
A generic menu item
"""
def __init__(self, text, menu=None, should_exit=False, menu_char=None):
"""
:ivar str text: The text shown for this menu item
:ivar ConsoleMenu menu: The menu to which this item belongs
:ivar bool should_exit: Whether the menu should exit once this item's action is done
:ivar str menu_char: The character used to select this menu item. Optional - defaults to None.
"""
self.text = text
self.menu = menu
self.should_exit = should_exit
self.index_item_separator = " - "
self.menu_char = menu_char
def __str__(self):
return "%s %s" % (self.menu.get_title(), self.get_text())
[docs] def show(self, index):
"""
How this item should be displayed in the menu. Can be overridden, but should keep the same signature.
Default is:
1 - Item 1
2 - Another Item
:param int index: The index of the item in the items list of the menu
:return: The representation of the item to be shown in a menu
:rtype: str
"""
self.index = index + 1
if self.menu_char is None:
ret = "%2d%s%s" % (index + 1, self.index_item_separator, self.get_text())
else:
ret = " %c%s%s" % (self.menu_char, self.index_item_separator, self.get_text())
return ret
[docs] def get_return(self):
"""
Override to change what the item returns.
Otherwise just returns the same value the last selected item did.
"""
return self.menu.returned_value
def __eq__(self, o):
return self.text == o.text and self.menu == o.menu and self.should_exit == o.should_exit
# Getters to get text in case method reference
def get_text(self):
return self.text() if callable(self.text) else self.text
[docs]class ExitItem(MenuItem):
"""
Used to exit the current menu. Handled by :class:`consolemenu.ConsoleMenu`
"""
def __init__(self, text="Exit", menu=None, menu_char=None):
super(ExitItem, self).__init__(text=text, menu=menu, should_exit=True, menu_char=menu_char)
[docs] def show(self, index, available_width=None):
"""
ExitItem overrides this method to display appropriate Exit or Return text.
"""
# If we have a parent menu, and no overriding exit text was specified,
# change Exit text to "Return to {Parent Menu Title}"
if self.menu and self.menu.parent and self.get_text() == 'Exit':
self.text = "Return to %s" % self.menu.parent.get_title()
return super(ExitItem, self).show(index)
[docs]def clear_terminal():
"""
Call the platform specific function to clear the terminal: cls on windows, reset otherwise
"""
if platform.system().lower() == "windows":
os.system('cls')
else:
os.system('reset')