OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/python |
| 2 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. |
| 5 |
| 6 """A module containing a function for presenting a text menu to the user.""" |
| 7 |
| 8 import sys |
| 9 |
| 10 |
| 11 def __ceil_div(numerator, denominator): |
| 12 """Do integer division, rounding up. |
| 13 |
| 14 >>> __ceil_div(-1, 2) |
| 15 0 |
| 16 >>> __ceil_div(0, 2) |
| 17 0 |
| 18 >>> __ceil_div(1, 2) |
| 19 1 |
| 20 >>> __ceil_div(2, 2) |
| 21 1 |
| 22 >>> __ceil_div(3, 2) |
| 23 2 |
| 24 |
| 25 Args: |
| 26 numerator: The number to divide. |
| 27 denominator: The number to divide by. |
| 28 |
| 29 Returns: |
| 30 (numberator) / denominator, rounded up. |
| 31 """ |
| 32 return (numerator + denominator - 1) // denominator |
| 33 |
| 34 |
| 35 def __build_menu_str(items, title, prompt, menu_width, spacing): |
| 36 """Build the menu string for TextMenu. |
| 37 |
| 38 See TextMenu for a description. This function mostly exists to simplify |
| 39 testing. |
| 40 |
| 41 >>> __build_menu_str(['A'], "Choose:", "Choice: ", 76, 2) |
| 42 'Choose:\\n\\n1. A\\n\\nChoice: ' |
| 43 |
| 44 >>> __build_menu_str(['B', 'A'], "Choose:", "Choice: ", 76, 2) |
| 45 'Choose:\\n\\n1. B 2. A\\n\\nChoice: ' |
| 46 |
| 47 >>> __build_menu_str(['A', 'B', 'C', 'D', 'E'], "Choose:", "Choice: ", 10, 2) |
| 48 'Choose:\\n\\n1. A 4. D\\n2. B 5. E\\n3. C\\n\\nChoice: ' |
| 49 |
| 50 >>> __build_menu_str(['A', 'B', 'C', 'D', 'E'], "Choose:", "Choice: ", 11, 2) |
| 51 'Choose:\\n\\n1. A 4. D\\n2. B 5. E\\n3. C\\n\\nChoice: ' |
| 52 |
| 53 >>> __build_menu_str(['A', 'B', 'C'], "Choose:", "Choice: ", 9, 2) |
| 54 'Choose:\\n\\n1. A\\n2. B\\n3. C\\n\\nChoice: ' |
| 55 |
| 56 >>> __build_menu_str(['A'*10, 'B'*10], "Choose:", "Choice: ", 0, 2) |
| 57 'Choose:\\n\\n1. AAAAAAAAAA\\n2. BBBBBBBBBB\\n\\nChoice: ' |
| 58 |
| 59 >>> __build_menu_str([], "Choose:", "Choice: ", 76, 2) |
| 60 Traceback (most recent call last): |
| 61 ... |
| 62 ValueError: Can't build a menu of empty choices. |
| 63 """ |
| 64 if not items: |
| 65 raise ValueError("Can't build a menu of empty choices.") |
| 66 |
| 67 # Figure out some basic stats about the items. |
| 68 num_items = len(items) |
| 69 longest_num = len(str(num_items)) |
| 70 longest_item = longest_num + len(". ") + max(len(item) for item in items) |
| 71 |
| 72 # Figure out number of rows / cols. |
| 73 num_cols = max(1, (menu_width + spacing) // (longest_item + spacing)) |
| 74 num_rows = __ceil_div(num_items, num_cols) |
| 75 |
| 76 # Construct "2D array" of lines. Remember that we go down first, then |
| 77 # right. This seems to mimic "ls" behavior. Note that, unlike "ls", we |
| 78 # currently make all columns have the same width. Shrinking small columns |
| 79 # would be a nice optimization, but complicates the algorithm a bit. |
| 80 lines = [[] for _ in xrange(num_rows)] |
| 81 for item_num, item in enumerate(items): |
| 82 col, row = divmod(item_num, num_rows) |
| 83 item_str = "%*d. %s" % (longest_num, item_num + 1, item) |
| 84 lines[row].append("%-*s" % (longest_item, item_str)) |
| 85 |
| 86 # Change lines from 2D array into 1D array (1 entry per row) by joining |
| 87 # columns with spaces. |
| 88 spaces = ' ' * spacing |
| 89 lines = [spaces.join(line) for line in lines] |
| 90 |
| 91 # Make the final menu string by adding some return and the prompt. |
| 92 menu_str = "%s\n\n%s\n\n%s" % (title, '\n'.join(lines), prompt) |
| 93 return menu_str |
| 94 |
| 95 |
| 96 def TextMenu(items, title="Choose one:", prompt="Choice: ", |
| 97 menu_width=76, spacing=4): |
| 98 """Display text-based menu to the user and get back a response. |
| 99 |
| 100 The menu will be printed to sys.stderr and input will be read from sys.stdin. |
| 101 |
| 102 If the user doesn't want to choose something, it is expected that he/she |
| 103 will press Ctrl-C to exit. |
| 104 |
| 105 The menu will look something like this: |
| 106 1. __init__.py 3. chromite 5. lib 7. tests |
| 107 2. bin 4. chroot_specs 6. specs |
| 108 |
| 109 Choice: |
| 110 |
| 111 Args: |
| 112 items: The strings to show in the menu. These should be sorted in whatever |
| 113 order you want to show to the user. |
| 114 prompt: The prompt to show to the user. |
| 115 menu_width: The maximum width to use for the menu; 0 forces things to single |
| 116 column. |
| 117 spacing: The spacing between items. |
| 118 |
| 119 Returns: |
| 120 The index of the item chosen by the user. Note that this is a 0-based |
| 121 index, even though the user is presented with the menu in 1-based format. |
| 122 """ |
| 123 # Call the helper to build the actual menu string. |
| 124 menu_str = __build_menu_str(items, title, prompt, menu_width, spacing) |
| 125 |
| 126 # Loop until we get a valid input from the user (or they hit Ctrl-C, which |
| 127 # will throw and exception). |
| 128 while True: |
| 129 # Write the menu to stderr, which makes it possible to use this with |
| 130 # commands where you want the output redirected. |
| 131 sys.stderr.write(menu_str) |
| 132 |
| 133 # Don't use a prompt with raw_input(), since that would go to stdout. |
| 134 result = raw_input() |
| 135 |
| 136 # Parse into a number and do error checking. If all good, return. |
| 137 try: |
| 138 result_int = int(result) |
| 139 if 1 <= result_int <= len(items): |
| 140 # Convert from 1-based to 0-based index! |
| 141 return result_int - 1 |
| 142 else: |
| 143 print >>sys.stderr, "\nERROR: %d out of range\n" % result_int |
| 144 except ValueError: |
| 145 print >>sys.stderr, "\nERROR: '%s' is not a valid choice\n" % result |
| 146 |
| 147 |
| 148 def __test(): |
| 149 """Run any built-in tests.""" |
| 150 import doctest |
| 151 doctest.testmod(verbose=True) |
| 152 |
| 153 |
| 154 # For testing purposes, you can run this on the command line... |
| 155 if __name__ == '__main__': |
| 156 # If first argument is --test, run testing code. |
| 157 # ...otherwise, pass all arguments as the menu to show. |
| 158 if sys.argv[1:2] == ['--test']: |
| 159 __test(*sys.argv[2:]) |
| 160 else: |
| 161 TextMenu(sys.argv[1:]) |
OLD | NEW |