Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Date prompt #74

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 34 additions & 26 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<a name="topic_13"></a>
## ⌨️ Using `Date` Objects<a name="topic_13"></a>
> 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<a name="topic_14"></a>
> Wrapping it all up.

### Using `VerticalPrompt` Object<a name="topic_14"></a>
### Using `VerticalPrompt` Object<a name="topic_15"></a>
- Stack `bullet` UI components into one vertically-rendered prompt.
- Returns a list of tuples `(prompt, result)`.
- `spacing`: number of lines between adjacent UI components.
Expand All @@ -169,20 +177,20 @@ cli = VerticalPrompt(
result = cli.launch()
```

### Using `SlidePrompt` Object<a name="topic_15"></a>
### Using `SlidePrompt` Object<a name="topic_16"></a>
- 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<a name="topic_16"></a>
## ⌨️ Using `ScrollBar` Object<a name="topic_17"></a>
> **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<a name="topic_17"></a>
# More Customization: Extending Existing Prompts<a name="topic_18"></a>
> 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.
Expand All @@ -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 name="topic_18"></a>
## A List of Default Keyboard Events<a name="topic_19"></a>
> 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
- `INTERRUPT_KEY`: Ctrl + C
3 changes: 2 additions & 1 deletion bullet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
from .client import Numbers
from .client import VerticalPrompt
from .client import SlidePrompt
from .client import ScrollBar
from .client import ScrollBar
from .client import Date
52 changes: 52 additions & 0 deletions bullet/client.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"])
35 changes: 35 additions & 0 deletions bullet/wrap_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import re

WORD_REGEX = re.compile(r"\s?(?P<word>\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)
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
author='Mckinsey666',
license='MIT',
packages=find_packages(),
python_requires=">=3.6"
)
python_requires=">=3.6",
install_requires=["python-dateutil"],
)