import curses import curses.ascii import sys import types class terminal_menu(object): """ title:: terminal_menu description:: A class for creating a simple, terminal-based menu using the "curses" module for terminal handling for character cell displays. The menu is constructed by adding entries. An entry is a tuple consisting of: label A string that will appear in the on-screen menu. function (optional) A "function" object or a "terminal_menu" object to be invoked/ displayed when the on-screen menu item is selected. parameters (optional) A "tuple" containing the parameters that are required, if any, by the associated function. A menu title (optional) can be specified to appear at the top of the screen in reverse-text. attributes:: title A string containing the menu title [default is None]. entries A list of tuples with each containing the label, function, and parameters for each menu entry [default is an empty list]. selected The index of the default menu entry to be selected upon instantiation [default is 0]. methods:: set_title(title) Set the title for the existing menu object. set_selected(entryIndex) Specify the entry index for the menu entry to be highlighted (selected) for the existing menu object. add_entry(label, function, parameters) Add a new menu entry to the end of the existing entries list for the existing menu object. This form implies that upon selection, the specified function should be called with the optional set of function parameters. add_entry(label, menu) Add a new menu entry to the end of the existing entries list for the existing menu object. This form implies that upon selection, the specified menu object will replace the currently displayed menu on the screen. add_entry() Add a blank menu entry to the end of the existing entries list for the existing menu object. This line will be skipped during navigation. display() Display the menu object and allow the user to interact with the menu. When a selection is made, the method will return a tuple containing the menu item number, label, function, and parameters. If the user hits the key, the returned menu item number will be -1. clear() Clear the currently displayed menu object from the screen. execute() Execute the function for the selected menu item, passing it the associated parameters. associated methods:: quit() Quit the menu and terminate the program with a system exit status of 0. author:: Carl Salvaggio copyright:: Copyright (C) 2007, Rochester Institute of Technology license:: GPL version:: 1.0.0 disclaimer:: This source code is provided "as is" and without warranties as to performance or merchantability. The author and/or distributors of this source code may have made statements about this source code. Any such statements do not constitute warranties and shall not be relied on by the user in deciding whether to use this source code. This source code is provided without any express or implied warranties whatsoever. Because of the diversity of conditions and hardware under which this source code may be used, no warranty of fitness for a particular purpose is offered. The user is advised to test the source code thoroughly before relying on it. The user must assume the entire risk of using the source code. """ LABEL = 0 FUNCTION = 1 PARAMETERS = 2 def __init__(self, title=None, entries=[], selected=0, noecho=True, cbreak=True, keypad=True): self.title = title self.entries = entries for entryIndex in range(len(self.entries)): while len(self.entries[entryIndex]) < 3: self.entries[entryIndex].append(None) if self.entries[entryIndex][self.LABEL] == None: self.entries[entryIndex][self.LABEL] = '' self.selected = selected # Determine the terminal type, send any required setup codes to # the terminal, and create various internal data structures for # this window object self.stdscr = curses.initscr() # Turn off automatic echoing of keys to the screen if noecho: curses.noecho() else: curses.echo() # Turn on "cbreak" mode to respond instantly to keys without # requiring "Enter" to be pressed if cbreak: curses.cbreak() else: curses.nocbreak() # Enable keypad mode for this window object to be able to respond # to keys that produce multibyte escape sequences if keypad: self.stdscr.keypad(1) else: self.stdscr.keypad(0) def set_title(self, title): self.title = title def set_selected(self, selected): self.selected = selected def add_entry(self, label=None, function=None, parameters=None): if isinstance(label, tuple): entry = label while len(entry) < 3: entry.append(None) else: entry = [label, function, parameters] if entry[self.LABEL] == None: entry[self.LABEL] = '' self.entries.append(entry) def display(self, showEscape=False): self.stdscr.clear() if self.title is None: self.offset = 0 else: self.offset = 2 self.stdscr.addstr(0, 0, self.title, curses.A_REVERSE) for entryIndex in range(len(self.entries)): self.stdscr.addstr(entryIndex+self.offset, 0, self.entries[entryIndex][self.LABEL], curses.A_NORMAL) if showEscape: self.stdscr.addstr(entryIndex+self.offset+3, 0, 'Press to exit', curses.A_REVERSE) self.stdscr.addstr(self.selected+self.offset, 0, self.entries[self.selected][self.LABEL], curses.A_REVERSE) self.stdscr.refresh() while True: previouslySelected = self.selected c = self.stdscr.getch() if c == curses.ascii.ESC: self.selected = -1 break if c == curses.ascii.LF: break if c == curses.KEY_UP: self.selected -= 1 if self.selected < 0: curses.beep() self.selected = 0 if self.entries[self.selected][self.LABEL] == '': self.selected -= 1 if c == curses.KEY_DOWN: self.selected += 1 if self.selected == len(self.entries): curses.beep() self.selected = len(self.entries) - 1 if self.entries[self.selected][self.LABEL] == '': self.selected += 1 self.stdscr.addstr(previouslySelected+self.offset, 0, self.entries[previouslySelected][self.LABEL], curses.A_NORMAL) self.stdscr.addstr(self.selected+self.offset, 0, self.entries[self.selected][self.LABEL], curses.A_REVERSE) self.stdscr.refresh() if self.selected < 0: return (self.selected, None, None, None) else: return (self.selected, self.entries[self.selected][self.LABEL], self.entries[self.selected][self.FUNCTION], self.entries[self.selected][self.PARAMETERS]) def clear(self): # Turn off keypad mode for this window object self.stdscr.keypad(0) # Turn off "cbreak" mode curses.nocbreak() # Allow keys to be echoed to the screen again curses.echo() # Restore the terminal to its normal operating mode curses.endwin() def execute(self): function = self.entries[self.selected][self.FUNCTION] parameters = self.entries[self.selected][self.PARAMETERS] if type(function) == types.FunctionType: if parameters is None: return function() else: if isinstance(parameters, tuple): return function(*parameters) else: return function(parameters) else: return None def quit(): # Turn off "cbreak" mode curses.nocbreak() # Allow keys to be echoed to the screen again curses.echo() # Restore the terminal to its normal operating mode curses.endwin() sys.exit(0) if __name__ == '__main__': import os import subprocess import sys import time import types from terminal_menu import * """ An example script for creating on-screen menus and testing the "terminal_menu" class and its methods. """ title = 'File Listing Menu' entries = [('ls', subprocess.check_output, (['ls'])), ('ls -1', subprocess.check_output, (['ls', '-1'])), ('ls -F', subprocess.check_output, (['ls', '-F']))] selected = 0 listingMenu = terminal_menu(title, entries, selected) title = 'File Listing Menu (More)' entries = [('ls -l', subprocess.check_output, (['ls', '-l'])), ('ls -la', subprocess.check_output, (['ls', '-la'])), ('ls -lag', subprocess.check_output, (['ls', '-lag']))] selected = 0 moreListingMenu = terminal_menu(title, entries, selected) listingMenu.add_entry() listingMenu.add_entry('More ...', moreListingMenu) listingMenu.add_entry() listingMenu.add_entry('Quit', quit) moreListingMenu.add_entry('') moreListingMenu.add_entry('Back ...', listingMenu) currentMenu = listingMenu while True: (item, label, function, parameters) = currentMenu.display() if item < 0: currentMenu.clear() print 'Menu terminated by hitting ...' sys.exit(0) else: if isinstance(function, terminal_menu): currentMenu = function continue if type(function) == types.FunctionType: os.system('clear') print 'Item: ' + str(item) + '\r' print 'Label: ' + str(label) + '\r' print 'Funtion: ' + str(function) + '\r' print 'Parameters: ' + str(parameters) + '\r' result = currentMenu.execute() print '\r' print 'Result:' + '\r' result = result.lstrip() result = str.replace(result, '\n', '\r\n') print result sys.stdout.flush() time.sleep(3)