DEV Community

Cover image for Textual: The Definitive Guide - Part 3.
Mahmoud Harmouch
Mahmoud Harmouch

Posted on • Edited on

Textual: The Definitive Guide - Part 3.

Hello, puppies and kittens! In this article, we will carry on our series demystifying every damn aspect of Textual. In the previous articles, we built a series of increasingly complex applications to demonstrate some of the concepts associated with developing TUIs with Textual, such as reactive attributes, custom widgets, event handlers, and state management, among many others.

In part 1, we introduced the concept of a GridView view, and we didn't explain much about how it works to group and align widgets on a rectangular layout. In part 2, we created a useless dummy login form without actions, such as redirection from one widget to another and refreshing the terminal. We also explained how widget events are handled and how reactive attributes work in Textual, but we didn't explain how to make our app handle events fired by another widget, such as the submit button that we have created. And that's what we are going to explore in this article by creating a fully dynamic register/login form.

Given the fact that textual is highly involved in object-oriented programming, this article assumes that you are already familiar with this concept. If not, consider reading one of the most comprehensive tutorials out there about this topic. This article also assumes that you know the basics of SQL since it is used to do CRUD operations on the database for the login and register forms.

Throughout this article, two terms, "message" and "event" were used interchangeably and referred to the same thing.

This article will shed some light on numerous Textual concepts used when developing custom TUI applications:

• Creating a Textual widget with custom rendering capabilities and behavior.
• Using GridView to arrange widgets in terms of columns and rows on the terminal.
• Rerender widgets using Textual's built-in support for clearing and rerendering widgets.

Spoilers ahead: By the end of this article, you will learn how to create a fully functional login/register screen in Textual, as shown in the recording below.

Login Form

👉 Table Of Content (TOC).

The Login Form

🔝 Go To TOC.

In part 2, we left off at this snippet of code that defines our login form logic.

It is made up of three main components: Two custom input widgets for entering username and password positioned at the top of the terminal and a submit button placed at the bottom of the terminal. Our submit button didn't do much other than logging the value of the username entered in a log file(notice the line self.log(f"username = {self.username}")). Now, we need to modify it to actually emulate the login process.

But first, let's tweak a bit the naming of some variables to reflect their functionalities and remove unnecessary variables.

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 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):
    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
        if button_name == "login":
            self.username = self.username_field.content
            self.password = self.password_field.content
            # Query the username and password

    async def on_mount(self) -> None:
        self.login_button = Button(label="login", name="login")
        self.username_field = InputText("username")
        self.password_field = InputText("password")
        await self.view.dock(self.login_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")
Enter fullscreen mode Exit fullscreen mode


A wacky login screen.

As you can tell, our login form looks wacky and a bit off proportions. We should organize the widgets in some sort of view. And that's what we are going to do in the next section.

GridView

🔝 Go To TOC.

As we saw in part 1 of the series, a grid lets us lay out our widgets in columns and rows. It is similar to HTML tables for doing layouts. We are going to illustrate the various ways you can tweak a grid to arrange and organize your widgets on your terminal.

In textual, a GridView is one of many available views, but it has some powerful capabilities for different use cases.

As mentioned above, a GridView provides us a way to align widgets in rows and columns that indicate the relative position of widgets. For example, widgets with the same column will be stacked above each other. Similarly, Those in the same row will be adjacent to each other.

Column and row numbers should be positive integers. The width of each column is fixed and defined beforehand. Let's take the following snippet of code to demonstrate that:

from textual.views import GridView

class LoginGrid(GridView):

    async def on_mount(self) -> None:
        self.grid.set_align("center", "center")
        self.grid.set_gap(1, 1)
        # Create rows / columns / areas
        self.grid.add_column("column", repeat=2, size=40)
        self.grid.add_row("row", repeat=3, size=3)
Enter fullscreen mode Exit fullscreen mode

As you can tell, we have extended the base class GridView to create our custom LoginGrid grid. The code is pretty straightforward, it defiens a 2x3 grid with 40 characters width and 3 characters height for each column and row respectively(notice the repeat argument of add_column and add_row).

Now, Let's group our widgets within this grid which can be done by calling the self.grid.add_widget function on each widget. By doing so, our code will result in the following listing:

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.views import GridView
from textual.widget import Widget
from textual.widgets import Button, ButtonPressed


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="",
            title_align="center",
            height=3,
            style="bold white on rgb(50,57,50)",
            border_style=Style(color="green"),
            box=DOUBLE,
        )


