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.29k stars 715 forks source link

Long-running bottom_toolbar callback makes program impossible to interrupt #862

Open RichardWarfield opened 5 years ago

RichardWarfield commented 5 years ago

I'm working on a front end to Haskell's GHCI based on prompt-toolkit, have noticed the following behavior.

If a call to the bottom_toolbar handler takes a long time, it seems that the application becomes entirely unresponsive and impossible to interrupt, even with Ctrl-C or Ctrl-Z (suspend).

By attaching a debugger externally I've determined that the issue seems to be that prompt-toolkit has the icrnl tty attribute disabled during the call to the handler. This prevents the tty from treating those Ctrl- combinations as expected.

Here's a simple reproducing example:

import prompt_toolkit
import time

def bottom_toolbar():
    time.sleep(10)

if __name__=='__main__':
    prompt_toolkit.shortcuts.prompt(bottom_toolbar=bottom_toolbar)

On a related note, it would be great if there were a way to have bottom_toolbar called asynchronously. My use case if that I'd like to display the type of the expression underlying the cursor in the bottom_toolbar, which involves a (sometimes) costly call to the Haskell interpreter backend.

Using prompt-toolkit 2.0.9 on Arch Linux with Python 3.7.

jonathanslenders commented 5 years ago

Hi Richard,

That sounds like a nice project!

What you experience is expected. Prompt_toolkit has an event-loop based architecture, which means that any blocking call blocks the application (similar to asyncio). The solution would be to run the expensive code in a thread. From within the thread, you can invalidate the UI when done, so that bottom_toolbar is called again. Something like the code below.

Make sure not to start a new thread, if there's already one running. The if not bottom_text probably has to be replaced with a condition that determines when it has to recompute.

from prompt_toolkit.application import get_app
import prompt_toolkit
import threading
import time

bottom_text = ''
bottom_thread_running = False

def bottom_toolbar():
    global bottom_thread_running

    if not bottom_text and not bottom_thread_running:
        bottom_thread_running = True
        threading.Thread(target=bottom_toolbar_async).start()
    return bottom_text

def bottom_toolbar_async():
    global bottom_text, bottom_thread_running
    time.sleep(5)
    bottom_text = 'new text'
    bottom_thread_running = False
    get_app().invalidate()

if __name__=='__main__':
    prompt_toolkit.shortcuts.prompt(bottom_toolbar=bottom_toolbar)

My (long term) plan is to upgrade prompt_toolkit to use asyncio natively. And then we can also consider coroutines for the bottom toolbar.

Hope that helps.

RichardWarfield commented 5 years ago

Thanks Jonathan for the prompt (pun not intended) reply and for putting together that code sample. I'll try it out.

It makes sense that the application should block during a long bottom_toolbar execution. But is it really expected that it should be impossible to interrupt/suspend with Ctrl-C/Ctrl-Z? It seems to me that perhaps the setting of icrnl should be restored before calling the bottom_toolbar callback. Perhaps using the cooked_mode class in vt100.py?