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.37k stars 716 forks source link

Let the multiline filter have access to the prompt text #1132

Open erezsh opened 4 years ago

erezsh commented 4 years ago

Right now there's no simple way to decide whether Enter should accept or open a new line based on the content of the text.

It seems like a very useful feature in general. For example, to write something like ptpython (that doesn't seem to use this feature)

Please correct me if I'm wrong about that.

Also, right now is_true(self.multiline) is called about 10 times for each character pressed. You really only need to call it once.

jonathanslenders commented 4 years ago

There are several ways right now to achieve this:

  1. Install one key binding for the enter key, but without filter, that handles both cases. Then in the key binding check whether it's multiline or not and act accordingly.

  2. get_app always returns the currently curring application. From there you can get the buffer named DEFAULT_BUFFER.

    text = get_app().layout.get_buffer_by_name('DEFAULT_BUFFER').text

    You can create a new filter that uses this and according to this value evaluates to true or false.

  3. Expose the PromptSession into the scope where the key bindings are created. Something like this:

    def main():
    kb = Keybindings()
    @kb.add('enter')
    def handle_enter(event):
        print(prompt_session.default_buffer.text)
    prompt_session = PromptSession(key_bindings=kb...)
    prompt_session.prompt()
erezsh commented 4 years ago

Thanks! One more question (I searched the docs but couldn't find it)

In option 3, How do I tell the event to continue as usual?

jonathanslenders commented 4 years ago

Good question!

Right now, we have two options, I think:

  1. Create a filter that activates this key binding only if the "usual" binding should never be executed. Of course, the filter is not supposed to change state. It should only observe state. This means that you can't add functionality to an existing key binding, this way.

  2. Call the original key binding from within the new key binding, passing along the event object. For PromptSession, the function for the enter binding is not exposed publicly, so you can't call that function. In this case however, the implementation is only one line: self.default_buffer.validate_and_handle(). If we add that to the above snippet, this becomes:

from prompt_toolkit.filters import has_focus
from prompt_toolkit.enums import DEFAULT_BUFFER

def main():
    kb = Keybindings()
    @kb.add('enter', filter=has_focus(DEFAULT_BUFFER))
    def handle_enter(event):
        # Your stuff.
        print(prompt_session.default_buffer.text)
        # Call the original handler.
        prompt_session.default_buffer.validate_and_handle()

    prompt_session = PromptSession(key_bindings=kb...)
    prompt_session.prompt()

I think it would be a nice addition to prompt_toolkit, to do stuff like:

event.call_other_handlers()

Something which is similar to a super() call. I'll think about that.

erezsh commented 4 years ago

Yes, I believe that would be helpful. Javascript goes the other way around, always calling super() unless you call event.stopPropagation(). Might be too much, but it's an indicator that it's a common pattern in ui.