Issue
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.
Solution
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.
Answered By - Will McGugan
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.