Skip to content

Actions

Actions are allow-listed functions with a string syntax you can embed in links and bind to keys. In this chapter we will discuss how to create actions and how to run them.

Action methods

Action methods are methods on your app or widgets prefixed with action_. Aside from the prefix these are regular methods which you could call directly if you wished.

Information

Action methods may be coroutines (defined with the async keyword).

Let's write an app with a simple action method.

actions01.py
from textual.app import App
from textual import events


class ActionsApp(App):
    def action_set_background(self, color: str) -> None:
        self.screen.styles.background = color

    def on_key(self, event: events.Key) -> None:
        if event.key == "r":
            self.action_set_background("red")


if __name__ == "__main__":
    app = ActionsApp()
    app.run()

The action_set_background method is an action method which sets the background of the screen. The key handler above will call this action method if you press the R key.

Although it is possible (and occasionally useful) to call action methods in this way, they are intended to be parsed from an action string. For instance, the string "set_background('red')" is an action string which would call self.action_set_background('red').

The following example replaces the immediate call with a call to run_action() which parses an action string and dispatches it to the appropriate method.

actions02.py
from textual import events
from textual.app import App


class ActionsApp(App):
    def action_set_background(self, color: str) -> None:
        self.screen.styles.background = color

    async def on_key(self, event: events.Key) -> None:
        if event.key == "r":
            await self.run_action("set_background('red')")


if __name__ == "__main__":
    app = ActionsApp()
    app.run()

Note that the run_action() method is a coroutine so on_key needs to be prefixed with the async keyword.

You will not typically need this in a real app as Textual will run actions in links or key bindings. Before we discuss these, let's have a closer look at the syntax for action strings.

Syntax

Action strings have a simple syntax, which for the most part replicates Python's function call syntax.

Important

As much as they look like Python code, Textual does not call Python's eval function to compile action strings.

Action strings have the following format:

  • The name of an action on its own will call the action method with no parameters. For example, an action string of "bell" will call action_bell().
  • Action strings may be followed by parenthesis containing Python objects. For example, the action string set_background("red") will call action_set_background("red").
  • Action strings may be prefixed with a namespace (see below) and a dot.
eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nNVaW2/bNlx1MDAxNH7vr1xivIduQK3y8M5cdTAwMDLDkNuKtFl6SdJ0XHUwMDFkhkGV6Fi1LGmScluR/75DJbFkxXZsx8kyIXAsklx1MDAxMlx1MDAwZlx1MDAwZs/3nVx1MDAwYv392dpap7zIbOfVWseeXHUwMDA3flx1MDAxY4W5f9Z54dpPbV5EaYJdtLov0pM8qEb2yzIrXr18OfTzgS2z2Fx1MDAwZqx3XHUwMDFhXHUwMDE1J35cXJQnYZR6QTp8XHUwMDE5lXZY/OI+9/yh/TlLh2GZe/UkXVx1MDAxYkZlml/NZWM7tElZ4Nv/wPu1te/VZ0O63Fx1MDAwNqWfXHUwMDFjx7Z6oOqqXHUwMDA1VES0W/fSpFx1MDAxMlx1MDAxNoQm1ChQajRcIiq2cL7ShtjdQ5lt3eOaOlx1MDAxZr6c9T92//HXeSo/fzvc3Oz725/qaXtRXHUwMDFj75dcdTAwMTdxJVaR4mrqvqLM04E9isKy7+ZutY+eXG79om9cdTAwMWKP5enJcT+xhVs/XHUwMDE5taaZXHUwMDFmROWFe1x1MDAxMalbr5TQXHUwMDFjd+62iHBPUy5cdTAwMDSVhlx1MDAxOcpGne5xajyptDJGa8XAXGLgLcE201x1MDAxOHdcdTAwMDJcdTAwMDX7gVRXLdlXP1x1MDAxOFx1MDAxY6N4SViP6fmC4jyyXHUwMDFldXa9YGk80CC4oVxcSqJlPU/fRsf9XHUwMDEyh3DuXHRmiKT1jlx1MDAxNbbaXG5DgSlupFx1MDAxOXW4ibOdsLKKP9u67Pt5dq2yTiVgQ2h3u902qaZZNXY7se+2j3bPw3LvaHt3a7d7MVx1MDAxNFx1MDAwN2b0rjFcdTAwMWL08zw964x6Lq+/1aKdZKF/ZVcgJeeodC6YqC0vjpJcdTAwMDF2JidxXFy3pcGgNsWq9fLFXHUwMDEyXHUwMDE4XHUwMDAwwshUXHUwMDEwXHUwMDE4YJQoQ+dcdTAwMDfBodj66O++zz92L96dXHL6XyCM5ZenXHUwMDBlXHUwMDAypT3FXHRcdTAwMTdcdTAwMWGhQCVt2FhcdTAwMDVcdTAwMDPtYVx1MDAxYpVMXHUwMDEypcBokPeCXHUwMDAx0K9ay0kwoFxiOM6N4kpcYqNcdTAwMTUsglx1MDAwMmCEK6VcdTAwMDU8Mlxm9lx1MDAwNjD4evr69MPml/M9+lv5XHUwMDFl5M5fK4OBxMU+XHUwMDEyXGaAT4dcdTAwMDE3XHUwMDA0jVx1MDAwMlx1MDAwMOaGXHUwMDAx/7xcdTAwMGXs8+DtRry1l4b7mdVvksMnXHUwMDBlXHUwMDAzXFygp5HrNdHGLVaNo0ChM+CK4ntcdTAwMTRChZt7gYBcdTAwMDfS9sQkXHUwMDEwXHUwMDAwUE9cdTAwMDNjaOSaKYq+aSFcdTAwMThcYsm4RCHF48KgSzdcdTAwMDb7hkT5/plMdXFA5MHu69XBQKNXXFxcdTAwMTVcZkp7Xk5EXHUwMDAwct9UXHUwMDA0XHUwMDEwyVx1MDAxOFx1MDAxN1x1MDAwNOZ3XHUwMDA04dveutjaevO7POSDzUMt4r9cdTAwMGbTlVwioPVUXHUwMDEzXHUwMDAwsFx1MDAxNFx1MDAwMFDdnpTCXHUwMDAwRdNcdTAwMTOKyjFcdTAwMDCAXHUwMDE2XHUwMDFlQ3rmoFxiekUj7+dcdTAwMDZmIMBMYP7bpq4lcIrRXHUwMDE5LG7phbt5knFcdTAwMGYzyixC+LU9pUm5XHUwMDFm/WOrmHas9Vd/XHUwMDE4xVx1MDAxN2NGUUFcdTAwMDBcdTAwMDV8l5Vo5H68lmCqUaCl2OaeXHUwMDE1XHUwMDE256+sX489uVx1MDAxZUfHXHUwMDBlMJ3Y9saRVEaYpYy6y7Sh41x1MDAwMCXx8XX5TtheUZpHx1x1MDAxMUpxMF2qpVx1MDAwMI1x+3Q8XHUwMDBizkBqMT+eTVx1MDAxMoP/Nt/RKflwXlx1MDAxY+yGcL71+mnjmVx04UmmXGIlwNBXqPHsXHUwMDA2uPKAMkFcZsewj90zqpvl0Golz4AzQ1JBbmH14EeB88PGb4rrhlN5aDivXHUwMDA3XHUwMDBlOFx1MDAxNWxcdTAwMTbCcYCKsvlcdTAwMDMguSnQUlx1MDAxMNbCtFtHXHUwMDEwXHUwMDA2iVlcdTAwMDJIPn9QSvzuMP37nLPy2+5OfiZcdTAwMDby8Nvm04Ywl8Zj2lx1MDAwNXVcdTAwMWHjTpef3fLJklx1MDAxOFx1MDAwNDFaXHUwMDFjqGZcdTAwMDSzWlx1MDAxMCOFzFx1MDAwM2JcdTAwMDOCXG7M3Fx1MDAxZdknP2z0+Vx1MDAxZvnkzM9cdTAwMTE3XGLM4mmAeZJgM0F9pepJkTafXHUwMDFlaWP2olx1MDAxNFVcctzfherZXHUwMDAx2Vx1MDAwMqhuY2dZVKs76y1cZlFcdTAwMGKMYqLJpFa6sZWVTSjmgeKCSiUx4aQzXHUwMDAy7Vx1MDAxMH036S1cdTAwMGJq5lx0TlxmwYlcYlx1MDAwN8x4xVx1MDAwNEetjUdccnZLKVx0XHUwMDEx0IghriFPMSXmmKku4bfvSjhXWSBsyOHn5UaUhFFyjJ01m9xcdTAwMTTTd+ZI39wq/axKXHUwMDFhcatQPdwwzbVq9PfS4MStoks86spdUlx1MDAxMalQlVxmdXk96nIklE3Cu0WaXV9cdTAwMWaJZDCHk5QrhZsloHaP4zJcdTAwMTFqXHUwMDE44J/AUNhVttktmWK/KDfT4TAqUfPv0ygp21x1MDAxYa5Uue5Q3rf+Lf7ANTX72nSQuTeOM3v9ba1GTHUz+v7ni4mjp1uyu7q3jLh+37Pm/8WZzGjWbr5hMlxumJ1yVPP8OcbsYPQpUlx1MDAxOTPGXHUwMDAzV1x1MDAxYmGaUIG23YpPmPKEQtunklx1MDAxM4lmJlqC1TQlXHUwMDAyw0m4dHziUUE4ZnWSMCMwXHUwMDAwmVA1XHUwMDEzzNNcdTAwMThHoaTKXGKuTENJV1SmMCNkgKB5XFwqWzpJmJPKZmeuXHLeQO05h8RBacxcdTAwMTh5Y0STzKRcdTAwMTGoYoJcdTAwMDRCXHUwMDE1Q+e0XHUwMDFjmc0+J6n5lXhCXHUwMDEzQVx1MDAxY98z9DL1vrboXGZcdTAwMDdhmFx1MDAwNoxJVCdy3/+azqZbs7u6t1xmeVV0hvQp280jOmNcZlWMucjcbDY7Kv9cdTAwMGbYTN95XHUwMDAy4Fx1MDAwZWJcdEZeLrdslz+V9ihSXHUwMDE51UZcdTAwMDOlzeJSm8qY5L1ALUtlxFx1MDAwM4NuTDrPjJmf1mzCcbBhXHUwMDFlikmFoe40gjeLN9dcdTAwMDdcdTAwMDGG4FuU4Fx1MDAwZnBcdTAwMTCwylL9omQ2O4dcdTAwMWbxhvJcdTAwMThzkSlobjjlnDVGNGmDaIyQjNCIMkOlXHUwMDA2s1x1MDAxY5vNPu5qRovS7arE0Fx1MDAxZThhdIpUKFx1MDAxMVx1MDAwM7fxXHUwMDFhjVFh9v+/ZrNcdTAwMTlcdTAwMDbtru4tW16QzqZcdTAwMTWP2PRcdTAwMTNNzShcdTAwMTdEwvxkZuS23GbBh4NPnzc+XHUwMDFkvTmKN/OMTCGzvlx1MDAxZvRPcjuNzlZVPTJ35pmAcbGghGFcdTAwMTiKITEh4+f6XGY8XHUwMDA21J1rOVx1MDAwZqu0udfPW8rcT4rMz1x1MDAxMVx1MDAxM7c5jcOE8lGD125Ii1OqMVA0S5DW0z3TXHUwMDExtFlcXF+yfqTHWkf1ozr7uKlcdTAwMWb5WeZcdTAwMTW2/Kveolx1MDAxZp/nNnz+08QqUuOXLY9xtDNduGc32qw06Vx1MDAwNu6XqMdcdTAwMTHnoiFE4bUy6ik6p5E925j0W6vqcm+tWMPh0zoz+H757PJfPFx1MDAxMrdyIn0= Optional namespaceAction nameOptional parametersapp.set_background('red')