class LoginGrid(GridView):
    username: Reactive[RenderableType] = Reactive("")
    password: Reactive[RenderableType] = Reactive("")

    async def on_mount(self) -> None:
        # define input fields
        self.username = InputText("username")
        self.password = InputText("password")
        self.grid.set_align("center", "center")
        self.grid.set_gap(1, 1)
        # Create rows / columns / areas
        self.grid.add_column("column", repeat=2, size=40)
        self.grid.add_row("row", repeat=3, size=3)
        # Place out widgets in to the layout
        button_style = "bold red on white"
        label_style = "bold white on rgb(60,60,60)"
        username_label = Button(label="username", name="username_label", style=label_style)
        password_label = Button(label="password", name="password_label", style=label_style)
        self.grid.add_widget(username_label)
        self.grid.add_widget(self.username)
        self.grid.add_widget(password_label)
        self.grid.add_widget(self.password)
        self.grid.add_widget(Button(label="register", name="register", style=button_style))
        self.grid.add_widget(Button(label="login", name="login", style=button_style))


class MainApp(App):
    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
        if button_name == "login":
            self.username = self.login_grid.username.content
            self.password = self.login_grid.password.content
            # Query username and password

    async def on_mount(self) -> None:
        self.login_grid = LoginGrid()
        await self.view.dock(self.login_grid)


if __name__ == "__main__":
    MainApp.run(log="textual.log")
Enter fullscreen mode Exit fullscreen mode

Notice, we have added six widgets to our LoginGrid and two reactive attributes to store the username and password entered by the user and make them accessible to the parent application MainApp. The add_widget function will group the widgets from left to right, top to bottom. Now, our app looks minimalistic and contains only the LoginGrid inside the on_mount event handler. Running the above snippet of code will result in the following:


A login screen.

The comment # Query username and password is where we need to add the necessary logic to check whether or not the username and password being entered match a record in a database. For that purpose, we are going to use SQLite3 to store credentials since it comes pre-installed with python, a "Batteries-Included" programming language.

One major caveat here is the login/register buttons; In textual, you can capture click messages from the parent app by overriding the handle_button_pressed method, which is a special handler to read messages sent by a child button. This function will help us create the necessary logic simulating the login and register screens concept.

Views Vs Layouts

🔝 Go To TOC.

The other day, I was hopping around the source code, and I noticed that there are two ways to create a grid: Using the GridView or the GridLayout class. Apparently, a GridView is a layer of abstraction on top of GridLayout with an extra readable grid attribute. As a programmer, you can use either class to define a grid. But, if you want to read the grid value associated with a layout, you can use GridView. However, if you're going to use the GridLayout class, you can refer to this example. I think GridView is more flexible to use. It can interact with the parent application, unlike GridLayout.

With that noted, let's continue the implementation of our login form. In the next section, we will use sqlite3 as a database.

Storing User Credentials

🔝 Go To TOC.

In this section, we will learn how to build a little database to store the data submitted by a user. Having sqlite3 installed on our machine, we need to create a file called db.py and add the following code:

import sqlite3
import contextlib


def create_users_table() -> None:
    create_table_query = """
        CREATE TABLE IF NOT EXISTS
            users(
                id integer primary key autoincrement,
                username VARCHAR UNIQUE not null,
                password VARCHAR not null
            )
    """
    # connect to the database
    # using contextlib to avoid connections issues with
    # sqlite when forget closing the connection and cursor
    with contextlib.closing(sqlite3.connect("./users.sqlite")) as connection:
        # create a crusor to interract with the database
        with contextlib.closing(connection.cursor()) as cursor:
            cursor.execute(create_table_query)
            # commit the changes
            connection.commit()
Enter fullscreen mode Exit fullscreen mode

This code snippet creates a table called "users" that will hold each user's id, username, and password. You can extend this table to store whatever information you want, such as phone number, gender, etc. For demonstration purposes, let's stick with these data fields. In our function, notice the use of contextlib, which is a helpful built-in module to control the context of running code; it is usually used for synchronization. In our case, we used it to allow only one connection to the database to perform CRUD operations and close it afterward. From what I have experienced, this is the best practice to interact with the database if you are using SQLite.

Similarly, let's build our function(similar to an SQL procedure) to register a user.

