Textual: The Definitive Guide - Part 2.
Tags: python, programming, textual, rich
Published: 2022-04-15 15:54:14

---
cover_image: https://raw.githubusercontent.com/Textualize/textual/main/imgs/textual.png
---

Greetings, everyone. In this article, we will pick up where we left off and continue our series on demystifying every aspect of Textual. In Part 1, we interacted with various Textual components such as views and widgets. In addition, we were able to customize the way these components are laid out on the terminal while retaining a very high level of abstraction working with fully-featured widgets. We also introduced the concepts of widget event handlers, watchers, and reactive attributes/properties, but we didn't elaborate much. And that's what we are going to focus on in this article. 

To ensure that we are all on the same page, this tutorial assumes that you are already familiar with object-oriented programming. If not, consider reading [one of the most comprehensive tutorials out there](https://realpython.com/python3-object-oriented-programming/) about this topic.

**Spoilers ahead**: By the end of this article, you will learn how to create a custom login screen in Textual, as shown in the figure below.

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/umckob063979532d8d1s.png">
  <figcaption>A Login Screen.</figcaption>
</figure> 

## 👉 Table Of Content (TOC). <a name="TOC"></a>

* [Events and Event Handlers](#Events)
* [Add Event Handlers To Custom Widgets](#EventsWidgets)
* [Reactive Attributes](#ReactiveAttributes)
* [Putting It All Together](#Puttingtogether)
* [Adding A Submit Button](#submitbutton)
* [Wrapping Up](#WrappingUp)

## Events and Event Handlers <a name="Events"></a>

##### 🔝 [Go To TOC](#TOC). 

An event is a signal that something interesting has happened. It can be triggered by different actions such as clicking on a button, typing on the keyboard, or making a mouse movement. Events are used to control and respond to what the user does with their Input/Output devices.

In Textual, events are being fired all the time. What makes Textual interesting is the fact that it is asynchronous, which means it provides an event loop that your program executes when you call the run method on the main app. This method is constantly cycling through events such as keyboard entry and mouse motion, among many others. When something interesting happens, it does the necessary processing to ensure that your code knows the event has happened and has a chance to respond.

So if an event happens, an event handler responds to what happened. In Textual, event handlers are just functions that usually take one argument: the event that occurred. An event handler can react to changes in the state of an object. 

## Add Event Handlers To Custom Widgets <a name="EventsWidgets"></a>

##### 🔝 [Go To TOC](#TOC). 

As with most Textual events, you can hook up event handlers in your program by implementing an `on_<event_name>` method. Let's start by creating an `app.py` file. Then add the following code definition:

```python
from textual.app import App
from textual.widget import Widget


class InputText(Widget):
    ...

class MainApp(App):
    ...

if __name__ == '__main__':
    MainApp.run()
``` 

The second line of code imports the `Widget` class to create a custom widget by extending it. Inheritance is used to create a new subclass `InputText` from `Widget` with no extra logic.

If you now run `python app.py` or `poetry run python app.py`, it will behave the same way as [the Basic Textual App Example](https://dev.to/wiseai/textual-the-definitive-guide-part-1-1i0p#BasicTextual) of the previous article. Now, we are going to add custom logic to these classes.

The event handler is accessed as a property on the widget object with the `on_` prefix. There are specific types of events for different widgets; for a button like a widget, the `click` event is kicked off by a mouse press.

Now let's add this event handler to our `InputText` widget.

```python
import sys

from textual.app import App
from textual.widget import Widget


class InputText(Widget):
    def on_click(self) -> None:
        sys.exit(0)

class MainApp(App):
    async def on_mount(self) -> None:
        await self.view.dock(InputText())

if __name__ == '__main__':
    MainApp.run()
``` 

As you can tell, we added an `on_click` and an `on_mount` event handlers to our classes. The latter was used to attach/dock the `InputText` widget to the terminal and make it visible. This event handler is kicked off when you first run your Textual app. Think of it as a pre-processing step to make the widgets ready for rendering. It is an async function since Textual is an asynchronous framework.

If you run this example, your terminal will look like the following:

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5zx6f776wi8o4t9cjwno.png">
  <figcaption>A raw rich panel.</figcaption>
</figure>

As you can see, our `InputText` looks like a raw placeholder because, by default, widgets are rendered as [rich panels](https://github.com/Textualize/textual/blob/f702d1ac23a22c5143d825ea46795fbf8ccdb12b/src/textual/widget.py#L251-L259). However, we will add some custom rendering capabilities by overriding this method.

Now, if you click anywhere on the widget, it will fire a `click` event which will trigger the `on_click` event handler. Then it executes the contents of that method `sys.exit(0)`, which will cause our program to exit.

Everything behaves as expected. Now we need to change the rendering of this widget to simulate the concept of a text box where a user can type in the text. To do so, we will add the following `render` function:

```python
import sys

from rich.align import Align
from rich.box import DOUBLE
from rich.console import RenderableType
from rich.panel import Panel
from rich.style import Style
from rich.text import Text
from textual.app import App
from textual.widget import Widget


class InputText(Widget):
    def on_click(self) -> None:
        sys.exit(0)

    def render(self) -> RenderableType:
        renderable = Align.left(Text("", style="bold"))
        return Panel(
            renderable,
            title="input_text",
            title_align="center",
            height=3,
            style="bold white on rgb(50,57,50)",
            border_style=Style(color="green"),
            box=DOUBLE,
        )


class MainApp(App):
    async def on_mount(self) -> None:
        await self.view.dock(InputText())


if __name__ == "__main__":
    MainApp.run()
```

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yrhxcq8kpp35xv6gta7x.png">
  <figcaption>Custom input field.</figcaption>
</figure>

Now, we need to allow the user to enter text into this widget which can be done by adding a `Reactive` attribute in the `InputText` class to store the value of the pressed keys on the keyboard and then render the value of that attribute on the widget. This is a good occasion to delve into the Textual concept of Reactive attributes/properties.

## Reactive Attributes <a name="ReactiveAttributes"></a>

##### 🔝 [Go To TOC](#TOC). 

Textual Reactive attributes/properties are somewhat magical. At their core, Reactive attributes are implemented using the concept of [python descriptors](https://docs.python.org/3/howto/descriptor.html). In Textual, properties have [type-validating features](https://github.com/Textualize/textual/blob/f702d1ac23a22c5143d825ea46795fbf8ccdb12b/src/textual/reactive.py#L68-L70) using the `validate_<attribute_name>` notation. For instance, you can always be sure that a string is being stored for a given attribute that does not contain an integer value. Another example, you can ensure that a number is within a specific range. To illustrate this, let's take the following snippet of code:

```python
import sys

from rich.align import Align
from rich.box import DOUBLE
from rich.console import RenderableType
from rich.panel import Panel
from rich.style import Style
from rich.text import Text
from textual.app import App
from textual.reactive import Reactive
from textual.widget import Widget


class InputText(Widget):

    # The property is created at the class level as an instance of the Reactive class.
    title: Reactive[RenderableType] = Reactive("")

    def __init__(self, title: str):
        super().__init__(title)
        self.title = title

    def validate_title(self, value) -> None:
        try:
          return value.lower()
        except (AttributeError, TypeError):
          raise AssertionError('title attribute should be a string.')

    def on_click(self) -> None:
        sys.exit(0)

    def render(self) -> RenderableType:
        renderable = Align.left(Text("", style="bold"))
        return Panel(
            renderable,
            title=self.title,
            title_align="center",
            height=3,
            style="bold white on rgb(50,57,50)",
            border_style=Style(color="green"),
            box=DOUBLE,
        )


class MainApp(App):
    async def on_mount(self) -> None:
        await self.view.dock(InputText(4096))


if __name__ == "__main__":
    MainApp.run()
```

If you run the above program, it will fail and throw the following exception:

```bash
AssertionError: title attribute should be a string.
```

And that's because of the line `await self.view.dock(InputText(4096))` that tries to instantiate the `InputText` class with an integer and assign that value to `title` at this line of code `self.title = title`. This line will trigger the `validate_title` function to run its inner code, making the program raise an `AttributeError` because, as you may know, an integer doesn't have a `lower` method. This way, you ensure that your attribute values are being validated before assignment.

Interestingly enough, Textual Reactive Attributes can [fire events](https://github.com/Textualize/textual/blob/f702d1ac23a22c5143d825ea46795fbf8ccdb12b/src/textual/reactive.py#L76) when their values change. This can be extremely useful, as you will see in subsequent articles. The fired event can be handled by implementing a `watch_<attribute_name>` method.

Let's introduce another attribute called `content` to store the keys being entered by a user using a keyboard. We need to figure out how to update the `content` field when the user presses a key. But first, let's add a handler responsible for that event called `on_key`. Textual takes care of [all sorts of keys](https://github.com/Textualize/textual/blob/f702d1ac23a22c5143d825ea46795fbf8ccdb12b/src/textual/keys.py#L7) available on your keyboard. 

```python
import sys

from rich.align import Align
from rich.box import DOUBLE
from rich.console import RenderableType
from rich.panel import Panel
from rich.style import Style
from rich.text import Text
from textual import events
from textual.app import App
from textual.reactive import Reactive
from textual.widget import Widget


class InputText(Widget):

    title: Reactive[RenderableType] = Reactive("")
    content: Reactive[RenderableType] = Reactive("")

    def __init__(self, title: str):
        super().__init__(title)
        self.title = title

    def on_key(self, event: events.Key) -> None:
        self.content += event.key

    def validate_title(self, value) -> None:
        try:
          return value.lower()
        except (AttributeError, TypeError):
          raise AssertionError('title attribute should be a string.')

    def on_click(self) -> None:
        sys.exit(0)

    def render(self) -> RenderableType:
        renderable = Align.left(Text("", style="bold"))
        return Panel(
            renderable,
            title=self.title,
            title_align="center",
            height=3,
            style="bold white on rgb(50,57,50)",
            border_style=Style(color="green"),
            box=DOUBLE,
        )


class MainApp(App):
    async def on_mount(self) -> None:
        await self.view.dock(InputText("input field"))


if __name__ == "__main__":
    MainApp.run()
```

Whenever you press a key, it will be appended to the `content` string variable. Now all you need to do is figure out how to reflect the value of this variable on the widget. As you may guess, this can be done by tweaking the render function as follows:

```python
import sys

from rich.align import Align
from rich.box import DOUBLE
from rich.console import RenderableType
from rich.panel import Panel
from rich.style import Style
from rich.text import Text
from textual import events
from textual.app import App
from textual.reactive import Reactive
from textual.widget import Widget


class InputText(Widget):

    title: Reactive[RenderableType] = Reactive("")
    content: Reactive[RenderableType] = Reactive("")

    def __init__(self, title: str):
        super().__init__(title)
        self.title = title

    def on_key(self, event: events.Key) -> None:
        self.content += event.key

    def validate_title(self, value) -> None:
        try:
          return value.lower()
        except (AttributeError, TypeError):
          raise AssertionError('title attribute should be a string.')

    def render(self) -> RenderableType:
        renderable = Align.left(Text(self.content, style="bold"))
        return Panel(
            renderable,
            title=self.title,
            title_align="center",
            height=3,
            style="bold white on rgb(50,57,50)",
            border_style=Style(color="green"),
            box=DOUBLE,
        )


class MainApp(App):
    async def on_mount(self) -> None:
        await self.view.dock(InputText("input field"))


if __name__ == "__main__":
    MainApp.run()
```

If you run the above code and start typing on your keyboard, you will notice that it is not rendered yet. And that's because your terminal needs a focus by clicking on it. Now, the text will be reflected on the widget as shown below.

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pcl5gdrbkz3p8szan08c.png">
  <figcaption>Custom input field.</figcaption>
</figure>

You can add the ability to remove a letter by checking if the key being pressed is `ctrl+h` which is the backspace on your keyboard. 

```python
    def on_key(self, event: events.Key) -> None:
        if event.key == "ctrl+h":
           self.content = self.content[:-1]
        else:
           self.content += event.key
```

## Putting It All Together <a name="Puttingtogether"></a>

##### 🔝 [Go To TOC](#TOC). 

Now, it is time to create a custom widget that has the following properties:

* If the widget is a text field like `username`, print out the letter on the widget.
* If the widget is a password field, hideout the letters being entered and display the character `*` instead.
* You can type out letters on the widget only if your mouse is hovering over it. This can be done using the `on_enter` and `on_leave` handlers.

Our final program is defined as follows:

```python
from rich.align import Align
from rich.box import DOUBLE
from rich.console import RenderableType
from rich.panel import Panel
from rich.style import Style
from rich.text import Text
from textual import events
from textual.app import App
from textual.reactive import Reactive
from textual.widget import Widget


class InputText(Widget):

    title: Reactive[RenderableType] = Reactive("")
    content: Reactive[RenderableType] = Reactive("")
    mouse_over: Reactive[RenderableType] = Reactive(False)

    def __init__(self, title: str):
        super().__init__(title)
        self.title = title

    def on_enter(self) -> None:
        self.mouse_over = True

    def on_leave(self) -> None:
        self.mouse_over = False

    def on_key(self, event: events.Key) -> None:
        if self.mouse_over == True:
            if event.key == "ctrl+h":
                self.content = self.content[:-1]
            else:
                self.content += event.key

    def validate_title(self, value) -> None:
        try:
            return value.lower()
        except (AttributeError, TypeError):
            raise AssertionError("title attribute should be a string.")

    def render(self) -> RenderableType:
        renderable = None
        if self.title.lower() == "password":
            renderable = "".join(map(lambda char: "*", self.content))
        else:
            renderable = Align.left(Text(self.content, style="bold"))
        return Panel(
            renderable,
            title=self.title,
            title_align="center",
            height=3,
            style="bold white on rgb(50,57,50)",
            border_style=Style(color="green"),
            box=DOUBLE,
        )


class MainApp(App):
    async def on_load(self) -> None:
        await self.bind("ctrl+c", "quit", "Quit")

    async def on_mount(self) -> None:
        await self.view.dock(InputText("user_name"), edge="left", size=50)
        await self.view.dock(InputText("password"), edge="left", size=50)


if __name__ == "__main__":
    MainApp.run()
```

If you run the above snippet of code, it will generate the following output. You can switch between widgets by clicking on them.

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w35k77fiufa8lzlj5xob.png">
  <figcaption>Custom input fields.</figcaption>
</figure>

## Adding A Submit Button <a name="submitbutton"></a>

##### 🔝 [Go To TOC](#TOC).

Thanks to @Darkstarinternet suggestion, we can extend the capabilities of our app to handle a submit button that will store the values of the username and password for future processing. It can be accomplished with the help of the `handle_button_pressed` method, which will capture the pressed button event as the name suggests. Our final application will look like the following:

```python
from rich.align import Align
from rich.box import DOUBLE
from rich.console import RenderableType
from rich.panel import Panel
from rich.style import Style
from rich.text import Text
from textual import events
from textual.app import App
from textual.reactive import Reactive
from textual.widget import Widget
from textual.widgets import Button
from textual.widgets import Button, ButtonPressed


class Submit(Button):

    clicked: Reactive[RenderableType] = Reactive(False)

    def on_click(self) -> None:
        self.clicked = True


class InputText(Widget):

    title: Reactive[RenderableType] = Reactive("")
    content: Reactive[RenderableType] = Reactive("")
    mouse_over: Reactive[RenderableType] = Reactive(False)

    def __init__(self, title: str):
        super().__init__(title)
        self.title = title

    def on_enter(self) -> None:
        self.mouse_over = True

    def on_leave(self) -> None:
        self.mouse_over = False

    def on_key(self, event: events.Key) -> None:
        if self.mouse_over == True:
            if event.key == "ctrl+h":
                self.content = self.content[:-1]
            else:
                self.content += event.key

    def validate_title(self, value) -> None:
        try:
            return value.lower()
        except (AttributeError, TypeError):
            raise AssertionError("title attribute should be a string.")

    def render(self) -> RenderableType:
        renderable = None
        if self.title.lower() == "password":
            renderable = "".join(map(lambda char: "*", self.content))
        else:
            renderable = Align.left(Text(self.content, style="bold"))
        return Panel(
            renderable,
            title=self.title,
            title_align="center",
            height=3,
            style="bold white on rgb(50,57,50)",
            border_style=Style(color="green"),
            box=DOUBLE,
        )


class MainApp(App):
    submit: Reactive[RenderableType] = Reactive(False)
    username: Reactive[RenderableType] = Reactive("")
    password: Reactive[RenderableType] = Reactive("")

    def handle_button_pressed(self, message: ButtonPressed) -> None:
        """A message sent by the submit button"""
        assert isinstance(message.sender, Button)
        button_name = message.sender.name
        self.submit = message.sender.clicked
        if button_name == "submit" and self.submit:
            self.submit_button.clicked = False
            self.username = self.username_field.content
            self.password = self.password_field.content
            self.log(f"username = {self.username}")

    async def on_mount(self) -> None:
        self.submit_button = Submit(
            label="Submit", name="submit", style="black on white"
        )
        self.submit = self.submit_button.clicked
        self.username_field = InputText("username")
        self.password_field = InputText("password")
        await self.view.dock(self.submit_button, edge="bottom", size=3)
        await self.view.dock(self.username_field, edge="left", size=50)
        await self.view.dock(self.password_field, edge="left", size=50)


if __name__ == "__main__":
    MainApp.run(log="textual.log")
```

<figure>
  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/umckob063979532d8d1s.png">
  <figcaption>A Login Screen.</figcaption>
</figure> 

## Wrapping Up <a name="WrappingUp"></a>

##### 🔝 [Go To TOC](#TOC). 

Textual is an exciting TUI toolkit that you can use to create whatever type of terminal user interface. This article highlighted a few common practices for developing custom Textual widgets, such as an input field, hooking up event handlers, reactive attributes, handling keyboard and mouse events, etc.

There are many components and concepts about Textual that we haven't covered yet. So if you are interested, make sure to check out the Textual repo for examples and tutorials, and don't forget to stay tuned to this blog. I recently found that [the creator of Textual](https://github.com/willmcgugan) has a [blog](https://www.willmcgugan.com/) where he documents all the major changes, releases about Textual. I also discovered a [css branch](https://github.com/Textualize/textual/tree/css) for Textual where you can use CSS to customize your widgets. That's a pretty impressive feature, which will be the topic of future articles. Imagine creating a web browser within your terminal using Textual; that would be extremely sick!

As always, this article is a gift to you, and you can share it with whomever you like or use it in any way that would be beneficial to your personal and professional development. By supporting this blog, you keep me motivated to publish high-quality content related to python in general and textual specifically. Thank you in advance for your ultimate support! 

You are free to use the code in this article, which is licensed under [the MIT licence](https://github.com/Harmouch101/Textual-Experimentations), as a starting point for various needs. Don’t forget to look at [the readme file](https://github.com/Textualize/textual) and use your imagination to make more complex apps meaningful to your use case.

Happy Coding, folks; see you in the next one.