Parameters

If the action string contains parameters, these must be valid Python literals, which means you can include numbers, strings, dicts, lists, etc., but you can't include variables or references to any other Python symbols.

Consequently "set_background('blue')" is a valid action string, but "set_background(new_color)" is not — because new_color is a variable and not a literal.

Actions may be embedded as links within console markup. You can create such links with a @click tag.

The following example mounts simple static text with embedded action links.

actions03.py
from textual.app import App, ComposeResult
from textual.widgets import Static

TEXT = """
[b]Set your background[/b]
[@click=set_background('red')]Red[/]
[@click=set_background('green')]Green[/]
[@click=set_background('blue')]Blue[/]
"""


class ActionsApp(App):
    def compose(self) -> ComposeResult:
        yield Static(TEXT)

    def action_set_background(self, color: str) -> None:
        self.screen.styles.background = color


if __name__ == "__main__":
    app = ActionsApp()
    app.run()

ActionsApp Set your background Red Green Blue

When you click any of the links, Textual runs the "set_background" action to change the background to the given color.

Bindings

Textual will run actions bound to keys. The following example adds key bindings for the R, G, and B keys which call the "set_background" action.

actions04.py
from textual.app import App, ComposeResult
from textual.widgets import Static

TEXT = """
[b]Set your background[/b]
[@click=set_background('red')]Red[/]
[@click=set_background('green')]Green[/]
[@click=set_background('blue')]Blue[/]
"""