def register_user(username: str, password: str) -> bool:
    query = """
        INSERT INTO 
            users (
                username
                , password
            )
        VALUES (
                ?
                , ?
            )
    """
    try:
        with contextlib.closing(sqlite3.connect("./users.sqlite")) as connection:
            # create a crusor to interract with the database
            with contextlib.closing(connection.cursor()) as cursor:
                cursor.execute(query, (username, password))
                # commit the changes
                connection.commit()
        return True
    except:
        return False
Enter fullscreen mode Exit fullscreen mode

Notice the use of the ? symbol, which will control the values of the username and the password entered by a user. This common practice shields you against the well-known TOP 10 OWASP attacks. Notice also the use of a try-catch block to handle the case of a user registering twice. This function returns True if a user has registered successfully and False if the user already exists.

Bear in mind that, in a real-world application, you never store sensitive information inside a database; you store its hashes instead. But, for demonstration purposes, let's bypass this requirement.

Moving on, we need to create a function to check if a user exists in the database as a record.

def check_user(username: str, password: str) -> str:
    result = ""
    select_query = """
        SELECT
        *
        FROM
            users
        WHERE
            username = ? 
        AND
            password = ?
    """
    with contextlib.closing(sqlite3.connect("./users.sqlite")) as connection:
        # create a crusor to interract with the database
        with contextlib.closing(connection.cursor()) as cursor:
            cursor.execute(select_query, (username, password))
            # commit the changes
            connection.commit()
            result = cursor.fetchone()
    return result
Enter fullscreen mode Exit fullscreen mode

Take a look at the result = cursor.fetchone() statement, which will return a record(e.g., a tuple of id, username, and a password) if the user exists, None otherwise.

Up until now, we have utility scripts that help us interact with the database with a level of abstraction. Now, let's go back to our main application.

Connecting The Database

🔝 Go To TOC.

Now, let's add the necessary logic for our login form.

class MainApp(App):
    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.username = self.login_grid.username.content
        self.password = self.login_grid.password.content
        if button_name == "login":
            # Query username and password to check if exists


    async def on_mount(self) -> None:
        self.login_grid = LoginGrid()
        await self.view.dock(self.login_grid)
Enter fullscreen mode Exit fullscreen mode

Let's begin with the edge cases. The first one is where a user enters an empty string in the username field and/or the password field. To address this case, we can add the following if statement:

if len(self.username) == 0 or len(self.password) == 0:
Enter fullscreen mode Exit fullscreen mode

Now, we need to display a nicely formatted message to prompt users to reenter a valid username and/or password. In Textual, there is no Label widget yet, but we can use a button for that purpose:

# add new widget
await self.view.dock(
    Button(
        label="Please enter a valid username and password!",
        style="bold white on rgb(50,57,50)",
    )
) 
Enter fullscreen mode Exit fullscreen mode

But first, we need to clear up the screen to draw this button on the terminal. To do so, we can use the clear function on the docks and widgets associated with the app.

self.view.layout.docks.clear()
self.view.widgets.clear()
Enter fullscreen mode Exit fullscreen mode

After adding these lines of code, our main app becomes:

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

    async 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.username = self.login_grid.username.content
        self.password = self.login_grid.password.content
        if button_name == "login":
            # clear widgets and docks
            self.view.layout.docks.clear()
            self.view.widgets.clear()
            if len(self.username) == 0 or len(self.password) == 0:
                # add new widget
                await self.view.dock(
                    Button(
                        label="Please enter a valid username and password!",
                        style="bold white on rgb(50,57,50)",
                    )
                )

    async def on_mount(self) -> None:
        self.login_grid = LoginGrid()
        await self.view.dock(self.login_grid)
Enter fullscreen mode Exit fullscreen mode

Notice that our handle_button_pressed becomes asynchronous because of the await self.view.dock statements. Now, we need to display this message for a couple of seconds before redrawing back the widgets to prompt the user to enter a new username and password:

await asyncio.sleep(2)
# clear widgets
self.view.layout.docks.clear()
self.view.widgets.clear()
# redraw back the grid
await self.view.dock(self.login_grid)
Enter fullscreen mode Exit fullscreen mode

If our program didn't hit this case, it means the user has entered both username and password. Now, we need to check whether or not the user exists in our database. To do so, we can add the following if-else statements:

