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"],
+)