prompt-toolkit / python-prompt-toolkit

Library for building powerful interactive command line applications in Python
https://python-prompt-toolkit.readthedocs.io/
BSD 3-Clause "New" or "Revised" License
9.28k stars 715 forks source link

Print warnings from validators #1197

Open maingoh opened 4 years ago

maingoh commented 4 years ago

I would like to print a warning from validator.validate(), using a different color/style. This warning would not be blocking for the user. I tried to dig a bit into the code but couldn't find how to do it, is it easily doable ?

Zylvian commented 4 years ago

Not be blocking as in that they could finish the message whilst the warning was there, or just as in being of small size?

Anyhow, if you want non-blocking dynamic messages, toolbars are the way to go: https://python-prompt-toolkit.readthedocs.io/en/master/pages/asking_for_input.html#adding-a-bottom-toolbar

maingoh commented 4 years ago

Not be blocking as in that they could finish the message whilst the warning was there, or just as in being of small size?

They could finish and validate the input. Basically I would like to print something from the validator because it is triggered while typing, but in the bottom (so either in the validator section or bottom toolbar).

I can probably change the style and raise a ValidationError but it will be blocking. About the toolbar, can I edit it or add it from the document ? Could we dynamically add a new toolbar if one is already existing ?

Zylvian commented 4 years ago

When a function is given, it will be called every time the prompt is rendered, so the bottom toolbar can be used to display dynamic information.

maingoh commented 4 years ago

Oh, my bad I misread ! Thank you :)

What is the cleanest way to do so, can I share the state between the validator and the toolbar function without using a global variable ? Maybe we could pass the document to the toolbar function and in some way share some user data ?

Zylvian commented 4 years ago

Hi.

Try this:

from prompt_toolkit.application import get_app
from prompt_toolkit import prompt, HTML

def example_callable():
 document = get_app().current_buffer.document
 currline = document.current_line 
 return HTML(f'Current line: <b><style bg="ansired">{currline}</style></b>!')

text = prompt('> ', bottom_toolbar=example_callable)
print(text)
maingoh commented 4 years ago

Oh thank you, I didn't see about the get_app function, maybe we could add an example like this in the documentation in Asking for input section to show how to access the current text from anywhere.

One more question :smile: how can I dynamically change the style of the toolbar ? I would like that sometimes it get's hidden (nothing to display, so no white background) and sometimes the background (foreground since it is reversed) get changed. But it seems the style for the bottom-toolbar class is static (can only be send from the prompt function), can we do it from the callable ?

If I try to change the style in HTML, it only changes the background of the text but not of the full row/line.

maingoh commented 4 years ago

I managed to do what I wanted, the key was to disable the reverse style of the toolbar at the beginning:

from prompt_toolkit.application import get_app
from prompt_toolkit import prompt, HTML
from prompt_toolkit.styles import Style

def toolbar_callable():
    document = get_app().current_buffer.document
    currline = document.current_line
    if currline == 'bad':
        return HTML('<b bg="orange">WARNING: you are doing bad !</b>')
    return ''  # Nothing to display, everything is fine

normal_style = Style.from_dict({
    'bottom-toolbar': 'noreverse',
})

prompt("How are you doing: ", bottom_toolbar=toolbar_callable,
       style=normal_style)

Also I managed to pass variables to the toolbar_callable using functools.partial.

This is however not so intuitive for someone new to the library. It would be very helpful to have a more dynamic way to update styles from anywere, a bit like in javascript using selectors.

Something like:

def toolbar_callable():
    app = get_app()
    # select a container from the attribute
    toolbar = app.select_by_class('bottom-toolbar')[0]
    toolbar.set_style('fg: red bg: white')  # add a style on the toolbar or edit directly the toolbar
    # or updating the style for all containers using the class attribute
    app.styles.update('bottom-toolbar', 'fg: red bg: black')

This is probably quite a lot of work but would be nice no ?