2

Textual is considered as a front-end to consume events (e.g., using Redis' PUBSUB to consume and show incoming events and data).

Below is generic code attempting a background async task to run indefinitely while preserving all of Textual's functionality:

#!/usr/bin/env python

import asyncio
from datetime import datetime

from rich.align import Align
from textual.app import App
from textual.widget import Widget


class AsyncWidget(Widget):

    counter = 0

    async def async_functionality(self):
        while True:
            await asyncio.sleep(0.2)  # Mock async functionality
            self.counter += 1
            self.refresh()  # This is required for ongoing refresh
            self.app.refresh()  # Also required for ongoing refresh, unclear why, but commenting-out breaks live refresh.

    async def on_mount(self):
        await self.async_functionality()

    def render(self) -> Align:
        now = datetime.strftime(datetime.now(), "%H:%M:%S.%f")[:-5]
        text = f"{now}\nCounter: {self.counter}"
        return Align.center(text, vertical="middle")


class AsyncApp(App):
    async def on_load(self) -> None:
        await self.bind("escape", "quit", "Quit")

    async def on_mount(self) -> None:
        await self.view.dock(AsyncWidget())


AsyncApp.run(title="AsyncApp", log="async_app.log")

Running the app and immediately pressing the bound esc key terminates the program gracefully. Here's the resulting log (also, counter is visibly increasing on the TUI widget every 0.2s):

# WITH ASYNC FUNCTIONALITY, NO MOUSE EVENT

driver=<class 'textual.drivers.linux_driver.LinuxDriver'>
Load() >>> AsyncApp(title='Textual')
Mount() >>> AsyncApp(title='AsyncApp')
Mount() >>> DockView(name='DockView#1')
Mount() >>> AsyncWidget(name='AsyncWidget#1')
view.forwarded Key(key='escape')
Key(key='escape') >>> AsyncApp(title='AsyncApp')
ACTION AsyncApp(title='AsyncApp') quit
CLOSED AsyncApp(title='AsyncApp')
PROCESS END

However- a mouse click on the widget's body (which triggers a set_focus event) somehow [b]locks further key functionality (i.e. esc doesn't work, view.forwarded Key doesn't fire as seen in the log below). Moreover, ctrl-c must be used to terminate:

# WITH ASYNC FUNCTIONALITY, MOUSE EVENT BREAKS TUI

driver=<class 'textual.drivers.linux_driver.LinuxDriver'>
Load() >>> AsyncApp(title='Textual')
Mount() >>> AsyncApp(title='AsyncApp')
Mount() >>> DockView(name='DockView#1')
Mount() >>> AsyncWidget(name='AsyncWidget#1')
set_focus AsyncWidget(name='AsyncWidget#1') <--- mouse clicked anywhere on widget body
Key(key='ctrl+c') >>> AsyncApp(title='AsyncApp') <-- only way to terminate
ACTION AsyncApp(title='AsyncApp') quit
CLOSED AsyncApp(title='AsyncApp')
PROCESS END

This issue is clearly related to the long-running async functionality. Commenting out the await self.async_functionality() from the widget's on_mount directive shows the expected behavior for Textual (mouse click down/up events trigger, esc quit works):

# WORKING EXAMPLE, W/O ASYNC FUNCTIONALITY

driver=<class 'textual.drivers.linux_driver.LinuxDriver'>
Load() >>> AsyncApp(title='Textual')
Mount() >>> AsyncApp(title='AsyncApp')
Mount() >>> DockView(name='DockView#1')
Mount() >>> AsyncWidget(name='AsyncWidget#1')
set_focus AsyncWidget(name='AsyncWidget#1')
MouseDown(x=56, y=18, button=1) >>> AsyncWidget(name='AsyncWidget#1')
MouseUp(x=56, y=18, button=1) >>> AsyncWidget(name='AsyncWidget#1')
Click(x=56, y=18, button=1) >>> AsyncWidget(name='AsyncWidget#1')
Key(key='escape') >>> AsyncApp(title='AsyncApp')
ACTION AsyncApp(title='AsyncApp') quit
CLOSED AsyncApp(title='AsyncApp')
PROCESS END

Any advise on how to implement this long-running async functionality while interacting with the TUI would be great.

GG_Python
  • 3,436
  • 5
  • 34
  • 46
  • 1
    Without having any knowledge of the framework, I assume that awaiting a coroutine with an infinite loop is the problem. What happens if you use `asyncio.create_task(self.async_functionality())`? – thisisalsomypassword Mar 26 '22 at 21:49
  • @thisisalsomypassword- no impact, behavior is the same. – GG_Python Mar 26 '22 at 22:04
  • @thisisalsomypassword, your comment + Will's guidance below (accepted answer) did the trick. It's critical to *not* await the created task (as it never returns given the `while True` loop). thx. – GG_Python Mar 28 '22 at 00:49

1 Answers1

3

Textual widgets have an internal message queue that processes events sequentially. Your on_mount handler is processing one of these events, but because it is an infinite loop, you are preventing further events for being processed.

If you want to process something in the background you will need to creat a new asyncio Task. Note that you can’t await that task, since that will also prevent the handler from returning.

See the asyncio docs for more information on tasks.

Will McGugan
  • 2,005
  • 13
  • 10
  • 2
    A more detailed version of this answer should be in textual docs. It might sound easy for you but for a beginner in async programming like me it wasn't obvious what to do. It works like a charm, ty :) – qkzk Jun 30 '22 at 17:37