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

Prompt that reads/processes chars as they are fed to stdin? [Question] #1109

Open Datamance opened 4 years ago

Datamance commented 4 years ago

Hi there,

I've started building something in the vein of jq/jiq, but for HTML, and you can see a demo of it in this asciicast: asciicast.

Right now, I'm doing the input processing and display parts with raw ANSI codes and mostly leveraging termios/tty/os. This has proven quite fiddly; and prompt_toolkit seems pretty mature/has a whole other bunch of nice things, but I would need this one critical feature of character-by-character reading from stdin.

Does prompt_toolkit support this?

jonathanslenders commented 4 years ago

Absolutely, I think something like this should do the job:

#!/usr/bin/env python
from prompt_toolkit.application import Application, get_app
from prompt_toolkit.document import Document
from prompt_toolkit.filters import has_focus
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout.containers import HSplit, Window
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit.styles import Style
from prompt_toolkit.widgets import SearchToolbar, TextArea

def main():
    output_field = TextArea(style="class:output-field")
    input_field = TextArea(
        height=1,
        prompt=">>> ",
        style="class:input-field",
        multiline=False,
        wrap_lines=False,
    )

    container = HSplit(
        [
            input_field,
            Window(height=1, char="-", style="class:line"),
            output_field,
        ]
    )

    # Attach accept handler to the input field. We do this by assigning the
    # handler to the `TextArea` that we created earlier. it is also possible to
    # pass it to the constructor of `TextArea`.
    # NOTE: It's better to assign an `accept_handler`, rather then adding a
    #       custom ENTER key binding. This will automatically reset the input
    #       field and add the strings to the history.
    def accept(buff):
        output_field.text = ''
        get_app().exit(result=buff.text)

    input_field.accept_handler = accept

    def input_field_changed(_):
        output_field.text += 'some output...\n'  #·

    input_field.buffer.on_text_changed += input_field_changed

    # The key bindings.
    kb = KeyBindings()

    @kb.add("c-c")
    @kb.add("c-q")
    def _(event):
        " Pressing Ctrl-Q or Ctrl-C will exit the user interface. "
        event.app.exit()

    # Style.
    style = Style.from_dict({
        "output-field": "#888888",
        "input-field": "bold",
        "line": "#004400",
    })

    # Run application.
    application = Application(
        layout=Layout(container, focused_element=input_field),
        key_bindings=kb,
        style=style,
        full_screen=False,
    )

    result = application.run()
    print('Got result:', result)

if __name__ == "__main__":
    main()
jonathanslenders commented 4 years ago

Probably, you also want to use the always_use_tty option, so that we can still read input from the terminal when other data is piped in:

from prompt_toolkit.input.defaults import create_input
...
application = Application(
     ...
    input=create_input(always_prefer_tty=True)
)
...