Skip to content

Textual 0.23.0 improves message handling

It's been a busy couple of weeks at Textualize. We've been building apps with Textual, as part of our dog-fooding week. The first app, Frogmouth, was released at the weekend and already has 1K GitHub stars! Expect two more such apps this month.

Frogmouth /Users/willmcgugan/projects/textual/FAQ.md ContentsLocalBookmarksHistory▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ How do I pass arguments to an app? ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ▼  Frequently Asked Questions▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ ├──  Does Textual support images?When creating your App class, override __init__ as you would wheninheriting normally. For example: ├──  How can I fix ImportError cannot i ├──  How can I select and copy text in  ├──  How can I set a translucent app bafromtextual.appimportApp,ComposeResult ├──  How do I center a widget in a screfromtextual.widgetsimportStatic ├──  How do I pass arguments to an app? ├──  Why do some key combinations neverclassGreetings(App[None]): ├──  Why doesn't Textual look good on m│    └──  Why doesn't Textual support ANSI t│   def__init__(self,greeting:str="Hello",to_greet:str="World")->None: │   │   self.greeting=greeting │   │   self.to_greet=to_greet │   │   super().__init__() │    │   defcompose(self)->ComposeResult: │   │   yieldStatic(f"{self.greeting}{self.to_greet}") Then the app can be run, passing in various arguments; for example: ▅▅ # Running with default arguments. Greetings().run() # Running with a keyword arguyment. Greetings(to_greet="davep").run()▅▅ # Running with both positional arguments. Greetings("Well hello","there").run() ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  F1  Help  F2  About  CTRL+N  Navigation  CTRL+Q  Quit 

Tip

Join our mailing list if you would like to be the first to hear about our apps.

We haven't stopped developing Textual in that time. Today we released version 0.23.0 which has a really interesting API update I'd like to introduce.

Textual widgets can send messages to each other. To respond to those messages, you implement a message handler with a naming convention. For instance, the Button widget sends a Pressed event. To handle that event, you implement a method called on_button_pressed.

Simple enough, but handler methods are called to handle pressed events from all Buttons. To manage multiple buttons you typically had to write a large if statement to wire up each button to the code it should run. It didn't take many Buttons before the handler became hard to follow.

On decorator

Version 0.23.0 introduces the @on decorator which allows you to dispatch events based on the widget that initiated them.

This is probably best explained in code. The following two listings respond to buttons being pressed. The first uses a single message handler, the second uses the decorator approach:

on_decorator01.py
from textual.app import App, ComposeResult
from textual.widgets import Button


class OnDecoratorApp(App):
    CSS_PATH = "on_decorator.tcss"

    def compose(self) -> ComposeResult:
        """Three buttons."""
        yield Button("Bell", id="bell")
        yield Button("Toggle dark", classes="toggle dark")
        yield Button("Quit", id="quit")

    def on_button_pressed(self, event: Button.Pressed) -> None:  # (1)!
        """Handle all button pressed events."""
        if event.button.id == "bell":
            self.bell()
        elif event.button.has_class("toggle", "dark"):
            self.dark = not self.dark
        elif event.button.id == "quit":
            self.exit()


if __name__ == "__main__":
    app = OnDecoratorApp()
    app.run()
  1. The message handler is called when any button is pressed
on_decorator02.py
from textual import on
from textual.app import App, ComposeResult
from textual.widgets import Button


class OnDecoratorApp(App):
    CSS_PATH = "on_decorator.tcss"

    def compose(self) -> ComposeResult:
        """Three buttons."""
        yield Button("Bell", id="bell")
        yield Button("Toggle dark", classes="toggle dark")
        yield Button("Quit", id="quit")

    @on(Button.Pressed, "#bell")  # (1)!
    def play_bell(self):
        """Called when the bell button is pressed."""
        self.bell()

    @on(Button.Pressed, ".toggle.dark")  # (2)!
    def toggle_dark(self):
        """Called when the 'toggle dark' button is pressed."""
        self.dark = not self.dark

    @on(Button.Pressed, "#quit")  # (3)!
    def quit(self):
        """Called when the quit button is pressed."""
        self.exit()


if __name__ == "__main__":
    app = OnDecoratorApp()
    app.run()
  1. Matches the button with an id of "bell" (note the # to match the id)
  2. Matches the button with class names "toggle" and "dark"
  3. Matches the button with an id of "quit"

OnDecoratorApp ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ BellToggle darkQuit ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

The decorator dispatches events based on a CSS selector. This means that you could have a handler per button, or a handler for buttons with a shared class, or parent.

We think this is a very flexible mechanism that will help keep code readable and maintainable.

Why didn't we do this earlier?

It's a reasonable question to ask: why didn't we implement this in an earlier version? We were certainly aware there was a deficiency in the API.

The truth is simply that we didn't have an elegant solution in mind until recently. The @on decorator is, I believe, an elegant and powerful mechanism for dispatching handlers. It might seem obvious in hindsight, but it took many iterations and brainstorming in the office to come up with it!

Join us

If you want to talk about this update or anything else Textual related, join us on our Discord server.