Skip to content

More Examples

select/select_multiple

Functionality

Return index

Selective elements default to return the selected item (in case of select) or list of items (in case of select_multiple). This behavior can be modified by return_index parameter (or return_indices in case of the latter), see example,

result_index = select(options=['I\'ll be returned as 0', 'I\'ll be returned as 1'],
                      return_index=True)

Starting cursor index

By default cursor is placed on the first element, this can be configured by cursor_index parameter as follows,

results = select(['Not here either', 'Not here', 'Start from here'],
                 cursor_index=1)

Preticked indices for select_multiple

You can have preticked options using ticked_indices in select_multiple:

loved_children = select_multiple(['Oldest child', 'Middle child', 'Youngest Child'],
                                 ticked_indices=[0,2])

Maximal and minimal count for select_multiple

With select_multiple you can restrict maximum and minimum count of elements using maximal_count and minimal_count respectively,

pizza_toppings = select_multiple(['pineapple', 'olives', 'anchovies', 'mozzarella', 'parma ham']
                                 maximal_count=3,
                                 minimal_count=1)

Styling

Tip

For styling you can leverage numerous styling options provided by rich

Style as text

stylish = select(options = ["red", "on", "white"],
                 cursor = "x",
                 cursor_style= "red on white")

Style as hex

selections = select_multiple(options = ["s", "h", "e", "", "b", "e", "l", "i", "e", "v", "e", "d"],
                             tick_style="#af00ff",
                             ticked_indices=[1,2,6,7,8,11])

Cursor characters

Emoji as a cursor

Bug

Some emojis can appear as one character instead of two!

result = select(options = ["here", "comes", "the", "sun"],
                cursor = "🌞")

Non-ascii as a cursor

result = select(options = ["hardcore", "unicode"],
                cursor = "⇉")

Multi-character cursors/ticks

Tip

You can use multiple characters as a cursor

correct_abba_lyric = select_multiple(options = ["queen", "bean"],
                                     tick_character = "dancing")

prompt

Functionality

You can have a default prompt, which will collect the user typed response to the mood variable as string

mood = prompt("How are you today?")

Validation

Additionally, you can validate the input using some sort of Callable validator, for example a lambda expression to make sure input is not numeric,

answer = prompt(prompt="What is the answer to life the universe and everything?"
                validator=lambda val: not val.isnumeric())

Type Conversion

You might want to convert types for some sort of downstream functionality using target_type,

Note

Validation is always second to type conversion

number_between_1_and_10 = prompt("Give me a number between 1 and 10",
                                 target_type=int
                                 validator=lambda n: 0 < n <= 10)

Hidden/secure input

For sensitive input, secure flag can be utilized, replacing user entered input with *

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.

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

A more complex example with path completion:

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

Everything after the first argument (the animation) is keyword-only. A spinner can be driven by hand with start()/stop(), or used as a context manager, which also stops it if the body raises:

from beaupy.spinners import Spinner, DOTS

with Spinner(DOTS, text="Packing things..."):
    do_some_work()

To render on the same console as the other elements (e.g. a console you pass through Config), pass console=:

Spinner(DOTS, text="Packing things...", console=my_console)

Styling

Spinner Animation

There are few built in spinner animations, namely: ARC, ARROWS, BARS, CLOCK, DIAMOND, DOT, DOTS, LINE, LOADING and MOON

Each of these can be used in a spinner:

from beaupy.spinners import Spinner, ARC
spinner = Spinner(ARC, text="Doing some heavy work")
spinner.start()

All that "animations" are, is but a list of string, so making your own is as trivial as this:

from beaupy.spinners import Spinner
spinner = Spinner(['whee', 'whe ', 'wh  ', 'w   ', 'wh  ', 'whe ', 'whee'], text="Whee!")
spinner.start()

Rich styling

Every text in spinner does accept and respect rich styles, so the following works:

from beaupy.spinners import Spinner
spinner = Spinner(['[red]⬤[/red] ', '[green]⬤[/green] ', '[blue]⬤[/blue] '], text='[pink1]Setting[/pink1] colors!')
spinner.start()

Animation speed

Animation speed can be set using refresh_per_second parameter:

from beaupy.spinners import Spinner, LOADING
spinner = Spinner(LOADING, text="something", refresh_per_second=4)
spinner.start()

Configuration

Each element accepts a config argument taking a Config instance. Options:

  • raise_on_interrupt: If True, functions will raise KeyboardInterrupt whenever Ctrl+C is pressed when waiting for input, otherwise, they will return some sane alternative to their usual return. For select, prompt and confirm this means None, while for select_multiple it means an empty list - []. Defaults to True.
  • raise_on_escape: If True, functions will raise Abort whenever the escape key is encountered when waiting for input, otherwise, they will return some sane alternative to their usual return. For select, prompt and confirm this means None, while for select_multiple it means an empty list - []. Defaults to False.
  • transient: If False, elements will remain displayed after their context has ended. Defaults to True.
  • console: rich.console.Console the elements render to. Defaults to Console(stderr=True, highlight=False).
  • keys: KeyBindings used by the elements. Defaults to KeyBindings().
