Auto-refresh of prompt-toolkit fullscreen app

496 Views Asked by At

I'm trying to get a bit into prompt-toolkit for building CLI apps. Maybe it's a trivial question, but I could not find any solution in the docs or in other posts.

I want to build a full-screen app with prompt-toolkit, that monitors and displays periodically changing information. As a demonstrator, I just wanted to display the current date and time, and have it update every second.

However, I could not yet find any way for a prompt-toolkit app to update itself without user input. I assume, I need to add some callback somewhere, but the documentation is not very clear on that and I have not found a good example yet.


Update 1: After some more trial&error I found that the following code produces the expected result, although I'm still not a 100% sure if this is the best way of doing it.

With app.create_background_task() one can add coroutines that to the event loop. In this example I update the text in the upper part of the window. However, this does not do anything, unless the user manually refreshes the app (I think with app.invalidate()) OR by providing the refresh argument in Application.

I think this solution is sufficient unless too much other stuff is happening so that the refresh-rate cannot be maintained..

import datetime
import asyncio

from prompt_toolkit import Application
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout.containers import Window, HSplit
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit import widgets


static_text = FormattedTextControl(text=f"Time: {datetime.datetime.now().strftime('%H:%M:%S')}")
static_window = Window(content=static_text, height=2)

telemetry_window = Window(content=FormattedTextControl(text="Some fixed text"))

root_container = HSplit([
    widgets.Frame(body=static_window),
    widgets.Frame(body=telemetry_window)
])

layout = Layout(root_container)

kb = KeyBindings()


@kb.add('c-q')
@kb.add('c-c')
def exit_(event):
    event.app.exit()


app = Application(layout=layout, full_screen=True, key_bindings=kb, refresh_interval=0.5)


async def refresh():
    while True:
        static_text.text = f"Time: {datetime.datetime.now().strftime('%H:%M:%S')}"
        await asyncio.sleep(0.1)


app.create_background_task(refresh())
app.run()


Update 2:

It's also possible to update text controls through running threads. Every time the UI is invalidated, it renders itself with the new values set to text. So, a coroutine is not strictly necessary.

1

There are 1 best solutions below

1
furas On

Documentation for Application shows:

refresh_interval – Automatically invalidate the UI every so many seconds. 
When None (the default), only invalidate when invalidate has been called.

and it also shows:

on_invalidate – Called when the UI has been invalidated.

It automatically runs refresh when I assign it to on_invalidate=

def refresh(app):
    #print(app)
    static_text.text = f"Time: {datetime.datetime.now().strftime('%H:%M:%S')}"

app = Application(..., refresh_interval=0.5, on_invalidate=refresh)

or to before_render=

def refresh(app):
    #print(app)
    static_text.text = f"Time: {datetime.datetime.now().strftime('%H:%M:%S')}"

app = Application(..., refresh_interval=0.5, before_render=refresh)