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 _CeilDiv(numerator, denominator): |
| 12 """Do integer division, rounding up. |
| 13 |
| 14 >>> _CeilDiv(-1, 2) |
| 15 0 |
| 16 >>> _CeilDiv(0, 2) |
| 17 0 |
| 18 >>> _CeilDiv(1, 2) |
| 19 1 |
| 20 >>> _CeilDiv(2, 2) |
| 21 1 |
| 22 >>> _CeilDiv(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 _BuildMenuStr(items, title, prompt, menu_width, spacing, add_quit): |
| 36 """Build the menu string for TextMenu. |
| 37 |
| 38 See TextMenu for a description. This function mostly exists to simplify |
| 39 testing. |
| 40 |
| 41 >>> _BuildMenuStr(['A'], "Choose", "Choice", 76, 2, True) |
| 42 'Choose:\\n\\n1. A\\n\\nChoice (q to quit): ' |
| 43 |
| 44 >>> _BuildMenuStr(['B', 'A'], "Choose", "Choice", 76, 2, False) |
| 45 'Choose:\\n\\n1. B 2. A\\n\\nChoice: ' |
| 46 |
| 47 >>> _BuildMenuStr(['A', 'B', 'C', 'D', 'E'], "Choose", "Choice", 10, 2, False) |
| 48 'Choose:\\n\\n1. A 4. D\\n2. B 5. E\\n3. C\\n\\nChoice: ' |
| 49 |
| 50 >>> _BuildMenuStr(['A', 'B', 'C', 'D', 'E'], "Choose", "Choice", 11, 2, False) |
| 51 'Choose:\\n\\n1. A 4. D\\n2. B 5. E\\n3. C\\n\\nChoice: ' |
| 52 |
| 53 >>> _BuildMenuStr(['A', 'B', 'C'], "Choose", "Choice", 9, 2, False) |
| 54 'Choose:\\n\\n1. A\\n2. B\\n3. C\\n\\nChoice: ' |
| 55 |
| 56 >>> _BuildMenuStr(['A'*10, 'B'*10], "Choose", "Choice", 0, 2, False) |
| 57 'Choose:\\n\\n1. AAAAAAAAAA\\n2. BBBBBBBBBB\\n\\nChoice: ' |
| 58 |
| 59 >>> _BuildMenuStr([], "Choose", "Choice", 76, 2, False) |
| 60 Traceback (most recent call last): |
| 61 ... |
| 62 ValueError: Can't build a menu of empty choices. |
| 63 |
| 64 Args: |
| 65 items: See TextMenu(). |
| 66 title: See TextMenu(). |
| 67 prompt: See TextMenu(). |
| 68 menu_width: See TextMenu(). |
| 69 spacing: See TextMenu(). |
| 70 add_quit: See TextMenu(). |
| 71 |
| 72 Returns: |
| 73 See TextMenu(). |
| 74 |
| 75 Raises: |
| 76 ValueError: If no items. |
| 77 """ |
| 78 if not items: |
| 79 raise ValueError("Can't build a menu of empty choices.") |
| 80 |
| 81 # Figure out some basic stats about the items. |
| 82 num_items = len(items) |
| 83 longest_num = len(str(num_items)) |
| 84 longest_item = longest_num + len(". ") + max(len(item) for item in items) |
| 85 |
| 86 # Figure out number of rows / cols. |
| 87 num_cols = max(1, (menu_width + spacing) // (longest_item + spacing)) |
| 88 num_rows = _CeilDiv(num_items, num_cols) |
| 89 |
| 90 # Construct "2D array" of lines. Remember that we go down first, then |
| 91 # right. This seems to mimic "ls" behavior. Note that, unlike "ls", we |
| 92 # currently make all columns have the same width. Shrinking small columns |
| 93 # would be a nice optimization, but complicates the algorithm a bit. |
| 94 lines = [[] for _ in xrange(num_rows)] |
| 95 for item_num, item in enumerate(items): |
| 96 row = item_num % num_rows |
| 97 item_str = "%*d. %s" % (longest_num, item_num + 1, item) |
| 98 lines[row].append("%-*s" % (longest_item, item_str)) |
| 99 |
| 100 # Change lines from 2D array into 1D array (1 entry per row) by joining |
| 101 # columns with spaces. |
| 102 spaces = " " * spacing |
| 103 lines = [spaces.join(line) for line in lines] |
| 104 |
| 105 # Add '(q to quit)' string to prompt if requested... |
| 106 if add_quit: |
| 107 prompt = "%s (q to quit)" % prompt |
| 108 |
| 109 # Make the final menu string by adding some return and the prompt. |
| 110 menu_str = "%s:\n\n%s\n\n%s: " % (title, "\n".join(lines), prompt) |
| 111 return menu_str |
| 112 |
| 113 |
| 114 def TextMenu(items, title="Choose one", prompt="Choice", |
| 115 menu_width=76, spacing=4, add_quit=True): |
| 116 """Display text-based menu to the user and get back a response. |
| 117 |
| 118 The menu will be printed to sys.stderr and input will be read from sys.stdin. |
| 119 |
| 120 If the user doesn't want to choose something, he/she can use the 'q' to quit |
| 121 or press Ctrl-C (which will be caught and treated the same). |
| 122 |
| 123 The menu will look something like this: |
| 124 1. __init__.py 3. chromite 5. lib 7. tests |
| 125 2. bin 4. chroot_specs 6. specs |
| 126 |
| 127 Choice (q to quit): |
| 128 |
| 129 Args: |
| 130 items: The strings to show in the menu. These should be sorted in whatever |
| 131 order you want to show to the user. |
| 132 title: The title of the menu. |
| 133 prompt: The prompt to show to the user. |
| 134 menu_width: The maximum width to use for the menu; 0 forces things to single |
| 135 column. |
| 136 spacing: The spacing between items. |
| 137 add_quit: Let the user type 'q' to quit the menu (we'll return None). |
| 138 |
| 139 Returns: |
| 140 The index of the item chosen by the user. Note that this is a 0-based |
| 141 index, even though the user is presented with the menu in 1-based format. |
| 142 Will be None if the user hits Ctrl-C, or chooses q to quit. |
| 143 |
| 144 Raises: |
| 145 ValueError: If no items. |
| 146 """ |
| 147 # Call the helper to build the actual menu string. |
| 148 menu_str = _BuildMenuStr(items, title, prompt, menu_width, spacing, |
| 149 add_quit) |
| 150 |
| 151 # Loop until we get a valid input from the user (or they hit Ctrl-C, which |
| 152 # will throw and exception). |
| 153 while True: |
| 154 # Write the menu to stderr, which makes it possible to use this with |
| 155 # commands where you want the output redirected. |
| 156 sys.stderr.write(menu_str) |
| 157 |
| 158 # Don't use a prompt with raw_input(), since that would go to stdout. |
| 159 try: |
| 160 result = raw_input() |
| 161 except KeyboardInterrupt: |
| 162 # Consider this a quit. |
| 163 return None |
| 164 |
| 165 # Check for quit request |
| 166 if add_quit and result.lower() in ("q", "quit"): |
| 167 return None |
| 168 |
| 169 # Parse into a number and do error checking. If all good, return. |
| 170 try: |
| 171 result_int = int(result) |
| 172 if 1 <= result_int <= len(items): |
| 173 # Convert from 1-based to 0-based index! |
| 174 return result_int - 1 |
| 175 else: |
| 176 print >>sys.stderr, "\nERROR: %d out of range.\n\n" % result_int |
| 177 except ValueError: |
| 178 print >>sys.stderr, "\nERROR: '%s' is not a valid choice.\n\n" % result |
| 179 |
| 180 |
| 181 def _Test(): |
| 182 """Run any built-in tests.""" |
| 183 import doctest |
| 184 doctest.testmod(verbose=True) |
| 185 |
| 186 |
| 187 # For testing purposes, you can run this on the command line... |
| 188 if __name__ == "__main__": |
| 189 # If first argument is --test, run testing code. |
| 190 # ...otherwise, pass all arguments as the menu to show. |
| 191 if sys.argv[1:2] == ["--test"]: |
| 192 _Test(*sys.argv[2:]) |
| 193 else: |
| 194 TextMenu(sys.argv[1:]) |
OLD | NEW |