from db import check_user

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

    async 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.username = self.login_grid.username.content
        self.password = self.login_grid.password.content
        if button_name == "login":
            # clear widgets and docks
            self.view.layout.docks.clear()
            self.view.widgets.clear()
            if len(self.username) == 0 or len(self.password) == 0:
                # add new widget
                await self.view.dock(
                    Button(
                        label="Please enter a valid username and password!",
                        style="bold white on rgb(50,57,50)",
                    )
                )
            elif check_user(self.username, self.password):
                # clear widgets
                self.view.layout.docks.clear()
                self.view.widgets.clear()
                # add new widget
                await self.view.dock(
                    Button(
                        label=f"Weclome back {self.username}!",
                        style="bold white on rgb(50,57,50)",
                    )
                )
            else:
                # clear widgets
                self.view.layout.docks.clear()
                self.view.widgets.clear()
                # add new widget
                await self.view.dock(
                    Button(
                        label="Invalid Credentials!",
                        style="bold white on rgb(50,57,50)",
                    )
                )
                await asyncio.sleep(2)
                # clear widgets
                self.view.layout.docks.clear()
                self.view.widgets.clear()
                # redraw back the grid
                await self.view.dock(self.login_grid)
Enter fullscreen mode Exit fullscreen mode

Alternatively, we can implement our register form:

        elif button_name == "register":
                result = None
                self.view.layout.docks.clear()
                self.view.widgets.clear()
                if len(self.username) == 0 or len(self.password) == 0:
                    # add new widget
                    await self.view.dock(
                        Button(
                            label="Please enter a valid username and password!",
                            style="bold white on rgb(50,57,50)",
                        )
                    )
                    await asyncio.sleep(2)
                    # clear widgets
                    self.view.layout.docks.clear()
                    self.view.widgets.clear()
                    # redraw back the grid
                    await self.view.dock(self.login_grid)
                else:
                    result = register_user(self.username, self.password)
                if result:
                    # add new widget
                    await self.view.dock(
                        Button(
                            label="User Registered Successfully!",
                            style="bold white on rgb(50,57,50)",
                        )
                    )
                    await asyncio.sleep(2)
                    # clear widgets
                    self.view.layout.docks.clear()
                    self.view.widgets.clear()
                    # redraw back the grid
                    await self.view.dock(self.login_grid)
                elif not result and len(self.username) > 0:
                    # add new widget
                    await self.view.dock(
                        Button(
                            label="Username Already Exists!",
                            style="bold white on rgb(50,57,50)",
                        )
                    )
                    await asyncio.sleep(2)
                    # clear widgets
                    self.view.layout.docks.clear()
                    self.view.widgets.clear()
                    # redraw back the grid
                    await self.view.dock(self.login_grid)
Enter fullscreen mode Exit fullscreen mode

Putting It All Together

🔝 Go To TOC.

The workflow of our program is the following after running python login.py:

  • The login screen opens up.
  • Type in your user and password.
  • Click on register to add a new user to the database.
  • The application will display the text:
    • "Please enter a valid username and password!" if one of the fields is empty
    • "User Registered Successfully!" for a new user.
    • "Username Already Exists!" for registered user.
  • Click on login to login into the app.
  • The application will display the text:
    • "Weclome back username!" if the user has registered to the application.
    • "Invalid Credentials!" otherwise.

The complete listing for our application looks like the following:

import asyncio

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.views import GridView
from textual.widget import Widget
from textual.widgets import Button, ButtonPressed

from db import check_user, create_users_table, register_user


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="",
            title_align="center",
            height=3,
            style="bold white on rgb(50,57,50)",
            border_style=Style(color="green"),
            box=DOUBLE,
        )