class ActionsApp(App):
    BINDINGS = [
        ("r", "set_background('red')", "Red"),
        ("g", "set_background('green')", "Green"),
        ("b", "set_background('blue')", "Blue"),
    ]

    def compose(self) -> ComposeResult:
        yield Static(TEXT)

    def action_set_background(self, color: str) -> None:
        self.screen.styles.background = color


if __name__ == "__main__":
    app = ActionsApp()
    app.run()

ActionsApp Set your background Red Green Blue

If you run this example, you can change the background by pressing keys in addition to clicking links.

See the previous section on input for more information on bindings.

Namespaces

Textual will look for action methods in the class where they are defined (App, Screen, or Widget). If we were to create a custom widget it can have its own set of actions.

The following example defines a custom widget with its own set_background action.

actions05.py
from textual.app import App, ComposeResult
from textual.widgets import Static

TEXT = """
[b]Set your background[/b]
[@click=set_background('cyan')]Cyan[/]
[@click=set_background('magenta')]Magenta[/]
[@click=set_background('yellow')]Yellow[/]
"""


class ColorSwitcher(Static):
    def action_set_background(self, color: str) -> None:
        self.styles.background = color


class ActionsApp(App):
    CSS_PATH = "actions05.tcss"
    BINDINGS = [
        ("r", "set_background('red')", "Red"),
        ("g", "set_background('green')", "Green"),
        ("b", "set_background('blue')", "Blue"),
    ]

    def compose(self) -> ComposeResult:
        yield ColorSwitcher(TEXT)
        yield ColorSwitcher(TEXT)

    def action_set_background(self, color: str) -> None:
        self.screen.styles.background = color


if __name__ == "__main__":
    app = ActionsApp()
    app.run()
actions05.tcss
Screen {
    layout: grid;
    grid-size: 1;
    grid-gutter: 2 4;
    grid-rows: 1fr;
}

ColorSwitcher {
   height: 100%;
   margin: 2 4;
}

There are two instances of the custom widget mounted. If you click the links in either of them it will change the background for that widget only. The R, G, and B key bindings are set on the App so will set the background for the screen.

You can optionally prefix an action with a namespace, which tells Textual to run actions for a different object.

Textual supports the following action namespaces:

  • app invokes actions on the App.
  • screen invokes actions on the screen.
  • focused invokes actions on the currently focused widget (if there is one).

In the previous example if you wanted a link to set the background on the app rather than the widget, we could set a link to app.set_background('red').

Dynamic actions

Added in version 0.61.0

There may be situations where an action is temporarily unavailable due to some internal state within your app. For instance, consider an app with a fixed number of pages and actions to go to the next and previous page. It doesn't make sense to go to the previous page if we are on the first, or the next page when we are on the last page.

We could easily add this logic to the action methods, but the footer would still display the keys even if they would have no effect. The user may wonder why the app is showing keys that don't appear to work.