from beaupy import Config, select

# Ctrl+C raises KeyboardInterrupt by default; opt out to get `None` back instead
result = select(['Option 1', 'Option 2'], config=Config(raise_on_interrupt=False))
if result is None:
    print("User pressed Ctrl+C")

Using your own console

Pass the console you already use, e.g. the one of a running rich.live.Live, so the two don't fight over the terminal:

from rich.console import Console
from rich.live import Live
from rich.panel import Panel
from beaupy import Config, select_multiple

console = Console()
with Live(Panel("Foo"), console=console):
    select_multiple(list(range(15)), config=Config(console=console))

Keybindings

from beaupy import Config, KeyBindings, select

vim = Config(keys=KeyBindings(up=['k'], down=['j']))
select(['one', 'two'], config=vim)

Title, instructions and sections

select and select_multiple take a title shown above the options. All elements take instructions shown below; pass None to hide it.

select(['red', 'green'], title='Pick a color', instructions='([bold]enter[/bold] to pick)')

Pass a dict to show options in sections. Indices (cursor_index, ticked_indices, return_index, return_indices) are then always (section_name, index_within_section) tuples; a plain int raises TypeError (and is flagged by type checkers):

select_multiple({'Fruit': ['apple', 'pear'], 'Veg': ['leek']},
                ticked_indices=[('Veg', 0)],
                return_indices=True)  # e.g. [('Veg', 0), ('Fruit', 1)]

Filtering

With filterable=True, typing narrows the options to those whose displayed text contains what was typed (case-insensitive, markup ignored). Backspace removes the last typed character; the query is shown next to the title, along with a (matches/total) count.

select(['apple', 'banana', 'cherry'], title='Fruit', filterable=True)

Keys bound in KeyBindings keep their action, so in select_multiple space still ticks and ctrl+a ticks/unticks all visible options.

Ticked options stay ticked while hidden by the filter.

Pagination

Options that don't fit in the terminal are paginated automatically, so there is nothing to turn on. To choose the page size yourself, pass page_size — options are then always paginated with that many per page:

select(many_options, page_size=10)

Input validation

Empty options raises ValueError by default; pass strict=False to get None (select) or [] (select_multiple) back instead:

select(options=[])                 # raises ValueError
select(options=[], strict=False)   # returns None

options itself must be a sequence (or a dict of sections), never a bare string: select('abc') raises TypeError instead of silently offering the letters.

A blank option (after preprocessing and stripping markup, e.g. '', ' ', or '[red][/red]') or a blank section name always raises ValueError, regardless of strict — there's no legitimate reason to want a menu row with nothing in it.

confirm applies the same rule to yes_text/no_text, and also raises if both labels start with the same letter while the (Y/N) hint is shown (pass char_prompt=False or use different labels).

All elements read keypresses from the terminal, so they need an interactive one. When stdin isn't a TTY (piped input, most CI runners), they raise RuntimeError('Interactive terminal required').

Migrating from 3.x

4.0.0 removes process-wide state and makes every argument after the first keyword-only.

3.x 4.x
Config.raise_on_escape = True select(..., config=Config(raise_on_escape=True))
beaupy._beaupy.console = my_console select(..., config=Config(console=my_console))
DefaultKeys.up.append('k') select(..., config=Config(keys=KeyBindings(up=[Keys.UP_ARROW, 'k'])))
select(options, my_preprocessor) select(options, preprocessor=my_preprocessor)
a ticks/unticks all in select_multiple ctrl+a; restore with KeyBindings(select_all=['a'])
Spinner(DOTS, "text") Spinner(DOTS, text="text") (options are keyword-only); also usable as with Spinner(...):
select(options, pagination=True, page_size=5) select(options, page_size=5) — pagination is gone; page_size alone paginates, and with neither set options are paginated only when they don't fit the terminal
Ctrl+C returns None/[] (raise_on_interrupt=False) Raises KeyboardInterrupt; pass Config(raise_on_interrupt=False) for the old behavior
strict defaults to False (empty options returns None/[]) Defaults to True (raises ValueError); pass strict=False for the old behavior

beaupy now needs Rich 14.3 or newer (to measure emoji cursors and ticks correctly) and no longer depends on emoji.

ValidationError and ConversionError (from prompt) are now ValueError subclasses, so except ValueError catches them too.

Keys above comes from yakh.key. Using a removed global raises RemovedInV4Error (or AttributeError when assigning on Config/KeyBindings) with a message explaining the replacement.