class LoginGrid(GridView):
    username: Reactive[RenderableType] = Reactive("")
    password: Reactive[RenderableType] = Reactive("")

    async def on_mount(self) -> None:
        # define input fields
        self.username = InputText("username")
        self.password = InputText("password")
        self.grid.set_align("center", "center")
        self.grid.set_gap(1, 1)
        # Create rows / columns / areas
        self.grid.add_column("column", repeat=2, size=40)
        self.grid.add_row("row", repeat=3, size=3)
        # Place out widgets in to the layout
        button_style = "bold red on white"
        label_style = "bold white on rgb(60,60,60)"
        username_label = Button(
            label="username", name="username_label", style=label_style
        )
        password_label = Button(
            label="password", name="password_label", style=label_style
        )
        self.grid.add_widget(username_label)
        self.grid.add_widget(self.username)
        self.grid.add_widget(password_label)
        self.grid.add_widget(self.password)
        self.grid.add_widget(
            Button(label="register", name="register", style=button_style)
        )
        self.grid.add_widget(Button(label="login", name="login", style=button_style))


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

    async 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.username = self.login_grid.username.content
        self.password = self.login_grid.password.content
        if button_name == "login":
            # clear widgets
            self.view.layout.docks.clear()
            self.view.widgets.clear()
            if len(self.username) == 0 or len(self.password) == 0:
                # add new widget
                await self.view.dock(
                    Button(
                        label="Please enter a valid username and password!",
                        style="bold white on rgb(50,57,50)",
                    )
                )
                await asyncio.sleep(2)
                # clear widgets
                self.view.layout.docks.clear()
                self.view.widgets.clear()
                # redraw back the grid
                await self.view.dock(self.login_grid)
            elif check_user(self.username, self.password):
                # clear widgets
                self.view.layout.docks.clear()
                self.view.widgets.clear()
                # add new widget
                await self.view.dock(
                    Button(
                        label=f"Weclome back {self.username}!",
                        style="bold white on rgb(50,57,50)",
                    )
                )
            else:
                # clear widgets
                self.view.layout.docks.clear()
                self.view.widgets.clear()
                # add new widget
                await self.view.dock(
                    Button(
                        label="Invalid Credentials!",
                        style="bold white on rgb(50,57,50)",
                    )
                )
                await asyncio.sleep(2)
                # clear widgets
                self.view.layout.docks.clear()
                self.view.widgets.clear()
                # redraw back the grid
                await self.view.dock(self.login_grid)
        elif button_name == "register":
            result = None
            self.view.layout.docks.clear()
            self.view.widgets.clear()
            if len(self.username) == 0 or len(self.password) == 0:
                # add new widget
                await self.view.dock(
                    Button(
                        label="Please enter a valid username and password!",
                        style="bold white on rgb(50,57,50)",
                    )
                )
                await asyncio.sleep(2)
                # clear widgets
                self.view.layout.docks.clear()
                self.view.widgets.clear()
                # redraw back the grid
                await self.view.dock(self.login_grid)
            else:
                result = register_user(self.username, self.password)
            if result:
                # add new widget
                await self.view.dock(
                    Button(
                        label="User Registered Successfully!",
                        style="bold white on rgb(50,57,50)",
                    )
                )
                await asyncio.sleep(2)
                # clear widgets
                self.view.layout.docks.clear()
                self.view.widgets.clear()
                # redraw back the grid
                await self.view.dock(self.login_grid)
            elif not result and len(self.username) > 0:
                # add new widget
                await self.view.dock(
                    Button(
                        label="Username Already Exists!",
                        style="bold white on rgb(50,57,50)",
                    )
                )
                await asyncio.sleep(2)
                # clear widgets
                self.view.layout.docks.clear()
                self.view.widgets.clear()
                # redraw back the grid
                await self.view.dock(self.login_grid)

    async def on_mount(self) -> None:
        create_users_table()
        self.login_grid = LoginGrid()
        await self.view.dock(self.login_grid)


if __name__ == "__main__":
    MainApp.run(log="textual.log")
Enter fullscreen mode Exit fullscreen mode

Wrapping up

🔝 Go To TOC.

A view is one of the essential textual components that instructs the child components or widgets to render in the available space on the terminal. For example, as we discussed, a Grid View arranges child components by a combination of rows and columns.

And this constitutes the Login app walkthrough. The resulting application can actually register a new user and login into the application.

As always, this article is a gift to you from a higher-dimensional entity. 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 license, as a starting point for various needs. Don’t forget to look at the readme file and use your imagination to make more complex apps meaningful to your use case.

Future Work

🔝 Go To TOC.

Now that you have created a fully functional login/register screen in textual, which worked as intended, you can make a chat room that uses these forms.

Keep in mind that this task is somewhat a medium level. It is objectively not easy to implement such applications. But, Textual provides us with a great toolkit on the user interface side of things, facilitating the ease of creating fully-featured, blazingly fast rendered widgets.

If you get stuck, it is not that big of a deal because nothing works immediately as you implement it; Sometimes, you will go through trial and error, which is so common in the fascinating programming world. Try to focus on one task at a time, and you will be surprised by your capabilities.

There is so much to explore about the fantastic world of Textal, and UI designers that are knowledgeable in this domain are few and highly valued. Remember that it will certainly pay off to dig into such topics about TUI at some point.

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

Top comments (3)

Collapse
 
slothyrulez profile image
Alex

Keep the hard work, thanks

Collapse
 
wiseai profile image
Mahmoud Harmouch

Absolutely!

Collapse
 
saze profile image
Saeed

Amazing, thank you for breaking this down!