Skip to content

Commit

Permalink
Merge pull request #61 from petereon/prompt_completion
Browse files Browse the repository at this point in the history
feat: completion callable
  • Loading branch information
petereon authored Sep 24, 2023
2 parents 4a62069 + 278863c commit 5c1b665
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 9 deletions.
27 changes: 24 additions & 3 deletions beaupy/_beaupy.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import math
import warnings
from itertools import cycle
from typing import Any, Callable, List, Optional, Tuple, Type, Union

from rich.console import Console
Expand Down Expand Up @@ -128,6 +129,7 @@ def prompt(
raise_validation_fail: bool = True,
raise_type_conversion_fail: bool = True,
initial_value: Optional[str] = None,
completion: Optional[Callable[[str], List[str]]] = None,
) -> TargetType:
"""Function that prompts the user for written input
Expand Down Expand Up @@ -155,11 +157,29 @@ def prompt(
value: List[str] = [*initial_value] if initial_value else []
cursor_index = len(initial_value) if initial_value else 0
error: str = ''
completion_context = False
completion_options: List[str] = []
while True:
rendered = _render_prompt(secure, value, prompt, cursor_index, error)
rendered = _render_prompt(secure, value, prompt, cursor_index, error, completion_options)
error = ''
_update_rendered(live, rendered)
keypress = get_key()
if keypress in DefaultKeys.tab:
if completion:
if not completion_context:
completion_options = completion(''.join(value))
completion_options_iter = cycle(completion_options)
if completion_options:
completion_context = True

if completion_context:
value = [*next(completion_options_iter)]
cursor_index = len(value)
else:
completion_context = False
else:
completion_context = False

if keypress in DefaultKeys.interrupt:
if Config.raise_on_interrupt:
raise KeyboardInterrupt()
Expand Down Expand Up @@ -204,8 +224,9 @@ def prompt(
raise Abort(keypress)
return None
else:
value.insert(cursor_index, str(keypress))
cursor_index += 1
if not (keypress in DefaultKeys.tab and completion_context):
value.insert(cursor_index, str(keypress))
cursor_index += 1


def select(
Expand Down
18 changes: 13 additions & 5 deletions beaupy/_internals.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,26 @@ def _update_rendered(live: Live, renderable: Union[ConsoleRenderable, str]) -> N
live.refresh()


def _render_prompt(secure: bool, typed_values: List[str], prompt: str, cursor_position: int, error: str) -> str:
render_value = (len(typed_values) * '*' if secure else ''.join(typed_values)) + ' '
def _render_prompt(
secure: bool, typed_values: List[str], prompt: str, cursor_position: int, error: str, completion_options: List[str] = []
) -> str:
input_value = len(typed_values) * '*' if secure else ''.join(typed_values)
render_value = (
render_value[:cursor_position]
(input_value + ' ')[:cursor_position]
+ '[black on white]' # noqa: W503
+ render_value[cursor_position] # noqa: W503
+ (input_value + ' ')[cursor_position] # noqa: W503
+ '[/black on white]' # noqa: W503
+ render_value[(cursor_position + 1) :] # noqa: W503,E203
+ (input_value + ' ')[(cursor_position + 1) :] # noqa: W503,E203
)

if completion_options and not secure:
rendered_completion_options = ' '.join(completion_options).replace(input_value, f'[black on white]{input_value}[/black on white]')
render_value = f'{render_value}\n{rendered_completion_options}'

render_value = f'{prompt}\n> {render_value}\n\n(Confirm with [bold]enter[/bold])'
if error:
render_value = f'{render_value}\n[red]Error:[/red] {error}'

return render_value


Expand Down
39 changes: 39 additions & 0 deletions docs/content/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,45 @@ very_secret_info = prompt("Type you API key, hehe",
secure=True)
```

#### Completion

You can provide a python callable such as `Callable[[str], List[str]]` to provide completion options. String passed to the callable is the current user input.

```python
favorite_color = prompt("What is your favorite color?",
completion=lambda _: ["pink", "PINK", "P1NK"])
```

A more complex example with path completion:

```python
from os import listdir
from pathlib import Path

# ugly hacky path completion callable:
def path_completion(str_path: str = ""):
if not str_path:
return []
try:
path = Path(str_path)
rest = ''
if not path.exists():
str_path, rest = str_path.rsplit('/', 1)
path = Path(str_path or '/')

filtered_list_dir = [i for i in listdir(path) if i.startswith(rest)]

if not path.is_absolute():
return ['./'+str(Path(path)/i) for i in filtered_list_dir]
else:
return [str(Path(path)/i) for i in filtered_list_dir]
except Exception as e:
return []

prompt(">", completion=path_completion)
```


## Spinners

### Styling
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = 'beaupy'
version = '3.5.4'
version = '3.6.0'
description = 'A library of elements for interactive TUIs in Python'
authors = ['Peter Vyboch <[email protected]>']
license = 'MIT'
Expand Down
26 changes: 26 additions & 0 deletions test/beaupy_prompt_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,29 @@ def _(set_raise_on_escape=set_raise_on_escape):
with raises(Abort) as e:
prompt(prompt="Try test")
assert str(e.raised) == "Aborted by user with key (27,)"

@test("Verify that completion works")
def _():
steps = iter([Keys.TAB, Keys.TAB, Keys.ENTER])

b.get_key = lambda: next(steps)
res = prompt(prompt="Try test", completion=lambda _: ["Hello", "World"])
assert res == "World"

@test("Verify that completion renders properly")
def _():
steps = iter([Keys.TAB, Keys.TAB, Keys.TAB, Keys.ENTER])

Live.update = mock.MagicMock()
b.get_key = lambda: next(steps)
res = prompt(prompt="Try test", completion=lambda _: ["Hello", "World"])

assert Live.update.call_args_list == [
mock.call(renderable='Try test\n> [black on white] [/black on white]\n\n(Confirm with [bold]enter[/bold])'),
mock.call(renderable='Try test\n> Hello[black on white] [/black on white]\n[black on white]Hello[/black on white] World\n\n(Confirm with [bold]enter[/bold])'),
mock.call(renderable='Try test\n> World[black on white] [/black on white]\nHello [black on white]World[/black on white]\n\n(Confirm with [bold]enter[/bold])'),
mock.call(renderable='Try test\n> Hello[black on white] [/black on white]\n[black on white]Hello[/black on white] World\n\n(Confirm with [bold]enter[/bold])'),
]


assert res == "Hello"

0 comments on commit 5c1b665

Please sign in to comment.