diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 3b02c5c..7f46130 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -25,12 +25,13 @@ - [Using `YesNo` Object](#topic_10) - [Using `Password` Object](#topic_11) - [Using `Numbers` Object](#topic_12) - - [Using Prompt Objects](#topic_13) - - [Using `VerticalPrompt` Object](#topic_14) - - [Using `SlidePrompt` Object](#topic_15) - - [Using `ScrollBar` Object](#topic_16) -- [More Customization: Extending Existing Prompts](#topic_17) - - [A List of Default Keyboard Events](#topic_18) + - [Using `Date` Object](#topic_13) + - [Using Prompt Objects](#topic_14) + - [Using `VerticalPrompt` Object](#topic_15) + - [Using `SlidePrompt` Object](#topic_16) + - [Using `ScrollBar` Object](#topic_17) +- [More Customization: Extending Existing Prompts](#topic_18) + - [A List of Default Keyboard Events](#topic_19) # General @@ -144,10 +145,17 @@ client = Bullet(**styles.Greece) - Non-numeric values will be guarded, and the user will be asked to re-enter. - Define `type` to cast return value. For example, `type = float`, will cast return value to `float`. -## ⌨️ Using `Prompt` Objects +## ⌨️ Using `Date` Objects +> Enter date values +- Values will be [parsed with `dateutil.parser`](https://dateutil.readthedocs.io/en/stable/parser.html), which is capable of handling strings in many different formats (e.g., "2020-8-21", "08/21/2020", or "Aug 21 2020" would all be parsed as the same date). +- If the value provided cannot be parsed, the user will be asked to re-enter. +- Returns a `datetime.date` object +- `format_str: str`: [Format string](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes) used to display default value, defaults to `%m/%d/%Y` + +## ⌨️ Using `Prompt` Objects > Wrapping it all up. -### Using `VerticalPrompt` Object +### Using `VerticalPrompt` Object - Stack `bullet` UI components into one vertically-rendered prompt. - Returns a list of tuples `(prompt, result)`. - `spacing`: number of lines between adjacent UI components. @@ -169,20 +177,20 @@ cli = VerticalPrompt( result = cli.launch() ``` -### Using `SlidePrompt` Object +### Using `SlidePrompt` Object - Link `bullet` UI components into a multi-stage prompt. Previous prompts will be cleared upon entering the next stage. - Returns a list of tuples `(prompt, result)`. > For `Prompt` ojects, call `summarize()` after launching the prompt to print out user input. -## ⌨️ Using `ScrollBar` Object +## ⌨️ Using `ScrollBar` Object > **Enhanced `Bullet`**: Too many items? It's OK! - `pointer`: points to item currently selected. - `up_indicator`, `down_indicator`: indicators shown in first and last row of the rendered items. - `height`: maximum items rendered on terminal. - For example, your can have 100 choices (`len(choices) = 100`) but define `height = 5`. -# More Customization: Extending Existing Prompts +# More Customization: Extending Existing Prompts > See `./examples/check.py` for the big picture of what's going on. In `bullet`, you can easily inherit a base class (existing `bullet` objects) and create your customized prompt. This is done by introducing the `keyhandler` module to register user-defined keyboard events. @@ -196,22 +204,22 @@ def accept(self): # do some validation checks: chosen items >= 1 and <= 3. ``` Note that `accept()` is the method for **all** prompts to return user input. The binded keyboard event by default is `NEWLINE_KEY` pressed. -## A List of Default Keyboard Events +## A List of Default Keyboard Events > See `./bullet/charDef.py` - `LINE_BEGIN_KEY` : Ctrl + H -- `LINE_END_KEY`: Ctrl + E -- `TAB_KEY` +- `LINE_END_KEY`: Ctrl + E +- `TAB_KEY` - `NEWLINE_KEY`: Enter -- `ESC_KEY` -- `BACK_SPACE_KEY` -- `ARROW_UP_KEY` -- `ARROW_DOWN_KEY` -- `ARROW_RIGHT_KEY` -- `ARROW_LEFT_KEY` -- `INSERT_KEY` -- `DELETE_KEY` -- `END_KEY` -- `PG_UP_KEY` -- `PG_DOWN_KEY` +- `ESC_KEY` +- `BACK_SPACE_KEY` +- `ARROW_UP_KEY` +- `ARROW_DOWN_KEY` +- `ARROW_RIGHT_KEY` +- `ARROW_LEFT_KEY` +- `INSERT_KEY` +- `DELETE_KEY` +- `END_KEY` +- `PG_UP_KEY` +- `PG_DOWN_KEY` - `SPACE_CHAR` -- `INTERRUPT_KEY`: Ctrl + C \ No newline at end of file +- `INTERRUPT_KEY`: Ctrl + C diff --git a/bullet/__init__.py b/bullet/__init__.py index 708138b..420690a 100644 --- a/bullet/__init__.py +++ b/bullet/__init__.py @@ -6,4 +6,5 @@ from .client import Numbers from .client import VerticalPrompt from .client import SlidePrompt -from .client import ScrollBar \ No newline at end of file +from .client import ScrollBar +from .client import Date diff --git a/bullet/client.py b/bullet/client.py index be3b061..f29bba9 100644 --- a/bullet/client.py +++ b/bullet/client.py @@ -1,5 +1,10 @@ import sys +from datetime import date + +from dateutil import parser as date_parser + from .charDef import * +from .wrap_text import wrap_text from . import colors from . import utils from . import cursor @@ -733,3 +738,50 @@ def launch(self): utils.clearConsoleUp(d + 1) utils.moveCursorDown(1) return self.result + +class Date(Input): + ''' Prompt user for a `date` value until successfully parsed. + + String provided by user can be provided in any format recognized by + `dateutil.parser`. + + Args: + prompt (str): Required. Text to display to user before input prompt. + default (date): Optional. Default `date` value if user provides no + input. + format_str (str): Format string used to display default value, + defaults to '%m/%d/%Y' + indent (int): Distance between left-boundary and start of prompt. + word_color (str): Optional. The color of the prompt and user input. + ''' + + def __init__( + self, + prompt: str, + default: date = None, + format_str: str = "%m/%d/%Y", + indent: int = 0, + word_color: str = colors.foreground["default"], + ): + if default: + default = default.strftime(format_str) + super().__init__(prompt, default=default, indent=indent, word_color=word_color) + + def launch(self): + while True: + result = super().launch() + if not result: + continue + try: + date = date_parser.parse(result) + return date.date() + except ValueError: + error = f"Error! '{result}' could not be parsed as a valid date.\n" + help = ( + "You can use any format recognized by dateutil.parser. For example, all of " + "the strings below are valid ways to represent the same date:\n" + ) + examples = '\n"2018-5-13" -or- "05/13/2018" -or- "May 13 2018"\n' + utils.cprint(error, color=colors.bright(colors.foreground["red"])) + utils.cprint(wrap_text(help, max_len=70), color=colors.foreground["red"]) + utils.cprint(examples, color=colors.foreground["red"]) diff --git a/bullet/wrap_text.py b/bullet/wrap_text.py new file mode 100644 index 0000000..28f3f7e --- /dev/null +++ b/bullet/wrap_text.py @@ -0,0 +1,35 @@ +import re + +WORD_REGEX = re.compile(r"\s?(?P\b\w+\b)\s?") + + +def wrap_text(s: str, max_len: int): + ''' Wrap text at word boundaries. + + Args: + s (str): The string to be wrapped. + max_len (int): Maximum length of each substring + Returns: + str: Multiline string where the length of each line is less than or + equal to `max_len`. Wrapping will not occur in the middle of a + word for prettier output. + ''' + substrings = [] + while True: + if len(s) <= max_len: + substrings.append(s) + break + (wrapped, s) = _wrap_string(s, max_len) + substrings.append(wrapped) + return "\n".join(substrings) + + +def _wrap_string(s, max_len): + last_word_boundary = max_len + for match in WORD_REGEX.finditer(s): + if match.end("word") > max_len: + break + last_word_boundary = match.end("word") + 1 + wrapped = s[:last_word_boundary] + s = s[last_word_boundary:].strip() + return (wrapped, s) diff --git a/setup.py b/setup.py index fae0e27..a0159aa 100644 --- a/setup.py +++ b/setup.py @@ -11,5 +11,6 @@ author='Mckinsey666', license='MIT', packages=find_packages(), - python_requires=">=3.6" -) \ No newline at end of file + python_requires=">=3.6", + install_requires=["python-dateutil"], +)