| Index: chromite/lib/text_menu.py
|
| diff --git a/chromite/lib/text_menu.py b/chromite/lib/text_menu.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..9e034c7ebbc6e17f9296474ae506bce59bfb7d86
|
| --- /dev/null
|
| +++ b/chromite/lib/text_menu.py
|
| @@ -0,0 +1,194 @@
|
| +#!/usr/bin/python
|
| +# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +"""A module containing a function for presenting a text menu to the user."""
|
| +
|
| +import sys
|
| +
|
| +
|
| +def _CeilDiv(numerator, denominator):
|
| + """Do integer division, rounding up.
|
| +
|
| + >>> _CeilDiv(-1, 2)
|
| + 0
|
| + >>> _CeilDiv(0, 2)
|
| + 0
|
| + >>> _CeilDiv(1, 2)
|
| + 1
|
| + >>> _CeilDiv(2, 2)
|
| + 1
|
| + >>> _CeilDiv(3, 2)
|
| + 2
|
| +
|
| + Args:
|
| + numerator: The number to divide.
|
| + denominator: The number to divide by.
|
| +
|
| + Returns:
|
| + (numberator) / denominator, rounded up.
|
| + """
|
| + return (numerator + denominator - 1) // denominator
|
| +
|
| +
|
| +def _BuildMenuStr(items, title, prompt, menu_width, spacing, add_quit):
|
| + """Build the menu string for TextMenu.
|
| +
|
| + See TextMenu for a description. This function mostly exists to simplify
|
| + testing.
|
| +
|
| + >>> _BuildMenuStr(['A'], "Choose", "Choice", 76, 2, True)
|
| + 'Choose:\\n\\n1. A\\n\\nChoice (q to quit): '
|
| +
|
| + >>> _BuildMenuStr(['B', 'A'], "Choose", "Choice", 76, 2, False)
|
| + 'Choose:\\n\\n1. B 2. A\\n\\nChoice: '
|
| +
|
| + >>> _BuildMenuStr(['A', 'B', 'C', 'D', 'E'], "Choose", "Choice", 10, 2, False)
|
| + 'Choose:\\n\\n1. A 4. D\\n2. B 5. E\\n3. C\\n\\nChoice: '
|
| +
|
| + >>> _BuildMenuStr(['A', 'B', 'C', 'D', 'E'], "Choose", "Choice", 11, 2, False)
|
| + 'Choose:\\n\\n1. A 4. D\\n2. B 5. E\\n3. C\\n\\nChoice: '
|
| +
|
| + >>> _BuildMenuStr(['A', 'B', 'C'], "Choose", "Choice", 9, 2, False)
|
| + 'Choose:\\n\\n1. A\\n2. B\\n3. C\\n\\nChoice: '
|
| +
|
| + >>> _BuildMenuStr(['A'*10, 'B'*10], "Choose", "Choice", 0, 2, False)
|
| + 'Choose:\\n\\n1. AAAAAAAAAA\\n2. BBBBBBBBBB\\n\\nChoice: '
|
| +
|
| + >>> _BuildMenuStr([], "Choose", "Choice", 76, 2, False)
|
| + Traceback (most recent call last):
|
| + ...
|
| + ValueError: Can't build a menu of empty choices.
|
| +
|
| + Args:
|
| + items: See TextMenu().
|
| + title: See TextMenu().
|
| + prompt: See TextMenu().
|
| + menu_width: See TextMenu().
|
| + spacing: See TextMenu().
|
| + add_quit: See TextMenu().
|
| +
|
| + Returns:
|
| + See TextMenu().
|
| +
|
| + Raises:
|
| + ValueError: If no items.
|
| + """
|
| + if not items:
|
| + raise ValueError("Can't build a menu of empty choices.")
|
| +
|
| + # Figure out some basic stats about the items.
|
| + num_items = len(items)
|
| + longest_num = len(str(num_items))
|
| + longest_item = longest_num + len(". ") + max(len(item) for item in items)
|
| +
|
| + # Figure out number of rows / cols.
|
| + num_cols = max(1, (menu_width + spacing) // (longest_item + spacing))
|
| + num_rows = _CeilDiv(num_items, num_cols)
|
| +
|
| + # Construct "2D array" of lines. Remember that we go down first, then
|
| + # right. This seems to mimic "ls" behavior. Note that, unlike "ls", we
|
| + # currently make all columns have the same width. Shrinking small columns
|
| + # would be a nice optimization, but complicates the algorithm a bit.
|
| + lines = [[] for _ in xrange(num_rows)]
|
| + for item_num, item in enumerate(items):
|
| + row = item_num % num_rows
|
| + item_str = "%*d. %s" % (longest_num, item_num + 1, item)
|
| + lines[row].append("%-*s" % (longest_item, item_str))
|
| +
|
| + # Change lines from 2D array into 1D array (1 entry per row) by joining
|
| + # columns with spaces.
|
| + spaces = " " * spacing
|
| + lines = [spaces.join(line) for line in lines]
|
| +
|
| + # Add '(q to quit)' string to prompt if requested...
|
| + if add_quit:
|
| + prompt = "%s (q to quit)" % prompt
|
| +
|
| + # Make the final menu string by adding some return and the prompt.
|
| + menu_str = "%s:\n\n%s\n\n%s: " % (title, "\n".join(lines), prompt)
|
| + return menu_str
|
| +
|
| +
|
| +def TextMenu(items, title="Choose one", prompt="Choice",
|
| + menu_width=76, spacing=4, add_quit=True):
|
| + """Display text-based menu to the user and get back a response.
|
| +
|
| + The menu will be printed to sys.stderr and input will be read from sys.stdin.
|
| +
|
| + If the user doesn't want to choose something, he/she can use the 'q' to quit
|
| + or press Ctrl-C (which will be caught and treated the same).
|
| +
|
| + The menu will look something like this:
|
| + 1. __init__.py 3. chromite 5. lib 7. tests
|
| + 2. bin 4. chroot_specs 6. specs
|
| +
|
| + Choice (q to quit):
|
| +
|
| + Args:
|
| + items: The strings to show in the menu. These should be sorted in whatever
|
| + order you want to show to the user.
|
| + title: The title of the menu.
|
| + prompt: The prompt to show to the user.
|
| + menu_width: The maximum width to use for the menu; 0 forces things to single
|
| + column.
|
| + spacing: The spacing between items.
|
| + add_quit: Let the user type 'q' to quit the menu (we'll return None).
|
| +
|
| + Returns:
|
| + The index of the item chosen by the user. Note that this is a 0-based
|
| + index, even though the user is presented with the menu in 1-based format.
|
| + Will be None if the user hits Ctrl-C, or chooses q to quit.
|
| +
|
| + Raises:
|
| + ValueError: If no items.
|
| + """
|
| + # Call the helper to build the actual menu string.
|
| + menu_str = _BuildMenuStr(items, title, prompt, menu_width, spacing,
|
| + add_quit)
|
| +
|
| + # Loop until we get a valid input from the user (or they hit Ctrl-C, which
|
| + # will throw and exception).
|
| + while True:
|
| + # Write the menu to stderr, which makes it possible to use this with
|
| + # commands where you want the output redirected.
|
| + sys.stderr.write(menu_str)
|
| +
|
| + # Don't use a prompt with raw_input(), since that would go to stdout.
|
| + try:
|
| + result = raw_input()
|
| + except KeyboardInterrupt:
|
| + # Consider this a quit.
|
| + return None
|
| +
|
| + # Check for quit request
|
| + if add_quit and result.lower() in ("q", "quit"):
|
| + return None
|
| +
|
| + # Parse into a number and do error checking. If all good, return.
|
| + try:
|
| + result_int = int(result)
|
| + if 1 <= result_int <= len(items):
|
| + # Convert from 1-based to 0-based index!
|
| + return result_int - 1
|
| + else:
|
| + print >>sys.stderr, "\nERROR: %d out of range.\n\n" % result_int
|
| + except ValueError:
|
| + print >>sys.stderr, "\nERROR: '%s' is not a valid choice.\n\n" % result
|
| +
|
| +
|
| +def _Test():
|
| + """Run any built-in tests."""
|
| + import doctest
|
| + doctest.testmod(verbose=True)
|
| +
|
| +
|
| +# For testing purposes, you can run this on the command line...
|
| +if __name__ == "__main__":
|
| + # If first argument is --test, run testing code.
|
| + # ...otherwise, pass all arguments as the menu to show.
|
| + if sys.argv[1:2] == ["--test"]:
|
| + _Test(*sys.argv[2:])
|
| + else:
|
| + TextMenu(sys.argv[1:])
|
|
|