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.