We can solve this issue by implementing the check_action on our app, screen, or widget. This method is called with the name of the action and any parameters, prior to running actions or refreshing the footer. It should return one of the following values:

  • True to show the key and run the action as normal.
  • False to hide the key and prevent the action running.
  • None to disable the key (show dimmed), and prevent the action running.

Let's write an app to put this into practice:

actions06.py
from textual.app import App, ComposeResult
from textual.containers import HorizontalScroll
from textual.reactive import reactive
from textual.widgets import Footer, Placeholder

PAGES_COUNT = 5


class PagesApp(App):
    BINDINGS = [
        ("n", "next", "Next"),
        ("p", "previous", "Previous"),
    ]

    CSS_PATH = "actions06.tcss"

    page_no = reactive(0)

    def compose(self) -> ComposeResult:
        with HorizontalScroll(id="page-container"):
            for page_no in range(PAGES_COUNT):
                yield Placeholder(f"Page {page_no}", id=f"page-{page_no}")
        yield Footer()

    def action_next(self) -> None:
        self.page_no += 1
        self.refresh_bindings()  # (1)!
        self.query_one(f"#page-{self.page_no}").scroll_visible()

    def action_previous(self) -> None:
        self.page_no -= 1
        self.refresh_bindings()  # (2)!
        self.query_one(f"#page-{self.page_no}").scroll_visible()

    def check_action(
        self, action: str, parameters: tuple[object, ...]
    ) -> bool | None:  # (3)!
        """Check if an action may run."""
        if action == "next" and self.page_no == PAGES_COUNT - 1:
            return False
        if action == "previous" and self.page_no == 0:
            return False
        return True


if __name__ == "__main__":
    app = PagesApp()
    app.run()
  1. Prompts the footer to refresh, if bindings change.
  2. Prompts the footer to refresh, if bindings change.
  3. Guards the actions from running and also what keys are displayed in the footer.
actions06.tcss
#page-container {
    # This hides the scrollbar
    scrollbar-size: 0 0;
}

PagesApp Page 0  N  Next 

This app has key bindings for N and P to navigate the pages. Notice how the keys are hidden from the footer when they would have no effect.

The actions above call refresh_bindings to prompt Textual to refresh the footer. An alternative to doing this manually is to set bindings=True on a reactive, which will refresh the bindings if the reactive changes.

Let's make this change. We will also demonstrate what the footer will show if we return None from check_action (rather than False):

actions06.py
from textual.app import App, ComposeResult
from textual.containers import HorizontalScroll
from textual.reactive import reactive
from textual.widgets import Footer, Placeholder

PAGES_COUNT = 5


class PagesApp(App):
    BINDINGS = [
        ("n", "next", "Next"),
        ("p", "previous", "Previous"),
    ]

    CSS_PATH = "actions06.tcss"

    page_no = reactive(0, bindings=True)  # (1)!

    def compose(self) -> ComposeResult:
        with HorizontalScroll(id="page-container"):
            for page_no in range(PAGES_COUNT):
                yield Placeholder(f"Page {page_no}", id=f"page-{page_no}")
        yield Footer()

    def action_next(self) -> None:
        self.page_no += 1
        self.query_one(f"#page-{self.page_no}").scroll_visible()

    def action_previous(self) -> None:
        self.page_no -= 1
        self.query_one(f"#page-{self.page_no}").scroll_visible()

    def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
        """Check if an action may run."""
        if action == "next" and self.page_no == PAGES_COUNT - 1:
            return None  # (2)!
        if action == "previous" and self.page_no == 0:
            return None  # (3)!
        return True


if __name__ == "__main__":
    app = PagesApp()
    app.run()
  1. The bindings=True causes the footer to refresh when page_no changes.
  2. Returning None disables the key in the footer rather than hides it
  3. Returning None disables the key in the footer rather than hides it.
actions06.tcss
#page-container {
    # This hides the scrollbar
    scrollbar-size: 0 0;
}

PagesApp Page 0  N  Next  P  Previous 

Note how the logic is the same but we don't need to explicitly call refresh_bindings. The change to check_action also causes the disabled footer keys to be grayed out, indicating they are temporarily unavailable.

Builtin actions

Textual supports the following builtin actions which are defined on the app.